Page MenuHomec4science

No OneTemporary

File Metadata

Created
Sat, Jun 14, 08:21
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/bin/sms b/bin/sms
deleted file mode 120000
index e622b5074..000000000
--- a/bin/sms
+++ /dev/null
@@ -1 +0,0 @@
-../scripts/sms/manage_sms.php
\ No newline at end of file
diff --git a/conf/__init_conf__.php b/conf/__init_conf__.php
index 3dd66b2dd..18c132c6d 100644
--- a/conf/__init_conf__.php
+++ b/conf/__init_conf__.php
@@ -1,69 +1,68 @@
<?php
function phabricator_read_config_file($original_config) {
$root = dirname(dirname(__FILE__));
// Accept either "myconfig" (preferred) or "myconfig.conf.php".
$config = preg_replace('/\.conf\.php$/', '', $original_config);
$full_config_path = $root.'/conf/'.$config.'.conf.php';
if (!Filesystem::pathExists($full_config_path)) {
// These are very old configuration files which we used to ship with
// by default. File based configuration was de-emphasized once web-based
// configuration was built. The actual files were removed to reduce
// user confusion over how to configure Phabricator.
switch ($config) {
case 'default':
case 'production':
return array();
case 'development':
return array(
'phabricator.developer-mode' => true,
'darkconsole.enabled' => true,
- 'celerity.minify' => false,
);
}
$files = id(new FileFinder($root.'/conf/'))
->withType('f')
->withSuffix('conf.php')
->withFollowSymlinks(true)
->find();
foreach ($files as $key => $file) {
$file = trim($file, './');
$files[$key] = preg_replace('/\.conf\.php$/', '', $file);
}
$files = ' '.implode("\n ", $files);
throw new Exception(
pht(
"CONFIGURATION ERROR\n".
"Config file '%s' does not exist. Valid config files are:\n\n%s",
$original_config,
$files));
}
// Make sure config file errors are reported.
$old_error_level = error_reporting(E_ALL | E_STRICT);
$old_display_errors = ini_get('display_errors');
ini_set('display_errors', 1);
ob_start();
$conf = include $full_config_path;
$errors = ob_get_clean();
error_reporting($old_error_level);
ini_set('display_errors', $old_display_errors);
if ($conf === false) {
throw new Exception(
pht(
"Failed to read config file '%s': %s",
$config,
$errors));
}
return $conf;
}
diff --git a/externals/phpmailer/class.phpmailer-lite.php b/externals/phpmailer/class.phpmailer-lite.php
index 610de9943..335625eba 100644
--- a/externals/phpmailer/class.phpmailer-lite.php
+++ b/externals/phpmailer/class.phpmailer-lite.php
@@ -1,2086 +1,2182 @@
<?php
/*~ class.phpmailer-lite.php
.---------------------------------------------------------------------------.
| Software: PHPMailer Lite - PHP email class |
| Version: 5.1 |
| Contact: via sourceforge.net support pages (also www.codeworxtech.com) |
| Info: http://phpmailer.sourceforge.net |
| Support: http://sourceforge.net/projects/phpmailer/ |
| ------------------------------------------------------------------------- |
| Admin: Andy Prevost (project admininistrator) |
| Authors: Andy Prevost (codeworxtech) codeworxtech@users.sourceforge.net |
| : Marcus Bointon (coolbru) coolbru@users.sourceforge.net |
| Founder: Brent R. Matzelle (original founder) |
| Copyright (c) 2004-2009, Andy Prevost. All Rights Reserved. |
| Copyright (c) 2001-2003, Brent R. Matzelle |
| ------------------------------------------------------------------------- |
| License: Distributed under the Lesser General Public License (LGPL) |
| http://www.gnu.org/copyleft/lesser.html |
| This program is distributed in the hope that it will be useful - WITHOUT |
| ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| FITNESS FOR A PARTICULAR PURPOSE. |
| ------------------------------------------------------------------------- |
| We offer a number of paid services (www.codeworxtech.com): |
| - Web Hosting on highly optimized fast and secure servers |
| - Technology Consulting |
| - Oursourcing (highly qualified programmers and graphic designers) |
'---------------------------------------------------------------------------'
*/
/**
* PHPMailer Lite - PHP email transport class
* NOTE: Requires PHP version 5 or later
* @package PHPMailer Lite
* @author Andy Prevost
* @author Marcus Bointon
* @copyright 2004 - 2009 Andy Prevost
* @version $Id: class.phpmailer-lite.php 447 2009-09-12 13:21:38Z codeworxtech $
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
*/
if (version_compare(PHP_VERSION, '5.0.0', '<') ) exit("Sorry, this version of PHPMailer will only run on PHP version 5 or greater!\n");
class PHPMailerLite {
+ public static function newFromMessage(
+ PhabricatorMailExternalMessage $message) {
+
+ $mailer = new self($use_exceptions = true);
+
+ // By default, PHPMailerLite sends one mail per recipient. We handle
+ // combining or separating To and Cc higher in the stack, so tell it to
+ // send mail exactly like we ask.
+ $mailer->SingleTo = false;
+
+ $mailer->CharSet = 'utf-8';
+ $mailer->Encoding = 'base64';
+
+ $subject = $message->getSubject();
+ if ($subject !== null) {
+ $mailer->Subject = $subject;
+ }
+
+ $from_address = $message->getFromAddress();
+ if ($from_address) {
+ $mailer->SetFrom(
+ $from_address->getAddress(),
+ (string)$from_address->getDisplayName(),
+ $crazy_side_effects = false);
+ }
+
+ $reply_address = $message->getReplyToAddress();
+ if ($reply_address) {
+ $mailer->AddReplyTo(
+ $reply_address->getAddress(),
+ (string)$reply_address->getDisplayName());
+ }
+
+ $to_addresses = $message->getToAddresses();
+ if ($to_addresses) {
+ foreach ($to_addresses as $address) {
+ $mailer->AddAddress(
+ $address->getAddress(),
+ (string)$address->getDisplayName());
+ }
+ }
+
+ $cc_addresses = $message->getCCAddresses();
+ if ($cc_addresses) {
+ foreach ($cc_addresses as $address) {
+ $mailer->AddCC(
+ $address->getAddress(),
+ (string)$address->getDisplayName());
+ }
+ }
+
+ $headers = $message->getHeaders();
+ if ($headers) {
+ foreach ($headers as $header) {
+ $name = $header->getName();
+ $value = $header->getValue();
+
+ if (phutil_utf8_strtolower($name) === 'message-id') {
+ $mailer->MessageID = $value;
+ } else {
+ $mailer->AddCustomHeader("{$name}: {$value}");
+ }
+ }
+ }
+
+ $attachments = $message->getAttachments();
+ if ($attachments) {
+ foreach ($attachments as $attachment) {
+ $mailer->AddStringAttachment(
+ $attachment->getData(),
+ $attachment->getFilename(),
+ 'base64',
+ $attachment->getMimeType());
+ }
+ }
+
+ $text_body = $message->getTextBody();
+ if ($text_body !== null) {
+ $mailer->Body = $text_body;
+ }
+
+ $html_body = $message->getHTMLBody();
+ if ($html_body !== null) {
+ $mailer->IsHTML(true);
+ $mailer->Body = $html_body;
+ if ($text_body !== null) {
+ $mailer->AltBody = $text_body;
+ }
+ }
+
+ return $mailer;
+ }
+
+
+
+
/////////////////////////////////////////////////
// PROPERTIES, PUBLIC
/////////////////////////////////////////////////
/**
* Email priority (1 = High, 3 = Normal, 5 = low).
* @var int
*/
public $Priority = 3;
/**
* Sets the CharSet of the message.
* @var string
*/
public $CharSet = 'iso-8859-1';
/**
* Sets the Content-type of the message.
* @var string
*/
public $ContentType = 'text/plain';
/**
* Sets the Encoding of the message. Options for this are
* "8bit", "7bit", "binary", "base64", and "quoted-printable".
* @var string
*/
public $Encoding = '8bit';
/**
* Holds the most recent mailer error message.
* @var string
*/
public $ErrorInfo = '';
/**
* Sets the From email address for the message.
* @var string
*/
public $From = 'root@localhost';
/**
* Sets the From name of the message.
* @var string
*/
public $FromName = 'Root User';
/**
* Sets the Sender email (Return-Path) of the message. If not empty,
* will be sent via -f to sendmail
* @var string
*/
public $Sender = '';
/**
* Sets the Subject of the message.
* @var string
*/
public $Subject = '';
/**
* Sets the Body of the message. This can be either an HTML or text body.
* If HTML then run IsHTML(true).
* @var string
*/
public $Body = '';
/**
* Sets the text-only body of the message. This automatically sets the
* email to multipart/alternative. This body can be read by mail
* clients that do not have HTML email capability such as mutt. Clients
* that can read HTML will view the normal Body.
* @var string
*/
public $AltBody = '';
/**
* Sets word wrapping on the body of the message to a given number of
* characters.
* @var int
*/
public $WordWrap = 0;
/**
* Method to send mail: ("mail", or "sendmail").
* @var string
*/
public $Mailer = 'sendmail';
/**
* Sets the path of the sendmail program.
* @var string
*/
public $Sendmail = '/usr/sbin/sendmail';
/**
* Sets the email address that a reading confirmation will be sent.
* @var string
*/
public $ConfirmReadingTo = '';
/**
* Sets the hostname to use in Message-Id and Received headers
* and as default HELO string. If empty, the value returned
* by SERVER_NAME is used or 'localhost.localdomain'.
* @var string
*/
public $Hostname = '';
/**
* Sets the message ID to be used in the Message-Id header.
* If empty, a unique id will be generated.
* @var string
*/
public $MessageID = '';
/**
* Provides the ability to have the TO field process individual
* emails, instead of sending to entire TO addresses
* @var bool
*/
public $SingleTo = true;
/**
* If SingleTo is true, this provides the array to hold the email addresses
* @var bool
*/
public $SingleToArray = array();
/**
* Provides the ability to change the line ending
* @var string
*/
public $LE = "\n";
/**
* Used with DKIM DNS Resource Record
* @var string
*/
public $DKIM_selector = 'phpmailer';
/**
* Used with DKIM DNS Resource Record
* optional, in format of email address 'you@yourdomain.com'
* @var string
*/
public $DKIM_identity = '';
/**
* Used with DKIM DNS Resource Record
* required, in format of base domain 'yourdomain.com'
* @var string
*/
public $DKIM_domain = '';
/**
* Used with DKIM Digital Signing process
* optional
* @var string
*/
public $DKIM_passphrase = '';
/**
* Used with DKIM DNS Resource Record
* required, private key (read from /.htprivkey)
* @var string
*/
public $DKIM_private = '';
/**
* Callback Action function name
* the function that handles the result of the send email action. Parameters:
* bool $result result of the send action
* string $to email address of the recipient
* string $cc cc email addresses
* string $bcc bcc email addresses
* string $subject the subject
* string $body the email body
* @var string
*/
public $action_function = ''; //'callbackAction';
/**
* Sets the PHPMailer Version number
* @var string
*/
public $Version = 'Lite 5.1';
/////////////////////////////////////////////////
// PROPERTIES, PRIVATE AND PROTECTED
/////////////////////////////////////////////////
private $to = array();
private $cc = array();
private $bcc = array();
private $ReplyTo = array();
private $all_recipients = array();
private $attachment = array();
private $CustomHeader = array();
private $message_type = '';
private $boundary = array();
protected $language = array();
private $error_count = 0;
private $sign_cert_file = "";
private $sign_key_file = "";
private $sign_key_pass = "";
private $exceptions = false;
/////////////////////////////////////////////////
// CONSTANTS
/////////////////////////////////////////////////
const STOP_MESSAGE = 0; // message only, continue processing
const STOP_CONTINUE = 1; // message?, likely ok to continue processing
const STOP_CRITICAL = 2; // message, plus full stop, critical error reached
/////////////////////////////////////////////////
// METHODS, VARIABLES
/////////////////////////////////////////////////
/**
* Constructor
* @param boolean $exceptions Should we throw external exceptions?
*/
public function __construct($exceptions = false) {
$this->exceptions = ($exceptions == true);
}
/**
* Sets message type to HTML.
* @param bool $ishtml
* @return void
*/
public function IsHTML($ishtml = true) {
if ($ishtml) {
$this->ContentType = 'text/html';
} else {
$this->ContentType = 'text/plain';
}
}
/**
* Sets Mailer to send message using PHP mail() function.
* @return void
*/
public function IsMail() {
$this->Mailer = 'mail';
}
/**
* Sets Mailer to send message using the $Sendmail program.
* @return void
*/
public function IsSendmail() {
if (!stristr(ini_get('sendmail_path'), 'sendmail')) {
$this->Sendmail = '/var/qmail/bin/sendmail';
}
$this->Mailer = 'sendmail';
}
/**
* Sets Mailer to send message using the qmail MTA.
* @return void
*/
public function IsQmail() {
if (stristr(ini_get('sendmail_path'), 'qmail')) {
$this->Sendmail = '/var/qmail/bin/sendmail';
}
$this->Mailer = 'sendmail';
}
/////////////////////////////////////////////////
// METHODS, RECIPIENTS
/////////////////////////////////////////////////
/**
* Adds a "To" address.
* @param string $address
* @param string $name
* @return boolean true on success, false if address already used
*/
public function AddAddress($address, $name = '') {
return $this->AddAnAddress('to', $address, $name);
}
/**
* Adds a "Cc" address.
* Note: this function works with the SMTP mailer on win32, not with the "mail" mailer.
* @param string $address
* @param string $name
* @return boolean true on success, false if address already used
*/
public function AddCC($address, $name = '') {
return $this->AddAnAddress('cc', $address, $name);
}
/**
* Adds a "Bcc" address.
* Note: this function works with the SMTP mailer on win32, not with the "mail" mailer.
* @param string $address
* @param string $name
* @return boolean true on success, false if address already used
*/
public function AddBCC($address, $name = '') {
return $this->AddAnAddress('bcc', $address, $name);
}
/**
* Adds a "Reply-to" address.
* @param string $address
* @param string $name
* @return boolean
*/
public function AddReplyTo($address, $name = '') {
return $this->AddAnAddress('ReplyTo', $address, $name);
}
/**
* Adds an address to one of the recipient arrays
* Addresses that have been added already return false, but do not throw exceptions
* @param string $kind One of 'to', 'cc', 'bcc', 'ReplyTo'
* @param string $address The email address to send to
* @param string $name
* @return boolean true on success, false if address already used or invalid in some way
* @access private
*/
private function AddAnAddress($kind, $address, $name = '') {
if (!preg_match('/^(to|cc|bcc|ReplyTo)$/', $kind)) {
echo 'Invalid recipient array: ' . $kind;
return false;
}
$address = trim($address);
$name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
if (!self::ValidateAddress($address)) {
$this->SetError($this->Lang('invalid_address').': '. $address);
if ($this->exceptions) {
throw new phpmailerException($this->Lang('invalid_address').': '.$address);
}
echo $this->Lang('invalid_address').': '.$address;
return false;
}
if ($kind != 'ReplyTo') {
if (!isset($this->all_recipients[strtolower($address)])) {
array_push($this->$kind, array($address, $name));
$this->all_recipients[strtolower($address)] = true;
return true;
}
} else {
if (!array_key_exists(strtolower($address), $this->ReplyTo)) {
$this->ReplyTo[strtolower($address)] = array($address, $name);
return true;
}
}
return false;
}
/**
* Set the From and FromName properties
* @param string $address
* @param string $name
* @return boolean
*/
public function SetFrom($address, $name = '',$auto=1) {
$address = trim($address);
$name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
if (!self::ValidateAddress($address)) {
$this->SetError($this->Lang('invalid_address').': '. $address);
if ($this->exceptions) {
throw new phpmailerException($this->Lang('invalid_address').': '.$address);
}
echo $this->Lang('invalid_address').': '.$address;
return false;
}
$this->From = $address;
$this->FromName = $name;
if ($auto) {
if (empty($this->ReplyTo)) {
$this->AddAnAddress('ReplyTo', $address, $name);
}
if (empty($this->Sender)) {
$this->Sender = $address;
}
}
return true;
}
/**
* Check that a string looks roughly like an email address should
* Static so it can be used without instantiation
* Tries to use PHP built-in validator in the filter extension (from PHP 5.2), falls back to a reasonably competent regex validator
* Conforms approximately to RFC2822
* @link http://www.hexillion.com/samples/#Regex Original pattern found here
* @param string $address The email address to check
* @return boolean
* @static
* @access public
*/
public static function ValidateAddress($address) {
if (function_exists('filter_var')) { //Introduced in PHP 5.2
if(filter_var($address, FILTER_VALIDATE_EMAIL) === FALSE) {
return false;
} else {
return true;
}
} else {
return preg_match('/^(?:[\w\!\#\$\%\&\'\*\+\-\/\=\?\^\`\{\|\}\~]+\.)*[\w\!\#\$\%\&\'\*\+\-\/\=\?\^\`\{\|\}\~]+@(?:(?:(?:[a-zA-Z0-9_](?:[a-zA-Z0-9_\-](?!\.)){0,61}[a-zA-Z0-9_-]?\.)+[a-zA-Z0-9_](?:[a-zA-Z0-9_\-](?!$)){0,61}[a-zA-Z0-9_]?)|(?:\[(?:(?:[01]?\d{1,2}|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d{1,2}|2[0-4]\d|25[0-5])\]))$/', $address);
}
}
/////////////////////////////////////////////////
// METHODS, MAIL SENDING
/////////////////////////////////////////////////
/**
* Creates message and assigns Mailer. If the message is
* not sent successfully then it returns false. Use the ErrorInfo
* variable to view description of the error.
* @return bool
*/
public function Send() {
try {
if ((count($this->to) + count($this->cc) + count($this->bcc)) < 1) {
throw new phpmailerException($this->Lang('provide_address'), self::STOP_CRITICAL);
}
// Set whether the message is multipart/alternative
if(!empty($this->AltBody)) {
$this->ContentType = 'multipart/alternative';
}
$this->error_count = 0; // reset errors
$this->SetMessageType();
$header = $this->CreateHeader();
$body = $this->CreateBody();
if (empty($this->Body)) {
throw new phpmailerException($this->Lang('empty_message'), self::STOP_CRITICAL);
}
// digitally sign with DKIM if enabled
if ($this->DKIM_domain && $this->DKIM_private) {
$header_dkim = $this->DKIM_Add($header,$this->Subject,$body);
$header = str_replace("\r\n","\n",$header_dkim) . $header;
}
// Choose the mailer and send through it
switch($this->Mailer) {
case 'amazon-ses':
return $this->customMailer->executeSend(
$header.
$body);
case 'sendmail':
$sendAction = $this->SendmailSend($header, $body);
return $sendAction;
default:
$sendAction = $this->MailSend($header, $body);
return $sendAction;
}
} catch (phpmailerException $e) {
$this->SetError($e->getMessage());
if ($this->exceptions) {
throw $e;
}
echo $e->getMessage()."\n";
return false;
}
}
/**
* Sends mail using the $Sendmail program.
* @param string $header The message headers
* @param string $body The message body
* @access protected
* @return bool
*/
protected function SendmailSend($header, $body) {
if ($this->Sender != '') {
$sendmail = sprintf("%s -oi -f %s -t", escapeshellcmd($this->Sendmail), escapeshellarg($this->Sender));
} else {
$sendmail = sprintf("%s -oi -t", escapeshellcmd($this->Sendmail));
}
if ($this->SingleTo === true) {
foreach ($this->SingleToArray as $key => $val) {
$mail = new ExecFuture('%C', $sendmail);
$mail->write("To: {$val}\n", true);
$mail->write($header.$body);
$mail->resolvex();
}
} else {
$mail = new ExecFuture('%C', $sendmail);
$mail->write($header.$body);
$mail->resolvex();
}
return true;
}
/**
* Sends mail using the PHP mail() function.
* @param string $header The message headers
* @param string $body The message body
* @access protected
* @return bool
*/
protected function MailSend($header, $body) {
$toArr = array();
foreach($this->to as $t) {
$toArr[] = $this->AddrFormat($t);
}
$to = implode(', ', $toArr);
$params = sprintf("-oi -f %s", $this->Sender);
if ($this->Sender != '' && strlen(ini_get('safe_mode'))< 1) {
$old_from = ini_get('sendmail_from');
ini_set('sendmail_from', $this->Sender);
if ($this->SingleTo === true && count($toArr) > 1) {
foreach ($toArr as $key => $val) {
$rt = @mail($val, $this->EncodeHeader($this->SecureHeader($this->Subject)), $body, $header, $params);
// implement call back function if it exists
$isSent = ($rt == 1) ? 1 : 0;
$this->doCallback($isSent,$val,$this->cc,$this->bcc,$this->Subject,$body);
}
} else {
$rt = @mail($to, $this->EncodeHeader($this->SecureHeader($this->Subject)), $body, $header, $params);
// implement call back function if it exists
$isSent = ($rt == 1) ? 1 : 0;
$this->doCallback($isSent,$to,$this->cc,$this->bcc,$this->Subject,$body);
}
} else {
if ($this->SingleTo === true && count($toArr) > 1) {
foreach ($toArr as $key => $val) {
$rt = @mail($val, $this->EncodeHeader($this->SecureHeader($this->Subject)), $body, $header, $params);
// implement call back function if it exists
$isSent = ($rt == 1) ? 1 : 0;
$this->doCallback($isSent,$val,$this->cc,$this->bcc,$this->Subject,$body);
}
} else {
$rt = @mail($to, $this->EncodeHeader($this->SecureHeader($this->Subject)), $body, $header);
// implement call back function if it exists
$isSent = ($rt == 1) ? 1 : 0;
$this->doCallback($isSent,$to,$this->cc,$this->bcc,$this->Subject,$body);
}
}
if (isset($old_from)) {
ini_set('sendmail_from', $old_from);
}
if(!$rt) {
throw new phpmailerException($this->Lang('instantiate'), self::STOP_CRITICAL);
}
return true;
}
/**
* Sets the language for all class error messages.
* Returns false if it cannot load the language file. The default language is English.
* @param string $langcode ISO 639-1 2-character language code (e.g. Portuguese: "br")
* @param string $lang_path Path to the language file directory
* @access public
*/
function SetLanguage($langcode = 'en', $lang_path = 'language/') {
//Define full set of translatable strings
$PHPMAILER_LANG = array(
'provide_address' => 'You must provide at least one recipient email address.',
'mailer_not_supported' => ' mailer is not supported.',
'execute' => 'Could not execute: ',
'instantiate' => 'Could not instantiate mail function.',
'from_failed' => 'The following From address failed: ',
'file_access' => 'Could not access file: ',
'file_open' => 'File Error: Could not open file: ',
'encoding' => 'Unknown encoding: ',
'signing' => 'Signing Error: ',
'empty_message' => 'Message body empty',
'invalid_address' => 'Invalid address',
'variable_set' => 'Cannot set or reset variable: '
);
//Overwrite language-specific strings. This way we'll never have missing translations - no more "language string failed to load"!
$l = true;
if ($langcode != 'en') { //There is no English translation file
$l = @include $lang_path.'phpmailer.lang-'.$langcode.'.php';
}
$this->language = $PHPMAILER_LANG;
return ($l == true); //Returns false if language not found
}
/**
* Return the current array of language strings
* @return array
*/
public function GetTranslations() {
return $this->language;
}
/////////////////////////////////////////////////
// METHODS, MESSAGE CREATION
/////////////////////////////////////////////////
/**
* Creates recipient headers.
* @access public
* @return string
*/
public function AddrAppend($type, $addr) {
$addr_str = $type . ': ';
$addresses = array();
foreach ($addr as $a) {
$addresses[] = $this->AddrFormat($a);
}
$addr_str .= implode(', ', $addresses);
$addr_str .= $this->LE;
// NOTE: This is a narrow hack to fix an issue with 1000+ characters of
// recipients, described in T12372.
$addr_str = wordwrap($addr_str, 75, "\n ");
return $addr_str;
}
/**
* Formats an address correctly.
* @access public
* @return string
*/
public function AddrFormat($addr) {
if (empty($addr[1])) {
return $this->SecureHeader($addr[0]);
} else {
return $this->EncodeHeader($this->SecureHeader($addr[1]), 'phrase') . " <" . $this->SecureHeader($addr[0]) . ">";
}
}
/**
* Wraps message for use with mailers that do not
* automatically perform wrapping and for quoted-printable.
* Original written by philippe.
* @param string $message The message to wrap
* @param integer $length The line length to wrap to
* @param boolean $qp_mode Whether to run in Quoted-Printable mode
* @access public
* @return string
*/
public function WrapText($message, $length, $qp_mode = false) {
$soft_break = ($qp_mode) ? sprintf(" =%s", $this->LE) : $this->LE;
// If utf-8 encoding is used, we will need to make sure we don't
// split multibyte characters when we wrap
$is_utf8 = (strtolower($this->CharSet) == "utf-8");
$message = $this->FixEOL($message);
if (substr($message, -1) == $this->LE) {
$message = substr($message, 0, -1);
}
$line = explode($this->LE, $message);
$message = '';
for ($i=0 ;$i < count($line); $i++) {
$line_part = explode(' ', $line[$i]);
$buf = '';
for ($e = 0; $e<count($line_part); $e++) {
$word = $line_part[$e];
if ($qp_mode and (strlen($word) > $length)) {
$space_left = $length - strlen($buf) - 1;
if ($e != 0) {
if ($space_left > 20) {
$len = $space_left;
if ($is_utf8) {
$len = $this->UTF8CharBoundary($word, $len);
} elseif (substr($word, $len - 1, 1) == "=") {
$len--;
} elseif (substr($word, $len - 2, 1) == "=") {
$len -= 2;
}
$part = substr($word, 0, $len);
$word = substr($word, $len);
$buf .= ' ' . $part;
$message .= $buf . sprintf("=%s", $this->LE);
} else {
$message .= $buf . $soft_break;
}
$buf = '';
}
while (strlen($word) > 0) {
$len = $length;
if ($is_utf8) {
$len = $this->UTF8CharBoundary($word, $len);
} elseif (substr($word, $len - 1, 1) == "=") {
$len--;
} elseif (substr($word, $len - 2, 1) == "=") {
$len -= 2;
}
$part = substr($word, 0, $len);
$word = substr($word, $len);
if (strlen($word) > 0) {
$message .= $part . sprintf("=%s", $this->LE);
} else {
$buf = $part;
}
}
} else {
$buf_o = $buf;
$buf .= ($e == 0) ? $word : (' ' . $word);
if (strlen($buf) > $length and $buf_o != '') {
$message .= $buf_o . $soft_break;
$buf = $word;
}
}
}
$message .= $buf . $this->LE;
}
return $message;
}
/**
* Finds last character boundary prior to maxLength in a utf-8
* quoted (printable) encoded string.
* Original written by Colin Brown.
* @access public
* @param string $encodedText utf-8 QP text
* @param int $maxLength find last character boundary prior to this length
* @return int
*/
public function UTF8CharBoundary($encodedText, $maxLength) {
$foundSplitPos = false;
$lookBack = 3;
while (!$foundSplitPos) {
$lastChunk = substr($encodedText, $maxLength - $lookBack, $lookBack);
$encodedCharPos = strpos($lastChunk, "=");
if ($encodedCharPos !== false) {
// Found start of encoded character byte within $lookBack block.
// Check the encoded byte value (the 2 chars after the '=')
$hex = substr($encodedText, $maxLength - $lookBack + $encodedCharPos + 1, 2);
$dec = hexdec($hex);
if ($dec < 128) { // Single byte character.
// If the encoded char was found at pos 0, it will fit
// otherwise reduce maxLength to start of the encoded char
$maxLength = ($encodedCharPos == 0) ? $maxLength :
$maxLength - ($lookBack - $encodedCharPos);
$foundSplitPos = true;
} elseif ($dec >= 192) { // First byte of a multi byte character
// Reduce maxLength to split at start of character
$maxLength = $maxLength - ($lookBack - $encodedCharPos);
$foundSplitPos = true;
} elseif ($dec < 192) { // Middle byte of a multi byte character, look further back
$lookBack += 3;
}
} else {
// No encoded character found
$foundSplitPos = true;
}
}
return $maxLength;
}
/**
* Set the body wrapping.
* @access public
* @return void
*/
public function SetWordWrap() {
if($this->WordWrap < 1) {
return;
}
switch($this->message_type) {
case 'alt':
case 'alt_attachments':
$this->AltBody = $this->WrapText($this->AltBody, $this->WordWrap);
break;
default:
$this->Body = $this->WrapText($this->Body, $this->WordWrap);
break;
}
}
/**
* Assembles message header.
* @access public
* @return string The assembled header
*/
public function CreateHeader() {
$result = '';
// Set the boundaries
$uniq_id = md5(uniqid(time()));
$this->boundary[1] = 'b1_' . $uniq_id;
$this->boundary[2] = 'b2_' . $uniq_id;
$result .= $this->HeaderLine('Date', self::RFCDate());
if($this->Sender == '') {
$result .= $this->HeaderLine('Return-Path', trim($this->From));
} else {
$result .= $this->HeaderLine('Return-Path', trim($this->Sender));
}
// To be created automatically by mail()
if($this->Mailer != 'mail') {
if ($this->SingleTo === true) {
foreach($this->to as $t) {
$this->SingleToArray[] = $this->AddrFormat($t);
}
} else {
if(count($this->to) > 0) {
$result .= $this->AddrAppend('To', $this->to);
} elseif (count($this->cc) == 0) {
$result .= $this->HeaderLine('To', 'undisclosed-recipients:;');
}
}
}
$from = array();
$from[0][0] = trim($this->From);
$from[0][1] = $this->FromName;
$result .= $this->AddrAppend('From', $from);
// sendmail and mail() extract Cc from the header before sending
if(count($this->cc) > 0) {
$result .= $this->AddrAppend('Cc', $this->cc);
}
// sendmail and mail() extract Bcc from the header before sending
if(count($this->bcc) > 0) {
$result .= $this->AddrAppend('Bcc', $this->bcc);
}
if(count($this->ReplyTo) > 0) {
$result .= $this->AddrAppend('Reply-to', $this->ReplyTo);
}
// mail() sets the subject itself
if($this->Mailer != 'mail') {
$result .= $this->HeaderLine('Subject', $this->EncodeHeader($this->SecureHeader($this->Subject)));
}
if($this->MessageID != '') {
$result .= $this->HeaderLine('Message-ID',$this->MessageID);
} else {
$result .= sprintf("Message-ID: <%s@%s>%s", $uniq_id, $this->ServerHostname(), $this->LE);
}
$result .= $this->HeaderLine('X-Priority', $this->Priority);
if($this->ConfirmReadingTo != '') {
$result .= $this->HeaderLine('Disposition-Notification-To', '<' . trim($this->ConfirmReadingTo) . '>');
}
// Add custom headers
for($index = 0; $index < count($this->CustomHeader); $index++) {
$result .= $this->HeaderLine(trim($this->CustomHeader[$index][0]), $this->EncodeHeader(trim($this->CustomHeader[$index][1])));
}
if (!$this->sign_key_file) {
$result .= $this->HeaderLine('MIME-Version', '1.0');
$result .= $this->GetMailMIME();
}
return $result;
}
/**
* Returns the message MIME.
* @access public
* @return string
*/
public function GetMailMIME() {
$result = '';
switch($this->message_type) {
case 'plain':
$result .= $this->HeaderLine('Content-Transfer-Encoding', $this->Encoding);
$result .= sprintf("Content-Type: %s; charset=\"%s\"", $this->ContentType, $this->CharSet);
break;
case 'attachments':
case 'alt_attachments':
if($this->InlineImageExists()){
$result .= sprintf("Content-Type: %s;%s\ttype=\"text/html\";%s\tboundary=\"%s\"%s", 'multipart/related', $this->LE, $this->LE, $this->boundary[1], $this->LE);
} else {
$result .= $this->HeaderLine('Content-Type', 'multipart/mixed;');
$result .= $this->TextLine("\tboundary=\"" . $this->boundary[1] . '"');
}
break;
case 'alt':
$result .= $this->HeaderLine('Content-Type', 'multipart/alternative;');
$result .= $this->TextLine("\tboundary=\"" . $this->boundary[1] . '"');
break;
}
if($this->Mailer != 'mail') {
$result .= $this->LE.$this->LE;
}
return $result;
}
/**
* Assembles the message body. Returns an empty string on failure.
* @access public
* @return string The assembled message body
*/
public function CreateBody() {
$body = '';
if ($this->sign_key_file) {
$body .= $this->GetMailMIME();
}
$this->SetWordWrap();
switch($this->message_type) {
case 'alt':
$body .= $this->GetBoundary($this->boundary[1], '', 'text/plain', '');
$body .= $this->EncodeString($this->AltBody, $this->Encoding);
$body .= $this->LE.$this->LE;
$body .= $this->GetBoundary($this->boundary[1], '', 'text/html', '');
$body .= $this->EncodeString($this->Body, $this->Encoding);
$body .= $this->LE.$this->LE;
$body .= $this->EndBoundary($this->boundary[1]);
break;
case 'plain':
$body .= $this->EncodeString($this->Body, $this->Encoding);
break;
case 'attachments':
$body .= $this->GetBoundary($this->boundary[1], '', '', '');
$body .= $this->EncodeString($this->Body, $this->Encoding);
$body .= $this->LE;
$body .= $this->AttachAll();
break;
case 'alt_attachments':
$body .= sprintf("--%s%s", $this->boundary[1], $this->LE);
$body .= sprintf("Content-Type: %s;%s" . "\tboundary=\"%s\"%s", 'multipart/alternative', $this->LE, $this->boundary[2], $this->LE.$this->LE);
$body .= $this->GetBoundary($this->boundary[2], '', 'text/plain', '') . $this->LE; // Create text body
$body .= $this->EncodeString($this->AltBody, $this->Encoding);
$body .= $this->LE.$this->LE;
$body .= $this->GetBoundary($this->boundary[2], '', 'text/html', '') . $this->LE; // Create the HTML body
$body .= $this->EncodeString($this->Body, $this->Encoding);
$body .= $this->LE.$this->LE;
$body .= $this->EndBoundary($this->boundary[2]);
$body .= $this->AttachAll();
break;
}
if ($this->IsError()) {
$body = '';
} elseif ($this->sign_key_file) {
try {
$file = tempnam('', 'mail');
file_put_contents($file, $body); //TODO check this worked
$signed = tempnam("", "signed");
if (@openssl_pkcs7_sign($file, $signed, "file://".$this->sign_cert_file, array("file://".$this->sign_key_file, $this->sign_key_pass), NULL)) {
@unlink($file);
@unlink($signed);
$body = file_get_contents($signed);
} else {
@unlink($file);
@unlink($signed);
throw new phpmailerException($this->Lang("signing").openssl_error_string());
}
} catch (phpmailerException $e) {
$body = '';
if ($this->exceptions) {
throw $e;
}
}
}
return $body;
}
/**
* Returns the start of a message boundary.
* @access private
*/
private function GetBoundary($boundary, $charSet, $contentType, $encoding) {
$result = '';
if($charSet == '') {
$charSet = $this->CharSet;
}
if($contentType == '') {
$contentType = $this->ContentType;
}
if($encoding == '') {
$encoding = $this->Encoding;
}
$result .= $this->TextLine('--' . $boundary);
$result .= sprintf("Content-Type: %s; charset = \"%s\"", $contentType, $charSet);
$result .= $this->LE;
$result .= $this->HeaderLine('Content-Transfer-Encoding', $encoding);
$result .= $this->LE;
return $result;
}
/**
* Returns the end of a message boundary.
* @access private
*/
private function EndBoundary($boundary) {
return $this->LE . '--' . $boundary . '--' . $this->LE;
}
/**
* Sets the message type.
* @access private
* @return void
*/
private function SetMessageType() {
if(count($this->attachment) < 1 && strlen($this->AltBody) < 1) {
$this->message_type = 'plain';
} else {
if(count($this->attachment) > 0) {
$this->message_type = 'attachments';
}
if(strlen($this->AltBody) > 0 && count($this->attachment) < 1) {
$this->message_type = 'alt';
}
if(strlen($this->AltBody) > 0 && count($this->attachment) > 0) {
$this->message_type = 'alt_attachments';
}
}
}
/**
* Returns a formatted header line.
* @access public
* @return string
*/
public function HeaderLine($name, $value) {
return $name . ': ' . $value . $this->LE;
}
/**
* Returns a formatted mail line.
* @access public
* @return string
*/
public function TextLine($value) {
return $value . $this->LE;
}
/////////////////////////////////////////////////
// CLASS METHODS, ATTACHMENTS
/////////////////////////////////////////////////
/**
* Adds an attachment from a path on the filesystem.
* Returns false if the file could not be found
* or accessed.
* @param string $path Path to the attachment.
* @param string $name Overrides the attachment name.
* @param string $encoding File encoding (see $Encoding).
* @param string $type File extension (MIME) type.
* @return bool
*/
public function AddAttachment($path, $name = '', $encoding = 'base64', $type = 'application/octet-stream') {
try {
if ( !@is_file($path) ) {
throw new phpmailerException($this->Lang('file_access') . $path, self::STOP_CONTINUE);
}
$filename = basename($path);
if ( $name == '' ) {
$name = $filename;
}
$this->attachment[] = array(
0 => $path,
1 => $filename,
2 => $name,
3 => $encoding,
4 => $type,
5 => false, // isStringAttachment
6 => 'attachment',
7 => 0
);
} catch (phpmailerException $e) {
$this->SetError($e->getMessage());
if ($this->exceptions) {
throw $e;
}
echo $e->getMessage()."\n";
if ( $e->getCode() == self::STOP_CRITICAL ) {
return false;
}
}
return true;
}
/**
* Return the current array of attachments
* @return array
*/
public function GetAttachments() {
return $this->attachment;
}
/**
* Attaches all fs, string, and binary attachments to the message.
* Returns an empty string on failure.
* @access private
* @return string
*/
private function AttachAll() {
// Return text of body
$mime = array();
$cidUniq = array();
$incl = array();
// Add all attachments
foreach ($this->attachment as $attachment) {
// Check for string attachment
$bString = $attachment[5];
if ($bString) {
$string = $attachment[0];
} else {
$path = $attachment[0];
}
if (in_array($attachment[0], $incl)) { continue; }
$filename = $attachment[1];
$name = $attachment[2];
$encoding = $attachment[3];
$type = $attachment[4];
$disposition = $attachment[6];
$cid = $attachment[7];
$incl[] = $attachment[0];
if ( $disposition == 'inline' && isset($cidUniq[$cid]) ) { continue; }
$cidUniq[$cid] = true;
$mime[] = sprintf("--%s%s", $this->boundary[1], $this->LE);
$mime[] = sprintf("Content-Type: %s; name=\"%s\"%s", $type, $this->EncodeHeader($this->SecureHeader($name)), $this->LE);
$mime[] = sprintf("Content-Transfer-Encoding: %s%s", $encoding, $this->LE);
if($disposition == 'inline') {
$mime[] = sprintf("Content-ID: <%s>%s", $cid, $this->LE);
}
$mime[] = sprintf("Content-Disposition: %s; filename=\"%s\"%s", $disposition, $this->EncodeHeader($this->SecureHeader($name)), $this->LE.$this->LE);
// Encode as string attachment
if($bString) {
$mime[] = $this->EncodeString($string, $encoding);
if($this->IsError()) {
return '';
}
$mime[] = $this->LE.$this->LE;
} else {
$mime[] = $this->EncodeFile($path, $encoding);
if($this->IsError()) {
return '';
}
$mime[] = $this->LE.$this->LE;
}
}
$mime[] = sprintf("--%s--%s", $this->boundary[1], $this->LE);
return join('', $mime);
}
/**
* Encodes attachment in requested format.
* Returns an empty string on failure.
* @param string $path The full path to the file
* @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable'
* @see EncodeFile()
* @access private
* @return string
*/
private function EncodeFile($path, $encoding = 'base64') {
try {
if (!is_readable($path)) {
throw new phpmailerException($this->Lang('file_open') . $path, self::STOP_CONTINUE);
}
if (function_exists('get_magic_quotes')) {
function get_magic_quotes() {
return false;
}
}
if (PHP_VERSION < 6) {
$magic_quotes = get_magic_quotes_runtime();
set_magic_quotes_runtime(0);
}
$file_buffer = file_get_contents($path);
$file_buffer = $this->EncodeString($file_buffer, $encoding);
if (PHP_VERSION < 6) { set_magic_quotes_runtime($magic_quotes); }
return $file_buffer;
} catch (Exception $e) {
$this->SetError($e->getMessage());
return '';
}
}
/**
* Encodes string to requested format.
* Returns an empty string on failure.
* @param string $str The text to encode
* @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable'
* @access public
* @return string
*/
public function EncodeString ($str, $encoding = 'base64') {
$encoded = '';
switch(strtolower($encoding)) {
case 'base64':
$encoded = chunk_split(base64_encode($str), 76, $this->LE);
break;
case '7bit':
case '8bit':
$encoded = $this->FixEOL($str);
//Make sure it ends with a line break
if (substr($encoded, -(strlen($this->LE))) != $this->LE)
$encoded .= $this->LE;
break;
case 'binary':
$encoded = $str;
break;
case 'quoted-printable':
$encoded = $this->EncodeQP($str);
break;
default:
$this->SetError($this->Lang('encoding') . $encoding);
break;
}
return $encoded;
}
/**
* Encode a header string to best (shortest) of Q, B, quoted or none.
* @access public
* @return string
*/
public function EncodeHeader($str, $position = 'text') {
$x = 0;
switch (strtolower($position)) {
case 'phrase':
if (!preg_match('/[\200-\377]/', $str)) {
// Can't use addslashes as we don't know what value has magic_quotes_sybase
$encoded = addcslashes($str, "\0..\37\177\\\"");
if (($str == $encoded) && !preg_match('/[^A-Za-z0-9!#$%&\'*+\/=?^_`{|}~ -]/', $str)) {
return ($encoded);
} else {
return ("\"$encoded\"");
}
}
$x = preg_match_all('/[^\040\041\043-\133\135-\176]/', $str, $matches);
break;
case 'comment':
$x = preg_match_all('/[()"]/', $str, $matches);
// Fall-through
case 'text':
default:
$x += preg_match_all('/[\000-\010\013\014\016-\037\177-\377]/', $str, $matches);
break;
}
if ($x == 0) {
return ($str);
}
$maxlen = 75 - 7 - strlen($this->CharSet);
// Try to select the encoding which should produce the shortest output
if (strlen($str)/3 < $x) {
$encoding = 'B';
if (function_exists('mb_strlen') && $this->HasMultiBytes($str)) {
// Use a custom function which correctly encodes and wraps long
// multibyte strings without breaking lines within a character
$encoded = $this->Base64EncodeWrapMB($str);
} else {
$encoded = base64_encode($str);
$maxlen -= $maxlen % 4;
$encoded = trim(chunk_split($encoded, $maxlen, "\n"));
}
} else {
$encoding = 'Q';
$encoded = $this->EncodeQ($str, $position);
$encoded = $this->WrapText($encoded, $maxlen, true);
$encoded = str_replace('='.$this->LE, "\n", trim($encoded));
}
$encoded = preg_replace('/^(.*)$/m', " =?".$this->CharSet."?$encoding?\\1?=", $encoded);
$encoded = trim(str_replace("\n", $this->LE, $encoded));
return $encoded;
}
/**
* Checks if a string contains multibyte characters.
* @access public
* @param string $str multi-byte text to wrap encode
* @return bool
*/
public function HasMultiBytes($str) {
if (function_exists('mb_strlen')) {
return (strlen($str) > mb_strlen($str, $this->CharSet));
} else { // Assume no multibytes (we can't handle without mbstring functions anyway)
return false;
}
}
/**
* Correctly encodes and wraps long multibyte strings for mail headers
* without breaking lines within a character.
* Adapted from a function by paravoid at http://uk.php.net/manual/en/function.mb-encode-mimeheader.php
* @access public
* @param string $str multi-byte text to wrap encode
* @return string
*/
public function Base64EncodeWrapMB($str) {
$start = "=?".$this->CharSet."?B?";
$end = "?=";
$encoded = "";
$mb_length = mb_strlen($str, $this->CharSet);
// Each line must have length <= 75, including $start and $end
$length = 75 - strlen($start) - strlen($end);
// Average multi-byte ratio
$ratio = $mb_length / strlen($str);
// Base64 has a 4:3 ratio
$offset = $avgLength = floor($length * $ratio * .75);
for ($i = 0; $i < $mb_length; $i += $offset) {
$lookBack = 0;
do {
$offset = $avgLength - $lookBack;
$chunk = mb_substr($str, $i, $offset, $this->CharSet);
$chunk = base64_encode($chunk);
$lookBack++;
}
while (strlen($chunk) > $length);
$encoded .= $chunk . $this->LE;
}
// Chomp the last linefeed
$encoded = substr($encoded, 0, -strlen($this->LE));
return $encoded;
}
/**
* Encode string to quoted-printable.
* Only uses standard PHP, slow, but will always work
* @access public
* @param string $string the text to encode
* @param integer $line_max Number of chars allowed on a line before wrapping
* @return string
*/
public function EncodeQPphp( $input = '', $line_max = 76, $space_conv = false) {
$hex = array('0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F');
$lines = preg_split('/(?:\r\n|\r|\n)/', $input);
$eol = "\r\n";
$escape = '=';
$output = '';
while( list(, $line) = each($lines) ) {
$linlen = strlen($line);
$newline = '';
for($i = 0; $i < $linlen; $i++) {
$c = substr( $line, $i, 1 );
$dec = ord( $c );
if ( ( $i == 0 ) && ( $dec == 46 ) ) { // convert first point in the line into =2E
$c = '=2E';
}
if ( $dec == 32 ) {
if ( $i == ( $linlen - 1 ) ) { // convert space at eol only
$c = '=20';
} else if ( $space_conv ) {
$c = '=20';
}
} elseif ( ($dec == 61) || ($dec < 32 ) || ($dec > 126) ) { // always encode "\t", which is *not* required
$h2 = floor($dec/16);
$h1 = floor($dec%16);
$c = $escape.$hex[$h2].$hex[$h1];
}
if ( (strlen($newline) + strlen($c)) >= $line_max ) { // CRLF is not counted
$output .= $newline.$escape.$eol; // soft line break; " =\r\n" is okay
$newline = '';
// check if newline first character will be point or not
if ( $dec == 46 ) {
$c = '=2E';
}
}
$newline .= $c;
} // end of for
$output .= $newline.$eol;
} // end of while
return $output;
}
/**
* Encode string to RFC2045 (6.7) quoted-printable format
* Uses a PHP5 stream filter to do the encoding about 64x faster than the old version
* Also results in same content as you started with after decoding
* @see EncodeQPphp()
* @access public
* @param string $string the text to encode
* @param integer $line_max Number of chars allowed on a line before wrapping
* @param boolean $space_conv Dummy param for compatibility with existing EncodeQP function
* @return string
* @author Marcus Bointon
*/
public function EncodeQP($string, $line_max = 76, $space_conv = false) {
if (function_exists('quoted_printable_encode')) { //Use native function if it's available (>= PHP5.3)
return quoted_printable_encode($string);
}
$filters = stream_get_filters();
if (!in_array('convert.*', $filters)) { //Got convert stream filter?
return $this->EncodeQPphp($string, $line_max, $space_conv); //Fall back to old implementation
}
$fp = fopen('php://temp/', 'r+');
$string = preg_replace('/\r\n?/', $this->LE, $string); //Normalise line breaks
$params = array('line-length' => $line_max, 'line-break-chars' => $this->LE);
$s = stream_filter_append($fp, 'convert.quoted-printable-encode', STREAM_FILTER_READ, $params);
fputs($fp, $string);
rewind($fp);
$out = stream_get_contents($fp);
stream_filter_remove($s);
$out = preg_replace('/^\./m', '=2E', $out); //Encode . if it is first char on a line, workaround for bug in Exchange
fclose($fp);
return $out;
}
/**
* NOTE: Phabricator patch to remove use of "/e". See D2147.
*/
private function encodeQCallback(array $matches) {
return '='.sprintf('%02X', ord($matches[1]));
}
/**
* Encode string to q encoding.
* @link http://tools.ietf.org/html/rfc2047
* @param string $str the text to encode
* @param string $position Where the text is going to be used, see the RFC for what that means
* @access public
* @return string
*/
public function EncodeQ ($str, $position = 'text') {
// NOTE: Phabricator patch to remove use of "/e". See D2147.
// There should not be any EOL in the string
$encoded = preg_replace('/[\r\n]*/', '', $str);
switch (strtolower($position)) {
case 'phrase':
$encoded = preg_replace_callback(
"/([^A-Za-z0-9!*+\/ -])/",
array($this, 'encodeQCallback'),
$encoded);
break;
case 'comment':
$encoded = preg_replace_callback(
"/([\(\)\"])/",
array($this, 'encodeQCallback'),
$encoded);
break;
case 'text':
default:
// Replace every high ascii, control =, ? and _ characters
$encoded = preg_replace_callback(
'/([\000-\011\013\014\016-\037\075\077\137\177-\377])/',
array($this, 'encodeQCallback'),
$encoded);
break;
}
// Replace every spaces to _ (more readable than =20)
$encoded = str_replace(' ', '_', $encoded);
return $encoded;
}
/**
* Adds a string or binary attachment (non-filesystem) to the list.
* This method can be used to attach ascii or binary data,
* such as a BLOB record from a database.
* @param string $string String attachment data.
* @param string $filename Name of the attachment.
* @param string $encoding File encoding (see $Encoding).
* @param string $type File extension (MIME) type.
* @return void
*/
public function AddStringAttachment($string, $filename, $encoding = 'base64', $type = 'application/octet-stream') {
// Append to $attachment array
$this->attachment[] = array(
0 => $string,
1 => $filename,
2 => basename($filename),
3 => $encoding,
4 => $type,
5 => true, // isStringAttachment
6 => 'attachment',
7 => 0
);
}
/**
* Adds an embedded attachment. This can include images, sounds, and
* just about any other document. Make sure to set the $type to an
* image type. For JPEG images use "image/jpeg" and for GIF images
* use "image/gif".
* @param string $path Path to the attachment.
* @param string $cid Content ID of the attachment. Use this to identify
* the Id for accessing the image in an HTML form.
* @param string $name Overrides the attachment name.
* @param string $encoding File encoding (see $Encoding).
* @param string $type File extension (MIME) type.
* @return bool
*/
public function AddEmbeddedImage($path, $cid, $name = '', $encoding = 'base64', $type = 'application/octet-stream') {
if ( !@is_file($path) ) {
$this->SetError($this->Lang('file_access') . $path);
return false;
}
$filename = basename($path);
if ( $name == '' ) {
$name = $filename;
}
// Append to $attachment array
$this->attachment[] = array(
0 => $path,
1 => $filename,
2 => $name,
3 => $encoding,
4 => $type,
5 => false, // isStringAttachment
6 => 'inline',
7 => $cid
);
return true;
}
/**
* Returns true if an inline attachment is present.
* @access public
* @return bool
*/
public function InlineImageExists() {
foreach($this->attachment as $attachment) {
if ($attachment[6] == 'inline') {
return true;
}
}
return false;
}
/////////////////////////////////////////////////
// CLASS METHODS, MESSAGE RESET
/////////////////////////////////////////////////
/**
* Clears all recipients assigned in the TO array. Returns void.
* @return void
*/
public function ClearAddresses() {
foreach($this->to as $to) {
unset($this->all_recipients[strtolower($to[0])]);
}
$this->to = array();
}
/**
* Clears all recipients assigned in the CC array. Returns void.
* @return void
*/
public function ClearCCs() {
foreach($this->cc as $cc) {
unset($this->all_recipients[strtolower($cc[0])]);
}
$this->cc = array();
}
/**
* Clears all recipients assigned in the BCC array. Returns void.
* @return void
*/
public function ClearBCCs() {
foreach($this->bcc as $bcc) {
unset($this->all_recipients[strtolower($bcc[0])]);
}
$this->bcc = array();
}
/**
* Clears all recipients assigned in the ReplyTo array. Returns void.
* @return void
*/
public function ClearReplyTos() {
$this->ReplyTo = array();
}
/**
* Clears all recipients assigned in the TO, CC and BCC
* array. Returns void.
* @return void
*/
public function ClearAllRecipients() {
$this->to = array();
$this->cc = array();
$this->bcc = array();
$this->all_recipients = array();
}
/**
* Clears all previously set filesystem, string, and binary
* attachments. Returns void.
* @return void
*/
public function ClearAttachments() {
$this->attachment = array();
}
/**
* Clears all custom headers. Returns void.
* @return void
*/
public function ClearCustomHeaders() {
$this->CustomHeader = array();
}
/////////////////////////////////////////////////
// CLASS METHODS, MISCELLANEOUS
/////////////////////////////////////////////////
/**
* Adds the error message to the error container.
* @access protected
* @return void
*/
protected function SetError($msg) {
$this->error_count++;
$this->ErrorInfo = $msg;
}
/**
* Returns the proper RFC 822 formatted date.
* @access public
* @return string
* @static
*/
public static function RFCDate() {
$tz = date('Z');
$tzs = ($tz < 0) ? '-' : '+';
$tz = abs($tz);
$tz = (int)($tz/3600)*100 + ($tz%3600)/60;
$result = sprintf("%s %s%04d", date('D, j M Y H:i:s'), $tzs, $tz);
return $result;
}
/**
* Returns the server hostname or 'localhost.localdomain' if unknown.
* @access private
* @return string
*/
private function ServerHostname() {
if (!empty($this->Hostname)) {
$result = $this->Hostname;
} elseif (isset($_SERVER['SERVER_NAME'])) {
$result = $_SERVER['SERVER_NAME'];
} else {
$result = 'localhost.localdomain';
}
return $result;
}
/**
* Returns a message in the appropriate language.
* @access private
* @return string
*/
private function Lang($key) {
if(count($this->language) < 1) {
$this->SetLanguage('en'); // set the default language
}
if(isset($this->language[$key])) {
return $this->language[$key];
} else {
return 'Language string failed to load: ' . $key;
}
}
/**
* Returns true if an error occurred.
* @access public
* @return bool
*/
public function IsError() {
return ($this->error_count > 0);
}
/**
* Changes every end of line from CR or LF to CRLF.
* @access private
* @return string
*/
private function FixEOL($str) {
$str = str_replace("\r\n", "\n", $str);
$str = str_replace("\r", "\n", $str);
$str = str_replace("\n", $this->LE, $str);
return $str;
}
/**
* Adds a custom header.
* @access public
* @return void
*/
public function AddCustomHeader($custom_header) {
$this->CustomHeader[] = explode(':', $custom_header, 2);
}
/**
* Evaluates the message and returns modifications for inline images and backgrounds
* @access public
* @return $message
*/
public function MsgHTML($message, $basedir = '') {
preg_match_all("/(src|background)=\"(.*)\"/Ui", $message, $images);
if(isset($images[2])) {
foreach($images[2] as $i => $url) {
// do not change urls for absolute images (thanks to corvuscorax)
if (!preg_match('#^[A-z]+://#',$url)) {
$filename = basename($url);
$directory = dirname($url);
($directory == '.')?$directory='':'';
$cid = 'cid:' . md5($filename);
$ext = pathinfo($filename, PATHINFO_EXTENSION);
$mimeType = self::_mime_types($ext);
if ( strlen($basedir) > 1 && substr($basedir,-1) != '/') { $basedir .= '/'; }
if ( strlen($directory) > 1 && substr($directory,-1) != '/') { $directory .= '/'; }
if ( $this->AddEmbeddedImage($basedir.$directory.$filename, md5($filename), $filename, 'base64',$mimeType) ) {
$message = preg_replace("/".$images[1][$i]."=\"".preg_quote($url, '/')."\"/Ui", $images[1][$i]."=\"".$cid."\"", $message);
}
}
}
}
$this->IsHTML(true);
$this->Body = $message;
$textMsg = trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/s','',$message)));
if (!empty($textMsg) && empty($this->AltBody)) {
$this->AltBody = html_entity_decode($textMsg);
}
if (empty($this->AltBody)) {
$this->AltBody = 'To view this email message, open it in a program that understands HTML!' . "\n\n";
}
}
/**
* Gets the MIME type of the embedded or inline image
* @param string File extension
* @access public
* @return string MIME type of ext
* @static
*/
public static function _mime_types($ext = '') {
$mimes = array(
'hqx' => 'application/mac-binhex40',
'cpt' => 'application/mac-compactpro',
'doc' => 'application/msword',
'bin' => 'application/macbinary',
'dms' => 'application/octet-stream',
'lha' => 'application/octet-stream',
'lzh' => 'application/octet-stream',
'exe' => 'application/octet-stream',
'class' => 'application/octet-stream',
'psd' => 'application/octet-stream',
'so' => 'application/octet-stream',
'sea' => 'application/octet-stream',
'dll' => 'application/octet-stream',
'oda' => 'application/oda',
'pdf' => 'application/pdf',
'ai' => 'application/postscript',
'eps' => 'application/postscript',
'ps' => 'application/postscript',
'smi' => 'application/smil',
'smil' => 'application/smil',
'mif' => 'application/vnd.mif',
'xls' => 'application/vnd.ms-excel',
'ppt' => 'application/vnd.ms-powerpoint',
'wbxml' => 'application/vnd.wap.wbxml',
'wmlc' => 'application/vnd.wap.wmlc',
'dcr' => 'application/x-director',
'dir' => 'application/x-director',
'dxr' => 'application/x-director',
'dvi' => 'application/x-dvi',
'gtar' => 'application/x-gtar',
'php' => 'application/x-httpd-php',
'php4' => 'application/x-httpd-php',
'php3' => 'application/x-httpd-php',
'phtml' => 'application/x-httpd-php',
'phps' => 'application/x-httpd-php-source',
'js' => 'application/x-javascript',
'swf' => 'application/x-shockwave-flash',
'sit' => 'application/x-stuffit',
'tar' => 'application/x-tar',
'tgz' => 'application/x-tar',
'xhtml' => 'application/xhtml+xml',
'xht' => 'application/xhtml+xml',
'zip' => 'application/zip',
'mid' => 'audio/midi',
'midi' => 'audio/midi',
'mpga' => 'audio/mpeg',
'mp2' => 'audio/mpeg',
'mp3' => 'audio/mpeg',
'aif' => 'audio/x-aiff',
'aiff' => 'audio/x-aiff',
'aifc' => 'audio/x-aiff',
'ram' => 'audio/x-pn-realaudio',
'rm' => 'audio/x-pn-realaudio',
'rpm' => 'audio/x-pn-realaudio-plugin',
'ra' => 'audio/x-realaudio',
'rv' => 'video/vnd.rn-realvideo',
'wav' => 'audio/x-wav',
'bmp' => 'image/bmp',
'gif' => 'image/gif',
'jpeg' => 'image/jpeg',
'jpg' => 'image/jpeg',
'jpe' => 'image/jpeg',
'png' => 'image/png',
'tiff' => 'image/tiff',
'tif' => 'image/tiff',
'css' => 'text/css',
'html' => 'text/html',
'htm' => 'text/html',
'shtml' => 'text/html',
'txt' => 'text/plain',
'text' => 'text/plain',
'log' => 'text/plain',
'rtx' => 'text/richtext',
'rtf' => 'text/rtf',
'xml' => 'text/xml',
'xsl' => 'text/xml',
'mpeg' => 'video/mpeg',
'mpg' => 'video/mpeg',
'mpe' => 'video/mpeg',
'qt' => 'video/quicktime',
'mov' => 'video/quicktime',
'avi' => 'video/x-msvideo',
'movie' => 'video/x-sgi-movie',
'doc' => 'application/msword',
'word' => 'application/msword',
'xl' => 'application/excel',
'eml' => 'message/rfc822'
);
return (!isset($mimes[strtolower($ext)])) ? 'application/octet-stream' : $mimes[strtolower($ext)];
}
/**
* Set (or reset) Class Objects (variables)
*
* Usage Example:
* $page->set('X-Priority', '3');
*
* @access public
* @param string $name Parameter Name
* @param mixed $value Parameter Value
* NOTE: will not work with arrays, there are no arrays to set/reset
* @todo Should this not be using __set() magic function?
*/
public function set($name, $value = '') {
try {
if (isset($this->$name) ) {
$this->$name = $value;
} else {
throw new phpmailerException($this->Lang('variable_set') . $name, self::STOP_CRITICAL);
}
} catch (Exception $e) {
$this->SetError($e->getMessage());
if ($e->getCode() == self::STOP_CRITICAL) {
return false;
}
}
return true;
}
/**
* Strips newlines to prevent header injection.
* @access public
* @param string $str String
* @return string
*/
public function SecureHeader($str) {
$str = str_replace("\r", '', $str);
$str = str_replace("\n", '', $str);
return trim($str);
}
/**
* Set the private key file and password to sign the message.
*
* @access public
* @param string $key_filename Parameter File Name
* @param string $key_pass Password for private key
*/
public function Sign($cert_filename, $key_filename, $key_pass) {
$this->sign_cert_file = $cert_filename;
$this->sign_key_file = $key_filename;
$this->sign_key_pass = $key_pass;
}
/**
* Set the private key file and password to sign the message.
*
* @access public
* @param string $key_filename Parameter File Name
* @param string $key_pass Password for private key
*/
public function DKIM_QP($txt) {
$tmp="";
$line="";
for ($i=0;$i<strlen($txt);$i++) {
$ord=ord($txt[$i]);
if ( ((0x21 <= $ord) && ($ord <= 0x3A)) || $ord == 0x3C || ((0x3E <= $ord) && ($ord <= 0x7E)) ) {
$line.=$txt[$i];
} else {
$line.="=".sprintf("%02X",$ord);
}
}
return $line;
}
/**
* Generate DKIM signature
*
* @access public
* @param string $s Header
*/
public function DKIM_Sign($s) {
$privKeyStr = file_get_contents($this->DKIM_private);
if ($this->DKIM_passphrase!='') {
$privKey = openssl_pkey_get_private($privKeyStr,$this->DKIM_passphrase);
} else {
$privKey = $privKeyStr;
}
if (openssl_sign($s, $signature, $privKey)) {
return base64_encode($signature);
}
}
/**
* Generate DKIM Canonicalization Header
*
* @access public
* @param string $s Header
*/
public function DKIM_HeaderC($s) {
$s=preg_replace("/\r\n\s+/"," ",$s);
$lines=explode("\r\n",$s);
foreach ($lines as $key=>$line) {
list($heading,$value)=explode(":",$line,2);
$heading=strtolower($heading);
$value=preg_replace("/\s+/"," ",$value) ; // Compress useless spaces
$lines[$key]=$heading.":".trim($value) ; // Don't forget to remove WSP around the value
}
$s=implode("\r\n",$lines);
return $s;
}
/**
* Generate DKIM Canonicalization Body
*
* @access public
* @param string $body Message Body
*/
public function DKIM_BodyC($body) {
if ($body == '') return "\r\n";
// stabilize line endings
$body=str_replace("\r\n","\n",$body);
$body=str_replace("\n","\r\n",$body);
// END stabilize line endings
while (substr($body,strlen($body)-4,4) == "\r\n\r\n") {
$body=substr($body,0,strlen($body)-2);
}
return $body;
}
/**
* Create the DKIM header, body, as new header
*
* @access public
* @param string $headers_line Header lines
* @param string $subject Subject
* @param string $body Body
*/
public function DKIM_Add($headers_line,$subject,$body) {
$DKIMsignatureType = 'rsa-sha1'; // Signature & hash algorithms
$DKIMcanonicalization = 'relaxed/simple'; // Canonicalization of header/body
$DKIMquery = 'dns/txt'; // Query method
$DKIMtime = time() ; // Signature Timestamp = seconds since 00:00:00 - Jan 1, 1970 (UTC time zone)
$subject_header = "Subject: $subject";
$headers = explode("\r\n",$headers_line);
foreach($headers as $header) {
if (strpos($header,'From:') === 0) {
$from_header=$header;
} elseif (strpos($header,'To:') === 0) {
$to_header=$header;
}
}
$from = str_replace('|','=7C',$this->DKIM_QP($from_header));
$to = str_replace('|','=7C',$this->DKIM_QP($to_header));
$subject = str_replace('|','=7C',$this->DKIM_QP($subject_header)) ; // Copied header fields (dkim-quoted-printable
$body = $this->DKIM_BodyC($body);
$DKIMlen = strlen($body) ; // Length of body
$DKIMb64 = base64_encode(pack("H*", sha1($body))) ; // Base64 of packed binary SHA-1 hash of body
$ident = ($this->DKIM_identity == '')? '' : " i=" . $this->DKIM_identity . ";";
$dkimhdrs = "DKIM-Signature: v=1; a=" . $DKIMsignatureType . "; q=" . $DKIMquery . "; l=" . $DKIMlen . "; s=" . $this->DKIM_selector . ";\r\n".
"\tt=" . $DKIMtime . "; c=" . $DKIMcanonicalization . ";\r\n".
"\th=From:To:Subject;\r\n".
"\td=" . $this->DKIM_domain . ";" . $ident . "\r\n".
"\tz=$from\r\n".
"\t|$to\r\n".
"\t|$subject;\r\n".
"\tbh=" . $DKIMb64 . ";\r\n".
"\tb=";
$toSign = $this->DKIM_HeaderC($from_header . "\r\n" . $to_header . "\r\n" . $subject_header . "\r\n" . $dkimhdrs);
$signed = $this->DKIM_Sign($toSign);
return "X-PHPMAILER-DKIM: phpmailer.sourceforge.net\r\n".$dkimhdrs.$signed."\r\n";
}
protected function doCallback($isSent,$to,$cc,$bcc,$subject,$body) {
if (!empty($this->action_function) && function_exists($this->action_function)) {
$params = array($isSent,$to,$cc,$bcc,$subject,$body);
call_user_func_array($this->action_function,$params);
}
}
}
class phpmailerException extends Exception {
public function errorMessage() {
$errorMsg = '<strong>' . $this->getMessage() . "</strong><br />\n";
return $errorMsg;
}
}
?>
diff --git a/externals/twilio-php/AUTHORS.md b/externals/twilio-php/AUTHORS.md
deleted file mode 100644
index 0ece74d23..000000000
--- a/externals/twilio-php/AUTHORS.md
+++ /dev/null
@@ -1,35 +0,0 @@
-Authors
-=======
-
-A huge thanks to all of our contributors:
-
-
-- =noloh
-- Adam Ballai
-- Alex Chan
-- Alex Rowley
-- Brett Gerry
-- Bulat Shakirzyanov
-- Chris Barr
-- D Keith Casey Jr
-- D. Keith Casey, Jr.
-- Doug Black
-- John Britton
-- Jordi Boggiano
-- Keith Casey
-- Kevin Burke
-- Kyle
-- Kyle Conroy
-- Luke Waite
-- Neuman
-- Neuman Vong
-- Peter Meth
-- Ryan Brideau
-- Sam Kimbrel
-- Shawn Parker
-- Stuart Langley
-- Taichiro Yoshida
-- Trenton McManus
-- aaronfoss
-- sashalaundy
-- till
diff --git a/externals/twilio-php/CHANGES.md b/externals/twilio-php/CHANGES.md
deleted file mode 100644
index 1bd673138..000000000
--- a/externals/twilio-php/CHANGES.md
+++ /dev/null
@@ -1,261 +0,0 @@
-twilio-php Changelog
-====================
-
-Version 3.12.4
---------------
-
-Released on January 30, 2014
-
-- Fix incorrect use of static:: which broke compatibility with PHP 5.2.
-
-Version 3.12.3
---------------
-
-Released on January 28, 2014
-
-- Add link from recordings to associated transcriptions.
-- Document how to debug requests, improve TwiML generation docs.
-
-Version 3.12.2
---------------
-
-Released on January 5, 2014
-
-- Fixes string representation of resources
-- Support PHP 5.5
-
-Version 3.12.1
---------------
-
-Released on October 21, 2013
-
-- Add support for filtering by type for IncomingPhoneNumbers.
-- Add support for searching for mobile numbers for both
-IncomingPhoneNumbers and AvailablePhoneNumbers.
-
-Version 3.12.0
---------------
-
-Released on September 18, 2013
-
-- Support MMS
-- Support SIP In
-- $params arrays will now turn lists into multiple HTTP keys with the same name,
-
- array("Twilio" => array('foo', 'bar'))
-
- will turn into Twilio=foo&Twilio=bar when sent to the API.
-
-- Update the documentation to use php-autodoc and Sphinx.
-
-Version 3.11.0
---------------
-
-Released on June 13
-
-- Support Streams when curl is not available for PHP installations
-
-Version 3.10.0
---------------
-
-Released on February 2, 2013
-
-- Uses the [HTTP status code for error reporting][http], instead of the
-`status` attribute of the JSON response. (Reporter: [Ruud Kamphuis](/ruudk))
-
-[http]: https://github.com/twilio/twilio-php/pull/116
-
-Version 3.9.1
--------------
-
-Released on December 30, 2012
-
-- Adds a `$last_response` parameter to the `$client` object that can be
-used to [retrieve the raw API response][last-response]. (Reporter: [David
-Jones](/dxjones))
-
-[last-response]: https://github.com/twilio/twilio-php/pull/112/files
-
-Version 3.9.0
--------------
-
-Released on December 20, 2012
-
-- [Fixes TwiML generation to handle non-ASCII characters properly][utf-8]. Note
- that as of version 3.9.0, **the library requires PHP version 5.2.3, at least
- for TwiML generation**. (Reporter: [Walker Hamilton](/walker))
-
-[utf-8]: https://github.com/twilio/twilio-php/pull/111
-
-Version 3.8.3
--------------
-
-Released on December 15, 2012
-
-- [Fixes the ShortCode resource][shortcode] so it is queryable via the PHP library.
-
- [shortcode]: https://github.com/twilio/twilio-php/pull/108
-
-Version 3.8.2
--------------
-
-Released on November 26, 2012
-
-- Fixes an issue where you [could not iterate over the members in a
-queue][queue-members]. (Reporter: [Alex Chan](/alexcchan))
-
-[queue-members]: https://github.com/twilio/twilio-php/pull/107
-
-Version 3.8.1
--------------
-
-Released on November 23, 2012
-
-- [Implements the Countable interface on the ListResource][countable], so you
- can call count() on any resource.
-- [Adds a convenience method for retrieving a phone number object][get-number],
- so you can retrieve all of a number's properties by its E.164 representation.
-
-Internally:
-
-- Adds [unit tests for url encoding of Unicode characters][unicode-tests].
-- Updates [Travis CI configuration to use Composer][travis-composer],
-shortening build time from 83 seconds to 21 seconds.
-
-[countable]: https://twilio-php.readthedocs.org/en/latest/usage/rest.html#retrieving-the-total-number-of-resources
-[get-number]: https://twilio-php.readthedocs.org/en/latest/usage/rest/phonenumbers.html#retrieving-all-of-a-number-s-properties
-[unicode-tests]: https://github.com/twilio/twilio-php/commit/6f8aa57885796691858e460c8cea748e241c47e3
-[travis-composer]: https://github.com/twilio/twilio-php/commit/a732358e90e1ae9a5a3348ad77dda8cc8b5ec6bc
-
-Version 3.8.0
--------------
-
-Released on October 17, 2012
-
-- Support the new Usage API, with Usage Records and Usage Triggers. Read the
- PHP documentation for [usage records][records] or [usage triggers][triggers]
-
- [records]: https://twilio-php.readthedocs.org/en/latest/usage/rest/usage-records.html
- [triggers]: https://twilio-php.readthedocs.org/en/latest/usage/rest/usage-triggers.html
-
-Version 3.7.2
--------------
-
-- The library will now [use a standard CA cert whitelist][whitelist] for SSL
- validation, replacing a file that contained only Twilio's SSL certificate.
- (Reporter: [Andrew Benton](/andrewmbenton))
-
- [whitelist]: https://github.com/twilio/twilio-php/issues/88
-
-Version 3.7.1
--------------
-
-Released on August 16, 2012
-
-- Fix a bug in the 3.5.0 release where [updating an instance
- resource would cause subsequent updates to request an incorrect
- URI](/twilio/twilio-php/pull/82).
- (Reporter: [Dan Bowen](/crucialwebstudio))
-
-Version 3.7.0
--------------
-
-Released on August 6, 2012
-
-- Add retry support for idempotent HTTP requests that result in a 500 server
- error (default is 1 attempt, however this can be configured).
-- Throw a Services_Twilio_RestException instead of a DomainException if the
- response content cannot be parsed as JSON (usually indicates a 500 error)
-
-Version 3.6.0
--------------
-
-Released on August 5, 2012
-
-- Add support for Queues and Members. Includes tests and documentation for the
- new functionality.
-
-Version 3.5.2
--------------
-
-Released on July 23, 2012
-
-- Fix an issue introduced in the 3.5.0 release where updating or muting
- a participant would [throw an exception instead of muting the
- participant][mute-request].
- (Reporter: [Alex Chan](/alexcchan))
-
-- Fix an issue introduced in the 3.5.0 release where [filtering an iterator
-with parameters would not work properly][paging-request] on subsequent HTTP
-requests. (Reporters: [Alex Chan](/alexcchan), Ivor O'Connor)
-
-[mute-request]: /twilio/twilio-php/pull/74
-[paging-request]: /twilio/twilio-php/pull/75
-
-Version 3.5.1
--------------
-
-Released on July 2, 2012
-
-- Fix an issue introduced in the 3.5.0 release that would cause a second HTTP
-request for an instance resource [to request an incorrect URI][issue-71].
-
-[issue-71]: https://github.com/twilio/twilio-php/pull/71
-
-Version 3.5.0
--------------
-
-Released on June 30, 2012
-
-- Support paging through resources using the `next_page_uri` parameter instead
-of manually constructing parameters using the `Page` and `PageSize` parameters.
-Specifically, this allows the library to use the `AfterSid` parameter, which
-leads to improved performance when paging deep into your resource list.
-
-This involved a major refactor of the library. The documented interface to
-twilio-php will not change. However, some undocumented public methods are no
-longer supported. Specifically, the following classes are no longer available:
-
-- `Services/Twilio/ArrayDataProxy.php`
-- `Services/Twilio/CachingDataProxy.php`
-- `Services/Twilio/DataProxy.php`
-
-In addition, the following public methods have been removed:
-
-- `setProxy`, in `Services/Twilio/InstanceResource.php`
-- `getSchema`, in `Services/Twilio/ListResource.php`,
- `Services/Twilio/Rest/AvailablePhoneNumbers.php`,
- `Services/Twilio/Rest/SMSMessages.php`
-
-- `retrieveData`, in `Services/Twilio/Resource.php`
-- `deleteData`, in `Services/Twilio/Resource.php`
-- `addSubresource`, in `Services/Twilio/Resource.php`
-
-Please check your own code for compatibility before upgrading.
-
-Version 3.3.2
--------------
-
-Released on May 3, 2012
-
-- If you pass booleans in as TwiML (ex transcribe="true"), convert them to
- the strings "true" and "false" instead of outputting the incorrect values
- 1 and "".
-
-Version 3.3.1
--------------
-
-Released on May 1, 2012
-
-- Use the 'Accept-Charset' header to specify we want to receive UTF-8 encoded
-data from the Twilio API. Remove unused XML parsing logic, as the library never
-requests XML data.
-
-Version 3.2.4
--------------
-
-Released on March 14, 2012
-
-- If no version is passed to the Services_Twilio constructor, the library will
- default to the most recent API version.
-
diff --git a/externals/twilio-php/LICENSE b/externals/twilio-php/LICENSE
deleted file mode 100644
index a81ef8bef..000000000
--- a/externals/twilio-php/LICENSE
+++ /dev/null
@@ -1,22 +0,0 @@
-MIT License
-
-Copyright (C) 2011, Twilio, Inc. <help at twilio dot com>
-Copyright (C) 2011, Neuman Vong <neuman at twilio dot com>
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-of the Software, and to permit persons to whom the Software is furnished to do
-so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/externals/twilio-php/Makefile b/externals/twilio-php/Makefile
deleted file mode 100644
index fad48f634..000000000
--- a/externals/twilio-php/Makefile
+++ /dev/null
@@ -1,72 +0,0 @@
-# Twilio API helper library.
-# See LICENSE file for copyright and license details.
-
-define LICENSE
-<?php
-
-/**
- * Twilio API helper library.
- *
- * @category Services
- * @package Services_Twilio
- * @author Neuman Vong <neuman@twilio.com>
- * @license http://creativecommons.org/licenses/MIT/ MIT
- * @link http://pear.php.net/package/Services_Twilio
- */
-endef
-export LICENSE
-
-COMPOSER = $(shell which composer)
-ifeq ($(strip $(COMPOSER)),)
- COMPOSER = php composer.phar
-endif
-
-all: test
-
-clean:
- @rm -rf dist venv
-
-PHP_FILES = `find dist -name \*.php`
-dist: clean
- @mkdir dist
- @git archive master | (cd dist; tar xf -)
- @for php in $(PHP_FILES); do\
- echo "$$LICENSE" > $$php.new; \
- tail -n+2 $$php >> $$php.new; \
- mv $$php.new $$php; \
- done
-
-test-install:
- # Composer: http://getcomposer.org/download/
- $(COMPOSER) install
-
-install:
- pear channel-discover twilio.github.com/pear
- pear install twilio/Services_Twilio
-
-# if these fail, you may need to install the helper library - run "make
-# test-install"
-test:
- @PATH=vendor/bin:$(PATH) phpunit --strict --colors --configuration tests/phpunit.xml;
-
-venv:
- virtualenv venv
-
-docs-install: venv
- . venv/bin/activate; pip install -r docs/requirements.txt
-
-docs:
- . venv/bin/activate; cd docs && make html
-
-release-install:
- pear channel-discover twilio.github.com/pear || true
- pear channel-discover pear.pirum-project.org || true
- pear install pirum/Pirum || true
- pear install XML_Serializer-0.20.2 || true
- pear install PEAR_PackageFileManager2 || true
-
-authors:
- echo "Authors\n=======\n\nA huge thanks to all of our contributors:\n\n" > AUTHORS.md
- git log --raw | grep "^Author: " | cut -d ' ' -f2- | cut -d '<' -f1 | sed 's/^/- /' | sort | uniq >> AUTHORS.md
-
-.PHONY: all clean dist test docs docs-install test-install authors
diff --git a/externals/twilio-php/README.md b/externals/twilio-php/README.md
deleted file mode 100644
index b9a28a0e0..000000000
--- a/externals/twilio-php/README.md
+++ /dev/null
@@ -1,136 +0,0 @@
-[![Build Status](https://secure.travis-ci.org/twilio/twilio-php.png?branch=master)](http://travis-ci.org/twilio/twilio-php)
-
-## Installation
-
-You can install **twilio-php** via PEAR or by downloading the source.
-
-#### Via PEAR (>= 1.9.3):
-
-PEAR is a package manager for PHP. Open a command line and use these PEAR
-commands to download the helper library:
-
- $ pear channel-discover twilio-pear.herokuapp.com/pear
- $ pear install twilio/Services_Twilio
-
-If you get the following message:
-
- $ -bash: pear: command not found
-
-you can install PEAR from their website, or download the source directly.
-
-#### Via Composer:
-
-**twilio-php** is available on Packagist as the
-[`twilio/sdk`](http://packagist.org/packages/twilio/sdk) package.
-
-#### Via ZIP file:
-
-[Click here to download the source
-(.zip)](https://github.com/twilio/twilio-php/zipball/master) which includes all
-dependencies.
-
-Once you download the library, move the twilio-php folder to your project
-directory and then include the library file:
-
- require '/path/to/twilio-php/Services/Twilio.php';
-
-and you're good to go!
-
-## A Brief Introduction
-
-With the twilio-php library, we've simplified interaction with the
-Twilio REST API. No need to manually create URLS or parse XML/JSON.
-You now interact with resources directly. Follow the [Quickstart
-Guide](http://readthedocs.org/docs/twilio-php/en/latest/#quickstart)
-to get up and running right now. The [User
-Guide](http://readthedocs.org/docs/twilio-php/en/latest/#user-guide) shows you
-how to get the most out of **twilio-php**.
-
-## Quickstart
-
-### Send an SMS
-
-```php
-<?php
-// Install the library via PEAR or download the .zip file to your project folder.
-// This line loads the library
-require('/path/to/twilio-php/Services/Twilio.php');
-
-$sid = "ACXXXXXX"; // Your Account SID from www.twilio.com/user/account
-$token = "YYYYYY"; // Your Auth Token from www.twilio.com/user/account
-
-$client = new Services_Twilio($sid, $token);
-$message = $client->account->messages->sendMessage(
- '9991231234', // From a valid Twilio number
- '8881231234', // Text this number
- "Hello monkey!"
-);
-
-print $message->sid;
-```
-
-### Make a Call
-
-```php
-<?php
-// Install the library via PEAR or download the .zip file to your project folder.
-// This line loads the library
-require('/path/to/twilio-php/Services/Twilio.php');
-
-$sid = "ACXXXXXX"; // Your Account SID from www.twilio.com/user/account
-$token = "YYYYYY"; // Your Auth Token from www.twilio.com/user/account
-
-$client = new Services_Twilio($sid, $token);
-$call = $client->account->calls->create(
- '9991231234', // From a valid Twilio number
- '8881231234', // Call this number
-
- // Read TwiML at this URL when a call connects (hold music)
- 'http://twimlets.com/holdmusic?Bucket=com.twilio.music.ambient'
-);
-```
-
-### Generating TwiML
-
-To control phone calls, your application needs to output
-[TwiML](http://www.twilio.com/docs/api/twiml/ "Twilio Markup Language"). Use
-`Services_Twilio_Twiml` to easily create such responses.
-
-```php
-<?php
-require('/path/to/twilio-php/Services/Twilio.php');
-
-$response = new Services_Twilio_Twiml();
-$response->say('Hello');
-$response->play('https://api.twilio.com/cowbell.mp3', array("loop" => 5));
-print $response;
-```
-
-That will output XML that looks like this:
-
-```xml
-<?xml version="1.0" encoding="utf-8"?>
-<Response>
- <Say>Hello</Say>
- <Play loop="5">https://api.twilio.com/cowbell.mp3</Play>
-<Response>
-```
-
-## [Full Documentation](http://readthedocs.org/docs/twilio-php/en/latest/ "Twilio PHP Library Documentation")
-
-The documentation for **twilio-php** is hosted
-at Read the Docs. [Click here to read our full
-documentation.](http://readthedocs.org/docs/twilio-php/en/latest/ "Twilio PHP
-Library Documentation")
-
-## Prerequisites
-
-* PHP >= 5.2.3
-* The PHP JSON extension
-
-## Reporting Issues
-
-We would love to hear your feedback. Report issues using the [Github
-Issue Tracker](https://github.com/twilio/twilio-php/issues) or email
-[help@twilio.com](mailto:help@twilio.com).
-
diff --git a/externals/twilio-php/Services/Twilio.php b/externals/twilio-php/Services/Twilio.php
deleted file mode 100644
index f15245bfb..000000000
--- a/externals/twilio-php/Services/Twilio.php
+++ /dev/null
@@ -1,313 +0,0 @@
-<?php
-
-/*
- * Author: Neuman Vong neuman@twilio.com
- * License: http://creativecommons.org/licenses/MIT/ MIT
- * Link: https://twilio-php.readthedocs.org/en/latest/
- */
-
-function Services_Twilio_autoload($className) {
- if (substr($className, 0, 15) != 'Services_Twilio') {
- return false;
- }
- $file = str_replace('_', '/', $className);
- $file = str_replace('Services/', '', $file);
- return include dirname(__FILE__) . "/$file.php";
-}
-
-spl_autoload_register('Services_Twilio_autoload');
-
-/**
- * Create a client to talk to the Twilio API.
- *
- *
- * :param string $sid: Your Account SID
- * :param string $token: Your Auth Token from `your dashboard
- * <https://www.twilio.com/user/account>`_
- * :param string $version: API version to use
- * :param $_http: A HTTP client for making requests.
- * :type $_http: :php:class:`Services_Twilio_TinyHttp`
- * :param int $retryAttempts:
- * Number of times to retry failed requests. Currently only idempotent
- * requests (GET's and DELETE's) are retried.
- *
- * Here's an example:
- *
- * .. code-block:: php
- *
- * require('Services/Twilio.php');
- * $client = new Services_Twilio('AC123', '456bef', null, null, 3);
- * // Take some action with the client, etc.
- */
-class Services_Twilio extends Services_Twilio_Resource
-{
- const USER_AGENT = 'twilio-php/3.12.4';
-
- protected $http;
- protected $retryAttempts;
- protected $last_response;
- protected $version;
- protected $versions = array('2008-08-01', '2010-04-01');
-
- public function __construct(
- $sid,
- $token,
- $version = null,
- Services_Twilio_TinyHttp $_http = null,
- $retryAttempts = 1
- ) {
- $this->version = in_array($version, $this->versions) ?
- $version : end($this->versions);
-
- if (null === $_http) {
- if (!in_array('openssl', get_loaded_extensions())) {
- throw new Services_Twilio_HttpException("The OpenSSL extension is required but not currently enabled. For more information, see http://php.net/manual/en/book.openssl.php");
- }
- if (in_array('curl', get_loaded_extensions())) {
- $_http = new Services_Twilio_TinyHttp(
- "https://api.twilio.com",
- array(
- "curlopts" => array(
- CURLOPT_USERAGENT => self::qualifiedUserAgent(phpversion()),
- CURLOPT_HTTPHEADER => array('Accept-Charset: utf-8'),
- CURLOPT_CAINFO => dirname(__FILE__) . '/cacert.pem',
- ),
- )
- );
- } else {
- $_http = new Services_Twilio_HttpStream(
- "https://api.twilio.com",
- array(
- "http_options" => array(
- "http" => array(
- "user_agent" => self::qualifiedUserAgent(phpversion()),
- "header" => "Accept-Charset: utf-8\r\n",
- ),
- "ssl" => array(
- 'verify_peer' => true,
- 'cafile' => dirname(__FILE__) . '/cacert.pem',
- 'verify_depth' => 5,
- ),
- ),
- )
- );
- }
- }
- $_http->authenticate($sid, $token);
- $this->http = $_http;
- $this->accounts = new Services_Twilio_Rest_Accounts($this, "/{$this->version}/Accounts");
- $this->account = $this->accounts->get($sid);
- $this->retryAttempts = $retryAttempts;
- }
-
- /**
- * Fully qualified user agent with the current PHP Version.
- *
- * :return: the user agent
- * :rtype: string
- */
- public static function qualifiedUserAgent($php_version) {
- return self::USER_AGENT . " (php $php_version)";
- }
-
- /**
- * Get the api version used by the rest client
- *
- * :return: the API version in use
- * :returntype: string
- */
- public function getVersion() {
- return $this->version;
- }
-
- /**
- * Get the retry attempt limit used by the rest client
- *
- * :return: the number of retry attempts
- * :rtype: int
- */
- public function getRetryAttempts() {
- return $this->retryAttempts;
- }
-
- /**
- * Construct a URI based on initial path, query params, and paging
- * information
- *
- * We want to use the query params, unless we have a next_page_uri from the
- * API.
- *
- * :param string $path: The request path (may contain query params if it's
- * a next_page_uri)
- * :param array $params: Query parameters to use with the request
- * :param boolean $full_uri: Whether the $path contains the full uri
- *
- * :return: the URI that should be requested by the library
- * :returntype: string
- */
- public static function getRequestUri($path, $params, $full_uri = false) {
- $json_path = $full_uri ? $path : "$path.json";
- if (!$full_uri && !empty($params)) {
- $query_path = $json_path . '?' . http_build_query($params, '', '&');
- } else {
- $query_path = $json_path;
- }
- return $query_path;
- }
-
- /**
- * Helper method for implementing request retry logic
- *
- * :param array $callable: The function that makes an HTTP request
- * :param string $uri: The URI to request
- * :param int $retriesLeft: Number of times to retry
- *
- * :return: The object representation of the resource
- * :rtype: object
- */
- protected function _makeIdempotentRequest($callable, $uri, $retriesLeft) {
- $response = call_user_func_array($callable, array($uri));
- list($status, $headers, $body) = $response;
- if ($status >= 500 && $retriesLeft > 0) {
- return $this->_makeIdempotentRequest($callable, $uri, $retriesLeft - 1);
- } else {
- return $this->_processResponse($response);
- }
- }
-
- /**
- * GET the resource at the specified path.
- *
- * :param string $path: Path to the resource
- * :param array $params: Query string parameters
- * :param boolean $full_uri: Whether the full URI has been passed as an
- * argument
- *
- * :return: The object representation of the resource
- * :rtype: object
- */
- public function retrieveData($path, $params = array(),
- $full_uri = false
- ) {
- $uri = self::getRequestUri($path, $params, $full_uri);
- return $this->_makeIdempotentRequest(array($this->http, 'get'),
- $uri, $this->retryAttempts);
- }
-
- /**
- * DELETE the resource at the specified path.
- *
- * :param string $path: Path to the resource
- * :param array $params: Query string parameters
- *
- * :return: The object representation of the resource
- * :rtype: object
- */
- public function deleteData($path, $params = array())
- {
- $uri = self::getRequestUri($path, $params);
- return $this->_makeIdempotentRequest(array($this->http, 'delete'),
- $uri, $this->retryAttempts);
- }
-
- /**
- * POST to the resource at the specified path.
- *
- * :param string $path: Path to the resource
- * :param array $params: Query string parameters
- *
- * :return: The object representation of the resource
- * :rtype: object
- */
- public function createData($path, $params = array())
- {
- $path = "$path.json";
- $headers = array('Content-Type' => 'application/x-www-form-urlencoded');
- $response = $this->http->post(
- $path, $headers, self::buildQuery($params, '')
- );
- return $this->_processResponse($response);
- }
-
- /**
- * Build a query string from query data
- *
- * :param array $queryData: An associative array of keys and values. The
- * values can be a simple type or a list, in which case the list is
- * converted to multiple query parameters with the same key.
- * :param string $numericPrefix:
- * :param string $queryStringStyle: Determine how to build the url
- * - strict: Build a standards compliant query string without braces (can be hacked by using braces in key)
- * - php: Build a PHP compatible query string with nested array syntax
- * :return: The encoded query string
- * :rtype: string
- */
- public static function buildQuery($queryData, $numericPrefix = '') {
- $query = '';
- // Loop through all of the $query_data
- foreach ($queryData as $key => $value) {
- // If the key is an int, add the numeric_prefix to the beginning
- if (is_int($key)) {
- $key = $numericPrefix . $key;
- }
-
- // If the value is an array, we will end up recursing
- if (is_array($value)) {
- // Loop through the values
- foreach ($value as $value2) {
- // Add an arg_separator if needed
- if ($query !== '') {
- $query .= '&';
- }
- // Recurse
- $query .= self::buildQuery(array($key => $value2), $numericPrefix);
- }
- } else {
- // Add an arg_separator if needed
- if ($query !== '') {
- $query .= '&';
- }
- // Add the key and the urlencoded value (as a string)
- $query .= $key . '=' . urlencode((string)$value);
- }
- }
- return $query;
- }
-
- /**
- * Convert the JSON encoded resource into a PHP object.
- *
- * :param array $response: 3-tuple containing status, headers, and body
- *
- * :return: PHP object decoded from JSON
- * :rtype: object
- * :throws: A :php:class:`Services_Twilio_RestException` if the Response is
- * in the 300-500 range of status codes.
- */
- private function _processResponse($response)
- {
- list($status, $headers, $body) = $response;
- if ($status === 204) {
- return true;
- }
- $decoded = json_decode($body);
- if ($decoded === null) {
- throw new Services_Twilio_RestException(
- $status,
- 'Could not decode response body as JSON. ' .
- 'This likely indicates a 500 server error'
- );
- }
- if (200 <= $status && $status < 300) {
- $this->last_response = $decoded;
- return $decoded;
- }
- throw new Services_Twilio_RestException(
- $status,
- isset($decoded->message) ? $decoded->message : '',
- isset($decoded->code) ? $decoded->code : null,
- isset($decoded->more_info) ? $decoded->more_info : null
- );
- }
-}
-
diff --git a/externals/twilio-php/Services/Twilio/AutoPagingIterator.php b/externals/twilio-php/Services/Twilio/AutoPagingIterator.php
deleted file mode 100644
index af09c791e..000000000
--- a/externals/twilio-php/Services/Twilio/AutoPagingIterator.php
+++ /dev/null
@@ -1,109 +0,0 @@
-<?php
-
-class Services_Twilio_AutoPagingIterator
- implements Iterator
-{
- protected $generator;
- protected $args;
- protected $items;
-
- private $_args;
-
- public function __construct($generator, $page, $size, $filters) {
- $this->generator = $generator;
- $this->page = $page;
- $this->size = $size;
- $this->filters = $filters;
- $this->items = array();
-
- // Save a backup for rewind()
- $this->_args = array(
- 'page' => $page,
- 'size' => $size,
- 'filters' => $filters,
- );
- }
-
- public function current()
- {
- return current($this->items);
- }
-
- public function key()
- {
- return key($this->items);
- }
-
- /*
- * Return the next item in the list, making another HTTP call to the next
- * page of resources if necessary.
- */
- public function next()
- {
- try {
- $this->loadIfNecessary();
- return next($this->items);
- }
- catch (Services_Twilio_RestException $e) {
- // 20006 is an out of range paging error, everything else is valid
- if ($e->getCode() != 20006) {
- throw $e;
- }
- }
- }
-
- /*
- * Restore everything to the way it was before we began paging. This gets
- * called at the beginning of any foreach() loop
- */
- public function rewind()
- {
- foreach ($this->_args as $arg => $val) {
- $this->$arg = $val;
- }
- $this->items = array();
- $this->next_page_uri = null;
- }
-
- public function count()
- {
- throw new BadMethodCallException('Not allowed');
- }
-
- public function valid()
- {
- try {
- $this->loadIfNecessary();
- return key($this->items) !== null;
- }
- catch (Services_Twilio_RestException $e) {
- // 20006 is an out of range paging error, everything else is valid
- if ($e->getCode() != 20006) {
- throw $e;
- }
- }
- return false;
- }
-
- /*
- * Fill $this->items with a new page from the API, if necessary.
- */
- protected function loadIfNecessary()
- {
- if (// Empty because it's the first time or last page was empty
- empty($this->items)
- // null key when the items list is iterated over completely
- || key($this->items) === null
- ) {
- $page = call_user_func_array($this->generator, array(
- $this->page,
- $this->size,
- $this->filters,
- $this->next_page_uri,
- ));
- $this->next_page_uri = $page->next_page_uri;
- $this->items = $page->getItems();
- $this->page = $this->page + 1;
- }
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/Capability.php b/externals/twilio-php/Services/Twilio/Capability.php
deleted file mode 100644
index 9f02e4522..000000000
--- a/externals/twilio-php/Services/Twilio/Capability.php
+++ /dev/null
@@ -1,346 +0,0 @@
-<?php
-
-/**
- * Twilio Capability Token generator
- *
- * @category Services
- * @package Services_Twilio
- * @author Jeff Lindsay <jeff.lindsay@twilio.com>
- * @license http://creativecommons.org/licenses/MIT/ MIT
- */
-class Services_Twilio_Capability
-{
- public $accountSid;
- public $authToken;
- public $scopes;
-
- /**
- * Create a new TwilioCapability with zero permissions. Next steps are to
- * grant access to resources by configuring this token through the
- * functions allowXXXX.
- *
- * @param $accountSid the account sid to which this token is granted access
- * @param $authToken the secret key used to sign the token. Note, this auth
- * token is not visible to the user of the token.
- */
- public function __construct($accountSid, $authToken)
- {
- $this->accountSid = $accountSid;
- $this->authToken = $authToken;
- $this->scopes = array();
- $this->clientName = false;
- }
-
- /**
- * If the user of this token should be allowed to accept incoming
- * connections then configure the TwilioCapability through this method and
- * specify the client name.
- *
- * @param $clientName
- */
- public function allowClientIncoming($clientName)
- {
-
- // clientName must be a non-zero length alphanumeric string
- if (preg_match('/\W/', $clientName)) {
- throw new InvalidArgumentException(
- 'Only alphanumeric characters allowed in client name.');
- }
-
- if (strlen($clientName) == 0) {
- throw new InvalidArgumentException(
- 'Client name must not be a zero length string.');
- }
-
- $this->clientName = $clientName;
- $this->allow('client', 'incoming',
- array('clientName' => $clientName));
- }
-
- /**
- * Allow the user of this token to make outgoing connections.
- *
- * @param $appSid the application to which this token grants access
- * @param $appParams signed parameters that the user of this token cannot
- * overwrite.
- */
- public function allowClientOutgoing($appSid, array $appParams=array())
- {
- $this->allow('client', 'outgoing', array(
- 'appSid' => $appSid,
- 'appParams' => http_build_query($appParams, '', '&')));
- }
-
- /**
- * Allow the user of this token to access their event stream.
- *
- * @param $filters key/value filters to apply to the event stream
- */
- public function allowEventStream(array $filters=array())
- {
- $this->allow('stream', 'subscribe', array(
- 'path' => '/2010-04-01/Events',
- 'params' => http_build_query($filters, '', '&'),
- ));
- }
-
- /**
- * Generates a new token based on the credentials and permissions that
- * previously has been granted to this token.
- *
- * @param $ttl the expiration time of the token (in seconds). Default
- * value is 3600 (1hr)
- * @return the newly generated token that is valid for $ttl seconds
- */
- public function generateToken($ttl = 3600)
- {
- $payload = array(
- 'scope' => array(),
- 'iss' => $this->accountSid,
- 'exp' => time() + $ttl,
- );
- $scopeStrings = array();
-
- foreach ($this->scopes as $scope) {
- if ($scope->privilege == "outgoing" && $this->clientName)
- $scope->params["clientName"] = $this->clientName;
- $scopeStrings[] = $scope->toString();
- }
-
- $payload['scope'] = implode(' ', $scopeStrings);
- return JWT::encode($payload, $this->authToken, 'HS256');
- }
-
- protected function allow($service, $privilege, $params) {
- $this->scopes[] = new ScopeURI($service, $privilege, $params);
- }
-}
-
-/**
- * Scope URI implementation
- *
- * Simple way to represent configurable privileges in an OAuth
- * friendly way. For our case, they look like this:
- *
- * scope:<service>:<privilege>?<params>
- *
- * For example:
- * scope:client:incoming?name=jonas
- *
- * @author Jeff Lindsay <jeff.lindsay@twilio.com>
- */
-class ScopeURI
-{
- public $service;
- public $privilege;
- public $params;
-
- public function __construct($service, $privilege, $params = array())
- {
- $this->service = $service;
- $this->privilege = $privilege;
- $this->params = $params;
- }
-
- public function toString()
- {
- $uri = "scope:{$this->service}:{$this->privilege}";
- if (count($this->params)) {
- $uri .= "?".http_build_query($this->params, '', '&');
- }
- return $uri;
- }
-
- /**
- * Parse a scope URI into a ScopeURI object
- *
- * @param string $uri The scope URI
- * @return ScopeURI The parsed scope uri
- */
- public static function parse($uri)
- {
- if (strpos($uri, 'scope:') !== 0) {
- throw new UnexpectedValueException(
- 'Not a scope URI according to scheme');
- }
-
- $parts = explode('?', $uri, 1);
- $params = null;
-
- if (count($parts) > 1) {
- parse_str($parts[1], $params);
- }
-
- $parts = explode(':', $parts[0], 2);
-
- if (count($parts) != 3) {
- throw new UnexpectedValueException(
- 'Not enough parts for scope URI');
- }
-
- list($scheme, $service, $privilege) = $parts;
- return new ScopeURI($service, $privilege, $params);
- }
-
-}
-
-/**
- * JSON Web Token implementation
- *
- * Minimum implementation used by Realtime auth, based on this spec:
- * http://self-issued.info/docs/draft-jones-json-web-token-01.html.
- *
- * @author Neuman Vong <neuman@twilio.com>
- */
-class JWT
-{
- /**
- * @param string $jwt The JWT
- * @param string|null $key The secret key
- * @param bool $verify Don't skip verification process
- *
- * @return object The JWT's payload as a PHP object
- */
- public static function decode($jwt, $key = null, $verify = true)
- {
- $tks = explode('.', $jwt);
- if (count($tks) != 3) {
- throw new UnexpectedValueException('Wrong number of segments');
- }
- list($headb64, $payloadb64, $cryptob64) = $tks;
- if (null === ($header = JWT::jsonDecode(JWT::urlsafeB64Decode($headb64)))
- ) {
- throw new UnexpectedValueException('Invalid segment encoding');
- }
- if (null === $payload = JWT::jsonDecode(JWT::urlsafeB64Decode($payloadb64))
- ) {
- throw new UnexpectedValueException('Invalid segment encoding');
- }
- $sig = JWT::urlsafeB64Decode($cryptob64);
- if ($verify) {
- if (empty($header->alg)) {
- throw new DomainException('Empty algorithm');
- }
- if ($sig != JWT::sign("$headb64.$payloadb64", $key, $header->alg)) {
- throw new UnexpectedValueException('Signature verification failed');
- }
- }
- return $payload;
- }
-
- /**
- * @param object|array $payload PHP object or array
- * @param string $key The secret key
- * @param string $algo The signing algorithm
- *
- * @return string A JWT
- */
- public static function encode($payload, $key, $algo = 'HS256')
- {
- $header = array('typ' => 'JWT', 'alg' => $algo);
-
- $segments = array();
- $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($header));
- $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($payload));
- $signing_input = implode('.', $segments);
-
- $signature = JWT::sign($signing_input, $key, $algo);
- $segments[] = JWT::urlsafeB64Encode($signature);
-
- return implode('.', $segments);
- }
-
- /**
- * @param string $msg The message to sign
- * @param string $key The secret key
- * @param string $method The signing algorithm
- *
- * @return string An encrypted message
- */
- public static function sign($msg, $key, $method = 'HS256')
- {
- $methods = array(
- 'HS256' => 'sha256',
- 'HS384' => 'sha384',
- 'HS512' => 'sha512',
- );
- if (empty($methods[$method])) {
- throw new DomainException('Algorithm not supported');
- }
- return hash_hmac($methods[$method], $msg, $key, true);
- }
-
- /**
- * @param string $input JSON string
- *
- * @return object Object representation of JSON string
- */
- public static function jsonDecode($input)
- {
- $obj = json_decode($input);
- if (function_exists('json_last_error') && $errno = json_last_error()) {
- JWT::handleJsonError($errno);
- }
- else if ($obj === null && $input !== 'null') {
- throw new DomainException('Null result with non-null input');
- }
- return $obj;
- }
-
- /**
- * @param object|array $input A PHP object or array
- *
- * @return string JSON representation of the PHP object or array
- */
- public static function jsonEncode($input)
- {
- $json = json_encode($input);
- if (function_exists('json_last_error') && $errno = json_last_error()) {
- JWT::handleJsonError($errno);
- }
- else if ($json === 'null' && $input !== null) {
- throw new DomainException('Null result with non-null input');
- }
- return $json;
- }
-
- /**
- * @param string $input A base64 encoded string
- *
- * @return string A decoded string
- */
- public static function urlsafeB64Decode($input)
- {
- $padlen = 4 - strlen($input) % 4;
- $input .= str_repeat('=', $padlen);
- return base64_decode(strtr($input, '-_', '+/'));
- }
-
- /**
- * @param string $input Anything really
- *
- * @return string The base64 encode of what you passed in
- */
- public static function urlsafeB64Encode($input)
- {
- return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
- }
-
- /**
- * @param int $errno An error number from json_last_error()
- *
- * @return void
- */
- private static function handleJsonError($errno)
- {
- $messages = array(
- JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
- JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
- JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON'
- );
- throw new DomainException(isset($messages[$errno])
- ? $messages[$errno]
- : 'Unknown JSON error: ' . $errno
- );
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/HttpException.php b/externals/twilio-php/Services/Twilio/HttpException.php
deleted file mode 100644
index b79a357d7..000000000
--- a/externals/twilio-php/Services/Twilio/HttpException.php
+++ /dev/null
@@ -1,3 +0,0 @@
-<?php
-
-class Services_Twilio_HttpException extends ErrorException {}
\ No newline at end of file
diff --git a/externals/twilio-php/Services/Twilio/HttpStream.php b/externals/twilio-php/Services/Twilio/HttpStream.php
deleted file mode 100644
index 73ae73a8e..000000000
--- a/externals/twilio-php/Services/Twilio/HttpStream.php
+++ /dev/null
@@ -1,94 +0,0 @@
-<?php
-/**
- * HTTP Stream version of the TinyHttp Client used to connect to Twilio
- * services.
- */
-
-class Services_Twilio_HttpStreamException extends ErrorException {}
-
-class Services_Twilio_HttpStream {
-
- private $auth_header = null;
- private $uri = null;
- private $debug = false;
- private static $default_options = array(
- "http" => array(
- "headers" => "",
- "timeout" => 60,
- "follow_location" => true,
- "ignore_errors" => true,
- ),
- "ssl" => array(),
- );
- private $options = array();
-
- public function __construct($uri = '', $kwargs = array()) {
- $this->uri = $uri;
- if (isset($kwargs['debug'])) {
- $this->debug = true;
- }
- if (isset($kwargs['http_options'])) {
- $this->options = $kwargs['http_options'] + self::$default_options;
- } else {
- $this->options = self::$default_options;
- }
- }
-
- public function __call($name, $args) {
- list($res, $req_headers, $req_body) = $args + array(0, array(), '');
-
- $request_options = $this->options;
- $url = $this->uri . $res;
-
- if (isset($req_body) && strlen($req_body) > 0) {
- $request_options['http']['content'] = $req_body;
- }
-
- foreach($req_headers as $key => $value) {
- $request_options['http']['header'] .= sprintf("%s: %s\r\n", $key, $value);
- }
-
- if (isset($this->auth_header)) {
- $request_options['http']['header'] .= $this->auth_header;
- }
-
- $request_options['http']['method'] = strtoupper($name);
- $request_options['http']['ignore_errors'] = true;
-
- if ($this->debug) {
- error_log(var_export($request_options, true));
- }
- $ctx = stream_context_create($request_options);
- $result = file_get_contents($url, false, $ctx);
-
- if (false === $result) {
- throw new Services_Twilio_HttpStreamException(
- "Unable to connect to service");
- }
-
- $status_header = array_shift($http_response_header);
- if (1 !== preg_match('#HTTP/\d+\.\d+ (\d+)#', $status_header, $matches)) {
- throw new Services_Twilio_HttpStreamException(
- "Unable to detect the status code in the HTTP result.");
- }
-
- $status_code = intval($matches[1]);
- $response_headers = array();
-
- foreach($http_response_header as $header) {
- list($key, $val) = explode(":", $header);
- $response_headers[trim($key)] = trim($val);
- }
-
- return array($status_code, $response_headers, $result);
- }
-
- public function authenticate($user, $pass) {
- if (isset($user) && isset($pass)) {
- $this->auth_header = sprintf("Authorization: Basic %s",
- base64_encode(sprintf("%s:%s", $user, $pass)));
- } else {
- $this->auth_header = null;
- }
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/InstanceResource.php b/externals/twilio-php/Services/Twilio/InstanceResource.php
deleted file mode 100644
index 011d8f998..000000000
--- a/externals/twilio-php/Services/Twilio/InstanceResource.php
+++ /dev/null
@@ -1,84 +0,0 @@
-<?php
-
-/**
- * @category Services
- * @package Services_Twilio
- * @author Neuman Vong <neuman@twilio.com>
- * @license http://creativecommons.org/licenses/MIT/ MIT
- * @link http://pear.php.net/package/Services_Twilio
- */
-
-/**
- * Abstraction of an instance resource from the Twilio API.
- */
-abstract class Services_Twilio_InstanceResource extends Services_Twilio_Resource {
-
- /**
- * Make a request to the API to update an instance resource
- *
- * :param mixed $params: An array of updates, or a property name
- * :param mixed $value: A value with which to update the resource
- *
- * :rtype: null
- * :throws: a :php:class:`RestException <Services_Twilio_RestException>` if
- * the update fails.
- */
- public function update($params, $value = null)
- {
- if (!is_array($params)) {
- $params = array($params => $value);
- }
- $decamelizedParams = $this->client->createData($this->uri, $params);
- $this->updateAttributes($decamelizedParams);
- }
-
- /*
- * Add all properties from an associative array (the JSON response body) as
- * properties on this instance resource, except the URI
- *
- * :param stdClass $params: An object containing all of the parameters of
- * this instance
- * :return: Nothing, this is purely side effecting
- * :rtype: null
- */
- public function updateAttributes($params) {
- unset($params->uri);
- foreach ($params as $name => $value) {
- $this->$name = $value;
- }
- }
-
- /**
- * Get the value of a property on this resource.
- *
- * Instead of defining all of the properties of an object directly, we rely
- * on the API to tell us which properties an object has. This method will
- * query the API to retrieve a property for an object, if it is not already
- * set on the object.
- *
- * If the call is to a subresource, eg ``$client->account->messages``, no
- * request is made.
- *
- * To help with lazy HTTP requests, we don't actually retrieve an object
- * from the API unless you really need it. Hence, this function may make API
- * requests even if the property you're requesting isn't available on the
- * resource.
- *
- * :param string $key: The property name
- *
- * :return mixed: Could be anything.
- * :throws: a :php:class:`RestException <Services_Twilio_RestException>` if
- * the update fails.
- */
- public function __get($key)
- {
- if ($subresource = $this->getSubresources($key)) {
- return $subresource;
- }
- if (!isset($this->$key)) {
- $params = $this->client->retrieveData($this->uri);
- $this->updateAttributes($params);
- }
- return $this->$key;
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/ListResource.php b/externals/twilio-php/Services/Twilio/ListResource.php
deleted file mode 100644
index 4d7bc3c3e..000000000
--- a/externals/twilio-php/Services/Twilio/ListResource.php
+++ /dev/null
@@ -1,203 +0,0 @@
-<?php
-
-/**
- * @author Neuman Vong neuman@twilio.com
- * @license http://creativecommons.org/licenses/MIT/ MIT
- * @link http://pear.php.net/package/Services_Twilio
- */
-
-/**
- * Abstraction of a list resource from the Twilio API.
- *
- * The list resource implements the `IteratorAggregate
- * <http://php.net/manual/en/class.iteratoraggregate.php>`_ and the `Countable
- * <http://php.net/manual/en/class.countable.php>`_ interfaces.
- *
- */
-abstract class Services_Twilio_ListResource extends Services_Twilio_Resource
- implements IteratorAggregate, Countable
-{
-
- public function __construct($client, $uri) {
- $name = $this->getResourceName(true);
- /*
- * By default trim the 's' from the end of the list name to get the
- * instance name (ex Accounts -> Account). This behavior can be
- * overridden by child classes if the rule doesn't work.
- */
- if (!isset($this->instance_name)) {
- $this->instance_name = "Services_Twilio_Rest_" . rtrim($name, 's');
- }
-
- parent::__construct($client, $uri);
- }
-
- /**
- * Gets a resource from this list.
- *
- * :param string $sid: The resource SID
- * :return: The resource
- * :rtype: :php:class:`InstanceResource <Services_Twilio_InstanceResource>`
- */
- public function get($sid) {
- $instance = new $this->instance_name(
- $this->client, $this->uri . "/$sid"
- );
- // XXX check if this is actually a sid in all cases.
- $instance->sid = $sid;
- return $instance;
- }
-
- /**
- * Construct an :php:class:`InstanceResource
- * <Services_Twilio_InstanceResource>` with the specified params.
- *
- * :param array $params: usually a JSON HTTP response from the API
- * :return: An instance with properties
- * initialized to the values in the params array.
- * :rtype: :php:class:`InstanceResource <Services_Twilio_InstanceResource>`
- */
- public function getObjectFromJson($params, $idParam = "sid")
- {
- if (isset($params->{$idParam})) {
- $uri = $this->uri . "/" . $params->{$idParam};
- } else {
- $uri = $this->uri;
- }
- return new $this->instance_name($this->client, $uri, $params);
- }
-
- /**
- * Deletes a resource from this list.
- *
- * :param string $sid: The resource SID
- * :rtype: null
- */
- public function delete($sid, $params = array())
- {
- $this->client->deleteData($this->uri . '/' . $sid, $params);
- }
-
- /**
- * Create a resource on the list and then return its representation as an
- * InstanceResource.
- *
- * :param array $params: The parameters with which to create the resource
- *
- * :return: The created resource
- * :rtype: :php:class:`InstanceResource <Services_Twilio_InstanceResource>`
- */
- protected function _create($params)
- {
- $params = $this->client->createData($this->uri, $params);
- /* Some methods like verified caller ID don't return sids. */
- if (isset($params->sid)) {
- $resource_uri = $this->uri . '/' . $params->sid;
- } else {
- $resource_uri = $this->uri;
- }
- return new $this->instance_name($this->client, $resource_uri, $params);
- }
-
- /**
- * Returns a page of :php:class:`InstanceResources
- * <Services_Twilio_InstanceResource>` from this list.
- *
- * :param int $page: The start page
- * :param int $size: Number of items per page
- * :param array $filters: Optional filters
- * :param string $deep_paging_uri: if provided, the $page and $size
- * parameters will be ignored and this URI will be requested directly.
- *
- * :return: A page of resources
- * :rtype: :php:class:`Services_Twilio_Page`
- */
- public function getPage(
- $page = 0, $size = 50, $filters = array(), $deep_paging_uri = null
- ) {
- $list_name = $this->getResourceName();
- if ($deep_paging_uri !== null) {
- $page = $this->client->retrieveData($deep_paging_uri, array(), true);
- } else {
- $page = $this->client->retrieveData($this->uri, array(
- 'Page' => $page,
- 'PageSize' => $size,
- ) + $filters);
- }
-
- /* create a new PHP object for each json obj in the api response. */
- $page->$list_name = array_map(
- array($this, 'getObjectFromJson'),
- $page->$list_name
- );
- if (isset($page->next_page_uri)) {
- $next_page_uri = $page->next_page_uri;
- } else {
- $next_page_uri = null;
- }
- return new Services_Twilio_Page($page, $list_name, $next_page_uri);
- }
-
- /**
- * Get the total number of instances for this list.
- *
- * This will make one HTTP request to retrieve the total, every time this
- * method is called.
- *
- * If the total is not set, or an Exception was thrown, returns 0
- *
- * :return: The total number of instance members
- * :rtype: integer
- */
- public function count() {
- try {
- $page = $this->getPage(0, 1);
- return $page ? (int)$page->total : 0;
- } catch (Exception $e) {
- return 0;
- }
- }
-
-
- /**
- * Returns an iterable list of
- * :php:class:`instance resources <Services_Twilio_InstanceResource>`.
- *
- * :param int $page: The start page
- * :param int $size: Number of items per page
- * :param array $filters: Optional filters.
- * The filter array can accept full datetimes when StartTime or DateCreated
- * are used. Inequalities should be within the key portion of the array and
- * multiple filter parameters can be combined for more specific searches.
- *
- * .. code-block:: php
- *
- * array('DateCreated>' => '2011-07-05 08:00:00', 'DateCreated<' => '2011-08-01')
- *
- * .. code-block:: php
- *
- * array('StartTime<' => '2011-07-05 08:00:00')
- *
- * :return: An iterator
- * :rtype: :php:class:`Services_Twilio_AutoPagingIterator`
- */
- public function getIterator(
- $page = 0, $size = 50, $filters = array()
- ) {
- return new Services_Twilio_AutoPagingIterator(
- array($this, 'getPageGenerator'), $page, $size, $filters
- );
- }
-
- /**
- * Retrieve a new page of API results, and update iterator parameters. This
- * function is called by the paging iterator to retrieve a new page and
- * shouldn't be called directly.
- */
- public function getPageGenerator(
- $page, $size, $filters = array(), $deep_paging_uri = null
- ) {
- return $this->getPage($page, $size, $filters, $deep_paging_uri);
- }
-}
-
diff --git a/externals/twilio-php/Services/Twilio/NumberType.php b/externals/twilio-php/Services/Twilio/NumberType.php
deleted file mode 100644
index 0683d9961..000000000
--- a/externals/twilio-php/Services/Twilio/NumberType.php
+++ /dev/null
@@ -1,35 +0,0 @@
-<?php
-
-class Services_Twilio_NumberType extends Services_Twilio_ListResource
-{
- public function getResourceName($camelized = false) {
- $this->instance_name = 'Services_Twilio_Rest_IncomingPhoneNumber';
- return $camelized ? 'IncomingPhoneNumbers' : 'incoming_phone_numbers';
- }
-
- /**
- * Purchase a new phone number.
- *
- * Example usage:
- *
- * .. code-block:: php
- *
- * $marlosBurner = '+14105551234';
- * $client->account->incoming_phone_numbers->local->purchase($marlosBurner);
- *
- * :param string $phone_number: The phone number to purchase
- * :param array $params: An optional array of parameters to pass along with
- * the request (to configure the phone number)
- */
- public function purchase($phone_number, array $params = array()) {
- $postParams = array(
- 'PhoneNumber' => $phone_number
- );
- return $this->create($postParams + $params);
- }
-
- public function create(array $params = array()) {
- return parent::_create($params);
- }
-
-}
diff --git a/externals/twilio-php/Services/Twilio/Page.php b/externals/twilio-php/Services/Twilio/Page.php
deleted file mode 100644
index 61ddb073e..000000000
--- a/externals/twilio-php/Services/Twilio/Page.php
+++ /dev/null
@@ -1,68 +0,0 @@
-<?php
-
-/**
- * A representation of a page of resources.
- *
- * @category Services
- * @package Services_Twilio
- * @author Neuman Vong <neuman@twilio.com>
- * @license http://creativecommons.org/licenses/MIT/ MIT
- * @link http://pear.php.net/package/Services_Twilio
- */
-class Services_Twilio_Page
- implements IteratorAggregate
-{
-
- /**
- * The item list.
- *
- * @var array $items
- */
- protected $items;
-
- /**
- * Constructs a page.
- *
- * @param object $page The page object
- * @param string $name The key of the item list
- */
- public function __construct($page, $name, $next_page_uri = null)
- {
- $this->page = $page;
- $this->items = $page->{$name};
- $this->next_page_uri = $next_page_uri;
- }
-
- /**
- * The item list of the page.
- *
- * @return array A list of instance resources
- */
- public function getItems()
- {
- return $this->items;
- }
-
- /**
- * Magic method to allow retrieving the properties of the wrapped page.
- *
- * @param string $prop The property name
- *
- * @return mixed Could be anything
- */
- public function __get($prop)
- {
- return $this->page->$prop;
- }
-
- /**
- * Implementation of IteratorAggregate::getIterator().
- *
- * @return Traversable
- */
- public function getIterator()
- {
- return $this->getItems();
- }
-}
-
diff --git a/externals/twilio-php/Services/Twilio/PartialApplicationHelper.php b/externals/twilio-php/Services/Twilio/PartialApplicationHelper.php
deleted file mode 100644
index 639ca5101..000000000
--- a/externals/twilio-php/Services/Twilio/PartialApplicationHelper.php
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php
-
-/**
- * Helper class to wrap an object with a modified interface created by
- * a partial application of its existing methods.
- *
- * @category Services
- * @package Services_Twilio
- * @author Neuman Vong <neuman@twilio.com>
- * @license http://creativecommons.org/licenses/MIT/ MIT
- * @link http://pear.php.net/package/Services_Twilio
- */
-class Services_Twilio_PartialApplicationHelper
-{
- private $callbacks;
-
- public function __construct()
- {
- $this->callbacks = array();
- }
-
- public function set($method, $callback, array $args)
- {
- if (!is_callable($callback)) {
- return FALSE;
- }
- $this->callbacks[$method] = array($callback, $args);
- }
-
- public function __call($method, $args)
- {
- if (!isset($this->callbacks[$method])) {
- throw new Exception("Method not found: $method");
- }
- list($callback, $cb_args) = $this->callbacks[$method];
- return call_user_func_array(
- $callback,
- array_merge($cb_args, $args)
- );
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/RequestValidator.php b/externals/twilio-php/Services/Twilio/RequestValidator.php
deleted file mode 100644
index 52a40d91b..000000000
--- a/externals/twilio-php/Services/Twilio/RequestValidator.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-
-class Services_Twilio_RequestValidator
-{
-
- protected $AuthToken;
-
- function __construct($token)
- {
- $this->AuthToken = $token;
- }
-
- public function computeSignature($url, $data = array())
- {
- // sort the array by keys
- ksort($data);
-
- // append them to the data string in order
- // with no delimiters
- foreach($data as $key => $value)
- $url .= "$key$value";
-
- // This function calculates the HMAC hash of the data with the key
- // passed in
- // Note: hash_hmac requires PHP 5 >= 5.1.2 or PECL hash:1.1-1.5
- // Or http://pear.php.net/package/Crypt_HMAC/
- return base64_encode(hash_hmac("sha1", $url, $this->AuthToken, true));
- }
-
- public function validate($expectedSignature, $url, $data = array())
- {
- return $this->computeSignature($url, $data)
- == $expectedSignature;
- }
-
-}
diff --git a/externals/twilio-php/Services/Twilio/Resource.php b/externals/twilio-php/Services/Twilio/Resource.php
deleted file mode 100644
index e6f1feb54..000000000
--- a/externals/twilio-php/Services/Twilio/Resource.php
+++ /dev/null
@@ -1,134 +0,0 @@
-<?php
-
-/**
- * Abstraction of a Twilio resource.
- *
- * @category Services
- * @package Services_Twilio
- * @author Neuman Vong <neuman@twilio.com>
- * @license http://creativecommons.org/licenses/MIT/ MIT
- * @link http://pear.php.net/package/Services_Twilio
- */
-abstract class Services_Twilio_Resource {
- protected $subresources;
-
- public function __construct($client, $uri, $params = array())
- {
- $this->subresources = array();
- $this->client = $client;
-
- foreach ($params as $name => $param) {
- $this->$name = $param;
- }
-
- $this->uri = $uri;
- $this->init($client, $uri);
- }
-
- protected function init($client, $uri)
- {
- // Left empty for derived classes to implement
- }
-
- public function getSubresources($name = null) {
- if (isset($name)) {
- return isset($this->subresources[$name])
- ? $this->subresources[$name]
- : null;
- }
- return $this->subresources;
- }
-
- protected function setupSubresources()
- {
- foreach (func_get_args() as $name) {
- $constantized = ucfirst(self::camelize($name));
- $type = "Services_Twilio_Rest_" . $constantized;
- $this->subresources[$name] = new $type(
- $this->client, $this->uri . "/$constantized"
- );
- }
- }
-
- /*
- * Get the resource name from the classname
- *
- * Ex: Services_Twilio_Rest_Accounts -> Accounts
- *
- * @param boolean $camelized Whether to return camel case or not
- */
- public function getResourceName($camelized = false)
- {
- $name = get_class($this);
- $parts = explode('_', $name);
- $basename = end($parts);
- if ($camelized) {
- return $basename;
- } else {
- return self::decamelize($basename);
- }
- }
-
- public static function decamelize($word)
- {
- $callback = create_function('$matches',
- 'return strtolower(strlen("$matches[1]") ? "$matches[1]_$matches[2]" : "$matches[2]");');
-
- return preg_replace_callback(
- '/(^|[a-z])([A-Z])/',
- $callback,
- $word
- );
- }
-
- /**
- * Return camelized version of a word
- * Examples: sms_messages => SMSMessages, calls => Calls,
- * incoming_phone_numbers => IncomingPhoneNumbers
- *
- * @param string $word The word to camelize
- * @return string
- */
- public static function camelize($word) {
- $callback = create_function('$matches', 'return strtoupper("$matches[2]");');
-
- return preg_replace_callback('/(^|_)([a-z])/',
- $callback,
- $word);
- }
-
- /**
- * Get the value of a property on this resource.
- *
- * @param string $key The property name
- * @return mixed Could be anything.
- */
- public function __get($key) {
- if ($subresource = $this->getSubresources($key)) {
- return $subresource;
- }
- return $this->$key;
- }
-
- /**
- * Print a JSON representation of this object. Strips the HTTP client
- * before returning.
- *
- * Note, this should mainly be used for debugging, and is not guaranteed
- * to correspond 1:1 with the JSON API output.
- *
- * Note that echoing an object before an HTTP request has been made to
- * "fill in" its properties may return an empty object
- */
- public function __toString() {
- $out = array();
- foreach ($this as $key => $value) {
- if ($key !== 'client' && $key !== 'subresources') {
- $out[$key] = $value;
- }
- }
- return json_encode($out, true);
- }
-
-}
-
diff --git a/externals/twilio-php/Services/Twilio/Rest/Account.php b/externals/twilio-php/Services/Twilio/Rest/Account.php
deleted file mode 100644
index e1181b7d9..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/Account.php
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_Account extends Services_Twilio_InstanceResource {
-
- protected function init($client, $uri) {
- $this->setupSubresources(
- 'applications',
- 'available_phone_numbers',
- 'outgoing_caller_ids',
- 'calls',
- 'conferences',
- 'incoming_phone_numbers',
- 'media',
- 'messages',
- 'notifications',
- 'outgoing_callerids',
- 'recordings',
- 'sms_messages',
- 'short_codes',
- 'transcriptions',
- 'connect_apps',
- 'authorized_connect_apps',
- 'usage_records',
- 'usage_triggers',
- 'queues',
- 'sip'
- );
-
- $this->sandbox = new Services_Twilio_Rest_Sandbox(
- $client, $uri . '/Sandbox'
- );
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/Accounts.php b/externals/twilio-php/Services/Twilio/Rest/Accounts.php
deleted file mode 100644
index 0e7ea0a58..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/Accounts.php
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-
-/**
- * For more information, see the `Account List Resource
- * <http://www.twilio.com/docs/api/rest/account#list>`_ documentation.
- */
-class Services_Twilio_Rest_Accounts extends Services_Twilio_ListResource {
-
- /**
- * Create a new subaccount.
- *
- * :param array $params: An array of parameters describing the new
- * subaccount. The ``$params`` array can contain the following keys:
- *
- * *FriendlyName*
- * A description of this account, up to 64 characters long
- *
- * :returns: The new subaccount
- * :rtype: :php:class:`Services_Twilio_Rest_Account`
- *
- */
- public function create($params = array()) {
- return parent::_create($params);
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/Application.php b/externals/twilio-php/Services/Twilio/Rest/Application.php
deleted file mode 100644
index 0db8a052d..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/Application.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_Application
- extends Services_Twilio_InstanceResource
-{
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/Applications.php b/externals/twilio-php/Services/Twilio/Rest/Applications.php
deleted file mode 100644
index a1c3bd85c..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/Applications.php
+++ /dev/null
@@ -1,12 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_Applications
- extends Services_Twilio_ListResource
-{
- public function create($name, array $params = array())
- {
- return parent::_create(array(
- 'FriendlyName' => $name
- ) + $params);
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/AuthorizedConnectApp.php b/externals/twilio-php/Services/Twilio/Rest/AuthorizedConnectApp.php
deleted file mode 100644
index 037262939..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/AuthorizedConnectApp.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_AuthorizedConnectApp
- extends Services_Twilio_InstanceResource
-{
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/AuthorizedConnectApps.php b/externals/twilio-php/Services/Twilio/Rest/AuthorizedConnectApps.php
deleted file mode 100644
index c19f3ae38..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/AuthorizedConnectApps.php
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_AuthorizedConnectApps
- extends Services_Twilio_ListResource
-{
- public function create($name, array $params = array())
- {
- throw new BadMethodCallException('Not allowed');
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/AvailablePhoneNumber.php b/externals/twilio-php/Services/Twilio/Rest/AvailablePhoneNumber.php
deleted file mode 100644
index d018b362b..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/AvailablePhoneNumber.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_AvailablePhoneNumber
- extends Services_Twilio_InstanceResource
-{
-}
-
diff --git a/externals/twilio-php/Services/Twilio/Rest/AvailablePhoneNumbers.php b/externals/twilio-php/Services/Twilio/Rest/AvailablePhoneNumbers.php
deleted file mode 100644
index 9d371411f..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/AvailablePhoneNumbers.php
+++ /dev/null
@@ -1,54 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_AvailablePhoneNumbers
- extends Services_Twilio_ListResource
-{
- public function getLocal($country) {
- $curried = new Services_Twilio_PartialApplicationHelper();
- $curried->set(
- 'getList',
- array($this, 'getList'),
- array($country, 'Local')
- );
- return $curried;
- }
- public function getTollFree($country) {
- $curried = new Services_Twilio_PartialApplicationHelper();
- $curried->set(
- 'getList',
- array($this, 'getList'),
- array($country, 'TollFree')
- );
- return $curried;
- }
-
- public function getMobile($country)
- {
- $curried = new Services_Twilio_PartialApplicationHelper();
- $curried->set(
- 'getList',
- array($this, 'getList'),
- array($country, 'Mobile')
- );
- return $curried;
- }
-
- /**
- * Get a list of available phone numbers.
- *
- * @param string $country The 2-digit country code you'd like to search for
- * numbers e.g. ('US', 'CA', 'GB')
- * @param string $type The type of number ('Local', 'TollFree', or 'Mobile')
- * @return object The object representation of the resource
- */
- public function getList($country, $type, array $params = array())
- {
- return $this->client->retrieveData($this->uri . "/$country/$type", $params);
- }
-
- public function getResourceName($camelized = false) {
- // You can't page through the list of available phone numbers.
- $this->instance_name = 'Services_Twilio_Rest_AvailablePhoneNumber';
- return $camelized ? 'Countries' : 'countries';
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/Call.php b/externals/twilio-php/Services/Twilio/Rest/Call.php
deleted file mode 100644
index 755656847..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/Call.php
+++ /dev/null
@@ -1,105 +0,0 @@
-<?php
-
-/**
- * For more information, see the `Call Instance Resource <http://www.twilio.com/docs/api/rest/call#instance>`_ documentation.
- *
- * .. php:attr:: sid
- *
- * A 34 character string that uniquely identifies this resource.
- *
- * .. php:attr:: parent_call_sid
- *
- * A 34 character string that uniquely identifies the call that created this leg.
- *
- * .. php:attr:: date_created
- *
- * The date that this resource was created, given as GMT in RFC 2822 format.
- *
- * .. php:attr:: date_updated
- *
- * The date that this resource was last updated, given as GMT in RFC 2822 format.
- *
- * .. php:attr:: account_sid
- *
- * The unique id of the Account responsible for creating this call.
- *
- * .. php:attr:: to
- *
- * The phone number that received this call. e.g., +16175551212 (E.164 format)
- *
- * .. php:attr:: from
- *
- * The phone number that made this call. e.g., +16175551212 (E.164 format)
- *
- * .. php:attr:: phone_number_sid
- *
- * If the call was inbound, this is the Sid of the IncomingPhoneNumber that
- * received the call. If the call was outbound, it is the Sid of the
- * OutgoingCallerId from which the call was placed.
- *
- * .. php:attr:: status
- *
- * A string representing the status of the call. May be `QUEUED`, `RINGING`,
- * `IN-PROGRESS`, `COMPLETED`, `FAILED`, `BUSY` or `NO_ANSWER`.
- *
- * .. php:attr:: stat_time
- *
- * The start time of the call, given as GMT in RFC 2822 format. Empty if the call has not yet been dialed.
- *
- * .. php:attr:: end_time
- *
- * The end time of the call, given as GMT in RFC 2822 format. Empty if the call did not complete successfully.
- *
- * .. php:attr:: duration
- *
- * The length of the call in seconds. This value is empty for busy, failed, unanswered or ongoing calls.
- *
- * .. php:attr:: price
- *
- * The charge for this call in USD. Populated after the call is completed. May not be immediately available.
- *
- * .. php:attr:: direction
- *
- * A string describing the direction of the call. inbound for inbound
- * calls, outbound-api for calls initiated via the REST API or
- * outbound-dial for calls initiated by a <Dial> verb.
- *
- * .. php:attr:: answered_by
- *
- * If this call was initiated with answering machine detection, either human or machine. Empty otherwise.
- *
- * .. php:attr:: forwarded_from
- *
- * If this call was an incoming call forwarded from another number, the
- * forwarding phone number (depends on carrier supporting forwarding).
- * Empty otherwise.
- *
- * .. php:attr:: caller_name
- *
- * If this call was an incoming call from a phone number with Caller ID Lookup enabled, the caller's name. Empty otherwise.
- */
-class Services_Twilio_Rest_Call extends Services_Twilio_InstanceResource {
-
- /**
- * Hang up the call
- */
- public function hangup() {
- $this->update('Status', 'completed');
- }
-
- /**
- * Redirect the call to a new URL
- *
- * :param string $url: the new URL to retrieve call flow from.
- */
- public function route($url) {
- $this->update('Url', $url);
- }
-
- protected function init($client, $uri) {
- $this->setupSubresources(
- 'notifications',
- 'recordings'
- );
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/Calls.php b/externals/twilio-php/Services/Twilio/Rest/Calls.php
deleted file mode 100644
index 429ae9764..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/Calls.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_Calls
- extends Services_Twilio_ListResource
-{
-
- public static function isApplicationSid($value)
- {
- return strlen($value) == 34
- && !(strpos($value, "AP") === false);
- }
-
- public function create($from, $to, $url, array $params = array())
- {
-
- $params["To"] = $to;
- $params["From"] = $from;
-
- if (self::isApplicationSid($url)) {
- $params["ApplicationSid"] = $url;
- } else {
- $params["Url"] = $url;
- }
-
- return parent::_create($params);
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/Conference.php b/externals/twilio-php/Services/Twilio/Rest/Conference.php
deleted file mode 100644
index 9a36916e5..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/Conference.php
+++ /dev/null
@@ -1,12 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_Conference
- extends Services_Twilio_InstanceResource
-{
- protected function init($client, $uri)
- {
- $this->setupSubresources(
- 'participants'
- );
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/Conferences.php b/externals/twilio-php/Services/Twilio/Rest/Conferences.php
deleted file mode 100644
index 5e92e3710..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/Conferences.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_Conferences
- extends Services_Twilio_ListResource
-{
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/ConnectApp.php b/externals/twilio-php/Services/Twilio/Rest/ConnectApp.php
deleted file mode 100644
index dac005f1c..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/ConnectApp.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_ConnectApp
- extends Services_Twilio_InstanceResource
-{
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/ConnectApps.php b/externals/twilio-php/Services/Twilio/Rest/ConnectApps.php
deleted file mode 100644
index 33c97cdba..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/ConnectApps.php
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_ConnectApps
- extends Services_Twilio_ListResource
-{
- public function create($name, array $params = array())
- {
- throw new BadMethodCallException('Not allowed');
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/Credential.php b/externals/twilio-php/Services/Twilio/Rest/Credential.php
deleted file mode 100644
index 03d82652b..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/Credential.php
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-
-/**
- * A single Credential
- *
- * .. php:attr:: date_created
- *
- * The date the Credential was created
- *
- * .. php:attr:: date_updated
- *
- * The date the Credential was updated
- *
- * .. php:attr:: sid
- *
- * A 34 character string that identifies this object
- *
- * .. php:attr:: account_sid
- *
- * The account that created this credential
- *
- * .. php:attr:: username
- *
- * The username of this Credential object
- *
- * .. php:attr:: uri
- *
- * The uri of this Credential object
- */
-class Services_Twilio_Rest_Credential extends Services_Twilio_InstanceResource { }
diff --git a/externals/twilio-php/Services/Twilio/Rest/CredentialList.php b/externals/twilio-php/Services/Twilio/Rest/CredentialList.php
deleted file mode 100644
index 4f4da06f4..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/CredentialList.php
+++ /dev/null
@@ -1,42 +0,0 @@
-<?php
-
-/**
- * A single CredentialList
- *
- * .. php:attr:: date_created
- *
- * The date the credential list was created
- *
- * .. php:attr:: date_updated
- *
- * The date the credential list was updated
- *
- * .. php:attr:: sid
- *
- * A 34 character string that identifies this object
- *
- * .. php:attr:: account_sid
- *
- * The account that created the credential list
- *
- * .. php:attr:: friendly_name
- *
- * The friendly name of the credential list
- *
- * .. php:attr:: uri
- *
- * The uri of the credential list
- *
- * .. php:attr:: subresource_uris
- *
- * The subresources associated with this credential list (Credentials)
- */
-
-class Services_Twilio_Rest_CredentialList extends Services_Twilio_InstanceResource {
- protected function init($client, $uri) {
- $this->setupSubresources(
- 'credentials'
- );
- }
-}
-
diff --git a/externals/twilio-php/Services/Twilio/Rest/CredentialListMapping.php b/externals/twilio-php/Services/Twilio/Rest/CredentialListMapping.php
deleted file mode 100644
index 3f9c3054e..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/CredentialListMapping.php
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-
-/**
- * A single CredentialListMapping
- *
- * .. php:attr:: date_created
- *
- * The date this mapping was created
- *
- * .. php:attr:: date_updated
- *
- * The date this mapping was updated
- *
- * .. php:attr:: sid
- *
- * The sid of this mapping
- *
- * .. php:attr:: friendly_name
- *
- * The friendly name of this mapping
- *
- * .. php:attr:: uri
- *
- * The uri of this mapping
- *
- * .. php:attr:: subresource_uris
- *
- * The subresources associated with this mapping (Credentials)
- */
-
-class Services_Twilio_Rest_CredentialListMapping extends Services_Twilio_InstanceResource {
- protected function init($client, $uri) {
- $this->setupSubresources(
- 'credentials'
- );
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/CredentialListMappings.php b/externals/twilio-php/Services/Twilio/Rest/CredentialListMappings.php
deleted file mode 100644
index ab34f60ca..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/CredentialListMappings.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_CredentialListMappings extends Services_Twilio_SIPListResource {
-
- /**
- * Creates a new CredentialListMapping instance
- *
- * Example usage:
- *
- * .. code-block:: php
- *
- * $client->account->sip->domains->get('SDXXX')->credential_list_mappings->create("CLXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
- *
- * :param string $credential_list_sid: the sid of the CredentialList you're adding to this domain.
- * :param array $params: a single array of parameters which is serialized and
- * sent directly to the Twilio API.
- */
- public function create($credential_list_sid, $params = array()) {
- return parent::_create(array(
- 'CredentialListSid' => $credential_list_sid,
- ) + $params);
- }
-}
-
diff --git a/externals/twilio-php/Services/Twilio/Rest/CredentialLists.php b/externals/twilio-php/Services/Twilio/Rest/CredentialLists.php
deleted file mode 100644
index e8eb1a699..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/CredentialLists.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_CredentialLists extends Services_Twilio_SIPListResource {
-
- /**
- * Creates a new CredentialList instance
- *
- * Example usage:
- *
- * .. code-block:: php
- *
- * $client->account->sip->credential_lists->create("MyFriendlyName");
- *
- * :param string $friendly_name: the friendly name of this credential list
- * :param array $params: a single array of parameters which is serialized and
- * sent directly to the Twilio API.
- */
- public function create($friendly_name, $params = array()) {
- return parent::_create(array(
- 'FriendlyName' => $friendly_name,
- ) + $params);
- }
-
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/Credentials.php b/externals/twilio-php/Services/Twilio/Rest/Credentials.php
deleted file mode 100644
index 129a44dbd..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/Credentials.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_Credentials extends Services_Twilio_SIPListResource {
-
- /**
- * Creates a new Credential instance
- *
- * Example usage:
- *
- * .. code-block:: php
- *
- * $client->account->sip->credential_lists->get('CLXXX')->credentials->create(
- * "AwesomeUsername", "SuperSecretPassword",
- * );
- *
- * :param string $username: the username for the new Credential object
- * :param string $password: the password for the new Credential object
- * :param array $params: a single array of parameters which is serialized and
- * sent directly to the Twilio API.
- */
- public function create($username, $password, $params = array()) {
- return parent::_create(array(
- 'Username' => $username,
- 'Password' => $password,
- ) + $params);
- }
-
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/Domain.php b/externals/twilio-php/Services/Twilio/Rest/Domain.php
deleted file mode 100644
index 7d5a5c263..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/Domain.php
+++ /dev/null
@@ -1,70 +0,0 @@
-<?php
-
-/**
- * A single Domain
- *
- * .. php:attr:: date_created
- *
- * The date the domain was created
- *
- * .. php:attr:: date_updated
- *
- * The date the domain was updated
- *
- * .. php:attr:: sid
- *
- * A 34 character string that identifies this object
- *
- * .. php:attr:: account_sid
- *
- * The account that created the domain
- *
- * .. php:attr:: friendly_name
- *
- * The friendly name of the domain
- *
- * .. php:attr:: domain_name
- *
- * The *.sip.twilio domain for the domain
- *
- * .. php:attr:: auth_type
- *
- * The auth type used for the domain
- *
- * .. php:attr:: voice_url
- *
- * The voice url for the domain
- *
- * .. php:attr:: voice_fallback_url
- *
- * The voice fallback url for the domain
- *
- * .. php:attr:: voice_fallback_method
- *
- * The voice fallback method for the domain
- *
- * .. php:attr:: voice_status_callback_url
- *
- * The voice status callback url for the domain
- *
- * .. php:attr:: voice_status_callback_method
- *
- * The voice status_callback_method for the domain
- *
- * .. php:attr:: uri
- *
- * The uri of the domain
- *
- * .. php:attr:: subresource_uris
- *
- * The subresources associated with this domain (IpAccessControlListMappings, CredentialListMappings)
- *
- */
-class Services_Twilio_Rest_Domain extends Services_Twilio_InstanceResource {
- protected function init($client, $uri) {
- $this->setupSubresources(
- 'ip_access_control_list_mappings',
- 'credential_list_mappings'
- );
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/Domains.php b/externals/twilio-php/Services/Twilio/Rest/Domains.php
deleted file mode 100644
index 041c080c9..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/Domains.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_Domains extends Services_Twilio_SIPListResource {
-
- /**
- * Creates a new Domain instance
- *
- * Example usage:
- *
- * .. code-block:: php
- *
- * $client->account->sip->domains->create(
- * "MyFriendlyName", "MyDomainName"
- * );
- *
- * :param string $friendly_name: the friendly name of this domain
- * :param string $domain_name: the domain name for this domain
- * :param array $params: a single array of parameters which is serialized and
- * sent directly to the Twilio API.
- */
- public function create($friendly_name, $domain_name, $params = array()) {
- return parent::_create(array(
- 'FriendlyName' => $friendly_name,
- 'DomainName' => $domain_name,
- ) + $params);
- }
-}
-
diff --git a/externals/twilio-php/Services/Twilio/Rest/IncomingPhoneNumber.php b/externals/twilio-php/Services/Twilio/Rest/IncomingPhoneNumber.php
deleted file mode 100644
index ab72eafe1..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/IncomingPhoneNumber.php
+++ /dev/null
@@ -1,91 +0,0 @@
-<?php
-
-/**
- * An object representing a single phone number. For more
- * information, see the `IncomingPhoneNumber Instance Resource
- * <http://www.twilio.com/docs/api/rest/incoming-phone-numbers#instance>`_
- * documentation.
- *
- * .. php:attr:: sid
- *
- * A 34 character string that uniquely idetifies this resource.
- *
- * .. php:attr:: date_created
- *
- * The date that this resource was created, given as GMT RFC 2822 format.
- *
- * .. php:attr:: date_updated
- *
- * The date that this resource was last updated, given as GMT RFC 2822 format.
- *
- * .. php:attr:: friendly_name
- *
- * A human readable descriptive text for this resource, up to 64
- * characters long. By default, the FriendlyName is a nicely formatted
- * version of the phone number.
- *
- * .. php:attr:: account_sid
- *
- * The unique id of the Account responsible for this phone number.
- *
- * .. php:attr:: phone_number
- *
- * The incoming phone number. e.g., +16175551212 (E.164 format)
- *
- * .. php:attr:: api_version
- *
- * Calls to this phone number will start a new TwiML session with this
- * API version.
- *
- * .. php:attr:: voice_caller_id_lookup
- *
- * Look up the caller's caller-ID name from the CNAM database (additional charges apply). Either true or false.
- *
- * .. php:attr:: voice_url
- *
- * The URL Twilio will request when this phone number receives a call.
- *
- * .. php:attr:: voice_method
- *
- * The HTTP method Twilio will use when requesting the above Url. Either GET or POST.
- *
- * .. php:attr:: voice_fallback_url
- *
- * The URL that Twilio will request if an error occurs retrieving or executing the TwiML requested by Url.
- *
- * .. php:attr:: voice_fallback_method
- *
- * The HTTP method Twilio will use when requesting the VoiceFallbackUrl. Either GET or POST.
- *
- * .. php:attr:: status_callback
- *
- * The URL that Twilio will request to pass status parameters (such as call ended) to your application.
- *
- * .. php:attr:: status_callback_method
- *
- * The HTTP method Twilio will use to make requests to the StatusCallback URL. Either GET or POST.
- *
- * .. php:attr:: sms_url
- *
- * The URL Twilio will request when receiving an incoming SMS message to this number.
- *
- * .. php:attr:: sms_method
- *
- * The HTTP method Twilio will use when making requests to the SmsUrl. Either GET or POST.
- *
- * .. php:attr:: sms_fallback_url
- *
- * The URL that Twilio will request if an error occurs retrieving or executing the TwiML from SmsUrl.
- *
- * .. php:attr:: sms_fallback_method
- *
- * The HTTP method Twilio will use when requesting the above URL. Either GET or POST.
- *
- * .. php:attr:: uri
- *
- * The URI for this resource, relative to https://api.twilio.com.
- */
-class Services_Twilio_Rest_IncomingPhoneNumber
- extends Services_Twilio_InstanceResource
-{
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/IncomingPhoneNumbers.php b/externals/twilio-php/Services/Twilio/Rest/IncomingPhoneNumbers.php
deleted file mode 100644
index 48ce4cab2..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/IncomingPhoneNumbers.php
+++ /dev/null
@@ -1,59 +0,0 @@
-<?php
-
-/**
- * For more information, see the
- * `IncomingPhoneNumbers API Resource
- * <http://www.twilio.com/docs/api/rest/incoming-phone-numbers#local>`_
- * documentation at twilio.com.
- */
-class Services_Twilio_Rest_IncomingPhoneNumbers extends Services_Twilio_ListResource {
- function init($client, $uri) {
- $this->setupSubresources(
- 'local',
- 'toll_free',
- 'mobile'
- );
- }
-
- function create(array $params = array()) {
- return parent::_create($params);
- }
-
- function getList($type, array $params = array())
- {
- return $this->client->retrieveData($this->uri . "/$type", $params);
- }
-
- /**
- * Return a phone number instance from its E.164 representation. If more
- * than one number matches the search string, returns the first one.
- *
- * Example usage:
- *
- * .. code-block:: php
- *
- * $number = $client->account->incoming_phone_numbers->getNumber('+14105551234');
- * echo $number->sid;
- *
- * :param string $number: The number in E.164 format, eg "+684105551234"
- * :return: A :php:class:`Services_Twilio_Rest_IncomingPhoneNumber` object, or null
- * :raises: a A :php:class:`Services_Twilio_RestException` if the number is
- * invalid, not provided in E.164 format or for any other API exception.
- */
- public function getNumber($number) {
- $page = $this->getPage(0, 1, array(
- 'PhoneNumber' => $number
- ));
- $items = $page->getItems();
- if (is_null($items) || empty($items)) {
- return null;
- }
- return $items[0];
- }
-}
-
-class Services_Twilio_Rest_Local extends Services_Twilio_NumberType { }
-
-class Services_Twilio_Rest_Mobile extends Services_Twilio_NumberType { }
-
-class Services_Twilio_Rest_TollFree extends Services_Twilio_NumberType { }
diff --git a/externals/twilio-php/Services/Twilio/Rest/IpAccessControlList.php b/externals/twilio-php/Services/Twilio/Rest/IpAccessControlList.php
deleted file mode 100644
index 5ba83f3ed..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/IpAccessControlList.php
+++ /dev/null
@@ -1,40 +0,0 @@
-<?php
-
-/**
- * A single IpAccessControlList
- *
- * .. php:attr:: date_created
- *
- * The date the ip access control list was created
- *
- * .. php:attr:: date_updated
- *
- * The date the ip access control list was updated
- *
- * .. php:attr:: sid
- *
- * A 34 character string that identifies this object
- *
- * .. php:attr:: account_sid
- *
- * The account that created the ip access control list
- *
- * .. php:attr:: friendly_name
- *
- * The friendly name of the ip access control list
- *
- * .. php:attr:: uri
- *
- * The uri of the ip access control list
- *
- * .. php:attr:: subresource_uris
- *
- * The subresources associated with this ip access control list (IpAddresses)
- */
-class Services_Twilio_Rest_IpAccessControlList extends Services_Twilio_InstanceResource {
- protected function init($client, $uri) {
- $this->setupSubresources(
- 'ip_addresses'
- );
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/IpAccessControlListMapping.php b/externals/twilio-php/Services/Twilio/Rest/IpAccessControlListMapping.php
deleted file mode 100644
index ce5bc5a93..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/IpAccessControlListMapping.php
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-
-/**
- * A single IpAccessControlListMapping
- *
- * .. php:attr:: date_created
- *
- * The date this mapping was created
- *
- * .. php:attr:: date_updated
- *
- * The date this mapping was updated
- *
- * .. php:attr:: sid
- *
- * The sid of this mapping
- *
- * .. php:attr:: friendly_name
- *
- * The friendly name of this mapping
- *
- * .. php:attr:: uri
- *
- * The uri of this mapping
- *
- * .. php:attr:: subresource_uris
- *
- * The subresources associated with this mapping (IpAddresses)
- */
-class Services_Twilio_Rest_IpAccessControlListMapping extends Services_Twilio_InstanceResource {
- protected function init($client, $uri) {
- $this->setupSubresources(
- 'ip_addresses'
- );
- }
-}
-
diff --git a/externals/twilio-php/Services/Twilio/Rest/IpAccessControlListMappings.php b/externals/twilio-php/Services/Twilio/Rest/IpAccessControlListMappings.php
deleted file mode 100644
index f58e1b95c..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/IpAccessControlListMappings.php
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_IpAccessControlListMappings extends Services_Twilio_SIPListResource {
-
- /**
- * Creates a new IpAccessControlListMapping instance
- *
- * Example usage:
- *
- * .. code-block:: php
- *
- * $client->account->sip->domains->get('SDXXX')->ip_access_control_list_mappings->create("ALXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
- *
- * :param string $ip_access_control_list_sid: the sid of the IpAccessControList
- * you're adding to this domain.
- * :param array $params: a single array of parameters which is serialized and
- * sent directly to the Twilio API.
- */
- public function create($ip_access_control_list_sid, $params = array()) {
- return parent::_create(array(
- 'IpAccessControlListSid' => $ip_access_control_list_sid,
- ) + $params);
- }
-}
-
diff --git a/externals/twilio-php/Services/Twilio/Rest/IpAccessControlLists.php b/externals/twilio-php/Services/Twilio/Rest/IpAccessControlLists.php
deleted file mode 100644
index 88b9d14f0..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/IpAccessControlLists.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_IpAccessControlLists extends Services_Twilio_SIPListResource {
-
- /**
- * Creates a new IpAccessControlLists instance
- *
- * Example usage:
- *
- * .. code-block:: php
- *
- * $client->account->sip->ip_access_control_lists->create("MyFriendlyName");
- *
- * :param string $friendly_name: the friendly name of this ip access control list
- * :param array $params: a single array of parameters which is serialized and
- * sent directly to the Twilio API.
- * :return: the created list
- * :rtype: :class:`Services_Twilio_Rest_IpAccessControlList`
- *
- */
- public function create($friendly_name, $params = array()) {
- return parent::_create(array(
- 'FriendlyName' => $friendly_name,
- ) + $params);
- }
-
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/IpAddress.php b/externals/twilio-php/Services/Twilio/Rest/IpAddress.php
deleted file mode 100644
index 38b716e61..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/IpAddress.php
+++ /dev/null
@@ -1,34 +0,0 @@
-<?php
-
-/**
- * A single IpAddress
- *
- * .. php:attr:: date_created
- *
- * The date the IpAddress was created
- *
- * .. php:attr:: date_updated
- *
- * The date the IpAddress was updated
- *
- * .. php:attr:: sid
- *
- * A 34 character string that identifies this object
- *
- * .. php:attr:: account_sid
- *
- * The account that created this credential
- *
- * .. php:attr:: friendly_name
- *
- * The friendly name of the IpAddress
- *
- * .. php:attr:: ip_address
- *
- * The ip address of this IpAddress object
- *
- * .. php:attr:: uri
- *
- * The uri of this IpAddress object
- */
-class Services_Twilio_Rest_IpAddress extends Services_Twilio_InstanceResource { }
diff --git a/externals/twilio-php/Services/Twilio/Rest/IpAddresses.php b/externals/twilio-php/Services/Twilio/Rest/IpAddresses.php
deleted file mode 100644
index 798cd863b..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/IpAddresses.php
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_IpAddresses extends Services_Twilio_SIPListResource {
-
- public function __construct($client, $uri) {
- $this->instance_name = "Services_Twilio_Rest_IpAddress";
- parent::__construct($client, $uri);
- }
-
- /**
- * Creates a new IpAddress instance
- *
- * Example usage:
- *
- * .. code-block:: php
- *
- * $client->account->sip->ip_access_control_lists->get('ALXXX')->ip_addresses->create(
- * "FriendlyName", "127.0.0.1"
- * );
- *
- * :param string $friendly_name: the friendly name for the new IpAddress object
- * :param string $ip_address: the ip address for the new IpAddress object
- * :param array $params: a single array of parameters which is serialized and
- * sent directly to the Twilio API.
- */
- public function create($friendly_name, $ip_address, $params = array()) {
- return parent::_create(array(
- 'FriendlyName' => $friendly_name,
- 'IpAddress' => $ip_address,
- ) + $params);
- }
-}
-
diff --git a/externals/twilio-php/Services/Twilio/Rest/Media.php b/externals/twilio-php/Services/Twilio/Rest/Media.php
deleted file mode 100644
index 0d8a19f38..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/Media.php
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-/**
- * A list of :class:`Media <Services_Twilio_Rest_MediaInstance>` objects.
- * For the definitive reference, see the `Twilio Media List Documentation
- * <https://www.twilio.com/docs/api/rest/media>`_.
- */
-class Services_Twilio_Rest_Media extends Services_Twilio_ListResource {
-
-
- // This is overridden because the list key in the Twilio response
- // is "media_list", not "media".
- public function getResourceName($camelized = false)
- {
- if ($camelized) {
- return "MediaList";
- } else {
- return "media_list";
- }
- }
-
- // We manually set the instance name here so that the parent
- // constructor doesn't attempt to figure out it. It would do it
- // incorrectly because we override getResourceName above.
- public function __construct($client, $uri) {
- $this->instance_name = "Services_Twilio_Rest_MediaInstance";
- parent::__construct($client, $uri);
- }
-
-}
-
diff --git a/externals/twilio-php/Services/Twilio/Rest/MediaInstance.php b/externals/twilio-php/Services/Twilio/Rest/MediaInstance.php
deleted file mode 100644
index 1a152b2ea..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/MediaInstance.php
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-
-/**
- * A single Media object. For the definitive reference, see the `Twilio Media
- * Documentation <https://www.twilio.com/docs/api/rest/media>`_.
- *
- * .. php:attr:: sid
- *
- * A 34 character string that identifies this object
- *
- * .. php:attr:: account_sid
- *
- * A 34 character string representing the account that sent the message
- *
- * .. php:attr:: parent_sid
- *
- * The sid of the message that created this media.
- *
- * .. php:attr:: date_created
- *
- * The date the message was created
- *
- * .. php:attr:: date_updated
- *
- * The date the message was updated
- *
- * .. php:attr:: content_type
- *
- * The content-type of the media.
- */
-class Services_Twilio_Rest_MediaInstance extends Services_Twilio_InstanceResource {
- public function __construct($client, $uri) {
- $uri = str_replace('MediaInstance', 'Media', $uri);
- parent::__construct($client, $uri);
- }
-}
-
diff --git a/externals/twilio-php/Services/Twilio/Rest/Member.php b/externals/twilio-php/Services/Twilio/Rest/Member.php
deleted file mode 100644
index 8067cddfe..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/Member.php
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_Member
- extends Services_Twilio_InstanceResource
-{
-
- /**
- * Dequeue this member
- *
- * @param string $url The Twiml URL to play for this member, after
- * dequeueing them
- * @param string $method The HTTP method to use when fetching the Twiml
- * URL. Defaults to POST.
- * @return Services_Twilio_Rest_Member The dequeued member
- */
- public function dequeue($url, $method = 'POST') {
- return self::update(array(
- 'Url' => $url,
- 'Method' => $method,
- ));
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/Members.php b/externals/twilio-php/Services/Twilio/Rest/Members.php
deleted file mode 100644
index 61e05decc..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/Members.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_Members
- extends Services_Twilio_ListResource
-{
- /**
- * Return the member at the front of the queue. Note that any operations
- * performed on the Member returned from this function will use the /Front
- * Uri, not the Member's CallSid.
- *
- * @return Services_Twilio_Rest_Member The member at the front of the queue
- */
- public function front() {
- return new $this->instance_name($this->client, $this->uri . '/Front');
- }
-
- /* Participants are identified by CallSid, not like ME123 */
- public function getObjectFromJson($params, $idParam = 'sid') {
- return parent::getObjectFromJson($params, 'call_sid');
- }
-
- public function getResourceName($camelized = false)
- {
- // The JSON property name is atypical.
- return $camelized ? 'Members' : 'queue_members';
- }
-}
-
diff --git a/externals/twilio-php/Services/Twilio/Rest/Message.php b/externals/twilio-php/Services/Twilio/Rest/Message.php
deleted file mode 100644
index afc6cf5b0..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/Message.php
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-
-/**
- * A single Message
- *
- * .. php:attr:: date_created
- *
- * The date the message was created
- *
- * .. php:attr:: date_updated
- *
- * The date the message was updated
- *
- * .. php:attr:: sid
- *
- * A 34 character string that identifies this object
- *
- * .. php:attr:: account_sid
- *
- * The account that sent the message
- *
- * .. php:attr:: body
- *
- * The body of the message
- *
- * .. php:attr:: num_segments
- *
- * The number of sms messages used to deliver the body
- *
- * .. php:attr:: num_media
- *
- * The number of media that are associated with the image
- *
- * .. php:attr:: subresource_uris
- *
- * The subresources associated with this message (just Media at the moment)
- *
- * .. php:attr:: from
- *
- * The number this message was sent from
- *
- * .. php:attr:: to
- *
- * The phone number this message was sent to
- */
-class Services_Twilio_Rest_Message extends Services_Twilio_InstanceResource {
- protected function init($client, $uri) {
- $this->setupSubresources(
- 'media'
- );
- }
-}
-
diff --git a/externals/twilio-php/Services/Twilio/Rest/Messages.php b/externals/twilio-php/Services/Twilio/Rest/Messages.php
deleted file mode 100644
index 355fce039..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/Messages.php
+++ /dev/null
@@ -1,73 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_Messages extends Services_Twilio_ListResource {
-
- /**
- * Create a new Message instance
- *
- * Example usage:
- *
- * .. code-block:: php
- *
- * $client->account->messages->create(array(
- * "Body" => "foo",
- * "From" => "+14105551234",
- * "To" => "+14105556789",
- * ));
- *
- * :param array $params: a single array of parameters which is serialized and
- * sent directly to the Twilio API. You may find it easier to use the
- * sendMessage helper instead of this function.
- *
- */
- public function create($params = array()) {
- return parent::_create($params);
- }
-
- /**
- * Send a message
- *
- * .. code-block:: php
- *
- * $client = new Services_Twilio('AC123', '123');
- * $message = $client->account->messages->sendMessage(
- * '+14105551234', // From a Twilio number in your account
- * '+14105556789', // Text any number
- * 'Come at the king, you best not miss.' // Message body (if any)
- * array('https://demo.twilio.com/owl.png'), // An array of MediaUrls
- * );
- *
- * :param string $from: the from number for the message, this must be a
- * number you purchased from Twilio
- * :param string $to: the message recipient's phone number
- * :param $mediaUrls: the URLs of images to send in this MMS
- * :type $mediaUrls: null (don't include media), a single URL, or an array
- * of URLs to send as media with this message
- * :param string $body: the text to include along with this MMS
- * :param array $params: Any additional params (callback, etc) you'd like to
- * send with this request, these are serialized and sent as POST
- * parameters
- *
- * :return: The created :class:`Services_Twilio_Rest_Message`
- * :raises: :class:`Services_Twilio_RestException`
- * An exception if the parameters are invalid (for example, the from
- * number is not a Twilio number registered to your account, or is
- * unable to send MMS)
- */
- public function sendMessage($from, $to, $body = null, $mediaUrls = null,
- $params = array()
- ) {
- $postParams = array(
- 'From' => $from,
- 'To' => $to,
- );
- // When the request is made, this will get serialized into MediaUrl=a&MediaUrl=b
- if (!is_null($mediaUrls)) {
- $postParams['MediaUrl'] = $mediaUrls;
- }
- if (!is_null($body)) {
- $postParams['Body'] = $body;
- }
- return self::create($postParams + $params);
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/Notification.php b/externals/twilio-php/Services/Twilio/Rest/Notification.php
deleted file mode 100644
index ef89247a5..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/Notification.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_Notification
- extends Services_Twilio_InstanceResource
-{
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/Notifications.php b/externals/twilio-php/Services/Twilio/Rest/Notifications.php
deleted file mode 100644
index caaba2072..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/Notifications.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_Notifications
- extends Services_Twilio_ListResource
-{
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/OutgoingCallerId.php b/externals/twilio-php/Services/Twilio/Rest/OutgoingCallerId.php
deleted file mode 100644
index d10f1fd48..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/OutgoingCallerId.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_OutgoingCallerId
- extends Services_Twilio_InstanceResource
-{
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/OutgoingCallerIds.php b/externals/twilio-php/Services/Twilio/Rest/OutgoingCallerIds.php
deleted file mode 100644
index 0a94fc697..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/OutgoingCallerIds.php
+++ /dev/null
@@ -1,12 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_OutgoingCallerIds
- extends Services_Twilio_ListResource
-{
- public function create($phoneNumber, array $params = array())
- {
- return parent::_create(array(
- 'PhoneNumber' => $phoneNumber,
- ) + $params);
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/Participant.php b/externals/twilio-php/Services/Twilio/Rest/Participant.php
deleted file mode 100644
index b62920b2d..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/Participant.php
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_Participant
- extends Services_Twilio_InstanceResource
-{
- public function mute()
- {
- $this->update('Muted', 'true');
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/Participants.php b/externals/twilio-php/Services/Twilio/Rest/Participants.php
deleted file mode 100644
index 3b0464eea..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/Participants.php
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_Participants
- extends Services_Twilio_ListResource
-{
- /* Participants are identified by CallSid, not like PI123 */
- public function getObjectFromJson($params, $idParam = "sid") {
- return parent::getObjectFromJson($params, "call_sid");
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/Queue.php b/externals/twilio-php/Services/Twilio/Rest/Queue.php
deleted file mode 100644
index fa0f2f7e2..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/Queue.php
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_Queue
- extends Services_Twilio_InstanceResource {
-
- protected function init($client, $uri) {
- $this->setupSubresources('members');
- }
-}
-
diff --git a/externals/twilio-php/Services/Twilio/Rest/Queues.php b/externals/twilio-php/Services/Twilio/Rest/Queues.php
deleted file mode 100644
index bc35c830f..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/Queues.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_Queues
- extends Services_Twilio_ListResource
-{
- /**
- * Create a new Queue
- *
- * @param string $friendly_name The name of this queue
- * @param array $params A list of optional parameters, and their values
- * @return Services_Twilio_Rest_Queue The created Queue
- */
- function create($friendly_name, array $params = array()) {
- return parent::_create(array(
- 'FriendlyName' => $friendly_name,
- ) + $params);
- }
-}
-
diff --git a/externals/twilio-php/Services/Twilio/Rest/Recording.php b/externals/twilio-php/Services/Twilio/Rest/Recording.php
deleted file mode 100644
index a76014c25..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/Recording.php
+++ /dev/null
@@ -1,9 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_Recording
- extends Services_Twilio_InstanceResource
-{
- protected function init($client, $uri) {
- $this->setupSubresources('transcriptions');
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/Recordings.php b/externals/twilio-php/Services/Twilio/Rest/Recordings.php
deleted file mode 100644
index 47ec0d547..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/Recordings.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_Recordings
- extends Services_Twilio_ListResource
-{
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/Sandbox.php b/externals/twilio-php/Services/Twilio/Rest/Sandbox.php
deleted file mode 100644
index a39339851..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/Sandbox.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_Sandbox
- extends Services_Twilio_InstanceResource
-{
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/ShortCode.php b/externals/twilio-php/Services/Twilio/Rest/ShortCode.php
deleted file mode 100644
index 3ce6d7b1b..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/ShortCode.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_ShortCode
- extends Services_Twilio_InstanceResource
-{
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/ShortCodes.php b/externals/twilio-php/Services/Twilio/Rest/ShortCodes.php
deleted file mode 100644
index b080a972e..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/ShortCodes.php
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_ShortCodes
- extends Services_Twilio_ListResource
-{
- public function __construct($client, $uri) {
- $uri = preg_replace("#ShortCodes#", "SMS/ShortCodes", $uri);
- parent::__construct($client, $uri);
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/Sip.php b/externals/twilio-php/Services/Twilio/Rest/Sip.php
deleted file mode 100644
index 8c4bdb5da..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/Sip.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-/**
- * For Linux filename compatibility, this file needs to be named Sip.php, or
- * camelize() needs to be special cased in setupSubresources
- */
-class Services_Twilio_Rest_SIP extends Services_Twilio_InstanceResource {
- protected function init($client, $uri) {
- $this->setupSubresources(
- 'domains',
- 'ip_access_control_lists',
- 'credential_lists'
- );
- }
-
- public function getResourceName($camelized = false) {
- return "SIP";
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/SmsMessage.php b/externals/twilio-php/Services/Twilio/Rest/SmsMessage.php
deleted file mode 100644
index 6bd3f9ca0..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/SmsMessage.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_SmsMessage
- extends Services_Twilio_InstanceResource
-{
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/SmsMessages.php b/externals/twilio-php/Services/Twilio/Rest/SmsMessages.php
deleted file mode 100644
index 0d779218f..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/SmsMessages.php
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_SmsMessages
- extends Services_Twilio_ListResource
-{
- public function __construct($client, $uri) {
- $uri = preg_replace("#SmsMessages#", "SMS/Messages", $uri);
- parent::__construct($client, $uri);
- }
-
- function create($from, $to, $body, array $params = array()) {
- return parent::_create(array(
- 'From' => $from,
- 'To' => $to,
- 'Body' => $body
- ) + $params);
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/Transcription.php b/externals/twilio-php/Services/Twilio/Rest/Transcription.php
deleted file mode 100644
index 83c139cb2..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/Transcription.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_Transcription
- extends Services_Twilio_InstanceResource
-{
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/Transcriptions.php b/externals/twilio-php/Services/Twilio/Rest/Transcriptions.php
deleted file mode 100644
index ea35446a0..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/Transcriptions.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_Transcriptions
- extends Services_Twilio_ListResource
-{
-}
diff --git a/externals/twilio-php/Services/Twilio/Rest/UsageRecord.php b/externals/twilio-php/Services/Twilio/Rest/UsageRecord.php
deleted file mode 100644
index eee88f3af..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/UsageRecord.php
+++ /dev/null
@@ -1,6 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_UsageRecord extends Services_Twilio_InstanceResource
-{
-}
-
diff --git a/externals/twilio-php/Services/Twilio/Rest/UsageRecords.php b/externals/twilio-php/Services/Twilio/Rest/UsageRecords.php
deleted file mode 100644
index d53c27f4c..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/UsageRecords.php
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_UsageRecords extends Services_Twilio_TimeRangeResource {
-
- public function init($client, $uri) {
- $this->setupSubresources(
- 'today',
- 'yesterday',
- 'all_time',
- 'this_month',
- 'last_month',
- 'daily',
- 'monthly',
- 'yearly'
- );
- }
-}
-
-class Services_Twilio_Rest_Today extends Services_Twilio_TimeRangeResource { }
-
-class Services_Twilio_Rest_Yesterday extends Services_Twilio_TimeRangeResource { }
-
-class Services_Twilio_Rest_LastMonth extends Services_Twilio_TimeRangeResource { }
-
-class Services_Twilio_Rest_ThisMonth extends Services_Twilio_TimeRangeResource { }
-
-class Services_Twilio_Rest_AllTime extends Services_Twilio_TimeRangeResource { }
-
-class Services_Twilio_Rest_Daily extends Services_Twilio_UsageResource { }
-
-class Services_Twilio_Rest_Monthly extends Services_Twilio_UsageResource { }
-
-class Services_Twilio_Rest_Yearly extends Services_Twilio_UsageResource { }
diff --git a/externals/twilio-php/Services/Twilio/Rest/UsageTrigger.php b/externals/twilio-php/Services/Twilio/Rest/UsageTrigger.php
deleted file mode 100644
index 44c8cf5cf..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/UsageTrigger.php
+++ /dev/null
@@ -1,5 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_UsageTrigger
- extends Services_Twilio_InstanceResource { }
-
diff --git a/externals/twilio-php/Services/Twilio/Rest/UsageTriggers.php b/externals/twilio-php/Services/Twilio/Rest/UsageTriggers.php
deleted file mode 100644
index 6c198f7d1..000000000
--- a/externals/twilio-php/Services/Twilio/Rest/UsageTriggers.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-class Services_Twilio_Rest_UsageTriggers extends Services_Twilio_ListResource {
-
- public function __construct($client, $uri) {
- $uri = preg_replace("#UsageTriggers#", "Usage/Triggers", $uri);
- parent::__construct($client, $uri);
- }
-
- /**
- * Create a new UsageTrigger
- * @param string $category The category of usage to fire a trigger for. A full list of categories can be found in the `Usage Categories documentation <http://www.twilio.com/docs/api/rest/usage-records#usage-categories>`_.
- * @param string $value Fire the trigger when usage crosses this value.
- * @param string $url The URL to request when the trigger fires.
- * @param array $params Optional parameters for this trigger. A full list of parameters can be found in the `Usage Trigger documentation <http://www.twilio.com/docs/api/rest/usage-triggers#list-post-optional-parameters>`_.
- * @return Services_Twilio_Rest_UsageTrigger The created trigger
- */
- function create($category, $value, $url, array $params = array()) {
- return parent::_create(array(
- 'UsageCategory' => $category,
- 'TriggerValue' => $value,
- 'CallbackUrl' => $url,
- ) + $params);
- }
-
-}
-
diff --git a/externals/twilio-php/Services/Twilio/RestException.php b/externals/twilio-php/Services/Twilio/RestException.php
deleted file mode 100644
index c7de16ce4..000000000
--- a/externals/twilio-php/Services/Twilio/RestException.php
+++ /dev/null
@@ -1,44 +0,0 @@
-<?php
-
-/**
- * An exception talking to the Twilio API. This is thrown whenever the Twilio
- * API returns a 400 or 500-level exception.
- *
- * :param int $status: the HTTP status for the exception
- * :param string $message: a human-readable error message for the exception
- * :param int $code: a Twilio-specific error code for the exception
- * :param string $info: a link to more information
- */
-class Services_Twilio_RestException extends Exception {
-
- /**
- * The HTTP status for the exception.
- */
- protected $status;
-
- /**
- * A URL to get more information about the error. This is not always
- * available
- */
- protected $info;
-
- public function __construct($status, $message, $code = 0, $info = '') {
- $this->status = $status;
- $this->info = $info;
- parent::__construct($message, $code);
- }
-
- /**
- * Get the HTTP status code
- */
- public function getStatus() {
- return $this->status;
- }
-
- /**
- * Get a link to more information
- */
- public function getInfo() {
- return $this->info;
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/SIPListResource.php b/externals/twilio-php/Services/Twilio/SIPListResource.php
deleted file mode 100644
index 1e63b67f4..000000000
--- a/externals/twilio-php/Services/Twilio/SIPListResource.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php
-
-/**
- * This subclass of ListResource is used solely to update
- * the URI for sip resources.
- */
-abstract class Services_Twilio_SIPListResource extends Services_Twilio_ListResource {
- public function __construct($client, $uri) {
- // Rename all /Sip/ uris to /SIP/
- $uri = preg_replace("#/Sip#", "/SIP", $uri);
- parent::__construct($client, $uri);
- }
-}
-
diff --git a/externals/twilio-php/Services/Twilio/TimeRangeResource.php b/externals/twilio-php/Services/Twilio/TimeRangeResource.php
deleted file mode 100644
index ebf199091..000000000
--- a/externals/twilio-php/Services/Twilio/TimeRangeResource.php
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-/**
- * Parent class for usage resources that expose a single date, eg 'Today', 'ThisMonth', etc
- * @author Kevin Burke <kevin@twilio.com>
- * @license http://creativecommons.org/licenses/MIT/ MIT
- * @link http://pear.php.net/package/Services_Twilio
- */
-class Services_Twilio_TimeRangeResource extends Services_Twilio_UsageResource {
-
- /**
- * Return a UsageRecord corresponding to the given category.
- *
- * @param string $category The category of usage to retrieve. For a full
- * list of valid categories, please see the documentation at
- * http://www.twilio.com/docs/api/rest/usage-records#usage-all-categories
- * @return Services_Twilio_Rest_UsageRecord
- * @throws Services_Twilio_RestException
- */
- public function getCategory($category) {
- $page = $this->getPage(0, 1, array(
- 'Category' => $category,
- ));
- $items = $page->getItems();
- if (!is_array($items) || count($items) === 0) {
- throw new Services_Twilio_RestException(
- 400, "Usage record data is unformattable.");
- }
- return $items[0];
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/TinyHttp.php b/externals/twilio-php/Services/Twilio/TinyHttp.php
deleted file mode 100644
index 83eb9a24a..000000000
--- a/externals/twilio-php/Services/Twilio/TinyHttp.php
+++ /dev/null
@@ -1,126 +0,0 @@
-<?php
-/**
- * Based on TinyHttp from https://gist.github.com/618157.
- * Copyright 2011, Neuman Vong. BSD License.
- */
-
-class Services_Twilio_TinyHttpException extends ErrorException {}
-
-/**
- * An HTTP client that makes requests
- *
- * :param string $uri: The base uri to use for requests
- * :param array $kwargs: An array of additional arguments to pass to the
- * library. Accepted arguments are:
- *
- * - **debug** - Print the HTTP request before making it to Twilio
- * - **curlopts** - An array of keys and values that are passed to
- * ``curl_setopt_array``.
- *
- * Here's an example. This is the default HTTP client used by the library.
- *
- * .. code-block:: php
- *
- * $_http = new Services_Twilio_TinyHttp(
- * "https://api.twilio.com",
- * array("curlopts" => array(
- * CURLOPT_USERAGENT => self::USER_AGENT,
- * CURLOPT_HTTPHEADER => array('Accept-Charset: utf-8'),
- * CURLOPT_CAINFO => dirname(__FILE__) . '/cacert.pem',
- * ))
- * );
- */
-class Services_Twilio_TinyHttp {
- var $user, $pass, $scheme, $host, $port, $debug, $curlopts;
-
- public function __construct($uri = '', $kwargs = array()) {
- foreach (parse_url($uri) as $name => $value) $this->$name = $value;
- $this->debug = isset($kwargs['debug']) ? !!$kwargs['debug'] : NULL;
- $this->curlopts = isset($kwargs['curlopts']) ? $kwargs['curlopts'] : array();
- }
-
- public function __call($name, $args) {
- list($res, $req_headers, $req_body) = $args + array(0, array(), '');
-
- $opts = $this->curlopts + array(
- CURLOPT_URL => "$this->scheme://$this->host$res",
- CURLOPT_HEADER => TRUE,
- CURLOPT_RETURNTRANSFER => TRUE,
- CURLOPT_INFILESIZE => -1,
- CURLOPT_POSTFIELDS => NULL,
- CURLOPT_TIMEOUT => 60,
- );
-
- foreach ($req_headers as $k => $v) $opts[CURLOPT_HTTPHEADER][] = "$k: $v";
- if ($this->port) $opts[CURLOPT_PORT] = $this->port;
- if ($this->debug) $opts[CURLINFO_HEADER_OUT] = TRUE;
- if ($this->user && $this->pass) $opts[CURLOPT_USERPWD] = "$this->user:$this->pass";
- switch ($name) {
- case 'get':
- $opts[CURLOPT_HTTPGET] = TRUE;
- break;
- case 'post':
- $opts[CURLOPT_POST] = TRUE;
- $opts[CURLOPT_POSTFIELDS] = $req_body;
- break;
- case 'put':
- $opts[CURLOPT_PUT] = TRUE;
- if (strlen($req_body)) {
- if ($buf = fopen('php://memory', 'w+')) {
- fwrite($buf, $req_body);
- fseek($buf, 0);
- $opts[CURLOPT_INFILE] = $buf;
- $opts[CURLOPT_INFILESIZE] = strlen($req_body);
- } else throw new Services_Twilio_TinyHttpException('unable to open temporary file');
- }
- break;
- case 'head':
- $opts[CURLOPT_NOBODY] = TRUE;
- break;
- default:
- $opts[CURLOPT_CUSTOMREQUEST] = strtoupper($name);
- break;
- }
- try {
- if ($curl = curl_init()) {
- if (curl_setopt_array($curl, $opts)) {
- if ($response = curl_exec($curl)) {
- $parts = explode("\r\n\r\n", $response, 3);
- list($head, $body) = ($parts[0] == 'HTTP/1.1 100 Continue')
- ? array($parts[1], $parts[2])
- : array($parts[0], $parts[1]);
- $status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
- if ($this->debug) {
- error_log(
- curl_getinfo($curl, CURLINFO_HEADER_OUT) .
- $req_body
- );
- }
- $header_lines = explode("\r\n", $head);
- array_shift($header_lines);
- foreach ($header_lines as $line) {
- list($key, $value) = explode(":", $line, 2);
- $headers[$key] = trim($value);
- }
- curl_close($curl);
- if (isset($buf) && is_resource($buf)) {
- fclose($buf);
- }
- return array($status, $headers, $body);
- } else {
- throw new Services_Twilio_TinyHttpException(curl_error($curl));
- }
- } else throw new Services_Twilio_TinyHttpException(curl_error($curl));
- } else throw new Services_Twilio_TinyHttpException('unable to initialize cURL');
- } catch (ErrorException $e) {
- if (is_resource($curl)) curl_close($curl);
- if (isset($buf) && is_resource($buf)) fclose($buf);
- throw $e;
- }
- }
-
- public function authenticate($user, $pass) {
- $this->user = $user;
- $this->pass = $pass;
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/Twiml.php b/externals/twilio-php/Services/Twilio/Twiml.php
deleted file mode 100644
index d546b054a..000000000
--- a/externals/twilio-php/Services/Twilio/Twiml.php
+++ /dev/null
@@ -1,137 +0,0 @@
-<?php
-
-/**
- * Exception class for Services_Twilio_Twiml.
- */
-class Services_Twilio_TwimlException extends Exception {}
-
-/**
- * Twiml response generator.
- *
- * Author: Neuman Vong <neuman at ashmoremusic dot com>
- * License: http://creativecommons.org/licenses/MIT/ MIT
- */
-class Services_Twilio_Twiml {
-
- protected $element;
-
- /**
- * Constructs a Twiml response.
- *
- * :param SimpleXmlElement|array $arg: Can be any of
- *
- * - the element to wrap
- * - attributes to add to the element
- * - if null, initialize an empty element named 'Response'
- */
- public function __construct($arg = null) {
- switch (true) {
- case $arg instanceof SimpleXmlElement:
- $this->element = $arg;
- break;
- case $arg === null:
- $this->element = new SimpleXmlElement('<Response/>');
- break;
- case is_array($arg):
- $this->element = new SimpleXmlElement('<Response/>');
- foreach ($arg as $name => $value) {
- $this->element->addAttribute($name, $value);
- }
- break;
- default:
- throw new TwimlException('Invalid argument');
- }
- }
-
- /**
- * Converts method calls into Twiml verbs.
- *
- * A basic example:
- *
- * .. code-block:: php
- *
- * php> print $this->say('hello');
- * <Say>hello</Say>
- *
- * An example with attributes:
- *
- * .. code-block:: php
- *
- * print $this->say('hello', array('voice' => 'woman'));
- * <Say voice="woman">hello</Say>
- *
- * You could even just pass in an attributes array, omitting the noun:
- *
- * .. code-block:: php
- *
- * print $this->gather(array('timeout' => '20'));
- * <Gather timeout="20"/>
- *
- * :param string $verb: The Twiml verb.
- * :param array $args:
- * - (noun string)
- * - (noun string, attributes array)
- * - (attributes array)
- *
- * :return: A SimpleXmlElement
- * :rtype: SimpleXmlElement
- */
- public function __call($verb, array $args)
- {
- list($noun, $attrs) = $args + array('', array());
- if (is_array($noun)) {
- list($attrs, $noun) = array($noun, '');
- }
- /* addChild does not escape XML, while addAttribute does. This means if
- * you pass unescaped ampersands ("&") to addChild, you will generate
- * an error.
- *
- * Some inexperienced developers will pass in unescaped ampersands, and
- * we want to make their code work, by escaping the ampersands for them
- * before passing the string to addChild. (with htmlentities)
- *
- * However other people will know what to do, and their code
- * already escapes ampersands before passing them to addChild. We don't
- * want to break their existing code by turning their &amp;'s into
- * &amp;amp;
- *
- * We also want to use numeric entities, not named entities so that we
- * are fully compatible with XML
- *
- * The following lines accomplish the desired behavior.
- */
- $decoded = html_entity_decode($noun, ENT_COMPAT, 'UTF-8');
- $normalized = htmlspecialchars($decoded, ENT_COMPAT, 'UTF-8', false);
- $child = empty($noun)
- ? $this->element->addChild(ucfirst($verb))
- : $this->element->addChild(ucfirst($verb), $normalized);
- foreach ($attrs as $name => $value) {
- /* Note that addAttribute escapes raw ampersands by default, so we
- * haven't touched its implementation. So this is the matrix for
- * addAttribute:
- *
- * & turns into &amp;
- * &amp; turns into &amp;amp;
- */
- if (is_bool($value)) {
- $value = ($value === true) ? 'true' : 'false';
- }
- $child->addAttribute($name, $value);
- }
- return new static($child);
- }
-
- /**
- * Returns the object as XML.
- *
- * :return: The response as an XML string
- * :rtype: string
- */
- public function __toString()
- {
- $xml = $this->element->asXml();
- return str_replace(
- '<?xml version="1.0"?>',
- '<?xml version="1.0" encoding="UTF-8"?>', $xml);
- }
-}
diff --git a/externals/twilio-php/Services/Twilio/UsageResource.php b/externals/twilio-php/Services/Twilio/UsageResource.php
deleted file mode 100644
index b9b929cfc..000000000
--- a/externals/twilio-php/Services/Twilio/UsageResource.php
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-
-/**
- * Parent class for all UsageRecord subclasses
- * @author Kevin Burke <kevin@twilio.com>
- * @license http://creativecommons.org/licenses/MIT/ MIT
- * @link http://pear.php.net/package/Services_Twilio
- */
-class Services_Twilio_UsageResource extends Services_Twilio_ListResource {
- public function getResourceName($camelized = false) {
- $this->instance_name = 'Services_Twilio_Rest_UsageRecord';
- return $camelized ? 'UsageRecords' : 'usage_records';
- }
-
- public function __construct($client, $uri) {
- $uri = preg_replace("#UsageRecords#", "Usage/Records", $uri);
- parent::__construct($client, $uri);
- }
-}
-
diff --git a/externals/twilio-php/Services/cacert.pem b/externals/twilio-php/Services/cacert.pem
deleted file mode 100644
index 20fd34a95..000000000
--- a/externals/twilio-php/Services/cacert.pem
+++ /dev/null
@@ -1,3849 +0,0 @@
-##
-## ca-bundle.crt -- Bundle of CA Root Certificates
-##
-## Certificate data from Mozilla as of: Thu Jun 28 15:03:08 2012
-##
-## This is a bundle of X.509 certificates of public Certificate Authorities
-## (CA). These were automatically extracted from Mozilla's root certificates
-## file (certdata.txt). This file can be found in the mozilla source tree:
-## http://mxr.mozilla.org/mozilla/source/security/nss/lib/ckfw/builtins/certdata.txt?raw=1
-##
-## It contains the certificates in PEM format and therefore
-## can be directly used with curl / libcurl / php_curl, or with
-## an Apache+mod_ssl webserver for SSL client authentication.
-## Just configure this file as the SSLCACertificateFile.
-##
-
-# @(#) $RCSfile: certdata.txt,v $ $Revision: 1.85 $ $Date: 2012/06/28 13:50:18 $
-
-GTE CyberTrust Global Root
-==========================
------BEGIN CERTIFICATE-----
-MIICWjCCAcMCAgGlMA0GCSqGSIb3DQEBBAUAMHUxCzAJBgNVBAYTAlVTMRgwFgYDVQQKEw9HVEUg
-Q29ycG9yYXRpb24xJzAlBgNVBAsTHkdURSBDeWJlclRydXN0IFNvbHV0aW9ucywgSW5jLjEjMCEG
-A1UEAxMaR1RFIEN5YmVyVHJ1c3QgR2xvYmFsIFJvb3QwHhcNOTgwODEzMDAyOTAwWhcNMTgwODEz
-MjM1OTAwWjB1MQswCQYDVQQGEwJVUzEYMBYGA1UEChMPR1RFIENvcnBvcmF0aW9uMScwJQYDVQQL
-Ex5HVEUgQ3liZXJUcnVzdCBTb2x1dGlvbnMsIEluYy4xIzAhBgNVBAMTGkdURSBDeWJlclRydXN0
-IEdsb2JhbCBSb290MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCVD6C28FCc6HrHiM3dFw4u
-sJTQGz0O9pTAipTHBsiQl8i4ZBp6fmw8U+E3KHNgf7KXUwefU/ltWJTSr41tiGeA5u2ylc9yMcql
-HHK6XALnZELn+aks1joNrI1CqiQBOeacPwGFVw1Yh0X404Wqk2kmhXBIgD8SFcd5tB8FLztimQID
-AQABMA0GCSqGSIb3DQEBBAUAA4GBAG3rGwnpXtlR22ciYaQqPEh346B8pt5zohQDhT37qw4wxYMW
-M4ETCJ57NE7fQMh017l93PR2VX2bY1QY6fDq81yx2YtCHrnAlU66+tXifPVoYb+O7AWXX1uw16OF
-NMQkpw0PlZPvy5TYnh+dXIVtx6quTx8itc2VrbqnzPmrC3p/
------END CERTIFICATE-----
-
-Thawte Server CA
-================
------BEGIN CERTIFICATE-----
-MIIDEzCCAnygAwIBAgIBATANBgkqhkiG9w0BAQQFADCBxDELMAkGA1UEBhMCWkExFTATBgNVBAgT
-DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3dGUgQ29uc3Vs
-dGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcGA1UE
-AxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5j
-b20wHhcNOTYwODAxMDAwMDAwWhcNMjAxMjMxMjM1OTU5WjCBxDELMAkGA1UEBhMCWkExFTATBgNV
-BAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3dGUgQ29u
-c3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcG
-A1UEAxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0
-ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANOkUG7I/1Zr5s9dtuoMaHVHoqrC2oQl
-/Kj0R1HahbUgdJSGHg91yekIYfUGbTBuFRkC6VLAYttNmZ7iagxEOM3+vuNkCXDF/rFrKbYvScg7
-1CcEJRCXL+eQbcAoQpnXTEPew/UhbVSfXcNY4cDk2VuwuNy0e982OsK1ZiIS1ocNAgMBAAGjEzAR
-MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAB/pMaVz7lcxG7oWDTSEwjsrZqG9J
-GubaUeNgcGyEYRGhGshIPllDfU+VPaGLtwtimHp1it2ITk6eQNuozDJ0uW8NxuOzRAvZim+aKZuZ
-GCg70eNAKJpaPNW15yAbi8qkq43pUdniTCxZqdq5snUb9kLy78fyGPmJvKP/iiMucEc=
------END CERTIFICATE-----
-
-Thawte Premium Server CA
-========================
------BEGIN CERTIFICATE-----
-MIIDJzCCApCgAwIBAgIBATANBgkqhkiG9w0BAQQFADCBzjELMAkGA1UEBhMCWkExFTATBgNVBAgT
-DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3dGUgQ29uc3Vs
-dGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UE
-AxMYVGhhd3RlIFByZW1pdW0gU2VydmVyIENBMSgwJgYJKoZIhvcNAQkBFhlwcmVtaXVtLXNlcnZl
-ckB0aGF3dGUuY29tMB4XDTk2MDgwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgc4xCzAJBgNVBAYT
-AlpBMRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEdMBsGA1UEChMU
-VGhhd3RlIENvbnN1bHRpbmcgY2MxKDAmBgNVBAsTH0NlcnRpZmljYXRpb24gU2VydmljZXMgRGl2
-aXNpb24xITAfBgNVBAMTGFRoYXd0ZSBQcmVtaXVtIFNlcnZlciBDQTEoMCYGCSqGSIb3DQEJARYZ
-cHJlbWl1bS1zZXJ2ZXJAdGhhd3RlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0jY2
-aovXwlue2oFBYo847kkEVdbQ7xwblRZH7xhINTpS9CtqBo87L+pW46+GjZ4X9560ZXUCTe/LCaIh
-Udib0GfQug2SBhRz1JPLlyoAnFxODLz6FVL88kRu2hFKbgifLy3j+ao6hnO2RlNYyIkFvYMRuHM/
-qgeN9EJN50CdHDcCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQQFAAOBgQAm
-SCwWwlj66BZ0DKqqX1Q/8tfJeGBeXm43YyJ3Nn6yF8Q0ufUIhfzJATj/Tb7yFkJD57taRvvBxhEf
-8UqwKEbJw8RCfbz6q1lu1bdRiBHjpIUZa4JMpAwSremkrj/xw0llmozFyD4lt5SZu5IycQfwhl7t
-UCemDaYj+bvLpgcUQg==
------END CERTIFICATE-----
-
-Equifax Secure CA
-=================
------BEGIN CERTIFICATE-----
-MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJVUzEQMA4GA1UE
-ChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlmaWNhdGUgQXV0aG9yaXR5
-MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoT
-B0VxdWlmYXgxLTArBgNVBAsTJEVxdWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCB
-nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPR
-fM6fBeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+AcJkVV5MW
-8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kCAwEAAaOCAQkwggEFMHAG
-A1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UE
-CxMkRXF1aWZheCBTZWN1cmUgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoG
-A1UdEAQTMBGBDzIwMTgwODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvS
-spXXR9gjIBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQFMAMB
-Af8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUAA4GBAFjOKer89961
-zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y7qj/WsjTVbJmcVfewCHrPSqnI0kB
-BIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee95
-70+sB3c4
------END CERTIFICATE-----
-
-Digital Signature Trust Co. Global CA 1
-=======================================
------BEGIN CERTIFICATE-----
-MIIDKTCCApKgAwIBAgIENnAVljANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJVUzEkMCIGA1UE
-ChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMREwDwYDVQQLEwhEU1RDQSBFMTAeFw05ODEy
-MTAxODEwMjNaFw0xODEyMTAxODQwMjNaMEYxCzAJBgNVBAYTAlVTMSQwIgYDVQQKExtEaWdpdGFs
-IFNpZ25hdHVyZSBUcnVzdCBDby4xETAPBgNVBAsTCERTVENBIEUxMIGdMA0GCSqGSIb3DQEBAQUA
-A4GLADCBhwKBgQCgbIGpzzQeJN3+hijM3oMv+V7UQtLodGBmE5gGHKlREmlvMVW5SXIACH7TpWJE
-NySZj9mDSI+ZbZUTu0M7LklOiDfBu1h//uG9+LthzfNHwJmm8fOR6Hh8AMthyUQncWlVSn5JTe2i
-o74CTADKAqjuAQIxZA9SLRN0dja1erQtcQIBA6OCASQwggEgMBEGCWCGSAGG+EIBAQQEAwIABzBo
-BgNVHR8EYTBfMF2gW6BZpFcwVTELMAkGA1UEBhMCVVMxJDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0
-dXJlIFRydXN0IENvLjERMA8GA1UECxMIRFNUQ0EgRTExDTALBgNVBAMTBENSTDEwKwYDVR0QBCQw
-IoAPMTk5ODEyMTAxODEwMjNagQ8yMDE4MTIxMDE4MTAyM1owCwYDVR0PBAQDAgEGMB8GA1UdIwQY
-MBaAFGp5fpFpRhgTCgJ3pVlbYJglDqL4MB0GA1UdDgQWBBRqeX6RaUYYEwoCd6VZW2CYJQ6i+DAM
-BgNVHRMEBTADAQH/MBkGCSqGSIb2fQdBAAQMMAobBFY0LjADAgSQMA0GCSqGSIb3DQEBBQUAA4GB
-ACIS2Hod3IEGtgllsofIH160L+nEHvI8wbsEkBFKg05+k7lNQseSJqBcNJo4cvj9axY+IO6CizEq
-kzaFI4iKPANo08kJD038bKTaKHKTDomAsH3+gG9lbRgzl4vCa4nuYD3Im+9/KzJic5PLPON74nZ4
-RbyhkwS7hp86W0N6w4pl
------END CERTIFICATE-----
-
-Digital Signature Trust Co. Global CA 3
-=======================================
------BEGIN CERTIFICATE-----
-MIIDKTCCApKgAwIBAgIENm7TzjANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJVUzEkMCIGA1UE
-ChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMREwDwYDVQQLEwhEU1RDQSBFMjAeFw05ODEy
-MDkxOTE3MjZaFw0xODEyMDkxOTQ3MjZaMEYxCzAJBgNVBAYTAlVTMSQwIgYDVQQKExtEaWdpdGFs
-IFNpZ25hdHVyZSBUcnVzdCBDby4xETAPBgNVBAsTCERTVENBIEUyMIGdMA0GCSqGSIb3DQEBAQUA
-A4GLADCBhwKBgQC/k48Xku8zExjrEH9OFr//Bo8qhbxe+SSmJIi2A7fBw18DW9Fvrn5C6mYjuGOD
-VvsoLeE4i7TuqAHhzhy2iCoiRoX7n6dwqUcUP87eZfCocfdPJmyMvMa1795JJ/9IKn3oTQPMx7JS
-xhcxEzu1TdvIxPbDDyQq2gyd55FbgM2UnQIBA6OCASQwggEgMBEGCWCGSAGG+EIBAQQEAwIABzBo
-BgNVHR8EYTBfMF2gW6BZpFcwVTELMAkGA1UEBhMCVVMxJDAiBgNVBAoTG0RpZ2l0YWwgU2lnbmF0
-dXJlIFRydXN0IENvLjERMA8GA1UECxMIRFNUQ0EgRTIxDTALBgNVBAMTBENSTDEwKwYDVR0QBCQw
-IoAPMTk5ODEyMDkxOTE3MjZagQ8yMDE4MTIwOTE5MTcyNlowCwYDVR0PBAQDAgEGMB8GA1UdIwQY
-MBaAFB6CTShlgDzJQW6sNS5ay97u+DlbMB0GA1UdDgQWBBQegk0oZYA8yUFurDUuWsve7vg5WzAM
-BgNVHRMEBTADAQH/MBkGCSqGSIb2fQdBAAQMMAobBFY0LjADAgSQMA0GCSqGSIb3DQEBBQUAA4GB
-AEeNg61i8tuwnkUiBbmi1gMOOHLnnvx75pO2mqWilMg0HZHRxdf0CiUPPXiBng+xZ8SQTGPdXqfi
-up/1902lMXucKS1M/mQ+7LZT/uqb7YLbdHVLB3luHtgZg3Pe9T7Qtd7nS2h9Qy4qIOF+oHhEngj1
-mPnHfxsb1gYgAlihw6ID
------END CERTIFICATE-----
-
-Verisign Class 3 Public Primary Certification Authority
-=======================================================
------BEGIN CERTIFICATE-----
-MIICPDCCAaUCEHC65B0Q2Sk0tjjKewPMur8wDQYJKoZIhvcNAQECBQAwXzELMAkGA1UEBhMCVVMx
-FzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmltYXJ5
-IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVow
-XzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAz
-IFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUA
-A4GNADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhEBarsAx94
-f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/isI19wKTakyYbnsZogy1Ol
-hec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0GCSqGSIb3DQEBAgUAA4GBALtMEivPLCYA
-TxQT3ab7/AoRhIzzKBxnki98tsX63/Dolbwdj2wsqFHMc9ikwFPwTtYmwHYBV4GSXiHx0bH/59Ah
-WM1pF+NEHJwZRDmJXNycAA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2Omuf
-Tqj/ZA1k
------END CERTIFICATE-----
-
-Verisign Class 1 Public Primary Certification Authority - G2
-============================================================
------BEGIN CERTIFICATE-----
-MIIDAjCCAmsCEEzH6qqYPnHTkxD4PTqJkZIwDQYJKoZIhvcNAQEFBQAwgcExCzAJBgNVBAYTAlVT
-MRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMSBQdWJsaWMgUHJpbWFy
-eSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2ln
-biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVz
-dCBOZXR3b3JrMB4XDTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVT
-MRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMSBQdWJsaWMgUHJpbWFy
-eSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2ln
-biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVz
-dCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCq0Lq+Fi24g9TK0g+8djHKlNgd
-k4xWArzZbxpvUjZudVYKVdPfQ4chEWWKfo+9Id5rMj8bhDSVBZ1BNeuS65bdqlk/AVNtmU/t5eIq
-WpDBucSmFc/IReumXY6cPvBkJHalzasab7bYe1FhbqZ/h8jit+U03EGI6glAvnOSPWvndQIDAQAB
-MA0GCSqGSIb3DQEBBQUAA4GBAKlPww3HZ74sy9mozS11534Vnjty637rXC0Jh9ZrbWB85a7FkCMM
-XErQr7Fd88e2CtvgFZMN3QO8x3aKtd1Pw5sTdbgBwObJW2uluIncrKTdcu1OofdPvAbT6shkdHvC
-lUGcZXNY8ZCaPGqxmMnEh7zPRW1F4m4iP/68DzFc6PLZ
------END CERTIFICATE-----
-
-Verisign Class 2 Public Primary Certification Authority - G2
-============================================================
------BEGIN CERTIFICATE-----
-MIIDAzCCAmwCEQC5L2DMiJ+hekYJuFtwbIqvMA0GCSqGSIb3DQEBBQUAMIHBMQswCQYDVQQGEwJV
-UzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xPDA6BgNVBAsTM0NsYXNzIDIgUHVibGljIFByaW1h
-cnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMjE6MDgGA1UECxMxKGMpIDE5OTggVmVyaVNp
-Z24sIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1
-c3QgTmV0d29yazAeFw05ODA1MTgwMDAwMDBaFw0yODA4MDEyMzU5NTlaMIHBMQswCQYDVQQGEwJV
-UzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xPDA6BgNVBAsTM0NsYXNzIDIgUHVibGljIFByaW1h
-cnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMjE6MDgGA1UECxMxKGMpIDE5OTggVmVyaVNp
-Z24sIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1
-c3QgTmV0d29yazCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAp4gBIXQs5xoD8JjhlzwPIQjx
-nNuX6Zr8wgQGE75fUsjMHiwSViy4AWkszJkfrbCWrnkE8hM5wXuYuggs6MKEEyyqaekJ9MepAqRC
-wiNPStjwDqL7MWzJ5m+ZJwf15vRMeJ5t60aG+rmGyVTyssSv1EYcWskVMP8NbPUtDm3Of3cCAwEA
-ATANBgkqhkiG9w0BAQUFAAOBgQByLvl/0fFx+8Se9sVeUYpAmLho+Jscg9jinb3/7aHmZuovCfTK
-1+qlK5X2JGCGTUQug6XELaDTrnhpb3LabK4I8GOSN+a7xDAXrXfMSTWqz9iP0b63GJZHc2pUIjRk
-LbYWm1lbtFFZOrMLFPQS32eg9K0yZF6xRnInjBJ7xUS0rg==
------END CERTIFICATE-----
-
-Verisign Class 3 Public Primary Certification Authority - G2
-============================================================
------BEGIN CERTIFICATE-----
-MIIDAjCCAmsCEH3Z/gfPqB63EHln+6eJNMYwDQYJKoZIhvcNAQEFBQAwgcExCzAJBgNVBAYTAlVT
-MRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMgUHJpbWFy
-eSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2ln
-biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVz
-dCBOZXR3b3JrMB4XDTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVT
-MRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMgUHJpbWFy
-eSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2ln
-biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVz
-dCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMXtERXVxp0KvTuWpMmR9ZmDCO
-FoUgRm1HP9SFIIThbbP4pO0M8RcPO/mn+SXXwc+EY/J8Y8+iR/LGWzOOZEAEaMGAuWQcRXfH2G71
-lSk8UOg013gfqLptQ5GVj0VXXn7F+8qkBOvqlzdUMG+7AUcyM83cV5tkaWH4mx0ciU9cZwIDAQAB
-MA0GCSqGSIb3DQEBBQUAA4GBAFFNzb5cy5gZnBWyATl4Lk0PZ3BwmcYQWpSkU01UbSuvDV1Ai2TT
-1+7eVmGSX6bEHRBhNtMsJzzoKQm5EWR0zLVznxxIqbxhAe7iF6YM40AIOw7n60RzKprxaZLvcRTD
-Oaxxp5EJb+RxBrO6WVcmeQD2+A2iMzAo1KpYoJ2daZH9
------END CERTIFICATE-----
-
-GlobalSign Root CA
-==================
------BEGIN CERTIFICATE-----
-MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkGA1UEBhMCQkUx
-GTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jvb3QgQ0ExGzAZBgNVBAMTEkds
-b2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAwMDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNV
-BAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYD
-VQQDExJHbG9iYWxTaWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDa
-DuaZjc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavpxy0Sy6sc
-THAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp1Wrjsok6Vjk4bwY8iGlb
-Kk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdGsnUOhugZitVtbNV4FpWi6cgKOOvyJBNP
-c1STE4U6G7weNLWLBYy5d4ux2x8gkasJU26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrX
-gzT/LCrBbBlDSgeF59N89iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
-HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0BAQUF
-AAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOzyj1hTdNGCbM+w6Dj
-Y1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE38NflNUVyRRBnMRddWQVDf9VMOyG
-j/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymPAbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhH
-hm4qxFYxldBniYUr+WymXUadDKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveC
-X4XSQRjbgbMEHMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A==
------END CERTIFICATE-----
-
-GlobalSign Root CA - R2
-=======================
------BEGIN CERTIFICATE-----
-MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4GA1UECxMXR2xv
-YmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2Jh
-bFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxT
-aWduIFJvb3QgQ0EgLSBSMjETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2ln
-bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6
-ErPLv4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8eoLrvozp
-s6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklqtTleiDTsvHgMCJiEbKjN
-S7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzdC9XZzPnqJworc5HGnRusyMvo4KD0L5CL
-TfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pazq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6C
-ygPCm48CAwEAAaOBnDCBmTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E
-FgQUm+IHV2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5nbG9i
-YWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG3lm0mi3f3BmGLjAN
-BgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4GsJ0/WwbgcQ3izDJr86iw8bmEbTUsp
-9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu
-01yiPqFbQfXf5WRDLenVOavSot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG7
-9G+dwfCMNYxdAfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7
-TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg==
------END CERTIFICATE-----
-
-ValiCert Class 1 VA
-===================
------BEGIN CERTIFICATE-----
-MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRp
-b24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs
-YXNzIDEgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZh
-bGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNTIy
-MjM0OFoXDTE5MDYyNTIyMjM0OFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0
-d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENsYXNzIDEg
-UG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0
-LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMIGfMA0GCSqGSIb3DQEBAQUA
-A4GNADCBiQKBgQDYWYJ6ibiWuqYvaG9YLqdUHAZu9OqNSLwxlBfw8068srg1knaw0KWlAdcAAxIi
-GQj4/xEjm84H9b9pGib+TunRf50sQB1ZaG6m+FiwnRqP0z/x3BkGgagO4DrdyFNFCQbmD3DD+kCm
-DuJWBQ8YTfwggtFzVXSNdnKgHZ0dwN0/cQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAFBoPUn0LBwG
-lN+VYH+Wexf+T3GtZMjdd9LvWVXoP+iOBSoh8gfStadS/pyxtuJbdxdA6nLWI8sogTLDAHkY7FkX
-icnGah5xyf23dKUlRWnFSKsZ4UWKJWsZ7uW7EvV/96aNUcPwnXS3qT6gpf+2SQMT2iLM7XGCK5nP
-Orf1LXLI
------END CERTIFICATE-----
-
-ValiCert Class 2 VA
-===================
------BEGIN CERTIFICATE-----
-MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRp
-b24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs
-YXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZh
-bGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAw
-MTk1NFoXDTE5MDYyNjAwMTk1NFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0
-d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENsYXNzIDIg
-UG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0
-LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMIGfMA0GCSqGSIb3DQEBAQUA
-A4GNADCBiQKBgQDOOnHK5avIWZJV16vYdA757tn2VUdZZUcOBVXc65g2PFxTXdMwzzjsvUGJ7SVC
-CSRrCl6zfN1SLUzm1NZ9WlmpZdRJEy0kTRxQb7XBhVQ7/nHk01xC+YDgkRoKWzk2Z/M/VXwbP7Rf
-ZHM047QSv4dk+NoS/zcnwbNDu+97bi5p9wIDAQABMA0GCSqGSIb3DQEBBQUAA4GBADt/UG9vUJSZ
-SWI4OB9L+KXIPqeCgfYrx+jFzug6EILLGACOTb2oWH+heQC1u+mNr0HZDzTuIYEZoDJJKPTEjlbV
-UjP9UNV+mWwD5MlM/Mtsq2azSiGM5bUMMj4QssxsodyamEwCW/POuZ6lcg5Ktz885hZo+L7tdEy8
-W9ViH0Pd
------END CERTIFICATE-----
-
-RSA Root Certificate 1
-======================
------BEGIN CERTIFICATE-----
-MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRp
-b24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs
-YXNzIDMgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZh
-bGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAw
-MjIzM1oXDTE5MDYyNjAwMjIzM1owgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0
-d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENsYXNzIDMg
-UG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0
-LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMIGfMA0GCSqGSIb3DQEBAQUA
-A4GNADCBiQKBgQDjmFGWHOjVsQaBalfDcnWTq8+epvzzFlLWLU2fNUSoLgRNB0mKOCn1dzfnt6td
-3zZxFJmP3MKS8edgkpfs2Ejcv8ECIMYkpChMMFp2bbFc893enhBxoYjHW5tBbcqwuI4V7q0zK89H
-BFx1cQqYJJgpp0lZpd34t0NiYfPT4tBVPwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAFa7AliEZwgs
-3x/be0kz9dNnnfS0ChCzycUs4pJqcXgn8nCDQtM+z6lU9PHYkhaM0QTLS6vJn0WuPIqpsHEzXcjF
-V9+vqDWzf4mH6eglkrh/hXqu1rweN1gqZ8mRzyqBPu3GOd/APhmcGcwTTYJBtYze4D1gCCAPRX5r
-on+jjBXu
------END CERTIFICATE-----
-
-Verisign Class 1 Public Primary Certification Authority - G3
-============================================================
------BEGIN CERTIFICATE-----
-MIIEGjCCAwICEQCLW3VWhFSFCwDPrzhIzrGkMA0GCSqGSIb3DQEBBQUAMIHKMQswCQYDVQQGEwJV
-UzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0IE5ldHdv
-cmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl
-IG9ubHkxRTBDBgNVBAMTPFZlcmlTaWduIENsYXNzIDEgUHVibGljIFByaW1hcnkgQ2VydGlmaWNh
-dGlvbiBBdXRob3JpdHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQsw
-CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRy
-dXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhv
-cml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWduIENsYXNzIDEgUHVibGljIFByaW1hcnkg
-Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
-ggEBAN2E1Lm0+afY8wR4nN493GwTFtl63SRRZsDHJlkNrAYIwpTRMx/wgzUfbhvI3qpuFU5UJ+/E
-bRrsC+MO8ESlV8dAWB6jRx9x7GD2bZTIGDnt/kIYVt/kTEkQeE4BdjVjEjbdZrwBBDajVWjVojYJ
-rKshJlQGrT/KFOCsyq0GHZXi+J3x4GD/wn91K0zM2v6HmSHquv4+VNfSWXjbPG7PoBMAGrgnoeS+
-Z5bKoMWznN3JdZ7rMJpfo83ZrngZPyPpXNspva1VyBtUjGP26KbqxzcSXKMpHgLZ2x87tNcPVkeB
-FQRKr4Mn0cVYiMHd9qqnoxjaaKptEVHhv2Vrn5Z20T0CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEA
-q2aN17O6x5q25lXQBfGfMY1aqtmqRiYPce2lrVNWYgFHKkTp/j90CxObufRNG7LRX7K20ohcs5/N
-y9Sn2WCVhDr4wTcdYcrnsMXlkdpUpqwxga6X3s0IrLjAl4B/bnKk52kTlWUfxJM8/XmPBNQ+T+r3
-ns7NZ3xPZQL/kYVUc8f/NveGLezQXk//EZ9yBta4GvFMDSZl4kSAHsef493oCtrspSCAaWihT37h
-a88HQfqDjrw43bAuEbFrskLMmrz5SCJ5ShkPshw+IHTZasO+8ih4E1Z5T21Q6huwtVexN2ZYI/Pc
-D98Kh8TvhgXVOBRgmaNL3gaWcSzy27YfpO8/7g==
------END CERTIFICATE-----
-
-Verisign Class 2 Public Primary Certification Authority - G3
-============================================================
------BEGIN CERTIFICATE-----
-MIIEGTCCAwECEGFwy0mMX5hFKeewptlQW3owDQYJKoZIhvcNAQEFBQAwgcoxCzAJBgNVBAYTAlVT
-MRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29y
-azE6MDgGA1UECxMxKGMpIDE5OTkgVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ug
-b25seTFFMEMGA1UEAxM8VmVyaVNpZ24gQ2xhc3MgMiBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0
-aW9uIEF1dGhvcml0eSAtIEczMB4XDTk5MTAwMTAwMDAwMFoXDTM2MDcxNjIzNTk1OVowgcoxCzAJ
-BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1
-c3QgTmV0d29yazE6MDgGA1UECxMxKGMpIDE5OTkgVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9y
-aXplZCB1c2Ugb25seTFFMEMGA1UEAxM8VmVyaVNpZ24gQ2xhc3MgMiBQdWJsaWMgUHJpbWFyeSBD
-ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEczMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
-AQEArwoNwtUs22e5LeWUJ92lvuCwTY+zYVY81nzD9M0+hsuiiOLh2KRpxbXiv8GmR1BeRjmL1Za6
-tW8UvxDOJxOeBUebMXoT2B/Z0wI3i60sR/COgQanDTAM6/c8DyAd3HJG7qUCyFvDyVZpTMUYwZF7
-C9UTAJu878NIPkZgIIUq1ZC2zYugzDLdt/1AVbJQHFauzI13TccgTacxdu9okoqQHgiBVrKtaaNS
-0MscxCM9H5n+TOgWY47GCI72MfbS+uV23bUckqNJzc0BzWjNqWm6o+sdDZykIKbBoMXRRkwXbdKs
-Zj+WjOCE1Db/IlnF+RFgqF8EffIa9iVCYQ/ESrg+iQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQA0
-JhU8wI1NQ0kdvekhktdmnLfexbjQ5F1fdiLAJvmEOjr5jLX77GDx6M4EsMjdpwOPMPOY36TmpDHf
-0xwLRtxyID+u7gU8pDM/CzmscHhzS5kr3zDCVLCoO1Wh/hYozUK9dG6A2ydEp85EXdQbkJgNHkKU
-sQAsBNB0owIFImNjzYO1+8FtYmtpdf1dcEG59b98377BMnMiIYtYgXsVkXq642RIsH/7NiXaldDx
-JBQX3RiAa0YjOVT1jmIJBB2UkKab5iXiQkWquJCtvgiPqQtCGJTPcjnhsUPgKM+351psE2tJs//j
-GHyJizNdrDPXp/naOlXJWBD5qu9ats9LS98q
------END CERTIFICATE-----
-
-Verisign Class 3 Public Primary Certification Authority - G3
-============================================================
------BEGIN CERTIFICATE-----
-MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQswCQYDVQQGEwJV
-UzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0IE5ldHdv
-cmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl
-IG9ubHkxRTBDBgNVBAMTPFZlcmlTaWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNh
-dGlvbiBBdXRob3JpdHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQsw
-CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRy
-dXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhv
-cml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWduIENsYXNzIDMgUHVibGljIFByaW1hcnkg
-Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
-ggEBAMu6nFL8eB8aHm8bN3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1
-EUGO+i2tKmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGukxUc
-cLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBmCC+Vk7+qRy+oRpfw
-EuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJXwzw3sJ2zq/3avL6QaaiMxTJ5Xpj
-055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWuimi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEA
-ERSWwauSCPc/L8my/uRan2Te2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5f
-j267Cz3qWhMeDGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC
-/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565pF4ErWjfJXir0
-xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGtTxzhT5yvDwyd93gN2PQ1VoDa
-t20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ==
------END CERTIFICATE-----
-
-Verisign Class 4 Public Primary Certification Authority - G3
-============================================================
------BEGIN CERTIFICATE-----
-MIIEGjCCAwICEQDsoKeLbnVqAc/EfMwvlF7XMA0GCSqGSIb3DQEBBQUAMIHKMQswCQYDVQQGEwJV
-UzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0IE5ldHdv
-cmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl
-IG9ubHkxRTBDBgNVBAMTPFZlcmlTaWduIENsYXNzIDQgUHVibGljIFByaW1hcnkgQ2VydGlmaWNh
-dGlvbiBBdXRob3JpdHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQsw
-CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRy
-dXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhv
-cml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWduIENsYXNzIDQgUHVibGljIFByaW1hcnkg
-Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
-ggEBAK3LpRFpxlmr8Y+1GQ9Wzsy1HyDkniYlS+BzZYlZ3tCD5PUPtbut8XzoIfzk6AzufEUiGXaS
-tBO3IFsJ+mGuqPKljYXCKtbeZjbSmwL0qJJgfJxptI8kHtCGUvYynEFYHiK9zUVilQhu0GbdU6LM
-8BDcVHOLBKFGMzNcF0C5nk3T875Vg+ixiY5afJqWIpA7iCXy0lOIAgwLePLmNxdLMEYH5IBtptiW
-Lugs+BGzOA1mppvqySNb247i8xOOGlktqgLw7KSHZtzBP/XYufTsgsbSPZUd5cBPhMnZo0QoBmrX
-Razwa2rvTl/4EYIeOGM0ZlDUPpNz+jDDZq3/ky2X7wMCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEA
-j/ola09b5KROJ1WrIhVZPMq1CtRK26vdoV9TxaBXOcLORyu+OshWv8LZJxA6sQU8wHcxuzrTBXtt
-mhwwjIDLk5Mqg6sFUYICABFna/OIYUdfA5PVWw3g8dShMjWFsjrbsIKr0csKvE+MW8VLADsfKoKm
-fjaF3H48ZwC15DtS4KjrXRX5xm3wrR0OhbepmnMUWluPQSjA1egtTaRezarZ7c7c2NU8Qh0XwRJd
-RTjDOPP8hS6DRkiy1yBfkjaP53kPmF6Z6PDQpLv1U70qzlmwr25/bLvSHgCwIe34QWKCudiyxLtG
-UPMxxY8BqHTr9Xgn2uf3ZkPznoM+IKrDNWCRzg==
------END CERTIFICATE-----
-
-Entrust.net Secure Server CA
-============================
------BEGIN CERTIFICATE-----
-MIIE2DCCBEGgAwIBAgIEN0rSQzANBgkqhkiG9w0BAQUFADCBwzELMAkGA1UEBhMCVVMxFDASBgNV
-BAoTC0VudHJ1c3QubmV0MTswOQYDVQQLEzJ3d3cuZW50cnVzdC5uZXQvQ1BTIGluY29ycC4gYnkg
-cmVmLiAobGltaXRzIGxpYWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRl
-ZDE6MDgGA1UEAxMxRW50cnVzdC5uZXQgU2VjdXJlIFNlcnZlciBDZXJ0aWZpY2F0aW9uIEF1dGhv
-cml0eTAeFw05OTA1MjUxNjA5NDBaFw0xOTA1MjUxNjM5NDBaMIHDMQswCQYDVQQGEwJVUzEUMBIG
-A1UEChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5jb3JwLiBi
-eSByZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBFbnRydXN0Lm5ldCBMaW1p
-dGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENlcnRpZmljYXRpb24gQXV0
-aG9yaXR5MIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQDNKIM0VBuJ8w+vN5Ex/68xYMmo6LIQ
-aO2f55M28Qpku0f1BBc/I0dNxScZgSYMVHINiC3ZH5oSn7yzcdOAGT9HZnuMNSjSuQrfJNqc1lB5
-gXpa0zf3wkrYKZImZNHkmGw6AIr1NJtl+O3jEP/9uElY3KDegjlrgbEWGWG5VLbmQwIBA6OCAdcw
-ggHTMBEGCWCGSAGG+EIBAQQEAwIABzCCARkGA1UdHwSCARAwggEMMIHeoIHboIHYpIHVMIHSMQsw
-CQYDVQQGEwJVUzEUMBIGA1UEChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3dy5lbnRydXN0Lm5l
-dC9DUFMgaW5jb3JwLiBieSByZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBF
-bnRydXN0Lm5ldCBMaW1pdGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENl
-cnRpZmljYXRpb24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMCmgJ6AlhiNodHRwOi8vd3d3LmVu
-dHJ1c3QubmV0L0NSTC9uZXQxLmNybDArBgNVHRAEJDAigA8xOTk5MDUyNTE2MDk0MFqBDzIwMTkw
-NTI1MTYwOTQwWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAU8BdiE1U9s/8KAGv7UISX8+1i0Bow
-HQYDVR0OBBYEFPAXYhNVPbP/CgBr+1CEl/PtYtAaMAwGA1UdEwQFMAMBAf8wGQYJKoZIhvZ9B0EA
-BAwwChsEVjQuMAMCBJAwDQYJKoZIhvcNAQEFBQADgYEAkNwwAvpkdMKnCqV8IY00F6j7Rw7/JXyN
-Ewr75Ji174z4xRAN95K+8cPV1ZVqBLssziY2ZcgxxufuP+NXdYR6Ee9GTxj005i7qIcyunL2POI9
-n9cd2cNgQ4xYDiKWL2KjLB+6rQXvqzJ4h6BUcxm1XAX5Uj5tLUUL9wqT6u0G+bI=
------END CERTIFICATE-----
-
-Entrust.net Premium 2048 Secure Server CA
-=========================================
------BEGIN CERTIFICATE-----
-MIIEXDCCA0SgAwIBAgIEOGO5ZjANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChMLRW50cnVzdC5u
-ZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBpbmNvcnAuIGJ5IHJlZi4gKGxp
-bWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNV
-BAMTKkVudHJ1c3QubmV0IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQx
-NzUwNTFaFw0xOTEyMjQxODIwNTFaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3
-d3d3LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTEl
-MCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEGA1UEAxMqRW50cnVzdC5u
-ZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
-MIIBCgKCAQEArU1LqRKGsuqjIAcVFmQqK0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOL
-Gp18EzoOH1u3Hs/lJBQesYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSr
-hRSGlVuXMlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVTXTzW
-nLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/HoZdenoVve8AjhUi
-VBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH4QIDAQABo3QwcjARBglghkgBhvhC
-AQEEBAMCAAcwHwYDVR0jBBgwFoAUVeSB0RGAvtiJuQijMfmhJAkWuXAwHQYDVR0OBBYEFFXkgdER
-gL7YibkIozH5oSQJFrlwMB0GCSqGSIb2fQdBAAQQMA4bCFY1LjA6NC4wAwIEkDANBgkqhkiG9w0B
-AQUFAAOCAQEAWUesIYSKF8mciVMeuoCFGsY8Tj6xnLZ8xpJdGGQC49MGCBFhfGPjK50xA3B20qMo
-oPS7mmNz7W3lKtvtFKkrxjYR0CvrB4ul2p5cGZ1WEvVUKcgF7bISKo30Axv/55IQh7A6tcOdBTcS
-o8f0FbnVpDkWm1M6I5HxqIKiaohowXkCIryqptau37AUX7iH0N18f3v/rxzP5tsHrV7bhZ3QKw0z
-2wTR5klAEyt2+z7pnIkPFc4YsIV4IU9rTw76NmfNB/L/CNDi3tm/Kq+4h4YhPATKt5Rof8886ZjX
-OP/swNlQ8C5LWK5Gb9Auw2DaclVyvUxFnmG6v4SBkgPR0ml8xQ==
------END CERTIFICATE-----
-
-Baltimore CyberTrust Root
-=========================
------BEGIN CERTIFICATE-----
-MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJRTESMBAGA1UE
-ChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYDVQQDExlCYWx0aW1vcmUgQ3li
-ZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoXDTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMC
-SUUxEjAQBgNVBAoTCUJhbHRpbW9yZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFs
-dGltb3JlIEN5YmVyVHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKME
-uyKrmD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjrIZ3AQSsB
-UnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeKmpYcqWe4PwzV9/lSEy/C
-G9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSuXmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9
-XbIGevOF6uvUA65ehD5f/xXtabz5OTZydc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjpr
-l3RjM71oGDHweI12v/yejl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoI
-VDaGezq1BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEB
-BQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT929hkTI7gQCvlYpNRh
-cL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3WgxjkzSswF07r51XgdIGn9w/xZchMB5
-hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsa
-Y71k5h+3zvDyny67G7fyUIhzksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9H
-RCwBXbsdtTLSR9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp
------END CERTIFICATE-----
-
-Equifax Secure Global eBusiness CA
-==================================
------BEGIN CERTIFICATE-----
-MIICkDCCAfmgAwIBAgIBATANBgkqhkiG9w0BAQQFADBaMQswCQYDVQQGEwJVUzEcMBoGA1UEChMT
-RXF1aWZheCBTZWN1cmUgSW5jLjEtMCsGA1UEAxMkRXF1aWZheCBTZWN1cmUgR2xvYmFsIGVCdXNp
-bmVzcyBDQS0xMB4XDTk5MDYyMTA0MDAwMFoXDTIwMDYyMTA0MDAwMFowWjELMAkGA1UEBhMCVVMx
-HDAaBgNVBAoTE0VxdWlmYXggU2VjdXJlIEluYy4xLTArBgNVBAMTJEVxdWlmYXggU2VjdXJlIEds
-b2JhbCBlQnVzaW5lc3MgQ0EtMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAuucXkAJlsTRV
-PEnCUdXfp9E3j9HngXNBUmCbnaEXJnitx7HoJpQytd4zjTov2/KaelpzmKNc6fuKcxtc58O/gGzN
-qfTWK8D3+ZmqY6KxRwIP1ORROhI8bIpaVIRw28HFkM9yRcuoWcDNM50/o5brhTMhHD4ePmBudpxn
-hcXIw2ECAwEAAaNmMGQwEQYJYIZIAYb4QgEBBAQDAgAHMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0j
-BBgwFoAUvqigdHJQa0S3ySPY+6j/s1draGwwHQYDVR0OBBYEFL6ooHRyUGtEt8kj2Puo/7NXa2hs
-MA0GCSqGSIb3DQEBBAUAA4GBADDiAVGqx+pf2rnQZQ8w1j7aDRRJbpGTJxQx78T3LUX47Me/okEN
-I7SS+RkAZ70Br83gcfxaz2TE4JaY0KNA4gGK7ycH8WUBikQtBmV1UsCGECAhX2xrD2yuCRyv8qIY
-NMR1pHMc8Y3c7635s3a0kr/clRAevsvIO1qEYBlWlKlV
------END CERTIFICATE-----
-
-Equifax Secure eBusiness CA 1
-=============================
------BEGIN CERTIFICATE-----
-MIICgjCCAeugAwIBAgIBBDANBgkqhkiG9w0BAQQFADBTMQswCQYDVQQGEwJVUzEcMBoGA1UEChMT
-RXF1aWZheCBTZWN1cmUgSW5jLjEmMCQGA1UEAxMdRXF1aWZheCBTZWN1cmUgZUJ1c2luZXNzIENB
-LTEwHhcNOTkwNjIxMDQwMDAwWhcNMjAwNjIxMDQwMDAwWjBTMQswCQYDVQQGEwJVUzEcMBoGA1UE
-ChMTRXF1aWZheCBTZWN1cmUgSW5jLjEmMCQGA1UEAxMdRXF1aWZheCBTZWN1cmUgZUJ1c2luZXNz
-IENBLTEwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAM4vGbwXt3fek6lfWg0XTzQaDJj0ItlZ
-1MRoRvC0NcWFAyDGr0WlIVFFQesWWDYyb+JQYmT5/VGcqiTZ9J2DKocKIdMSODRsjQBuWqDZQu4a
-IZX5UkxVWsUPOE9G+m34LjXWHXzr4vCwdYDIqROsvojvOm6rXyo4YgKwEnv+j6YDAgMBAAGjZjBk
-MBEGCWCGSAGG+EIBAQQEAwIABzAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFEp4MlIR21kW
-Nl7fwRQ2QGpHfEyhMB0GA1UdDgQWBBRKeDJSEdtZFjZe38EUNkBqR3xMoTANBgkqhkiG9w0BAQQF
-AAOBgQB1W6ibAxHm6VZMzfmpTMANmvPMZWnmJXbMWbfWVMMdzZmsGd20hdXgPfxiIKeES1hl8eL5
-lSE/9dR+WB5Hh1Q+WKG1tfgq73HnvMP2sUlG4tega+VWeponmHxGYhTnyfxuAxJ5gDgdSIKN/Bf+
-KpYrtWKmpj29f5JZzVoqgrI3eQ==
------END CERTIFICATE-----
-
-Equifax Secure eBusiness CA 2
-=============================
------BEGIN CERTIFICATE-----
-MIIDIDCCAomgAwIBAgIEN3DPtTANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJVUzEXMBUGA1UE
-ChMORXF1aWZheCBTZWN1cmUxJjAkBgNVBAsTHUVxdWlmYXggU2VjdXJlIGVCdXNpbmVzcyBDQS0y
-MB4XDTk5MDYyMzEyMTQ0NVoXDTE5MDYyMzEyMTQ0NVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoT
-DkVxdWlmYXggU2VjdXJlMSYwJAYDVQQLEx1FcXVpZmF4IFNlY3VyZSBlQnVzaW5lc3MgQ0EtMjCB
-nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA5Dk5kx5SBhsoNviyoynF7Y6yEb3+6+e0dMKP/wXn
-2Z0GvxLIPw7y1tEkshHe0XMJitSxLJgJDR5QRrKDpkWNYmi7hRsgcDKqQM2mll/EcTc/BPO3QSQ5
-BxoeLmFYoBIL5aXfxavqN3HMHMg3OrmXUqesxWoklE6ce8/AatbfIb0CAwEAAaOCAQkwggEFMHAG
-A1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORXF1aWZheCBTZWN1cmUx
-JjAkBgNVBAsTHUVxdWlmYXggU2VjdXJlIGVCdXNpbmVzcyBDQS0yMQ0wCwYDVQQDEwRDUkwxMBoG
-A1UdEAQTMBGBDzIwMTkwNjIzMTIxNDQ1WjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUUJ4L6q9e
-uSBIplBqy/3YIHqngnYwHQYDVR0OBBYEFFCeC+qvXrkgSKZQasv92CB6p4J2MAwGA1UdEwQFMAMB
-Af8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUAA4GBAAyGgq3oThr1
-jokn4jVYPSm0B482UJW/bsGe68SQsoWou7dC4A8HOd/7npCy0cE+U58DRLB+S/Rv5Hwf5+Kx5Lia
-78O9zt4LMjTZ3ijtM2vE1Nc9ElirfQkty3D1E4qUoSek1nDFbZS1yX2doNLGCEnZZpum0/QL3MUm
-V+GRMOrN
------END CERTIFICATE-----
-
-AddTrust Low-Value Services Root
-================================
------BEGIN CERTIFICATE-----
-MIIEGDCCAwCgAwIBAgIBATANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQGEwJTRTEUMBIGA1UEChML
-QWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSEwHwYDVQQDExhBZGRU
-cnVzdCBDbGFzcyAxIENBIFJvb3QwHhcNMDAwNTMwMTAzODMxWhcNMjAwNTMwMTAzODMxWjBlMQsw
-CQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBO
-ZXR3b3JrMSEwHwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3QwggEiMA0GCSqGSIb3DQEB
-AQUAA4IBDwAwggEKAoIBAQCWltQhSWDia+hBBwzexODcEyPNwTXH+9ZOEQpnXvUGW2ulCDtbKRY6
-54eyNAbFvAWlA3yCyykQruGIgb3WntP+LVbBFc7jJp0VLhD7Bo8wBN6ntGO0/7Gcrjyvd7ZWxbWr
-oulpOj0OM3kyP3CCkplhbY0wCI9xP6ZIVxn4JdxLZlyldI+Yrsj5wAYi56xz36Uu+1LcsRVlIPo1
-Zmne3yzxbrww2ywkEtvrNTVokMsAsJchPXQhI2U0K7t4WaPW4XY5mqRJjox0r26kmqPZm9I4XJui
-GMx1I4S+6+JNM3GOGvDC+Mcdoq0Dlyz4zyXG9rgkMbFjXZJ/Y/AlyVMuH79NAgMBAAGjgdIwgc8w
-HQYDVR0OBBYEFJWxtPCUtr3H2tERCSG+wa9J/RB7MAsGA1UdDwQEAwIBBjAPBgNVHRMBAf8EBTAD
-AQH/MIGPBgNVHSMEgYcwgYSAFJWxtPCUtr3H2tERCSG+wa9J/RB7oWmkZzBlMQswCQYDVQQGEwJT
-RTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSEw
-HwYDVQQDExhBZGRUcnVzdCBDbGFzcyAxIENBIFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBACxt
-ZBsfzQ3duQH6lmM0MkhHma6X7f1yFqZzR1r0693p9db7RcwpiURdv0Y5PejuvE1Uhh4dbOMXJ0Ph
-iVYrqW9yTkkz43J8KiOavD7/KCrto/8cI7pDVwlnTUtiBi34/2ydYB7YHEt9tTEv2dB8Xfjea4MY
-eDdXL+gzB2ffHsdrKpV2ro9Xo/D0UrSpUwjP4E/TelOL/bscVjby/rK25Xa71SJlpz/+0WatC7xr
-mYbvP33zGDLKe8bjq2RGlfgmadlVg3sslgf/WSxEo8bl6ancoWOAWiFeIc9TVPC6b4nbqKqVz4vj
-ccweGyBECMB6tkD9xOQ14R0WHNC8K47Wcdk=
------END CERTIFICATE-----
-
-AddTrust External Root
-======================
------BEGIN CERTIFICATE-----
-MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEUMBIGA1UEChML
-QWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYD
-VQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEw
-NDgzOFowbzELMAkGA1UEBhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRU
-cnVzdCBFeHRlcm5hbCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0Eg
-Um9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvtH7xsD821
-+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9uMq/NzgtHj6RQa1wVsfw
-Tz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzXmk6vBbOmcZSccbNQYArHE504B4YCqOmo
-aSYYkKtMsE8jqzpPhNjfzp/haW+710LXa0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy
-2xSoRcRdKn23tNbE7qzNE0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv7
-7+ldU9U0WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYDVR0P
-BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0Jvf6xCZU7wO94CTL
-VBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRUcnVzdCBBQjEmMCQGA1UECxMdQWRk
-VHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsxIjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENB
-IFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZl
-j7DYd7usQWxHYINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5
-6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvCNr4TDea9Y355
-e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEXc4g/VhsxOBi0cQ+azcgOno4u
-G+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5amnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ=
------END CERTIFICATE-----
-
-AddTrust Public Services Root
-=============================
------BEGIN CERTIFICATE-----
-MIIEFTCCAv2gAwIBAgIBATANBgkqhkiG9w0BAQUFADBkMQswCQYDVQQGEwJTRTEUMBIGA1UEChML
-QWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSAwHgYDVQQDExdBZGRU
-cnVzdCBQdWJsaWMgQ0EgUm9vdDAeFw0wMDA1MzAxMDQxNTBaFw0yMDA1MzAxMDQxNTBaMGQxCzAJ
-BgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQIE5l
-dHdvcmsxIDAeBgNVBAMTF0FkZFRydXN0IFB1YmxpYyBDQSBSb290MIIBIjANBgkqhkiG9w0BAQEF
-AAOCAQ8AMIIBCgKCAQEA6Rowj4OIFMEg2Dybjxt+A3S72mnTRqX4jsIMEZBRpS9mVEBV6tsfSlbu
-nyNu9DnLoblv8n75XYcmYZ4c+OLspoH4IcUkzBEMP9smcnrHAZcHF/nXGCwwfQ56HmIexkvA/X1i
-d9NEHif2P0tEs7c42TkfYNVRknMDtABp4/MUTu7R3AnPdzRGULD4EfL+OHn3Bzn+UZKXC1sIXzSG
-Aa2Il+tmzV7R/9x98oTaunet3IAIx6eH1lWfl2royBFkuucZKT8Rs3iQhCBSWxHveNCD9tVIkNAw
-HM+A+WD+eeSI8t0A65RF62WUaUC6wNW0uLp9BBGo6zEFlpROWCGOn9Bg/QIDAQABo4HRMIHOMB0G
-A1UdDgQWBBSBPjfYkrAfd59ctKtzquf2NGAv+jALBgNVHQ8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB
-/zCBjgYDVR0jBIGGMIGDgBSBPjfYkrAfd59ctKtzquf2NGAv+qFopGYwZDELMAkGA1UEBhMCU0Ux
-FDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQLExRBZGRUcnVzdCBUVFAgTmV0d29yazEgMB4G
-A1UEAxMXQWRkVHJ1c3QgUHVibGljIENBIFJvb3SCAQEwDQYJKoZIhvcNAQEFBQADggEBAAP3FUr4
-JNojVhaTdt02KLmuG7jD8WS6IBh4lSknVwW8fCr0uVFV2ocC3g8WFzH4qnkuCRO7r7IgGRLlk/lL
-+YPoRNWyQSW/iHVv/xD8SlTQX/D67zZzfRs2RcYhbbQVuE7PnFylPVoAjgbjPGsye/Kf8Lb93/Ao
-GEjwxrzQvzSAlsJKsW2Ox5BF3i9nrEUEo3rcVZLJR2bYGozH7ZxOmuASu7VqTITh4SINhwBk/ox9
-Yjllpu9CtoAlEmEBqCQTcAARJl/6NVDFSMwGR+gn2HCNX2TmoUQmXiLsks3/QppEIW1cxeMiHV9H
-EufOX1362KqxMy3ZdvJOOjMMK7MtkAY=
------END CERTIFICATE-----
-
-AddTrust Qualified Certificates Root
-====================================
------BEGIN CERTIFICATE-----
-MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJTRTEUMBIGA1UEChML
-QWRkVHJ1c3QgQUIxHTAbBgNVBAsTFEFkZFRydXN0IFRUUCBOZXR3b3JrMSMwIQYDVQQDExpBZGRU
-cnVzdCBRdWFsaWZpZWQgQ0EgUm9vdDAeFw0wMDA1MzAxMDQ0NTBaFw0yMDA1MzAxMDQ0NTBaMGcx
-CzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRUcnVzdCBBQjEdMBsGA1UECxMUQWRkVHJ1c3QgVFRQ
-IE5ldHdvcmsxIzAhBgNVBAMTGkFkZFRydXN0IFF1YWxpZmllZCBDQSBSb290MIIBIjANBgkqhkiG
-9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5B6a/twJWoekn0e+EV+vhDTbYjx5eLfpMLXsDBwqxBb/4Oxx
-64r1EW7tTw2R0hIYLUkVAcKkIhPHEWT/IhKauY5cLwjPcWqzZwFZ8V1G87B4pfYOQnrjfxvM0PC3
-KP0q6p6zsLkEqv32x7SxuCqg+1jxGaBvcCV+PmlKfw8i2O+tCBGaKZnhqkRFmhJePp1tUvznoD1o
-L/BLcHwTOK28FSXx1s6rosAx1i+f4P8UWfyEk9mHfExUE+uf0S0R+Bg6Ot4l2ffTQO2kBhLEO+GR
-wVY18BTcZTYJbqukB8c10cIDMzZbdSZtQvESa0NvS3GU+jQd7RNuyoB/mC9suWXY6QIDAQABo4HU
-MIHRMB0GA1UdDgQWBBQ5lYtii1zJ1IC6WA+XPxUIQ8yYpzALBgNVHQ8EBAMCAQYwDwYDVR0TAQH/
-BAUwAwEB/zCBkQYDVR0jBIGJMIGGgBQ5lYtii1zJ1IC6WA+XPxUIQ8yYp6FrpGkwZzELMAkGA1UE
-BhMCU0UxFDASBgNVBAoTC0FkZFRydXN0IEFCMR0wGwYDVQQLExRBZGRUcnVzdCBUVFAgTmV0d29y
-azEjMCEGA1UEAxMaQWRkVHJ1c3QgUXVhbGlmaWVkIENBIFJvb3SCAQEwDQYJKoZIhvcNAQEFBQAD
-ggEBABmrder4i2VhlRO6aQTvhsoToMeqT2QbPxj2qC0sVY8FtzDqQmodwCVRLae/DLPt7wh/bDxG
-GuoYQ992zPlmhpwsaPXpF/gxsxjE1kh9I0xowX67ARRvxdlu3rsEQmr49lx95dr6h+sNNVJn0J6X
-dgWTP5XHAeZpVTh/EGGZyeNfpso+gmNIquIISD6q8rKFYqa0p9m9N5xotS1WfbC3P6CxB9bpT9ze
-RXEwMn8bLgn5v1Kh7sKAPgZcLlVAwRv1cEWw3F369nJad9Jjzc9YiQBCYz95OdBEsIJuQRno3eDB
-iFrRHnGTHyQwdOUeqN48Jzd/g66ed8/wMLH/S5noxqE=
------END CERTIFICATE-----
-
-Entrust Root Certification Authority
-====================================
------BEGIN CERTIFICATE-----
-MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMCVVMxFjAUBgNV
-BAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0Lm5ldC9DUFMgaXMgaW5jb3Jw
-b3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMWKGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsG
-A1UEAxMkRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0
-MloXDTI2MTEyNzIwNTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMu
-MTkwNwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSByZWZlcmVu
-Y2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNVBAMTJEVudHJ1c3QgUm9v
-dCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
-ALaVtkNC+sZtKm9I35RMOVcF7sN5EUFoNu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYsz
-A9u3g3s+IIRe7bJWKKf44LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOww
-Cj0Yzfv9KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGIrb68
-j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi94DkZfs0Nw4pgHBN
-rziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOBsDCBrTAOBgNVHQ8BAf8EBAMCAQYw
-DwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAigA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1
-MzQyWjAfBgNVHSMEGDAWgBRokORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DH
-hmak8fdLQ/uEvW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA
-A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9tO1KzKtvn1ISM
-Y/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6ZuaAGAT/3B+XxFNSRuzFVJ7yVTa
-v52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTS
-W3iDVuycNsMm4hH2Z0kdkquM++v/eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0
-tHuu2guQOHXvgR1m0vdXcDazv/wor3ElhVsT/h5/WrQ8
------END CERTIFICATE-----
-
-RSA Security 2048 v3
-====================
------BEGIN CERTIFICATE-----
-MIIDYTCCAkmgAwIBAgIQCgEBAQAAAnwAAAAKAAAAAjANBgkqhkiG9w0BAQUFADA6MRkwFwYDVQQK
-ExBSU0EgU2VjdXJpdHkgSW5jMR0wGwYDVQQLExRSU0EgU2VjdXJpdHkgMjA0OCBWMzAeFw0wMTAy
-MjIyMDM5MjNaFw0yNjAyMjIyMDM5MjNaMDoxGTAXBgNVBAoTEFJTQSBTZWN1cml0eSBJbmMxHTAb
-BgNVBAsTFFJTQSBTZWN1cml0eSAyMDQ4IFYzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
-AQEAt49VcdKA3XtpeafwGFAyPGJn9gqVB93mG/Oe2dJBVGutn3y+Gc37RqtBaB4Y6lXIL5F4iSj7
-Jylg/9+PjDvJSZu1pJTOAeo+tWN7fyb9Gd3AIb2E0S1PRsNO3Ng3OTsor8udGuorryGlwSMiuLgb
-WhOHV4PR8CDn6E8jQrAApX2J6elhc5SYcSa8LWrg903w8bYqODGBDSnhAMFRD0xS+ARaqn1y07iH
-KrtjEAMqs6FPDVpeRrc9DvV07Jmf+T0kgYim3WBU6JU2PcYJk5qjEoAAVZkZR73QpXzDuvsf9/UP
-+Ky5tfQ3mBMY3oVbtwyCO4dvlTlYMNpuAWgXIszACwIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/
-MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQHw1EwpKrpRa41JPr/JCwz0LGdjDAdBgNVHQ4E
-FgQUB8NRMKSq6UWuNST6/yQsM9CxnYwwDQYJKoZIhvcNAQEFBQADggEBAF8+hnZuuDU8TjYcHnmY
-v/3VEhF5Ug7uMYm83X/50cYVIeiKAVQNOvtUudZj1LGqlk2iQk3UUx+LEN5/Zb5gEydxiKRz44Rj
-0aRV4VCT5hsOedBnvEbIvz8XDZXmxpBp3ue0L96VfdASPz0+f00/FGj1EVDVwfSQpQgdMWD/YIwj
-VAqv/qFuxdF6Kmh4zx6CCiC0H63lhbJqaHVOrSU3lIW+vaHU6rcMSzyd6BIA8F+sDeGscGNz9395
-nzIlQnQFgCi/vcEkllgVsRch6YlL2weIZ/QVrXA+L02FO8K32/6YaCOJ4XQP3vTFhGMpG8zLB8kA
-pKnXwiJPZ9d37CAFYd4=
------END CERTIFICATE-----
-
-GeoTrust Global CA
-==================
------BEGIN CERTIFICATE-----
-MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVTMRYwFAYDVQQK
-Ew1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9iYWwgQ0EwHhcNMDIwNTIxMDQw
-MDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5j
-LjEbMBkGA1UEAxMSR2VvVHJ1c3QgR2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
-CgKCAQEA2swYYzD99BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjo
-BbdqfnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDviS2Aelet
-8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU1XupGc1V3sjs0l44U+Vc
-T4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+bw8HHa8sHo9gOeL6NlMTOdReJivbPagU
-vTLrGAMoUgRx5aszPeE4uwc2hGKceeoWMPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTAD
-AQH/MB0GA1UdDgQWBBTAephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVk
-DBF9qn1luMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKInZ57Q
-zxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfStQWVYrmm3ok9Nns4
-d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcFPseKUgzbFbS9bZvlxrFUaKnjaZC2
-mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Unhw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6p
-XE0zX5IJL4hmXXeXxx12E6nV5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvm
-Mw==
------END CERTIFICATE-----
-
-GeoTrust Global CA 2
-====================
------BEGIN CERTIFICATE-----
-MIIDZjCCAk6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBEMQswCQYDVQQGEwJVUzEWMBQGA1UEChMN
-R2VvVHJ1c3QgSW5jLjEdMBsGA1UEAxMUR2VvVHJ1c3QgR2xvYmFsIENBIDIwHhcNMDQwMzA0MDUw
-MDAwWhcNMTkwMzA0MDUwMDAwWjBEMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5j
-LjEdMBsGA1UEAxMUR2VvVHJ1c3QgR2xvYmFsIENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
-ggEKAoIBAQDvPE1APRDfO1MA4Wf+lGAVPoWI8YkNkMgoI5kF6CsgncbzYEbYwbLVjDHZ3CB5JIG/
-NTL8Y2nbsSpr7iFY8gjpeMtvy/wWUsiRxP89c96xPqfCfWbB9X5SJBri1WeR0IIQ13hLTytCOb1k
-LUCgsBDTOEhGiKEMuzozKmKY+wCdE1l/bztyqu6mD4b5BWHqZ38MN5aL5mkWRxHCJ1kDs6ZgwiFA
-Vvqgx306E+PsV8ez1q6diYD3Aecs9pYrEw15LNnA5IZ7S4wMcoKK+xfNAGw6EzywhIdLFnopsk/b
-HdQL82Y3vdj2V7teJHq4PIu5+pIaGoSe2HSPqht/XvT+RSIhAgMBAAGjYzBhMA8GA1UdEwEB/wQF
-MAMBAf8wHQYDVR0OBBYEFHE4NvICMVNHK266ZUapEBVYIAUJMB8GA1UdIwQYMBaAFHE4NvICMVNH
-K266ZUapEBVYIAUJMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQUFAAOCAQEAA/e1K6tdEPx7
-srJerJsOflN4WT5CBP51o62sgU7XAotexC3IUnbHLB/8gTKY0UvGkpMzNTEv/NgdRN3ggX+d6Yvh
-ZJFiCzkIjKx0nVnZellSlxG5FntvRdOW2TF9AjYPnDtuzywNA0ZF66D0f0hExghAzN4bcLUprbqL
-OzRldRtxIR0sFAqwlpW41uryZfspuk/qkZN0abby/+Ea0AzRdoXLiiW9l14sbxWZJue2Kf8i7MkC
-x1YAzUm5s2x7UwQa4qjJqhIFI8LO57sEAszAR6LkxCkvW0VXiVHuPOtSCP8HNR6fNWpHSlaY0VqF
-H4z1Ir+rzoPz4iIprn2DQKi6bA==
------END CERTIFICATE-----
-
-GeoTrust Universal CA
-=====================
------BEGIN CERTIFICATE-----
-MIIFaDCCA1CgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJVUzEWMBQGA1UEChMN
-R2VvVHJ1c3QgSW5jLjEeMBwGA1UEAxMVR2VvVHJ1c3QgVW5pdmVyc2FsIENBMB4XDTA0MDMwNDA1
-MDAwMFoXDTI5MDMwNDA1MDAwMFowRTELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IElu
-Yy4xHjAcBgNVBAMTFUdlb1RydXN0IFVuaXZlcnNhbCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
-ADCCAgoCggIBAKYVVaCjxuAfjJ0hUNfBvitbtaSeodlyWL0AG0y/YckUHUWCq8YdgNY96xCcOq9t
-JPi8cQGeBvV8Xx7BDlXKg5pZMK4ZyzBIle0iN430SppyZj6tlcDgFgDgEB8rMQ7XlFTTQjOgNB0e
-RXbdT8oYN+yFFXoZCPzVx5zw8qkuEKmS5j1YPakWaDwvdSEYfyh3peFhF7em6fgemdtzbvQKoiFs
-7tqqhZJmr/Z6a4LauiIINQ/PQvE1+mrufislzDoR5G2vc7J2Ha3QsnhnGqQ5HFELZ1aD/ThdDc7d
-8Lsrlh/eezJS/R27tQahsiFepdaVaH/wmZ7cRQg+59IJDTWU3YBOU5fXtQlEIGQWFwMCTFMNaN7V
-qnJNk22CDtucvc+081xdVHppCZbW2xHBjXWotM85yM48vCR85mLK4b19p71XZQvk/iXttmkQ3Cga
-Rr0BHdCXteGYO8A3ZNY9lO4L4fUorgtWv3GLIylBjobFS1J72HGrH4oVpjuDWtdYAVHGTEHZf9hB
-Z3KiKN9gg6meyHv8U3NyWfWTehd2Ds735VzZC1U0oqpbtWpU5xPKV+yXbfReBi9Fi1jUIxaS5BZu
-KGNZMN9QAZxjiRqf2xeUgnA3wySemkfWWspOqGmJch+RbNt+nhutxx9z3SxPGWX9f5NAEC7S8O08
-ni4oPmkmM8V7AgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNq7LqqwDLiIJlF0
-XG0D08DYj3rWMB8GA1UdIwQYMBaAFNq7LqqwDLiIJlF0XG0D08DYj3rWMA4GA1UdDwEB/wQEAwIB
-hjANBgkqhkiG9w0BAQUFAAOCAgEAMXjmx7XfuJRAyXHEqDXsRh3ChfMoWIawC/yOsjmPRFWrZIRc
-aanQmjg8+uUfNeVE44B5lGiku8SfPeE0zTBGi1QrlaXv9z+ZhP015s8xxtxqv6fXIwjhmF7DWgh2
-qaavdy+3YL1ERmrvl/9zlcGO6JP7/TG37FcREUWbMPEaiDnBTzynANXH/KttgCJwpQzgXQQpAvvL
-oJHRfNbDflDVnVi+QTjruXU8FdmbyUqDWcDaU/0zuzYYm4UPFd3uLax2k7nZAY1IEKj79TiG8dsK
-xr2EoyNB3tZ3b4XUhRxQ4K5RirqNPnbiucon8l+f725ZDQbYKxek0nxru18UGkiPGkzns0ccjkxF
-KyDuSN/n3QmOGKjaQI2SJhFTYXNd673nxE0pN2HrrDktZy4W1vUAg4WhzH92xH3kt0tm7wNFYGm2
-DFKWkoRepqO1pD4r2czYG0eq8kTaT/kD6PAUyz/zg97QwVTjt+gKN02LIFkDMBmhLMi9ER/frslK
-xfMnZmaGrGiR/9nmUxwPi1xpZQomyB40w11Re9epnAahNt3ViZS82eQtDF4JbAiXfKM9fJP/P6EU
-p8+1Xevb2xzEdt+Iub1FBZUbrvxGakyvSOPOrg/SfuvmbJxPgWp6ZKy7PtXny3YuxadIwVyQD8vI
-P/rmMuGNG2+k5o7Y+SlIis5z/iw=
------END CERTIFICATE-----
-
-GeoTrust Universal CA 2
-=======================
------BEGIN CERTIFICATE-----
-MIIFbDCCA1SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBHMQswCQYDVQQGEwJVUzEWMBQGA1UEChMN
-R2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVyc2FsIENBIDIwHhcNMDQwMzA0
-MDUwMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3Qg
-SW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVyc2FsIENBIDIwggIiMA0GCSqGSIb3DQEBAQUA
-A4ICDwAwggIKAoICAQCzVFLByT7y2dyxUxpZKeexw0Uo5dfR7cXFS6GqdHtXr0om/Nj1XqduGdt0
-DE81WzILAePb63p3NeqqWuDW6KFXlPCQo3RWlEQwAx5cTiuFJnSCegx2oG9NzkEtoBUGFF+3Qs17
-j1hhNNwqCPkuwwGmIkQcTAeC5lvO0Ep8BNMZcyfwqph/Lq9O64ceJHdqXbboW0W63MOhBW9Wjo8Q
-JqVJwy7XQYci4E+GymC16qFjwAGXEHm9ADwSbSsVsaxLse4YuU6W3Nx2/zu+z18DwPw76L5GG//a
-QMJS9/7jOvdqdzXQ2o3rXhhqMcceujwbKNZrVMaqW9eiLBsZzKIC9ptZvTdrhrVtgrrY6slWvKk2
-WP0+GfPtDCapkzj4T8FdIgbQl+rhrcZV4IErKIM6+vR7IVEAvlI4zs1meaj0gVbi0IMJR1FbUGrP
-20gaXT73y/Zl92zxlfgCOzJWgjl6W70viRu/obTo/3+NjN8D8WBOWBFM66M/ECuDmgFz2ZRthAAn
-ZqzwcEAJQpKtT5MNYQlRJNiS1QuUYbKHsu3/mjX/hVTK7URDrBs8FmtISgocQIgfksILAAX/8sgC
-SqSqqcyZlpwvWOB94b67B9xfBHJcMTTD7F8t4D1kkCLm0ey4Lt1ZrtmhN79UNdxzMk+MBB4zsslG
-8dhcyFVQyWi9qLo2CQIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR281Xh+qQ2
-+/CfXGJx7Tz0RzgQKzAfBgNVHSMEGDAWgBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAOBgNVHQ8BAf8E
-BAMCAYYwDQYJKoZIhvcNAQEFBQADggIBAGbBxiPz2eAubl/oz66wsCVNK/g7WJtAJDday6sWSf+z
-dXkzoS9tcBc0kf5nfo/sm+VegqlVHy/c1FEHEv6sFj4sNcZj/NwQ6w2jqtB8zNHQL1EuxBRa3ugZ
-4T7GzKQp5y6EqgYweHZUcyiYWTjgAA1i00J9IZ+uPTqM1fp3DRgrFg5fNuH8KrUwJM/gYwx7WBr+
-mbpCErGR9Hxo4sjoryzqyX6uuyo9DRXcNJW2GHSoag/HtPQTxORb7QrSpJdMKu0vbBKJPfEncKpq
-A1Ihn0CoZ1Dy81of398j9tx4TuaYT1U6U+Pv8vSfx3zYWK8pIpe44L2RLrB27FcRz+8pRPPphXpg
-Y+RdM4kX2TGq2tbzGDVyz4crL2MjhF2EjD9XoIj8mZEoJmmZ1I+XRL6O1UixpCgp8RW04eWe3fiP
-pm8m1wk8OhwRDqZsN/etRIcsKMfYdIKz0G9KV7s1KSegi+ghp4dkNl3M2Basx7InQJJVOCiNUW7d
-FGdTbHFcJoRNdVq2fmBWqU2t+5sel/MN2dKXVHfaPRK34B7vCAas+YWH6aLcr34YEoP9VhdBLtUp
-gn2Z9DH2canPLAEnpQW5qrJITirvn5NSUZU8UnOOVkwXQMAJKOSLakhT2+zNVVXxxvjpoixMptEm
-X36vWkzaH6byHCx+rgIW0lbQL1dTR+iS
------END CERTIFICATE-----
-
-UTN-USER First-Network Applications
-===================================
------BEGIN CERTIFICATE-----
-MIIEZDCCA0ygAwIBAgIQRL4Mi1AAJLQR0zYwS8AzdzANBgkqhkiG9w0BAQUFADCBozELMAkGA1UE
-BhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEeMBwGA1UEChMVVGhl
-IFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xKzAp
-BgNVBAMTIlVUTi1VU0VSRmlyc3QtTmV0d29yayBBcHBsaWNhdGlvbnMwHhcNOTkwNzA5MTg0ODM5
-WhcNMTkwNzA5MTg1NzQ5WjCBozELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5T
-YWx0IExha2UgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho
-dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xKzApBgNVBAMTIlVUTi1VU0VSRmlyc3QtTmV0d29yayBB
-cHBsaWNhdGlvbnMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCz+5Gh5DZVhawGNFug
-mliy+LUPBXeDrjKxdpJo7CNKyXY/45y2N3kDuatpjQclthln5LAbGHNhSuh+zdMvZOOmfAz6F4Cj
-DUeJT1FxL+78P/m4FoCHiZMlIJpDgmkkdihZNaEdwH+DBmQWICzTSaSFtMBhf1EI+GgVkYDLpdXu
-Ozr0hAReYFmnjDRy7rh4xdE7EkpvfmUnuaRVxblvQ6TFHSyZwFKkeEwVs0CYCGtDxgGwenv1axwi
-P8vv/6jQOkt2FZ7S0cYu49tXGzKiuG/ohqY/cKvlcJKrRB5AUPuco2LkbG6gyN7igEL66S/ozjIE
-j3yNtxyjNTwV3Z7DrpelAgMBAAGjgZEwgY4wCwYDVR0PBAQDAgHGMA8GA1UdEwEB/wQFMAMBAf8w
-HQYDVR0OBBYEFPqGydvguul49Uuo1hXf8NPhahQ8ME8GA1UdHwRIMEYwRKBCoECGPmh0dHA6Ly9j
-cmwudXNlcnRydXN0LmNvbS9VVE4tVVNFUkZpcnN0LU5ldHdvcmtBcHBsaWNhdGlvbnMuY3JsMA0G
-CSqGSIb3DQEBBQUAA4IBAQCk8yXM0dSRgyLQzDKrm5ZONJFUICU0YV8qAhXhi6r/fWRRzwr/vH3Y
-IWp4yy9Rb/hCHTO967V7lMPDqaAt39EpHx3+jz+7qEUqf9FuVSTiuwL7MT++6LzsQCv4AdRWOOTK
-RIK1YSAhZ2X28AvnNPilwpyjXEAfhZOVBt5P1CeptqX8Fs1zMT+4ZSfP1FMa8Kxun08FDAOBp4Qp
-xFq9ZFdyrTvPNximmMatBrTcCKME1SmklpoSZ0qMYEWd8SOasACcaLWYUNPvji6SZbFIPiG+FTAq
-DbUMo2s/rn9X9R+WfN9v3YIwLGUbQErNaLly7HF27FSOH4UMAWr6pjisH8SE
------END CERTIFICATE-----
-
-America Online Root Certification Authority 1
-=============================================
------BEGIN CERTIFICATE-----
-MIIDpDCCAoygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEcMBoGA1UEChMT
-QW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBPbmxpbmUgUm9vdCBDZXJ0aWZp
-Y2F0aW9uIEF1dGhvcml0eSAxMB4XDTAyMDUyODA2MDAwMFoXDTM3MTExOTIwNDMwMFowYzELMAkG
-A1UEBhMCVVMxHDAaBgNVBAoTE0FtZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2Eg
-T25saW5lIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMTCCASIwDQYJKoZIhvcNAQEBBQAD
-ggEPADCCAQoCggEBAKgv6KRpBgNHw+kqmP8ZonCaxlCyfqXfaE0bfA+2l2h9LaaLl+lkhsmj76CG
-v2BlnEtUiMJIxUo5vxTjWVXlGbR0yLQFOVwWpeKVBeASrlmLojNoWBym1BW32J/X3HGrfpq/m44z
-DyL9Hy7nBzbvYjnF3cu6JRQj3gzGPTzOggjmZj7aUTsWOqMFf6Dch9Wc/HKpoH145LcxVR5lu9Rh
-sCFg7RAycsWSJR74kEoYeEfffjA3PlAb2xzTa5qGUwew76wGePiEmf4hjUyAtgyC9mZweRrTT6PP
-8c9GsEsPPt2IYriMqQkoO3rHl+Ee5fSfwMCuJKDIodkP1nsmgmkyPacCAwEAAaNjMGEwDwYDVR0T
-AQH/BAUwAwEB/zAdBgNVHQ4EFgQUAK3Zo/Z59m50qX8zPYEX10zPM94wHwYDVR0jBBgwFoAUAK3Z
-o/Z59m50qX8zPYEX10zPM94wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBBQUAA4IBAQB8itEf
-GDeC4Liwo+1WlchiYZwFos3CYiZhzRAW18y0ZTTQEYqtqKkFZu90821fnZmv9ov761KyBZiibyrF
-VL0lvV+uyIbqRizBs73B6UlwGBaXCBOMIOAbLjpHyx7kADCVW/RFo8AasAFOq73AI25jP4BKxQft
-3OJvx8Fi8eNy1gTIdGcL+oiroQHIb/AUr9KZzVGTfu0uOMe9zkZQPXLjeSWdm4grECDdpbgyn43g
-Kd8hdIaC2y+CMMbHNYaz+ZZfRtsMRf3zUMNvxsNIrUam4SdHCh0Om7bCd39j8uB9Gr784N/Xx6ds
-sPmuujz9dLQR6FgNgLzTqIA6me11zEZ7
------END CERTIFICATE-----
-
-America Online Root Certification Authority 2
-=============================================
------BEGIN CERTIFICATE-----
-MIIFpDCCA4ygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEcMBoGA1UEChMT
-QW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBPbmxpbmUgUm9vdCBDZXJ0aWZp
-Y2F0aW9uIEF1dGhvcml0eSAyMB4XDTAyMDUyODA2MDAwMFoXDTM3MDkyOTE0MDgwMFowYzELMAkG
-A1UEBhMCVVMxHDAaBgNVBAoTE0FtZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2Eg
-T25saW5lIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMjCCAiIwDQYJKoZIhvcNAQEBBQAD
-ggIPADCCAgoCggIBAMxBRR3pPU0Q9oyxQcngXssNt79Hc9PwVU3dxgz6sWYFas14tNwC206B89en
-fHG8dWOgXeMHDEjsJcQDIPT/DjsS/5uN4cbVG7RtIuOx238hZK+GvFciKtZHgVdEglZTvYYUAQv8
-f3SkWq7xuhG1m1hagLQ3eAkzfDJHA1zEpYNI9FdWboE2JxhP7JsowtS013wMPgwr38oE18aO6lhO
-qKSlGBxsRZijQdEt0sdtjRnxrXm3gT+9BoInLRBYBbV4Bbkv2wxrkJB+FFk4u5QkE+XRnRTf04JN
-RvCAOVIyD+OEsnpD8l7eXz8d3eOyG6ChKiMDbi4BFYdcpnV1x5dhvt6G3NRI270qv0pV2uh9UPu0
-gBe4lL8BPeraunzgWGcXuVjgiIZGZ2ydEEdYMtA1fHkqkKJaEBEjNa0vzORKW6fIJ/KD3l67Xnfn
-6KVuY8INXWHQjNJsWiEOyiijzirplcdIz5ZvHZIlyMbGwcEMBawmxNJ10uEqZ8A9W6Wa6897Gqid
-FEXlD6CaZd4vKL3Ob5Rmg0gp2OpljK+T2WSfVVcmv2/LNzGZo2C7HK2JNDJiuEMhBnIMoVxtRsX6
-Kc8w3onccVvdtjc+31D1uAclJuW8tf48ArO3+L5DwYcRlJ4jbBeKuIonDFRH8KmzwICMoCfrHRnj
-B453cMor9H124HhnAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFE1FwWg4u3Op
-aaEg5+31IqEjFNeeMB8GA1UdIwQYMBaAFE1FwWg4u3OpaaEg5+31IqEjFNeeMA4GA1UdDwEB/wQE
-AwIBhjANBgkqhkiG9w0BAQUFAAOCAgEAZ2sGuV9FOypLM7PmG2tZTiLMubekJcmnxPBUlgtk87FY
-T15R/LKXeydlwuXK5w0MJXti4/qftIe3RUavg6WXSIylvfEWK5t2LHo1YGwRgJfMqZJS5ivmae2p
-+DYtLHe/YUjRYwu5W1LtGLBDQiKmsXeu3mnFzcccobGlHBD7GL4acN3Bkku+KVqdPzW+5X1R+FXg
-JXUjhx5c3LqdsKyzadsXg8n33gy8CNyRnqjQ1xU3c6U1uPx+xURABsPr+CKAXEfOAuMRn0T//Zoy
-zH1kUQ7rVyZ2OuMeIjzCpjbdGe+n/BLzJsBZMYVMnNjP36TMzCmT/5RtdlwTCJfy7aULTd3oyWgO
-ZtMADjMSW7yV5TKQqLPGbIOtd+6Lfn6xqavT4fG2wLHqiMDn05DpKJKUe2h7lyoKZy2FAjgQ5ANh
-1NolNscIWC2hp1GvMApJ9aZphwctREZ2jirlmjvXGKL8nDgQzMY70rUXOm/9riW99XJZZLF0Kjhf
-GEzfz3EEWjbUvy+ZnOjZurGV5gJLIaFb1cFPj65pbVPbAZO1XB4Y3WRayhgoPmMEEf0cjQAPuDff
-Z4qdZqkCapH/E8ovXYO8h5Ns3CRRFgQlZvqz2cK6Kb6aSDiCmfS/O0oxGfm/jiEzFMpPVF/7zvuP
-cX/9XhmgD0uRuMRUvAawRY8mkaKO/qk=
------END CERTIFICATE-----
-
-Visa eCommerce Root
-===================
------BEGIN CERTIFICATE-----
-MIIDojCCAoqgAwIBAgIQE4Y1TR0/BvLB+WUF1ZAcYjANBgkqhkiG9w0BAQUFADBrMQswCQYDVQQG
-EwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMmVmlzYSBJbnRlcm5hdGlvbmFsIFNlcnZpY2Ug
-QXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNvbW1lcmNlIFJvb3QwHhcNMDIwNjI2MDIxODM2
-WhcNMjIwNjI0MDAxNjEyWjBrMQswCQYDVQQGEwJVUzENMAsGA1UEChMEVklTQTEvMC0GA1UECxMm
-VmlzYSBJbnRlcm5hdGlvbmFsIFNlcnZpY2UgQXNzb2NpYXRpb24xHDAaBgNVBAMTE1Zpc2EgZUNv
-bW1lcmNlIFJvb3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvV95WHm6h2mCxlCfL
-F9sHP4CFT8icttD0b0/Pmdjh28JIXDqsOTPHH2qLJj0rNfVIsZHBAk4ElpF7sDPwsRROEW+1QK8b
-RaVK7362rPKgH1g/EkZgPI2h4H3PVz4zHvtH8aoVlwdVZqW1LS7YgFmypw23RuwhY/81q6UCzyr0
-TP579ZRdhE2o8mCP2w4lPJ9zcc+U30rq299yOIzzlr3xF7zSujtFWsan9sYXiwGd/BmoKoMWuDpI
-/k4+oKsGGelT84ATB+0tvz8KPFUgOSwsAGl0lUq8ILKpeeUYiZGo3BxN77t+Nwtd/jmliFKMAGzs
-GHxBvfaLdXe6YJ2E5/4tAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEG
-MB0GA1UdDgQWBBQVOIMPPyw/cDMezUb+B4wg4NfDtzANBgkqhkiG9w0BAQUFAAOCAQEAX/FBfXxc
-CLkr4NWSR/pnXKUTwwMhmytMiUbPWU3J/qVAtmPN3XEolWcRzCSs00Rsca4BIGsDoo8Ytyk6feUW
-YFN4PMCvFYP3j1IzJL1kk5fui/fbGKhtcbP3LBfQdCVp9/5rPJS+TUtBjE7ic9DjkCJzQ83z7+pz
-zkWKsKZJ/0x9nXGIxHYdkFsd7v3M9+79YKWxehZx0RbQfBI8bGmX265fOZpwLwU8GUYEmSA20GBu
-YQa7FkKMcPcw++DbZqMAAb3mLNqRX6BGi01qnD093QVG/na/oAo85ADmJ7f/hC3euiInlhBx6yLt
-398znM/jra6O1I7mT1GvFpLgXPYHDw==
------END CERTIFICATE-----
-
-Certum Root CA
-==============
------BEGIN CERTIFICATE-----
-MIIDDDCCAfSgAwIBAgIDAQAgMA0GCSqGSIb3DQEBBQUAMD4xCzAJBgNVBAYTAlBMMRswGQYDVQQK
-ExJVbml6ZXRvIFNwLiB6IG8uby4xEjAQBgNVBAMTCUNlcnR1bSBDQTAeFw0wMjA2MTExMDQ2Mzla
-Fw0yNzA2MTExMDQ2MzlaMD4xCzAJBgNVBAYTAlBMMRswGQYDVQQKExJVbml6ZXRvIFNwLiB6IG8u
-by4xEjAQBgNVBAMTCUNlcnR1bSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM6x
-wS7TT3zNJc4YPk/EjG+AanPIW1H4m9LcuwBcsaD8dQPugfCI7iNS6eYVM42sLQnFdvkrOYCJ5JdL
-kKWoePhzQ3ukYbDYWMzhbGZ+nPMJXlVjhNWo7/OxLjBos8Q82KxujZlakE403Daaj4GIULdtlkIJ
-89eVgw1BS7Bqa/j8D35in2fE7SZfECYPCE/wpFcozo+47UX2bu4lXapuOb7kky/ZR6By6/qmW6/K
-Uz/iDsaWVhFu9+lmqSbYf5VT7QqFiLpPKaVCjF62/IUgAKpoC6EahQGcxEZjgoi2IrHu/qpGWX7P
-NSzVttpd90gzFFS269lvzs2I1qsb2pY7HVkCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkq
-hkiG9w0BAQUFAAOCAQEAuI3O7+cUus/usESSbLQ5PqKEbq24IXfS1HeCh+YgQYHu4vgRt2PRFze+
-GXYkHAQaTOs9qmdvLdTN/mUxcMUbpgIKumB7bVjCmkn+YzILa+M6wKyrO7Do0wlRjBCDxjTgxSvg
-GrZgFCdsMneMvLJymM/NzD+5yCRCFNZX/OYmQ6kd5YCQzgNUKD73P9P4Te1qCjqTE5s7FCMTY5w/
-0YcneeVMUeMBrYVdGjux1XMQpNPyvG5k9VpWkKjHDkx0Dy5xO/fIR/RpbxXyEV6DHpx8Uq79AtoS
-qFlnGNu8cN2bsWntgM6JQEhqDjXKKWYVIZQs6GAqm4VKQPNriiTsBhYscw==
------END CERTIFICATE-----
-
-Comodo AAA Services root
-========================
------BEGIN CERTIFICATE-----
-MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEbMBkGA1UECAwS
-R3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0Eg
-TGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAw
-MFoXDTI4MTIzMTIzNTk1OVowezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hl
-c3RlcjEQMA4GA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV
-BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
-ggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQuaBtDFcCLNSS1UY8y2bmhG
-C1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe3M/vg4aijJRPn2jymJBGhCfHdr/jzDUs
-i14HZGWCwEiwqJH5YZ92IFCokcdmtet4YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszW
-Y19zjNoFmag4qMsXeDZRrOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjH
-Ypy+g8cmez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQUoBEK
-Iz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wewYDVR0f
-BHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNl
-cy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29tb2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2Vz
-LmNybDANBgkqhkiG9w0BAQUFAAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm
-7l3sAg9g1o1QGE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz
-Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2G9w84FoVxp7Z
-8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsil2D4kF501KKaU73yqWjgom7C
-12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg==
------END CERTIFICATE-----
-
-Comodo Secure Services root
-===========================
------BEGIN CERTIFICATE-----
-MIIEPzCCAyegAwIBAgIBATANBgkqhkiG9w0BAQUFADB+MQswCQYDVQQGEwJHQjEbMBkGA1UECAwS
-R3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0Eg
-TGltaXRlZDEkMCIGA1UEAwwbU2VjdXJlIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAw
-MDAwMFoXDTI4MTIzMTIzNTk1OVowfjELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFu
-Y2hlc3RlcjEQMA4GA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxJDAi
-BgNVBAMMG1NlY3VyZSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP
-ADCCAQoCggEBAMBxM4KK0HDrc4eCQNUd5MvJDkKQ+d40uaG6EfQlhfPMcm3ye5drswfxdySRXyWP
-9nQ95IDC+DwN879A6vfIUtFyb+/Iq0G4bi4XKpVpDM3SHpR7LZQdqnXXs5jLrLxkU0C8j6ysNstc
-rbvd4JQX7NFc0L/vpZXJkMWwrPsbQ996CF23uPJAGysnnlDOXmWCiIxe004MeuoIkbY2qitC++rC
-oznl2yY4rYsK7hljxxwk3wN42ubqwUcaCwtGCd0C/N7Lh1/XMGNooa7cMqG6vv5Eq2i2pRcV/b3V
-p6ea5EQz6YiO/O1R65NxTq0B50SOqy3LqP4BSUjwwN3HaNiS/j0CAwEAAaOBxzCBxDAdBgNVHQ4E
-FgQUPNiTiMLAggnMAZkGkyDpnnAJY08wDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8w
-gYEGA1UdHwR6MHgwO6A5oDeGNWh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL1NlY3VyZUNlcnRpZmlj
-YXRlU2VydmljZXMuY3JsMDmgN6A1hjNodHRwOi8vY3JsLmNvbW9kby5uZXQvU2VjdXJlQ2VydGlm
-aWNhdGVTZXJ2aWNlcy5jcmwwDQYJKoZIhvcNAQEFBQADggEBAIcBbSMdflsXfcFhMs+P5/OKlFlm
-4J4oqF7Tt/Q05qo5spcWxYJvMqTpjOev/e/C6LlLqqP05tqNZSH7uoDrJiiFGv45jN5bBAS0VPmj
-Z55B+glSzAVIqMk/IQQezkhr/IXownuvf7fM+F86/TXGDe+X3EyrEeFryzHRbPtIgKvcnDe4IRRL
-DXE97IMzbtFuMhbsmMcWi1mmNKsFVy2T96oTy9IT4rcuO81rUBcJaD61JlfutuC23bkpgHl9j6Pw
-pCikFcSF9CfUa7/lXORlAnZUtOM3ZiTTGWHIUhDlizeauan5Hb/qmZJhlv8BzaFfDbxxvA6sCx1H
-RR3B7Hzs/Sk=
------END CERTIFICATE-----
-
-Comodo Trusted Services root
-============================
------BEGIN CERTIFICATE-----
-MIIEQzCCAyugAwIBAgIBATANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJHQjEbMBkGA1UECAwS
-R3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0Eg
-TGltaXRlZDElMCMGA1UEAwwcVHJ1c3RlZCBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0wNDAxMDEw
-MDAwMDBaFw0yODEyMzEyMzU5NTlaMH8xCzAJBgNVBAYTAkdCMRswGQYDVQQIDBJHcmVhdGVyIE1h
-bmNoZXN0ZXIxEDAOBgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoMEUNvbW9kbyBDQSBMaW1pdGVkMSUw
-IwYDVQQDDBxUcnVzdGVkIENlcnRpZmljYXRlIFNlcnZpY2VzMIIBIjANBgkqhkiG9w0BAQEFAAOC
-AQ8AMIIBCgKCAQEA33FvNlhTWvI2VFeAxHQIIO0Yfyod5jWaHiWsnOWWfnJSoBVC21ndZHoa0Lh7
-3TkVvFVIxO06AOoxEbrycXQaZ7jPM8yoMa+j49d/vzMtTGo87IvDktJTdyR0nAducPy9C1t2ul/y
-/9c3S0pgePfw+spwtOpZqqPOSC+pw7ILfhdyFgymBwwbOM/JYrc/oJOlh0Hyt3BAd9i+FHzjqMB6
-juljatEPmsbS9Is6FARW1O24zG71++IsWL1/T2sr92AkWCTOJu80kTrV44HQsvAEAtdbtz6SrGsS
-ivnkBbA7kUlcsutT6vifR4buv5XAwAaf0lteERv0xwQ1KdJVXOTt6wIDAQABo4HJMIHGMB0GA1Ud
-DgQWBBTFe1i97doladL3WRaoszLAeydb9DAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB
-/zCBgwYDVR0fBHwwejA8oDqgOIY2aHR0cDovL2NybC5jb21vZG9jYS5jb20vVHJ1c3RlZENlcnRp
-ZmljYXRlU2VydmljZXMuY3JsMDqgOKA2hjRodHRwOi8vY3JsLmNvbW9kby5uZXQvVHJ1c3RlZENl
-cnRpZmljYXRlU2VydmljZXMuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQDIk4E7ibSvuIQSTI3S8Ntw
-uleGFTQQuS9/HrCoiWChisJ3DFBKmwCL2Iv0QeLQg4pKHBQGsKNoBXAxMKdTmw7pSqBYaWcOrp32
-pSxBvzwGa+RZzG0Q8ZZvH9/0BAKkn0U+yNj6NkZEUD+Cl5EfKNsYEYwq5GWDVxISjBc/lDb+XbDA
-BHcTuPQV1T84zJQ6VdCsmPW6AF/ghhmBeC8owH7TzEIK9a5QoNE+xqFx7D+gIIxmOom0jtTYsU0l
-R+4viMi14QVFwL4Ucd56/Y57fU0IlqUSc/AtyjcndBInTMu2l+nZrghtWjlA3QVHdWpaIbOjGM9O
-9y5Xt5hwXsjEeLBi
------END CERTIFICATE-----
-
-QuoVadis Root CA
-================
------BEGIN CERTIFICATE-----
-MIIF0DCCBLigAwIBAgIEOrZQizANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJCTTEZMBcGA1UE
-ChMQUXVvVmFkaXMgTGltaXRlZDElMCMGA1UECxMcUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0
-eTEuMCwGA1UEAxMlUXVvVmFkaXMgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMTAz
-MTkxODMzMzNaFw0yMTAzMTcxODMzMzNaMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRp
-cyBMaW1pdGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYDVQQD
-EyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEF
-AAOCAQ8AMIIBCgKCAQEAv2G1lVO6V/z68mcLOhrfEYBklbTRvM16z/Ypli4kVEAkOPcahdxYTMuk
-J0KX0J+DisPkBgNbAKVRHnAEdOLB1Dqr1607BxgFjv2DrOpm2RgbaIr1VxqYuvXtdj182d6UajtL
-F8HVj71lODqV0D1VNk7feVcxKh7YWWVJWCCYfqtffp/p1k3sg3Spx2zY7ilKhSoGFPlU5tPaZQeL
-YzcS19Dsw3sgQUSj7cugF+FxZc4dZjH3dgEZyH0DWLaVSR2mEiboxgx24ONmy+pdpibu5cxfvWen
-AScOospUxbF6lR1xHkopigPcakXBpBlebzbNw6Kwt/5cOOJSvPhEQ+aQuwIDAQABo4ICUjCCAk4w
-PQYIKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwczovL29jc3AucXVvdmFkaXNvZmZzaG9y
-ZS5jb20wDwYDVR0TAQH/BAUwAwEB/zCCARoGA1UdIASCAREwggENMIIBCQYJKwYBBAG+WAABMIH7
-MIHUBggrBgEFBQcCAjCBxxqBxFJlbGlhbmNlIG9uIHRoZSBRdW9WYWRpcyBSb290IENlcnRpZmlj
-YXRlIGJ5IGFueSBwYXJ0eSBhc3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJs
-ZSBzdGFuZGFyZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRpb24gcHJh
-Y3RpY2VzLCBhbmQgdGhlIFF1b1ZhZGlzIENlcnRpZmljYXRlIFBvbGljeS4wIgYIKwYBBQUHAgEW
-Fmh0dHA6Ly93d3cucXVvdmFkaXMuYm0wHQYDVR0OBBYEFItLbe3TKbkGGew5Oanwl4Rqy+/fMIGu
-BgNVHSMEgaYwgaOAFItLbe3TKbkGGew5Oanwl4Rqy+/foYGEpIGBMH8xCzAJBgNVBAYTAkJNMRkw
-FwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0
-aG9yaXR5MS4wLAYDVQQDEyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggQ6
-tlCLMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAitQUtf70mpKnGdSkfnIYj9lo
-fFIk3WdvOXrEql494liwTXCYhGHoG+NpGA7O+0dQoE7/8CQfvbLO9Sf87C9TqnN7Az10buYWnuul
-LsS/VidQK2K6vkscPFVcQR0kvoIgR13VRH56FmjffU1RcHhXHTMe/QKZnAzNCgVPx7uOpHX6Sm2x
-gI4JVrmcGmD+XcHXetwReNDWXcG31a0ymQM6isxUJTkxgXsTIlG6Rmyhu576BGxJJnSP0nPrzDCi
-5upZIof4l/UO/erMkqQWxFIY6iHOsfHmhIHluqmGKPJDWl0Snawe2ajlCmqnf6CHKc/yiU3U7MXi
-5nrQNiOKSnQ2+Q==
------END CERTIFICATE-----
-
-QuoVadis Root CA 2
-==================
------BEGIN CERTIFICATE-----
-MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoT
-EFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMjAeFw0wNjExMjQx
-ODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM
-aW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4IC
-DwAwggIKAoICAQCaGMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6
-XJxgFyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55JWpzmM+Yk
-lvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bBrrcCaoF6qUWD4gXmuVbB
-lDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp+ARz8un+XJiM9XOva7R+zdRcAitMOeGy
-lZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt
-66/3FsvbzSUr5R/7mp/iUcw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1Jdxn
-wQ5hYIizPtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og/zOh
-D7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UHoycR7hYQe7xFSkyy
-BNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuIyV77zGHcizN300QyNQliBJIWENie
-J0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1Ud
-DgQWBBQahGK8SEwzJQTU7tD2A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGU
-a6FJpEcwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT
-ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2fBluornFdLwUv
-Z+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzng/iN/Ae42l9NLmeyhP3ZRPx3
-UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2BlfF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodm
-VjB3pjd4M1IQWK4/YY7yarHvGH5KWWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK
-+JDSV6IZUaUtl0HaB0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrW
-IozchLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPRTUIZ3Ph1
-WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWDmbA4CD/pXvk1B+TJYm5X
-f6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0ZohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II
-4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8
-VCLAAVBpQ570su9t+Oza8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u
------END CERTIFICATE-----
-
-QuoVadis Root CA 3
-==================
------BEGIN CERTIFICATE-----
-MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoT
-EFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMzAeFw0wNjExMjQx
-OTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM
-aW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4IC
-DwAwggIKAoICAQDMV0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNgg
-DhoB4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUrH556VOij
-KTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd8lyyBTNvijbO0BNO/79K
-DDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9CabwvvWhDFlaJKjdhkf2mrk7AyxRllDdLkgbv
-BNDInIjbC3uBr7E9KsRlOni27tyAsdLTmZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwp
-p5ijJUMv7/FfJuGITfhebtfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8
-nT8KKdjcT5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDtWAEX
-MJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZc6tsgLjoC2SToJyM
-Gf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A4iLItLRkT9a6fUg+qGkM17uGcclz
-uD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYDVR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHT
-BgkrBgEEAb5YAAMwgcUwgZMGCCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmlj
-YXRlIGNvbnN0aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0
-aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVudC4wLQYIKwYB
-BQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2NwczALBgNVHQ8EBAMCAQYwHQYD
-VR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4GA1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4
-ywLQoUmkRzBFMQswCQYDVQQGEwJCTTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UE
-AxMSUXVvVmFkaXMgUm9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZV
-qyM07ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSemd1o417+s
-hvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd+LJ2w/w4E6oM3kJpK27z
-POuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2
-Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadNt54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp
-8kokUvd0/bpO5qgdAm6xDYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBC
-bjPsMZ57k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6szHXu
-g/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0jWy10QJLZYxkNc91p
-vGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeTmJlglFwjz1onl14LBQaTNx47aTbr
-qZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK4SVhM7JZG+Ju1zdXtg2pEto=
------END CERTIFICATE-----
-
-Security Communication Root CA
-==============================
------BEGIN CERTIFICATE-----
-MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMP
-U0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEw
-HhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMP
-U0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEw
-ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw
-8yl89f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJDKaVv0uM
-DPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9Ms+k2Y7CI9eNqPPYJayX
-5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/NQV3Is00qVUarH9oe4kA92819uZKAnDfd
-DJZkndwi92SL32HeFZRSFaB9UslLqCHJxrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2
-JChzAgMBAAGjPzA9MB0GA1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYw
-DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vGkl3g
-0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfrUj94nK9NrvjVT8+a
-mCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5Bw+SUEmK3TGXX8npN6o7WWWXlDLJ
-s58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJUJRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ
-6rBK+1YWc26sTfcioU+tHXotRSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAi
-FL39vmwLAw==
------END CERTIFICATE-----
-
-Sonera Class 1 Root CA
-======================
------BEGIN CERTIFICATE-----
-MIIDIDCCAgigAwIBAgIBJDANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEPMA0GA1UEChMG
-U29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MxIENBMB4XDTAxMDQwNjEwNDkxM1oXDTIxMDQw
-NjEwNDkxM1owOTELMAkGA1UEBhMCRkkxDzANBgNVBAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJh
-IENsYXNzMSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALWJHytPZwp5/8Ue+H88
-7dF+2rDNbS82rDTG29lkFwhjMDMiikzujrsPDUJVyZ0upe/3p4zDq7mXy47vPxVnqIJyY1MPQYx9
-EJUkoVqlBvqSV536pQHydekfvFYmUk54GWVYVQNYwBSujHxVX3BbdyMGNpfzJLWaRpXk3w0LBUXl
-0fIdgrvGE+D+qnr9aTCU89JFhfzyMlsy3uhsXR/LpCJ0sICOXZT3BgBLqdReLjVQCfOAl/QMF645
-2F/NM8EcyonCIvdFEu1eEpOdY6uCLrnrQkFEy0oaAIINnvmLVz5MxxftLItyM19yejhW1ebZrgUa
-HXVFsculJRwSVzb9IjcCAwEAAaMzMDEwDwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQIR+IMi/ZT
-iFIwCwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQCLGrLJXWG04bkruVPRsoWdd44W7hE9
-28Jj2VuXZfsSZ9gqXLar5V7DtxYvyOirHYr9qxp81V9jz9yw3Xe5qObSIjiHBxTZ/75Wtf0HDjxV
-yhbMp6Z3N/vbXB9OWQaHowND9Rart4S9Tu+fMTfwRvFAttEMpWT4Y14h21VOTzF2nBBhjrZTOqMR
-vq9tfB69ri3iDGnHhVNoomG6xT60eVR4ngrHAr5i0RGCS2UvkVrCqIexVmiUefkl98HVrhq4uz2P
-qYo4Ffdz0Fpg0YCw8NzVUM1O7pJIae2yIx4wzMiUyLb1O4Z/P6Yun/Y+LLWSlj7fLJOK/4GMDw9Z
-IRlXvVWa
------END CERTIFICATE-----
-
-Sonera Class 2 Root CA
-======================
------BEGIN CERTIFICATE-----
-MIIDIDCCAgigAwIBAgIBHTANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEPMA0GA1UEChMG
-U29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MyIENBMB4XDTAxMDQwNjA3Mjk0MFoXDTIxMDQw
-NjA3Mjk0MFowOTELMAkGA1UEBhMCRkkxDzANBgNVBAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJh
-IENsYXNzMiBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJAXSjWdyvANlsdE+hY3
-/Ei9vX+ALTU74W+oZ6m/AxxNjG8yR9VBaKQTBME1DJqEQ/xcHf+Js+gXGM2RX/uJ4+q/Tl18GybT
-dXnt5oTjV+WtKcT0OijnpXuENmmz/V52vaMtmdOQTiMofRhj8VQ7Jp12W5dCsv+u8E7s3TmVToMG
-f+dJQMjFAbJUWmYdPfz56TwKnoG4cPABi+QjVHzIrviQHgCWctRUz2EjvOr7nQKV0ba5cTppCD8P
-tOFCx4j1P5iop7oc4HFx71hXgVB6XGt0Rg6DA5jDjqhu8nYybieDwnPz3BjotJPqdURrBGAgcVeH
-nfO+oJAjPYok4doh28MCAwEAAaMzMDEwDwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQISqCqWITT
-XjwwCwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQBazof5FnIVV0sd2ZvnoiYw7JNn39Yt
-0jSv9zilzqsWuasvfDXLrNAPtEwr/IDva4yRXzZ299uzGxnq9LIR/WFxRL8oszodv7ND6J+/3DEI
-cbCdjdY0RzKQxmUk96BKfARzjzlvF4xytb1LyHr4e4PDKE6cCepnP7JnBBvDFNr450kkkdAdavph
-Oe9r5yF1BgfYErQhIHBCcYHaPJo2vqZbDWpsmh+Re/n570K6Tk6ezAyNlNzZRZxe7EJQY670XcSx
-EtzKO6gunRRaBXW37Ndj4ro1tgQIkejanZz2ZrUYrAqmVCY0M9IbwdR/GjqOC6oybtv8TyWf2TLH
-llpwrN9M
------END CERTIFICATE-----
-
-Staat der Nederlanden Root CA
-=============================
------BEGIN CERTIFICATE-----
-MIIDujCCAqKgAwIBAgIEAJiWijANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQGEwJOTDEeMBwGA1UE
-ChMVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSYwJAYDVQQDEx1TdGFhdCBkZXIgTmVkZXJsYW5kZW4g
-Um9vdCBDQTAeFw0wMjEyMTcwOTIzNDlaFw0xNTEyMTYwOTE1MzhaMFUxCzAJBgNVBAYTAk5MMR4w
-HAYDVQQKExVTdGFhdCBkZXIgTmVkZXJsYW5kZW4xJjAkBgNVBAMTHVN0YWF0IGRlciBOZWRlcmxh
-bmRlbiBSb290IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmNK1URF6gaYUmHFt
-vsznExvWJw56s2oYHLZhWtVhCb/ekBPHZ+7d89rFDBKeNVU+LCeIQGv33N0iYfXCxw719tV2U02P
-jLwYdjeFnejKScfST5gTCaI+Ioicf9byEGW07l8Y1Rfj+MX94p2i71MOhXeiD+EwR+4A5zN9RGca
-C1Hoi6CeUJhoNFIfLm0B8mBF8jHrqTFoKbt6QZ7GGX+UtFE5A3+y3qcym7RHjm+0Sq7lr7HcsBth
-vJly3uSJt3omXdozSVtSnA71iq3DuD3oBmrC1SoLbHuEvVYFy4ZlkuxEK7COudxwC0barbxjiDn6
-22r+I/q85Ej0ZytqERAhSQIDAQABo4GRMIGOMAwGA1UdEwQFMAMBAf8wTwYDVR0gBEgwRjBEBgRV
-HSAAMDwwOgYIKwYBBQUHAgEWLmh0dHA6Ly93d3cucGtpb3ZlcmhlaWQubmwvcG9saWNpZXMvcm9v
-dC1wb2xpY3kwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSofeu8Y6R0E3QA7Jbg0zTBLL9s+DAN
-BgkqhkiG9w0BAQUFAAOCAQEABYSHVXQ2YcG70dTGFagTtJ+k/rvuFbQvBgwp8qiSpGEN/KtcCFtR
-EytNwiphyPgJWPwtArI5fZlmgb9uXJVFIGzmeafR2Bwp/MIgJ1HI8XxdNGdphREwxgDS1/PTfLbw
-MVcoEoJz6TMvplW0C5GUR5z6u3pCMuiufi3IvKwUv9kP2Vv8wfl6leF9fpb8cbDCTMjfRTTJzg3y
-nGQI0DvDKcWy7ZAEwbEpkcUwb8GpcjPM/l0WFywRaed+/sWDCN+83CI6LiBpIzlWYGeQiy52OfsR
-iJf2fL1LuCAWZwWN4jvBcj+UlTfHXbme2JOhF4//DGYVwSR8MnwDHTuhWEUykw==
------END CERTIFICATE-----
-
-TDC Internet Root CA
-====================
------BEGIN CERTIFICATE-----
-MIIEKzCCAxOgAwIBAgIEOsylTDANBgkqhkiG9w0BAQUFADBDMQswCQYDVQQGEwJESzEVMBMGA1UE
-ChMMVERDIEludGVybmV0MR0wGwYDVQQLExRUREMgSW50ZXJuZXQgUm9vdCBDQTAeFw0wMTA0MDUx
-NjMzMTdaFw0yMTA0MDUxNzAzMTdaMEMxCzAJBgNVBAYTAkRLMRUwEwYDVQQKEwxUREMgSW50ZXJu
-ZXQxHTAbBgNVBAsTFFREQyBJbnRlcm5ldCBSb290IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
-MIIBCgKCAQEAxLhAvJHVYx/XmaCLDEAedLdInUaMArLgJF/wGROnN4NrXceO+YQwzho7+vvOi20j
-xsNuZp+Jpd/gQlBn+h9sHvTQBda/ytZO5GhgbEaqHF1j4QeGDmUApy6mcca8uYGoOn0a0vnRrEvL
-znWv3Hv6gXPU/Lq9QYjUdLP5Xjg6PEOo0pVOd20TDJ2PeAG3WiAfAzc14izbSysseLlJ28TQx5yc
-5IogCSEWVmb/Bexb4/DPqyQkXsN/cHoSxNK1EKC2IeGNeGlVRGn1ypYcNIUXJXfi9i8nmHj9eQY6
-otZaQ8H/7AQ77hPv01ha/5Lr7K7a8jcDR0G2l8ktCkEiu7vmpwIDAQABo4IBJTCCASEwEQYJYIZI
-AYb4QgEBBAQDAgAHMGUGA1UdHwReMFwwWqBYoFakVDBSMQswCQYDVQQGEwJESzEVMBMGA1UEChMM
-VERDIEludGVybmV0MR0wGwYDVQQLExRUREMgSW50ZXJuZXQgUm9vdCBDQTENMAsGA1UEAxMEQ1JM
-MTArBgNVHRAEJDAigA8yMDAxMDQwNTE2MzMxN1qBDzIwMjEwNDA1MTcwMzE3WjALBgNVHQ8EBAMC
-AQYwHwYDVR0jBBgwFoAUbGQBx/2FbazI2p5QCIUItTxWqFAwHQYDVR0OBBYEFGxkAcf9hW2syNqe
-UAiFCLU8VqhQMAwGA1UdEwQFMAMBAf8wHQYJKoZIhvZ9B0EABBAwDhsIVjUuMDo0LjADAgSQMA0G
-CSqGSIb3DQEBBQUAA4IBAQBOQ8zR3R0QGwZ/t6T609lN+yOfI1Rb5osvBCiLtSdtiaHsmGnc540m
-gwV5dOy0uaOXwTUA/RXaOYE6lTGQ3pfphqiZdwzlWqCE/xIWrG64jcN7ksKsLtB9KOy282A4aW8+
-2ARVPp7MVdK6/rtHBNcK2RYKNCn1WBPVT8+PVkuzHu7TmHnaCB4Mb7j4Fifvwm899qNLPg7kbWzb
-O0ESm70NRyN/PErQr8Cv9u8btRXE64PECV90i9kR+8JWsTz4cMo0jUNAE4z9mQNUecYu6oah9jrU
-Cbz0vGbMPVjQV0kK7iXiQe4T+Zs4NNEA9X7nlB38aQNiuJkFBT1reBK9sG9l
------END CERTIFICATE-----
-
-TDC OCES Root CA
-================
------BEGIN CERTIFICATE-----
-MIIFGTCCBAGgAwIBAgIEPki9xDANBgkqhkiG9w0BAQUFADAxMQswCQYDVQQGEwJESzEMMAoGA1UE
-ChMDVERDMRQwEgYDVQQDEwtUREMgT0NFUyBDQTAeFw0wMzAyMTEwODM5MzBaFw0zNzAyMTEwOTA5
-MzBaMDExCzAJBgNVBAYTAkRLMQwwCgYDVQQKEwNUREMxFDASBgNVBAMTC1REQyBPQ0VTIENBMIIB
-IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArGL2YSCyz8DGhdfjeebM7fI5kqSXLmSjhFuH
-nEz9pPPEXyG9VhDr2y5h7JNp46PMvZnDBfwGuMo2HP6QjklMxFaaL1a8z3sM8W9Hpg1DTeLpHTk0
-zY0s2RKY+ePhwUp8hjjEqcRhiNJerxomTdXkoCJHhNlktxmW/OwZ5LKXJk5KTMuPJItUGBxIYXvV
-iGjaXbXqzRowwYCDdlCqT9HU3Tjw7xb04QxQBr/q+3pJoSgrHPb8FTKjdGqPqcNiKXEx5TukYBde
-dObaE+3pHx8b0bJoc8YQNHVGEBDjkAB2QMuLt0MJIf+rTpPGWOmlgtt3xDqZsXKVSQTwtyv6e1mO
-3QIDAQABo4ICNzCCAjMwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwgewGA1UdIASB
-5DCB4TCB3gYIKoFQgSkBAQEwgdEwLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuY2VydGlmaWthdC5k
-ay9yZXBvc2l0b3J5MIGdBggrBgEFBQcCAjCBkDAKFgNUREMwAwIBARqBgUNlcnRpZmlrYXRlciBm
-cmEgZGVubmUgQ0EgdWRzdGVkZXMgdW5kZXIgT0lEIDEuMi4yMDguMTY5LjEuMS4xLiBDZXJ0aWZp
-Y2F0ZXMgZnJvbSB0aGlzIENBIGFyZSBpc3N1ZWQgdW5kZXIgT0lEIDEuMi4yMDguMTY5LjEuMS4x
-LjARBglghkgBhvhCAQEEBAMCAAcwgYEGA1UdHwR6MHgwSKBGoESkQjBAMQswCQYDVQQGEwJESzEM
-MAoGA1UEChMDVERDMRQwEgYDVQQDEwtUREMgT0NFUyBDQTENMAsGA1UEAxMEQ1JMMTAsoCqgKIYm
-aHR0cDovL2NybC5vY2VzLmNlcnRpZmlrYXQuZGsvb2Nlcy5jcmwwKwYDVR0QBCQwIoAPMjAwMzAy
-MTEwODM5MzBagQ8yMDM3MDIxMTA5MDkzMFowHwYDVR0jBBgwFoAUYLWF7FZkfhIZJ2cdUBVLc647
-+RIwHQYDVR0OBBYEFGC1hexWZH4SGSdnHVAVS3OuO/kSMB0GCSqGSIb2fQdBAAQQMA4bCFY2LjA6
-NC4wAwIEkDANBgkqhkiG9w0BAQUFAAOCAQEACromJkbTc6gJ82sLMJn9iuFXehHTuJTXCRBuo7E4
-A9G28kNBKWKnctj7fAXmMXAnVBhOinxO5dHKjHiIzxvTkIvmI/gLDjNDfZziChmPyQE+dF10yYsc
-A+UYyAFMP8uXBV2YcaaYb7Z8vTd/vuGTJW1v8AqtFxjhA7wHKcitJuj4YfD9IQl+mo6paH1IYnK9
-AOoBmbgGglGBTvH1tJFUuSN6AJqfXY3gPGS5GhKSKseCRHI53OI8xthV9RVOyAUO28bQYqbsFbS1
-AoLbrIyigfCbmTH1ICCoiGEKB5+U/NDXG8wuF/MEJ3Zn61SD/aSQfgY9BKNDLdr8C2LqL19iUw==
------END CERTIFICATE-----
-
-UTN DATACorp SGC Root CA
-========================
------BEGIN CERTIFICATE-----
-MIIEXjCCA0agAwIBAgIQRL4Mi1AAIbQR0ypoBqmtaTANBgkqhkiG9w0BAQUFADCBkzELMAkGA1UE
-BhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEeMBwGA1UEChMVVGhl
-IFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xGzAZ
-BgNVBAMTElVUTiAtIERBVEFDb3JwIFNHQzAeFw05OTA2MjQxODU3MjFaFw0xOTA2MjQxOTA2MzBa
-MIGTMQswCQYDVQQGEwJVUzELMAkGA1UECBMCVVQxFzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5MR4w
-HAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxITAfBgNVBAsTGGh0dHA6Ly93d3cudXNlcnRy
-dXN0LmNvbTEbMBkGA1UEAxMSVVROIC0gREFUQUNvcnAgU0dDMIIBIjANBgkqhkiG9w0BAQEFAAOC
-AQ8AMIIBCgKCAQEA3+5YEKIrblXEjr8uRgnn4AgPLit6E5Qbvfa2gI5lBZMAHryv4g+OGQ0SR+ys
-raP6LnD43m77VkIVni5c7yPeIbkFdicZD0/Ww5y0vpQZY/KmEQrrU0icvvIpOxboGqBMpsn0GFlo
-wHDyUwDAXlCCpVZvNvlK4ESGoE1O1kduSUrLZ9emxAW5jh70/P/N5zbgnAVssjMiFdC04MwXwLLA
-9P4yPykqlXvY8qdOD1R8oQ2AswkDwf9c3V6aPryuvEeKaq5xyh+xKrhfQgUL7EYw0XILyulWbfXv
-33i+Ybqypa4ETLyorGkVl73v67SMvzX41MPRKA5cOp9wGDMgd8SirwIDAQABo4GrMIGoMAsGA1Ud
-DwQEAwIBxjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRTMtGzz3/64PGgXYVOktKeRR20TzA9
-BgNVHR8ENjA0MDKgMKAuhixodHRwOi8vY3JsLnVzZXJ0cnVzdC5jb20vVVROLURBVEFDb3JwU0dD
-LmNybDAqBgNVHSUEIzAhBggrBgEFBQcDAQYKKwYBBAGCNwoDAwYJYIZIAYb4QgQBMA0GCSqGSIb3
-DQEBBQUAA4IBAQAnNZcAiosovcYzMB4p/OL31ZjUQLtgyr+rFywJNn9Q+kHcrpY6CiM+iVnJowft
-Gzet/Hy+UUla3joKVAgWRcKZsYfNjGjgaQPpxE6YsjuMFrMOoAyYUJuTqXAJyCyjj98C5OBxOvG0
-I3KgqgHf35g+FFCgMSa9KOlaMCZ1+XtgHI3zzVAmbQQnmt/VDUVHKWss5nbZqSl9Mt3JNjy9rjXx
-EZ4du5A/EkdOjtd+D2JzHVImOBwYSf0wdJrE5SIv2MCN7ZF6TACPcn9d2t0bi0Vr591pl6jFVkwP
-DPafepE39peC4N1xaf92P2BNPM/3mfnGV/TJVTl4uix5yaaIK/QI
------END CERTIFICATE-----
-
-UTN USERFirst Email Root CA
-===========================
------BEGIN CERTIFICATE-----
-MIIEojCCA4qgAwIBAgIQRL4Mi1AAJLQR0zYlJWfJiTANBgkqhkiG9w0BAQUFADCBrjELMAkGA1UE
-BhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEeMBwGA1UEChMVVGhl
-IFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xNjA0
-BgNVBAMTLVVUTi1VU0VSRmlyc3QtQ2xpZW50IEF1dGhlbnRpY2F0aW9uIGFuZCBFbWFpbDAeFw05
-OTA3MDkxNzI4NTBaFw0xOTA3MDkxNzM2NThaMIGuMQswCQYDVQQGEwJVUzELMAkGA1UECBMCVVQx
-FzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsx
-ITAfBgNVBAsTGGh0dHA6Ly93d3cudXNlcnRydXN0LmNvbTE2MDQGA1UEAxMtVVROLVVTRVJGaXJz
-dC1DbGllbnQgQXV0aGVudGljYXRpb24gYW5kIEVtYWlsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
-MIIBCgKCAQEAsjmFpPJ9q0E7YkY3rs3BYHW8OWX5ShpHornMSMxqmNVNNRm5pELlzkniii8efNIx
-B8dOtINknS4p1aJkxIW9hVE1eaROaJB7HHqkkqgX8pgV8pPMyaQylbsMTzC9mKALi+VuG6JG+ni8
-om+rWV6lL8/K2m2qL+usobNqqrcuZzWLeeEeaYji5kbNoKXqvgvOdjp6Dpvq/NonWz1zHyLmSGHG
-TPNpsaguG7bUMSAsvIKKjqQOpdeJQ/wWWq8dcdcRWdq6hw2v+vPhwvCkxWeM1tZUOt4KpLoDd7Nl
-yP0e03RiqhjKaJMeoYV+9Udly/hNVyh00jT/MLbu9mIwFIws6wIDAQABo4G5MIG2MAsGA1UdDwQE
-AwIBxjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSJgmd9xJ0mcABLtFBIfN49rgRufTBYBgNV
-HR8EUTBPME2gS6BJhkdodHRwOi8vY3JsLnVzZXJ0cnVzdC5jb20vVVROLVVTRVJGaXJzdC1DbGll
-bnRBdXRoZW50aWNhdGlvbmFuZEVtYWlsLmNybDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUH
-AwQwDQYJKoZIhvcNAQEFBQADggEBALFtYV2mGn98q0rkMPxTbyUkxsrt4jFcKw7u7mFVbwQ+zzne
-xRtJlOTrIEy05p5QLnLZjfWqo7NK2lYcYJeA3IKirUq9iiv/Cwm0xtcgBEXkzYABurorbs6q15L+
-5K/r9CYdFip/bDCVNy8zEqx/3cfREYxRmLLQo5HQrfafnoOTHh1CuEava2bwm3/q4wMC5QJRwarV
-NZ1yQAOJujEdxRBoUp7fooXFXAimeOZTT7Hot9MUnpOmw2TjrH5xzbyf6QMbzPvprDHBr3wVdAKZ
-w7JHpsIyYdfHb0gkUSeh1YdV8nuPmD0Wnu51tvjQjvLzxq4oW6fw8zYX/MMF08oDSlQ=
------END CERTIFICATE-----
-
-UTN USERFirst Hardware Root CA
-==============================
------BEGIN CERTIFICATE-----
-MIIEdDCCA1ygAwIBAgIQRL4Mi1AAJLQR0zYq/mUK/TANBgkqhkiG9w0BAQUFADCBlzELMAkGA1UE
-BhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEeMBwGA1UEChMVVGhl
-IFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xHzAd
-BgNVBAMTFlVUTi1VU0VSRmlyc3QtSGFyZHdhcmUwHhcNOTkwNzA5MTgxMDQyWhcNMTkwNzA5MTgx
-OTIyWjCBlzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0
-eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8vd3d3LnVz
-ZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3QtSGFyZHdhcmUwggEiMA0GCSqGSIb3
-DQEBAQUAA4IBDwAwggEKAoIBAQCx98M4P7Sof885glFn0G2f0v9Y8+efK+wNiVSZuTiZFvfgIXlI
-wrthdBKWHTxqctU8EGc6Oe0rE81m65UJM6Rsl7HoxuzBdXmcRl6Nq9Bq/bkqVRcQVLMZ8Jr28bFd
-tqdt++BxF2uiiPsA3/4aMXcMmgF6sTLjKwEHOG7DpV4jvEWbe1DByTCP2+UretNb+zNAHqDVmBe8
-i4fDidNdoI6yqqr2jmmIBsX6iSHzCJ1pLgkzmykNRg+MzEk0sGlRvfkGzWitZky8PqxhvQqIDsjf
-Pe58BEydCl5rkdbux+0ojatNh4lz0G6k0B4WixThdkQDf2Os5M1JnMWS9KsyoUhbAgMBAAGjgbkw
-gbYwCwYDVR0PBAQDAgHGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFKFyXyYbKJhDlV0HN9WF
-lp1L0sNFMEQGA1UdHwQ9MDswOaA3oDWGM2h0dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9VVE4tVVNF
-UkZpcnN0LUhhcmR3YXJlLmNybDAxBgNVHSUEKjAoBggrBgEFBQcDAQYIKwYBBQUHAwUGCCsGAQUF
-BwMGBggrBgEFBQcDBzANBgkqhkiG9w0BAQUFAAOCAQEARxkP3nTGmZev/K0oXnWO6y1n7k57K9cM
-//bey1WiCuFMVGWTYGufEpytXoMs61quwOQt9ABjHbjAbPLPSbtNk28GpgoiskliCE7/yMgUsogW
-XecB5BKV5UU0s4tpvc+0hY91UZ59Ojg6FEgSxvunOxqNDYJAB+gECJChicsZUN/KHAG8HQQZexB2
-lzvukJDKxA4fFm517zP4029bHpbj4HR3dHuKom4t3XbWOTCC8KucUvIqx69JXn7HaOWCgchqJ/kn
-iCrVWFCVH/A7HFe7fRQ5YiuayZSSKqMiDP+JJn1fIytH1xUdqWqeUQ0qUZ6B+dQ7XnASfxAynB67
-nfhmqA==
------END CERTIFICATE-----
-
-UTN USERFirst Object Root CA
-============================
------BEGIN CERTIFICATE-----
-MIIEZjCCA06gAwIBAgIQRL4Mi1AAJLQR0zYt4LNfGzANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UE
-BhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEeMBwGA1UEChMVVGhl
-IFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xHTAb
-BgNVBAMTFFVUTi1VU0VSRmlyc3QtT2JqZWN0MB4XDTk5MDcwOTE4MzEyMFoXDTE5MDcwOTE4NDAz
-NlowgZUxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJVVDEXMBUGA1UEBxMOU2FsdCBMYWtlIENpdHkx
-HjAcBgNVBAoTFVRoZSBVU0VSVFJVU1QgTmV0d29yazEhMB8GA1UECxMYaHR0cDovL3d3dy51c2Vy
-dHJ1c3QuY29tMR0wGwYDVQQDExRVVE4tVVNFUkZpcnN0LU9iamVjdDCCASIwDQYJKoZIhvcNAQEB
-BQADggEPADCCAQoCggEBAM6qgT+jo2F4qjEAVZURnicPHxzfOpuCaDDASmEd8S8O+r5596Uj71VR
-loTN2+O5bj4x2AogZ8f02b+U60cEPgLOKqJdhwQJ9jCdGIqXsqoc/EHSoTbL+z2RuufZcDX65OeQ
-w5ujm9M89RKZd7G3CeBo5hy485RjiGpq/gt2yb70IuRnuasaXnfBhQfdDWy/7gbHd2pBnqcP1/vu
-lBe3/IW+pKvEHDHd17bR5PDv3xaPslKT16HUiaEHLr/hARJCHhrh2JU022R5KP+6LhHC5ehbkkj7
-RwvCbNqtMoNB86XlQXD9ZZBt+vpRxPm9lisZBCzTbafc8H9vg2XiaquHhnUCAwEAAaOBrzCBrDAL
-BgNVHQ8EBAMCAcYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU2u1kdBScFDyr3ZmpvVsoTYs8
-ydgwQgYDVR0fBDswOTA3oDWgM4YxaHR0cDovL2NybC51c2VydHJ1c3QuY29tL1VUTi1VU0VSRmly
-c3QtT2JqZWN0LmNybDApBgNVHSUEIjAgBggrBgEFBQcDAwYIKwYBBQUHAwgGCisGAQQBgjcKAwQw
-DQYJKoZIhvcNAQEFBQADggEBAAgfUrE3RHjb/c652pWWmKpVZIC1WkDdIaXFwfNfLEzIR1pp6ujw
-NTX00CXzyKakh0q9G7FzCL3Uw8q2NbtZhncxzaeAFK4T7/yxSPlrJSUtUbYsbUXBmMiKVl0+7kNO
-PmsnjtA6S4ULX9Ptaqd1y9Fahy85dRNacrACgZ++8A+EVCBibGnU4U3GDZlDAQ0Slox4nb9QorFE
-qmrPF3rPbw/U+CRVX/A0FklmPlBGyWNxODFiuGK581OtbLUrohKqGU8J2l7nk8aOFAj+8DCAGKCG
-hU3IfdeLA/5u1fedFqySLKAj5ZyRUh+U3xeUc8OzwcFxBSAAeL0TUh2oPs0AH8g=
------END CERTIFICATE-----
-
-Camerfirma Chambers of Commerce Root
-====================================
------BEGIN CERTIFICATE-----
-MIIEvTCCA6WgAwIBAgIBADANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJFVTEnMCUGA1UEChMe
-QUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQLExpodHRwOi8vd3d3LmNoYW1i
-ZXJzaWduLm9yZzEiMCAGA1UEAxMZQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdDAeFw0wMzA5MzAx
-NjEzNDNaFw0zNzA5MzAxNjEzNDRaMH8xCzAJBgNVBAYTAkVVMScwJQYDVQQKEx5BQyBDYW1lcmZp
-cm1hIFNBIENJRiBBODI3NDMyODcxIzAhBgNVBAsTGmh0dHA6Ly93d3cuY2hhbWJlcnNpZ24ub3Jn
-MSIwIAYDVQQDExlDaGFtYmVycyBvZiBDb21tZXJjZSBSb290MIIBIDANBgkqhkiG9w0BAQEFAAOC
-AQ0AMIIBCAKCAQEAtzZV5aVdGDDg2olUkfzIx1L4L1DZ77F1c2VHfRtbunXF/KGIJPov7coISjlU
-xFF6tdpg6jg8gbLL8bvZkSM/SAFwdakFKq0fcfPJVD0dBmpAPrMMhe5cG3nCYsS4No41XQEMIwRH
-NaqbYE6gZj3LJgqcQKH0XZi/caulAGgq7YN6D6IUtdQis4CwPAxaUWktWBiP7Zme8a7ileb2R6jW
-DA+wWFjbw2Y3npuRVDM30pQcakjJyfKl2qUMI/cjDpwyVV5xnIQFUZot/eZOKjRa3spAN2cMVCFV
-d9oKDMyXroDclDZK9D7ONhMeU+SsTjoF7Nuucpw4i9A5O4kKPnf+dQIBA6OCAUQwggFAMBIGA1Ud
-EwEB/wQIMAYBAf8CAQwwPAYDVR0fBDUwMzAxoC+gLYYraHR0cDovL2NybC5jaGFtYmVyc2lnbi5v
-cmcvY2hhbWJlcnNyb290LmNybDAdBgNVHQ4EFgQU45T1sU3p26EpW1eLTXYGduHRooowDgYDVR0P
-AQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzAnBgNVHREEIDAegRxjaGFtYmVyc3Jvb3RAY2hh
-bWJlcnNpZ24ub3JnMCcGA1UdEgQgMB6BHGNoYW1iZXJzcm9vdEBjaGFtYmVyc2lnbi5vcmcwWAYD
-VR0gBFEwTzBNBgsrBgEEAYGHLgoDATA+MDwGCCsGAQUFBwIBFjBodHRwOi8vY3BzLmNoYW1iZXJz
-aWduLm9yZy9jcHMvY2hhbWJlcnNyb290Lmh0bWwwDQYJKoZIhvcNAQEFBQADggEBAAxBl8IahsAi
-fJ/7kPMa0QOx7xP5IV8EnNrJpY0nbJaHkb5BkAFyk+cefV/2icZdp0AJPaxJRUXcLo0waLIJuvvD
-L8y6C98/d3tGfToSJI6WjzwFCm/SlCgdbQzALogi1djPHRPH8EjX1wWnz8dHnjs8NMiAT9QUu/wN
-UPf6s+xCX6ndbcj0dc97wXImsQEcXCz9ek60AcUFV7nnPKoF2YjpB0ZBzu9Bga5Y34OirsrXdx/n
-ADydb47kMgkdTXg0eDQ8lJsm7U9xxhl6vSAiSFr+S30Dt+dYvsYyTnQeaN2oaFuzPu5ifdmA6Ap1
-erfutGWaIZDgqtCYvDi1czyL+Nw=
------END CERTIFICATE-----
-
-Camerfirma Global Chambersign Root
-==================================
------BEGIN CERTIFICATE-----
-MIIExTCCA62gAwIBAgIBADANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJFVTEnMCUGA1UEChMe
-QUMgQ2FtZXJmaXJtYSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQLExpodHRwOi8vd3d3LmNoYW1i
-ZXJzaWduLm9yZzEgMB4GA1UEAxMXR2xvYmFsIENoYW1iZXJzaWduIFJvb3QwHhcNMDMwOTMwMTYx
-NDE4WhcNMzcwOTMwMTYxNDE4WjB9MQswCQYDVQQGEwJFVTEnMCUGA1UEChMeQUMgQ2FtZXJmaXJt
-YSBTQSBDSUYgQTgyNzQzMjg3MSMwIQYDVQQLExpodHRwOi8vd3d3LmNoYW1iZXJzaWduLm9yZzEg
-MB4GA1UEAxMXR2xvYmFsIENoYW1iZXJzaWduIFJvb3QwggEgMA0GCSqGSIb3DQEBAQUAA4IBDQAw
-ggEIAoIBAQCicKLQn0KuWxfH2H3PFIP8T8mhtxOviteePgQKkotgVvq0Mi+ITaFgCPS3CU6gSS9J
-1tPfnZdan5QEcOw/Wdm3zGaLmFIoCQLfxS+EjXqXd7/sQJ0lcqu1PzKY+7e3/HKE5TWH+VX6ox8O
-by4o3Wmg2UIQxvi1RMLQQ3/bvOSiPGpVeAp3qdjqGTK3L/5cPxvusZjsyq16aUXjlg9V9ubtdepl
-6DJWk0aJqCWKZQbua795B9Dxt6/tLE2Su8CoX6dnfQTyFQhwrJLWfQTSM/tMtgsL+xrJxI0DqX5c
-8lCrEqWhz0hQpe/SyBoT+rB/sYIcd2oPX9wLlY/vQ37mRQklAgEDo4IBUDCCAUwwEgYDVR0TAQH/
-BAgwBgEB/wIBDDA/BgNVHR8EODA2MDSgMqAwhi5odHRwOi8vY3JsLmNoYW1iZXJzaWduLm9yZy9j
-aGFtYmVyc2lnbnJvb3QuY3JsMB0GA1UdDgQWBBRDnDafsJ4wTcbOX60Qq+UDpfqpFDAOBgNVHQ8B
-Af8EBAMCAQYwEQYJYIZIAYb4QgEBBAQDAgAHMCoGA1UdEQQjMCGBH2NoYW1iZXJzaWducm9vdEBj
-aGFtYmVyc2lnbi5vcmcwKgYDVR0SBCMwIYEfY2hhbWJlcnNpZ25yb290QGNoYW1iZXJzaWduLm9y
-ZzBbBgNVHSAEVDBSMFAGCysGAQQBgYcuCgEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly9jcHMuY2hh
-bWJlcnNpZ24ub3JnL2Nwcy9jaGFtYmVyc2lnbnJvb3QuaHRtbDANBgkqhkiG9w0BAQUFAAOCAQEA
-PDtwkfkEVCeR4e3t/mh/YV3lQWVPMvEYBZRqHN4fcNs+ezICNLUMbKGKfKX0j//U2K0X1S0E0T9Y
-gOKBWYi+wONGkyT+kL0mojAt6JcmVzWJdJYY9hXiryQZVgICsroPFOrGimbBhkVVi76SvpykBMdJ
-PJ7oKXqJ1/6v/2j1pReQvayZzKWGVwlnRtvWFsJG8eSpUPWP0ZIV018+xgBJOm5YstHRJw0lyDL4
-IBHNfTIzSJRUTN3cecQwn+uOuFW114hcxWokPbLTBQNRxgfvzBRydD1ucs4YKIxKoHflCStFREes
-t2d/AYoFWpO+ocH/+OcOZ6RHSXZddZAa9SaP8A==
------END CERTIFICATE-----
-
-NetLock Qualified (Class QA) Root
-=================================
------BEGIN CERTIFICATE-----
-MIIG0TCCBbmgAwIBAgIBezANBgkqhkiG9w0BAQUFADCByTELMAkGA1UEBhMCSFUxETAPBgNVBAcT
-CEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0b25zYWdpIEtmdC4xGjAYBgNV
-BAsTEVRhbnVzaXR2YW55a2lhZG9rMUIwQAYDVQQDEzlOZXRMb2NrIE1pbm9zaXRldHQgS296amVn
-eXpvaSAoQ2xhc3MgUUEpIFRhbnVzaXR2YW55a2lhZG8xHjAcBgkqhkiG9w0BCQEWD2luZm9AbmV0
-bG9jay5odTAeFw0wMzAzMzAwMTQ3MTFaFw0yMjEyMTUwMTQ3MTFaMIHJMQswCQYDVQQGEwJIVTER
-MA8GA1UEBxMIQnVkYXBlc3QxJzAlBgNVBAoTHk5ldExvY2sgSGFsb3phdGJpenRvbnNhZ2kgS2Z0
-LjEaMBgGA1UECxMRVGFudXNpdHZhbnlraWFkb2sxQjBABgNVBAMTOU5ldExvY2sgTWlub3NpdGV0
-dCBLb3pqZWd5em9pIChDbGFzcyBRQSkgVGFudXNpdHZhbnlraWFkbzEeMBwGCSqGSIb3DQEJARYP
-aW5mb0BuZXRsb2NrLmh1MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx1Ilstg91IRV
-CacbvWy5FPSKAtt2/GoqeKvld/Bu4IwjZ9ulZJm53QE+b+8tmjwi8F3JV6BVQX/yQ15YglMxZc4e
-8ia6AFQer7C8HORSjKAyr7c3sVNnaHRnUPYtLmTeriZ539+Zhqurf4XsoPuAzPS4DB6TRWO53Lhb
-m+1bOdRfYrCnjnxmOCyqsQhjF2d9zL2z8cM/z1A57dEZgxXbhxInlrfa6uWdvLrqOU+L73Sa58XQ
-0uqGURzk/mQIKAR5BevKxXEOC++r6uwSEaEYBTJp0QwsGj0lmT+1fMptsK6ZmfoIYOcZwvK9UdPM
-0wKswREMgM6r3JSda6M5UzrWhQIDAMV9o4ICwDCCArwwEgYDVR0TAQH/BAgwBgEB/wIBBDAOBgNV
-HQ8BAf8EBAMCAQYwggJ1BglghkgBhvhCAQ0EggJmFoICYkZJR1lFTEVNISBFemVuIHRhbnVzaXR2
-YW55IGEgTmV0TG9jayBLZnQuIE1pbm9zaXRldHQgU3pvbGdhbHRhdGFzaSBTemFiYWx5emF0YWJh
-biBsZWlydCBlbGphcmFzb2sgYWxhcGphbiBrZXN6dWx0LiBBIG1pbm9zaXRldHQgZWxla3Ryb25p
-a3VzIGFsYWlyYXMgam9naGF0YXMgZXJ2ZW55ZXN1bGVzZW5laywgdmFsYW1pbnQgZWxmb2dhZGFz
-YW5hayBmZWx0ZXRlbGUgYSBNaW5vc2l0ZXR0IFN6b2xnYWx0YXRhc2kgU3phYmFseXphdGJhbiwg
-YXogQWx0YWxhbm9zIFN6ZXJ6b2Rlc2kgRmVsdGV0ZWxla2JlbiBlbG9pcnQgZWxsZW5vcnplc2kg
-ZWxqYXJhcyBtZWd0ZXRlbGUuIEEgZG9rdW1lbnR1bW9rIG1lZ3RhbGFsaGF0b2sgYSBodHRwczov
-L3d3dy5uZXRsb2NrLmh1L2RvY3MvIGNpbWVuIHZhZ3kga2VyaGV0b2sgYXogaW5mb0BuZXRsb2Nr
-Lm5ldCBlLW1haWwgY2ltZW4uIFdBUk5JTkchIFRoZSBpc3N1YW5jZSBhbmQgdGhlIHVzZSBvZiB0
-aGlzIGNlcnRpZmljYXRlIGFyZSBzdWJqZWN0IHRvIHRoZSBOZXRMb2NrIFF1YWxpZmllZCBDUFMg
-YXZhaWxhYmxlIGF0IGh0dHBzOi8vd3d3Lm5ldGxvY2suaHUvZG9jcy8gb3IgYnkgZS1tYWlsIGF0
-IGluZm9AbmV0bG9jay5uZXQwHQYDVR0OBBYEFAlqYhaSsFq7VQ7LdTI6MuWyIckoMA0GCSqGSIb3
-DQEBBQUAA4IBAQCRalCc23iBmz+LQuM7/KbD7kPgz/PigDVJRXYC4uMvBcXxKufAQTPGtpvQMznN
-wNuhrWw3AkxYQTvyl5LGSKjN5Yo5iWH5Upfpvfb5lHTocQ68d4bDBsxafEp+NFAwLvt/MpqNPfMg
-W/hqyobzMUwsWYACff44yTB1HLdV47yfuqhthCgFdbOLDcCRVCHnpgu0mfVRQdzNo0ci2ccBgcTc
-R08m6h/t280NmPSjnLRzMkqWmf68f8glWPhY83ZmiVSkpj7EUFy6iRiCdUgh0k8T6GB+B3bbELVR
-5qq5aKrN9p2QdRLqOBrKROi3macqaJVmlaut74nLYKkGEsaUR+ko
------END CERTIFICATE-----
-
-NetLock Notary (Class A) Root
-=============================
------BEGIN CERTIFICATE-----
-MIIGfTCCBWWgAwIBAgICAQMwDQYJKoZIhvcNAQEEBQAwga8xCzAJBgNVBAYTAkhVMRAwDgYDVQQI
-EwdIdW5nYXJ5MREwDwYDVQQHEwhCdWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6
-dG9uc2FnaSBLZnQuMRowGAYDVQQLExFUYW51c2l0dmFueWtpYWRvazE2MDQGA1UEAxMtTmV0TG9j
-ayBLb3pqZWd5em9pIChDbGFzcyBBKSBUYW51c2l0dmFueWtpYWRvMB4XDTk5MDIyNDIzMTQ0N1oX
-DTE5MDIxOTIzMTQ0N1owga8xCzAJBgNVBAYTAkhVMRAwDgYDVQQIEwdIdW5nYXJ5MREwDwYDVQQH
-EwhCdWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6dG9uc2FnaSBLZnQuMRowGAYD
-VQQLExFUYW51c2l0dmFueWtpYWRvazE2MDQGA1UEAxMtTmV0TG9jayBLb3pqZWd5em9pIChDbGFz
-cyBBKSBUYW51c2l0dmFueWtpYWRvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvHSM
-D7tM9DceqQWC2ObhbHDqeLVu0ThEDaiDzl3S1tWBxdRL51uUcCbbO51qTGL3cfNk1mE7PetzozfZ
-z+qMkjvN9wfcZnSX9EUi3fRc4L9t875lM+QVOr/bmJBVOMTtplVjC7B4BPTjbsE/jvxReB+SnoPC
-/tmwqcm8WgD/qaiYdPv2LD4VOQ22BFWoDpggQrOxJa1+mm9dU7GrDPzr4PN6s6iz/0b2Y6LYOph7
-tqyF/7AlT3Rj5xMHpQqPBffAZG9+pyeAlt7ULoZgx2srXnN7F+eRP2QM2EsiNCubMvJIH5+hCoR6
-4sKtlz2O1cH5VqNQ6ca0+pii7pXmKgOM3wIDAQABo4ICnzCCApswDgYDVR0PAQH/BAQDAgAGMBIG
-A1UdEwEB/wQIMAYBAf8CAQQwEQYJYIZIAYb4QgEBBAQDAgAHMIICYAYJYIZIAYb4QgENBIICURaC
-Ak1GSUdZRUxFTSEgRXplbiB0YW51c2l0dmFueSBhIE5ldExvY2sgS2Z0LiBBbHRhbGFub3MgU3pv
-bGdhbHRhdGFzaSBGZWx0ZXRlbGVpYmVuIGxlaXJ0IGVsamFyYXNvayBhbGFwamFuIGtlc3p1bHQu
-IEEgaGl0ZWxlc2l0ZXMgZm9seWFtYXRhdCBhIE5ldExvY2sgS2Z0LiB0ZXJtZWtmZWxlbG9zc2Vn
-LWJpenRvc2l0YXNhIHZlZGkuIEEgZGlnaXRhbGlzIGFsYWlyYXMgZWxmb2dhZGFzYW5hayBmZWx0
-ZXRlbGUgYXogZWxvaXJ0IGVsbGVub3J6ZXNpIGVsamFyYXMgbWVndGV0ZWxlLiBBeiBlbGphcmFz
-IGxlaXJhc2EgbWVndGFsYWxoYXRvIGEgTmV0TG9jayBLZnQuIEludGVybmV0IGhvbmxhcGphbiBh
-IGh0dHBzOi8vd3d3Lm5ldGxvY2submV0L2RvY3MgY2ltZW4gdmFneSBrZXJoZXRvIGF6IGVsbGVu
-b3J6ZXNAbmV0bG9jay5uZXQgZS1tYWlsIGNpbWVuLiBJTVBPUlRBTlQhIFRoZSBpc3N1YW5jZSBh
-bmQgdGhlIHVzZSBvZiB0aGlzIGNlcnRpZmljYXRlIGlzIHN1YmplY3QgdG8gdGhlIE5ldExvY2sg
-Q1BTIGF2YWlsYWJsZSBhdCBodHRwczovL3d3dy5uZXRsb2NrLm5ldC9kb2NzIG9yIGJ5IGUtbWFp
-bCBhdCBjcHNAbmV0bG9jay5uZXQuMA0GCSqGSIb3DQEBBAUAA4IBAQBIJEb3ulZv+sgoA0BO5TE5
-ayZrU3/b39/zcT0mwBQOxmd7I6gMc90Bu8bKbjc5VdXHjFYgDigKDtIqpLBJUsY4B/6+CgmM0ZjP
-ytoUMaFP0jn8DxEsQ8Pdq5PHVT5HfBgaANzze9jyf1JsIPQLX2lS9O74silg6+NJMSEN1rUQQeJB
-CWziGppWS3cC9qCbmieH6FUpccKQn0V4GuEVZD3QDtigdp+uxdAu6tYPVuxkf1qbFFgBJ34TUMdr
-KuZoPL9coAob4Q566eKAw+np9v1sEZ7Q5SgnK1QyQhSCdeZK8CtmdWOMovsEPoMOmzbwGOQmIMOM
-8CgHrTwXZoi1/baI
------END CERTIFICATE-----
-
-NetLock Business (Class B) Root
-===============================
------BEGIN CERTIFICATE-----
-MIIFSzCCBLSgAwIBAgIBaTANBgkqhkiG9w0BAQQFADCBmTELMAkGA1UEBhMCSFUxETAPBgNVBAcT
-CEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0b25zYWdpIEtmdC4xGjAYBgNV
-BAsTEVRhbnVzaXR2YW55a2lhZG9rMTIwMAYDVQQDEylOZXRMb2NrIFV6bGV0aSAoQ2xhc3MgQikg
-VGFudXNpdHZhbnlraWFkbzAeFw05OTAyMjUxNDEwMjJaFw0xOTAyMjAxNDEwMjJaMIGZMQswCQYD
-VQQGEwJIVTERMA8GA1UEBxMIQnVkYXBlc3QxJzAlBgNVBAoTHk5ldExvY2sgSGFsb3phdGJpenRv
-bnNhZ2kgS2Z0LjEaMBgGA1UECxMRVGFudXNpdHZhbnlraWFkb2sxMjAwBgNVBAMTKU5ldExvY2sg
-VXpsZXRpIChDbGFzcyBCKSBUYW51c2l0dmFueWtpYWRvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB
-iQKBgQCx6gTsIKAjwo84YM/HRrPVG/77uZmeBNwcf4xKgZjupNTKihe5In+DCnVMm8Bp2GQ5o+2S
-o/1bXHQawEfKOml2mrriRBf8TKPV/riXiK+IA4kfpPIEPsgHC+b5sy96YhQJRhTKZPWLgLViqNhr
-1nGTLbO/CVRY7QbrqHvcQ7GhaQIDAQABo4ICnzCCApswEgYDVR0TAQH/BAgwBgEB/wIBBDAOBgNV
-HQ8BAf8EBAMCAAYwEQYJYIZIAYb4QgEBBAQDAgAHMIICYAYJYIZIAYb4QgENBIICURaCAk1GSUdZ
-RUxFTSEgRXplbiB0YW51c2l0dmFueSBhIE5ldExvY2sgS2Z0LiBBbHRhbGFub3MgU3pvbGdhbHRh
-dGFzaSBGZWx0ZXRlbGVpYmVuIGxlaXJ0IGVsamFyYXNvayBhbGFwamFuIGtlc3p1bHQuIEEgaGl0
-ZWxlc2l0ZXMgZm9seWFtYXRhdCBhIE5ldExvY2sgS2Z0LiB0ZXJtZWtmZWxlbG9zc2VnLWJpenRv
-c2l0YXNhIHZlZGkuIEEgZGlnaXRhbGlzIGFsYWlyYXMgZWxmb2dhZGFzYW5hayBmZWx0ZXRlbGUg
-YXogZWxvaXJ0IGVsbGVub3J6ZXNpIGVsamFyYXMgbWVndGV0ZWxlLiBBeiBlbGphcmFzIGxlaXJh
-c2EgbWVndGFsYWxoYXRvIGEgTmV0TG9jayBLZnQuIEludGVybmV0IGhvbmxhcGphbiBhIGh0dHBz
-Oi8vd3d3Lm5ldGxvY2submV0L2RvY3MgY2ltZW4gdmFneSBrZXJoZXRvIGF6IGVsbGVub3J6ZXNA
-bmV0bG9jay5uZXQgZS1tYWlsIGNpbWVuLiBJTVBPUlRBTlQhIFRoZSBpc3N1YW5jZSBhbmQgdGhl
-IHVzZSBvZiB0aGlzIGNlcnRpZmljYXRlIGlzIHN1YmplY3QgdG8gdGhlIE5ldExvY2sgQ1BTIGF2
-YWlsYWJsZSBhdCBodHRwczovL3d3dy5uZXRsb2NrLm5ldC9kb2NzIG9yIGJ5IGUtbWFpbCBhdCBj
-cHNAbmV0bG9jay5uZXQuMA0GCSqGSIb3DQEBBAUAA4GBAATbrowXr/gOkDFOzT4JwG06sPgzTEdM
-43WIEJessDgVkcYplswhwG08pXTP2IKlOcNl40JwuyKQ433bNXbhoLXan3BukxowOR0w2y7jfLKR
-stE3Kfq51hdcR0/jHTjrn9V7lagonhVK0dHQKwCXoOKSNitjrFgBazMpUIaD8QFI
------END CERTIFICATE-----
-
-NetLock Express (Class C) Root
-==============================
------BEGIN CERTIFICATE-----
-MIIFTzCCBLigAwIBAgIBaDANBgkqhkiG9w0BAQQFADCBmzELMAkGA1UEBhMCSFUxETAPBgNVBAcT
-CEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0b25zYWdpIEtmdC4xGjAYBgNV
-BAsTEVRhbnVzaXR2YW55a2lhZG9rMTQwMgYDVQQDEytOZXRMb2NrIEV4cHJlc3N6IChDbGFzcyBD
-KSBUYW51c2l0dmFueWtpYWRvMB4XDTk5MDIyNTE0MDgxMVoXDTE5MDIyMDE0MDgxMVowgZsxCzAJ
-BgNVBAYTAkhVMREwDwYDVQQHEwhCdWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6
-dG9uc2FnaSBLZnQuMRowGAYDVQQLExFUYW51c2l0dmFueWtpYWRvazE0MDIGA1UEAxMrTmV0TG9j
-ayBFeHByZXNzeiAoQ2xhc3MgQykgVGFudXNpdHZhbnlraWFkbzCBnzANBgkqhkiG9w0BAQEFAAOB
-jQAwgYkCgYEA6+ywbGGKIyWvYCDj2Z/8kwvbXY2wobNAOoLO/XXgeDIDhlqGlZHtU/qdQPzm6N3Z
-W3oDvV3zOwzDUXmbrVWg6dADEK8KuhRC2VImESLH0iDMgqSaqf64gXadarfSNnU+sYYJ9m5tfk63
-euyucYT2BDMIJTLrdKwWRMbkQJMdf60CAwEAAaOCAp8wggKbMBIGA1UdEwEB/wQIMAYBAf8CAQQw
-DgYDVR0PAQH/BAQDAgAGMBEGCWCGSAGG+EIBAQQEAwIABzCCAmAGCWCGSAGG+EIBDQSCAlEWggJN
-RklHWUVMRU0hIEV6ZW4gdGFudXNpdHZhbnkgYSBOZXRMb2NrIEtmdC4gQWx0YWxhbm9zIFN6b2xn
-YWx0YXRhc2kgRmVsdGV0ZWxlaWJlbiBsZWlydCBlbGphcmFzb2sgYWxhcGphbiBrZXN6dWx0LiBB
-IGhpdGVsZXNpdGVzIGZvbHlhbWF0YXQgYSBOZXRMb2NrIEtmdC4gdGVybWVrZmVsZWxvc3NlZy1i
-aXp0b3NpdGFzYSB2ZWRpLiBBIGRpZ2l0YWxpcyBhbGFpcmFzIGVsZm9nYWRhc2FuYWsgZmVsdGV0
-ZWxlIGF6IGVsb2lydCBlbGxlbm9yemVzaSBlbGphcmFzIG1lZ3RldGVsZS4gQXogZWxqYXJhcyBs
-ZWlyYXNhIG1lZ3RhbGFsaGF0byBhIE5ldExvY2sgS2Z0LiBJbnRlcm5ldCBob25sYXBqYW4gYSBo
-dHRwczovL3d3dy5uZXRsb2NrLm5ldC9kb2NzIGNpbWVuIHZhZ3kga2VyaGV0byBheiBlbGxlbm9y
-emVzQG5ldGxvY2submV0IGUtbWFpbCBjaW1lbi4gSU1QT1JUQU5UISBUaGUgaXNzdWFuY2UgYW5k
-IHRoZSB1c2Ugb2YgdGhpcyBjZXJ0aWZpY2F0ZSBpcyBzdWJqZWN0IHRvIHRoZSBOZXRMb2NrIENQ
-UyBhdmFpbGFibGUgYXQgaHR0cHM6Ly93d3cubmV0bG9jay5uZXQvZG9jcyBvciBieSBlLW1haWwg
-YXQgY3BzQG5ldGxvY2submV0LjANBgkqhkiG9w0BAQQFAAOBgQAQrX/XDDKACtiG8XmYta3UzbM2
-xJZIwVzNmtkFLp++UOv0JhQQLdRmF/iewSf98e3ke0ugbLWrmldwpu2gpO0u9f38vf5NNwgMvOOW
-gyL1SRt/Syu0VMGAfJlOHdCM7tCs5ZL6dVb+ZKATj7i4Fp1hBWeAyNDYpQcCNJgEjTME1A==
------END CERTIFICATE-----
-
-XRamp Global CA Root
-====================
------BEGIN CERTIFICATE-----
-MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCBgjELMAkGA1UE
-BhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2Vj
-dXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB
-dXRob3JpdHkwHhcNMDQxMTAxMTcxNDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMx
-HjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkg
-U2VydmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3Jp
-dHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS638eMpSe2OAtp87ZOqCwu
-IR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCPKZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMx
-foArtYzAQDsRhtDLooY2YKTVMIJt2W7QDxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FE
-zG+gSqmUsE3a56k0enI4qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqs
-AxcZZPRaJSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNViPvry
-xS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud
-EwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASsjVy16bYbMDYGA1UdHwQvMC0wK6Ap
-oCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMC
-AQEwDQYJKoZIhvcNAQEFBQADggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc
-/Kh4ZzXxHfARvbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt
-qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLaIR9NmXmd4c8n
-nxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSyi6mx5O+aGtA9aZnuqCij4Tyz
-8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQO+7ETPTsJ3xCwnR8gooJybQDJbw=
------END CERTIFICATE-----
-
-Go Daddy Class 2 CA
-===================
------BEGIN CERTIFICATE-----
-MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMY
-VGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRp
-ZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkG
-A1UEBhMCVVMxITAfBgNVBAoTGFRoZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28g
-RGFkZHkgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQAD
-ggENADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCAPVYYYwhv
-2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6wwdhFJ2+qN1j3hybX2C32
-qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXiEqITLdiOr18SPaAIBQi2XKVlOARFmR6j
-YGB0xUGlcmIbYsUfb18aQr4CUWWoriMYavx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmY
-vLEHZ6IVDd2gWMZEewo+YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0O
-BBYEFNLEsNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h/t2o
-atTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMu
-MTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwG
-A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wim
-PQoZ+YeAEW5p5JYXMP80kWNyOO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKt
-I3lpjbi2Tc7PTMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ
-HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mERdEr/VxqHD3VI
-Ls9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5CufReYNnyicsbkqWletNw+vHX/b
-vZ8=
------END CERTIFICATE-----
-
-Starfield Class 2 CA
-====================
------BEGIN CERTIFICATE-----
-MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzElMCMGA1UEChMc
-U3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZpZWxkIENsYXNzIDIg
-Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQwNjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBo
-MQswCQYDVQQGEwJVUzElMCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAG
-A1UECxMpU3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqG
-SIb3DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf8MOh2tTY
-bitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN+lq2cwQlZut3f+dZxkqZ
-JRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVm
-epsZGD3/cVE8MC5fvj13c7JdBmzDI1aaK4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSN
-F4Azbl5KXZnJHoe0nRrA1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HF
-MIHCMB0GA1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fRzt0f
-hvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNo
-bm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBDbGFzcyAyIENlcnRpZmljYXRpb24g
-QXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGs
-afPzWdqbAYcaT1epoXkJKtv3L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLM
-PUxA2IGvd56Deruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl
-xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynpVSJYACPq4xJD
-KVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEYWQPJIrSPnNVeKtelttQKbfi3
-QBFGmh95DmK/D5fs4C8fF5Q=
------END CERTIFICATE-----
-
-StartCom Certification Authority
-================================
------BEGIN CERTIFICATE-----
-MIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMN
-U3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmlu
-ZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0
-NjM2WhcNMzYwOTE3MTk0NjM2WjB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRk
-LjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMg
-U3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw
-ggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZkpMyONvg45iPwbm2xPN1y
-o4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rfOQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/
-Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/CJi/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/d
-eMotHweXMAEtcnn6RtYTKqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt
-2PZE4XNiHzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMMAv+Z
-6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w+2OqqGwaVLRcJXrJ
-osmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/
-untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVc
-UjyJthkqcwEKDwOzEmDyei+B26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT
-37uMdBNSSwIDAQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE
-FE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9jZXJ0LnN0YXJ0
-Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3JsLnN0YXJ0Y29tLm9yZy9zZnNj
-YS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFMBgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUH
-AgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0Y29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRw
-Oi8vY2VydC5zdGFydGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYg
-U3RhcnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlhYmlsaXR5
-LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2YgdGhlIFN0YXJ0Q29tIENl
-cnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFpbGFibGUgYXQgaHR0cDovL2NlcnQuc3Rh
-cnRjb20ub3JnL3BvbGljeS5wZGYwEQYJYIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilT
-dGFydENvbSBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOC
-AgEAFmyZ9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8jhvh
-3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUWFjgKXlf2Ysd6AgXm
-vB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJzewT4F+irsfMuXGRuczE6Eri8sxHk
-fY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1ny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3
-fsNrarnDy0RLrHiQi+fHLB5LEUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZ
-EoalHmdkrQYuL6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq
-yvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuCO3NJo2pXh5Tl
-1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6Vum0ABj6y6koQOdjQK/W/7HW/
-lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkyShNOsF/5oirpt9P/FlUQqmMGqz9IgcgA38coro
-g14=
------END CERTIFICATE-----
-
-Taiwan GRCA
-===========
------BEGIN CERTIFICATE-----
-MIIFcjCCA1qgAwIBAgIQH51ZWtcvwgZEpYAIaeNe9jANBgkqhkiG9w0BAQUFADA/MQswCQYDVQQG
-EwJUVzEwMC4GA1UECgwnR292ZXJubWVudCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4X
-DTAyMTIwNTEzMjMzM1oXDTMyMTIwNTEzMjMzM1owPzELMAkGA1UEBhMCVFcxMDAuBgNVBAoMJ0dv
-dmVybm1lbnQgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQAD
-ggIPADCCAgoCggIBAJoluOzMonWoe/fOW1mKydGGEghU7Jzy50b2iPN86aXfTEc2pBsBHH8eV4qN
-w8XRIePaJD9IK/ufLqGU5ywck9G/GwGHU5nOp/UKIXZ3/6m3xnOUT0b3EEk3+qhZSV1qgQdW8or5
-BtD3cCJNtLdBuTK4sfCxw5w/cP1T3YGq2GN49thTbqGsaoQkclSGxtKyyhwOeYHWtXBiCAEuTk8O
-1RGvqa/lmr/czIdtJuTJV6L7lvnM4T9TjGxMfptTCAtsF/tnyMKtsc2AtJfcdgEWFelq16TheEfO
-htX7MfP6Mb40qij7cEwdScevLJ1tZqa2jWR+tSBqnTuBto9AAGdLiYa4zGX+FVPpBMHWXx1E1wov
-J5pGfaENda1UhhXcSTvxls4Pm6Dso3pdvtUqdULle96ltqqvKKyskKw4t9VoNSZ63Pc78/1Fm9G7
-Q3hub/FCVGqY8A2tl+lSXunVanLeavcbYBT0peS2cWeqH+riTcFCQP5nRhc4L0c/cZyu5SHKYS1t
-B6iEfC3uUSXxY5Ce/eFXiGvviiNtsea9P63RPZYLhY3Naye7twWb7LuRqQoHEgKXTiCQ8P8NHuJB
-O9NAOueNXdpm5AKwB1KYXA6OM5zCppX7VRluTI6uSw+9wThNXo+EHWbNxWCWtFJaBYmOlXqYwZE8
-lSOyDvR5tMl8wUohAgMBAAGjajBoMB0GA1UdDgQWBBTMzO/MKWCkO7GStjz6MmKPrCUVOzAMBgNV
-HRMEBTADAQH/MDkGBGcqBwAEMTAvMC0CAQAwCQYFKw4DAhoFADAHBgVnKgMAAAQUA5vwIhP/lSg2
-09yewDL7MTqKUWUwDQYJKoZIhvcNAQEFBQADggIBAECASvomyc5eMN1PhnR2WPWus4MzeKR6dBcZ
-TulStbngCnRiqmjKeKBMmo4sIy7VahIkv9Ro04rQ2JyftB8M3jh+Vzj8jeJPXgyfqzvS/3WXy6Tj
-Zwj/5cAWtUgBfen5Cv8b5Wppv3ghqMKnI6mGq3ZW6A4M9hPdKmaKZEk9GhiHkASfQlK3T8v+R0F2
-Ne//AHY2RTKbxkaFXeIksB7jSJaYV0eUVXoPQbFEJPPB/hprv4j9wabak2BegUqZIJxIZhm1AHlU
-D7gsL0u8qV1bYH+Mh6XgUmMqvtg7hUAV/h62ZT/FS9p+tXo1KaMuephgIqP0fSdOLeq0dDzpD6Qz
-DxARvBMB1uUO07+1EqLhRSPAzAhuYbeJq4PjJB7mXQfnHyA+z2fI56wwbSdLaG5LKlwCCDTb+Hbk
-Z6MmnD+iMsJKxYEYMRBWqoTvLQr/uB930r+lWKBi5NdLkXWNiYCYfm3LU05er/ayl4WXudpVBrkk
-7tfGOB5jGxI7leFYrPLfhNVfmS8NVVvmONsuP3LpSIXLuykTjx44VbnzssQwmSNOXfJIoRIM3BKQ
-CZBUkQM8R+XVyWXgt0t97EfTsws+rZ7QdAAO671RrcDeLMDDav7v3Aun+kbfYNucpllQdSNpc5Oy
-+fwC00fmcc4QAu4njIT/rEUNE1yDMuAlpYYsfPQS
------END CERTIFICATE-----
-
-Firmaprofesional Root CA
-========================
------BEGIN CERTIFICATE-----
-MIIEVzCCAz+gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBnTELMAkGA1UEBhMCRVMxIjAgBgNVBAcT
-GUMvIE11bnRhbmVyIDI0NCBCYXJjZWxvbmExQjBABgNVBAMTOUF1dG9yaWRhZCBkZSBDZXJ0aWZp
-Y2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODEmMCQGCSqGSIb3DQEJARYXY2FA
-ZmlybWFwcm9mZXNpb25hbC5jb20wHhcNMDExMDI0MjIwMDAwWhcNMTMxMDI0MjIwMDAwWjCBnTEL
-MAkGA1UEBhMCRVMxIjAgBgNVBAcTGUMvIE11bnRhbmVyIDI0NCBCYXJjZWxvbmExQjBABgNVBAMT
-OUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2
-ODEmMCQGCSqGSIb3DQEJARYXY2FAZmlybWFwcm9mZXNpb25hbC5jb20wggEiMA0GCSqGSIb3DQEB
-AQUAA4IBDwAwggEKAoIBAQDnIwNvbyOlXnjOlSztlB5uCp4Bx+ow0Syd3Tfom5h5VtP8c9/Qit5V
-j1H5WuretXDE7aTt/6MNbg9kUDGvASdYrv5sp0ovFy3Tc9UTHI9ZpTQsHVQERc1ouKDAA6XPhUJH
-lShbz++AbOCQl4oBPB3zhxAwJkh91/zpnZFx/0GaqUC1N5wpIE8fUuOgfRNtVLcK3ulqTgesrBlf
-3H5idPayBQC6haD9HThuy1q7hryUZzM1gywfI834yJFxzJeL764P3CkDG8A563DtwW4O2GcLiam8
-NeTvtjS0pbbELaW+0MOUJEjb35bTALVmGotmBQ/dPz/LP6pemkr4tErvlTcbAgMBAAGjgZ8wgZww
-KgYDVR0RBCMwIYYfaHR0cDovL3d3dy5maXJtYXByb2Zlc2lvbmFsLmNvbTASBgNVHRMBAf8ECDAG
-AQH/AgEBMCsGA1UdEAQkMCKADzIwMDExMDI0MjIwMDAwWoEPMjAxMzEwMjQyMjAwMDBaMA4GA1Ud
-DwEB/wQEAwIBBjAdBgNVHQ4EFgQUMwugZtHq2s7eYpMEKFK1FH84aLcwDQYJKoZIhvcNAQEFBQAD
-ggEBAEdz/o0nVPD11HecJ3lXV7cVVuzH2Fi3AQL0M+2TUIiefEaxvT8Ub/GzR0iLjJcG1+p+o1wq
-u00vR+L4OQbJnC4xGgN49Lw4xiKLMzHwFgQEffl25EvXwOaD7FnMP97/T2u3Z36mhoEyIwOdyPdf
-wUpgpZKpsaSgYMN4h7Mi8yrrW6ntBas3D7Hi05V2Y1Z0jFhyGzflZKG+TQyTmAyX9odtsz/ny4Cm
-7YjHX1BiAuiZdBbQ5rQ58SfLyEDW44YQqSMSkuBpQWOnryULwMWSyx6Yo1q6xTMPoJcB3X/ge9YG
-VM+h4k0460tQtcsm9MracEpqoeJ5quGnM/b9Sh/22WA=
------END CERTIFICATE-----
-
-Wells Fargo Root CA
-===================
------BEGIN CERTIFICATE-----
-MIID5TCCAs2gAwIBAgIEOeSXnjANBgkqhkiG9w0BAQUFADCBgjELMAkGA1UEBhMCVVMxFDASBgNV
-BAoTC1dlbGxzIEZhcmdvMSwwKgYDVQQLEyNXZWxscyBGYXJnbyBDZXJ0aWZpY2F0aW9uIEF1dGhv
-cml0eTEvMC0GA1UEAxMmV2VsbHMgRmFyZ28gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcN
-MDAxMDExMTY0MTI4WhcNMjEwMTE0MTY0MTI4WjCBgjELMAkGA1UEBhMCVVMxFDASBgNVBAoTC1dl
-bGxzIEZhcmdvMSwwKgYDVQQLEyNXZWxscyBGYXJnbyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEv
-MC0GA1UEAxMmV2VsbHMgRmFyZ28gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0GCSqG
-SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVqDM7Jvk0/82bfuUER84A4n135zHCLielTWi5MbqNQ1mX
-x3Oqfz1cQJ4F5aHiidlMuD+b+Qy0yGIZLEWukR5zcUHESxP9cMIlrCL1dQu3U+SlK93OvRw6esP3
-E48mVJwWa2uv+9iWsWCaSOAlIiR5NM4OJgALTqv9i86C1y8IcGjBqAr5dE8Hq6T54oN+J3N0Prj5
-OEL8pahbSCOz6+MlsoCultQKnMJ4msZoGK43YjdeUXWoWGPAUe5AeH6orxqg4bB4nVCMe+ez/I4j
-sNtlAHCEAQgAFG5Uhpq6zPk3EPbg3oQtnaSFN9OH4xXQwReQfhkhahKpdv0SAulPIV4XAgMBAAGj
-YTBfMA8GA1UdEwEB/wQFMAMBAf8wTAYDVR0gBEUwQzBBBgtghkgBhvt7hwcBCzAyMDAGCCsGAQUF
-BwIBFiRodHRwOi8vd3d3LndlbGxzZmFyZ28uY29tL2NlcnRwb2xpY3kwDQYJKoZIhvcNAQEFBQAD
-ggEBANIn3ZwKdyu7IvICtUpKkfnRLb7kuxpo7w6kAOnu5+/u9vnldKTC2FJYxHT7zmu1Oyl5GFrv
-m+0fazbuSCUlFLZWohDo7qd/0D+j0MNdJu4HzMPBJCGHHt8qElNvQRbn7a6U+oxy+hNH8Dx+rn0R
-OhPs7fpvcmR7nX1/Jv16+yWt6j4pf0zjAFcysLPp7VMX2YuyFA4w6OXVE8Zkr8QA1dhYJPz1j+zx
-x32l2w8n0cbyQIjmH/ZhqPRCyLk306m+LFZ4wnKbWV01QIroTmMatukgalHizqSQ33ZwmVxwQ023
-tqcZZE6St8WRPH9IFmV7Fv3L/PvZ1dZPIWU7Sn9Ho/s=
------END CERTIFICATE-----
-
-Swisscom Root CA 1
-==================
------BEGIN CERTIFICATE-----
-MIIF2TCCA8GgAwIBAgIQXAuFXAvnWUHfV8w/f52oNjANBgkqhkiG9w0BAQUFADBkMQswCQYDVQQG
-EwJjaDERMA8GA1UEChMIU3dpc3Njb20xJTAjBgNVBAsTHERpZ2l0YWwgQ2VydGlmaWNhdGUgU2Vy
-dmljZXMxGzAZBgNVBAMTElN3aXNzY29tIFJvb3QgQ0EgMTAeFw0wNTA4MTgxMjA2MjBaFw0yNTA4
-MTgyMjA2MjBaMGQxCzAJBgNVBAYTAmNoMREwDwYDVQQKEwhTd2lzc2NvbTElMCMGA1UECxMcRGln
-aXRhbCBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczEbMBkGA1UEAxMSU3dpc3Njb20gUm9vdCBDQSAxMIIC
-IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0LmwqAzZuz8h+BvVM5OAFmUgdbI9m2BtRsiM
-MW8Xw/qabFbtPMWRV8PNq5ZJkCoZSx6jbVfd8StiKHVFXqrWW/oLJdihFvkcxC7mlSpnzNApbjyF
-NDhhSbEAn9Y6cV9Nbc5fuankiX9qUvrKm/LcqfmdmUc/TilftKaNXXsLmREDA/7n29uj/x2lzZAe
-AR81sH8A25Bvxn570e56eqeqDFdvpG3FEzuwpdntMhy0XmeLVNxzh+XTF3xmUHJd1BpYwdnP2IkC
-b6dJtDZd0KTeByy2dbcokdaXvij1mB7qWybJvbCXc9qukSbraMH5ORXWZ0sKbU/Lz7DkQnGMU3nn
-7uHbHaBuHYwadzVcFh4rUx80i9Fs/PJnB3r1re3WmquhsUvhzDdf/X/NTa64H5xD+SpYVUNFvJbN
-cA78yeNmuk6NO4HLFWR7uZToXTNShXEuT46iBhFRyePLoW4xCGQMwtI89Tbo19AOeCMgkckkKmUp
-WyL3Ic6DXqTz3kvTaI9GdVyDCW4pa8RwjPWd1yAv/0bSKzjCL3UcPX7ape8eYIVpQtPM+GP+HkM5
-haa2Y0EQs3MevNP6yn0WR+Kn1dCjigoIlmJWbjTb2QK5MHXjBNLnj8KwEUAKrNVxAmKLMb7dxiNY
-MUJDLXT5xp6mig/p/r+D5kNXJLrvRjSq1xIBOO0CAwEAAaOBhjCBgzAOBgNVHQ8BAf8EBAMCAYYw
-HQYDVR0hBBYwFDASBgdghXQBUwABBgdghXQBUwABMBIGA1UdEwEB/wQIMAYBAf8CAQcwHwYDVR0j
-BBgwFoAUAyUv3m+CATpcLNwroWm1Z9SM0/0wHQYDVR0OBBYEFAMlL95vggE6XCzcK6FptWfUjNP9
-MA0GCSqGSIb3DQEBBQUAA4ICAQA1EMvspgQNDQ/NwNurqPKIlwzfky9NfEBWMXrrpA9gzXrzvsMn
-jgM+pN0S734edAY8PzHyHHuRMSG08NBsl9Tpl7IkVh5WwzW9iAUPWxAaZOHHgjD5Mq2eUCzneAXQ
-MbFamIp1TpBcahQq4FJHgmDmHtqBsfsUC1rxn9KVuj7QG9YVHaO+htXbD8BJZLsuUBlL0iT43R4H
-VtA4oJVwIHaM190e3p9xxCPvgxNcoyQVTSlAPGrEqdi3pkSlDfTgnXceQHAm/NrZNuR55LU/vJtl
-vrsRls/bxig5OgjOR1tTWsWZ/l2p3e9M1MalrQLmjAcSHm8D0W+go/MpvRLHUKKwf4ipmXeascCl
-OS5cfGniLLDqN2qk4Vrh9VDlg++luyqI54zb/W1elxmofmZ1a3Hqv7HHb6D0jqTsNFFbjCYDcKF3
-1QESVwA12yPeDooomf2xEG9L/zgtYE4snOtnta1J7ksfrK/7DZBaZmBwXarNeNQk7shBoJMBkpxq
-nvy5JMWzFYJ+vq6VK+uxwNrjAWALXmmshFZhvnEX/h0TD/7Gh0Xp/jKgGg0TpJRVcaUWi7rKibCy
-x/yP2FS1k2Kdzs9Z+z0YzirLNRWCXf9UIltxUvu3yf5gmwBBZPCqKuy2QkPOiWaByIufOVQDJdMW
-NY6E0F/6MBr1mmz0DlP5OlvRHA==
------END CERTIFICATE-----
-
-DigiCert Assured ID Root CA
-===========================
------BEGIN CERTIFICATE-----
-MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQG
-EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw
-IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzEx
-MTEwMDAwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL
-ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0Ew
-ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7cJpSIqvTO
-9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYPmDI2dsze3Tyoou9q+yHy
-UmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW
-/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpy
-oeb6pNnVFzF1roV9Iq4/AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whf
-GHdPAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRF
-66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzANBgkq
-hkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRCdWKuh+vy1dneVrOfzM4UKLkNl2Bc
-EkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTffwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38Fn
-SbNd67IJKusm7Xi+fT8r87cmNW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i
-8b5QZ7dsvfPxH2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe
-+o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g==
------END CERTIFICATE-----
-
-DigiCert Global Root CA
-=======================
------BEGIN CERTIFICATE-----
-MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQG
-EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw
-HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAw
-MDAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
-dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkq
-hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsBCSDMAZOn
-TjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97nh6Vfe63SKMI2tavegw5
-BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt43C/dxC//AH2hdmoRBBYMql1GNXRor5H
-4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7PT19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y
-7vrTC0LUq7dBMtoM1O/4gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQAB
-o2MwYTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbRTLtm
-8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUwDQYJKoZIhvcNAQEF
-BQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/EsrhMAtudXH/vTBH1jLuG2cenTnmCmr
-EbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIt
-tep3Sp+dWOIrWcBAI+0tKIJFPnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886
-UAb3LujEV0lsYSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk
-CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=
------END CERTIFICATE-----
-
-DigiCert High Assurance EV Root CA
-==================================
------BEGIN CERTIFICATE-----
-MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBsMQswCQYDVQQG
-EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSsw
-KQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5jZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAw
-MFoXDTMxMTExMDAwMDAwMFowbDELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ
-MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFu
-Y2UgRVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm+9S75S0t
-Mqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTWPNt0OKRKzE0lgvdKpVMS
-OO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEMxChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3
-MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFBIk5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQ
-NAQTXKFx01p8VdteZOE3hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUe
-h10aUAsgEsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMB
-Af8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaAFLE+w2kD+L9HAdSY
-JhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3NecnzyIZgYIVyHbIUf4KmeqvxgydkAQ
-V8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6zeM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFp
-myPInngiK3BD41VHMWEZ71jFhS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkK
-mNEVX58Svnw2Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe
-vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep+OkuE6N36B9K
------END CERTIFICATE-----
-
-Certplus Class 2 Primary CA
-===========================
------BEGIN CERTIFICATE-----
-MIIDkjCCAnqgAwIBAgIRAIW9S/PY2uNp9pTXX8OlRCMwDQYJKoZIhvcNAQEFBQAwPTELMAkGA1UE
-BhMCRlIxETAPBgNVBAoTCENlcnRwbHVzMRswGQYDVQQDExJDbGFzcyAyIFByaW1hcnkgQ0EwHhcN
-OTkwNzA3MTcwNTAwWhcNMTkwNzA2MjM1OTU5WjA9MQswCQYDVQQGEwJGUjERMA8GA1UEChMIQ2Vy
-dHBsdXMxGzAZBgNVBAMTEkNsYXNzIDIgUHJpbWFyeSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP
-ADCCAQoCggEBANxQltAS+DXSCHh6tlJw/W/uz7kRy1134ezpfgSN1sxvc0NXYKwzCkTsA18cgCSR
-5aiRVhKC9+Ar9NuuYS6JEI1rbLqzAr3VNsVINyPi8Fo3UjMXEuLRYE2+L0ER4/YXJQyLkcAbmXuZ
-Vg2v7tK8R1fjeUl7NIknJITesezpWE7+Tt9avkGtrAjFGA7v0lPubNCdEgETjdyAYveVqUSISnFO
-YFWe2yMZeVYHDD9jC1yw4r5+FfyUM1hBOHTE4Y+L3yasH7WLO7dDWWuwJKZtkIvEcupdM5i3y95e
-e++U8Rs+yskhwcWYAqqi9lt3m/V+llU0HGdpwPFC40es/CgcZlUCAwEAAaOBjDCBiTAPBgNVHRME
-CDAGAQH/AgEKMAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQU43Mt38sOKAze3bOkynm4jrvoMIkwEQYJ
-YIZIAYb4QgEBBAQDAgEGMDcGA1UdHwQwMC4wLKAqoCiGJmh0dHA6Ly93d3cuY2VydHBsdXMuY29t
-L0NSTC9jbGFzczIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQCnVM+IRBnL39R/AN9WM2K191EBkOvD
-P9GIROkkXe/nFL0gt5o8AP5tn9uQ3Nf0YtaLcF3n5QRIqWh8yfFC82x/xXp8HVGIutIKPidd3i1R
-TtMTZGnkLuPT55sJmabglZvOGtd/vjzOUrMRFcEPF80Du5wlFbqidon8BvEY0JNLDnyCt6X09l/+
-7UCmnYR0ObncHoUW2ikbhiMAybuJfm6AiB4vFLQDJKgybwOaRywwvlbGp0ICcBvqQNi6BQNwB6SW
-//1IMwrh3KWBkJtN3X3n57LNXMhqlfil9o3EXXgIvnsG1knPGTZQIy4I5p4FTUcY1Rbpsda2ENW7
-l7+ijrRU
------END CERTIFICATE-----
-
-DST Root CA X3
-==============
------BEGIN CERTIFICATE-----
-MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/MSQwIgYDVQQK
-ExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4X
-DTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVowPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1
-cmUgVHJ1c3QgQ28uMRcwFQYDVQQDEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQAD
-ggEPADCCAQoCggEBAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmT
-rE4Orz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEqOLl5CjH9
-UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9bxiqKqy69cK3FCxolkHRy
-xXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40d
-utolucbY38EVAjqr2m7xPi71XAicPNaDaeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0T
-AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQ
-MA0GCSqGSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69ikug
-dB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXrAvHRAosZy5Q6XkjE
-GB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZzR8srzJmwN0jP41ZL9c8PDHIyh8bw
-RLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubS
-fZGL+T0yjWW06XyxV3bqxbYoOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
------END CERTIFICATE-----
-
-DST ACES CA X6
-==============
------BEGIN CERTIFICATE-----
-MIIECTCCAvGgAwIBAgIQDV6ZCtadt3js2AdWO4YV2TANBgkqhkiG9w0BAQUFADBbMQswCQYDVQQG
-EwJVUzEgMB4GA1UEChMXRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QxETAPBgNVBAsTCERTVCBBQ0VT
-MRcwFQYDVQQDEw5EU1QgQUNFUyBDQSBYNjAeFw0wMzExMjAyMTE5NThaFw0xNzExMjAyMTE5NTha
-MFsxCzAJBgNVBAYTAlVTMSAwHgYDVQQKExdEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdDERMA8GA1UE
-CxMIRFNUIEFDRVMxFzAVBgNVBAMTDkRTVCBBQ0VTIENBIFg2MIIBIjANBgkqhkiG9w0BAQEFAAOC
-AQ8AMIIBCgKCAQEAuT31LMmU3HWKlV1j6IR3dma5WZFcRt2SPp/5DgO0PWGSvSMmtWPuktKe1jzI
-DZBfZIGxqAgNTNj50wUoUrQBJcWVHAx+PhCEdc/BGZFjz+iokYi5Q1K7gLFViYsx+tC3dr5BPTCa
-pCIlF3PoHuLTrCq9Wzgh1SpL11V94zpVvddtawJXa+ZHfAjIgrrep4c9oW24MFbCswKBXy314pow
-GCi4ZtPLAZZv6opFVdbgnf9nKxcCpk4aahELfrd755jWjHZvwTvbUJN+5dCOHze4vbrGn2zpfDPy
-MjwmR/onJALJfh1biEITajV8fTXpLmaRcpPVMibEdPVTo7NdmvYJywIDAQABo4HIMIHFMA8GA1Ud
-EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgHGMB8GA1UdEQQYMBaBFHBraS1vcHNAdHJ1c3Rkc3Qu
-Y29tMGIGA1UdIARbMFkwVwYKYIZIAWUDAgEBATBJMEcGCCsGAQUFBwIBFjtodHRwOi8vd3d3LnRy
-dXN0ZHN0LmNvbS9jZXJ0aWZpY2F0ZXMvcG9saWN5L0FDRVMtaW5kZXguaHRtbDAdBgNVHQ4EFgQU
-CXIGThhDD+XWzMNqizF7eI+og7gwDQYJKoZIhvcNAQEFBQADggEBAKPYjtay284F5zLNAdMEA+V2
-5FYrnJmQ6AgwbN99Pe7lv7UkQIRJ4dEorsTCOlMwiPH1d25Ryvr/ma8kXxug/fKshMrfqfBfBC6t
-Fr8hlxCBPeP/h40y3JTlR4peahPJlJU90u7INJXQgNStMgiAVDzgvVJT11J8smk/f3rPanTK+gQq
-nExaBqXpIK1FZg9p8d2/6eMyi/rgwYZNcjwu2JN4Cir42NInPRmJX1p7ijvMDNpRrscL9yuwNwXs
-vFcj4jjSm2jzVhKIT0J8uDHEtdvkyCE06UgRNe76x5JXxZ805Mf29w4LTJxoeHtxMcfrHuBnQfO3
-oKfN5XozNmr6mis=
------END CERTIFICATE-----
-
-TURKTRUST Certificate Services Provider Root 1
-==============================================
------BEGIN CERTIFICATE-----
-MIID+zCCAuOgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBtzE/MD0GA1UEAww2VMOcUktUUlVTVCBF
-bGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxMQswCQYDVQQGDAJUUjEP
-MA0GA1UEBwwGQU5LQVJBMVYwVAYDVQQKDE0oYykgMjAwNSBUw5xSS1RSVVNUIEJpbGdpIMSwbGV0
-acWfaW0gdmUgQmlsacWfaW0gR8O8dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWeLjAeFw0wNTA1MTMx
-MDI3MTdaFw0xNTAzMjIxMDI3MTdaMIG3MT8wPQYDVQQDDDZUw5xSS1RSVVNUIEVsZWt0cm9uaWsg
-U2VydGlmaWthIEhpem1ldCBTYcSfbGF5xLFjxLFzxLExCzAJBgNVBAYMAlRSMQ8wDQYDVQQHDAZB
-TktBUkExVjBUBgNVBAoMTShjKSAyMDA1IFTDnFJLVFJVU1QgQmlsZ2kgxLBsZXRpxZ9pbSB2ZSBC
-aWxpxZ9pbSBHw7x2ZW5sacSfaSBIaXptZXRsZXJpIEEuxZ4uMIIBIjANBgkqhkiG9w0BAQEFAAOC
-AQ8AMIIBCgKCAQEAylIF1mMD2Bxf3dJ7XfIMYGFbazt0K3gNfUW9InTojAPBxhEqPZW8qZSwu5GX
-yGl8hMW0kWxsE2qkVa2kheiVfrMArwDCBRj1cJ02i67L5BuBf5OI+2pVu32Fks66WJ/bMsW9Xe8i
-Si9BB35JYbOG7E6mQW6EvAPs9TscyB/C7qju6hJKjRTP8wrgUDn5CDX4EVmt5yLqS8oUBt5CurKZ
-8y1UiBAG6uEaPj1nH/vO+3yC6BFdSsG5FOpU2WabfIl9BJpiyelSPJ6c79L1JuTm5Rh8i27fbMx4
-W09ysstcP4wFjdFMjK2Sx+F4f2VsSQZQLJ4ywtdKxnWKWU51b0dewQIDAQABoxAwDjAMBgNVHRME
-BTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAV9VX/N5aAWSGk/KEVTCD21F/aAyT8z5Aa9CEKmu46
-sWrv7/hg0Uw2ZkUd82YCdAR7kjCo3gp2D++Vbr3JN+YaDayJSFvMgzbC9UZcWYJWtNX+I7TYVBxE
-q8Sn5RTOPEFhfEPmzcSBCYsk+1Ql1haolgxnB2+zUEfjHCQo3SqYpGH+2+oSN7wBGjSFvW5P55Fy
-B0SFHljKVETd96y5y4khctuPwGkplyqjrhgjlxxBKot8KsF8kOipKMDTkcatKIdAaLX/7KfS0zgY
-nNN9aV3wxqUeJBujR/xpB2jn5Jq07Q+hh4cCzofSSE7hvP/L8XKSRGQDJereW26fyfJOrN3H
------END CERTIFICATE-----
-
-TURKTRUST Certificate Services Provider Root 2
-==============================================
------BEGIN CERTIFICATE-----
-MIIEPDCCAySgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBvjE/MD0GA1UEAww2VMOcUktUUlVTVCBF
-bGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxMQswCQYDVQQGEwJUUjEP
-MA0GA1UEBwwGQW5rYXJhMV0wWwYDVQQKDFRUw5xSS1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUg
-QmlsacWfaW0gR8O8dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWeLiAoYykgS2FzxLFtIDIwMDUwHhcN
-MDUxMTA3MTAwNzU3WhcNMTUwOTE2MTAwNzU3WjCBvjE/MD0GA1UEAww2VMOcUktUUlVTVCBFbGVr
-dHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxMQswCQYDVQQGEwJUUjEPMA0G
-A1UEBwwGQW5rYXJhMV0wWwYDVQQKDFRUw5xSS1RSVVNUIEJpbGdpIMSwbGV0acWfaW0gdmUgQmls
-acWfaW0gR8O8dmVubGnEn2kgSGl6bWV0bGVyaSBBLsWeLiAoYykgS2FzxLFtIDIwMDUwggEiMA0G
-CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCpNn7DkUNMwxmYCMjHWHtPFoylzkkBH3MOrHUTpvqe
-LCDe2JAOCtFp0if7qnefJ1Il4std2NiDUBd9irWCPwSOtNXwSadktx4uXyCcUHVPr+G1QRT0mJKI
-x+XlZEdhR3n9wFHxwZnn3M5q+6+1ATDcRhzviuyV79z/rxAc653YsKpqhRgNF8k+v/Gb0AmJQv2g
-QrSdiVFVKc8bcLyEVK3BEx+Y9C52YItdP5qtygy/p1Zbj3e41Z55SZI/4PGXJHpsmxcPbe9TmJEr
-5A++WXkHeLuXlfSfadRYhwqp48y2WBmfJiGxxFmNskF1wK1pzpwACPI2/z7woQ8arBT9pmAPAgMB
-AAGjQzBBMB0GA1UdDgQWBBTZN7NOBf3Zz58SFq62iS/rJTqIHDAPBgNVHQ8BAf8EBQMDBwYAMA8G
-A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAHJglrfJ3NgpXiOFX7KzLXb7iNcX/ntt
-Rbj2hWyfIvwqECLsqrkw9qtY1jkQMZkpAL2JZkH7dN6RwRgLn7Vhy506vvWolKMiVW4XSf/SKfE4
-Jl3vpao6+XF75tpYHdN0wgH6PmlYX63LaL4ULptswLbcoCb6dxriJNoaN+BnrdFzgw2lGh1uEpJ+
-hGIAF728JRhX8tepb1mIvDS3LoV4nZbcFMMsilKbloxSZj2GFotHuFEJjOp9zYhys2AzsfAKRO8P
-9Qk3iCQOLGsgOqL6EfJANZxEaGM7rDNvY7wsu/LSy3Z9fYjYHcgFHW68lKlmjHdxx/qR+i9Rnuk5
-UrbnBEI=
------END CERTIFICATE-----
-
-SwissSign Platinum CA - G2
-==========================
------BEGIN CERTIFICATE-----
-MIIFwTCCA6mgAwIBAgIITrIAZwwDXU8wDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UEBhMCQ0gxFTAT
-BgNVBAoTDFN3aXNzU2lnbiBBRzEjMCEGA1UEAxMaU3dpc3NTaWduIFBsYXRpbnVtIENBIC0gRzIw
-HhcNMDYxMDI1MDgzNjAwWhcNMzYxMDI1MDgzNjAwWjBJMQswCQYDVQQGEwJDSDEVMBMGA1UEChMM
-U3dpc3NTaWduIEFHMSMwIQYDVQQDExpTd2lzc1NpZ24gUGxhdGludW0gQ0EgLSBHMjCCAiIwDQYJ
-KoZIhvcNAQEBBQADggIPADCCAgoCggIBAMrfogLi2vj8Bxax3mCq3pZcZB/HL37PZ/pEQtZ2Y5Wu
-669yIIpFR4ZieIbWIDkm9K6j/SPnpZy1IiEZtzeTIsBQnIJ71NUERFzLtMKfkr4k2HtnIuJpX+UF
-eNSH2XFwMyVTtIc7KZAoNppVRDBopIOXfw0enHb/FZ1glwCNioUD7IC+6ixuEFGSzH7VozPY1kne
-WCqv9hbrS3uQMpe5up1Y8fhXSQQeol0GcN1x2/ndi5objM89o03Oy3z2u5yg+gnOI2Ky6Q0f4nIo
-j5+saCB9bzuohTEJfwvH6GXp43gOCWcwizSC+13gzJ2BbWLuCB4ELE6b7P6pT1/9aXjvCR+htL/6
-8++QHkwFix7qepF6w9fl+zC8bBsQWJj3Gl/QKTIDE0ZNYWqFTFJ0LwYfexHihJfGmfNtf9dng34T
-aNhxKFrYzt3oEBSa/m0jh26OWnA81Y0JAKeqvLAxN23IhBQeW71FYyBrS3SMvds6DsHPWhaPpZjy
-domyExI7C3d3rLvlPClKknLKYRorXkzig3R3+jVIeoVNjZpTxN94ypeRSCtFKwH3HBqi7Ri6Cr2D
-+m+8jVeTO9TUps4e8aCxzqv9KyiaTxvXw3LbpMS/XUz13XuWae5ogObnmLo2t/5u7Su9IPhlGdpV
-CX4l3P5hYnL5fhgC72O00Puv5TtjjGePAgMBAAGjgawwgakwDgYDVR0PAQH/BAQDAgEGMA8GA1Ud
-EwEB/wQFMAMBAf8wHQYDVR0OBBYEFFCvzAeHFUdvOMW0ZdHelarp35zMMB8GA1UdIwQYMBaAFFCv
-zAeHFUdvOMW0ZdHelarp35zMMEYGA1UdIAQ/MD0wOwYJYIV0AVkBAQEBMC4wLAYIKwYBBQUHAgEW
-IGh0dHA6Ly9yZXBvc2l0b3J5LnN3aXNzc2lnbi5jb20vMA0GCSqGSIb3DQEBBQUAA4ICAQAIhab1
-Fgz8RBrBY+D5VUYI/HAcQiiWjrfFwUF1TglxeeVtlspLpYhg0DB0uMoI3LQwnkAHFmtllXcBrqS3
-NQuB2nEVqXQXOHtYyvkv+8Bldo1bAbl93oI9ZLi+FHSjClTTLJUYFzX1UWs/j6KWYTl4a0vlpqD4
-U99REJNi54Av4tHgvI42Rncz7Lj7jposiU0xEQ8mngS7twSNC/K5/FqdOxa3L8iYq/6KUFkuozv8
-KV2LwUvJ4ooTHbG/u0IdUt1O2BReEMYxB+9xJ/cbOQncguqLs5WGXv312l0xpuAxtpTmREl0xRbl
-9x8DYSjFyMsSoEJL+WuICI20MhjzdZ/EfwBPBZWcoxcCw7NTm6ogOSkrZvqdr16zktK1puEa+S1B
-aYEUtLS17Yk9zvupnTVCRLEcFHOBzyoBNZox1S2PbYTfgE1X4z/FhHXaicYwu+uPyyIIoK6q8QNs
-OktNCaUOcsZWayFCTiMlFGiudgp8DAdwZPmaL/YFOSbGDI8Zf0NebvRbFS/bYV3mZy8/CJT5YLSY
-Mdp08YSTcU1f+2BY0fvEwW2JorsgH51xkcsymxM9Pn2SUjWskpSi0xjCfMfqr3YFFt1nJ8J+HAci
-IfNAChs0B0QTwoRqjt8ZWr9/6x3iGjjRXK9HkmuAtTClyY3YqzGBH9/CZjfTk6mFhnll0g==
------END CERTIFICATE-----
-
-SwissSign Gold CA - G2
-======================
------BEGIN CERTIFICATE-----
-MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNVBAYTAkNIMRUw
-EwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2lnbiBHb2xkIENBIC0gRzIwHhcN
-MDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBFMQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dp
-c3NTaWduIEFHMR8wHQYDVQQDExZTd2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0B
-AQEFAAOCAg8AMIICCgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUq
-t2/876LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+bbqBHH5C
-jCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c6bM8K8vzARO/Ws/BtQpg
-vd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqEemA8atufK+ze3gE/bk3lUIbLtK/tREDF
-ylqM2tIrfKjuvqblCqoOpd8FUrdVxyJdMmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvR
-AiTysybUa9oEVeXBCsdtMDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuend
-jIj3o02yMszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69yFGkO
-peUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPiaG59je883WX0XaxR
-7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxMgI93e2CaHt+28kgeDrpOVG2Y4OGi
-GqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw
-AwEB/zAdBgNVHQ4EFgQUWyV7lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64
-OfPAeGZe6Drn8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov
-L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe645R88a7A3hfm
-5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczOUYrHUDFu4Up+GC9pWbY9ZIEr
-44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOf
-Mke6UiI0HTJ6CVanfCU2qT1L2sCCbwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6m
-Gu6uLftIdxf+u+yvGPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxp
-mo/a77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCChdiDyyJk
-vC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid392qgQmwLOM7XdVAyksLf
-KzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEppLd6leNcG2mqeSz53OiATIgHQv2ieY2Br
-NU0LbbqhPcCT4H8js1WtciVORvnSFu+wZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6Lqj
-viOvrv1vA+ACOzB2+httQc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ
------END CERTIFICATE-----
-
-SwissSign Silver CA - G2
-========================
------BEGIN CERTIFICATE-----
-MIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCQ0gxFTAT
-BgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMB4X
-DTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0NlowRzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3
-aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG
-9w0BAQEFAAOCAg8AMIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644
-N0MvFz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7brYT7QbNHm
-+/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieFnbAVlDLaYQ1HTWBCrpJH
-6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH6ATK72oxh9TAtvmUcXtnZLi2kUpCe2Uu
-MGoM9ZDulebyzYLs2aFK7PayS+VFheZteJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5h
-qAaEuSh6XzjZG6k4sIN/c8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5
-FZGkECwJMoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRHHTBs
-ROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTfjNFusB3hB48IHpmc
-celM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb65i/4z3GcRm25xBWNOHkDRUjvxF3X
-CO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/
-BAUwAwEB/zAdBgNVHQ4EFgQUF6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRB
-tjpbO8tFnb0cwpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0
-cDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBAHPGgeAn0i0P
-4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShpWJHckRE1qTodvBqlYJ7YH39F
-kWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9xCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L
-3XWgwF15kIwb4FDm3jH+mHtwX6WQ2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx
-/uNncqCxv1yL5PqZIseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFa
-DGi8aRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2Xem1ZqSqP
-e97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQRdAtq/gsD/KNVV4n+Ssuu
-WxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/OMpXEA29MC/HpeZBoNquBYeaoKRlbEwJ
-DIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ub
-DgEj8Z+7fNzcbBGXJbLytGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u
------END CERTIFICATE-----
-
-GeoTrust Primary Certification Authority
-========================================
------BEGIN CERTIFICATE-----
-MIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQG
-EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMoR2VvVHJ1c3QgUHJpbWFyeSBD
-ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjExMjcwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMFgx
-CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTEwLwYDVQQDEyhHZW9UcnVzdCBQ
-cmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
-CgKCAQEAvrgVe//UfH1nrYNke8hCUy3f9oQIIGHWAVlqnEQRr+92/ZV+zmEwu3qDXwK9AWbK7hWN
-b6EwnL2hhZ6UOvNWiAAxz9juapYC2e0DjPt1befquFUWBRaa9OBesYjAZIVcFU2Ix7e64HXprQU9
-nceJSOC7KMgD4TCTZF5SwFlwIjVXiIrxlQqD17wxcwE07e9GceBrAqg1cmuXm2bgyxx5X9gaBGge
-RwLmnWDiNpcB3841kt++Z8dtd1k7j53WkBWUvEI0EME5+bEnPn7WinXFsq+W06Lem+SYvn3h6YGt
-tm/81w7a4DSwDRp35+MImO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD
-AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQULNVQQZcVi/CPNmFbSvtr2ZnJM5IwDQYJKoZI
-hvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqSa+S7iq8XEN3GHHoOo0Hnp3DwQ16CePbJC/kRYkRj5K
-Ts4rFtULUh38H2eiAkUxT87z+gOneZ1TatnaYzr4gNfTmeGl4b7UVXGYNTq+k+qurUKykG/g/CFN
-NWMziUnWm07Kx+dOCQD32sfvmWKZd7aVIl6KoKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gZHa
-Floxt/m0cYASSJlyc1pZU8FjUjPtp8nSOQJw+uCxQmYpqptR7TBUIhRf2asdweSU8Pj1K/fqynhG
-1riR/aYNKxoUAT6A8EKglQdebc3MS6RFjasS6LPeWuWgfOgPIh1a6Vk=
------END CERTIFICATE-----
-
-thawte Primary Root CA
-======================
------BEGIN CERTIFICATE-----
-MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCBqTELMAkGA1UE
-BhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2
-aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhv
-cml6ZWQgdXNlIG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3
-MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwg
-SW5jLjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMv
-KGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNVBAMT
-FnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCs
-oPD7gFnUnMekz52hWXMJEEUMDSxuaPFsW0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ
-1CRfBsDMRJSUjQJib+ta3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGc
-q/gcfomk6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6Sk/K
-aAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94JNqR32HuHUETVPm4p
-afs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD
-VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XPr87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUF
-AAOCAQEAeRHAS7ORtvzw6WfUDW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeE
-uzLlQRHAd9mzYJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX
-xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2/qxAeeWsEG89
-jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/LHbTY5xZ3Y+m4Q6gLkH3LpVH
-z7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7jVaMaA==
------END CERTIFICATE-----
-
-VeriSign Class 3 Public Primary Certification Authority - G5
-============================================================
------BEGIN CERTIFICATE-----
-MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCByjELMAkGA1UE
-BhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBO
-ZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVk
-IHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRp
-ZmljYXRpb24gQXV0aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCB
-yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2ln
-biBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2lnbiwgSW5jLiAtIEZvciBh
-dXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmlt
-YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
-ggEKAoIBAQCvJAgIKXo1nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKz
-j/i5Vbext0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIzSdhD
-Y2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQGBO+QueQA5N06tRn/
-Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+rCpSx4/VBEnkjWNHiDxpg8v+R70r
-fk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/
-BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2Uv
-Z2lmMCEwHzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy
-aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKvMzEzMA0GCSqG
-SIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzEp6B4Eq1iDkVwZMXnl2YtmAl+
-X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKE
-KQsTb47bDN0lAtukixlE0kF6BWlKWE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiC
-Km0oHw0LxOXnGiYZ4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vE
-ZV8NhnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq
------END CERTIFICATE-----
-
-SecureTrust CA
-==============
------BEGIN CERTIFICATE-----
-MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBIMQswCQYDVQQG
-EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xFzAVBgNVBAMTDlNlY3VyZVRy
-dXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIzMTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAe
-BgNVBAoTF1NlY3VyZVRydXN0IENvcnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCC
-ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQX
-OZEzZum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO0gMdA+9t
-DWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIaowW8xQmxSPmjL8xk037uH
-GFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b
-01k/unK8RCSc43Oz969XL0Imnal0ugBS8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmH
-ursCAwEAAaOBnTCBmjATBgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/
-BAUwAwEB/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCegJYYj
-aHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQAwDQYJ
-KoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt36Z3q059c4EVlew3KW+JwULKUBRSu
-SceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHf
-mbx8IVQr5Fiiu1cprp6poxkmD5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZ
-nMUFdAvnZyPSCPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR
-3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE=
------END CERTIFICATE-----
-
-Secure Global CA
-================
------BEGIN CERTIFICATE-----
-MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBKMQswCQYDVQQG
-EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBH
-bG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkxMjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEg
-MB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwg
-Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jx
-YDiJiQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa/FHtaMbQ
-bqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJjnIFHovdRIWCQtBJwB1g
-8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnIHmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYV
-HDGA76oYa8J719rO+TMg1fW9ajMtgQT7sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi
-0XPnj3pDAgMBAAGjgZ0wgZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud
-EwEB/wQFMAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCswKaAn
-oCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsGAQQBgjcVAQQDAgEA
-MA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0LURYD7xh8yOOvaliTFGCRsoTciE6+
-OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXOH0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cn
-CDpOGR86p1hcF895P4vkp9MmI50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/5
-3CYNv6ZHdAbYiNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc
-f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW
------END CERTIFICATE-----
-
-COMODO Certification Authority
-==============================
------BEGIN CERTIFICATE-----
-MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCBgTELMAkGA1UE
-BhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG
-A1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNVBAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1
-dGhvcml0eTAeFw0wNjEyMDEwMDAwMDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEb
-MBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFD
-T01PRE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0aG9yaXR5
-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3UcEbVASY06m/weaKXTuH
-+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI2GqGd0S7WWaXUF601CxwRM/aN5VCaTww
-xHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV
-4EajcNxo2f8ESIl33rXp+2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA
-1KGzqSX+DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5OnKVI
-rLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW/zAOBgNVHQ8BAf8E
-BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6gPKA6hjhodHRwOi8vY3JsLmNvbW9k
-b2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOC
-AQEAPpiem/Yb6dc5t3iuHXIYSdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CP
-OGEIqB6BCsAvIC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/
-RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4zJVSk/BwJVmc
-IGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5ddBA6+C4OmF4O5MBKgxTMVBbkN
-+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IBZQ==
------END CERTIFICATE-----
-
-Network Solutions Certificate Authority
-=======================================
------BEGIN CERTIFICATE-----
-MIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQG
-EwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydOZXR3b3Jr
-IFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMx
-MjM1OTU5WjBiMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu
-MTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0G
-CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwzc7MEL7xx
-jOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPPOCwGJgl6cvf6UDL4wpPT
-aaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rlmGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXT
-crA/vGp97Eh/jcOrqnErU2lBUzS1sLnFBgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc
-/Qzpf14Dl847ABSHJ3A4qY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMB
-AAGjgZcwgZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIBBjAP
-BgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwubmV0c29sc3NsLmNv
-bS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3JpdHkuY3JsMA0GCSqGSIb3DQEBBQUA
-A4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc86fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q
-4LqILPxFzBiwmZVRDuwduIj/h1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/
-GGUsyfJj4akH/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv
-wKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHNpGxlaKFJdlxD
-ydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey
------END CERTIFICATE-----
-
-WellsSecure Public Root Certificate Authority
-=============================================
------BEGIN CERTIFICATE-----
-MIIEvTCCA6WgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBhTELMAkGA1UEBhMCVVMxIDAeBgNVBAoM
-F1dlbGxzIEZhcmdvIFdlbGxzU2VjdXJlMRwwGgYDVQQLDBNXZWxscyBGYXJnbyBCYW5rIE5BMTYw
-NAYDVQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcN
-MDcxMjEzMTcwNzU0WhcNMjIxMjE0MDAwNzU0WjCBhTELMAkGA1UEBhMCVVMxIDAeBgNVBAoMF1dl
-bGxzIEZhcmdvIFdlbGxzU2VjdXJlMRwwGgYDVQQLDBNXZWxscyBGYXJnbyBCYW5rIE5BMTYwNAYD
-VQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0G
-CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDub7S9eeKPCCGeOARBJe+rWxxTkqxtnt3CxC5FlAM1
-iGd0V+PfjLindo8796jE2yljDpFoNoqXjopxaAkH5OjUDk/41itMpBb570OYj7OeUt9tkTmPOL13
-i0Nj67eT/DBMHAGTthP796EfvyXhdDcsHqRePGj4S78NuR4uNuip5Kf4D8uCdXw1LSLWwr8L87T8
-bJVhHlfXBIEyg1J55oNjz7fLY4sR4r1e6/aN7ZVyKLSsEmLpSjPmgzKuBXWVvYSV2ypcm44uDLiB
-K0HmOFafSZtsdvqKXfcBeYF8wYNABf5x/Qw/zE5gCQ5lRxAvAcAFP4/4s0HvWkJ+We/SlwxlAgMB
-AAGjggE0MIIBMDAPBgNVHRMBAf8EBTADAQH/MDkGA1UdHwQyMDAwLqAsoCqGKGh0dHA6Ly9jcmwu
-cGtpLndlbGxzZmFyZ28uY29tL3dzcHJjYS5jcmwwDgYDVR0PAQH/BAQDAgHGMB0GA1UdDgQWBBQm
-lRkQ2eihl5H/3BnZtQQ+0nMKajCBsgYDVR0jBIGqMIGngBQmlRkQ2eihl5H/3BnZtQQ+0nMKaqGB
-i6SBiDCBhTELMAkGA1UEBhMCVVMxIDAeBgNVBAoMF1dlbGxzIEZhcmdvIFdlbGxzU2VjdXJlMRww
-GgYDVQQLDBNXZWxscyBGYXJnbyBCYW5rIE5BMTYwNAYDVQQDDC1XZWxsc1NlY3VyZSBQdWJsaWMg
-Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHmCAQEwDQYJKoZIhvcNAQEFBQADggEBALkVsUSRzCPI
-K0134/iaeycNzXK7mQDKfGYZUMbVmO2rvwNa5U3lHshPcZeG1eMd/ZDJPHV3V3p9+N701NX3leZ0
-bh08rnyd2wIDBSxxSyU+B+NemvVmFymIGjifz6pBA4SXa5M4esowRBskRDPQ5NHcKDj0E0M1NSlj
-qHyita04pO2t/caaH/+Xc/77szWnk4bGdpEA5qxRFsQnMlzbc9qlk1eOPm01JghZ1edE13YgY+es
-E2fDbbFwRnzVlhE9iW9dqKHrjQrawx0zbKPqZxmamX9LPYNRKh3KL4YMon4QLSvUFpULB6ouFJJJ
-tylv2G0xffX8oRAHh84vWdw+WNs=
------END CERTIFICATE-----
-
-COMODO ECC Certification Authority
-==================================
------BEGIN CERTIFICATE-----
-MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTELMAkGA1UEBhMC
-R0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE
-ChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBB
-dXRob3JpdHkwHhcNMDgwMzA2MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0Ix
-GzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR
-Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRo
-b3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSRFtSrYpn1PlILBs5BAH+X
-4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0JcfRK9ChQtP6IHG4/bC8vCVlbpVsLM5ni
-wz2J+Wos77LTBumjQjBAMB0GA1UdDgQWBBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8E
-BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VG
-FAkK+qDmfQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdvGDeA
-U/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY=
------END CERTIFICATE-----
-
-IGC/A
-=====
------BEGIN CERTIFICATE-----
-MIIEAjCCAuqgAwIBAgIFORFFEJQwDQYJKoZIhvcNAQEFBQAwgYUxCzAJBgNVBAYTAkZSMQ8wDQYD
-VQQIEwZGcmFuY2UxDjAMBgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQTS9TR0ROMQ4wDAYDVQQLEwVE
-Q1NTSTEOMAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2LmZy
-MB4XDTAyMTIxMzE0MjkyM1oXDTIwMTAxNzE0MjkyMlowgYUxCzAJBgNVBAYTAkZSMQ8wDQYDVQQI
-EwZGcmFuY2UxDjAMBgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQTS9TR0ROMQ4wDAYDVQQLEwVEQ1NT
-STEOMAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2LmZyMIIB
-IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsh/R0GLFMzvABIaIs9z4iPf930Pfeo2aSVz2
-TqrMHLmh6yeJ8kbpO0px1R2OLc/mratjUMdUC24SyZA2xtgv2pGqaMVy/hcKshd+ebUyiHDKcMCW
-So7kVc0dJ5S/znIq7Fz5cyD+vfcuiWe4u0dzEvfRNWk68gq5rv9GQkaiv6GFGvm/5P9JhfejcIYy
-HF2fYPepraX/z9E0+X1bF8bc1g4oa8Ld8fUzaJ1O/Id8NhLWo4DoQw1VYZTqZDdH6nfK0LJYBcNd
-frGoRpAxVs5wKpayMLh35nnAvSk7/ZR3TL0gzUEl4C7HG7vupARB0l2tEmqKm0f7yd1GQOGdPDPQ
-tQIDAQABo3cwdTAPBgNVHRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBRjAVBgNVHSAEDjAMMAoGCCqB
-egF5AQEBMB0GA1UdDgQWBBSjBS8YYFDCiQrdKyFP/45OqDAxNjAfBgNVHSMEGDAWgBSjBS8YYFDC
-iQrdKyFP/45OqDAxNjANBgkqhkiG9w0BAQUFAAOCAQEABdwm2Pp3FURo/C9mOnTgXeQp/wYHE4RK
-q89toB9RlPhJy3Q2FLwV3duJL92PoF189RLrn544pEfMs5bZvpwlqwN+Mw+VgQ39FuCIvjfwbF3Q
-MZsyK10XZZOYYLxuj7GoPB7ZHPOpJkL5ZB3C55L29B5aqhlSXa/oovdgoPaN8In1buAKBQGVyYsg
-Crpa/JosPL3Dt8ldeCUFP1YUmwza+zpI/pdpXsoQhvdOlgQITeywvl3cO45Pwf2aNjSaTFR+FwNI
-lQgRHAdvhQh+XU3Endv7rs6y0bO4g2wdsrN58dhwmX7wEwLOXt1R0982gaEbeC9xs/FZTEYYKKuF
-0mBWWg==
------END CERTIFICATE-----
-
-Security Communication EV RootCA1
-=================================
------BEGIN CERTIFICATE-----
-MIIDfTCCAmWgAwIBAgIBADANBgkqhkiG9w0BAQUFADBgMQswCQYDVQQGEwJKUDElMCMGA1UEChMc
-U0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEqMCgGA1UECxMhU2VjdXJpdHkgQ29tbXVuaWNh
-dGlvbiBFViBSb290Q0ExMB4XDTA3MDYwNjAyMTIzMloXDTM3MDYwNjAyMTIzMlowYDELMAkGA1UE
-BhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKjAoBgNVBAsTIVNl
-Y3VyaXR5IENvbW11bmljYXRpb24gRVYgUm9vdENBMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
-AQoCggEBALx/7FebJOD+nLpCeamIivqA4PUHKUPqjgo0No0c+qe1OXj/l3X3L+SqawSERMqm4miO
-/VVQYg+kcQ7OBzgtQoVQrTyWb4vVog7P3kmJPdZkLjjlHmy1V4qe70gOzXppFodEtZDkBp2uoQSX
-WHnvIEqCa4wiv+wfD+mEce3xDuS4GBPMVjZd0ZoeUWs5bmB2iDQL87PRsJ3KYeJkHcFGB7hj3R4z
-ZbOOCVVSPbW9/wfrrWFVGCypaZhKqkDFMxRldAD5kd6vA0jFQFTcD4SQaCDFkpbcLuUCRarAX1T4
-bepJz11sS6/vmsJWXMY1VkJqMF/Cq/biPT+zyRGPMUzXn0kCAwEAAaNCMEAwHQYDVR0OBBYEFDVK
-9U2vP9eCOKyrcWUXdYydVZPmMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqG
-SIb3DQEBBQUAA4IBAQCoh+ns+EBnXcPBZsdAS5f8hxOQWsTvoMpfi7ent/HWtWS3irO4G8za+6xm
-iEHO6Pzk2x6Ipu0nUBsCMCRGef4Eh3CXQHPRwMFXGZpppSeZq51ihPZRwSzJIxXYKLerJRO1RuGG
-Av8mjMSIkh1W/hln8lXkgKNrnKt34VFxDSDbEJrbvXZ5B3eZKK2aXtqxT0QsNY6llsf9g/BYxnnW
-mHyojf6GPgcWkuF75x3sM3Z+Qi5KhfmRiWiEA4Glm5q+4zfFVKtWOxgtQaQM+ELbmaDgcm+7XeEW
-T1MKZPlO9L9OVL14bIjqv5wTJMJwaaJ/D8g8rQjJsJhAoyrniIPtd490
------END CERTIFICATE-----
-
-OISTE WISeKey Global Root GA CA
-===============================
------BEGIN CERTIFICATE-----
-MIID8TCCAtmgAwIBAgIQQT1yx/RrH4FDffHSKFTfmjANBgkqhkiG9w0BAQUFADCBijELMAkGA1UE
-BhMCQ0gxEDAOBgNVBAoTB1dJU2VLZXkxGzAZBgNVBAsTEkNvcHlyaWdodCAoYykgMjAwNTEiMCAG
-A1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBH
-bG9iYWwgUm9vdCBHQSBDQTAeFw0wNTEyMTExNjAzNDRaFw0zNzEyMTExNjA5NTFaMIGKMQswCQYD
-VQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEbMBkGA1UECxMSQ29weXJpZ2h0IChjKSAyMDA1MSIw
-IAYDVQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5
-IEdsb2JhbCBSb290IEdBIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy0+zAJs9
-Nt350UlqaxBJH+zYK7LG+DKBKUOVTJoZIyEVRd7jyBxRVVuuk+g3/ytr6dTqvirdqFEr12bDYVxg
-Asj1znJ7O7jyTmUIms2kahnBAbtzptf2w93NvKSLtZlhuAGio9RN1AU9ka34tAhxZK9w8RxrfvbD
-d50kc3vkDIzh2TbhmYsFmQvtRTEJysIA2/dyoJaqlYfQjse2YXMNdmaM3Bu0Y6Kff5MTMPGhJ9vZ
-/yxViJGg4E8HsChWjBgbl0SOid3gF27nKu+POQoxhILYQBRJLnpB5Kf+42TMwVlxSywhp1t94B3R
-LoGbw9ho972WG6xwsRYUC9tguSYBBQIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw
-AwEB/zAdBgNVHQ4EFgQUswN+rja8sHnR3JQmthG+IbJphpQwEAYJKwYBBAGCNxUBBAMCAQAwDQYJ
-KoZIhvcNAQEFBQADggEBAEuh/wuHbrP5wUOxSPMowB0uyQlB+pQAHKSkq0lPjz0e701vvbyk9vIm
-MMkQyh2I+3QZH4VFvbBsUfk2ftv1TDI6QU9bR8/oCy22xBmddMVHxjtqD6wU2zz0c5ypBd8A3HR4
-+vg1YFkCExh8vPtNsCBtQ7tgMHpnM1zFmdH4LTlSc/uMqpclXHLZCB6rTjzjgTGfA6b7wP4piFXa
-hNVQA7bihKOmNqoROgHhGEvWRGizPflTdISzRpFGlgC3gCy24eMQ4tui5yiPAZZiFj4A4xylNoEY
-okxSdsARo27mHbrjWr42U8U+dY+GaSlYU7Wcu2+fXMUY7N0v4ZjJ/L7fCg0=
------END CERTIFICATE-----
-
-S-TRUST Authentication and Encryption Root CA 2005 PN
-=====================================================
------BEGIN CERTIFICATE-----
-MIIEezCCA2OgAwIBAgIQNxkY5lNUfBq1uMtZWts1tzANBgkqhkiG9w0BAQUFADCBrjELMAkGA1UE
-BhMCREUxIDAeBgNVBAgTF0JhZGVuLVd1ZXJ0dGVtYmVyZyAoQlcpMRIwEAYDVQQHEwlTdHV0dGdh
-cnQxKTAnBgNVBAoTIERldXRzY2hlciBTcGFya2Fzc2VuIFZlcmxhZyBHbWJIMT4wPAYDVQQDEzVT
-LVRSVVNUIEF1dGhlbnRpY2F0aW9uIGFuZCBFbmNyeXB0aW9uIFJvb3QgQ0EgMjAwNTpQTjAeFw0w
-NTA2MjIwMDAwMDBaFw0zMDA2MjEyMzU5NTlaMIGuMQswCQYDVQQGEwJERTEgMB4GA1UECBMXQmFk
-ZW4tV3VlcnR0ZW1iZXJnIChCVykxEjAQBgNVBAcTCVN0dXR0Z2FydDEpMCcGA1UEChMgRGV1dHNj
-aGVyIFNwYXJrYXNzZW4gVmVybGFnIEdtYkgxPjA8BgNVBAMTNVMtVFJVU1QgQXV0aGVudGljYXRp
-b24gYW5kIEVuY3J5cHRpb24gUm9vdCBDQSAyMDA1OlBOMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
-MIIBCgKCAQEA2bVKwdMz6tNGs9HiTNL1toPQb9UY6ZOvJ44TzbUlNlA0EmQpoVXhOmCTnijJ4/Ob
-4QSwI7+Vio5bG0F/WsPoTUzVJBY+h0jUJ67m91MduwwA7z5hca2/OnpYH5Q9XIHV1W/fuJvS9eXL
-g3KSwlOyggLrra1fFi2SU3bxibYs9cEv4KdKb6AwajLrmnQDaHgTncovmwsdvs91DSaXm8f1Xgqf
-eN+zvOyauu9VjxuapgdjKRdZYgkqeQd3peDRF2npW932kKvimAoA0SVtnteFhy+S8dF2g08LOlk3
-KC8zpxdQ1iALCvQm+Z845y2kuJuJja2tyWp9iRe79n+Ag3rm7QIDAQABo4GSMIGPMBIGA1UdEwEB
-/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMCkGA1UdEQQiMCCkHjAcMRowGAYDVQQDExFTVFJv
-bmxpbmUxLTIwNDgtNTAdBgNVHQ4EFgQUD8oeXHngovMpttKFswtKtWXsa1IwHwYDVR0jBBgwFoAU
-D8oeXHngovMpttKFswtKtWXsa1IwDQYJKoZIhvcNAQEFBQADggEBAK8B8O0ZPCjoTVy7pWMciDMD
-pwCHpB8gq9Yc4wYfl35UvbfRssnV2oDsF9eK9XvCAPbpEW+EoFolMeKJ+aQAPzFoLtU96G7m1R08
-P7K9n3frndOMusDXtk3sU5wPBG7qNWdX4wple5A64U8+wwCSersFiXOMy6ZNwPv2AtawB6MDwidA
-nwzkhYItr5pCHdDHjfhA7p0GVxzZotiAFP7hYy0yh9WUUpY6RsZxlj33mA6ykaqP2vROJAA5Veit
-F7nTNCtKqUDMFypVZUF0Qn71wK/Ik63yGFs9iQzbRzkk+OBM8h+wPQrKBU6JIRrjKpms/H+h8Q8b
-Hz2eBIPdltkdOpQ=
------END CERTIFICATE-----
-
-Microsec e-Szigno Root CA
-=========================
------BEGIN CERTIFICATE-----
-MIIHqDCCBpCgAwIBAgIRAMy4579OKRr9otxmpRwsDxEwDQYJKoZIhvcNAQEFBQAwcjELMAkGA1UE
-BhMCSFUxETAPBgNVBAcTCEJ1ZGFwZXN0MRYwFAYDVQQKEw1NaWNyb3NlYyBMdGQuMRQwEgYDVQQL
-EwtlLVN6aWdubyBDQTEiMCAGA1UEAxMZTWljcm9zZWMgZS1Temlnbm8gUm9vdCBDQTAeFw0wNTA0
-MDYxMjI4NDRaFw0xNzA0MDYxMjI4NDRaMHIxCzAJBgNVBAYTAkhVMREwDwYDVQQHEwhCdWRhcGVz
-dDEWMBQGA1UEChMNTWljcm9zZWMgTHRkLjEUMBIGA1UECxMLZS1Temlnbm8gQ0ExIjAgBgNVBAMT
-GU1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
-AQDtyADVgXvNOABHzNuEwSFpLHSQDCHZU4ftPkNEU6+r+ICbPHiN1I2uuO/TEdyB5s87lozWbxXG
-d36hL+BfkrYn13aaHUM86tnsL+4582pnS4uCzyL4ZVX+LMsvfUh6PXX5qqAnu3jCBspRwn5mS6/N
-oqdNAoI/gqyFxuEPkEeZlApxcpMqyabAvjxWTHOSJ/FrtfX9/DAFYJLG65Z+AZHCabEeHXtTRbjc
-QR/Ji3HWVBTji1R4P770Yjtb9aPs1ZJ04nQw7wHb4dSrmZsqa/i9phyGI0Jf7Enemotb9HI6QMVJ
-PqW+jqpx62z69Rrkav17fVVA71hu5tnVvCSrwe+3AgMBAAGjggQ3MIIEMzBnBggrBgEFBQcBAQRb
-MFkwKAYIKwYBBQUHMAGGHGh0dHBzOi8vcmNhLmUtc3ppZ25vLmh1L29jc3AwLQYIKwYBBQUHMAKG
-IWh0dHA6Ly93d3cuZS1zemlnbm8uaHUvUm9vdENBLmNydDAPBgNVHRMBAf8EBTADAQH/MIIBcwYD
-VR0gBIIBajCCAWYwggFiBgwrBgEEAYGoGAIBAQEwggFQMCgGCCsGAQUFBwIBFhxodHRwOi8vd3d3
-LmUtc3ppZ25vLmh1L1NaU1ovMIIBIgYIKwYBBQUHAgIwggEUHoIBEABBACAAdABhAG4A+gBzAO0A
-dAB2AOEAbgB5ACAA6QByAHQAZQBsAG0AZQB6AOkAcwDpAGgAZQB6ACAA6QBzACAAZQBsAGYAbwBn
-AGEAZADhAHMA4QBoAG8AegAgAGEAIABTAHoAbwBsAGcA4QBsAHQAYQB0APMAIABTAHoAbwBsAGcA
-4QBsAHQAYQB0AOEAcwBpACAAUwB6AGEAYgDhAGwAeQB6AGEAdABhACAAcwB6AGUAcgBpAG4AdAAg
-AGsAZQBsAGwAIABlAGwAagDhAHIAbgBpADoAIABoAHQAdABwADoALwAvAHcAdwB3AC4AZQAtAHMA
-egBpAGcAbgBvAC4AaAB1AC8AUwBaAFMAWgAvMIHIBgNVHR8EgcAwgb0wgbqggbeggbSGIWh0dHA6
-Ly93d3cuZS1zemlnbm8uaHUvUm9vdENBLmNybIaBjmxkYXA6Ly9sZGFwLmUtc3ppZ25vLmh1L0NO
-PU1pY3Jvc2VjJTIwZS1Temlnbm8lMjBSb290JTIwQ0EsT1U9ZS1Temlnbm8lMjBDQSxPPU1pY3Jv
-c2VjJTIwTHRkLixMPUJ1ZGFwZXN0LEM9SFU/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdDtiaW5h
-cnkwDgYDVR0PAQH/BAQDAgEGMIGWBgNVHREEgY4wgYuBEGluZm9AZS1zemlnbm8uaHWkdzB1MSMw
-IQYDVQQDDBpNaWNyb3NlYyBlLVN6aWduw7MgUm9vdCBDQTEWMBQGA1UECwwNZS1TemlnbsOzIEhT
-WjEWMBQGA1UEChMNTWljcm9zZWMgS2Z0LjERMA8GA1UEBxMIQnVkYXBlc3QxCzAJBgNVBAYTAkhV
-MIGsBgNVHSMEgaQwgaGAFMegSXUWYYTbMUuE0vE3QJDvTtz3oXakdDByMQswCQYDVQQGEwJIVTER
-MA8GA1UEBxMIQnVkYXBlc3QxFjAUBgNVBAoTDU1pY3Jvc2VjIEx0ZC4xFDASBgNVBAsTC2UtU3pp
-Z25vIENBMSIwIAYDVQQDExlNaWNyb3NlYyBlLVN6aWdubyBSb290IENBghEAzLjnv04pGv2i3Gal
-HCwPETAdBgNVHQ4EFgQUx6BJdRZhhNsxS4TS8TdAkO9O3PcwDQYJKoZIhvcNAQEFBQADggEBANMT
-nGZjWS7KXHAM/IO8VbH0jgdsZifOwTsgqRy7RlRw7lrMoHfqaEQn6/Ip3Xep1fvj1KcExJW4C+FE
-aGAHQzAxQmHl7tnlJNUb3+FKG6qfx1/4ehHqE5MAyopYse7tDk2016g2JnzgOsHVV4Lxdbb9iV/a
-86g4nzUGCM4ilb7N1fy+W955a9x6qWVmvrElWl/tftOsRm1M9DKHtCAE4Gx4sHfRhUZLphK3dehK
-yVZs15KrnfVJONJPU+NVkBHbmJbGSfI+9J8b4PeI3CVimUTYc78/MPMMNz7UwiiAc7EBt51alhQB
-S6kRnSlqLtBdgcDPsiBDxwPgN05dCtxZICU=
------END CERTIFICATE-----
-
-Certigna
-========
------BEGIN CERTIFICATE-----
-MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNVBAYTAkZSMRIw
-EAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4XDTA3MDYyOTE1MTMwNVoXDTI3
-MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwI
-Q2VydGlnbmEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7q
-XOEm7RFHYeGifBZ4QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyH
-GxnygQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbwzBfsV1/p
-ogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q130yGLMLLGq/jj8UEYkg
-DncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKf
-Irjxwo1p3Po6WAbfAgMBAAGjgbwwgbkwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQ
-tCRZvgHyUtVF9lo53BEwZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJ
-BgNVBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzjAQ/J
-SP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG9w0BAQUFAAOCAQEA
-hQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8hbV6lUmPOEvjvKtpv6zf+EwLHyzs+
-ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFncfca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1klu
-PBS1xp81HlDQwY9qcEQCYsuuHWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY
-1gkIl2PlwS6wt0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw
-WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg==
------END CERTIFICATE-----
-
-AC Ra\xC3\xADz Certic\xC3\xA1mara S.A.
-======================================
------BEGIN CERTIFICATE-----
-MIIGZjCCBE6gAwIBAgIPB35Sk3vgFeNX8GmMy+wMMA0GCSqGSIb3DQEBBQUAMHsxCzAJBgNVBAYT
-AkNPMUcwRQYDVQQKDD5Tb2NpZWRhZCBDYW1lcmFsIGRlIENlcnRpZmljYWNpw7NuIERpZ2l0YWwg
-LSBDZXJ0aWPDoW1hcmEgUy5BLjEjMCEGA1UEAwwaQUMgUmHDrXogQ2VydGljw6FtYXJhIFMuQS4w
-HhcNMDYxMTI3MjA0NjI5WhcNMzAwNDAyMjE0MjAyWjB7MQswCQYDVQQGEwJDTzFHMEUGA1UECgw+
-U29jaWVkYWQgQ2FtZXJhbCBkZSBDZXJ0aWZpY2FjacOzbiBEaWdpdGFsIC0gQ2VydGljw6FtYXJh
-IFMuQS4xIzAhBgNVBAMMGkFDIFJhw616IENlcnRpY8OhbWFyYSBTLkEuMIICIjANBgkqhkiG9w0B
-AQEFAAOCAg8AMIICCgKCAgEAq2uJo1PMSCMI+8PPUZYILrgIem08kBeGqentLhM0R7LQcNzJPNCN
-yu5LF6vQhbCnIwTLqKL85XXbQMpiiY9QngE9JlsYhBzLfDe3fezTf3MZsGqy2IiKLUV0qPezuMDU
-2s0iiXRNWhU5cxh0T7XrmafBHoi0wpOQY5fzp6cSsgkiBzPZkc0OnB8OIMfuuzONj8LSWKdf/WU3
-4ojC2I+GdV75LaeHM/J4Ny+LvB2GNzmxlPLYvEqcgxhaBvzz1NS6jBUJJfD5to0EfhcSM2tXSExP
-2yYe68yQ54v5aHxwD6Mq0Do43zeX4lvegGHTgNiRg0JaTASJaBE8rF9ogEHMYELODVoqDA+bMMCm
-8Ibbq0nXl21Ii/kDwFJnmxL3wvIumGVC2daa49AZMQyth9VXAnow6IYm+48jilSH5L887uvDdUhf
-HjlvgWJsxS3EF1QZtzeNnDeRyPYL1epjb4OsOMLzP96a++EjYfDIJss2yKHzMI+ko6Kh3VOz3vCa
-Mh+DkXkwwakfU5tTohVTP92dsxA7SH2JD/ztA/X7JWR1DhcZDY8AFmd5ekD8LVkH2ZD6mq093ICK
-5lw1omdMEWux+IBkAC1vImHFrEsm5VoQgpukg3s0956JkSCXjrdCx2bD0Omk1vUgjcTDlaxECp1b
-czwmPS9KvqfJpxAe+59QafMCAwEAAaOB5jCB4zAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE
-AwIBBjAdBgNVHQ4EFgQU0QnQ6dfOeXRU+Tows/RtLAMDG2gwgaAGA1UdIASBmDCBlTCBkgYEVR0g
-ADCBiTArBggrBgEFBQcCARYfaHR0cDovL3d3dy5jZXJ0aWNhbWFyYS5jb20vZHBjLzBaBggrBgEF
-BQcCAjBOGkxMaW1pdGFjaW9uZXMgZGUgZ2FyYW507WFzIGRlIGVzdGUgY2VydGlmaWNhZG8gc2Ug
-cHVlZGVuIGVuY29udHJhciBlbiBsYSBEUEMuMA0GCSqGSIb3DQEBBQUAA4ICAQBclLW4RZFNjmEf
-AygPU3zmpFmps4p6xbD/CHwso3EcIRNnoZUSQDWDg4902zNc8El2CoFS3UnUmjIz75uny3XlesuX
-EpBcunvFm9+7OSPI/5jOCk0iAUgHforA1SBClETvv3eiiWdIG0ADBaGJ7M9i4z0ldma/Jre7Ir5v
-/zlXdLp6yQGVwZVR6Kss+LGGIOk/yzVb0hfpKv6DExdA7ohiZVvVO2Dpezy4ydV/NgIlqmjCMRW3
-MGXrfx1IebHPOeJCgBbT9ZMj/EyXyVo3bHwi2ErN0o42gzmRkBDI8ck1fj+404HGIGQatlDCIaR4
-3NAvO2STdPCWkPHv+wlaNECW8DYSwaN0jJN+Qd53i+yG2dIPPy3RzECiiWZIHiCznCNZc6lEc7wk
-eZBWN7PGKX6jD/EpOe9+XCgycDWs2rjIdWb8m0w5R44bb5tNAlQiM+9hup4phO9OSzNHdpdqy35f
-/RWmnkJDW2ZaiogN9xa5P1FlK2Zqi9E4UqLWRhH6/JocdJ6PlwsCT2TG9WjTSy3/pDceiz+/RL5h
-RqGEPQgnTIEgd4kI6mdAXmwIUV80WoyWaM3X94nCHNMyAK9Sy9NgWyo6R35rMDOhYil/SrnhLecU
-Iw4OGEfhefwVVdCx/CVxY3UzHCMrr1zZ7Ud3YA47Dx7SwNxkBYn8eNZcLCZDqQ==
------END CERTIFICATE-----
-
-TC TrustCenter Class 2 CA II
-============================
------BEGIN CERTIFICATE-----
-MIIEqjCCA5KgAwIBAgIOLmoAAQACH9dSISwRXDswDQYJKoZIhvcNAQEFBQAwdjELMAkGA1UEBhMC
-REUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxIjAgBgNVBAsTGVRDIFRydXN0Q2VudGVy
-IENsYXNzIDIgQ0ExJTAjBgNVBAMTHFRDIFRydXN0Q2VudGVyIENsYXNzIDIgQ0EgSUkwHhcNMDYw
-MTEyMTQzODQzWhcNMjUxMjMxMjI1OTU5WjB2MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1
-c3RDZW50ZXIgR21iSDEiMCAGA1UECxMZVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMiBDQTElMCMGA1UE
-AxMcVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMiBDQSBJSTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
-AQoCggEBAKuAh5uO8MN8h9foJIIRszzdQ2Lu+MNF2ujhoF/RKrLqk2jftMjWQ+nEdVl//OEd+DFw
-IxuInie5e/060smp6RQvkL4DUsFJzfb95AhmC1eKokKguNV/aVyQMrKXDcpK3EY+AlWJU+MaWss2
-xgdW94zPEfRMuzBwBJWl9jmM/XOBCH2JXjIeIqkiRUuwZi4wzJ9l/fzLganx4Duvo4bRierERXlQ
-Xa7pIXSSTYtZgo+U4+lK8edJsBTj9WLL1XK9H7nSn6DNqPoByNkN39r8R52zyFTfSUrxIan+GE7u
-SNQZu+995OKdy1u2bv/jzVrndIIFuoAlOMvkaZ6vQaoahPUCAwEAAaOCATQwggEwMA8GA1UdEwEB
-/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTjq1RMgKHbVkO3kUrL84J6E1wIqzCB
-7QYDVR0fBIHlMIHiMIHfoIHcoIHZhjVodHRwOi8vd3d3LnRydXN0Y2VudGVyLmRlL2NybC92Mi90
-Y19jbGFzc18yX2NhX0lJLmNybIaBn2xkYXA6Ly93d3cudHJ1c3RjZW50ZXIuZGUvQ049VEMlMjBU
-cnVzdENlbnRlciUyMENsYXNzJTIwMiUyMENBJTIwSUksTz1UQyUyMFRydXN0Q2VudGVyJTIwR21i
-SCxPVT1yb290Y2VydHMsREM9dHJ1c3RjZW50ZXIsREM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9u
-TGlzdD9iYXNlPzANBgkqhkiG9w0BAQUFAAOCAQEAjNfffu4bgBCzg/XbEeprS6iSGNn3Bzn1LL4G
-dXpoUxUc6krtXvwjshOg0wn/9vYua0Fxec3ibf2uWWuFHbhOIprtZjluS5TmVfwLG4t3wVMTZonZ
-KNaL80VKY7f9ewthXbhtvsPcW3nS7Yblok2+XnR8au0WOB9/WIFaGusyiC2y8zl3gK9etmF1Kdsj
-TYjKUCjLhdLTEKJZbtOTVAB6okaVhgWcqRmY5TFyDADiZ9lA4CQze28suVyrZZ0srHbqNZn1l7kP
-JOzHdiEoZa5X6AeIdUpWoNIFOqTmjZKILPPy4cHGYdtBxceb9w4aUUXCYWvcZCcXjFq32nQozZfk
-vQ==
------END CERTIFICATE-----
-
-TC TrustCenter Class 3 CA II
-============================
------BEGIN CERTIFICATE-----
-MIIEqjCCA5KgAwIBAgIOSkcAAQAC5aBd1j8AUb8wDQYJKoZIhvcNAQEFBQAwdjELMAkGA1UEBhMC
-REUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxIjAgBgNVBAsTGVRDIFRydXN0Q2VudGVy
-IENsYXNzIDMgQ0ExJTAjBgNVBAMTHFRDIFRydXN0Q2VudGVyIENsYXNzIDMgQ0EgSUkwHhcNMDYw
-MTEyMTQ0MTU3WhcNMjUxMjMxMjI1OTU5WjB2MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1
-c3RDZW50ZXIgR21iSDEiMCAGA1UECxMZVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQTElMCMGA1UE
-AxMcVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQSBJSTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
-AQoCggEBALTgu1G7OVyLBMVMeRwjhjEQY0NVJz/GRcekPewJDRoeIMJWHt4bNwcwIi9v8Qbxq63W
-yKthoy9DxLCyLfzDlml7forkzMA5EpBCYMnMNWju2l+QVl/NHE1bWEnrDgFPZPosPIlY2C8u4rBo
-6SI7dYnWRBpl8huXJh0obazovVkdKyT21oQDZogkAHhg8fir/gKya/si+zXmFtGt9i4S5Po1auUZ
-uV3bOx4a+9P/FRQI2AlqukWdFHlgfa9Aigdzs5OW03Q0jTo3Kd5c7PXuLjHCINy+8U9/I1LZW+Jk
-2ZyqBwi1Rb3R0DHBq1SfqdLDYmAD8bs5SpJKPQq5ncWg/jcCAwEAAaOCATQwggEwMA8GA1UdEwEB
-/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTUovyfs8PYA9NXXAek0CSnwPIA1DCB
-7QYDVR0fBIHlMIHiMIHfoIHcoIHZhjVodHRwOi8vd3d3LnRydXN0Y2VudGVyLmRlL2NybC92Mi90
-Y19jbGFzc18zX2NhX0lJLmNybIaBn2xkYXA6Ly93d3cudHJ1c3RjZW50ZXIuZGUvQ049VEMlMjBU
-cnVzdENlbnRlciUyMENsYXNzJTIwMyUyMENBJTIwSUksTz1UQyUyMFRydXN0Q2VudGVyJTIwR21i
-SCxPVT1yb290Y2VydHMsREM9dHJ1c3RjZW50ZXIsREM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9u
-TGlzdD9iYXNlPzANBgkqhkiG9w0BAQUFAAOCAQEANmDkcPcGIEPZIxpC8vijsrlNirTzwppVMXzE
-O2eatN9NDoqTSheLG43KieHPOh6sHfGcMrSOWXaiQYUlN6AT0PV8TtXqluJucsG7Kv5sbviRmEb8
-yRtXW+rIGjs/sFGYPAfaLFkB2otE6OF0/ado3VS6g0bsyEa1+K+XwDsJHI/OcpY9M1ZwvJbL2NV9
-IJqDnxrcOfHFcqMRA/07QlIp2+gB95tejNaNhk4Z+rwcvsUhpYeeeC422wlxo3I0+GzjBgnyXlal
-092Y+tTmBvTwtiBjS+opvaqCZh77gaqnN60TGOaSw4HBM7uIHqHn4rS9MWwOUT1v+5ZWgOI2F9Hc
-5A==
------END CERTIFICATE-----
-
-TC TrustCenter Universal CA I
-=============================
------BEGIN CERTIFICATE-----
-MIID3TCCAsWgAwIBAgIOHaIAAQAC7LdggHiNtgYwDQYJKoZIhvcNAQEFBQAweTELMAkGA1UEBhMC
-REUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxJDAiBgNVBAsTG1RDIFRydXN0Q2VudGVy
-IFVuaXZlcnNhbCBDQTEmMCQGA1UEAxMdVEMgVHJ1c3RDZW50ZXIgVW5pdmVyc2FsIENBIEkwHhcN
-MDYwMzIyMTU1NDI4WhcNMjUxMjMxMjI1OTU5WjB5MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMg
-VHJ1c3RDZW50ZXIgR21iSDEkMCIGA1UECxMbVEMgVHJ1c3RDZW50ZXIgVW5pdmVyc2FsIENBMSYw
-JAYDVQQDEx1UQyBUcnVzdENlbnRlciBVbml2ZXJzYWwgQ0EgSTCCASIwDQYJKoZIhvcNAQEBBQAD
-ggEPADCCAQoCggEBAKR3I5ZEr5D0MacQ9CaHnPM42Q9e3s9B6DGtxnSRJJZ4Hgmgm5qVSkr1YnwC
-qMqs+1oEdjneX/H5s7/zA1hV0qq34wQi0fiU2iIIAI3TfCZdzHd55yx4Oagmcw6iXSVphU9VDprv
-xrlE4Vc93x9UIuVvZaozhDrzznq+VZeujRIPFDPiUHDDSYcTvFHe15gSWu86gzOSBnWLknwSaHtw
-ag+1m7Z3W0hZneTvWq3zwZ7U10VOylY0Ibw+F1tvdwxIAUMpsN0/lm7mlaoMwCC2/T42J5zjXM9O
-gdwZu5GQfezmlwQek8wiSdeXhrYTCjxDI3d+8NzmzSQfO4ObNDqDNOMCAwEAAaNjMGEwHwYDVR0j
-BBgwFoAUkqR1LKSevoFE63n8isWVpesQdXMwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
-AYYwHQYDVR0OBBYEFJKkdSyknr6BROt5/IrFlaXrEHVzMA0GCSqGSIb3DQEBBQUAA4IBAQAo0uCG
-1eb4e/CX3CJrO5UUVg8RMKWaTzqwOuAGy2X17caXJ/4l8lfmXpWMPmRgFVp/Lw0BxbFg/UU1z/Cy
-vwbZ71q+s2IhtNerNXxTPqYn8aEt2hojnczd7Dwtnic0XQ/CNnm8yUpiLe1r2X1BQ3y2qsrtYbE3
-ghUJGooWMNjsydZHcnhLEEYUjl8Or+zHL6sQ17bxbuyGssLoDZJz3KL0Dzq/YSMQiZxIQG5wALPT
-ujdEWBF6AmqI8Dc08BnprNRlc/ZpjGSUOnmFKbAWKwyCPwacx/0QK54PLLae4xW/2TYcuiUaUj0a
-7CIMHOCkoj3w6DnPgcB77V0fb8XQC9eY
------END CERTIFICATE-----
-
-Deutsche Telekom Root CA 2
-==========================
------BEGIN CERTIFICATE-----
-MIIDnzCCAoegAwIBAgIBJjANBgkqhkiG9w0BAQUFADBxMQswCQYDVQQGEwJERTEcMBoGA1UEChMT
-RGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxlU2VjIFRydXN0IENlbnRlcjEjMCEG
-A1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290IENBIDIwHhcNOTkwNzA5MTIxMTAwWhcNMTkwNzA5
-MjM1OTAwWjBxMQswCQYDVQQGEwJERTEcMBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0G
-A1UECxMWVC1UZWxlU2VjIFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBS
-b290IENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrC6M14IspFLEUha88EOQ5
-bzVdSq7d6mGNlUn0b2SjGmBmpKlAIoTZ1KXleJMOaAGtuU1cOs7TuKhCQN/Po7qCWWqSG6wcmtoI
-KyUn+WkjR/Hg6yx6m/UTAtB+NHzCnjwAWav12gz1MjwrrFDa1sPeg5TKqAyZMg4ISFZbavva4VhY
-AUlfckE8FQYBjl2tqriTtM2e66foai1SNNs671x1Udrb8zH57nGYMsRUFUQM+ZtV7a3fGAigo4aK
-Se5TBY8ZTNXeWHmb0mocQqvF1afPaA+W5OFhmHZhyJF81j4A4pFQh+GdCuatl9Idxjp9y7zaAzTV
-jlsB9WoHtxa2bkp/AgMBAAGjQjBAMB0GA1UdDgQWBBQxw3kbuvVT1xfgiXotF2wKsyudMzAPBgNV
-HRMECDAGAQH/AgEFMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAlGRZrTlk5ynr
-E/5aw4sTV8gEJPB0d8Bg42f76Ymmg7+Wgnxu1MM9756AbrsptJh6sTtU6zkXR34ajgv8HzFZMQSy
-zhfzLMdiNlXiItiJVbSYSKpk+tYcNthEeFpaIzpXl/V6ME+un2pMSyuOoAPjPuCp1NJ70rOo4nI8
-rZ7/gFnkm0W09juwzTkZmDLl6iFhkOQxIY40sfcvNUqFENrnijchvllj4PKFiDFT1FQUhXB59C4G
-dyd1Lx+4ivn+xbrYNuSD7Odlt79jWvNGr4GUN9RBjNYj1h7P9WgbRGOiWrqnNVmh5XAFmw4jV5mU
-Cm26OWMohpLzGITY+9HPBVZkVw==
------END CERTIFICATE-----
-
-ComSign CA
-==========
------BEGIN CERTIFICATE-----
-MIIDkzCCAnugAwIBAgIQFBOWgxRVjOp7Y+X8NId3RDANBgkqhkiG9w0BAQUFADA0MRMwEQYDVQQD
-EwpDb21TaWduIENBMRAwDgYDVQQKEwdDb21TaWduMQswCQYDVQQGEwJJTDAeFw0wNDAzMjQxMTMy
-MThaFw0yOTAzMTkxNTAyMThaMDQxEzARBgNVBAMTCkNvbVNpZ24gQ0ExEDAOBgNVBAoTB0NvbVNp
-Z24xCzAJBgNVBAYTAklMMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8ORUaSvTx49q
-ROR+WCf4C9DklBKK8Rs4OC8fMZwG1Cyn3gsqrhqg455qv588x26i+YtkbDqthVVRVKU4VbirgwTy
-P2Q298CNQ0NqZtH3FyrV7zb6MBBC11PN+fozc0yz6YQgitZBJzXkOPqUm7h65HkfM/sb2CEJKHxN
-GGleZIp6GZPKfuzzcuc3B1hZKKxC+cX/zT/npfo4sdAMx9lSGlPWgcxCejVb7Us6eva1jsz/D3zk
-YDaHL63woSV9/9JLEYhwVKZBqGdTUkJe5DSe5L6j7KpiXd3DTKaCQeQzC6zJMw9kglcq/QytNuEM
-rkvF7zuZ2SOzW120V+x0cAwqTwIDAQABo4GgMIGdMAwGA1UdEwQFMAMBAf8wPQYDVR0fBDYwNDAy
-oDCgLoYsaHR0cDovL2ZlZGlyLmNvbXNpZ24uY28uaWwvY3JsL0NvbVNpZ25DQS5jcmwwDgYDVR0P
-AQH/BAQDAgGGMB8GA1UdIwQYMBaAFEsBmz5WGmU2dst7l6qSBe4y5ygxMB0GA1UdDgQWBBRLAZs+
-VhplNnbLe5eqkgXuMucoMTANBgkqhkiG9w0BAQUFAAOCAQEA0Nmlfv4pYEWdfoPPbrxHbvUanlR2
-QnG0PFg/LUAlQvaBnPGJEMgOqnhPOAlXsDzACPw1jvFIUY0McXS6hMTXcpuEfDhOZAYnKuGntewI
-mbQKDdSFc8gS4TXt8QUxHXOZDOuWyt3T5oWq8Ir7dcHyCTxlZWTzTNity4hp8+SDtwy9F1qWF8pb
-/627HOkthIDYIb6FUtnUdLlphbpN7Sgy6/lhSuTENh4Z3G+EER+V9YMoGKgzkkMn3V0TBEVPh9VG
-zT2ouvDzuFYkRes3x+F2T3I5GN9+dHLHcy056mDmrRGiVod7w2ia/viMcKjfZTL0pECMocJEAw6U
-AGegcQCCSA==
------END CERTIFICATE-----
-
-ComSign Secured CA
-==================
------BEGIN CERTIFICATE-----
-MIIDqzCCApOgAwIBAgIRAMcoRwmzuGxFjB36JPU2TukwDQYJKoZIhvcNAQEFBQAwPDEbMBkGA1UE
-AxMSQ29tU2lnbiBTZWN1cmVkIENBMRAwDgYDVQQKEwdDb21TaWduMQswCQYDVQQGEwJJTDAeFw0w
-NDAzMjQxMTM3MjBaFw0yOTAzMTYxNTA0NTZaMDwxGzAZBgNVBAMTEkNvbVNpZ24gU2VjdXJlZCBD
-QTEQMA4GA1UEChMHQ29tU2lnbjELMAkGA1UEBhMCSUwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
-ggEKAoIBAQDGtWhfHZQVw6QIVS3joFd67+l0Kru5fFdJGhFeTymHDEjWaueP1H5XJLkGieQcPOqs
-49ohgHMhCu95mGwfCP+hUH3ymBvJVG8+pSjsIQQPRbsHPaHA+iqYHU4Gk/v1iDurX8sWv+bznkqH
-7Rnqwp9D5PGBpX8QTz7RSmKtUxvLg/8HZaWSLWapW7ha9B20IZFKF3ueMv5WJDmyVIRD9YTC2LxB
-kMyd1mja6YJQqTtoz7VdApRgFrFD2UNd3V2Hbuq7s8lr9gOUCXDeFhF6K+h2j0kQmHe5Y1yLM5d1
-9guMsqtb3nQgJT/j8xH5h2iGNXHDHYwt6+UarA9z1YJZQIDTAgMBAAGjgacwgaQwDAYDVR0TBAUw
-AwEB/zBEBgNVHR8EPTA7MDmgN6A1hjNodHRwOi8vZmVkaXIuY29tc2lnbi5jby5pbC9jcmwvQ29t
-U2lnblNlY3VyZWRDQS5jcmwwDgYDVR0PAQH/BAQDAgGGMB8GA1UdIwQYMBaAFMFL7XC29z58ADsA
-j8c+DkWfHl3sMB0GA1UdDgQWBBTBS+1wtvc+fAA7AI/HPg5Fnx5d7DANBgkqhkiG9w0BAQUFAAOC
-AQEAFs/ukhNQq3sUnjO2QiBq1BW9Cav8cujvR3qQrFHBZE7piL1DRYHjZiM/EoZNGeQFsOY3wo3a
-BijJD4mkU6l1P7CW+6tMM1X5eCZGbxs2mPtCdsGCuY7e+0X5YxtiOzkGynd6qDwJz2w2PQ8KRUtp
-FhpFfTMDZflScZAmlaxMDPWLkz/MdXSFmLr/YnpNH4n+rr2UAJm/EaXc4HnFFgt9AmEd6oX5AhVP
-51qJThRv4zdLhfXBPGHg/QVBspJ/wx2g0K5SZGBrGMYmnNj1ZOQ2GmKfig8+/21OGVZOIJFsnzQz
-OjRXUDpvgV4GxvU+fE6OK85lBi5d0ipTdF7Tbieejw==
------END CERTIFICATE-----
-
-Cybertrust Global Root
-======================
------BEGIN CERTIFICATE-----
-MIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYGA1UEChMPQ3li
-ZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBSb290MB4XDTA2MTIxNTA4
-MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQD
-ExZDeWJlcnRydXN0IEdsb2JhbCBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
-+Mi8vRRQZhP/8NN57CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW
-0ozSJ8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2yHLtgwEZL
-AfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iPt3sMpTjr3kfb1V05/Iin
-89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNzFtApD0mpSPCzqrdsxacwOUBdrsTiXSZT
-8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAYXSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAP
-BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2
-MDSgMqAwhi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3JsMB8G
-A1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUAA4IBAQBW7wojoFRO
-lZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMjWqd8BfP9IjsO0QbE2zZMcwSO5bAi
-5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUxXOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2
-hO0j9n0Hq0V+09+zv+mKts2oomcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+T
-X3EJIrduPuocA06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW
-WL1WMRJOEcgh4LMRkWXbtKaIOM5V
------END CERTIFICATE-----
-
-ePKI Root Certification Authority
-=================================
------BEGIN CERTIFICATE-----
-MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBeMQswCQYDVQQG
-EwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0ZC4xKjAoBgNVBAsMIWVQS0kg
-Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMx
-MjdaMF4xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEq
-MCgGA1UECwwhZVBLSSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0B
-AQEFAAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAHSyZbCUNs
-IZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAhijHyl3SJCRImHJ7K2RKi
-lTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3XDZoTM1PRYfl61dd4s5oz9wCGzh1NlDiv
-qOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX
-12ruOzjjK9SXDrkb5wdJfzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0O
-WQqraffAsgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uUWH1+
-ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLSnT0IFaUQAS2zMnao
-lQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pHdmX2Os+PYhcZewoozRrSgx4hxyy/
-vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJipNiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXi
-Zo1jDiVN1Rmy5nk3pyKdVDECAwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/Qkqi
-MAwGA1UdEwQFMAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH
-ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGBuvl2ICO1J2B0
-1GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6YlPwZpVnPDimZI+ymBV3QGypzq
-KOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkPJXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdV
-xrsStZf0X4OFunHB2WyBEXYKCrC/gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEP
-NXubrjlpC2JgQCA2j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+r
-GNm65ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUBo2M3IUxE
-xJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS/jQ6fbjpKdx2qcgw+BRx
-gMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2zGp1iro2C6pSe3VkQw63d4k3jMdXH7Ojy
-sP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTEW9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmOD
-BCEIZ43ygknQW/2xzQ+DhNQ+IIX3Sj0rnP0qCglN6oH4EZw=
------END CERTIFICATE-----
-
-T\xc3\x9c\x42\xC4\xB0TAK UEKAE K\xC3\xB6k Sertifika Hizmet Sa\xC4\x9Flay\xc4\xb1\x63\xc4\xb1s\xc4\xb1 - S\xC3\xBCr\xC3\xBCm 3
-=============================================================================================================================
------BEGIN CERTIFICATE-----
-MIIFFzCCA/+gAwIBAgIBETANBgkqhkiG9w0BAQUFADCCASsxCzAJBgNVBAYTAlRSMRgwFgYDVQQH
-DA9HZWJ6ZSAtIEtvY2FlbGkxRzBFBgNVBAoMPlTDvHJraXllIEJpbGltc2VsIHZlIFRla25vbG9q
-aWsgQXJhxZ90xLFybWEgS3VydW11IC0gVMOcQsSwVEFLMUgwRgYDVQQLDD9VbHVzYWwgRWxla3Ry
-b25payB2ZSBLcmlwdG9sb2ppIEFyYcWfdMSxcm1hIEVuc3RpdMO8c8O8IC0gVUVLQUUxIzAhBgNV
-BAsMGkthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppMUowSAYDVQQDDEFUw5xCxLBUQUsgVUVLQUUg
-S8O2ayBTZXJ0aWZpa2EgSGl6bWV0IFNhxJ9sYXnEsWPEsXPEsSAtIFPDvHLDvG0gMzAeFw0wNzA4
-MjQxMTM3MDdaFw0xNzA4MjExMTM3MDdaMIIBKzELMAkGA1UEBhMCVFIxGDAWBgNVBAcMD0dlYnpl
-IC0gS29jYWVsaTFHMEUGA1UECgw+VMO8cmtpeWUgQmlsaW1zZWwgdmUgVGVrbm9sb2ppayBBcmHF
-n3TEsXJtYSBLdXJ1bXUgLSBUw5xCxLBUQUsxSDBGBgNVBAsMP1VsdXNhbCBFbGVrdHJvbmlrIHZl
-IEtyaXB0b2xvamkgQXJhxZ90xLFybWEgRW5zdGl0w7xzw7wgLSBVRUtBRTEjMCEGA1UECwwaS2Ft
-dSBTZXJ0aWZpa2FzeW9uIE1lcmtlemkxSjBIBgNVBAMMQVTDnELEsFRBSyBVRUtBRSBLw7ZrIFNl
-cnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxIC0gU8O8csO8bSAzMIIBIjANBgkqhkiG9w0B
-AQEFAAOCAQ8AMIIBCgKCAQEAim1L/xCIOsP2fpTo6iBkcK4hgb46ezzb8R1Sf1n68yJMlaCQvEhO
-Eav7t7WNeoMojCZG2E6VQIdhn8WebYGHV2yKO7Rm6sxA/OOqbLLLAdsyv9Lrhc+hDVXDWzhXcLh1
-xnnRFDDtG1hba+818qEhTsXOfJlfbLm4IpNQp81McGq+agV/E5wrHur+R84EpW+sky58K5+eeROR
-6Oqeyjh1jmKwlZMq5d/pXpduIF9fhHpEORlAHLpVK/swsoHvhOPc7Jg4OQOFCKlUAwUp8MmPi+oL
-hmUZEdPpCSPeaJMDyTYcIW7OjGbxmTDY17PDHfiBLqi9ggtm/oLL4eAagsNAgQIDAQABo0IwQDAd
-BgNVHQ4EFgQUvYiHyY/2pAoLquvF/pEjnatKijIwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF
-MAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAB18+kmPNOm3JpIWmgV050vQbTlswyb2zrgxvMTfvCr4
-N5EY3ATIZJkrGG2AA1nJrvhY0D7twyOfaTyGOBye79oneNGEN3GKPEs5z35FBtYt2IpNeBLWrcLT
-y9LQQfMmNkqblWwM7uXRQydmwYj3erMgbOqwaSvHIOgMA8RBBZniP+Rr+KCGgceExh/VS4ESshYh
-LBOhgLJeDEoTniDYYkCrkOpkSi+sDQESeUWoL4cZaMjihccwsnX5OD+ywJO0a+IDRM5noN+J1q2M
-dqMTw5RhK2vZbMEHCiIHhWyFJEapvj+LeISCfiQMnf2BN+MlqO02TpUsyZyQ2uypQjyttgI=
------END CERTIFICATE-----
-
-Buypass Class 2 CA 1
-====================
------BEGIN CERTIFICATE-----
-MIIDUzCCAjugAwIBAgIBATANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU
-QnV5cGFzcyBBUy05ODMxNjMzMjcxHTAbBgNVBAMMFEJ1eXBhc3MgQ2xhc3MgMiBDQSAxMB4XDTA2
-MTAxMzEwMjUwOVoXDTE2MTAxMzEwMjUwOVowSzELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBh
-c3MgQVMtOTgzMTYzMzI3MR0wGwYDVQQDDBRCdXlwYXNzIENsYXNzIDIgQ0EgMTCCASIwDQYJKoZI
-hvcNAQEBBQADggEPADCCAQoCggEBAIs8B0XY9t/mx8q6jUPFR42wWsE425KEHK8T1A9vNkYgxC7M
-cXA0ojTTNy7Y3Tp3L8DrKehc0rWpkTSHIln+zNvnma+WwajHQN2lFYxuyHyXA8vmIPLXl18xoS83
-0r7uvqmtqEyeIWZDO6i88wmjONVZJMHCR3axiFyCO7srpgTXjAePzdVBHfCuuCkslFJgNJQ72uA4
-0Z0zPhX0kzLFANq1KWYOOngPIVJfAuWSeyXTkh4vFZ2B5J2O6O+JzhRMVB0cgRJNcKi+EAUXfh/R
-uFdV7c27UsKwHnjCTTZoy1YmwVLBvXb3WNVyfh9EdrsAiR0WnVE1703CVu9r4Iw7DekCAwEAAaNC
-MEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUP42aWYv8e3uco684sDntkHGA1sgwDgYDVR0P
-AQH/BAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQAVGn4TirnoB6NLJzKyQJHyIdFkhb5jatLPgcIV
-1Xp+DCmsNx4cfHZSldq1fyOhKXdlyTKdqC5Wq2B2zha0jX94wNWZUYN/Xtm+DKhQ7SLHrQVMdvvt
-7h5HZPb3J31cKA9FxVxiXqaakZG3Uxcu3K1gnZZkOb1naLKuBctN518fV4bVIJwo+28TOPX2EZL2
-fZleHwzoq0QkKXJAPTZSr4xYkHPB7GEseaHsh7U/2k3ZIQAw3pDaDtMaSKk+hQsUi4y8QZ5q9w5w
-wDX3OaJdZtB7WZ+oRxKaJyOkLY4ng5IgodcVf/EuGO70SH8vf/GhGLWhC5SgYiAynB321O+/TIho
------END CERTIFICATE-----
-
-Buypass Class 3 CA 1
-====================
------BEGIN CERTIFICATE-----
-MIIDUzCCAjugAwIBAgIBAjANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU
-QnV5cGFzcyBBUy05ODMxNjMzMjcxHTAbBgNVBAMMFEJ1eXBhc3MgQ2xhc3MgMyBDQSAxMB4XDTA1
-MDUwOTE0MTMwM1oXDTE1MDUwOTE0MTMwM1owSzELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBh
-c3MgQVMtOTgzMTYzMzI3MR0wGwYDVQQDDBRCdXlwYXNzIENsYXNzIDMgQ0EgMTCCASIwDQYJKoZI
-hvcNAQEBBQADggEPADCCAQoCggEBAKSO13TZKWTeXx+HgJHqTjnmGcZEC4DVC69TB4sSveZn8AKx
-ifZgisRbsELRwCGoy+Gb72RRtqfPFfV0gGgEkKBYouZ0plNTVUhjP5JW3SROjvi6K//zNIqeKNc0
-n6wv1g/xpC+9UrJJhW05NfBEMJNGJPO251P7vGGvqaMU+8IXF4Rs4HyI+MkcVyzwPX6UvCWThOia
-AJpFBUJXgPROztmuOfbIUxAMZTpHe2DC1vqRycZxbL2RhzyRhkmr8w+gbCZ2Xhysm3HljbybIR6c
-1jh+JIAVMYKWsUnTYjdbiAwKYjT+p0h+mbEwi5A3lRyoH6UsjfRVyNvdWQrCrXig9IsCAwEAAaNC
-MEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUOBTmyPCppAP0Tj4io1vy1uCtQHQwDgYDVR0P
-AQH/BAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQABZ6OMySU9E2NdFm/soT4JXJEVKirZgCFPBdy7
-pYmrEzMqnji3jG8CcmPHc3ceCQa6Oyh7pEfJYWsICCD8igWKH7y6xsL+z27sEzNxZy5p+qksP2bA
-EllNC1QCkoS72xLvg3BweMhT+t/Gxv/ciC8HwEmdMldg0/L2mSlf56oBzKwzqBwKu5HEA6BvtjT5
-htOzdlSY9EqBs1OdTUDs5XcTRa9bqh/YL0yCe/4qxFi7T/ye/QNlGioOw6UgFpRreaaiErS7GqQj
-el/wroQk5PMr+4okoyeYZdowdXb8GZHo2+ubPzK/QJcHJrrM85SFSnonk8+QQtS4Wxam58tAA915
------END CERTIFICATE-----
-
-EBG Elektronik Sertifika Hizmet Sa\xC4\x9Flay\xc4\xb1\x63\xc4\xb1s\xc4\xb1
-==========================================================================
------BEGIN CERTIFICATE-----
-MIIF5zCCA8+gAwIBAgIITK9zQhyOdAIwDQYJKoZIhvcNAQEFBQAwgYAxODA2BgNVBAMML0VCRyBF
-bGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sxc8SxMTcwNQYDVQQKDC5FQkcg
-QmlsacWfaW0gVGVrbm9sb2ppbGVyaSB2ZSBIaXptZXRsZXJpIEEuxZ4uMQswCQYDVQQGEwJUUjAe
-Fw0wNjA4MTcwMDIxMDlaFw0xNjA4MTQwMDMxMDlaMIGAMTgwNgYDVQQDDC9FQkcgRWxla3Ryb25p
-ayBTZXJ0aWZpa2EgSGl6bWV0IFNhxJ9sYXnEsWPEsXPEsTE3MDUGA1UECgwuRUJHIEJpbGnFn2lt
-IFRla25vbG9qaWxlcmkgdmUgSGl6bWV0bGVyaSBBLsWeLjELMAkGA1UEBhMCVFIwggIiMA0GCSqG
-SIb3DQEBAQUAA4ICDwAwggIKAoICAQDuoIRh0DpqZhAy2DE4f6en5f2h4fuXd7hxlugTlkaDT7by
-X3JWbhNgpQGR4lvFzVcfd2NR/y8927k/qqk153nQ9dAktiHq6yOU/im/+4mRDGSaBUorzAzu8T2b
-gmmkTPiab+ci2hC6X5L8GCcKqKpE+i4stPtGmggDg3KriORqcsnlZR9uKg+ds+g75AxuetpX/dfr
-eYteIAbTdgtsApWjluTLdlHRKJ2hGvxEok3MenaoDT2/F08iiFD9rrbskFBKW5+VQarKD7JK/oCZ
-TqNGFav4c0JqwmZ2sQomFd2TkuzbqV9UIlKRcF0T6kjsbgNs2d1s/OsNA/+mgxKb8amTD8UmTDGy
-Y5lhcucqZJnSuOl14nypqZoaqsNW2xCaPINStnuWt6yHd6i58mcLlEOzrz5z+kI2sSXFCjEmN1Zn
-uqMLfdb3ic1nobc6HmZP9qBVFCVMLDMNpkGMvQQxahByCp0OLna9XvNRiYuoP1Vzv9s6xiQFlpJI
-qkuNKgPlV5EQ9GooFW5Hd4RcUXSfGenmHmMWOeMRFeNYGkS9y8RsZteEBt8w9DeiQyJ50hBs37vm
-ExH8nYQKE3vwO9D8owrXieqWfo1IhR5kX9tUoqzVegJ5a9KK8GfaZXINFHDk6Y54jzJ0fFfy1tb0
-Nokb+Clsi7n2l9GkLqq+CxnCRelwXQIDAJ3Zo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB
-/wQEAwIBBjAdBgNVHQ4EFgQU587GT/wWZ5b6SqMHwQSny2re2kcwHwYDVR0jBBgwFoAU587GT/wW
-Z5b6SqMHwQSny2re2kcwDQYJKoZIhvcNAQEFBQADggIBAJuYml2+8ygjdsZs93/mQJ7ANtyVDR2t
-FcU22NU57/IeIl6zgrRdu0waypIN30ckHrMk2pGI6YNw3ZPX6bqz3xZaPt7gyPvT/Wwp+BVGoGgm
-zJNSroIBk5DKd8pNSe/iWtkqvTDOTLKBtjDOWU/aWR1qeqRFsIImgYZ29fUQALjuswnoT4cCB64k
-XPBfrAowzIpAoHMEwfuJJPaaHFy3PApnNgUIMbOv2AFoKuB4j3TeuFGkjGwgPaL7s9QJ/XvCgKqT
-bCmYIai7FvOpEl90tYeY8pUm3zTvilORiF0alKM/fCL414i6poyWqD1SNGKfAB5UVUJnxk1Gj7sU
-RT0KlhaOEKGXmdXTMIXM3rRyt7yKPBgpaP3ccQfuJDlq+u2lrDgv+R4QDgZxGhBM/nV+/x5XOULK
-1+EVoVZVWRvRo68R2E7DpSvvkL/A7IITW43WciyTTo9qKd+FPNMN4KIYEsxVL0e3p5sC/kH2iExt
-2qkBR4NkJ2IQgtYSe14DHzSpyZH+r11thie3I6p1GMog57AP14kOpmciY/SDQSsGS7tY1dHXt7kQ
-Y9iJSrSq3RZj9W6+YKH47ejWkE8axsWgKdOnIaj1Wjz3x0miIZpKlVIglnKaZsv30oZDfCK+lvm9
-AahH3eU7QPl1K5srRmSGjR70j/sHd9DqSaIcjVIUpgqT
------END CERTIFICATE-----
-
-certSIGN ROOT CA
-================
------BEGIN CERTIFICATE-----
-MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYTAlJPMREwDwYD
-VQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTAeFw0wNjA3MDQxNzIwMDRa
-Fw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UE
-CxMQY2VydFNJR04gUk9PVCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7I
-JUqOtdu0KBuqV5Do0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHH
-rfAQUySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5dRdY4zTW2
-ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQOA7+j0xbm0bqQfWwCHTD
-0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwvJoIQ4uNllAoEwF73XVv4EOLQunpL+943
-AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B
-Af8EBAMCAcYwHQYDVR0OBBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IB
-AQA+0hyJLjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecYMnQ8
-SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ44gx+FkagQnIl6Z0
-x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6IJd1hJyMctTEHBDa0GpC9oHRxUIlt
-vBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNwi/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7Nz
-TogVZ96edhBiIL5VaZVDADlN9u6wWk5JRFRYX0KD
------END CERTIFICATE-----
-
-CNNIC ROOT
-==========
------BEGIN CERTIFICATE-----
-MIIDVTCCAj2gAwIBAgIESTMAATANBgkqhkiG9w0BAQUFADAyMQswCQYDVQQGEwJDTjEOMAwGA1UE
-ChMFQ05OSUMxEzARBgNVBAMTCkNOTklDIFJPT1QwHhcNMDcwNDE2MDcwOTE0WhcNMjcwNDE2MDcw
-OTE0WjAyMQswCQYDVQQGEwJDTjEOMAwGA1UEChMFQ05OSUMxEzARBgNVBAMTCkNOTklDIFJPT1Qw
-ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDTNfc/c3et6FtzF8LRb+1VvG7q6KR5smzD
-o+/hn7E7SIX1mlwhIhAsxYLO2uOabjfhhyzcuQxauohV3/2q2x8x6gHx3zkBwRP9SFIhxFXf2tiz
-VHa6dLG3fdfA6PZZxU3Iva0fFNrfWEQlMhkqx35+jq44sDB7R3IJMfAw28Mbdim7aXZOV/kbZKKT
-VrdvmW7bCgScEeOAH8tjlBAKqeFkgjH5jCftppkA9nCTGPihNIaj3XrCGHn2emU1z5DrvTOTn1Or
-czvmmzQgLx3vqR1jGqCA2wMv+SYahtKNu6m+UjqHZ0gNv7Sg2Ca+I19zN38m5pIEo3/PIKe38zrK
-y5nLAgMBAAGjczBxMBEGCWCGSAGG+EIBAQQEAwIABzAfBgNVHSMEGDAWgBRl8jGtKvf33VKWCscC
-wQ7vptU7ETAPBgNVHRMBAf8EBTADAQH/MAsGA1UdDwQEAwIB/jAdBgNVHQ4EFgQUZfIxrSr3991S
-lgrHAsEO76bVOxEwDQYJKoZIhvcNAQEFBQADggEBAEs17szkrr/Dbq2flTtLP1se31cpolnKOOK5
-Gv+e5m4y3R6u6jW39ZORTtpC4cMXYFDy0VwmuYK36m3knITnA3kXr5g9lNvHugDnuL8BV8F3RTIM
-O/G0HAiw/VGgod2aHRM2mm23xzy54cXZF/qD1T0VoDy7HgviyJA/qIYM/PmLXoXLT1tLYhFHxUV8
-BS9BsZ4QaRuZluBVeftOhpm4lNqGOGqTo+fLbuXf6iFViZx9fX+Y9QCJ7uOEwFyWtcVG6kbghVW2
-G8kS1sHNzYDzAgE8yGnLRUhj2JTQ7IUOO04RZfSCjKY9ri4ilAnIXOo8gV0WKgOXFlUJ24pBgp5m
-mxE=
------END CERTIFICATE-----
-
-ApplicationCA - Japanese Government
-===================================
------BEGIN CERTIFICATE-----
-MIIDoDCCAoigAwIBAgIBMTANBgkqhkiG9w0BAQUFADBDMQswCQYDVQQGEwJKUDEcMBoGA1UEChMT
-SmFwYW5lc2UgR292ZXJubWVudDEWMBQGA1UECxMNQXBwbGljYXRpb25DQTAeFw0wNzEyMTIxNTAw
-MDBaFw0xNzEyMTIxNTAwMDBaMEMxCzAJBgNVBAYTAkpQMRwwGgYDVQQKExNKYXBhbmVzZSBHb3Zl
-cm5tZW50MRYwFAYDVQQLEw1BcHBsaWNhdGlvbkNBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
-CgKCAQEAp23gdE6Hj6UG3mii24aZS2QNcfAKBZuOquHMLtJqO8F6tJdhjYq+xpqcBrSGUeQ3DnR4
-fl+Kf5Sk10cI/VBaVuRorChzoHvpfxiSQE8tnfWuREhzNgaeZCw7NCPbXCbkcXmP1G55IrmTwcrN
-wVbtiGrXoDkhBFcsovW8R0FPXjQilbUfKW1eSvNNcr5BViCH/OlQR9cwFO5cjFW6WY2H/CPek9AE
-jP3vbb3QesmlOmpyM8ZKDQUXKi17safY1vC+9D/qDihtQWEjdnjDuGWk81quzMKq2edY3rZ+nYVu
-nyoKb58DKTCXKB28t89UKU5RMfkntigm/qJj5kEW8DOYRwIDAQABo4GeMIGbMB0GA1UdDgQWBBRU
-WssmP3HMlEYNllPqa0jQk/5CdTAOBgNVHQ8BAf8EBAMCAQYwWQYDVR0RBFIwUKROMEwxCzAJBgNV
-BAYTAkpQMRgwFgYDVQQKDA/ml6XmnKzlm73mlL/lupwxIzAhBgNVBAsMGuOCouODl+ODquOCseOD
-vOOCt+ODp+ODs0NBMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBADlqRHZ3ODrs
-o2dGD/mLBqj7apAxzn7s2tGJfHrrLgy9mTLnsCTWw//1sogJhyzjVOGjprIIC8CFqMjSnHH2HZ9g
-/DgzE+Ge3Atf2hZQKXsvcJEPmbo0NI2VdMV+eKlmXb3KIXdCEKxmJj3ekav9FfBv7WxfEPjzFvYD
-io+nEhEMy/0/ecGc/WLuo89UDNErXxc+4z6/wCs+CZv+iKZ+tJIX/COUgb1up8WMwusRRdv4QcmW
-dupwX3kSa+SjB1oF7ydJzyGfikwJcGapJsErEU4z0g781mzSDjJkaP+tBXhfAx2o45CsJOAPQKdL
-rosot4LKGAfmt1t06SAZf7IbiVQ=
------END CERTIFICATE-----
-
-GeoTrust Primary Certification Authority - G3
-=============================================
------BEGIN CERTIFICATE-----
-MIID/jCCAuagAwIBAgIQFaxulBmyeUtB9iepwxgPHzANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UE
-BhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChjKSAyMDA4IEdlb1RydXN0
-IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFy
-eSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEczMB4XDTA4MDQwMjAwMDAwMFoXDTM3MTIwMTIz
-NTk1OVowgZgxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAo
-YykgMjAwOCBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNVBAMT
-LUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMzCCASIwDQYJKoZI
-hvcNAQEBBQADggEPADCCAQoCggEBANziXmJYHTNXOTIz+uvLh4yn1ErdBojqZI4xmKU4kB6Yzy5j
-K/BGvESyiaHAKAxJcCGVn2TAppMSAmUmhsalifD614SgcK9PGpc/BkTVyetyEH3kMSj7HGHmKAdE
-c5IiaacDiGydY8hS2pgn5whMcD60yRLBxWeDXTPzAxHsatBT4tG6NmCUgLthY2xbF37fQJQeqw3C
-IShwiP/WJmxsYAQlTlV+fe+/lEjetx3dcI0FX4ilm/LC7urRQEFtYjgdVgbFA0dRIBn8exALDmKu
-dlW/X3e+PkkBUz2YJQN2JFodtNuJ6nnltrM7P7pMKEF/BqxqjsHQ9gUdfeZChuOl1UcCAwEAAaNC
-MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMR5yo6hTgMdHNxr
-2zFblD4/MH8tMA0GCSqGSIb3DQEBCwUAA4IBAQAtxRPPVoB7eni9n64smefv2t+UXglpp+duaIy9
-cr5HqQ6XErhK8WTTOd8lNNTBzU6B8A8ExCSzNJbGpqow32hhc9f5joWJ7w5elShKKiePEI4ufIbE
-Ap7aDHdlDkQNkv39sxY2+hENHYwOB4lqKVb3cvTdFZx3NWZXqxNT2I7BQMXXExZacse3aQHEerGD
-AWh9jUGhlBjBJVz88P6DAod8DQ3PLghcSkANPuyBYeYk28rgDi0Hsj5W3I31QYUHSJsMC8tJP33s
-t/3LjWeJGqvtux6jAAgIFyqCXDFdRootD4abdNlF+9RAsXqqaC2Gspki4cErx5z481+oghLrGREt
------END CERTIFICATE-----
-
-thawte Primary Root CA - G2
-===========================
------BEGIN CERTIFICATE-----
-MIICiDCCAg2gAwIBAgIQNfwmXNmET8k9Jj1Xm67XVjAKBggqhkjOPQQDAzCBhDELMAkGA1UEBhMC
-VVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjE4MDYGA1UECxMvKGMpIDIwMDcgdGhhd3RlLCBJbmMu
-IC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAiBgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3Qg
-Q0EgLSBHMjAeFw0wNzExMDUwMDAwMDBaFw0zODAxMTgyMzU5NTlaMIGEMQswCQYDVQQGEwJVUzEV
-MBMGA1UEChMMdGhhd3RlLCBJbmMuMTgwNgYDVQQLEy8oYykgMjAwNyB0aGF3dGUsIEluYy4gLSBG
-b3IgYXV0aG9yaXplZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAt
-IEcyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEotWcgnuVnfFSeIf+iha/BebfowJPDQfGAFG6DAJS
-LSKkQjnE/o/qycG+1E3/n3qe4rF8mq2nhglzh9HnmuN6papu+7qzcMBniKI11KOasf2twu8x+qi5
-8/sIxpHR+ymVo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU
-mtgAMADna3+FGO6Lts6KDPgR4bswCgYIKoZIzj0EAwMDaQAwZgIxAN344FdHW6fmCsO99YCKlzUN
-G4k8VIZ3KMqh9HneteY4sPBlcIx/AlTCv//YoT7ZzwIxAMSNlPzcU9LcnXgWHxUzI1NS41oxXZ3K
-rr0TKUQNJ1uo52icEvdYPy5yAlejj6EULg==
------END CERTIFICATE-----
-
-thawte Primary Root CA - G3
-===========================
------BEGIN CERTIFICATE-----
-MIIEKjCCAxKgAwIBAgIQYAGXt0an6rS0mtZLL/eQ+zANBgkqhkiG9w0BAQsFADCBrjELMAkGA1UE
-BhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2
-aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIwMDggdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhv
-cml6ZWQgdXNlIG9ubHkxJDAiBgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMzAeFw0w
-ODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIGuMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhh
-d3RlLCBJbmMuMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9uIFNlcnZpY2VzIERpdmlzaW9uMTgwNgYD
-VQQLEy8oYykgMjAwOCB0aGF3dGUsIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTEkMCIG
-A1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAtIEczMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
-MIIBCgKCAQEAsr8nLPvb2FvdeHsbnndmgcs+vHyu86YnmjSjaDFxODNi5PNxZnmxqWWjpYvVj2At
-P0LMqmsywCPLLEHd5N/8YZzic7IilRFDGF/Eth9XbAoFWCLINkw6fKXRz4aviKdEAhN0cXMKQlkC
-+BsUa0Lfb1+6a4KinVvnSr0eAXLbS3ToO39/fR8EtCab4LRarEc9VbjXsCZSKAExQGbY2SS99irY
-7CFJXJv2eul/VTV+lmuNk5Mny5K76qxAwJ/C+IDPXfRa3M50hqY+bAtTyr2SzhkGcuYMXDhpxwTW
-vGzOW/b3aJzcJRVIiKHpqfiYnODz1TEoYRFsZ5aNOZnLwkUkOQIDAQABo0IwQDAPBgNVHRMBAf8E
-BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUrWyqlGCc7eT/+j4KdCtjA/e2Wb8wDQYJ
-KoZIhvcNAQELBQADggEBABpA2JVlrAmSicY59BDlqQ5mU1143vokkbvnRFHfxhY0Cu9qRFHqKweK
-A3rD6z8KLFIWoCtDuSWQP3CpMyVtRRooOyfPqsMpQhvfO0zAMzRbQYi/aytlryjvsvXDqmbOe1bu
-t8jLZ8HJnBoYuMTDSQPxYA5QzUbF83d597YV4Djbxy8ooAw/dyZ02SUS2jHaGh7cKUGRIjxpp7sC
-8rZcJwOJ9Abqm+RyguOhCcHpABnTPtRwa7pxpqpYrvS76Wy274fMm7v/OeZWYdMKp8RcTGB7BXcm
-er/YB1IsYvdwY9k5vG8cwnncdimvzsUsZAReiDZuMdRAGmI0Nj81Aa6sY6A=
------END CERTIFICATE-----
-
-GeoTrust Primary Certification Authority - G2
-=============================================
------BEGIN CERTIFICATE-----
-MIICrjCCAjWgAwIBAgIQPLL0SAoA4v7rJDteYD7DazAKBggqhkjOPQQDAzCBmDELMAkGA1UEBhMC
-VVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChjKSAyMDA3IEdlb1RydXN0IElu
-Yy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBD
-ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMB4XDTA3MTEwNTAwMDAwMFoXDTM4MDExODIzNTk1
-OVowgZgxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykg
-MjAwNyBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNVBAMTLUdl
-b1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMjB2MBAGByqGSM49AgEG
-BSuBBAAiA2IABBWx6P0DFUPlrOuHNxFi79KDNlJ9RVcLSo17VDs6bl8VAsBQps8lL33KSLjHUGMc
-KiEIfJo22Av+0SbFWDEwKCXzXV2juLaltJLtbCyf691DiaI8S0iRHVDsJt/WYC69IaNCMEAwDwYD
-VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBVfNVdRVfslsq0DafwBo/q+
-EVXVMAoGCCqGSM49BAMDA2cAMGQCMGSWWaboCd6LuvpaiIjwH5HTRqjySkwCY/tsXzjbLkGTqQ7m
-ndwxHLKgpxgceeHHNgIwOlavmnRs9vuD4DPTCF+hnMJbn0bWtsuRBmOiBuczrD6ogRLQy7rQkgu2
-npaqBA+K
------END CERTIFICATE-----
-
-VeriSign Universal Root Certification Authority
-===============================================
------BEGIN CERTIFICATE-----
-MIIEuTCCA6GgAwIBAgIQQBrEZCGzEyEDDrvkEhrFHTANBgkqhkiG9w0BAQsFADCBvTELMAkGA1UE
-BhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBO
-ZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwOCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVk
-IHVzZSBvbmx5MTgwNgYDVQQDEy9WZXJpU2lnbiBVbml2ZXJzYWwgUm9vdCBDZXJ0aWZpY2F0aW9u
-IEF1dGhvcml0eTAeFw0wODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIG9MQswCQYDVQQGEwJV
-UzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0IE5ldHdv
-cmsxOjA4BgNVBAsTMShjKSAyMDA4IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl
-IG9ubHkxODA2BgNVBAMTL1ZlcmlTaWduIFVuaXZlcnNhbCBSb290IENlcnRpZmljYXRpb24gQXV0
-aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx2E3XrEBNNti1xWb/1hajCMj
-1mCOkdeQmIN65lgZOIzF9uVkhbSicfvtvbnazU0AtMgtc6XHaXGVHzk8skQHnOgO+k1KxCHfKWGP
-MiJhgsWHH26MfF8WIFFE0XBPV+rjHOPMee5Y2A7Cs0WTwCznmhcrewA3ekEzeOEz4vMQGn+HLL72
-9fdC4uW/h2KJXwBL38Xd5HVEMkE6HnFuacsLdUYI0crSK5XQz/u5QGtkjFdN/BMReYTtXlT2NJ8I
-AfMQJQYXStrxHXpma5hgZqTZ79IugvHw7wnqRMkVauIDbjPTrJ9VAMf2CGqUuV/c4DPxhGD5WycR
-tPwW8rtWaoAljQIDAQABo4GyMIGvMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMG0G
-CCsGAQUFBwEMBGEwX6FdoFswWTBXMFUWCWltYWdlL2dpZjAhMB8wBwYFKw4DAhoEFI/l0xqGrI2O
-a8PPgGrUSBgsexkuMCUWI2h0dHA6Ly9sb2dvLnZlcmlzaWduLmNvbS92c2xvZ28uZ2lmMB0GA1Ud
-DgQWBBS2d/ppSEefUxLVwuoHMnYH0ZcHGTANBgkqhkiG9w0BAQsFAAOCAQEASvj4sAPmLGd75JR3
-Y8xuTPl9Dg3cyLk1uXBPY/ok+myDjEedO2Pzmvl2MpWRsXe8rJq+seQxIcaBlVZaDrHC1LGmWazx
-Y8u4TB1ZkErvkBYoH1quEPuBUDgMbMzxPcP1Y+Oz4yHJJDnp/RVmRvQbEdBNc6N9Rvk97ahfYtTx
-P/jgdFcrGJ2BtMQo2pSXpXDrrB2+BxHw1dvd5Yzw1TKwg+ZX4o+/vqGqvz0dtdQ46tewXDpPaj+P
-wGZsY6rp2aQW9IHRlRQOfc2VNNnSj3BzgXucfr2YYdhFh5iQxeuGMMY1v/D/w1WIg0vvBZIGcfK4
-mJO37M2CYfE45k+XmCpajQ==
------END CERTIFICATE-----
-
-VeriSign Class 3 Public Primary Certification Authority - G4
-============================================================
------BEGIN CERTIFICATE-----
-MIIDhDCCAwqgAwIBAgIQL4D+I4wOIg9IZxIokYesszAKBggqhkjOPQQDAzCByjELMAkGA1UEBhMC
-VVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3
-b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVz
-ZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmlj
-YXRpb24gQXV0aG9yaXR5IC0gRzQwHhcNMDcxMTA1MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCByjEL
-MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBU
-cnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRo
-b3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5
-IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASnVnp8
-Utpkmw4tXNherJI9/gHmGUo9FANL+mAnINmDiWn6VMaaGF5VKmTeBvaNSjutEDxlPZCIBIngMGGz
-rl0Bp3vefLK+ymVhAIau2o970ImtTR1ZmkGxvEeA3J5iw/mjgbIwga8wDwYDVR0TAQH/BAUwAwEB
-/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEw
-HzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVyaXNpZ24u
-Y29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFLMWkf3upm7ktS5Jj4d4gYDs5bG1MAoGCCqGSM49BAMD
-A2gAMGUCMGYhDBgmYFo4e1ZC4Kf8NoRRkSAsdk1DPcQdhCPQrNZ8NQbOzWm9kA3bbEhCHQ6qQgIx
-AJw9SDkjOVgaFRJZap7v1VmyHVIsmXHNxynfGyphe3HR3vPA5Q06Sqotp9iGKt0uEA==
------END CERTIFICATE-----
-
-NetLock Arany (Class Gold) Főtanúsítvány
-============================================
------BEGIN CERTIFICATE-----
-MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQGEwJIVTERMA8G
-A1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3MDUGA1UECwwuVGFuw7pzw610
-dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBB
-cmFueSAoQ2xhc3MgR29sZCkgRsWRdGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgx
-MjA2MTUwODIxWjCBpzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxO
-ZXRMb2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlmaWNhdGlv
-biBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNzIEdvbGQpIEbFkXRhbsO6
-c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxCRec75LbRTDofTjl5Bu
-0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrTlF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw
-/HpYzY6b7cNGbIRwXdrzAZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAk
-H3B5r9s5VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRGILdw
-fzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2BJtr+UBdADTHLpl1
-neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIB
-BjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2MU9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwW
-qZw8UQCgwBEIBaeZ5m8BiFRhbvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTta
-YtOUZcTh5m2C+C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC
-bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2FuLjbvrW5Kfna
-NwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2XjG4Kvte9nHfRCaexOYNkbQu
-dZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E=
------END CERTIFICATE-----
-
-Staat der Nederlanden Root CA - G2
-==================================
------BEGIN CERTIFICATE-----
-MIIFyjCCA7KgAwIBAgIEAJiWjDANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJOTDEeMBwGA1UE
-CgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFhdCBkZXIgTmVkZXJsYW5kZW4g
-Um9vdCBDQSAtIEcyMB4XDTA4MDMyNjExMTgxN1oXDTIwMDMyNTExMDMxMFowWjELMAkGA1UEBhMC
-TkwxHjAcBgNVBAoMFVN0YWF0IGRlciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5l
-ZGVybGFuZGVuIFJvb3QgQ0EgLSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMVZ
-5291qj5LnLW4rJ4L5PnZyqtdj7U5EILXr1HgO+EASGrP2uEGQxGZqhQlEq0i6ABtQ8SpuOUfiUtn
-vWFI7/3S4GCI5bkYYCjDdyutsDeqN95kWSpGV+RLufg3fNU254DBtvPUZ5uW6M7XxgpT0GtJlvOj
-CwV3SPcl5XCsMBQgJeN/dVrlSPhOewMHBPqCYYdu8DvEpMfQ9XQ+pV0aCPKbJdL2rAQmPlU6Yiil
-e7Iwr/g3wtG61jj99O9JMDeZJiFIhQGp5Rbn3JBV3w/oOM2ZNyFPXfUib2rFEhZgF1XyZWampzCR
-OME4HYYEhLoaJXhena/MUGDWE4dS7WMfbWV9whUYdMrhfmQpjHLYFhN9C0lK8SgbIHRrxT3dsKpI
-CT0ugpTNGmXZK4iambwYfp/ufWZ8Pr2UuIHOzZgweMFvZ9C+X+Bo7d7iscksWXiSqt8rYGPy5V65
-48r6f1CGPqI0GAwJaCgRHOThuVw+R7oyPxjMW4T182t0xHJ04eOLoEq9jWYv6q012iDTiIJh8BIi
-trzQ1aTsr1SIJSQ8p22xcik/Plemf1WvbibG/ufMQFxRRIEKeN5KzlW/HdXZt1bv8Hb/C3m1r737
-qWmRRpdogBQ2HbN/uymYNqUg+oJgYjOk7Na6B6duxc8UpufWkjTYgfX8HV2qXB72o007uPc5AgMB
-AAGjgZcwgZQwDwYDVR0TAQH/BAUwAwEB/zBSBgNVHSAESzBJMEcGBFUdIAAwPzA9BggrBgEFBQcC
-ARYxaHR0cDovL3d3dy5wa2lvdmVyaGVpZC5ubC9wb2xpY2llcy9yb290LXBvbGljeS1HMjAOBgNV
-HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJFoMocVHYnitfGsNig0jQt8YojrMA0GCSqGSIb3DQEBCwUA
-A4ICAQCoQUpnKpKBglBu4dfYszk78wIVCVBR7y29JHuIhjv5tLySCZa59sCrI2AGeYwRTlHSeYAz
-+51IvuxBQ4EffkdAHOV6CMqqi3WtFMTC6GY8ggen5ieCWxjmD27ZUD6KQhgpxrRW/FYQoAUXvQwj
-f/ST7ZwaUb7dRUG/kSS0H4zpX897IZmflZ85OkYcbPnNe5yQzSipx6lVu6xiNGI1E0sUOlWDuYaN
-kqbG9AclVMwWVxJKgnjIFNkXgiYtXSAfea7+1HAWFpWD2DU5/1JddRwWxRNVz0fMdWVSSt7wsKfk
-CpYL+63C4iWEst3kvX5ZbJvw8NjnyvLplzh+ib7M+zkXYT9y2zqR2GUBGR2tUKRXCnxLvJxxcypF
-URmFzI79R6d0lR2o0a9OF7FpJsKqeFdbxU2n5Z4FF5TKsl+gSRiNNOkmbEgeqmiSBeGCc1qb3Adb
-CG19ndeNIdn8FCCqwkXfP+cAslHkwvgFuXkajDTznlvkN1trSt8sV4pAWja63XVECDdCcAz+3F4h
-oKOKwJCcaNpQ5kUQR3i2TtJlycM33+FCY7BXN0Ute4qcvwXqZVUz9zkQxSgqIXobisQk+T8VyJoV
-IPVVYpbtbZNQvOSqeK3Zywplh6ZmwcSBo3c6WB4L7oOLnR7SUqTMHW+wmG2UMbX4cQrcufx9MmDm
-66+KAQ==
------END CERTIFICATE-----
-
-CA Disig
-========
------BEGIN CERTIFICATE-----
-MIIEDzCCAvegAwIBAgIBATANBgkqhkiG9w0BAQUFADBKMQswCQYDVQQGEwJTSzETMBEGA1UEBxMK
-QnJhdGlzbGF2YTETMBEGA1UEChMKRGlzaWcgYS5zLjERMA8GA1UEAxMIQ0EgRGlzaWcwHhcNMDYw
-MzIyMDEzOTM0WhcNMTYwMzIyMDEzOTM0WjBKMQswCQYDVQQGEwJTSzETMBEGA1UEBxMKQnJhdGlz
-bGF2YTETMBEGA1UEChMKRGlzaWcgYS5zLjERMA8GA1UEAxMIQ0EgRGlzaWcwggEiMA0GCSqGSIb3
-DQEBAQUAA4IBDwAwggEKAoIBAQCS9jHBfYj9mQGp2HvycXXxMcbzdWb6UShGhJd4NLxs/LxFWYgm
-GErENx+hSkS943EE9UQX4j/8SFhvXJ56CbpRNyIjZkMhsDxkovhqFQ4/61HhVKndBpnXmjxUizkD
-Pw/Fzsbrg3ICqB9x8y34dQjbYkzo+s7552oftms1grrijxaSfQUMbEYDXcDtab86wYqg6I7ZuUUo
-hwjstMoVvoLdtUSLLa2GDGhibYVW8qwUYzrG0ZmsNHhWS8+2rT+MitcE5eN4TPWGqvWP+j1scaMt
-ymfraHtuM6kMgiioTGohQBUgDCZbg8KpFhXAJIJdKxatymP2dACw30PEEGBWZ2NFAgMBAAGjgf8w
-gfwwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUjbJJaJ1yCCW5wCf1UJNWSEZx+Y8wDgYDVR0P
-AQH/BAQDAgEGMDYGA1UdEQQvMC2BE2Nhb3BlcmF0b3JAZGlzaWcuc2uGFmh0dHA6Ly93d3cuZGlz
-aWcuc2svY2EwZgYDVR0fBF8wXTAtoCugKYYnaHR0cDovL3d3dy5kaXNpZy5zay9jYS9jcmwvY2Ff
-ZGlzaWcuY3JsMCygKqAohiZodHRwOi8vY2EuZGlzaWcuc2svY2EvY3JsL2NhX2Rpc2lnLmNybDAa
-BgNVHSAEEzARMA8GDSuBHpGT5goAAAABAQEwDQYJKoZIhvcNAQEFBQADggEBAF00dGFMrzvY/59t
-WDYcPQuBDRIrRhCA/ec8J9B6yKm2fnQwM6M6int0wHl5QpNt/7EpFIKrIYwvF/k/Ji/1WcbvgAa3
-mkkp7M5+cTxqEEHA9tOasnxakZzArFvITV734VP/Q3f8nktnbNfzg9Gg4H8l37iYC5oyOGwwoPP/
-CBUz91BKez6jPiCp3C9WgArtQVCwyfTssuMmRAAOb54GvCKWU3BlxFAKRmukLyeBEicTXxChds6K
-ezfqwzlhA5WYOudsiCUI/HloDYd9Yvi0X/vF2Ey9WLw/Q1vUHgFNPGO+I++MzVpQuGhU+QqZMxEA
-4Z7CRneC9VkGjCFMhwnN5ag=
------END CERTIFICATE-----
-
-Juur-SK
-=======
------BEGIN CERTIFICATE-----
-MIIE5jCCA86gAwIBAgIEO45L/DANBgkqhkiG9w0BAQUFADBdMRgwFgYJKoZIhvcNAQkBFglwa2lA
-c2suZWUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKExlBUyBTZXJ0aWZpdHNlZXJpbWlza2Vza3VzMRAw
-DgYDVQQDEwdKdXVyLVNLMB4XDTAxMDgzMDE0MjMwMVoXDTE2MDgyNjE0MjMwMVowXTEYMBYGCSqG
-SIb3DQEJARYJcGtpQHNrLmVlMQswCQYDVQQGEwJFRTEiMCAGA1UEChMZQVMgU2VydGlmaXRzZWVy
-aW1pc2tlc2t1czEQMA4GA1UEAxMHSnV1ci1TSzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
-ggEBAIFxNj4zB9bjMI0TfncyRsvPGbJgMUaXhvSYRqTCZUXP00B841oiqBB4M8yIsdOBSvZiF3tf
-TQou0M+LI+5PAk676w7KvRhj6IAcjeEcjT3g/1tf6mTll+g/mX8MCgkzABpTpyHhOEvWgxutr2TC
-+Rx6jGZITWYfGAriPrsfB2WThbkasLnE+w0R9vXW+RvHLCu3GFH+4Hv2qEivbDtPL+/40UceJlfw
-UR0zlv/vWT3aTdEVNMfqPxZIe5EcgEMPPbgFPtGzlc3Yyg/CQ2fbt5PgIoIuvvVoKIO5wTtpeyDa
-Tpxt4brNj3pssAki14sL2xzVWiZbDcDq5WDQn/413z8CAwEAAaOCAawwggGoMA8GA1UdEwEB/wQF
-MAMBAf8wggEWBgNVHSAEggENMIIBCTCCAQUGCisGAQQBzh8BAQEwgfYwgdAGCCsGAQUFBwICMIHD
-HoHAAFMAZQBlACAAcwBlAHIAdABpAGYAaQBrAGEAYQB0ACAAbwBuACAAdgDkAGwAagBhAHMAdABh
-AHQAdQBkACAAQQBTAC0AaQBzACAAUwBlAHIAdABpAGYAaQB0AHMAZQBlAHIAaQBtAGkAcwBrAGUA
-cwBrAHUAcwAgAGEAbABhAG0ALQBTAEsAIABzAGUAcgB0AGkAZgBpAGsAYQBhAHQAaQBkAGUAIABr
-AGkAbgBuAGkAdABhAG0AaQBzAGUAawBzMCEGCCsGAQUFBwIBFhVodHRwOi8vd3d3LnNrLmVlL2Nw
-cy8wKwYDVR0fBCQwIjAgoB6gHIYaaHR0cDovL3d3dy5zay5lZS9qdXVyL2NybC8wHQYDVR0OBBYE
-FASqekej5ImvGs8KQKcYP2/v6X2+MB8GA1UdIwQYMBaAFASqekej5ImvGs8KQKcYP2/v6X2+MA4G
-A1UdDwEB/wQEAwIB5jANBgkqhkiG9w0BAQUFAAOCAQEAe8EYlFOiCfP+JmeaUOTDBS8rNXiRTHyo
-ERF5TElZrMj3hWVcRrs7EKACr81Ptcw2Kuxd/u+gkcm2k298gFTsxwhwDY77guwqYHhpNjbRxZyL
-abVAyJRld/JXIWY7zoVAtjNjGr95HvxcHdMdkxuLDF2FvZkwMhgJkVLpfKG6/2SSmuz+Ne6ML678
-IIbsSt4beDI3poHSna9aEhbKmVv8b20OxaAehsmR0FyYgl9jDIpaq9iVpszLita/ZEuOyoqysOkh
-Mp6qqIWYNIE5ITuoOlIyPfZrN4YGWhWY3PARZv40ILcD9EEQfTmEeZZyY7aWAuVrua0ZTbvGRNs2
-yyqcjg==
------END CERTIFICATE-----
-
-Hongkong Post Root CA 1
-=======================
------BEGIN CERTIFICATE-----
-MIIDMDCCAhigAwIBAgICA+gwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCSEsxFjAUBgNVBAoT
-DUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3QgUm9vdCBDQSAxMB4XDTAzMDUx
-NTA1MTMxNFoXDTIzMDUxNTA0NTIyOVowRzELMAkGA1UEBhMCSEsxFjAUBgNVBAoTDUhvbmdrb25n
-IFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3QgUm9vdCBDQSAxMIIBIjANBgkqhkiG9w0BAQEF
-AAOCAQ8AMIIBCgKCAQEArP84tulmAknjorThkPlAj3n54r15/gK97iSSHSL22oVyaf7XPwnU3ZG1
-ApzQjVrhVcNQhrkpJsLj2aDxaQMoIIBFIi1WpztUlVYiWR8o3x8gPW2iNr4joLFutbEnPzlTCeqr
-auh0ssJlXI6/fMN4hM2eFvz1Lk8gKgifd/PFHsSaUmYeSF7jEAaPIpjhZY4bXSNmO7ilMlHIhqqh
-qZ5/dpTCpmy3QfDVyAY45tQM4vM7TG1QjMSDJ8EThFk9nnV0ttgCXjqQesBCNnLsak3c78QA3xMY
-V18meMjWCnl3v/evt3a5pQuEF10Q6m/hq5URX208o1xNg1vysxmKgIsLhwIDAQABoyYwJDASBgNV
-HRMBAf8ECDAGAQH/AgEDMA4GA1UdDwEB/wQEAwIBxjANBgkqhkiG9w0BAQUFAAOCAQEADkbVPK7i
-h9legYsCmEEIjEy82tvuJxuC52pF7BaLT4Wg87JwvVqWuspube5Gi27nKi6Wsxkz67SfqLI37pio
-l7Yutmcn1KZJ/RyTZXaeQi/cImyaT/JaFTmxcdcrUehtHJjA2Sr0oYJ71clBoiMBdDhViw+5Lmei
-IAQ32pwL0xch4I+XeTRvhEgCIDMb5jREn5Fw9IBehEPCKdJsEhTkYY2sEJCehFC78JZvRZ+K88ps
-T/oROhUVRsPNH4NbLUES7VBnQRM9IauUiqpOfMGx+6fWtScvl6tu4B3i0RwsH0Ti/L6RoZz71ilT
-c4afU9hDDl3WY4JxHYB0yvbiAmvZWg==
------END CERTIFICATE-----
-
-SecureSign RootCA11
-===================
------BEGIN CERTIFICATE-----
-MIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDErMCkGA1UEChMi
-SmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoGA1UEAxMTU2VjdXJlU2lnbiBS
-b290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0MDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSsw
-KQYDVQQKEyJKYXBhbiBDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1
-cmVTaWduIFJvb3RDQTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvL
-TJszi1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8h9uuywGO
-wvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOVMdrAG/LuYpmGYz+/3ZMq
-g6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9UK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rP
-O7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitA
-bpSACW22s293bzUIUPsCh8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZX
-t94wDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAKCh
-OBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xmKbabfSVSSUOrTC4r
-bnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQX5Ucv+2rIrVls4W6ng+4reV6G4pQ
-Oh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWrQbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01
-y8hSyn+B/tlr0/cR7SXf+Of5pPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061
-lgeLKBObjBmNQSdJQO7e5iNEOdyhIta6A/I=
------END CERTIFICATE-----
-
-ACEDICOM Root
-=============
------BEGIN CERTIFICATE-----
-MIIFtTCCA52gAwIBAgIIYY3HhjsBggUwDQYJKoZIhvcNAQEFBQAwRDEWMBQGA1UEAwwNQUNFRElD
-T00gUm9vdDEMMAoGA1UECwwDUEtJMQ8wDQYDVQQKDAZFRElDT00xCzAJBgNVBAYTAkVTMB4XDTA4
-MDQxODE2MjQyMloXDTI4MDQxMzE2MjQyMlowRDEWMBQGA1UEAwwNQUNFRElDT00gUm9vdDEMMAoG
-A1UECwwDUEtJMQ8wDQYDVQQKDAZFRElDT00xCzAJBgNVBAYTAkVTMIICIjANBgkqhkiG9w0BAQEF
-AAOCAg8AMIICCgKCAgEA/5KV4WgGdrQsyFhIyv2AVClVYyT/kGWbEHV7w2rbYgIB8hiGtXxaOLHk
-WLn709gtn70yN78sFW2+tfQh0hOR2QetAQXW8713zl9CgQr5auODAKgrLlUTY4HKRxx7XBZXehuD
-YAQ6PmXDzQHe3qTWDLqO3tkE7hdWIpuPY/1NFgu3e3eM+SW10W2ZEi5PGrjm6gSSrj0RuVFCPYew
-MYWveVqc/udOXpJPQ/yrOq2lEiZmueIM15jO1FillUAKt0SdE3QrwqXrIhWYENiLxQSfHY9g5QYb
-m8+5eaA9oiM/Qj9r+hwDezCNzmzAv+YbX79nuIQZ1RXve8uQNjFiybwCq0Zfm/4aaJQ0PZCOrfbk
-HQl/Sog4P75n/TSW9R28MHTLOO7VbKvU/PQAtwBbhTIWdjPp2KOZnQUAqhbm84F9b32qhm2tFXTT
-xKJxqvQUfecyuB+81fFOvW8XAjnXDpVCOscAPukmYxHqC9FK/xidstd7LzrZlvvoHpKuE1XI2Sf2
-3EgbsCTBheN3nZqk8wwRHQ3ItBTutYJXCb8gWH8vIiPYcMt5bMlL8qkqyPyHK9caUPgn6C9D4zq9
-2Fdx/c6mUlv53U3t5fZvie27k5x2IXXwkkwp9y+cAS7+UEaeZAwUswdbxcJzbPEHXEUkFDWug/Fq
-TYl6+rPYLWbwNof1K1MCAwEAAaOBqjCBpzAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKaz
-4SsrSbbXc6GqlPUB53NlTKxQMA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUprPhKytJttdzoaqU
-9QHnc2VMrFAwRAYDVR0gBD0wOzA5BgRVHSAAMDEwLwYIKwYBBQUHAgEWI2h0dHA6Ly9hY2VkaWNv
-bS5lZGljb21ncm91cC5jb20vZG9jMA0GCSqGSIb3DQEBBQUAA4ICAQDOLAtSUWImfQwng4/F9tqg
-aHtPkl7qpHMyEVNEskTLnewPeUKzEKbHDZ3Ltvo/Onzqv4hTGzz3gvoFNTPhNahXwOf9jU8/kzJP
-eGYDdwdY6ZXIfj7QeQCM8htRM5u8lOk6e25SLTKeI6RF+7YuE7CLGLHdztUdp0J/Vb77W7tH1Pwk
-zQSulgUV1qzOMPPKC8W64iLgpq0i5ALudBF/TP94HTXa5gI06xgSYXcGCRZj6hitoocf8seACQl1
-ThCojz2GuHURwCRiipZ7SkXp7FnFvmuD5uHorLUwHv4FB4D54SMNUI8FmP8sX+g7tq3PgbUhh8oI
-KiMnMCArz+2UW6yyetLHKKGKC5tNSixthT8Jcjxn4tncB7rrZXtaAWPWkFtPF2Y9fwsZo5NjEFIq
-nxQWWOLcpfShFosOkYuByptZ+thrkQdlVV9SH686+5DdaaVbnG0OLLb6zqylfDJKZ0DcMDQj3dcE
-I2bw/FWAp/tmGYI1Z2JwOV5vx+qQQEQIHriy1tvuWacNGHk0vFQYXlPKNFHtRQrmjseCNj6nOGOp
-MCwXEGCSn1WHElkQwg9naRHMTh5+Spqtr0CodaxWkHS4oJyleW/c6RrIaQXpuvoDs3zk4E7Czp3o
-tkYNbn5XOmeUwssfnHdKZ05phkOTOPu220+DkdRgfks+KzgHVZhepA==
------END CERTIFICATE-----
-
-Verisign Class 1 Public Primary Certification Authority
-=======================================================
------BEGIN CERTIFICATE-----
-MIICPDCCAaUCED9pHoGc8JpK83P/uUii5N0wDQYJKoZIhvcNAQEFBQAwXzELMAkGA1UEBhMCVVMx
-FzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAxIFB1YmxpYyBQcmltYXJ5
-IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2MDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVow
-XzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAx
-IFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUA
-A4GNADCBiQKBgQDlGb9to1ZhLZlIcfZn3rmN67eehoAKkQ76OCWvRoiC5XOooJskXQ0fzGVuDLDQ
-VoQYh5oGmxChc9+0WDlrbsH2FdWoqD+qEgaNMax/sDTXjzRniAnNFBHiTkVWaR94AoDa3EeRKbs2
-yWNcxeDXLYd7obcysHswuiovMaruo2fa2wIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAFgVKTk8d6Pa
-XCUDfGD67gmZPCcQcMgMCeazh88K4hiWNWLMv5sneYlfycQJ9M61Hd8qveXbhpxoJeUwfLaJFf5n
-0a3hUKw8fGJLj7qE1xIVGx/KXQ/BUpQqEZnae88MNhPVNdwQGVnqlMEAv3WP2fr9dgTbYruQagPZ
-RjXZ+Hxb
------END CERTIFICATE-----
-
-Verisign Class 3 Public Primary Certification Authority
-=======================================================
------BEGIN CERTIFICATE-----
-MIICPDCCAaUCEDyRMcsf9tAbDpq40ES/Er4wDQYJKoZIhvcNAQEFBQAwXzELMAkGA1UEBhMCVVMx
-FzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmltYXJ5
-IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2MDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVow
-XzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAz
-IFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUA
-A4GNADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhEBarsAx94
-f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/isI19wKTakyYbnsZogy1Ol
-hec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBABByUqkFFBky
-CEHwxWsKzH4PIRnN5GfcX6kb5sroc50i2JhucwNhkcV8sEVAbkSdjbCxlnRhLQ2pRdKkkirWmnWX
-bj9T/UWZYB2oK0z5XqcJ2HUw19JlYD1n1khVdWk/kfVIC0dpImmClr7JyDiGSnoscxlIaU5rfGW/
-D/xwzoiQ
------END CERTIFICATE-----
-
-Microsec e-Szigno Root CA 2009
-==============================
------BEGIN CERTIFICATE-----
-MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYDVQQGEwJIVTER
-MA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jv
-c2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o
-dTAeFw0wOTA2MTYxMTMwMThaFw0yOTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UE
-BwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUt
-U3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTCCASIw
-DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvPkd6mJviZpWNwrZuuyjNA
-fW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tccbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG
-0IMZfcChEhyVbUr02MelTTMuhTlAdX4UfIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKA
-pxn1ntxVUwOXewdI/5n7N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm
-1HxdrtbCxkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1+rUC
-AwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTLD8bf
-QkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAbBgNVHREE
-FDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqGSIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0o
-lZMEyL/azXm4Q5DwpL7v8u8hmLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfX
-I/OMn74dseGkddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775
-tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c2Pm2G2JwCz02
-yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5tHMN1Rq41Bab2XD0h7lbwyYIi
-LXpUq3DDfSJlgnCW
------END CERTIFICATE-----
-
-E-Guven Kok Elektronik Sertifika Hizmet Saglayicisi
-===================================================
------BEGIN CERTIFICATE-----
-MIIDtjCCAp6gAwIBAgIQRJmNPMADJ72cdpW56tustTANBgkqhkiG9w0BAQUFADB1MQswCQYDVQQG
-EwJUUjEoMCYGA1UEChMfRWxla3Ryb25payBCaWxnaSBHdXZlbmxpZ2kgQS5TLjE8MDoGA1UEAxMz
-ZS1HdXZlbiBLb2sgRWxla3Ryb25payBTZXJ0aWZpa2EgSGl6bWV0IFNhZ2xheWljaXNpMB4XDTA3
-MDEwNDExMzI0OFoXDTE3MDEwNDExMzI0OFowdTELMAkGA1UEBhMCVFIxKDAmBgNVBAoTH0VsZWt0
-cm9uaWsgQmlsZ2kgR3V2ZW5saWdpIEEuUy4xPDA6BgNVBAMTM2UtR3V2ZW4gS29rIEVsZWt0cm9u
-aWsgU2VydGlmaWthIEhpem1ldCBTYWdsYXlpY2lzaTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
-AQoCggEBAMMSIJ6wXgBljU5Gu4Bc6SwGl9XzcslwuedLZYDBS75+PNdUMZTe1RK6UxYC6lhj71vY
-8+0qGqpxSKPcEC1fX+tcS5yWCEIlKBHMilpiAVDV6wlTL/jDj/6z/P2douNffb7tC+Bg62nsM+3Y
-jfsSSYMAyYuXjDtzKjKzEve5TfL0TW3H5tYmNwjy2f1rXKPlSFxYvEK+A1qBuhw1DADT9SN+cTAI
-JjjcJRFHLfO6IxClv7wC90Nex/6wN1CZew+TzuZDLMN+DfIcQ2Zgy2ExR4ejT669VmxMvLz4Bcpk
-9Ok0oSy1c+HCPujIyTQlCFzz7abHlJ+tiEMl1+E5YP6sOVkCAwEAAaNCMEAwDgYDVR0PAQH/BAQD
-AgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJ/uRLOU1fqRTy7ZVZoEVtstxNulMA0GCSqG
-SIb3DQEBBQUAA4IBAQB/X7lTW2M9dTLn+sR0GstG30ZpHFLPqk/CaOv/gKlR6D1id4k9CnU58W5d
-F4dvaAXBlGzZXd/aslnLpRCKysw5zZ/rTt5S/wzw9JKp8mxTq5vSR6AfdPebmvEvFZ96ZDAYBzwq
-D2fK/A+JYZ1lpTzlvBNbCNvj/+27BrtqBrF6T2XGgv0enIu1De5Iu7i9qgi0+6N8y5/NkHZchpZ4
-Vwpm+Vganf2XKWDeEaaQHBkc7gGWIjQ0LpH5t8Qn0Xvmv/uARFoW5evg1Ao4vOSR49XrXMGs3xtq
-fJ7lddK2l4fbzIcrQzqECK+rPNv3PGYxhrCdU3nt+CPeQuMtgvEP5fqX
------END CERTIFICATE-----
-
-GlobalSign Root CA - R3
-=======================
------BEGIN CERTIFICATE-----
-MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4GA1UECxMXR2xv
-YmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2Jh
-bFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxT
-aWduIFJvb3QgQ0EgLSBSMzETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2ln
-bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWt
-iHL8RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsTgHeMCOFJ
-0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmmKPZpO/bLyCiR5Z2KYVc3
-rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zdQQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjl
-OCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZXriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2
-xmmFghcCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE
-FI/wS3+oLkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZURUm7
-lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMpjjM5RcOO5LlXbKr8
-EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK6fBdRoyV3XpYKBovHd7NADdBj+1E
-bddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQXmcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18
-YIvDQVETI53O9zJrlAGomecsMx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7r
-kpeDMdmztcpHWD9f
------END CERTIFICATE-----
-
-TC TrustCenter Universal CA III
-===============================
------BEGIN CERTIFICATE-----
-MIID4TCCAsmgAwIBAgIOYyUAAQACFI0zFQLkbPQwDQYJKoZIhvcNAQEFBQAwezELMAkGA1UEBhMC
-REUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxJDAiBgNVBAsTG1RDIFRydXN0Q2VudGVy
-IFVuaXZlcnNhbCBDQTEoMCYGA1UEAxMfVEMgVHJ1c3RDZW50ZXIgVW5pdmVyc2FsIENBIElJSTAe
-Fw0wOTA5MDkwODE1MjdaFw0yOTEyMzEyMzU5NTlaMHsxCzAJBgNVBAYTAkRFMRwwGgYDVQQKExNU
-QyBUcnVzdENlbnRlciBHbWJIMSQwIgYDVQQLExtUQyBUcnVzdENlbnRlciBVbml2ZXJzYWwgQ0Ex
-KDAmBgNVBAMTH1RDIFRydXN0Q2VudGVyIFVuaXZlcnNhbCBDQSBJSUkwggEiMA0GCSqGSIb3DQEB
-AQUAA4IBDwAwggEKAoIBAQDC2pxisLlxErALyBpXsq6DFJmzNEubkKLF5+cvAqBNLaT6hdqbJYUt
-QCggbergvbFIgyIpRJ9Og+41URNzdNW88jBmlFPAQDYvDIRlzg9uwliT6CwLOunBjvvya8o84pxO
-juT5fdMnnxvVZ3iHLX8LR7PH6MlIfK8vzArZQe+f/prhsq75U7Xl6UafYOPfjdN/+5Z+s7Vy+Eut
-CHnNaYlAJ/Uqwa1D7KRTyGG299J5KmcYdkhtWyUB0SbFt1dpIxVbYYqt8Bst2a9c8SaQaanVDED1
-M4BDj5yjdipFtK+/fz6HP3bFzSreIMUWWMv5G/UPyw0RUmS40nZid4PxWJ//AgMBAAGjYzBhMB8G
-A1UdIwQYMBaAFFbn4VslQ4Dg9ozhcbyO5YAvxEjiMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/
-BAQDAgEGMB0GA1UdDgQWBBRW5+FbJUOA4PaM4XG8juWAL8RI4jANBgkqhkiG9w0BAQUFAAOCAQEA
-g8ev6n9NCjw5sWi+e22JLumzCecYV42FmhfzdkJQEw/HkG8zrcVJYCtsSVgZ1OK+t7+rSbyUyKu+
-KGwWaODIl0YgoGhnYIg5IFHYaAERzqf2EQf27OysGh+yZm5WZ2B6dF7AbZc2rrUNXWZzwCUyRdhK
-BgePxLcHsU0GDeGl6/R1yrqc0L2z0zIkTO5+4nYES0lT2PLpVDP85XEfPRRclkvxOvIAu2y0+pZV
-CIgJwcyRGSmwIC3/yzikQOEXvnlhgP8HA4ZMTnsGnxGGjYnuJ8Tb4rwZjgvDwxPHLQNjO9Po5KIq
-woIIlBZU8O8fJ5AluA0OKBtHd0e9HKgl8ZS0Zg==
------END CERTIFICATE-----
-
-Autoridad de Certificacion Firmaprofesional CIF A62634068
-=========================================================
------BEGIN CERTIFICATE-----
-MIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UEBhMCRVMxQjBA
-BgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2
-MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEyMzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIw
-QAYDVQQDDDlBdXRvcmlkYWQgZGUgQ2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBB
-NjI2MzQwNjgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDD
-Utd9thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQMcas9UX4P
-B99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefGL9ItWY16Ck6WaVICqjaY
-7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15iNA9wBj4gGFrO93IbJWyTdBSTo3OxDqqH
-ECNZXyAFGUftaI6SEspd/NYrspI8IM/hX68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyI
-plD9amML9ZMWGxmPsu2bm8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctX
-MbScyJCyZ/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirjaEbsX
-LZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/TKI8xWVvTyQKmtFLK
-bpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF6NkBiDkal4ZkQdU7hwxu+g/GvUgU
-vzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVhOSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1Ud
-EwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNH
-DhpkLzCBpgYDVR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp
-cm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBvACAAZABlACAA
-bABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBlAGwAbwBuAGEAIAAwADgAMAAx
-ADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx
-51tkljYyGOylMnfX40S2wBEqgLk9am58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qk
-R71kMrv2JYSiJ0L1ILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaP
-T481PyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS3a/DTg4f
-Jl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5kSeTy36LssUzAKh3ntLFl
-osS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF3dvd6qJ2gHN99ZwExEWN57kci57q13XR
-crHedUTnQn3iV2t93Jm8PYMo6oCTjcVMZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoR
-saS8I8nkvof/uZS2+F0gStRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTD
-KCOM/iczQ0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQBjLMi
-6Et8Vcad+qMUu2WFbm5PEn4KPJ2V
------END CERTIFICATE-----
-
-Izenpe.com
-==========
------BEGIN CERTIFICATE-----
-MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4MQswCQYDVQQG
-EwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wHhcNMDcxMjEz
-MTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMu
-QS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ
-03rKDx6sp4boFmVqscIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAK
-ClaOxdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6HLmYRY2xU
-+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFXuaOKmMPsOzTFlUFpfnXC
-PCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQDyCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxT
-OTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbK
-F7jJeodWLBoBHmy+E60QrLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK
-0GqfvEyNBjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8Lhij+
-0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIBQFqNeb+Lz0vPqhbB
-leStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+HMh3/1uaD7euBUbl8agW7EekFwID
-AQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2luZm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+
-SVpFTlBFIFMuQS4gLSBDSUYgQTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBG
-NjIgUzgxQzBBBgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx
-MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O
-BBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUAA4ICAQB4pgwWSp9MiDrAyw6l
-Fn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWblaQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbga
-kEyrkgPH7UIBzg/YsfqikuFgba56awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8q
-hT/AQKM6WfxZSzwoJNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Cs
-g1lwLDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCTVyvehQP5
-aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGkLhObNA5me0mrZJfQRsN5
-nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJbUjWumDqtujWTI6cfSN01RpiyEGjkpTHC
-ClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZo
-Q0iy2+tzJOeRf1SktoA+naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1Z
-WrOZyGlsQyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw==
------END CERTIFICATE-----
-
-Chambers of Commerce Root - 2008
-================================
------BEGIN CERTIFICATE-----
-MIIHTzCCBTegAwIBAgIJAKPaQn6ksa7aMA0GCSqGSIb3DQEBBQUAMIGuMQswCQYDVQQGEwJFVTFD
-MEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNv
-bS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMu
-QS4xKTAnBgNVBAMTIENoYW1iZXJzIG9mIENvbW1lcmNlIFJvb3QgLSAyMDA4MB4XDTA4MDgwMTEy
-Mjk1MFoXDTM4MDczMTEyMjk1MFowga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNl
-ZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29tL2FkZHJlc3MpMRIwEAYDVQQF
-EwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVyZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJl
-cnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
-AQCvAMtwNyuAWko6bHiUfaN/Gh/2NdW928sNRHI+JrKQUrpjOyhYb6WzbZSm891kDFX29ufyIiKA
-XuFixrYp4YFs8r/lfTJqVKAyGVn+H4vXPWCGhSRv4xGzdz4gljUha7MI2XAuZPeEklPWDrCQiorj
-h40G072QDuKZoRuGDtqaCrsLYVAGUvGef3bsyw/QHg3PmTA9HMRFEFis1tPo1+XqxQEHd9ZR5gN/
-ikilTWh1uem8nk4ZcfUyS5xtYBkL+8ydddy/Js2Pk3g5eXNeJQ7KXOt3EgfLZEFHcpOrUMPrCXZk
-NNI5t3YRCQ12RcSprj1qr7V9ZS+UWBDsXHyvfuK2GNnQm05aSd+pZgvMPMZ4fKecHePOjlO+Bd5g
-D2vlGts/4+EhySnB8esHnFIbAURRPHsl18TlUlRdJQfKFiC4reRB7noI/plvg6aRArBsNlVq5331
-lubKgdaX8ZSD6e2wsWsSaR6s+12pxZjptFtYer49okQ6Y1nUCyXeG0+95QGezdIp1Z8XGQpvvwyQ
-0wlf2eOKNcx5Wk0ZN5K3xMGtr/R5JJqyAQuxr1yW84Ay+1w9mPGgP0revq+ULtlVmhduYJ1jbLhj
-ya6BXBg14JC7vjxPNyK5fuvPnnchpj04gftI2jE9K+OJ9dC1vX7gUMQSibMjmhAxhduub+84Mxh2
-EQIDAQABo4IBbDCCAWgwEgYDVR0TAQH/BAgwBgEB/wIBDDAdBgNVHQ4EFgQU+SSsD7K1+HnA+mCI
-G8TZTQKeFxkwgeMGA1UdIwSB2zCB2IAU+SSsD7K1+HnA+mCIG8TZTQKeFxmhgbSkgbEwga4xCzAJ
-BgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNlZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNh
-bWVyZmlybWEuY29tL2FkZHJlc3MpMRIwEAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENh
-bWVyZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDiC
-CQCj2kJ+pLGu2jAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUH
-AgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZIhvcNAQEFBQADggIBAJASryI1
-wqM58C7e6bXpeHxIvj99RZJe6dqxGfwWPJ+0W2aeaufDuV2I6A+tzyMP3iU6XsxPpcG1Lawk0lgH
-3qLPaYRgM+gQDROpI9CF5Y57pp49chNyM/WqfcZjHwj0/gF/JM8rLFQJ3uIrbZLGOU8W6jx+ekbU
-RWpGqOt1glanq6B8aBMz9p0w8G8nOSQjKpD9kCk18pPfNKXG9/jvjA9iSnyu0/VU+I22mlaHFoI6
-M6taIgj3grrqLuBHmrS1RaMFO9ncLkVAO+rcf+g769HsJtg1pDDFOqxXnrN2pSB7+R5KBWIBpih1
-YJeSDW4+TTdDDZIVnBgizVGZoCkaPF+KMjNbMMeJL0eYD6MDxvbxrN8y8NmBGuScvfaAFPDRLLmF
-9dijscilIeUcE5fuDr3fKanvNFNb0+RqE4QGtjICxFKuItLcsiFCGtpA8CnJ7AoMXOLQusxI0zcK
-zBIKinmwPQN/aUv0NCB9szTqjktk9T79syNnFQ0EuPAtwQlRPLJsFfClI9eDdOTlLsn+mCdCxqvG
-nrDQWzilm1DefhiYtUU79nm06PcaewaD+9CL2rvHvRirCG88gGtAPxkZumWK5r7VXNM21+9AUiRg
-OGcEMeyP84LG3rlV8zsxkVrctQgVrXYlCg17LofiDKYGvCYQbTed7N14jHyAxfDZd0jQ
------END CERTIFICATE-----
-
-Global Chambersign Root - 2008
-==============================
------BEGIN CERTIFICATE-----
-MIIHSTCCBTGgAwIBAgIJAMnN0+nVfSPOMA0GCSqGSIb3DQEBBQUAMIGsMQswCQYDVQQGEwJFVTFD
-MEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNv
-bS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMu
-QS4xJzAlBgNVBAMTHkdsb2JhbCBDaGFtYmVyc2lnbiBSb290IC0gMjAwODAeFw0wODA4MDExMjMx
-NDBaFw0zODA3MzExMjMxNDBaMIGsMQswCQYDVQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUg
-Y3VycmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJ
-QTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAlBgNVBAMTHkdsb2JhbCBD
-aGFtYmVyc2lnbiBSb290IC0gMjAwODCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDf
-VtPkOpt2RbQT2//BthmLN0EYlVJH6xedKYiONWwGMi5HYvNJBL99RDaxccy9Wglz1dmFRP+RVyXf
-XjaOcNFccUMd2drvXNL7G706tcuto8xEpw2uIRU/uXpbknXYpBI4iRmKt4DS4jJvVpyR1ogQC7N0
-ZJJ0YPP2zxhPYLIj0Mc7zmFLmY/CDNBAspjcDahOo7kKrmCgrUVSY7pmvWjg+b4aqIG7HkF4ddPB
-/gBVsIdU6CeQNR1MM62X/JcumIS/LMmjv9GYERTtY/jKmIhYF5ntRQOXfjyGHoiMvvKRhI9lNNgA
-TH23MRdaKXoKGCQwoze1eqkBfSbW+Q6OWfH9GzO1KTsXO0G2Id3UwD2ln58fQ1DJu7xsepeY7s2M
-H/ucUa6LcL0nn3HAa6x9kGbo1106DbDVwo3VyJ2dwW3Q0L9R5OP4wzg2rtandeavhENdk5IMagfe
-Ox2YItaswTXbo6Al/3K1dh3ebeksZixShNBFks4c5eUzHdwHU1SjqoI7mjcv3N2gZOnm3b2u/GSF
-HTynyQbehP9r6GsaPMWis0L7iwk+XwhSx2LE1AVxv8Rk5Pihg+g+EpuoHtQ2TS9x9o0o9oOpE9Jh
-wZG7SMA0j0GMS0zbaRL/UJScIINZc+18ofLx/d33SdNDWKBWY8o9PeU1VlnpDsogzCtLkykPAgMB
-AAGjggFqMIIBZjASBgNVHRMBAf8ECDAGAQH/AgEMMB0GA1UdDgQWBBS5CcqcHtvTbDprru1U8VuT
-BjUuXjCB4QYDVR0jBIHZMIHWgBS5CcqcHtvTbDprru1U8VuTBjUuXqGBsqSBrzCBrDELMAkGA1UE
-BhMCRVUxQzBBBgNVBAcTOk1hZHJpZCAoc2VlIGN1cnJlbnQgYWRkcmVzcyBhdCB3d3cuY2FtZXJm
-aXJtYS5jb20vYWRkcmVzcykxEjAQBgNVBAUTCUE4Mjc0MzI4NzEbMBkGA1UEChMSQUMgQ2FtZXJm
-aXJtYSBTLkEuMScwJQYDVQQDEx5HbG9iYWwgQ2hhbWJlcnNpZ24gUm9vdCAtIDIwMDiCCQDJzdPp
-1X0jzjAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUHAgEWHGh0
-dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZIhvcNAQEFBQADggIBAICIf3DekijZBZRG
-/5BXqfEv3xoNa/p8DhxJJHkn2EaqbylZUohwEurdPfWbU1Rv4WCiqAm57OtZfMY18dwY6fFn5a+6
-ReAJ3spED8IXDneRRXozX1+WLGiLwUePmJs9wOzL9dWCkoQ10b42OFZyMVtHLaoXpGNR6woBrX/s
-dZ7LoR/xfxKxueRkf2fWIyr0uDldmOghp+G9PUIadJpwr2hsUF1Jz//7Dl3mLEfXgTpZALVza2Mg
-9jFFCDkO9HB+QHBaP9BrQql0PSgvAm11cpUJjUhjxsYjV5KTXjXBjfkK9yydYhz2rXzdpjEetrHH
-foUm+qRqtdpjMNHvkzeyZi99Bffnt0uYlDXA2TopwZ2yUDMdSqlapskD7+3056huirRXhOukP9Du
-qqqHW2Pok+JrqNS4cnhrG+055F3Lm6qH1U9OAP7Zap88MQ8oAgF9mOinsKJknnn4SPIVqczmyETr
-P3iZ8ntxPjzxmKfFGBI/5rsoM0LpRQp8bfKGeS/Fghl9CYl8slR2iK7ewfPM4W7bMdaTrpmg7yVq
-c5iJWzouE4gev8CSlDQb4ye3ix5vQv/n6TebUB0tovkC7stYWDpxvGjjqsGvHCgfotwjZT+B6q6Z
-09gwzxMNTxXJhLynSC34MCN32EZLeW32jO06f2ARePTpm67VVMB0gNELQp/B
------END CERTIFICATE-----
-
-Go Daddy Root Certificate Authority - G2
-========================================
------BEGIN CERTIFICATE-----
-MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT
-B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoTEUdvRGFkZHkuY29tLCBJbmMu
-MTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5
-MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6
-b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8G
-A1UEAxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI
-hvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKDE6bFIEMBO4Tx5oVJnyfq
-9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD
-+qK+ihVqf94Lw7YZFAXK6sOoBJQ7RnwyDfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutd
-fMh8+7ArU6SSYmlRJQVhGkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMl
-NAJWJwGRtDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEAAaNC
-MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFDqahQcQZyi27/a9
-BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmXWWcDYfF+OwYxdS2hII5PZYe096ac
-vNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r
-5N9ss4UXnT3ZJE95kTXWXwTrgIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYV
-N8Gb5DKj7Tjo2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO
-LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI4uJEvlz36hz1
------END CERTIFICATE-----
-
-Starfield Root Certificate Authority - G2
-=========================================
------BEGIN CERTIFICATE-----
-MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT
-B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s
-b2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVsZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0
-eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAw
-DgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQg
-VGVjaG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZpY2F0ZSBB
-dXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL3twQP89o/8ArFv
-W59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMgnLRJdzIpVv257IzdIvpy3Cdhl+72WoTs
-bhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNk
-N3mSwOxGXn/hbVNMYq/NHwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7Nf
-ZTD4p7dNdloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0HZbU
-JtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
-AQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0GCSqGSIb3DQEBCwUAA4IBAQARWfol
-TwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjUsHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx
-4mcujJUDJi5DnUox9g61DLu34jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUw
-F5okxBDgBPfg8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K
-pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1mMpYjn0q7pBZ
-c2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0
------END CERTIFICATE-----
-
-Starfield Services Root Certificate Authority - G2
-==================================================
------BEGIN CERTIFICATE-----
-MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMxEDAOBgNVBAgT
-B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s
-b2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVsZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRl
-IEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNV
-BAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxT
-dGFyZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2VydmljZXMg
-Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
-AQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20pOsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2
-h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm28xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4Pa
-hHQUw2eeBGg6345AWh1KTs9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLP
-LJGmpufehRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk6mFB
-rMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAwDwYDVR0TAQH/BAUw
-AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+qAdcwKziIorhtSpzyEZGDMA0GCSqG
-SIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMIbw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPP
-E95Dz+I0swSdHynVv/heyNXBve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTy
-xQGjhdByPq1zqwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd
-iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn0q23KXB56jza
-YyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCNsSi6
------END CERTIFICATE-----
-
-AffirmTrust Commercial
-======================
------BEGIN CERTIFICATE-----
-MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UEBhMCVVMxFDAS
-BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMB4XDTEw
-MDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly
-bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEF
-AAOCAQ8AMIIBCgKCAQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6Eqdb
-DuKPHx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yrba0F8PrV
-C8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPALMeIrJmqbTFeurCA+ukV6
-BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1yHp52UKqK39c/s4mT6NmgTWvRLpUHhww
-MmWd5jyTXlBOeuM61G7MGvv50jeuJCqrVwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNV
-HQ4EFgQUnZPGU4teyq8/nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
-AQYwDQYJKoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYGXUPG
-hi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNjvbz4YYCanrHOQnDi
-qX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivtZ8SOyUOyXGsViQK8YvxO8rUzqrJv
-0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9gN53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0kh
-sUlHRUe072o0EclNmsxZt9YCnlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8=
------END CERTIFICATE-----
-
-AffirmTrust Networking
-======================
------BEGIN CERTIFICATE-----
-MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UEBhMCVVMxFDAS
-BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMB4XDTEw
-MDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly
-bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEF
-AAOCAQ8AMIIBCgKCAQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SE
-Hi3yYJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbuakCNrmreI
-dIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRLQESxG9fhwoXA3hA/Pe24
-/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gb
-h+0t+nvujArjqWaJGctB+d1ENmHP4ndGyH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNV
-HQ4EFgQUBx/S55zawm6iQLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
-AQYwDQYJKoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfOtDIu
-UFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzuQY0x2+c06lkh1QF6
-12S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZLgo/bNjR9eUJtGxUAArgFU2HdW23
-WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4uolu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9
-/ZFvgrG+CJPbFEfxojfHRZ48x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s=
------END CERTIFICATE-----
-
-AffirmTrust Premium
-===================
------BEGIN CERTIFICATE-----
-MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UEBhMCVVMxFDAS
-BgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMB4XDTEwMDEy
-OTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRy
-dXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
-MIICCgKCAgEAxBLfqV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtn
-BKAQJG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ+jjeRFcV
-5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrSs8PhaJyJ+HoAVt70VZVs
-+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmd
-GPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d770O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5R
-p9EixAqnOEhss/n/fauGV+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NI
-S+LI+H+SqHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S5u04
-6uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4IaC1nEWTJ3s7xgaVY5
-/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TXOwF0lkLgAOIua+rF7nKsu7/+6qqo
-+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYEFJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB
-/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByv
-MiPIs0laUZx2KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg
-Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B8OWycvpEgjNC
-6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQMKSOyARiqcTtNd56l+0OOF6S
-L5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK
-+4w1IX2COPKpVJEZNZOUbWo6xbLQu4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmV
-BtWVyuEklut89pMFu+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFg
-IxpHYoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8GKa1qF60
-g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaORtGdFNrHF+QFlozEJLUb
-zxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6eKeC2uAloGRwYQw==
------END CERTIFICATE-----
-
-AffirmTrust Premium ECC
-=======================
------BEGIN CERTIFICATE-----
-MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMCVVMxFDASBgNV
-BAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQcmVtaXVtIEVDQzAeFw0xMDAx
-MjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1U
-cnVzdDEgMB4GA1UEAwwXQWZmaXJtVHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQA
-IgNiAAQNMF4bFZ0D0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQ
-N8O9ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0GA1UdDgQW
-BBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAK
-BggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/VsaobgxCd05DhT1wV/GzTjxi+zygk8N53X
-57hG8f2h4nECMEJZh0PUUd+60wkyWs6Iflc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKM
-eQ==
------END CERTIFICATE-----
-
-Certum Trusted Network CA
-=========================
------BEGIN CERTIFICATE-----
-MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBMMSIwIAYDVQQK
-ExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlv
-biBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBUcnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIy
-MTIwNzM3WhcNMjkxMjMxMTIwNzM3WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBU
-ZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5
-MSIwIAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC
-AQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rHUV+rpDKmYYe2bg+G0jAC
-l/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LMTXPb865Px1bVWqeWifrzq2jUI4ZZJ88J
-J7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVUBBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4
-fOQtf/WsX+sWn7Et0brMkUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0
-cvW0QM8xAcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNVHRMB
-Af8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNVHQ8BAf8EBAMCAQYw
-DQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15ysHhE49wcrwn9I0j6vSrEuVUEtRCj
-jSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfLI9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1
-mS1FhIrlQgnXdAIv94nYmem8J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5aj
-Zt3hrvJBW8qYVoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI
-03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw=
------END CERTIFICATE-----
-
-Certinomis - Autorité Racine
-=============================
------BEGIN CERTIFICATE-----
-MIIFnDCCA4SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJGUjETMBEGA1UEChMK
-Q2VydGlub21pczEXMBUGA1UECxMOMDAwMiA0MzM5OTg5MDMxJjAkBgNVBAMMHUNlcnRpbm9taXMg
-LSBBdXRvcml0w6kgUmFjaW5lMB4XDTA4MDkxNzA4Mjg1OVoXDTI4MDkxNzA4Mjg1OVowYzELMAkG
-A1UEBhMCRlIxEzARBgNVBAoTCkNlcnRpbm9taXMxFzAVBgNVBAsTDjAwMDIgNDMzOTk4OTAzMSYw
-JAYDVQQDDB1DZXJ0aW5vbWlzIC0gQXV0b3JpdMOpIFJhY2luZTCCAiIwDQYJKoZIhvcNAQEBBQAD
-ggIPADCCAgoCggIBAJ2Fn4bT46/HsmtuM+Cet0I0VZ35gb5j2CN2DpdUzZlMGvE5x4jYF1AMnmHa
-wE5V3udauHpOd4cN5bjr+p5eex7Ezyh0x5P1FMYiKAT5kcOrJ3NqDi5N8y4oH3DfVS9O7cdxbwly
-Lu3VMpfQ8Vh30WC8Tl7bmoT2R2FFK/ZQpn9qcSdIhDWerP5pqZ56XjUl+rSnSTV3lqc2W+HN3yNw
-2F1MpQiD8aYkOBOo7C+ooWfHpi2GR+6K/OybDnT0K0kCe5B1jPyZOQE51kqJ5Z52qz6WKDgmi92N
-jMD2AR5vpTESOH2VwnHu7XSu5DaiQ3XV8QCb4uTXzEIDS3h65X27uK4uIJPT5GHfceF2Z5c/tt9q
-c1pkIuVC28+BA5PY9OMQ4HL2AHCs8MF6DwV/zzRpRbWT5BnbUhYjBYkOjUjkJW+zeL9i9Qf6lSTC
-lrLooyPCXQP8w9PlfMl1I9f09bze5N/NgL+RiH2nE7Q5uiy6vdFrzPOlKO1Enn1So2+WLhl+HPNb
-xxaOu2B9d2ZHVIIAEWBsMsGoOBvrbpgT1u449fCfDu/+MYHB0iSVL1N6aaLwD4ZFjliCK0wi1F6g
-530mJ0jfJUaNSih8hp75mxpZuWW/Bd22Ql095gBIgl4g9xGC3srYn+Y3RyYe63j3YcNBZFgCQfna
-4NH4+ej9Uji29YnfAgMBAAGjWzBZMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G
-A1UdDgQWBBQNjLZh2kS40RR9w759XkjwzspqsDAXBgNVHSAEEDAOMAwGCiqBegFWAgIAAQEwDQYJ
-KoZIhvcNAQEFBQADggIBACQ+YAZ+He86PtvqrxyaLAEL9MW12Ukx9F1BjYkMTv9sov3/4gbIOZ/x
-WqndIlgVqIrTseYyCYIDbNc/CMf4uboAbbnW/FIyXaR/pDGUu7ZMOH8oMDX/nyNTt7buFHAAQCva
-R6s0fl6nVjBhK4tDrP22iCj1a7Y+YEq6QpA0Z43q619FVDsXrIvkxmUP7tCMXWY5zjKn2BCXwH40
-nJ+U8/aGH88bc62UeYdocMMzpXDn2NU4lG9jeeu/Cg4I58UvD0KgKxRA/yHgBcUn4YQRE7rWhh1B
-CxMjidPJC+iKunqjo3M3NYB9Ergzd0A4wPpeMNLytqOx1qKVl4GbUu1pTP+A5FPbVFsDbVRfsbjv
-JL1vnxHDx2TCDyhihWZeGnuyt++uNckZM6i4J9szVb9o4XVIRFb7zdNIu0eJOqxp9YDG5ERQL1TE
-qkPFMTFYvZbF6nVsmnWxTfj3l/+WFvKXTej28xH5On2KOG4Ey+HTRRWqpdEdnV1j6CTmNhTih60b
-WfVEm/vXd3wfAXBioSAaosUaKPQhA+4u2cGA6rnZgtZbdsLLO7XSAPCjDuGtbkD326C00EauFddE
-wk01+dIL8hf2rGbVJLJP0RyZwG71fet0BLj5TXcJ17TPBzAJ8bgAVtkXFhYKK4bfjwEZGuW7gmP/
-vgt2Fl43N+bYdJeimUV5
------END CERTIFICATE-----
-
-Root CA Generalitat Valenciana
-==============================
------BEGIN CERTIFICATE-----
-MIIGizCCBXOgAwIBAgIEO0XlaDANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJFUzEfMB0GA1UE
-ChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UECxMGUEtJR1ZBMScwJQYDVQQDEx5Sb290
-IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmEwHhcNMDEwNzA2MTYyMjQ3WhcNMjEwNzAxMTUyMjQ3
-WjBoMQswCQYDVQQGEwJFUzEfMB0GA1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UE
-CxMGUEtJR1ZBMScwJQYDVQQDEx5Sb290IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmEwggEiMA0G
-CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGKqtXETcvIorKA3Qdyu0togu8M1JAJke+WmmmO3I2
-F0zo37i7L3bhQEZ0ZQKQUgi0/6iMweDHiVYQOTPvaLRfX9ptI6GJXiKjSgbwJ/BXufjpTjJ3Cj9B
-ZPPrZe52/lSqfR0grvPXdMIKX/UIKFIIzFVd0g/bmoGlu6GzwZTNVOAydTGRGmKy3nXiz0+J2ZGQ
-D0EbtFpKd71ng+CT516nDOeB0/RSrFOyA8dEJvt55cs0YFAQexvba9dHq198aMpunUEDEO5rmXte
-JajCq+TA81yc477OMUxkHl6AovWDfgzWyoxVjr7gvkkHD6MkQXpYHYTqWBLI4bft75PelAgxAgMB
-AAGjggM7MIIDNzAyBggrBgEFBQcBAQQmMCQwIgYIKwYBBQUHMAGGFmh0dHA6Ly9vY3NwLnBraS5n
-dmEuZXMwEgYDVR0TAQH/BAgwBgEB/wIBAjCCAjQGA1UdIASCAiswggInMIICIwYKKwYBBAG/VQIB
-ADCCAhMwggHoBggrBgEFBQcCAjCCAdoeggHWAEEAdQB0AG8AcgBpAGQAYQBkACAAZABlACAAQwBl
-AHIAdABpAGYAaQBjAGEAYwBpAPMAbgAgAFIAYQDtAHoAIABkAGUAIABsAGEAIABHAGUAbgBlAHIA
-YQBsAGkAdABhAHQAIABWAGEAbABlAG4AYwBpAGEAbgBhAC4ADQAKAEwAYQAgAEQAZQBjAGwAYQBy
-AGEAYwBpAPMAbgAgAGQAZQAgAFAAcgDhAGMAdABpAGMAYQBzACAAZABlACAAQwBlAHIAdABpAGYA
-aQBjAGEAYwBpAPMAbgAgAHEAdQBlACAAcgBpAGcAZQAgAGUAbAAgAGYAdQBuAGMAaQBvAG4AYQBt
-AGkAZQBuAHQAbwAgAGQAZQAgAGwAYQAgAHAAcgBlAHMAZQBuAHQAZQAgAEEAdQB0AG8AcgBpAGQA
-YQBkACAAZABlACAAQwBlAHIAdABpAGYAaQBjAGEAYwBpAPMAbgAgAHMAZQAgAGUAbgBjAHUAZQBu
-AHQAcgBhACAAZQBuACAAbABhACAAZABpAHIAZQBjAGMAaQDzAG4AIAB3AGUAYgAgAGgAdAB0AHAA
-OgAvAC8AdwB3AHcALgBwAGsAaQAuAGcAdgBhAC4AZQBzAC8AYwBwAHMwJQYIKwYBBQUHAgEWGWh0
-dHA6Ly93d3cucGtpLmd2YS5lcy9jcHMwHQYDVR0OBBYEFHs100DSHHgZZu90ECjcPk+yeAT8MIGV
-BgNVHSMEgY0wgYqAFHs100DSHHgZZu90ECjcPk+yeAT8oWykajBoMQswCQYDVQQGEwJFUzEfMB0G
-A1UEChMWR2VuZXJhbGl0YXQgVmFsZW5jaWFuYTEPMA0GA1UECxMGUEtJR1ZBMScwJQYDVQQDEx5S
-b290IENBIEdlbmVyYWxpdGF0IFZhbGVuY2lhbmGCBDtF5WgwDQYJKoZIhvcNAQEFBQADggEBACRh
-TvW1yEICKrNcda3FbcrnlD+laJWIwVTAEGmiEi8YPyVQqHxK6sYJ2fR1xkDar1CdPaUWu20xxsdz
-Ckj+IHLtb8zog2EWRpABlUt9jppSCS/2bxzkoXHPjCpaF3ODR00PNvsETUlR4hTJZGH71BTg9J63
-NI8KJr2XXPR5OkowGcytT6CYirQxlyric21+eLj4iIlPsSKRZEv1UN4D2+XFducTZnV+ZfsBn5OH
-iJ35Rld8TWCvmHMTI6QgkYH60GFmuH3Rr9ZvHmw96RH9qfmCIoaZM3Fa6hlXPZHNqcCjbgcTpsnt
-+GijnsNacgmHKNHEc8RzGF9QdRYxn7fofMM=
------END CERTIFICATE-----
-
-A-Trust-nQual-03
-================
------BEGIN CERTIFICATE-----
-MIIDzzCCAregAwIBAgIDAWweMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYDVQQGEwJBVDFIMEYGA1UE
-Cgw/QS1UcnVzdCBHZXMuIGYuIFNpY2hlcmhlaXRzc3lzdGVtZSBpbSBlbGVrdHIuIERhdGVudmVy
-a2VociBHbWJIMRkwFwYDVQQLDBBBLVRydXN0LW5RdWFsLTAzMRkwFwYDVQQDDBBBLVRydXN0LW5R
-dWFsLTAzMB4XDTA1MDgxNzIyMDAwMFoXDTE1MDgxNzIyMDAwMFowgY0xCzAJBgNVBAYTAkFUMUgw
-RgYDVQQKDD9BLVRydXN0IEdlcy4gZi4gU2ljaGVyaGVpdHNzeXN0ZW1lIGltIGVsZWt0ci4gRGF0
-ZW52ZXJrZWhyIEdtYkgxGTAXBgNVBAsMEEEtVHJ1c3QtblF1YWwtMDMxGTAXBgNVBAMMEEEtVHJ1
-c3QtblF1YWwtMDMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtPWFuA/OQO8BBC4SA
-zewqo51ru27CQoT3URThoKgtUaNR8t4j8DRE/5TrzAUjlUC5B3ilJfYKvUWG6Nm9wASOhURh73+n
-yfrBJcyFLGM/BWBzSQXgYHiVEEvc+RFZznF/QJuKqiTfC0Li21a8StKlDJu3Qz7dg9MmEALP6iPE
-SU7l0+m0iKsMrmKS1GWH2WrX9IWf5DMiJaXlyDO6w8dB3F/GaswADm0yqLaHNgBid5seHzTLkDx4
-iHQF63n1k3Flyp3HaxgtPVxO59X4PzF9j4fsCiIvI+n+u33J4PTs63zEsMMtYrWacdaxaujs2e3V
-cuy+VwHOBVWf3tFgiBCzAgMBAAGjNjA0MA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0OBAoECERqlWdV
-eRFPMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAVdRU0VlIXLOThaq/Yy/kgM40
-ozRiPvbY7meIMQQDbwvUB/tOdQ/TLtPAF8fGKOwGDREkDg6lXb+MshOWcdzUzg4NCmgybLlBMRmr
-sQd7TZjTXLDR8KdCoLXEjq/+8T/0709GAHbrAvv5ndJAlseIOrifEXnzgGWovR/TeIGgUUw3tKZd
-JXDRZslo+S4RFGjxVJgIrCaSD96JntT6s3kr0qN51OyLrIdTaEJMUVF0HhsnLuP1Hyl0Te2v9+GS
-mYHovjrHF1D2t8b8m7CKa9aIA5GPBnc6hQLdmNVDeD/GMBWsm2vLV7eJUYs66MmEDNuxUCAKGkq6
-ahq97BvIxYSazQ==
------END CERTIFICATE-----
-
-TWCA Root Certification Authority
-=================================
------BEGIN CERTIFICATE-----
-MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJ
-VEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlmaWNh
-dGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMzWhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQG
-EwJUVzESMBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NB
-IFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
-AoIBAQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFEAcK0HMMx
-QhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HHK3XLfJ+utdGdIzdjp9xC
-oi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeXRfwZVzsrb+RH9JlF/h3x+JejiB03HFyP
-4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/zrX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1r
-y+UPizgN7gr8/g+YnzAx3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIB
-BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkqhkiG
-9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeCMErJk/9q56YAf4lC
-mtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdlsXebQ79NqZp4VKIV66IIArB6nCWlW
-QtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62Dlhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVY
-T0bf+215WfKEIlKuD8z7fDvnaspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocny
-Yh0igzyXxfkZYiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw==
------END CERTIFICATE-----
-
-Security Communication RootCA2
-==============================
------BEGIN CERTIFICATE-----
-MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDElMCMGA1UEChMc
-U0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMeU2VjdXJpdHkgQ29tbXVuaWNh
-dGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoXDTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMC
-SlAxJTAjBgNVBAoTHFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3Vy
-aXR5IENvbW11bmljYXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
-ANAVOVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGrzbl+dp++
-+T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVMVAX3NuRFg3sUZdbcDE3R
-3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQhNBqyjoGADdH5H5XTz+L62e4iKrFvlNV
-spHEfbmwhRkGeC7bYRr6hfVKkaHnFtWOojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1K
-EOtOghY6rCcMU/Gt1SSwawNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8
-QIH4D5csOPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEB
-CwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpFcoJxDjrSzG+ntKEj
-u/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXcokgfGT+Ok+vx+hfuzU7jBBJV1uXk
-3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6q
-tnRGEmyR7jTV7JqR50S+kDFy1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29
-mvVXIwAHIRc/SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03
------END CERTIFICATE-----
-
-EC-ACC
-======
------BEGIN CERTIFICATE-----
-MIIFVjCCBD6gAwIBAgIQ7is969Qh3hSoYqwE893EATANBgkqhkiG9w0BAQUFADCB8zELMAkGA1UE
-BhMCRVMxOzA5BgNVBAoTMkFnZW5jaWEgQ2F0YWxhbmEgZGUgQ2VydGlmaWNhY2lvIChOSUYgUS0w
-ODAxMTc2LUkpMSgwJgYDVQQLEx9TZXJ2ZWlzIFB1YmxpY3MgZGUgQ2VydGlmaWNhY2lvMTUwMwYD
-VQQLEyxWZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAoYykwMzE1MDMGA1UE
-CxMsSmVyYXJxdWlhIEVudGl0YXRzIGRlIENlcnRpZmljYWNpbyBDYXRhbGFuZXMxDzANBgNVBAMT
-BkVDLUFDQzAeFw0wMzAxMDcyMzAwMDBaFw0zMTAxMDcyMjU5NTlaMIHzMQswCQYDVQQGEwJFUzE7
-MDkGA1UEChMyQWdlbmNpYSBDYXRhbGFuYSBkZSBDZXJ0aWZpY2FjaW8gKE5JRiBRLTA4MDExNzYt
-SSkxKDAmBgNVBAsTH1NlcnZlaXMgUHVibGljcyBkZSBDZXJ0aWZpY2FjaW8xNTAzBgNVBAsTLFZl
-Z2V1IGh0dHBzOi8vd3d3LmNhdGNlcnQubmV0L3ZlcmFycmVsIChjKTAzMTUwMwYDVQQLEyxKZXJh
-cnF1aWEgRW50aXRhdHMgZGUgQ2VydGlmaWNhY2lvIENhdGFsYW5lczEPMA0GA1UEAxMGRUMtQUND
-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsyLHT+KXQpWIR4NA9h0X84NzJB5R85iK
-w5K4/0CQBXCHYMkAqbWUZRkiFRfCQ2xmRJoNBD45b6VLeqpjt4pEndljkYRm4CgPukLjbo73FCeT
-ae6RDqNfDrHrZqJyTxIThmV6PttPB/SnCWDaOkKZx7J/sxaVHMf5NLWUhdWZXqBIoH7nF2W4onW4
-HvPlQn2v7fOKSGRdghST2MDk/7NQcvJ29rNdQlB50JQ+awwAvthrDk4q7D7SzIKiGGUzE3eeml0a
-E9jD2z3Il3rucO2n5nzbcc8tlGLfbdb1OL4/pYUKGbio2Al1QnDE6u/LDsg0qBIimAy4E5S2S+zw
-0JDnJwIDAQABo4HjMIHgMB0GA1UdEQQWMBSBEmVjX2FjY0BjYXRjZXJ0Lm5ldDAPBgNVHRMBAf8E
-BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUoMOLRKo3pUW/l4Ba0fF4opvpXY0wfwYD
-VR0gBHgwdjB0BgsrBgEEAfV4AQMBCjBlMCwGCCsGAQUFBwIBFiBodHRwczovL3d3dy5jYXRjZXJ0
-Lm5ldC92ZXJhcnJlbDA1BggrBgEFBQcCAjApGidWZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5l
-dC92ZXJhcnJlbCAwDQYJKoZIhvcNAQEFBQADggEBAKBIW4IB9k1IuDlVNZyAelOZ1Vr/sXE7zDkJ
-lF7W2u++AVtd0x7Y/X1PzaBB4DSTv8vihpw3kpBWHNzrKQXlxJ7HNd+KDM3FIUPpqojlNcAZQmNa
-Al6kSBg6hW/cnbw/nZzBh7h6YQjpdwt/cKt63dmXLGQehb+8dJahw3oS7AwaboMMPOhyRp/7SNVe
-l+axofjk70YllJyJ22k4vuxcDlbHZVHlUIiIv0LVKz3l+bqeLrPK9HOSAgu+TGbrIP65y7WZf+a2
-E/rKS03Z7lNGBjvGTq2TWoF+bCpLagVFjPIhpDGQh2xlnJ2lYJU6Un/10asIbvPuW/mIPX64b24D
-5EI=
------END CERTIFICATE-----
-
-Hellenic Academic and Research Institutions RootCA 2011
-=======================================================
------BEGIN CERTIFICATE-----
-MIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1IxRDBCBgNVBAoT
-O0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9y
-aXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25z
-IFJvb3RDQSAyMDExMB4XDTExMTIwNjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYT
-AkdSMUQwQgYDVQQKEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25z
-IENlcnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNo
-IEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
-AKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPzdYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI
-1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJfel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa
-71HFK9+WXesyHgLacEnsbgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u
-8yBRQlqD75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSPFEDH
-3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNVHRMBAf8EBTADAQH/
-MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp5dgTBCPuQSUwRwYDVR0eBEAwPqA8
-MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQub3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQu
-b3JnMA0GCSqGSIb3DQEBBQUAA4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVt
-XdMiKahsog2p6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8
-TqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7dIsXRSZMFpGD
-/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8AcysNnq/onN694/BtZqhFLKPM58N
-7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXIl7WdmplNsDz4SgCbZN2fOUvRJ9e4
------END CERTIFICATE-----
-
-Actalis Authentication Root CA
-==============================
------BEGIN CERTIFICATE-----
-MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCSVQxDjAM
-BgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UE
-AwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDky
-MjExMjIwMlowazELMAkGA1UEBhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlz
-IFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290
-IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNvUTufClrJ
-wkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX4ay8IMKx4INRimlNAJZa
-by/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9KK3giq0itFZljoZUj5NDKd45RnijMCO6
-zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1f
-YVEiVRvjRuPjPdA1YprbrxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2
-oxgkg4YQ51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2Fbe8l
-EfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxeKF+w6D9Fz8+vm2/7
-hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4Fv6MGn8i1zeQf1xcGDXqVdFUNaBr8
-EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbnfpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5
-jF66CyCU3nuDuP/jVo23Eek7jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLY
-iDrIn3hm7YnzezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt
-ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQALe3KHwGCmSUyI
-WOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70jsNjLiNmsGe+b7bAEzlgqqI0
-JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDzWochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKx
-K3JCaKygvU5a2hi/a5iB0P2avl4VSM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+
-Xlff1ANATIGk0k9jpwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC
-4yyXX04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+OkfcvHlXHo
-2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7RK4X9p2jIugErsWx0Hbhz
-lefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btUZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXem
-OR/qnuOf0GZvBeyqdn6/axag67XH/JJULysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9
-vwGYT7JZVEc+NHt4bVaTLnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg==
------END CERTIFICATE-----
-
-Trustis FPS Root CA
-===================
------BEGIN CERTIFICATE-----
-MIIDZzCCAk+gAwIBAgIQGx+ttiD5JNM2a/fH8YygWTANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQG
-EwJHQjEYMBYGA1UEChMPVHJ1c3RpcyBMaW1pdGVkMRwwGgYDVQQLExNUcnVzdGlzIEZQUyBSb290
-IENBMB4XDTAzMTIyMzEyMTQwNloXDTI0MDEyMTExMzY1NFowRTELMAkGA1UEBhMCR0IxGDAWBgNV
-BAoTD1RydXN0aXMgTGltaXRlZDEcMBoGA1UECxMTVHJ1c3RpcyBGUFMgUm9vdCBDQTCCASIwDQYJ
-KoZIhvcNAQEBBQADggEPADCCAQoCggEBAMVQe547NdDfxIzNjpvto8A2mfRC6qc+gIMPpqdZh8mQ
-RUN+AOqGeSoDvT03mYlmt+WKVoaTnGhLaASMk5MCPjDSNzoiYYkchU59j9WvezX2fihHiTHcDnlk
-H5nSW7r+f2C/revnPDgpai/lkQtV/+xvWNUtyd5MZnGPDNcE2gfmHhjjvSkCqPoc4Vu5g6hBSLwa
-cY3nYuUtsuvffM/bq1rKMfFMIvMFE/eC+XN5DL7XSxzA0RU8k0Fk0ea+IxciAIleH2ulrG6nS4zt
-o3Lmr2NNL4XSFDWaLk6M6jKYKIahkQlBOrTh4/L68MkKokHdqeMDx4gVOxzUGpTXn2RZEm0CAwEA
-AaNTMFEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS6+nEleYtXQSUhhgtx67JkDoshZzAd
-BgNVHQ4EFgQUuvpxJXmLV0ElIYYLceuyZA6LIWcwDQYJKoZIhvcNAQEFBQADggEBAH5Y//01GX2c
-GE+esCu8jowU/yyg2kdbw++BLa8F6nRIW/M+TgfHbcWzk88iNVy2P3UnXwmWzaD+vkAMXBJV+JOC
-yinpXj9WV4s4NvdFGkwozZ5BuO1WTISkQMi4sKUraXAEasP41BIy+Q7DsdwyhEQsb8tGD+pmQQ9P
-8Vilpg0ND2HepZ5dfWWhPBfnqFVO76DH7cZEf1T1o+CP8HxVIo8ptoGj4W1OLBuAZ+ytIJ8MYmHV
-l/9D7S3B2l0pKoU/rGXuhg8FjZBf3+6f9L/uHfuY5H+QK4R4EA5sSVPvFVtlRkpdr7r7OnIdzfYl
-iB6XzCGcKQENZetX2fNXlrtIzYE=
------END CERTIFICATE-----
-
-StartCom Certification Authority
-================================
------BEGIN CERTIFICATE-----
-MIIHhzCCBW+gAwIBAgIBLTANBgkqhkiG9w0BAQsFADB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMN
-U3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmlu
-ZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0
-NjM3WhcNMzYwOTE3MTk0NjM2WjB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRk
-LjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMg
-U3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw
-ggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZkpMyONvg45iPwbm2xPN1y
-o4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rfOQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/
-Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/CJi/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/d
-eMotHweXMAEtcnn6RtYTKqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt
-2PZE4XNiHzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMMAv+Z
-6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w+2OqqGwaVLRcJXrJ
-osmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/
-untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVc
-UjyJthkqcwEKDwOzEmDyei+B26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT
-37uMdBNSSwIDAQABo4ICEDCCAgwwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD
-VR0OBBYEFE4L7xqkQFulF2mHMMo0aEPQQa7yMB8GA1UdIwQYMBaAFE4L7xqkQFulF2mHMMo0aEPQ
-Qa7yMIIBWgYDVR0gBIIBUTCCAU0wggFJBgsrBgEEAYG1NwEBATCCATgwLgYIKwYBBQUHAgEWImh0
-dHA6Ly93d3cuc3RhcnRzc2wuY29tL3BvbGljeS5wZGYwNAYIKwYBBQUHAgEWKGh0dHA6Ly93d3cu
-c3RhcnRzc2wuY29tL2ludGVybWVkaWF0ZS5wZGYwgc8GCCsGAQUFBwICMIHCMCcWIFN0YXJ0IENv
-bW1lcmNpYWwgKFN0YXJ0Q29tKSBMdGQuMAMCAQEagZZMaW1pdGVkIExpYWJpbGl0eSwgcmVhZCB0
-aGUgc2VjdGlvbiAqTGVnYWwgTGltaXRhdGlvbnMqIG9mIHRoZSBTdGFydENvbSBDZXJ0aWZpY2F0
-aW9uIEF1dGhvcml0eSBQb2xpY3kgYXZhaWxhYmxlIGF0IGh0dHA6Ly93d3cuc3RhcnRzc2wuY29t
-L3BvbGljeS5wZGYwEQYJYIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilTdGFydENvbSBG
-cmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQsFAAOCAgEAjo/n3JR5
-fPGFf59Jb2vKXfuM/gTFwWLRfUKKvFO3lANmMD+x5wqnUCBVJX92ehQN6wQOQOY+2IirByeDqXWm
-N3PH/UvSTa0XQMhGvjt/UfzDtgUx3M2FIk5xt/JxXrAaxrqTi3iSSoX4eA+D/i+tLPfkpLst0OcN
-Org+zvZ49q5HJMqjNTbOx8aHmNrs++myziebiMMEofYLWWivydsQD032ZGNcpRJvkrKTlMeIFw6T
-tn5ii5B/q06f/ON1FE8qMt9bDeD1e5MNq6HPh+GlBEXoPBKlCcWw0bdT82AUuoVpaiF8H3VhFyAX
-e2w7QSlc4axa0c2Mm+tgHRns9+Ww2vl5GKVFP0lDV9LdJNUso/2RjSe15esUBppMeyG7Oq0wBhjA
-2MFrLH9ZXF2RsXAiV+uKa0hK1Q8p7MZAwC+ITGgBF3f0JBlPvfrhsiAhS90a2Cl9qrjeVOwhVYBs
-HvUwyKMQ5bLmKhQxw4UtjJixhlpPiVktucf3HMiKf8CdBUrmQk9io20ppB+Fq9vlgcitKj1MXVuE
-JnHEhV5xJMqlG2zYYdMa4FTbzrqpMrUi9nNBCV24F10OD5mQ1kfabwo6YigUZ4LZ8dCAWZvLMdib
-D4x3TrVoivJs9iQOLWxwxXPR3hTQcY+203sC9uO41Alua551hDnmfyWl8kgAwKQB2j8=
------END CERTIFICATE-----
-
-StartCom Certification Authority G2
-===================================
------BEGIN CERTIFICATE-----
-MIIFYzCCA0ugAwIBAgIBOzANBgkqhkiG9w0BAQsFADBTMQswCQYDVQQGEwJJTDEWMBQGA1UEChMN
-U3RhcnRDb20gTHRkLjEsMCoGA1UEAxMjU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg
-RzIwHhcNMTAwMTAxMDEwMDAxWhcNMzkxMjMxMjM1OTAxWjBTMQswCQYDVQQGEwJJTDEWMBQGA1UE
-ChMNU3RhcnRDb20gTHRkLjEsMCoGA1UEAxMjU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3Jp
-dHkgRzIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2iTZbB7cgNr2Cu+EWIAOVeq8O
-o1XJJZlKxdBWQYeQTSFgpBSHO839sj60ZwNq7eEPS8CRhXBF4EKe3ikj1AENoBB5uNsDvfOpL9HG
-4A/LnooUCri99lZi8cVytjIl2bLzvWXFDSxu1ZJvGIsAQRSCb0AgJnooD/Uefyf3lLE3PbfHkffi
-Aez9lInhzG7TNtYKGXmu1zSCZf98Qru23QumNK9LYP5/Q0kGi4xDuFby2X8hQxfqp0iVAXV16iul
-Q5XqFYSdCI0mblWbq9zSOdIxHWDirMxWRST1HFSr7obdljKF+ExP6JV2tgXdNiNnvP8V4so75qbs
-O+wmETRIjfaAKxojAuuKHDp2KntWFhxyKrOq42ClAJ8Em+JvHhRYW6Vsi1g8w7pOOlz34ZYrPu8H
-vKTlXcxNnw3h3Kq74W4a7I/htkxNeXJdFzULHdfBR9qWJODQcqhaX2YtENwvKhOuJv4KHBnM0D4L
-nMgJLvlblnpHnOl68wVQdJVznjAJ85eCXuaPOQgeWeU1FEIT/wCc976qUM/iUUjXuG+v+E5+M5iS
-FGI6dWPPe/regjupuznixL0sAA7IF6wT700ljtizkC+p2il9Ha90OrInwMEePnWjFqmveiJdnxMa
-z6eg6+OGCtP95paV1yPIN93EfKo2rJgaErHgTuixO/XWb/Ew1wIDAQABo0IwQDAPBgNVHRMBAf8E
-BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUS8W0QGutHLOlHGVuRjaJhwUMDrYwDQYJ
-KoZIhvcNAQELBQADggIBAHNXPyzVlTJ+N9uWkusZXn5T50HsEbZH77Xe7XRcxfGOSeD8bpkTzZ+K
-2s06Ctg6Wgk/XzTQLwPSZh0avZyQN8gMjgdalEVGKua+etqhqaRpEpKwfTbURIfXUfEpY9Z1zRbk
-J4kd+MIySP3bmdCPX1R0zKxnNBFi2QwKN4fRoxdIjtIXHfbX/dtl6/2o1PXWT6RbdejF0mCy2wl+
-JYt7ulKSnj7oxXehPOBKc2thz4bcQ///If4jXSRK9dNtD2IEBVeC2m6kMyV5Sy5UGYvMLD0w6dEG
-/+gyRr61M3Z3qAFdlsHB1b6uJcDJHgoJIIihDsnzb02CVAAgp9KP5DlUFy6NHrgbuxu9mk47EDTc
-nIhT76IxW1hPkWLIwpqazRVdOKnWvvgTtZ8SafJQYqz7Fzf07rh1Z2AQ+4NQ+US1dZxAF7L+/Xld
-blhYXzD8AK6vM8EOTmy6p6ahfzLbOOCxchcKK5HsamMm7YnUeMx0HgX4a/6ManY5Ka5lIxKVCCIc
-l85bBu4M4ru8H0ST9tg4RQUh7eStqxK2A6RCLi3ECToDZ2mEmuFZkIoohdVddLHRDiBYmxOlsGOm
-7XtH/UVVMKTumtTm4ofvmMkyghEpIrwACjFeLQ/Ajulrso8uBtjRkcfGEvRM/TAXw8HaOFvjqerm
-obp573PYtlNXLfbQ4ddI
------END CERTIFICATE-----
-
-Buypass Class 2 Root CA
-=======================
------BEGIN CERTIFICATE-----
-MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU
-QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMiBSb290IENBMB4X
-DTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1owTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1
-eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIw
-DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1
-g1Lr6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPVL4O2fuPn
-9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC911K2GScuVr1QGbNgGE41b
-/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHxMlAQTn/0hpPshNOOvEu/XAFOBz3cFIqU
-CqTqc/sLUegTBxj6DvEr0VQVfTzh97QZQmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeff
-awrbD02TTqigzXsu8lkBarcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgI
-zRFo1clrUs3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLiFRhn
-Bkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRSP/TizPJhk9H9Z2vX
-Uq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN9SG9dKpN6nIDSdvHXx1iY8f93ZHs
-M+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxPAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD
-VR0OBBYEFMmAd+BikoL1RpzzuvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF
-AAOCAgEAU18h9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s
-A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3tOluwlN5E40EI
-osHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo+fsicdl9sz1Gv7SEr5AcD48S
-aq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYd
-DnkM/crqJIByw5c/8nerQyIKx+u2DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWD
-LfJ6v9r9jv6ly0UsH8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0
-oyLQI+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK75t98biGC
-wWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h3PFaTWwyI0PurKju7koS
-CTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPzY11aWOIv4x3kqdbQCtCev9eBCfHJxyYN
-rJgWVqA=
------END CERTIFICATE-----
-
-Buypass Class 3 Root CA
-=======================
------BEGIN CERTIFICATE-----
-MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU
-QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMyBSb290IENBMB4X
-DTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFowTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1
-eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIw
-DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRH
-sJ8YZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3EN3coTRiR
-5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9tznDDgFHmV0ST9tD+leh
-7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX0DJq1l1sDPGzbjniazEuOQAnFN44wOwZ
-ZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH
-2xc519woe2v1n/MuwU8XKhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV
-/afmiSTYzIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvSO1UQ
-RwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D34xFMFbG02SrZvPA
-Xpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgPK9Dx2hzLabjKSWJtyNBjYt1gD1iq
-j6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD
-VR0OBBYEFEe4zf/lb+74suwvTg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF
-AAOCAgEAACAjQTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV
-cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXSIGrs/CIBKM+G
-uIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2HJLw5QY33KbmkJs4j1xrG0aG
-Q0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsaO5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8
-ZORK15FTAaggiG6cX0S5y2CBNOxv033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2
-KSb12tjE8nVhz36udmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz
-6MkEkbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg413OEMXbug
-UZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvDu79leNKGef9JOxqDDPDe
-eOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq4/g7u9xN12TyUb7mqqta6THuBrxzvxNi
-Cp/HuZc=
------END CERTIFICATE-----
diff --git a/externals/twilio-php/composer.json b/externals/twilio-php/composer.json
deleted file mode 100644
index bd955957d..000000000
--- a/externals/twilio-php/composer.json
+++ /dev/null
@@ -1,30 +0,0 @@
-{
- "name": "twilio/sdk",
- "type": "library",
- "description": "A PHP wrapper for Twilio's API",
- "keywords": ["twilio", "sms", "api"],
- "homepage": "http://github.com/twilio/twilio-php",
- "license": "MIT",
- "authors": [
- {
- "name": "Kevin Burke",
- "email": "kevin@twilio.com"
- },
- {
- "name": "Kyle Conroy",
- "email": "kyle+pear@twilio.com"
- }
- ],
- "require": {
- "php": ">=5.2.1"
- },
- "require-dev": {
- "mockery/mockery": ">=0.7.2",
- "phpunit/phpunit": "3.7.*"
- },
- "autoload": {
- "psr-0": {
- "Services_Twilio": ""
- }
- }
-}
diff --git a/externals/twilio-php/composer.lock b/externals/twilio-php/composer.lock
deleted file mode 100644
index 3265e5fd2..000000000
--- a/externals/twilio-php/composer.lock
+++ /dev/null
@@ -1,492 +0,0 @@
-{
- "_readme": [
- "This file locks the dependencies of your project to a known state",
- "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file"
- ],
- "hash": "b90999563f52df15b944f8defc454195",
- "packages": [
-
- ],
- "packages-dev": [
- {
- "name": "mockery/mockery",
- "version": "0.8.0",
- "source": {
- "type": "git",
- "url": "https://github.com/padraic/mockery.git",
- "reference": "35f0e18022f5538df9df8920a3d96c1761d63220"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/padraic/mockery/zipball/35f0e18022f5538df9df8920a3d96c1761d63220",
- "reference": "35f0e18022f5538df9df8920a3d96c1761d63220",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.2"
- },
- "require-dev": {
- "hamcrest/hamcrest": "1.1.0"
- },
- "type": "library",
- "autoload": {
- "psr-0": {
- "Mockery": "library/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Pádraic Brady",
- "email": "padraic.brady@gmail.com",
- "homepage": "http://blog.astrumfutura.com"
- }
- ],
- "description": "Mockery is a simple yet flexible PHP mock object framework for use in unit testing with PHPUnit, PHPSpec or any other testing framework. Its core goal is to offer a test double framework with a succint API capable of clearly defining all possible object operations and interactions using a human readable Domain Specific Language (DSL). Designed as a drop in alternative to PHPUnit's phpunit-mock-objects library, Mockery is easy to integrate with PHPUnit and can operate alongside phpunit-mock-objects without the World ending.",
- "homepage": "http://github.com/padraic/mockery",
- "keywords": [
- "BDD",
- "TDD",
- "library",
- "mock",
- "mock objects",
- "mockery",
- "stub",
- "test",
- "test double",
- "testing"
- ],
- "time": "2013-04-01 12:13:17"
- },
- {
- "name": "phpunit/php-code-coverage",
- "version": "1.2.13",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "466e7cd2554b4e264c9e3f31216d25ac0e5f3d94"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/466e7cd2554b4e264c9e3f31216d25ac0e5f3d94",
- "reference": "466e7cd2554b4e264c9e3f31216d25ac0e5f3d94",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.3",
- "phpunit/php-file-iterator": ">=1.3.0@stable",
- "phpunit/php-text-template": ">=1.1.1@stable",
- "phpunit/php-token-stream": ">=1.1.3@stable"
- },
- "require-dev": {
- "phpunit/phpunit": "3.7.*@dev"
- },
- "suggest": {
- "ext-dom": "*",
- "ext-xdebug": ">=2.0.5"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.2.x-dev"
- }
- },
- "autoload": {
- "classmap": [
- "PHP/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "include-path": [
- ""
- ],
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sb@sebastian-bergmann.de",
- "role": "lead"
- }
- ],
- "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
- "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
- "keywords": [
- "coverage",
- "testing",
- "xunit"
- ],
- "time": "2013-09-10 08:14:32"
- },
- {
- "name": "phpunit/php-file-iterator",
- "version": "1.3.3",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
- "reference": "16a78140ed2fc01b945cfa539665fadc6a038029"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/16a78140ed2fc01b945cfa539665fadc6a038029",
- "reference": "16a78140ed2fc01b945cfa539665fadc6a038029",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.3"
- },
- "type": "library",
- "autoload": {
- "classmap": [
- "File/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "include-path": [
- ""
- ],
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sb@sebastian-bergmann.de",
- "role": "lead"
- }
- ],
- "description": "FilterIterator implementation that filters files based on a list of suffixes.",
- "homepage": "http://www.phpunit.de/",
- "keywords": [
- "filesystem",
- "iterator"
- ],
- "time": "2012-10-11 11:44:38"
- },
- {
- "name": "phpunit/php-text-template",
- "version": "1.1.4",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/php-text-template.git",
- "reference": "5180896f51c5b3648ac946b05f9ec02be78a0b23"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5180896f51c5b3648ac946b05f9ec02be78a0b23",
- "reference": "5180896f51c5b3648ac946b05f9ec02be78a0b23",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.3"
- },
- "type": "library",
- "autoload": {
- "classmap": [
- "Text/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "include-path": [
- ""
- ],
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sb@sebastian-bergmann.de",
- "role": "lead"
- }
- ],
- "description": "Simple template engine.",
- "homepage": "https://github.com/sebastianbergmann/php-text-template/",
- "keywords": [
- "template"
- ],
- "time": "2012-10-31 18:15:28"
- },
- {
- "name": "phpunit/php-timer",
- "version": "1.0.5",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/php-timer.git",
- "reference": "19689d4354b295ee3d8c54b4f42c3efb69cbc17c"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/19689d4354b295ee3d8c54b4f42c3efb69cbc17c",
- "reference": "19689d4354b295ee3d8c54b4f42c3efb69cbc17c",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.3"
- },
- "type": "library",
- "autoload": {
- "classmap": [
- "PHP/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "include-path": [
- ""
- ],
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sb@sebastian-bergmann.de",
- "role": "lead"
- }
- ],
- "description": "Utility class for timing",
- "homepage": "https://github.com/sebastianbergmann/php-timer/",
- "keywords": [
- "timer"
- ],
- "time": "2013-08-02 07:42:54"
- },
- {
- "name": "phpunit/php-token-stream",
- "version": "1.2.1",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/php-token-stream.git",
- "reference": "5220af2a7929aa35cf663d97c89ad3d50cf5fa3e"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/5220af2a7929aa35cf663d97c89ad3d50cf5fa3e",
- "reference": "5220af2a7929aa35cf663d97c89ad3d50cf5fa3e",
- "shasum": ""
- },
- "require": {
- "ext-tokenizer": "*",
- "php": ">=5.3.3"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.2-dev"
- }
- },
- "autoload": {
- "classmap": [
- "PHP/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "include-path": [
- ""
- ],
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sb@sebastian-bergmann.de",
- "role": "lead"
- }
- ],
- "description": "Wrapper around PHP's tokenizer extension.",
- "homepage": "https://github.com/sebastianbergmann/php-token-stream/",
- "keywords": [
- "tokenizer"
- ],
- "time": "2013-09-13 04:58:23"
- },
- {
- "name": "phpunit/phpunit",
- "version": "3.7.27",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "4b024e753e3421837afbcca962c8724c58b39376"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4b024e753e3421837afbcca962c8724c58b39376",
- "reference": "4b024e753e3421837afbcca962c8724c58b39376",
- "shasum": ""
- },
- "require": {
- "ext-dom": "*",
- "ext-pcre": "*",
- "ext-reflection": "*",
- "ext-spl": "*",
- "php": ">=5.3.3",
- "phpunit/php-code-coverage": "~1.2.1",
- "phpunit/php-file-iterator": ">=1.3.1",
- "phpunit/php-text-template": ">=1.1.1",
- "phpunit/php-timer": ">=1.0.4",
- "phpunit/phpunit-mock-objects": "~1.2.0",
- "symfony/yaml": "~2.0"
- },
- "require-dev": {
- "pear-pear/pear": "1.9.4"
- },
- "suggest": {
- "ext-json": "*",
- "ext-simplexml": "*",
- "ext-tokenizer": "*",
- "phpunit/php-invoker": ">=1.1.0,<1.2.0"
- },
- "bin": [
- "composer/bin/phpunit"
- ],
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.7.x-dev"
- }
- },
- "autoload": {
- "classmap": [
- "PHPUnit/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "include-path": [
- "",
- "../../symfony/yaml/"
- ],
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de",
- "role": "lead"
- }
- ],
- "description": "The PHP Unit Testing framework.",
- "homepage": "http://www.phpunit.de/",
- "keywords": [
- "phpunit",
- "testing",
- "xunit"
- ],
- "time": "2013-09-16 03:09:52"
- },
- {
- "name": "phpunit/phpunit-mock-objects",
- "version": "1.2.3",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
- "reference": "5794e3c5c5ba0fb037b11d8151add2a07fa82875"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/5794e3c5c5ba0fb037b11d8151add2a07fa82875",
- "reference": "5794e3c5c5ba0fb037b11d8151add2a07fa82875",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.3",
- "phpunit/php-text-template": ">=1.1.1@stable"
- },
- "suggest": {
- "ext-soap": "*"
- },
- "type": "library",
- "autoload": {
- "classmap": [
- "PHPUnit/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "include-path": [
- ""
- ],
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sb@sebastian-bergmann.de",
- "role": "lead"
- }
- ],
- "description": "Mock Object library for PHPUnit",
- "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/",
- "keywords": [
- "mock",
- "xunit"
- ],
- "time": "2013-01-13 10:24:48"
- },
- {
- "name": "symfony/yaml",
- "version": "v2.3.4",
- "target-dir": "Symfony/Component/Yaml",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/Yaml.git",
- "reference": "5a279f1b5f5e1045a6c432354d9ea727ff3a9847"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/Yaml/zipball/5a279f1b5f5e1045a6c432354d9ea727ff3a9847",
- "reference": "5a279f1b5f5e1045a6c432354d9ea727ff3a9847",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.3"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "2.3-dev"
- }
- },
- "autoload": {
- "psr-0": {
- "Symfony\\Component\\Yaml\\": ""
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Fabien Potencier",
- "email": "fabien@symfony.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "http://symfony.com/contributors"
- }
- ],
- "description": "Symfony Yaml Component",
- "homepage": "http://symfony.com",
- "time": "2013-08-24 15:26:22"
- }
- ],
- "aliases": [
-
- ],
- "minimum-stability": "stable",
- "stability-flags": [
-
- ],
- "platform": {
- "php": ">=5.2.1"
- },
- "platform-dev": [
-
- ]
-}
diff --git a/externals/twilio-php/docs/Makefile b/externals/twilio-php/docs/Makefile
deleted file mode 100644
index d5756c701..000000000
--- a/externals/twilio-php/docs/Makefile
+++ /dev/null
@@ -1,130 +0,0 @@
-# Makefile for Sphinx documentation
-#
-
-# You can set these variables from the command line.
-SPHINXOPTS =
-SPHINXBUILD = sphinx-build
-PAPER =
-BUILDDIR = _build
-
-# Internal variables.
-PAPEROPT_a4 = -D latex_paper_size=a4
-PAPEROPT_letter = -D latex_paper_size=letter
-ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
-
-.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
-
-help:
- @echo "Please use \`make <target>' where <target> is one of"
- @echo " html to make standalone HTML files"
- @echo " dirhtml to make HTML files named index.html in directories"
- @echo " singlehtml to make a single large HTML file"
- @echo " pickle to make pickle files"
- @echo " json to make JSON files"
- @echo " htmlhelp to make HTML files and a HTML help project"
- @echo " qthelp to make HTML files and a qthelp project"
- @echo " devhelp to make HTML files and a Devhelp project"
- @echo " epub to make an epub"
- @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
- @echo " latexpdf to make LaTeX files and run them through pdflatex"
- @echo " text to make text files"
- @echo " man to make manual pages"
- @echo " changes to make an overview of all changed/added/deprecated items"
- @echo " linkcheck to check all external links for integrity"
- @echo " doctest to run all doctests embedded in the documentation (if enabled)"
-
-clean:
- -rm -rf $(BUILDDIR)/*
-
-html:
- $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
- @echo
- @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
-
-dirhtml:
- $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
- @echo
- @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
-
-singlehtml:
- $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
- @echo
- @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
-
-pickle:
- $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
- @echo
- @echo "Build finished; now you can process the pickle files."
-
-json:
- $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
- @echo
- @echo "Build finished; now you can process the JSON files."
-
-htmlhelp:
- $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
- @echo
- @echo "Build finished; now you can run HTML Help Workshop with the" \
- ".hhp project file in $(BUILDDIR)/htmlhelp."
-
-qthelp:
- $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
- @echo
- @echo "Build finished; now you can run "qcollectiongenerator" with the" \
- ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
- @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Services_Twilio.qhcp"
- @echo "To view the help file:"
- @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Services_Twilio.qhc"
-
-devhelp:
- $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
- @echo
- @echo "Build finished."
- @echo "To view the help file:"
- @echo "# mkdir -p $$HOME/.local/share/devhelp/Services_Twilio"
- @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Services_Twilio"
- @echo "# devhelp"
-
-epub:
- $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
- @echo
- @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
-
-latex:
- $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
- @echo
- @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
- @echo "Run \`make' in that directory to run these through (pdf)latex" \
- "(use \`make latexpdf' here to do that automatically)."
-
-latexpdf:
- $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
- @echo "Running LaTeX files through pdflatex..."
- make -C $(BUILDDIR)/latex all-pdf
- @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
-
-text:
- $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
- @echo
- @echo "Build finished. The text files are in $(BUILDDIR)/text."
-
-man:
- $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
- @echo
- @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
-
-changes:
- $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
- @echo
- @echo "The overview file is in $(BUILDDIR)/changes."
-
-linkcheck:
- $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
- @echo
- @echo "Link check complete; look for any errors in the above output " \
- "or in $(BUILDDIR)/linkcheck/output.txt."
-
-doctest:
- $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
- @echo "Testing of doctests in the sources finished, look at the " \
- "results in $(BUILDDIR)/doctest/output.txt."
diff --git a/externals/twilio-php/docs/_themes/.gitignore b/externals/twilio-php/docs/_themes/.gitignore
deleted file mode 100644
index 66b6e4c2f..000000000
--- a/externals/twilio-php/docs/_themes/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-*.pyc
-*.pyo
-.DS_Store
diff --git a/externals/twilio-php/docs/_themes/LICENSE b/externals/twilio-php/docs/_themes/LICENSE
deleted file mode 100644
index b160a8eeb..000000000
--- a/externals/twilio-php/docs/_themes/LICENSE
+++ /dev/null
@@ -1,45 +0,0 @@
-Modifications:
-
-Copyright (c) 2011 Kenneth Reitz.
-
-
-Original Project:
-
-Copyright (c) 2010 by Armin Ronacher.
-
-
-Some rights reserved.
-
-Redistribution and use in source and binary forms of the theme, with or
-without modification, are permitted provided that the following conditions
-are met:
-
-* Redistributions of source code must retain the above copyright
- notice, this list of conditions and the following disclaimer.
-
-* Redistributions in binary form must reproduce the above
- copyright notice, this list of conditions and the following
- disclaimer in the documentation and/or other materials provided
- with the distribution.
-
-* The names of the contributors may not be used to endorse or
- promote products derived from this software without specific
- prior written permission.
-
-We kindly ask you to only use these themes in an unmodified manner just
-for Flask and Flask-related products, not for unrelated projects. If you
-like the visual style and want to use it for your own projects, please
-consider making some larger changes to the themes (such as changing
-font faces, sizes, colors or margins).
-
-THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
-LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
-CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
-SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
-INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
-CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGE.
diff --git a/externals/twilio-php/docs/_themes/README.rst b/externals/twilio-php/docs/_themes/README.rst
deleted file mode 100644
index 8648482a3..000000000
--- a/externals/twilio-php/docs/_themes/README.rst
+++ /dev/null
@@ -1,25 +0,0 @@
-krTheme Sphinx Style
-====================
-
-This repository contains sphinx styles Kenneth Reitz uses in most of
-his projects. It is a drivative of Mitsuhiko's themes for Flask and Flask related
-projects. To use this style in your Sphinx documentation, follow
-this guide:
-
-1. put this folder as _themes into your docs folder. Alternatively
- you can also use git submodules to check out the contents there.
-
-2. add this to your conf.py: ::
-
- sys.path.append(os.path.abspath('_themes'))
- html_theme_path = ['_themes']
- html_theme = 'flask'
-
-The following themes exist:
-
-**kr**
- the standard flask documentation theme for large projects
-
-**kr_small**
- small one-page theme. Intended to be used by very small addon libraries.
-
diff --git a/externals/twilio-php/docs/_themes/flask_theme_support.py b/externals/twilio-php/docs/_themes/flask_theme_support.py
deleted file mode 100644
index 33f47449c..000000000
--- a/externals/twilio-php/docs/_themes/flask_theme_support.py
+++ /dev/null
@@ -1,86 +0,0 @@
-# flasky extensions. flasky pygments style based on tango style
-from pygments.style import Style
-from pygments.token import Keyword, Name, Comment, String, Error, \
- Number, Operator, Generic, Whitespace, Punctuation, Other, Literal
-
-
-class FlaskyStyle(Style):
- background_color = "#f8f8f8"
- default_style = ""
-
- styles = {
- # No corresponding class for the following:
- #Text: "", # class: ''
- Whitespace: "underline #f8f8f8", # class: 'w'
- Error: "#a40000 border:#ef2929", # class: 'err'
- Other: "#000000", # class 'x'
-
- Comment: "italic #8f5902", # class: 'c'
- Comment.Preproc: "noitalic", # class: 'cp'
-
- Keyword: "bold #004461", # class: 'k'
- Keyword.Constant: "bold #004461", # class: 'kc'
- Keyword.Declaration: "bold #004461", # class: 'kd'
- Keyword.Namespace: "bold #004461", # class: 'kn'
- Keyword.Pseudo: "bold #004461", # class: 'kp'
- Keyword.Reserved: "bold #004461", # class: 'kr'
- Keyword.Type: "bold #004461", # class: 'kt'
-
- Operator: "#582800", # class: 'o'
- Operator.Word: "bold #004461", # class: 'ow' - like keywords
-
- Punctuation: "bold #000000", # class: 'p'
-
- # because special names such as Name.Class, Name.Function, etc.
- # are not recognized as such later in the parsing, we choose them
- # to look the same as ordinary variables.
- Name: "#000000", # class: 'n'
- Name.Attribute: "#c4a000", # class: 'na' - to be revised
- Name.Builtin: "#004461", # class: 'nb'
- Name.Builtin.Pseudo: "#3465a4", # class: 'bp'
- Name.Class: "#000000", # class: 'nc' - to be revised
- Name.Constant: "#000000", # class: 'no' - to be revised
- Name.Decorator: "#888", # class: 'nd' - to be revised
- Name.Entity: "#ce5c00", # class: 'ni'
- Name.Exception: "bold #cc0000", # class: 'ne'
- Name.Function: "#000000", # class: 'nf'
- Name.Property: "#000000", # class: 'py'
- Name.Label: "#f57900", # class: 'nl'
- Name.Namespace: "#000000", # class: 'nn' - to be revised
- Name.Other: "#000000", # class: 'nx'
- Name.Tag: "bold #004461", # class: 'nt' - like a keyword
- Name.Variable: "#000000", # class: 'nv' - to be revised
- Name.Variable.Class: "#000000", # class: 'vc' - to be revised
- Name.Variable.Global: "#000000", # class: 'vg' - to be revised
- Name.Variable.Instance: "#000000", # class: 'vi' - to be revised
-
- Number: "#990000", # class: 'm'
-
- Literal: "#000000", # class: 'l'
- Literal.Date: "#000000", # class: 'ld'
-
- String: "#4e9a06", # class: 's'
- String.Backtick: "#4e9a06", # class: 'sb'
- String.Char: "#4e9a06", # class: 'sc'
- String.Doc: "italic #8f5902", # class: 'sd' - like a comment
- String.Double: "#4e9a06", # class: 's2'
- String.Escape: "#4e9a06", # class: 'se'
- String.Heredoc: "#4e9a06", # class: 'sh'
- String.Interpol: "#4e9a06", # class: 'si'
- String.Other: "#4e9a06", # class: 'sx'
- String.Regex: "#4e9a06", # class: 'sr'
- String.Single: "#4e9a06", # class: 's1'
- String.Symbol: "#4e9a06", # class: 'ss'
-
- Generic: "#000000", # class: 'g'
- Generic.Deleted: "#a40000", # class: 'gd'
- Generic.Emph: "italic #000000", # class: 'ge'
- Generic.Error: "#ef2929", # class: 'gr'
- Generic.Heading: "bold #000080", # class: 'gh'
- Generic.Inserted: "#00A000", # class: 'gi'
- Generic.Output: "#888", # class: 'go'
- Generic.Prompt: "#745334", # class: 'gp'
- Generic.Strong: "bold #000000", # class: 'gs'
- Generic.Subheading: "bold #800080", # class: 'gu'
- Generic.Traceback: "bold #a40000", # class: 'gt'
- }
diff --git a/externals/twilio-php/docs/_themes/kr/layout.html b/externals/twilio-php/docs/_themes/kr/layout.html
deleted file mode 100644
index 7dfedb125..000000000
--- a/externals/twilio-php/docs/_themes/kr/layout.html
+++ /dev/null
@@ -1,32 +0,0 @@
-{%- extends "basic/layout.html" %}
-{%- block extrahead %}
- {{ super() }}
- {% if theme_touch_icon %}
- <link rel="apple-touch-icon" href="{{ pathto('_static/' ~ theme_touch_icon, 1) }}" />
- {% endif %}
- <link media="only screen and (max-device-width: 480px)" href="{{
- pathto('_static/small_flask.css', 1) }}" type= "text/css" rel="stylesheet" />
-{% endblock %}
-{%- block relbar2 %}{% endblock %}
-{%- block footer %}
- <div class="footer">
- &copy; Copyright {{ copyright }}.
- </div>
- <script type="text/javascript">
- try {
- var _gaq = _gaq || [];
- _gaq.push(['_setAccount', 'UA-2900316-11']);
- _gaq.push(['_trackPageview']);
-
- (function() {
- var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
- ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
- var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
- })();
- } catch(err) {}
- </script>
- <a href="https://github.com/twilio/twilio-php">
- <img style="position: absolute; top: 0; right: 0; border: 0;" src="http://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" alt="Fork me on GitHub" />
- </a>
-
-{%- endblock %}
diff --git a/externals/twilio-php/docs/_themes/kr/relations.html b/externals/twilio-php/docs/_themes/kr/relations.html
deleted file mode 100644
index 3bbcde85b..000000000
--- a/externals/twilio-php/docs/_themes/kr/relations.html
+++ /dev/null
@@ -1,19 +0,0 @@
-<h3>Related Topics</h3>
-<ul>
- <li><a href="{{ pathto(master_doc) }}">Documentation overview</a><ul>
- {%- for parent in parents %}
- <li><a href="{{ parent.link|e }}">{{ parent.title }}</a><ul>
- {%- endfor %}
- {%- if prev %}
- <li>Previous: <a href="{{ prev.link|e }}" title="{{ _('previous chapter')
- }}">{{ prev.title }}</a></li>
- {%- endif %}
- {%- if next %}
- <li>Next: <a href="{{ next.link|e }}" title="{{ _('next chapter')
- }}">{{ next.title }}</a></li>
- {%- endif %}
- {%- for parent in parents %}
- </ul></li>
- {%- endfor %}
- </ul></li>
-</ul>
diff --git a/externals/twilio-php/docs/_themes/kr/static/flasky.css_t b/externals/twilio-php/docs/_themes/kr/static/flasky.css_t
deleted file mode 100644
index aa3e8ebbd..000000000
--- a/externals/twilio-php/docs/_themes/kr/static/flasky.css_t
+++ /dev/null
@@ -1,469 +0,0 @@
-/*
- * flasky.css_t
- * ~~~~~~~~~~~~
- *
- * :copyright: Copyright 2010 by Armin Ronacher. Modifications by Kenneth Reitz.
- * :license: Flask Design License, see LICENSE for details.
- */
-
-{% set page_width = '940px' %}
-{% set sidebar_width = '220px' %}
-
-@import url("basic.css");
-
-/* -- page layout ----------------------------------------------------------- */
-
-body {
- font-family: 'goudy old style', 'minion pro', 'bell mt', Georgia, 'Hiragino Mincho Pro';
- font-size: 17px;
- background-color: white;
- color: #000;
- margin: 0;
- padding: 0;
-}
-
-div.document {
- width: {{ page_width }};
- margin: 30px auto 0 auto;
-}
-
-div.documentwrapper {
- float: left;
- width: 100%;
-}
-
-div.bodywrapper {
- margin: 0 0 0 {{ sidebar_width }};
-}
-
-div.sphinxsidebar {
- width: {{ sidebar_width }};
-}
-
-hr {
- border: 1px solid #B1B4B6;
-}
-
-div.body {
- background-color: #ffffff;
- color: #3E4349;
- padding: 0 30px 0 30px;
-}
-
-img.floatingflask {
- padding: 0 0 10px 10px;
- float: right;
-}
-
-div.footer {
- width: {{ page_width }};
- margin: 20px auto 30px auto;
- font-size: 14px;
- color: #888;
- text-align: right;
-}
-
-div.footer a {
- color: #888;
-}
-
-div.related {
- display: none;
-}
-
-div.sphinxsidebar a {
- color: #444;
- text-decoration: none;
- border-bottom: 1px dotted #999;
-}
-
-div.sphinxsidebar a:hover {
- border-bottom: 1px solid #999;
-}
-
-div.sphinxsidebar {
- font-size: 14px;
- line-height: 1.5;
-}
-
-div.sphinxsidebarwrapper {
- padding: 18px 10px;
-}
-
-div.sphinxsidebarwrapper p.logo {
- padding: 0;
- margin: -10px 0 0 -20px;
- text-align: center;
-}
-
-div.sphinxsidebar h3,
-div.sphinxsidebar h4 {
- font-family: 'Garamond', 'Georgia', serif;
- color: #444;
- font-size: 24px;
- font-weight: normal;
- margin: 0 0 5px 0;
- padding: 0;
-}
-
-div.sphinxsidebar h4 {
- font-size: 20px;
-}
-
-div.sphinxsidebar h3 a {
- color: #444;
-}
-
-div.sphinxsidebar p.logo a,
-div.sphinxsidebar h3 a,
-div.sphinxsidebar p.logo a:hover,
-div.sphinxsidebar h3 a:hover {
- border: none;
-}
-
-div.sphinxsidebar p {
- color: #555;
- margin: 10px 0;
-}
-
-div.sphinxsidebar ul {
- margin: 10px 0;
- padding: 0;
- color: #000;
-}
-
-div.sphinxsidebar input {
- border: 1px solid #ccc;
- font-family: 'Georgia', serif;
- font-size: 1em;
-}
-
-/* -- body styles ----------------------------------------------------------- */
-
-a {
- color: #004B6B;
- text-decoration: underline;
-}
-
-a:hover {
- color: #6D4100;
- text-decoration: underline;
-}
-
-div.body h1,
-div.body h2,
-div.body h3,
-div.body h4,
-div.body h5,
-div.body h6 {
- font-family: 'Garamond', 'Georgia', serif;
- font-weight: normal;
- margin: 30px 0px 10px 0px;
- padding: 0;
-}
-
-div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; }
-div.body h2 { font-size: 180%; }
-div.body h3 { font-size: 150%; }
-div.body h4 { font-size: 130%; }
-div.body h5 { font-size: 100%; }
-div.body h6 { font-size: 100%; }
-
-a.headerlink {
- color: #ddd;
- padding: 0 4px;
- text-decoration: none;
-}
-
-a.headerlink:hover {
- color: #444;
- background: #eaeaea;
-}
-
-div.body p, div.body dd, div.body li {
- line-height: 1.4em;
-}
-
-div.admonition {
- background: #fafafa;
- margin: 20px -30px;
- padding: 10px 30px;
- border-top: 1px solid #ccc;
- border-bottom: 1px solid #ccc;
-}
-
-div.admonition tt.xref, div.admonition a tt {
- border-bottom: 1px solid #fafafa;
-}
-
-dd div.admonition {
- margin-left: -60px;
- padding-left: 60px;
-}
-
-div.admonition p.admonition-title {
- font-family: 'Garamond', 'Georgia', serif;
- font-weight: normal;
- font-size: 24px;
- margin: 0 0 10px 0;
- padding: 0;
- line-height: 1;
-}
-
-div.admonition p.last {
- margin-bottom: 0;
-}
-
-div.highlight {
- background-color: white;
-}
-
-dt:target, .highlight {
- background: #FAF3E8;
-}
-
-div.note {
- background-color: #eee;
- border: 1px solid #ccc;
-}
-
-div.seealso {
- background-color: #ffc;
- border: 1px solid #ff6;
-}
-
-div.topic {
- background-color: #eee;
-}
-
-p.admonition-title {
- display: inline;
-}
-
-p.admonition-title:after {
- content: ":";
-}
-
-pre, tt {
- font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
- font-size: 0.9em;
-}
-
-img.screenshot {
-}
-
-tt.descname, tt.descclassname {
- font-size: 0.95em;
-}
-
-tt.descname {
- padding-right: 0.08em;
-}
-
-img.screenshot {
- -moz-box-shadow: 2px 2px 4px #eee;
- -webkit-box-shadow: 2px 2px 4px #eee;
- box-shadow: 2px 2px 4px #eee;
-}
-
-table.docutils {
- border: 1px solid #888;
- -moz-box-shadow: 2px 2px 4px #eee;
- -webkit-box-shadow: 2px 2px 4px #eee;
- box-shadow: 2px 2px 4px #eee;
-}
-
-table.docutils td, table.docutils th {
- border: 1px solid #888;
- padding: 0.25em 0.7em;
-}
-
-table.field-list, table.footnote {
- border: none;
- -moz-box-shadow: none;
- -webkit-box-shadow: none;
- box-shadow: none;
-}
-
-table.footnote {
- margin: 15px 0;
- width: 100%;
- border: 1px solid #eee;
- background: #fdfdfd;
- font-size: 0.9em;
-}
-
-table.footnote + table.footnote {
- margin-top: -15px;
- border-top: none;
-}
-
-table.field-list th {
- padding: 0 0.8em 0 0;
-}
-
-table.field-list td {
- padding: 0;
-}
-
-table.footnote td.label {
- width: 0px;
- padding: 0.3em 0 0.3em 0.5em;
-}
-
-table.footnote td {
- padding: 0.3em 0.5em;
-}
-
-dl {
- margin: 0;
- padding: 0;
-}
-
-dl dd {
- margin-left: 30px;
-}
-
-blockquote {
- margin: 0 0 0 30px;
- padding: 0;
-}
-
-ul, ol {
- margin: 10px 0 10px 30px;
- padding: 0;
-}
-
-pre {
- background: #eee;
- padding: 7px 30px;
- margin: 15px -30px;
- line-height: 1.3em;
-}
-
-dl pre, blockquote pre, li pre {
- margin-left: -60px;
- padding-left: 60px;
-}
-
-dl dl pre {
- margin-left: -90px;
- padding-left: 90px;
-}
-
-tt {
- background-color: #ecf0f3;
- color: #222;
- /* padding: 1px 2px; */
-}
-
-tt.xref, a tt {
- background-color: #FBFBFB;
- border-bottom: 1px solid white;
-}
-
-a.reference {
- text-decoration: none;
- border-bottom: 1px dotted #004B6B;
-}
-
-a.reference:hover {
- border-bottom: 1px solid #6D4100;
-}
-
-a.footnote-reference {
- text-decoration: none;
- font-size: 0.7em;
- vertical-align: top;
- border-bottom: 1px dotted #004B6B;
-}
-
-a.footnote-reference:hover {
- border-bottom: 1px solid #6D4100;
-}
-
-a:hover tt {
- background: #EEE;
-}
-
-
-@media screen and (max-width: 600px) {
-
- div.sphinxsidebar {
- display: none;
- }
-
- div.documentwrapper {
- margin-left: 0;
- margin-top: 0;
- margin-right: 0;
- margin-bottom: 0;
- }
-
- div.bodywrapper {
- margin-top: 0;
- margin-right: 0;
- margin-bottom: 0;
- margin-left: 0;
- }
-
- ul {
- margin-left: 0;
- }
-
- .document {
- width: auto;
- }
-
- .bodywrapper {
- margin: 0;
- }
-
- .footer {
- width: auto;
- }
-
-
-
-}
-
-
-/* scrollbars */
-
-::-webkit-scrollbar {
- width: 6px;
- height: 6px;
-}
-
-::-webkit-scrollbar-button:start:decrement,
-::-webkit-scrollbar-button:end:increment {
- display: block;
- height: 10px;
-}
-
-::-webkit-scrollbar-button:vertical:increment {
- background-color: #fff;
-}
-
-::-webkit-scrollbar-track-piece {
- background-color: #eee;
- -webkit-border-radius: 3px;
-}
-
-::-webkit-scrollbar-thumb:vertical {
- height: 50px;
- background-color: #ccc;
- -webkit-border-radius: 3px;
-}
-
-::-webkit-scrollbar-thumb:horizontal {
- width: 50px;
- background-color: #ccc;
- -webkit-border-radius: 3px;
-}
-
-/* misc. */
-
-.revsys-inline {
- display: none!important;
-}
\ No newline at end of file
diff --git a/externals/twilio-php/docs/_themes/kr/static/small_flask.css b/externals/twilio-php/docs/_themes/kr/static/small_flask.css
deleted file mode 100644
index 1c6df309e..000000000
--- a/externals/twilio-php/docs/_themes/kr/static/small_flask.css
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * small_flask.css_t
- * ~~~~~~~~~~~~~~~~~
- *
- * :copyright: Copyright 2010 by Armin Ronacher.
- * :license: Flask Design License, see LICENSE for details.
- */
-
-body {
- margin: 0;
- padding: 20px 30px;
-}
-
-div.documentwrapper {
- float: none;
- background: white;
-}
-
-div.sphinxsidebar {
- display: block;
- float: none;
- width: 102.5%;
- margin: 50px -30px -20px -30px;
- padding: 10px 20px;
- background: #333;
- color: white;
-}
-
-div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p,
-div.sphinxsidebar h3 a {
- color: white;
-}
-
-div.sphinxsidebar a {
- color: #aaa;
-}
-
-div.sphinxsidebar p.logo {
- display: none;
-}
-
-div.document {
- width: 100%;
- margin: 0;
-}
-
-div.related {
- display: block;
- margin: 0;
- padding: 10px 0 20px 0;
-}
-
-div.related ul,
-div.related ul li {
- margin: 0;
- padding: 0;
-}
-
-div.footer {
- display: none;
-}
-
-div.bodywrapper {
- margin: 0;
-}
-
-div.body {
- min-height: 0;
- padding: 0;
-}
diff --git a/externals/twilio-php/docs/_themes/kr/theme.conf b/externals/twilio-php/docs/_themes/kr/theme.conf
deleted file mode 100644
index 307a1f0d6..000000000
--- a/externals/twilio-php/docs/_themes/kr/theme.conf
+++ /dev/null
@@ -1,7 +0,0 @@
-[theme]
-inherit = basic
-stylesheet = flasky.css
-pygments_style = flask_theme_support.FlaskyStyle
-
-[options]
-touch_icon =
diff --git a/externals/twilio-php/docs/_themes/kr_small/layout.html b/externals/twilio-php/docs/_themes/kr_small/layout.html
deleted file mode 100644
index aa1716aaf..000000000
--- a/externals/twilio-php/docs/_themes/kr_small/layout.html
+++ /dev/null
@@ -1,22 +0,0 @@
-{% extends "basic/layout.html" %}
-{% block header %}
- {{ super() }}
- {% if pagename == 'index' %}
- <div class=indexwrapper>
- {% endif %}
-{% endblock %}
-{% block footer %}
- {% if pagename == 'index' %}
- </div>
- {% endif %}
-{% endblock %}
-{# do not display relbars #}
-{% block relbar1 %}{% endblock %}
-{% block relbar2 %}
- {% if theme_github_fork %}
- <a href="http://github.com/{{ theme_github_fork }}"><img style="position: fixed; top: 0; right: 0; border: 0;"
- src="http://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" alt="Fork me on GitHub" /></a>
- {% endif %}
-{% endblock %}
-{% block sidebar1 %}{% endblock %}
-{% block sidebar2 %}{% endblock %}
diff --git a/externals/twilio-php/docs/_themes/kr_small/static/flasky.css_t b/externals/twilio-php/docs/_themes/kr_small/static/flasky.css_t
deleted file mode 100644
index fe2141c56..000000000
--- a/externals/twilio-php/docs/_themes/kr_small/static/flasky.css_t
+++ /dev/null
@@ -1,287 +0,0 @@
-/*
- * flasky.css_t
- * ~~~~~~~~~~~~
- *
- * Sphinx stylesheet -- flasky theme based on nature theme.
- *
- * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS.
- * :license: BSD, see LICENSE for details.
- *
- */
-
-@import url("basic.css");
-
-/* -- page layout ----------------------------------------------------------- */
-
-body {
- font-family: 'Georgia', serif;
- font-size: 17px;
- color: #000;
- background: white;
- margin: 0;
- padding: 0;
-}
-
-div.documentwrapper {
- float: left;
- width: 100%;
-}
-
-div.bodywrapper {
- margin: 40px auto 0 auto;
- width: 700px;
-}
-
-hr {
- border: 1px solid #B1B4B6;
-}
-
-div.body {
- background-color: #ffffff;
- color: #3E4349;
- padding: 0 30px 30px 30px;
-}
-
-img.floatingflask {
- padding: 0 0 10px 10px;
- float: right;
-}
-
-div.footer {
- text-align: right;
- color: #888;
- padding: 10px;
- font-size: 14px;
- width: 650px;
- margin: 0 auto 40px auto;
-}
-
-div.footer a {
- color: #888;
- text-decoration: underline;
-}
-
-div.related {
- line-height: 32px;
- color: #888;
-}
-
-div.related ul {
- padding: 0 0 0 10px;
-}
-
-div.related a {
- color: #444;
-}
-
-/* -- body styles ----------------------------------------------------------- */
-
-a {
- color: #004B6B;
- text-decoration: underline;
-}
-
-a:hover {
- color: #6D4100;
- text-decoration: underline;
-}
-
-div.body {
- padding-bottom: 40px; /* saved for footer */
-}
-
-div.body h1,
-div.body h2,
-div.body h3,
-div.body h4,
-div.body h5,
-div.body h6 {
- font-family: 'Garamond', 'Georgia', serif;
- font-weight: normal;
- margin: 30px 0px 10px 0px;
- padding: 0;
-}
-
-{% if theme_index_logo %}
-div.indexwrapper h1 {
- text-indent: -999999px;
- background: url({{ theme_index_logo }}) no-repeat center center;
- height: {{ theme_index_logo_height }};
-}
-{% endif %}
-
-div.body h2 { font-size: 180%; }
-div.body h3 { font-size: 150%; }
-div.body h4 { font-size: 130%; }
-div.body h5 { font-size: 100%; }
-div.body h6 { font-size: 100%; }
-
-a.headerlink {
- color: white;
- padding: 0 4px;
- text-decoration: none;
-}
-
-a.headerlink:hover {
- color: #444;
- background: #eaeaea;
-}
-
-div.body p, div.body dd, div.body li {
- line-height: 1.4em;
-}
-
-div.admonition {
- background: #fafafa;
- margin: 20px -30px;
- padding: 10px 30px;
- border-top: 1px solid #ccc;
- border-bottom: 1px solid #ccc;
-}
-
-div.admonition p.admonition-title {
- font-family: 'Garamond', 'Georgia', serif;
- font-weight: normal;
- font-size: 24px;
- margin: 0 0 10px 0;
- padding: 0;
- line-height: 1;
-}
-
-div.admonition p.last {
- margin-bottom: 0;
-}
-
-div.highlight{
- background-color: white;
-}
-
-dt:target, .highlight {
- background: #FAF3E8;
-}
-
-div.note {
- background-color: #eee;
- border: 1px solid #ccc;
-}
-
-div.seealso {
- background-color: #ffc;
- border: 1px solid #ff6;
-}
-
-div.topic {
- background-color: #eee;
-}
-
-div.warning {
- background-color: #ffe4e4;
- border: 1px solid #f66;
-}
-
-p.admonition-title {
- display: inline;
-}
-
-p.admonition-title:after {
- content: ":";
-}
-
-pre, tt {
- font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
- font-size: 0.85em;
-}
-
-img.screenshot {
-}
-
-tt.descname, tt.descclassname {
- font-size: 0.95em;
-}
-
-tt.descname {
- padding-right: 0.08em;
-}
-
-img.screenshot {
- -moz-box-shadow: 2px 2px 4px #eee;
- -webkit-box-shadow: 2px 2px 4px #eee;
- box-shadow: 2px 2px 4px #eee;
-}
-
-table.docutils {
- border: 1px solid #888;
- -moz-box-shadow: 2px 2px 4px #eee;
- -webkit-box-shadow: 2px 2px 4px #eee;
- box-shadow: 2px 2px 4px #eee;
-}
-
-table.docutils td, table.docutils th {
- border: 1px solid #888;
- padding: 0.25em 0.7em;
-}
-
-table.field-list, table.footnote {
- border: none;
- -moz-box-shadow: none;
- -webkit-box-shadow: none;
- box-shadow: none;
-}
-
-table.footnote {
- margin: 15px 0;
- width: 100%;
- border: 1px solid #eee;
-}
-
-table.field-list th {
- padding: 0 0.8em 0 0;
-}
-
-table.field-list td {
- padding: 0;
-}
-
-table.footnote td {
- padding: 0.5em;
-}
-
-dl {
- margin: 0;
- padding: 0;
-}
-
-dl dd {
- margin-left: 30px;
-}
-
-pre {
- padding: 0;
- margin: 15px -30px;
- padding: 8px;
- line-height: 1.3em;
- padding: 7px 30px;
- background: #eee;
- border-radius: 2px;
- -moz-border-radius: 2px;
- -webkit-border-radius: 2px;
-}
-
-dl pre {
- margin-left: -60px;
- padding-left: 60px;
-}
-
-tt {
- background-color: #ecf0f3;
- color: #222;
- /* padding: 1px 2px; */
-}
-
-tt.xref, a tt {
- background-color: #FBFBFB;
-}
-
-a:hover tt {
- background: #EEE;
-}
diff --git a/externals/twilio-php/docs/_themes/kr_small/theme.conf b/externals/twilio-php/docs/_themes/kr_small/theme.conf
deleted file mode 100644
index 542b46251..000000000
--- a/externals/twilio-php/docs/_themes/kr_small/theme.conf
+++ /dev/null
@@ -1,10 +0,0 @@
-[theme]
-inherit = basic
-stylesheet = flasky.css
-nosidebar = true
-pygments_style = flask_theme_support.FlaskyStyle
-
-[options]
-index_logo = ''
-index_logo_height = 120px
-github_fork = ''
diff --git a/externals/twilio-php/docs/api/rest.rst b/externals/twilio-php/docs/api/rest.rst
deleted file mode 100644
index 172fa9899..000000000
--- a/externals/twilio-php/docs/api/rest.rst
+++ /dev/null
@@ -1,872 +0,0 @@
-.. _api-rest:
-
-###############################
-Twilio Rest Resources
-###############################
-
-**************
-List Resources
-**************
-
-.. phpautoclass:: Services_Twilio_ListResource
- :filename: ../Services/Twilio/ListResource.php
- :members:
-
-All of the below classes inherit from the :php:class:`ListResource
-<Services_Twilio_ListResource>`.
-
-Accounts
-===========
-
-.. phpautoclass:: Services_Twilio_Rest_Accounts
- :filename: ../Services/Twilio/Rest/Accounts.php
- :members:
-
-AvailablePhoneNumbers
-========================
-
-.. php:class:: Services_Twilio_Rest_AvailablePhoneNumbers
-
- For more information, see the `AvailablePhoneNumbers API Resource <http://www.twilio.com/docs/api/rest/available-phone-numbers#local>`_ documentation at twilio.com.
-
- .. php:method:: getList($country, $type)
-
- Get a list of available phone numbers.
-
- :param string country: The 2-digit country code for numbers ('US', 'GB',
- 'CA')
- :param string type: The type of phone number ('TollFree' or 'Local')
- :return: An instance of the :php:class:`Services_Twilio_Rest_AvailablePhoneNumbers` resource.
-
- .. php:attr:: available_phone_numbers
-
- A list of :php:class:`Services_Twilio_Rest_AvailablePhoneNumber` instances.
-
- .. php:attr:: uri
-
- The uri representing this resource, relative to https://api.twilio.com.
-
-
-Calls
-=======
-
-.. php:class:: Services_Twilio_Rest_Calls
-
- For more information, see the `Call List Resource <http://www.twilio.com/docs/api/rest/call#list>`_ documentation.
-
- .. php:method:: create($from, $to, $url, params = array())
-
- Make an outgoing call
-
- :param string $from: The phone number to use as the caller id.
- :param string $to: The number to call formatted with a '+' and country code
- :param string $url: The fully qualified URL that should be consulted when
- the call connects. This value can also be an ApplicationSid.
- :param array $params: An array of optional parameters for this call
-
- The **$params** array can contain the following keys:
-
- *Method*
- The HTTP method Twilio should use when making its request to the above Url parameter's value. Defaults to POST. If an ApplicationSid parameter is present, this parameter is ignored.
-
- *FallbackUrl*
- A URL that Twilio will request if an error occurs requesting or executing the TwiML at Url. If an ApplicationSid parameter is present, this parameter is ignored.
-
- *FallbackMethod*
- The HTTP method that Twilio should use to request the FallbackUrl. Must be either GET or POST. Defaults to POST. If an ApplicationSid parameter is present, this parameter is ignored.
-
- *StatusCallback*
- A URL that Twilio will request when the call ends to notify your app. If an ApplicationSid parameter is present, this parameter is ignored.
-
- *StatusCallbackMethod*
- The HTTP method Twilio should use when requesting the above URL. Defaults to POST. If an ApplicationSid parameter is present, this parameter is ignored.
-
- *SendDigits*
- A string of keys to dial after connecting to the number. Valid digits in the string include: any digit (0-9), '#' and '*'. For example, if you connected to a company phone number, and wanted to dial extension 1234 and then the pound key, use SendDigits=1234#. Remember to URL-encode this string, since the '#' character has special meaning in a URL.
-
- *IfMachine*
- Tell Twilio to try and determine if a machine (like voicemail) or a human has answered the call. Possible values are Continue and Hangup. See the answering machines section below for more info.
-
- *Timeout*
- The integer number of seconds that Twilio should allow the phone to ring before assuming there is no answer. Default is 60 seconds, the maximum is 999 seconds. Note, you could set this to a low value, such as 15, to hangup before reaching an answering machine or voicemail.
-
-CredentialListMappings
-=========================
-
-.. phpautoclass:: Services_Twilio_Rest_CredentialListMappings
- :filename: ../Services/Twilio/Rest/CredentialListMappings.php
- :members:
-
-
-CredentialLists
-=================
-
-.. phpautoclass:: Services_Twilio_Rest_CredentialLists
- :filename: ../Services/Twilio/Rest/CredentialLists.php
- :members:
-
-Credentials
-==============
-
-.. phpautoclass:: Services_Twilio_Rest_Credentials
- :filename: ../Services/Twilio/Rest/Credentials.php
- :members:
-
-Domains
-==========
-
-.. phpautoclass:: Services_Twilio_Rest_Domains
- :filename: ../Services/Twilio/Rest/Domains.php
- :members:
-
-
-IncomingPhoneNumbers
-========================
-
-.. phpautoclass:: Services_Twilio_Rest_IncomingPhoneNumbers,Services_Twilio_Rest_Local,Services_Twilio_Rest_Mobile,Services_Twilio_Rest_TollFree
- :filename: ../Services/Twilio/Rest/IncomingPhoneNumbers.php
- :members:
-
-IpAccessControlListMappings
-==============================
-
-.. phpautoclass:: Services_Twilio_Rest_IpAccessControlListMappings
- :filename: ../Services/Twilio/Rest/IpAccessControlListMappings.php
- :members:
-
-IpAccessControlLists
-=======================
-
-.. phpautoclass:: Services_Twilio_Rest_IpAccessControlLists
- :filename: ../Services/Twilio/Rest/IpAccessControlLists.php
- :members:
-
-IpAddresses
-=======================
-
-.. phpautoclass:: Services_Twilio_Rest_IpAddresses
- :filename: ../Services/Twilio/Rest/IpAddresses.php
- :members:
-
-Media
-======
-
-.. phpautoclass:: Services_Twilio_Rest_Media
- :filename: ../Services/Twilio/Rest/Media.php
- :members:
-
-Members
-===========
-
-.. php:class:: Services_Twilio_Rest_Members
-
- For more information, including a list of filter parameters, see the `Member List Resource <http://www.twilio.com/docs/api/rest/member#list>`_ documentation.
-
- .. php:method:: front()
-
- Return the :php:class:`Services_Twilio_Rest_Member` at the front of the
- queue.
-
-Messages
-========
-
-.. phpautoclass:: Services_Twilio_Rest_Messages
- :filename: ../Services/Twilio/Rest/Messages.php
- :members:
-
-Queues
-===========
-
-.. php:class:: Services_Twilio_Rest_Queues
-
- For more information, including a list of filter parameters, see the
- `Queues List Resource <http://www.twilio.com/docs/api/rest/queues#list>`_
- documentation.
-
- .. php:method:: create($friendly_name, $params = array())
-
- Create a new :php:class:`Services_Twilio_Rest_Queue`.
-
- :param string $friendly_name: The name of the new Queue.
- :param array $params: An array of optional parameters and their values,
- like `MaxSize`.
- :returns: A new :php:class:`Services_Twilio_Rest_Queue`
-
-
-UsageRecords
-==============
-
-.. php:class:: Services_Twilio_Rest_UsageRecords
-
- For more information, including a list of filter parameters, see the `UsageRecords List Resource <http://www.twilio.com/docs/api/rest/usage-records#list>`_ documentation.
-
- .. php:method:: getCategory($category)
-
- Return the single UsageRecord corresponding to this category of usage.
- Valid only for the `Records`, `Today`, `Yesterday`, `ThisMonth`,
- `LastMonth` and `AllTime` resources.
-
- :param string $category: The category to retrieve a usage record for. For a full list of valid categories, see the full `Usage Category documentation <http://www.twilio.com/docs/api/rest/usage-records#usage-all-categories>`_.
- :returns: :php:class:`Services_Twilio_Rest_UsageRecord` A single usage record
-
-UsageTriggers
-=============
-
-.. php:class:: Services_Twilio_Rest_UsageTriggers
-
- For more information, including a list of filter parameters, see the `UsageTriggers List Resource <http://www.twilio.com/docs/api/rest/usage-triggers#list>`_ documentation.
-
- .. php:method:: create($category, $value, $url, $params = array())
-
- Create a new UsageTrigger.
-
- :param string $category: The category of usage to fire a trigger for. A full list of categories can be found in the `Usage Categories documentation <http://www.twilio.com/docs/api/rest/usage-records#usage-categories>`_.
- :param string $value: Fire the trigger when usage crosses this value.
- :param string $url: The URL to request when the trigger fires.
- :param array $params: Optional parameters for this trigger. A full list of parameters can be found in the `Usage Trigger documentation <http://www.twilio.com/docs/api/rest/usage-triggers#list-post-optional-parameters>`_.
- :returns: :php:class:`Services_Twilio_Rest_UsageTrigger` The created trigger.
-
-
-********************
-Instance Resources
-********************
-
-.. phpautoclass:: Services_Twilio_InstanceResource
- :filename: ../Services/Twilio/InstanceResource.php
- :members:
-
-Below you will find a list of objects created by interacting with the Twilio
-API, and the methods and properties that can be called on them. These are
-derived from the :php:class:`ListResource <Services_Twilio_ListResource>` and
-:php:class:`InstanceResource <Services_Twilio_InstanceResource>` above.
-
-
-Account
-========
-
-.. php:class:: Services_Twilio_Rest_Account
-
- For more information, see the `Account Instance Resource <http://www.twilio.com/docs/api/rest/account#instance>`_ documentation.
-
- .. php:method:: update($params)
-
- Update the account
-
- The **$params** array is the same as in :php:meth:`Services_Twilio_Rest_Accounts::create`
-
- .. php:attr:: sid
-
- A 34 character string that uniquely identifies this account.
-
- .. php:attr:: date_created
-
- The date that this account was created, in GMT in RFC 2822 format
-
- .. php:attr:: date_updated
-
- The date that this account was last updated, in GMT in RFC 2822 format.
-
- .. php:attr:: friendly_name
-
- A human readable description of this account, up to 64 characters long. By default the FriendlyName is your email address.
-
- .. php:attr:: status
-
- The status of this account. Usually active, but can be suspended if you've been bad, or closed if you've been horrible.
-
- .. php:attr:: auth_token
-
- The authorization token for this account. This token should be kept a secret, so no sharing.
-
-Application
-===========
-
-.. php:class:: Services_Twilio_Rest_Application
-
- For more information, see the `Application Instance Resource <http://www.twilio.com/docs/api/rest/applications#instance>`_ documentation.
-
- .. php:attr:: sid
-
- A 34 character string that uniquely idetifies this resource.
-
- .. php:attr:: date_created
-
- The date that this resource was created, given as GMT RFC 2822 format.
-
- .. php:attr:: date_updated
-
- The date that this resource was last updated, given as GMT RFC 2822 format.
-
- .. php:attr:: friendly_name
-
- A human readable descriptive text for this resource, up to 64 characters long. By default, the FriendlyName is a nicely formatted version of the phone number.
-
- .. php:attr:: account_sid
-
- The unique id of the Account responsible for this phone number.
-
- .. php:attr:: api_version
-
- Calls to this phone number will start a new TwiML session with this API version.
-
- .. php:attr:: voice_caller_id_lookup
-
- Look up the caller's caller-ID name from the CNAM database (additional charges apply). Either true or false.
-
- .. php:attr:: voice_url
-
- The URL Twilio will request when this phone number receives a call.
-
- .. php:attr:: voice_method
-
- The HTTP method Twilio will use when requesting the above Url. Either GET or POST.
-
- .. php:attr:: voice_fallback_url
-
- The URL that Twilio will request if an error occurs retrieving or executing the TwiML requested by Url.
-
- .. php:attr:: voice_fallback_method
-
- The HTTP method Twilio will use when requesting the VoiceFallbackUrl. Either GET or POST.
-
- .. php:attr:: status_callback
-
- The URL that Twilio will request to pass status parameters (such as call ended) to your application.
-
- .. php:attr:: status_callback_method
-
- The HTTP method Twilio will use to make requests to the StatusCallback URL. Either GET or POST.
-
- .. php:attr:: sms_url
-
- The URL Twilio will request when receiving an incoming SMS message to this number.
-
- .. php:attr:: sms_method
-
- The HTTP method Twilio will use when making requests to the SmsUrl. Either GET or POST.
-
- .. php:attr:: sms_fallback_url
-
- The URL that Twilio will request if an error occurs retrieving or executing the TwiML from SmsUrl.
-
- .. php:attr:: sms_fallback_method
-
- The HTTP method Twilio will use when requesting the above URL. Either GET or POST.
-
- .. php:attr:: uri
-
- The URI for this resource, relative to https://api.twilio.com.
-
-AvailablePhoneNumber
-========================
-
-.. php:class:: Services_Twilio_Rest_AvailablePhoneNumber
-
- For more information, see the `AvailablePhoneNumber Instance Resource <http://www.twilio.com/docs/api/rest/available-phone-numbers#instance>`_ documentation.
-
- .. php:attr:: friendly_name
-
- A nicely-formatted version of the phone number.
-
- .. php:attr:: phone_number
-
- The phone number, in E.164 (i.e. "+1") format.
-
- .. php:attr:: lata
-
- The LATA of this phone number.
-
- .. php:attr:: rate_center
-
- The rate center of this phone number.
-
- .. php:attr:: latitude
-
- The latitude coordinate of this phone number.
-
- .. php:attr:: longitude
-
- The longitude coordinate of this phone number.
-
- .. php:attr:: region
-
- The two-letter state or province abbreviation of this phone number.
-
- .. php:attr:: postal_code
-
- The postal (zip) code of this phone number.
-
- .. php:attr:: iso_country
-
-Call
-====
-
-.. phpautoclass:: Services_Twilio_Rest_Call
- :filename: ../Services/Twilio/Rest/Call.php
- :members:
-
-CallerId
-============
-
-.. php:class:: Services_Twilio_Rest_OutgoingCallerId
-
- For more information, see the `OutgoingCallerId Instance Resource <http://www.twilio.com/docs/api/rest/outgoing-caller-ids#instance>`_ documentation.
-
- .. php:attr:: sid
-
- A 34 character string that uniquely identifies this resource.
-
- .. php:attr:: date_created
-
- The date that this resource was created, given in RFC 2822 format.
-
- .. php:attr:: date_updated
-
- The date that this resource was last updated, given in RFC 2822 format.
-
- .. php:attr:: friendly_name
-
- A human readable descriptive text for this resource, up to 64 characters long. By default, the FriendlyName is a nicely formatted version of the phone number.
-
- .. php:attr:: account_sid
-
- The unique id of the Account responsible for this Caller Id.
-
- .. php:attr:: phone_number
-
- The incoming phone number. Formatted with a '+' and country code e.g., +16175551212 (E.164 format).
-
- .. php:attr:: uri
-
- The URI for this resource, relative to https://api.twilio.com.
-
-Conference
-=============
-
-.. php:class:: Services_Twilio_Rest_Conference
-
- For more information, see the `Conference Instance Resource <http://www.twilio.com/docs/api/rest/conference#instance>`_ documentation.
-
- .. php:attr:: sid
-
- A 34 character string that uniquely identifies this conference.
-
- .. php:attr:: friendly_name
-
- A user provided string that identifies this conference room.
-
- .. php:attr:: status
-
- A string representing the status of the conference. May be init, in-progress, or completed.
-
- .. php:attr:: date_created
-
- The date that this conference was created, given as GMT in RFC 2822 format.
-
- .. php:attr:: date_updated
-
- The date that this conference was last updated, given as GMT in RFC 2822 format.
-
- .. php:attr:: account_sid
-
- The unique id of the Account responsible for creating this conference.
-
- .. php:attr:: uri
-
- The URI for this resource, relative to https://api.twilio.com.
-
- .. php:attr:: participants
-
- The :php:class:`Services_Twilio_Rest_Participants` instance, listing people currently in this conference
-
-CredentialListMapping
-=========================
-
-.. phpautoclass:: Services_Twilio_Rest_CredentialListMapping
- :filename: ../Services/Twilio/Rest/CredentialListMapping.php
- :members:
-
-
-CredentialList
-=================
-
-.. phpautoclass:: Services_Twilio_Rest_CredentialList
- :filename: ../Services/Twilio/Rest/CredentialList.php
- :members:
-
-Credential
-==============
-
-.. phpautoclass:: Services_Twilio_Rest_Credential
- :filename: ../Services/Twilio/Rest/Credential.php
- :members:
-
-Domain
-==========
-
-.. phpautoclass:: Services_Twilio_Rest_Domain
- :filename: ../Services/Twilio/Rest/Domain.php
- :members:
-
-IncomingPhoneNumber
-===================
-
-.. phpautoclass:: Services_Twilio_Rest_IncomingPhoneNumber
- :filename: ../Services/Twilio/Rest/IncomingPhoneNumber.php
- :members:
-
-IpAccessControlListMapping
-==============================
-
-.. phpautoclass:: Services_Twilio_Rest_IpAccessControlListMapping
- :filename: ../Services/Twilio/Rest/IpAccessControlListMapping.php
- :members:
-
-IpAccessControlList
-=======================
-
-.. phpautoclass:: Services_Twilio_Rest_IpAccessControlList
- :filename: ../Services/Twilio/Rest/IpAccessControlList.php
- :members:
-
-IpAddress
-==============
-.. phpautoclass:: Services_Twilio_Rest_IpAddress
- :filename: ../Services/Twilio/Rest/IpAddress.php
- :members:
-
-
-Notification
-=============
-
-.. php:class:: Services_Twilio_Rest_Notification
-
- For more information, see the `Notification Instance Resource <http://www.twilio.com/docs/api/rest/notification#instance>`_ documentation.
-
- .. php:attr:: sid
-
- A 34 character string that uniquely identifies this resource.
-
- .. php:attr:: date_created
-
- The date that this resource was created, given in RFC 2822 format.
-
- .. php:attr:: date_updated
-
- The date that this resource was last updated, given in RFC 2822 format.
-
- .. php:attr:: account_sid
-
- The unique id of the Account responsible for this notification.
-
- .. php:attr:: call_sid
-
- CallSid is the unique id of the call during which the notification was generated. Empty if the notification was generated by the REST API without regard to a specific phone call.
-
- .. php:attr:: api_version
-
- The version of the Twilio in use when this notification was generated.
-
- .. php:attr:: log
-
- An integer log level corresponding to the type of notification: 0 is ERROR, 1 is WARNING.
-
- .. php:attr:: error_code
-
- A unique error code for the error condition. You can lookup errors, with possible causes and solutions, in our `Error Dictionary <http://www.twilio.com/docs/errors/reference>`_.
-
- .. php:attr:: more_info
-
- A URL for more information about the error condition. The URL is a page in our `Error Dictionary <http://www.twilio.com/docs/errors/reference>`_.
-
- .. php:attr:: message_text
-
- The text of the notification.
-
- .. php:attr:: message_date
-
- The date the notification was actually generated, given in RFC 2822
- format. Due to buffering, this may be slightly different than the
- DateCreated date.
-
- .. php:attr:: request_url
-
- The URL of the resource that generated the notification. If the
- notification was generated during a phone call: This is the URL of the
- resource on YOUR SERVER that caused the notification. If the notification
- was generated by your use of the REST API: This is the URL of the REST
- resource you were attempting to request on Twilio's servers.
-
- .. php:attr:: request_method
-
- The HTTP method in use for the request that generated the notification. If
- the notification was generated during a phone call: The HTTP Method use to
- request the resource on your server. If the notification was generated by
- your use of the REST API: This is the HTTP method used in your request to
- the REST resource on Twilio's servers.
-
- .. php:attr:: request_variables
-
- The Twilio-generated HTTP GET or POST variables sent to your server. Alternatively, if the notification was generated by the REST API, this field will include any HTTP POST or PUT variables you sent to the REST API.
-
- .. php:attr:: response_headers
-
- The HTTP headers returned by your server.
-
- .. php:attr:: response_body
-
- The HTTP body returned by your server.
-
- .. php:attr:: uri
-
- The URI for this resource, relative to https://api.twilio.com
-
-Media
-=======
-
-.. phpautoclass:: Services_Twilio_Rest_MediaInstance
- :filename: ../Services/Twilio/Rest/MediaInstance.php
- :members:
-
-Member
-=======
-
-.. php:class:: Services_Twilio_Rest_Member
-
- For more information about available properties, see the `Member Instance Resource <http://www.twilio.com/docs/api/rest/member#instance>`_ documentation.
-
- .. php:method:: dequeue($url, $method = 'POST')
-
- Dequeue this member and immediately play the Twiml at the given ``$url``.
-
- :param string $url: The Twiml URL to play for this member, after dequeuing them
- :param string $method: The HTTP method to use when fetching the Twiml URL. Defaults to POST.
- :return: The dequeued member
- :rtype: :php:class:`Member <Services_Twilio_Rest_Member>`
-
-
-Participant
-=============
-
-.. php:class:: Services_Twilio_Rest_Participant
-
- For more information, see the `Participant Instance Resource <http://www.twilio.com/docs/api/rest/participant#instance>`_ documentation.
-
- .. php:attr:: call_sid
-
- A 34 character string that uniquely identifies the call that is connected to this conference
-
- .. php:attr:: conference_sid
-
- A 34 character string that identifies the conference this participant is in
-
- .. php:attr:: date_created
-
- The date that this resource was created, given in RFC 2822 format.
-
- .. php:attr:: date_updated
-
- The date that this resource was last updated, given in RFC 2822 format.
-
- .. php:attr:: account_sid
-
- The unique id of the Account that created this conference
-
- .. php:attr:: muted
-
- true if this participant is currently muted. false otherwise.
-
- .. php:attr:: start_conference_on_enter
-
- Was the startConferenceOnEnter attribute set on this participant (true or false)?
-
- .. php:attr:: end_conference_on_exit
-
- Was the endConferenceOnExit attribute set on this participant (true or false)?
-
- .. php:attr:: uri
-
- The URI for this resource, relative to https://api.twilio.com.
-
-Queue
-============
-
-.. php:class:: Services_Twilio_Rest_Queue
-
- For more information about available properties of a queue, see the `Queue
- Instance Resource <http://www.twilio.com/docs/api/rest/queue#instance>`_
- documentation. A Queue has one subresource, a list of
- :php:class:`Services_Twilio_Rest_Members`.
-
-Recording
-=============
-
-.. php:class:: Services_Twilio_Rest_Recording
-
- For more information, see the `Recording Instance Resource <http://www.twilio.com/docs/api/rest/recording#instance>`_ documentation.
-
- .. php:attr:: sid
-
- A 34 character string that uniquely identifies this resource.
-
- .. php:attr:: date_created
-
- The date that this resource was created, given in RFC 2822 format.
-
- .. php:attr:: date_updated
-
- The date that this resource was last updated, given in RFC 2822 format.
-
- .. php:attr:: account_sid
-
- The unique id of the Account responsible for this recording.
-
- .. php:attr:: call_sid
-
- The call during which the recording was made.
-
- .. php:attr:: duration
-
- The length of the recording, in seconds.
-
- .. php:attr:: api_version
-
- The version of the API in use during the recording.
-
- .. php:attr:: uri
-
- The URI for this resource, relative to https://api.twilio.com
-
- .. php:attr:: subresource_uris
-
- The list of subresources under this account
-
- .. php:attr:: formats
-
- A dictionary of the audio formats available for this recording
-
- .. code-block:: php
-
- array(
- 'wav' => 'https://api.twilio.com/path/to/recording.wav',
- 'mp3' => 'https://api.twilio.com/path/to/recording.mp3',
- )
-
-Message
-=======
-
-.. phpautoclass:: Services_Twilio_Rest_Message
- :filename: ../Services/Twilio/Rest/Message.php
- :members:
-
-SmsMessage
-===========
-
-.. php:class:: Services_Twilio_Rest_SmsMessage
-
- For more information, see the `SMS Message Instance Resource <http://www.twilio.com/docs/api/rest/sms#instance>`_ documentation.
-
- .. php:attr:: sid
-
- A 34 character string that uniquely identifies this resource.
-
- .. php:attr:: date_created
-
- The date that this resource was created, given in RFC 2822 format.
-
- .. php:attr:: date_updated
-
- The date that this resource was last updated, given in RFC 2822 format.
-
- .. php:attr:: date_sent
-
- The date that the SMS was sent, given in RFC 2822 format.
-
- .. php:attr:: account_sid
-
- The unique id of the Account that sent this SMS message.
-
- .. php:attr:: from
-
- The phone number that initiated the message in E.164 format. For incoming messages, this will be the remote phone. For outgoing messages, this will be one of your Twilio phone numbers.
-
- .. php:attr:: to
-
- The phone number that received the message in E.164 format. For incoming messages, this will be one of your Twilio phone numbers. For outgoing messages, this will be the remote phone.
-
- .. php:attr:: body
-
- The text body of the SMS message. Up to 160 characters long.
-
- .. php:attr:: status
-
- The status of this SMS message. Either queued, sending, sent, or failed.
-
- .. php:attr:: direction
-
- The direction of this SMS message. ``incoming`` for incoming messages,
- ``outbound-api`` for messages initiated via the REST API, ``outbound-call`` for
- messages initiated during a call or ``outbound-reply`` for messages initiated in
- response to an incoming SMS.
-
- .. php:attr:: price
-
- The amount billed for the message.
-
- .. php:attr:: api_version
-
- The version of the Twilio API used to process the SMS message.
-
- .. php:attr:: uri
-
- The URI for this resource, relative to https://api.twilio.com
-
-
-Transcription
-==================
-
-.. php:class:: Services_Twilio_Rest_Transcription
-
- For more information, see the `Transcription Instance Resource <http://www.twilio.com/docs/api/rest/transcription#instance>`_ documentation.
-
- .. php:attr:: sid
-
- A 34 character string that uniquely identifies this resource.
-
- .. php:attr:: date_created
-
- The date that this resource was created, given in RFC 2822 format.
-
- .. php:attr:: date_updated
-
- The date that this resource was last updated, given in RFC 2822 format.
-
- .. php:attr:: account_sid
-
- The unique id of the Account responsible for this transcription.
-
- .. php:attr:: status
-
- A string representing the status of the transcription: ``in-progress``, ``completed`` or ``failed``.
-
- .. php:attr:: recording_sid
-
- The unique id of the Recording this Transcription was made of.
-
- .. php:attr:: duration
-
- The duration of the transcribed audio, in seconds.
-
- .. php:attr:: transcription_text
-
- The text content of the transcription.
-
- .. php:attr:: price
-
- The charge for this transcript in USD. Populated after the transcript is completed. Note, this value may not be immediately available.
-
- .. php:attr:: uri
-
- The URI for this resource, relative to https://api.twilio.com
-
-
diff --git a/externals/twilio-php/docs/api/services.rst b/externals/twilio-php/docs/api/services.rst
deleted file mode 100644
index a69f212e9..000000000
--- a/externals/twilio-php/docs/api/services.rst
+++ /dev/null
@@ -1,26 +0,0 @@
-###############################
-HTTP Helper Classes
-###############################
-
-**********************
-The Twilio Rest Client
-**********************
-
-.. phpautoclass:: Services_Twilio
- :filename: ../Services/Twilio.php
- :members:
-
-***************************
-Twilio's Custom HTTP Client
-***************************
-
-.. phpautoclass:: Services_Twilio_TinyHttp
- :filename: ../Services/Twilio/TinyHttp.php
- :members:
-
-***********************
-Twilio Rest Exceptions
-***********************
-.. phpautoclass:: Services_Twilio_RestException
- :filename: ../Services/Twilio/RestException.php
- :members:
diff --git a/externals/twilio-php/docs/api/twiml.rst b/externals/twilio-php/docs/api/twiml.rst
deleted file mode 100644
index de9f4c4a7..000000000
--- a/externals/twilio-php/docs/api/twiml.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-###########################################
-API for TwiML Generation
-###########################################
-
-.. phpautoclass:: Services_Twilio_Twiml
- :filename: ../Services/Twilio/Twiml.php
- :members:
diff --git a/externals/twilio-php/docs/conf.py b/externals/twilio-php/docs/conf.py
deleted file mode 100644
index 94fff8449..000000000
--- a/externals/twilio-php/docs/conf.py
+++ /dev/null
@@ -1,226 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Services_Twilio documentation build configuration file, created by
-# sphinx-quickstart on Tue Mar 8 04:02:01 2011.
-#
-# This file is execfile()d with the current directory set to its containing dir.
-#
-# Note that not all possible configuration values are present in this
-# autogenerated file.
-#
-# All configuration values have a default; values that are commented out
-# serve to show the default.
-
-import sys, os
-from datetime import datetime
-
-# If extensions (or modules to document with autodoc) are in another directory,
-# add these directories to sys.path here. If the directory is relative to the
-# documentation root, use os.path.abspath to make it absolute, like shown here.
-#sys.path.insert(0, os.path.abspath('.'))
-
-# -- General configuration -----------------------------------------------------
-
-# If your documentation needs a minimal Sphinx version, state it here.
-#needs_sphinx = '1.0'
-
-# Add any Sphinx extension module names here, as strings. They can be extensions
-# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinxcontrib.phpdomain', 'sphinxcontrib_phpautodoc']
-
-primary_domain = 'php'
-
-# Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
-
-# The suffix of source filenames.
-source_suffix = '.rst'
-
-# The encoding of source files.
-#source_encoding = 'utf-8-sig'
-
-# The master toctree document.
-master_doc = 'index'
-
-# General information about the project.
-project = u'Services_Twilio'
-copyright = unicode(datetime.utcnow().year) + u', Twilio Inc'
-
-# The version info for the project you're documenting, acts as replacement for
-# |version| and |release|, also used in various other places throughout the
-# built documents.
-#
-# The short X.Y version.
-version = '3.12'
-# The full version, including alpha/beta/rc tags.
-release = '3.12.4'
-
-# The language for content autogenerated by Sphinx. Refer to documentation
-# for a list of supported languages.
-#language = None
-
-# There are two options for replacing |today|: either, you set today to some
-# non-false value, then it is used:
-#today = ''
-# Else, today_fmt is used as the format for a strftime call.
-#today_fmt = '%B %d, %Y'
-
-# List of patterns, relative to source directory, that match files and
-# directories to ignore when looking for source files.
-exclude_patterns = ['_build']
-
-# The reST default role (used for this markup: `text`) to use for all documents.
-#default_role = None
-
-# If true, '()' will be appended to :func: etc. cross-reference text.
-#add_function_parentheses = True
-
-# If true, the current module name will be prepended to all description
-# unit titles (such as .. function::).
-#add_module_names = True
-
-# If true, sectionauthor and moduleauthor directives will be shown in the
-# output. They are ignored by default.
-#show_authors = False
-
-# The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
-
-# A list of ignored prefixes for module index sorting.
-#modindex_common_prefix = []
-
-
-# -- Options for HTML output ---------------------------------------------------
-sys.path.append(os.path.abspath('_themes'))
-html_theme_path = ['_themes']
-html_theme = 'kr'
-
-from sphinx.highlighting import lexers
-from pygments.lexers.web import PhpLexer
-lexers['php'] = PhpLexer(startinline=True)
-
-# The theme to use for HTML and HTML Help pages. See the documentation for
-# a list of builtin themes.
-#html_theme = 'default'
-
-# Theme options are theme-specific and customize the look and feel of a theme
-# further. For a list of options available for each theme, see the
-# documentation.
-#html_theme_options = {}
-
-# Add any paths that contain custom themes here, relative to this directory.
-#html_theme_path = []
-
-# The name for this set of Sphinx documents. If None, it defaults to
-# "<project> v<release> documentation".
-#html_title = None
-
-# A shorter title for the navigation bar. Default is the same as html_title.
-#html_short_title = None
-
-# The name of an image file (relative to this directory) to place at the top
-# of the sidebar.
-#html_logo = None
-
-# The name of an image file (within the static path) to use as favicon of the
-# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
-# pixels large.
-#html_favicon = None
-
-# Add any paths that contain custom static files (such as style sheets) here,
-# relative to this directory. They are copied after the builtin static files,
-# so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
-
-# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
-# using the given strftime format.
-#html_last_updated_fmt = '%b %d, %Y'
-
-# If true, SmartyPants will be used to convert quotes and dashes to
-# typographically correct entities.
-#html_use_smartypants = True
-
-# Custom sidebar templates, maps document names to template names.
-#html_sidebars = {}
-
-# Additional templates that should be rendered to pages, maps page names to
-# template names.
-#html_additional_pages = {}
-
-# If false, no module index is generated.
-#html_domain_indices = True
-
-# If false, no index is generated.
-#html_use_index = True
-
-# If true, the index is split into individual pages for each letter.
-#html_split_index = False
-
-# If true, links to the reST sources are added to the pages.
-#html_show_sourcelink = True
-
-# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
-#html_show_sphinx = True
-
-# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
-#html_show_copyright = True
-
-# If true, an OpenSearch description file will be output, and all pages will
-# contain a <link> tag referring to it. The value of this option must be the
-# base URL from which the finished HTML is served.
-#html_use_opensearch = ''
-
-# This is the file name suffix for HTML files (e.g. ".xhtml").
-#html_file_suffix = None
-
-# Output file base name for HTML help builder.
-htmlhelp_basename = 'Services_Twiliodoc'
-
-
-# -- Options for LaTeX output --------------------------------------------------
-
-# The paper size ('letter' or 'a4').
-#latex_paper_size = 'letter'
-
-# The font size ('10pt', '11pt' or '12pt').
-#latex_font_size = '10pt'
-
-# Grouping the document tree into LaTeX files. List of tuples
-# (source start file, target name, title, author, documentclass [howto/manual]).
-latex_documents = [
- ('index', 'Services_Twilio.tex', u'Services\\_Twilio Documentation',
- u'Neuman Vong', 'manual'),
-]
-
-# The name of an image file (relative to this directory) to place at the top of
-# the title page.
-#latex_logo = None
-
-# For "manual" documents, if this is true, then toplevel headings are parts,
-# not chapters.
-#latex_use_parts = False
-
-# If true, show page references after internal links.
-#latex_show_pagerefs = False
-
-# If true, show URL addresses after external links.
-#latex_show_urls = False
-
-# Additional stuff for the LaTeX preamble.
-#latex_preamble = ''
-
-# Documents to append as an appendix to all manuals.
-#latex_appendices = []
-
-# If false, no module index is generated.
-#latex_domain_indices = True
-
-
-# -- Options for manual page output --------------------------------------------
-
-# One entry per manual page. List of tuples
-# (source start file, name, description, authors, manual section).
-man_pages = [
- ('index', 'services_twilio', u'Services_Twilio Documentation',
- [u'Neuman Vong'], 1)
-]
diff --git a/externals/twilio-php/docs/faq.rst b/externals/twilio-php/docs/faq.rst
deleted file mode 100644
index b9b2e0808..000000000
--- a/externals/twilio-php/docs/faq.rst
+++ /dev/null
@@ -1,176 +0,0 @@
-==========================
-Frequently Asked Questions
-==========================
-
-Hopefully you can find an answer here to one of your questions. If not, please
-contact `help@twilio.com <mailto:help@twilio.com>`_.
-
-Debugging Requests
-------------------
-
-Sometimes the library generates unexpected output. The simplest way to debug is
-to examine the HTTP request that twilio-php actually sent over the wire. You
-can turn on debugging with a simple flag:
-
-.. code-block:: php
-
- require('Services/Twilio.php');
-
- $client = new Services_Twilio('AC123', '456bef');
- $client->http->debug = true;
-
-Then make requests as you normally would. The URI, method, headers, and body
-of HTTP requests will be logged via the ``error_log`` function.
-
-
-require: Failed to open stream messages
------------------------------------------
-
-If you are trying to use the helper library and you get an error message that
-looks like this:
-
-.. code-block:: php
-
- PHP Warning: require(Services/Twilio.php): failed to open stream: No such
- file or directory in /path/to/file
-
- Fatal error: require(): Failed opening required 'Services/Twilio.php'
- (include_path='.:/usr/lib/php:/usr/local/php-5.3.8/lib/php') in
- /Library/Python/2.6/site-packages/phpsh/phpsh.php(578): on line 1
-
-Your PHP file can't find the Twilio library. The easiest way to do this is to
-move the Services folder from the twilio-php library into the folder containing
-your file. So if you have a file called ``send-sms.php``, your folder structure
-should look like this:
-
-.. code-block:: bash
-
- .
- ├── send-sms.php
- ├── Services
- │   ├── Twilio.php
- │   ├── Twilio
- │   │   ├── ArrayDataProxy.php
- │   │   ├── (..about 50 other files...)
-
-If you need to copy all of these files to your web hosting server, the easiest
-way is to compress them into a ZIP file, copy that to your server with FTP, and
-then unzip it back into a folder in your CPanel or similar.
-
-You can also try changing the ``require`` line like this:
-
-.. code-block:: php
-
- require('/path/to/twilio-php/Services/Twilio.php');
-
-You could also try downloading the library via PEAR, a package manager for PHP,
-which will add the library to your PHP path, so you can load the Twilio library
-from anywhere. Run this at the command line:
-
-.. code-block:: bash
-
- $ pear channel-discover twilio.github.com/pear
- $ pear install twilio/Services_Twilio
-
-If you get the following message:
-
-.. code-block:: bash
-
- $ -bash: pear: command not found
-
-you can install PEAR from their website.
-
-SSL Validation Exceptions
--------------------------
-
-If you are using an outdated version of `libcurl`, you may encounter
-SSL validation exceptions. If you see the following error message, you have
-a SSL validation exception: ::
-
- Fatal error: Uncaught exception 'Services_Twilio_TinyHttpException'
- with message 'SSL certificate problem, verify that the CA cert is OK.
-
- Details: error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate
- verify failed' in [MY PATH]\Services\Twilio\TinyHttp.php:89
-
-This means that Twilio is trying to offer a certificate to verify that you are
-actually connecting to `https://api.twilio.com <https://api.twilio.com>`_, but
-your curl client cannot verify our certificate.
-
-There are four solutions to this problem:
-
-Upgrade your version of the twilio-php library
-==============================================
-
-Since November 2011, the SSL certificate has been built in to the helper
-library, and it is used to sign requests made to our API. If you are still
-encountering this problem, you can upgrade your helper library to the latest
-version, and you should not encounter this error anymore.
-
-If you are using an older version of the helper library, you can try one of the
-following three methods:
-
-Upgrade your version of libcurl
-===============================
-
-The Twilio certificate is included in the latest version of the
-``libcurl`` library. Upgrading your system version of ``libcurl`` will
-resolve the SSL error. `Click here to download the latest version of
-libcurl <http://curl.haxx.se/download.html>`_.
-
-Manually add Twilio's SSL certificate
-=====================================
-
-The PHP curl library can also manually verify an SSL certificate. In your
-browser, navigate to
-`https://github.com/twilio/twilio-php/blob/master/Services/cacert.pem
-<https://github.com/twilio/twilio-php/blob/master/Services/cacert.pem>`_
-and download the file. (**Note**: If your browser presents ANY warnings
-at this time, your Internet connection may be compromised. Do not download the
-file, and do not proceed with this step). Place this file in the same folder as
-your PHP script. Then, replace this line in your script:
-
-.. code-block:: php
-
- $client = new Services_Twilio($sid, $token);
-
-with this one:
-
-.. code-block:: php
-
- $http = new Services_Twilio_TinyHttp(
- 'https://api.twilio.com',
- array('curlopts' => array(
- CURLOPT_SSL_VERIFYPEER => true,
- CURLOPT_SSL_VERIFYHOST => 2,
- CURLOPT_CAINFO => getcwd() . "/cacert.pem")));
-
- $client = new Services_Twilio($sid, $token, "2010-04-01", $http);
-
-Disable certificate checking
-============================
-
-A final option is to disable checking the certificate. Disabling the
-certificate check means that a malicious third party can pretend to be
-Twilio, intercept your data, and gain access to your Account SID and
-Auth Token in the process. Because this is a security vulnerability,
-we **strongly discourage** you from disabling certificate checking in
-a production environment. This is known as a `man-in-the-middle attack
-<http://en.wikipedia.org/wiki/Man-in-the-middle_attack>`_.
-
-If you still want to proceed, here is code that will disable certificate
-checking:
-
-.. code-block:: php
-
- $http = new Services_Twilio_TinyHttp(
- 'https://api.twilio.com',
- array('curlopts' => array(CURLOPT_SSL_VERIFYPEER => false))
- );
-
- $client = new Services_Twilio('AC123', 'token', '2010-04-01', $http);
-
-If this does not work, double check your Account SID, token, and that you do
-not have errors anywhere else in your code. If you need further assistance,
-please email our customer support at `help@twilio.com`_.
-
diff --git a/externals/twilio-php/docs/index.rst b/externals/twilio-php/docs/index.rst
deleted file mode 100644
index ae990fb18..000000000
--- a/externals/twilio-php/docs/index.rst
+++ /dev/null
@@ -1,188 +0,0 @@
-.. Services_Twilio documentation master file, created by
- sphinx-quickstart on Tue Mar 8 04:02:01 2011.
- You can adapt this file completely to your liking, but it should at least
- contain the root `toctree` directive.
-
-=================
-**twilio-php**
-=================
-
-Status
-=======
-
-This documentation is for version 3.12.2 of `twilio-php
-<https://www.github.com/twilio/twilio-php>`_.
-
-Quickstart
-============
-
-Send an SMS
->>>>>>>>>>>
-
-.. code-block:: php
-
- // Download the library and copy into the folder containing this file.
- require('/path/to/twilio-php/Services/Twilio.php');
-
- $account_sid = "ACXXXXXX"; // Your Twilio account sid
- $auth_token = "YYYYYY"; // Your Twilio auth token
-
- $client = new Services_Twilio($account_sid, $auth_token);
- $message = $client->account->messages->sendMessage(
- '+14085551234', // From a Twilio number in your account
- '+12125551234', // Text any number
- "Hello monkey!"
- );
-
- print $message->sid;
-
-Make a Call
->>>>>>>>>>>>>>
-
-.. code-block:: php
-
- // Download the library and copy into the folder containing this file.
- require('/path/to/twilio-php/Services/Twilio.php');
-
- $account_sid = "ACXXXXXX"; // Your Twilio account sid
- $auth_token = "YYYYYY"; // Your Twilio auth token
-
- $client = new Services_Twilio($account_sid, $auth_token);
- $call = $client->account->calls->create(
- '+14085551234', // From a Twilio number in your account
- '+12125551234', // Call any number
-
- // Read TwiML at this URL when a call connects (hold music)
- 'http://twimlets.com/holdmusic?Bucket=com.twilio.music.ambient'
- );
-
-Generating TwiML
->>>>>>>>>>>>>>>>
-
-To control phone calls, your application needs to output `TwiML
-<http://www.twilio.com/docs/api/twiml/>`_. Use :class:`Services_Twilio_Twiml`
-to easily create such responses.
-
-.. code-block:: php
-
- $response = new Services_Twilio_Twiml();
- $response->say('Hello');
- $response->play('https://api.twilio.com/cowbell.mp3', array("loop" => 5));
- print $response;
-
-.. code-block:: xml
-
- <?xml version="1.0" encoding="utf-8"?>
- <Response>
- <Say>Hello</Say>
- <Play loop="5">https://api.twilio.com/cowbell.mp3</Play>
- </Response>
-
-View more examples of TwiML generation here: :ref:`usage-twiml`
-
-Installation
-============
-
-There are two ways to install **twilio-php**: via the PEAR installer, or by
-downloading the source.
-
-Via PEAR
->>>>>>>>>>>>>
-
-Use the ``Makefile`` in the repo's top
-
-.. code-block:: bash
-
- pear channel-discover twilio.github.com/pear
- pear install twilio/Services_Twilio
-
-From Source
->>>>>>>>>>>>>
-
-If you aren't using PEAR, download the `source (.zip)
-<https://github.com/twilio/twilio-php/zipball/master>`_, which includes all the
-dependencies.
-
-User Guide
-==================
-
-REST API
->>>>>>>>>>
-
-.. toctree::
- :maxdepth: 2
- :glob:
-
- usage/rest
- usage/rest/*
-
-TwiML and other utilities
->>>>>>>>>>>>>>>>>>>>>>>>>>
-
-.. toctree::
- :maxdepth: 1
-
- usage/twiml
- usage/validation
- usage/token-generation
- faq/
-
-API Documentation
-==================
-
-.. toctree::
- :maxdepth: 3
- :glob:
-
- api/*
-
-
-Support and Development
-===========================
-
-All development occurs on `Github <https://github.com/twilio/twilio-php>`_. To
-check out the source, run
-
-.. code-block:: bash
-
- git clone git@github.com:twilio/twilio-php.git
-
-Report bugs using the Github `issue tracker <https://github.com/twilio/twilio-php/issues>`_.
-
-If you've got questions that aren't answered by this documentation, ask the
-Twilio support team at help@twilio.com.
-
-Running the Tests
->>>>>>>>>>>>>>>>>>>>>>>>>
-
-The unit tests depend on `Mockery <https://github.com/padraic/mockery>`_ and
-`PHPUnit <https://github.com/sebastianbergmann/phpunit>`_. First, 'discover' all
-the necessary `PEAR` channels:
-
-.. code-block:: bash
-
- make test-install
-
-After installation, run the tests with :data:`make`.
-
-.. code-block:: bash
-
- make test
-
-
-Making the Documentation
->>>>>>>>>>>>>>>>>>>>>>>>>>
-
-Our documentation is written using `Sphinx <http://sphinx.pocoo.org/>`_. You'll
-need to install Sphinx and the Sphinx PHP domain before you can build the docs.
-
-.. code-block:: bash
-
- make docs-install
-
-Once you have those installed, making the docs is easy.
-
-.. code-block:: bash
-
- make docs
-
diff --git a/externals/twilio-php/docs/quickstart.rst b/externals/twilio-php/docs/quickstart.rst
deleted file mode 100644
index f8441c3a8..000000000
--- a/externals/twilio-php/docs/quickstart.rst
+++ /dev/null
@@ -1,34 +0,0 @@
-=============
-Quickstart
-=============
-
-Making a Call
-==============
-
-.. code-block:: php
-
- $sid = "ACXXXXXX"; // Your Twilio account sid
- $token = "YYYYYY"; // Your Twilio auth token
-
- $client = new Services_Twilio($sid, $token);
- $call = $client->account->calls->create(
- '9991231234', // From this number
- '8881231234', // Call this number
- 'http://foo.com/call.xml'
- );
-
-Generating TwiML
-==================
-
-To control phone calls, your application need to output TwiML. Use :class:`Services_Twilio_Twiml` to easily create such responses.
-
-.. code-block:: php
-
- $response = new Services_Twilio_Twiml();
- $response->say('Hello');
- print $response;
-
-.. code-block:: xml
-
- <?xml version="1.0" encoding="utf-8"?>
- <Response><Play loop="5">monkey.mp3</Play><Response>
diff --git a/externals/twilio-php/docs/requirements.txt b/externals/twilio-php/docs/requirements.txt
deleted file mode 100644
index 8c327a5f9..000000000
--- a/externals/twilio-php/docs/requirements.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-sphinxcontrib-phpdomain
-hg+https://bitbucket.org/tk0miya/tk.phpautodoc
-
diff --git a/externals/twilio-php/docs/usage/rest.rst b/externals/twilio-php/docs/usage/rest.rst
deleted file mode 100644
index e6edade5b..000000000
--- a/externals/twilio-php/docs/usage/rest.rst
+++ /dev/null
@@ -1,98 +0,0 @@
-.. _ref-rest:
-
-==========================
-Using the Twilio REST API
-==========================
-
-Since version 3.0, we've introduced an updated API for interacting with the
-Twilio REST API. Gone are the days of manual URL creation and XML parsing.
-
-Creating a REST Client
-=======================
-
-Before querying the API, you'll need to create a :php:class:`Services_Twilio`
-instance. The constructor takes your Twilio Account Sid and Auth
-Token (both available through your `Twilio Account Dashboard
-<http:www.twilio.com/user/account>`_).
-
-.. code-block:: php
-
- $ACCOUNT_SID = "AC123";
- $AUTH_TOKEN = "secret";
- $client = new Services_Twilio($ACCOUNT_SID, $AUTH_TOKEN);
-
-The :attr:`account` attribute
------------------------------
-
-You access the Twilio API resources through this :attr:`$client`,
-specifically the :attr:`$account` attribute, which is an instance of
-:php:class:`Services_Twilio_Rest_Account`. We'll use the `Calls resource
-<http://www.twilio.com/docs/api/rest/call>`_ as an example.
-
-Listing Resources
-====================
-
-Iterating over the :attr:`calls` attribute will iterate over all of your call
-records, handling paging for you. Only use this when you need to get all your
-records.
-
-The :attr:`$call` object is a :php:class:`Services_Twilio_Rest_Call`, which
-means you can easily access fields through it's properties. The attribute names
-are lowercase and use underscores for sepearators. All the available attributes
-are documented in the :doc:`/api/rest` documentation.
-
-.. code-block:: php
-
- // If you have many calls, this could take a while
- foreach($client->account->calls as $call) {
- print $call->price . '\n';
- print $call->duration . '\n';
- }
-
-Filtering Resources
--------------------
-
-Many Twilio list resources allow for filtering via :php:meth:`getIterator`
-which takes an optional array of filter parameters. These parameters correspond
-directlty to the listed query string parameters in the REST API documentation.
-
-You can create a filtered iterator like this:
-
-.. code-block:: php
-
- $filteredCalls = $client->account->calls->getIterator(
- 0, 50, array("Status" => "in-progress"));
- foreach($filteredCalls as $call) {
- print $call->price . '\n';
- print $call->duration . '\n';
- }
-
-Retrieving the Total Number of Resources
-----------------------------------------
-
-Each of the list resources supports the `Countable` interface, which means you
-can retrieve the total number of list items like so:
-
-.. code-block:: php
-
- echo count($client->account->calls);
-
-Getting a Specific Resource
-=============================
-
-If you know the unique identifier for a resource, you can get that resource
-using the :php:meth:`get` method on the list resource.
-
-.. code-block:: php
-
- $call = $client->account->calls->get("CA123");
-
-:php:meth:`get` fetches objects lazily, so it will only load a resource when it
-is needed. This allows you to get nested objects without making multiple HTTP
-requests.
-
-.. code-block:: php
-
- $participant = $client->account->conferences
- ->get("CO123")->participants->get("PF123");
-
diff --git a/externals/twilio-php/docs/usage/rest/accounts.rst b/externals/twilio-php/docs/usage/rest/accounts.rst
deleted file mode 100644
index de9bbe702..000000000
--- a/externals/twilio-php/docs/usage/rest/accounts.rst
+++ /dev/null
@@ -1,24 +0,0 @@
-==================
-Accounts
-==================
-
-Updating Account Information
-==============================
-
-Updating :class:`Account <Services_Twilio_Rest_Account>` information is really easy:
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '123');
- $account = $client->account;
- $account->update(array('FriendlyName' => 'My Awesome Account'));
-
-Creating a Subaccount
-==============================
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '123');
- $subaccount = $client->accounts->create(array(
- 'FriendlyName' => 'My Awesome SubAccount'
- ));
diff --git a/externals/twilio-php/docs/usage/rest/applications.rst b/externals/twilio-php/docs/usage/rest/applications.rst
deleted file mode 100644
index 337cbbabf..000000000
--- a/externals/twilio-php/docs/usage/rest/applications.rst
+++ /dev/null
@@ -1,50 +0,0 @@
-==================
-Applications
-==================
-
-Creating Applications
-==============================
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '123');
- $application = $client->account->applications->create('Application Friendly Name',
- array(
- 'FriendlyName' => 'My Application Name',
- 'VoiceUrl' => 'http://foo.com/voice/url',
- 'VoiceFallbackUrl' => 'http://foo.com/voice/fallback/url',
- 'VoiceMethod' => 'POST',
- 'SmsUrl' => 'http://foo.com/sms/url',
- 'SmsFallbackUrl' => 'http://foo.com/sms/fallback/url',
- 'SmsMethod' => 'POST'
- )
- );
-
-
-Updating An Application
-==============================
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '123');
- $application = $client->account->applications->get('AP123');
- $application->update(array(
- 'VoiceUrl' => 'http://foo.com/new/voice/url'
- ));
-
-
-Finding an Application by Name
-==============================
-
-Find an :class:`Application` by its name (full name match).
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '123');
- $application = false;
- $params = array(
- 'FriendlyName' => 'My Application Name'
- );
- foreach($client->account->applications->getIterator(0, 1, $params) as $_application) {
- $application = $_application;
- }
\ No newline at end of file
diff --git a/externals/twilio-php/docs/usage/rest/callerids.rst b/externals/twilio-php/docs/usage/rest/callerids.rst
deleted file mode 100644
index 40841be54..000000000
--- a/externals/twilio-php/docs/usage/rest/callerids.rst
+++ /dev/null
@@ -1,27 +0,0 @@
-============
- Caller Ids
-============
-
-Validate a Phone Number
-=======================
-Adding a new phone number to your validated numbers is quick and easy:
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '123');
- $response = $client->account->outgoing_caller_ids->create('+15554441234');
- print $response->validation_code;
-
-Twilio will call the provided number and for the validation code to be entered.
-
-Listing all Validated Phone Numbers
-===================================
-
-Show all the current caller_ids:
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '123');
- foreach ($client->account->outgoing_caller_ids as $caller_id) {
- print $caller_id->friendly_name;
- }
diff --git a/externals/twilio-php/docs/usage/rest/calls.rst b/externals/twilio-php/docs/usage/rest/calls.rst
deleted file mode 100644
index dcde3d61c..000000000
--- a/externals/twilio-php/docs/usage/rest/calls.rst
+++ /dev/null
@@ -1,141 +0,0 @@
-=============
- Phone Calls
-=============
-
-Making a Phone Call
-===================
-
-The :class:`Calls` resource allows you to make outgoing calls:
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '123');
- $call = $client->account->calls->create(
- '9991231234', // From this number
- '8881231234', // Call this number
- 'http://foo.com/call.xml'
- );
- print $call->length;
- print $call->sid;
-
-Adding Extra Call Parameters
-============================
-
-Add extra parameters, like a `StatusCallback` when the call ends, like this:
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '123');
- $call = $client->account->calls->create(
- '9991231234', // From this number
- '8881231234', // Call this number
- 'http://foo.com/call.xml',
- array(
- 'StatusCallback' => 'http://foo.com/callback',
- 'StatusCallbackMethod' => 'GET'
- )
- );
-
-A full list of extra parameters can be found `here
-<http://www.twilio.com/docs/api/rest/making-calls#post-parameters-optional>`_.
-
-Listing Calls
-=============
-
-It's easy to iterate over your list of calls.
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '123');
- foreach ($client->account->calls as $call) {
- echo "From: {$call->from}\nTo: {$call->to}\nSid: {$call->sid}\n\n";
- }
-
-Filtering Calls
-======================
-
-Let's say you want to find all of the calls that have been sent from
-a particular number. You can do so by constructing an iterator explicitly:
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '123');
- foreach ($client->account->calls->getIterator(0, 50, array(
- 'From' => '+14105551234'
- )) as $call) {
- echo "From: {$call->from}\nTo: {$call->to}\nSid: {$call->sid}\n\n";
- }
-
-Accessing Resources from a Specific Call
-========================================
-
-The :class:`Call` resource has some subresources you can access, such as
-notifications and recordings. If you have already have a :class:`Call`
-resource, they are easy to get:
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '123');
- foreach ($client->account->calls as $call) {
- $notifications = $call->notifications;
- if (is_array($notifications)) {
- foreach ($notifications as $notification) {
- print $notification->sid;
- }
- }
-
- $transcriptions = $call->transcriptions;
- if (is_array($transcriptions)) {
- foreach ($transcriptions as $transcription) {
- print $transcription->sid;
- }
- }
-
- $recordings = $call->recordings;
- if (is_array($recordings)) {
- foreach ($recordings as $recording) {
- print $recording->sid;
- }
- }
- }
-
-Be careful, as the above code makes quite a few HTTP requests and may display
-PHP warnings for unintialized variables.
-
-Retrieve a Call Record
-======================
-
-If you already have a :class:`Call` sid, you can use the client to retrieve
-that record.:
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '123');
- $sid = "CA12341234"
- $call = $client->account->calls->get($sid)
-
-Modifying live calls
-====================
-
-The :class:`Call` resource makes it easy to find current live calls and
-redirect them as necessary:
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '123');
- $calls = $client->account->calls->getIterator(0, 50, array('Status' => 'in-progress'));
- foreach ($calls as $call) {
- $call->update(array('Url' => 'http://foo.com/new.xml', 'Method' => 'POST'));
- }
-
-Ending all live calls is also possible:
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '123');
- $calls = $client->account->calls->getIterator(0, 50, array('Status' => 'in-progress'));
- foreach ($calls as $call) {
- $call->hangup();
- }
-
-Note that :meth:`hangup` will also cancel calls currently queued.
diff --git a/externals/twilio-php/docs/usage/rest/conferences.rst b/externals/twilio-php/docs/usage/rest/conferences.rst
deleted file mode 100644
index a47d7dde9..000000000
--- a/externals/twilio-php/docs/usage/rest/conferences.rst
+++ /dev/null
@@ -1,48 +0,0 @@
-=============
- Conferences
-=============
-
-List All Conferences
-====================
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '123');
- foreach ($client->account->conferences as $conference) {
- print $conference->friendly_name;
- }
-
-For a full list of properties available on a conference, as well as a full list
-of ways to filter a conference, please see the `Conference API Documentation
-<http://www.twilio.com/docs/api/rest/conference>`_ on our website.
-
-Filter Conferences by Status
-============================
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '123');
- foreach ($client->account->conferences->getIterator(0, 50, array(
- 'Status' => 'in-progress'
- )) as $conf) {
- print $conf->sid;
- }
-
-Mute all participants
-=====================
-
-At the moment, using an iterator directly will cause this method to infinitely
-loop. Instead, use the getPage function. As conferences are limited to 40
-participants, getPage(0, 50) should return a list of every participant in
-a conference.
-
-.. code-block:: php
-
- $sid = "CO119231312";
- $client = new Services_Twilio('AC123', '123');
- $conference = $client->account->conferences->get($sid);
- $page = $conference->participants->getPage(0, 50);
- $participants = $page->participants;
- foreach ($participants as $p) {
- $p->mute();
- }
diff --git a/externals/twilio-php/docs/usage/rest/members.rst b/externals/twilio-php/docs/usage/rest/members.rst
deleted file mode 100644
index 464fda19c..000000000
--- a/externals/twilio-php/docs/usage/rest/members.rst
+++ /dev/null
@@ -1,46 +0,0 @@
-=============
-Members
-=============
-
-List All Members in a Queue
-============================
-
-Each queue instance resource has a list of members.
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '123');
- $queue_sid = 'QQ123';
- $queue = $client->account->queues->get('QQ123');
- foreach ($queue->members as $member) {
- echo "Call Sid: {$member->call_sid}\nWait Time: {$member->wait_time}\n";
- }
-
-Dequeue a Member
-=================
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '123');
- $queue = $client->account->queues->get('QQ123');
- foreach ($queue->members as $member) {
- // Dequeue the first member and fetch the Forward twimlet for that
- // member.
- $member->dequeue('http://twimlets.com/forward', 'GET');
- break;
- }
-
-Retrieve the Member at the Front of a Queue
-===========================================
-
-The Members class has a method called ``front`` which can be used to retrieve
-the member at the front of the queue.
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '123');
- $queue = $client->account->queues->get('QQ123');
- $firstMember = $queue->members->front();
- echo $firstMember->position;
- echo $firstMember->call_sid;
-
diff --git a/externals/twilio-php/docs/usage/rest/messages.rst b/externals/twilio-php/docs/usage/rest/messages.rst
deleted file mode 100644
index e3bcff117..000000000
--- a/externals/twilio-php/docs/usage/rest/messages.rst
+++ /dev/null
@@ -1,50 +0,0 @@
-=============
-Messages
-=============
-
-Sending a Message
-=====================
-
-The :class:`Messages <Services_Twilio_Rest_Messages>` resource allows you to
-send outgoing SMS or MMS messages.
-
-.. code-block:: php
-
- require('/path/to/twilio-php/Services/Twilio.php');
-
- $client = new Services_Twilio('AC123', '123');
- $message = $client->account->messages->sendMessage(
- '+14085551234', // From a Twilio number in your account
- '+12125551234', // Text any number
- 'Hello monkey!', // Message body (if any)
- array('http://example.com/image.jpg'), // An array of MediaUrls
- );
-
- echo $message->sid;
-
-Listing Messages
-====================
-
-It's easy to iterate over your messages.
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '123');
- foreach ($client->account->messages as $message) {
- echo "From: {$message->from}\nTo: {$message->to}\nBody: " . $message->body;
- }
-
-Filtering Messages
-======================
-
-Let's say you want to find all of the messages that have been sent from
-a particular number. You can do so by constructing an iterator explicitly:
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '123');
- foreach ($client->account->messages->getIterator(0, 50, array(
- 'From' => '+14105551234',
- )) as $message) {
- echo "From: {$message->from}\nTo: {$message->to}\nBody: " . $message->body;
- }
diff --git a/externals/twilio-php/docs/usage/rest/notifications.rst b/externals/twilio-php/docs/usage/rest/notifications.rst
deleted file mode 100644
index 0df0c43fa..000000000
--- a/externals/twilio-php/docs/usage/rest/notifications.rst
+++ /dev/null
@@ -1,13 +0,0 @@
-===============
- Notifications
-===============
-
-Filter Notifications by Log Level
-=================================
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '123');
- foreach ($client->account->notifications->getIterator(0, 50, array("LogLevel" => 0)) as $n) {
- print $n->error_code;
- }
diff --git a/externals/twilio-php/docs/usage/rest/phonenumbers.rst b/externals/twilio-php/docs/usage/rest/phonenumbers.rst
deleted file mode 100644
index e929882ea..000000000
--- a/externals/twilio-php/docs/usage/rest/phonenumbers.rst
+++ /dev/null
@@ -1,187 +0,0 @@
-=================
- Phone Numbers
-=================
-
-Purchasing phone numbers is a two step process.
-
-Searching For a Number
-----------------------
-
-First, we need to search for an available phone number. Use the
-:php:meth:`Services_Twilio_Rest_AvailablePhoneNumbers::getList` method of the
-:php:class:`Services_Twilio_Rest_AvailablePhoneNumbers` list resource.
-
-.. code-block:: php
-
- $accountSid = 'AC1234567890abcdef1234567890a';
- $authToken = 'abcdef1234567890abcdefabcde9';
-
- $client = new Services_Twilio($accountSid, $authToken);
- $numbers = $client->account->available_phone_numbers->getList('US', 'TollFree');
- foreach($numbers->available_phone_numbers as $number) {
- echo 'Number: ' + $number->phone_number + "\n";
- }
-
-You can also pass in parameters to search for phone numbers in a certain area
-code, or which contain a certain pattern.
-
-.. code-block:: php
-
- $accountSid = 'AC1234567890abcdef1234567890a';
- $authToken = 'abcdef1234567890abcdefabcde9';
-
- $client = new Services_Twilio($accountSid, $authToken);
-
- // Full parameter documentation at http://www.twilio.com/docs/api/rest/available-phone-numbers#local
- $params = array('AreaCode' => '925', 'Contains' => 'hi');
- $numbers = $client->account->available_phone_numbers->getList('US', 'Local', $params);
- foreach($numbers->available_phone_numbers as $number) {
- echo 'Number: ' + $number->phone_number + "\n";
- }
-
-You can also use the type subresources to search for a given type.
-
-Available types include:
-- `local`
-- `toll_free`
-- `mobile`
-
-.. code-block:: php
-
- // Local
- $numbers = $client->account->available_phone_numbers->local;
- foreach($numbers as $number) {
- echo 'Number: ' + $number->phone_number + "\n";
- }
-
- // TollFree
- $numbers = $client->account->available_phone_numbers->toll_free;
- foreach($numbers as $number) {
- echo 'Number: ' + $number->phone_number + "\n";
- }
-
- // Mobile
- $numbers = $client->account->available_phone_numbers->mobile;
- foreach($numbers as $number) {
- echo 'Number: ' + $number->phone_number + "\n";
- }
-
-
-Buying a Number
----------------
-
-Once you have a phone number, purchase it by creating a new
-:php:class:`Services_Twilio_Rest_IncomingPhoneNumber` instance.
-
-.. code-block:: php
-
- $accountSid = 'AC1234567890abcdef1234567890a';
- $authToken = 'abcdef1234567890abcdefabcde9';
-
- $client = new Services_Twilio($accountSid, $authToken);
-
- $phoneNumber = '+44XXXYYYZZZZ';
- $purchasedNumber = $client->account->incoming_phone_numbers->create(array('PhoneNumber' => $phoneNumber));
-
- echo $purchasedNumber->sid;
-
-Tying the two together, you can search for a number, and then purchase it.
-
-.. code-block:: php
-
- $accountSid = 'AC1234567890abcdef1234567890a';
- $authToken = 'abcdef1234567890abcdefabcde9';
-
- $client = new Services_Twilio($accountSid, $authToken);
-
- // Full parameter documentation at http://www.twilio.com/docs/api/rest/available-phone-numbers#local
- $params = array('AreaCode' => '800', 'Contains' => 'hi');
-
- $numbers = $client->account->available_phone_numbers->getList('CA', 'TollFree', $params);
- $firstNumber = $numbers->available_phone_numbers[0]->phone_number;
- $purchasedNumber = $client->account->incoming_phone_numbers->create(array('PhoneNumber' => $firstNumber));
-
- echo $purchasedNumber->sid;
-
-You can also purchase a random number with a given area code (US/Canada only):
-
-.. code-block:: php
-
- $accountSid = 'AC1234567890abcdef1234567890a';
- $authToken = 'abcdef1234567890abcdefabcde9';
-
- $client = new Services_Twilio($accountSid, $authToken);
- $purchasedNumber = $client->account->incoming_phone_numbers->create(array('AreaCode' => '925'));
-
- echo $purchasedNumber->sid;
-
-Retrieving All of a Number's Properties
----------------------------------------
-
-If you know the number and you want to retrieve all of the properties of that
-number, such as the ``voice_url`` or the ``sms_method``, you can use the
-:php:meth:`Services_Twilio_Rest_IncomingPhoneNumbers::getNumber` convenience
-function.
-
-.. code-block:: php
-
- $accountSid = 'AC1234567890abcdef1234567890a';
- $authToken = 'abcdef1234567890abcdefabcde9';
-
- $client = new Services_Twilio($accountSid, $authToken);
-
- // Number must be in e.164 format.
- $number = $client->account->incoming_phone_numbers->getNumber('+14105551234');
- echo $number->voice_url;
-
-If you know the ``sid`` of a phone number, you can retrieve it using the
-``get()`` function.
-
-.. code-block:: php
-
- $accountSid = 'AC1234567890abcdef1234567890a';
- $authToken = 'abcdef1234567890abcdefabcde9';
-
- $client = new Services_Twilio($accountSid, $authToken);
-
- $number = $client->account->incoming_phone_numbers->get('PN123456');
- echo $number->voice_url;
-
-Updating a Number
------------------
-
-You can easily update any of the properties of your
-phone number. A full list of parameters is available
-in the `Incoming Phone Number REST API Documentation.
-<http://www.twilio.com/docs/api/rest/incoming-phone-numbers#instance-post>`_
-
-.. code-block:: php
-
- $accountSid = 'AC1234567890abcdef1234567890a';
- $authToken = 'abcdef1234567890abcdefabcde9';
-
- $client = new Services_Twilio($accountSid, $authToken);
- $numbers = $client->account->incoming_phone_numbers;
- foreach ($numbers as $number) {
- $number->update(array('VoiceMethod' => 'POST'));
- }
-
-Deleting a Number
------------------
-
-You can delete numbers by specifying the Sid of the phone number you'd like to
-delete, from the incoming phone numbers object.
-
-.. code-block:: php
-
- $accountSid = 'AC1234567890abcdef1234567890a';
- $authToken = 'abcdef1234567890abcdefabcde9';
-
- $client = new Services_Twilio($accountSid, $authToken);
- $numbers = $client->account->incoming_phone_numbers;
- foreach($numbers as $number) {
- // Delete just the first number, then quit.
- $client->account->incoming_phone_numbers->delete($number->sid);
- break;
- }
-
diff --git a/externals/twilio-php/docs/usage/rest/queues.rst b/externals/twilio-php/docs/usage/rest/queues.rst
deleted file mode 100644
index f75f3334c..000000000
--- a/externals/twilio-php/docs/usage/rest/queues.rst
+++ /dev/null
@@ -1,56 +0,0 @@
-=============
-Queues
-=============
-
-Create a New Queue
-=====================
-
-To create a new queue, make an HTTP POST request to the Queues resource.
-
-.. code-block:: php
-
- require('/path/to/twilio-php/Services/Twilio.php');
-
- $client = new Services_Twilio('AC123', '123');
- // Default MaxSize is 100. Or change it by adding a parameter, like so
- $queue = $client->account->queues->create('First Queue',
- array('MaxSize' => 10));
-
- print $queue->sid;
- print $queue->friendly_name;
-
-Listing Queues
-====================
-
-It's easy to iterate over your list of queues.
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '123');
- foreach ($client->account->queues as $queue) {
- echo $queue->sid;
- }
-
-Deleting Queues
-====================
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '123');
- $queue_sid = 'QQ123';
- $client->account->queues->delete('QQ123');
-
-Retrieve the Member at the Front of a Queue
-===========================================
-
-The Members class has a method called ``front`` which can be used to retrieve
-the member at the front of the queue.
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '123');
- $queue = $client->account->queues->get('QQ123');
- $firstMember = $queue->members->front();
- echo $firstMember->position;
- echo $firstMember->call_sid;
-
diff --git a/externals/twilio-php/docs/usage/rest/recordings.rst b/externals/twilio-php/docs/usage/rest/recordings.rst
deleted file mode 100644
index 69cce543b..000000000
--- a/externals/twilio-php/docs/usage/rest/recordings.rst
+++ /dev/null
@@ -1,61 +0,0 @@
-==========
-Recordings
-==========
-
-Listing Recordings
-------------------
-
-Run the following to get a list of all of your recordings:
-
-.. code-block:: php
-
- $accountSid = 'AC1234567890abcdef1234567890a';
- $authToken = 'abcdef1234567890abcdefabcde9';
- $client = new Services_Twilio($accountSid, $authToken);
-
- foreach($client->account->recordings as $recording) {
- echo "Access recording {$recording->sid} at:" . "\n";
- echo $recording->uri;
- }
-
-For more information about which properties are available for a recording
-object, please see the `Twilio Recordings API Documentation <http://www.twilio.com/docs/api/rest/recording>`_.
-
-Please note that the ``uri`` returned by default is a JSON dictionary
-containing metadata about the recording; you can access the .wav version by
-stripping the ``.json`` extension from the ``uri`` returned by the library.
-
-Filtering Recordings By Call Sid
---------------------------------
-
-Pass filters as an array to filter your list of recordings, with any of the
-filters listed in the `recording list documentation <http://www.twilio.com/docs/api/rest/recording#list-get-filters>`_.
-
-.. code-block:: php
-
- $accountSid = 'AC1234567890abcdef1234567890a';
- $authToken = 'abcdef1234567890abcdefabcde9';
- $client = new Services_Twilio($accountSid, $authToken);
-
- foreach($client->account->recordings->getIterator(0, 50, array('DateCreated>' => '2011-01-01')) as $recording) {
- echo $recording->uri . "\n";
- }
-
-Deleting a Recording
---------------------
-
-To delete a recording, get the sid of the recording, and then pass it to the
-client.
-
-.. code-block:: php
-
- $accountSid = 'AC1234567890abcdef1234567890a';
- $authToken = 'abcdef1234567890abcdefabcde9';
- $client = new Services_Twilio($accountSid, $authToken);
-
- foreach($client->account->recordings as $recording) {
- $client->account->recordings->delete($recording->sid);
- echo "Deleted recording {$recording->sid}, the first one in the list.";
- break;
- }
-
diff --git a/externals/twilio-php/docs/usage/rest/sip.rst b/externals/twilio-php/docs/usage/rest/sip.rst
deleted file mode 100644
index 5a9cfeee8..000000000
--- a/externals/twilio-php/docs/usage/rest/sip.rst
+++ /dev/null
@@ -1,88 +0,0 @@
-=============
-Sip In
-=============
-
-Getting started with Sip
-==========================
-
-If you're unfamiliar with SIP, please see the `SIP API Documentation
-<https://www.twilio.com/docs/api/rest/sip>`_ on our website.
-
-Creating a Sip Domain
-=========================
-
-The :class:`Domains <Services_Twilio_Rest_Domains>` resource allows you to
-create a new domain. To create a new domain, you'll need to choose a unique
-domain that lives under sip.twilio.com. For example, doug.sip.twilio.com.
-
-.. code-block:: php
-
- require('/path/to/twilio-php/Services/Twilio.php');
-
- $client = new Services_Twilio('AC123', '123');
- $domain = $client->account->sip->domains->create(
- "Doug's Domain", // The FriendlyName for your new domain
- "doug.sip.twilio.com", // The sip domain for your new domain
- array(
- 'VoiceUrl' => 'http://example.com/voice',
- ));
-
- echo $domain->sid;
-
-Creating a new IpAccessControlList
-====================================
-
-To control access to your new domain, you'll need to explicitly grant access
-to individual ip addresses. To do this, you'll first need to create an
-:class:`IpAccessControlList <Services_Twilio_Rest_IpAccessControlList>` to hold
-the ip addresses you wish to allow.
-
-.. code-block:: php
-
- require('/path/to/twilio-php/Services/Twilio.php');
-
- $client = new Services_Twilio('AC123', '123');
- $ip_access_control_list = $client->account->sip->ip_access_control_lists->create(
- "Doug's IpAccessControlList", // The FriendlyName for your new ip access control list
- );
-
- echo $ip_access_control_list->sid;
-
-Adding an IpAddress to an IpAccessControlList
-==============================================
-
-Now it's time to add an :class:`IpAddress
-<Services_Twilio_Rest_IpAddress>` to your new :class:`IpAccessControlList
-<Services_Twilio_Rest_IpAccessControlList>`.
-
-.. code-block:: php
-
- require('/path/to/twilio-php/Services/Twilio.php');
-
- $client = new Services_Twilio('AC123', '123');
- $ip_address = $client->account->sip->ip_access_control_lists->get('AC123')->ip_addresses->create(
- "Doug's IpAddress", // The FriendlyName for this IpAddress
- '127.0.0.1', // The ip address for this IpAddress
- );
-
- echo $ip_address->sid;
-
-Adding an IpAccessControlList to a Domain
-===========================================
-
-Once you've created a :class:`Domain <Services_Twilio_Rest_Domain>` and an
-:class:`IpAccessControlList <Services_Twilio_Rest_IpAccessControlList>`
-you need to associate them. To do this,
-create an :class:`IpAccessControlListMapping
-<Services_Twilio_Rest_IpAccessControlListMapping>`.
-
-.. code-block:: php
-
- require('/path/to/twilio-php/Services/Twilio.php');
-
- $client = new Services_Twilio('AC123', '123');
- $ip_access_control_list_mapping = $client->account->sip->domains->get('SD123')->ip_access_control_list_mappings->create(
- 'AL123', // The sid of your IpAccessControlList
- );
-
- echo $ip_access_control_list_mapping->sid;
diff --git a/externals/twilio-php/docs/usage/rest/sms-messages.rst b/externals/twilio-php/docs/usage/rest/sms-messages.rst
deleted file mode 100644
index c1ccffe2f..000000000
--- a/externals/twilio-php/docs/usage/rest/sms-messages.rst
+++ /dev/null
@@ -1,50 +0,0 @@
-=============
-SMS Messages
-=============
-
-Sending a SMS Message
-=====================
-
-
-The :php:class:`Services_Twilio_Rest_SmsMessages` resource allows you to send
-outgoing text messages.
-
-.. code-block:: php
-
- require('/path/to/twilio-php/Services/Twilio.php');
-
- $client = new Services_Twilio('AC123', '123');
- $message = $client->account->sms_messages->create(
- '+14085551234', // From a Twilio number in your account
- '+12125551234', // Text any number
- "Hello monkey!"
- );
-
- print $message->sid;
-
-Listing SMS Messages
-====================
-
-It's easy to iterate over your SMS messages.
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '123');
- foreach ($client->account->sms_messages as $message) {
- echo "From: {$message->from}\nTo: {$message->to}\nBody: " . $message->body;
- }
-
-Filtering SMS Messages
-======================
-
-Let's say you want to find all of the SMS messages that have been sent from
-a particular number. You can do so by constructing an iterator explicitly:
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '123');
- foreach ($client->account->sms_messages->getIterator(0, 50, array(
- 'From' => '+14105551234',
- )) as $message) {
- echo "From: {$message->from}\nTo: {$message->to}\nBody: " . $message->body;
- }
diff --git a/externals/twilio-php/docs/usage/rest/transcriptions.rst b/externals/twilio-php/docs/usage/rest/transcriptions.rst
deleted file mode 100644
index dfe38d603..000000000
--- a/externals/twilio-php/docs/usage/rest/transcriptions.rst
+++ /dev/null
@@ -1,13 +0,0 @@
-================
-Transcriptions
-================
-
-Show all Transcribed Messages
-=============================
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '123');
- foreach ($client->account->transcriptions as $t) {
- print $t->transcription_text;
- }
diff --git a/externals/twilio-php/docs/usage/rest/usage-records.rst b/externals/twilio-php/docs/usage/rest/usage-records.rst
deleted file mode 100644
index bf33836ab..000000000
--- a/externals/twilio-php/docs/usage/rest/usage-records.rst
+++ /dev/null
@@ -1,91 +0,0 @@
-=============
-Usage Records
-=============
-
-Twilio offers a Usage Record API so you can better measure how much you've been
-using Twilio. Here are some examples of how you can use PHP to access the usage
-API.
-
-Retrieve All Usage Records
-==========================
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '456bef');
- foreach ($client->account->usage_records as $record) {
- echo "Record: $record";
- }
-
-Retrieve Usage Records For A Time Interval
-==========================================
-
-UsageRecords support `several convenience subresources
-<http://www.twilio.com/docs/api/rest/usage-records#list-subresources>`_ that
-can be accessed as properties on the `record` object.
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '456bef');
- foreach ($client->account->usage_records->last_month as $record) {
- echo "Record: $record";
- }
-
-Retrieve All Time Usage for A Usage Category
-============================================
-
-By default, Twilio will return your all-time usage for a given usage category.
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '456bef');
- $callRecord = $client->account->usage_records->getCategory('calls');
- echo $callRecord->usage;
-
-Retrieve All Usage for a Given Time Period
-==========================================
-
-You can filter your UsageRecord list by providing `StartDate` and `EndDate`
-parameters.
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '456bef');
- foreach ($client->account->usage_records->getIterator(0, 50, array(
- 'StartDate' => '2012-08-01',
- 'EndDate' => '2012-08-31',
- )) as $record) {
- echo $record->description . "\n";
- echo $record->usage . "\n";
- }
-
-Retrieve Today's SMS Usage
-==========================
-
-You can use the `today` record subresource, and then retrieve the record
-directly with the `getCategory` function.
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '456bef');
- // You can substitute 'yesterday', 'all_time' for 'today' below
- $smsRecord = $client->account->usage_records->today->getCategory('sms');
- echo $smsRecord->usage;
-
-Retrieve Daily Usage Over a One-Month Period
-=============================================
-
-The code below will retrieve daily summaries of recordings usage for August
-2012. To retrieve all categories of usage, remove the 'Category' filter from
-the `getIterator` array.
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '456bef');
- foreach ($client->account->usage_records->daily->getIterator(0, 50, array(
- 'StartDate' => '2012-08-01',
- 'EndDate' => '2012-08-31',
- 'Category' => 'recordings',
- )) as $record) {
- echo $record->usage;
- }
-
diff --git a/externals/twilio-php/docs/usage/rest/usage-triggers.rst b/externals/twilio-php/docs/usage/rest/usage-triggers.rst
deleted file mode 100644
index 18cce82e9..000000000
--- a/externals/twilio-php/docs/usage/rest/usage-triggers.rst
+++ /dev/null
@@ -1,92 +0,0 @@
-==============
-Usage Triggers
-==============
-
-Twilio offers a Usage Trigger API so you can get notifications when your Twilio
-usage exceeds a given level. Here are some examples of how you can
-use PHP to create new usage triggers or modify existing triggers.
-
-Retrieve A Usage Trigger's Properties
-=====================================
-
-If you know the Sid of your usage trigger, retrieving it is easy.
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '456bef');
- $usageSid = 'UT123';
- $usageTrigger = $client->account->usage_triggers->get($usageSid);
- echo $usageTrigger->usage_category;
-
-Update Properties on a UsageTrigger
-===================================
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '456bef');
- $usageSid = 'UT123';
- $usageTrigger = $client->account->usage_triggers->get($usageSid);
- $usageTrigger->update(array(
- 'FriendlyName' => 'New usage trigger friendly name',
- 'CallbackUrl' => 'http://example.com/new-trigger-url',
- ));
-
-Retrieve All Triggers
-=====================
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '456bef');
- foreach ($client->account->usage_triggers as $trigger) {
- echo "Category: {$trigger->usage_category}\nTriggerValue: {$trigger->trigger_value}\n";
- }
-
-Filter Trigger List By Category
-===============================
-
-Pass filters to the `getIterator` function to create a filtered list.
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '456bef');
- foreach ($client->account->usage_triggers->getIterator(
- 0, 50, array(
- 'UsageCategory' => 'sms',
- )) as $trigger
- ) {
- echo "Value: " . $trigger->trigger_value . "\n";
- }
-
-Create a New Trigger
-====================
-
-Pass a usage category, a value and a callback URL to the `create` method.
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '456bef');
- $trigger = $client->account->usage_triggers->create(
- 'totalprice',
- '250.75',
- 'http://example.com/usage'
- );
-
-Create a Recurring Trigger
-==========================
-
-To have your trigger reset once every day, month, or year, pass the
-`Recurring` key as part of the params array. A list of optional
-trigger parameters can be found in the `Usage Triggers Documentation
-<http://www.twilio.com/docs/api/rest/usage-triggers#list-post-optional-paramete
-rs>`_.
-
-.. code-block:: php
-
- $client = new Services_Twilio('AC123', '456bef');
- $trigger = $client->account->usage_triggers->create(
- 'totalprice',
- '250.75',
- 'http://example.com/usage',
- array('Recurring' => 'monthly', 'TriggerBy' => 'price')
- );
-
diff --git a/externals/twilio-php/docs/usage/token-generation.rst b/externals/twilio-php/docs/usage/token-generation.rst
deleted file mode 100644
index 6a2698120..000000000
--- a/externals/twilio-php/docs/usage/token-generation.rst
+++ /dev/null
@@ -1,64 +0,0 @@
-===========================
-Generate Capability Tokens
-===========================
-
-`Twilio Client <http://www.twilio.com/api/client>`_ allows you to make and recieve connections in the browser. You can place a call to a phone on the PSTN network, all without leaving your browser. See the `Twilio Client Quickstart <http:/www.twilio.com/docs/quickstart/client>`_ to get up and running with Twilio Client.
-
-Capability tokens are used by `Twilio Client <http://www.twilio.com/api/client>`_ to provide connection security and authorization. The `Capability Token documentation <http://www.twilio.con/docs/tokens>`_ explains indepth the purpose and features of these tokens.
-
-:php:class:`Services_Twilio_Capability` is responsible for the creation of these capability tokens. You'll need your Twilio AccountSid and AuthToken.
-
-.. code-block:: php
-
- require('/path/to/twilio-php/Services/Twilio/Capability.php');
-
- $accountSid = "AC123123";
- $authToken = "secret";
-
- $capability = new Services_Twilio_Capability($accountSid, $authToken);
-
-
-Allow Incoming Connections
-==============================
-
-Before a device running `Twilio Client <http://www.twilio.com/api/client>`_ can recieve incoming connections, the instance must first register a name (such as "Alice" or "Bob"). The :php:meth:`allowCclientIncoming` method adds the client name to the capability token.
-
-.. code-block:: php
-
- $capability->allowClientIncoming("Alice");
-
-
-Allow Outgoing Connections
-==============================
-
-To make an outgoing connection from a `Twilio Client <http://www.twilio.com/api/client>`_ device, you'll need to choose a `Twilio Application <http://www.twilio.com/docs/api/rest/applications>`_ to handle TwiML URLs. A Twilio Application is a collection of URLs responsible for outputing valid TwiML to control phone calls and SMS.
-
-.. code-block:: php
-
- $applicationSid = "AP123123"; // Twilio Application Sid
- $capability->allowClientOutgoing($applicationSid);
-
-:php:meth:`allowClientOutgoing` accepts an optional array of parameters. These parameters will be passed along when Twilio requests TwiML from the application.
-
-.. code-block:: php
-
- $applicationSid = "AP123123"; // Twilio Application Sid
- $params = array("Foo" => "Bar"); // Parameters to be passed
- $capability->allowClientOutgoing($applicationSid, $params);
-
-
-Generate a Token
-==================
-
-.. code-block:: php
-
- $token = $capability->generateToken();
-
-By default, this token will expire in one hour. If you'd like to change the token expiration time, :php:meth:`generateToken` takes an optional argument which specifies `time to live` in seconds.
-
-.. code-block:: php
-
- $token = $capability->generateToken(600);
-
-This token will now expire in 10 minutes.
-
diff --git a/externals/twilio-php/docs/usage/twiml.rst b/externals/twilio-php/docs/usage/twiml.rst
deleted file mode 100644
index 892735c9f..000000000
--- a/externals/twilio-php/docs/usage/twiml.rst
+++ /dev/null
@@ -1,347 +0,0 @@
-.. _usage-twiml:
-
-==============
-TwiML Creation
-==============
-
-TwiML creation begins with the :class:`Services_Twilio_Twiml` verb. Each
-succesive verb is created by calling various methods on the response, such as
-:meth:`say` or :meth:`play`. These methods return the verbs they create to ease
-the creation of nested TwiML.
-
-.. code-block:: php
-
- $response = new Services_Twilio_Twiml;
- $response->say('Hello');
- print $response;
-
-.. code-block:: xml
-
- <?xml version="1.0" encoding="UTF-8"?>
- <Response>
- <Say>Hello</Say>
- <Response>
-
-Primary Verbs
-=============
-
-Response
---------
-
-All TwiML starts with the `<Response>` verb. The following code creates an empty response.
-
-.. code-block:: php
-
- $response = new Services_Twilio_Twiml;
- print $response;
-
-.. code-block:: xml
-
- <?xml version="1.0" encoding="UTF-8"?>
- <Response></Response>
-
-Say
----
-
-.. code-block:: php
-
- $response = new Services_Twilio_Twiml;
- $response->say("Hello World");
- print $response;
-
-.. code-block:: xml
-
- <?xml version="1.0" encoding="UTF-8"?>
- <Response>
- <Say>Hello World</Say>
- </Response>
-
-Play
-----
-
-.. code-block:: php
-
- $response = new Services_Twilio_Twiml;
- $response->play("https://api.twilio.com/cowbell.mp3", array('loop' => 5));
- print $response;
-
-.. code-block:: xml
-
- <?xml version="1.0" encoding="UTF-8"?>
- <Response>
- <Play loop="5">https://api.twilio.com/cowbell.mp3</Play>
- <Response>
-
-Gather
-------
-
-.. code-block:: php
-
- $response = new Services_Twilio_Twiml;
- $gather = $response->gather(array('numDigits' => 5));
- $gather->say("Hello Caller");
- print $response;
-
-.. code-block:: xml
-
- <?xml version="1.0" encoding="UTF-8"?>
- <Response>
- <Gather numDigits="5">
- <Say>Hello Caller</Say>
- </Gather>
- <Response>
-
-Record
-------
-
-.. code-block:: php
-
- $response = new Services_Twilio_Twiml;
- $response->record(array(
- 'action' => 'http://foo.com/path/to/redirect',
- 'maxLength' => 20
- ));
- print $response;
-
-.. code-block:: xml
-
- <?xml version="1.0" encoding="UTF-8"?>
- <Response>
- <Record action="http://foo.com/path/to/redirect" maxLength="20"/>
- </Response>
-
-Message
--------
-
-.. code-block:: php
-
- $response = new Services_Twilio_Twiml;
- $response->message('Hello World', array(
- 'to' => '+14150001111',
- 'from' => '+14152223333'
- ));
- print $response;
-
-.. code-block:: xml
-
- <?xml version="1.0" encoding="UTF-8"?>
- <Response>
- <Message to="+14150001111" from="+14152223333">Hello World</Message>
- </Response>
-
-Dial
-----
-
-.. code-block:: php
-
- $response = new Services_Twilio_Twiml;
- $response->dial('+14150001111', array(
- 'callerId' => '+14152223333'
- ));
- print $response;
-
-.. code-block:: xml
-
- <?xml version="1.0" encoding="UTF-8"?>
- <Response>
- <Dial callerId="+14152223333">+14150001111</Dial>
- </Response>
-
-Number
-~~~~~~
-
-Dial out to phone numbers easily.
-
-.. code-block:: php
-
- $response = new Services_Twilio_Twiml;
- $dial = $response->dial(NULL, array(
- 'callerId' => '+14152223333'
- ));
- $dial->number('+14151112222', array(
- 'sendDigits' => '2'
- ));
- print $response;
-
-.. code-block:: xml
-
- <?xml version="1.0" encoding="UTF-8"?>
- <Response>
- <Dial callerId="+14152223333">
- <Number sendDigits="2">+14151112222</Number>
- </Dial>
- </Response>
-
-Client
-~~~~~~
-
-.. code-block:: php
-
- $response = new Services_Twilio_Twiml;
- $dial = $response->dial(NULL, array(
- 'callerId' => '+14152223333'
- ));
- $dial->client('client-id');
- print $response;
-
-.. code-block:: xml
-
- <?xml version="1.0" encoding="UTF-8"?>
- <Response>
- <Dial callerId="+14152223333">
- <Client>client-id</Client>
- </Dial>
- </Response>
-
-Conference
-~~~~~~~~~~
-
-.. code-block:: php
-
- require("Services/Twilio.php");
- $response = new Services_Twilio_Twiml;
- $dial = $response->dial();
- $dial->conference('Customer Waiting Room', array(
- "startConferenceOnEnter" => "true",
- "muted" => "true",
- "beep" => "false",
- ));
- print $response;
-
-.. code-block:: xml
-
- <?xml version="1.0" encoding="UTF-8"?>
- <Response>
- <Dial>
- <Conference startConferenceOnEnter="true" muted="true" beep="false">
- Customer Waiting Room
- </Conference>
- </Dial>
- </Response>
-
-Sip
-~~~
-
-To dial out to a Sip number, put the Sip address in the `sip()` method call.
-
-.. code-block:: php
-
- require("Services/Twilio.php");
- $response = new Services_Twilio_Twiml;
- $dial = $response->dial();
- $sip = $dial->sip();
- $sip->uri('alice@foo.com?X-Header-1=value1&X-Header-2=value2', array(
- "username" => "admin",
- "password" => "1234",
- ));
- print $response;
-
-.. code-block:: xml
-
- <?xml version="1.0" encoding="UTF‐8"?>
- <Response>
- <Dial>
- <Sip>
- <Uri username='admin' password='1234'>
- alice@foo.com?X-Header-1=value1&X-Header-2=value2
- </Uri>
- </Sip>
- </Dial>
- </Response>
-
-
-Secondary Verbs
-===============
-
-Hangup
-------
-
-.. code-block:: php
-
- $response = new Services_Twilio_Twiml;
- $response->hangup();
- print $response;
-
-.. code-block:: xml
-
- <?xml version="1.0" encoding="UTF-8"?>
- <Response>
- <Hangup />
- </Response>
-
-Redirect
---------
-
-.. code-block:: php
-
- $response = new Services_Twilio_Twiml;
- $response->redirect('http://twimlets.com/voicemail?Email=somebody@somedomain.com');
- print $response;
-
-.. code-block:: xml
-
- <?xml version="1.0" encoding="UTF-8"?>
- <Response>
- <Redirect>http://twimlets.com/voicemail?Email=somebody@somedomain.com</Redirect>
- </Response>
-
-
-Reject
-------
-
-.. code-block:: php
-
- $response = new Services_Twilio_Twiml;
- $response->reject(array(
- 'reason' => 'busy'
- ));
- print $response;
-
-.. code-block:: xml
-
- <?xml version="1.0" encoding="UTF-8"?>
- <Response>
- <Reject reason="busy" />
- </Response>
-
-
-Pause
------
-
-.. code-block:: php
-
- $response = new Services_Twilio_Twiml;
- $response->say('Hello');
- $response->pause("");
- $response->say('World');
- print $response;
-
-.. code-block:: xml
-
- <?xml version="1.0" encoding="UTF-8"?>
- <Response>
- <Say>Hello</Say>
- <Pause />
- <Say>World</Say>
- </Response>
-
-Enqueue
--------
-
-.. code-block:: php
-
- $response = new Services_Twilio_Twiml;
- $response->say("You're being added to the queue.");
- $response->enqueue('queue-name');
- print $response;
-
-.. code-block:: xml
-
- <?xml version="1.0" encoding="UTF-8"?>
- <Response>
- <Say>You're being added to the queue.</Say>
- <Enqueue>queue-name</Enqueue>
- </Response>
-
-The verb methods (outlined in the complete reference) take the body (only text)
-of the verb as the first argument. All attributes are keyword arguments.
diff --git a/externals/twilio-php/docs/usage/validation.rst b/externals/twilio-php/docs/usage/validation.rst
deleted file mode 100644
index b361f3b24..000000000
--- a/externals/twilio-php/docs/usage/validation.rst
+++ /dev/null
@@ -1,66 +0,0 @@
-===========================
-Validate Incoming Requests
-===========================
-
-Twilio requires that your TwiML-serving web server be open to the public. This is necessary so that Twilio can retrieve TwiML from urls and POST data back to your server.
-
-However, there may be people out there trying to spoof the Twilio service. Luckily, there's an easy way to validate that incoming requests are from Twilio and Twilio alone.
-
-An `indepth guide <http://www.twilio.com/docs/security>`_ to our security features can be found in our online documentation.
-
-Before you can validate requests, you'll need four pieces of information
-
-* your Twilio Auth Token
-* the POST data for the request
-* the requested URL
-* the X-Twilio-Signature header value
-
-Get your Auth Token from the `Twilio User Dashboard <https://www.twilio.com/user/account>`_.
-
-Obtaining the other three pieces of information depends on the framework of your choosing. I will assume that you have the POST data as an array and the url and X-Twilio-Signature as strings.
-
-The below example will print out a confirmation message if the request is actually from Twilio.com
-
-.. code-block:: php
-
- // Your auth token from twilio.com/user/account
- $authToken = '12345';
-
- // Download the twilio-php library from twilio.com/docs/php/install, include it
- // here
- require_once('/path/to/twilio-php/Services/Twilio.php');
- $validator = new Services_Twilio_RequestValidator($authToken);
-
- // The Twilio request URL. You may be able to retrieve this from
- // $_SERVER['SCRIPT_URI']
- $url = 'https://mycompany.com/myapp.php?foo=1&bar=2';
-
- // The post variables in the Twilio request. You may be able to use
- // $postVars = $_POST
- $postVars = array(
- 'CallSid' => 'CA1234567890ABCDE',
- 'Caller' => '+14158675309',
- 'Digits' => '1234',
- 'From' => '+14158675309',
- 'To' => '+18005551212'
- );
-
- // The X-Twilio-Signature header - in PHP this should be
- // $_SERVER["HTTP_X_TWILIO_SIGNATURE"];
- $signature = 'RSOYDt4T1cUTdK1PDd93/VVr8B8=';
-
- if ($validator->validate($signature, $url, $postVars)) {
- echo "Confirmed to have come from Twilio.";
- } else {
- echo "NOT VALID. It might have been spoofed!";
- }
-
-Trailing Slashes
-==================
-
-If your URL uses an "index" page, such as index.php or index.html to handle the request, such as: https://mycompany.com/twilio where the real page is served from https://mycompany.com/twilio/index.php, then Apache or PHP may rewrite that URL a little bit so it's got a trailing slash... https://mycompany.com/twilio/ for example.
-
-Using the code above, or similar code in another language, you could end up with an incorrect hash because, Twilio built the hash using https://mycompany.com/twilio and you may have built the hash using https://mycompany.com/twilio/.
-
-
-
diff --git a/externals/twilio-php/package.php b/externals/twilio-php/package.php
deleted file mode 100644
index 4f30b365a..000000000
--- a/externals/twilio-php/package.php
+++ /dev/null
@@ -1,117 +0,0 @@
-<?php
-
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
-
-/**
- * This is the package.xml generator for Services_Twilio
- *
- * PHP version 5
- *
- * LICENSE:
- *
- * Copyright 2014 Twilio.
- *
- * 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.
- *
- * @category Services
- * @package Services_Twilio
- * @author Neuman Vong <neuman@twilio.com>
- * @copyright 2014 Twilio
- * @license http://creativecommons.org/licenses/MIT/
- * @link http://pear.php.net/package/Services_Twilio
- */
-
-ini_set('display_errors', '0');
-error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT);
-require_once 'PEAR/PackageFileManager/File.php';
-require_once 'PEAR/PackageFileManager2.php';
-PEAR::setErrorHandling(PEAR_ERROR_DIE);
-
-$api_version = '3.12.4';
-$api_state = 'stable';
-
-$release_version = '3.12.4';
-$release_state = 'stable';
-$release_notes = 'Add transcription link to recordings';
-
-$description = <<<DESC
-A SDK (or helper library, as we're calling them) for PHP developers to write
-applications against Twilio's REST API and generate TwiML responses.
-DESC;
-
-$package = new PEAR_PackageFileManager2();
-
-$package->setOptions(
- array(
- 'filelistgenerator' => 'file',
- 'simpleoutput' => true,
- 'baseinstalldir' => '/',
- 'packagedirectory' => './',
- 'dir_roles' => array(
- 'Services' => 'php',
- 'Services/Twilio' => 'php',
- 'tests' => 'test'
- ),
- 'ignore' => array(
- 'package.php',
- '*.tgz',
- 'scratch/*',
- 'vendor/*',
- 'composer.*',
- 'coverage/*',
- '.travis.yml',
- 'venv/*',
- )
- )
-);
-
-$package->setPackage('Services_Twilio');
-$package->setSummary('PHP helper library for Twilio');
-$package->setDescription($description);
-$package->setChannel('twilio.github.com/pear');
-$package->setPackageType('php');
-$package->setLicense(
- 'MIT License',
- 'http://creativecommons.org/licenses/MIT/'
-);
-
-$package->setNotes($release_notes);
-$package->setReleaseVersion($release_version);
-$package->setReleaseStability($release_state);
-$package->setAPIVersion($api_version);
-$package->setAPIStability($api_state);
-
-$package->addMaintainer(
- 'lead',
- 'kevinburke',
- 'Kevin Burke',
- 'kevin@twilio.com'
-);
-
-
-$package->setPhpDep('5.2.1');
-
-$package->addPackageDepWithChannel('optional', 'Mockery', 'pear.survivethedeepend.com');
-
-$package->setPearInstallerDep('1.9.3');
-$package->generateContents();
-$package->addRelease();
-
-if (isset($_GET['make'])
- || (isset($_SERVER['argv']) && @$_SERVER['argv'][1] == 'make')
-) {
- $package->writePackageFile();
-} else {
- $package->debugPackageFile();
-}
-
diff --git a/externals/twilio-php/tests/Bootstrap.php b/externals/twilio-php/tests/Bootstrap.php
deleted file mode 100644
index e01b873e0..000000000
--- a/externals/twilio-php/tests/Bootstrap.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-error_reporting(E_ALL | E_STRICT);
-ini_set('display_errors', 1);
-
-$root = realpath(dirname(dirname(__FILE__)));
-$library = "$root/Services";
-$tests = "$root/tests";
-
-$path = array($library, $tests, get_include_path());
-set_include_path(implode(PATH_SEPARATOR, $path));
-
-$vendorFilename = dirname(__FILE__) . '/../vendor/autoload.php';
-if (file_exists($vendorFilename)) {
- /* composer install */
- require $vendorFilename;
-} else {
- /* hope you have it installed somewhere. */
- require_once 'Mockery/Loader.php';
-}
-$loader = new \Mockery\Loader;
-$loader->register();
-
-require_once 'Twilio.php';
-
-unset($root, $library, $tests, $path);
-
diff --git a/externals/twilio-php/tests/BuildQueryTest.php b/externals/twilio-php/tests/BuildQueryTest.php
deleted file mode 100644
index 5140d537e..000000000
--- a/externals/twilio-php/tests/BuildQueryTest.php
+++ /dev/null
@@ -1,56 +0,0 @@
-<?php
-
-require_once 'Twilio.php';
-
-class BuildQueryTest extends PHPUnit_Framework_TestCase {
-
- public function testSimpleQueryString() {
- $data = array(
- 'foo' => 'bar',
- 'baz' => 'bin',
- );
-
- $this->assertEquals(Services_Twilio::buildQuery($data), 'foo=bar&baz=bin');
- }
-
- public function testSameKey() {
- $data = array(
- 'foo' => array(
- 'bar',
- 'baz',
- 'bin',
- ),
- 'boo' => 'bah',
- );
-
- $this->assertEquals(Services_Twilio::buildQuery($data),
- 'foo=bar&foo=baz&foo=bin&boo=bah');
- }
-
- public function testKeylessData() {
- $data = array(
- 'bar',
- 'baz',
- 'bin',
- );
-
- $this->assertEquals(Services_Twilio::buildQuery($data), '0=bar&1=baz&2=bin');
- }
-
- public function testKeylessDataPrefix() {
- $data = array(
- 'bar',
- 'baz',
- 'bin',
- );
-
- $this->assertEquals(Services_Twilio::buildQuery($data, 'var'), 'var0=bar&var1=baz&var2=bin');
- }
-
- public function testQualifiedUserAgent() {
- $expected = Services_Twilio::USER_AGENT . " (php 5.4)";
- $this->assertEquals(Services_Twilio::qualifiedUserAgent("5.4"), $expected);
- }
-
-}
-
diff --git a/externals/twilio-php/tests/CapabilityTest.php b/externals/twilio-php/tests/CapabilityTest.php
deleted file mode 100644
index 143145050..000000000
--- a/externals/twilio-php/tests/CapabilityTest.php
+++ /dev/null
@@ -1,106 +0,0 @@
-<?php
-
-require_once 'Twilio/Capability.php';
-
-class CapabilityTest extends PHPUnit_Framework_TestCase {
-
- public function testNoPermissions() {
- $token = new Services_Twilio_Capability('AC123', 'foo');
- $payload = JWT::decode($token->generateToken(), 'foo');
- $this->assertEquals($payload->iss, "AC123");
- $this->assertEquals($payload->scope, '');
- }
-
- public function testInboundPermissions() {
- $token = new Services_Twilio_Capability('AC123', 'foo');
- $token->allowClientIncoming("andy");
- $payload = JWT::decode($token->generateToken(), 'foo');
-
- $eurl = "scope:client:incoming?clientName=andy";
- $this->assertEquals($payload->scope, $eurl);
- }
-
- public function testOutboundPermissions() {
- $token = new Services_Twilio_Capability('AC123', 'foo');
- $token->allowClientOutgoing("AP123");
- $payload = JWT::decode($token->generateToken(), 'foo');;
- $eurl = "scope:client:outgoing?appSid=AP123";
- $this->assertContains($eurl, $payload->scope);
- }
-
- public function testOutboundPermissionsParams() {
- $token = new Services_Twilio_Capability('AC123', 'foo');
- $token->allowClientOutgoing("AP123", array("foobar" => 3));
- $payload = JWT::decode($token->generateToken(), 'foo');
-
- $eurl = "scope:client:outgoing?appSid=AP123&appParams=foobar%3D3";
- $this->assertEquals($payload->scope, $eurl);
- }
-
- public function testEvents() {
- $token = new Services_Twilio_Capability('AC123', 'foo');
- $token->allowEventStream();
- $payload = JWT::decode($token->generateToken(), 'foo');
-
- $event_uri = "scope:stream:subscribe?path=%2F2010"
- . "-04-01%2FEvents&params=";
- $this->assertEquals($payload->scope, $event_uri);
- }
-
- public function testEventsWithFilters() {
- $token = new Services_Twilio_Capability('AC123', 'foo');
- $token->allowEventStream(array("foobar" => "hey"));
- $payload = JWT::decode($token->generateToken(), 'foo');
-
- $event_uri = "scope:stream:subscribe?path=%2F2010-"
- . "04-01%2FEvents&params=foobar%3Dhey";
- $this->assertEquals($payload->scope, $event_uri);
- }
-
-
- public function testDecode() {
- $token = new Services_Twilio_Capability('AC123', 'foo');
- $token->allowClientOutgoing("AP123", array("foobar"=> 3));
- $token->allowClientIncoming("andy");
- $token->allowEventStream();
-
- $outgoing_uri = "scope:client:outgoing?appSid="
- . "AP123&appParams=foobar%3D3&clientName=andy";
- $incoming_uri = "scope:client:incoming?clientName=andy";
- $event_uri = "scope:stream:subscribe?path=%2F2010-04-01%2FEvents";
-
- $payload = JWT::decode($token->generateToken(), 'foo');
- $scope = $payload->scope;
-
- $this->assertContains($outgoing_uri, $scope);
- $this->assertContains($incoming_uri, $scope);
- $this->assertContains($event_uri, $scope);
- }
-
-
- function testDecodeWithAuthToken() {
- try {
- $token = new Services_Twilio_Capability('AC123', 'foo');
- $payload = JWT::decode($token->generateToken(), 'foo');
- $this->assertSame($payload->iss, 'AC123');
- } catch (UnexpectedValueException $e) {
- $this->assertTrue(false, "Could not decode with 'foo'");
- }
- }
-
- function testClientNameValidation() {
- $this->setExpectedException('InvalidArgumentException');
- $token = new Services_Twilio_Capability('AC123', 'foo');
- $token->allowClientIncoming('@');
- $this->fail('exception should have been raised');
- }
-
- function zeroLengthNameInvalid() {
- $this->setExpectedException('InvalidArgumentException');
- $token = new Services_Twilio_Capability('AC123', 'foo');
- $token->allowClientIncoming("");
- $this->fail('exception should have been raised');
- }
-
-
-}
diff --git a/externals/twilio-php/tests/README b/externals/twilio-php/tests/README
deleted file mode 100644
index 2392c3318..000000000
--- a/externals/twilio-php/tests/README
+++ /dev/null
@@ -1,3 +0,0 @@
-# To run the tests, navigate to the twilio-php home directory, then run:
-
-make test
diff --git a/externals/twilio-php/tests/RequestValidatorTest.php b/externals/twilio-php/tests/RequestValidatorTest.php
deleted file mode 100644
index e38a66898..000000000
--- a/externals/twilio-php/tests/RequestValidatorTest.php
+++ /dev/null
@@ -1,48 +0,0 @@
-<?php
-
-require_once 'Twilio/RequestValidator.php';
-
-class RequestValidatorTest extends PHPUnit_Framework_TestCase {
-
- function testRequestValidation() {
- $token = "1c892n40nd03kdnc0112slzkl3091j20";
- $validator = new Services_Twilio_RequestValidator($token);
-
- $uri = "http://www.postbin.org/1ed898x";
- $params = array(
- "CalledZip" => "94612",
- "AccountSid" => "AC9a9f9392lad99kla0sklakjs90j092j3",
- "ApiVersion" => "2010-04-01",
- "CallSid" => "CAd800bb12c0426a7ea4230e492fef2a4f",
- "CallStatus" => "ringing",
- "Called" => "+15306384866",
- "CalledCity" => "OAKLAND",
- "CalledCountry" => "US",
- "CalledState" => "CA",
- "Caller" => "+15306666666",
- "CallerCity" => "SOUTH LAKE TAHOE",
- "CallerCountry" => "US",
- "CallerName" => "CA Wireless Call",
- "CallerState" => "CA",
- "CallerZip" => "89449",
- "Direction" => "inbound",
- "From" => "+15306666666",
- "FromCity" => "SOUTH LAKE TAHOE",
- "FromCountry" => "US",
- "FromState" => "CA",
- "FromZip" => "89449",
- "To" => "+15306384866",
- "ToCity" => "OAKLAND",
- "ToCountry" => "US",
- "ToState" => "CA",
- "ToZip" => "94612",
- );
-
- $expected = "fF+xx6dTinOaCdZ0aIeNkHr/ZAA=";
-
- $this->assertEquals(
- $validator->computeSignature($uri, $params), $expected);
- $this->assertTrue($validator->validate($expected, $uri, $params));
- }
-
-}
diff --git a/externals/twilio-php/tests/TwilioTest.php b/externals/twilio-php/tests/TwilioTest.php
deleted file mode 100644
index 9108a9bbd..000000000
--- a/externals/twilio-php/tests/TwilioTest.php
+++ /dev/null
@@ -1,672 +0,0 @@
-<?php
-
-use \Mockery as m;
-
-class TwilioTest extends PHPUnit_Framework_TestCase {
-
- protected $formHeaders = array('Content-Type' => 'application/x-www-form-urlencoded');
- protected $callParams = array('To' => '123', 'From' => '123', 'Url' => 'http://example.com');
- protected $nginxError = array(500, array('Content-Type' => 'text/html'),
- '<html>Nginx 500 error</html>'
- );
-
- protected $pagingParams = array('Page' => '0', 'PageSize' => '10');
- function tearDown() {
- m::close();
- }
-
- function getClient($http) {
- return new Services_Twilio('AC123', '123', '2010-04-01', $http);
- }
-
- function createMockHttp($url, $method, $response, $params = null,
- $status = 200
- ) {
- $http = m::mock(new Services_Twilio_TinyHttp);
- if ($method === 'post') {
- $http->shouldReceive('post')->once()->with(
- "/2010-04-01/Accounts/AC123$url.json",
- $this->formHeaders,
- http_build_query($params)
- )->andReturn(array(
- $status,
- array('Content-Type' => 'application/json'),
- json_encode($response)
- )
- );
- } else {
- $query = empty($params) ? '' : '?' . http_build_query($params);
- $http->shouldReceive($method)->once()->with(
- "/2010-04-01/Accounts/AC123$url.json$query"
- )->andReturn(array(
- $status,
- array('Content-Type' => 'application/json'),
- json_encode($response)
- )
- );
- }
- return $http;
- }
-
- /**
- * @dataProvider uriTestProvider
- */
- function testRequestUriConstructedProperly($path, $params, $full_uri, $end_string) {
- $this->assertSame($end_string, Services_Twilio::getRequestUri(
- $path, $params, $full_uri
- ));
- }
-
- function uriTestProvider() {
- return array(
- array('/2010-04-01/Accounts', array('FriendlyName' => 'hi'), false,
- '/2010-04-01/Accounts.json?FriendlyName=hi'),
- array('/2010-04-01/Accounts', array(), false,
- '/2010-04-01/Accounts.json'),
- array('/2010-04-01/Accounts.json', array(), true,
- '/2010-04-01/Accounts.json'),
- array('/2010-04-01/Accounts.json', array('FriendlyName' => 'hi'), true,
- '/2010-04-01/Accounts.json'),
- array('/2010-04-01/Accounts', array(
- 'FriendlyName' => 'hi', 'foo' => 'bar'
- ), false, '/2010-04-01/Accounts.json?FriendlyName=hi&foo=bar'),
- );
- }
-
- function testNeedsRefining() {
- $http = $this->createMockHttp('', 'get', array(
- 'sid' => 'AC123',
- 'friendly_name' => 'Robert Paulson',
- )
- );
- $client = $this->getClient($http);
- $this->assertEquals('AC123', $client->account->sid);
- $this->assertEquals('Robert Paulson', $client->account->friendly_name);
- }
-
- function testAccessSidAvoidsNetworkCall() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('get')->never();
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $client->account->sid;
- }
-
- function testOnlyOneClientCreated() {
- $client = new Services_Twilio('AC123', '456');
- $client->account->client->sid = 'CL456';
- $this->assertSame('CL456', $client->account->sandbox->client->sid);
- }
-
- function testNullVersionReturnsNewest() {
- $client = new Services_Twilio('AC123', '123', null);
- $this->assertEquals('2010-04-01', $client->getVersion());
- $client = new Services_Twilio('AC123', '123', 'v1');
- $this->assertEquals('2010-04-01', $client->getVersion());
- $client = new Services_Twilio('AC123', '123', '2010-04-01');
- $this->assertEquals('2010-04-01', $client->getVersion());
- $client = new Services_Twilio('AC123', '123', '2008-08-01');
- $this->assertEquals('2008-08-01', $client->getVersion());
- }
-
- function testObjectLoadsOnlyOnce() {
- $http = $this->createMockHttp('', 'get', array(
- 'sid' => 'AC123',
- 'friendly_name' => 'Robert Paulson',
- 'status' => 'active',
- ));
- $client = $this->getClient($http);
- $client->account->friendly_name;
- $client->account->friendly_name;
- $client->account->status;
- }
-
- function testSubresourceLoad() {
- $http = $this->createMockHttp('/Calls/CA123', 'get',
- array('status' => 'Completed')
- );
- $client = $this->getClient($http);
- $this->assertEquals(
- 'Completed',
- $client->account->calls->get('CA123')->status
- );
- }
-
- function testSubresourceSubresource() {
- $http = $this->createMockHttp('/Calls/CA123/Notifications/NO123', 'get',
- array('message_text' => 'Foo')
- );
-
- $client = $this->getClient($http);
- $notifs = $client->account->calls->get('CA123')->notifications;
- $this->assertEquals('Foo', $notifs->get('NO123')->message_text);
- }
-
- function testGetIteratorUsesFilters() {
- $params = array_merge($this->pagingParams, array(
- 'StartTime>' => '2012-07-06',
- ));
- $response = array(
- 'total' => 1,
- 'calls' => array(array('status' => 'Completed', 'sid' => 'CA123'))
- );
- $http = $this->createMockHttp('/Calls', 'get', $response, $params);
- $client = $this->getClient($http);
-
- $iterator = $client->account->calls->getIterator(
- 0, 10, array('StartTime>' => '2012-07-06'));
- foreach ($iterator as $call) {
- $this->assertEquals('Completed', $call->status);
- break;
- }
- }
-
- function testListResource() {
- $response = array(
- 'total' => 1,
- 'calls' => array(array('status' => 'completed', 'sid' => 'CA123'))
- );
- $http = $this->createMockHttp('/Calls', 'get', $response,
- $this->pagingParams);
- $client = $this->getClient($http);
-
- $page = $client->account->calls->getPage(0, 10);
- $call = current($page->getItems());
- $this->assertEquals('completed', $call->status);
- $this->assertEquals(1, $page->total);
- }
-
- function testInstanceResourceUriConstructionFromList() {
- $response = array(
- 'total' => 1,
- 'calls' => array(array(
- 'status' => 'in-progress',
- 'sid' => 'CA123',
- 'uri' => 'junk_uri'
- ))
- );
- $http = $this->createMockHttp('/Calls', 'get', $response,
- $this->pagingParams);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/Calls/CA123.json')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array(
- 'status' => 'completed'
- ))
- ));
- $client = $this->getClient($http);
- $page = $client->account->calls->getPage(0, 10);
- $call = current($page->getItems());
-
- /* trigger api fetch by trying to retrieve nonexistent var */
- try {
- $call->nonexistent;
- } catch (Exception $e) {
- // pass
- }
- $this->assertSame($call->status, 'completed');
- }
-
- function testInstanceResourceUriConstructionFromGet() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/IncomingPhoneNumbers/PN123.json')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array(
- 'sms_method' => 'POST',
- 'sid' => 'PN123',
- 'uri' => 'junk_uri',
- ))
- ));
- $http->shouldReceive('post')->once()
- ->with('/2010-04-01/Accounts/AC123/IncomingPhoneNumbers/PN123.json',
- $this->formHeaders, 'SmsMethod=GET')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array(
- 'sms_method' => 'GET',
- 'sid' => 'PN123',
- 'uri' => 'junk_uri'
- ))
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $number = $client->account->incoming_phone_numbers->get('PN123');
- $this->assertSame($number->sms_method, 'POST');
-
- $number->update(array('SmsMethod' => 'GET'));
- $this->assertSame($number->sms_method, 'GET');
- }
-
- function testIterateOverPage() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/Calls.json?Page=0&PageSize=10')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array(
- 'total' => 1,
- 'calls' => array(array('status' => 'Completed', 'sid' => 'CA123'))
- ))
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $page = $client->account->calls->getPage(0, 10);
- foreach ($page->getIterator() as $pageitems) {
- $this->assertSame('CA123', $pageitems->sid);
- }
- }
-
- function testAsymmetricallyNamedResources() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/SMS/Messages.json?Page=0&PageSize=10')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('sms_messages' => array(
- array('status' => 'sent', 'sid' => 'SM123')
- )))
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $sms = current($client->account->sms_messages->getPage(0, 10)->getItems());
- $this->assertEquals('sent', $sms->status);
- }
-
- function testParams() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $qs = 'Page=0&PageSize=10&FriendlyName=foo&Status=active';
- $http->shouldReceive('get')
- ->with('/2010-04-01/Accounts.json?' . $qs)
- ->andReturn(array(
- 200,
- array('Content-Type' => 'application/json'),
- '{"accounts":[]}'
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $client->accounts->getPage(0, 10, array(
- 'FriendlyName' => 'foo',
- 'Status' => 'active',
- ));
- }
-
- function testUpdate() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('post')->once()->with(
- '/2010-04-01/Accounts/AC123/Calls.json', $this->formHeaders,
- http_build_query($this->callParams)
- )->andReturn(
- array(200, array('Content-Type' => 'application/json'),
- '{"sid":"CA123"}')
- );
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $client->account->calls->create('123', '123', 'http://example.com');
- }
-
- function testModifyLiveCall() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('post')->once()->with(
- '/2010-04-01/Accounts/AC123/Calls.json', $this->formHeaders,
- http_build_query($this->callParams)
- )->andReturn(
- array(200, array('Content-Type' => 'application/json'),
- '{"sid":"CA123"}')
- );
- $http->shouldReceive('post')->once()->with(
- '/2010-04-01/Accounts/AC123/Calls/CA123.json',
- $this->formHeaders,
- 'Status=completed'
- )->andReturn(
- array(200, array('Content-Type' => 'application/json'),
- '{"sid":"CA123"}'
- )
- );
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $calls = $client->account->calls;
- $call = $calls->create('123', '123', 'http://example.com');
- $call->hangup();
- }
-
- function testUnmute() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('get')->once()
- ->with(
- '/2010-04-01/Accounts/AC123/Conferences/CF123/Participants.json?Page=0&PageSize=10')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array(
- 'participants' => array(array('call_sid' => 'CA123'))
- ))
- ));
- $http->shouldReceive('post')->once()
- ->with(
- '/2010-04-01/Accounts/AC123/Conferences/CF123/Participants/CA123.json',
- $this->formHeaders,
- 'Muted=true'
- )->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array())
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $conf = $client->account->conferences->get('CF123');
- $page = $conf->participants->getPage(0, 10);
- foreach ($page->getItems() as $participant) {
- $participant->mute();
- }
- }
-
- function testResourcePropertiesReflectUpdates() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123.json')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('friendly_name' => 'foo'))
- ));
- $http->shouldReceive('post')->once()
- ->with('/2010-04-01/Accounts/AC123.json', $this->formHeaders, 'FriendlyName=bar')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('friendly_name' => 'bar'))
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $this->assertEquals('foo', $client->account->friendly_name);
- $client->account->update('FriendlyName', 'bar');
- $this->assertEquals('bar', $client->account->friendly_name);
- }
-
- //function testAccessingNonExistentPropertiesErrorsOut
-
- function testArrayAccessForListResources() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/Calls.json?Page=0&PageSize=50')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array(
- 'calls' => array(array('sid' => 'CA123'))
- ))
- ));
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/Calls.json?Page=1&PageSize=50')
- ->andReturn(array(400, array('Content-Type' => 'application/json'),
- '{"status":400,"message":"foo", "code": "20006"}'
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- foreach ($client->account->calls as $call) {
- $this->assertEquals('CA123', $call->sid);
- }
- $this->assertInstanceOf('Traversable', $client->account->calls);
- }
-
- function testDeepPagingUsesAfterSid() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $callsBase = '/2010-04-01/Accounts/AC123/Calls.json';
- $firstPageUri = $callsBase . '?Page=0&PageSize=1';
- $afterSidUri = $callsBase . '?Page=1&PageSize=1&AfterSid=CA123';
- $secondAfterSidUri = $callsBase . '?Page=2&PageSize=1&AfterSid=CA456';
- $http->shouldReceive('get')->once()
- ->with($firstPageUri)
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array(
- 'next_page_uri' => $afterSidUri,
- 'calls' => array(array(
- 'sid' => 'CA123',
- 'price' => '-0.02000',
- ))
- ))
- ));
- $http->shouldReceive('get')->once()
- ->with($afterSidUri)
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array(
- 'next_page_uri' => $secondAfterSidUri,
- 'calls' => array(array(
- 'sid' => 'CA456',
- 'price' => '-0.02000',
- ))
- ))
- ));
- $http->shouldReceive('get')->once()
- ->with($secondAfterSidUri)
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array(
- 'next_page_uri' => null,
- 'calls' => array(array(
- 'sid' => 'CA789',
- 'price' => '-0.02000',
- ))
- ))
- ));
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/Calls.json?Page=3&PageSize=1')
- ->andReturn(array(400, array('Content-Type' => 'application/json'),
- '{"status":400,"message":"foo", "code": "20006"}'
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- foreach ($client->account->calls->getIterator(0, 1) as $call) {
- $this->assertSame($call->price, '-0.02000');
- }
- }
-
- function testIteratorWithFiltersPagesCorrectly() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $recordingsBase = '/2010-04-01/Accounts/AC123/Recordings.json';
- $firstPageUri = $recordingsBase . '?Page=0&PageSize=1&DateCreated%3E=2011-01-01';
- $secondPageUri = $recordingsBase . '?DateCreated%3E=2011-01-01&Page=1&PageSize=1';
- $thirdPageUri = $recordingsBase . '?DateCreated%3E=2011-01-01&Page=2&PageSize=1';
- $http->shouldReceive('get')->once()
- ->with($firstPageUri)
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array(
- 'next_page_uri' => $secondPageUri,
- 'recordings' => array(array(
- 'sid' => 'RE123',
- 'duration' => 7,
- ))
- ))
- ));
- $http->shouldReceive('get')->once()
- ->with($secondPageUri)
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array(
- 'next_page_uri' => $thirdPageUri,
- 'recordings' => array(array(
- 'sid' => 'RE123',
- 'duration' => 7,
- ))
- ))
- ));
- $http->shouldReceive('get')->once()
- ->with($thirdPageUri)
- ->andReturn(array(400, array('Content-Type' => 'application/json'),
- '{"status":400,"message":"foo", "code": "20006"}'
- ));
-
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- foreach ($client->account->recordings->getIterator(0, 1, array('DateCreated>' => '2011-01-01')) as $recording) {
- $this->assertSame($recording->duration, 7);
- }
- }
-
- function testRetryOn500() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/SMS/Messages/SM123.json')
- ->andReturn($this->nginxError);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/SMS/Messages/SM123.json')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('price' => 0.5))
- )
- );
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $message = $client->account->sms_messages->get('SM123');
- $this->assertSame($message->price, 0.5);
- }
-
- function testDeleteOn500() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('delete')->once()
- ->with('/2010-04-01/Accounts/AC123/SMS/Messages/SM123.json')
- ->andReturn($this->nginxError);
- $http->shouldReceive('delete')->once()
- ->with('/2010-04-01/Accounts/AC123/SMS/Messages/SM123.json')
- ->andReturn(
- array(204, array('Content-Type' => 'application/json'), '')
- );
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $client->account->sms_messages->delete('SM123');
- }
-
- function testSetExplicitRetryLimit() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/SMS/Messages/SM123.json')
- ->andReturn($this->nginxError);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/SMS/Messages/SM123.json')
- ->andReturn($this->nginxError);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/SMS/Messages/SM123.json')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('price' => 0.5))
- )
- );
- // retry twice
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http, 2);
- $message = $client->account->sms_messages->get('SM123');
- $this->assertSame($message->price, 0.5);
- }
-
- function testRetryLimitIsHonored() {
- $this->setExpectedException('Services_Twilio_RestException');
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/SMS/Messages/SM123.json')
- ->andReturn($this->nginxError);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/SMS/Messages/SM123.json')
- ->andReturn($this->nginxError);
- $http->shouldReceive('get')->never()
- ->with('/2010-04-01/Accounts/AC123/SMS/Messages/SM123.json')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('price' => 0.5))
- )
- );
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $message = $client->account->sms_messages->get('SM123');
- $this->assertSame($message->price, 0.5);
- }
-
- function testRetryIdempotentFunctionsOnly() {
- $this->setExpectedException('Services_Twilio_RestException');
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('post')->once()
- ->with('/2010-04-01/Accounts/AC123/SMS/Messages.json', $this->formHeaders,
- 'From=%2B14105551234&To=%2B14102221234&Body=bar')
- ->andReturn($this->nginxError);
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $message = $client->account->sms_messages->create('+14105551234',
- '+14102221234', 'bar');
- }
-
- function testExceptionUsesHttpStatus() {
- $http = $this->createMockHttp('/Queues/QU123/Members/Front', 'post',
- array(), array('Url' => 'http://google.com'), 400);
- $client = $this->getClient($http);
- try {
- $front = $client->account->queues->get('QU123')->members->front();
- $front->update(array('Url' => 'http://google.com'));
- $this->fail('should throw rest exception before reaching this line.');
- } catch (Services_Twilio_RestException $e) {
- $this->assertSame($e->getStatus(), 400);
- $this->assertSame($e->getMessage(), '');
- }
- }
-
- function testUnicode() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('post')->once()
- ->with('/2010-04-01/Accounts/AC123/SMS/Messages.json', $this->formHeaders,
- 'From=123&To=123&Body=Hello+%E2%98%BA')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('sid' => 'SM123'))
- )
- );
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $message = $client->account->sms_messages->create('123', '123',
- 'Hello ☺');
- $this->assertSame($message->sid, 'SM123');
- }
-
- function testCount() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/Calls.json?Page=0&PageSize=1')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array(
- 'total' => '1474',
- 'calls' => array(),
- ))
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $this->assertSame(count($client->account->calls), 1474);
- }
-
- function testCountNoTotal() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/Calls.json?Page=0&PageSize=1')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array(
- 'calls' => array(),
- ))
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $this->assertSame(count($client->account->calls), 0);
- }
-
- function testPostMultivaluedForm() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('post')->once()
- ->with('/2010-04-01/Accounts/AC123/Messages.json', $this->formHeaders,
- 'From=123&To=123&MediaUrl=http%3A%2F%2Fexample.com%2Fimage1&MediaUrl=http%3A%2F%2Fexample.com%2Fimage2')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('sid' => 'SM123'))
- )
- );
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $message = $client->account->messages->sendMessage('123', '123', null,
- array('http://example.com/image1', 'http://example.com/image2')
- );
- $this->assertSame($message->sid, 'SM123');
- }
-
- function testToString() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $resp = <<<JSON
- {
- "account_sid": "AC123",
- "api_version": "2010-04-01",
- "body": "Hello world!",
- "date_created": "Mon, 06 Jan 2014 04:54:34 +0000",
- "date_sent": "Mon, 06 Jan 2014 04:54:34 +0000",
- "date_updated": "Mon, 06 Jan 2014 04:54:34 +0000",
- "direction": "outbound-api",
- "from": "+19255556789",
- "num_media": null,
- "num_segments": "1",
- "price": "-0.00750",
- "price_unit": "USD",
- "sid": "SM77d5ccc71419444fb730541daaaaaaaa",
- "status": "sent",
- "subresource_uris": {
- "media": "/2010-04-01/Accounts/AC123/Messages/SM77d5ccc71419444fb730541daaaaaaaa/Media.json"
- },
- "to": "+19255551234",
- "uri": "/2010-04-01/Accounts/AC123/Messages/SM77d5ccc71419444fb730541daaaaaaaa.json"
- }
-JSON;
- $sampleMessage = new Services_Twilio_Rest_Message($http, '/foo',
- json_decode($resp)
- );
- $expected = '{"account_sid":"AC123","api_version":"2010-04-01","body":"Hello world!","date_created":"Mon, 06 Jan 2014 04:54:34 +0000","date_sent":"Mon, 06 Jan 2014 04:54:34 +0000","date_updated":"Mon, 06 Jan 2014 04:54:34 +0000","direction":"outbound-api","from":"+19255556789","num_media":null,"num_segments":"1","price":"-0.00750","price_unit":"USD","sid":"SM77d5ccc71419444fb730541daaaaaaaa","status":"sent","subresource_uris":{"media":"\/2010-04-01\/Accounts\/AC123\/Messages\/SM77d5ccc71419444fb730541daaaaaaaa\/Media.json"},"to":"+19255551234","uri":"\/foo"}';
- $this->assertSame((string)$sampleMessage, $expected);
- }
-
- function testSubresourceUris() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $call = new Services_Twilio_Rest_Call($http, '/foo');
- $recordings = $call->subresources['recordings'];
- $this->assertSame($recordings->uri, '/foo/Recordings');
- }
-}
diff --git a/externals/twilio-php/tests/TwimlTest.php b/externals/twilio-php/tests/TwimlTest.php
deleted file mode 100644
index d606bf706..000000000
--- a/externals/twilio-php/tests/TwimlTest.php
+++ /dev/null
@@ -1,377 +0,0 @@
-<?php
-
-use \Mockery as m;
-
-require_once 'Twilio/Twiml.php';
-
-class TwimlTest extends PHPUnit_Framework_TestCase {
-
- function tearDown() {
- m::close();
- }
-
- function testEmptyResponse() {
- $r = new Services_Twilio_Twiml();
- $expected = '<Response></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r,
- "Should be an empty response");
- }
-
- public function testSayBasic() {
- $r = new Services_Twilio_Twiml();
- $r->say("Hello Monkey");
- $expected = '<Response><Say>Hello Monkey</Say></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testSayLoopThree() {
- $r = new Services_Twilio_Twiml();
- $r->say("Hello Monkey", array("loop" => 3));
- $expected = '<Response><Say loop="3">Hello Monkey</Say></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testSayLoopThreeWoman() {
- $r = new Services_Twilio_Twiml();
- $r->say("Hello Monkey", array("loop" => 3, "voice"=>"woman"));
- $expected = '<Response><Say loop="3" voice="woman">'
- . 'Hello Monkey</Say></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testSayConvienceMethod() {
- $r = new Services_Twilio_Twiml();
- $r->say("Hello Monkey", array("language" => "fr"));
- $expected = '<Response><Say language="fr">'
- . 'Hello Monkey</Say></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testSayUTF8() {
- $r = new Services_Twilio_Twiml();
- $r->say("é tü & må");
- $expected = '<Response><Say>'
- . '&#xE9; t&#xFC; &amp; m&#xE5;</Say></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testSayNamedEntities() {
- $r = new Services_Twilio_Twiml();
- $r->say("&eacute; t&uuml; &amp; m&aring;");
- $expected = '<Response><Say>'
- . '&#xE9; t&#xFC; &amp; m&#xE5;</Say></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testSayNumericEntities() {
- $r = new Services_Twilio_Twiml();
- $r->say("&#xE9; t&#xFC; &amp; m&#xE5;");
- $expected = '<Response><Say>'
- . '&#xE9; t&#xFC; &amp; m&#xE5;</Say></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testPlayBasic() {
- $r = new Services_Twilio_Twiml();
- $r->play("hello-monkey.mp3");
- $expected = '<Response><Play>hello-monkey.mp3</Play></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testPlayLoopThree() {
- $r = new Services_Twilio_Twiml();
- $r->play("hello-monkey.mp3", array("loop" => 3));
- $expected = '<Response><Play loop="3">'
- . 'hello-monkey.mp3</Play></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testPlayConvienceMethod() {
- $r = new Services_Twilio_Twiml();
- $r->play("hello-monkey.mp3", array("loop" => 3));
- $expected = '<Response><Play loop="3">'
- . 'hello-monkey.mp3</Play></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- //Test Record Verb
- public function testRecord() {
- $r = new Services_Twilio_Twiml();
- $r->record();
- $expected = '<Response><Record></Record></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testRecordActionMethod() {
- $r = new Services_Twilio_Twiml();
- $r->record(array("action" => "example.com", "method" => "GET"));
- $expected = '<Response><Record action="example.com" '
- . 'method="GET"></Record></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testBooleanBecomesString() {
- $r = new Services_Twilio_Twiml();
- $r->record(array("transcribe" => true));
- $expected = '<Response><Record transcribe="true" '
- . '></Record></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testRecordMaxLengthKeyTimeout(){
- $r = new Services_Twilio_Twiml();
- $r->record(array("timeout" => 4, "finishOnKey" => "#",
- "maxLength" => 30));
- $expected = '<Response><Record timeout="4" finishOnKey="#" '
- . 'maxLength="30"></Record></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testRecordConvienceMethod(){
- $r = new Services_Twilio_Twiml();
- $r->record(array("transcribeCallback" => "example.com"));
- $expected = '<Response><Record '
- . 'transcribeCallback="example.com"></Record></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testRecordAddAttribute(){
- $r = new Services_Twilio_Twiml();
- $r->record(array("foo" => "bar"));
- $expected = '<Response><Record foo="bar"></Record></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- //Test Redirect Verb
- public function testRedirect() {
- $r = new Services_Twilio_Twiml();
- $r->redirect();
- $expected = '<Response><Redirect></Redirect></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testAmpersandEscaping() {
- $r = new Services_Twilio_Twiml();
- $test_amp = "test&two&amp;three";
- $r->redirect($test_amp);
- $expected = '<Response><Redirect>' .
- 'test&amp;two&amp;three</Redirect></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testRedirectConvience() {
- $r = new Services_Twilio_Twiml();
- $r->redirect();
- $expected = '<Response><Redirect></Redirect></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
- public function testRedirectAddAttribute(){
- $r = new Services_Twilio_Twiml();
- $r->redirect(array("foo" => "bar"));
- $expected = '<Response><Redirect foo="bar"></Redirect></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- //Test Hangup Verb
- public function testHangup() {
- $r = new Services_Twilio_Twiml();
- $r->hangup();
- $expected = '<Response><Hangup></Hangup></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testHangupConvience() {
- $r = new Services_Twilio_Twiml();
- $r->hangup();
- $expected = '<Response><Hangup></Hangup></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testHangupAddAttribute(){
- $r = new Services_Twilio_Twiml();
- $r->hangup(array("foo" => "bar"));
- $expected = '<Response><Hangup foo="bar"></Hangup></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- //Test Pause Verb
- public function testPause() {
- $r = new Services_Twilio_Twiml();
- $r->pause();
- $expected = '<Response><Pause></Pause></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testPauseConvience() {
- $r = new Services_Twilio_Twiml();
- $r->pause();
- $expected = '<Response><Pause></Pause></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testPauseAddAttribute(){
- $r = new Services_Twilio_Twiml();
- $r->pause(array("foo" => "bar"));
- $expected = '<Response><Pause foo="bar"></Pause></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- //Test Dial Verb
- public function testDial() {
- $r = new Services_Twilio_Twiml();
- $r->dial("1231231234");
- $expected = '<Response><Dial>1231231234</Dial></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testDialConvience() {
- $r = new Services_Twilio_Twiml();
- $r->dial();
- $expected = '<Response><Dial></Dial></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testDialAddNumber() {
- $r = new Services_Twilio_Twiml();
- $d = $r->dial();
- $d->number("1231231234");
- $expected = '<Response><Dial><Number>'
- . '1231231234</Number></Dial></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testDialAddConference() {
- $r = new Services_Twilio_Twiml();
- $d = $r->dial();
- $d->conference("MyRoom");
- $expected = '<Response><Dial><Conference>'
- . 'MyRoom</Conference></Dial></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testDialAddConferenceConvience() {
- $r = new Services_Twilio_Twiml();
- $d = $r->dial();
- $d->conference("MyRoom", array("startConferenceOnEnter" => "false"));
- $expected = '<Response><Dial><Conference startConferenceOnEnter='
- . '"false">MyRoom</Conference></Dial></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testDialAddAttribute() {
- $r = new Services_Twilio_Twiml();
- $r->dial(array("foo" => "bar"));
- $expected = '<Response><Dial foo="bar"></Dial></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- //Test Gather Verb
- public function testGather() {
- $r = new Services_Twilio_Twiml();
- $r->gather();
- $expected = '<Response><Gather></Gather></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testGatherMethodAction(){
- $r = new Services_Twilio_Twiml();
- $r->gather(array("action"=>"example.com", "method"=>"GET"));
- $expected = '<Response><Gather action="example.com" '
- . 'method="GET"></Gather></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testGatherActionWithParams(){
- $r = new Services_Twilio_Twiml();
- $r->gather(array("action" => "record.php?action=recordPageNow"
- . "&id=4&page=3"));
- $expected = '<Response><Gather action="record.php?action='
- . 'recordPageNow&amp;id=4&amp;page=3"></Gather></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testGatherNestedVerbs(){
- $r = new Services_Twilio_Twiml();
- $g = $r->gather(array("action"=>"example.com", "method"=>"GET"));
- $g->say("Hello World");
- $g->play("helloworld.mp3");
- $g->pause();
- $expected = '
- <Response>
- <Gather action="example.com" method="GET">
- <Say>Hello World</Say>
- <Play>helloworld.mp3</Play>
- <Pause></Pause>
- </Gather>
- </Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testGatherNestedVerbsConvienceMethods(){
- $r = new Services_Twilio_Twiml();
- $g = $r->gather(array("action"=>"example.com", "method"=>"GET"));
- $g->say("Hello World");
- $g->play("helloworld.mp3");
- $g->pause();
- $expected = '
- <Response>
- <Gather action="example.com" method="GET">
- <Say>Hello World</Say>
- <Play>helloworld.mp3</Play>
- <Pause></Pause>
- </Gather>
- </Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testGatherAddAttribute(){
- $r = new Services_Twilio_Twiml();
- $r->gather(array("foo" => "bar"));
- $expected = '<Response><Gather foo="bar"></Gather></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testSms() {
- $r = new Services_Twilio_Twiml();
- $r->sms("Hello World");
- $expected = '<Response><Sms>Hello World</Sms></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testSmsConvience() {
- $r = new Services_Twilio_Twiml();
- $r->sms("Hello World");
- $expected = '<Response><Sms>Hello World</Sms></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testSmsAddAttribute() {
- $r = new Services_Twilio_Twiml();
- $r->sms(array("foo" => "bar"));
- $expected = '<Response><Sms foo="bar"></Sms></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- public function testReject() {
- $r = new Services_Twilio_Twiml();
- $r->reject();
- $expected = '<Response><Reject></Reject></Response>';
- $this->assertXmlStringEqualsXmlString($expected, $r);
- }
-
- function testGeneration() {
-
- $r = new Services_Twilio_Twiml();
- $r->say('hello');
- $r->dial()->number('123', array('sendDigits' => '456'));
- $r->gather(array('timeout' => 15));
-
- $doc = simplexml_load_string($r);
- $this->assertEquals('Response', $doc->getName());
- $this->assertEquals('hello', (string) $doc->Say);
- $this->assertEquals('456', (string) $doc->Dial->Number['sendDigits']);
- $this->assertEquals('123', (string) $doc->Dial->Number);
- $this->assertEquals('15', (string) $doc->Gather['timeout']);
- }
-
-}
diff --git a/externals/twilio-php/tests/phpunit.xml b/externals/twilio-php/tests/phpunit.xml
deleted file mode 100644
index ebfe3cf51..000000000
--- a/externals/twilio-php/tests/phpunit.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-<phpunit bootstrap="./Bootstrap.php">
- <testsuites>
- <testsuite name="Services Twilio Test Suite">
- <directory>./</directory>
- </testsuite>
- </testsuites>
-</phpunit>
diff --git a/externals/twilio-php/tests/resources/AccountsTest.php b/externals/twilio-php/tests/resources/AccountsTest.php
deleted file mode 100644
index 65f9b83fd..000000000
--- a/externals/twilio-php/tests/resources/AccountsTest.php
+++ /dev/null
@@ -1,29 +0,0 @@
-<?php
-
-use \Mockery as m;
-
-class AccountsTest extends PHPUnit_Framework_TestCase
-{
- protected $formHeaders = array('Content-Type' => 'application/x-www-form-urlencoded');
- function testPost()
- {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('post')->once()
- ->with('/2010-04-01/Accounts.json',
- $this->formHeaders, 'FriendlyName=foo')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('sid' => 'AC345'))
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $account = $client->accounts->create(array(
- 'FriendlyName' => 'foo',
- ));
- $this->assertEquals('AC345', $account->sid);
- }
-
- function tearDown()
- {
- m::close();
- }
-}
-
diff --git a/externals/twilio-php/tests/resources/ApplicationsTest.php b/externals/twilio-php/tests/resources/ApplicationsTest.php
deleted file mode 100644
index ff87675a7..000000000
--- a/externals/twilio-php/tests/resources/ApplicationsTest.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-use \Mockery as m;
-
-class ApplicationsTest extends PHPUnit_Framework_TestCase {
- protected $formHeaders = array('Content-Type' => 'application/x-www-form-urlencoded');
- function testPost()
- {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('post')->once()
- ->with('/2010-04-01/Accounts/AC123/Applications.json',
- $this->formHeaders, 'FriendlyName=foo&VoiceUrl=bar')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('sid' => 'AP123'))
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $app = $client->account->applications->create('foo', array(
- 'VoiceUrl' => 'bar',
- ));
- $this->assertEquals('AP123', $app->sid);
- }
-
- function tearDown()
- {
- m::close();
- }
-}
-
diff --git a/externals/twilio-php/tests/resources/AvailablePhoneNumbersTest.php b/externals/twilio-php/tests/resources/AvailablePhoneNumbersTest.php
deleted file mode 100644
index 441f46bb5..000000000
--- a/externals/twilio-php/tests/resources/AvailablePhoneNumbersTest.php
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php
-
-use \Mockery as m;
-
-class AvailablePhoneNumbersTest extends PHPUnit_Framework_TestCase {
- function testPartialApplication() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/AvailablePhoneNumbers/US/Local.json?AreaCode=510')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('available_phone_numbers' => array(
- 'friendly_name' => '(510) 564-7903'
- )))
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $nums = $client->account->available_phone_numbers->getLocal('US');
- $numsList = $nums->getList(array('AreaCode' => '510'));
- foreach ($numsList as $num) {
- $this->assertEquals('(510) 564-7903', $num->friendly_name);
- }
- }
-
- function testPagePhoneNumberResource() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/AvailablePhoneNumbers.json?Page=0&PageSize=50')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array(
- 'total' => 1,
- 'countries' => array(array('country_code' => 'CA'))
- ))
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $page = $client->account->available_phone_numbers->getPage('0');
- $this->assertEquals('CA', $page->countries[0]->country_code);
- }
-
- function testGetMobile() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/AvailablePhoneNumbers/GB/Mobile.json')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('available_phone_numbers' => array(
- 'friendly_name' => '(510) 564-7903'
- )))
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $nums = $client->account->available_phone_numbers->getMobile('GB')->getList();
- foreach ($nums as $num) {
- $this->assertEquals('(510) 564-7903', $num->friendly_name);
- }
- }
-
- function tearDown() {
- m::close();
- }
-}
diff --git a/externals/twilio-php/tests/resources/CallsTest.php b/externals/twilio-php/tests/resources/CallsTest.php
deleted file mode 100644
index 3759cf415..000000000
--- a/externals/twilio-php/tests/resources/CallsTest.php
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-
-use \Mockery as m;
-
-class CallsTest extends PHPUnit_Framework_TestCase {
- /**
- * @dataProvider sidProvider
- */
- function testApplicationSid($sid, $expected)
- {
- $result = Services_Twilio_Rest_Calls::isApplicationSid($sid);
- $this->assertEquals($expected, $result);
- }
-
- function sidProvider()
- {
- return array(
- array("AP2a0747eba6abf96b7e3c3ff0b4530f6e", true),
- array("CA2a0747eba6abf96b7e3c3ff0b4530f6e", false),
- array("AP2a0747eba6abf96b7e3c3ff0b4530f", false),
- array("http://www.google.com/asdfasdfAP", false),
- );
- }
-}
-
diff --git a/externals/twilio-php/tests/resources/ConnectAppsTest.php b/externals/twilio-php/tests/resources/ConnectAppsTest.php
deleted file mode 100644
index 7cda22067..000000000
--- a/externals/twilio-php/tests/resources/ConnectAppsTest.php
+++ /dev/null
@@ -1,54 +0,0 @@
-<?php
-
-use \Mockery as m;
-
-class ConnectAppsTest extends PHPUnit_Framework_TestCase {
-
- function testUpdateWithArray() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/ConnectApps/CN123.json')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('friendly_name' => 'foo'))
- ));
- $http->shouldReceive('post')->once()
- ->with('/2010-04-01/Accounts/AC123/ConnectApps/CN123.json',
- array('Content-Type' => 'application/x-www-form-urlencoded'),
- 'FriendlyName=Bar')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('friendly_name' => 'Bar'))
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $cn = $client->account->connect_apps->get('CN123');
- $this->assertEquals('foo', $cn->friendly_name);
- $cn->update(array('FriendlyName' => 'Bar'));
- $this->assertEquals('Bar', $cn->friendly_name);
- }
-
- function testUpdateWithOneParam()
- {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/ConnectApps/CN123.json')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('friendly_name' => 'foo'))
- ));
- $http->shouldReceive('post')->once()
- ->with('/2010-04-01/Accounts/AC123/ConnectApps/CN123.json',
- array('Content-Type' => 'application/x-www-form-urlencoded'),
- 'FriendlyName=Bar')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('friendly_name' => 'Bar'))
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $cn = $client->account->connect_apps->get('CN123');
- $this->assertEquals('foo', $cn->friendly_name);
- $cn->update('FriendlyName', 'Bar');
- $this->assertEquals('Bar', $cn->friendly_name);
- }
-
- function tearDown()
- {
- m::close();
- }
-}
diff --git a/externals/twilio-php/tests/resources/IncomingPhoneNumbersTest.php b/externals/twilio-php/tests/resources/IncomingPhoneNumbersTest.php
deleted file mode 100644
index 3d3ebb466..000000000
--- a/externals/twilio-php/tests/resources/IncomingPhoneNumbersTest.php
+++ /dev/null
@@ -1,104 +0,0 @@
-<?php
-
-use \Mockery as m;
-
-class IncomingPhoneNumbersTest extends PHPUnit_Framework_TestCase {
-
- protected $apiResponse = array(
- 'incoming_phone_numbers' => array(
- array(
- 'sid' => 'PN123',
- 'sms_fallback_method' => 'POST',
- 'voice_method' => 'POST',
- 'friendly_name' => '(510) 564-7903',
- )
- ),
- );
-
- function testGetNumberWithResult() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/IncomingPhoneNumbers.json?Page=0&PageSize=1&PhoneNumber=%2B14105551234')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode($this->apiResponse)
- )
- );
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $number = $client->account->incoming_phone_numbers->getNumber('+14105551234');
- $this->assertEquals('PN123', $number->sid);
- }
-
- function testGetNumberNoResults() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/IncomingPhoneNumbers.json?Page=0&PageSize=1&PhoneNumber=%2B14105551234')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array(
- 'incoming_phone_numbers' => array(),
- 'page' => 0,
- 'page_size' => 1,
- ))
- )
- );
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $number = $client->account->incoming_phone_numbers->getNumber('+14105551234');
- $this->assertNull($number);
- }
-
- function testGetMobile() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/IncomingPhoneNumbers/Mobile.json?Page=0&PageSize=50')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode($this->apiResponse)
- ));
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/IncomingPhoneNumbers/Mobile.json?Page=1&PageSize=50')
- ->andReturn(array(400, array('Content-Type' => 'application/json'),
- '{"status":400,"message":"foo", "code": "20006"}'
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- foreach ($client->account->incoming_phone_numbers->mobile as $num) {
- $this->assertEquals('(510) 564-7903', $num->friendly_name);
- }
- }
-
- function testGetLocal() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/IncomingPhoneNumbers/Local.json?Page=0&PageSize=50')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode($this->apiResponse)
- ));
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/IncomingPhoneNumbers/Local.json?Page=1&PageSize=50')
- ->andReturn(array(400, array('Content-Type' => 'application/json'),
- '{"status":400,"message":"foo", "code": "20006"}'
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
-
- foreach ($client->account->incoming_phone_numbers->local as $num) {
- $this->assertEquals('(510) 564-7903', $num->friendly_name);
- }
- }
-
- function testGetTollFree() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/IncomingPhoneNumbers/TollFree.json?Page=0&PageSize=50')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode($this->apiResponse)
- ));
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/IncomingPhoneNumbers/TollFree.json?Page=1&PageSize=50')
- ->andReturn(array(400, array('Content-Type' => 'application/json'),
- '{"status":400,"message":"foo", "code": "20006"}'
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- foreach ($client->account->incoming_phone_numbers->toll_free as $num) {
- $this->assertEquals('(510) 564-7903', $num->friendly_name);
- }
- }
-
-}
-
diff --git a/externals/twilio-php/tests/resources/MediaTest.php b/externals/twilio-php/tests/resources/MediaTest.php
deleted file mode 100644
index b8edc4f1b..000000000
--- a/externals/twilio-php/tests/resources/MediaTest.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-use \Mockery as m;
-
-class MediaTest extends PHPUnit_Framework_TestCase {
-
- function testUseSpecialListKey() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/Messages/MM123/Media.json?Page=0&PageSize=50')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array(
- 'end' => '0',
- 'total' => '2',
- 'media_list' => array(
- array('sid' => 'ME123'),
- array('sid' => 'ME456')
- ),
- 'next_page_uri' => 'null',
- 'start' => 0
- ))
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $media_list = $client->account->messages->get('MM123')->media->getPage()->getItems();
- $this->assertEquals(count($media_list), 2);
- }
-
-}
diff --git a/externals/twilio-php/tests/resources/MembersTest.php b/externals/twilio-php/tests/resources/MembersTest.php
deleted file mode 100644
index 45711cee3..000000000
--- a/externals/twilio-php/tests/resources/MembersTest.php
+++ /dev/null
@@ -1,83 +0,0 @@
-<?php
-
-use \Mockery as m;
-
-class MembersTest extends PHPUnit_Framework_TestCase {
-
- protected $formHeaders = array('Content-Type' => 'application/x-www-form-urlencoded');
-
- function testFront() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/Queues/QQ123/Members/Front.json')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('call_sid' => 'CA123', 'position' => 0))
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $queue = $client->account->queues->get('QQ123');
- $firstMember = $queue->members->front();
- $this->assertSame($firstMember->call_sid, 'CA123');
- }
-
- function testDequeueFront() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('post')->once()
- ->with('/2010-04-01/Accounts/AC123/Queues/QQ123/Members/Front.json',
- $this->formHeaders, 'Url=http%3A%2F%2Ffoo.com&Method=POST')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('call_sid' => 'CA123', 'position' => 0))
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $queue = $client->account->queues->get('QQ123');
- $firstMember = $queue->members->front();
- $firstMember->dequeue('http://foo.com');
- }
-
- function testDequeueSid() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('post')->once()
- ->with('/2010-04-01/Accounts/AC123/Queues/QQ123/Members/CA123.json',
- $this->formHeaders, 'Url=http%3A%2F%2Ffoo.com&Method=GET')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('call_sid' => 'CA123', 'position' => 0))
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $queue = $client->account->queues->get('QQ123');
- $firstMember = $queue->members->get('CA123');
- $firstMember->dequeue('http://foo.com', 'GET');
- }
-
- function testMemberIterate() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $resp = json_encode(
- array(
- 'queue_members' => array(
- array('call_sid' => 'CA123', 'wait_time' => 30)
- ),
- 'end' => 1,
- )
- );
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/Queues/QQ123/Members.json?Page=0&PageSize=50')
- ->andReturn(array(200, array('Content-Type' => 'application/json'), $resp
- ));
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/Queues/QQ123/Members.json?Page=1&PageSize=50')
- ->andReturn(array(400, array('Content-Type' => 'application/json'),
- '{"status":400,"message":"foo", "code": "20006"}'
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $queue = $client->account->queues->get('QQ123');
- foreach($queue->members as $member) {
- $this->assertSame($member->call_sid, 'CA123');
- $this->assertSame($member->wait_time, 30);
- }
- }
-
- function tearDown() {
- m::close();
- }
-
-}
-
-
diff --git a/externals/twilio-php/tests/resources/MessagesTest.php b/externals/twilio-php/tests/resources/MessagesTest.php
deleted file mode 100644
index c697a5276..000000000
--- a/externals/twilio-php/tests/resources/MessagesTest.php
+++ /dev/null
@@ -1,123 +0,0 @@
-<?php
-
-use \Mockery as m;
-
-class MessagesTest extends PHPUnit_Framework_TestCase
-{
- protected $formHeaders = array('Content-Type' => 'application/x-www-form-urlencoded');
-
- function testCreateMessage() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('post')->once()
- ->with('/2010-04-01/Accounts/AC123/Messages.json', $this->formHeaders,
- 'From=%2B1222&To=%2B44123&Body=Hi+there')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('sid' => 'SM123'))
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $msg = $client->account->messages->sendMessage('+1222', '+44123', 'Hi there');
- $this->assertSame('SM123', $msg->sid);
- }
-
- function testCreateMessageWithMedia() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('post')->once()
- ->with('/2010-04-01/Accounts/AC123/Messages.json', $this->formHeaders,
- 'From=%2B1222&To=%2B44123&MediaUrl=http%3A%2F%2Fexample.com%2Fimage1')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('sid' => 'SM123'))
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $msg = $client->account->messages->sendMessage('+1222', '+44123', null,
- array('http://example.com/image1'));
- $this->assertSame('SM123', $msg->sid);
- }
-
- function testCreateMessageWithMediaAndBody() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('post')->once()
- ->with('/2010-04-01/Accounts/AC123/Messages.json', $this->formHeaders,
- 'From=%2B1222&To=%2B44123&MediaUrl=http%3A%2F%2Fexample.com%2Fimage1&Body=Hi+there')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('sid' => 'SM123'))
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $msg = $client->account->messages->sendMessage('+1222', '+44123', 'Hi there',
- array('http://example.com/image1')
- );
- $this->assertSame('SM123', $msg->sid);
- }
-
- function testCreateMessageWithMultipleMedia() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('post')->once()
- ->with('/2010-04-01/Accounts/AC123/Messages.json', $this->formHeaders,
- 'From=%2B1222&To=%2B44123&MediaUrl=http%3A%2F%2Fexample.com%2Fimage1&MediaUrl=http%3A%2F%2Fexample.com%2Fimage2')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('sid' => 'SM123'))
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $msg = $client->account->messages->sendMessage('+1222', '+44123', null,
- array('http://example.com/image1', 'http://example.com/image2'));
- $this->assertSame('SM123', $msg->sid);
- }
-
- function testBadMessageThrowsException() {
- $this->setExpectedException('Services_Twilio_RestException');
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('post')->once()
- ->with('/2010-04-01/Accounts/AC123/Messages.json', $this->formHeaders,
- 'From=%2B1222&To=%2B44123&Body=' . str_repeat('hi', 801))
- ->andReturn(array(400, array('Content-Type' => 'application/json'),
- json_encode(array(
- 'status' => '400',
- 'message' => 'Too long',
- ))
- ));
- $client = new Services_Twilio('AC123', '123', null, $http);
- $msg = $client->account->messages->sendMessage('+1222', '+44123', str_repeat('hi', 801));
- }
-
- function testRawCreate() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('post')->once()
- ->with('/2010-04-01/Accounts/AC123/Messages.json', $this->formHeaders,
- 'From=%2B1222&To=%2B44123&MediaUrl=http%3A%2F%2Fexample.com%2Fimage1&MediaUrl=http%3A%2F%2Fexample.com%2Fimage2')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('sid' => 'SM123'))
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $msg = $client->account->messages->create(array(
- 'From' => '+1222',
- 'To' => '+44123',
- 'MediaUrl' => array('http://example.com/image1', 'http://example.com/image2')
- ));
- $this->assertSame('SM123', $msg->sid);
- }
-
- function testDeleteMessage() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('delete')->once()
- ->with('/2010-04-01/Accounts/AC123/Messages/ME123.json')
- ->andReturn(array(204, array('Content-Type' => 'application/json'), ''
- ));
- $client = new Services_Twilio('AC123', '123', null, $http);
- $client->account->messages->delete('ME123');
- }
-
- function testNewline() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('post')->once()
- ->with('/2010-04-01/Accounts/AC123/Messages.json', $this->formHeaders,
- 'Body=Hello%0A%0AHello')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('sid' => 'SM123'))
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $msg = $client->account->messages->create(array(
- 'Body' => "Hello\n\nHello"
- ));
- $this->assertSame('SM123', $msg->sid);
- }
-}
-
diff --git a/externals/twilio-php/tests/resources/NotificationTest.php b/externals/twilio-php/tests/resources/NotificationTest.php
deleted file mode 100644
index 704230e6f..000000000
--- a/externals/twilio-php/tests/resources/NotificationTest.php
+++ /dev/null
@@ -1,20 +0,0 @@
-<?php
-
-use \Mockery as m;
-
-class NotificationTest extends PHPUnit_Framework_TestCase {
- function testDelete() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('delete')->once()
- ->with('/2010-04-01/Accounts/AC123/Notifications/NO123.json')
- ->andReturn(array(204, array(), ''));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $client->account->notifications->delete('NO123');
- }
-
- function tearDown()
- {
- m::close();
- }
-}
-
diff --git a/externals/twilio-php/tests/resources/OutgoingCallerIdsTest.php b/externals/twilio-php/tests/resources/OutgoingCallerIdsTest.php
deleted file mode 100644
index 65d1ecd5b..000000000
--- a/externals/twilio-php/tests/resources/OutgoingCallerIdsTest.php
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-
-use \Mockery as m;
-
-class OutgoingCallerIdsTest extends PHPUnit_Framework_TestCase {
- protected $formHeaders = array('Content-Type' => 'application/x-www-form-urlencoded');
- function testPost() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('post')->once()
- ->with('/2010-04-01/Accounts/AC123/OutgoingCallerIds.json',
- $this->formHeaders, 'PhoneNumber=%2B14158675309&FriendlyName=My+Home+Phone+Number')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array(
- 'account_sid' => 'AC123',
- 'phone_number' => '+14158675309',
- 'friendly_name' => 'My Home Phone Number',
- 'validation_code' => 123456,
- ))
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $request = $client->account->outgoing_caller_ids->create('+14158675309', array(
- 'FriendlyName' => 'My Home Phone Number',
- ));
- $this->assertEquals(123456, $request->validation_code);
- }
-
- function tearDown() {
- m::close();
- }
-}
diff --git a/externals/twilio-php/tests/resources/QueuesTest.php b/externals/twilio-php/tests/resources/QueuesTest.php
deleted file mode 100644
index 282a142d2..000000000
--- a/externals/twilio-php/tests/resources/QueuesTest.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-use \Mockery as m;
-
-class QueuesTest extends PHPUnit_Framework_TestCase {
-
- protected $formHeaders = array('Content-Type' => 'application/x-www-form-urlencoded');
-
- function testCreate() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('post')->once()
- ->with('/2010-04-01/Accounts/AC123/Queues.json', $this->formHeaders,
- 'FriendlyName=foo&MaxSize=123')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('sid' => 'QQ123', 'average_wait_time' => 0))
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $queue = $client->account->queues->create('foo',
- array('MaxSize' => 123));
- $this->assertSame($queue->sid, 'QQ123');
- $this->assertSame($queue->average_wait_time, 0);
- }
-
- function tearDown() {
- m::close();
- }
-}
-
diff --git a/externals/twilio-php/tests/resources/SMSMessagesTest.php b/externals/twilio-php/tests/resources/SMSMessagesTest.php
deleted file mode 100644
index afe5b6c6d..000000000
--- a/externals/twilio-php/tests/resources/SMSMessagesTest.php
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php
-
-use \Mockery as m;
-
-class SMSMessagesTest extends PHPUnit_Framework_TestCase {
- protected $formHeaders = array('Content-Type' => 'application/x-www-form-urlencoded');
-
- function testCreateMessage() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('post')->once()
- ->with('/2010-04-01/Accounts/AC123/SMS/Messages.json', $this->formHeaders,
- 'From=%2B1222&To=%2B44123&Body=Hi+there')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('sid' => 'SM123'))
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $sms = $client->account->sms_messages->create('+1222', '+44123', 'Hi there');
- $this->assertSame('SM123', $sms->sid);
- }
-
- function testBadMessageThrowsException() {
- $this->setExpectedException('Services_Twilio_RestException');
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('post')->once()
- ->with('/2010-04-01/Accounts/AC123/SMS/Messages.json', $this->formHeaders,
- 'From=%2B1222&To=%2B44123&Body=' . str_repeat('hi', 81))
- ->andReturn(array(400, array('Content-Type' => 'application/json'),
- json_encode(array(
- 'status' => '400',
- 'message' => 'Too long',
- ))
- ));
- $client = new Services_Twilio('AC123', '123', null, $http);
- $sms = $client->account->sms_messages->create('+1222', '+44123',
- str_repeat('hi', 81));
- }
-}
-
diff --git a/externals/twilio-php/tests/resources/SandboxTest.php b/externals/twilio-php/tests/resources/SandboxTest.php
deleted file mode 100644
index 4d6623f92..000000000
--- a/externals/twilio-php/tests/resources/SandboxTest.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-use \Mockery as m;
-
-class SandboxTest extends PHPUnit_Framework_TestCase {
- protected $formHeaders = array('Content-Type' => 'application/x-www-form-urlencoded');
- function testUpdateVoiceUrl()
- {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('post')->once()
- ->with('/2010-04-01/Accounts/AC123/Sandbox.json', $this->formHeaders, 'VoiceUrl=foo')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('voice_url' => 'foo'))
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $client->account->sandbox->update('VoiceUrl', 'foo');
- $this->assertEquals('foo', $client->account->sandbox->voice_url);
- }
-
- function tearDown() {
- m::close();
- }
-}
diff --git a/externals/twilio-php/tests/resources/ShortCodesTest.php b/externals/twilio-php/tests/resources/ShortCodesTest.php
deleted file mode 100644
index aee9e40e2..000000000
--- a/externals/twilio-php/tests/resources/ShortCodesTest.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-use \Mockery as m;
-
-class ShortCodesTest extends PHPUnit_Framework_TestCase {
-
- function testShortcodeResource() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/SMS/ShortCodes/SC123.json')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('sid' => 'SC123', 'short_code' => '1234'))
- ));
- $client = new Services_Twilio('AC123', '123', '2010-04-01', $http);
- $sms = $client->account->short_codes->get('SC123');
- $this->assertSame('1234', $sms->short_code);
- }
-}
-
diff --git a/externals/twilio-php/tests/resources/UsageRecordsTest.php b/externals/twilio-php/tests/resources/UsageRecordsTest.php
deleted file mode 100644
index 5f0f27bf0..000000000
--- a/externals/twilio-php/tests/resources/UsageRecordsTest.php
+++ /dev/null
@@ -1,180 +0,0 @@
-<?php
-
-use \Mockery as m;
-
-class UsageRecordsTest extends PHPUnit_Framework_TestCase {
-
- function testGetBaseRecord() {
-
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/Usage/Records.json?Page=0&PageSize=50')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('usage_records' => array(
- array(
- 'category' => 'sms',
- 'count' => 5,
- 'end_date' => '2012-08-01',
- ),
- array(
- 'category' => 'calleridlookups',
- 'count' => 5,
- 'end_date' => '2012-08-01',
- ))
- ))
- ));
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/Usage/Records.json?Page=1&PageSize=50')
- ->andReturn(array(400, array('Content-Type' => 'application/json'),
- '{"status":400,"message":"foo", "code": "20006"}'
- ));
-
- $client = new Services_Twilio('AC123', '456bef', '2010-04-01', $http);
- foreach ($client->account->usage_records as $record) {
- $this->assertSame(5, $record->count);
- }
- }
-
- function testUsageRecordSubresource() {
-
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/Usage/Records/LastMonth.json?Page=0&PageSize=50')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('usage_records' => array(
- array(
- 'category' => 'sms',
- 'count' => 4,
- 'end_date' => '2012-08-01',
- ),
- array(
- 'category' => 'calleridlookups',
- 'count' => 4,
- 'end_date' => '2012-08-01',
- ))
- ))
- ));
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/Usage/Records/LastMonth.json?Page=1&PageSize=50')
- ->andReturn(array(400, array('Content-Type' => 'application/json'),
- '{"status":400,"message":"foo", "code": "20006"}'
- ));
-
- $client = new Services_Twilio('AC123', '456bef', '2010-04-01', $http);
- foreach ($client->account->usage_records->last_month as $record) {
- $this->assertSame('2012-08-01', $record->end_date);
- }
- }
-
- function testGetCategory() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/Usage/Records.json?Page=0&PageSize=1&Category=calls')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('usage_records' => array(
- array(
- 'category' => 'calls',
- 'count' => 4,
- 'price' => '100.30',
- 'end_date' => '2012-08-01',
- )),
- ))
- ));
- $client = new Services_Twilio('AC123', '456bef', '2010-04-01', $http);
- $callRecord = $client->account->usage_records->getCategory('calls');
- $this->assertSame('100.30', $callRecord->price);
- }
-
- function testFilterUsageRecords() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $params = 'Page=0&PageSize=50&StartDate=2012-08-01&EndDate=2012-08-31';
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/Usage/Records.json?' . $params)
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('usage_records' => array(
- array(
- 'category' => 'sms',
- 'count' => 4,
- 'price' => '300.30',
- ),
- array(
- 'category' => 'calls',
- 'count' => 4,
- 'price' => '100.30',
- )),
- ))
- ));
- $params = 'Page=1&PageSize=50&StartDate=2012-08-01&EndDate=2012-08-31';
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/Usage/Records.json?' . $params)
- ->andReturn(array(400, array('Content-Type' => 'application/json'),
- '{"status":400,"message":"foo", "code": "20006"}'
- ));
- $client = new Services_Twilio('AC123', '456bef', '2010-04-01', $http);
- foreach ($client->account->usage_records->getIterator(0, 50, array(
- 'StartDate' => '2012-08-01',
- 'EndDate' => '2012-08-31',
- )) as $record) {
- $this->assertSame(4, $record->count);
- }
- }
-
- function testGetCategoryOnSubresource() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $params = 'Page=0&PageSize=1&Category=sms';
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/Usage/Records/Today.json?' . $params)
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('usage_records' => array(
- array(
- 'category' => 'sms',
- 'count' => 4,
- 'price' => '100.30',
- 'end_date' => '2012-08-30'
- )),
- ))
- ));
- $client = new Services_Twilio('AC123', '456bef', '2010-04-01', $http);
- $smsRecord = $client->account->usage_records->today->getCategory('sms');
- $this->assertSame($smsRecord->end_date, '2012-08-30');
- }
-
- function testTimeSeriesFilters() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $params = 'Page=0&PageSize=50&StartDate=2012-08-01&EndDate=2012-08-31&Category=recordings';
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/Usage/Records/Daily.json?' . $params)
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('usage_records' => array(
- array(
- 'category' => 'recordings',
- 'count' => 4,
- 'price' => '100.30',
- 'end_date' => '2012-08-31'
- ),
- array(
- 'category' => 'recordings',
- 'count' => 4,
- 'price' => '100.30',
- 'end_date' => '2012-08-30'
- )),
- ))
- ));
- $params = 'Page=1&PageSize=50&StartDate=2012-08-01&EndDate=2012-08-31&Category=recordings';
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/Usage/Records/Daily.json?' . $params)
- ->andReturn(array(400, array('Content-Type' => 'application/json'),
- '{"status":400,"message":"foo", "code": "20006"}'
- ));
- $client = new Services_Twilio('AC123', '456bef', '2010-04-01', $http);
- foreach ($client->account->usage_records->daily->getIterator(0, 50, array(
- 'StartDate' => '2012-08-01',
- 'EndDate' => '2012-08-31',
- 'Category' => 'recordings',
- )) as $record) {
- $this->assertSame($record->category, 'recordings');
- $this->assertSame($record->price, '100.30');
- }
- }
-}
-
diff --git a/externals/twilio-php/tests/resources/UsageTriggersTest.php b/externals/twilio-php/tests/resources/UsageTriggersTest.php
deleted file mode 100644
index 96d49cc69..000000000
--- a/externals/twilio-php/tests/resources/UsageTriggersTest.php
+++ /dev/null
@@ -1,114 +0,0 @@
-<?php
-
-use \Mockery as m;
-
-class UsageTriggersTest extends PHPUnit_Framework_TestCase {
- function testRetrieveTrigger() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/Usage/Triggers/UT123.json')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array(
- 'sid' => 'UT123',
- 'date_created' => 'Tue, 09 Oct 2012 19:27:24 +0000',
- 'recurring' => null,
- 'usage_category' => 'totalprice',
- ))
- ));
- $client = new Services_Twilio('AC123', '456bef', '2010-04-01', $http);
- $usageSid = 'UT123';
- $usageTrigger = $client->account->usage_triggers->get($usageSid);
- $this->assertSame('totalprice', $usageTrigger->usage_category);
- }
-
- protected $formHeaders = array('Content-Type' => 'application/x-www-form-urlencoded');
-
- function testUpdateTrigger() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $usageSid = 'UT123';
- $http->shouldReceive('post')->once()
- ->with('/2010-04-01/Accounts/AC123/Usage/Triggers/UT123.json',
- $this->formHeaders, 'FriendlyName=new')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array(
- 'friendly_name' => 'new',
- 'sid' => 'UT123',
- 'uri' => '/2010-04-01/Accounts/AC123/Usage/Triggers/UT123.json'
- ))
- ));
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/Usage/Triggers/UT123.json')
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array(
- 'sid' => 'UT123',
- 'friendly_name' => 'new',
- ))
- ));
- $client = new Services_Twilio('AC123', '456bef', '2010-04-01', $http);
- $usageTrigger = $client->account->usage_triggers->get($usageSid);
- $usageTrigger->update(array(
- 'FriendlyName' => 'new',
- ));
- $usageTrigger2 = $client->account->usage_triggers->get($usageSid);
- $this->assertSame('new', $usageTrigger2->friendly_name);
- }
-
- function testFilterTriggerList() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $params = 'Page=0&PageSize=50&UsageCategory=sms';
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/Usage/Triggers.json?' . $params)
- ->andReturn(array(200, array('Content-Type' => 'application/json'),
- json_encode(array('usage_triggers' => array(
- array(
- 'usage_category' => 'sms',
- 'current_value' => '4',
- 'trigger_value' => '100.30',
- ),
- array(
- 'usage_category' => 'sms',
- 'current_value' => '4',
- 'trigger_value' => '400.30',
- )),
- 'next_page_uri' => '/2010-04-01/Accounts/AC123/Usage/Triggers.json?UsageCategory=sms&Page=1&PageSize=50',
- ))
- ));
- $params = 'UsageCategory=sms&Page=1&PageSize=50';
- $http->shouldReceive('get')->once()
- ->with('/2010-04-01/Accounts/AC123/Usage/Triggers.json?' . $params)
- ->andReturn(array(400, array('Content-Type' => 'application/json'),
- '{"status":400,"message":"foo", "code": "20006"}'
- ));
- $client = new Services_Twilio('AC123', '456bef', '2010-04-01', $http);
- foreach ($client->account->usage_triggers->getIterator(
- 0, 50, array(
- 'UsageCategory' => 'sms',
- )) as $trigger
- ) {
- $this->assertSame($trigger->current_value, "4");
- }
- }
-
- function testCreateTrigger() {
- $http = m::mock(new Services_Twilio_TinyHttp);
- $params = 'UsageCategory=sms&TriggerValue=100&CallbackUrl=foo';
- $http->shouldReceive('post')->once()
- ->with('/2010-04-01/Accounts/AC123/Usage/Triggers.json',
- $this->formHeaders, $params)
- ->andReturn(array(201, array('Content-Type' => 'application/json'),
- json_encode(array(
- 'usage_category' => 'sms',
- 'sid' => 'UT123',
- 'uri' => '/2010-04-01/Accounts/AC123/Usage/Triggers/UT123.json'
- ))
- ));
- $client = new Services_Twilio('AC123', '456bef', '2010-04-01', $http);
- $trigger = $client->account->usage_triggers->create(
- 'sms',
- '100',
- 'foo'
- );
- $this->assertSame('sms', $trigger->usage_category);
- }
-}
-
diff --git a/resources/celerity/map.php b/resources/celerity/map.php
index b342c38f0..51bc9338d 100644
--- a/resources/celerity/map.php
+++ b/resources/celerity/map.php
@@ -1,2458 +1,2356 @@
<?php
/**
* This file is automatically generated. Use 'bin/celerity map' to rebuild it.
*
* @generated
*/
return array(
'names' => array(
- 'conpherence.pkg.css' => 'e68cf1fa',
- 'conpherence.pkg.js' => '15191c65',
- 'core.pkg.css' => '7e14b04a',
- 'core.pkg.js' => '4bde473b',
- 'differential.pkg.css' => '06dc617c',
- 'differential.pkg.js' => 'ef0b989b',
- 'diffusion.pkg.css' => 'a2d17c7d',
- 'diffusion.pkg.js' => '6134c5a1',
- 'maniphest.pkg.css' => '4845691a',
- 'maniphest.pkg.js' => '4d7e79c8',
- 'rsrc/audio/basic/alert.mp3' => '98461568',
- 'rsrc/audio/basic/bing.mp3' => 'ab8603a5',
- 'rsrc/audio/basic/pock.mp3' => '0cc772f5',
- 'rsrc/audio/basic/tap.mp3' => 'fc2fd796',
- 'rsrc/audio/basic/ting.mp3' => '17660001',
- 'rsrc/css/aphront/aphront-bars.css' => '231ac33c',
- 'rsrc/css/aphront/dark-console.css' => '0e14e8f6',
- 'rsrc/css/aphront/dialog-view.css' => '6bfc244b',
- 'rsrc/css/aphront/list-filter-view.css' => '5d6f0526',
- 'rsrc/css/aphront/multi-column.css' => '84cc6640',
- 'rsrc/css/aphront/notification.css' => '457861ec',
- 'rsrc/css/aphront/panel-view.css' => '8427b78d',
- 'rsrc/css/aphront/phabricator-nav-view.css' => '694d7723',
- 'rsrc/css/aphront/table-view.css' => '8c9bbafe',
- 'rsrc/css/aphront/tokenizer.css' => '15d5ff71',
- 'rsrc/css/aphront/tooltip.css' => 'cb1397a4',
- 'rsrc/css/aphront/typeahead-browse.css' => 'f2818435',
- 'rsrc/css/aphront/typeahead.css' => 'a4a21016',
- 'rsrc/css/application/almanac/almanac.css' => 'dbb9b3af',
- 'rsrc/css/application/auth/auth.css' => '0877ed6e',
- 'rsrc/css/application/base/main-menu-view.css' => '1802a242',
- 'rsrc/css/application/base/notification-menu.css' => 'ef480927',
- 'rsrc/css/application/base/phui-theme.css' => '9f261c6b',
- 'rsrc/css/application/base/standard-page-view.css' => '34ee718b',
- 'rsrc/css/application/chatlog/chatlog.css' => 'd295b020',
- 'rsrc/css/application/conduit/conduit-api.css' => '7bc725c4',
- 'rsrc/css/application/config/config-options.css' => '4615667b',
- 'rsrc/css/application/config/config-template.css' => '8f18fa41',
- 'rsrc/css/application/config/setup-issue.css' => '30ee0173',
- 'rsrc/css/application/config/unhandled-exception.css' => '4c96257a',
- 'rsrc/css/application/conpherence/color.css' => 'abb4c358',
- 'rsrc/css/application/conpherence/durable-column.css' => '89ea6bef',
- 'rsrc/css/application/conpherence/header-pane.css' => 'cb6f4e19',
- 'rsrc/css/application/conpherence/menu.css' => '69368e97',
- 'rsrc/css/application/conpherence/message-pane.css' => 'b0f55ecc',
- 'rsrc/css/application/conpherence/notification.css' => 'cef0a3fc',
- 'rsrc/css/application/conpherence/participant-pane.css' => '26a3ce56',
- 'rsrc/css/application/conpherence/transaction.css' => '85129c68',
- 'rsrc/css/application/contentsource/content-source-view.css' => '4b8b05d4',
- 'rsrc/css/application/countdown/timer.css' => '16c52f5c',
- 'rsrc/css/application/daemon/bulk-job.css' => 'df9c1d4a',
- 'rsrc/css/application/dashboard/dashboard.css' => 'fe5b1869',
- 'rsrc/css/application/diff/inline-comment-summary.css' => 'f23d4e8f',
- 'rsrc/css/application/differential/add-comment.css' => 'c47f8c40',
- 'rsrc/css/application/differential/changeset-view.css' => 'db34a142',
- 'rsrc/css/application/differential/core.css' => '5b7b8ff4',
- 'rsrc/css/application/differential/phui-inline-comment.css' => '65ae3bc2',
- 'rsrc/css/application/differential/revision-comment.css' => '14b8565a',
- 'rsrc/css/application/differential/revision-history.css' => '0e8eb855',
- 'rsrc/css/application/differential/revision-list.css' => 'f3c47d33',
- 'rsrc/css/application/differential/table-of-contents.css' => 'ae4b7a55',
- 'rsrc/css/application/diffusion/diffusion-icons.css' => '0c15255e',
- 'rsrc/css/application/diffusion/diffusion-readme.css' => '419dd5b6',
- 'rsrc/css/application/diffusion/diffusion-repository.css' => 'ee6f20ec',
- 'rsrc/css/application/diffusion/diffusion.css' => '45727264',
- 'rsrc/css/application/feed/feed.css' => 'ecd4ec57',
- 'rsrc/css/application/files/global-drag-and-drop.css' => 'b556a948',
- 'rsrc/css/application/flag/flag.css' => 'bba8f811',
- 'rsrc/css/application/harbormaster/harbormaster.css' => '7446ce72',
- 'rsrc/css/application/herald/herald-test.css' => 'a52e323e',
- 'rsrc/css/application/herald/herald.css' => 'cd8d0134',
- 'rsrc/css/application/maniphest/report.css' => '9b9580b7',
- 'rsrc/css/application/maniphest/task-edit.css' => 'fda62a9b',
- 'rsrc/css/application/maniphest/task-summary.css' => '11cc5344',
- 'rsrc/css/application/objectselector/object-selector.css' => '85ee8ce6',
- 'rsrc/css/application/owners/owners-path-editor.css' => '9c136c29',
- 'rsrc/css/application/paste/paste.css' => '9fcc9773',
- 'rsrc/css/application/people/people-picture-menu-item.css' => 'a06f7f34',
- 'rsrc/css/application/people/people-profile.css' => '4df76faf',
- 'rsrc/css/application/phame/phame.css' => '8cb3afcd',
- 'rsrc/css/application/pholio/pholio-edit.css' => '07676f51',
- 'rsrc/css/application/pholio/pholio-inline-comments.css' => '8e545e49',
- 'rsrc/css/application/pholio/pholio.css' => 'ca89d380',
- 'rsrc/css/application/phortune/phortune-credit-card-form.css' => '8391eb02',
- 'rsrc/css/application/phortune/phortune-invoice.css' => '476055e2',
- 'rsrc/css/application/phortune/phortune.css' => '5b99dae0',
- 'rsrc/css/application/phrequent/phrequent.css' => 'ffc185ad',
- 'rsrc/css/application/phriction/phriction-document-css.css' => '4282e4ad',
- 'rsrc/css/application/policy/policy-edit.css' => '815c66f7',
- 'rsrc/css/application/policy/policy-transaction-detail.css' => '82100a43',
- 'rsrc/css/application/policy/policy.css' => '957ea14c',
- 'rsrc/css/application/ponder/ponder-view.css' => 'fbd45f96',
- 'rsrc/css/application/project/project-card-view.css' => '0010bb52',
- 'rsrc/css/application/project/project-view.css' => '792c9057',
- 'rsrc/css/application/releeph/releeph-core.css' => '9b3c5733',
- 'rsrc/css/application/releeph/releeph-preview-branch.css' => 'b7a6f4a5',
- 'rsrc/css/application/releeph/releeph-request-differential-create-dialog.css' => '8d8b92cd',
- 'rsrc/css/application/releeph/releeph-request-typeahead.css' => '667a48ae',
- 'rsrc/css/application/search/application-search-view.css' => '787f5b76',
- 'rsrc/css/application/search/search-results.css' => '505dd8cf',
- 'rsrc/css/application/slowvote/slowvote.css' => 'a94b7230',
- 'rsrc/css/application/tokens/tokens.css' => '3d0f239e',
- 'rsrc/css/application/uiexample/example.css' => '528b19de',
- 'rsrc/css/core/core.css' => '62fa3ace',
- 'rsrc/css/core/remarkup.css' => 'f1701b75',
- 'rsrc/css/core/syntax.css' => 'e9c95dd4',
- 'rsrc/css/core/z-index.css' => '9d8f7c4b',
- 'rsrc/css/diviner/diviner-shared.css' => '896f1d43',
- 'rsrc/css/font/font-awesome.css' => 'e838e088',
- 'rsrc/css/font/font-lato.css' => 'c7ccd872',
- 'rsrc/css/font/phui-font-icon-base.css' => '870a7360',
- 'rsrc/css/katex.min.css' => '297123ca',
- 'rsrc/css/layout/phabricator-filetree-view.css' => 'b912ad97',
- 'rsrc/css/layout/phabricator-source-code-view.css' => '2ab25dfa',
- 'rsrc/css/phui/button/phui-button-bar.css' => 'f1ff5494',
- 'rsrc/css/phui/button/phui-button-simple.css' => '8e1baf68',
- 'rsrc/css/phui/button/phui-button.css' => '6ccb303c',
- 'rsrc/css/phui/calendar/phui-calendar-day.css' => '572b1893',
- 'rsrc/css/phui/calendar/phui-calendar-list.css' => '576be600',
- 'rsrc/css/phui/calendar/phui-calendar-month.css' => '21154caf',
- 'rsrc/css/phui/calendar/phui-calendar.css' => 'f1ddf11c',
- 'rsrc/css/phui/object-item/phui-oi-big-ui.css' => '7a7c22af',
- 'rsrc/css/phui/object-item/phui-oi-color.css' => 'cd2b9b77',
- 'rsrc/css/phui/object-item/phui-oi-drag-ui.css' => '08f4ccc3',
- 'rsrc/css/phui/object-item/phui-oi-flush-ui.css' => '9d9685d6',
- 'rsrc/css/phui/object-item/phui-oi-list-view.css' => '7c5c1291',
- 'rsrc/css/phui/object-item/phui-oi-simple-ui.css' => 'a8beebea',
- 'rsrc/css/phui/phui-action-list.css' => '0bcd9a45',
- 'rsrc/css/phui/phui-action-panel.css' => 'b4798122',
- 'rsrc/css/phui/phui-badge.css' => '22c0cf4f',
- 'rsrc/css/phui/phui-basic-nav-view.css' => '98c11ab3',
- 'rsrc/css/phui/phui-big-info-view.css' => 'acc3492c',
- 'rsrc/css/phui/phui-box.css' => '4bd6cdb9',
- 'rsrc/css/phui/phui-bulk-editor.css' => '9a81e5d5',
- 'rsrc/css/phui/phui-chart.css' => '6bf6f78e',
- 'rsrc/css/phui/phui-cms.css' => '504b4b23',
- 'rsrc/css/phui/phui-comment-form.css' => 'ac68149f',
- 'rsrc/css/phui/phui-comment-panel.css' => 'f50152ad',
- 'rsrc/css/phui/phui-crumbs-view.css' => '10728aaa',
- 'rsrc/css/phui/phui-curtain-view.css' => '2bdaf026',
- 'rsrc/css/phui/phui-document-pro.css' => 'dd79b5df',
- 'rsrc/css/phui/phui-document-summary.css' => '9ca48bdf',
- 'rsrc/css/phui/phui-document.css' => '443bb464',
- 'rsrc/css/phui/phui-feed-story.css' => '44a9c8e9',
- 'rsrc/css/phui/phui-fontkit.css' => '1320ed01',
- 'rsrc/css/phui/phui-form-view.css' => '2f43fae7',
- 'rsrc/css/phui/phui-form.css' => '7aaa04e3',
- 'rsrc/css/phui/phui-head-thing.css' => 'fd311e5f',
- 'rsrc/css/phui/phui-header-view.css' => '1ba8b707',
- 'rsrc/css/phui/phui-hovercard.css' => '4a484541',
- 'rsrc/css/phui/phui-icon-set-selector.css' => '87db8fee',
- 'rsrc/css/phui/phui-icon.css' => 'cf24ceec',
- 'rsrc/css/phui/phui-image-mask.css' => 'a8498f9c',
- 'rsrc/css/phui/phui-info-view.css' => 'e929f98c',
- 'rsrc/css/phui/phui-invisible-character-view.css' => '6993d9f0',
- 'rsrc/css/phui/phui-left-right.css' => '75227a4d',
- 'rsrc/css/phui/phui-lightbox.css' => '0a035e40',
- 'rsrc/css/phui/phui-list.css' => '38f8c9bd',
- 'rsrc/css/phui/phui-object-box.css' => '9cff003c',
- 'rsrc/css/phui/phui-pager.css' => 'edcbc226',
- 'rsrc/css/phui/phui-pinboard-view.css' => '2495140e',
- 'rsrc/css/phui/phui-property-list-view.css' => '546a04ae',
- 'rsrc/css/phui/phui-remarkup-preview.css' => '54a34863',
- 'rsrc/css/phui/phui-segment-bar-view.css' => 'b1d1b892',
- 'rsrc/css/phui/phui-spacing.css' => '042804d6',
- 'rsrc/css/phui/phui-status.css' => 'd5263e49',
- 'rsrc/css/phui/phui-tag-view.css' => 'b4719c50',
- 'rsrc/css/phui/phui-timeline-view.css' => '6ddf8126',
- 'rsrc/css/phui/phui-two-column-view.css' => '44ec4951',
- 'rsrc/css/phui/workboards/phui-workboard-color.css' => '783cdff5',
- 'rsrc/css/phui/workboards/phui-workboard.css' => '3bc85455',
- 'rsrc/css/phui/workboards/phui-workcard.css' => 'cca5fa92',
- 'rsrc/css/phui/workboards/phui-workpanel.css' => 'a3a63478',
- 'rsrc/css/sprite-login.css' => '396f3c3a',
- 'rsrc/css/sprite-tokens.css' => '9cdfd599',
- 'rsrc/css/syntax/syntax-default.css' => '9923583c',
- 'rsrc/externals/d3/d3.min.js' => 'a11a5ff2',
- 'rsrc/externals/font/fontawesome/fontawesome-webfont.eot' => '24a7064f',
- 'rsrc/externals/font/fontawesome/fontawesome-webfont.ttf' => '0039fe26',
- 'rsrc/externals/font/fontawesome/fontawesome-webfont.woff' => 'de978a43',
- 'rsrc/externals/font/fontawesome/fontawesome-webfont.woff2' => '2a832057',
- 'rsrc/externals/font/katex/KaTeX_AMS-Regular.eot' => 'a072da31',
- 'rsrc/externals/font/katex/KaTeX_AMS-Regular.ttf' => '9b7d7c57',
- 'rsrc/externals/font/katex/KaTeX_AMS-Regular.woff' => '68bf842e',
- 'rsrc/externals/font/katex/KaTeX_AMS-Regular.woff2' => '1726711a',
- 'rsrc/externals/font/katex/KaTeX_Caligraphic-Bold.eot' => '5d91cbdb',
- 'rsrc/externals/font/katex/KaTeX_Caligraphic-Bold.ttf' => '03559cd7',
- 'rsrc/externals/font/katex/KaTeX_Caligraphic-Bold.woff' => 'cb0acbc6',
- 'rsrc/externals/font/katex/KaTeX_Caligraphic-Bold.woff2' => 'a98ed9ec',
- 'rsrc/externals/font/katex/KaTeX_Caligraphic-Regular.eot' => '40c2c8a3',
- 'rsrc/externals/font/katex/KaTeX_Caligraphic-Regular.ttf' => 'a48d62bc',
- 'rsrc/externals/font/katex/KaTeX_Caligraphic-Regular.woff' => 'be0d08c8',
- 'rsrc/externals/font/katex/KaTeX_Caligraphic-Regular.woff2' => 'bdc3c550',
- 'rsrc/externals/font/katex/KaTeX_Fraktur-Bold.eot' => '2729cba3',
- 'rsrc/externals/font/katex/KaTeX_Fraktur-Bold.ttf' => '8116dfda',
- 'rsrc/externals/font/katex/KaTeX_Fraktur-Bold.woff' => '46415366',
- 'rsrc/externals/font/katex/KaTeX_Fraktur-Bold.woff2' => '26b00736',
- 'rsrc/externals/font/katex/KaTeX_Fraktur-Regular.eot' => '5a494f47',
- 'rsrc/externals/font/katex/KaTeX_Fraktur-Regular.ttf' => 'fc73f831',
- 'rsrc/externals/font/katex/KaTeX_Fraktur-Regular.woff' => 'c15bd024',
- 'rsrc/externals/font/katex/KaTeX_Fraktur-Regular.woff2' => 'daf53ac5',
- 'rsrc/externals/font/katex/KaTeX_Main-Bold.eot' => '0022698c',
- 'rsrc/externals/font/katex/KaTeX_Main-Bold.ttf' => '096cbaff',
- 'rsrc/externals/font/katex/KaTeX_Main-Bold.woff' => '531dbee4',
- 'rsrc/externals/font/katex/KaTeX_Main-Bold.woff2' => '5b6df003',
- 'rsrc/externals/font/katex/KaTeX_Main-Italic.eot' => '9d90bdcf',
- 'rsrc/externals/font/katex/KaTeX_Main-Italic.ttf' => 'a56597b9',
- 'rsrc/externals/font/katex/KaTeX_Main-Italic.woff' => '5b5baa86',
- 'rsrc/externals/font/katex/KaTeX_Main-Italic.woff2' => '0285a44c',
- 'rsrc/externals/font/katex/KaTeX_Main-Regular.eot' => '91d757f2',
- 'rsrc/externals/font/katex/KaTeX_Main-Regular.ttf' => '4d5728b0',
- 'rsrc/externals/font/katex/KaTeX_Main-Regular.woff' => '4547be88',
- 'rsrc/externals/font/katex/KaTeX_Main-Regular.woff2' => '47d552f7',
- 'rsrc/externals/font/katex/KaTeX_Math-BoldItalic.eot' => '7b1aa71e',
- 'rsrc/externals/font/katex/KaTeX_Math-BoldItalic.ttf' => '6170bdba',
- 'rsrc/externals/font/katex/KaTeX_Math-BoldItalic.woff' => '4bc7d0b0',
- 'rsrc/externals/font/katex/KaTeX_Math-BoldItalic.woff2' => '2e87e47c',
- 'rsrc/externals/font/katex/KaTeX_Math-Italic.eot' => '7c4b77f8',
- 'rsrc/externals/font/katex/KaTeX_Math-Italic.ttf' => '7486170d',
- 'rsrc/externals/font/katex/KaTeX_Math-Italic.woff' => '19751090',
- 'rsrc/externals/font/katex/KaTeX_Math-Italic.woff2' => 'f5f1ab2d',
- 'rsrc/externals/font/katex/KaTeX_Math-Regular.eot' => 'b0da8961',
- 'rsrc/externals/font/katex/KaTeX_Math-Regular.ttf' => '846a9d9b',
- 'rsrc/externals/font/katex/KaTeX_Math-Regular.woff' => '9c57b47d',
- 'rsrc/externals/font/katex/KaTeX_Math-Regular.woff2' => 'ac338b97',
- 'rsrc/externals/font/katex/KaTeX_SansSerif-Bold.eot' => '2dcf1c55',
- 'rsrc/externals/font/katex/KaTeX_SansSerif-Bold.ttf' => '0711f827',
- 'rsrc/externals/font/katex/KaTeX_SansSerif-Bold.woff' => '844ead36',
- 'rsrc/externals/font/katex/KaTeX_SansSerif-Bold.woff2' => '76d32fb1',
- 'rsrc/externals/font/katex/KaTeX_SansSerif-Italic.eot' => '741eb796',
- 'rsrc/externals/font/katex/KaTeX_SansSerif-Italic.ttf' => '632cf898',
- 'rsrc/externals/font/katex/KaTeX_SansSerif-Italic.woff' => 'f3bc7fd8',
- 'rsrc/externals/font/katex/KaTeX_SansSerif-Italic.woff2' => '3526241c',
- 'rsrc/externals/font/katex/KaTeX_SansSerif-Regular.eot' => '0577c8eb',
- 'rsrc/externals/font/katex/KaTeX_SansSerif-Regular.ttf' => 'b1c7474c',
- 'rsrc/externals/font/katex/KaTeX_SansSerif-Regular.woff' => '18381725',
- 'rsrc/externals/font/katex/KaTeX_SansSerif-Regular.woff2' => 'f66cabf5',
- 'rsrc/externals/font/katex/KaTeX_Script-Regular.eot' => '72a9ab41',
- 'rsrc/externals/font/katex/KaTeX_Script-Regular.ttf' => '7a1f0af6',
- 'rsrc/externals/font/katex/KaTeX_Script-Regular.woff' => '9cea9f6b',
- 'rsrc/externals/font/katex/KaTeX_Script-Regular.woff2' => '77a8bf3d',
- 'rsrc/externals/font/katex/KaTeX_Size1-Regular.eot' => 'ba072f12',
- 'rsrc/externals/font/katex/KaTeX_Size1-Regular.ttf' => '7902fc1d',
- 'rsrc/externals/font/katex/KaTeX_Size1-Regular.woff' => '97d7f188',
- 'rsrc/externals/font/katex/KaTeX_Size1-Regular.woff2' => '8b119e4c',
- 'rsrc/externals/font/katex/KaTeX_Size2-Regular.eot' => 'ca4ff245',
- 'rsrc/externals/font/katex/KaTeX_Size2-Regular.ttf' => '0c3d257a',
- 'rsrc/externals/font/katex/KaTeX_Size2-Regular.woff' => '802221cb',
- 'rsrc/externals/font/katex/KaTeX_Size2-Regular.woff2' => '65aed7c3',
- 'rsrc/externals/font/katex/KaTeX_Size3-Regular.eot' => '82fa9ba9',
- 'rsrc/externals/font/katex/KaTeX_Size3-Regular.ttf' => '75619111',
- 'rsrc/externals/font/katex/KaTeX_Size3-Regular.woff' => 'd4dd5063',
- 'rsrc/externals/font/katex/KaTeX_Size3-Regular.woff2' => 'f7cc13ed',
- 'rsrc/externals/font/katex/KaTeX_Size4-Regular.eot' => '3638e7a5',
- 'rsrc/externals/font/katex/KaTeX_Size4-Regular.ttf' => '6e7a06d3',
- 'rsrc/externals/font/katex/KaTeX_Size4-Regular.woff' => '25250cb8',
- 'rsrc/externals/font/katex/KaTeX_Size4-Regular.woff2' => 'f3e00be6',
- 'rsrc/externals/font/katex/KaTeX_Typewriter-Regular.eot' => '589b02a7',
- 'rsrc/externals/font/katex/KaTeX_Typewriter-Regular.ttf' => 'c61ce183',
- 'rsrc/externals/font/katex/KaTeX_Typewriter-Regular.woff' => '7b2cd934',
- 'rsrc/externals/font/katex/KaTeX_Typewriter-Regular.woff2' => 'ccbb0a99',
- 'rsrc/externals/font/lato/lato-bold.eot' => '99fbcf8c',
- 'rsrc/externals/font/lato/lato-bold.svg' => '2aa83045',
- 'rsrc/externals/font/lato/lato-bold.ttf' => '0a7141f7',
- 'rsrc/externals/font/lato/lato-bold.woff' => 'f5db2061',
- 'rsrc/externals/font/lato/lato-bold.woff2' => '37a94ecd',
- 'rsrc/externals/font/lato/lato-bolditalic.eot' => 'b93389d0',
- 'rsrc/externals/font/lato/lato-bolditalic.svg' => '5442e1ef',
- 'rsrc/externals/font/lato/lato-bolditalic.ttf' => 'dad31252',
- 'rsrc/externals/font/lato/lato-bolditalic.woff' => 'e53bcf47',
- 'rsrc/externals/font/lato/lato-bolditalic.woff2' => 'd035007f',
- 'rsrc/externals/font/lato/lato-italic.eot' => '6a903f5d',
- 'rsrc/externals/font/lato/lato-italic.svg' => '0dc7cf2f',
- 'rsrc/externals/font/lato/lato-italic.ttf' => '629f64f0',
- 'rsrc/externals/font/lato/lato-italic.woff' => '678dc4bb',
- 'rsrc/externals/font/lato/lato-italic.woff2' => '7c8dd650',
- 'rsrc/externals/font/lato/lato-regular.eot' => '848dfb1e',
- 'rsrc/externals/font/lato/lato-regular.svg' => 'cbd5fd6b',
- 'rsrc/externals/font/lato/lato-regular.ttf' => 'e270165b',
- 'rsrc/externals/font/lato/lato-regular.woff' => '13d39fe2',
- 'rsrc/externals/font/lato/lato-regular.woff2' => '57a9f742',
- 'rsrc/externals/javelin/core/Event.js' => 'ef7e057f',
- 'rsrc/externals/javelin/core/Stratcom.js' => '327f418a',
- 'rsrc/externals/javelin/core/__tests__/event-stop-and-kill.js' => '717554e4',
- 'rsrc/externals/javelin/core/__tests__/install.js' => 'c432ee85',
- 'rsrc/externals/javelin/core/__tests__/stratcom.js' => '88bf7313',
- 'rsrc/externals/javelin/core/__tests__/util.js' => 'e251703d',
- 'rsrc/externals/javelin/core/init.js' => '8d83d2a1',
- 'rsrc/externals/javelin/core/init_node.js' => 'f7732951',
- 'rsrc/externals/javelin/core/install.js' => '05270951',
- 'rsrc/externals/javelin/core/util.js' => '93cc50d6',
- 'rsrc/externals/javelin/docs/Base.js' => '74676256',
- 'rsrc/externals/javelin/docs/onload.js' => 'e819c479',
- 'rsrc/externals/javelin/ext/fx/Color.js' => '7e41274a',
- 'rsrc/externals/javelin/ext/fx/FX.js' => '54b612ba',
- 'rsrc/externals/javelin/ext/reactor/core/DynVal.js' => 'f6555212',
- 'rsrc/externals/javelin/ext/reactor/core/Reactor.js' => '2b8de964',
- 'rsrc/externals/javelin/ext/reactor/core/ReactorNode.js' => '1ad0a787',
- 'rsrc/externals/javelin/ext/reactor/core/ReactorNodeCalmer.js' => '76f4ebed',
- 'rsrc/externals/javelin/ext/reactor/dom/RDOM.js' => 'c90a04fc',
- 'rsrc/externals/javelin/ext/view/HTMLView.js' => 'fe287620',
- 'rsrc/externals/javelin/ext/view/View.js' => '0f764c35',
- 'rsrc/externals/javelin/ext/view/ViewInterpreter.js' => 'f829edb3',
- 'rsrc/externals/javelin/ext/view/ViewPlaceholder.js' => '47830651',
- 'rsrc/externals/javelin/ext/view/ViewRenderer.js' => '6c2b09a2',
- 'rsrc/externals/javelin/ext/view/ViewVisitor.js' => 'efe49472',
- 'rsrc/externals/javelin/ext/view/__tests__/HTMLView.js' => 'f92d7bcb',
- 'rsrc/externals/javelin/ext/view/__tests__/View.js' => '6450b38b',
- 'rsrc/externals/javelin/ext/view/__tests__/ViewInterpreter.js' => '7a94d6a5',
- 'rsrc/externals/javelin/ext/view/__tests__/ViewRenderer.js' => '6ea96ac9',
- 'rsrc/externals/javelin/lib/Cookie.js' => '62dfea03',
- 'rsrc/externals/javelin/lib/DOM.js' => '4976858c',
- 'rsrc/externals/javelin/lib/History.js' => 'd4505101',
- 'rsrc/externals/javelin/lib/JSON.js' => '69adf288',
- 'rsrc/externals/javelin/lib/Leader.js' => '7f243deb',
- 'rsrc/externals/javelin/lib/Mask.js' => '8a41885b',
- 'rsrc/externals/javelin/lib/Quicksand.js' => '6b8ef10b',
- 'rsrc/externals/javelin/lib/Request.js' => '94b750d2',
- 'rsrc/externals/javelin/lib/Resource.js' => '44959b73',
- 'rsrc/externals/javelin/lib/Routable.js' => 'b3e7d692',
- 'rsrc/externals/javelin/lib/Router.js' => '29274e2b',
- 'rsrc/externals/javelin/lib/Scrollbar.js' => '9065f639',
- 'rsrc/externals/javelin/lib/Sound.js' => '949c0fe5',
- 'rsrc/externals/javelin/lib/URI.js' => 'c989ade3',
- 'rsrc/externals/javelin/lib/Vector.js' => '2caa8fb8',
- 'rsrc/externals/javelin/lib/WebSocket.js' => '3ffe32d6',
- 'rsrc/externals/javelin/lib/Workflow.js' => '6a726c55',
- 'rsrc/externals/javelin/lib/__tests__/Cookie.js' => '5ed109e8',
- 'rsrc/externals/javelin/lib/__tests__/DOM.js' => 'c984504b',
- 'rsrc/externals/javelin/lib/__tests__/JSON.js' => '837a7d68',
- 'rsrc/externals/javelin/lib/__tests__/URI.js' => '1e45fda9',
- 'rsrc/externals/javelin/lib/__tests__/behavior.js' => '1ea62783',
- 'rsrc/externals/javelin/lib/behavior.js' => '61cbc29a',
- 'rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js' => 'bb6e5c16',
- 'rsrc/externals/javelin/lib/control/typeahead/Typeahead.js' => '70baed2f',
- 'rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js' => '185bbd53',
- 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadCompositeSource.js' => '503e17fd',
- 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js' => '013ffff9',
- 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js' => '54f314a0',
- 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js' => 'ab9e0a82',
- 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadStaticSource.js' => '6c0e62fa',
- 'rsrc/favicons/favicon-16x16.png' => 'fc6275ba',
- 'rsrc/favicons/mask-icon.svg' => 'e132a80f',
- 'rsrc/image/BFCFDA.png' => 'd5ec91f4',
- 'rsrc/image/actions/edit.png' => '2fc41442',
- 'rsrc/image/avatar.png' => '17d346a4',
- 'rsrc/image/checker_dark.png' => 'd8e65881',
- 'rsrc/image/checker_light.png' => 'a0155918',
- 'rsrc/image/checker_lighter.png' => 'd5da91b6',
- 'rsrc/image/controls/checkbox-checked.png' => 'ad6441ea',
- 'rsrc/image/controls/checkbox-unchecked.png' => '8eb1f0ae',
- 'rsrc/image/d5d8e1.png' => '0c2a1497',
- 'rsrc/image/darkload.gif' => '1ffd3ec6',
- 'rsrc/image/divot.png' => '94dded62',
- 'rsrc/image/examples/hero.png' => '979a86ae',
- 'rsrc/image/grippy_texture.png' => 'aca81e2f',
- 'rsrc/image/icon/fatcow/arrow_branch.png' => '2537c01c',
- 'rsrc/image/icon/fatcow/arrow_merge.png' => '21b660e0',
- 'rsrc/image/icon/fatcow/calendar_edit.png' => '24632275',
- 'rsrc/image/icon/fatcow/document_black.png' => '45fe1c60',
- 'rsrc/image/icon/fatcow/flag_blue.png' => 'a01abb1d',
- 'rsrc/image/icon/fatcow/flag_finish.png' => '67825cee',
- 'rsrc/image/icon/fatcow/flag_ghost.png' => '20ca8783',
- 'rsrc/image/icon/fatcow/flag_green.png' => '7e0eaa7a',
- 'rsrc/image/icon/fatcow/flag_orange.png' => '9e73df66',
- 'rsrc/image/icon/fatcow/flag_pink.png' => '7e92f3b2',
- 'rsrc/image/icon/fatcow/flag_purple.png' => 'cc517522',
- 'rsrc/image/icon/fatcow/flag_red.png' => '04ec726f',
- 'rsrc/image/icon/fatcow/flag_yellow.png' => '73946fd4',
- 'rsrc/image/icon/fatcow/key_question.png' => '52a0c26a',
- 'rsrc/image/icon/fatcow/link.png' => '7afd4d5e',
- 'rsrc/image/icon/fatcow/page_white_edit.png' => '39a2eed8',
- 'rsrc/image/icon/fatcow/page_white_put.png' => '08c95a0c',
- 'rsrc/image/icon/fatcow/source/conduit.png' => '4ea01d2f',
- 'rsrc/image/icon/fatcow/source/email.png' => '9bab3239',
- 'rsrc/image/icon/fatcow/source/fax.png' => '04195e68',
- 'rsrc/image/icon/fatcow/source/mobile.png' => 'f1321264',
- 'rsrc/image/icon/fatcow/source/tablet.png' => '49396799',
- 'rsrc/image/icon/fatcow/source/web.png' => '136ccb5d',
- 'rsrc/image/icon/subscribe.png' => 'd03ed5a5',
- 'rsrc/image/icon/tango/attachment.png' => 'ecc8022e',
- 'rsrc/image/icon/tango/edit.png' => '929a1363',
- 'rsrc/image/icon/tango/go-down.png' => '96d95e43',
- 'rsrc/image/icon/tango/log.png' => 'b08cc63a',
- 'rsrc/image/icon/tango/upload.png' => '7bbb7984',
- 'rsrc/image/icon/unsubscribe.png' => '25725013',
- 'rsrc/image/lightblue-header.png' => '5c168b6d',
- 'rsrc/image/logo/light-eye.png' => '1a576ddd',
- 'rsrc/image/main_texture.png' => '29a2c5ad',
- 'rsrc/image/menu_texture.png' => '5a17580d',
- 'rsrc/image/people/harding.png' => '45aa614e',
- 'rsrc/image/people/jefferson.png' => 'afca0e53',
- 'rsrc/image/people/lincoln.png' => '9369126d',
- 'rsrc/image/people/mckinley.png' => 'fb8f16ce',
- 'rsrc/image/people/taft.png' => 'd7bc402c',
- 'rsrc/image/people/user0.png' => '03dacaea',
- 'rsrc/image/people/user1.png' => '4a4e7702',
- 'rsrc/image/people/user2.png' => '47a0ee40',
- 'rsrc/image/people/user3.png' => '835ff627',
- 'rsrc/image/people/user4.png' => 'b0e830f1',
- 'rsrc/image/people/user5.png' => '9c95b369',
- 'rsrc/image/people/user6.png' => 'ba3fbfb0',
- 'rsrc/image/people/user7.png' => 'da613924',
- 'rsrc/image/people/user8.png' => 'f1035edf',
- 'rsrc/image/people/user9.png' => '66730be3',
- 'rsrc/image/people/washington.png' => '40dd301c',
- 'rsrc/image/phrequent_active.png' => 'a466a8ed',
- 'rsrc/image/phrequent_inactive.png' => 'bfc15a69',
- 'rsrc/image/resize.png' => 'fd476de4',
- 'rsrc/image/sprite-login-X2.png' => '308c92c4',
- 'rsrc/image/sprite-login.png' => '9ec54245',
- 'rsrc/image/sprite-tokens-X2.png' => '804a5232',
- 'rsrc/image/sprite-tokens.png' => 'b41d03da',
- 'rsrc/image/texture/card-gradient.png' => '815f26e8',
- 'rsrc/image/texture/dark-menu-hover.png' => '5fa7ece8',
- 'rsrc/image/texture/dark-menu.png' => '7e22296e',
- 'rsrc/image/texture/grip.png' => '719404f3',
- 'rsrc/image/texture/panel-header-gradient.png' => 'e3b8dcfe',
- 'rsrc/image/texture/phlnx-bg.png' => '8d819209',
- 'rsrc/image/texture/pholio-background.gif' => 'ba29239c',
- 'rsrc/image/texture/table_header.png' => '5c433037',
- 'rsrc/image/texture/table_header_hover.png' => '038ec3b9',
- 'rsrc/image/texture/table_header_tall.png' => 'd56b434f',
- 'rsrc/js/application/aphlict/Aphlict.js' => 'e1d4b11a',
- 'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => 'caade6f2',
- 'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => '599a8f5f',
- 'rsrc/js/application/aphlict/behavior-aphlict-status.js' => '5e2634b9',
- 'rsrc/js/application/aphlict/behavior-desktop-notifications-control.js' => '27ca6289',
- 'rsrc/js/application/calendar/behavior-day-view.js' => '4b3c4443',
- 'rsrc/js/application/calendar/behavior-event-all-day.js' => 'b41537c9',
- 'rsrc/js/application/calendar/behavior-month-view.js' => 'fe33e256',
- 'rsrc/js/application/config/behavior-reorder-fields.js' => 'b6993408',
- 'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => '4d863052',
- 'rsrc/js/application/conpherence/behavior-conpherence-search.js' => '9bbf3762',
- 'rsrc/js/application/conpherence/behavior-durable-column.js' => '2ae077e1',
- 'rsrc/js/application/conpherence/behavior-menu.js' => '4047cd35',
- 'rsrc/js/application/conpherence/behavior-participant-pane.js' => 'd057e45a',
- 'rsrc/js/application/conpherence/behavior-pontificate.js' => '55616e04',
- 'rsrc/js/application/conpherence/behavior-quicksand-blacklist.js' => '7927a7d3',
- 'rsrc/js/application/conpherence/behavior-toggle-widget.js' => '3dbf94d5',
- 'rsrc/js/application/countdown/timer.js' => 'e4cc26b3',
- 'rsrc/js/application/daemon/behavior-bulk-job-reload.js' => 'edf8a145',
- 'rsrc/js/application/dashboard/behavior-dashboard-async-panel.js' => '469c0d9e',
- 'rsrc/js/application/dashboard/behavior-dashboard-move-panels.js' => '408bf173',
- 'rsrc/js/application/dashboard/behavior-dashboard-query-panel-select.js' => '453c5375',
- 'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => 'd4eecc63',
- 'rsrc/js/application/diff/DiffChangeset.js' => 'b49b59d6',
- 'rsrc/js/application/diff/DiffChangesetList.js' => '0a84bcc1',
- 'rsrc/js/application/diff/DiffInline.js' => 'e83d28f3',
- 'rsrc/js/application/diff/behavior-preview-link.js' => '051c7832',
- 'rsrc/js/application/differential/behavior-comment-preview.js' => '51c5ad07',
- 'rsrc/js/application/differential/behavior-diff-radios.js' => 'e1ff79b1',
- 'rsrc/js/application/differential/behavior-populate.js' => 'f0eb6708',
- 'rsrc/js/application/differential/behavior-user-select.js' => 'a8d8459d',
- 'rsrc/js/application/diffusion/DiffusionLocateFileSource.js' => '00676f00',
- 'rsrc/js/application/diffusion/behavior-audit-preview.js' => 'd835b03a',
- 'rsrc/js/application/diffusion/behavior-commit-branches.js' => 'bdaf4d04',
- 'rsrc/js/application/diffusion/behavior-commit-graph.js' => '75b83cbb',
- 'rsrc/js/application/diffusion/behavior-locate-file.js' => '6d3e1947',
- 'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => 'f01586dc',
- 'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => '1db13e70',
- 'rsrc/js/application/drydock/drydock-live-operation-status.js' => '901935ef',
- 'rsrc/js/application/files/behavior-document-engine.js' => '3935d8c4',
- 'rsrc/js/application/files/behavior-icon-composer.js' => '8499b6ab',
- 'rsrc/js/application/files/behavior-launch-icon-composer.js' => '48086888',
- 'rsrc/js/application/harbormaster/behavior-harbormaster-log.js' => '549459b8',
- 'rsrc/js/application/herald/HeraldRuleEditor.js' => 'dca75c0e',
- 'rsrc/js/application/herald/PathTypeahead.js' => '6d8c7912',
- 'rsrc/js/application/herald/herald-rule-editor.js' => '7ebaeed3',
- 'rsrc/js/application/maniphest/behavior-batch-selector.js' => 'ad54037e',
- 'rsrc/js/application/maniphest/behavior-line-chart.js' => 'e4232876',
- 'rsrc/js/application/maniphest/behavior-list-edit.js' => 'a9f88de2',
- 'rsrc/js/application/maniphest/behavior-subpriorityeditor.js' => '71237763',
- 'rsrc/js/application/owners/OwnersPathEditor.js' => 'c96502cf',
- 'rsrc/js/application/owners/owners-path-editor.js' => '7a68dda3',
- 'rsrc/js/application/passphrase/passphrase-credential-control.js' => '3cb0b2fc',
- 'rsrc/js/application/pholio/behavior-pholio-mock-edit.js' => 'bee502c8',
- 'rsrc/js/application/pholio/behavior-pholio-mock-view.js' => 'ec1f3669',
- 'rsrc/js/application/phortune/behavior-stripe-payment-form.js' => 'a6b98425',
- 'rsrc/js/application/phortune/behavior-test-payment-form.js' => 'fc91ab6c',
- 'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef',
- 'rsrc/js/application/policy/behavior-policy-control.js' => 'd0c516d5',
- 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '5e9f347c',
- 'rsrc/js/application/projects/WorkboardBoard.js' => '8935deef',
- 'rsrc/js/application/projects/WorkboardCard.js' => 'c587b80f',
- 'rsrc/js/application/projects/WorkboardColumn.js' => '758b4758',
- 'rsrc/js/application/projects/WorkboardController.js' => '26167537',
- 'rsrc/js/application/projects/behavior-project-boards.js' => '4250a34e',
- 'rsrc/js/application/projects/behavior-project-create.js' => '065227cc',
- 'rsrc/js/application/projects/behavior-reorder-columns.js' => 'e1d25dfb',
- 'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf',
- 'rsrc/js/application/releeph/releeph-request-state-change.js' => 'a0b57eb8',
- 'rsrc/js/application/releeph/releeph-request-typeahead.js' => 'de2e896f',
- 'rsrc/js/application/repository/repository-crossreference.js' => '9a860428',
- 'rsrc/js/application/search/behavior-reorder-profile-menu-items.js' => 'e2e0a072',
- 'rsrc/js/application/search/behavior-reorder-queries.js' => 'e9581f08',
- 'rsrc/js/application/transactions/behavior-comment-actions.js' => '59e27e74',
- 'rsrc/js/application/transactions/behavior-reorder-configs.js' => 'd7a74243',
- 'rsrc/js/application/transactions/behavior-reorder-fields.js' => 'b59e1e96',
- 'rsrc/js/application/transactions/behavior-show-older-transactions.js' => '8f29b364',
- 'rsrc/js/application/transactions/behavior-transaction-comment-form.js' => 'b23b49e6',
- 'rsrc/js/application/transactions/behavior-transaction-list.js' => '1f6794f6',
- 'rsrc/js/application/typeahead/behavior-typeahead-browse.js' => '635de1ec',
- 'rsrc/js/application/typeahead/behavior-typeahead-search.js' => '93d0c9e3',
- 'rsrc/js/application/uiexample/gesture-example.js' => '558829c2',
- 'rsrc/js/application/uiexample/notification-example.js' => '8ce821c5',
- 'rsrc/js/core/Busy.js' => '59a7976a',
- 'rsrc/js/core/DragAndDropFileUpload.js' => '58dea2fa',
- 'rsrc/js/core/DraggableList.js' => 'bea6e7f4',
- 'rsrc/js/core/Favicon.js' => '1fe2510c',
- 'rsrc/js/core/FileUpload.js' => '680ea2c8',
- 'rsrc/js/core/Hovercard.js' => '1bd28176',
- 'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2',
- 'rsrc/js/core/KeyboardShortcutManager.js' => 'c19dd9b9',
- 'rsrc/js/core/MultirowRowManager.js' => 'b5d57730',
- 'rsrc/js/core/Notification.js' => '4f774dac',
- 'rsrc/js/core/Prefab.js' => '77b0ae28',
- 'rsrc/js/core/ShapedRequest.js' => '7cbe244b',
- 'rsrc/js/core/TextAreaUtils.js' => '320810c8',
- 'rsrc/js/core/Title.js' => '485aaa6c',
- 'rsrc/js/core/ToolTip.js' => '358b8c04',
- 'rsrc/js/core/behavior-active-nav.js' => 'e379b58e',
- 'rsrc/js/core/behavior-audio-source.js' => '59b251eb',
- 'rsrc/js/core/behavior-autofocus.js' => '7319e029',
- 'rsrc/js/core/behavior-badge-view.js' => '8ff5e24c',
- 'rsrc/js/core/behavior-bulk-editor.js' => '66a6def1',
- 'rsrc/js/core/behavior-choose-control.js' => '327a00d1',
- 'rsrc/js/core/behavior-copy.js' => 'b0b8f86d',
- 'rsrc/js/core/behavior-detect-timezone.js' => '4c193c96',
- 'rsrc/js/core/behavior-device.js' => 'a3714c76',
- 'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '484a6e22',
- 'rsrc/js/core/behavior-fancy-datepicker.js' => 'ecf4e799',
- 'rsrc/js/core/behavior-file-tree.js' => '88236f00',
- 'rsrc/js/core/behavior-form.js' => '5c54cbf3',
- 'rsrc/js/core/behavior-gesture.js' => '3ab51e2c',
- 'rsrc/js/core/behavior-global-drag-and-drop.js' => '960f6a39',
- 'rsrc/js/core/behavior-high-security-warning.js' => 'a464fe03',
- 'rsrc/js/core/behavior-history-install.js' => '7ee2b591',
- 'rsrc/js/core/behavior-hovercard.js' => 'bcaccd64',
- 'rsrc/js/core/behavior-keyboard-pager.js' => 'a8da01f0',
- 'rsrc/js/core/behavior-keyboard-shortcuts.js' => '01fca1f0',
- 'rsrc/js/core/behavior-lightbox-attachments.js' => '6b31879a',
- 'rsrc/js/core/behavior-line-linker.js' => '66a62306',
- 'rsrc/js/core/behavior-linked-container.js' => '291da458',
- 'rsrc/js/core/behavior-more.js' => 'a80d0378',
- 'rsrc/js/core/behavior-object-selector.js' => '77c1f0b0',
- 'rsrc/js/core/behavior-oncopy.js' => '2926fff2',
- 'rsrc/js/core/behavior-phabricator-nav.js' => '9d32bc88',
- 'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => 'acd29eee',
- 'rsrc/js/core/behavior-read-only-warning.js' => 'ba158207',
- 'rsrc/js/core/behavior-redirect.js' => '0213259f',
- 'rsrc/js/core/behavior-refresh-csrf.js' => 'ab2f381b',
- 'rsrc/js/core/behavior-remarkup-load-image.js' => '040fce04',
- 'rsrc/js/core/behavior-remarkup-preview.js' => '4b700e9e',
- 'rsrc/js/core/behavior-reorder-applications.js' => '76b9fc3e',
- 'rsrc/js/core/behavior-reveal-content.js' => '60821bc7',
- 'rsrc/js/core/behavior-scrollbar.js' => '834a1173',
- 'rsrc/js/core/behavior-search-typeahead.js' => 'c3e917d9',
- 'rsrc/js/core/behavior-select-content.js' => 'bf5374ef',
- 'rsrc/js/core/behavior-select-on-click.js' => '4e3e79a6',
- 'rsrc/js/core/behavior-setup-check-https.js' => '491416b3',
- 'rsrc/js/core/behavior-time-typeahead.js' => '522431f7',
- 'rsrc/js/core/behavior-toggle-class.js' => '92b9ec77',
- 'rsrc/js/core/behavior-tokenizer.js' => 'b3a4b884',
- 'rsrc/js/core/behavior-tooltip.js' => 'c420b0b9',
- 'rsrc/js/core/behavior-user-menu.js' => '31420f77',
- 'rsrc/js/core/behavior-watch-anchor.js' => '9f36c42d',
- 'rsrc/js/core/behavior-workflow.js' => '0a3f3021',
- 'rsrc/js/core/darkconsole/DarkLog.js' => 'c8e1ffe3',
- 'rsrc/js/core/darkconsole/DarkMessage.js' => 'c48cccdd',
- 'rsrc/js/core/darkconsole/behavior-dark-console.js' => '66888767',
- 'rsrc/js/core/phtize.js' => 'd254d646',
- 'rsrc/js/phui/behavior-phui-dropdown-menu.js' => 'b95d6f7d',
- 'rsrc/js/phui/behavior-phui-file-upload.js' => 'b003d4fb',
- 'rsrc/js/phui/behavior-phui-selectable-list.js' => '464259a2',
- 'rsrc/js/phui/behavior-phui-submenu.js' => 'a6f7a73b',
- 'rsrc/js/phui/behavior-phui-tab-group.js' => '0a0b10e9',
- 'rsrc/js/phuix/PHUIXActionListView.js' => 'b5c256b8',
- 'rsrc/js/phuix/PHUIXActionView.js' => '8d4a8c72',
- 'rsrc/js/phuix/PHUIXAutocomplete.js' => 'df1bbd34',
- 'rsrc/js/phuix/PHUIXButtonView.js' => '85ac9772',
- 'rsrc/js/phuix/PHUIXDropdownMenu.js' => '04b2ae03',
- 'rsrc/js/phuix/PHUIXExample.js' => '68af71ca',
- 'rsrc/js/phuix/PHUIXFormControl.js' => '210a16c1',
- 'rsrc/js/phuix/PHUIXIconView.js' => 'bff6884b',
+ 'conpherence.pkg.css' => '3c8a0668',
+ 'conpherence.pkg.js' => '020aebcf',
+ 'core.pkg.css' => 'e0cb8094',
+ 'core.pkg.js' => '5c737607',
+ 'differential.pkg.css' => 'b8df73d4',
+ 'differential.pkg.js' => '67c9ea4c',
+ 'diffusion.pkg.css' => '42c75c37',
+ 'diffusion.pkg.js' => '91192d85',
+ 'maniphest.pkg.css' => '35995d6d',
+ 'maniphest.pkg.js' => '286955ae',
+ 'rsrc/audio/basic/alert.mp3' => '17889334',
+ 'rsrc/audio/basic/bing.mp3' => 'a817a0c3',
+ 'rsrc/audio/basic/pock.mp3' => '0fa843d0',
+ 'rsrc/audio/basic/tap.mp3' => '02d16994',
+ 'rsrc/audio/basic/ting.mp3' => 'a6b6540e',
+ 'rsrc/css/aphront/aphront-bars.css' => '4a327b4a',
+ 'rsrc/css/aphront/dark-console.css' => '7f06cda2',
+ 'rsrc/css/aphront/dialog-view.css' => 'b70c70df',
+ 'rsrc/css/aphront/list-filter-view.css' => 'feb64255',
+ 'rsrc/css/aphront/multi-column.css' => 'fbc00ba3',
+ 'rsrc/css/aphront/notification.css' => '30240bd2',
+ 'rsrc/css/aphront/panel-view.css' => '46923d46',
+ 'rsrc/css/aphront/phabricator-nav-view.css' => 'f8a0c1bf',
+ 'rsrc/css/aphront/table-view.css' => '76eda3f8',
+ 'rsrc/css/aphront/tokenizer.css' => 'b52d0668',
+ 'rsrc/css/aphront/tooltip.css' => 'e3f2412f',
+ 'rsrc/css/aphront/typeahead-browse.css' => 'b7ed02d2',
+ 'rsrc/css/aphront/typeahead.css' => '8779483d',
+ 'rsrc/css/application/almanac/almanac.css' => '2e050f4f',
+ 'rsrc/css/application/auth/auth.css' => 'add92fd8',
+ 'rsrc/css/application/base/main-menu-view.css' => '8e2d9a28',
+ 'rsrc/css/application/base/notification-menu.css' => 'e6962e89',
+ 'rsrc/css/application/base/phui-theme.css' => '35883b37',
+ 'rsrc/css/application/base/standard-page-view.css' => '8a295cb9',
+ 'rsrc/css/application/chatlog/chatlog.css' => 'abdc76ee',
+ 'rsrc/css/application/conduit/conduit-api.css' => 'ce2cfc41',
+ 'rsrc/css/application/config/config-options.css' => '16c920ae',
+ 'rsrc/css/application/config/config-template.css' => '20babf50',
+ 'rsrc/css/application/config/setup-issue.css' => '5eed85b2',
+ 'rsrc/css/application/config/unhandled-exception.css' => '9da8fdab',
+ 'rsrc/css/application/conpherence/color.css' => 'b17746b0',
+ 'rsrc/css/application/conpherence/durable-column.css' => '2d57072b',
+ 'rsrc/css/application/conpherence/header-pane.css' => 'c9a3db8e',
+ 'rsrc/css/application/conpherence/menu.css' => '67f4680d',
+ 'rsrc/css/application/conpherence/message-pane.css' => 'd244db1e',
+ 'rsrc/css/application/conpherence/notification.css' => '6a3d4e58',
+ 'rsrc/css/application/conpherence/participant-pane.css' => '69e0058a',
+ 'rsrc/css/application/conpherence/transaction.css' => '3a3f5e7e',
+ 'rsrc/css/application/contentsource/content-source-view.css' => 'cdf0d579',
+ 'rsrc/css/application/countdown/timer.css' => 'bff8012f',
+ 'rsrc/css/application/daemon/bulk-job.css' => '73af99f5',
+ 'rsrc/css/application/dashboard/dashboard.css' => '4267d6c6',
+ 'rsrc/css/application/diff/inline-comment-summary.css' => '81eb368d',
+ 'rsrc/css/application/differential/add-comment.css' => '7e5900d9',
+ 'rsrc/css/application/differential/changeset-view.css' => '73660575',
+ 'rsrc/css/application/differential/core.css' => 'bdb93065',
+ 'rsrc/css/application/differential/phui-inline-comment.css' => '48acce5b',
+ 'rsrc/css/application/differential/revision-comment.css' => '7dbc8d1d',
+ 'rsrc/css/application/differential/revision-history.css' => '8aa3eac5',
+ 'rsrc/css/application/differential/revision-list.css' => '93d2df7d',
+ 'rsrc/css/application/differential/table-of-contents.css' => '0e3364c7',
+ 'rsrc/css/application/diffusion/diffusion-icons.css' => '23b31a1b',
+ 'rsrc/css/application/diffusion/diffusion-readme.css' => 'b68a76e4',
+ 'rsrc/css/application/diffusion/diffusion-repository.css' => 'b89e8c6c',
+ 'rsrc/css/application/diffusion/diffusion.css' => 'b54c77b0',
+ 'rsrc/css/application/feed/feed.css' => 'd8b6e3f8',
+ 'rsrc/css/application/files/global-drag-and-drop.css' => '1d2713a4',
+ 'rsrc/css/application/flag/flag.css' => '2b77be8d',
+ 'rsrc/css/application/harbormaster/harbormaster.css' => '8dfe16b2',
+ 'rsrc/css/application/herald/herald-test.css' => 'e004176f',
+ 'rsrc/css/application/herald/herald.css' => '648d39e2',
+ 'rsrc/css/application/maniphest/report.css' => '3d53188b',
+ 'rsrc/css/application/maniphest/task-edit.css' => '272daa84',
+ 'rsrc/css/application/maniphest/task-summary.css' => '61d1667e',
+ 'rsrc/css/application/objectselector/object-selector.css' => 'ee77366f',
+ 'rsrc/css/application/owners/owners-path-editor.css' => 'fa7c13ef',
+ 'rsrc/css/application/paste/paste.css' => 'b37bcd38',
+ 'rsrc/css/application/people/people-picture-menu-item.css' => 'fe8e07cf',
+ 'rsrc/css/application/people/people-profile.css' => '2ea2daa1',
+ 'rsrc/css/application/phame/phame.css' => '799febf9',
+ 'rsrc/css/application/pholio/pholio-edit.css' => '4df55b3b',
+ 'rsrc/css/application/pholio/pholio-inline-comments.css' => '722b48c2',
+ 'rsrc/css/application/pholio/pholio.css' => '88ef5ef1',
+ 'rsrc/css/application/phortune/phortune-credit-card-form.css' => '3b9868a8',
+ 'rsrc/css/application/phortune/phortune-invoice.css' => '4436b241',
+ 'rsrc/css/application/phortune/phortune.css' => '12e8251a',
+ 'rsrc/css/application/phrequent/phrequent.css' => 'bd79cc67',
+ 'rsrc/css/application/phriction/phriction-document-css.css' => '03380da0',
+ 'rsrc/css/application/policy/policy-edit.css' => '8794e2ed',
+ 'rsrc/css/application/policy/policy-transaction-detail.css' => 'c02b8384',
+ 'rsrc/css/application/policy/policy.css' => 'ceb56a08',
+ 'rsrc/css/application/ponder/ponder-view.css' => '05a09d0a',
+ 'rsrc/css/application/project/project-card-view.css' => '3b1f7b20',
+ 'rsrc/css/application/project/project-view.css' => '567858b3',
+ 'rsrc/css/application/releeph/releeph-core.css' => 'f81ff2db',
+ 'rsrc/css/application/releeph/releeph-preview-branch.css' => '22db5c07',
+ 'rsrc/css/application/releeph/releeph-request-differential-create-dialog.css' => '0ac1ea31',
+ 'rsrc/css/application/releeph/releeph-request-typeahead.css' => 'bce37359',
+ 'rsrc/css/application/search/application-search-view.css' => '0f7c06d8',
+ 'rsrc/css/application/search/search-results.css' => '9ea70ace',
+ 'rsrc/css/application/slowvote/slowvote.css' => '1694baed',
+ 'rsrc/css/application/tokens/tokens.css' => 'ce5a50bd',
+ 'rsrc/css/application/uiexample/example.css' => 'b4795059',
+ 'rsrc/css/core/core.css' => '1b29ed61',
+ 'rsrc/css/core/remarkup.css' => '9e627d41',
+ 'rsrc/css/core/syntax.css' => '8a16f91b',
+ 'rsrc/css/core/z-index.css' => '99c0f5eb',
+ 'rsrc/css/diviner/diviner-shared.css' => '4bd263b0',
+ 'rsrc/css/font/font-awesome.css' => '3883938a',
+ 'rsrc/css/font/font-lato.css' => '23631304',
+ 'rsrc/css/font/phui-font-icon-base.css' => 'd7994e06',
+ 'rsrc/css/layout/phabricator-filetree-view.css' => '56cdd875',
+ 'rsrc/css/layout/phabricator-source-code-view.css' => '03d7ac28',
+ 'rsrc/css/phui/button/phui-button-bar.css' => 'a4aa75c4',
+ 'rsrc/css/phui/button/phui-button-simple.css' => '1ff278aa',
+ 'rsrc/css/phui/button/phui-button.css' => 'ea704902',
+ 'rsrc/css/phui/calendar/phui-calendar-day.css' => '9597d706',
+ 'rsrc/css/phui/calendar/phui-calendar-list.css' => 'ccd7e4e2',
+ 'rsrc/css/phui/calendar/phui-calendar-month.css' => 'cb758c42',
+ 'rsrc/css/phui/calendar/phui-calendar.css' => 'f11073aa',
+ 'rsrc/css/phui/object-item/phui-oi-big-ui.css' => '9e037c7a',
+ 'rsrc/css/phui/object-item/phui-oi-color.css' => 'b517bfa0',
+ 'rsrc/css/phui/object-item/phui-oi-drag-ui.css' => 'da15d3dc',
+ 'rsrc/css/phui/object-item/phui-oi-flush-ui.css' => '490e2e2e',
+ 'rsrc/css/phui/object-item/phui-oi-list-view.css' => '909f3844',
+ 'rsrc/css/phui/object-item/phui-oi-simple-ui.css' => '6a30fa46',
+ 'rsrc/css/phui/phui-action-list.css' => 'c1a7631d',
+ 'rsrc/css/phui/phui-action-panel.css' => '6c386cbf',
+ 'rsrc/css/phui/phui-badge.css' => '666e25ad',
+ 'rsrc/css/phui/phui-basic-nav-view.css' => '56ebd66d',
+ 'rsrc/css/phui/phui-big-info-view.css' => '362ad37b',
+ 'rsrc/css/phui/phui-box.css' => '5ed3b8cb',
+ 'rsrc/css/phui/phui-bulk-editor.css' => '374d5e30',
+ 'rsrc/css/phui/phui-chart.css' => '7853a69b',
+ 'rsrc/css/phui/phui-cms.css' => '8c05c41e',
+ 'rsrc/css/phui/phui-comment-form.css' => '68a2d99a',
+ 'rsrc/css/phui/phui-comment-panel.css' => 'ec4e31c0',
+ 'rsrc/css/phui/phui-crumbs-view.css' => '614f43cf',
+ 'rsrc/css/phui/phui-curtain-view.css' => '68c5efb6',
+ 'rsrc/css/phui/phui-document-pro.css' => 'b9613a10',
+ 'rsrc/css/phui/phui-document-summary.css' => 'b068eed1',
+ 'rsrc/css/phui/phui-document.css' => '52b748a5',
+ 'rsrc/css/phui/phui-feed-story.css' => 'a0c05029',
+ 'rsrc/css/phui/phui-fontkit.css' => '9b714a5e',
+ 'rsrc/css/phui/phui-form-view.css' => '0807e7ac',
+ 'rsrc/css/phui/phui-form.css' => '159e2d9c',
+ 'rsrc/css/phui/phui-head-thing.css' => 'd7f293df',
+ 'rsrc/css/phui/phui-header-view.css' => '93cea4ec',
+ 'rsrc/css/phui/phui-hovercard.css' => '6ca90fa0',
+ 'rsrc/css/phui/phui-icon-set-selector.css' => '7aa5f3ec',
+ 'rsrc/css/phui/phui-icon.css' => '281f964d',
+ 'rsrc/css/phui/phui-image-mask.css' => '62c7f4d2',
+ 'rsrc/css/phui/phui-info-view.css' => '37b8d9ce',
+ 'rsrc/css/phui/phui-invisible-character-view.css' => 'c694c4a4',
+ 'rsrc/css/phui/phui-left-right.css' => '68513c34',
+ 'rsrc/css/phui/phui-lightbox.css' => '4ebf22da',
+ 'rsrc/css/phui/phui-list.css' => '470b1adb',
+ 'rsrc/css/phui/phui-object-box.css' => '9b58483d',
+ 'rsrc/css/phui/phui-pager.css' => 'd022c7ad',
+ 'rsrc/css/phui/phui-pinboard-view.css' => '1f08f5d8',
+ 'rsrc/css/phui/phui-property-list-view.css' => 'cad62236',
+ 'rsrc/css/phui/phui-remarkup-preview.css' => '91767007',
+ 'rsrc/css/phui/phui-segment-bar-view.css' => '5166b370',
+ 'rsrc/css/phui/phui-spacing.css' => 'b05cadc3',
+ 'rsrc/css/phui/phui-status.css' => 'e5ff8be0',
+ 'rsrc/css/phui/phui-tag-view.css' => 'a42fe34f',
+ 'rsrc/css/phui/phui-timeline-view.css' => '1e348e4b',
+ 'rsrc/css/phui/phui-two-column-view.css' => '01e6991e',
+ 'rsrc/css/phui/workboards/phui-workboard-color.css' => 'e86de308',
+ 'rsrc/css/phui/workboards/phui-workboard.css' => '74fc9d98',
+ 'rsrc/css/phui/workboards/phui-workcard.css' => '8c536f90',
+ 'rsrc/css/phui/workboards/phui-workpanel.css' => 'bd546a49',
+ 'rsrc/css/sprite-login.css' => '18b368a6',
+ 'rsrc/css/sprite-tokens.css' => 'f1896dc5',
+ 'rsrc/css/syntax/syntax-default.css' => '055fc231',
+ 'rsrc/externals/d3/d3.min.js' => 'd67475f5',
+ 'rsrc/externals/font/fontawesome/fontawesome-webfont.eot' => '23f8c698',
+ 'rsrc/externals/font/fontawesome/fontawesome-webfont.ttf' => '70983df0',
+ 'rsrc/externals/font/fontawesome/fontawesome-webfont.woff' => 'cd02f93b',
+ 'rsrc/externals/font/fontawesome/fontawesome-webfont.woff2' => '351fd46a',
+ 'rsrc/externals/font/lato/lato-bold.eot' => '7367aa5e',
+ 'rsrc/externals/font/lato/lato-bold.svg' => '681aa4f5',
+ 'rsrc/externals/font/lato/lato-bold.ttf' => '66d3c296',
+ 'rsrc/externals/font/lato/lato-bold.woff' => '89d9fba7',
+ 'rsrc/externals/font/lato/lato-bold.woff2' => '389fcdb1',
+ 'rsrc/externals/font/lato/lato-bolditalic.eot' => '03eeb4da',
+ 'rsrc/externals/font/lato/lato-bolditalic.svg' => 'f56fa11c',
+ 'rsrc/externals/font/lato/lato-bolditalic.ttf' => '9c3aec21',
+ 'rsrc/externals/font/lato/lato-bolditalic.woff' => 'bfbd0616',
+ 'rsrc/externals/font/lato/lato-bolditalic.woff2' => 'bc7d1274',
+ 'rsrc/externals/font/lato/lato-italic.eot' => '7db5b247',
+ 'rsrc/externals/font/lato/lato-italic.svg' => 'b1ae496f',
+ 'rsrc/externals/font/lato/lato-italic.ttf' => '43eed813',
+ 'rsrc/externals/font/lato/lato-italic.woff' => 'c28975e1',
+ 'rsrc/externals/font/lato/lato-italic.woff2' => 'fffc0d8c',
+ 'rsrc/externals/font/lato/lato-regular.eot' => '06e0c291',
+ 'rsrc/externals/font/lato/lato-regular.svg' => '3ad95f53',
+ 'rsrc/externals/font/lato/lato-regular.ttf' => 'e2e9c398',
+ 'rsrc/externals/font/lato/lato-regular.woff' => '0b13d332',
+ 'rsrc/externals/font/lato/lato-regular.woff2' => '8f846797',
+ 'rsrc/externals/javelin/core/Event.js' => 'c03f2fb4',
+ 'rsrc/externals/javelin/core/Stratcom.js' => '0889b835',
+ 'rsrc/externals/javelin/core/__tests__/event-stop-and-kill.js' => '048472d2',
+ 'rsrc/externals/javelin/core/__tests__/install.js' => '14a7e671',
+ 'rsrc/externals/javelin/core/__tests__/stratcom.js' => 'a28464bb',
+ 'rsrc/externals/javelin/core/__tests__/util.js' => 'e29a4354',
+ 'rsrc/externals/javelin/core/init.js' => '98e6504a',
+ 'rsrc/externals/javelin/core/init_node.js' => '16961339',
+ 'rsrc/externals/javelin/core/install.js' => '5902260c',
+ 'rsrc/externals/javelin/core/util.js' => '22ae1776',
+ 'rsrc/externals/javelin/docs/Base.js' => '5a401d7d',
+ 'rsrc/externals/javelin/docs/onload.js' => 'ee58fb62',
+ 'rsrc/externals/javelin/ext/fx/Color.js' => '78f811c9',
+ 'rsrc/externals/javelin/ext/fx/FX.js' => '34450586',
+ 'rsrc/externals/javelin/ext/reactor/core/DynVal.js' => '202a2e85',
+ 'rsrc/externals/javelin/ext/reactor/core/Reactor.js' => '1c850a26',
+ 'rsrc/externals/javelin/ext/reactor/core/ReactorNode.js' => '72960bc1',
+ 'rsrc/externals/javelin/ext/reactor/core/ReactorNodeCalmer.js' => '225bbb98',
+ 'rsrc/externals/javelin/ext/reactor/dom/RDOM.js' => '6cfa0008',
+ 'rsrc/externals/javelin/ext/view/HTMLView.js' => 'f8c4e135',
+ 'rsrc/externals/javelin/ext/view/View.js' => '289bf236',
+ 'rsrc/externals/javelin/ext/view/ViewInterpreter.js' => '876506b6',
+ 'rsrc/externals/javelin/ext/view/ViewPlaceholder.js' => 'a9942052',
+ 'rsrc/externals/javelin/ext/view/ViewRenderer.js' => '9aae2b66',
+ 'rsrc/externals/javelin/ext/view/ViewVisitor.js' => '308f9fe4',
+ 'rsrc/externals/javelin/ext/view/__tests__/HTMLView.js' => '6e50a13f',
+ 'rsrc/externals/javelin/ext/view/__tests__/View.js' => 'd284be5d',
+ 'rsrc/externals/javelin/ext/view/__tests__/ViewInterpreter.js' => 'a9f35511',
+ 'rsrc/externals/javelin/ext/view/__tests__/ViewRenderer.js' => '3a1b81f6',
+ 'rsrc/externals/javelin/lib/Cookie.js' => '05d290ef',
+ 'rsrc/externals/javelin/lib/DOM.js' => '94681e22',
+ 'rsrc/externals/javelin/lib/History.js' => '030b4f7a',
+ 'rsrc/externals/javelin/lib/JSON.js' => '541f81c3',
+ 'rsrc/externals/javelin/lib/Leader.js' => '0d2490ce',
+ 'rsrc/externals/javelin/lib/Mask.js' => '7c4d8998',
+ 'rsrc/externals/javelin/lib/Quicksand.js' => 'd3799cb4',
+ 'rsrc/externals/javelin/lib/Request.js' => '91863989',
+ 'rsrc/externals/javelin/lib/Resource.js' => '740956e1',
+ 'rsrc/externals/javelin/lib/Routable.js' => '6a18c42e',
+ 'rsrc/externals/javelin/lib/Router.js' => '32755edb',
+ 'rsrc/externals/javelin/lib/Scrollbar.js' => 'a43ae2ae',
+ 'rsrc/externals/javelin/lib/Sound.js' => 'e562708c',
+ 'rsrc/externals/javelin/lib/URI.js' => '2e255291',
+ 'rsrc/externals/javelin/lib/Vector.js' => 'e9c80beb',
+ 'rsrc/externals/javelin/lib/WebSocket.js' => 'fdc13e4e',
+ 'rsrc/externals/javelin/lib/Workflow.js' => '958e9045',
+ 'rsrc/externals/javelin/lib/__tests__/Cookie.js' => 'ca686f71',
+ 'rsrc/externals/javelin/lib/__tests__/DOM.js' => '4566e249',
+ 'rsrc/externals/javelin/lib/__tests__/JSON.js' => '710377ae',
+ 'rsrc/externals/javelin/lib/__tests__/URI.js' => '6fff0c2b',
+ 'rsrc/externals/javelin/lib/__tests__/behavior.js' => '8426ebeb',
+ 'rsrc/externals/javelin/lib/behavior.js' => 'fce5d170',
+ 'rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js' => '89a1ae3a',
+ 'rsrc/externals/javelin/lib/control/typeahead/Typeahead.js' => 'a4356cde',
+ 'rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js' => 'a241536a',
+ 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadCompositeSource.js' => '22ee68a5',
+ 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js' => '23387297',
+ 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js' => '5a79f6c3',
+ 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js' => '8badee71',
+ 'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadStaticSource.js' => '80bff3af',
+ 'rsrc/favicons/favicon-16x16.png' => '4c51a03a',
+ 'rsrc/favicons/mask-icon.svg' => 'db699fe1',
+ 'rsrc/image/BFCFDA.png' => '74b5c88b',
+ 'rsrc/image/actions/edit.png' => 'fd987dff',
+ 'rsrc/image/avatar.png' => '0d17c6c4',
+ 'rsrc/image/checker_dark.png' => '7fc8fa7b',
+ 'rsrc/image/checker_light.png' => '3157a202',
+ 'rsrc/image/checker_lighter.png' => 'c45928c1',
+ 'rsrc/image/controls/checkbox-checked.png' => '1770d7a0',
+ 'rsrc/image/controls/checkbox-unchecked.png' => 'e1deba0a',
+ 'rsrc/image/d5d8e1.png' => '6764616e',
+ 'rsrc/image/darkload.gif' => '5bd41a89',
+ 'rsrc/image/divot.png' => '0fbe2453',
+ 'rsrc/image/examples/hero.png' => '5d8c4b21',
+ 'rsrc/image/grippy_texture.png' => 'a7d222b5',
+ 'rsrc/image/icon/fatcow/arrow_branch.png' => '98149d9f',
+ 'rsrc/image/icon/fatcow/arrow_merge.png' => 'e142f4f8',
+ 'rsrc/image/icon/fatcow/calendar_edit.png' => '5ff44a08',
+ 'rsrc/image/icon/fatcow/document_black.png' => 'd3515fa5',
+ 'rsrc/image/icon/fatcow/flag_blue.png' => '54db2e5c',
+ 'rsrc/image/icon/fatcow/flag_finish.png' => '2953a51b',
+ 'rsrc/image/icon/fatcow/flag_ghost.png' => '7d9ada92',
+ 'rsrc/image/icon/fatcow/flag_green.png' => '010f7161',
+ 'rsrc/image/icon/fatcow/flag_orange.png' => '6c384ca5',
+ 'rsrc/image/icon/fatcow/flag_pink.png' => '11ac6b12',
+ 'rsrc/image/icon/fatcow/flag_purple.png' => 'c4f423a4',
+ 'rsrc/image/icon/fatcow/flag_red.png' => '9e6d8817',
+ 'rsrc/image/icon/fatcow/flag_yellow.png' => '906733f4',
+ 'rsrc/image/icon/fatcow/key_question.png' => 'c10c26db',
+ 'rsrc/image/icon/fatcow/link.png' => '8edbf327',
+ 'rsrc/image/icon/fatcow/page_white_edit.png' => '17ef5625',
+ 'rsrc/image/icon/fatcow/page_white_put.png' => '82430c91',
+ 'rsrc/image/icon/fatcow/source/conduit.png' => '5b55130c',
+ 'rsrc/image/icon/fatcow/source/email.png' => '8a32b77f',
+ 'rsrc/image/icon/fatcow/source/fax.png' => '8bc2a49b',
+ 'rsrc/image/icon/fatcow/source/mobile.png' => '0a918412',
+ 'rsrc/image/icon/fatcow/source/tablet.png' => 'fc50b050',
+ 'rsrc/image/icon/fatcow/source/web.png' => '70433af3',
+ 'rsrc/image/icon/subscribe.png' => '07ef454e',
+ 'rsrc/image/icon/tango/attachment.png' => 'bac9032d',
+ 'rsrc/image/icon/tango/edit.png' => 'e6296206',
+ 'rsrc/image/icon/tango/go-down.png' => '0b903712',
+ 'rsrc/image/icon/tango/log.png' => '86b6a6f4',
+ 'rsrc/image/icon/tango/upload.png' => '3fe6b92d',
+ 'rsrc/image/icon/unsubscribe.png' => 'db04378a',
+ 'rsrc/image/lightblue-header.png' => 'e6d483c6',
+ 'rsrc/image/logo/light-eye.png' => '72337472',
+ 'rsrc/image/main_texture.png' => '894d03c4',
+ 'rsrc/image/menu_texture.png' => '896c9ade',
+ 'rsrc/image/people/harding.png' => '95b2db63',
+ 'rsrc/image/people/jefferson.png' => 'e883a3a2',
+ 'rsrc/image/people/lincoln.png' => 'be2c07c5',
+ 'rsrc/image/people/mckinley.png' => '6af510a0',
+ 'rsrc/image/people/taft.png' => 'b15ab07e',
+ 'rsrc/image/people/user0.png' => '4bc64b40',
+ 'rsrc/image/people/user1.png' => '8063f445',
+ 'rsrc/image/people/user2.png' => 'd28246c0',
+ 'rsrc/image/people/user3.png' => 'fb1ac12d',
+ 'rsrc/image/people/user4.png' => 'fe4fac8f',
+ 'rsrc/image/people/user5.png' => '3d07065c',
+ 'rsrc/image/people/user6.png' => 'e4bd47c8',
+ 'rsrc/image/people/user7.png' => '71d8fe8b',
+ 'rsrc/image/people/user8.png' => '85f86bf7',
+ 'rsrc/image/people/user9.png' => '523db8aa',
+ 'rsrc/image/people/washington.png' => '86159e68',
+ 'rsrc/image/phrequent_active.png' => 'de66dc50',
+ 'rsrc/image/phrequent_inactive.png' => '79c61baf',
+ 'rsrc/image/resize.png' => '9cc83373',
+ 'rsrc/image/sprite-login-X2.png' => '604545f6',
+ 'rsrc/image/sprite-login.png' => '7a001a9a',
+ 'rsrc/image/sprite-tokens-X2.png' => '21621dd9',
+ 'rsrc/image/sprite-tokens.png' => 'bede2580',
+ 'rsrc/image/texture/card-gradient.png' => 'e6892cb4',
+ 'rsrc/image/texture/dark-menu-hover.png' => '390a4fa1',
+ 'rsrc/image/texture/dark-menu.png' => '542f699c',
+ 'rsrc/image/texture/grip.png' => 'bc80753a',
+ 'rsrc/image/texture/panel-header-gradient.png' => '65004dbf',
+ 'rsrc/image/texture/phlnx-bg.png' => '6c9cd31d',
+ 'rsrc/image/texture/pholio-background.gif' => '84910bfc',
+ 'rsrc/image/texture/table_header.png' => '7652d1ad',
+ 'rsrc/image/texture/table_header_hover.png' => '12ea5236',
+ 'rsrc/image/texture/table_header_tall.png' => '5cc420c4',
+ 'rsrc/js/application/aphlict/Aphlict.js' => '022516b4',
+ 'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => 'e9a2940f',
+ 'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => '4e61fa88',
+ 'rsrc/js/application/aphlict/behavior-aphlict-status.js' => 'c3703a16',
+ 'rsrc/js/application/aphlict/behavior-desktop-notifications-control.js' => '070679fe',
+ 'rsrc/js/application/calendar/behavior-day-view.js' => '727a5a61',
+ 'rsrc/js/application/calendar/behavior-event-all-day.js' => '0b1bc990',
+ 'rsrc/js/application/calendar/behavior-month-view.js' => '158c64e0',
+ 'rsrc/js/application/config/behavior-reorder-fields.js' => '2539f834',
+ 'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => 'aec8e38c',
+ 'rsrc/js/application/conpherence/behavior-conpherence-search.js' => '91befbcc',
+ 'rsrc/js/application/conpherence/behavior-durable-column.js' => 'fa6f30b2',
+ 'rsrc/js/application/conpherence/behavior-menu.js' => '8c2ed2bf',
+ 'rsrc/js/application/conpherence/behavior-participant-pane.js' => '43ba89a2',
+ 'rsrc/js/application/conpherence/behavior-pontificate.js' => '4ae58b5a',
+ 'rsrc/js/application/conpherence/behavior-quicksand-blacklist.js' => '5a6f6a06',
+ 'rsrc/js/application/conpherence/behavior-toggle-widget.js' => '8f959ad0',
+ 'rsrc/js/application/countdown/timer.js' => '6a162524',
+ 'rsrc/js/application/daemon/behavior-bulk-job-reload.js' => '3829a3cf',
+ 'rsrc/js/application/dashboard/behavior-dashboard-async-panel.js' => '09ecf50c',
+ 'rsrc/js/application/dashboard/behavior-dashboard-move-panels.js' => '076bd092',
+ 'rsrc/js/application/dashboard/behavior-dashboard-query-panel-select.js' => '1e413dc9',
+ 'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => '9b1cbd76',
+ 'rsrc/js/application/diff/DiffChangeset.js' => 'e7cf10d6',
+ 'rsrc/js/application/diff/DiffChangesetList.js' => 'b91204e9',
+ 'rsrc/js/application/diff/DiffInline.js' => 'a4a14a94',
+ 'rsrc/js/application/diff/behavior-preview-link.js' => 'f51e9c17',
+ 'rsrc/js/application/differential/behavior-diff-radios.js' => '925fe8cd',
+ 'rsrc/js/application/differential/behavior-populate.js' => 'dfa1d313',
+ 'rsrc/js/application/differential/behavior-user-select.js' => 'e18685c0',
+ 'rsrc/js/application/diffusion/DiffusionLocateFileSource.js' => '94243d89',
+ 'rsrc/js/application/diffusion/behavior-audit-preview.js' => 'b7b73831',
+ 'rsrc/js/application/diffusion/behavior-commit-branches.js' => '4b671572',
+ 'rsrc/js/application/diffusion/behavior-commit-graph.js' => '1c88f154',
+ 'rsrc/js/application/diffusion/behavior-locate-file.js' => '87428eb2',
+ 'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => 'c715c123',
+ 'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => '6a85bc5a',
+ 'rsrc/js/application/drydock/drydock-live-operation-status.js' => '47a0728b',
+ 'rsrc/js/application/files/behavior-document-engine.js' => '243d6c22',
+ 'rsrc/js/application/files/behavior-icon-composer.js' => '38a6cedb',
+ 'rsrc/js/application/files/behavior-launch-icon-composer.js' => 'a17b84f1',
+ 'rsrc/js/application/harbormaster/behavior-harbormaster-log.js' => 'b347a301',
+ 'rsrc/js/application/herald/HeraldRuleEditor.js' => '27daef73',
+ 'rsrc/js/application/herald/PathTypeahead.js' => 'ad486db3',
+ 'rsrc/js/application/herald/herald-rule-editor.js' => '0922e81d',
+ 'rsrc/js/application/maniphest/behavior-batch-selector.js' => 'cffd39b4',
+ 'rsrc/js/application/maniphest/behavior-line-chart.js' => 'c8147a20',
+ 'rsrc/js/application/maniphest/behavior-list-edit.js' => 'c687e867',
+ 'rsrc/js/application/maniphest/behavior-subpriorityeditor.js' => '8400307c',
+ 'rsrc/js/application/owners/OwnersPathEditor.js' => '2a8b62d9',
+ 'rsrc/js/application/owners/owners-path-editor.js' => 'ff688a7a',
+ 'rsrc/js/application/passphrase/passphrase-credential-control.js' => '48fe33d0',
+ 'rsrc/js/application/pholio/behavior-pholio-mock-edit.js' => '3eed1f2b',
+ 'rsrc/js/application/pholio/behavior-pholio-mock-view.js' => '5aa1544e',
+ 'rsrc/js/application/phortune/behavior-stripe-payment-form.js' => '02cb4398',
+ 'rsrc/js/application/phortune/behavior-test-payment-form.js' => '4a7fb02b',
+ 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f',
+ 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9',
+ 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172',
+ 'rsrc/js/application/projects/WorkboardBoard.js' => '45d0b2b1',
+ 'rsrc/js/application/projects/WorkboardCard.js' => '9a513421',
+ 'rsrc/js/application/projects/WorkboardColumn.js' => '8573dc1b',
+ 'rsrc/js/application/projects/WorkboardController.js' => '42c7a5a7',
+ 'rsrc/js/application/projects/behavior-project-boards.js' => '05c74d65',
+ 'rsrc/js/application/projects/behavior-project-create.js' => '34c53422',
+ 'rsrc/js/application/projects/behavior-reorder-columns.js' => '8ac32fd9',
+ 'rsrc/js/application/releeph/releeph-preview-branch.js' => '75184d68',
+ 'rsrc/js/application/releeph/releeph-request-state-change.js' => '9f081f05',
+ 'rsrc/js/application/releeph/releeph-request-typeahead.js' => 'aa3a100c',
+ 'rsrc/js/application/repository/repository-crossreference.js' => 'db0c0214',
+ 'rsrc/js/application/search/behavior-reorder-profile-menu-items.js' => 'e5bdb730',
+ 'rsrc/js/application/search/behavior-reorder-queries.js' => 'b86f297f',
+ 'rsrc/js/application/transactions/behavior-comment-actions.js' => '4dffaeb2',
+ 'rsrc/js/application/transactions/behavior-reorder-configs.js' => '4842f137',
+ 'rsrc/js/application/transactions/behavior-reorder-fields.js' => '0ad8d31f',
+ 'rsrc/js/application/transactions/behavior-show-older-transactions.js' => '600f440c',
+ 'rsrc/js/application/transactions/behavior-transaction-comment-form.js' => '2bdadf1a',
+ 'rsrc/js/application/transactions/behavior-transaction-list.js' => '9cec214e',
+ 'rsrc/js/application/typeahead/behavior-typeahead-browse.js' => '70245195',
+ 'rsrc/js/application/typeahead/behavior-typeahead-search.js' => '7b139193',
+ 'rsrc/js/application/uiexample/gesture-example.js' => '242dedd0',
+ 'rsrc/js/application/uiexample/notification-example.js' => '29819b75',
+ 'rsrc/js/core/Busy.js' => '5202e831',
+ 'rsrc/js/core/DragAndDropFileUpload.js' => '4370900d',
+ 'rsrc/js/core/DraggableList.js' => '3c6bd549',
+ 'rsrc/js/core/Favicon.js' => '7930776a',
+ 'rsrc/js/core/FileUpload.js' => 'ab85e184',
+ 'rsrc/js/core/Hovercard.js' => '074f0783',
+ 'rsrc/js/core/KeyboardShortcut.js' => 'c9749dcd',
+ 'rsrc/js/core/KeyboardShortcutManager.js' => '37b8a04a',
+ 'rsrc/js/core/MultirowRowManager.js' => '5b54c823',
+ 'rsrc/js/core/Notification.js' => 'a9b91e3f',
+ 'rsrc/js/core/Prefab.js' => 'bf457520',
+ 'rsrc/js/core/ShapedRequest.js' => 'abf88db8',
+ 'rsrc/js/core/TextAreaUtils.js' => 'f340a484',
+ 'rsrc/js/core/Title.js' => '43bc9360',
+ 'rsrc/js/core/ToolTip.js' => '83754533',
+ 'rsrc/js/core/behavior-active-nav.js' => '7353f43d',
+ 'rsrc/js/core/behavior-audio-source.js' => '3dc5ad43',
+ 'rsrc/js/core/behavior-autofocus.js' => '65bb0011',
+ 'rsrc/js/core/behavior-badge-view.js' => '92cdd7b6',
+ 'rsrc/js/core/behavior-bulk-editor.js' => 'aa6d2308',
+ 'rsrc/js/core/behavior-choose-control.js' => '04f8a1e3',
+ 'rsrc/js/core/behavior-copy.js' => 'cf32921f',
+ 'rsrc/js/core/behavior-detect-timezone.js' => '78bc5d94',
+ 'rsrc/js/core/behavior-device.js' => '0cf79f45',
+ 'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '7ad020a5',
+ 'rsrc/js/core/behavior-fancy-datepicker.js' => '956f3eeb',
+ 'rsrc/js/core/behavior-file-tree.js' => 'ee82cedb',
+ 'rsrc/js/core/behavior-form.js' => '55d7b788',
+ 'rsrc/js/core/behavior-gesture.js' => 'b58d1a2a',
+ 'rsrc/js/core/behavior-global-drag-and-drop.js' => '1cab0e9a',
+ 'rsrc/js/core/behavior-high-security-warning.js' => 'dae2d55b',
+ 'rsrc/js/core/behavior-history-install.js' => '6a1583a8',
+ 'rsrc/js/core/behavior-hovercard.js' => '6c379000',
+ 'rsrc/js/core/behavior-keyboard-pager.js' => '1325b731',
+ 'rsrc/js/core/behavior-keyboard-shortcuts.js' => '2cc87f49',
+ 'rsrc/js/core/behavior-lightbox-attachments.js' => 'c7e748bf',
+ 'rsrc/js/core/behavior-line-linker.js' => 'e15c8b1f',
+ 'rsrc/js/core/behavior-linked-container.js' => '74446546',
+ 'rsrc/js/core/behavior-more.js' => '506aa3f4',
+ 'rsrc/js/core/behavior-object-selector.js' => 'a4af0b4a',
+ 'rsrc/js/core/behavior-oncopy.js' => '418f6684',
+ 'rsrc/js/core/behavior-phabricator-nav.js' => 'f166c949',
+ 'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => '2f80333f',
+ 'rsrc/js/core/behavior-read-only-warning.js' => 'b9109f8f',
+ 'rsrc/js/core/behavior-redirect.js' => '407ee861',
+ 'rsrc/js/core/behavior-refresh-csrf.js' => '46116c01',
+ 'rsrc/js/core/behavior-remarkup-load-image.js' => '202bfa3f',
+ 'rsrc/js/core/behavior-remarkup-preview.js' => 'd8a86cfb',
+ 'rsrc/js/core/behavior-reorder-applications.js' => 'aa371860',
+ 'rsrc/js/core/behavior-reveal-content.js' => 'b105a3a6',
+ 'rsrc/js/core/behavior-scrollbar.js' => '92388bae',
+ 'rsrc/js/core/behavior-search-typeahead.js' => '1cb7d027',
+ 'rsrc/js/core/behavior-select-content.js' => 'e8240b50',
+ 'rsrc/js/core/behavior-select-on-click.js' => '66365ee2',
+ 'rsrc/js/core/behavior-setup-check-https.js' => '01384686',
+ 'rsrc/js/core/behavior-time-typeahead.js' => '5803b9e7',
+ 'rsrc/js/core/behavior-toggle-class.js' => 'f5c78ae3',
+ 'rsrc/js/core/behavior-tokenizer.js' => '3b4899b0',
+ 'rsrc/js/core/behavior-tooltip.js' => '73ecc1f8',
+ 'rsrc/js/core/behavior-user-menu.js' => '60cd9241',
+ 'rsrc/js/core/behavior-watch-anchor.js' => '0e6d261f',
+ 'rsrc/js/core/behavior-workflow.js' => '9623adc1',
+ 'rsrc/js/core/darkconsole/DarkLog.js' => '3b869402',
+ 'rsrc/js/core/darkconsole/DarkMessage.js' => '26cd4b73',
+ 'rsrc/js/core/darkconsole/behavior-dark-console.js' => 'f39d968b',
+ 'rsrc/js/core/phtize.js' => '2f1db1ed',
+ 'rsrc/js/phui/behavior-phui-dropdown-menu.js' => '5cf0501a',
+ 'rsrc/js/phui/behavior-phui-file-upload.js' => 'e150bd50',
+ 'rsrc/js/phui/behavior-phui-selectable-list.js' => 'b26a41e4',
+ 'rsrc/js/phui/behavior-phui-submenu.js' => 'b5e9bff9',
+ 'rsrc/js/phui/behavior-phui-tab-group.js' => '242aa08b',
+ 'rsrc/js/phuix/PHUIXActionListView.js' => 'c68f183f',
+ 'rsrc/js/phuix/PHUIXActionView.js' => 'aaa08f3b',
+ 'rsrc/js/phuix/PHUIXAutocomplete.js' => '58cc4ab8',
+ 'rsrc/js/phuix/PHUIXButtonView.js' => '55a24e84',
+ 'rsrc/js/phuix/PHUIXDropdownMenu.js' => 'bdce4d78',
+ 'rsrc/js/phuix/PHUIXExample.js' => 'c2c500a7',
+ 'rsrc/js/phuix/PHUIXFormControl.js' => '38c1f3fb',
+ 'rsrc/js/phuix/PHUIXIconView.js' => 'a5257c4e',
),
'symbols' => array(
- 'almanac-css' => 'dbb9b3af',
- 'aphront-bars' => '231ac33c',
- 'aphront-dark-console-css' => '0e14e8f6',
- 'aphront-dialog-view-css' => '6bfc244b',
- 'aphront-list-filter-view-css' => '5d6f0526',
- 'aphront-multi-column-view-css' => '84cc6640',
- 'aphront-panel-view-css' => '8427b78d',
- 'aphront-table-view-css' => '8c9bbafe',
- 'aphront-tokenizer-control-css' => '15d5ff71',
- 'aphront-tooltip-css' => 'cb1397a4',
- 'aphront-typeahead-control-css' => 'a4a21016',
- 'application-search-view-css' => '787f5b76',
- 'auth-css' => '0877ed6e',
- 'bulk-job-css' => 'df9c1d4a',
- 'conduit-api-css' => '7bc725c4',
- 'config-options-css' => '4615667b',
- 'conpherence-color-css' => 'abb4c358',
- 'conpherence-durable-column-view' => '89ea6bef',
- 'conpherence-header-pane-css' => 'cb6f4e19',
- 'conpherence-menu-css' => '69368e97',
- 'conpherence-message-pane-css' => 'b0f55ecc',
- 'conpherence-notification-css' => 'cef0a3fc',
- 'conpherence-participant-pane-css' => '26a3ce56',
- 'conpherence-thread-manager' => '4d863052',
- 'conpherence-transaction-css' => '85129c68',
- 'd3' => 'a11a5ff2',
- 'differential-changeset-view-css' => 'db34a142',
- 'differential-core-view-css' => '5b7b8ff4',
- 'differential-revision-add-comment-css' => 'c47f8c40',
- 'differential-revision-comment-css' => '14b8565a',
- 'differential-revision-history-css' => '0e8eb855',
- 'differential-revision-list-css' => 'f3c47d33',
- 'differential-table-of-contents-css' => 'ae4b7a55',
- 'diffusion-css' => '45727264',
- 'diffusion-icons-css' => '0c15255e',
- 'diffusion-readme-css' => '419dd5b6',
- 'diffusion-repository-css' => 'ee6f20ec',
- 'diviner-shared-css' => '896f1d43',
- 'font-fontawesome' => 'e838e088',
- 'font-lato' => 'c7ccd872',
- 'global-drag-and-drop-css' => 'b556a948',
- 'harbormaster-css' => '7446ce72',
- 'herald-css' => 'cd8d0134',
- 'herald-rule-editor' => 'dca75c0e',
- 'herald-test-css' => 'a52e323e',
- 'inline-comment-summary-css' => 'f23d4e8f',
- 'javelin-aphlict' => 'e1d4b11a',
- 'javelin-behavior' => '61cbc29a',
- 'javelin-behavior-aphlict-dropdown' => 'caade6f2',
- 'javelin-behavior-aphlict-listen' => '599a8f5f',
- 'javelin-behavior-aphlict-status' => '5e2634b9',
- 'javelin-behavior-aphront-basic-tokenizer' => 'b3a4b884',
- 'javelin-behavior-aphront-drag-and-drop-textarea' => '484a6e22',
- 'javelin-behavior-aphront-form-disable-on-submit' => '5c54cbf3',
- 'javelin-behavior-aphront-more' => 'a80d0378',
- 'javelin-behavior-audio-source' => '59b251eb',
- 'javelin-behavior-audit-preview' => 'd835b03a',
- 'javelin-behavior-badge-view' => '8ff5e24c',
- 'javelin-behavior-bulk-editor' => '66a6def1',
- 'javelin-behavior-bulk-job-reload' => 'edf8a145',
- 'javelin-behavior-calendar-month-view' => 'fe33e256',
- 'javelin-behavior-choose-control' => '327a00d1',
- 'javelin-behavior-comment-actions' => '59e27e74',
- 'javelin-behavior-config-reorder-fields' => 'b6993408',
- 'javelin-behavior-conpherence-menu' => '4047cd35',
- 'javelin-behavior-conpherence-participant-pane' => 'd057e45a',
- 'javelin-behavior-conpherence-pontificate' => '55616e04',
- 'javelin-behavior-conpherence-search' => '9bbf3762',
- 'javelin-behavior-countdown-timer' => 'e4cc26b3',
- 'javelin-behavior-dark-console' => '66888767',
- 'javelin-behavior-dashboard-async-panel' => '469c0d9e',
- 'javelin-behavior-dashboard-move-panels' => '408bf173',
- 'javelin-behavior-dashboard-query-panel-select' => '453c5375',
- 'javelin-behavior-dashboard-tab-panel' => 'd4eecc63',
- 'javelin-behavior-day-view' => '4b3c4443',
- 'javelin-behavior-desktop-notifications-control' => '27ca6289',
- 'javelin-behavior-detect-timezone' => '4c193c96',
- 'javelin-behavior-device' => 'a3714c76',
- 'javelin-behavior-diff-preview-link' => '051c7832',
- 'javelin-behavior-differential-diff-radios' => 'e1ff79b1',
- 'javelin-behavior-differential-feedback-preview' => '51c5ad07',
- 'javelin-behavior-differential-populate' => 'f0eb6708',
- 'javelin-behavior-differential-user-select' => 'a8d8459d',
- 'javelin-behavior-diffusion-commit-branches' => 'bdaf4d04',
- 'javelin-behavior-diffusion-commit-graph' => '75b83cbb',
- 'javelin-behavior-diffusion-locate-file' => '6d3e1947',
- 'javelin-behavior-diffusion-pull-lastmodified' => 'f01586dc',
- 'javelin-behavior-document-engine' => '3935d8c4',
- 'javelin-behavior-doorkeeper-tag' => '1db13e70',
- 'javelin-behavior-drydock-live-operation-status' => '901935ef',
- 'javelin-behavior-durable-column' => '2ae077e1',
- 'javelin-behavior-editengine-reorder-configs' => 'd7a74243',
- 'javelin-behavior-editengine-reorder-fields' => 'b59e1e96',
- 'javelin-behavior-event-all-day' => 'b41537c9',
- 'javelin-behavior-fancy-datepicker' => 'ecf4e799',
- 'javelin-behavior-global-drag-and-drop' => '960f6a39',
- 'javelin-behavior-harbormaster-log' => '549459b8',
- 'javelin-behavior-herald-rule-editor' => '7ebaeed3',
- 'javelin-behavior-high-security-warning' => 'a464fe03',
- 'javelin-behavior-history-install' => '7ee2b591',
- 'javelin-behavior-icon-composer' => '8499b6ab',
- 'javelin-behavior-launch-icon-composer' => '48086888',
- 'javelin-behavior-lightbox-attachments' => '6b31879a',
- 'javelin-behavior-line-chart' => 'e4232876',
- 'javelin-behavior-linked-container' => '291da458',
- 'javelin-behavior-maniphest-batch-selector' => 'ad54037e',
- 'javelin-behavior-maniphest-list-editor' => 'a9f88de2',
- 'javelin-behavior-maniphest-subpriority-editor' => '71237763',
- 'javelin-behavior-owners-path-editor' => '7a68dda3',
- 'javelin-behavior-passphrase-credential-control' => '3cb0b2fc',
- 'javelin-behavior-phabricator-active-nav' => 'e379b58e',
- 'javelin-behavior-phabricator-autofocus' => '7319e029',
- 'javelin-behavior-phabricator-clipboard-copy' => 'b0b8f86d',
- 'javelin-behavior-phabricator-file-tree' => '88236f00',
- 'javelin-behavior-phabricator-gesture' => '3ab51e2c',
- 'javelin-behavior-phabricator-gesture-example' => '558829c2',
- 'javelin-behavior-phabricator-keyboard-pager' => 'a8da01f0',
- 'javelin-behavior-phabricator-keyboard-shortcuts' => '01fca1f0',
- 'javelin-behavior-phabricator-line-linker' => '66a62306',
- 'javelin-behavior-phabricator-nav' => '9d32bc88',
- 'javelin-behavior-phabricator-notification-example' => '8ce821c5',
- 'javelin-behavior-phabricator-object-selector' => '77c1f0b0',
- 'javelin-behavior-phabricator-oncopy' => '2926fff2',
- 'javelin-behavior-phabricator-remarkup-assist' => 'acd29eee',
- 'javelin-behavior-phabricator-reveal-content' => '60821bc7',
- 'javelin-behavior-phabricator-search-typeahead' => 'c3e917d9',
- 'javelin-behavior-phabricator-show-older-transactions' => '8f29b364',
- 'javelin-behavior-phabricator-tooltips' => 'c420b0b9',
- 'javelin-behavior-phabricator-transaction-comment-form' => 'b23b49e6',
- 'javelin-behavior-phabricator-transaction-list' => '1f6794f6',
- 'javelin-behavior-phabricator-watch-anchor' => '9f36c42d',
- 'javelin-behavior-pholio-mock-edit' => 'bee502c8',
- 'javelin-behavior-pholio-mock-view' => 'ec1f3669',
- 'javelin-behavior-phui-dropdown-menu' => 'b95d6f7d',
- 'javelin-behavior-phui-file-upload' => 'b003d4fb',
- 'javelin-behavior-phui-hovercards' => 'bcaccd64',
- 'javelin-behavior-phui-selectable-list' => '464259a2',
- 'javelin-behavior-phui-submenu' => 'a6f7a73b',
- 'javelin-behavior-phui-tab-group' => '0a0b10e9',
- 'javelin-behavior-phuix-example' => '68af71ca',
- 'javelin-behavior-policy-control' => 'd0c516d5',
- 'javelin-behavior-policy-rule-editor' => '5e9f347c',
- 'javelin-behavior-project-boards' => '4250a34e',
- 'javelin-behavior-project-create' => '065227cc',
- 'javelin-behavior-quicksand-blacklist' => '7927a7d3',
- 'javelin-behavior-read-only-warning' => 'ba158207',
- 'javelin-behavior-redirect' => '0213259f',
- 'javelin-behavior-refresh-csrf' => 'ab2f381b',
- 'javelin-behavior-releeph-preview-branch' => 'b2b4fbaf',
- 'javelin-behavior-releeph-request-state-change' => 'a0b57eb8',
- 'javelin-behavior-releeph-request-typeahead' => 'de2e896f',
- 'javelin-behavior-remarkup-load-image' => '040fce04',
- 'javelin-behavior-remarkup-preview' => '4b700e9e',
- 'javelin-behavior-reorder-applications' => '76b9fc3e',
- 'javelin-behavior-reorder-columns' => 'e1d25dfb',
- 'javelin-behavior-reorder-profile-menu-items' => 'e2e0a072',
- 'javelin-behavior-repository-crossreference' => '9a860428',
- 'javelin-behavior-scrollbar' => '834a1173',
- 'javelin-behavior-search-reorder-queries' => 'e9581f08',
- 'javelin-behavior-select-content' => 'bf5374ef',
- 'javelin-behavior-select-on-click' => '4e3e79a6',
- 'javelin-behavior-setup-check-https' => '491416b3',
- 'javelin-behavior-stripe-payment-form' => 'a6b98425',
- 'javelin-behavior-test-payment-form' => 'fc91ab6c',
- 'javelin-behavior-time-typeahead' => '522431f7',
- 'javelin-behavior-toggle-class' => '92b9ec77',
- 'javelin-behavior-toggle-widget' => '3dbf94d5',
- 'javelin-behavior-typeahead-browse' => '635de1ec',
- 'javelin-behavior-typeahead-search' => '93d0c9e3',
- 'javelin-behavior-user-menu' => '31420f77',
- 'javelin-behavior-view-placeholder' => '47830651',
- 'javelin-behavior-workflow' => '0a3f3021',
- 'javelin-color' => '7e41274a',
- 'javelin-cookie' => '62dfea03',
- 'javelin-diffusion-locate-file-source' => '00676f00',
- 'javelin-dom' => '4976858c',
- 'javelin-dynval' => 'f6555212',
- 'javelin-event' => 'ef7e057f',
- 'javelin-fx' => '54b612ba',
- 'javelin-history' => 'd4505101',
- 'javelin-install' => '05270951',
- 'javelin-json' => '69adf288',
- 'javelin-leader' => '7f243deb',
- 'javelin-magical-init' => '8d83d2a1',
- 'javelin-mask' => '8a41885b',
- 'javelin-quicksand' => '6b8ef10b',
- 'javelin-reactor' => '2b8de964',
- 'javelin-reactor-dom' => 'c90a04fc',
- 'javelin-reactor-node-calmer' => '76f4ebed',
- 'javelin-reactornode' => '1ad0a787',
- 'javelin-request' => '94b750d2',
- 'javelin-resource' => '44959b73',
- 'javelin-routable' => 'b3e7d692',
- 'javelin-router' => '29274e2b',
- 'javelin-scrollbar' => '9065f639',
- 'javelin-sound' => '949c0fe5',
- 'javelin-stratcom' => '327f418a',
- 'javelin-tokenizer' => 'bb6e5c16',
- 'javelin-typeahead' => '70baed2f',
- 'javelin-typeahead-composite-source' => '503e17fd',
- 'javelin-typeahead-normalizer' => '185bbd53',
- 'javelin-typeahead-ondemand-source' => '013ffff9',
- 'javelin-typeahead-preloaded-source' => '54f314a0',
- 'javelin-typeahead-source' => 'ab9e0a82',
- 'javelin-typeahead-static-source' => '6c0e62fa',
- 'javelin-uri' => 'c989ade3',
- 'javelin-util' => '93cc50d6',
- 'javelin-vector' => '2caa8fb8',
- 'javelin-view' => '0f764c35',
- 'javelin-view-html' => 'fe287620',
- 'javelin-view-interpreter' => 'f829edb3',
- 'javelin-view-renderer' => '6c2b09a2',
- 'javelin-view-visitor' => 'efe49472',
- 'javelin-websocket' => '3ffe32d6',
- 'javelin-workboard-board' => '8935deef',
- 'javelin-workboard-card' => 'c587b80f',
- 'javelin-workboard-column' => '758b4758',
- 'javelin-workboard-controller' => '26167537',
- 'javelin-workflow' => '6a726c55',
- 'katex-css' => '297123ca',
- 'maniphest-report-css' => '9b9580b7',
- 'maniphest-task-edit-css' => 'fda62a9b',
- 'maniphest-task-summary-css' => '11cc5344',
- 'multirow-row-manager' => 'b5d57730',
- 'owners-path-editor' => 'c96502cf',
- 'owners-path-editor-css' => '9c136c29',
- 'paste-css' => '9fcc9773',
- 'path-typeahead' => '6d8c7912',
- 'people-picture-menu-item-css' => 'a06f7f34',
- 'people-profile-css' => '4df76faf',
- 'phabricator-action-list-view-css' => '0bcd9a45',
- 'phabricator-busy' => '59a7976a',
- 'phabricator-chatlog-css' => 'd295b020',
- 'phabricator-content-source-view-css' => '4b8b05d4',
- 'phabricator-core-css' => '62fa3ace',
- 'phabricator-countdown-css' => '16c52f5c',
- 'phabricator-darklog' => 'c8e1ffe3',
- 'phabricator-darkmessage' => 'c48cccdd',
- 'phabricator-dashboard-css' => 'fe5b1869',
- 'phabricator-diff-changeset' => 'b49b59d6',
- 'phabricator-diff-changeset-list' => '0a84bcc1',
- 'phabricator-diff-inline' => 'e83d28f3',
- 'phabricator-drag-and-drop-file-upload' => '58dea2fa',
- 'phabricator-draggable-list' => 'bea6e7f4',
- 'phabricator-fatal-config-template-css' => '8f18fa41',
- 'phabricator-favicon' => '1fe2510c',
- 'phabricator-feed-css' => 'ecd4ec57',
- 'phabricator-file-upload' => '680ea2c8',
- 'phabricator-filetree-view-css' => 'b912ad97',
- 'phabricator-flag-css' => 'bba8f811',
- 'phabricator-keyboard-shortcut' => '1ae869f2',
- 'phabricator-keyboard-shortcut-manager' => 'c19dd9b9',
- 'phabricator-main-menu-view' => '1802a242',
- 'phabricator-nav-view-css' => '694d7723',
- 'phabricator-notification' => '4f774dac',
- 'phabricator-notification-css' => '457861ec',
- 'phabricator-notification-menu-css' => 'ef480927',
- 'phabricator-object-selector-css' => '85ee8ce6',
- 'phabricator-phtize' => 'd254d646',
- 'phabricator-prefab' => '77b0ae28',
- 'phabricator-remarkup-css' => 'f1701b75',
- 'phabricator-search-results-css' => '505dd8cf',
- 'phabricator-shaped-request' => '7cbe244b',
- 'phabricator-slowvote-css' => 'a94b7230',
- 'phabricator-source-code-view-css' => '2ab25dfa',
- 'phabricator-standard-page-view' => '34ee718b',
- 'phabricator-textareautils' => '320810c8',
- 'phabricator-title' => '485aaa6c',
- 'phabricator-tooltip' => '358b8c04',
- 'phabricator-ui-example-css' => '528b19de',
- 'phabricator-zindex-css' => '9d8f7c4b',
- 'phame-css' => '8cb3afcd',
- 'pholio-css' => 'ca89d380',
- 'pholio-edit-css' => '07676f51',
- 'pholio-inline-comments-css' => '8e545e49',
- 'phortune-credit-card-form' => '2290aeef',
- 'phortune-credit-card-form-css' => '8391eb02',
- 'phortune-css' => '5b99dae0',
- 'phortune-invoice-css' => '476055e2',
- 'phrequent-css' => 'ffc185ad',
- 'phriction-document-css' => '4282e4ad',
- 'phui-action-panel-css' => 'b4798122',
- 'phui-badge-view-css' => '22c0cf4f',
- 'phui-basic-nav-view-css' => '98c11ab3',
- 'phui-big-info-view-css' => 'acc3492c',
- 'phui-box-css' => '4bd6cdb9',
- 'phui-bulk-editor-css' => '9a81e5d5',
- 'phui-button-bar-css' => 'f1ff5494',
- 'phui-button-css' => '6ccb303c',
- 'phui-button-simple-css' => '8e1baf68',
- 'phui-calendar-css' => 'f1ddf11c',
- 'phui-calendar-day-css' => '572b1893',
- 'phui-calendar-list-css' => '576be600',
- 'phui-calendar-month-css' => '21154caf',
- 'phui-chart-css' => '6bf6f78e',
- 'phui-cms-css' => '504b4b23',
- 'phui-comment-form-css' => 'ac68149f',
- 'phui-comment-panel-css' => 'f50152ad',
- 'phui-crumbs-view-css' => '10728aaa',
- 'phui-curtain-view-css' => '2bdaf026',
- 'phui-document-summary-view-css' => '9ca48bdf',
- 'phui-document-view-css' => '443bb464',
- 'phui-document-view-pro-css' => 'dd79b5df',
- 'phui-feed-story-css' => '44a9c8e9',
- 'phui-font-icon-base-css' => '870a7360',
- 'phui-fontkit-css' => '1320ed01',
- 'phui-form-css' => '7aaa04e3',
- 'phui-form-view-css' => '2f43fae7',
- 'phui-head-thing-view-css' => 'fd311e5f',
- 'phui-header-view-css' => '1ba8b707',
- 'phui-hovercard' => '1bd28176',
- 'phui-hovercard-view-css' => '4a484541',
- 'phui-icon-set-selector-css' => '87db8fee',
- 'phui-icon-view-css' => 'cf24ceec',
- 'phui-image-mask-css' => 'a8498f9c',
- 'phui-info-view-css' => 'e929f98c',
- 'phui-inline-comment-view-css' => '65ae3bc2',
- 'phui-invisible-character-view-css' => '6993d9f0',
- 'phui-left-right-css' => '75227a4d',
- 'phui-lightbox-css' => '0a035e40',
- 'phui-list-view-css' => '38f8c9bd',
- 'phui-object-box-css' => '9cff003c',
- 'phui-oi-big-ui-css' => '7a7c22af',
- 'phui-oi-color-css' => 'cd2b9b77',
- 'phui-oi-drag-ui-css' => '08f4ccc3',
- 'phui-oi-flush-ui-css' => '9d9685d6',
- 'phui-oi-list-view-css' => '7c5c1291',
- 'phui-oi-simple-ui-css' => 'a8beebea',
- 'phui-pager-css' => 'edcbc226',
- 'phui-pinboard-view-css' => '2495140e',
- 'phui-property-list-view-css' => '546a04ae',
- 'phui-remarkup-preview-css' => '54a34863',
- 'phui-segment-bar-view-css' => 'b1d1b892',
- 'phui-spacing-css' => '042804d6',
- 'phui-status-list-view-css' => 'd5263e49',
- 'phui-tag-view-css' => 'b4719c50',
- 'phui-theme-css' => '9f261c6b',
- 'phui-timeline-view-css' => '6ddf8126',
- 'phui-two-column-view-css' => '44ec4951',
- 'phui-workboard-color-css' => '783cdff5',
- 'phui-workboard-view-css' => '3bc85455',
- 'phui-workcard-view-css' => 'cca5fa92',
- 'phui-workpanel-view-css' => 'a3a63478',
- 'phuix-action-list-view' => 'b5c256b8',
- 'phuix-action-view' => '8d4a8c72',
- 'phuix-autocomplete' => 'df1bbd34',
- 'phuix-button-view' => '85ac9772',
- 'phuix-dropdown-menu' => '04b2ae03',
- 'phuix-form-control-view' => '210a16c1',
- 'phuix-icon-view' => 'bff6884b',
- 'policy-css' => '957ea14c',
- 'policy-edit-css' => '815c66f7',
- 'policy-transaction-detail-css' => '82100a43',
- 'ponder-view-css' => 'fbd45f96',
- 'project-card-view-css' => '0010bb52',
- 'project-view-css' => '792c9057',
- 'releeph-core' => '9b3c5733',
- 'releeph-preview-branch' => 'b7a6f4a5',
- 'releeph-request-differential-create-dialog' => '8d8b92cd',
- 'releeph-request-typeahead-css' => '667a48ae',
- 'setup-issue-css' => '30ee0173',
- 'sprite-login-css' => '396f3c3a',
- 'sprite-tokens-css' => '9cdfd599',
- 'syntax-default-css' => '9923583c',
- 'syntax-highlighting-css' => 'e9c95dd4',
- 'tokens-css' => '3d0f239e',
- 'typeahead-browse-css' => 'f2818435',
- 'unhandled-exception-css' => '4c96257a',
+ 'almanac-css' => '2e050f4f',
+ 'aphront-bars' => '4a327b4a',
+ 'aphront-dark-console-css' => '7f06cda2',
+ 'aphront-dialog-view-css' => 'b70c70df',
+ 'aphront-list-filter-view-css' => 'feb64255',
+ 'aphront-multi-column-view-css' => 'fbc00ba3',
+ 'aphront-panel-view-css' => '46923d46',
+ 'aphront-table-view-css' => '76eda3f8',
+ 'aphront-tokenizer-control-css' => 'b52d0668',
+ 'aphront-tooltip-css' => 'e3f2412f',
+ 'aphront-typeahead-control-css' => '8779483d',
+ 'application-search-view-css' => '0f7c06d8',
+ 'auth-css' => 'add92fd8',
+ 'bulk-job-css' => '73af99f5',
+ 'conduit-api-css' => 'ce2cfc41',
+ 'config-options-css' => '16c920ae',
+ 'conpherence-color-css' => 'b17746b0',
+ 'conpherence-durable-column-view' => '2d57072b',
+ 'conpherence-header-pane-css' => 'c9a3db8e',
+ 'conpherence-menu-css' => '67f4680d',
+ 'conpherence-message-pane-css' => 'd244db1e',
+ 'conpherence-notification-css' => '6a3d4e58',
+ 'conpherence-participant-pane-css' => '69e0058a',
+ 'conpherence-thread-manager' => 'aec8e38c',
+ 'conpherence-transaction-css' => '3a3f5e7e',
+ 'd3' => 'd67475f5',
+ 'differential-changeset-view-css' => '73660575',
+ 'differential-core-view-css' => 'bdb93065',
+ 'differential-revision-add-comment-css' => '7e5900d9',
+ 'differential-revision-comment-css' => '7dbc8d1d',
+ 'differential-revision-history-css' => '8aa3eac5',
+ 'differential-revision-list-css' => '93d2df7d',
+ 'differential-table-of-contents-css' => '0e3364c7',
+ 'diffusion-css' => 'b54c77b0',
+ 'diffusion-icons-css' => '23b31a1b',
+ 'diffusion-readme-css' => 'b68a76e4',
+ 'diffusion-repository-css' => 'b89e8c6c',
+ 'diviner-shared-css' => '4bd263b0',
+ 'font-fontawesome' => '3883938a',
+ 'font-lato' => '23631304',
+ 'global-drag-and-drop-css' => '1d2713a4',
+ 'harbormaster-css' => '8dfe16b2',
+ 'herald-css' => '648d39e2',
+ 'herald-rule-editor' => '27daef73',
+ 'herald-test-css' => 'e004176f',
+ 'inline-comment-summary-css' => '81eb368d',
+ 'javelin-aphlict' => '022516b4',
+ 'javelin-behavior' => 'fce5d170',
+ 'javelin-behavior-aphlict-dropdown' => 'e9a2940f',
+ 'javelin-behavior-aphlict-listen' => '4e61fa88',
+ 'javelin-behavior-aphlict-status' => 'c3703a16',
+ 'javelin-behavior-aphront-basic-tokenizer' => '3b4899b0',
+ 'javelin-behavior-aphront-drag-and-drop-textarea' => '7ad020a5',
+ 'javelin-behavior-aphront-form-disable-on-submit' => '55d7b788',
+ 'javelin-behavior-aphront-more' => '506aa3f4',
+ 'javelin-behavior-audio-source' => '3dc5ad43',
+ 'javelin-behavior-audit-preview' => 'b7b73831',
+ 'javelin-behavior-badge-view' => '92cdd7b6',
+ 'javelin-behavior-bulk-editor' => 'aa6d2308',
+ 'javelin-behavior-bulk-job-reload' => '3829a3cf',
+ 'javelin-behavior-calendar-month-view' => '158c64e0',
+ 'javelin-behavior-choose-control' => '04f8a1e3',
+ 'javelin-behavior-comment-actions' => '4dffaeb2',
+ 'javelin-behavior-config-reorder-fields' => '2539f834',
+ 'javelin-behavior-conpherence-menu' => '8c2ed2bf',
+ 'javelin-behavior-conpherence-participant-pane' => '43ba89a2',
+ 'javelin-behavior-conpherence-pontificate' => '4ae58b5a',
+ 'javelin-behavior-conpherence-search' => '91befbcc',
+ 'javelin-behavior-countdown-timer' => '6a162524',
+ 'javelin-behavior-dark-console' => 'f39d968b',
+ 'javelin-behavior-dashboard-async-panel' => '09ecf50c',
+ 'javelin-behavior-dashboard-move-panels' => '076bd092',
+ 'javelin-behavior-dashboard-query-panel-select' => '1e413dc9',
+ 'javelin-behavior-dashboard-tab-panel' => '9b1cbd76',
+ 'javelin-behavior-day-view' => '727a5a61',
+ 'javelin-behavior-desktop-notifications-control' => '070679fe',
+ 'javelin-behavior-detect-timezone' => '78bc5d94',
+ 'javelin-behavior-device' => '0cf79f45',
+ 'javelin-behavior-diff-preview-link' => 'f51e9c17',
+ 'javelin-behavior-differential-diff-radios' => '925fe8cd',
+ 'javelin-behavior-differential-populate' => 'dfa1d313',
+ 'javelin-behavior-differential-user-select' => 'e18685c0',
+ 'javelin-behavior-diffusion-commit-branches' => '4b671572',
+ 'javelin-behavior-diffusion-commit-graph' => '1c88f154',
+ 'javelin-behavior-diffusion-locate-file' => '87428eb2',
+ 'javelin-behavior-diffusion-pull-lastmodified' => 'c715c123',
+ 'javelin-behavior-document-engine' => '243d6c22',
+ 'javelin-behavior-doorkeeper-tag' => '6a85bc5a',
+ 'javelin-behavior-drydock-live-operation-status' => '47a0728b',
+ 'javelin-behavior-durable-column' => 'fa6f30b2',
+ 'javelin-behavior-editengine-reorder-configs' => '4842f137',
+ 'javelin-behavior-editengine-reorder-fields' => '0ad8d31f',
+ 'javelin-behavior-event-all-day' => '0b1bc990',
+ 'javelin-behavior-fancy-datepicker' => '956f3eeb',
+ 'javelin-behavior-global-drag-and-drop' => '1cab0e9a',
+ 'javelin-behavior-harbormaster-log' => 'b347a301',
+ 'javelin-behavior-herald-rule-editor' => '0922e81d',
+ 'javelin-behavior-high-security-warning' => 'dae2d55b',
+ 'javelin-behavior-history-install' => '6a1583a8',
+ 'javelin-behavior-icon-composer' => '38a6cedb',
+ 'javelin-behavior-launch-icon-composer' => 'a17b84f1',
+ 'javelin-behavior-lightbox-attachments' => 'c7e748bf',
+ 'javelin-behavior-line-chart' => 'c8147a20',
+ 'javelin-behavior-linked-container' => '74446546',
+ 'javelin-behavior-maniphest-batch-selector' => 'cffd39b4',
+ 'javelin-behavior-maniphest-list-editor' => 'c687e867',
+ 'javelin-behavior-maniphest-subpriority-editor' => '8400307c',
+ 'javelin-behavior-owners-path-editor' => 'ff688a7a',
+ 'javelin-behavior-passphrase-credential-control' => '48fe33d0',
+ 'javelin-behavior-phabricator-active-nav' => '7353f43d',
+ 'javelin-behavior-phabricator-autofocus' => '65bb0011',
+ 'javelin-behavior-phabricator-clipboard-copy' => 'cf32921f',
+ 'javelin-behavior-phabricator-file-tree' => 'ee82cedb',
+ 'javelin-behavior-phabricator-gesture' => 'b58d1a2a',
+ 'javelin-behavior-phabricator-gesture-example' => '242dedd0',
+ 'javelin-behavior-phabricator-keyboard-pager' => '1325b731',
+ 'javelin-behavior-phabricator-keyboard-shortcuts' => '2cc87f49',
+ 'javelin-behavior-phabricator-line-linker' => 'e15c8b1f',
+ 'javelin-behavior-phabricator-nav' => 'f166c949',
+ 'javelin-behavior-phabricator-notification-example' => '29819b75',
+ 'javelin-behavior-phabricator-object-selector' => 'a4af0b4a',
+ 'javelin-behavior-phabricator-oncopy' => '418f6684',
+ 'javelin-behavior-phabricator-remarkup-assist' => '2f80333f',
+ 'javelin-behavior-phabricator-reveal-content' => 'b105a3a6',
+ 'javelin-behavior-phabricator-search-typeahead' => '1cb7d027',
+ 'javelin-behavior-phabricator-show-older-transactions' => '600f440c',
+ 'javelin-behavior-phabricator-tooltips' => '73ecc1f8',
+ 'javelin-behavior-phabricator-transaction-comment-form' => '2bdadf1a',
+ 'javelin-behavior-phabricator-transaction-list' => '9cec214e',
+ 'javelin-behavior-phabricator-watch-anchor' => '0e6d261f',
+ 'javelin-behavior-pholio-mock-edit' => '3eed1f2b',
+ 'javelin-behavior-pholio-mock-view' => '5aa1544e',
+ 'javelin-behavior-phui-dropdown-menu' => '5cf0501a',
+ 'javelin-behavior-phui-file-upload' => 'e150bd50',
+ 'javelin-behavior-phui-hovercards' => '6c379000',
+ 'javelin-behavior-phui-selectable-list' => 'b26a41e4',
+ 'javelin-behavior-phui-submenu' => 'b5e9bff9',
+ 'javelin-behavior-phui-tab-group' => '242aa08b',
+ 'javelin-behavior-phuix-example' => 'c2c500a7',
+ 'javelin-behavior-policy-control' => '0eaa33a9',
+ 'javelin-behavior-policy-rule-editor' => '9347f172',
+ 'javelin-behavior-project-boards' => '05c74d65',
+ 'javelin-behavior-project-create' => '34c53422',
+ 'javelin-behavior-quicksand-blacklist' => '5a6f6a06',
+ 'javelin-behavior-read-only-warning' => 'b9109f8f',
+ 'javelin-behavior-redirect' => '407ee861',
+ 'javelin-behavior-refresh-csrf' => '46116c01',
+ 'javelin-behavior-releeph-preview-branch' => '75184d68',
+ 'javelin-behavior-releeph-request-state-change' => '9f081f05',
+ 'javelin-behavior-releeph-request-typeahead' => 'aa3a100c',
+ 'javelin-behavior-remarkup-load-image' => '202bfa3f',
+ 'javelin-behavior-remarkup-preview' => 'd8a86cfb',
+ 'javelin-behavior-reorder-applications' => 'aa371860',
+ 'javelin-behavior-reorder-columns' => '8ac32fd9',
+ 'javelin-behavior-reorder-profile-menu-items' => 'e5bdb730',
+ 'javelin-behavior-repository-crossreference' => 'db0c0214',
+ 'javelin-behavior-scrollbar' => '92388bae',
+ 'javelin-behavior-search-reorder-queries' => 'b86f297f',
+ 'javelin-behavior-select-content' => 'e8240b50',
+ 'javelin-behavior-select-on-click' => '66365ee2',
+ 'javelin-behavior-setup-check-https' => '01384686',
+ 'javelin-behavior-stripe-payment-form' => '02cb4398',
+ 'javelin-behavior-test-payment-form' => '4a7fb02b',
+ 'javelin-behavior-time-typeahead' => '5803b9e7',
+ 'javelin-behavior-toggle-class' => 'f5c78ae3',
+ 'javelin-behavior-toggle-widget' => '8f959ad0',
+ 'javelin-behavior-typeahead-browse' => '70245195',
+ 'javelin-behavior-typeahead-search' => '7b139193',
+ 'javelin-behavior-user-menu' => '60cd9241',
+ 'javelin-behavior-view-placeholder' => 'a9942052',
+ 'javelin-behavior-workflow' => '9623adc1',
+ 'javelin-color' => '78f811c9',
+ 'javelin-cookie' => '05d290ef',
+ 'javelin-diffusion-locate-file-source' => '94243d89',
+ 'javelin-dom' => '94681e22',
+ 'javelin-dynval' => '202a2e85',
+ 'javelin-event' => 'c03f2fb4',
+ 'javelin-fx' => '34450586',
+ 'javelin-history' => '030b4f7a',
+ 'javelin-install' => '5902260c',
+ 'javelin-json' => '541f81c3',
+ 'javelin-leader' => '0d2490ce',
+ 'javelin-magical-init' => '98e6504a',
+ 'javelin-mask' => '7c4d8998',
+ 'javelin-quicksand' => 'd3799cb4',
+ 'javelin-reactor' => '1c850a26',
+ 'javelin-reactor-dom' => '6cfa0008',
+ 'javelin-reactor-node-calmer' => '225bbb98',
+ 'javelin-reactornode' => '72960bc1',
+ 'javelin-request' => '91863989',
+ 'javelin-resource' => '740956e1',
+ 'javelin-routable' => '6a18c42e',
+ 'javelin-router' => '32755edb',
+ 'javelin-scrollbar' => 'a43ae2ae',
+ 'javelin-sound' => 'e562708c',
+ 'javelin-stratcom' => '0889b835',
+ 'javelin-tokenizer' => '89a1ae3a',
+ 'javelin-typeahead' => 'a4356cde',
+ 'javelin-typeahead-composite-source' => '22ee68a5',
+ 'javelin-typeahead-normalizer' => 'a241536a',
+ 'javelin-typeahead-ondemand-source' => '23387297',
+ 'javelin-typeahead-preloaded-source' => '5a79f6c3',
+ 'javelin-typeahead-source' => '8badee71',
+ 'javelin-typeahead-static-source' => '80bff3af',
+ 'javelin-uri' => '2e255291',
+ 'javelin-util' => '22ae1776',
+ 'javelin-vector' => 'e9c80beb',
+ 'javelin-view' => '289bf236',
+ 'javelin-view-html' => 'f8c4e135',
+ 'javelin-view-interpreter' => '876506b6',
+ 'javelin-view-renderer' => '9aae2b66',
+ 'javelin-view-visitor' => '308f9fe4',
+ 'javelin-websocket' => 'fdc13e4e',
+ 'javelin-workboard-board' => '45d0b2b1',
+ 'javelin-workboard-card' => '9a513421',
+ 'javelin-workboard-column' => '8573dc1b',
+ 'javelin-workboard-controller' => '42c7a5a7',
+ 'javelin-workflow' => '958e9045',
+ 'maniphest-report-css' => '3d53188b',
+ 'maniphest-task-edit-css' => '272daa84',
+ 'maniphest-task-summary-css' => '61d1667e',
+ 'multirow-row-manager' => '5b54c823',
+ 'owners-path-editor' => '2a8b62d9',
+ 'owners-path-editor-css' => 'fa7c13ef',
+ 'paste-css' => 'b37bcd38',
+ 'path-typeahead' => 'ad486db3',
+ 'people-picture-menu-item-css' => 'fe8e07cf',
+ 'people-profile-css' => '2ea2daa1',
+ 'phabricator-action-list-view-css' => 'c1a7631d',
+ 'phabricator-busy' => '5202e831',
+ 'phabricator-chatlog-css' => 'abdc76ee',
+ 'phabricator-content-source-view-css' => 'cdf0d579',
+ 'phabricator-core-css' => '1b29ed61',
+ 'phabricator-countdown-css' => 'bff8012f',
+ 'phabricator-darklog' => '3b869402',
+ 'phabricator-darkmessage' => '26cd4b73',
+ 'phabricator-dashboard-css' => '4267d6c6',
+ 'phabricator-diff-changeset' => 'e7cf10d6',
+ 'phabricator-diff-changeset-list' => 'b91204e9',
+ 'phabricator-diff-inline' => 'a4a14a94',
+ 'phabricator-drag-and-drop-file-upload' => '4370900d',
+ 'phabricator-draggable-list' => '3c6bd549',
+ 'phabricator-fatal-config-template-css' => '20babf50',
+ 'phabricator-favicon' => '7930776a',
+ 'phabricator-feed-css' => 'd8b6e3f8',
+ 'phabricator-file-upload' => 'ab85e184',
+ 'phabricator-filetree-view-css' => '56cdd875',
+ 'phabricator-flag-css' => '2b77be8d',
+ 'phabricator-keyboard-shortcut' => 'c9749dcd',
+ 'phabricator-keyboard-shortcut-manager' => '37b8a04a',
+ 'phabricator-main-menu-view' => '8e2d9a28',
+ 'phabricator-nav-view-css' => 'f8a0c1bf',
+ 'phabricator-notification' => 'a9b91e3f',
+ 'phabricator-notification-css' => '30240bd2',
+ 'phabricator-notification-menu-css' => 'e6962e89',
+ 'phabricator-object-selector-css' => 'ee77366f',
+ 'phabricator-phtize' => '2f1db1ed',
+ 'phabricator-prefab' => 'bf457520',
+ 'phabricator-remarkup-css' => '9e627d41',
+ 'phabricator-search-results-css' => '9ea70ace',
+ 'phabricator-shaped-request' => 'abf88db8',
+ 'phabricator-slowvote-css' => '1694baed',
+ 'phabricator-source-code-view-css' => '03d7ac28',
+ 'phabricator-standard-page-view' => '8a295cb9',
+ 'phabricator-textareautils' => 'f340a484',
+ 'phabricator-title' => '43bc9360',
+ 'phabricator-tooltip' => '83754533',
+ 'phabricator-ui-example-css' => 'b4795059',
+ 'phabricator-zindex-css' => '99c0f5eb',
+ 'phame-css' => '799febf9',
+ 'pholio-css' => '88ef5ef1',
+ 'pholio-edit-css' => '4df55b3b',
+ 'pholio-inline-comments-css' => '722b48c2',
+ 'phortune-credit-card-form' => 'd12d214f',
+ 'phortune-credit-card-form-css' => '3b9868a8',
+ 'phortune-css' => '12e8251a',
+ 'phortune-invoice-css' => '4436b241',
+ 'phrequent-css' => 'bd79cc67',
+ 'phriction-document-css' => '03380da0',
+ 'phui-action-panel-css' => '6c386cbf',
+ 'phui-badge-view-css' => '666e25ad',
+ 'phui-basic-nav-view-css' => '56ebd66d',
+ 'phui-big-info-view-css' => '362ad37b',
+ 'phui-box-css' => '5ed3b8cb',
+ 'phui-bulk-editor-css' => '374d5e30',
+ 'phui-button-bar-css' => 'a4aa75c4',
+ 'phui-button-css' => 'ea704902',
+ 'phui-button-simple-css' => '1ff278aa',
+ 'phui-calendar-css' => 'f11073aa',
+ 'phui-calendar-day-css' => '9597d706',
+ 'phui-calendar-list-css' => 'ccd7e4e2',
+ 'phui-calendar-month-css' => 'cb758c42',
+ 'phui-chart-css' => '7853a69b',
+ 'phui-cms-css' => '8c05c41e',
+ 'phui-comment-form-css' => '68a2d99a',
+ 'phui-comment-panel-css' => 'ec4e31c0',
+ 'phui-crumbs-view-css' => '614f43cf',
+ 'phui-curtain-view-css' => '68c5efb6',
+ 'phui-document-summary-view-css' => 'b068eed1',
+ 'phui-document-view-css' => '52b748a5',
+ 'phui-document-view-pro-css' => 'b9613a10',
+ 'phui-feed-story-css' => 'a0c05029',
+ 'phui-font-icon-base-css' => 'd7994e06',
+ 'phui-fontkit-css' => '9b714a5e',
+ 'phui-form-css' => '159e2d9c',
+ 'phui-form-view-css' => '0807e7ac',
+ 'phui-head-thing-view-css' => 'd7f293df',
+ 'phui-header-view-css' => '93cea4ec',
+ 'phui-hovercard' => '074f0783',
+ 'phui-hovercard-view-css' => '6ca90fa0',
+ 'phui-icon-set-selector-css' => '7aa5f3ec',
+ 'phui-icon-view-css' => '281f964d',
+ 'phui-image-mask-css' => '62c7f4d2',
+ 'phui-info-view-css' => '37b8d9ce',
+ 'phui-inline-comment-view-css' => '48acce5b',
+ 'phui-invisible-character-view-css' => 'c694c4a4',
+ 'phui-left-right-css' => '68513c34',
+ 'phui-lightbox-css' => '4ebf22da',
+ 'phui-list-view-css' => '470b1adb',
+ 'phui-object-box-css' => '9b58483d',
+ 'phui-oi-big-ui-css' => '9e037c7a',
+ 'phui-oi-color-css' => 'b517bfa0',
+ 'phui-oi-drag-ui-css' => 'da15d3dc',
+ 'phui-oi-flush-ui-css' => '490e2e2e',
+ 'phui-oi-list-view-css' => '909f3844',
+ 'phui-oi-simple-ui-css' => '6a30fa46',
+ 'phui-pager-css' => 'd022c7ad',
+ 'phui-pinboard-view-css' => '1f08f5d8',
+ 'phui-property-list-view-css' => 'cad62236',
+ 'phui-remarkup-preview-css' => '91767007',
+ 'phui-segment-bar-view-css' => '5166b370',
+ 'phui-spacing-css' => 'b05cadc3',
+ 'phui-status-list-view-css' => 'e5ff8be0',
+ 'phui-tag-view-css' => 'a42fe34f',
+ 'phui-theme-css' => '35883b37',
+ 'phui-timeline-view-css' => '1e348e4b',
+ 'phui-two-column-view-css' => '01e6991e',
+ 'phui-workboard-color-css' => 'e86de308',
+ 'phui-workboard-view-css' => '74fc9d98',
+ 'phui-workcard-view-css' => '8c536f90',
+ 'phui-workpanel-view-css' => 'bd546a49',
+ 'phuix-action-list-view' => 'c68f183f',
+ 'phuix-action-view' => 'aaa08f3b',
+ 'phuix-autocomplete' => '58cc4ab8',
+ 'phuix-button-view' => '55a24e84',
+ 'phuix-dropdown-menu' => 'bdce4d78',
+ 'phuix-form-control-view' => '38c1f3fb',
+ 'phuix-icon-view' => 'a5257c4e',
+ 'policy-css' => 'ceb56a08',
+ 'policy-edit-css' => '8794e2ed',
+ 'policy-transaction-detail-css' => 'c02b8384',
+ 'ponder-view-css' => '05a09d0a',
+ 'project-card-view-css' => '3b1f7b20',
+ 'project-view-css' => '567858b3',
+ 'releeph-core' => 'f81ff2db',
+ 'releeph-preview-branch' => '22db5c07',
+ 'releeph-request-differential-create-dialog' => '0ac1ea31',
+ 'releeph-request-typeahead-css' => 'bce37359',
+ 'setup-issue-css' => '5eed85b2',
+ 'sprite-login-css' => '18b368a6',
+ 'sprite-tokens-css' => 'f1896dc5',
+ 'syntax-default-css' => '055fc231',
+ 'syntax-highlighting-css' => '8a16f91b',
+ 'tokens-css' => 'ce5a50bd',
+ 'typeahead-browse-css' => 'b7ed02d2',
+ 'unhandled-exception-css' => '9da8fdab',
),
'requires' => array(
- '00676f00' => array(
- 'javelin-install',
- 'javelin-dom',
- 'javelin-typeahead-preloaded-source',
- 'javelin-util',
- ),
- '013ffff9' => array(
- 'javelin-install',
- 'javelin-util',
- 'javelin-request',
- 'javelin-typeahead-source',
- ),
- '01fca1f0' => array(
- 'javelin-behavior',
- 'javelin-workflow',
- 'javelin-json',
- 'javelin-dom',
- 'phabricator-keyboard-shortcut',
- ),
- '0213259f' => array(
+ '01384686' => array(
'javelin-behavior',
'javelin-uri',
+ 'phabricator-notification',
),
- '040fce04' => array(
- 'javelin-behavior',
- 'javelin-request',
- ),
- '04b2ae03' => array(
+ '022516b4' => array(
'javelin-install',
'javelin-util',
- 'javelin-dom',
- 'javelin-vector',
- 'javelin-stratcom',
+ 'javelin-websocket',
+ 'javelin-leader',
+ 'javelin-json',
),
- '051c7832' => array(
+ '02cb4398' => array(
'javelin-behavior',
- 'javelin-stratcom',
'javelin-dom',
+ 'phortune-credit-card-form',
),
- '05270951' => array(
+ '030b4f7a' => array(
+ 'javelin-stratcom',
+ 'javelin-install',
+ 'javelin-uri',
'javelin-util',
- 'javelin-magical-init',
),
- '065227cc' => array(
+ '04f8a1e3' => array(
'javelin-behavior',
- 'javelin-dom',
'javelin-stratcom',
+ 'javelin-dom',
'javelin-workflow',
),
- '08f4ccc3' => array(
- 'phui-oi-list-view-css',
- ),
- '0a0b10e9' => array(
+ '05c74d65' => array(
'javelin-behavior',
- 'javelin-stratcom',
'javelin-dom',
- ),
- '0a3f3021' => array(
- 'javelin-behavior',
+ 'javelin-util',
+ 'javelin-vector',
'javelin-stratcom',
'javelin-workflow',
- 'javelin-dom',
- 'javelin-router',
- ),
- '0a84bcc1' => array(
- 'javelin-install',
- 'phuix-button-view',
+ 'javelin-workboard-controller',
),
- '0f764c35' => array(
+ '05d290ef' => array(
'javelin-install',
'javelin-util',
),
- '15d5ff71' => array(
- 'aphront-typeahead-control-css',
- 'phui-tag-view-css',
- ),
- '1802a242' => array(
- 'phui-theme-css',
+ '070679fe' => array(
+ 'javelin-behavior',
+ 'javelin-stratcom',
+ 'javelin-dom',
+ 'javelin-uri',
+ 'phabricator-notification',
),
- '185bbd53' => array(
+ '074f0783' => array(
'javelin-install',
+ 'javelin-dom',
+ 'javelin-vector',
+ 'javelin-request',
+ 'javelin-uri',
),
- '1ad0a787' => array(
- 'javelin-install',
- 'javelin-reactor',
+ '076bd092' => array(
+ 'javelin-behavior',
+ 'javelin-dom',
'javelin-util',
- 'javelin-reactor-node-calmer',
+ 'javelin-stratcom',
+ 'javelin-workflow',
+ 'phabricator-draggable-list',
),
- '1ae869f2' => array(
+ '0889b835' => array(
'javelin-install',
+ 'javelin-event',
'javelin-util',
- 'phabricator-keyboard-shortcut-manager',
+ 'javelin-magical-init',
),
- '1bd28176' => array(
- 'javelin-install',
- 'javelin-dom',
- 'javelin-vector',
- 'javelin-request',
- 'javelin-uri',
+ '0922e81d' => array(
+ 'herald-rule-editor',
+ 'javelin-behavior',
),
- '1db13e70' => array(
+ '09ecf50c' => array(
'javelin-behavior',
'javelin-dom',
- 'javelin-json',
'javelin-workflow',
- 'javelin-magical-init',
),
- '1f6794f6' => array(
+ '0ad8d31f' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
- 'javelin-uri',
- 'phabricator-textareautils',
+ 'phabricator-draggable-list',
),
- '1fe2510c' => array(
- 'javelin-install',
+ '0cf79f45' => array(
+ 'javelin-behavior',
+ 'javelin-stratcom',
'javelin-dom',
- ),
- '210a16c1' => array(
+ 'javelin-vector',
'javelin-install',
- 'javelin-dom',
),
- '2290aeef' => array(
+ '0d2490ce' => array(
'javelin-install',
+ ),
+ '0e6d261f' => array(
+ 'javelin-behavior',
+ 'javelin-stratcom',
'javelin-dom',
- 'javelin-json',
- 'javelin-workflow',
- 'javelin-util',
+ 'javelin-vector',
),
- 26167537 => array(
- 'javelin-install',
+ '0eaa33a9' => array(
+ 'javelin-behavior',
'javelin-dom',
'javelin-util',
- 'javelin-vector',
- 'javelin-stratcom',
+ 'phuix-dropdown-menu',
+ 'phuix-action-list-view',
+ 'phuix-action-view',
'javelin-workflow',
- 'phabricator-drag-and-drop-file-upload',
- 'javelin-workboard-board',
+ 'phuix-icon-view',
),
- '27ca6289' => array(
+ '1325b731' => array(
'javelin-behavior',
- 'javelin-stratcom',
- 'javelin-dom',
'javelin-uri',
- 'phabricator-notification',
+ 'phabricator-keyboard-shortcut',
+ ),
+ '1c850a26' => array(
+ 'javelin-install',
+ 'javelin-util',
),
- '291da458' => array(
+ '1c88f154' => array(
'javelin-behavior',
'javelin-dom',
+ 'javelin-stratcom',
),
- '2926fff2' => array(
+ '1cab0e9a' => array(
'javelin-behavior',
'javelin-dom',
+ 'javelin-uri',
+ 'javelin-mask',
+ 'phabricator-drag-and-drop-file-upload',
),
- '29274e2b' => array(
- 'javelin-install',
+ '1cb7d027' => array(
+ 'javelin-behavior',
+ 'javelin-typeahead-ondemand-source',
+ 'javelin-typeahead',
+ 'javelin-dom',
+ 'javelin-uri',
'javelin-util',
+ 'javelin-stratcom',
+ 'phabricator-prefab',
+ 'phuix-icon-view',
),
- '297123ca' => array(
- 'phui-fontkit-css',
- ),
- '2ae077e1' => array(
+ '1e413dc9' => array(
'javelin-behavior',
'javelin-dom',
- 'javelin-stratcom',
- 'javelin-behavior-device',
- 'javelin-scrollbar',
- 'javelin-quicksand',
- 'phabricator-keyboard-shortcut',
- 'conpherence-thread-manager',
),
- '2b8de964' => array(
- 'javelin-install',
- 'javelin-util',
+ '1ff278aa' => array(
+ 'phui-button-css',
),
- '2caa8fb8' => array(
+ '202a2e85' => array(
'javelin-install',
- 'javelin-event',
+ 'javelin-reactornode',
+ 'javelin-util',
+ 'javelin-reactor',
),
- '31420f77' => array(
+ '202bfa3f' => array(
'javelin-behavior',
+ 'javelin-request',
),
- '320810c8' => array(
+ '225bbb98' => array(
'javelin-install',
- 'javelin-dom',
- 'javelin-vector',
- ),
- '327a00d1' => array(
- 'javelin-behavior',
- 'javelin-stratcom',
- 'javelin-dom',
- 'javelin-workflow',
+ 'javelin-reactor',
+ 'javelin-util',
),
- '327f418a' => array(
+ '22ee68a5' => array(
'javelin-install',
- 'javelin-event',
+ 'javelin-typeahead-source',
'javelin-util',
- 'javelin-magical-init',
),
- '358b8c04' => array(
+ 23387297 => array(
'javelin-install',
'javelin-util',
- 'javelin-dom',
- 'javelin-vector',
+ 'javelin-request',
+ 'javelin-typeahead-source',
),
- '3935d8c4' => array(
+ 23631304 => array(
+ 'phui-fontkit-css',
+ ),
+ '242aa08b' => array(
'javelin-behavior',
- 'javelin-dom',
'javelin-stratcom',
+ 'javelin-dom',
),
- '3ab51e2c' => array(
- 'javelin-behavior',
- 'javelin-behavior-device',
+ '242dedd0' => array(
'javelin-stratcom',
+ 'javelin-behavior',
'javelin-vector',
'javelin-dom',
- 'javelin-magical-init',
),
- '3cb0b2fc' => array(
+ '243d6c22' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
- 'javelin-workflow',
- 'javelin-util',
- 'javelin-uri',
),
- '3dbf94d5' => array(
+ '2539f834' => array(
'javelin-behavior',
+ 'javelin-stratcom',
'javelin-dom',
+ 'javelin-json',
+ 'phabricator-draggable-list',
+ ),
+ '27daef73' => array(
+ 'multirow-row-manager',
+ 'javelin-install',
'javelin-util',
- 'javelin-workflow',
+ 'javelin-dom',
'javelin-stratcom',
+ 'javelin-json',
+ 'phabricator-prefab',
),
- '3ffe32d6' => array(
+ '289bf236' => array(
'javelin-install',
+ 'javelin-util',
),
- '4047cd35' => array(
+ '29819b75' => array(
+ 'phabricator-notification',
+ 'javelin-stratcom',
'javelin-behavior',
+ ),
+ '2a8b62d9' => array(
+ 'multirow-row-manager',
+ 'javelin-install',
+ 'path-typeahead',
'javelin-dom',
'javelin-util',
- 'javelin-stratcom',
- 'javelin-workflow',
- 'javelin-behavior-device',
- 'javelin-history',
- 'javelin-vector',
- 'javelin-scrollbar',
- 'phabricator-title',
- 'phabricator-shaped-request',
- 'conpherence-thread-manager',
+ 'phabricator-prefab',
+ 'phuix-form-control-view',
),
- '408bf173' => array(
+ '2bdadf1a' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
- 'javelin-stratcom',
- 'javelin-workflow',
- 'phabricator-draggable-list',
+ 'javelin-request',
+ 'phabricator-shaped-request',
),
- '4250a34e' => array(
+ '2cc87f49' => array(
'javelin-behavior',
+ 'javelin-workflow',
+ 'javelin-json',
'javelin-dom',
+ 'phabricator-keyboard-shortcut',
+ ),
+ '2e255291' => array(
+ 'javelin-install',
'javelin-util',
- 'javelin-vector',
'javelin-stratcom',
- 'javelin-workflow',
- 'javelin-workboard-controller',
- ),
- '443bb464' => array(
- 'katex-css',
),
- '44959b73' => array(
+ '2f1db1ed' => array(
'javelin-util',
- 'javelin-uri',
- 'javelin-install',
- ),
- '453c5375' => array(
- 'javelin-behavior',
- 'javelin-dom',
),
- '464259a2' => array(
+ '2f80333f' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
- ),
- '469c0d9e' => array(
- 'javelin-behavior',
- 'javelin-dom',
+ 'phabricator-phtize',
+ 'phabricator-textareautils',
'javelin-workflow',
+ 'javelin-vector',
+ 'phuix-autocomplete',
+ 'javelin-mask',
),
- 47830651 => array(
- 'javelin-behavior',
- 'javelin-dom',
- 'javelin-view-renderer',
+ '308f9fe4' => array(
'javelin-install',
+ 'javelin-util',
),
- 48086888 => array(
- 'javelin-behavior',
- 'javelin-dom',
- 'javelin-workflow',
- ),
- '484a6e22' => array(
- 'javelin-behavior',
- 'javelin-dom',
- 'phabricator-drag-and-drop-file-upload',
- 'phabricator-textareautils',
+ '32755edb' => array(
+ 'javelin-install',
+ 'javelin-util',
),
- '485aaa6c' => array(
+ 34450586 => array(
+ 'javelin-color',
'javelin-install',
+ 'javelin-util',
),
- '491416b3' => array(
+ '34c53422' => array(
'javelin-behavior',
- 'javelin-uri',
- 'phabricator-notification',
+ 'javelin-dom',
+ 'javelin-stratcom',
+ 'javelin-workflow',
),
- '4976858c' => array(
- 'javelin-magical-init',
+ '37b8a04a' => array(
'javelin-install',
'javelin-util',
- 'javelin-vector',
'javelin-stratcom',
- ),
- '4b3c4443' => array(
- 'phuix-icon-view',
- ),
- '4b700e9e' => array(
- 'javelin-behavior',
'javelin-dom',
- 'javelin-util',
- 'phabricator-shaped-request',
+ 'javelin-vector',
),
- '4c193c96' => array(
+ '3829a3cf' => array(
'javelin-behavior',
'javelin-uri',
- 'phabricator-notification',
),
- '4d863052' => array(
+ '38a6cedb' => array(
+ 'javelin-behavior',
'javelin-dom',
- 'javelin-util',
'javelin-stratcom',
+ ),
+ '38c1f3fb' => array(
'javelin-install',
- 'javelin-aphlict',
- 'javelin-workflow',
- 'javelin-router',
- 'javelin-behavior-device',
- 'javelin-vector',
+ 'javelin-dom',
),
- '4e3e79a6' => array(
+ '3b4899b0' => array(
'javelin-behavior',
- 'javelin-stratcom',
- 'javelin-dom',
+ 'phabricator-prefab',
),
- '4f774dac' => array(
+ '3c6bd549' => array(
'javelin-install',
'javelin-dom',
'javelin-stratcom',
'javelin-util',
- 'phabricator-notification-css',
- ),
- '503e17fd' => array(
- 'javelin-install',
- 'javelin-typeahead-source',
- 'javelin-util',
+ 'javelin-vector',
+ 'javelin-magical-init',
),
- '51c5ad07' => array(
+ '3dc5ad43' => array(
'javelin-behavior',
'javelin-stratcom',
+ 'javelin-vector',
'javelin-dom',
- 'javelin-request',
- 'javelin-util',
- 'phabricator-shaped-request',
),
- '522431f7' => array(
+ '3eed1f2b' => array(
'javelin-behavior',
- 'javelin-util',
- 'javelin-dom',
'javelin-stratcom',
- 'javelin-vector',
- 'javelin-typeahead-static-source',
+ 'javelin-dom',
+ 'javelin-workflow',
+ 'javelin-quicksand',
+ 'phabricator-phtize',
+ 'phabricator-drag-and-drop-file-upload',
+ 'phabricator-draggable-list',
),
- '549459b8' => array(
+ '407ee861' => array(
'javelin-behavior',
+ 'javelin-uri',
),
- '54b612ba' => array(
- 'javelin-color',
- 'javelin-install',
- 'javelin-util',
+ '418f6684' => array(
+ 'javelin-behavior',
+ 'javelin-dom',
),
- '54f314a0' => array(
+ '42c7a5a7' => array(
'javelin-install',
- 'javelin-util',
- 'javelin-request',
- 'javelin-typeahead-source',
- ),
- '55616e04' => array(
- 'javelin-behavior',
'javelin-dom',
'javelin-util',
- 'javelin-workflow',
- 'javelin-stratcom',
- 'conpherence-thread-manager',
- ),
- '558829c2' => array(
- 'javelin-stratcom',
- 'javelin-behavior',
'javelin-vector',
- 'javelin-dom',
+ 'javelin-stratcom',
+ 'javelin-workflow',
+ 'phabricator-drag-and-drop-file-upload',
+ 'javelin-workboard-board',
),
- '58dea2fa' => array(
+ '4370900d' => array(
'javelin-install',
'javelin-util',
'javelin-request',
'javelin-dom',
'javelin-uri',
'phabricator-file-upload',
),
- '599a8f5f' => array(
+ '43ba89a2' => array(
'javelin-behavior',
- 'javelin-aphlict',
- 'javelin-stratcom',
- 'javelin-request',
- 'javelin-uri',
'javelin-dom',
- 'javelin-json',
- 'javelin-router',
+ 'javelin-stratcom',
+ 'javelin-workflow',
'javelin-util',
- 'javelin-leader',
- 'javelin-sound',
'phabricator-notification',
+ 'conpherence-thread-manager',
+ ),
+ '43bc9360' => array(
+ 'javelin-install',
),
- '59a7976a' => array(
+ '45d0b2b1' => array(
'javelin-install',
'javelin-dom',
- 'javelin-fx',
+ 'javelin-util',
+ 'javelin-stratcom',
+ 'javelin-workflow',
+ 'phabricator-draggable-list',
+ 'javelin-workboard-column',
),
- '59b251eb' => array(
+ '46116c01' => array(
+ 'javelin-request',
'javelin-behavior',
- 'javelin-stratcom',
- 'javelin-vector',
'javelin-dom',
+ 'javelin-router',
+ 'javelin-util',
+ 'phabricator-busy',
),
- '59e27e74' => array(
+ '47a0728b' => array(
'javelin-behavior',
- 'javelin-stratcom',
- 'javelin-workflow',
'javelin-dom',
- 'phuix-form-control-view',
- 'phuix-icon-view',
- 'javelin-behavior-phabricator-gesture',
+ 'javelin-request',
),
- '5c54cbf3' => array(
+ '4842f137' => array(
'javelin-behavior',
'javelin-stratcom',
+ 'javelin-workflow',
'javelin-dom',
+ 'phabricator-draggable-list',
),
- '5e2634b9' => array(
+ '48fe33d0' => array(
'javelin-behavior',
- 'javelin-aphlict',
- 'phabricator-phtize',
'javelin-dom',
+ 'javelin-stratcom',
+ 'javelin-workflow',
+ 'javelin-util',
+ 'javelin-uri',
+ ),
+ '490e2e2e' => array(
+ 'phui-oi-list-view-css',
),
- '5e9f347c' => array(
+ '4a7fb02b' => array(
'javelin-behavior',
- 'multirow-row-manager',
'javelin-dom',
- 'javelin-util',
- 'phabricator-prefab',
- 'javelin-json',
+ 'phortune-credit-card-form',
),
- '60821bc7' => array(
+ '4ae58b5a' => array(
'javelin-behavior',
- 'javelin-stratcom',
'javelin-dom',
- ),
- '61cbc29a' => array(
- 'javelin-magical-init',
'javelin-util',
+ 'javelin-workflow',
+ 'javelin-stratcom',
+ 'conpherence-thread-manager',
),
- '62dfea03' => array(
- 'javelin-install',
+ '4b671572' => array(
+ 'javelin-behavior',
+ 'javelin-dom',
'javelin-util',
+ 'javelin-request',
),
- '635de1ec' => array(
+ '4dffaeb2' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
+ 'phuix-form-control-view',
+ 'phuix-icon-view',
+ 'javelin-behavior-phabricator-gesture',
),
- 66888767 => array(
+ '4e61fa88' => array(
'javelin-behavior',
+ 'javelin-aphlict',
'javelin-stratcom',
- 'javelin-util',
- 'javelin-dom',
'javelin-request',
- 'phabricator-keyboard-shortcut',
- 'phabricator-darklog',
- 'phabricator-darkmessage',
- ),
- '66a62306' => array(
- 'javelin-behavior',
- 'javelin-stratcom',
+ 'javelin-uri',
'javelin-dom',
- 'javelin-history',
+ 'javelin-json',
+ 'javelin-router',
+ 'javelin-util',
+ 'javelin-leader',
+ 'javelin-sound',
+ 'phabricator-notification',
),
- '66a6def1' => array(
+ '506aa3f4' => array(
'javelin-behavior',
+ 'javelin-stratcom',
'javelin-dom',
- 'javelin-util',
- 'multirow-row-manager',
- 'javelin-json',
- 'phuix-form-control-view',
),
- '680ea2c8' => array(
+ '5202e831' => array(
'javelin-install',
'javelin-dom',
- 'phabricator-notification',
+ 'javelin-fx',
),
- '68af71ca' => array(
+ '541f81c3' => array(
'javelin-install',
- 'javelin-dom',
- 'phuix-button-view',
),
- '69adf288' => array(
+ '55a24e84' => array(
'javelin-install',
+ 'javelin-dom',
),
- '6a726c55' => array(
+ '55d7b788' => array(
+ 'javelin-behavior',
'javelin-stratcom',
- 'javelin-request',
'javelin-dom',
- 'javelin-vector',
- 'javelin-install',
- 'javelin-util',
- 'javelin-mask',
- 'javelin-uri',
- 'javelin-routable',
),
- '6b31879a' => array(
+ '5803b9e7' => array(
'javelin-behavior',
+ 'javelin-util',
+ 'javelin-dom',
'javelin-stratcom',
+ 'javelin-vector',
+ 'javelin-typeahead-static-source',
+ ),
+ '58cc4ab8' => array(
+ 'javelin-install',
'javelin-dom',
- 'javelin-mask',
- 'javelin-util',
'phuix-icon-view',
- 'phabricator-busy',
+ 'phabricator-prefab',
),
- '6b8ef10b' => array(
- 'javelin-install',
+ '5902260c' => array(
+ 'javelin-util',
+ 'javelin-magical-init',
),
- '6c0e62fa' => array(
- 'javelin-install',
- 'javelin-typeahead-source',
+ '5a6f6a06' => array(
+ 'javelin-behavior',
+ 'javelin-quicksand',
),
- '6c2b09a2' => array(
+ '5a79f6c3' => array(
'javelin-install',
'javelin-util',
+ 'javelin-request',
+ 'javelin-typeahead-source',
),
- '6d3e1947' => array(
+ '5aa1544e' => array(
'javelin-behavior',
- 'javelin-diffusion-locate-file-source',
- 'javelin-dom',
- 'javelin-typeahead',
- 'javelin-uri',
- ),
- '6d8c7912' => array(
- 'javelin-install',
- 'javelin-typeahead',
+ 'javelin-util',
+ 'javelin-stratcom',
'javelin-dom',
+ 'javelin-vector',
+ 'javelin-magical-init',
'javelin-request',
- 'javelin-typeahead-ondemand-source',
- 'javelin-util',
+ 'javelin-history',
+ 'javelin-workflow',
+ 'javelin-mask',
+ 'javelin-behavior-device',
+ 'phabricator-keyboard-shortcut',
),
- '70baed2f' => array(
+ '5b54c823' => array(
'javelin-install',
+ 'javelin-stratcom',
'javelin-dom',
- 'javelin-vector',
'javelin-util',
),
- 71237763 => array(
+ '5cf0501a' => array(
'javelin-behavior',
- 'javelin-dom',
'javelin-stratcom',
- 'javelin-workflow',
- 'phabricator-draggable-list',
+ 'javelin-dom',
+ 'phuix-dropdown-menu',
),
- '7319e029' => array(
+ '600f440c' => array(
'javelin-behavior',
+ 'javelin-stratcom',
'javelin-dom',
+ 'phabricator-busy',
),
- '758b4758' => array(
- 'javelin-install',
- 'javelin-workboard-card',
+ '60cd9241' => array(
+ 'javelin-behavior',
),
- '75b83cbb' => array(
+ '65bb0011' => array(
'javelin-behavior',
'javelin-dom',
- 'javelin-stratcom',
),
- '76b9fc3e' => array(
+ '66365ee2' => array(
'javelin-behavior',
'javelin-stratcom',
- 'javelin-workflow',
'javelin-dom',
- 'phabricator-draggable-list',
),
- '76f4ebed' => array(
- 'javelin-install',
- 'javelin-reactor',
- 'javelin-util',
+ '6a1583a8' => array(
+ 'javelin-behavior',
+ 'javelin-history',
),
- '77b0ae28' => array(
- 'javelin-install',
- 'javelin-util',
- 'javelin-dom',
- 'javelin-typeahead',
- 'javelin-tokenizer',
- 'javelin-typeahead-preloaded-source',
- 'javelin-typeahead-ondemand-source',
+ '6a162524' => array(
+ 'javelin-behavior',
'javelin-dom',
- 'javelin-stratcom',
- 'javelin-util',
),
- '77c1f0b0' => array(
+ '6a18c42e' => array(
+ 'javelin-install',
+ ),
+ '6a30fa46' => array(
+ 'phui-oi-list-view-css',
+ ),
+ '6a85bc5a' => array(
'javelin-behavior',
'javelin-dom',
- 'javelin-request',
- 'javelin-util',
+ 'javelin-json',
+ 'javelin-workflow',
+ 'javelin-magical-init',
),
- '7927a7d3' => array(
+ '6c379000' => array(
'javelin-behavior',
- 'javelin-quicksand',
+ 'javelin-behavior-device',
+ 'javelin-stratcom',
+ 'javelin-vector',
+ 'phui-hovercard',
+ ),
+ '6cfa0008' => array(
+ 'javelin-dom',
+ 'javelin-dynval',
+ 'javelin-reactor',
+ 'javelin-reactornode',
+ 'javelin-install',
+ 'javelin-util',
),
- '7a68dda3' => array(
- 'owners-path-editor',
+ 70245195 => array(
'javelin-behavior',
+ 'javelin-stratcom',
+ 'javelin-workflow',
+ 'javelin-dom',
),
- '7a7c22af' => array(
- 'phui-oi-list-view-css',
+ '727a5a61' => array(
+ 'phuix-icon-view',
),
- '7cbe244b' => array(
+ '72960bc1' => array(
'javelin-install',
+ 'javelin-reactor',
'javelin-util',
- 'javelin-request',
- 'javelin-router',
- ),
- '7e41274a' => array(
- 'javelin-install',
+ 'javelin-reactor-node-calmer',
),
- '7ebaeed3' => array(
- 'herald-rule-editor',
+ '7353f43d' => array(
'javelin-behavior',
+ 'javelin-stratcom',
+ 'javelin-vector',
+ 'javelin-dom',
+ 'javelin-uri',
+ ),
+ 73660575 => array(
+ 'phui-inline-comment-view-css',
),
- '7ee2b591' => array(
+ '73ecc1f8' => array(
'javelin-behavior',
- 'javelin-history',
+ 'javelin-behavior-device',
+ 'javelin-stratcom',
+ 'phabricator-tooltip',
),
- '7f243deb' => array(
+ '740956e1' => array(
+ 'javelin-util',
+ 'javelin-uri',
'javelin-install',
),
- '834a1173' => array(
+ 74446546 => array(
'javelin-behavior',
- 'javelin-scrollbar',
+ 'javelin-dom',
),
- '8499b6ab' => array(
+ '75184d68' => array(
'javelin-behavior',
'javelin-dom',
- 'javelin-stratcom',
+ 'javelin-uri',
+ 'javelin-request',
+ ),
+ '78bc5d94' => array(
+ 'javelin-behavior',
+ 'javelin-uri',
+ 'phabricator-notification',
+ ),
+ '78f811c9' => array(
+ 'javelin-install',
),
- '85ac9772' => array(
+ '7930776a' => array(
'javelin-install',
'javelin-dom',
),
- '85ee8ce6' => array(
- 'aphront-dialog-view-css',
+ '7ad020a5' => array(
+ 'javelin-behavior',
+ 'javelin-dom',
+ 'phabricator-drag-and-drop-file-upload',
+ 'phabricator-textareautils',
),
- '88236f00' => array(
+ '7b139193' => array(
'javelin-behavior',
- 'phabricator-keyboard-shortcut',
'javelin-stratcom',
+ 'javelin-workflow',
+ 'javelin-dom',
),
- '8935deef' => array(
+ '7c4d8998' => array(
'javelin-install',
'javelin-dom',
+ ),
+ '80bff3af' => array(
+ 'javelin-install',
+ 'javelin-typeahead-source',
+ ),
+ 83754533 => array(
+ 'javelin-install',
'javelin-util',
+ 'javelin-dom',
+ 'javelin-vector',
+ ),
+ '8400307c' => array(
+ 'javelin-behavior',
+ 'javelin-dom',
'javelin-stratcom',
'javelin-workflow',
'phabricator-draggable-list',
- 'javelin-workboard-column',
),
- '8a41885b' => array(
+ '8573dc1b' => array(
'javelin-install',
- 'javelin-dom',
+ 'javelin-workboard-card',
),
- '8ce821c5' => array(
- 'phabricator-notification',
- 'javelin-stratcom',
+ '87428eb2' => array(
'javelin-behavior',
+ 'javelin-diffusion-locate-file-source',
+ 'javelin-dom',
+ 'javelin-typeahead',
+ 'javelin-uri',
),
- '8d4a8c72' => array(
+ '876506b6' => array(
+ 'javelin-view',
'javelin-install',
'javelin-dom',
+ ),
+ '89a1ae3a' => array(
+ 'javelin-dom',
'javelin-util',
+ 'javelin-stratcom',
+ 'javelin-install',
),
- '8e1baf68' => array(
- 'phui-button-css',
+ '8a16f91b' => array(
+ 'syntax-default-css',
),
- '8f29b364' => array(
+ '8ac32fd9' => array(
'javelin-behavior',
'javelin-stratcom',
+ 'javelin-workflow',
'javelin-dom',
- 'phabricator-busy',
+ 'phabricator-draggable-list',
),
- '8ff5e24c' => array(
- 'javelin-behavior',
- 'javelin-stratcom',
+ '8badee71' => array(
+ 'javelin-install',
+ 'javelin-util',
'javelin-dom',
+ 'javelin-typeahead-normalizer',
),
- '901935ef' => array(
+ '8c2ed2bf' => array(
'javelin-behavior',
'javelin-dom',
- 'javelin-request',
- ),
- '9065f639' => array(
- 'javelin-install',
- 'javelin-dom',
+ 'javelin-util',
'javelin-stratcom',
+ 'javelin-workflow',
+ 'javelin-behavior-device',
+ 'javelin-history',
'javelin-vector',
+ 'javelin-scrollbar',
+ 'phabricator-title',
+ 'phabricator-shaped-request',
+ 'conpherence-thread-manager',
),
- '92b9ec77' => array(
- 'javelin-behavior',
- 'javelin-stratcom',
- 'javelin-dom',
+ '8e2d9a28' => array(
+ 'phui-theme-css',
),
- '93d0c9e3' => array(
+ '8f959ad0' => array(
'javelin-behavior',
- 'javelin-stratcom',
- 'javelin-workflow',
'javelin-dom',
+ 'javelin-util',
+ 'javelin-workflow',
+ 'javelin-stratcom',
),
- '949c0fe5' => array(
- 'javelin-install',
- ),
- '94b750d2' => array(
+ 91863989 => array(
'javelin-install',
'javelin-stratcom',
'javelin-util',
'javelin-behavior',
'javelin-json',
'javelin-dom',
'javelin-resource',
'javelin-routable',
),
- '960f6a39' => array(
+ '91befbcc' => array(
'javelin-behavior',
'javelin-dom',
- 'javelin-uri',
- 'javelin-mask',
- 'phabricator-drag-and-drop-file-upload',
+ 'javelin-util',
+ 'javelin-workflow',
+ 'javelin-stratcom',
),
- '9a860428' => array(
+ '92388bae' => array(
'javelin-behavior',
- 'javelin-dom',
- 'javelin-stratcom',
- 'javelin-uri',
+ 'javelin-scrollbar',
),
- '9bbf3762' => array(
+ '925fe8cd' => array(
'javelin-behavior',
- 'javelin-dom',
- 'javelin-util',
- 'javelin-workflow',
'javelin-stratcom',
+ 'javelin-dom',
),
- '9d32bc88' => array(
+ '92cdd7b6' => array(
'javelin-behavior',
- 'javelin-behavior-device',
'javelin-stratcom',
'javelin-dom',
- 'javelin-magical-init',
- 'javelin-vector',
- 'javelin-request',
- 'javelin-util',
- ),
- '9d9685d6' => array(
- 'phui-oi-list-view-css',
),
- '9f36c42d' => array(
+ '9347f172' => array(
'javelin-behavior',
- 'javelin-stratcom',
+ 'multirow-row-manager',
+ 'javelin-dom',
+ 'javelin-util',
+ 'phabricator-prefab',
+ 'javelin-json',
+ ),
+ '94243d89' => array(
+ 'javelin-install',
'javelin-dom',
+ 'javelin-typeahead-preloaded-source',
+ 'javelin-util',
+ ),
+ '94681e22' => array(
+ 'javelin-magical-init',
+ 'javelin-install',
+ 'javelin-util',
'javelin-vector',
+ 'javelin-stratcom',
),
- 'a0b57eb8' => array(
+ '956f3eeb' => array(
'javelin-behavior',
+ 'javelin-util',
'javelin-dom',
'javelin-stratcom',
- 'javelin-workflow',
- 'javelin-util',
- 'phabricator-keyboard-shortcut',
+ 'javelin-vector',
),
- 'a3714c76' => array(
- 'javelin-behavior',
+ '958e9045' => array(
'javelin-stratcom',
+ 'javelin-request',
'javelin-dom',
'javelin-vector',
'javelin-install',
- ),
- 'a3a63478' => array(
- 'phui-workcard-view-css',
- ),
- 'a464fe03' => array(
- 'javelin-behavior',
+ 'javelin-util',
+ 'javelin-mask',
'javelin-uri',
- 'phabricator-notification',
+ 'javelin-routable',
),
- 'a6b98425' => array(
+ '9623adc1' => array(
'javelin-behavior',
+ 'javelin-stratcom',
+ 'javelin-workflow',
'javelin-dom',
- 'phortune-credit-card-form',
+ 'javelin-router',
+ ),
+ '9a513421' => array(
+ 'javelin-install',
+ ),
+ '9aae2b66' => array(
+ 'javelin-install',
+ 'javelin-util',
),
- 'a6f7a73b' => array(
+ '9b1cbd76' => array(
'javelin-behavior',
- 'javelin-stratcom',
'javelin-dom',
+ 'javelin-stratcom',
),
- 'a80d0378' => array(
+ '9cec214e' => array(
'javelin-behavior',
'javelin-stratcom',
+ 'javelin-workflow',
'javelin-dom',
+ 'javelin-uri',
+ 'phabricator-textareautils',
),
- 'a8beebea' => array(
+ '9e037c7a' => array(
'phui-oi-list-view-css',
),
- 'a8d8459d' => array(
+ '9f081f05' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
- ),
- 'a8da01f0' => array(
- 'javelin-behavior',
- 'javelin-uri',
+ 'javelin-workflow',
+ 'javelin-util',
'phabricator-keyboard-shortcut',
),
- 'a9f88de2' => array(
+ 'a17b84f1' => array(
'javelin-behavior',
'javelin-dom',
- 'javelin-stratcom',
'javelin-workflow',
- 'javelin-fx',
+ ),
+ 'a241536a' => array(
+ 'javelin-install',
+ ),
+ 'a4356cde' => array(
+ 'javelin-install',
+ 'javelin-dom',
+ 'javelin-vector',
'javelin-util',
),
- 'ab2f381b' => array(
- 'javelin-request',
+ 'a43ae2ae' => array(
+ 'javelin-install',
+ 'javelin-dom',
+ 'javelin-stratcom',
+ 'javelin-vector',
+ ),
+ 'a4a14a94' => array(
+ 'javelin-dom',
+ ),
+ 'a4aa75c4' => array(
+ 'phui-button-css',
+ 'phui-button-simple-css',
+ ),
+ 'a4af0b4a' => array(
'javelin-behavior',
'javelin-dom',
- 'javelin-router',
+ 'javelin-request',
'javelin-util',
- 'phabricator-busy',
),
- 'ab9e0a82' => array(
+ 'a5257c4e' => array(
'javelin-install',
- 'javelin-util',
'javelin-dom',
- 'javelin-typeahead-normalizer',
),
- 'acd29eee' => array(
+ 'a9942052' => array(
'javelin-behavior',
- 'javelin-stratcom',
'javelin-dom',
- 'phabricator-phtize',
- 'phabricator-textareautils',
- 'javelin-workflow',
- 'javelin-vector',
- 'phuix-autocomplete',
- 'javelin-mask',
+ 'javelin-view-renderer',
+ 'javelin-install',
),
- 'ad54037e' => array(
- 'javelin-behavior',
+ 'a9b91e3f' => array(
+ 'javelin-install',
'javelin-dom',
'javelin-stratcom',
'javelin-util',
+ 'phabricator-notification-css',
),
- 'b003d4fb' => array(
+ 'aa371860' => array(
'javelin-behavior',
'javelin-stratcom',
+ 'javelin-workflow',
'javelin-dom',
- 'phuix-dropdown-menu',
+ 'phabricator-draggable-list',
),
- 'b0b8f86d' => array(
+ 'aa3a100c' => array(
'javelin-behavior',
'javelin-dom',
- 'javelin-stratcom',
+ 'javelin-typeahead',
+ 'javelin-typeahead-ondemand-source',
+ 'javelin-dom',
),
- 'b23b49e6' => array(
+ 'aa6d2308' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
- 'javelin-request',
- 'phabricator-shaped-request',
+ 'multirow-row-manager',
+ 'javelin-json',
+ 'phuix-form-control-view',
),
- 'b2b4fbaf' => array(
- 'javelin-behavior',
+ 'aaa08f3b' => array(
+ 'javelin-install',
'javelin-dom',
- 'javelin-uri',
- 'javelin-request',
+ 'javelin-util',
),
- 'b3a4b884' => array(
- 'javelin-behavior',
- 'phabricator-prefab',
+ 'ab85e184' => array(
+ 'javelin-install',
+ 'javelin-dom',
+ 'phabricator-notification',
+ ),
+ 'abf88db8' => array(
+ 'javelin-install',
+ 'javelin-util',
+ 'javelin-request',
+ 'javelin-router',
),
- 'b3e7d692' => array(
+ 'ad486db3' => array(
'javelin-install',
+ 'javelin-typeahead',
+ 'javelin-dom',
+ 'javelin-request',
+ 'javelin-typeahead-ondemand-source',
+ 'javelin-util',
),
- 'b49b59d6' => array(
+ 'aec8e38c' => array(
'javelin-dom',
'javelin-util',
'javelin-stratcom',
'javelin-install',
+ 'javelin-aphlict',
'javelin-workflow',
'javelin-router',
'javelin-behavior-device',
'javelin-vector',
- 'phabricator-diff-inline',
),
- 'b59e1e96' => array(
+ 'b105a3a6' => array(
+ 'javelin-behavior',
+ 'javelin-stratcom',
+ 'javelin-dom',
+ ),
+ 'b26a41e4' => array(
+ 'javelin-behavior',
+ 'javelin-stratcom',
+ 'javelin-dom',
+ ),
+ 'b347a301' => array(
+ 'javelin-behavior',
+ ),
+ 'b517bfa0' => array(
+ 'phui-oi-list-view-css',
+ ),
+ 'b52d0668' => array(
+ 'aphront-typeahead-control-css',
+ 'phui-tag-view-css',
+ ),
+ 'b58d1a2a' => array(
'javelin-behavior',
+ 'javelin-behavior-device',
'javelin-stratcom',
- 'javelin-workflow',
- 'javelin-dom',
- 'phabricator-draggable-list',
- ),
- 'b5c256b8' => array(
- 'javelin-install',
+ 'javelin-vector',
'javelin-dom',
+ 'javelin-magical-init',
),
- 'b5d57730' => array(
- 'javelin-install',
+ 'b5e9bff9' => array(
+ 'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
- 'javelin-util',
),
- 'b6993408' => array(
+ 'b7b73831' => array(
'javelin-behavior',
- 'javelin-stratcom',
'javelin-dom',
- 'javelin-json',
- 'phabricator-draggable-list',
+ 'javelin-util',
+ 'phabricator-shaped-request',
),
- 'b95d6f7d' => array(
+ 'b86f297f' => array(
'javelin-behavior',
'javelin-stratcom',
+ 'javelin-workflow',
'javelin-dom',
- 'phuix-dropdown-menu',
+ 'phabricator-draggable-list',
),
- 'ba158207' => array(
+ 'b9109f8f' => array(
'javelin-behavior',
'javelin-uri',
'phabricator-notification',
),
- 'bb6e5c16' => array(
+ 'b91204e9' => array(
+ 'javelin-install',
+ 'phuix-button-view',
+ ),
+ 'bd546a49' => array(
+ 'phui-workcard-view-css',
+ ),
+ 'bdce4d78' => array(
+ 'javelin-install',
+ 'javelin-util',
'javelin-dom',
+ 'javelin-vector',
+ 'javelin-stratcom',
+ ),
+ 'bf457520' => array(
+ 'javelin-install',
'javelin-util',
+ 'javelin-dom',
+ 'javelin-typeahead',
+ 'javelin-tokenizer',
+ 'javelin-typeahead-preloaded-source',
+ 'javelin-typeahead-ondemand-source',
+ 'javelin-dom',
'javelin-stratcom',
+ 'javelin-util',
+ ),
+ 'c03f2fb4' => array(
+ 'javelin-install',
+ ),
+ 'c2c500a7' => array(
'javelin-install',
+ 'javelin-dom',
+ 'phuix-button-view',
),
- 'bcaccd64' => array(
+ 'c3703a16' => array(
'javelin-behavior',
- 'javelin-behavior-device',
- 'javelin-stratcom',
- 'javelin-vector',
- 'phui-hovercard',
+ 'javelin-aphlict',
+ 'phabricator-phtize',
+ 'javelin-dom',
),
- 'bdaf4d04' => array(
+ 'c687e867' => array(
'javelin-behavior',
'javelin-dom',
+ 'javelin-stratcom',
+ 'javelin-workflow',
+ 'javelin-fx',
'javelin-util',
- 'javelin-request',
),
- 'bea6e7f4' => array(
+ 'c68f183f' => array(
'javelin-install',
'javelin-dom',
- 'javelin-stratcom',
- 'javelin-util',
- 'javelin-vector',
- 'javelin-magical-init',
),
- 'bee502c8' => array(
+ 'c715c123' => array(
'javelin-behavior',
- 'javelin-stratcom',
'javelin-dom',
+ 'javelin-util',
'javelin-workflow',
- 'javelin-quicksand',
- 'phabricator-phtize',
- 'phabricator-drag-and-drop-file-upload',
- 'phabricator-draggable-list',
+ 'javelin-json',
),
- 'bf5374ef' => array(
+ 'c7e748bf' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
+ 'javelin-mask',
+ 'javelin-util',
+ 'phuix-icon-view',
+ 'phabricator-busy',
),
- 'bff6884b' => array(
- 'javelin-install',
+ 'c8147a20' => array(
+ 'javelin-behavior',
'javelin-dom',
+ 'javelin-vector',
+ 'phui-chart-css',
),
- 'c19dd9b9' => array(
+ 'c9749dcd' => array(
'javelin-install',
'javelin-util',
- 'javelin-stratcom',
- 'javelin-dom',
- 'javelin-vector',
+ 'phabricator-keyboard-shortcut-manager',
),
- 'c3e917d9' => array(
+ 'cf32921f' => array(
'javelin-behavior',
- 'javelin-typeahead-ondemand-source',
- 'javelin-typeahead',
'javelin-dom',
- 'javelin-uri',
- 'javelin-util',
'javelin-stratcom',
- 'phabricator-prefab',
- 'phuix-icon-view',
),
- 'c420b0b9' => array(
+ 'cffd39b4' => array(
'javelin-behavior',
- 'javelin-behavior-device',
- 'javelin-stratcom',
- 'phabricator-tooltip',
- ),
- 'c587b80f' => array(
- 'javelin-install',
- ),
- 'c7ccd872' => array(
- 'phui-fontkit-css',
- ),
- 'c90a04fc' => array(
'javelin-dom',
- 'javelin-dynval',
- 'javelin-reactor',
- 'javelin-reactornode',
- 'javelin-install',
+ 'javelin-stratcom',
'javelin-util',
),
- 'c96502cf' => array(
- 'multirow-row-manager',
+ 'd12d214f' => array(
'javelin-install',
- 'path-typeahead',
'javelin-dom',
+ 'javelin-json',
+ 'javelin-workflow',
'javelin-util',
- 'phabricator-prefab',
- 'phuix-form-control-view',
),
- 'c989ade3' => array(
+ 'd3799cb4' => array(
'javelin-install',
- 'javelin-util',
- 'javelin-stratcom',
),
- 'caade6f2' => array(
+ 'd8a86cfb' => array(
'javelin-behavior',
- 'javelin-request',
- 'javelin-stratcom',
- 'javelin-vector',
'javelin-dom',
- 'javelin-uri',
- 'javelin-behavior-device',
- 'phabricator-title',
- 'phabricator-favicon',
+ 'javelin-util',
+ 'phabricator-shaped-request',
),
- 'cd2b9b77' => array(
+ 'da15d3dc' => array(
'phui-oi-list-view-css',
),
- 'd057e45a' => array(
+ 'dae2d55b' => array(
'javelin-behavior',
- 'javelin-dom',
- 'javelin-stratcom',
- 'javelin-workflow',
- 'javelin-util',
+ 'javelin-uri',
'phabricator-notification',
- 'conpherence-thread-manager',
),
- 'd0c516d5' => array(
+ 'db0c0214' => array(
'javelin-behavior',
'javelin-dom',
- 'javelin-util',
- 'phuix-dropdown-menu',
- 'phuix-action-list-view',
- 'phuix-action-view',
- 'javelin-workflow',
- 'phuix-icon-view',
- ),
- 'd254d646' => array(
- 'javelin-util',
- ),
- 'd4505101' => array(
'javelin-stratcom',
- 'javelin-install',
'javelin-uri',
- 'javelin-util',
),
- 'd4eecc63' => array(
+ 'dfa1d313' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
+ 'phabricator-tooltip',
+ 'phabricator-diff-changeset-list',
+ 'phabricator-diff-changeset',
),
- 'd7a74243' => array(
+ 'e150bd50' => array(
'javelin-behavior',
'javelin-stratcom',
- 'javelin-workflow',
'javelin-dom',
- 'phabricator-draggable-list',
+ 'phuix-dropdown-menu',
),
- 'd835b03a' => array(
+ 'e15c8b1f' => array(
'javelin-behavior',
- 'javelin-dom',
- 'javelin-util',
- 'phabricator-shaped-request',
- ),
- 'db34a142' => array(
- 'phui-inline-comment-view-css',
- ),
- 'dca75c0e' => array(
- 'multirow-row-manager',
- 'javelin-install',
- 'javelin-util',
- 'javelin-dom',
'javelin-stratcom',
- 'javelin-json',
- 'phabricator-prefab',
+ 'javelin-dom',
+ 'javelin-history',
),
- 'de2e896f' => array(
+ 'e18685c0' => array(
'javelin-behavior',
'javelin-dom',
- 'javelin-typeahead',
- 'javelin-typeahead-ondemand-source',
- 'javelin-dom',
+ 'javelin-stratcom',
),
- 'df1bbd34' => array(
+ 'e562708c' => array(
'javelin-install',
- 'javelin-dom',
- 'phuix-icon-view',
- 'phabricator-prefab',
),
- 'e1d25dfb' => array(
+ 'e5bdb730' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-workflow',
'javelin-dom',
'phabricator-draggable-list',
),
- 'e1d4b11a' => array(
- 'javelin-install',
+ 'e7cf10d6' => array(
+ 'javelin-dom',
'javelin-util',
- 'javelin-websocket',
- 'javelin-leader',
- 'javelin-json',
- ),
- 'e1ff79b1' => array(
- 'javelin-behavior',
'javelin-stratcom',
- 'javelin-dom',
+ 'javelin-install',
+ 'javelin-workflow',
+ 'javelin-router',
+ 'javelin-behavior-device',
+ 'javelin-vector',
+ 'phabricator-diff-inline',
),
- 'e2e0a072' => array(
+ 'e8240b50' => array(
'javelin-behavior',
'javelin-stratcom',
- 'javelin-workflow',
'javelin-dom',
- 'phabricator-draggable-list',
),
- 'e379b58e' => array(
+ 'e9a2940f' => array(
'javelin-behavior',
+ 'javelin-request',
'javelin-stratcom',
'javelin-vector',
'javelin-dom',
'javelin-uri',
+ 'javelin-behavior-device',
+ 'phabricator-title',
+ 'phabricator-favicon',
),
- 'e4232876' => array(
- 'javelin-behavior',
- 'javelin-dom',
- 'javelin-vector',
- 'phui-chart-css',
+ 'e9c80beb' => array(
+ 'javelin-install',
+ 'javelin-event',
),
- 'e4cc26b3' => array(
- 'javelin-behavior',
- 'javelin-dom',
+ 'ec4e31c0' => array(
+ 'phui-timeline-view-css',
),
- 'e83d28f3' => array(
- 'javelin-dom',
+ 'ee77366f' => array(
+ 'aphront-dialog-view-css',
),
- 'e9581f08' => array(
+ 'ee82cedb' => array(
'javelin-behavior',
+ 'phabricator-keyboard-shortcut',
'javelin-stratcom',
- 'javelin-workflow',
- 'javelin-dom',
- 'phabricator-draggable-list',
),
- 'e9c95dd4' => array(
- 'syntax-default-css',
- ),
- 'ec1f3669' => array(
+ 'f166c949' => array(
'javelin-behavior',
- 'javelin-util',
+ 'javelin-behavior-device',
'javelin-stratcom',
'javelin-dom',
- 'javelin-vector',
'javelin-magical-init',
+ 'javelin-vector',
'javelin-request',
- 'javelin-history',
- 'javelin-workflow',
- 'javelin-mask',
- 'javelin-behavior-device',
- 'phabricator-keyboard-shortcut',
+ 'javelin-util',
+ ),
+ 'f340a484' => array(
+ 'javelin-install',
+ 'javelin-dom',
+ 'javelin-vector',
),
- 'ecf4e799' => array(
+ 'f39d968b' => array(
'javelin-behavior',
+ 'javelin-stratcom',
'javelin-util',
'javelin-dom',
- 'javelin-stratcom',
- 'javelin-vector',
+ 'javelin-request',
+ 'phabricator-keyboard-shortcut',
+ 'phabricator-darklog',
+ 'phabricator-darkmessage',
),
- 'edf8a145' => array(
+ 'f51e9c17' => array(
'javelin-behavior',
- 'javelin-uri',
+ 'javelin-stratcom',
+ 'javelin-dom',
),
- 'ef7e057f' => array(
- 'javelin-install',
+ 'f5c78ae3' => array(
+ 'javelin-behavior',
+ 'javelin-stratcom',
+ 'javelin-dom',
),
- 'efe49472' => array(
+ 'f8c4e135' => array(
'javelin-install',
- 'javelin-util',
- ),
- 'f01586dc' => array(
- 'javelin-behavior',
'javelin-dom',
+ 'javelin-view-visitor',
'javelin-util',
- 'javelin-workflow',
- 'javelin-json',
),
- 'f0eb6708' => array(
+ 'fa6f30b2' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
- 'phabricator-tooltip',
- 'phabricator-diff-changeset-list',
- 'phabricator-diff-changeset',
- ),
- 'f1701b75' => array(
- 'katex-css',
- ),
- 'f1ff5494' => array(
- 'phui-button-css',
- 'phui-button-simple-css',
- ),
- 'f50152ad' => array(
- 'phui-timeline-view-css',
+ 'javelin-behavior-device',
+ 'javelin-scrollbar',
+ 'javelin-quicksand',
+ 'phabricator-keyboard-shortcut',
+ 'conpherence-thread-manager',
),
- 'f6555212' => array(
- 'javelin-install',
- 'javelin-reactornode',
+ 'fce5d170' => array(
+ 'javelin-magical-init',
'javelin-util',
- 'javelin-reactor',
),
- 'f829edb3' => array(
- 'javelin-view',
+ 'fdc13e4e' => array(
'javelin-install',
- 'javelin-dom',
),
- 'fc91ab6c' => array(
+ 'ff688a7a' => array(
+ 'owners-path-editor',
'javelin-behavior',
- 'javelin-dom',
- 'phortune-credit-card-form',
- ),
- 'fe287620' => array(
- 'javelin-install',
- 'javelin-dom',
- 'javelin-view-visitor',
- 'javelin-util',
),
),
'packages' => array(
'conpherence.pkg.css' => array(
'conpherence-durable-column-view',
'conpherence-menu-css',
'conpherence-color-css',
'conpherence-message-pane-css',
'conpherence-notification-css',
'conpherence-transaction-css',
'conpherence-participant-pane-css',
'conpherence-header-pane-css',
),
'conpherence.pkg.js' => array(
'javelin-behavior-conpherence-menu',
'javelin-behavior-conpherence-participant-pane',
'javelin-behavior-conpherence-pontificate',
'javelin-behavior-toggle-widget',
),
'core.pkg.css' => array(
'phabricator-core-css',
'phabricator-zindex-css',
'phui-button-css',
'phui-button-simple-css',
'phui-theme-css',
'phabricator-standard-page-view',
'aphront-dialog-view-css',
'phui-form-view-css',
'aphront-panel-view-css',
'aphront-table-view-css',
'aphront-tokenizer-control-css',
'aphront-typeahead-control-css',
'aphront-list-filter-view-css',
'application-search-view-css',
'phabricator-remarkup-css',
'syntax-highlighting-css',
'syntax-default-css',
'phui-pager-css',
'aphront-tooltip-css',
'phabricator-flag-css',
'phui-info-view-css',
'phabricator-main-menu-view',
'phabricator-notification-css',
'phabricator-notification-menu-css',
'phui-lightbox-css',
'phui-comment-panel-css',
'phui-header-view-css',
'phabricator-nav-view-css',
'phui-basic-nav-view-css',
'phui-crumbs-view-css',
'phui-oi-list-view-css',
'phui-oi-color-css',
'phui-oi-big-ui-css',
'phui-oi-drag-ui-css',
'phui-oi-simple-ui-css',
'phui-oi-flush-ui-css',
'global-drag-and-drop-css',
'phui-spacing-css',
'phui-form-css',
'phui-icon-view-css',
'phabricator-action-list-view-css',
'phui-property-list-view-css',
'phui-tag-view-css',
'phui-list-view-css',
'font-fontawesome',
'font-lato',
'phui-font-icon-base-css',
'phui-fontkit-css',
'phui-box-css',
'phui-object-box-css',
'phui-timeline-view-css',
'phui-two-column-view-css',
'phui-curtain-view-css',
'sprite-login-css',
'sprite-tokens-css',
'tokens-css',
'auth-css',
'phui-status-list-view-css',
'phui-feed-story-css',
'phabricator-feed-css',
'phabricator-dashboard-css',
'aphront-multi-column-view-css',
),
'core.pkg.js' => array(
'javelin-util',
'javelin-install',
'javelin-event',
'javelin-stratcom',
'javelin-behavior',
'javelin-resource',
'javelin-request',
'javelin-vector',
'javelin-dom',
'javelin-json',
'javelin-uri',
'javelin-workflow',
'javelin-mask',
'javelin-typeahead',
'javelin-typeahead-normalizer',
'javelin-typeahead-source',
'javelin-typeahead-preloaded-source',
'javelin-typeahead-ondemand-source',
'javelin-tokenizer',
'javelin-history',
'javelin-router',
'javelin-routable',
'javelin-behavior-aphront-basic-tokenizer',
'javelin-behavior-workflow',
'javelin-behavior-aphront-form-disable-on-submit',
'phabricator-keyboard-shortcut-manager',
'phabricator-keyboard-shortcut',
'javelin-behavior-phabricator-keyboard-shortcuts',
'javelin-behavior-refresh-csrf',
'javelin-behavior-phabricator-watch-anchor',
'javelin-behavior-phabricator-autofocus',
'phuix-dropdown-menu',
'phuix-action-list-view',
'phuix-action-view',
'phuix-icon-view',
'phabricator-phtize',
'javelin-behavior-phabricator-oncopy',
'phabricator-tooltip',
'javelin-behavior-phabricator-tooltips',
'phabricator-prefab',
'javelin-behavior-device',
'javelin-behavior-toggle-class',
'javelin-behavior-lightbox-attachments',
'phabricator-busy',
'javelin-sound',
'javelin-aphlict',
'phabricator-notification',
'javelin-behavior-aphlict-listen',
'javelin-behavior-phabricator-search-typeahead',
'javelin-behavior-aphlict-dropdown',
'javelin-behavior-history-install',
'javelin-behavior-phabricator-gesture',
'javelin-behavior-phabricator-active-nav',
'javelin-behavior-phabricator-nav',
'javelin-behavior-phabricator-remarkup-assist',
'phabricator-textareautils',
'phabricator-file-upload',
'javelin-behavior-global-drag-and-drop',
'javelin-behavior-phabricator-reveal-content',
'phui-hovercard',
'javelin-behavior-phui-hovercards',
'javelin-color',
'javelin-fx',
'phabricator-draggable-list',
'javelin-behavior-phabricator-transaction-list',
'javelin-behavior-phabricator-show-older-transactions',
'javelin-behavior-phui-dropdown-menu',
'javelin-behavior-doorkeeper-tag',
'phabricator-title',
'javelin-leader',
'javelin-websocket',
'javelin-behavior-dashboard-async-panel',
'javelin-behavior-dashboard-tab-panel',
'javelin-quicksand',
'javelin-behavior-quicksand-blacklist',
'javelin-behavior-high-security-warning',
'javelin-behavior-read-only-warning',
'javelin-scrollbar',
'javelin-behavior-scrollbar',
'javelin-behavior-durable-column',
'conpherence-thread-manager',
'javelin-behavior-detect-timezone',
'javelin-behavior-setup-check-https',
'javelin-behavior-aphlict-status',
'javelin-behavior-user-menu',
'phabricator-favicon',
),
'differential.pkg.css' => array(
'differential-core-view-css',
'differential-changeset-view-css',
'differential-revision-history-css',
'differential-revision-list-css',
'differential-table-of-contents-css',
'differential-revision-comment-css',
'differential-revision-add-comment-css',
'phabricator-object-selector-css',
'phabricator-content-source-view-css',
'inline-comment-summary-css',
'phui-inline-comment-view-css',
'phabricator-filetree-view-css',
),
'differential.pkg.js' => array(
'phabricator-drag-and-drop-file-upload',
'phabricator-shaped-request',
- 'javelin-behavior-differential-feedback-preview',
'javelin-behavior-differential-populate',
'javelin-behavior-differential-diff-radios',
'javelin-behavior-aphront-drag-and-drop-textarea',
'javelin-behavior-phabricator-object-selector',
'javelin-behavior-repository-crossreference',
'javelin-behavior-differential-user-select',
'javelin-behavior-aphront-more',
'phabricator-diff-inline',
'phabricator-diff-changeset',
'phabricator-diff-changeset-list',
),
'diffusion.pkg.css' => array(
'diffusion-icons-css',
),
'diffusion.pkg.js' => array(
'javelin-behavior-diffusion-pull-lastmodified',
'javelin-behavior-diffusion-commit-graph',
'javelin-behavior-audit-preview',
),
'maniphest.pkg.css' => array(
'maniphest-task-summary-css',
),
'maniphest.pkg.js' => array(
'javelin-behavior-maniphest-batch-selector',
'javelin-behavior-maniphest-subpriority-editor',
'javelin-behavior-maniphest-list-editor',
),
),
);
diff --git a/resources/celerity/packages.php b/resources/celerity/packages.php
index 958b1d6af..4005e064b 100644
--- a/resources/celerity/packages.php
+++ b/resources/celerity/packages.php
@@ -1,226 +1,225 @@
<?php
return array(
'core.pkg.js' => array(
'javelin-util',
'javelin-install',
'javelin-event',
'javelin-stratcom',
'javelin-behavior',
'javelin-resource',
'javelin-request',
'javelin-vector',
'javelin-dom',
'javelin-json',
'javelin-uri',
'javelin-workflow',
'javelin-mask',
'javelin-typeahead',
'javelin-typeahead-normalizer',
'javelin-typeahead-source',
'javelin-typeahead-preloaded-source',
'javelin-typeahead-ondemand-source',
'javelin-tokenizer',
'javelin-history',
'javelin-router',
'javelin-routable',
'javelin-behavior-aphront-basic-tokenizer',
'javelin-behavior-workflow',
'javelin-behavior-aphront-form-disable-on-submit',
'phabricator-keyboard-shortcut-manager',
'phabricator-keyboard-shortcut',
'javelin-behavior-phabricator-keyboard-shortcuts',
'javelin-behavior-refresh-csrf',
'javelin-behavior-phabricator-watch-anchor',
'javelin-behavior-phabricator-autofocus',
'phuix-dropdown-menu',
'phuix-action-list-view',
'phuix-action-view',
'phuix-icon-view',
'phabricator-phtize',
'javelin-behavior-phabricator-oncopy',
'phabricator-tooltip',
'javelin-behavior-phabricator-tooltips',
'phabricator-prefab',
'javelin-behavior-device',
'javelin-behavior-toggle-class',
'javelin-behavior-lightbox-attachments',
'phabricator-busy',
'javelin-sound',
'javelin-aphlict',
'phabricator-notification',
'javelin-behavior-aphlict-listen',
'javelin-behavior-phabricator-search-typeahead',
'javelin-behavior-aphlict-dropdown',
'javelin-behavior-history-install',
'javelin-behavior-phabricator-gesture',
'javelin-behavior-phabricator-active-nav',
'javelin-behavior-phabricator-nav',
'javelin-behavior-phabricator-remarkup-assist',
'phabricator-textareautils',
'phabricator-file-upload',
'javelin-behavior-global-drag-and-drop',
'javelin-behavior-phabricator-reveal-content',
'phui-hovercard',
'javelin-behavior-phui-hovercards',
'javelin-color',
'javelin-fx',
'phabricator-draggable-list',
'javelin-behavior-phabricator-transaction-list',
'javelin-behavior-phabricator-show-older-transactions',
'javelin-behavior-phui-dropdown-menu',
'javelin-behavior-doorkeeper-tag',
'phabricator-title',
'javelin-leader',
'javelin-websocket',
'javelin-behavior-dashboard-async-panel',
'javelin-behavior-dashboard-tab-panel',
'javelin-quicksand',
'javelin-behavior-quicksand-blacklist',
'javelin-behavior-high-security-warning',
'javelin-behavior-read-only-warning',
'javelin-scrollbar',
'javelin-behavior-scrollbar',
'javelin-behavior-durable-column',
'conpherence-thread-manager',
'javelin-behavior-detect-timezone',
'javelin-behavior-setup-check-https',
'javelin-behavior-aphlict-status',
'javelin-behavior-user-menu',
'phabricator-favicon',
),
'core.pkg.css' => array(
'phabricator-core-css',
'phabricator-zindex-css',
'phui-button-css',
'phui-button-simple-css',
'phui-theme-css',
'phabricator-standard-page-view',
'aphront-dialog-view-css',
'phui-form-view-css',
'aphront-panel-view-css',
'aphront-table-view-css',
'aphront-tokenizer-control-css',
'aphront-typeahead-control-css',
'aphront-list-filter-view-css',
'application-search-view-css',
'phabricator-remarkup-css',
'syntax-highlighting-css',
'syntax-default-css',
'phui-pager-css',
'aphront-tooltip-css',
'phabricator-flag-css',
'phui-info-view-css',
'phabricator-main-menu-view',
'phabricator-notification-css',
'phabricator-notification-menu-css',
'phui-lightbox-css',
'phui-comment-panel-css',
'phui-header-view-css',
'phabricator-nav-view-css',
'phui-basic-nav-view-css',
'phui-crumbs-view-css',
'phui-oi-list-view-css',
'phui-oi-color-css',
'phui-oi-big-ui-css',
'phui-oi-drag-ui-css',
'phui-oi-simple-ui-css',
'phui-oi-flush-ui-css',
'global-drag-and-drop-css',
'phui-spacing-css',
'phui-form-css',
'phui-icon-view-css',
'phabricator-action-list-view-css',
'phui-property-list-view-css',
'phui-tag-view-css',
'phui-list-view-css',
'font-fontawesome',
'font-lato',
'phui-font-icon-base-css',
'phui-fontkit-css',
'phui-box-css',
'phui-object-box-css',
'phui-timeline-view-css',
'phui-two-column-view-css',
'phui-curtain-view-css',
'sprite-login-css',
'sprite-tokens-css',
'tokens-css',
'auth-css',
'phui-status-list-view-css',
'phui-feed-story-css',
'phabricator-feed-css',
'phabricator-dashboard-css',
'aphront-multi-column-view-css',
),
'conpherence.pkg.css' => array(
'conpherence-durable-column-view',
'conpherence-menu-css',
'conpherence-color-css',
'conpherence-message-pane-css',
'conpherence-notification-css',
'conpherence-transaction-css',
'conpherence-participant-pane-css',
'conpherence-header-pane-css',
),
'conpherence.pkg.js' => array(
'javelin-behavior-conpherence-menu',
'javelin-behavior-conpherence-participant-pane',
'javelin-behavior-conpherence-pontificate',
'javelin-behavior-toggle-widget',
),
'differential.pkg.css' => array(
'differential-core-view-css',
'differential-changeset-view-css',
'differential-revision-history-css',
'differential-revision-list-css',
'differential-table-of-contents-css',
'differential-revision-comment-css',
'differential-revision-add-comment-css',
'phabricator-object-selector-css',
'phabricator-content-source-view-css',
'inline-comment-summary-css',
'phui-inline-comment-view-css',
'phabricator-filetree-view-css',
),
'differential.pkg.js' => array(
'phabricator-drag-and-drop-file-upload',
'phabricator-shaped-request',
- 'javelin-behavior-differential-feedback-preview',
'javelin-behavior-differential-populate',
'javelin-behavior-differential-diff-radios',
'javelin-behavior-aphront-drag-and-drop-textarea',
'javelin-behavior-phabricator-object-selector',
'javelin-behavior-repository-crossreference',
'javelin-behavior-differential-user-select',
'javelin-behavior-aphront-more',
'phabricator-diff-inline',
'phabricator-diff-changeset',
'phabricator-diff-changeset-list',
),
'diffusion.pkg.css' => array(
'diffusion-icons-css',
),
'diffusion.pkg.js' => array(
'javelin-behavior-diffusion-pull-lastmodified',
'javelin-behavior-diffusion-commit-graph',
'javelin-behavior-audit-preview',
),
'maniphest.pkg.css' => array(
'maniphest-task-summary-css',
),
'maniphest.pkg.js' => array(
'javelin-behavior-maniphest-batch-selector',
'javelin-behavior-maniphest-subpriority-editor',
'javelin-behavior-maniphest-list-editor',
),
);
diff --git a/resources/sql/autopatches/20181213.auth.06.challenge.sql b/resources/sql/autopatches/20181213.auth.06.challenge.sql
new file mode 100644
index 000000000..0e5eeb35f
--- /dev/null
+++ b/resources/sql/autopatches/20181213.auth.06.challenge.sql
@@ -0,0 +1,12 @@
+CREATE TABLE {$NAMESPACE}_auth.auth_challenge (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ phid VARBINARY(64) NOT NULL,
+ userPHID VARBINARY(64) NOT NULL,
+ factorPHID VARBINARY(64) NOT NULL,
+ sessionPHID VARBINARY(64) NOT NULL,
+ challengeKey VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT},
+ challengeTTL INT UNSIGNED NOT NULL,
+ properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT},
+ dateCreated INT UNSIGNED NOT NULL,
+ dateModified INT UNSIGNED NOT NULL
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20181214.auth.01.workflowkey.sql b/resources/sql/autopatches/20181214.auth.01.workflowkey.sql
new file mode 100644
index 000000000..538778e21
--- /dev/null
+++ b/resources/sql/autopatches/20181214.auth.01.workflowkey.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_auth.auth_challenge
+ ADD workflowKey VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20181217.auth.01.digest.sql b/resources/sql/autopatches/20181217.auth.01.digest.sql
new file mode 100644
index 000000000..8e30143e8
--- /dev/null
+++ b/resources/sql/autopatches/20181217.auth.01.digest.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_auth.auth_challenge
+ ADD responseDigest VARCHAR(255) COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20181217.auth.02.ttl.sql b/resources/sql/autopatches/20181217.auth.02.ttl.sql
new file mode 100644
index 000000000..c8e883dbe
--- /dev/null
+++ b/resources/sql/autopatches/20181217.auth.02.ttl.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_auth.auth_challenge
+ ADD responseTTL INT UNSIGNED;
diff --git a/resources/sql/autopatches/20181217.auth.03.completed.sql b/resources/sql/autopatches/20181217.auth.03.completed.sql
new file mode 100644
index 000000000..22ca6e21f
--- /dev/null
+++ b/resources/sql/autopatches/20181217.auth.03.completed.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_auth.auth_challenge
+ ADD isCompleted BOOL NOT NULL;
diff --git a/resources/sql/autopatches/20181218.pholio.01.imageauthor.sql b/resources/sql/autopatches/20181218.pholio.01.imageauthor.sql
new file mode 100644
index 000000000..4ff0a1625
--- /dev/null
+++ b/resources/sql/autopatches/20181218.pholio.01.imageauthor.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_pholio.pholio_image
+ ADD authorPHID VARBINARY(64) NOT NULL;
diff --git a/resources/sql/autopatches/20181219.pholio.01.imagephid.sql b/resources/sql/autopatches/20181219.pholio.01.imagephid.sql
new file mode 100644
index 000000000..870cddd95
--- /dev/null
+++ b/resources/sql/autopatches/20181219.pholio.01.imagephid.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_pholio.pholio_image
+ ADD mockPHID VARBINARY(64);
diff --git a/resources/sql/autopatches/20181219.pholio.02.imagemigrate.php b/resources/sql/autopatches/20181219.pholio.02.imagemigrate.php
new file mode 100644
index 000000000..f1fc1b3c3
--- /dev/null
+++ b/resources/sql/autopatches/20181219.pholio.02.imagemigrate.php
@@ -0,0 +1,35 @@
+<?php
+
+// Old images used a "mockID" instead of a "mockPHID" to reference mocks.
+// Set the "mockPHID" column to the value that corresponds to the "mockID".
+
+$image = new PholioImage();
+$mock = new PholioMock();
+
+$conn = $image->establishConnection('w');
+$iterator = new LiskRawMigrationIterator($conn, $image->getTableName());
+
+foreach ($iterator as $image_row) {
+ if ($image_row['mockPHID']) {
+ continue;
+ }
+
+ $mock_id = $image_row['mockID'];
+
+ $mock_row = queryfx_one(
+ $conn,
+ 'SELECT phid FROM %R WHERE id = %d',
+ $mock,
+ $mock_id);
+
+ if (!$mock_row) {
+ continue;
+ }
+
+ queryfx(
+ $conn,
+ 'UPDATE %R SET mockPHID = %s WHERE id = %d',
+ $image,
+ $mock_row['phid'],
+ $image_row['id']);
+}
diff --git a/resources/sql/autopatches/20181219.pholio.03.imageid.sql b/resources/sql/autopatches/20181219.pholio.03.imageid.sql
new file mode 100644
index 000000000..3a3cb029a
--- /dev/null
+++ b/resources/sql/autopatches/20181219.pholio.03.imageid.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_pholio.pholio_image
+ DROP mockID;
diff --git a/resources/sql/autopatches/20181220.pholio.01.mailkey.php b/resources/sql/autopatches/20181220.pholio.01.mailkey.php
new file mode 100644
index 000000000..37dcfd143
--- /dev/null
+++ b/resources/sql/autopatches/20181220.pholio.01.mailkey.php
@@ -0,0 +1,28 @@
+<?php
+
+$mock_table = new PholioMock();
+$mock_conn = $mock_table->establishConnection('w');
+
+$properties_table = new PhabricatorMetaMTAMailProperties();
+$conn = $properties_table->establishConnection('w');
+
+$iterator = new LiskRawMigrationIterator(
+ $mock_conn,
+ $mock_table->getTableName());
+
+foreach ($iterator as $row) {
+ queryfx(
+ $conn,
+ 'INSERT IGNORE INTO %T
+ (objectPHID, mailProperties, dateCreated, dateModified)
+ VALUES
+ (%s, %s, %d, %d)',
+ $properties_table->getTableName(),
+ $row['phid'],
+ phutil_json_encode(
+ array(
+ 'mailKey' => $row['mailKey'],
+ )),
+ PhabricatorTime::getNow(),
+ PhabricatorTime::getNow());
+}
diff --git a/resources/sql/autopatches/20181220.pholio.02.dropmailkey.sql b/resources/sql/autopatches/20181220.pholio.02.dropmailkey.sql
new file mode 100644
index 000000000..a71bc5dc6
--- /dev/null
+++ b/resources/sql/autopatches/20181220.pholio.02.dropmailkey.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_pholio.pholio_mock
+ DROP mailKey;
diff --git a/resources/sql/autopatches/20181228.auth.01.provider.sql b/resources/sql/autopatches/20181228.auth.01.provider.sql
new file mode 100644
index 000000000..4ffd23c84
--- /dev/null
+++ b/resources/sql/autopatches/20181228.auth.01.provider.sql
@@ -0,0 +1,9 @@
+CREATE TABLE {$NAMESPACE}_auth.auth_factorprovider (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ phid VARBINARY(64) NOT NULL,
+ providerFactorKey VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT},
+ status VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT},
+ properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT},
+ dateCreated INT UNSIGNED NOT NULL,
+ dateModified INT UNSIGNED NOT NULL
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20181228.auth.02.xaction.sql b/resources/sql/autopatches/20181228.auth.02.xaction.sql
new file mode 100644
index 000000000..c595cdd8f
--- /dev/null
+++ b/resources/sql/autopatches/20181228.auth.02.xaction.sql
@@ -0,0 +1,19 @@
+CREATE TABLE {$NAMESPACE}_auth.auth_factorprovidertransaction (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ phid VARBINARY(64) NOT NULL,
+ authorPHID VARBINARY(64) NOT NULL,
+ objectPHID VARBINARY(64) NOT NULL,
+ viewPolicy VARBINARY(64) NOT NULL,
+ editPolicy VARBINARY(64) NOT NULL,
+ commentPHID VARBINARY(64) DEFAULT NULL,
+ commentVersion INT UNSIGNED NOT NULL,
+ transactionType VARCHAR(32) NOT NULL,
+ oldValue LONGTEXT NOT NULL,
+ newValue LONGTEXT NOT NULL,
+ contentSource LONGTEXT NOT NULL,
+ metadata LONGTEXT NOT NULL,
+ dateCreated INT UNSIGNED NOT NULL,
+ dateModified INT UNSIGNED NOT NULL,
+ UNIQUE KEY `key_phid` (`phid`),
+ KEY `key_object` (`objectPHID`)
+) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20181228.auth.03.name.sql b/resources/sql/autopatches/20181228.auth.03.name.sql
new file mode 100644
index 000000000..856c10287
--- /dev/null
+++ b/resources/sql/autopatches/20181228.auth.03.name.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_auth.auth_factorprovider
+ ADD name VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20190101.sms.01.drop.sql b/resources/sql/autopatches/20190101.sms.01.drop.sql
new file mode 100644
index 000000000..b233f7ab7
--- /dev/null
+++ b/resources/sql/autopatches/20190101.sms.01.drop.sql
@@ -0,0 +1 @@
+DROP TABLE {$NAMESPACE}_metamta.sms;
diff --git a/resources/sql/autopatches/20190115.mfa.01.provider.sql b/resources/sql/autopatches/20190115.mfa.01.provider.sql
new file mode 100644
index 000000000..52e818f8d
--- /dev/null
+++ b/resources/sql/autopatches/20190115.mfa.01.provider.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_auth.auth_factorconfig
+ ADD factorProviderPHID VARBINARY(64) NOT NULL;
diff --git a/resources/sql/autopatches/20190115.mfa.02.migrate.php b/resources/sql/autopatches/20190115.mfa.02.migrate.php
new file mode 100644
index 000000000..95a60789c
--- /dev/null
+++ b/resources/sql/autopatches/20190115.mfa.02.migrate.php
@@ -0,0 +1,72 @@
+<?php
+
+// Previously, MFA factors for individual users were bound to raw factor types.
+// The only factor type ever implemented in the upstream was "totp".
+
+// Going forward, individual factors are bound to a provider instead. This
+// allows factor types to have some configuration, like API keys for
+// service-based MFA. It also allows installs to select which types of factors
+// they want users to be able to set up.
+
+// Migrate all existing TOTP factors to the first available TOTP provider,
+// creating one if none exists. This migration is a little bit messy, but
+// gives us a clean slate going forward with no "builtin" providers.
+
+$table = new PhabricatorAuthFactorConfig();
+$conn = $table->establishConnection('w');
+
+$provider_table = new PhabricatorAuthFactorProvider();
+$provider_phid = null;
+$iterator = new LiskRawMigrationIterator($conn, $table->getTableName());
+$totp_key = 'totp';
+foreach ($iterator as $row) {
+
+ // This wasn't a TOTP factor, so skip it.
+ if ($row['factorKey'] !== $totp_key) {
+ continue;
+ }
+
+ // This factor already has an associated provider.
+ if (strlen($row['factorProviderPHID'])) {
+ continue;
+ }
+
+ // Find (or create) a suitable TOTP provider. Note that we can't "save()"
+ // an object or this migration will break if the object ever gets new
+ // columns; just INSERT the raw fields instead.
+
+ if ($provider_phid === null) {
+ $provider_row = queryfx_one(
+ $conn,
+ 'SELECT phid FROM %R WHERE providerFactorKey = %s LIMIT 1',
+ $provider_table,
+ $totp_key);
+
+ if ($provider_row) {
+ $provider_phid = $provider_row['phid'];
+ } else {
+ $provider_phid = $provider_table->generatePHID();
+ queryfx(
+ $conn,
+ 'INSERT INTO %R
+ (phid, providerFactorKey, name, status, properties,
+ dateCreated, dateModified)
+ VALUES (%s, %s, %s, %s, %s, %d, %d)',
+ $provider_table,
+ $provider_phid,
+ $totp_key,
+ '',
+ 'active',
+ '{}',
+ PhabricatorTime::getNow(),
+ PhabricatorTime::getNow());
+ }
+ }
+
+ queryfx(
+ $conn,
+ 'UPDATE %R SET factorProviderPHID = %s WHERE id = %d',
+ $table,
+ $provider_phid,
+ $row['id']);
+}
diff --git a/resources/sql/autopatches/20190115.mfa.03.factorkey.sql b/resources/sql/autopatches/20190115.mfa.03.factorkey.sql
new file mode 100644
index 000000000..619787a83
--- /dev/null
+++ b/resources/sql/autopatches/20190115.mfa.03.factorkey.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_auth.auth_factorconfig
+ DROP factorKey;
diff --git a/resources/sql/autopatches/20190116.contact.01.number.sql b/resources/sql/autopatches/20190116.contact.01.number.sql
new file mode 100644
index 000000000..14e2b78d1
--- /dev/null
+++ b/resources/sql/autopatches/20190116.contact.01.number.sql
@@ -0,0 +1,11 @@
+CREATE TABLE {$NAMESPACE}_auth.auth_contactnumber (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ phid VARBINARY(64) NOT NULL,
+ objectPHID VARBINARY(64) NOT NULL,
+ contactNumber VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT},
+ status VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT},
+ properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT},
+ uniqueKey BINARY(12),
+ dateCreated INT UNSIGNED NOT NULL,
+ dateModified INT UNSIGNED NOT NULL
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20190116.contact.02.xaction.sql b/resources/sql/autopatches/20190116.contact.02.xaction.sql
new file mode 100644
index 000000000..bd0d361bc
--- /dev/null
+++ b/resources/sql/autopatches/20190116.contact.02.xaction.sql
@@ -0,0 +1,19 @@
+CREATE TABLE {$NAMESPACE}_auth.auth_contactnumbertransaction (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ phid VARBINARY(64) NOT NULL,
+ authorPHID VARBINARY(64) NOT NULL,
+ objectPHID VARBINARY(64) NOT NULL,
+ viewPolicy VARBINARY(64) NOT NULL,
+ editPolicy VARBINARY(64) NOT NULL,
+ commentPHID VARBINARY(64) DEFAULT NULL,
+ commentVersion INT UNSIGNED NOT NULL,
+ transactionType VARCHAR(32) NOT NULL,
+ oldValue LONGTEXT NOT NULL,
+ newValue LONGTEXT NOT NULL,
+ contentSource LONGTEXT NOT NULL,
+ metadata LONGTEXT NOT NULL,
+ dateCreated INT UNSIGNED NOT NULL,
+ dateModified INT UNSIGNED NOT NULL,
+ UNIQUE KEY `key_phid` (`phid`),
+ KEY `key_object` (`objectPHID`)
+) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20190116.phortune.01.billing.sql b/resources/sql/autopatches/20190116.phortune.01.billing.sql
new file mode 100644
index 000000000..77d00e220
--- /dev/null
+++ b/resources/sql/autopatches/20190116.phortune.01.billing.sql
@@ -0,0 +1,3 @@
+ALTER TABLE {$NAMESPACE}_phortune.phortune_account
+ ADD billingName VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT},
+ ADD billingAddress LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20190117.authmessage.01.message.sql b/resources/sql/autopatches/20190117.authmessage.01.message.sql
new file mode 100644
index 000000000..9f4afa264
--- /dev/null
+++ b/resources/sql/autopatches/20190117.authmessage.01.message.sql
@@ -0,0 +1,8 @@
+CREATE TABLE {$NAMESPACE}_auth.auth_message (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ phid VARBINARY(64) NOT NULL,
+ messageKey VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT},
+ messageText LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT},
+ dateCreated INT UNSIGNED NOT NULL,
+ dateModified INT UNSIGNED NOT NULL
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20190117.authmessage.02.xaction.sql b/resources/sql/autopatches/20190117.authmessage.02.xaction.sql
new file mode 100644
index 000000000..944de129a
--- /dev/null
+++ b/resources/sql/autopatches/20190117.authmessage.02.xaction.sql
@@ -0,0 +1,19 @@
+CREATE TABLE {$NAMESPACE}_auth.auth_messagetransaction (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ phid VARBINARY(64) NOT NULL,
+ authorPHID VARBINARY(64) NOT NULL,
+ objectPHID VARBINARY(64) NOT NULL,
+ viewPolicy VARBINARY(64) NOT NULL,
+ editPolicy VARBINARY(64) NOT NULL,
+ commentPHID VARBINARY(64) DEFAULT NULL,
+ commentVersion INT UNSIGNED NOT NULL,
+ transactionType VARCHAR(32) NOT NULL,
+ oldValue LONGTEXT NOT NULL,
+ newValue LONGTEXT NOT NULL,
+ contentSource LONGTEXT NOT NULL,
+ metadata LONGTEXT NOT NULL,
+ dateCreated INT UNSIGNED NOT NULL,
+ dateModified INT UNSIGNED NOT NULL,
+ UNIQUE KEY `key_phid` (`phid`),
+ KEY `key_object` (`objectPHID`)
+) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20190121.contact.01.primary.sql b/resources/sql/autopatches/20190121.contact.01.primary.sql
new file mode 100644
index 000000000..84a757067
--- /dev/null
+++ b/resources/sql/autopatches/20190121.contact.01.primary.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_auth.auth_contactnumber
+ ADD isPrimary BOOL NOT NULL;
diff --git a/resources/sql/autopatches/20190127.project.01.subtype.sql b/resources/sql/autopatches/20190127.project.01.subtype.sql
new file mode 100644
index 000000000..107f9202d
--- /dev/null
+++ b/resources/sql/autopatches/20190127.project.01.subtype.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_project.project
+ ADD subtype VARCHAR(64) COLLATE {$COLLATE_TEXT} NOT NULL;
diff --git a/resources/sql/autopatches/20190127.project.02.default.sql b/resources/sql/autopatches/20190127.project.02.default.sql
new file mode 100644
index 000000000..1a74506cf
--- /dev/null
+++ b/resources/sql/autopatches/20190127.project.02.default.sql
@@ -0,0 +1,2 @@
+UPDATE {$NAMESPACE}_project.project
+ SET subtype = 'default' WHERE subtype = '';
diff --git a/resources/sql/autopatches/20190129.project.01.spaces.php b/resources/sql/autopatches/20190129.project.01.spaces.php
new file mode 100644
index 000000000..845b4ff25
--- /dev/null
+++ b/resources/sql/autopatches/20190129.project.01.spaces.php
@@ -0,0 +1,18 @@
+<?php
+
+// See PHI1046. The "spacePHID" column for milestones may have fallen out of
+// sync; correct all existing values.
+
+$table = new PhabricatorProject();
+$conn = $table->establishConnection('w');
+$table_name = $table->getTableName();
+
+foreach (new LiskRawMigrationIterator($conn, $table_name) as $project_row) {
+ queryfx(
+ $conn,
+ 'UPDATE %R SET spacePHID = %ns
+ WHERE parentProjectPHID = %s AND milestoneNumber IS NOT NULL',
+ $table,
+ $project_row['spacePHID'],
+ $project_row['phid']);
+}
diff --git a/scripts/mail/mail_handler.php b/scripts/mail/mail_handler.php
index 1c3c71f30..bf6f315f3 100755
--- a/scripts/mail/mail_handler.php
+++ b/scripts/mail/mail_handler.php
@@ -1,99 +1,99 @@
#!/usr/bin/env php
<?php
// NOTE: This script is very oldschool and takes the environment as an argument.
// Some day, we could take a shot at cleaning this up.
if ($argc > 1) {
foreach (array_slice($argv, 1) as $arg) {
if (!preg_match('/^-/', $arg)) {
$_SERVER['PHABRICATOR_ENV'] = $arg;
break;
}
}
}
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
require_once $root.'/externals/mimemailparser/MimeMailParser.class.php';
$args = new PhutilArgumentParser($argv);
$args->parseStandardArguments();
$args->parse(
array(
array(
'name' => 'process-duplicates',
'help' => pht(
"Process this message, even if it's a duplicate of another message. ".
"This is mostly useful when debugging issues with mail routing."),
),
array(
'name' => 'env',
'wildcard' => true,
),
));
$parser = new MimeMailParser();
$parser->setText(file_get_contents('php://stdin'));
$content = array();
foreach (array('text', 'html') as $part) {
$part_body = $parser->getMessageBody($part);
if (strlen($part_body) && !phutil_is_utf8($part_body)) {
$part_headers = $parser->getMessageBodyHeaders($part);
if (!is_array($part_headers)) {
$part_headers = array();
}
$content_type = idx($part_headers, 'content-type');
if (preg_match('/charset="(.*?)"/', $content_type, $matches) ||
preg_match('/charset=(\S+)/', $content_type, $matches)) {
$part_body = phutil_utf8_convert($part_body, 'UTF-8', $matches[1]);
}
}
$content[$part] = $part_body;
}
$headers = $parser->getHeaders();
-$headers['subject'] = iconv_mime_decode($headers['subject'], 0, 'UTF-8');
-$headers['from'] = iconv_mime_decode($headers['from'], 0, 'UTF-8');
+$headers['subject'] = phutil_decode_mime_header($headers['subject']);
+$headers['from'] = phutil_decode_mime_header($headers['from']);
if ($args->getArg('process-duplicates')) {
$headers['message-id'] = Filesystem::readRandomCharacters(64);
}
$received = new PhabricatorMetaMTAReceivedMail();
$received->setHeaders($headers);
$received->setBodies($content);
$attachments = array();
foreach ($parser->getAttachments() as $attachment) {
if (preg_match('@text/(plain|html)@', $attachment->getContentType()) &&
$attachment->getContentDisposition() == 'inline') {
// If this is an "inline" attachment with some sort of text content-type,
// do not treat it as a file for attachment. MimeMailParser already picked
// it up in the getMessageBody() call above. We still want to treat 'inline'
// attachments with other content types (e.g., images) as attachments.
continue;
}
$file = PhabricatorFile::newFromFileData(
$attachment->getContent(),
array(
'name' => $attachment->getFilename(),
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
));
$attachments[] = $file->getPHID();
}
try {
$received->setAttachments($attachments);
$received->save();
$received->processReceivedMail();
} catch (Exception $e) {
$received
->setMessage(pht('EXCEPTION: %s', $e->getMessage()))
->save();
throw $e;
}
diff --git a/scripts/sms/manage_sms.php b/scripts/sms/manage_sms.php
deleted file mode 100755
index 25a41d5d4..000000000
--- a/scripts/sms/manage_sms.php
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/usr/bin/env php
-<?php
-
-$root = dirname(dirname(dirname(__FILE__)));
-require_once $root.'/scripts/__init_script__.php';
-
-$args = new PhutilArgumentParser($argv);
-$args->setTagline(pht('manage SMS'));
-$args->setSynopsis(<<<EOSYNOPSIS
-**sms** __command__ [__options__]
- Manage Phabricator SMS stuff.
-
-EOSYNOPSIS
- );
-$args->parseStandardArguments();
-
-$workflows = id(new PhutilClassMapQuery())
- ->setAncestorClass('PhabricatorSMSManagementWorkflow')
- ->execute();
-$workflows[] = new PhutilHelpArgumentWorkflow();
-$args->parseWorkflows($workflows);
diff --git a/scripts/symbols/import_repository_symbols.php b/scripts/symbols/import_repository_symbols.php
index b84aea348..24a0624d6 100755
--- a/scripts/symbols/import_repository_symbols.php
+++ b/scripts/symbols/import_repository_symbols.php
@@ -1,231 +1,231 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
$args->setSynopsis(<<<EOSYNOPSIS
**import_repository_symbols.php** [__options__] __repository__ < symbols
Import repository symbols (symbols are read from stdin).
EOSYNOPSIS
);
$args->parseStandardArguments();
$args->parse(
array(
array(
'name' => 'no-purge',
'help' => pht(
'Do not clear all symbols for this repository before '.
'uploading new symbols. Useful for incremental updating.'),
),
array(
'name' => 'ignore-errors',
'help' => pht(
"If a line can't be parsed, ignore that line and ".
"continue instead of exiting."),
),
array(
'name' => 'max-transaction',
'param' => 'num-syms',
'default' => '100000',
'help' => pht(
'Maximum number of symbols that should '.
'be part of a single transaction.'),
),
array(
'name' => 'repository',
'wildcard' => true,
),
));
$identifiers = $args->getArg('repository');
if (count($identifiers) !== 1) {
$args->printHelpAndExit();
}
$identifier = head($identifiers);
$repository = id(new PhabricatorRepositoryQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIdentifiers($identifiers)
->executeOne();
if (!$repository) {
echo tsprintf(
"%s\n",
pht('Repository "%s" does not exist.', $identifier));
exit(1);
}
if (!function_exists('posix_isatty') || posix_isatty(STDIN)) {
echo pht('Parsing input from stdin...'), "\n";
}
$input = file_get_contents('php://stdin');
$input = trim($input);
$input = explode("\n", $input);
function commit_symbols(
array $symbols,
PhabricatorRepository $repository,
$no_purge) {
echo pht('Looking up path IDs...'), "\n";
$path_map =
PhabricatorRepositoryCommitChangeParserWorker::lookupOrCreatePaths(
ipull($symbols, 'path'));
$symbol = new PhabricatorRepositorySymbol();
$conn_w = $symbol->establishConnection('w');
echo pht('Preparing queries...'), "\n";
$sql = array();
foreach ($symbols as $dict) {
$sql[] = qsprintf(
$conn_w,
'(%s, %s, %s, %s, %s, %d, %d)',
$repository->getPHID(),
$dict['ctxt'],
$dict['name'],
$dict['type'],
$dict['lang'],
$dict['line'],
$path_map[$dict['path']]);
}
if (!$no_purge) {
echo pht('Purging old symbols...'), "\n";
queryfx(
$conn_w,
'DELETE FROM %T WHERE repositoryPHID = %s',
$symbol->getTableName(),
$repository->getPHID());
}
echo pht('Loading %s symbols...', phutil_count($sql)), "\n";
foreach (array_chunk($sql, 128) as $chunk) {
queryfx(
$conn_w,
'INSERT INTO %T
(repositoryPHID, symbolContext, symbolName, symbolType,
- symbolLanguage, lineNumber, pathID) VALUES %Q',
+ symbolLanguage, lineNumber, pathID) VALUES %LQ',
$symbol->getTableName(),
- implode(', ', $chunk));
+ $chunk);
}
}
function check_string_value($value, $field_name, $line_no, $max_length) {
if (strlen($value) > $max_length) {
throw new Exception(
pht(
"%s '%s' defined on line #%d is too long, ".
"maximum %s length is %d characters.",
$field_name,
$value,
$line_no,
$field_name,
$max_length));
}
if (!phutil_is_utf8_with_only_bmp_characters($value)) {
throw new Exception(
pht(
"%s '%s' defined on line #%d is not a valid ".
"UTF-8 string, it should contain only UTF-8 characters.",
$field_name,
$value,
$line_no));
}
}
$no_purge = $args->getArg('no-purge');
$symbols = array();
foreach ($input as $key => $line) {
try {
$line_no = $key + 1;
$matches = null;
$ok = preg_match(
'/^((?P<context>[^ ]+)? )?(?P<name>[^ ]+) (?P<type>[^ ]+) '.
'(?P<lang>[^ ]+) (?P<line>\d+) (?P<path>.*)$/',
$line,
$matches);
if (!$ok) {
throw new Exception(
pht(
"Line #%d of input is invalid. Expected five or six space-delimited ".
"fields: maybe symbol context, symbol name, symbol type, symbol ".
"language, line number, path. For example:\n\n%s\n\n".
"Actual line was:\n\n%s",
$line_no,
'idx function php 13 /path/to/some/file.php',
$line));
}
if (empty($matches['context'])) {
$matches['context'] = '';
}
$context = $matches['context'];
$name = $matches['name'];
$type = $matches['type'];
$lang = $matches['lang'];
$line_number = $matches['line'];
$path = $matches['path'];
check_string_value($context, pht('Symbol context'), $line_no, 128);
check_string_value($name, pht('Symbol name'), $line_no, 128);
check_string_value($type, pht('Symbol type'), $line_no, 12);
check_string_value($lang, pht('Symbol language'), $line_no, 32);
check_string_value($path, pht('Path'), $line_no, 512);
if (!strlen($path) || $path[0] != '/') {
throw new Exception(
pht(
"Path '%s' defined on line #%d is invalid. Paths should begin with ".
"'%s' and specify a path from the root of the project, like '%s'.",
$path,
$line_no,
'/',
'/src/utils/utils.php'));
}
$symbols[] = array(
'ctxt' => $context,
'name' => $name,
'type' => $type,
'lang' => $lang,
'line' => $line_number,
'path' => $path,
);
} catch (Exception $e) {
if ($args->getArg('ignore-errors')) {
continue;
} else {
throw $e;
}
}
if (count($symbols) >= $args->getArg('max-transaction')) {
try {
echo pht(
"Committing %s symbols...\n",
new PhutilNumber($args->getArg('max-transaction')));
commit_symbols($symbols, $repository, $no_purge);
$no_purge = true;
unset($symbols);
$symbols = array();
} catch (Exception $e) {
if ($args->getArg('ignore-errors')) {
continue;
} else {
throw $e;
}
}
}
}
if (count($symbols)) {
commit_symbols($symbols, $repository, $no_purge);
}
echo pht('Done.')."\n";
diff --git a/scripts/user/account_admin.php b/scripts/user/account_admin.php
index 9e0189663..4e4500a2f 100755
--- a/scripts/user/account_admin.php
+++ b/scripts/user/account_admin.php
@@ -1,208 +1,228 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$table = new PhabricatorUser();
$any_user = queryfx_one(
$table->establishConnection('r'),
'SELECT * FROM %T LIMIT 1',
$table->getTableName());
$is_first_user = (!$any_user);
if ($is_first_user) {
echo pht(
"WARNING\n\n".
"You're about to create the first account on this install. Normally, ".
"you should use the web interface to create the first account, not ".
"this script.\n\n".
"If you use the web interface, it will drop you into a nice UI workflow ".
"which gives you more help setting up your install. If you create an ".
"account with this script instead, you will skip the setup help and you ".
"will not be able to access it later.");
if (!phutil_console_confirm(pht('Skip easy setup and create account?'))) {
echo pht('Cancelled.')."\n";
exit(1);
}
}
echo pht(
'Enter a username to create a new account or edit an existing account.');
$username = phutil_console_prompt(pht('Enter a username:'));
if (!strlen($username)) {
echo pht('Cancelled.')."\n";
exit(1);
}
if (!PhabricatorUser::validateUsername($username)) {
$valid = PhabricatorUser::describeValidUsername();
echo pht("The username '%s' is invalid. %s", $username, $valid)."\n";
exit(1);
}
$user = id(new PhabricatorUser())->loadOneWhere(
'username = %s',
$username);
if (!$user) {
$original = new PhabricatorUser();
echo pht("There is no existing user account '%s'.", $username)."\n";
$ok = phutil_console_confirm(
pht("Do you want to create a new '%s' account?", $username),
$default_no = false);
if (!$ok) {
echo pht('Cancelled.')."\n";
exit(1);
}
$user = new PhabricatorUser();
$user->setUsername($username);
$is_new = true;
} else {
$original = clone $user;
echo pht("There is an existing user account '%s'.", $username)."\n";
$ok = phutil_console_confirm(
pht("Do you want to edit the existing '%s' account?", $username),
$default_no = false);
if (!$ok) {
echo pht('Cancelled.')."\n";
exit(1);
}
$is_new = false;
}
$user_realname = $user->getRealName();
if (strlen($user_realname)) {
$realname_prompt = ' ['.$user_realname.']:';
} else {
$realname_prompt = ':';
}
$realname = nonempty(
phutil_console_prompt(pht('Enter user real name').$realname_prompt),
$user_realname);
$user->setRealName($realname);
// When creating a new user we prompt for an email address; when editing an
// existing user we just skip this because it would be quite involved to provide
// a reasonable CLI interface for editing multiple addresses and managing email
// verification and primary addresses.
$create_email = null;
if ($is_new) {
do {
$email = phutil_console_prompt(pht('Enter user email address:'));
$duplicate = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$email);
if ($duplicate) {
echo pht(
"ERROR: There is already a user with that email address. ".
"Each user must have a unique email address.\n");
} else {
break;
}
} while (true);
$create_email = $email;
}
$is_system_agent = $user->getIsSystemAgent();
$set_system_agent = phutil_console_confirm(
pht('Is this user a bot?'),
$default_no = !$is_system_agent);
$verify_email = null;
$set_verified = false;
// Allow administrators to verify primary email addresses at this time in edit
// scenarios. (Create will work just fine from here as we auto-verify email
// on create.)
if (!$is_new) {
$verify_email = $user->loadPrimaryEmail();
if (!$verify_email->getIsVerified()) {
$set_verified = phutil_console_confirm(
pht('Should the primary email address be verified?'),
$default_no = true);
} else {
// Already verified so let's not make a fuss.
$verify_email = null;
}
}
$is_admin = $user->getIsAdmin();
$set_admin = phutil_console_confirm(
pht('Should this user be an administrator?'),
$default_no = !$is_admin);
echo "\n\n".pht('ACCOUNT SUMMARY')."\n\n";
$tpl = "%12s %-30s %-30s\n";
printf($tpl, null, pht('OLD VALUE'), pht('NEW VALUE'));
printf($tpl, pht('Username'), $original->getUsername(), $user->getUsername());
printf($tpl, pht('Real Name'), $original->getRealName(), $user->getRealName());
if ($is_new) {
printf($tpl, pht('Email'), '', $create_email);
}
printf(
$tpl,
pht('Bot'),
$original->getIsSystemAgent() ? 'Y' : 'N',
$set_system_agent ? 'Y' : 'N');
if ($verify_email) {
printf(
$tpl,
pht('Verify Email'),
$verify_email->getIsVerified() ? 'Y' : 'N',
$set_verified ? 'Y' : 'N');
}
printf(
$tpl,
pht('Admin'),
$original->getIsAdmin() ? 'Y' : 'N',
$set_admin ? 'Y' : 'N');
echo "\n";
if (!phutil_console_confirm(pht('Save these changes?'), $default_no = false)) {
echo pht('Cancelled.')."\n";
exit(1);
}
$user->openTransaction();
$editor = new PhabricatorUserEditor();
// TODO: This is wrong, but we have a chicken-and-egg problem when you use
// this script to create the first user.
$editor->setActor($user);
if ($is_new) {
$email = id(new PhabricatorUserEmail())
->setAddress($create_email)
->setIsVerified(1);
// Unconditionally approve new accounts created from the CLI.
$user->setIsApproved(1);
$editor->createNewUser($user, $email);
} else {
if ($verify_email) {
$user->setIsEmailVerified(1);
$verify_email->setIsVerified($set_verified ? 1 : 0);
}
$editor->updateUser($user, $verify_email);
}
- $editor->makeAdminUser($user, $set_admin);
$editor->makeSystemAgentUser($user, $set_system_agent);
+ $xactions = array();
+ $xactions[] = id(new PhabricatorUserTransaction())
+ ->setTransactionType(
+ PhabricatorUserEmpowerTransaction::TRANSACTIONTYPE)
+ ->setNewValue($set_admin);
+
+ $actor = PhabricatorUser::getOmnipotentUser();
+ $content_source = PhabricatorContentSource::newForSource(
+ PhabricatorConsoleContentSource::SOURCECONST);
+
+ $people_application_phid = id(new PhabricatorPeopleApplication())->getPHID();
+
+ $transaction_editor = id(new PhabricatorUserTransactionEditor())
+ ->setActor($actor)
+ ->setActingAsPHID($people_application_phid)
+ ->setContentSource($content_source)
+ ->setContinueOnNoEffect(true)
+ ->setContinueOnMissingFields(true);
+
+ $transaction_editor->applyTransactions($user, $xactions);
+
$user->saveTransaction();
echo pht('Saved changes.')."\n";
diff --git a/scripts/user/add_user.php b/scripts/user/add_user.php
index 4c598e47e..2554ab3dd 100755
--- a/scripts/user/add_user.php
+++ b/scripts/user/add_user.php
@@ -1,68 +1,73 @@
#!/usr/bin/env php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
if ($argc !== 5) {
echo pht(
"Usage: %s\n",
'add_user.php <username> <email> <realname> <admin_user>');
exit(1);
}
$username = $argv[1];
$email = $argv[2];
$realname = $argv[3];
$admin = $argv[4];
$admin = id(new PhabricatorUser())->loadOneWhere(
'username = %s',
$argv[4]);
if (!$admin) {
throw new Exception(
pht(
'Admin user must be the username of a valid Phabricator account, used '.
'to send the new user a welcome email.'));
}
$existing_user = id(new PhabricatorUser())->loadOneWhere(
'username = %s',
$username);
if ($existing_user) {
throw new Exception(
pht(
"There is already a user with the username '%s'!",
$username));
}
$existing_email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$email);
if ($existing_email) {
throw new Exception(
pht(
"There is already a user with the email '%s'!",
$email));
}
$user = new PhabricatorUser();
$user->setUsername($username);
$user->setRealname($realname);
$user->setIsApproved(1);
$email_object = id(new PhabricatorUserEmail())
->setAddress($email)
->setIsVerified(1);
id(new PhabricatorUserEditor())
->setActor($admin)
->createNewUser($user, $email_object);
-$user->sendWelcomeEmail($admin);
+$welcome_engine = id(new PhabricatorPeopleWelcomeMailEngine())
+ ->setSender($admin)
+ ->setRecipient($user);
+if ($welcome_engine->canSendMail()) {
+ $welcome_engine->sendMail();
+}
echo pht(
"Created user '%s' (realname='%s', email='%s').\n",
$username,
$realname,
$email);
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index b0408e127..324fc3c5d 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1,11526 +1,11722 @@
<?php
/**
* This file is automatically generated. Use 'arc liberate' to rebuild it.
*
* @generated
* @phutil-library-version 2
*/
phutil_register_library_map(array(
'__library_version__' => 2,
'class' => array(
'AlmanacAddress' => 'applications/almanac/util/AlmanacAddress.php',
'AlmanacBinding' => 'applications/almanac/storage/AlmanacBinding.php',
'AlmanacBindingDeletePropertyTransaction' => 'applications/almanac/xaction/AlmanacBindingDeletePropertyTransaction.php',
'AlmanacBindingDisableController' => 'applications/almanac/controller/AlmanacBindingDisableController.php',
'AlmanacBindingDisableTransaction' => 'applications/almanac/xaction/AlmanacBindingDisableTransaction.php',
'AlmanacBindingEditConduitAPIMethod' => 'applications/almanac/conduit/AlmanacBindingEditConduitAPIMethod.php',
'AlmanacBindingEditController' => 'applications/almanac/controller/AlmanacBindingEditController.php',
'AlmanacBindingEditEngine' => 'applications/almanac/editor/AlmanacBindingEditEngine.php',
'AlmanacBindingEditor' => 'applications/almanac/editor/AlmanacBindingEditor.php',
'AlmanacBindingInterfaceTransaction' => 'applications/almanac/xaction/AlmanacBindingInterfaceTransaction.php',
'AlmanacBindingPHIDType' => 'applications/almanac/phid/AlmanacBindingPHIDType.php',
'AlmanacBindingPropertyEditEngine' => 'applications/almanac/editor/AlmanacBindingPropertyEditEngine.php',
'AlmanacBindingQuery' => 'applications/almanac/query/AlmanacBindingQuery.php',
'AlmanacBindingSearchConduitAPIMethod' => 'applications/almanac/conduit/AlmanacBindingSearchConduitAPIMethod.php',
'AlmanacBindingSearchEngine' => 'applications/almanac/query/AlmanacBindingSearchEngine.php',
'AlmanacBindingServiceTransaction' => 'applications/almanac/xaction/AlmanacBindingServiceTransaction.php',
'AlmanacBindingSetPropertyTransaction' => 'applications/almanac/xaction/AlmanacBindingSetPropertyTransaction.php',
'AlmanacBindingTableView' => 'applications/almanac/view/AlmanacBindingTableView.php',
'AlmanacBindingTransaction' => 'applications/almanac/storage/AlmanacBindingTransaction.php',
'AlmanacBindingTransactionQuery' => 'applications/almanac/query/AlmanacBindingTransactionQuery.php',
'AlmanacBindingTransactionType' => 'applications/almanac/xaction/AlmanacBindingTransactionType.php',
'AlmanacBindingViewController' => 'applications/almanac/controller/AlmanacBindingViewController.php',
'AlmanacBindingsSearchEngineAttachment' => 'applications/almanac/engineextension/AlmanacBindingsSearchEngineAttachment.php',
'AlmanacCacheEngineExtension' => 'applications/almanac/engineextension/AlmanacCacheEngineExtension.php',
'AlmanacClusterDatabaseServiceType' => 'applications/almanac/servicetype/AlmanacClusterDatabaseServiceType.php',
'AlmanacClusterRepositoryServiceType' => 'applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php',
'AlmanacClusterServiceType' => 'applications/almanac/servicetype/AlmanacClusterServiceType.php',
'AlmanacConsoleController' => 'applications/almanac/controller/AlmanacConsoleController.php',
'AlmanacController' => 'applications/almanac/controller/AlmanacController.php',
'AlmanacCreateDevicesCapability' => 'applications/almanac/capability/AlmanacCreateDevicesCapability.php',
'AlmanacCreateNamespacesCapability' => 'applications/almanac/capability/AlmanacCreateNamespacesCapability.php',
'AlmanacCreateNetworksCapability' => 'applications/almanac/capability/AlmanacCreateNetworksCapability.php',
'AlmanacCreateServicesCapability' => 'applications/almanac/capability/AlmanacCreateServicesCapability.php',
'AlmanacCustomServiceType' => 'applications/almanac/servicetype/AlmanacCustomServiceType.php',
'AlmanacDAO' => 'applications/almanac/storage/AlmanacDAO.php',
'AlmanacDeletePropertyEditField' => 'applications/almanac/engineextension/AlmanacDeletePropertyEditField.php',
'AlmanacDeletePropertyEditType' => 'applications/almanac/engineextension/AlmanacDeletePropertyEditType.php',
'AlmanacDevice' => 'applications/almanac/storage/AlmanacDevice.php',
'AlmanacDeviceController' => 'applications/almanac/controller/AlmanacDeviceController.php',
'AlmanacDeviceDeletePropertyTransaction' => 'applications/almanac/xaction/AlmanacDeviceDeletePropertyTransaction.php',
'AlmanacDeviceEditConduitAPIMethod' => 'applications/almanac/conduit/AlmanacDeviceEditConduitAPIMethod.php',
'AlmanacDeviceEditController' => 'applications/almanac/controller/AlmanacDeviceEditController.php',
'AlmanacDeviceEditEngine' => 'applications/almanac/editor/AlmanacDeviceEditEngine.php',
'AlmanacDeviceEditor' => 'applications/almanac/editor/AlmanacDeviceEditor.php',
'AlmanacDeviceListController' => 'applications/almanac/controller/AlmanacDeviceListController.php',
'AlmanacDeviceNameNgrams' => 'applications/almanac/storage/AlmanacDeviceNameNgrams.php',
'AlmanacDeviceNameTransaction' => 'applications/almanac/xaction/AlmanacDeviceNameTransaction.php',
'AlmanacDevicePHIDType' => 'applications/almanac/phid/AlmanacDevicePHIDType.php',
'AlmanacDevicePropertyEditEngine' => 'applications/almanac/editor/AlmanacDevicePropertyEditEngine.php',
'AlmanacDeviceQuery' => 'applications/almanac/query/AlmanacDeviceQuery.php',
'AlmanacDeviceSearchConduitAPIMethod' => 'applications/almanac/conduit/AlmanacDeviceSearchConduitAPIMethod.php',
'AlmanacDeviceSearchEngine' => 'applications/almanac/query/AlmanacDeviceSearchEngine.php',
'AlmanacDeviceSetPropertyTransaction' => 'applications/almanac/xaction/AlmanacDeviceSetPropertyTransaction.php',
'AlmanacDeviceTransaction' => 'applications/almanac/storage/AlmanacDeviceTransaction.php',
'AlmanacDeviceTransactionQuery' => 'applications/almanac/query/AlmanacDeviceTransactionQuery.php',
'AlmanacDeviceTransactionType' => 'applications/almanac/xaction/AlmanacDeviceTransactionType.php',
'AlmanacDeviceViewController' => 'applications/almanac/controller/AlmanacDeviceViewController.php',
'AlmanacDrydockPoolServiceType' => 'applications/almanac/servicetype/AlmanacDrydockPoolServiceType.php',
'AlmanacEditor' => 'applications/almanac/editor/AlmanacEditor.php',
'AlmanacInterface' => 'applications/almanac/storage/AlmanacInterface.php',
'AlmanacInterfaceAddressTransaction' => 'applications/almanac/xaction/AlmanacInterfaceAddressTransaction.php',
'AlmanacInterfaceDatasource' => 'applications/almanac/typeahead/AlmanacInterfaceDatasource.php',
'AlmanacInterfaceDeleteController' => 'applications/almanac/controller/AlmanacInterfaceDeleteController.php',
'AlmanacInterfaceDestroyTransaction' => 'applications/almanac/xaction/AlmanacInterfaceDestroyTransaction.php',
'AlmanacInterfaceDeviceTransaction' => 'applications/almanac/xaction/AlmanacInterfaceDeviceTransaction.php',
'AlmanacInterfaceEditConduitAPIMethod' => 'applications/almanac/conduit/AlmanacInterfaceEditConduitAPIMethod.php',
'AlmanacInterfaceEditController' => 'applications/almanac/controller/AlmanacInterfaceEditController.php',
'AlmanacInterfaceEditEngine' => 'applications/almanac/editor/AlmanacInterfaceEditEngine.php',
'AlmanacInterfaceEditor' => 'applications/almanac/editor/AlmanacInterfaceEditor.php',
'AlmanacInterfaceNetworkTransaction' => 'applications/almanac/xaction/AlmanacInterfaceNetworkTransaction.php',
'AlmanacInterfacePHIDType' => 'applications/almanac/phid/AlmanacInterfacePHIDType.php',
'AlmanacInterfacePortTransaction' => 'applications/almanac/xaction/AlmanacInterfacePortTransaction.php',
'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php',
'AlmanacInterfaceSearchConduitAPIMethod' => 'applications/almanac/conduit/AlmanacInterfaceSearchConduitAPIMethod.php',
'AlmanacInterfaceSearchEngine' => 'applications/almanac/query/AlmanacInterfaceSearchEngine.php',
'AlmanacInterfaceTableView' => 'applications/almanac/view/AlmanacInterfaceTableView.php',
'AlmanacInterfaceTransaction' => 'applications/almanac/storage/AlmanacInterfaceTransaction.php',
'AlmanacInterfaceTransactionType' => 'applications/almanac/xaction/AlmanacInterfaceTransactionType.php',
'AlmanacKeys' => 'applications/almanac/util/AlmanacKeys.php',
'AlmanacManageClusterServicesCapability' => 'applications/almanac/capability/AlmanacManageClusterServicesCapability.php',
'AlmanacManagementRegisterWorkflow' => 'applications/almanac/management/AlmanacManagementRegisterWorkflow.php',
'AlmanacManagementTrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php',
'AlmanacManagementUntrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php',
'AlmanacManagementWorkflow' => 'applications/almanac/management/AlmanacManagementWorkflow.php',
'AlmanacModularTransaction' => 'applications/almanac/storage/AlmanacModularTransaction.php',
'AlmanacNames' => 'applications/almanac/util/AlmanacNames.php',
'AlmanacNamesTestCase' => 'applications/almanac/util/__tests__/AlmanacNamesTestCase.php',
'AlmanacNamespace' => 'applications/almanac/storage/AlmanacNamespace.php',
'AlmanacNamespaceController' => 'applications/almanac/controller/AlmanacNamespaceController.php',
'AlmanacNamespaceEditConduitAPIMethod' => 'applications/almanac/conduit/AlmanacNamespaceEditConduitAPIMethod.php',
'AlmanacNamespaceEditController' => 'applications/almanac/controller/AlmanacNamespaceEditController.php',
'AlmanacNamespaceEditEngine' => 'applications/almanac/editor/AlmanacNamespaceEditEngine.php',
'AlmanacNamespaceEditor' => 'applications/almanac/editor/AlmanacNamespaceEditor.php',
'AlmanacNamespaceListController' => 'applications/almanac/controller/AlmanacNamespaceListController.php',
'AlmanacNamespaceNameNgrams' => 'applications/almanac/storage/AlmanacNamespaceNameNgrams.php',
'AlmanacNamespaceNameTransaction' => 'applications/almanac/xaction/AlmanacNamespaceNameTransaction.php',
'AlmanacNamespacePHIDType' => 'applications/almanac/phid/AlmanacNamespacePHIDType.php',
'AlmanacNamespaceQuery' => 'applications/almanac/query/AlmanacNamespaceQuery.php',
'AlmanacNamespaceSearchConduitAPIMethod' => 'applications/almanac/conduit/AlmanacNamespaceSearchConduitAPIMethod.php',
'AlmanacNamespaceSearchEngine' => 'applications/almanac/query/AlmanacNamespaceSearchEngine.php',
'AlmanacNamespaceTransaction' => 'applications/almanac/storage/AlmanacNamespaceTransaction.php',
'AlmanacNamespaceTransactionQuery' => 'applications/almanac/query/AlmanacNamespaceTransactionQuery.php',
'AlmanacNamespaceTransactionType' => 'applications/almanac/xaction/AlmanacNamespaceTransactionType.php',
'AlmanacNamespaceViewController' => 'applications/almanac/controller/AlmanacNamespaceViewController.php',
'AlmanacNetwork' => 'applications/almanac/storage/AlmanacNetwork.php',
'AlmanacNetworkController' => 'applications/almanac/controller/AlmanacNetworkController.php',
'AlmanacNetworkEditConduitAPIMethod' => 'applications/almanac/conduit/AlmanacNetworkEditConduitAPIMethod.php',
'AlmanacNetworkEditController' => 'applications/almanac/controller/AlmanacNetworkEditController.php',
'AlmanacNetworkEditEngine' => 'applications/almanac/editor/AlmanacNetworkEditEngine.php',
'AlmanacNetworkEditor' => 'applications/almanac/editor/AlmanacNetworkEditor.php',
'AlmanacNetworkListController' => 'applications/almanac/controller/AlmanacNetworkListController.php',
'AlmanacNetworkNameNgrams' => 'applications/almanac/storage/AlmanacNetworkNameNgrams.php',
'AlmanacNetworkNameTransaction' => 'applications/almanac/xaction/AlmanacNetworkNameTransaction.php',
'AlmanacNetworkPHIDType' => 'applications/almanac/phid/AlmanacNetworkPHIDType.php',
'AlmanacNetworkQuery' => 'applications/almanac/query/AlmanacNetworkQuery.php',
'AlmanacNetworkSearchConduitAPIMethod' => 'applications/almanac/conduit/AlmanacNetworkSearchConduitAPIMethod.php',
'AlmanacNetworkSearchEngine' => 'applications/almanac/query/AlmanacNetworkSearchEngine.php',
'AlmanacNetworkTransaction' => 'applications/almanac/storage/AlmanacNetworkTransaction.php',
'AlmanacNetworkTransactionQuery' => 'applications/almanac/query/AlmanacNetworkTransactionQuery.php',
'AlmanacNetworkTransactionType' => 'applications/almanac/xaction/AlmanacNetworkTransactionType.php',
'AlmanacNetworkViewController' => 'applications/almanac/controller/AlmanacNetworkViewController.php',
'AlmanacPropertiesDestructionEngineExtension' => 'applications/almanac/engineextension/AlmanacPropertiesDestructionEngineExtension.php',
'AlmanacPropertiesEditEngineExtension' => 'applications/almanac/engineextension/AlmanacPropertiesEditEngineExtension.php',
'AlmanacPropertiesSearchEngineAttachment' => 'applications/almanac/engineextension/AlmanacPropertiesSearchEngineAttachment.php',
'AlmanacProperty' => 'applications/almanac/storage/AlmanacProperty.php',
'AlmanacPropertyController' => 'applications/almanac/controller/AlmanacPropertyController.php',
'AlmanacPropertyDeleteController' => 'applications/almanac/controller/AlmanacPropertyDeleteController.php',
'AlmanacPropertyEditController' => 'applications/almanac/controller/AlmanacPropertyEditController.php',
'AlmanacPropertyEditEngine' => 'applications/almanac/editor/AlmanacPropertyEditEngine.php',
'AlmanacPropertyInterface' => 'applications/almanac/property/AlmanacPropertyInterface.php',
'AlmanacPropertyQuery' => 'applications/almanac/query/AlmanacPropertyQuery.php',
'AlmanacQuery' => 'applications/almanac/query/AlmanacQuery.php',
'AlmanacSchemaSpec' => 'applications/almanac/storage/AlmanacSchemaSpec.php',
'AlmanacSearchEngineAttachment' => 'applications/almanac/engineextension/AlmanacSearchEngineAttachment.php',
'AlmanacService' => 'applications/almanac/storage/AlmanacService.php',
'AlmanacServiceController' => 'applications/almanac/controller/AlmanacServiceController.php',
'AlmanacServiceDatasource' => 'applications/almanac/typeahead/AlmanacServiceDatasource.php',
'AlmanacServiceDeletePropertyTransaction' => 'applications/almanac/xaction/AlmanacServiceDeletePropertyTransaction.php',
'AlmanacServiceEditConduitAPIMethod' => 'applications/almanac/conduit/AlmanacServiceEditConduitAPIMethod.php',
'AlmanacServiceEditController' => 'applications/almanac/controller/AlmanacServiceEditController.php',
'AlmanacServiceEditEngine' => 'applications/almanac/editor/AlmanacServiceEditEngine.php',
'AlmanacServiceEditor' => 'applications/almanac/editor/AlmanacServiceEditor.php',
'AlmanacServiceListController' => 'applications/almanac/controller/AlmanacServiceListController.php',
'AlmanacServiceNameNgrams' => 'applications/almanac/storage/AlmanacServiceNameNgrams.php',
'AlmanacServiceNameTransaction' => 'applications/almanac/xaction/AlmanacServiceNameTransaction.php',
'AlmanacServicePHIDType' => 'applications/almanac/phid/AlmanacServicePHIDType.php',
'AlmanacServicePropertyEditEngine' => 'applications/almanac/editor/AlmanacServicePropertyEditEngine.php',
'AlmanacServiceQuery' => 'applications/almanac/query/AlmanacServiceQuery.php',
'AlmanacServiceSearchConduitAPIMethod' => 'applications/almanac/conduit/AlmanacServiceSearchConduitAPIMethod.php',
'AlmanacServiceSearchEngine' => 'applications/almanac/query/AlmanacServiceSearchEngine.php',
'AlmanacServiceSetPropertyTransaction' => 'applications/almanac/xaction/AlmanacServiceSetPropertyTransaction.php',
'AlmanacServiceTransaction' => 'applications/almanac/storage/AlmanacServiceTransaction.php',
'AlmanacServiceTransactionQuery' => 'applications/almanac/query/AlmanacServiceTransactionQuery.php',
'AlmanacServiceTransactionType' => 'applications/almanac/xaction/AlmanacServiceTransactionType.php',
'AlmanacServiceType' => 'applications/almanac/servicetype/AlmanacServiceType.php',
'AlmanacServiceTypeDatasource' => 'applications/almanac/typeahead/AlmanacServiceTypeDatasource.php',
'AlmanacServiceTypeTestCase' => 'applications/almanac/servicetype/__tests__/AlmanacServiceTypeTestCase.php',
'AlmanacServiceTypeTransaction' => 'applications/almanac/xaction/AlmanacServiceTypeTransaction.php',
'AlmanacServiceViewController' => 'applications/almanac/controller/AlmanacServiceViewController.php',
'AlmanacSetPropertyEditField' => 'applications/almanac/engineextension/AlmanacSetPropertyEditField.php',
'AlmanacSetPropertyEditType' => 'applications/almanac/engineextension/AlmanacSetPropertyEditType.php',
'AlmanacTransactionType' => 'applications/almanac/xaction/AlmanacTransactionType.php',
'AphlictDropdownDataQuery' => 'applications/aphlict/query/AphlictDropdownDataQuery.php',
'Aphront304Response' => 'aphront/response/Aphront304Response.php',
'Aphront400Response' => 'aphront/response/Aphront400Response.php',
'Aphront403Response' => 'aphront/response/Aphront403Response.php',
'Aphront404Response' => 'aphront/response/Aphront404Response.php',
'AphrontAjaxResponse' => 'aphront/response/AphrontAjaxResponse.php',
'AphrontApplicationConfiguration' => 'aphront/configuration/AphrontApplicationConfiguration.php',
'AphrontBarView' => 'view/widget/bars/AphrontBarView.php',
'AphrontBoolHTTPParameterType' => 'aphront/httpparametertype/AphrontBoolHTTPParameterType.php',
'AphrontCalendarEventView' => 'applications/calendar/view/AphrontCalendarEventView.php',
'AphrontController' => 'aphront/AphrontController.php',
'AphrontCursorPagerView' => 'view/control/AphrontCursorPagerView.php',
- 'AphrontDefaultApplicationConfiguration' => 'aphront/configuration/AphrontDefaultApplicationConfiguration.php',
'AphrontDialogResponse' => 'aphront/response/AphrontDialogResponse.php',
'AphrontDialogView' => 'view/AphrontDialogView.php',
'AphrontEpochHTTPParameterType' => 'aphront/httpparametertype/AphrontEpochHTTPParameterType.php',
'AphrontException' => 'aphront/exception/AphrontException.php',
'AphrontFileHTTPParameterType' => 'aphront/httpparametertype/AphrontFileHTTPParameterType.php',
'AphrontFileResponse' => 'aphront/response/AphrontFileResponse.php',
'AphrontFormCheckboxControl' => 'view/form/control/AphrontFormCheckboxControl.php',
'AphrontFormControl' => 'view/form/control/AphrontFormControl.php',
'AphrontFormDateControl' => 'view/form/control/AphrontFormDateControl.php',
'AphrontFormDateControlValue' => 'view/form/control/AphrontFormDateControlValue.php',
'AphrontFormDividerControl' => 'view/form/control/AphrontFormDividerControl.php',
'AphrontFormFileControl' => 'view/form/control/AphrontFormFileControl.php',
'AphrontFormHandlesControl' => 'view/form/control/AphrontFormHandlesControl.php',
'AphrontFormMarkupControl' => 'view/form/control/AphrontFormMarkupControl.php',
'AphrontFormPasswordControl' => 'view/form/control/AphrontFormPasswordControl.php',
'AphrontFormPolicyControl' => 'view/form/control/AphrontFormPolicyControl.php',
'AphrontFormRadioButtonControl' => 'view/form/control/AphrontFormRadioButtonControl.php',
'AphrontFormRecaptchaControl' => 'view/form/control/AphrontFormRecaptchaControl.php',
'AphrontFormSelectControl' => 'view/form/control/AphrontFormSelectControl.php',
'AphrontFormStaticControl' => 'view/form/control/AphrontFormStaticControl.php',
'AphrontFormSubmitControl' => 'view/form/control/AphrontFormSubmitControl.php',
'AphrontFormTextAreaControl' => 'view/form/control/AphrontFormTextAreaControl.php',
'AphrontFormTextControl' => 'view/form/control/AphrontFormTextControl.php',
'AphrontFormTextWithSubmitControl' => 'view/form/control/AphrontFormTextWithSubmitControl.php',
'AphrontFormTokenizerControl' => 'view/form/control/AphrontFormTokenizerControl.php',
'AphrontFormTypeaheadControl' => 'view/form/control/AphrontFormTypeaheadControl.php',
'AphrontFormView' => 'view/form/AphrontFormView.php',
'AphrontGlyphBarView' => 'view/widget/bars/AphrontGlyphBarView.php',
'AphrontHTMLResponse' => 'aphront/response/AphrontHTMLResponse.php',
'AphrontHTTPParameterType' => 'aphront/httpparametertype/AphrontHTTPParameterType.php',
'AphrontHTTPProxyResponse' => 'aphront/response/AphrontHTTPProxyResponse.php',
'AphrontHTTPSink' => 'aphront/sink/AphrontHTTPSink.php',
'AphrontHTTPSinkTestCase' => 'aphront/sink/__tests__/AphrontHTTPSinkTestCase.php',
'AphrontIntHTTPParameterType' => 'aphront/httpparametertype/AphrontIntHTTPParameterType.php',
'AphrontIsolatedDatabaseConnectionTestCase' => 'infrastructure/storage/__tests__/AphrontIsolatedDatabaseConnectionTestCase.php',
'AphrontIsolatedHTTPSink' => 'aphront/sink/AphrontIsolatedHTTPSink.php',
'AphrontJSONResponse' => 'aphront/response/AphrontJSONResponse.php',
'AphrontJavelinView' => 'view/AphrontJavelinView.php',
'AphrontKeyboardShortcutsAvailableView' => 'view/widget/AphrontKeyboardShortcutsAvailableView.php',
'AphrontListFilterView' => 'view/layout/AphrontListFilterView.php',
'AphrontListHTTPParameterType' => 'aphront/httpparametertype/AphrontListHTTPParameterType.php',
'AphrontMalformedRequestException' => 'aphront/exception/AphrontMalformedRequestException.php',
'AphrontMoreView' => 'view/layout/AphrontMoreView.php',
'AphrontMultiColumnView' => 'view/layout/AphrontMultiColumnView.php',
'AphrontMySQLDatabaseConnectionTestCase' => 'infrastructure/storage/__tests__/AphrontMySQLDatabaseConnectionTestCase.php',
'AphrontNullView' => 'view/AphrontNullView.php',
'AphrontPHIDHTTPParameterType' => 'aphront/httpparametertype/AphrontPHIDHTTPParameterType.php',
'AphrontPHIDListHTTPParameterType' => 'aphront/httpparametertype/AphrontPHIDListHTTPParameterType.php',
'AphrontPHPHTTPSink' => 'aphront/sink/AphrontPHPHTTPSink.php',
'AphrontPageView' => 'view/page/AphrontPageView.php',
'AphrontPlainTextResponse' => 'aphront/response/AphrontPlainTextResponse.php',
'AphrontProgressBarView' => 'view/widget/bars/AphrontProgressBarView.php',
'AphrontProjectListHTTPParameterType' => 'aphront/httpparametertype/AphrontProjectListHTTPParameterType.php',
'AphrontProxyResponse' => 'aphront/response/AphrontProxyResponse.php',
'AphrontRedirectResponse' => 'aphront/response/AphrontRedirectResponse.php',
'AphrontRedirectResponseTestCase' => 'aphront/response/__tests__/AphrontRedirectResponseTestCase.php',
'AphrontReloadResponse' => 'aphront/response/AphrontReloadResponse.php',
'AphrontRequest' => 'aphront/AphrontRequest.php',
'AphrontRequestExceptionHandler' => 'aphront/handler/AphrontRequestExceptionHandler.php',
'AphrontRequestTestCase' => 'aphront/__tests__/AphrontRequestTestCase.php',
'AphrontResponse' => 'aphront/response/AphrontResponse.php',
'AphrontResponseProducerInterface' => 'aphront/interface/AphrontResponseProducerInterface.php',
'AphrontRoutingMap' => 'aphront/site/AphrontRoutingMap.php',
'AphrontRoutingResult' => 'aphront/site/AphrontRoutingResult.php',
'AphrontSelectHTTPParameterType' => 'aphront/httpparametertype/AphrontSelectHTTPParameterType.php',
'AphrontSideNavFilterView' => 'view/layout/AphrontSideNavFilterView.php',
'AphrontSite' => 'aphront/site/AphrontSite.php',
'AphrontStackTraceView' => 'view/widget/AphrontStackTraceView.php',
'AphrontStandaloneHTMLResponse' => 'aphront/response/AphrontStandaloneHTMLResponse.php',
'AphrontStringHTTPParameterType' => 'aphront/httpparametertype/AphrontStringHTTPParameterType.php',
'AphrontStringListHTTPParameterType' => 'aphront/httpparametertype/AphrontStringListHTTPParameterType.php',
'AphrontTableView' => 'view/control/AphrontTableView.php',
'AphrontTagView' => 'view/AphrontTagView.php',
'AphrontTokenizerTemplateView' => 'view/control/AphrontTokenizerTemplateView.php',
'AphrontTypeaheadTemplateView' => 'view/control/AphrontTypeaheadTemplateView.php',
'AphrontUnhandledExceptionResponse' => 'aphront/response/AphrontUnhandledExceptionResponse.php',
'AphrontUserListHTTPParameterType' => 'aphront/httpparametertype/AphrontUserListHTTPParameterType.php',
'AphrontView' => 'view/AphrontView.php',
'AphrontWebpageResponse' => 'aphront/response/AphrontWebpageResponse.php',
'ArcanistConduitAPIMethod' => 'applications/arcanist/conduit/ArcanistConduitAPIMethod.php',
'AuditConduitAPIMethod' => 'applications/audit/conduit/AuditConduitAPIMethod.php',
'AuditQueryConduitAPIMethod' => 'applications/audit/conduit/AuditQueryConduitAPIMethod.php',
'AuthManageProvidersCapability' => 'applications/auth/capability/AuthManageProvidersCapability.php',
'BulkParameterType' => 'applications/transactions/bulk/type/BulkParameterType.php',
'BulkPointsParameterType' => 'applications/transactions/bulk/type/BulkPointsParameterType.php',
'BulkRemarkupParameterType' => 'applications/transactions/bulk/type/BulkRemarkupParameterType.php',
'BulkSelectParameterType' => 'applications/transactions/bulk/type/BulkSelectParameterType.php',
'BulkStringParameterType' => 'applications/transactions/bulk/type/BulkStringParameterType.php',
'BulkTokenizerParameterType' => 'applications/transactions/bulk/type/BulkTokenizerParameterType.php',
'CalendarTimeUtil' => 'applications/calendar/util/CalendarTimeUtil.php',
'CalendarTimeUtilTestCase' => 'applications/calendar/__tests__/CalendarTimeUtilTestCase.php',
'CelerityAPI' => 'applications/celerity/CelerityAPI.php',
'CelerityDarkModePostprocessor' => 'applications/celerity/postprocessor/CelerityDarkModePostprocessor.php',
'CelerityDefaultPostprocessor' => 'applications/celerity/postprocessor/CelerityDefaultPostprocessor.php',
'CelerityHighContrastPostprocessor' => 'applications/celerity/postprocessor/CelerityHighContrastPostprocessor.php',
'CelerityLargeFontPostprocessor' => 'applications/celerity/postprocessor/CelerityLargeFontPostprocessor.php',
'CelerityManagementMapWorkflow' => 'applications/celerity/management/CelerityManagementMapWorkflow.php',
'CelerityManagementSyntaxWorkflow' => 'applications/celerity/management/CelerityManagementSyntaxWorkflow.php',
'CelerityManagementWorkflow' => 'applications/celerity/management/CelerityManagementWorkflow.php',
'CelerityPhabricatorResourceController' => 'applications/celerity/controller/CelerityPhabricatorResourceController.php',
'CelerityPhabricatorResources' => 'applications/celerity/resources/CelerityPhabricatorResources.php',
'CelerityPhysicalResources' => 'applications/celerity/resources/CelerityPhysicalResources.php',
'CelerityPhysicalResourcesTestCase' => 'applications/celerity/resources/__tests__/CelerityPhysicalResourcesTestCase.php',
'CelerityPostprocessor' => 'applications/celerity/postprocessor/CelerityPostprocessor.php',
'CelerityPostprocessorTestCase' => 'applications/celerity/__tests__/CelerityPostprocessorTestCase.php',
'CelerityRedGreenPostprocessor' => 'applications/celerity/postprocessor/CelerityRedGreenPostprocessor.php',
'CelerityResourceController' => 'applications/celerity/controller/CelerityResourceController.php',
'CelerityResourceGraph' => 'applications/celerity/CelerityResourceGraph.php',
'CelerityResourceMap' => 'applications/celerity/CelerityResourceMap.php',
'CelerityResourceMapGenerator' => 'applications/celerity/CelerityResourceMapGenerator.php',
'CelerityResourceTransformer' => 'applications/celerity/CelerityResourceTransformer.php',
'CelerityResourceTransformerTestCase' => 'applications/celerity/__tests__/CelerityResourceTransformerTestCase.php',
'CelerityResources' => 'applications/celerity/resources/CelerityResources.php',
'CelerityResourcesOnDisk' => 'applications/celerity/resources/CelerityResourcesOnDisk.php',
'CeleritySpriteGenerator' => 'applications/celerity/CeleritySpriteGenerator.php',
'CelerityStaticResourceResponse' => 'applications/celerity/CelerityStaticResourceResponse.php',
'ChatLogConduitAPIMethod' => 'applications/chatlog/conduit/ChatLogConduitAPIMethod.php',
'ChatLogQueryConduitAPIMethod' => 'applications/chatlog/conduit/ChatLogQueryConduitAPIMethod.php',
'ChatLogRecordConduitAPIMethod' => 'applications/chatlog/conduit/ChatLogRecordConduitAPIMethod.php',
'ConduitAPIMethod' => 'applications/conduit/method/ConduitAPIMethod.php',
'ConduitAPIMethodTestCase' => 'applications/conduit/method/__tests__/ConduitAPIMethodTestCase.php',
'ConduitAPIRequest' => 'applications/conduit/protocol/ConduitAPIRequest.php',
'ConduitAPIResponse' => 'applications/conduit/protocol/ConduitAPIResponse.php',
'ConduitApplicationNotInstalledException' => 'applications/conduit/protocol/exception/ConduitApplicationNotInstalledException.php',
'ConduitBoolParameterType' => 'applications/conduit/parametertype/ConduitBoolParameterType.php',
'ConduitCall' => 'applications/conduit/call/ConduitCall.php',
'ConduitCallTestCase' => 'applications/conduit/call/__tests__/ConduitCallTestCase.php',
'ConduitColumnsParameterType' => 'applications/conduit/parametertype/ConduitColumnsParameterType.php',
'ConduitConnectConduitAPIMethod' => 'applications/conduit/method/ConduitConnectConduitAPIMethod.php',
'ConduitConstantDescription' => 'applications/conduit/data/ConduitConstantDescription.php',
'ConduitEpochParameterType' => 'applications/conduit/parametertype/ConduitEpochParameterType.php',
'ConduitException' => 'applications/conduit/protocol/exception/ConduitException.php',
'ConduitGetCapabilitiesConduitAPIMethod' => 'applications/conduit/method/ConduitGetCapabilitiesConduitAPIMethod.php',
'ConduitGetCertificateConduitAPIMethod' => 'applications/conduit/method/ConduitGetCertificateConduitAPIMethod.php',
'ConduitIntListParameterType' => 'applications/conduit/parametertype/ConduitIntListParameterType.php',
'ConduitIntParameterType' => 'applications/conduit/parametertype/ConduitIntParameterType.php',
'ConduitListParameterType' => 'applications/conduit/parametertype/ConduitListParameterType.php',
'ConduitLogGarbageCollector' => 'applications/conduit/garbagecollector/ConduitLogGarbageCollector.php',
'ConduitMethodDoesNotExistException' => 'applications/conduit/protocol/exception/ConduitMethodDoesNotExistException.php',
'ConduitMethodNotFoundException' => 'applications/conduit/protocol/exception/ConduitMethodNotFoundException.php',
'ConduitPHIDListParameterType' => 'applications/conduit/parametertype/ConduitPHIDListParameterType.php',
'ConduitPHIDParameterType' => 'applications/conduit/parametertype/ConduitPHIDParameterType.php',
'ConduitParameterType' => 'applications/conduit/parametertype/ConduitParameterType.php',
'ConduitPingConduitAPIMethod' => 'applications/conduit/method/ConduitPingConduitAPIMethod.php',
'ConduitPointsParameterType' => 'applications/conduit/parametertype/ConduitPointsParameterType.php',
'ConduitProjectListParameterType' => 'applications/conduit/parametertype/ConduitProjectListParameterType.php',
'ConduitQueryConduitAPIMethod' => 'applications/conduit/method/ConduitQueryConduitAPIMethod.php',
'ConduitResultSearchEngineExtension' => 'applications/conduit/query/ConduitResultSearchEngineExtension.php',
'ConduitSSHWorkflow' => 'applications/conduit/ssh/ConduitSSHWorkflow.php',
'ConduitStringListParameterType' => 'applications/conduit/parametertype/ConduitStringListParameterType.php',
'ConduitStringParameterType' => 'applications/conduit/parametertype/ConduitStringParameterType.php',
'ConduitTokenGarbageCollector' => 'applications/conduit/garbagecollector/ConduitTokenGarbageCollector.php',
'ConduitUserListParameterType' => 'applications/conduit/parametertype/ConduitUserListParameterType.php',
'ConduitUserParameterType' => 'applications/conduit/parametertype/ConduitUserParameterType.php',
'ConduitWildParameterType' => 'applications/conduit/parametertype/ConduitWildParameterType.php',
'ConpherenceColumnViewController' => 'applications/conpherence/controller/ConpherenceColumnViewController.php',
'ConpherenceConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceConduitAPIMethod.php',
- 'ConpherenceConfigOptions' => 'applications/conpherence/config/ConpherenceConfigOptions.php',
'ConpherenceConstants' => 'applications/conpherence/constants/ConpherenceConstants.php',
'ConpherenceController' => 'applications/conpherence/controller/ConpherenceController.php',
'ConpherenceCreateThreadConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceCreateThreadConduitAPIMethod.php',
'ConpherenceDAO' => 'applications/conpherence/storage/ConpherenceDAO.php',
'ConpherenceDurableColumnView' => 'applications/conpherence/view/ConpherenceDurableColumnView.php',
'ConpherenceEditConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceEditConduitAPIMethod.php',
'ConpherenceEditEngine' => 'applications/conpherence/editor/ConpherenceEditEngine.php',
'ConpherenceEditor' => 'applications/conpherence/editor/ConpherenceEditor.php',
'ConpherenceFulltextQuery' => 'applications/conpherence/query/ConpherenceFulltextQuery.php',
'ConpherenceIndex' => 'applications/conpherence/storage/ConpherenceIndex.php',
'ConpherenceLayoutView' => 'applications/conpherence/view/ConpherenceLayoutView.php',
'ConpherenceListController' => 'applications/conpherence/controller/ConpherenceListController.php',
'ConpherenceMenuItemView' => 'applications/conpherence/view/ConpherenceMenuItemView.php',
'ConpherenceNotificationPanelController' => 'applications/conpherence/controller/ConpherenceNotificationPanelController.php',
'ConpherenceParticipant' => 'applications/conpherence/storage/ConpherenceParticipant.php',
'ConpherenceParticipantController' => 'applications/conpherence/controller/ConpherenceParticipantController.php',
'ConpherenceParticipantCountQuery' => 'applications/conpherence/query/ConpherenceParticipantCountQuery.php',
'ConpherenceParticipantQuery' => 'applications/conpherence/query/ConpherenceParticipantQuery.php',
'ConpherenceParticipantView' => 'applications/conpherence/view/ConpherenceParticipantView.php',
'ConpherenceQueryThreadConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceQueryThreadConduitAPIMethod.php',
'ConpherenceQueryTransactionConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceQueryTransactionConduitAPIMethod.php',
'ConpherenceReplyHandler' => 'applications/conpherence/mail/ConpherenceReplyHandler.php',
'ConpherenceRoomEditController' => 'applications/conpherence/controller/ConpherenceRoomEditController.php',
'ConpherenceRoomListController' => 'applications/conpherence/controller/ConpherenceRoomListController.php',
'ConpherenceRoomPictureController' => 'applications/conpherence/controller/ConpherenceRoomPictureController.php',
'ConpherenceRoomPreferencesController' => 'applications/conpherence/controller/ConpherenceRoomPreferencesController.php',
'ConpherenceRoomSettings' => 'applications/conpherence/constants/ConpherenceRoomSettings.php',
'ConpherenceRoomTestCase' => 'applications/conpherence/__tests__/ConpherenceRoomTestCase.php',
'ConpherenceSchemaSpec' => 'applications/conpherence/storage/ConpherenceSchemaSpec.php',
'ConpherenceTestCase' => 'applications/conpherence/__tests__/ConpherenceTestCase.php',
'ConpherenceThread' => 'applications/conpherence/storage/ConpherenceThread.php',
'ConpherenceThreadDatasource' => 'applications/conpherence/typeahead/ConpherenceThreadDatasource.php',
'ConpherenceThreadDateMarkerTransaction' => 'applications/conpherence/xaction/ConpherenceThreadDateMarkerTransaction.php',
'ConpherenceThreadIndexEngineExtension' => 'applications/conpherence/engineextension/ConpherenceThreadIndexEngineExtension.php',
'ConpherenceThreadListView' => 'applications/conpherence/view/ConpherenceThreadListView.php',
'ConpherenceThreadMailReceiver' => 'applications/conpherence/mail/ConpherenceThreadMailReceiver.php',
'ConpherenceThreadMembersPolicyRule' => 'applications/conpherence/policyrule/ConpherenceThreadMembersPolicyRule.php',
'ConpherenceThreadParticipantsTransaction' => 'applications/conpherence/xaction/ConpherenceThreadParticipantsTransaction.php',
'ConpherenceThreadPictureTransaction' => 'applications/conpherence/xaction/ConpherenceThreadPictureTransaction.php',
'ConpherenceThreadQuery' => 'applications/conpherence/query/ConpherenceThreadQuery.php',
'ConpherenceThreadRemarkupRule' => 'applications/conpherence/remarkup/ConpherenceThreadRemarkupRule.php',
'ConpherenceThreadSearchController' => 'applications/conpherence/controller/ConpherenceThreadSearchController.php',
'ConpherenceThreadSearchEngine' => 'applications/conpherence/query/ConpherenceThreadSearchEngine.php',
'ConpherenceThreadTitleNgrams' => 'applications/conpherence/storage/ConpherenceThreadTitleNgrams.php',
'ConpherenceThreadTitleTransaction' => 'applications/conpherence/xaction/ConpherenceThreadTitleTransaction.php',
'ConpherenceThreadTopicTransaction' => 'applications/conpherence/xaction/ConpherenceThreadTopicTransaction.php',
'ConpherenceThreadTransactionType' => 'applications/conpherence/xaction/ConpherenceThreadTransactionType.php',
'ConpherenceTransaction' => 'applications/conpherence/storage/ConpherenceTransaction.php',
'ConpherenceTransactionComment' => 'applications/conpherence/storage/ConpherenceTransactionComment.php',
'ConpherenceTransactionQuery' => 'applications/conpherence/query/ConpherenceTransactionQuery.php',
'ConpherenceTransactionRenderer' => 'applications/conpherence/ConpherenceTransactionRenderer.php',
'ConpherenceTransactionView' => 'applications/conpherence/view/ConpherenceTransactionView.php',
'ConpherenceUpdateActions' => 'applications/conpherence/constants/ConpherenceUpdateActions.php',
'ConpherenceUpdateController' => 'applications/conpherence/controller/ConpherenceUpdateController.php',
'ConpherenceUpdateThreadConduitAPIMethod' => 'applications/conpherence/conduit/ConpherenceUpdateThreadConduitAPIMethod.php',
'ConpherenceViewController' => 'applications/conpherence/controller/ConpherenceViewController.php',
'CountdownEditConduitAPIMethod' => 'applications/countdown/conduit/CountdownEditConduitAPIMethod.php',
'CountdownSearchConduitAPIMethod' => 'applications/countdown/conduit/CountdownSearchConduitAPIMethod.php',
'DarkConsoleController' => 'applications/console/controller/DarkConsoleController.php',
'DarkConsoleCore' => 'applications/console/core/DarkConsoleCore.php',
'DarkConsoleDataController' => 'applications/console/controller/DarkConsoleDataController.php',
'DarkConsoleErrorLogPlugin' => 'applications/console/plugin/DarkConsoleErrorLogPlugin.php',
'DarkConsoleErrorLogPluginAPI' => 'applications/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php',
'DarkConsoleEventPlugin' => 'applications/console/plugin/DarkConsoleEventPlugin.php',
'DarkConsoleEventPluginAPI' => 'applications/console/plugin/event/DarkConsoleEventPluginAPI.php',
'DarkConsolePlugin' => 'applications/console/plugin/DarkConsolePlugin.php',
'DarkConsoleRealtimePlugin' => 'applications/console/plugin/DarkConsoleRealtimePlugin.php',
'DarkConsoleRequestPlugin' => 'applications/console/plugin/DarkConsoleRequestPlugin.php',
'DarkConsoleServicesPlugin' => 'applications/console/plugin/DarkConsoleServicesPlugin.php',
'DarkConsoleStartupPlugin' => 'applications/console/plugin/DarkConsoleStartupPlugin.php',
'DarkConsoleXHProfPlugin' => 'applications/console/plugin/DarkConsoleXHProfPlugin.php',
'DarkConsoleXHProfPluginAPI' => 'applications/console/plugin/xhprof/DarkConsoleXHProfPluginAPI.php',
'DifferentialAction' => 'applications/differential/constants/DifferentialAction.php',
'DifferentialActionEmailCommand' => 'applications/differential/command/DifferentialActionEmailCommand.php',
'DifferentialAdjustmentMapTestCase' => 'applications/differential/storage/__tests__/DifferentialAdjustmentMapTestCase.php',
'DifferentialAffectedPath' => 'applications/differential/storage/DifferentialAffectedPath.php',
'DifferentialAsanaRepresentationField' => 'applications/differential/customfield/DifferentialAsanaRepresentationField.php',
'DifferentialAuditorsCommitMessageField' => 'applications/differential/field/DifferentialAuditorsCommitMessageField.php',
'DifferentialAuditorsField' => 'applications/differential/customfield/DifferentialAuditorsField.php',
'DifferentialBlameRevisionCommitMessageField' => 'applications/differential/field/DifferentialBlameRevisionCommitMessageField.php',
'DifferentialBlameRevisionField' => 'applications/differential/customfield/DifferentialBlameRevisionField.php',
'DifferentialBlockHeraldAction' => 'applications/differential/herald/DifferentialBlockHeraldAction.php',
'DifferentialBlockingReviewerDatasource' => 'applications/differential/typeahead/DifferentialBlockingReviewerDatasource.php',
'DifferentialBranchField' => 'applications/differential/customfield/DifferentialBranchField.php',
'DifferentialBuildableEngine' => 'applications/differential/harbormaster/DifferentialBuildableEngine.php',
'DifferentialChangeDetailMailView' => 'applications/differential/mail/DifferentialChangeDetailMailView.php',
'DifferentialChangeHeraldFieldGroup' => 'applications/differential/herald/DifferentialChangeHeraldFieldGroup.php',
'DifferentialChangeType' => 'applications/differential/constants/DifferentialChangeType.php',
'DifferentialChangesSinceLastUpdateField' => 'applications/differential/customfield/DifferentialChangesSinceLastUpdateField.php',
'DifferentialChangeset' => 'applications/differential/storage/DifferentialChangeset.php',
'DifferentialChangesetDetailView' => 'applications/differential/view/DifferentialChangesetDetailView.php',
'DifferentialChangesetEngine' => 'applications/differential/engine/DifferentialChangesetEngine.php',
'DifferentialChangesetFileTreeSideNavBuilder' => 'applications/differential/view/DifferentialChangesetFileTreeSideNavBuilder.php',
'DifferentialChangesetHTMLRenderer' => 'applications/differential/render/DifferentialChangesetHTMLRenderer.php',
'DifferentialChangesetListController' => 'applications/differential/controller/DifferentialChangesetListController.php',
'DifferentialChangesetListView' => 'applications/differential/view/DifferentialChangesetListView.php',
'DifferentialChangesetOneUpMailRenderer' => 'applications/differential/render/DifferentialChangesetOneUpMailRenderer.php',
'DifferentialChangesetOneUpRenderer' => 'applications/differential/render/DifferentialChangesetOneUpRenderer.php',
'DifferentialChangesetOneUpTestRenderer' => 'applications/differential/render/DifferentialChangesetOneUpTestRenderer.php',
'DifferentialChangesetParser' => 'applications/differential/parser/DifferentialChangesetParser.php',
'DifferentialChangesetParserTestCase' => 'applications/differential/parser/__tests__/DifferentialChangesetParserTestCase.php',
'DifferentialChangesetQuery' => 'applications/differential/query/DifferentialChangesetQuery.php',
'DifferentialChangesetRenderer' => 'applications/differential/render/DifferentialChangesetRenderer.php',
'DifferentialChangesetSearchEngine' => 'applications/differential/query/DifferentialChangesetSearchEngine.php',
'DifferentialChangesetTestRenderer' => 'applications/differential/render/DifferentialChangesetTestRenderer.php',
'DifferentialChangesetTwoUpRenderer' => 'applications/differential/render/DifferentialChangesetTwoUpRenderer.php',
'DifferentialChangesetTwoUpTestRenderer' => 'applications/differential/render/DifferentialChangesetTwoUpTestRenderer.php',
'DifferentialChangesetViewController' => 'applications/differential/controller/DifferentialChangesetViewController.php',
'DifferentialCloseConduitAPIMethod' => 'applications/differential/conduit/DifferentialCloseConduitAPIMethod.php',
'DifferentialCommitMessageCustomField' => 'applications/differential/field/DifferentialCommitMessageCustomField.php',
'DifferentialCommitMessageField' => 'applications/differential/field/DifferentialCommitMessageField.php',
'DifferentialCommitMessageFieldTestCase' => 'applications/differential/field/__tests__/DifferentialCommitMessageFieldTestCase.php',
'DifferentialCommitMessageParser' => 'applications/differential/parser/DifferentialCommitMessageParser.php',
'DifferentialCommitMessageParserTestCase' => 'applications/differential/parser/__tests__/DifferentialCommitMessageParserTestCase.php',
'DifferentialCommitsField' => 'applications/differential/customfield/DifferentialCommitsField.php',
'DifferentialCommitsSearchEngineAttachment' => 'applications/differential/engineextension/DifferentialCommitsSearchEngineAttachment.php',
'DifferentialConduitAPIMethod' => 'applications/differential/conduit/DifferentialConduitAPIMethod.php',
'DifferentialConflictsCommitMessageField' => 'applications/differential/field/DifferentialConflictsCommitMessageField.php',
'DifferentialController' => 'applications/differential/controller/DifferentialController.php',
'DifferentialCoreCustomField' => 'applications/differential/customfield/DifferentialCoreCustomField.php',
'DifferentialCreateCommentConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateCommentConduitAPIMethod.php',
'DifferentialCreateDiffConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateDiffConduitAPIMethod.php',
'DifferentialCreateInlineConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateInlineConduitAPIMethod.php',
'DifferentialCreateMailReceiver' => 'applications/differential/mail/DifferentialCreateMailReceiver.php',
'DifferentialCreateRawDiffConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateRawDiffConduitAPIMethod.php',
'DifferentialCreateRevisionConduitAPIMethod' => 'applications/differential/conduit/DifferentialCreateRevisionConduitAPIMethod.php',
'DifferentialCustomField' => 'applications/differential/customfield/DifferentialCustomField.php',
'DifferentialCustomFieldDependsOnParser' => 'applications/differential/parser/DifferentialCustomFieldDependsOnParser.php',
'DifferentialCustomFieldDependsOnParserTestCase' => 'applications/differential/parser/__tests__/DifferentialCustomFieldDependsOnParserTestCase.php',
'DifferentialCustomFieldNumericIndex' => 'applications/differential/storage/DifferentialCustomFieldNumericIndex.php',
'DifferentialCustomFieldRevertsParser' => 'applications/differential/parser/DifferentialCustomFieldRevertsParser.php',
'DifferentialCustomFieldRevertsParserTestCase' => 'applications/differential/parser/__tests__/DifferentialCustomFieldRevertsParserTestCase.php',
'DifferentialCustomFieldStorage' => 'applications/differential/storage/DifferentialCustomFieldStorage.php',
'DifferentialCustomFieldStringIndex' => 'applications/differential/storage/DifferentialCustomFieldStringIndex.php',
'DifferentialDAO' => 'applications/differential/storage/DifferentialDAO.php',
'DifferentialDefaultViewCapability' => 'applications/differential/capability/DifferentialDefaultViewCapability.php',
'DifferentialDiff' => 'applications/differential/storage/DifferentialDiff.php',
'DifferentialDiffAffectedFilesHeraldField' => 'applications/differential/herald/DifferentialDiffAffectedFilesHeraldField.php',
'DifferentialDiffAuthorHeraldField' => 'applications/differential/herald/DifferentialDiffAuthorHeraldField.php',
'DifferentialDiffAuthorProjectsHeraldField' => 'applications/differential/herald/DifferentialDiffAuthorProjectsHeraldField.php',
'DifferentialDiffContentAddedHeraldField' => 'applications/differential/herald/DifferentialDiffContentAddedHeraldField.php',
'DifferentialDiffContentHeraldField' => 'applications/differential/herald/DifferentialDiffContentHeraldField.php',
'DifferentialDiffContentRemovedHeraldField' => 'applications/differential/herald/DifferentialDiffContentRemovedHeraldField.php',
'DifferentialDiffCreateController' => 'applications/differential/controller/DifferentialDiffCreateController.php',
'DifferentialDiffEditor' => 'applications/differential/editor/DifferentialDiffEditor.php',
'DifferentialDiffExtractionEngine' => 'applications/differential/engine/DifferentialDiffExtractionEngine.php',
'DifferentialDiffHeraldField' => 'applications/differential/herald/DifferentialDiffHeraldField.php',
'DifferentialDiffHeraldFieldGroup' => 'applications/differential/herald/DifferentialDiffHeraldFieldGroup.php',
'DifferentialDiffInlineCommentQuery' => 'applications/differential/query/DifferentialDiffInlineCommentQuery.php',
'DifferentialDiffPHIDType' => 'applications/differential/phid/DifferentialDiffPHIDType.php',
'DifferentialDiffProperty' => 'applications/differential/storage/DifferentialDiffProperty.php',
'DifferentialDiffQuery' => 'applications/differential/query/DifferentialDiffQuery.php',
'DifferentialDiffRepositoryHeraldField' => 'applications/differential/herald/DifferentialDiffRepositoryHeraldField.php',
'DifferentialDiffRepositoryProjectsHeraldField' => 'applications/differential/herald/DifferentialDiffRepositoryProjectsHeraldField.php',
'DifferentialDiffSearchConduitAPIMethod' => 'applications/differential/conduit/DifferentialDiffSearchConduitAPIMethod.php',
'DifferentialDiffSearchEngine' => 'applications/differential/query/DifferentialDiffSearchEngine.php',
'DifferentialDiffTestCase' => 'applications/differential/storage/__tests__/DifferentialDiffTestCase.php',
'DifferentialDiffTransaction' => 'applications/differential/storage/DifferentialDiffTransaction.php',
'DifferentialDiffTransactionQuery' => 'applications/differential/query/DifferentialDiffTransactionQuery.php',
'DifferentialDiffViewController' => 'applications/differential/controller/DifferentialDiffViewController.php',
'DifferentialDoorkeeperRevisionFeedStoryPublisher' => 'applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php',
'DifferentialDraftField' => 'applications/differential/customfield/DifferentialDraftField.php',
'DifferentialExactUserFunctionDatasource' => 'applications/differential/typeahead/DifferentialExactUserFunctionDatasource.php',
'DifferentialFieldParseException' => 'applications/differential/exception/DifferentialFieldParseException.php',
'DifferentialFieldValidationException' => 'applications/differential/exception/DifferentialFieldValidationException.php',
'DifferentialGetAllDiffsConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetAllDiffsConduitAPIMethod.php',
'DifferentialGetCommitMessageConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetCommitMessageConduitAPIMethod.php',
'DifferentialGetCommitPathsConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetCommitPathsConduitAPIMethod.php',
'DifferentialGetDiffConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetDiffConduitAPIMethod.php',
'DifferentialGetRawDiffConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetRawDiffConduitAPIMethod.php',
'DifferentialGetRevisionCommentsConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetRevisionCommentsConduitAPIMethod.php',
'DifferentialGetRevisionConduitAPIMethod' => 'applications/differential/conduit/DifferentialGetRevisionConduitAPIMethod.php',
'DifferentialGetWorkingCopy' => 'applications/differential/DifferentialGetWorkingCopy.php',
'DifferentialGitSVNIDCommitMessageField' => 'applications/differential/field/DifferentialGitSVNIDCommitMessageField.php',
'DifferentialHarbormasterField' => 'applications/differential/customfield/DifferentialHarbormasterField.php',
'DifferentialHeraldStateReasons' => 'applications/differential/herald/DifferentialHeraldStateReasons.php',
'DifferentialHiddenComment' => 'applications/differential/storage/DifferentialHiddenComment.php',
'DifferentialHostField' => 'applications/differential/customfield/DifferentialHostField.php',
'DifferentialHovercardEngineExtension' => 'applications/differential/engineextension/DifferentialHovercardEngineExtension.php',
'DifferentialHunk' => 'applications/differential/storage/DifferentialHunk.php',
'DifferentialHunkParser' => 'applications/differential/parser/DifferentialHunkParser.php',
'DifferentialHunkParserTestCase' => 'applications/differential/parser/__tests__/DifferentialHunkParserTestCase.php',
'DifferentialHunkQuery' => 'applications/differential/query/DifferentialHunkQuery.php',
'DifferentialHunkTestCase' => 'applications/differential/storage/__tests__/DifferentialHunkTestCase.php',
'DifferentialInlineComment' => 'applications/differential/storage/DifferentialInlineComment.php',
'DifferentialInlineCommentEditController' => 'applications/differential/controller/DifferentialInlineCommentEditController.php',
'DifferentialInlineCommentMailView' => 'applications/differential/mail/DifferentialInlineCommentMailView.php',
'DifferentialInlineCommentQuery' => 'applications/differential/query/DifferentialInlineCommentQuery.php',
'DifferentialJIRAIssuesCommitMessageField' => 'applications/differential/field/DifferentialJIRAIssuesCommitMessageField.php',
'DifferentialJIRAIssuesField' => 'applications/differential/customfield/DifferentialJIRAIssuesField.php',
'DifferentialLegacyQuery' => 'applications/differential/constants/DifferentialLegacyQuery.php',
'DifferentialLineAdjustmentMap' => 'applications/differential/parser/DifferentialLineAdjustmentMap.php',
'DifferentialLintField' => 'applications/differential/customfield/DifferentialLintField.php',
'DifferentialLintStatus' => 'applications/differential/constants/DifferentialLintStatus.php',
'DifferentialLocalCommitsView' => 'applications/differential/view/DifferentialLocalCommitsView.php',
'DifferentialMailEngineExtension' => 'applications/differential/engineextension/DifferentialMailEngineExtension.php',
'DifferentialMailView' => 'applications/differential/mail/DifferentialMailView.php',
'DifferentialManiphestTasksField' => 'applications/differential/customfield/DifferentialManiphestTasksField.php',
'DifferentialParseCacheGarbageCollector' => 'applications/differential/garbagecollector/DifferentialParseCacheGarbageCollector.php',
'DifferentialParseCommitMessageConduitAPIMethod' => 'applications/differential/conduit/DifferentialParseCommitMessageConduitAPIMethod.php',
'DifferentialParseRenderTestCase' => 'applications/differential/__tests__/DifferentialParseRenderTestCase.php',
'DifferentialPathField' => 'applications/differential/customfield/DifferentialPathField.php',
'DifferentialProjectReviewersField' => 'applications/differential/customfield/DifferentialProjectReviewersField.php',
'DifferentialQueryConduitAPIMethod' => 'applications/differential/conduit/DifferentialQueryConduitAPIMethod.php',
'DifferentialQueryDiffsConduitAPIMethod' => 'applications/differential/conduit/DifferentialQueryDiffsConduitAPIMethod.php',
'DifferentialRawDiffRenderer' => 'applications/differential/render/DifferentialRawDiffRenderer.php',
'DifferentialReleephRequestFieldSpecification' => 'applications/releeph/differential/DifferentialReleephRequestFieldSpecification.php',
'DifferentialRemarkupRule' => 'applications/differential/remarkup/DifferentialRemarkupRule.php',
'DifferentialReplyHandler' => 'applications/differential/mail/DifferentialReplyHandler.php',
'DifferentialRepositoryField' => 'applications/differential/customfield/DifferentialRepositoryField.php',
'DifferentialRepositoryLookup' => 'applications/differential/query/DifferentialRepositoryLookup.php',
'DifferentialRequiredSignaturesField' => 'applications/differential/customfield/DifferentialRequiredSignaturesField.php',
'DifferentialResponsibleDatasource' => 'applications/differential/typeahead/DifferentialResponsibleDatasource.php',
'DifferentialResponsibleUserDatasource' => 'applications/differential/typeahead/DifferentialResponsibleUserDatasource.php',
'DifferentialResponsibleViewerFunctionDatasource' => 'applications/differential/typeahead/DifferentialResponsibleViewerFunctionDatasource.php',
'DifferentialRevertPlanCommitMessageField' => 'applications/differential/field/DifferentialRevertPlanCommitMessageField.php',
'DifferentialRevertPlanField' => 'applications/differential/customfield/DifferentialRevertPlanField.php',
'DifferentialReviewedByCommitMessageField' => 'applications/differential/field/DifferentialReviewedByCommitMessageField.php',
'DifferentialReviewer' => 'applications/differential/storage/DifferentialReviewer.php',
'DifferentialReviewerDatasource' => 'applications/differential/typeahead/DifferentialReviewerDatasource.php',
'DifferentialReviewerForRevisionEdgeType' => 'applications/differential/edge/DifferentialReviewerForRevisionEdgeType.php',
'DifferentialReviewerStatus' => 'applications/differential/constants/DifferentialReviewerStatus.php',
'DifferentialReviewersAddBlockingReviewersHeraldAction' => 'applications/differential/herald/DifferentialReviewersAddBlockingReviewersHeraldAction.php',
'DifferentialReviewersAddBlockingSelfHeraldAction' => 'applications/differential/herald/DifferentialReviewersAddBlockingSelfHeraldAction.php',
'DifferentialReviewersAddReviewersHeraldAction' => 'applications/differential/herald/DifferentialReviewersAddReviewersHeraldAction.php',
'DifferentialReviewersAddSelfHeraldAction' => 'applications/differential/herald/DifferentialReviewersAddSelfHeraldAction.php',
'DifferentialReviewersCommitMessageField' => 'applications/differential/field/DifferentialReviewersCommitMessageField.php',
'DifferentialReviewersField' => 'applications/differential/customfield/DifferentialReviewersField.php',
'DifferentialReviewersHeraldAction' => 'applications/differential/herald/DifferentialReviewersHeraldAction.php',
'DifferentialReviewersSearchEngineAttachment' => 'applications/differential/engineextension/DifferentialReviewersSearchEngineAttachment.php',
'DifferentialReviewersView' => 'applications/differential/view/DifferentialReviewersView.php',
'DifferentialRevision' => 'applications/differential/storage/DifferentialRevision.php',
'DifferentialRevisionAbandonTransaction' => 'applications/differential/xaction/DifferentialRevisionAbandonTransaction.php',
'DifferentialRevisionAcceptTransaction' => 'applications/differential/xaction/DifferentialRevisionAcceptTransaction.php',
'DifferentialRevisionActionTransaction' => 'applications/differential/xaction/DifferentialRevisionActionTransaction.php',
'DifferentialRevisionAffectedFilesHeraldField' => 'applications/differential/herald/DifferentialRevisionAffectedFilesHeraldField.php',
'DifferentialRevisionAuthorHeraldField' => 'applications/differential/herald/DifferentialRevisionAuthorHeraldField.php',
'DifferentialRevisionAuthorProjectsHeraldField' => 'applications/differential/herald/DifferentialRevisionAuthorProjectsHeraldField.php',
'DifferentialRevisionBuildableTransaction' => 'applications/differential/xaction/DifferentialRevisionBuildableTransaction.php',
'DifferentialRevisionCloseDetailsController' => 'applications/differential/controller/DifferentialRevisionCloseDetailsController.php',
'DifferentialRevisionCloseTransaction' => 'applications/differential/xaction/DifferentialRevisionCloseTransaction.php',
'DifferentialRevisionClosedStatusDatasource' => 'applications/differential/typeahead/DifferentialRevisionClosedStatusDatasource.php',
'DifferentialRevisionCommandeerTransaction' => 'applications/differential/xaction/DifferentialRevisionCommandeerTransaction.php',
'DifferentialRevisionContentAddedHeraldField' => 'applications/differential/herald/DifferentialRevisionContentAddedHeraldField.php',
'DifferentialRevisionContentHeraldField' => 'applications/differential/herald/DifferentialRevisionContentHeraldField.php',
'DifferentialRevisionContentRemovedHeraldField' => 'applications/differential/herald/DifferentialRevisionContentRemovedHeraldField.php',
'DifferentialRevisionControlSystem' => 'applications/differential/constants/DifferentialRevisionControlSystem.php',
'DifferentialRevisionDependedOnByRevisionEdgeType' => 'applications/differential/edge/DifferentialRevisionDependedOnByRevisionEdgeType.php',
'DifferentialRevisionDependsOnRevisionEdgeType' => 'applications/differential/edge/DifferentialRevisionDependsOnRevisionEdgeType.php',
'DifferentialRevisionDraftEngine' => 'applications/differential/engine/DifferentialRevisionDraftEngine.php',
'DifferentialRevisionEditConduitAPIMethod' => 'applications/differential/conduit/DifferentialRevisionEditConduitAPIMethod.php',
'DifferentialRevisionEditController' => 'applications/differential/controller/DifferentialRevisionEditController.php',
'DifferentialRevisionEditEngine' => 'applications/differential/editor/DifferentialRevisionEditEngine.php',
'DifferentialRevisionFerretEngine' => 'applications/differential/search/DifferentialRevisionFerretEngine.php',
'DifferentialRevisionFulltextEngine' => 'applications/differential/search/DifferentialRevisionFulltextEngine.php',
'DifferentialRevisionGraph' => 'infrastructure/graph/DifferentialRevisionGraph.php',
'DifferentialRevisionHasChildRelationship' => 'applications/differential/relationships/DifferentialRevisionHasChildRelationship.php',
'DifferentialRevisionHasCommitEdgeType' => 'applications/differential/edge/DifferentialRevisionHasCommitEdgeType.php',
'DifferentialRevisionHasCommitRelationship' => 'applications/differential/relationships/DifferentialRevisionHasCommitRelationship.php',
'DifferentialRevisionHasParentRelationship' => 'applications/differential/relationships/DifferentialRevisionHasParentRelationship.php',
'DifferentialRevisionHasReviewerEdgeType' => 'applications/differential/edge/DifferentialRevisionHasReviewerEdgeType.php',
'DifferentialRevisionHasTaskEdgeType' => 'applications/differential/edge/DifferentialRevisionHasTaskEdgeType.php',
'DifferentialRevisionHasTaskRelationship' => 'applications/differential/relationships/DifferentialRevisionHasTaskRelationship.php',
'DifferentialRevisionHeraldField' => 'applications/differential/herald/DifferentialRevisionHeraldField.php',
'DifferentialRevisionHeraldFieldGroup' => 'applications/differential/herald/DifferentialRevisionHeraldFieldGroup.php',
'DifferentialRevisionHoldDraftTransaction' => 'applications/differential/xaction/DifferentialRevisionHoldDraftTransaction.php',
'DifferentialRevisionIDCommitMessageField' => 'applications/differential/field/DifferentialRevisionIDCommitMessageField.php',
'DifferentialRevisionInlineTransaction' => 'applications/differential/xaction/DifferentialRevisionInlineTransaction.php',
'DifferentialRevisionInlinesController' => 'applications/differential/controller/DifferentialRevisionInlinesController.php',
'DifferentialRevisionListController' => 'applications/differential/controller/DifferentialRevisionListController.php',
'DifferentialRevisionListView' => 'applications/differential/view/DifferentialRevisionListView.php',
'DifferentialRevisionMailReceiver' => 'applications/differential/mail/DifferentialRevisionMailReceiver.php',
'DifferentialRevisionOpenStatusDatasource' => 'applications/differential/typeahead/DifferentialRevisionOpenStatusDatasource.php',
'DifferentialRevisionOperationController' => 'applications/differential/controller/DifferentialRevisionOperationController.php',
'DifferentialRevisionPHIDType' => 'applications/differential/phid/DifferentialRevisionPHIDType.php',
'DifferentialRevisionPackageHeraldField' => 'applications/differential/herald/DifferentialRevisionPackageHeraldField.php',
'DifferentialRevisionPackageOwnerHeraldField' => 'applications/differential/herald/DifferentialRevisionPackageOwnerHeraldField.php',
'DifferentialRevisionPlanChangesTransaction' => 'applications/differential/xaction/DifferentialRevisionPlanChangesTransaction.php',
'DifferentialRevisionQuery' => 'applications/differential/query/DifferentialRevisionQuery.php',
'DifferentialRevisionReclaimTransaction' => 'applications/differential/xaction/DifferentialRevisionReclaimTransaction.php',
'DifferentialRevisionRejectTransaction' => 'applications/differential/xaction/DifferentialRevisionRejectTransaction.php',
'DifferentialRevisionRelationship' => 'applications/differential/relationships/DifferentialRevisionRelationship.php',
'DifferentialRevisionRelationshipSource' => 'applications/search/relationship/DifferentialRevisionRelationshipSource.php',
'DifferentialRevisionReopenTransaction' => 'applications/differential/xaction/DifferentialRevisionReopenTransaction.php',
'DifferentialRevisionRepositoryHeraldField' => 'applications/differential/herald/DifferentialRevisionRepositoryHeraldField.php',
'DifferentialRevisionRepositoryProjectsHeraldField' => 'applications/differential/herald/DifferentialRevisionRepositoryProjectsHeraldField.php',
'DifferentialRevisionRepositoryTransaction' => 'applications/differential/xaction/DifferentialRevisionRepositoryTransaction.php',
'DifferentialRevisionRequestReviewTransaction' => 'applications/differential/xaction/DifferentialRevisionRequestReviewTransaction.php',
'DifferentialRevisionRequiredActionResultBucket' => 'applications/differential/query/DifferentialRevisionRequiredActionResultBucket.php',
'DifferentialRevisionResignTransaction' => 'applications/differential/xaction/DifferentialRevisionResignTransaction.php',
'DifferentialRevisionResultBucket' => 'applications/differential/query/DifferentialRevisionResultBucket.php',
'DifferentialRevisionReviewTransaction' => 'applications/differential/xaction/DifferentialRevisionReviewTransaction.php',
'DifferentialRevisionReviewersHeraldField' => 'applications/differential/herald/DifferentialRevisionReviewersHeraldField.php',
'DifferentialRevisionReviewersTransaction' => 'applications/differential/xaction/DifferentialRevisionReviewersTransaction.php',
'DifferentialRevisionSearchConduitAPIMethod' => 'applications/differential/conduit/DifferentialRevisionSearchConduitAPIMethod.php',
'DifferentialRevisionSearchEngine' => 'applications/differential/query/DifferentialRevisionSearchEngine.php',
'DifferentialRevisionStatus' => 'applications/differential/constants/DifferentialRevisionStatus.php',
'DifferentialRevisionStatusDatasource' => 'applications/differential/typeahead/DifferentialRevisionStatusDatasource.php',
'DifferentialRevisionStatusFunctionDatasource' => 'applications/differential/typeahead/DifferentialRevisionStatusFunctionDatasource.php',
'DifferentialRevisionStatusHeraldField' => 'applications/differential/herald/DifferentialRevisionStatusHeraldField.php',
'DifferentialRevisionStatusTransaction' => 'applications/differential/xaction/DifferentialRevisionStatusTransaction.php',
'DifferentialRevisionSummaryHeraldField' => 'applications/differential/herald/DifferentialRevisionSummaryHeraldField.php',
'DifferentialRevisionSummaryTransaction' => 'applications/differential/xaction/DifferentialRevisionSummaryTransaction.php',
'DifferentialRevisionTestPlanHeraldField' => 'applications/differential/herald/DifferentialRevisionTestPlanHeraldField.php',
'DifferentialRevisionTestPlanTransaction' => 'applications/differential/xaction/DifferentialRevisionTestPlanTransaction.php',
+ 'DifferentialRevisionTimelineEngine' => 'applications/differential/engine/DifferentialRevisionTimelineEngine.php',
'DifferentialRevisionTitleHeraldField' => 'applications/differential/herald/DifferentialRevisionTitleHeraldField.php',
'DifferentialRevisionTitleTransaction' => 'applications/differential/xaction/DifferentialRevisionTitleTransaction.php',
'DifferentialRevisionTransactionType' => 'applications/differential/xaction/DifferentialRevisionTransactionType.php',
'DifferentialRevisionUpdateHistoryView' => 'applications/differential/view/DifferentialRevisionUpdateHistoryView.php',
'DifferentialRevisionUpdateTransaction' => 'applications/differential/xaction/DifferentialRevisionUpdateTransaction.php',
'DifferentialRevisionViewController' => 'applications/differential/controller/DifferentialRevisionViewController.php',
'DifferentialRevisionVoidTransaction' => 'applications/differential/xaction/DifferentialRevisionVoidTransaction.php',
'DifferentialRevisionWrongStateTransaction' => 'applications/differential/xaction/DifferentialRevisionWrongStateTransaction.php',
'DifferentialSchemaSpec' => 'applications/differential/storage/DifferentialSchemaSpec.php',
'DifferentialSetDiffPropertyConduitAPIMethod' => 'applications/differential/conduit/DifferentialSetDiffPropertyConduitAPIMethod.php',
'DifferentialStoredCustomField' => 'applications/differential/customfield/DifferentialStoredCustomField.php',
'DifferentialSubscribersCommitMessageField' => 'applications/differential/field/DifferentialSubscribersCommitMessageField.php',
'DifferentialSummaryCommitMessageField' => 'applications/differential/field/DifferentialSummaryCommitMessageField.php',
'DifferentialSummaryField' => 'applications/differential/customfield/DifferentialSummaryField.php',
'DifferentialTagsCommitMessageField' => 'applications/differential/field/DifferentialTagsCommitMessageField.php',
'DifferentialTasksCommitMessageField' => 'applications/differential/field/DifferentialTasksCommitMessageField.php',
'DifferentialTestPlanCommitMessageField' => 'applications/differential/field/DifferentialTestPlanCommitMessageField.php',
'DifferentialTestPlanField' => 'applications/differential/customfield/DifferentialTestPlanField.php',
'DifferentialTitleCommitMessageField' => 'applications/differential/field/DifferentialTitleCommitMessageField.php',
'DifferentialTransaction' => 'applications/differential/storage/DifferentialTransaction.php',
'DifferentialTransactionComment' => 'applications/differential/storage/DifferentialTransactionComment.php',
'DifferentialTransactionEditor' => 'applications/differential/editor/DifferentialTransactionEditor.php',
'DifferentialTransactionQuery' => 'applications/differential/query/DifferentialTransactionQuery.php',
'DifferentialTransactionView' => 'applications/differential/view/DifferentialTransactionView.php',
'DifferentialUnitField' => 'applications/differential/customfield/DifferentialUnitField.php',
'DifferentialUnitStatus' => 'applications/differential/constants/DifferentialUnitStatus.php',
'DifferentialUnitTestResult' => 'applications/differential/constants/DifferentialUnitTestResult.php',
'DifferentialUpdateRevisionConduitAPIMethod' => 'applications/differential/conduit/DifferentialUpdateRevisionConduitAPIMethod.php',
'DiffusionAuditorDatasource' => 'applications/diffusion/typeahead/DiffusionAuditorDatasource.php',
'DiffusionAuditorFunctionDatasource' => 'applications/diffusion/typeahead/DiffusionAuditorFunctionDatasource.php',
'DiffusionAuditorsAddAuditorsHeraldAction' => 'applications/diffusion/herald/DiffusionAuditorsAddAuditorsHeraldAction.php',
'DiffusionAuditorsAddSelfHeraldAction' => 'applications/diffusion/herald/DiffusionAuditorsAddSelfHeraldAction.php',
'DiffusionAuditorsHeraldAction' => 'applications/diffusion/herald/DiffusionAuditorsHeraldAction.php',
'DiffusionBlameConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionBlameConduitAPIMethod.php',
'DiffusionBlameController' => 'applications/diffusion/controller/DiffusionBlameController.php',
'DiffusionBlameQuery' => 'applications/diffusion/query/blame/DiffusionBlameQuery.php',
'DiffusionBlockHeraldAction' => 'applications/diffusion/herald/DiffusionBlockHeraldAction.php',
'DiffusionBranchListView' => 'applications/diffusion/view/DiffusionBranchListView.php',
'DiffusionBranchQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionBranchQueryConduitAPIMethod.php',
'DiffusionBranchTableController' => 'applications/diffusion/controller/DiffusionBranchTableController.php',
'DiffusionBranchTableView' => 'applications/diffusion/view/DiffusionBranchTableView.php',
'DiffusionBrowseController' => 'applications/diffusion/controller/DiffusionBrowseController.php',
'DiffusionBrowseQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionBrowseQueryConduitAPIMethod.php',
'DiffusionBrowseResultSet' => 'applications/diffusion/data/DiffusionBrowseResultSet.php',
'DiffusionBrowseTableView' => 'applications/diffusion/view/DiffusionBrowseTableView.php',
'DiffusionBuildableEngine' => 'applications/diffusion/harbormaster/DiffusionBuildableEngine.php',
'DiffusionCacheEngineExtension' => 'applications/diffusion/engineextension/DiffusionCacheEngineExtension.php',
'DiffusionCachedResolveRefsQuery' => 'applications/diffusion/query/DiffusionCachedResolveRefsQuery.php',
'DiffusionChangeController' => 'applications/diffusion/controller/DiffusionChangeController.php',
'DiffusionChangeHeraldFieldGroup' => 'applications/diffusion/herald/DiffusionChangeHeraldFieldGroup.php',
'DiffusionCloneController' => 'applications/diffusion/controller/DiffusionCloneController.php',
'DiffusionCloneURIView' => 'applications/diffusion/view/DiffusionCloneURIView.php',
'DiffusionCommandEngine' => 'applications/diffusion/protocol/DiffusionCommandEngine.php',
'DiffusionCommandEngineTestCase' => 'applications/diffusion/protocol/__tests__/DiffusionCommandEngineTestCase.php',
'DiffusionCommitAcceptTransaction' => 'applications/diffusion/xaction/DiffusionCommitAcceptTransaction.php',
'DiffusionCommitActionTransaction' => 'applications/diffusion/xaction/DiffusionCommitActionTransaction.php',
'DiffusionCommitAffectedFilesHeraldField' => 'applications/diffusion/herald/DiffusionCommitAffectedFilesHeraldField.php',
'DiffusionCommitAuditStatus' => 'applications/diffusion/DiffusionCommitAuditStatus.php',
'DiffusionCommitAuditTransaction' => 'applications/diffusion/xaction/DiffusionCommitAuditTransaction.php',
'DiffusionCommitAuditorsHeraldField' => 'applications/diffusion/herald/DiffusionCommitAuditorsHeraldField.php',
'DiffusionCommitAuditorsTransaction' => 'applications/diffusion/xaction/DiffusionCommitAuditorsTransaction.php',
'DiffusionCommitAuthorHeraldField' => 'applications/diffusion/herald/DiffusionCommitAuthorHeraldField.php',
'DiffusionCommitAuthorProjectsHeraldField' => 'applications/diffusion/herald/DiffusionCommitAuthorProjectsHeraldField.php',
'DiffusionCommitAutocloseHeraldField' => 'applications/diffusion/herald/DiffusionCommitAutocloseHeraldField.php',
'DiffusionCommitBranchesController' => 'applications/diffusion/controller/DiffusionCommitBranchesController.php',
'DiffusionCommitBranchesHeraldField' => 'applications/diffusion/herald/DiffusionCommitBranchesHeraldField.php',
'DiffusionCommitBuildableTransaction' => 'applications/diffusion/xaction/DiffusionCommitBuildableTransaction.php',
'DiffusionCommitCommitterHeraldField' => 'applications/diffusion/herald/DiffusionCommitCommitterHeraldField.php',
'DiffusionCommitCommitterProjectsHeraldField' => 'applications/diffusion/herald/DiffusionCommitCommitterProjectsHeraldField.php',
'DiffusionCommitConcernTransaction' => 'applications/diffusion/xaction/DiffusionCommitConcernTransaction.php',
'DiffusionCommitController' => 'applications/diffusion/controller/DiffusionCommitController.php',
'DiffusionCommitDiffContentAddedHeraldField' => 'applications/diffusion/herald/DiffusionCommitDiffContentAddedHeraldField.php',
'DiffusionCommitDiffContentHeraldField' => 'applications/diffusion/herald/DiffusionCommitDiffContentHeraldField.php',
'DiffusionCommitDiffContentRemovedHeraldField' => 'applications/diffusion/herald/DiffusionCommitDiffContentRemovedHeraldField.php',
'DiffusionCommitDiffEnormousHeraldField' => 'applications/diffusion/herald/DiffusionCommitDiffEnormousHeraldField.php',
'DiffusionCommitDraftEngine' => 'applications/diffusion/engine/DiffusionCommitDraftEngine.php',
'DiffusionCommitEditConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionCommitEditConduitAPIMethod.php',
'DiffusionCommitEditController' => 'applications/diffusion/controller/DiffusionCommitEditController.php',
'DiffusionCommitEditEngine' => 'applications/diffusion/editor/DiffusionCommitEditEngine.php',
'DiffusionCommitFerretEngine' => 'applications/repository/search/DiffusionCommitFerretEngine.php',
'DiffusionCommitFulltextEngine' => 'applications/repository/search/DiffusionCommitFulltextEngine.php',
'DiffusionCommitHasPackageEdgeType' => 'applications/diffusion/edge/DiffusionCommitHasPackageEdgeType.php',
'DiffusionCommitHasRevisionEdgeType' => 'applications/diffusion/edge/DiffusionCommitHasRevisionEdgeType.php',
'DiffusionCommitHasRevisionRelationship' => 'applications/diffusion/relationships/DiffusionCommitHasRevisionRelationship.php',
'DiffusionCommitHasTaskEdgeType' => 'applications/diffusion/edge/DiffusionCommitHasTaskEdgeType.php',
'DiffusionCommitHasTaskRelationship' => 'applications/diffusion/relationships/DiffusionCommitHasTaskRelationship.php',
'DiffusionCommitHash' => 'applications/diffusion/data/DiffusionCommitHash.php',
'DiffusionCommitHeraldField' => 'applications/diffusion/herald/DiffusionCommitHeraldField.php',
'DiffusionCommitHeraldFieldGroup' => 'applications/diffusion/herald/DiffusionCommitHeraldFieldGroup.php',
'DiffusionCommitHintQuery' => 'applications/diffusion/query/DiffusionCommitHintQuery.php',
'DiffusionCommitHookEngine' => 'applications/diffusion/engine/DiffusionCommitHookEngine.php',
'DiffusionCommitHookRejectException' => 'applications/diffusion/exception/DiffusionCommitHookRejectException.php',
'DiffusionCommitListController' => 'applications/diffusion/controller/DiffusionCommitListController.php',
'DiffusionCommitListView' => 'applications/diffusion/view/DiffusionCommitListView.php',
'DiffusionCommitMergeHeraldField' => 'applications/diffusion/herald/DiffusionCommitMergeHeraldField.php',
'DiffusionCommitMessageHeraldField' => 'applications/diffusion/herald/DiffusionCommitMessageHeraldField.php',
'DiffusionCommitPackageAuditHeraldField' => 'applications/diffusion/herald/DiffusionCommitPackageAuditHeraldField.php',
'DiffusionCommitPackageHeraldField' => 'applications/diffusion/herald/DiffusionCommitPackageHeraldField.php',
'DiffusionCommitPackageOwnerHeraldField' => 'applications/diffusion/herald/DiffusionCommitPackageOwnerHeraldField.php',
'DiffusionCommitParentsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionCommitParentsQueryConduitAPIMethod.php',
'DiffusionCommitQuery' => 'applications/diffusion/query/DiffusionCommitQuery.php',
'DiffusionCommitRef' => 'applications/diffusion/data/DiffusionCommitRef.php',
'DiffusionCommitRelationship' => 'applications/diffusion/relationships/DiffusionCommitRelationship.php',
'DiffusionCommitRelationshipSource' => 'applications/search/relationship/DiffusionCommitRelationshipSource.php',
'DiffusionCommitRemarkupRule' => 'applications/diffusion/remarkup/DiffusionCommitRemarkupRule.php',
'DiffusionCommitRemarkupRuleTestCase' => 'applications/diffusion/remarkup/__tests__/DiffusionCommitRemarkupRuleTestCase.php',
'DiffusionCommitRepositoryHeraldField' => 'applications/diffusion/herald/DiffusionCommitRepositoryHeraldField.php',
'DiffusionCommitRepositoryProjectsHeraldField' => 'applications/diffusion/herald/DiffusionCommitRepositoryProjectsHeraldField.php',
'DiffusionCommitRequiredActionResultBucket' => 'applications/diffusion/query/DiffusionCommitRequiredActionResultBucket.php',
'DiffusionCommitResignTransaction' => 'applications/diffusion/xaction/DiffusionCommitResignTransaction.php',
'DiffusionCommitResultBucket' => 'applications/diffusion/query/DiffusionCommitResultBucket.php',
'DiffusionCommitRevertedByCommitEdgeType' => 'applications/diffusion/edge/DiffusionCommitRevertedByCommitEdgeType.php',
'DiffusionCommitRevertsCommitEdgeType' => 'applications/diffusion/edge/DiffusionCommitRevertsCommitEdgeType.php',
'DiffusionCommitReviewerHeraldField' => 'applications/diffusion/herald/DiffusionCommitReviewerHeraldField.php',
'DiffusionCommitRevisionAcceptedHeraldField' => 'applications/diffusion/herald/DiffusionCommitRevisionAcceptedHeraldField.php',
'DiffusionCommitRevisionAcceptingReviewersHeraldField' => 'applications/diffusion/herald/DiffusionCommitRevisionAcceptingReviewersHeraldField.php',
'DiffusionCommitRevisionHeraldField' => 'applications/diffusion/herald/DiffusionCommitRevisionHeraldField.php',
'DiffusionCommitRevisionReviewersHeraldField' => 'applications/diffusion/herald/DiffusionCommitRevisionReviewersHeraldField.php',
'DiffusionCommitRevisionSubscribersHeraldField' => 'applications/diffusion/herald/DiffusionCommitRevisionSubscribersHeraldField.php',
'DiffusionCommitSearchConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionCommitSearchConduitAPIMethod.php',
'DiffusionCommitStateTransaction' => 'applications/diffusion/xaction/DiffusionCommitStateTransaction.php',
'DiffusionCommitTagsController' => 'applications/diffusion/controller/DiffusionCommitTagsController.php',
+ 'DiffusionCommitTimelineEngine' => 'applications/diffusion/engine/DiffusionCommitTimelineEngine.php',
'DiffusionCommitTransactionType' => 'applications/diffusion/xaction/DiffusionCommitTransactionType.php',
'DiffusionCommitVerifyTransaction' => 'applications/diffusion/xaction/DiffusionCommitVerifyTransaction.php',
'DiffusionCompareController' => 'applications/diffusion/controller/DiffusionCompareController.php',
'DiffusionConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionConduitAPIMethod.php',
'DiffusionController' => 'applications/diffusion/controller/DiffusionController.php',
'DiffusionCreateRepositoriesCapability' => 'applications/diffusion/capability/DiffusionCreateRepositoriesCapability.php',
'DiffusionDaemonLockException' => 'applications/diffusion/exception/DiffusionDaemonLockException.php',
'DiffusionDatasourceEngineExtension' => 'applications/diffusion/engineextension/DiffusionDatasourceEngineExtension.php',
'DiffusionDefaultEditCapability' => 'applications/diffusion/capability/DiffusionDefaultEditCapability.php',
'DiffusionDefaultPushCapability' => 'applications/diffusion/capability/DiffusionDefaultPushCapability.php',
'DiffusionDefaultViewCapability' => 'applications/diffusion/capability/DiffusionDefaultViewCapability.php',
'DiffusionDiffController' => 'applications/diffusion/controller/DiffusionDiffController.php',
'DiffusionDiffInlineCommentQuery' => 'applications/diffusion/query/DiffusionDiffInlineCommentQuery.php',
'DiffusionDiffQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionDiffQueryConduitAPIMethod.php',
'DiffusionDocumentController' => 'applications/diffusion/controller/DiffusionDocumentController.php',
'DiffusionDocumentRenderingEngine' => 'applications/diffusion/document/DiffusionDocumentRenderingEngine.php',
'DiffusionDoorkeeperCommitFeedStoryPublisher' => 'applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php',
'DiffusionEmptyResultView' => 'applications/diffusion/view/DiffusionEmptyResultView.php',
'DiffusionExistsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionExistsQueryConduitAPIMethod.php',
'DiffusionExternalController' => 'applications/diffusion/controller/DiffusionExternalController.php',
'DiffusionExternalSymbolQuery' => 'applications/diffusion/symbol/DiffusionExternalSymbolQuery.php',
'DiffusionExternalSymbolsSource' => 'applications/diffusion/symbol/DiffusionExternalSymbolsSource.php',
'DiffusionFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionFileContentQuery.php',
'DiffusionFileContentQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionFileContentQueryConduitAPIMethod.php',
'DiffusionFileFutureQuery' => 'applications/diffusion/query/DiffusionFileFutureQuery.php',
'DiffusionFindSymbolsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionFindSymbolsConduitAPIMethod.php',
'DiffusionGetLintMessagesConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionGetLintMessagesConduitAPIMethod.php',
'DiffusionGetRecentCommitsByPathConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionGetRecentCommitsByPathConduitAPIMethod.php',
'DiffusionGitBlameQuery' => 'applications/diffusion/query/blame/DiffusionGitBlameQuery.php',
'DiffusionGitBranch' => 'applications/diffusion/data/DiffusionGitBranch.php',
'DiffusionGitBranchTestCase' => 'applications/diffusion/data/__tests__/DiffusionGitBranchTestCase.php',
'DiffusionGitCommandEngine' => 'applications/diffusion/protocol/DiffusionGitCommandEngine.php',
'DiffusionGitFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionGitFileContentQuery.php',
'DiffusionGitLFSAuthenticateWorkflow' => 'applications/diffusion/gitlfs/DiffusionGitLFSAuthenticateWorkflow.php',
'DiffusionGitLFSResponse' => 'applications/diffusion/response/DiffusionGitLFSResponse.php',
'DiffusionGitLFSTemporaryTokenType' => 'applications/diffusion/gitlfs/DiffusionGitLFSTemporaryTokenType.php',
'DiffusionGitRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionGitRawDiffQuery.php',
'DiffusionGitReceivePackSSHWorkflow' => 'applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php',
'DiffusionGitRequest' => 'applications/diffusion/request/DiffusionGitRequest.php',
'DiffusionGitResponse' => 'applications/diffusion/response/DiffusionGitResponse.php',
'DiffusionGitSSHWorkflow' => 'applications/diffusion/ssh/DiffusionGitSSHWorkflow.php',
'DiffusionGitUploadPackSSHWorkflow' => 'applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php',
'DiffusionGraphController' => 'applications/diffusion/controller/DiffusionGraphController.php',
'DiffusionHistoryController' => 'applications/diffusion/controller/DiffusionHistoryController.php',
'DiffusionHistoryListView' => 'applications/diffusion/view/DiffusionHistoryListView.php',
'DiffusionHistoryQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionHistoryQueryConduitAPIMethod.php',
'DiffusionHistoryTableView' => 'applications/diffusion/view/DiffusionHistoryTableView.php',
'DiffusionHistoryView' => 'applications/diffusion/view/DiffusionHistoryView.php',
'DiffusionHovercardEngineExtension' => 'applications/diffusion/engineextension/DiffusionHovercardEngineExtension.php',
'DiffusionIdentityAssigneeDatasource' => 'applications/diffusion/typeahead/DiffusionIdentityAssigneeDatasource.php',
'DiffusionIdentityAssigneeEditField' => 'applications/diffusion/editfield/DiffusionIdentityAssigneeEditField.php',
'DiffusionIdentityAssigneeSearchField' => 'applications/diffusion/searchfield/DiffusionIdentityAssigneeSearchField.php',
'DiffusionIdentityEditController' => 'applications/diffusion/controller/DiffusionIdentityEditController.php',
'DiffusionIdentityListController' => 'applications/diffusion/controller/DiffusionIdentityListController.php',
'DiffusionIdentityUnassignedDatasource' => 'applications/diffusion/typeahead/DiffusionIdentityUnassignedDatasource.php',
'DiffusionIdentityViewController' => 'applications/diffusion/controller/DiffusionIdentityViewController.php',
'DiffusionInlineCommentController' => 'applications/diffusion/controller/DiffusionInlineCommentController.php',
'DiffusionInlineCommentPreviewController' => 'applications/diffusion/controller/DiffusionInlineCommentPreviewController.php',
'DiffusionInternalAncestorsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionInternalAncestorsConduitAPIMethod.php',
'DiffusionInternalGitRawDiffQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionInternalGitRawDiffQueryConduitAPIMethod.php',
'DiffusionLastModifiedController' => 'applications/diffusion/controller/DiffusionLastModifiedController.php',
'DiffusionLastModifiedQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionLastModifiedQueryConduitAPIMethod.php',
'DiffusionLintController' => 'applications/diffusion/controller/DiffusionLintController.php',
'DiffusionLintCountQuery' => 'applications/diffusion/query/DiffusionLintCountQuery.php',
'DiffusionLintSaveRunner' => 'applications/diffusion/DiffusionLintSaveRunner.php',
'DiffusionLocalRepositoryFilter' => 'applications/diffusion/data/DiffusionLocalRepositoryFilter.php',
'DiffusionLogController' => 'applications/diffusion/controller/DiffusionLogController.php',
'DiffusionLookSoonConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionLookSoonConduitAPIMethod.php',
'DiffusionLowLevelCommitFieldsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelCommitFieldsQuery.php',
'DiffusionLowLevelCommitQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelCommitQuery.php',
'DiffusionLowLevelFilesizeQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelFilesizeQuery.php',
'DiffusionLowLevelGitRefQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelGitRefQuery.php',
'DiffusionLowLevelMercurialBranchesQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelMercurialBranchesQuery.php',
'DiffusionLowLevelMercurialPathsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelMercurialPathsQuery.php',
'DiffusionLowLevelParentsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelParentsQuery.php',
'DiffusionLowLevelQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelQuery.php',
'DiffusionLowLevelResolveRefsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php',
'DiffusionMercurialBlameQuery' => 'applications/diffusion/query/blame/DiffusionMercurialBlameQuery.php',
'DiffusionMercurialCommandEngine' => 'applications/diffusion/protocol/DiffusionMercurialCommandEngine.php',
'DiffusionMercurialFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionMercurialFileContentQuery.php',
'DiffusionMercurialFlagInjectionException' => 'applications/diffusion/exception/DiffusionMercurialFlagInjectionException.php',
'DiffusionMercurialRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionMercurialRawDiffQuery.php',
'DiffusionMercurialRequest' => 'applications/diffusion/request/DiffusionMercurialRequest.php',
'DiffusionMercurialResponse' => 'applications/diffusion/response/DiffusionMercurialResponse.php',
'DiffusionMercurialSSHWorkflow' => 'applications/diffusion/ssh/DiffusionMercurialSSHWorkflow.php',
'DiffusionMercurialServeSSHWorkflow' => 'applications/diffusion/ssh/DiffusionMercurialServeSSHWorkflow.php',
'DiffusionMercurialWireClientSSHProtocolChannel' => 'applications/diffusion/ssh/DiffusionMercurialWireClientSSHProtocolChannel.php',
'DiffusionMercurialWireProtocol' => 'applications/diffusion/protocol/DiffusionMercurialWireProtocol.php',
'DiffusionMercurialWireProtocolTests' => 'applications/diffusion/protocol/__tests__/DiffusionMercurialWireProtocolTests.php',
'DiffusionMercurialWireSSHTestCase' => 'applications/diffusion/ssh/__tests__/DiffusionMercurialWireSSHTestCase.php',
'DiffusionMergedCommitsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionMergedCommitsQueryConduitAPIMethod.php',
'DiffusionPathChange' => 'applications/diffusion/data/DiffusionPathChange.php',
'DiffusionPathChangeQuery' => 'applications/diffusion/query/pathchange/DiffusionPathChangeQuery.php',
'DiffusionPathCompleteController' => 'applications/diffusion/controller/DiffusionPathCompleteController.php',
'DiffusionPathIDQuery' => 'applications/diffusion/query/pathid/DiffusionPathIDQuery.php',
'DiffusionPathQuery' => 'applications/diffusion/query/DiffusionPathQuery.php',
'DiffusionPathQueryTestCase' => 'applications/diffusion/query/pathid/__tests__/DiffusionPathQueryTestCase.php',
'DiffusionPathTreeController' => 'applications/diffusion/controller/DiffusionPathTreeController.php',
'DiffusionPathValidateController' => 'applications/diffusion/controller/DiffusionPathValidateController.php',
'DiffusionPatternSearchView' => 'applications/diffusion/view/DiffusionPatternSearchView.php',
'DiffusionPhpExternalSymbolsSource' => 'applications/diffusion/symbol/DiffusionPhpExternalSymbolsSource.php',
'DiffusionPreCommitContentAffectedFilesHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAffectedFilesHeraldField.php',
'DiffusionPreCommitContentAuthorHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAuthorHeraldField.php',
'DiffusionPreCommitContentAuthorProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAuthorProjectsHeraldField.php',
'DiffusionPreCommitContentAuthorRawHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAuthorRawHeraldField.php',
'DiffusionPreCommitContentBranchesHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentBranchesHeraldField.php',
'DiffusionPreCommitContentCommitterHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentCommitterHeraldField.php',
'DiffusionPreCommitContentCommitterProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentCommitterProjectsHeraldField.php',
'DiffusionPreCommitContentCommitterRawHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentCommitterRawHeraldField.php',
'DiffusionPreCommitContentDiffContentAddedHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentDiffContentAddedHeraldField.php',
'DiffusionPreCommitContentDiffContentHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentDiffContentHeraldField.php',
'DiffusionPreCommitContentDiffContentRemovedHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentDiffContentRemovedHeraldField.php',
'DiffusionPreCommitContentDiffEnormousHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentDiffEnormousHeraldField.php',
'DiffusionPreCommitContentHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentHeraldField.php',
'DiffusionPreCommitContentMergeHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentMergeHeraldField.php',
'DiffusionPreCommitContentMessageHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentMessageHeraldField.php',
'DiffusionPreCommitContentPackageHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentPackageHeraldField.php',
'DiffusionPreCommitContentPackageOwnerHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentPackageOwnerHeraldField.php',
'DiffusionPreCommitContentPusherHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentPusherHeraldField.php',
'DiffusionPreCommitContentPusherIsCommitterHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentPusherIsCommitterHeraldField.php',
'DiffusionPreCommitContentPusherProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentPusherProjectsHeraldField.php',
'DiffusionPreCommitContentRepositoryHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentRepositoryHeraldField.php',
'DiffusionPreCommitContentRepositoryProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentRepositoryProjectsHeraldField.php',
'DiffusionPreCommitContentRevisionAcceptedHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentRevisionAcceptedHeraldField.php',
'DiffusionPreCommitContentRevisionAcceptingReviewersHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentRevisionAcceptingReviewersHeraldField.php',
'DiffusionPreCommitContentRevisionHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentRevisionHeraldField.php',
'DiffusionPreCommitContentRevisionReviewersHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentRevisionReviewersHeraldField.php',
'DiffusionPreCommitContentRevisionSubscribersHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentRevisionSubscribersHeraldField.php',
'DiffusionPreCommitRefChangeHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefChangeHeraldField.php',
'DiffusionPreCommitRefHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefHeraldField.php',
'DiffusionPreCommitRefHeraldFieldGroup' => 'applications/diffusion/herald/DiffusionPreCommitRefHeraldFieldGroup.php',
'DiffusionPreCommitRefNameHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefNameHeraldField.php',
'DiffusionPreCommitRefPusherHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefPusherHeraldField.php',
'DiffusionPreCommitRefPusherProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefPusherProjectsHeraldField.php',
'DiffusionPreCommitRefRepositoryHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefRepositoryHeraldField.php',
'DiffusionPreCommitRefRepositoryProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefRepositoryProjectsHeraldField.php',
'DiffusionPreCommitRefTypeHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitRefTypeHeraldField.php',
'DiffusionPreCommitUsesGitLFSHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitUsesGitLFSHeraldField.php',
'DiffusionPullEventGarbageCollector' => 'applications/diffusion/garbagecollector/DiffusionPullEventGarbageCollector.php',
'DiffusionPullLogListController' => 'applications/diffusion/controller/DiffusionPullLogListController.php',
'DiffusionPullLogListView' => 'applications/diffusion/view/DiffusionPullLogListView.php',
'DiffusionPullLogSearchEngine' => 'applications/diffusion/query/DiffusionPullLogSearchEngine.php',
'DiffusionPushCapability' => 'applications/diffusion/capability/DiffusionPushCapability.php',
'DiffusionPushEventViewController' => 'applications/diffusion/controller/DiffusionPushEventViewController.php',
'DiffusionPushLogListController' => 'applications/diffusion/controller/DiffusionPushLogListController.php',
'DiffusionPushLogListView' => 'applications/diffusion/view/DiffusionPushLogListView.php',
'DiffusionPythonExternalSymbolsSource' => 'applications/diffusion/symbol/DiffusionPythonExternalSymbolsSource.php',
'DiffusionQuery' => 'applications/diffusion/query/DiffusionQuery.php',
'DiffusionQueryCommitsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionQueryCommitsConduitAPIMethod.php',
'DiffusionQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionQueryConduitAPIMethod.php',
'DiffusionQueryPathsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionQueryPathsConduitAPIMethod.php',
'DiffusionRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionRawDiffQuery.php',
'DiffusionRawDiffQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRawDiffQueryConduitAPIMethod.php',
'DiffusionReadmeView' => 'applications/diffusion/view/DiffusionReadmeView.php',
'DiffusionRefDatasource' => 'applications/diffusion/typeahead/DiffusionRefDatasource.php',
'DiffusionRefNotFoundException' => 'applications/diffusion/exception/DiffusionRefNotFoundException.php',
'DiffusionRefTableController' => 'applications/diffusion/controller/DiffusionRefTableController.php',
'DiffusionRefsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRefsQueryConduitAPIMethod.php',
'DiffusionRenameHistoryQuery' => 'applications/diffusion/query/DiffusionRenameHistoryQuery.php',
'DiffusionRepositoryActionsManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryActionsManagementPanel.php',
'DiffusionRepositoryAutomationManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryAutomationManagementPanel.php',
'DiffusionRepositoryBasicsManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php',
'DiffusionRepositoryBranchesManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryBranchesManagementPanel.php',
'DiffusionRepositoryByIDRemarkupRule' => 'applications/diffusion/remarkup/DiffusionRepositoryByIDRemarkupRule.php',
'DiffusionRepositoryClusterEngine' => 'applications/diffusion/protocol/DiffusionRepositoryClusterEngine.php',
'DiffusionRepositoryClusterEngineLogInterface' => 'applications/diffusion/protocol/DiffusionRepositoryClusterEngineLogInterface.php',
'DiffusionRepositoryController' => 'applications/diffusion/controller/DiffusionRepositoryController.php',
'DiffusionRepositoryDatasource' => 'applications/diffusion/typeahead/DiffusionRepositoryDatasource.php',
'DiffusionRepositoryDefaultController' => 'applications/diffusion/controller/DiffusionRepositoryDefaultController.php',
'DiffusionRepositoryEditActivateController' => 'applications/diffusion/controller/DiffusionRepositoryEditActivateController.php',
'DiffusionRepositoryEditConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRepositoryEditConduitAPIMethod.php',
'DiffusionRepositoryEditController' => 'applications/diffusion/controller/DiffusionRepositoryEditController.php',
'DiffusionRepositoryEditDangerousController' => 'applications/diffusion/controller/DiffusionRepositoryEditDangerousController.php',
'DiffusionRepositoryEditDeleteController' => 'applications/diffusion/controller/DiffusionRepositoryEditDeleteController.php',
'DiffusionRepositoryEditEngine' => 'applications/diffusion/editor/DiffusionRepositoryEditEngine.php',
'DiffusionRepositoryEditEnormousController' => 'applications/diffusion/controller/DiffusionRepositoryEditEnormousController.php',
'DiffusionRepositoryEditUpdateController' => 'applications/diffusion/controller/DiffusionRepositoryEditUpdateController.php',
'DiffusionRepositoryFunctionDatasource' => 'applications/diffusion/typeahead/DiffusionRepositoryFunctionDatasource.php',
'DiffusionRepositoryHistoryManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryHistoryManagementPanel.php',
'DiffusionRepositoryIdentityEditor' => 'applications/diffusion/editor/DiffusionRepositoryIdentityEditor.php',
'DiffusionRepositoryIdentitySearchEngine' => 'applications/diffusion/query/DiffusionRepositoryIdentitySearchEngine.php',
'DiffusionRepositoryLimitsManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryLimitsManagementPanel.php',
'DiffusionRepositoryListController' => 'applications/diffusion/controller/DiffusionRepositoryListController.php',
'DiffusionRepositoryManageController' => 'applications/diffusion/controller/DiffusionRepositoryManageController.php',
'DiffusionRepositoryManagePanelsController' => 'applications/diffusion/controller/DiffusionRepositoryManagePanelsController.php',
'DiffusionRepositoryManagementBuildsPanelGroup' => 'applications/diffusion/management/DiffusionRepositoryManagementBuildsPanelGroup.php',
'DiffusionRepositoryManagementIntegrationsPanelGroup' => 'applications/diffusion/management/DiffusionRepositoryManagementIntegrationsPanelGroup.php',
'DiffusionRepositoryManagementMainPanelGroup' => 'applications/diffusion/management/DiffusionRepositoryManagementMainPanelGroup.php',
'DiffusionRepositoryManagementOtherPanelGroup' => 'applications/diffusion/management/DiffusionRepositoryManagementOtherPanelGroup.php',
'DiffusionRepositoryManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryManagementPanel.php',
'DiffusionRepositoryManagementPanelGroup' => 'applications/diffusion/management/DiffusionRepositoryManagementPanelGroup.php',
'DiffusionRepositoryPath' => 'applications/diffusion/data/DiffusionRepositoryPath.php',
'DiffusionRepositoryPoliciesManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryPoliciesManagementPanel.php',
'DiffusionRepositoryProfilePictureController' => 'applications/diffusion/controller/DiffusionRepositoryProfilePictureController.php',
'DiffusionRepositoryRef' => 'applications/diffusion/data/DiffusionRepositoryRef.php',
'DiffusionRepositoryRemarkupRule' => 'applications/diffusion/remarkup/DiffusionRepositoryRemarkupRule.php',
'DiffusionRepositorySearchConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRepositorySearchConduitAPIMethod.php',
'DiffusionRepositoryStagingManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryStagingManagementPanel.php',
'DiffusionRepositoryStorageManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryStorageManagementPanel.php',
'DiffusionRepositorySubversionManagementPanel' => 'applications/diffusion/management/DiffusionRepositorySubversionManagementPanel.php',
'DiffusionRepositorySymbolsManagementPanel' => 'applications/diffusion/management/DiffusionRepositorySymbolsManagementPanel.php',
'DiffusionRepositoryTag' => 'applications/diffusion/data/DiffusionRepositoryTag.php',
'DiffusionRepositoryTestAutomationController' => 'applications/diffusion/controller/DiffusionRepositoryTestAutomationController.php',
'DiffusionRepositoryURICredentialController' => 'applications/diffusion/controller/DiffusionRepositoryURICredentialController.php',
'DiffusionRepositoryURIDisableController' => 'applications/diffusion/controller/DiffusionRepositoryURIDisableController.php',
'DiffusionRepositoryURIEditController' => 'applications/diffusion/controller/DiffusionRepositoryURIEditController.php',
'DiffusionRepositoryURIViewController' => 'applications/diffusion/controller/DiffusionRepositoryURIViewController.php',
'DiffusionRepositoryURIsIndexEngineExtension' => 'applications/diffusion/engineextension/DiffusionRepositoryURIsIndexEngineExtension.php',
'DiffusionRepositoryURIsManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryURIsManagementPanel.php',
'DiffusionRepositoryURIsSearchEngineAttachment' => 'applications/diffusion/engineextension/DiffusionRepositoryURIsSearchEngineAttachment.php',
'DiffusionRequest' => 'applications/diffusion/request/DiffusionRequest.php',
'DiffusionResolveRefsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionResolveRefsConduitAPIMethod.php',
'DiffusionResolveUserQuery' => 'applications/diffusion/query/DiffusionResolveUserQuery.php',
'DiffusionSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSSHWorkflow.php',
'DiffusionSearchQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionSearchQueryConduitAPIMethod.php',
'DiffusionServeController' => 'applications/diffusion/controller/DiffusionServeController.php',
'DiffusionSetPasswordSettingsPanel' => 'applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php',
'DiffusionSetupException' => 'applications/diffusion/exception/DiffusionSetupException.php',
'DiffusionSubversionCommandEngine' => 'applications/diffusion/protocol/DiffusionSubversionCommandEngine.php',
'DiffusionSubversionSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSubversionSSHWorkflow.php',
'DiffusionSubversionServeSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php',
'DiffusionSubversionWireProtocol' => 'applications/diffusion/protocol/DiffusionSubversionWireProtocol.php',
'DiffusionSubversionWireProtocolTestCase' => 'applications/diffusion/protocol/__tests__/DiffusionSubversionWireProtocolTestCase.php',
'DiffusionSvnBlameQuery' => 'applications/diffusion/query/blame/DiffusionSvnBlameQuery.php',
'DiffusionSvnFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionSvnFileContentQuery.php',
'DiffusionSvnRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionSvnRawDiffQuery.php',
'DiffusionSvnRequest' => 'applications/diffusion/request/DiffusionSvnRequest.php',
'DiffusionSymbolController' => 'applications/diffusion/controller/DiffusionSymbolController.php',
'DiffusionSymbolDatasource' => 'applications/diffusion/typeahead/DiffusionSymbolDatasource.php',
'DiffusionSymbolQuery' => 'applications/diffusion/query/DiffusionSymbolQuery.php',
'DiffusionSyncLogListController' => 'applications/diffusion/controller/DiffusionSyncLogListController.php',
'DiffusionSyncLogListView' => 'applications/diffusion/view/DiffusionSyncLogListView.php',
'DiffusionSyncLogSearchEngine' => 'applications/diffusion/query/DiffusionSyncLogSearchEngine.php',
'DiffusionTagListController' => 'applications/diffusion/controller/DiffusionTagListController.php',
'DiffusionTagListView' => 'applications/diffusion/view/DiffusionTagListView.php',
'DiffusionTagTableView' => 'applications/diffusion/view/DiffusionTagTableView.php',
'DiffusionTaggedRepositoriesFunctionDatasource' => 'applications/diffusion/typeahead/DiffusionTaggedRepositoriesFunctionDatasource.php',
'DiffusionTagsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionTagsQueryConduitAPIMethod.php',
'DiffusionURIEditConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionURIEditConduitAPIMethod.php',
'DiffusionURIEditEngine' => 'applications/diffusion/editor/DiffusionURIEditEngine.php',
'DiffusionURIEditor' => 'applications/diffusion/editor/DiffusionURIEditor.php',
'DiffusionURITestCase' => 'applications/diffusion/request/__tests__/DiffusionURITestCase.php',
'DiffusionUpdateCoverageConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionUpdateCoverageConduitAPIMethod.php',
'DiffusionView' => 'applications/diffusion/view/DiffusionView.php',
'DivinerArticleAtomizer' => 'applications/diviner/atomizer/DivinerArticleAtomizer.php',
'DivinerAtom' => 'applications/diviner/atom/DivinerAtom.php',
'DivinerAtomCache' => 'applications/diviner/cache/DivinerAtomCache.php',
'DivinerAtomController' => 'applications/diviner/controller/DivinerAtomController.php',
'DivinerAtomListController' => 'applications/diviner/controller/DivinerAtomListController.php',
'DivinerAtomPHIDType' => 'applications/diviner/phid/DivinerAtomPHIDType.php',
'DivinerAtomQuery' => 'applications/diviner/query/DivinerAtomQuery.php',
'DivinerAtomRef' => 'applications/diviner/atom/DivinerAtomRef.php',
'DivinerAtomSearchEngine' => 'applications/diviner/query/DivinerAtomSearchEngine.php',
'DivinerAtomizeWorkflow' => 'applications/diviner/workflow/DivinerAtomizeWorkflow.php',
'DivinerAtomizer' => 'applications/diviner/atomizer/DivinerAtomizer.php',
'DivinerBookController' => 'applications/diviner/controller/DivinerBookController.php',
'DivinerBookDatasource' => 'applications/diviner/typeahead/DivinerBookDatasource.php',
'DivinerBookEditController' => 'applications/diviner/controller/DivinerBookEditController.php',
'DivinerBookItemView' => 'applications/diviner/view/DivinerBookItemView.php',
'DivinerBookPHIDType' => 'applications/diviner/phid/DivinerBookPHIDType.php',
'DivinerBookQuery' => 'applications/diviner/query/DivinerBookQuery.php',
'DivinerController' => 'applications/diviner/controller/DivinerController.php',
'DivinerDAO' => 'applications/diviner/storage/DivinerDAO.php',
'DivinerDefaultEditCapability' => 'applications/diviner/capability/DivinerDefaultEditCapability.php',
'DivinerDefaultRenderer' => 'applications/diviner/renderer/DivinerDefaultRenderer.php',
'DivinerDefaultViewCapability' => 'applications/diviner/capability/DivinerDefaultViewCapability.php',
'DivinerDiskCache' => 'applications/diviner/cache/DivinerDiskCache.php',
'DivinerFileAtomizer' => 'applications/diviner/atomizer/DivinerFileAtomizer.php',
'DivinerFindController' => 'applications/diviner/controller/DivinerFindController.php',
'DivinerGenerateWorkflow' => 'applications/diviner/workflow/DivinerGenerateWorkflow.php',
'DivinerLiveAtom' => 'applications/diviner/storage/DivinerLiveAtom.php',
'DivinerLiveBook' => 'applications/diviner/storage/DivinerLiveBook.php',
'DivinerLiveBookEditor' => 'applications/diviner/editor/DivinerLiveBookEditor.php',
'DivinerLiveBookFulltextEngine' => 'applications/diviner/search/DivinerLiveBookFulltextEngine.php',
'DivinerLiveBookTransaction' => 'applications/diviner/storage/DivinerLiveBookTransaction.php',
'DivinerLiveBookTransactionQuery' => 'applications/diviner/query/DivinerLiveBookTransactionQuery.php',
'DivinerLivePublisher' => 'applications/diviner/publisher/DivinerLivePublisher.php',
'DivinerLiveSymbol' => 'applications/diviner/storage/DivinerLiveSymbol.php',
'DivinerLiveSymbolFulltextEngine' => 'applications/diviner/search/DivinerLiveSymbolFulltextEngine.php',
'DivinerMainController' => 'applications/diviner/controller/DivinerMainController.php',
'DivinerPHPAtomizer' => 'applications/diviner/atomizer/DivinerPHPAtomizer.php',
'DivinerParameterTableView' => 'applications/diviner/view/DivinerParameterTableView.php',
'DivinerPublishCache' => 'applications/diviner/cache/DivinerPublishCache.php',
'DivinerPublisher' => 'applications/diviner/publisher/DivinerPublisher.php',
'DivinerRenderer' => 'applications/diviner/renderer/DivinerRenderer.php',
'DivinerReturnTableView' => 'applications/diviner/view/DivinerReturnTableView.php',
'DivinerSchemaSpec' => 'applications/diviner/storage/DivinerSchemaSpec.php',
'DivinerSectionView' => 'applications/diviner/view/DivinerSectionView.php',
'DivinerStaticPublisher' => 'applications/diviner/publisher/DivinerStaticPublisher.php',
'DivinerSymbolRemarkupRule' => 'applications/diviner/markup/DivinerSymbolRemarkupRule.php',
'DivinerWorkflow' => 'applications/diviner/workflow/DivinerWorkflow.php',
'DoorkeeperAsanaFeedWorker' => 'applications/doorkeeper/worker/DoorkeeperAsanaFeedWorker.php',
'DoorkeeperAsanaRemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperAsanaRemarkupRule.php',
'DoorkeeperBridge' => 'applications/doorkeeper/bridge/DoorkeeperBridge.php',
'DoorkeeperBridgeAsana' => 'applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php',
'DoorkeeperBridgeGitHub' => 'applications/doorkeeper/bridge/DoorkeeperBridgeGitHub.php',
'DoorkeeperBridgeGitHubIssue' => 'applications/doorkeeper/bridge/DoorkeeperBridgeGitHubIssue.php',
'DoorkeeperBridgeGitHubUser' => 'applications/doorkeeper/bridge/DoorkeeperBridgeGitHubUser.php',
'DoorkeeperBridgeJIRA' => 'applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php',
'DoorkeeperBridgeJIRATestCase' => 'applications/doorkeeper/bridge/__tests__/DoorkeeperBridgeJIRATestCase.php',
'DoorkeeperBridgedObjectCurtainExtension' => 'applications/doorkeeper/engineextension/DoorkeeperBridgedObjectCurtainExtension.php',
'DoorkeeperBridgedObjectInterface' => 'applications/doorkeeper/bridge/DoorkeeperBridgedObjectInterface.php',
'DoorkeeperDAO' => 'applications/doorkeeper/storage/DoorkeeperDAO.php',
'DoorkeeperExternalObject' => 'applications/doorkeeper/storage/DoorkeeperExternalObject.php',
'DoorkeeperExternalObjectPHIDType' => 'applications/doorkeeper/phid/DoorkeeperExternalObjectPHIDType.php',
'DoorkeeperExternalObjectQuery' => 'applications/doorkeeper/query/DoorkeeperExternalObjectQuery.php',
'DoorkeeperFeedStoryPublisher' => 'applications/doorkeeper/engine/DoorkeeperFeedStoryPublisher.php',
'DoorkeeperFeedWorker' => 'applications/doorkeeper/worker/DoorkeeperFeedWorker.php',
'DoorkeeperImportEngine' => 'applications/doorkeeper/engine/DoorkeeperImportEngine.php',
'DoorkeeperJIRAFeedWorker' => 'applications/doorkeeper/worker/DoorkeeperJIRAFeedWorker.php',
'DoorkeeperJIRARemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperJIRARemarkupRule.php',
'DoorkeeperMissingLinkException' => 'applications/doorkeeper/exception/DoorkeeperMissingLinkException.php',
'DoorkeeperObjectRef' => 'applications/doorkeeper/engine/DoorkeeperObjectRef.php',
'DoorkeeperRemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php',
'DoorkeeperSchemaSpec' => 'applications/doorkeeper/storage/DoorkeeperSchemaSpec.php',
'DoorkeeperTagView' => 'applications/doorkeeper/view/DoorkeeperTagView.php',
'DoorkeeperTagsController' => 'applications/doorkeeper/controller/DoorkeeperTagsController.php',
'DrydockAcquiredBrokenResourceException' => 'applications/drydock/exception/DrydockAcquiredBrokenResourceException.php',
'DrydockAlmanacServiceHostBlueprintImplementation' => 'applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php',
'DrydockApacheWebrootInterface' => 'applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php',
'DrydockAuthorization' => 'applications/drydock/storage/DrydockAuthorization.php',
'DrydockAuthorizationAuthorizeController' => 'applications/drydock/controller/DrydockAuthorizationAuthorizeController.php',
'DrydockAuthorizationListController' => 'applications/drydock/controller/DrydockAuthorizationListController.php',
'DrydockAuthorizationListView' => 'applications/drydock/view/DrydockAuthorizationListView.php',
'DrydockAuthorizationPHIDType' => 'applications/drydock/phid/DrydockAuthorizationPHIDType.php',
'DrydockAuthorizationQuery' => 'applications/drydock/query/DrydockAuthorizationQuery.php',
'DrydockAuthorizationSearchConduitAPIMethod' => 'applications/drydock/conduit/DrydockAuthorizationSearchConduitAPIMethod.php',
'DrydockAuthorizationSearchEngine' => 'applications/drydock/query/DrydockAuthorizationSearchEngine.php',
'DrydockAuthorizationViewController' => 'applications/drydock/controller/DrydockAuthorizationViewController.php',
'DrydockBlueprint' => 'applications/drydock/storage/DrydockBlueprint.php',
'DrydockBlueprintController' => 'applications/drydock/controller/DrydockBlueprintController.php',
'DrydockBlueprintCoreCustomField' => 'applications/drydock/customfield/DrydockBlueprintCoreCustomField.php',
'DrydockBlueprintCustomField' => 'applications/drydock/customfield/DrydockBlueprintCustomField.php',
'DrydockBlueprintDatasource' => 'applications/drydock/typeahead/DrydockBlueprintDatasource.php',
'DrydockBlueprintDisableController' => 'applications/drydock/controller/DrydockBlueprintDisableController.php',
'DrydockBlueprintDisableTransaction' => 'applications/drydock/xaction/DrydockBlueprintDisableTransaction.php',
'DrydockBlueprintEditConduitAPIMethod' => 'applications/drydock/conduit/DrydockBlueprintEditConduitAPIMethod.php',
'DrydockBlueprintEditController' => 'applications/drydock/controller/DrydockBlueprintEditController.php',
'DrydockBlueprintEditEngine' => 'applications/drydock/editor/DrydockBlueprintEditEngine.php',
'DrydockBlueprintEditor' => 'applications/drydock/editor/DrydockBlueprintEditor.php',
'DrydockBlueprintImplementation' => 'applications/drydock/blueprint/DrydockBlueprintImplementation.php',
'DrydockBlueprintImplementationTestCase' => 'applications/drydock/blueprint/__tests__/DrydockBlueprintImplementationTestCase.php',
'DrydockBlueprintListController' => 'applications/drydock/controller/DrydockBlueprintListController.php',
'DrydockBlueprintNameNgrams' => 'applications/drydock/storage/DrydockBlueprintNameNgrams.php',
'DrydockBlueprintNameTransaction' => 'applications/drydock/xaction/DrydockBlueprintNameTransaction.php',
'DrydockBlueprintPHIDType' => 'applications/drydock/phid/DrydockBlueprintPHIDType.php',
'DrydockBlueprintQuery' => 'applications/drydock/query/DrydockBlueprintQuery.php',
'DrydockBlueprintSearchConduitAPIMethod' => 'applications/drydock/conduit/DrydockBlueprintSearchConduitAPIMethod.php',
'DrydockBlueprintSearchEngine' => 'applications/drydock/query/DrydockBlueprintSearchEngine.php',
'DrydockBlueprintTransaction' => 'applications/drydock/storage/DrydockBlueprintTransaction.php',
'DrydockBlueprintTransactionQuery' => 'applications/drydock/query/DrydockBlueprintTransactionQuery.php',
'DrydockBlueprintTransactionType' => 'applications/drydock/xaction/DrydockBlueprintTransactionType.php',
'DrydockBlueprintTypeTransaction' => 'applications/drydock/xaction/DrydockBlueprintTypeTransaction.php',
'DrydockBlueprintViewController' => 'applications/drydock/controller/DrydockBlueprintViewController.php',
'DrydockCommand' => 'applications/drydock/storage/DrydockCommand.php',
'DrydockCommandError' => 'applications/drydock/exception/DrydockCommandError.php',
'DrydockCommandInterface' => 'applications/drydock/interface/command/DrydockCommandInterface.php',
'DrydockCommandQuery' => 'applications/drydock/query/DrydockCommandQuery.php',
'DrydockConsoleController' => 'applications/drydock/controller/DrydockConsoleController.php',
'DrydockController' => 'applications/drydock/controller/DrydockController.php',
'DrydockCreateBlueprintsCapability' => 'applications/drydock/capability/DrydockCreateBlueprintsCapability.php',
'DrydockDAO' => 'applications/drydock/storage/DrydockDAO.php',
'DrydockDefaultEditCapability' => 'applications/drydock/capability/DrydockDefaultEditCapability.php',
'DrydockDefaultViewCapability' => 'applications/drydock/capability/DrydockDefaultViewCapability.php',
'DrydockFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockFilesystemInterface.php',
'DrydockInterface' => 'applications/drydock/interface/DrydockInterface.php',
'DrydockLandRepositoryOperation' => 'applications/drydock/operation/DrydockLandRepositoryOperation.php',
'DrydockLease' => 'applications/drydock/storage/DrydockLease.php',
'DrydockLeaseAcquiredLogType' => 'applications/drydock/logtype/DrydockLeaseAcquiredLogType.php',
'DrydockLeaseActivatedLogType' => 'applications/drydock/logtype/DrydockLeaseActivatedLogType.php',
'DrydockLeaseActivationFailureLogType' => 'applications/drydock/logtype/DrydockLeaseActivationFailureLogType.php',
'DrydockLeaseActivationYieldLogType' => 'applications/drydock/logtype/DrydockLeaseActivationYieldLogType.php',
'DrydockLeaseAllocationFailureLogType' => 'applications/drydock/logtype/DrydockLeaseAllocationFailureLogType.php',
'DrydockLeaseController' => 'applications/drydock/controller/DrydockLeaseController.php',
'DrydockLeaseDatasource' => 'applications/drydock/typeahead/DrydockLeaseDatasource.php',
'DrydockLeaseDestroyedLogType' => 'applications/drydock/logtype/DrydockLeaseDestroyedLogType.php',
'DrydockLeaseListController' => 'applications/drydock/controller/DrydockLeaseListController.php',
'DrydockLeaseListView' => 'applications/drydock/view/DrydockLeaseListView.php',
'DrydockLeaseNoAuthorizationsLogType' => 'applications/drydock/logtype/DrydockLeaseNoAuthorizationsLogType.php',
'DrydockLeaseNoBlueprintsLogType' => 'applications/drydock/logtype/DrydockLeaseNoBlueprintsLogType.php',
'DrydockLeasePHIDType' => 'applications/drydock/phid/DrydockLeasePHIDType.php',
'DrydockLeaseQuery' => 'applications/drydock/query/DrydockLeaseQuery.php',
'DrydockLeaseQueuedLogType' => 'applications/drydock/logtype/DrydockLeaseQueuedLogType.php',
'DrydockLeaseReacquireLogType' => 'applications/drydock/logtype/DrydockLeaseReacquireLogType.php',
'DrydockLeaseReclaimLogType' => 'applications/drydock/logtype/DrydockLeaseReclaimLogType.php',
'DrydockLeaseReleaseController' => 'applications/drydock/controller/DrydockLeaseReleaseController.php',
'DrydockLeaseReleasedLogType' => 'applications/drydock/logtype/DrydockLeaseReleasedLogType.php',
'DrydockLeaseSearchConduitAPIMethod' => 'applications/drydock/conduit/DrydockLeaseSearchConduitAPIMethod.php',
'DrydockLeaseSearchEngine' => 'applications/drydock/query/DrydockLeaseSearchEngine.php',
'DrydockLeaseStatus' => 'applications/drydock/constants/DrydockLeaseStatus.php',
'DrydockLeaseUpdateWorker' => 'applications/drydock/worker/DrydockLeaseUpdateWorker.php',
'DrydockLeaseViewController' => 'applications/drydock/controller/DrydockLeaseViewController.php',
'DrydockLeaseWaitingForResourcesLogType' => 'applications/drydock/logtype/DrydockLeaseWaitingForResourcesLogType.php',
'DrydockLog' => 'applications/drydock/storage/DrydockLog.php',
'DrydockLogController' => 'applications/drydock/controller/DrydockLogController.php',
'DrydockLogGarbageCollector' => 'applications/drydock/garbagecollector/DrydockLogGarbageCollector.php',
'DrydockLogListController' => 'applications/drydock/controller/DrydockLogListController.php',
'DrydockLogListView' => 'applications/drydock/view/DrydockLogListView.php',
'DrydockLogQuery' => 'applications/drydock/query/DrydockLogQuery.php',
'DrydockLogSearchEngine' => 'applications/drydock/query/DrydockLogSearchEngine.php',
'DrydockLogType' => 'applications/drydock/logtype/DrydockLogType.php',
'DrydockManagementCommandWorkflow' => 'applications/drydock/management/DrydockManagementCommandWorkflow.php',
'DrydockManagementLeaseWorkflow' => 'applications/drydock/management/DrydockManagementLeaseWorkflow.php',
'DrydockManagementReclaimWorkflow' => 'applications/drydock/management/DrydockManagementReclaimWorkflow.php',
'DrydockManagementReleaseLeaseWorkflow' => 'applications/drydock/management/DrydockManagementReleaseLeaseWorkflow.php',
'DrydockManagementReleaseResourceWorkflow' => 'applications/drydock/management/DrydockManagementReleaseResourceWorkflow.php',
'DrydockManagementUpdateLeaseWorkflow' => 'applications/drydock/management/DrydockManagementUpdateLeaseWorkflow.php',
'DrydockManagementUpdateResourceWorkflow' => 'applications/drydock/management/DrydockManagementUpdateResourceWorkflow.php',
'DrydockManagementWorkflow' => 'applications/drydock/management/DrydockManagementWorkflow.php',
'DrydockObjectAuthorizationView' => 'applications/drydock/view/DrydockObjectAuthorizationView.php',
'DrydockOperationWorkLogType' => 'applications/drydock/logtype/DrydockOperationWorkLogType.php',
'DrydockQuery' => 'applications/drydock/query/DrydockQuery.php',
'DrydockRepositoryOperation' => 'applications/drydock/storage/DrydockRepositoryOperation.php',
'DrydockRepositoryOperationController' => 'applications/drydock/controller/DrydockRepositoryOperationController.php',
'DrydockRepositoryOperationDismissController' => 'applications/drydock/controller/DrydockRepositoryOperationDismissController.php',
'DrydockRepositoryOperationListController' => 'applications/drydock/controller/DrydockRepositoryOperationListController.php',
'DrydockRepositoryOperationPHIDType' => 'applications/drydock/phid/DrydockRepositoryOperationPHIDType.php',
'DrydockRepositoryOperationQuery' => 'applications/drydock/query/DrydockRepositoryOperationQuery.php',
'DrydockRepositoryOperationSearchEngine' => 'applications/drydock/query/DrydockRepositoryOperationSearchEngine.php',
'DrydockRepositoryOperationStatusController' => 'applications/drydock/controller/DrydockRepositoryOperationStatusController.php',
'DrydockRepositoryOperationStatusView' => 'applications/drydock/view/DrydockRepositoryOperationStatusView.php',
'DrydockRepositoryOperationType' => 'applications/drydock/operation/DrydockRepositoryOperationType.php',
'DrydockRepositoryOperationUpdateWorker' => 'applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php',
'DrydockRepositoryOperationViewController' => 'applications/drydock/controller/DrydockRepositoryOperationViewController.php',
'DrydockResource' => 'applications/drydock/storage/DrydockResource.php',
'DrydockResourceActivationFailureLogType' => 'applications/drydock/logtype/DrydockResourceActivationFailureLogType.php',
'DrydockResourceActivationYieldLogType' => 'applications/drydock/logtype/DrydockResourceActivationYieldLogType.php',
'DrydockResourceAllocationFailureLogType' => 'applications/drydock/logtype/DrydockResourceAllocationFailureLogType.php',
'DrydockResourceController' => 'applications/drydock/controller/DrydockResourceController.php',
'DrydockResourceDatasource' => 'applications/drydock/typeahead/DrydockResourceDatasource.php',
'DrydockResourceListController' => 'applications/drydock/controller/DrydockResourceListController.php',
'DrydockResourceListView' => 'applications/drydock/view/DrydockResourceListView.php',
'DrydockResourceLockException' => 'applications/drydock/exception/DrydockResourceLockException.php',
'DrydockResourcePHIDType' => 'applications/drydock/phid/DrydockResourcePHIDType.php',
'DrydockResourceQuery' => 'applications/drydock/query/DrydockResourceQuery.php',
'DrydockResourceReclaimLogType' => 'applications/drydock/logtype/DrydockResourceReclaimLogType.php',
'DrydockResourceReleaseController' => 'applications/drydock/controller/DrydockResourceReleaseController.php',
'DrydockResourceSearchEngine' => 'applications/drydock/query/DrydockResourceSearchEngine.php',
'DrydockResourceStatus' => 'applications/drydock/constants/DrydockResourceStatus.php',
'DrydockResourceUpdateWorker' => 'applications/drydock/worker/DrydockResourceUpdateWorker.php',
'DrydockResourceViewController' => 'applications/drydock/controller/DrydockResourceViewController.php',
'DrydockSFTPFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockSFTPFilesystemInterface.php',
'DrydockSSHCommandInterface' => 'applications/drydock/interface/command/DrydockSSHCommandInterface.php',
'DrydockSchemaSpec' => 'applications/drydock/storage/DrydockSchemaSpec.php',
'DrydockSlotLock' => 'applications/drydock/storage/DrydockSlotLock.php',
'DrydockSlotLockException' => 'applications/drydock/exception/DrydockSlotLockException.php',
'DrydockSlotLockFailureLogType' => 'applications/drydock/logtype/DrydockSlotLockFailureLogType.php',
'DrydockTestRepositoryOperation' => 'applications/drydock/operation/DrydockTestRepositoryOperation.php',
'DrydockTextLogType' => 'applications/drydock/logtype/DrydockTextLogType.php',
'DrydockWebrootInterface' => 'applications/drydock/interface/webroot/DrydockWebrootInterface.php',
'DrydockWorker' => 'applications/drydock/worker/DrydockWorker.php',
'DrydockWorkingCopyBlueprintImplementation' => 'applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php',
'EdgeSearchConduitAPIMethod' => 'infrastructure/edges/conduit/EdgeSearchConduitAPIMethod.php',
'FeedConduitAPIMethod' => 'applications/feed/conduit/FeedConduitAPIMethod.php',
'FeedPublishConduitAPIMethod' => 'applications/feed/conduit/FeedPublishConduitAPIMethod.php',
'FeedPublisherHTTPWorker' => 'applications/feed/worker/FeedPublisherHTTPWorker.php',
'FeedPublisherWorker' => 'applications/feed/worker/FeedPublisherWorker.php',
'FeedPushWorker' => 'applications/feed/worker/FeedPushWorker.php',
'FeedQueryConduitAPIMethod' => 'applications/feed/conduit/FeedQueryConduitAPIMethod.php',
'FeedStoryNotificationGarbageCollector' => 'applications/notification/garbagecollector/FeedStoryNotificationGarbageCollector.php',
'FileAllocateConduitAPIMethod' => 'applications/files/conduit/FileAllocateConduitAPIMethod.php',
'FileConduitAPIMethod' => 'applications/files/conduit/FileConduitAPIMethod.php',
'FileCreateMailReceiver' => 'applications/files/mail/FileCreateMailReceiver.php',
'FileDeletionWorker' => 'applications/files/worker/FileDeletionWorker.php',
'FileDownloadConduitAPIMethod' => 'applications/files/conduit/FileDownloadConduitAPIMethod.php',
'FileInfoConduitAPIMethod' => 'applications/files/conduit/FileInfoConduitAPIMethod.php',
'FileMailReceiver' => 'applications/files/mail/FileMailReceiver.php',
'FileQueryChunksConduitAPIMethod' => 'applications/files/conduit/FileQueryChunksConduitAPIMethod.php',
'FileReplyHandler' => 'applications/files/mail/FileReplyHandler.php',
'FileTypeIcon' => 'applications/files/constants/FileTypeIcon.php',
'FileUploadChunkConduitAPIMethod' => 'applications/files/conduit/FileUploadChunkConduitAPIMethod.php',
'FileUploadConduitAPIMethod' => 'applications/files/conduit/FileUploadConduitAPIMethod.php',
'FileUploadHashConduitAPIMethod' => 'applications/files/conduit/FileUploadHashConduitAPIMethod.php',
'FilesDefaultViewCapability' => 'applications/files/capability/FilesDefaultViewCapability.php',
'FlagConduitAPIMethod' => 'applications/flag/conduit/FlagConduitAPIMethod.php',
'FlagDeleteConduitAPIMethod' => 'applications/flag/conduit/FlagDeleteConduitAPIMethod.php',
'FlagEditConduitAPIMethod' => 'applications/flag/conduit/FlagEditConduitAPIMethod.php',
'FlagQueryConduitAPIMethod' => 'applications/flag/conduit/FlagQueryConduitAPIMethod.php',
'FundBacker' => 'applications/fund/storage/FundBacker.php',
'FundBackerCart' => 'applications/fund/phortune/FundBackerCart.php',
'FundBackerEditor' => 'applications/fund/editor/FundBackerEditor.php',
'FundBackerListController' => 'applications/fund/controller/FundBackerListController.php',
'FundBackerPHIDType' => 'applications/fund/phid/FundBackerPHIDType.php',
'FundBackerProduct' => 'applications/fund/phortune/FundBackerProduct.php',
'FundBackerQuery' => 'applications/fund/query/FundBackerQuery.php',
'FundBackerRefundTransaction' => 'applications/fund/xaction/FundBackerRefundTransaction.php',
'FundBackerSearchEngine' => 'applications/fund/query/FundBackerSearchEngine.php',
'FundBackerStatusTransaction' => 'applications/fund/xaction/FundBackerStatusTransaction.php',
'FundBackerTransaction' => 'applications/fund/storage/FundBackerTransaction.php',
'FundBackerTransactionQuery' => 'applications/fund/query/FundBackerTransactionQuery.php',
'FundBackerTransactionType' => 'applications/fund/xaction/FundBackerTransactionType.php',
'FundController' => 'applications/fund/controller/FundController.php',
'FundCreateInitiativesCapability' => 'applications/fund/capability/FundCreateInitiativesCapability.php',
'FundDAO' => 'applications/fund/storage/FundDAO.php',
'FundDefaultViewCapability' => 'applications/fund/capability/FundDefaultViewCapability.php',
'FundInitiative' => 'applications/fund/storage/FundInitiative.php',
'FundInitiativeBackController' => 'applications/fund/controller/FundInitiativeBackController.php',
'FundInitiativeBackerTransaction' => 'applications/fund/xaction/FundInitiativeBackerTransaction.php',
'FundInitiativeCloseController' => 'applications/fund/controller/FundInitiativeCloseController.php',
'FundInitiativeDescriptionTransaction' => 'applications/fund/xaction/FundInitiativeDescriptionTransaction.php',
'FundInitiativeEditController' => 'applications/fund/controller/FundInitiativeEditController.php',
'FundInitiativeEditEngine' => 'applications/fund/editor/FundInitiativeEditEngine.php',
'FundInitiativeEditor' => 'applications/fund/editor/FundInitiativeEditor.php',
'FundInitiativeFerretEngine' => 'applications/fund/search/FundInitiativeFerretEngine.php',
'FundInitiativeFulltextEngine' => 'applications/fund/search/FundInitiativeFulltextEngine.php',
'FundInitiativeListController' => 'applications/fund/controller/FundInitiativeListController.php',
'FundInitiativeMerchantTransaction' => 'applications/fund/xaction/FundInitiativeMerchantTransaction.php',
'FundInitiativeNameTransaction' => 'applications/fund/xaction/FundInitiativeNameTransaction.php',
'FundInitiativePHIDType' => 'applications/fund/phid/FundInitiativePHIDType.php',
'FundInitiativeQuery' => 'applications/fund/query/FundInitiativeQuery.php',
'FundInitiativeRefundTransaction' => 'applications/fund/xaction/FundInitiativeRefundTransaction.php',
'FundInitiativeRemarkupRule' => 'applications/fund/remarkup/FundInitiativeRemarkupRule.php',
'FundInitiativeReplyHandler' => 'applications/fund/mail/FundInitiativeReplyHandler.php',
'FundInitiativeRisksTransaction' => 'applications/fund/xaction/FundInitiativeRisksTransaction.php',
'FundInitiativeSearchEngine' => 'applications/fund/query/FundInitiativeSearchEngine.php',
'FundInitiativeStatusTransaction' => 'applications/fund/xaction/FundInitiativeStatusTransaction.php',
'FundInitiativeTransaction' => 'applications/fund/storage/FundInitiativeTransaction.php',
'FundInitiativeTransactionComment' => 'applications/fund/storage/FundInitiativeTransactionComment.php',
'FundInitiativeTransactionQuery' => 'applications/fund/query/FundInitiativeTransactionQuery.php',
'FundInitiativeTransactionType' => 'applications/fund/xaction/FundInitiativeTransactionType.php',
'FundInitiativeViewController' => 'applications/fund/controller/FundInitiativeViewController.php',
'FundSchemaSpec' => 'applications/fund/storage/FundSchemaSpec.php',
'HarbormasterAbortOlderBuildsBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterAbortOlderBuildsBuildStepImplementation.php',
'HarbormasterArcLintBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterArcLintBuildStepImplementation.php',
'HarbormasterArcUnitBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterArcUnitBuildStepImplementation.php',
'HarbormasterArtifact' => 'applications/harbormaster/artifact/HarbormasterArtifact.php',
'HarbormasterAutotargetsTestCase' => 'applications/harbormaster/__tests__/HarbormasterAutotargetsTestCase.php',
'HarbormasterBuild' => 'applications/harbormaster/storage/build/HarbormasterBuild.php',
'HarbormasterBuildAbortedException' => 'applications/harbormaster/exception/HarbormasterBuildAbortedException.php',
'HarbormasterBuildActionController' => 'applications/harbormaster/controller/HarbormasterBuildActionController.php',
'HarbormasterBuildArcanistAutoplan' => 'applications/harbormaster/autoplan/HarbormasterBuildArcanistAutoplan.php',
'HarbormasterBuildArtifact' => 'applications/harbormaster/storage/build/HarbormasterBuildArtifact.php',
'HarbormasterBuildArtifactPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildArtifactPHIDType.php',
'HarbormasterBuildArtifactQuery' => 'applications/harbormaster/query/HarbormasterBuildArtifactQuery.php',
'HarbormasterBuildAutoplan' => 'applications/harbormaster/autoplan/HarbormasterBuildAutoplan.php',
'HarbormasterBuildCommand' => 'applications/harbormaster/storage/HarbormasterBuildCommand.php',
'HarbormasterBuildDependencyDatasource' => 'applications/harbormaster/typeahead/HarbormasterBuildDependencyDatasource.php',
'HarbormasterBuildEngine' => 'applications/harbormaster/engine/HarbormasterBuildEngine.php',
'HarbormasterBuildFailureException' => 'applications/harbormaster/exception/HarbormasterBuildFailureException.php',
'HarbormasterBuildGraph' => 'applications/harbormaster/engine/HarbormasterBuildGraph.php',
'HarbormasterBuildInitiatorDatasource' => 'applications/harbormaster/typeahead/HarbormasterBuildInitiatorDatasource.php',
'HarbormasterBuildLintMessage' => 'applications/harbormaster/storage/build/HarbormasterBuildLintMessage.php',
'HarbormasterBuildListController' => 'applications/harbormaster/controller/HarbormasterBuildListController.php',
'HarbormasterBuildLog' => 'applications/harbormaster/storage/build/HarbormasterBuildLog.php',
'HarbormasterBuildLogChunk' => 'applications/harbormaster/storage/build/HarbormasterBuildLogChunk.php',
'HarbormasterBuildLogChunkIterator' => 'applications/harbormaster/storage/build/HarbormasterBuildLogChunkIterator.php',
'HarbormasterBuildLogDownloadController' => 'applications/harbormaster/controller/HarbormasterBuildLogDownloadController.php',
'HarbormasterBuildLogPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildLogPHIDType.php',
'HarbormasterBuildLogQuery' => 'applications/harbormaster/query/HarbormasterBuildLogQuery.php',
'HarbormasterBuildLogRenderController' => 'applications/harbormaster/controller/HarbormasterBuildLogRenderController.php',
'HarbormasterBuildLogSearchConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterBuildLogSearchConduitAPIMethod.php',
'HarbormasterBuildLogSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildLogSearchEngine.php',
'HarbormasterBuildLogTestCase' => 'applications/harbormaster/__tests__/HarbormasterBuildLogTestCase.php',
'HarbormasterBuildLogView' => 'applications/harbormaster/view/HarbormasterBuildLogView.php',
'HarbormasterBuildLogViewController' => 'applications/harbormaster/controller/HarbormasterBuildLogViewController.php',
'HarbormasterBuildMessage' => 'applications/harbormaster/storage/HarbormasterBuildMessage.php',
'HarbormasterBuildMessageQuery' => 'applications/harbormaster/query/HarbormasterBuildMessageQuery.php',
'HarbormasterBuildPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildPHIDType.php',
'HarbormasterBuildPlan' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php',
'HarbormasterBuildPlanDatasource' => 'applications/harbormaster/typeahead/HarbormasterBuildPlanDatasource.php',
'HarbormasterBuildPlanDefaultEditCapability' => 'applications/harbormaster/capability/HarbormasterBuildPlanDefaultEditCapability.php',
'HarbormasterBuildPlanDefaultViewCapability' => 'applications/harbormaster/capability/HarbormasterBuildPlanDefaultViewCapability.php',
'HarbormasterBuildPlanEditEngine' => 'applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php',
'HarbormasterBuildPlanEditor' => 'applications/harbormaster/editor/HarbormasterBuildPlanEditor.php',
'HarbormasterBuildPlanNameNgrams' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlanNameNgrams.php',
'HarbormasterBuildPlanPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildPlanPHIDType.php',
'HarbormasterBuildPlanQuery' => 'applications/harbormaster/query/HarbormasterBuildPlanQuery.php',
'HarbormasterBuildPlanSearchAPIMethod' => 'applications/harbormaster/conduit/HarbormasterBuildPlanSearchAPIMethod.php',
'HarbormasterBuildPlanSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildPlanSearchEngine.php',
'HarbormasterBuildPlanTransaction' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlanTransaction.php',
'HarbormasterBuildPlanTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildPlanTransactionQuery.php',
'HarbormasterBuildQuery' => 'applications/harbormaster/query/HarbormasterBuildQuery.php',
'HarbormasterBuildRequest' => 'applications/harbormaster/engine/HarbormasterBuildRequest.php',
'HarbormasterBuildSearchConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterBuildSearchConduitAPIMethod.php',
'HarbormasterBuildSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildSearchEngine.php',
'HarbormasterBuildStatus' => 'applications/harbormaster/constants/HarbormasterBuildStatus.php',
'HarbormasterBuildStatusDatasource' => 'applications/harbormaster/typeahead/HarbormasterBuildStatusDatasource.php',
'HarbormasterBuildStep' => 'applications/harbormaster/storage/configuration/HarbormasterBuildStep.php',
'HarbormasterBuildStepCoreCustomField' => 'applications/harbormaster/customfield/HarbormasterBuildStepCoreCustomField.php',
'HarbormasterBuildStepCustomField' => 'applications/harbormaster/customfield/HarbormasterBuildStepCustomField.php',
'HarbormasterBuildStepEditor' => 'applications/harbormaster/editor/HarbormasterBuildStepEditor.php',
'HarbormasterBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterBuildStepGroup.php',
'HarbormasterBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterBuildStepImplementation.php',
'HarbormasterBuildStepImplementationTestCase' => 'applications/harbormaster/step/__tests__/HarbormasterBuildStepImplementationTestCase.php',
'HarbormasterBuildStepPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildStepPHIDType.php',
'HarbormasterBuildStepQuery' => 'applications/harbormaster/query/HarbormasterBuildStepQuery.php',
'HarbormasterBuildStepTransaction' => 'applications/harbormaster/storage/configuration/HarbormasterBuildStepTransaction.php',
'HarbormasterBuildStepTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildStepTransactionQuery.php',
'HarbormasterBuildTarget' => 'applications/harbormaster/storage/build/HarbormasterBuildTarget.php',
'HarbormasterBuildTargetPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildTargetPHIDType.php',
'HarbormasterBuildTargetQuery' => 'applications/harbormaster/query/HarbormasterBuildTargetQuery.php',
'HarbormasterBuildTargetSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildTargetSearchEngine.php',
'HarbormasterBuildTransaction' => 'applications/harbormaster/storage/HarbormasterBuildTransaction.php',
'HarbormasterBuildTransactionEditor' => 'applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php',
'HarbormasterBuildTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildTransactionQuery.php',
'HarbormasterBuildUnitMessage' => 'applications/harbormaster/storage/build/HarbormasterBuildUnitMessage.php',
'HarbormasterBuildViewController' => 'applications/harbormaster/controller/HarbormasterBuildViewController.php',
'HarbormasterBuildWorker' => 'applications/harbormaster/worker/HarbormasterBuildWorker.php',
'HarbormasterBuildable' => 'applications/harbormaster/storage/HarbormasterBuildable.php',
'HarbormasterBuildableActionController' => 'applications/harbormaster/controller/HarbormasterBuildableActionController.php',
'HarbormasterBuildableAdapterInterface' => 'applications/harbormaster/herald/HarbormasterBuildableAdapterInterface.php',
'HarbormasterBuildableEngine' => 'applications/harbormaster/engine/HarbormasterBuildableEngine.php',
'HarbormasterBuildableInterface' => 'applications/harbormaster/interface/HarbormasterBuildableInterface.php',
'HarbormasterBuildableListController' => 'applications/harbormaster/controller/HarbormasterBuildableListController.php',
'HarbormasterBuildablePHIDType' => 'applications/harbormaster/phid/HarbormasterBuildablePHIDType.php',
'HarbormasterBuildableQuery' => 'applications/harbormaster/query/HarbormasterBuildableQuery.php',
'HarbormasterBuildableSearchAPIMethod' => 'applications/harbormaster/conduit/HarbormasterBuildableSearchAPIMethod.php',
'HarbormasterBuildableSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildableSearchEngine.php',
'HarbormasterBuildableStatus' => 'applications/harbormaster/constants/HarbormasterBuildableStatus.php',
'HarbormasterBuildableTransaction' => 'applications/harbormaster/storage/HarbormasterBuildableTransaction.php',
'HarbormasterBuildableTransactionEditor' => 'applications/harbormaster/editor/HarbormasterBuildableTransactionEditor.php',
'HarbormasterBuildableTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildableTransactionQuery.php',
'HarbormasterBuildableViewController' => 'applications/harbormaster/controller/HarbormasterBuildableViewController.php',
'HarbormasterBuildkiteBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterBuildkiteBuildStepImplementation.php',
'HarbormasterBuildkiteBuildableInterface' => 'applications/harbormaster/interface/HarbormasterBuildkiteBuildableInterface.php',
'HarbormasterBuildkiteHookController' => 'applications/harbormaster/controller/HarbormasterBuildkiteHookController.php',
'HarbormasterBuiltinBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterBuiltinBuildStepGroup.php',
'HarbormasterCircleCIBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterCircleCIBuildStepImplementation.php',
'HarbormasterCircleCIBuildableInterface' => 'applications/harbormaster/interface/HarbormasterCircleCIBuildableInterface.php',
'HarbormasterCircleCIHookController' => 'applications/harbormaster/controller/HarbormasterCircleCIHookController.php',
'HarbormasterConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterConduitAPIMethod.php',
'HarbormasterControlBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterControlBuildStepGroup.php',
'HarbormasterController' => 'applications/harbormaster/controller/HarbormasterController.php',
'HarbormasterCreateArtifactConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterCreateArtifactConduitAPIMethod.php',
'HarbormasterCreatePlansCapability' => 'applications/harbormaster/capability/HarbormasterCreatePlansCapability.php',
'HarbormasterDAO' => 'applications/harbormaster/storage/HarbormasterDAO.php',
'HarbormasterDrydockBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterDrydockBuildStepGroup.php',
'HarbormasterDrydockCommandBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterDrydockCommandBuildStepImplementation.php',
'HarbormasterDrydockLeaseArtifact' => 'applications/harbormaster/artifact/HarbormasterDrydockLeaseArtifact.php',
'HarbormasterExecFuture' => 'applications/harbormaster/future/HarbormasterExecFuture.php',
'HarbormasterExternalBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterExternalBuildStepGroup.php',
'HarbormasterFileArtifact' => 'applications/harbormaster/artifact/HarbormasterFileArtifact.php',
'HarbormasterHTTPRequestBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php',
'HarbormasterHostArtifact' => 'applications/harbormaster/artifact/HarbormasterHostArtifact.php',
'HarbormasterLeaseWorkingCopyBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterLeaseWorkingCopyBuildStepImplementation.php',
'HarbormasterLintMessagesController' => 'applications/harbormaster/controller/HarbormasterLintMessagesController.php',
'HarbormasterLintPropertyView' => 'applications/harbormaster/view/HarbormasterLintPropertyView.php',
'HarbormasterLogWorker' => 'applications/harbormaster/worker/HarbormasterLogWorker.php',
'HarbormasterManagementArchiveLogsWorkflow' => 'applications/harbormaster/management/HarbormasterManagementArchiveLogsWorkflow.php',
'HarbormasterManagementBuildWorkflow' => 'applications/harbormaster/management/HarbormasterManagementBuildWorkflow.php',
'HarbormasterManagementPublishWorkflow' => 'applications/harbormaster/management/HarbormasterManagementPublishWorkflow.php',
'HarbormasterManagementRebuildLogWorkflow' => 'applications/harbormaster/management/HarbormasterManagementRebuildLogWorkflow.php',
'HarbormasterManagementRestartWorkflow' => 'applications/harbormaster/management/HarbormasterManagementRestartWorkflow.php',
'HarbormasterManagementUpdateWorkflow' => 'applications/harbormaster/management/HarbormasterManagementUpdateWorkflow.php',
'HarbormasterManagementWorkflow' => 'applications/harbormaster/management/HarbormasterManagementWorkflow.php',
'HarbormasterManagementWriteLogWorkflow' => 'applications/harbormaster/management/HarbormasterManagementWriteLogWorkflow.php',
'HarbormasterMessageType' => 'applications/harbormaster/engine/HarbormasterMessageType.php',
'HarbormasterObject' => 'applications/harbormaster/storage/HarbormasterObject.php',
'HarbormasterOtherBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterOtherBuildStepGroup.php',
'HarbormasterPlanController' => 'applications/harbormaster/controller/HarbormasterPlanController.php',
'HarbormasterPlanDisableController' => 'applications/harbormaster/controller/HarbormasterPlanDisableController.php',
'HarbormasterPlanEditController' => 'applications/harbormaster/controller/HarbormasterPlanEditController.php',
'HarbormasterPlanListController' => 'applications/harbormaster/controller/HarbormasterPlanListController.php',
'HarbormasterPlanRunController' => 'applications/harbormaster/controller/HarbormasterPlanRunController.php',
'HarbormasterPlanViewController' => 'applications/harbormaster/controller/HarbormasterPlanViewController.php',
'HarbormasterPrototypeBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterPrototypeBuildStepGroup.php',
'HarbormasterPublishFragmentBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterPublishFragmentBuildStepImplementation.php',
'HarbormasterQueryAutotargetsConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterQueryAutotargetsConduitAPIMethod.php',
'HarbormasterQueryBuildablesConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterQueryBuildablesConduitAPIMethod.php',
'HarbormasterQueryBuildsConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterQueryBuildsConduitAPIMethod.php',
'HarbormasterQueryBuildsSearchEngineAttachment' => 'applications/harbormaster/engineextension/HarbormasterQueryBuildsSearchEngineAttachment.php',
'HarbormasterRemarkupRule' => 'applications/harbormaster/remarkup/HarbormasterRemarkupRule.php',
'HarbormasterRunBuildPlansHeraldAction' => 'applications/harbormaster/herald/HarbormasterRunBuildPlansHeraldAction.php',
'HarbormasterSchemaSpec' => 'applications/harbormaster/storage/HarbormasterSchemaSpec.php',
'HarbormasterScratchTable' => 'applications/harbormaster/storage/HarbormasterScratchTable.php',
'HarbormasterSendMessageConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterSendMessageConduitAPIMethod.php',
'HarbormasterSleepBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterSleepBuildStepImplementation.php',
'HarbormasterStepAddController' => 'applications/harbormaster/controller/HarbormasterStepAddController.php',
'HarbormasterStepDeleteController' => 'applications/harbormaster/controller/HarbormasterStepDeleteController.php',
'HarbormasterStepEditController' => 'applications/harbormaster/controller/HarbormasterStepEditController.php',
'HarbormasterStepViewController' => 'applications/harbormaster/controller/HarbormasterStepViewController.php',
'HarbormasterTargetEngine' => 'applications/harbormaster/engine/HarbormasterTargetEngine.php',
'HarbormasterTargetSearchAPIMethod' => 'applications/harbormaster/conduit/HarbormasterTargetSearchAPIMethod.php',
'HarbormasterTargetWorker' => 'applications/harbormaster/worker/HarbormasterTargetWorker.php',
'HarbormasterTestBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterTestBuildStepGroup.php',
'HarbormasterThrowExceptionBuildStep' => 'applications/harbormaster/step/HarbormasterThrowExceptionBuildStep.php',
'HarbormasterUIEventListener' => 'applications/harbormaster/event/HarbormasterUIEventListener.php',
'HarbormasterURIArtifact' => 'applications/harbormaster/artifact/HarbormasterURIArtifact.php',
'HarbormasterUnitMessageListController' => 'applications/harbormaster/controller/HarbormasterUnitMessageListController.php',
'HarbormasterUnitMessageViewController' => 'applications/harbormaster/controller/HarbormasterUnitMessageViewController.php',
'HarbormasterUnitPropertyView' => 'applications/harbormaster/view/HarbormasterUnitPropertyView.php',
'HarbormasterUnitStatus' => 'applications/harbormaster/constants/HarbormasterUnitStatus.php',
'HarbormasterUnitSummaryView' => 'applications/harbormaster/view/HarbormasterUnitSummaryView.php',
'HarbormasterUploadArtifactBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterUploadArtifactBuildStepImplementation.php',
'HarbormasterWaitForPreviousBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterWaitForPreviousBuildStepImplementation.php',
'HarbormasterWorker' => 'applications/harbormaster/worker/HarbormasterWorker.php',
'HarbormasterWorkingCopyArtifact' => 'applications/harbormaster/artifact/HarbormasterWorkingCopyArtifact.php',
'HeraldActingUserField' => 'applications/herald/field/HeraldActingUserField.php',
'HeraldAction' => 'applications/herald/action/HeraldAction.php',
'HeraldActionGroup' => 'applications/herald/action/HeraldActionGroup.php',
'HeraldActionRecord' => 'applications/herald/storage/HeraldActionRecord.php',
'HeraldAdapter' => 'applications/herald/adapter/HeraldAdapter.php',
'HeraldAdapterDatasource' => 'applications/herald/typeahead/HeraldAdapterDatasource.php',
'HeraldAlwaysField' => 'applications/herald/field/HeraldAlwaysField.php',
'HeraldAnotherRuleField' => 'applications/herald/field/HeraldAnotherRuleField.php',
'HeraldApplicationActionGroup' => 'applications/herald/action/HeraldApplicationActionGroup.php',
'HeraldApplyTranscript' => 'applications/herald/storage/transcript/HeraldApplyTranscript.php',
'HeraldBasicFieldGroup' => 'applications/herald/field/HeraldBasicFieldGroup.php',
'HeraldBuildableState' => 'applications/herald/state/HeraldBuildableState.php',
'HeraldCallWebhookAction' => 'applications/herald/action/HeraldCallWebhookAction.php',
'HeraldCommentAction' => 'applications/herald/action/HeraldCommentAction.php',
'HeraldCommitAdapter' => 'applications/diffusion/herald/HeraldCommitAdapter.php',
'HeraldCondition' => 'applications/herald/storage/HeraldCondition.php',
'HeraldConditionTranscript' => 'applications/herald/storage/transcript/HeraldConditionTranscript.php',
'HeraldContentSourceField' => 'applications/herald/field/HeraldContentSourceField.php',
'HeraldController' => 'applications/herald/controller/HeraldController.php',
'HeraldCoreStateReasons' => 'applications/herald/state/HeraldCoreStateReasons.php',
'HeraldCreateWebhooksCapability' => 'applications/herald/capability/HeraldCreateWebhooksCapability.php',
'HeraldDAO' => 'applications/herald/storage/HeraldDAO.php',
'HeraldDeprecatedFieldGroup' => 'applications/herald/field/HeraldDeprecatedFieldGroup.php',
'HeraldDifferentialAdapter' => 'applications/differential/herald/HeraldDifferentialAdapter.php',
'HeraldDifferentialDiffAdapter' => 'applications/differential/herald/HeraldDifferentialDiffAdapter.php',
'HeraldDifferentialRevisionAdapter' => 'applications/differential/herald/HeraldDifferentialRevisionAdapter.php',
'HeraldDisableController' => 'applications/herald/controller/HeraldDisableController.php',
'HeraldDoNothingAction' => 'applications/herald/action/HeraldDoNothingAction.php',
'HeraldEditFieldGroup' => 'applications/herald/field/HeraldEditFieldGroup.php',
'HeraldEffect' => 'applications/herald/engine/HeraldEffect.php',
'HeraldEmptyFieldValue' => 'applications/herald/value/HeraldEmptyFieldValue.php',
'HeraldEngine' => 'applications/herald/engine/HeraldEngine.php',
'HeraldExactProjectsField' => 'applications/project/herald/HeraldExactProjectsField.php',
'HeraldField' => 'applications/herald/field/HeraldField.php',
'HeraldFieldGroup' => 'applications/herald/field/HeraldFieldGroup.php',
'HeraldFieldTestCase' => 'applications/herald/field/__tests__/HeraldFieldTestCase.php',
'HeraldFieldValue' => 'applications/herald/value/HeraldFieldValue.php',
'HeraldGroup' => 'applications/herald/group/HeraldGroup.php',
'HeraldInvalidActionException' => 'applications/herald/engine/exception/HeraldInvalidActionException.php',
'HeraldInvalidConditionException' => 'applications/herald/engine/exception/HeraldInvalidConditionException.php',
'HeraldMailableState' => 'applications/herald/state/HeraldMailableState.php',
'HeraldManageGlobalRulesCapability' => 'applications/herald/capability/HeraldManageGlobalRulesCapability.php',
'HeraldManagementWorkflow' => 'applications/herald/management/HeraldManagementWorkflow.php',
'HeraldManiphestTaskAdapter' => 'applications/maniphest/herald/HeraldManiphestTaskAdapter.php',
'HeraldNewController' => 'applications/herald/controller/HeraldNewController.php',
'HeraldNewObjectField' => 'applications/herald/field/HeraldNewObjectField.php',
'HeraldNotifyActionGroup' => 'applications/herald/action/HeraldNotifyActionGroup.php',
'HeraldObjectTranscript' => 'applications/herald/storage/transcript/HeraldObjectTranscript.php',
'HeraldPhameBlogAdapter' => 'applications/phame/herald/HeraldPhameBlogAdapter.php',
'HeraldPhamePostAdapter' => 'applications/phame/herald/HeraldPhamePostAdapter.php',
'HeraldPholioMockAdapter' => 'applications/pholio/herald/HeraldPholioMockAdapter.php',
'HeraldPonderQuestionAdapter' => 'applications/ponder/herald/HeraldPonderQuestionAdapter.php',
'HeraldPreCommitAdapter' => 'applications/diffusion/herald/HeraldPreCommitAdapter.php',
'HeraldPreCommitContentAdapter' => 'applications/diffusion/herald/HeraldPreCommitContentAdapter.php',
'HeraldPreCommitRefAdapter' => 'applications/diffusion/herald/HeraldPreCommitRefAdapter.php',
'HeraldPreventActionGroup' => 'applications/herald/action/HeraldPreventActionGroup.php',
'HeraldProjectsField' => 'applications/project/herald/HeraldProjectsField.php',
'HeraldRecursiveConditionsException' => 'applications/herald/engine/exception/HeraldRecursiveConditionsException.php',
'HeraldRelatedFieldGroup' => 'applications/herald/field/HeraldRelatedFieldGroup.php',
'HeraldRemarkupFieldValue' => 'applications/herald/value/HeraldRemarkupFieldValue.php',
'HeraldRemarkupRule' => 'applications/herald/remarkup/HeraldRemarkupRule.php',
'HeraldRule' => 'applications/herald/storage/HeraldRule.php',
'HeraldRuleAdapter' => 'applications/herald/adapter/HeraldRuleAdapter.php',
'HeraldRuleAdapterField' => 'applications/herald/field/rule/HeraldRuleAdapterField.php',
'HeraldRuleController' => 'applications/herald/controller/HeraldRuleController.php',
'HeraldRuleDatasource' => 'applications/herald/typeahead/HeraldRuleDatasource.php',
'HeraldRuleEditor' => 'applications/herald/editor/HeraldRuleEditor.php',
'HeraldRuleField' => 'applications/herald/field/rule/HeraldRuleField.php',
'HeraldRuleFieldGroup' => 'applications/herald/field/rule/HeraldRuleFieldGroup.php',
'HeraldRuleListController' => 'applications/herald/controller/HeraldRuleListController.php',
'HeraldRulePHIDType' => 'applications/herald/phid/HeraldRulePHIDType.php',
'HeraldRuleQuery' => 'applications/herald/query/HeraldRuleQuery.php',
'HeraldRuleReplyHandler' => 'applications/herald/mail/HeraldRuleReplyHandler.php',
'HeraldRuleSearchEngine' => 'applications/herald/query/HeraldRuleSearchEngine.php',
'HeraldRuleSerializer' => 'applications/herald/editor/HeraldRuleSerializer.php',
'HeraldRuleTestCase' => 'applications/herald/storage/__tests__/HeraldRuleTestCase.php',
'HeraldRuleTransaction' => 'applications/herald/storage/HeraldRuleTransaction.php',
'HeraldRuleTransactionComment' => 'applications/herald/storage/HeraldRuleTransactionComment.php',
'HeraldRuleTranscript' => 'applications/herald/storage/transcript/HeraldRuleTranscript.php',
'HeraldRuleTypeConfig' => 'applications/herald/config/HeraldRuleTypeConfig.php',
'HeraldRuleTypeDatasource' => 'applications/herald/typeahead/HeraldRuleTypeDatasource.php',
'HeraldRuleTypeField' => 'applications/herald/field/rule/HeraldRuleTypeField.php',
'HeraldRuleViewController' => 'applications/herald/controller/HeraldRuleViewController.php',
'HeraldSchemaSpec' => 'applications/herald/storage/HeraldSchemaSpec.php',
'HeraldSelectFieldValue' => 'applications/herald/value/HeraldSelectFieldValue.php',
'HeraldSpaceField' => 'applications/spaces/herald/HeraldSpaceField.php',
'HeraldState' => 'applications/herald/state/HeraldState.php',
'HeraldStateReasons' => 'applications/herald/state/HeraldStateReasons.php',
'HeraldSubscribersField' => 'applications/subscriptions/herald/HeraldSubscribersField.php',
'HeraldSupportActionGroup' => 'applications/herald/action/HeraldSupportActionGroup.php',
'HeraldSupportFieldGroup' => 'applications/herald/field/HeraldSupportFieldGroup.php',
'HeraldTestConsoleController' => 'applications/herald/controller/HeraldTestConsoleController.php',
'HeraldTestManagementWorkflow' => 'applications/herald/management/HeraldTestManagementWorkflow.php',
'HeraldTextFieldValue' => 'applications/herald/value/HeraldTextFieldValue.php',
'HeraldTokenizerFieldValue' => 'applications/herald/value/HeraldTokenizerFieldValue.php',
'HeraldTransactionQuery' => 'applications/herald/query/HeraldTransactionQuery.php',
'HeraldTranscript' => 'applications/herald/storage/transcript/HeraldTranscript.php',
'HeraldTranscriptController' => 'applications/herald/controller/HeraldTranscriptController.php',
'HeraldTranscriptDestructionEngineExtension' => 'applications/herald/engineextension/HeraldTranscriptDestructionEngineExtension.php',
'HeraldTranscriptGarbageCollector' => 'applications/herald/garbagecollector/HeraldTranscriptGarbageCollector.php',
'HeraldTranscriptListController' => 'applications/herald/controller/HeraldTranscriptListController.php',
'HeraldTranscriptPHIDType' => 'applications/herald/phid/HeraldTranscriptPHIDType.php',
'HeraldTranscriptQuery' => 'applications/herald/query/HeraldTranscriptQuery.php',
'HeraldTranscriptSearchEngine' => 'applications/herald/query/HeraldTranscriptSearchEngine.php',
'HeraldTranscriptTestCase' => 'applications/herald/storage/__tests__/HeraldTranscriptTestCase.php',
'HeraldUtilityActionGroup' => 'applications/herald/action/HeraldUtilityActionGroup.php',
'HeraldWebhook' => 'applications/herald/storage/HeraldWebhook.php',
'HeraldWebhookCallManagementWorkflow' => 'applications/herald/management/HeraldWebhookCallManagementWorkflow.php',
'HeraldWebhookController' => 'applications/herald/controller/HeraldWebhookController.php',
'HeraldWebhookDatasource' => 'applications/herald/typeahead/HeraldWebhookDatasource.php',
'HeraldWebhookEditController' => 'applications/herald/controller/HeraldWebhookEditController.php',
'HeraldWebhookEditEngine' => 'applications/herald/editor/HeraldWebhookEditEngine.php',
'HeraldWebhookEditor' => 'applications/herald/editor/HeraldWebhookEditor.php',
'HeraldWebhookKeyController' => 'applications/herald/controller/HeraldWebhookKeyController.php',
'HeraldWebhookListController' => 'applications/herald/controller/HeraldWebhookListController.php',
'HeraldWebhookManagementWorkflow' => 'applications/herald/management/HeraldWebhookManagementWorkflow.php',
'HeraldWebhookNameTransaction' => 'applications/herald/xaction/HeraldWebhookNameTransaction.php',
'HeraldWebhookPHIDType' => 'applications/herald/phid/HeraldWebhookPHIDType.php',
'HeraldWebhookQuery' => 'applications/herald/query/HeraldWebhookQuery.php',
'HeraldWebhookRequest' => 'applications/herald/storage/HeraldWebhookRequest.php',
'HeraldWebhookRequestGarbageCollector' => 'applications/herald/garbagecollector/HeraldWebhookRequestGarbageCollector.php',
'HeraldWebhookRequestListView' => 'applications/herald/view/HeraldWebhookRequestListView.php',
'HeraldWebhookRequestPHIDType' => 'applications/herald/phid/HeraldWebhookRequestPHIDType.php',
'HeraldWebhookRequestQuery' => 'applications/herald/query/HeraldWebhookRequestQuery.php',
'HeraldWebhookSearchEngine' => 'applications/herald/query/HeraldWebhookSearchEngine.php',
'HeraldWebhookStatusTransaction' => 'applications/herald/xaction/HeraldWebhookStatusTransaction.php',
'HeraldWebhookTestController' => 'applications/herald/controller/HeraldWebhookTestController.php',
'HeraldWebhookTransaction' => 'applications/herald/storage/HeraldWebhookTransaction.php',
'HeraldWebhookTransactionQuery' => 'applications/herald/query/HeraldWebhookTransactionQuery.php',
'HeraldWebhookTransactionType' => 'applications/herald/xaction/HeraldWebhookTransactionType.php',
'HeraldWebhookURITransaction' => 'applications/herald/xaction/HeraldWebhookURITransaction.php',
'HeraldWebhookViewController' => 'applications/herald/controller/HeraldWebhookViewController.php',
'HeraldWebhookWorker' => 'applications/herald/worker/HeraldWebhookWorker.php',
'Javelin' => 'infrastructure/javelin/Javelin.php',
'LegalpadController' => 'applications/legalpad/controller/LegalpadController.php',
'LegalpadCreateDocumentsCapability' => 'applications/legalpad/capability/LegalpadCreateDocumentsCapability.php',
'LegalpadDAO' => 'applications/legalpad/storage/LegalpadDAO.php',
'LegalpadDefaultEditCapability' => 'applications/legalpad/capability/LegalpadDefaultEditCapability.php',
'LegalpadDefaultViewCapability' => 'applications/legalpad/capability/LegalpadDefaultViewCapability.php',
'LegalpadDocument' => 'applications/legalpad/storage/LegalpadDocument.php',
'LegalpadDocumentBody' => 'applications/legalpad/storage/LegalpadDocumentBody.php',
'LegalpadDocumentDatasource' => 'applications/legalpad/typeahead/LegalpadDocumentDatasource.php',
'LegalpadDocumentDoneController' => 'applications/legalpad/controller/LegalpadDocumentDoneController.php',
'LegalpadDocumentEditController' => 'applications/legalpad/controller/LegalpadDocumentEditController.php',
'LegalpadDocumentEditEngine' => 'applications/legalpad/editor/LegalpadDocumentEditEngine.php',
'LegalpadDocumentEditor' => 'applications/legalpad/editor/LegalpadDocumentEditor.php',
'LegalpadDocumentListController' => 'applications/legalpad/controller/LegalpadDocumentListController.php',
'LegalpadDocumentManageController' => 'applications/legalpad/controller/LegalpadDocumentManageController.php',
'LegalpadDocumentPreambleTransaction' => 'applications/legalpad/xaction/LegalpadDocumentPreambleTransaction.php',
'LegalpadDocumentQuery' => 'applications/legalpad/query/LegalpadDocumentQuery.php',
'LegalpadDocumentRemarkupRule' => 'applications/legalpad/remarkup/LegalpadDocumentRemarkupRule.php',
'LegalpadDocumentRequireSignatureTransaction' => 'applications/legalpad/xaction/LegalpadDocumentRequireSignatureTransaction.php',
'LegalpadDocumentSearchEngine' => 'applications/legalpad/query/LegalpadDocumentSearchEngine.php',
'LegalpadDocumentSignController' => 'applications/legalpad/controller/LegalpadDocumentSignController.php',
'LegalpadDocumentSignature' => 'applications/legalpad/storage/LegalpadDocumentSignature.php',
'LegalpadDocumentSignatureAddController' => 'applications/legalpad/controller/LegalpadDocumentSignatureAddController.php',
'LegalpadDocumentSignatureListController' => 'applications/legalpad/controller/LegalpadDocumentSignatureListController.php',
'LegalpadDocumentSignatureQuery' => 'applications/legalpad/query/LegalpadDocumentSignatureQuery.php',
'LegalpadDocumentSignatureSearchEngine' => 'applications/legalpad/query/LegalpadDocumentSignatureSearchEngine.php',
'LegalpadDocumentSignatureTypeTransaction' => 'applications/legalpad/xaction/LegalpadDocumentSignatureTypeTransaction.php',
'LegalpadDocumentSignatureVerificationController' => 'applications/legalpad/controller/LegalpadDocumentSignatureVerificationController.php',
'LegalpadDocumentSignatureViewController' => 'applications/legalpad/controller/LegalpadDocumentSignatureViewController.php',
'LegalpadDocumentTextTransaction' => 'applications/legalpad/xaction/LegalpadDocumentTextTransaction.php',
'LegalpadDocumentTitleTransaction' => 'applications/legalpad/xaction/LegalpadDocumentTitleTransaction.php',
'LegalpadDocumentTransactionType' => 'applications/legalpad/xaction/LegalpadDocumentTransactionType.php',
'LegalpadMailReceiver' => 'applications/legalpad/mail/LegalpadMailReceiver.php',
'LegalpadObjectNeedsSignatureEdgeType' => 'applications/legalpad/edge/LegalpadObjectNeedsSignatureEdgeType.php',
'LegalpadReplyHandler' => 'applications/legalpad/mail/LegalpadReplyHandler.php',
'LegalpadRequireSignatureHeraldAction' => 'applications/legalpad/herald/LegalpadRequireSignatureHeraldAction.php',
'LegalpadSchemaSpec' => 'applications/legalpad/storage/LegalpadSchemaSpec.php',
'LegalpadSignatureNeededByObjectEdgeType' => 'applications/legalpad/edge/LegalpadSignatureNeededByObjectEdgeType.php',
'LegalpadTransaction' => 'applications/legalpad/storage/LegalpadTransaction.php',
'LegalpadTransactionComment' => 'applications/legalpad/storage/LegalpadTransactionComment.php',
'LegalpadTransactionQuery' => 'applications/legalpad/query/LegalpadTransactionQuery.php',
- 'LegalpadTransactionView' => 'applications/legalpad/view/LegalpadTransactionView.php',
'LiskChunkTestCase' => 'infrastructure/storage/lisk/__tests__/LiskChunkTestCase.php',
'LiskDAO' => 'infrastructure/storage/lisk/LiskDAO.php',
'LiskDAOTestCase' => 'infrastructure/storage/lisk/__tests__/LiskDAOTestCase.php',
'LiskEphemeralObjectException' => 'infrastructure/storage/lisk/LiskEphemeralObjectException.php',
'LiskFixtureTestCase' => 'infrastructure/storage/lisk/__tests__/LiskFixtureTestCase.php',
'LiskIsolationTestCase' => 'infrastructure/storage/lisk/__tests__/LiskIsolationTestCase.php',
'LiskIsolationTestDAO' => 'infrastructure/storage/lisk/__tests__/LiskIsolationTestDAO.php',
'LiskIsolationTestDAOException' => 'infrastructure/storage/lisk/__tests__/LiskIsolationTestDAOException.php',
'LiskMigrationIterator' => 'infrastructure/storage/lisk/LiskMigrationIterator.php',
'LiskRawMigrationIterator' => 'infrastructure/storage/lisk/LiskRawMigrationIterator.php',
'MacroConduitAPIMethod' => 'applications/macro/conduit/MacroConduitAPIMethod.php',
'MacroCreateMemeConduitAPIMethod' => 'applications/macro/conduit/MacroCreateMemeConduitAPIMethod.php',
'MacroEditConduitAPIMethod' => 'applications/macro/conduit/MacroEditConduitAPIMethod.php',
'MacroEmojiExample' => 'applications/uiexample/examples/MacroEmojiExample.php',
'MacroQueryConduitAPIMethod' => 'applications/macro/conduit/MacroQueryConduitAPIMethod.php',
'ManiphestAssignEmailCommand' => 'applications/maniphest/command/ManiphestAssignEmailCommand.php',
'ManiphestAssigneeDatasource' => 'applications/maniphest/typeahead/ManiphestAssigneeDatasource.php',
'ManiphestBulkEditCapability' => 'applications/maniphest/capability/ManiphestBulkEditCapability.php',
'ManiphestBulkEditController' => 'applications/maniphest/controller/ManiphestBulkEditController.php',
'ManiphestClaimEmailCommand' => 'applications/maniphest/command/ManiphestClaimEmailCommand.php',
'ManiphestCloseEmailCommand' => 'applications/maniphest/command/ManiphestCloseEmailCommand.php',
'ManiphestConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestConduitAPIMethod.php',
'ManiphestConfiguredCustomField' => 'applications/maniphest/field/ManiphestConfiguredCustomField.php',
'ManiphestConstants' => 'applications/maniphest/constants/ManiphestConstants.php',
'ManiphestController' => 'applications/maniphest/controller/ManiphestController.php',
'ManiphestCreateMailReceiver' => 'applications/maniphest/mail/ManiphestCreateMailReceiver.php',
'ManiphestCreateTaskConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestCreateTaskConduitAPIMethod.php',
'ManiphestCustomField' => 'applications/maniphest/field/ManiphestCustomField.php',
'ManiphestCustomFieldNumericIndex' => 'applications/maniphest/storage/ManiphestCustomFieldNumericIndex.php',
'ManiphestCustomFieldStatusParser' => 'applications/maniphest/field/parser/ManiphestCustomFieldStatusParser.php',
'ManiphestCustomFieldStatusParserTestCase' => 'applications/maniphest/field/parser/__tests__/ManiphestCustomFieldStatusParserTestCase.php',
'ManiphestCustomFieldStorage' => 'applications/maniphest/storage/ManiphestCustomFieldStorage.php',
'ManiphestCustomFieldStringIndex' => 'applications/maniphest/storage/ManiphestCustomFieldStringIndex.php',
'ManiphestDAO' => 'applications/maniphest/storage/ManiphestDAO.php',
'ManiphestDefaultEditCapability' => 'applications/maniphest/capability/ManiphestDefaultEditCapability.php',
'ManiphestDefaultViewCapability' => 'applications/maniphest/capability/ManiphestDefaultViewCapability.php',
'ManiphestEditConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestEditConduitAPIMethod.php',
'ManiphestEditEngine' => 'applications/maniphest/editor/ManiphestEditEngine.php',
'ManiphestEmailCommand' => 'applications/maniphest/command/ManiphestEmailCommand.php',
'ManiphestGetTaskTransactionsConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestGetTaskTransactionsConduitAPIMethod.php',
'ManiphestHovercardEngineExtension' => 'applications/maniphest/engineextension/ManiphestHovercardEngineExtension.php',
'ManiphestInfoConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestInfoConduitAPIMethod.php',
'ManiphestMailEngineExtension' => 'applications/maniphest/engineextension/ManiphestMailEngineExtension.php',
'ManiphestNameIndex' => 'applications/maniphest/storage/ManiphestNameIndex.php',
'ManiphestPointsConfigType' => 'applications/maniphest/config/ManiphestPointsConfigType.php',
'ManiphestPrioritiesConfigType' => 'applications/maniphest/config/ManiphestPrioritiesConfigType.php',
'ManiphestPriorityEmailCommand' => 'applications/maniphest/command/ManiphestPriorityEmailCommand.php',
'ManiphestPrioritySearchConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestPrioritySearchConduitAPIMethod.php',
'ManiphestProjectNameFulltextEngineExtension' => 'applications/maniphest/engineextension/ManiphestProjectNameFulltextEngineExtension.php',
'ManiphestQueryConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestQueryConduitAPIMethod.php',
'ManiphestQueryStatusesConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestQueryStatusesConduitAPIMethod.php',
'ManiphestRemarkupRule' => 'applications/maniphest/remarkup/ManiphestRemarkupRule.php',
'ManiphestReplyHandler' => 'applications/maniphest/mail/ManiphestReplyHandler.php',
'ManiphestReportController' => 'applications/maniphest/controller/ManiphestReportController.php',
'ManiphestSchemaSpec' => 'applications/maniphest/storage/ManiphestSchemaSpec.php',
'ManiphestSearchConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestSearchConduitAPIMethod.php',
'ManiphestStatusEmailCommand' => 'applications/maniphest/command/ManiphestStatusEmailCommand.php',
'ManiphestStatusSearchConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestStatusSearchConduitAPIMethod.php',
'ManiphestStatusesConfigType' => 'applications/maniphest/config/ManiphestStatusesConfigType.php',
'ManiphestSubpriorityController' => 'applications/maniphest/controller/ManiphestSubpriorityController.php',
'ManiphestSubtypesConfigType' => 'applications/maniphest/config/ManiphestSubtypesConfigType.php',
'ManiphestTask' => 'applications/maniphest/storage/ManiphestTask.php',
'ManiphestTaskAssignHeraldAction' => 'applications/maniphest/herald/ManiphestTaskAssignHeraldAction.php',
'ManiphestTaskAssignOtherHeraldAction' => 'applications/maniphest/herald/ManiphestTaskAssignOtherHeraldAction.php',
'ManiphestTaskAssignSelfHeraldAction' => 'applications/maniphest/herald/ManiphestTaskAssignSelfHeraldAction.php',
'ManiphestTaskAssigneeHeraldField' => 'applications/maniphest/herald/ManiphestTaskAssigneeHeraldField.php',
'ManiphestTaskAttachTransaction' => 'applications/maniphest/xaction/ManiphestTaskAttachTransaction.php',
'ManiphestTaskAuthorHeraldField' => 'applications/maniphest/herald/ManiphestTaskAuthorHeraldField.php',
'ManiphestTaskAuthorPolicyRule' => 'applications/maniphest/policyrule/ManiphestTaskAuthorPolicyRule.php',
'ManiphestTaskBulkEngine' => 'applications/maniphest/bulk/ManiphestTaskBulkEngine.php',
'ManiphestTaskCloseAsDuplicateRelationship' => 'applications/maniphest/relationship/ManiphestTaskCloseAsDuplicateRelationship.php',
'ManiphestTaskClosedStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskClosedStatusDatasource.php',
'ManiphestTaskCoverImageTransaction' => 'applications/maniphest/xaction/ManiphestTaskCoverImageTransaction.php',
'ManiphestTaskDependedOnByTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependedOnByTaskEdgeType.php',
'ManiphestTaskDependsOnTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependsOnTaskEdgeType.php',
'ManiphestTaskDescriptionHeraldField' => 'applications/maniphest/herald/ManiphestTaskDescriptionHeraldField.php',
'ManiphestTaskDescriptionTransaction' => 'applications/maniphest/xaction/ManiphestTaskDescriptionTransaction.php',
'ManiphestTaskDetailController' => 'applications/maniphest/controller/ManiphestTaskDetailController.php',
'ManiphestTaskEdgeTransaction' => 'applications/maniphest/xaction/ManiphestTaskEdgeTransaction.php',
'ManiphestTaskEditController' => 'applications/maniphest/controller/ManiphestTaskEditController.php',
'ManiphestTaskEditEngineLock' => 'applications/maniphest/editor/ManiphestTaskEditEngineLock.php',
'ManiphestTaskFerretEngine' => 'applications/maniphest/search/ManiphestTaskFerretEngine.php',
'ManiphestTaskFulltextEngine' => 'applications/maniphest/search/ManiphestTaskFulltextEngine.php',
'ManiphestTaskGraph' => 'infrastructure/graph/ManiphestTaskGraph.php',
'ManiphestTaskHasCommitEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasCommitEdgeType.php',
'ManiphestTaskHasCommitRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasCommitRelationship.php',
'ManiphestTaskHasDuplicateTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasDuplicateTaskEdgeType.php',
'ManiphestTaskHasMockEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasMockEdgeType.php',
'ManiphestTaskHasMockRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasMockRelationship.php',
'ManiphestTaskHasParentRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasParentRelationship.php',
'ManiphestTaskHasRevisionEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasRevisionEdgeType.php',
'ManiphestTaskHasRevisionRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasRevisionRelationship.php',
'ManiphestTaskHasSubtaskRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasSubtaskRelationship.php',
'ManiphestTaskHeraldField' => 'applications/maniphest/herald/ManiphestTaskHeraldField.php',
'ManiphestTaskHeraldFieldGroup' => 'applications/maniphest/herald/ManiphestTaskHeraldFieldGroup.php',
'ManiphestTaskIsDuplicateOfTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskIsDuplicateOfTaskEdgeType.php',
'ManiphestTaskListController' => 'applications/maniphest/controller/ManiphestTaskListController.php',
'ManiphestTaskListHTTPParameterType' => 'applications/maniphest/httpparametertype/ManiphestTaskListHTTPParameterType.php',
'ManiphestTaskListView' => 'applications/maniphest/view/ManiphestTaskListView.php',
+ 'ManiphestTaskMFAEngine' => 'applications/maniphest/engine/ManiphestTaskMFAEngine.php',
'ManiphestTaskMailReceiver' => 'applications/maniphest/mail/ManiphestTaskMailReceiver.php',
'ManiphestTaskMergeInRelationship' => 'applications/maniphest/relationship/ManiphestTaskMergeInRelationship.php',
'ManiphestTaskMergedFromTransaction' => 'applications/maniphest/xaction/ManiphestTaskMergedFromTransaction.php',
'ManiphestTaskMergedIntoTransaction' => 'applications/maniphest/xaction/ManiphestTaskMergedIntoTransaction.php',
'ManiphestTaskOpenStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskOpenStatusDatasource.php',
'ManiphestTaskOwnerTransaction' => 'applications/maniphest/xaction/ManiphestTaskOwnerTransaction.php',
'ManiphestTaskPHIDResolver' => 'applications/maniphest/httpparametertype/ManiphestTaskPHIDResolver.php',
'ManiphestTaskPHIDType' => 'applications/maniphest/phid/ManiphestTaskPHIDType.php',
'ManiphestTaskParentTransaction' => 'applications/maniphest/xaction/ManiphestTaskParentTransaction.php',
'ManiphestTaskPoints' => 'applications/maniphest/constants/ManiphestTaskPoints.php',
'ManiphestTaskPointsTransaction' => 'applications/maniphest/xaction/ManiphestTaskPointsTransaction.php',
'ManiphestTaskPriority' => 'applications/maniphest/constants/ManiphestTaskPriority.php',
'ManiphestTaskPriorityDatasource' => 'applications/maniphest/typeahead/ManiphestTaskPriorityDatasource.php',
'ManiphestTaskPriorityHeraldAction' => 'applications/maniphest/herald/ManiphestTaskPriorityHeraldAction.php',
'ManiphestTaskPriorityHeraldField' => 'applications/maniphest/herald/ManiphestTaskPriorityHeraldField.php',
'ManiphestTaskPriorityTransaction' => 'applications/maniphest/xaction/ManiphestTaskPriorityTransaction.php',
'ManiphestTaskQuery' => 'applications/maniphest/query/ManiphestTaskQuery.php',
'ManiphestTaskRelationship' => 'applications/maniphest/relationship/ManiphestTaskRelationship.php',
'ManiphestTaskRelationshipSource' => 'applications/search/relationship/ManiphestTaskRelationshipSource.php',
'ManiphestTaskResultListView' => 'applications/maniphest/view/ManiphestTaskResultListView.php',
'ManiphestTaskSearchEngine' => 'applications/maniphest/query/ManiphestTaskSearchEngine.php',
'ManiphestTaskStatus' => 'applications/maniphest/constants/ManiphestTaskStatus.php',
'ManiphestTaskStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskStatusDatasource.php',
'ManiphestTaskStatusFunctionDatasource' => 'applications/maniphest/typeahead/ManiphestTaskStatusFunctionDatasource.php',
'ManiphestTaskStatusHeraldAction' => 'applications/maniphest/herald/ManiphestTaskStatusHeraldAction.php',
'ManiphestTaskStatusHeraldField' => 'applications/maniphest/herald/ManiphestTaskStatusHeraldField.php',
'ManiphestTaskStatusTestCase' => 'applications/maniphest/constants/__tests__/ManiphestTaskStatusTestCase.php',
'ManiphestTaskStatusTransaction' => 'applications/maniphest/xaction/ManiphestTaskStatusTransaction.php',
'ManiphestTaskSubpriorityTransaction' => 'applications/maniphest/xaction/ManiphestTaskSubpriorityTransaction.php',
'ManiphestTaskSubtaskController' => 'applications/maniphest/controller/ManiphestTaskSubtaskController.php',
'ManiphestTaskSubtypeDatasource' => 'applications/maniphest/typeahead/ManiphestTaskSubtypeDatasource.php',
'ManiphestTaskTestCase' => 'applications/maniphest/__tests__/ManiphestTaskTestCase.php',
'ManiphestTaskTitleHeraldField' => 'applications/maniphest/herald/ManiphestTaskTitleHeraldField.php',
'ManiphestTaskTitleTransaction' => 'applications/maniphest/xaction/ManiphestTaskTitleTransaction.php',
'ManiphestTaskTransactionType' => 'applications/maniphest/xaction/ManiphestTaskTransactionType.php',
'ManiphestTaskUnblockTransaction' => 'applications/maniphest/xaction/ManiphestTaskUnblockTransaction.php',
'ManiphestTransaction' => 'applications/maniphest/storage/ManiphestTransaction.php',
'ManiphestTransactionComment' => 'applications/maniphest/storage/ManiphestTransactionComment.php',
'ManiphestTransactionEditor' => 'applications/maniphest/editor/ManiphestTransactionEditor.php',
'ManiphestTransactionQuery' => 'applications/maniphest/query/ManiphestTransactionQuery.php',
'ManiphestUpdateConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestUpdateConduitAPIMethod.php',
'ManiphestView' => 'applications/maniphest/view/ManiphestView.php',
'MetaMTAEmailTransactionCommand' => 'applications/metamta/command/MetaMTAEmailTransactionCommand.php',
'MetaMTAEmailTransactionCommandTestCase' => 'applications/metamta/command/__tests__/MetaMTAEmailTransactionCommandTestCase.php',
'MetaMTAMailReceivedGarbageCollector' => 'applications/metamta/garbagecollector/MetaMTAMailReceivedGarbageCollector.php',
'MetaMTAMailSentGarbageCollector' => 'applications/metamta/garbagecollector/MetaMTAMailSentGarbageCollector.php',
'MetaMTAReceivedMailStatus' => 'applications/metamta/constants/MetaMTAReceivedMailStatus.php',
'MultimeterContext' => 'applications/multimeter/storage/MultimeterContext.php',
'MultimeterControl' => 'applications/multimeter/data/MultimeterControl.php',
'MultimeterController' => 'applications/multimeter/controller/MultimeterController.php',
'MultimeterDAO' => 'applications/multimeter/storage/MultimeterDAO.php',
'MultimeterDimension' => 'applications/multimeter/storage/MultimeterDimension.php',
'MultimeterEvent' => 'applications/multimeter/storage/MultimeterEvent.php',
'MultimeterEventGarbageCollector' => 'applications/multimeter/garbagecollector/MultimeterEventGarbageCollector.php',
'MultimeterHost' => 'applications/multimeter/storage/MultimeterHost.php',
'MultimeterLabel' => 'applications/multimeter/storage/MultimeterLabel.php',
'MultimeterSampleController' => 'applications/multimeter/controller/MultimeterSampleController.php',
'MultimeterViewer' => 'applications/multimeter/storage/MultimeterViewer.php',
'NuanceCommandImplementation' => 'applications/nuance/command/NuanceCommandImplementation.php',
'NuanceConduitAPIMethod' => 'applications/nuance/conduit/NuanceConduitAPIMethod.php',
'NuanceConsoleController' => 'applications/nuance/controller/NuanceConsoleController.php',
'NuanceContentSource' => 'applications/nuance/contentsource/NuanceContentSource.php',
'NuanceController' => 'applications/nuance/controller/NuanceController.php',
'NuanceDAO' => 'applications/nuance/storage/NuanceDAO.php',
'NuanceFormItemType' => 'applications/nuance/item/NuanceFormItemType.php',
'NuanceGitHubEventItemType' => 'applications/nuance/item/NuanceGitHubEventItemType.php',
'NuanceGitHubImportCursor' => 'applications/nuance/cursor/NuanceGitHubImportCursor.php',
'NuanceGitHubIssuesImportCursor' => 'applications/nuance/cursor/NuanceGitHubIssuesImportCursor.php',
'NuanceGitHubRawEvent' => 'applications/nuance/github/NuanceGitHubRawEvent.php',
'NuanceGitHubRawEventTestCase' => 'applications/nuance/github/__tests__/NuanceGitHubRawEventTestCase.php',
'NuanceGitHubRepositoryImportCursor' => 'applications/nuance/cursor/NuanceGitHubRepositoryImportCursor.php',
'NuanceGitHubRepositorySourceDefinition' => 'applications/nuance/source/NuanceGitHubRepositorySourceDefinition.php',
'NuanceImportCursor' => 'applications/nuance/cursor/NuanceImportCursor.php',
'NuanceImportCursorData' => 'applications/nuance/storage/NuanceImportCursorData.php',
'NuanceImportCursorDataQuery' => 'applications/nuance/query/NuanceImportCursorDataQuery.php',
'NuanceImportCursorPHIDType' => 'applications/nuance/phid/NuanceImportCursorPHIDType.php',
'NuanceItem' => 'applications/nuance/storage/NuanceItem.php',
'NuanceItemActionController' => 'applications/nuance/controller/NuanceItemActionController.php',
'NuanceItemCommand' => 'applications/nuance/storage/NuanceItemCommand.php',
'NuanceItemCommandQuery' => 'applications/nuance/query/NuanceItemCommandQuery.php',
'NuanceItemCommandSpec' => 'applications/nuance/command/NuanceItemCommandSpec.php',
'NuanceItemCommandTransaction' => 'applications/nuance/xaction/NuanceItemCommandTransaction.php',
'NuanceItemController' => 'applications/nuance/controller/NuanceItemController.php',
'NuanceItemEditor' => 'applications/nuance/editor/NuanceItemEditor.php',
'NuanceItemListController' => 'applications/nuance/controller/NuanceItemListController.php',
'NuanceItemManageController' => 'applications/nuance/controller/NuanceItemManageController.php',
'NuanceItemOwnerTransaction' => 'applications/nuance/xaction/NuanceItemOwnerTransaction.php',
'NuanceItemPHIDType' => 'applications/nuance/phid/NuanceItemPHIDType.php',
'NuanceItemPropertyTransaction' => 'applications/nuance/xaction/NuanceItemPropertyTransaction.php',
'NuanceItemQuery' => 'applications/nuance/query/NuanceItemQuery.php',
'NuanceItemQueueTransaction' => 'applications/nuance/xaction/NuanceItemQueueTransaction.php',
'NuanceItemRequestorTransaction' => 'applications/nuance/xaction/NuanceItemRequestorTransaction.php',
'NuanceItemSearchEngine' => 'applications/nuance/query/NuanceItemSearchEngine.php',
'NuanceItemSourceTransaction' => 'applications/nuance/xaction/NuanceItemSourceTransaction.php',
'NuanceItemStatusTransaction' => 'applications/nuance/xaction/NuanceItemStatusTransaction.php',
'NuanceItemTransaction' => 'applications/nuance/storage/NuanceItemTransaction.php',
'NuanceItemTransactionComment' => 'applications/nuance/storage/NuanceItemTransactionComment.php',
'NuanceItemTransactionQuery' => 'applications/nuance/query/NuanceItemTransactionQuery.php',
'NuanceItemTransactionType' => 'applications/nuance/xaction/NuanceItemTransactionType.php',
'NuanceItemType' => 'applications/nuance/item/NuanceItemType.php',
'NuanceItemUpdateWorker' => 'applications/nuance/worker/NuanceItemUpdateWorker.php',
'NuanceItemViewController' => 'applications/nuance/controller/NuanceItemViewController.php',
'NuanceManagementImportWorkflow' => 'applications/nuance/management/NuanceManagementImportWorkflow.php',
'NuanceManagementUpdateWorkflow' => 'applications/nuance/management/NuanceManagementUpdateWorkflow.php',
'NuanceManagementWorkflow' => 'applications/nuance/management/NuanceManagementWorkflow.php',
'NuancePhabricatorFormSourceDefinition' => 'applications/nuance/source/NuancePhabricatorFormSourceDefinition.php',
'NuanceQuery' => 'applications/nuance/query/NuanceQuery.php',
'NuanceQueue' => 'applications/nuance/storage/NuanceQueue.php',
'NuanceQueueController' => 'applications/nuance/controller/NuanceQueueController.php',
'NuanceQueueDatasource' => 'applications/nuance/typeahead/NuanceQueueDatasource.php',
'NuanceQueueEditController' => 'applications/nuance/controller/NuanceQueueEditController.php',
'NuanceQueueEditEngine' => 'applications/nuance/editor/NuanceQueueEditEngine.php',
'NuanceQueueEditor' => 'applications/nuance/editor/NuanceQueueEditor.php',
'NuanceQueueListController' => 'applications/nuance/controller/NuanceQueueListController.php',
'NuanceQueueNameTransaction' => 'applications/nuance/xaction/NuanceQueueNameTransaction.php',
'NuanceQueuePHIDType' => 'applications/nuance/phid/NuanceQueuePHIDType.php',
'NuanceQueueQuery' => 'applications/nuance/query/NuanceQueueQuery.php',
'NuanceQueueSearchEngine' => 'applications/nuance/query/NuanceQueueSearchEngine.php',
'NuanceQueueTransaction' => 'applications/nuance/storage/NuanceQueueTransaction.php',
'NuanceQueueTransactionComment' => 'applications/nuance/storage/NuanceQueueTransactionComment.php',
'NuanceQueueTransactionQuery' => 'applications/nuance/query/NuanceQueueTransactionQuery.php',
'NuanceQueueTransactionType' => 'applications/nuance/xaction/NuanceQueueTransactionType.php',
'NuanceQueueViewController' => 'applications/nuance/controller/NuanceQueueViewController.php',
'NuanceQueueWorkController' => 'applications/nuance/controller/NuanceQueueWorkController.php',
'NuanceSchemaSpec' => 'applications/nuance/storage/NuanceSchemaSpec.php',
'NuanceSource' => 'applications/nuance/storage/NuanceSource.php',
'NuanceSourceActionController' => 'applications/nuance/controller/NuanceSourceActionController.php',
'NuanceSourceController' => 'applications/nuance/controller/NuanceSourceController.php',
'NuanceSourceDefaultEditCapability' => 'applications/nuance/capability/NuanceSourceDefaultEditCapability.php',
'NuanceSourceDefaultQueueTransaction' => 'applications/nuance/xaction/NuanceSourceDefaultQueueTransaction.php',
'NuanceSourceDefaultViewCapability' => 'applications/nuance/capability/NuanceSourceDefaultViewCapability.php',
'NuanceSourceDefinition' => 'applications/nuance/source/NuanceSourceDefinition.php',
'NuanceSourceDefinitionTestCase' => 'applications/nuance/source/__tests__/NuanceSourceDefinitionTestCase.php',
'NuanceSourceEditController' => 'applications/nuance/controller/NuanceSourceEditController.php',
'NuanceSourceEditEngine' => 'applications/nuance/editor/NuanceSourceEditEngine.php',
'NuanceSourceEditor' => 'applications/nuance/editor/NuanceSourceEditor.php',
'NuanceSourceListController' => 'applications/nuance/controller/NuanceSourceListController.php',
'NuanceSourceManageCapability' => 'applications/nuance/capability/NuanceSourceManageCapability.php',
'NuanceSourceNameNgrams' => 'applications/nuance/storage/NuanceSourceNameNgrams.php',
'NuanceSourceNameTransaction' => 'applications/nuance/xaction/NuanceSourceNameTransaction.php',
'NuanceSourcePHIDType' => 'applications/nuance/phid/NuanceSourcePHIDType.php',
'NuanceSourceQuery' => 'applications/nuance/query/NuanceSourceQuery.php',
'NuanceSourceSearchEngine' => 'applications/nuance/query/NuanceSourceSearchEngine.php',
'NuanceSourceTransaction' => 'applications/nuance/storage/NuanceSourceTransaction.php',
'NuanceSourceTransactionComment' => 'applications/nuance/storage/NuanceSourceTransactionComment.php',
'NuanceSourceTransactionQuery' => 'applications/nuance/query/NuanceSourceTransactionQuery.php',
'NuanceSourceTransactionType' => 'applications/nuance/xaction/NuanceSourceTransactionType.php',
'NuanceSourceViewController' => 'applications/nuance/controller/NuanceSourceViewController.php',
'NuanceTransaction' => 'applications/nuance/storage/NuanceTransaction.php',
'NuanceTrashCommand' => 'applications/nuance/command/NuanceTrashCommand.php',
'NuanceWorker' => 'applications/nuance/worker/NuanceWorker.php',
'OwnersConduitAPIMethod' => 'applications/owners/conduit/OwnersConduitAPIMethod.php',
'OwnersEditConduitAPIMethod' => 'applications/owners/conduit/OwnersEditConduitAPIMethod.php',
'OwnersPackageReplyHandler' => 'applications/owners/mail/OwnersPackageReplyHandler.php',
'OwnersQueryConduitAPIMethod' => 'applications/owners/conduit/OwnersQueryConduitAPIMethod.php',
'OwnersSearchConduitAPIMethod' => 'applications/owners/conduit/OwnersSearchConduitAPIMethod.php',
'PHIDConduitAPIMethod' => 'applications/phid/conduit/PHIDConduitAPIMethod.php',
'PHIDInfoConduitAPIMethod' => 'applications/phid/conduit/PHIDInfoConduitAPIMethod.php',
'PHIDLookupConduitAPIMethod' => 'applications/phid/conduit/PHIDLookupConduitAPIMethod.php',
'PHIDQueryConduitAPIMethod' => 'applications/phid/conduit/PHIDQueryConduitAPIMethod.php',
'PHUI' => 'view/phui/PHUI.php',
'PHUIActionPanelExample' => 'applications/uiexample/examples/PHUIActionPanelExample.php',
'PHUIActionPanelView' => 'view/phui/PHUIActionPanelView.php',
'PHUIApplicationMenuView' => 'view/layout/PHUIApplicationMenuView.php',
'PHUIBadgeBoxView' => 'view/phui/PHUIBadgeBoxView.php',
'PHUIBadgeExample' => 'applications/uiexample/examples/PHUIBadgeExample.php',
'PHUIBadgeMiniView' => 'view/phui/PHUIBadgeMiniView.php',
'PHUIBadgeView' => 'view/phui/PHUIBadgeView.php',
'PHUIBigInfoExample' => 'applications/uiexample/examples/PHUIBigInfoExample.php',
'PHUIBigInfoView' => 'view/phui/PHUIBigInfoView.php',
'PHUIBoxExample' => 'applications/uiexample/examples/PHUIBoxExample.php',
'PHUIBoxView' => 'view/phui/PHUIBoxView.php',
'PHUIButtonBarExample' => 'applications/uiexample/examples/PHUIButtonBarExample.php',
'PHUIButtonBarView' => 'view/phui/PHUIButtonBarView.php',
'PHUIButtonExample' => 'applications/uiexample/examples/PHUIButtonExample.php',
'PHUIButtonView' => 'view/phui/PHUIButtonView.php',
'PHUICMSView' => 'view/phui/PHUICMSView.php',
'PHUICalendarDayView' => 'view/phui/calendar/PHUICalendarDayView.php',
'PHUICalendarListView' => 'view/phui/calendar/PHUICalendarListView.php',
'PHUICalendarMonthView' => 'view/phui/calendar/PHUICalendarMonthView.php',
'PHUICalendarWeekView' => 'view/phui/calendar/PHUICalendarWeekView.php',
'PHUICalendarWidgetView' => 'view/phui/calendar/PHUICalendarWidgetView.php',
'PHUIColorPalletteExample' => 'applications/uiexample/examples/PHUIColorPalletteExample.php',
'PHUICrumbView' => 'view/phui/PHUICrumbView.php',
'PHUICrumbsView' => 'view/phui/PHUICrumbsView.php',
'PHUICurtainExtension' => 'view/extension/PHUICurtainExtension.php',
'PHUICurtainPanelView' => 'view/layout/PHUICurtainPanelView.php',
'PHUICurtainView' => 'view/layout/PHUICurtainView.php',
'PHUIDiffGraphView' => 'infrastructure/diff/view/PHUIDiffGraphView.php',
'PHUIDiffGraphViewTestCase' => 'infrastructure/diff/view/__tests__/PHUIDiffGraphViewTestCase.php',
'PHUIDiffInlineCommentDetailView' => 'infrastructure/diff/view/PHUIDiffInlineCommentDetailView.php',
'PHUIDiffInlineCommentEditView' => 'infrastructure/diff/view/PHUIDiffInlineCommentEditView.php',
'PHUIDiffInlineCommentPreviewListView' => 'infrastructure/diff/view/PHUIDiffInlineCommentPreviewListView.php',
'PHUIDiffInlineCommentRowScaffold' => 'infrastructure/diff/view/PHUIDiffInlineCommentRowScaffold.php',
'PHUIDiffInlineCommentTableScaffold' => 'infrastructure/diff/view/PHUIDiffInlineCommentTableScaffold.php',
'PHUIDiffInlineCommentUndoView' => 'infrastructure/diff/view/PHUIDiffInlineCommentUndoView.php',
'PHUIDiffInlineCommentView' => 'infrastructure/diff/view/PHUIDiffInlineCommentView.php',
'PHUIDiffInlineThreader' => 'infrastructure/diff/view/PHUIDiffInlineThreader.php',
'PHUIDiffOneUpInlineCommentRowScaffold' => 'infrastructure/diff/view/PHUIDiffOneUpInlineCommentRowScaffold.php',
'PHUIDiffRevealIconView' => 'infrastructure/diff/view/PHUIDiffRevealIconView.php',
'PHUIDiffTableOfContentsItemView' => 'infrastructure/diff/view/PHUIDiffTableOfContentsItemView.php',
'PHUIDiffTableOfContentsListView' => 'infrastructure/diff/view/PHUIDiffTableOfContentsListView.php',
'PHUIDiffTwoUpInlineCommentRowScaffold' => 'infrastructure/diff/view/PHUIDiffTwoUpInlineCommentRowScaffold.php',
'PHUIDocumentSummaryView' => 'view/phui/PHUIDocumentSummaryView.php',
'PHUIDocumentView' => 'view/phui/PHUIDocumentView.php',
'PHUIFeedStoryExample' => 'applications/uiexample/examples/PHUIFeedStoryExample.php',
'PHUIFeedStoryView' => 'view/phui/PHUIFeedStoryView.php',
'PHUIFormDividerControl' => 'view/form/control/PHUIFormDividerControl.php',
'PHUIFormFileControl' => 'view/form/control/PHUIFormFileControl.php',
'PHUIFormFreeformDateControl' => 'view/form/control/PHUIFormFreeformDateControl.php',
'PHUIFormIconSetControl' => 'view/form/control/PHUIFormIconSetControl.php',
'PHUIFormInsetView' => 'view/form/PHUIFormInsetView.php',
'PHUIFormLayoutView' => 'view/form/PHUIFormLayoutView.php',
'PHUIFormNumberControl' => 'view/form/control/PHUIFormNumberControl.php',
+ 'PHUIFormTimerControl' => 'view/form/control/PHUIFormTimerControl.php',
'PHUIHandleListView' => 'applications/phid/view/PHUIHandleListView.php',
'PHUIHandleTagListView' => 'applications/phid/view/PHUIHandleTagListView.php',
'PHUIHandleView' => 'applications/phid/view/PHUIHandleView.php',
'PHUIHeadThingView' => 'view/phui/PHUIHeadThingView.php',
'PHUIHeaderView' => 'view/phui/PHUIHeaderView.php',
'PHUIHomeView' => 'applications/home/view/PHUIHomeView.php',
'PHUIHovercardUIExample' => 'applications/uiexample/examples/PHUIHovercardUIExample.php',
'PHUIHovercardView' => 'view/phui/PHUIHovercardView.php',
'PHUIIconCircleView' => 'view/phui/PHUIIconCircleView.php',
'PHUIIconExample' => 'applications/uiexample/examples/PHUIIconExample.php',
'PHUIIconView' => 'view/phui/PHUIIconView.php',
'PHUIImageMaskExample' => 'applications/uiexample/examples/PHUIImageMaskExample.php',
'PHUIImageMaskView' => 'view/phui/PHUIImageMaskView.php',
'PHUIInfoExample' => 'applications/uiexample/examples/PHUIInfoExample.php',
'PHUIInfoView' => 'view/phui/PHUIInfoView.php',
'PHUIInvisibleCharacterTestCase' => 'view/phui/__tests__/PHUIInvisibleCharacterTestCase.php',
'PHUIInvisibleCharacterView' => 'view/phui/PHUIInvisibleCharacterView.php',
'PHUILeftRightExample' => 'applications/uiexample/examples/PHUILeftRightExample.php',
'PHUILeftRightView' => 'view/phui/PHUILeftRightView.php',
'PHUIListExample' => 'applications/uiexample/examples/PHUIListExample.php',
'PHUIListItemView' => 'view/phui/PHUIListItemView.php',
'PHUIListView' => 'view/phui/PHUIListView.php',
'PHUIListViewTestCase' => 'view/layout/__tests__/PHUIListViewTestCase.php',
'PHUIObjectBoxView' => 'view/phui/PHUIObjectBoxView.php',
'PHUIObjectItemListExample' => 'applications/uiexample/examples/PHUIObjectItemListExample.php',
'PHUIObjectItemListView' => 'view/phui/PHUIObjectItemListView.php',
'PHUIObjectItemView' => 'view/phui/PHUIObjectItemView.php',
'PHUIPagerView' => 'view/phui/PHUIPagerView.php',
'PHUIPinboardItemView' => 'view/phui/PHUIPinboardItemView.php',
'PHUIPinboardView' => 'view/phui/PHUIPinboardView.php',
'PHUIPolicySectionView' => 'applications/policy/view/PHUIPolicySectionView.php',
'PHUIPropertyGroupView' => 'view/phui/PHUIPropertyGroupView.php',
'PHUIPropertyListExample' => 'applications/uiexample/examples/PHUIPropertyListExample.php',
'PHUIPropertyListView' => 'view/phui/PHUIPropertyListView.php',
'PHUIRemarkupImageView' => 'infrastructure/markup/view/PHUIRemarkupImageView.php',
'PHUIRemarkupPreviewPanel' => 'view/phui/PHUIRemarkupPreviewPanel.php',
'PHUIRemarkupView' => 'infrastructure/markup/view/PHUIRemarkupView.php',
'PHUISegmentBarSegmentView' => 'view/phui/PHUISegmentBarSegmentView.php',
'PHUISegmentBarView' => 'view/phui/PHUISegmentBarView.php',
'PHUISpacesNamespaceContextView' => 'applications/spaces/view/PHUISpacesNamespaceContextView.php',
'PHUIStatusItemView' => 'view/phui/PHUIStatusItemView.php',
'PHUIStatusListView' => 'view/phui/PHUIStatusListView.php',
'PHUITabGroupView' => 'view/phui/PHUITabGroupView.php',
'PHUITabView' => 'view/phui/PHUITabView.php',
'PHUITagExample' => 'applications/uiexample/examples/PHUITagExample.php',
'PHUITagView' => 'view/phui/PHUITagView.php',
'PHUITimelineEventView' => 'view/phui/PHUITimelineEventView.php',
'PHUITimelineExample' => 'applications/uiexample/examples/PHUITimelineExample.php',
'PHUITimelineView' => 'view/phui/PHUITimelineView.php',
'PHUITwoColumnView' => 'view/phui/PHUITwoColumnView.php',
'PHUITypeaheadExample' => 'applications/uiexample/examples/PHUITypeaheadExample.php',
'PHUIUserAvailabilityView' => 'applications/calendar/view/PHUIUserAvailabilityView.php',
'PHUIWorkboardView' => 'view/phui/PHUIWorkboardView.php',
'PHUIWorkpanelView' => 'view/phui/PHUIWorkpanelView.php',
'PHUIXComponentsExample' => 'applications/uiexample/examples/PHUIXComponentsExample.php',
'PassphraseAbstractKey' => 'applications/passphrase/keys/PassphraseAbstractKey.php',
'PassphraseConduitAPIMethod' => 'applications/passphrase/conduit/PassphraseConduitAPIMethod.php',
'PassphraseController' => 'applications/passphrase/controller/PassphraseController.php',
'PassphraseCredential' => 'applications/passphrase/storage/PassphraseCredential.php',
'PassphraseCredentialAuthorPolicyRule' => 'applications/passphrase/policyrule/PassphraseCredentialAuthorPolicyRule.php',
'PassphraseCredentialConduitController' => 'applications/passphrase/controller/PassphraseCredentialConduitController.php',
'PassphraseCredentialConduitTransaction' => 'applications/passphrase/xaction/PassphraseCredentialConduitTransaction.php',
'PassphraseCredentialControl' => 'applications/passphrase/view/PassphraseCredentialControl.php',
'PassphraseCredentialCreateController' => 'applications/passphrase/controller/PassphraseCredentialCreateController.php',
'PassphraseCredentialDescriptionTransaction' => 'applications/passphrase/xaction/PassphraseCredentialDescriptionTransaction.php',
'PassphraseCredentialDestroyController' => 'applications/passphrase/controller/PassphraseCredentialDestroyController.php',
'PassphraseCredentialDestroyTransaction' => 'applications/passphrase/xaction/PassphraseCredentialDestroyTransaction.php',
'PassphraseCredentialEditController' => 'applications/passphrase/controller/PassphraseCredentialEditController.php',
'PassphraseCredentialFerretEngine' => 'applications/passphrase/search/PassphraseCredentialFerretEngine.php',
'PassphraseCredentialFulltextEngine' => 'applications/passphrase/search/PassphraseCredentialFulltextEngine.php',
'PassphraseCredentialListController' => 'applications/passphrase/controller/PassphraseCredentialListController.php',
'PassphraseCredentialLockController' => 'applications/passphrase/controller/PassphraseCredentialLockController.php',
'PassphraseCredentialLockTransaction' => 'applications/passphrase/xaction/PassphraseCredentialLockTransaction.php',
'PassphraseCredentialLookedAtTransaction' => 'applications/passphrase/xaction/PassphraseCredentialLookedAtTransaction.php',
'PassphraseCredentialNameTransaction' => 'applications/passphrase/xaction/PassphraseCredentialNameTransaction.php',
'PassphraseCredentialPHIDType' => 'applications/passphrase/phid/PassphraseCredentialPHIDType.php',
'PassphraseCredentialPublicController' => 'applications/passphrase/controller/PassphraseCredentialPublicController.php',
'PassphraseCredentialQuery' => 'applications/passphrase/query/PassphraseCredentialQuery.php',
'PassphraseCredentialRevealController' => 'applications/passphrase/controller/PassphraseCredentialRevealController.php',
'PassphraseCredentialSearchEngine' => 'applications/passphrase/query/PassphraseCredentialSearchEngine.php',
'PassphraseCredentialSecretIDTransaction' => 'applications/passphrase/xaction/PassphraseCredentialSecretIDTransaction.php',
'PassphraseCredentialTransaction' => 'applications/passphrase/storage/PassphraseCredentialTransaction.php',
'PassphraseCredentialTransactionEditor' => 'applications/passphrase/editor/PassphraseCredentialTransactionEditor.php',
'PassphraseCredentialTransactionQuery' => 'applications/passphrase/query/PassphraseCredentialTransactionQuery.php',
'PassphraseCredentialTransactionType' => 'applications/passphrase/xaction/PassphraseCredentialTransactionType.php',
'PassphraseCredentialType' => 'applications/passphrase/credentialtype/PassphraseCredentialType.php',
'PassphraseCredentialTypeTestCase' => 'applications/passphrase/credentialtype/__tests__/PassphraseCredentialTypeTestCase.php',
'PassphraseCredentialUsernameTransaction' => 'applications/passphrase/xaction/PassphraseCredentialUsernameTransaction.php',
'PassphraseCredentialViewController' => 'applications/passphrase/controller/PassphraseCredentialViewController.php',
'PassphraseDAO' => 'applications/passphrase/storage/PassphraseDAO.php',
'PassphraseDefaultEditCapability' => 'applications/passphrase/capability/PassphraseDefaultEditCapability.php',
'PassphraseDefaultViewCapability' => 'applications/passphrase/capability/PassphraseDefaultViewCapability.php',
'PassphraseNoteCredentialType' => 'applications/passphrase/credentialtype/PassphraseNoteCredentialType.php',
'PassphrasePasswordCredentialType' => 'applications/passphrase/credentialtype/PassphrasePasswordCredentialType.php',
'PassphrasePasswordKey' => 'applications/passphrase/keys/PassphrasePasswordKey.php',
'PassphraseQueryConduitAPIMethod' => 'applications/passphrase/conduit/PassphraseQueryConduitAPIMethod.php',
'PassphraseRemarkupRule' => 'applications/passphrase/remarkup/PassphraseRemarkupRule.php',
'PassphraseSSHGeneratedKeyCredentialType' => 'applications/passphrase/credentialtype/PassphraseSSHGeneratedKeyCredentialType.php',
'PassphraseSSHKey' => 'applications/passphrase/keys/PassphraseSSHKey.php',
'PassphraseSSHPrivateKeyCredentialType' => 'applications/passphrase/credentialtype/PassphraseSSHPrivateKeyCredentialType.php',
'PassphraseSSHPrivateKeyFileCredentialType' => 'applications/passphrase/credentialtype/PassphraseSSHPrivateKeyFileCredentialType.php',
'PassphraseSSHPrivateKeyTextCredentialType' => 'applications/passphrase/credentialtype/PassphraseSSHPrivateKeyTextCredentialType.php',
'PassphraseSchemaSpec' => 'applications/passphrase/storage/PassphraseSchemaSpec.php',
'PassphraseSecret' => 'applications/passphrase/storage/PassphraseSecret.php',
'PassphraseTokenCredentialType' => 'applications/passphrase/credentialtype/PassphraseTokenCredentialType.php',
'PasteConduitAPIMethod' => 'applications/paste/conduit/PasteConduitAPIMethod.php',
'PasteCreateConduitAPIMethod' => 'applications/paste/conduit/PasteCreateConduitAPIMethod.php',
'PasteCreateMailReceiver' => 'applications/paste/mail/PasteCreateMailReceiver.php',
'PasteDefaultEditCapability' => 'applications/paste/capability/PasteDefaultEditCapability.php',
'PasteDefaultViewCapability' => 'applications/paste/capability/PasteDefaultViewCapability.php',
'PasteEditConduitAPIMethod' => 'applications/paste/conduit/PasteEditConduitAPIMethod.php',
'PasteEmbedView' => 'applications/paste/view/PasteEmbedView.php',
'PasteInfoConduitAPIMethod' => 'applications/paste/conduit/PasteInfoConduitAPIMethod.php',
'PasteLanguageSelectDatasource' => 'applications/paste/typeahead/PasteLanguageSelectDatasource.php',
'PasteMailReceiver' => 'applications/paste/mail/PasteMailReceiver.php',
'PasteQueryConduitAPIMethod' => 'applications/paste/conduit/PasteQueryConduitAPIMethod.php',
'PasteReplyHandler' => 'applications/paste/mail/PasteReplyHandler.php',
'PasteSearchConduitAPIMethod' => 'applications/paste/conduit/PasteSearchConduitAPIMethod.php',
'PeopleBrowseUserDirectoryCapability' => 'applications/people/capability/PeopleBrowseUserDirectoryCapability.php',
'PeopleCreateUsersCapability' => 'applications/people/capability/PeopleCreateUsersCapability.php',
'PeopleDisableUsersCapability' => 'applications/people/capability/PeopleDisableUsersCapability.php',
'PeopleHovercardEngineExtension' => 'applications/people/engineextension/PeopleHovercardEngineExtension.php',
'PeopleMainMenuBarExtension' => 'applications/people/engineextension/PeopleMainMenuBarExtension.php',
'PeopleUserLogGarbageCollector' => 'applications/people/garbagecollector/PeopleUserLogGarbageCollector.php',
'Phabricator404Controller' => 'applications/base/controller/Phabricator404Controller.php',
'PhabricatorAWSConfigOptions' => 'applications/config/option/PhabricatorAWSConfigOptions.php',
'PhabricatorAccessControlTestCase' => 'applications/base/controller/__tests__/PhabricatorAccessControlTestCase.php',
'PhabricatorAccessLog' => 'infrastructure/log/PhabricatorAccessLog.php',
'PhabricatorAccessLogConfigOptions' => 'applications/config/option/PhabricatorAccessLogConfigOptions.php',
'PhabricatorAccessibilitySetting' => 'applications/settings/setting/PhabricatorAccessibilitySetting.php',
- 'PhabricatorAccountSettingsPanel' => 'applications/settings/panel/PhabricatorAccountSettingsPanel.php',
'PhabricatorActionListView' => 'view/layout/PhabricatorActionListView.php',
'PhabricatorActionView' => 'view/layout/PhabricatorActionView.php',
'PhabricatorActivitySettingsPanel' => 'applications/settings/panel/PhabricatorActivitySettingsPanel.php',
'PhabricatorAdministratorsPolicyRule' => 'applications/people/policyrule/PhabricatorAdministratorsPolicyRule.php',
'PhabricatorAjaxRequestExceptionHandler' => 'aphront/handler/PhabricatorAjaxRequestExceptionHandler.php',
'PhabricatorAlmanacApplication' => 'applications/almanac/application/PhabricatorAlmanacApplication.php',
'PhabricatorAmazonAuthProvider' => 'applications/auth/provider/PhabricatorAmazonAuthProvider.php',
+ 'PhabricatorAmazonSNSFuture' => 'applications/metamta/future/PhabricatorAmazonSNSFuture.php',
'PhabricatorAnchorView' => 'view/layout/PhabricatorAnchorView.php',
'PhabricatorAphlictManagementDebugWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementDebugWorkflow.php',
'PhabricatorAphlictManagementNotifyWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementNotifyWorkflow.php',
'PhabricatorAphlictManagementRestartWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementRestartWorkflow.php',
'PhabricatorAphlictManagementStartWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementStartWorkflow.php',
'PhabricatorAphlictManagementStatusWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementStatusWorkflow.php',
'PhabricatorAphlictManagementStopWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementStopWorkflow.php',
'PhabricatorAphlictManagementWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php',
'PhabricatorAphlictSetupCheck' => 'applications/notification/setup/PhabricatorAphlictSetupCheck.php',
'PhabricatorAphrontBarUIExample' => 'applications/uiexample/examples/PhabricatorAphrontBarUIExample.php',
'PhabricatorAphrontViewTestCase' => 'view/__tests__/PhabricatorAphrontViewTestCase.php',
'PhabricatorAppSearchEngine' => 'applications/meta/query/PhabricatorAppSearchEngine.php',
'PhabricatorApplication' => 'applications/base/PhabricatorApplication.php',
'PhabricatorApplicationApplicationPHIDType' => 'applications/meta/phid/PhabricatorApplicationApplicationPHIDType.php',
'PhabricatorApplicationApplicationTransaction' => 'applications/meta/storage/PhabricatorApplicationApplicationTransaction.php',
'PhabricatorApplicationApplicationTransactionQuery' => 'applications/meta/query/PhabricatorApplicationApplicationTransactionQuery.php',
'PhabricatorApplicationConfigOptions' => 'applications/config/option/PhabricatorApplicationConfigOptions.php',
'PhabricatorApplicationConfigurationPanel' => 'applications/meta/panel/PhabricatorApplicationConfigurationPanel.php',
'PhabricatorApplicationConfigurationPanelTestCase' => 'applications/meta/panel/__tests__/PhabricatorApplicationConfigurationPanelTestCase.php',
'PhabricatorApplicationDatasource' => 'applications/meta/typeahead/PhabricatorApplicationDatasource.php',
'PhabricatorApplicationDetailViewController' => 'applications/meta/controller/PhabricatorApplicationDetailViewController.php',
'PhabricatorApplicationEditController' => 'applications/meta/controller/PhabricatorApplicationEditController.php',
'PhabricatorApplicationEditEngine' => 'applications/meta/editor/PhabricatorApplicationEditEngine.php',
'PhabricatorApplicationEditHTTPParameterHelpView' => 'applications/transactions/view/PhabricatorApplicationEditHTTPParameterHelpView.php',
'PhabricatorApplicationEditor' => 'applications/meta/editor/PhabricatorApplicationEditor.php',
'PhabricatorApplicationEmailCommandsController' => 'applications/meta/controller/PhabricatorApplicationEmailCommandsController.php',
+ 'PhabricatorApplicationMailReceiver' => 'applications/metamta/receiver/PhabricatorApplicationMailReceiver.php',
'PhabricatorApplicationObjectMailEngineExtension' => 'applications/transactions/engineextension/PhabricatorApplicationObjectMailEngineExtension.php',
'PhabricatorApplicationPanelController' => 'applications/meta/controller/PhabricatorApplicationPanelController.php',
'PhabricatorApplicationPolicyChangeTransaction' => 'applications/meta/xactions/PhabricatorApplicationPolicyChangeTransaction.php',
'PhabricatorApplicationProfileMenuItem' => 'applications/search/menuitem/PhabricatorApplicationProfileMenuItem.php',
'PhabricatorApplicationQuery' => 'applications/meta/query/PhabricatorApplicationQuery.php',
'PhabricatorApplicationSchemaSpec' => 'applications/meta/storage/PhabricatorApplicationSchemaSpec.php',
'PhabricatorApplicationSearchController' => 'applications/search/controller/PhabricatorApplicationSearchController.php',
'PhabricatorApplicationSearchEngine' => 'applications/search/engine/PhabricatorApplicationSearchEngine.php',
'PhabricatorApplicationSearchEngineTestCase' => 'applications/search/engine/__tests__/PhabricatorApplicationSearchEngineTestCase.php',
'PhabricatorApplicationSearchResultView' => 'applications/search/view/PhabricatorApplicationSearchResultView.php',
'PhabricatorApplicationTestCase' => 'applications/base/__tests__/PhabricatorApplicationTestCase.php',
'PhabricatorApplicationTransaction' => 'applications/transactions/storage/PhabricatorApplicationTransaction.php',
'PhabricatorApplicationTransactionComment' => 'applications/transactions/storage/PhabricatorApplicationTransactionComment.php',
'PhabricatorApplicationTransactionCommentEditController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php',
'PhabricatorApplicationTransactionCommentEditor' => 'applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php',
'PhabricatorApplicationTransactionCommentHistoryController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentHistoryController.php',
'PhabricatorApplicationTransactionCommentQuery' => 'applications/transactions/query/PhabricatorApplicationTransactionCommentQuery.php',
'PhabricatorApplicationTransactionCommentQuoteController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentQuoteController.php',
'PhabricatorApplicationTransactionCommentRawController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentRawController.php',
'PhabricatorApplicationTransactionCommentRemoveController' => 'applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php',
'PhabricatorApplicationTransactionCommentView' => 'applications/transactions/view/PhabricatorApplicationTransactionCommentView.php',
'PhabricatorApplicationTransactionController' => 'applications/transactions/controller/PhabricatorApplicationTransactionController.php',
'PhabricatorApplicationTransactionDetailController' => 'applications/transactions/controller/PhabricatorApplicationTransactionDetailController.php',
'PhabricatorApplicationTransactionEditor' => 'applications/transactions/editor/PhabricatorApplicationTransactionEditor.php',
'PhabricatorApplicationTransactionFeedStory' => 'applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php',
'PhabricatorApplicationTransactionInterface' => 'applications/transactions/interface/PhabricatorApplicationTransactionInterface.php',
'PhabricatorApplicationTransactionNoEffectException' => 'applications/transactions/exception/PhabricatorApplicationTransactionNoEffectException.php',
'PhabricatorApplicationTransactionNoEffectResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionNoEffectResponse.php',
'PhabricatorApplicationTransactionPublishWorker' => 'applications/transactions/worker/PhabricatorApplicationTransactionPublishWorker.php',
'PhabricatorApplicationTransactionQuery' => 'applications/transactions/query/PhabricatorApplicationTransactionQuery.php',
'PhabricatorApplicationTransactionRemarkupPreviewController' => 'applications/transactions/controller/PhabricatorApplicationTransactionRemarkupPreviewController.php',
'PhabricatorApplicationTransactionReplyHandler' => 'applications/transactions/replyhandler/PhabricatorApplicationTransactionReplyHandler.php',
'PhabricatorApplicationTransactionResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionResponse.php',
'PhabricatorApplicationTransactionShowOlderController' => 'applications/transactions/controller/PhabricatorApplicationTransactionShowOlderController.php',
'PhabricatorApplicationTransactionStructureException' => 'applications/transactions/exception/PhabricatorApplicationTransactionStructureException.php',
'PhabricatorApplicationTransactionTemplatedCommentQuery' => 'applications/transactions/query/PhabricatorApplicationTransactionTemplatedCommentQuery.php',
'PhabricatorApplicationTransactionTextDiffDetailView' => 'applications/transactions/view/PhabricatorApplicationTransactionTextDiffDetailView.php',
'PhabricatorApplicationTransactionTransactionPHIDType' => 'applications/transactions/phid/PhabricatorApplicationTransactionTransactionPHIDType.php',
'PhabricatorApplicationTransactionType' => 'applications/meta/xactions/PhabricatorApplicationTransactionType.php',
'PhabricatorApplicationTransactionValidationError' => 'applications/transactions/error/PhabricatorApplicationTransactionValidationError.php',
'PhabricatorApplicationTransactionValidationException' => 'applications/transactions/exception/PhabricatorApplicationTransactionValidationException.php',
'PhabricatorApplicationTransactionValidationResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionValidationResponse.php',
'PhabricatorApplicationTransactionValueController' => 'applications/transactions/controller/PhabricatorApplicationTransactionValueController.php',
'PhabricatorApplicationTransactionView' => 'applications/transactions/view/PhabricatorApplicationTransactionView.php',
'PhabricatorApplicationTransactionWarningException' => 'applications/transactions/exception/PhabricatorApplicationTransactionWarningException.php',
'PhabricatorApplicationTransactionWarningResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionWarningResponse.php',
'PhabricatorApplicationUninstallController' => 'applications/meta/controller/PhabricatorApplicationUninstallController.php',
'PhabricatorApplicationUninstallTransaction' => 'applications/meta/xactions/PhabricatorApplicationUninstallTransaction.php',
'PhabricatorApplicationsApplication' => 'applications/meta/application/PhabricatorApplicationsApplication.php',
'PhabricatorApplicationsController' => 'applications/meta/controller/PhabricatorApplicationsController.php',
'PhabricatorApplicationsListController' => 'applications/meta/controller/PhabricatorApplicationsListController.php',
'PhabricatorApplyEditField' => 'applications/transactions/editfield/PhabricatorApplyEditField.php',
'PhabricatorAsanaAuthProvider' => 'applications/auth/provider/PhabricatorAsanaAuthProvider.php',
'PhabricatorAsanaConfigOptions' => 'applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php',
'PhabricatorAsanaSubtaskHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorAsanaSubtaskHasObjectEdgeType.php',
'PhabricatorAsanaTaskHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorAsanaTaskHasObjectEdgeType.php',
'PhabricatorAudioDocumentEngine' => 'applications/files/document/PhabricatorAudioDocumentEngine.php',
'PhabricatorAuditActionConstants' => 'applications/audit/constants/PhabricatorAuditActionConstants.php',
'PhabricatorAuditApplication' => 'applications/audit/application/PhabricatorAuditApplication.php',
'PhabricatorAuditCommentEditor' => 'applications/audit/editor/PhabricatorAuditCommentEditor.php',
'PhabricatorAuditController' => 'applications/audit/controller/PhabricatorAuditController.php',
'PhabricatorAuditEditor' => 'applications/audit/editor/PhabricatorAuditEditor.php',
'PhabricatorAuditInlineComment' => 'applications/audit/storage/PhabricatorAuditInlineComment.php',
'PhabricatorAuditListView' => 'applications/audit/view/PhabricatorAuditListView.php',
'PhabricatorAuditMailReceiver' => 'applications/audit/mail/PhabricatorAuditMailReceiver.php',
'PhabricatorAuditManagementDeleteWorkflow' => 'applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php',
'PhabricatorAuditManagementWorkflow' => 'applications/audit/management/PhabricatorAuditManagementWorkflow.php',
'PhabricatorAuditReplyHandler' => 'applications/audit/mail/PhabricatorAuditReplyHandler.php',
'PhabricatorAuditStatusConstants' => 'applications/audit/constants/PhabricatorAuditStatusConstants.php',
'PhabricatorAuditSynchronizeManagementWorkflow' => 'applications/audit/management/PhabricatorAuditSynchronizeManagementWorkflow.php',
'PhabricatorAuditTransaction' => 'applications/audit/storage/PhabricatorAuditTransaction.php',
'PhabricatorAuditTransactionComment' => 'applications/audit/storage/PhabricatorAuditTransactionComment.php',
'PhabricatorAuditTransactionQuery' => 'applications/audit/query/PhabricatorAuditTransactionQuery.php',
'PhabricatorAuditTransactionView' => 'applications/audit/view/PhabricatorAuditTransactionView.php',
'PhabricatorAuditUpdateOwnersManagementWorkflow' => 'applications/audit/management/PhabricatorAuditUpdateOwnersManagementWorkflow.php',
'PhabricatorAuthAccountView' => 'applications/auth/view/PhabricatorAuthAccountView.php',
'PhabricatorAuthApplication' => 'applications/auth/application/PhabricatorAuthApplication.php',
'PhabricatorAuthAuthFactorPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthFactorPHIDType.php',
+ 'PhabricatorAuthAuthFactorProviderPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthFactorProviderPHIDType.php',
'PhabricatorAuthAuthProviderPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthProviderPHIDType.php',
+ 'PhabricatorAuthCSRFEngine' => 'applications/auth/engine/PhabricatorAuthCSRFEngine.php',
+ 'PhabricatorAuthChallenge' => 'applications/auth/storage/PhabricatorAuthChallenge.php',
+ 'PhabricatorAuthChallengeGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthChallengeGarbageCollector.php',
+ 'PhabricatorAuthChallengePHIDType' => 'applications/auth/phid/PhabricatorAuthChallengePHIDType.php',
+ 'PhabricatorAuthChallengeQuery' => 'applications/auth/query/PhabricatorAuthChallengeQuery.php',
'PhabricatorAuthChangePasswordAction' => 'applications/auth/action/PhabricatorAuthChangePasswordAction.php',
'PhabricatorAuthConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthConduitAPIMethod.php',
'PhabricatorAuthConduitTokenRevoker' => 'applications/auth/revoker/PhabricatorAuthConduitTokenRevoker.php',
'PhabricatorAuthConfirmLinkController' => 'applications/auth/controller/PhabricatorAuthConfirmLinkController.php',
+ 'PhabricatorAuthContactNumber' => 'applications/auth/storage/PhabricatorAuthContactNumber.php',
+ 'PhabricatorAuthContactNumberController' => 'applications/auth/controller/contact/PhabricatorAuthContactNumberController.php',
+ 'PhabricatorAuthContactNumberDisableController' => 'applications/auth/controller/contact/PhabricatorAuthContactNumberDisableController.php',
+ 'PhabricatorAuthContactNumberEditController' => 'applications/auth/controller/contact/PhabricatorAuthContactNumberEditController.php',
+ 'PhabricatorAuthContactNumberEditEngine' => 'applications/auth/editor/PhabricatorAuthContactNumberEditEngine.php',
+ 'PhabricatorAuthContactNumberEditor' => 'applications/auth/editor/PhabricatorAuthContactNumberEditor.php',
+ 'PhabricatorAuthContactNumberMFAEngine' => 'applications/auth/engine/PhabricatorAuthContactNumberMFAEngine.php',
+ 'PhabricatorAuthContactNumberNumberTransaction' => 'applications/auth/xaction/PhabricatorAuthContactNumberNumberTransaction.php',
+ 'PhabricatorAuthContactNumberPHIDType' => 'applications/auth/phid/PhabricatorAuthContactNumberPHIDType.php',
+ 'PhabricatorAuthContactNumberPrimaryController' => 'applications/auth/controller/contact/PhabricatorAuthContactNumberPrimaryController.php',
+ 'PhabricatorAuthContactNumberPrimaryTransaction' => 'applications/auth/xaction/PhabricatorAuthContactNumberPrimaryTransaction.php',
+ 'PhabricatorAuthContactNumberQuery' => 'applications/auth/query/PhabricatorAuthContactNumberQuery.php',
+ 'PhabricatorAuthContactNumberStatusTransaction' => 'applications/auth/xaction/PhabricatorAuthContactNumberStatusTransaction.php',
+ 'PhabricatorAuthContactNumberTestController' => 'applications/auth/controller/contact/PhabricatorAuthContactNumberTestController.php',
+ 'PhabricatorAuthContactNumberTransaction' => 'applications/auth/storage/PhabricatorAuthContactNumberTransaction.php',
+ 'PhabricatorAuthContactNumberTransactionQuery' => 'applications/auth/query/PhabricatorAuthContactNumberTransactionQuery.php',
+ 'PhabricatorAuthContactNumberTransactionType' => 'applications/auth/xaction/PhabricatorAuthContactNumberTransactionType.php',
+ 'PhabricatorAuthContactNumberViewController' => 'applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php',
'PhabricatorAuthController' => 'applications/auth/controller/PhabricatorAuthController.php',
'PhabricatorAuthDAO' => 'applications/auth/storage/PhabricatorAuthDAO.php',
'PhabricatorAuthDisableController' => 'applications/auth/controller/config/PhabricatorAuthDisableController.php',
'PhabricatorAuthDowngradeSessionController' => 'applications/auth/controller/PhabricatorAuthDowngradeSessionController.php',
'PhabricatorAuthEditController' => 'applications/auth/controller/config/PhabricatorAuthEditController.php',
'PhabricatorAuthFactor' => 'applications/auth/factor/PhabricatorAuthFactor.php',
'PhabricatorAuthFactorConfig' => 'applications/auth/storage/PhabricatorAuthFactorConfig.php',
+ 'PhabricatorAuthFactorConfigQuery' => 'applications/auth/query/PhabricatorAuthFactorConfigQuery.php',
+ 'PhabricatorAuthFactorProvider' => 'applications/auth/storage/PhabricatorAuthFactorProvider.php',
+ 'PhabricatorAuthFactorProviderController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderController.php',
+ 'PhabricatorAuthFactorProviderDuoCredentialTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderDuoCredentialTransaction.php',
+ 'PhabricatorAuthFactorProviderDuoEnrollTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderDuoEnrollTransaction.php',
+ 'PhabricatorAuthFactorProviderDuoHostnameTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderDuoHostnameTransaction.php',
+ 'PhabricatorAuthFactorProviderDuoUsernamesTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderDuoUsernamesTransaction.php',
+ 'PhabricatorAuthFactorProviderEditController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php',
+ 'PhabricatorAuthFactorProviderEditEngine' => 'applications/auth/editor/PhabricatorAuthFactorProviderEditEngine.php',
+ 'PhabricatorAuthFactorProviderEditor' => 'applications/auth/editor/PhabricatorAuthFactorProviderEditor.php',
+ 'PhabricatorAuthFactorProviderEnrollMessageTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderEnrollMessageTransaction.php',
+ 'PhabricatorAuthFactorProviderListController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderListController.php',
+ 'PhabricatorAuthFactorProviderMFAEngine' => 'applications/auth/engine/PhabricatorAuthFactorProviderMFAEngine.php',
+ 'PhabricatorAuthFactorProviderMessageController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderMessageController.php',
+ 'PhabricatorAuthFactorProviderNameTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderNameTransaction.php',
+ 'PhabricatorAuthFactorProviderQuery' => 'applications/auth/query/PhabricatorAuthFactorProviderQuery.php',
+ 'PhabricatorAuthFactorProviderStatus' => 'applications/auth/constants/PhabricatorAuthFactorProviderStatus.php',
+ 'PhabricatorAuthFactorProviderStatusTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderStatusTransaction.php',
+ 'PhabricatorAuthFactorProviderTransaction' => 'applications/auth/storage/PhabricatorAuthFactorProviderTransaction.php',
+ 'PhabricatorAuthFactorProviderTransactionQuery' => 'applications/auth/query/PhabricatorAuthFactorProviderTransactionQuery.php',
+ 'PhabricatorAuthFactorProviderTransactionType' => 'applications/auth/xaction/PhabricatorAuthFactorProviderTransactionType.php',
+ 'PhabricatorAuthFactorProviderViewController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php',
+ 'PhabricatorAuthFactorResult' => 'applications/auth/factor/PhabricatorAuthFactorResult.php',
'PhabricatorAuthFactorTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthFactorTestCase.php',
'PhabricatorAuthFinishController' => 'applications/auth/controller/PhabricatorAuthFinishController.php',
'PhabricatorAuthHMACKey' => 'applications/auth/storage/PhabricatorAuthHMACKey.php',
'PhabricatorAuthHighSecurityRequiredException' => 'applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php',
'PhabricatorAuthHighSecurityToken' => 'applications/auth/data/PhabricatorAuthHighSecurityToken.php',
'PhabricatorAuthInvite' => 'applications/auth/storage/PhabricatorAuthInvite.php',
'PhabricatorAuthInviteAccountException' => 'applications/auth/exception/PhabricatorAuthInviteAccountException.php',
'PhabricatorAuthInviteAction' => 'applications/auth/data/PhabricatorAuthInviteAction.php',
'PhabricatorAuthInviteActionTableView' => 'applications/auth/view/PhabricatorAuthInviteActionTableView.php',
'PhabricatorAuthInviteController' => 'applications/auth/controller/PhabricatorAuthInviteController.php',
'PhabricatorAuthInviteDialogException' => 'applications/auth/exception/PhabricatorAuthInviteDialogException.php',
'PhabricatorAuthInviteEngine' => 'applications/auth/engine/PhabricatorAuthInviteEngine.php',
'PhabricatorAuthInviteException' => 'applications/auth/exception/PhabricatorAuthInviteException.php',
'PhabricatorAuthInviteInvalidException' => 'applications/auth/exception/PhabricatorAuthInviteInvalidException.php',
'PhabricatorAuthInviteLoginException' => 'applications/auth/exception/PhabricatorAuthInviteLoginException.php',
'PhabricatorAuthInvitePHIDType' => 'applications/auth/phid/PhabricatorAuthInvitePHIDType.php',
'PhabricatorAuthInviteQuery' => 'applications/auth/query/PhabricatorAuthInviteQuery.php',
'PhabricatorAuthInviteRegisteredException' => 'applications/auth/exception/PhabricatorAuthInviteRegisteredException.php',
'PhabricatorAuthInviteSearchEngine' => 'applications/auth/query/PhabricatorAuthInviteSearchEngine.php',
'PhabricatorAuthInviteTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthInviteTestCase.php',
'PhabricatorAuthInviteVerifyException' => 'applications/auth/exception/PhabricatorAuthInviteVerifyException.php',
'PhabricatorAuthInviteWorker' => 'applications/auth/worker/PhabricatorAuthInviteWorker.php',
'PhabricatorAuthLinkController' => 'applications/auth/controller/PhabricatorAuthLinkController.php',
'PhabricatorAuthListController' => 'applications/auth/controller/config/PhabricatorAuthListController.php',
'PhabricatorAuthLoginController' => 'applications/auth/controller/PhabricatorAuthLoginController.php',
'PhabricatorAuthLoginHandler' => 'applications/auth/handler/PhabricatorAuthLoginHandler.php',
+ 'PhabricatorAuthLoginMessageType' => 'applications/auth/message/PhabricatorAuthLoginMessageType.php',
'PhabricatorAuthLogoutConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthLogoutConduitAPIMethod.php',
+ 'PhabricatorAuthMFAEditEngineExtension' => 'applications/auth/engineextension/PhabricatorAuthMFAEditEngineExtension.php',
+ 'PhabricatorAuthMFASyncTemporaryTokenType' => 'applications/auth/factor/PhabricatorAuthMFASyncTemporaryTokenType.php',
'PhabricatorAuthMainMenuBarExtension' => 'applications/auth/extension/PhabricatorAuthMainMenuBarExtension.php',
'PhabricatorAuthManagementCachePKCS8Workflow' => 'applications/auth/management/PhabricatorAuthManagementCachePKCS8Workflow.php',
'PhabricatorAuthManagementLDAPWorkflow' => 'applications/auth/management/PhabricatorAuthManagementLDAPWorkflow.php',
'PhabricatorAuthManagementListFactorsWorkflow' => 'applications/auth/management/PhabricatorAuthManagementListFactorsWorkflow.php',
+ 'PhabricatorAuthManagementListMFAProvidersWorkflow' => 'applications/auth/management/PhabricatorAuthManagementListMFAProvidersWorkflow.php',
'PhabricatorAuthManagementRecoverWorkflow' => 'applications/auth/management/PhabricatorAuthManagementRecoverWorkflow.php',
'PhabricatorAuthManagementRefreshWorkflow' => 'applications/auth/management/PhabricatorAuthManagementRefreshWorkflow.php',
'PhabricatorAuthManagementRevokeWorkflow' => 'applications/auth/management/PhabricatorAuthManagementRevokeWorkflow.php',
'PhabricatorAuthManagementStripWorkflow' => 'applications/auth/management/PhabricatorAuthManagementStripWorkflow.php',
'PhabricatorAuthManagementTrustOAuthClientWorkflow' => 'applications/auth/management/PhabricatorAuthManagementTrustOAuthClientWorkflow.php',
'PhabricatorAuthManagementUnlimitWorkflow' => 'applications/auth/management/PhabricatorAuthManagementUnlimitWorkflow.php',
'PhabricatorAuthManagementUntrustOAuthClientWorkflow' => 'applications/auth/management/PhabricatorAuthManagementUntrustOAuthClientWorkflow.php',
'PhabricatorAuthManagementVerifyWorkflow' => 'applications/auth/management/PhabricatorAuthManagementVerifyWorkflow.php',
'PhabricatorAuthManagementWorkflow' => 'applications/auth/management/PhabricatorAuthManagementWorkflow.php',
+ 'PhabricatorAuthMessage' => 'applications/auth/storage/PhabricatorAuthMessage.php',
+ 'PhabricatorAuthMessageController' => 'applications/auth/controller/message/PhabricatorAuthMessageController.php',
+ 'PhabricatorAuthMessageEditController' => 'applications/auth/controller/message/PhabricatorAuthMessageEditController.php',
+ 'PhabricatorAuthMessageEditEngine' => 'applications/auth/editor/PhabricatorAuthMessageEditEngine.php',
+ 'PhabricatorAuthMessageEditor' => 'applications/auth/editor/PhabricatorAuthMessageEditor.php',
+ 'PhabricatorAuthMessageListController' => 'applications/auth/controller/message/PhabricatorAuthMessageListController.php',
+ 'PhabricatorAuthMessagePHIDType' => 'applications/auth/phid/PhabricatorAuthMessagePHIDType.php',
+ 'PhabricatorAuthMessageQuery' => 'applications/auth/query/PhabricatorAuthMessageQuery.php',
+ 'PhabricatorAuthMessageTextTransaction' => 'applications/auth/xaction/PhabricatorAuthMessageTextTransaction.php',
+ 'PhabricatorAuthMessageTransaction' => 'applications/auth/storage/PhabricatorAuthMessageTransaction.php',
+ 'PhabricatorAuthMessageTransactionQuery' => 'applications/auth/query/PhabricatorAuthMessageTransactionQuery.php',
+ 'PhabricatorAuthMessageTransactionType' => 'applications/auth/xaction/PhabricatorAuthMessageTransactionType.php',
+ 'PhabricatorAuthMessageType' => 'applications/auth/message/PhabricatorAuthMessageType.php',
+ 'PhabricatorAuthMessageViewController' => 'applications/auth/controller/message/PhabricatorAuthMessageViewController.php',
'PhabricatorAuthNeedsApprovalController' => 'applications/auth/controller/PhabricatorAuthNeedsApprovalController.php',
'PhabricatorAuthNeedsMultiFactorController' => 'applications/auth/controller/PhabricatorAuthNeedsMultiFactorController.php',
'PhabricatorAuthNewController' => 'applications/auth/controller/config/PhabricatorAuthNewController.php',
+ 'PhabricatorAuthNewFactorAction' => 'applications/auth/action/PhabricatorAuthNewFactorAction.php',
'PhabricatorAuthOldOAuthRedirectController' => 'applications/auth/controller/PhabricatorAuthOldOAuthRedirectController.php',
'PhabricatorAuthOneTimeLoginController' => 'applications/auth/controller/PhabricatorAuthOneTimeLoginController.php',
'PhabricatorAuthOneTimeLoginTemporaryTokenType' => 'applications/auth/tokentype/PhabricatorAuthOneTimeLoginTemporaryTokenType.php',
'PhabricatorAuthPassword' => 'applications/auth/storage/PhabricatorAuthPassword.php',
'PhabricatorAuthPasswordEditor' => 'applications/auth/editor/PhabricatorAuthPasswordEditor.php',
'PhabricatorAuthPasswordEngine' => 'applications/auth/engine/PhabricatorAuthPasswordEngine.php',
'PhabricatorAuthPasswordException' => 'applications/auth/password/PhabricatorAuthPasswordException.php',
'PhabricatorAuthPasswordHashInterface' => 'applications/auth/password/PhabricatorAuthPasswordHashInterface.php',
'PhabricatorAuthPasswordPHIDType' => 'applications/auth/phid/PhabricatorAuthPasswordPHIDType.php',
'PhabricatorAuthPasswordQuery' => 'applications/auth/query/PhabricatorAuthPasswordQuery.php',
'PhabricatorAuthPasswordResetTemporaryTokenType' => 'applications/auth/tokentype/PhabricatorAuthPasswordResetTemporaryTokenType.php',
'PhabricatorAuthPasswordRevokeTransaction' => 'applications/auth/xaction/PhabricatorAuthPasswordRevokeTransaction.php',
'PhabricatorAuthPasswordRevoker' => 'applications/auth/revoker/PhabricatorAuthPasswordRevoker.php',
'PhabricatorAuthPasswordTestCase' => 'applications/auth/__tests__/PhabricatorAuthPasswordTestCase.php',
'PhabricatorAuthPasswordTransaction' => 'applications/auth/storage/PhabricatorAuthPasswordTransaction.php',
'PhabricatorAuthPasswordTransactionQuery' => 'applications/auth/query/PhabricatorAuthPasswordTransactionQuery.php',
'PhabricatorAuthPasswordTransactionType' => 'applications/auth/xaction/PhabricatorAuthPasswordTransactionType.php',
'PhabricatorAuthPasswordUpgradeTransaction' => 'applications/auth/xaction/PhabricatorAuthPasswordUpgradeTransaction.php',
'PhabricatorAuthProvider' => 'applications/auth/provider/PhabricatorAuthProvider.php',
'PhabricatorAuthProviderConfig' => 'applications/auth/storage/PhabricatorAuthProviderConfig.php',
'PhabricatorAuthProviderConfigController' => 'applications/auth/controller/config/PhabricatorAuthProviderConfigController.php',
'PhabricatorAuthProviderConfigEditor' => 'applications/auth/editor/PhabricatorAuthProviderConfigEditor.php',
'PhabricatorAuthProviderConfigQuery' => 'applications/auth/query/PhabricatorAuthProviderConfigQuery.php',
'PhabricatorAuthProviderConfigTransaction' => 'applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php',
'PhabricatorAuthProviderConfigTransactionQuery' => 'applications/auth/query/PhabricatorAuthProviderConfigTransactionQuery.php',
+ 'PhabricatorAuthProviderController' => 'applications/auth/controller/config/PhabricatorAuthProviderController.php',
'PhabricatorAuthProvidersGuidanceContext' => 'applications/auth/guidance/PhabricatorAuthProvidersGuidanceContext.php',
'PhabricatorAuthProvidersGuidanceEngineExtension' => 'applications/auth/guidance/PhabricatorAuthProvidersGuidanceEngineExtension.php',
'PhabricatorAuthQueryPublicKeysConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthQueryPublicKeysConduitAPIMethod.php',
'PhabricatorAuthRegisterController' => 'applications/auth/controller/PhabricatorAuthRegisterController.php',
'PhabricatorAuthRevokeTokenController' => 'applications/auth/controller/PhabricatorAuthRevokeTokenController.php',
'PhabricatorAuthRevoker' => 'applications/auth/revoker/PhabricatorAuthRevoker.php',
'PhabricatorAuthSSHKey' => 'applications/auth/storage/PhabricatorAuthSSHKey.php',
'PhabricatorAuthSSHKeyController' => 'applications/auth/controller/PhabricatorAuthSSHKeyController.php',
'PhabricatorAuthSSHKeyEditController' => 'applications/auth/controller/PhabricatorAuthSSHKeyEditController.php',
'PhabricatorAuthSSHKeyEditor' => 'applications/auth/editor/PhabricatorAuthSSHKeyEditor.php',
'PhabricatorAuthSSHKeyGenerateController' => 'applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php',
'PhabricatorAuthSSHKeyListController' => 'applications/auth/controller/PhabricatorAuthSSHKeyListController.php',
'PhabricatorAuthSSHKeyPHIDType' => 'applications/auth/phid/PhabricatorAuthSSHKeyPHIDType.php',
'PhabricatorAuthSSHKeyQuery' => 'applications/auth/query/PhabricatorAuthSSHKeyQuery.php',
'PhabricatorAuthSSHKeyReplyHandler' => 'applications/auth/mail/PhabricatorAuthSSHKeyReplyHandler.php',
'PhabricatorAuthSSHKeyRevokeController' => 'applications/auth/controller/PhabricatorAuthSSHKeyRevokeController.php',
'PhabricatorAuthSSHKeySearchEngine' => 'applications/auth/query/PhabricatorAuthSSHKeySearchEngine.php',
'PhabricatorAuthSSHKeyTableView' => 'applications/auth/view/PhabricatorAuthSSHKeyTableView.php',
'PhabricatorAuthSSHKeyTestCase' => 'applications/auth/__tests__/PhabricatorAuthSSHKeyTestCase.php',
'PhabricatorAuthSSHKeyTransaction' => 'applications/auth/storage/PhabricatorAuthSSHKeyTransaction.php',
'PhabricatorAuthSSHKeyTransactionQuery' => 'applications/auth/query/PhabricatorAuthSSHKeyTransactionQuery.php',
'PhabricatorAuthSSHKeyViewController' => 'applications/auth/controller/PhabricatorAuthSSHKeyViewController.php',
'PhabricatorAuthSSHPublicKey' => 'applications/auth/sshkey/PhabricatorAuthSSHPublicKey.php',
'PhabricatorAuthSSHRevoker' => 'applications/auth/revoker/PhabricatorAuthSSHRevoker.php',
'PhabricatorAuthSession' => 'applications/auth/storage/PhabricatorAuthSession.php',
'PhabricatorAuthSessionEngine' => 'applications/auth/engine/PhabricatorAuthSessionEngine.php',
'PhabricatorAuthSessionEngineExtension' => 'applications/auth/engine/PhabricatorAuthSessionEngineExtension.php',
'PhabricatorAuthSessionEngineExtensionModule' => 'applications/auth/engine/PhabricatorAuthSessionEngineExtensionModule.php',
'PhabricatorAuthSessionGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthSessionGarbageCollector.php',
'PhabricatorAuthSessionInfo' => 'applications/auth/data/PhabricatorAuthSessionInfo.php',
'PhabricatorAuthSessionPHIDType' => 'applications/auth/phid/PhabricatorAuthSessionPHIDType.php',
'PhabricatorAuthSessionQuery' => 'applications/auth/query/PhabricatorAuthSessionQuery.php',
'PhabricatorAuthSessionRevoker' => 'applications/auth/revoker/PhabricatorAuthSessionRevoker.php',
'PhabricatorAuthSetPasswordController' => 'applications/auth/controller/PhabricatorAuthSetPasswordController.php',
'PhabricatorAuthSetupCheck' => 'applications/config/check/PhabricatorAuthSetupCheck.php',
'PhabricatorAuthStartController' => 'applications/auth/controller/PhabricatorAuthStartController.php',
- 'PhabricatorAuthTOTPKeyTemporaryTokenType' => 'applications/auth/factor/PhabricatorAuthTOTPKeyTemporaryTokenType.php',
'PhabricatorAuthTemporaryToken' => 'applications/auth/storage/PhabricatorAuthTemporaryToken.php',
'PhabricatorAuthTemporaryTokenGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthTemporaryTokenGarbageCollector.php',
'PhabricatorAuthTemporaryTokenQuery' => 'applications/auth/query/PhabricatorAuthTemporaryTokenQuery.php',
'PhabricatorAuthTemporaryTokenRevoker' => 'applications/auth/revoker/PhabricatorAuthTemporaryTokenRevoker.php',
'PhabricatorAuthTemporaryTokenType' => 'applications/auth/tokentype/PhabricatorAuthTemporaryTokenType.php',
'PhabricatorAuthTemporaryTokenTypeModule' => 'applications/auth/tokentype/PhabricatorAuthTemporaryTokenTypeModule.php',
'PhabricatorAuthTerminateSessionController' => 'applications/auth/controller/PhabricatorAuthTerminateSessionController.php',
+ 'PhabricatorAuthTestSMSAction' => 'applications/auth/action/PhabricatorAuthTestSMSAction.php',
'PhabricatorAuthTryFactorAction' => 'applications/auth/action/PhabricatorAuthTryFactorAction.php',
'PhabricatorAuthUnlinkController' => 'applications/auth/controller/PhabricatorAuthUnlinkController.php',
'PhabricatorAuthValidateController' => 'applications/auth/controller/PhabricatorAuthValidateController.php',
+ 'PhabricatorAuthWelcomeMailMessageType' => 'applications/auth/message/PhabricatorAuthWelcomeMailMessageType.php',
'PhabricatorAuthenticationConfigOptions' => 'applications/config/option/PhabricatorAuthenticationConfigOptions.php',
'PhabricatorAutoEventListener' => 'infrastructure/events/PhabricatorAutoEventListener.php',
'PhabricatorBadgesApplication' => 'applications/badges/application/PhabricatorBadgesApplication.php',
'PhabricatorBadgesArchiveController' => 'applications/badges/controller/PhabricatorBadgesArchiveController.php',
'PhabricatorBadgesAward' => 'applications/badges/storage/PhabricatorBadgesAward.php',
'PhabricatorBadgesAwardController' => 'applications/badges/controller/PhabricatorBadgesAwardController.php',
'PhabricatorBadgesAwardQuery' => 'applications/badges/query/PhabricatorBadgesAwardQuery.php',
'PhabricatorBadgesAwardTestDataGenerator' => 'applications/badges/lipsum/PhabricatorBadgesAwardTestDataGenerator.php',
'PhabricatorBadgesBadge' => 'applications/badges/storage/PhabricatorBadgesBadge.php',
'PhabricatorBadgesBadgeAwardTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeAwardTransaction.php',
'PhabricatorBadgesBadgeDescriptionTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeDescriptionTransaction.php',
'PhabricatorBadgesBadgeFlavorTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeFlavorTransaction.php',
'PhabricatorBadgesBadgeIconTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeIconTransaction.php',
'PhabricatorBadgesBadgeNameNgrams' => 'applications/badges/storage/PhabricatorBadgesBadgeNameNgrams.php',
'PhabricatorBadgesBadgeNameTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeNameTransaction.php',
'PhabricatorBadgesBadgeQualityTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeQualityTransaction.php',
'PhabricatorBadgesBadgeRevokeTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeRevokeTransaction.php',
'PhabricatorBadgesBadgeStatusTransaction' => 'applications/badges/xaction/PhabricatorBadgesBadgeStatusTransaction.php',
'PhabricatorBadgesBadgeTestDataGenerator' => 'applications/badges/lipsum/PhabricatorBadgesBadgeTestDataGenerator.php',
'PhabricatorBadgesBadgeTransactionType' => 'applications/badges/xaction/PhabricatorBadgesBadgeTransactionType.php',
'PhabricatorBadgesCommentController' => 'applications/badges/controller/PhabricatorBadgesCommentController.php',
'PhabricatorBadgesController' => 'applications/badges/controller/PhabricatorBadgesController.php',
'PhabricatorBadgesCreateCapability' => 'applications/badges/capability/PhabricatorBadgesCreateCapability.php',
'PhabricatorBadgesDAO' => 'applications/badges/storage/PhabricatorBadgesDAO.php',
'PhabricatorBadgesDatasource' => 'applications/badges/typeahead/PhabricatorBadgesDatasource.php',
'PhabricatorBadgesDefaultEditCapability' => 'applications/badges/capability/PhabricatorBadgesDefaultEditCapability.php',
'PhabricatorBadgesEditConduitAPIMethod' => 'applications/badges/conduit/PhabricatorBadgesEditConduitAPIMethod.php',
'PhabricatorBadgesEditController' => 'applications/badges/controller/PhabricatorBadgesEditController.php',
'PhabricatorBadgesEditEngine' => 'applications/badges/editor/PhabricatorBadgesEditEngine.php',
'PhabricatorBadgesEditRecipientsController' => 'applications/badges/controller/PhabricatorBadgesEditRecipientsController.php',
'PhabricatorBadgesEditor' => 'applications/badges/editor/PhabricatorBadgesEditor.php',
'PhabricatorBadgesIconSet' => 'applications/badges/icon/PhabricatorBadgesIconSet.php',
'PhabricatorBadgesListController' => 'applications/badges/controller/PhabricatorBadgesListController.php',
'PhabricatorBadgesLootContextFreeGrammar' => 'applications/badges/lipsum/PhabricatorBadgesLootContextFreeGrammar.php',
'PhabricatorBadgesMailReceiver' => 'applications/badges/mail/PhabricatorBadgesMailReceiver.php',
'PhabricatorBadgesPHIDType' => 'applications/badges/phid/PhabricatorBadgesPHIDType.php',
'PhabricatorBadgesProfileController' => 'applications/badges/controller/PhabricatorBadgesProfileController.php',
'PhabricatorBadgesQuality' => 'applications/badges/constants/PhabricatorBadgesQuality.php',
'PhabricatorBadgesQuery' => 'applications/badges/query/PhabricatorBadgesQuery.php',
'PhabricatorBadgesRecipientsController' => 'applications/badges/controller/PhabricatorBadgesRecipientsController.php',
'PhabricatorBadgesRecipientsListView' => 'applications/badges/view/PhabricatorBadgesRecipientsListView.php',
'PhabricatorBadgesRemoveRecipientsController' => 'applications/badges/controller/PhabricatorBadgesRemoveRecipientsController.php',
'PhabricatorBadgesReplyHandler' => 'applications/badges/mail/PhabricatorBadgesReplyHandler.php',
'PhabricatorBadgesSchemaSpec' => 'applications/badges/storage/PhabricatorBadgesSchemaSpec.php',
'PhabricatorBadgesSearchConduitAPIMethod' => 'applications/badges/conduit/PhabricatorBadgesSearchConduitAPIMethod.php',
'PhabricatorBadgesSearchEngine' => 'applications/badges/query/PhabricatorBadgesSearchEngine.php',
'PhabricatorBadgesTransaction' => 'applications/badges/storage/PhabricatorBadgesTransaction.php',
'PhabricatorBadgesTransactionComment' => 'applications/badges/storage/PhabricatorBadgesTransactionComment.php',
'PhabricatorBadgesTransactionQuery' => 'applications/badges/query/PhabricatorBadgesTransactionQuery.php',
'PhabricatorBadgesViewController' => 'applications/badges/controller/PhabricatorBadgesViewController.php',
'PhabricatorBarePageView' => 'view/page/PhabricatorBarePageView.php',
'PhabricatorBaseURISetupCheck' => 'applications/config/check/PhabricatorBaseURISetupCheck.php',
'PhabricatorBcryptPasswordHasher' => 'infrastructure/util/password/PhabricatorBcryptPasswordHasher.php',
'PhabricatorBinariesSetupCheck' => 'applications/config/check/PhabricatorBinariesSetupCheck.php',
'PhabricatorBitbucketAuthProvider' => 'applications/auth/provider/PhabricatorBitbucketAuthProvider.php',
'PhabricatorBoardColumnsSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorBoardColumnsSearchEngineAttachment.php',
'PhabricatorBoardLayoutEngine' => 'applications/project/engine/PhabricatorBoardLayoutEngine.php',
'PhabricatorBoardRenderingEngine' => 'applications/project/engine/PhabricatorBoardRenderingEngine.php',
'PhabricatorBoardResponseEngine' => 'applications/project/engine/PhabricatorBoardResponseEngine.php',
'PhabricatorBoolConfigType' => 'applications/config/type/PhabricatorBoolConfigType.php',
'PhabricatorBoolEditField' => 'applications/transactions/editfield/PhabricatorBoolEditField.php',
'PhabricatorBoolMailStamp' => 'applications/metamta/stamp/PhabricatorBoolMailStamp.php',
'PhabricatorBritishEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorBritishEnglishTranslation.php',
'PhabricatorBuiltinDraftEngine' => 'applications/transactions/draft/PhabricatorBuiltinDraftEngine.php',
'PhabricatorBuiltinFileCachePurger' => 'applications/cache/purger/PhabricatorBuiltinFileCachePurger.php',
'PhabricatorBuiltinPatchList' => 'infrastructure/storage/patch/PhabricatorBuiltinPatchList.php',
'PhabricatorBulkContentSource' => 'infrastructure/daemon/contentsource/PhabricatorBulkContentSource.php',
'PhabricatorBulkEditGroup' => 'applications/transactions/bulk/PhabricatorBulkEditGroup.php',
'PhabricatorBulkEngine' => 'applications/transactions/bulk/PhabricatorBulkEngine.php',
'PhabricatorBulkManagementExportWorkflow' => 'applications/transactions/bulk/management/PhabricatorBulkManagementExportWorkflow.php',
'PhabricatorBulkManagementMakeSilentWorkflow' => 'applications/transactions/bulk/management/PhabricatorBulkManagementMakeSilentWorkflow.php',
'PhabricatorBulkManagementWorkflow' => 'applications/transactions/bulk/management/PhabricatorBulkManagementWorkflow.php',
'PhabricatorCSVExportFormat' => 'infrastructure/export/format/PhabricatorCSVExportFormat.php',
'PhabricatorCacheDAO' => 'applications/cache/storage/PhabricatorCacheDAO.php',
'PhabricatorCacheEngine' => 'applications/system/engine/PhabricatorCacheEngine.php',
'PhabricatorCacheEngineExtension' => 'applications/system/engine/PhabricatorCacheEngineExtension.php',
'PhabricatorCacheGeneralGarbageCollector' => 'applications/cache/garbagecollector/PhabricatorCacheGeneralGarbageCollector.php',
'PhabricatorCacheManagementPurgeWorkflow' => 'applications/cache/management/PhabricatorCacheManagementPurgeWorkflow.php',
'PhabricatorCacheManagementWorkflow' => 'applications/cache/management/PhabricatorCacheManagementWorkflow.php',
'PhabricatorCacheMarkupGarbageCollector' => 'applications/cache/garbagecollector/PhabricatorCacheMarkupGarbageCollector.php',
'PhabricatorCachePurger' => 'applications/cache/purger/PhabricatorCachePurger.php',
'PhabricatorCacheSchemaSpec' => 'applications/cache/storage/PhabricatorCacheSchemaSpec.php',
'PhabricatorCacheSetupCheck' => 'applications/config/check/PhabricatorCacheSetupCheck.php',
'PhabricatorCacheSpec' => 'applications/cache/spec/PhabricatorCacheSpec.php',
'PhabricatorCacheTTLGarbageCollector' => 'applications/cache/garbagecollector/PhabricatorCacheTTLGarbageCollector.php',
'PhabricatorCachedClassMapQuery' => 'applications/cache/PhabricatorCachedClassMapQuery.php',
'PhabricatorCaches' => 'applications/cache/PhabricatorCaches.php',
'PhabricatorCachesTestCase' => 'applications/cache/__tests__/PhabricatorCachesTestCase.php',
'PhabricatorCalendarApplication' => 'applications/calendar/application/PhabricatorCalendarApplication.php',
'PhabricatorCalendarController' => 'applications/calendar/controller/PhabricatorCalendarController.php',
'PhabricatorCalendarDAO' => 'applications/calendar/storage/PhabricatorCalendarDAO.php',
'PhabricatorCalendarEvent' => 'applications/calendar/storage/PhabricatorCalendarEvent.php',
'PhabricatorCalendarEventAcceptTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventAcceptTransaction.php',
'PhabricatorCalendarEventAllDayTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventAllDayTransaction.php',
'PhabricatorCalendarEventAvailabilityController' => 'applications/calendar/controller/PhabricatorCalendarEventAvailabilityController.php',
'PhabricatorCalendarEventCancelController' => 'applications/calendar/controller/PhabricatorCalendarEventCancelController.php',
'PhabricatorCalendarEventCancelTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventCancelTransaction.php',
'PhabricatorCalendarEventDateTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventDateTransaction.php',
'PhabricatorCalendarEventDeclineTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventDeclineTransaction.php',
'PhabricatorCalendarEventDefaultEditCapability' => 'applications/calendar/capability/PhabricatorCalendarEventDefaultEditCapability.php',
'PhabricatorCalendarEventDefaultViewCapability' => 'applications/calendar/capability/PhabricatorCalendarEventDefaultViewCapability.php',
'PhabricatorCalendarEventDescriptionTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventDescriptionTransaction.php',
'PhabricatorCalendarEventDragController' => 'applications/calendar/controller/PhabricatorCalendarEventDragController.php',
'PhabricatorCalendarEventEditConduitAPIMethod' => 'applications/calendar/conduit/PhabricatorCalendarEventEditConduitAPIMethod.php',
'PhabricatorCalendarEventEditController' => 'applications/calendar/controller/PhabricatorCalendarEventEditController.php',
'PhabricatorCalendarEventEditEngine' => 'applications/calendar/editor/PhabricatorCalendarEventEditEngine.php',
'PhabricatorCalendarEventEditor' => 'applications/calendar/editor/PhabricatorCalendarEventEditor.php',
'PhabricatorCalendarEventEmailCommand' => 'applications/calendar/command/PhabricatorCalendarEventEmailCommand.php',
'PhabricatorCalendarEventEndDateTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventEndDateTransaction.php',
'PhabricatorCalendarEventExportController' => 'applications/calendar/controller/PhabricatorCalendarEventExportController.php',
'PhabricatorCalendarEventFerretEngine' => 'applications/calendar/search/PhabricatorCalendarEventFerretEngine.php',
'PhabricatorCalendarEventForkTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventForkTransaction.php',
'PhabricatorCalendarEventFrequencyTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventFrequencyTransaction.php',
'PhabricatorCalendarEventFulltextEngine' => 'applications/calendar/search/PhabricatorCalendarEventFulltextEngine.php',
'PhabricatorCalendarEventHeraldAdapter' => 'applications/calendar/herald/PhabricatorCalendarEventHeraldAdapter.php',
'PhabricatorCalendarEventHeraldField' => 'applications/calendar/herald/PhabricatorCalendarEventHeraldField.php',
'PhabricatorCalendarEventHeraldFieldGroup' => 'applications/calendar/herald/PhabricatorCalendarEventHeraldFieldGroup.php',
'PhabricatorCalendarEventHostPolicyRule' => 'applications/calendar/policyrule/PhabricatorCalendarEventHostPolicyRule.php',
'PhabricatorCalendarEventHostTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventHostTransaction.php',
'PhabricatorCalendarEventIconTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventIconTransaction.php',
'PhabricatorCalendarEventInviteTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventInviteTransaction.php',
'PhabricatorCalendarEventInvitee' => 'applications/calendar/storage/PhabricatorCalendarEventInvitee.php',
'PhabricatorCalendarEventInviteeQuery' => 'applications/calendar/query/PhabricatorCalendarEventInviteeQuery.php',
'PhabricatorCalendarEventInviteesPolicyRule' => 'applications/calendar/policyrule/PhabricatorCalendarEventInviteesPolicyRule.php',
'PhabricatorCalendarEventJoinController' => 'applications/calendar/controller/PhabricatorCalendarEventJoinController.php',
'PhabricatorCalendarEventListController' => 'applications/calendar/controller/PhabricatorCalendarEventListController.php',
'PhabricatorCalendarEventMailReceiver' => 'applications/calendar/mail/PhabricatorCalendarEventMailReceiver.php',
'PhabricatorCalendarEventNameHeraldField' => 'applications/calendar/herald/PhabricatorCalendarEventNameHeraldField.php',
'PhabricatorCalendarEventNameTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventNameTransaction.php',
'PhabricatorCalendarEventNotificationView' => 'applications/calendar/notifications/PhabricatorCalendarEventNotificationView.php',
'PhabricatorCalendarEventPHIDType' => 'applications/calendar/phid/PhabricatorCalendarEventPHIDType.php',
'PhabricatorCalendarEventPolicyCodex' => 'applications/calendar/codex/PhabricatorCalendarEventPolicyCodex.php',
'PhabricatorCalendarEventQuery' => 'applications/calendar/query/PhabricatorCalendarEventQuery.php',
'PhabricatorCalendarEventRSVPEmailCommand' => 'applications/calendar/command/PhabricatorCalendarEventRSVPEmailCommand.php',
'PhabricatorCalendarEventRecurringTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventRecurringTransaction.php',
'PhabricatorCalendarEventReplyTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventReplyTransaction.php',
'PhabricatorCalendarEventSearchConduitAPIMethod' => 'applications/calendar/conduit/PhabricatorCalendarEventSearchConduitAPIMethod.php',
'PhabricatorCalendarEventSearchEngine' => 'applications/calendar/query/PhabricatorCalendarEventSearchEngine.php',
'PhabricatorCalendarEventStartDateTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventStartDateTransaction.php',
'PhabricatorCalendarEventTransaction' => 'applications/calendar/storage/PhabricatorCalendarEventTransaction.php',
'PhabricatorCalendarEventTransactionComment' => 'applications/calendar/storage/PhabricatorCalendarEventTransactionComment.php',
'PhabricatorCalendarEventTransactionQuery' => 'applications/calendar/query/PhabricatorCalendarEventTransactionQuery.php',
'PhabricatorCalendarEventTransactionType' => 'applications/calendar/xaction/PhabricatorCalendarEventTransactionType.php',
'PhabricatorCalendarEventUntilDateTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventUntilDateTransaction.php',
'PhabricatorCalendarEventViewController' => 'applications/calendar/controller/PhabricatorCalendarEventViewController.php',
'PhabricatorCalendarExport' => 'applications/calendar/storage/PhabricatorCalendarExport.php',
'PhabricatorCalendarExportDisableController' => 'applications/calendar/controller/PhabricatorCalendarExportDisableController.php',
'PhabricatorCalendarExportDisableTransaction' => 'applications/calendar/xaction/PhabricatorCalendarExportDisableTransaction.php',
'PhabricatorCalendarExportEditController' => 'applications/calendar/controller/PhabricatorCalendarExportEditController.php',
'PhabricatorCalendarExportEditEngine' => 'applications/calendar/editor/PhabricatorCalendarExportEditEngine.php',
'PhabricatorCalendarExportEditor' => 'applications/calendar/editor/PhabricatorCalendarExportEditor.php',
'PhabricatorCalendarExportICSController' => 'applications/calendar/controller/PhabricatorCalendarExportICSController.php',
'PhabricatorCalendarExportListController' => 'applications/calendar/controller/PhabricatorCalendarExportListController.php',
'PhabricatorCalendarExportModeTransaction' => 'applications/calendar/xaction/PhabricatorCalendarExportModeTransaction.php',
'PhabricatorCalendarExportNameTransaction' => 'applications/calendar/xaction/PhabricatorCalendarExportNameTransaction.php',
'PhabricatorCalendarExportPHIDType' => 'applications/calendar/phid/PhabricatorCalendarExportPHIDType.php',
'PhabricatorCalendarExportQuery' => 'applications/calendar/query/PhabricatorCalendarExportQuery.php',
'PhabricatorCalendarExportQueryKeyTransaction' => 'applications/calendar/xaction/PhabricatorCalendarExportQueryKeyTransaction.php',
'PhabricatorCalendarExportSearchEngine' => 'applications/calendar/query/PhabricatorCalendarExportSearchEngine.php',
'PhabricatorCalendarExportTransaction' => 'applications/calendar/storage/PhabricatorCalendarExportTransaction.php',
'PhabricatorCalendarExportTransactionQuery' => 'applications/calendar/query/PhabricatorCalendarExportTransactionQuery.php',
'PhabricatorCalendarExportTransactionType' => 'applications/calendar/xaction/PhabricatorCalendarExportTransactionType.php',
'PhabricatorCalendarExportViewController' => 'applications/calendar/controller/PhabricatorCalendarExportViewController.php',
'PhabricatorCalendarExternalInvitee' => 'applications/calendar/storage/PhabricatorCalendarExternalInvitee.php',
'PhabricatorCalendarExternalInviteePHIDType' => 'applications/calendar/phid/PhabricatorCalendarExternalInviteePHIDType.php',
'PhabricatorCalendarExternalInviteeQuery' => 'applications/calendar/query/PhabricatorCalendarExternalInviteeQuery.php',
'PhabricatorCalendarICSFileImportEngine' => 'applications/calendar/import/PhabricatorCalendarICSFileImportEngine.php',
'PhabricatorCalendarICSImportEngine' => 'applications/calendar/import/PhabricatorCalendarICSImportEngine.php',
'PhabricatorCalendarICSURIImportEngine' => 'applications/calendar/import/PhabricatorCalendarICSURIImportEngine.php',
'PhabricatorCalendarICSWriter' => 'applications/calendar/util/PhabricatorCalendarICSWriter.php',
'PhabricatorCalendarIconSet' => 'applications/calendar/icon/PhabricatorCalendarIconSet.php',
'PhabricatorCalendarImport' => 'applications/calendar/storage/PhabricatorCalendarImport.php',
'PhabricatorCalendarImportDefaultLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportDefaultLogType.php',
'PhabricatorCalendarImportDeleteController' => 'applications/calendar/controller/PhabricatorCalendarImportDeleteController.php',
'PhabricatorCalendarImportDeleteLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportDeleteLogType.php',
'PhabricatorCalendarImportDeleteTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportDeleteTransaction.php',
'PhabricatorCalendarImportDisableController' => 'applications/calendar/controller/PhabricatorCalendarImportDisableController.php',
'PhabricatorCalendarImportDisableTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportDisableTransaction.php',
'PhabricatorCalendarImportDropController' => 'applications/calendar/controller/PhabricatorCalendarImportDropController.php',
'PhabricatorCalendarImportDuplicateLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportDuplicateLogType.php',
'PhabricatorCalendarImportEditController' => 'applications/calendar/controller/PhabricatorCalendarImportEditController.php',
'PhabricatorCalendarImportEditEngine' => 'applications/calendar/editor/PhabricatorCalendarImportEditEngine.php',
'PhabricatorCalendarImportEditor' => 'applications/calendar/editor/PhabricatorCalendarImportEditor.php',
'PhabricatorCalendarImportEmptyLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportEmptyLogType.php',
'PhabricatorCalendarImportEngine' => 'applications/calendar/import/PhabricatorCalendarImportEngine.php',
'PhabricatorCalendarImportEpochLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportEpochLogType.php',
'PhabricatorCalendarImportFetchLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportFetchLogType.php',
'PhabricatorCalendarImportFrequencyLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportFrequencyLogType.php',
'PhabricatorCalendarImportFrequencyTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportFrequencyTransaction.php',
'PhabricatorCalendarImportICSFileTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportICSFileTransaction.php',
'PhabricatorCalendarImportICSLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportICSLogType.php',
'PhabricatorCalendarImportICSURITransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportICSURITransaction.php',
'PhabricatorCalendarImportICSWarningLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportICSWarningLogType.php',
'PhabricatorCalendarImportIgnoredNodeLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportIgnoredNodeLogType.php',
'PhabricatorCalendarImportListController' => 'applications/calendar/controller/PhabricatorCalendarImportListController.php',
'PhabricatorCalendarImportLog' => 'applications/calendar/storage/PhabricatorCalendarImportLog.php',
'PhabricatorCalendarImportLogListController' => 'applications/calendar/controller/PhabricatorCalendarImportLogListController.php',
'PhabricatorCalendarImportLogQuery' => 'applications/calendar/query/PhabricatorCalendarImportLogQuery.php',
'PhabricatorCalendarImportLogSearchEngine' => 'applications/calendar/query/PhabricatorCalendarImportLogSearchEngine.php',
'PhabricatorCalendarImportLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportLogType.php',
'PhabricatorCalendarImportLogView' => 'applications/calendar/view/PhabricatorCalendarImportLogView.php',
'PhabricatorCalendarImportNameTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportNameTransaction.php',
'PhabricatorCalendarImportOriginalLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportOriginalLogType.php',
'PhabricatorCalendarImportOrphanLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportOrphanLogType.php',
'PhabricatorCalendarImportPHIDType' => 'applications/calendar/phid/PhabricatorCalendarImportPHIDType.php',
'PhabricatorCalendarImportQuery' => 'applications/calendar/query/PhabricatorCalendarImportQuery.php',
'PhabricatorCalendarImportQueueLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportQueueLogType.php',
'PhabricatorCalendarImportReloadController' => 'applications/calendar/controller/PhabricatorCalendarImportReloadController.php',
'PhabricatorCalendarImportReloadTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportReloadTransaction.php',
'PhabricatorCalendarImportReloadWorker' => 'applications/calendar/worker/PhabricatorCalendarImportReloadWorker.php',
'PhabricatorCalendarImportSearchEngine' => 'applications/calendar/query/PhabricatorCalendarImportSearchEngine.php',
'PhabricatorCalendarImportTransaction' => 'applications/calendar/storage/PhabricatorCalendarImportTransaction.php',
'PhabricatorCalendarImportTransactionQuery' => 'applications/calendar/query/PhabricatorCalendarImportTransactionQuery.php',
'PhabricatorCalendarImportTransactionType' => 'applications/calendar/xaction/PhabricatorCalendarImportTransactionType.php',
'PhabricatorCalendarImportTriggerLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportTriggerLogType.php',
'PhabricatorCalendarImportUpdateLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportUpdateLogType.php',
'PhabricatorCalendarImportViewController' => 'applications/calendar/controller/PhabricatorCalendarImportViewController.php',
'PhabricatorCalendarInviteeDatasource' => 'applications/calendar/typeahead/PhabricatorCalendarInviteeDatasource.php',
'PhabricatorCalendarInviteeUserDatasource' => 'applications/calendar/typeahead/PhabricatorCalendarInviteeUserDatasource.php',
'PhabricatorCalendarInviteeViewerFunctionDatasource' => 'applications/calendar/typeahead/PhabricatorCalendarInviteeViewerFunctionDatasource.php',
'PhabricatorCalendarManagementNotifyWorkflow' => 'applications/calendar/management/PhabricatorCalendarManagementNotifyWorkflow.php',
'PhabricatorCalendarManagementReloadWorkflow' => 'applications/calendar/management/PhabricatorCalendarManagementReloadWorkflow.php',
'PhabricatorCalendarManagementWorkflow' => 'applications/calendar/management/PhabricatorCalendarManagementWorkflow.php',
'PhabricatorCalendarNotification' => 'applications/calendar/storage/PhabricatorCalendarNotification.php',
'PhabricatorCalendarNotificationEngine' => 'applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php',
'PhabricatorCalendarRemarkupRule' => 'applications/calendar/remarkup/PhabricatorCalendarRemarkupRule.php',
'PhabricatorCalendarReplyHandler' => 'applications/calendar/mail/PhabricatorCalendarReplyHandler.php',
'PhabricatorCalendarSchemaSpec' => 'applications/calendar/storage/PhabricatorCalendarSchemaSpec.php',
'PhabricatorCelerityApplication' => 'applications/celerity/application/PhabricatorCelerityApplication.php',
'PhabricatorCelerityTestCase' => '__tests__/PhabricatorCelerityTestCase.php',
'PhabricatorChangeParserTestCase' => 'applications/repository/worker/__tests__/PhabricatorChangeParserTestCase.php',
'PhabricatorChangesetCachePurger' => 'applications/cache/purger/PhabricatorChangesetCachePurger.php',
'PhabricatorChangesetResponse' => 'infrastructure/diff/PhabricatorChangesetResponse.php',
'PhabricatorChatLogApplication' => 'applications/chatlog/application/PhabricatorChatLogApplication.php',
'PhabricatorChatLogChannel' => 'applications/chatlog/storage/PhabricatorChatLogChannel.php',
'PhabricatorChatLogChannelListController' => 'applications/chatlog/controller/PhabricatorChatLogChannelListController.php',
'PhabricatorChatLogChannelLogController' => 'applications/chatlog/controller/PhabricatorChatLogChannelLogController.php',
'PhabricatorChatLogChannelQuery' => 'applications/chatlog/query/PhabricatorChatLogChannelQuery.php',
'PhabricatorChatLogController' => 'applications/chatlog/controller/PhabricatorChatLogController.php',
'PhabricatorChatLogDAO' => 'applications/chatlog/storage/PhabricatorChatLogDAO.php',
'PhabricatorChatLogEvent' => 'applications/chatlog/storage/PhabricatorChatLogEvent.php',
'PhabricatorChatLogQuery' => 'applications/chatlog/query/PhabricatorChatLogQuery.php',
'PhabricatorCheckboxesEditField' => 'applications/transactions/editfield/PhabricatorCheckboxesEditField.php',
'PhabricatorChunkedFileStorageEngine' => 'applications/files/engine/PhabricatorChunkedFileStorageEngine.php',
'PhabricatorClassConfigType' => 'applications/config/type/PhabricatorClassConfigType.php',
'PhabricatorClusterConfigOptions' => 'applications/config/option/PhabricatorClusterConfigOptions.php',
'PhabricatorClusterDatabasesConfigType' => 'infrastructure/cluster/config/PhabricatorClusterDatabasesConfigType.php',
'PhabricatorClusterException' => 'infrastructure/cluster/exception/PhabricatorClusterException.php',
'PhabricatorClusterExceptionHandler' => 'infrastructure/cluster/exception/PhabricatorClusterExceptionHandler.php',
'PhabricatorClusterImpossibleWriteException' => 'infrastructure/cluster/exception/PhabricatorClusterImpossibleWriteException.php',
'PhabricatorClusterImproperWriteException' => 'infrastructure/cluster/exception/PhabricatorClusterImproperWriteException.php',
'PhabricatorClusterMailersConfigType' => 'infrastructure/cluster/config/PhabricatorClusterMailersConfigType.php',
'PhabricatorClusterNoHostForRoleException' => 'infrastructure/cluster/exception/PhabricatorClusterNoHostForRoleException.php',
'PhabricatorClusterSearchConfigType' => 'infrastructure/cluster/config/PhabricatorClusterSearchConfigType.php',
'PhabricatorClusterServiceHealthRecord' => 'infrastructure/cluster/PhabricatorClusterServiceHealthRecord.php',
'PhabricatorClusterStrandedException' => 'infrastructure/cluster/exception/PhabricatorClusterStrandedException.php',
'PhabricatorColumnProxyInterface' => 'applications/project/interface/PhabricatorColumnProxyInterface.php',
'PhabricatorColumnsEditField' => 'applications/transactions/editfield/PhabricatorColumnsEditField.php',
'PhabricatorCommentEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php',
'PhabricatorCommentEditField' => 'applications/transactions/editfield/PhabricatorCommentEditField.php',
'PhabricatorCommentEditType' => 'applications/transactions/edittype/PhabricatorCommentEditType.php',
'PhabricatorCommitBranchesField' => 'applications/repository/customfield/PhabricatorCommitBranchesField.php',
'PhabricatorCommitCustomField' => 'applications/repository/customfield/PhabricatorCommitCustomField.php',
'PhabricatorCommitMergedCommitsField' => 'applications/repository/customfield/PhabricatorCommitMergedCommitsField.php',
'PhabricatorCommitRepositoryField' => 'applications/repository/customfield/PhabricatorCommitRepositoryField.php',
'PhabricatorCommitSearchEngine' => 'applications/audit/query/PhabricatorCommitSearchEngine.php',
'PhabricatorCommitTagsField' => 'applications/repository/customfield/PhabricatorCommitTagsField.php',
'PhabricatorCommonPasswords' => 'applications/auth/constants/PhabricatorCommonPasswords.php',
'PhabricatorConduitAPIController' => 'applications/conduit/controller/PhabricatorConduitAPIController.php',
'PhabricatorConduitApplication' => 'applications/conduit/application/PhabricatorConduitApplication.php',
'PhabricatorConduitCallManagementWorkflow' => 'applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php',
'PhabricatorConduitCertificateToken' => 'applications/conduit/storage/PhabricatorConduitCertificateToken.php',
'PhabricatorConduitConsoleController' => 'applications/conduit/controller/PhabricatorConduitConsoleController.php',
'PhabricatorConduitContentSource' => 'infrastructure/contentsource/PhabricatorConduitContentSource.php',
'PhabricatorConduitController' => 'applications/conduit/controller/PhabricatorConduitController.php',
'PhabricatorConduitDAO' => 'applications/conduit/storage/PhabricatorConduitDAO.php',
'PhabricatorConduitEditField' => 'applications/transactions/editfield/PhabricatorConduitEditField.php',
'PhabricatorConduitListController' => 'applications/conduit/controller/PhabricatorConduitListController.php',
'PhabricatorConduitLogController' => 'applications/conduit/controller/PhabricatorConduitLogController.php',
'PhabricatorConduitLogQuery' => 'applications/conduit/query/PhabricatorConduitLogQuery.php',
'PhabricatorConduitLogSearchEngine' => 'applications/conduit/query/PhabricatorConduitLogSearchEngine.php',
'PhabricatorConduitManagementWorkflow' => 'applications/conduit/management/PhabricatorConduitManagementWorkflow.php',
'PhabricatorConduitMethodCallLog' => 'applications/conduit/storage/PhabricatorConduitMethodCallLog.php',
'PhabricatorConduitMethodQuery' => 'applications/conduit/query/PhabricatorConduitMethodQuery.php',
'PhabricatorConduitRequestExceptionHandler' => 'aphront/handler/PhabricatorConduitRequestExceptionHandler.php',
'PhabricatorConduitResultInterface' => 'applications/conduit/interface/PhabricatorConduitResultInterface.php',
'PhabricatorConduitSearchEngine' => 'applications/conduit/query/PhabricatorConduitSearchEngine.php',
'PhabricatorConduitSearchFieldSpecification' => 'applications/conduit/interface/PhabricatorConduitSearchFieldSpecification.php',
'PhabricatorConduitTestCase' => '__tests__/PhabricatorConduitTestCase.php',
'PhabricatorConduitToken' => 'applications/conduit/storage/PhabricatorConduitToken.php',
'PhabricatorConduitTokenController' => 'applications/conduit/controller/PhabricatorConduitTokenController.php',
'PhabricatorConduitTokenEditController' => 'applications/conduit/controller/PhabricatorConduitTokenEditController.php',
'PhabricatorConduitTokenHandshakeController' => 'applications/conduit/controller/PhabricatorConduitTokenHandshakeController.php',
'PhabricatorConduitTokenQuery' => 'applications/conduit/query/PhabricatorConduitTokenQuery.php',
'PhabricatorConduitTokenTerminateController' => 'applications/conduit/controller/PhabricatorConduitTokenTerminateController.php',
'PhabricatorConduitTokensSettingsPanel' => 'applications/conduit/settings/PhabricatorConduitTokensSettingsPanel.php',
'PhabricatorConfigAllController' => 'applications/config/controller/PhabricatorConfigAllController.php',
'PhabricatorConfigApplication' => 'applications/config/application/PhabricatorConfigApplication.php',
'PhabricatorConfigApplicationController' => 'applications/config/controller/PhabricatorConfigApplicationController.php',
'PhabricatorConfigCacheController' => 'applications/config/controller/PhabricatorConfigCacheController.php',
'PhabricatorConfigClusterDatabasesController' => 'applications/config/controller/PhabricatorConfigClusterDatabasesController.php',
'PhabricatorConfigClusterNotificationsController' => 'applications/config/controller/PhabricatorConfigClusterNotificationsController.php',
'PhabricatorConfigClusterRepositoriesController' => 'applications/config/controller/PhabricatorConfigClusterRepositoriesController.php',
'PhabricatorConfigClusterSearchController' => 'applications/config/controller/PhabricatorConfigClusterSearchController.php',
'PhabricatorConfigCollectorsModule' => 'applications/config/module/PhabricatorConfigCollectorsModule.php',
'PhabricatorConfigColumnSchema' => 'applications/config/schema/PhabricatorConfigColumnSchema.php',
'PhabricatorConfigConfigPHIDType' => 'applications/config/phid/PhabricatorConfigConfigPHIDType.php',
'PhabricatorConfigConstants' => 'applications/config/constants/PhabricatorConfigConstants.php',
'PhabricatorConfigController' => 'applications/config/controller/PhabricatorConfigController.php',
'PhabricatorConfigCoreSchemaSpec' => 'applications/config/schema/PhabricatorConfigCoreSchemaSpec.php',
'PhabricatorConfigDatabaseController' => 'applications/config/controller/PhabricatorConfigDatabaseController.php',
'PhabricatorConfigDatabaseIssueController' => 'applications/config/controller/PhabricatorConfigDatabaseIssueController.php',
'PhabricatorConfigDatabaseSchema' => 'applications/config/schema/PhabricatorConfigDatabaseSchema.php',
'PhabricatorConfigDatabaseSource' => 'infrastructure/env/PhabricatorConfigDatabaseSource.php',
'PhabricatorConfigDatabaseStatusController' => 'applications/config/controller/PhabricatorConfigDatabaseStatusController.php',
'PhabricatorConfigDefaultSource' => 'infrastructure/env/PhabricatorConfigDefaultSource.php',
'PhabricatorConfigDictionarySource' => 'infrastructure/env/PhabricatorConfigDictionarySource.php',
'PhabricatorConfigEdgeModule' => 'applications/config/module/PhabricatorConfigEdgeModule.php',
'PhabricatorConfigEditController' => 'applications/config/controller/PhabricatorConfigEditController.php',
'PhabricatorConfigEditor' => 'applications/config/editor/PhabricatorConfigEditor.php',
'PhabricatorConfigEntry' => 'applications/config/storage/PhabricatorConfigEntry.php',
'PhabricatorConfigEntryDAO' => 'applications/config/storage/PhabricatorConfigEntryDAO.php',
'PhabricatorConfigEntryQuery' => 'applications/config/query/PhabricatorConfigEntryQuery.php',
'PhabricatorConfigFileSource' => 'infrastructure/env/PhabricatorConfigFileSource.php',
'PhabricatorConfigGroupConstants' => 'applications/config/constants/PhabricatorConfigGroupConstants.php',
'PhabricatorConfigGroupController' => 'applications/config/controller/PhabricatorConfigGroupController.php',
'PhabricatorConfigHTTPParameterTypesModule' => 'applications/config/module/PhabricatorConfigHTTPParameterTypesModule.php',
'PhabricatorConfigHistoryController' => 'applications/config/controller/PhabricatorConfigHistoryController.php',
'PhabricatorConfigIgnoreController' => 'applications/config/controller/PhabricatorConfigIgnoreController.php',
'PhabricatorConfigIssueListController' => 'applications/config/controller/PhabricatorConfigIssueListController.php',
'PhabricatorConfigIssuePanelController' => 'applications/config/controller/PhabricatorConfigIssuePanelController.php',
'PhabricatorConfigIssueViewController' => 'applications/config/controller/PhabricatorConfigIssueViewController.php',
'PhabricatorConfigJSON' => 'applications/config/json/PhabricatorConfigJSON.php',
'PhabricatorConfigJSONOptionType' => 'applications/config/custom/PhabricatorConfigJSONOptionType.php',
'PhabricatorConfigKeySchema' => 'applications/config/schema/PhabricatorConfigKeySchema.php',
'PhabricatorConfigListController' => 'applications/config/controller/PhabricatorConfigListController.php',
'PhabricatorConfigLocalSource' => 'infrastructure/env/PhabricatorConfigLocalSource.php',
'PhabricatorConfigManagementDeleteWorkflow' => 'applications/config/management/PhabricatorConfigManagementDeleteWorkflow.php',
'PhabricatorConfigManagementDoneWorkflow' => 'applications/config/management/PhabricatorConfigManagementDoneWorkflow.php',
'PhabricatorConfigManagementGetWorkflow' => 'applications/config/management/PhabricatorConfigManagementGetWorkflow.php',
'PhabricatorConfigManagementListWorkflow' => 'applications/config/management/PhabricatorConfigManagementListWorkflow.php',
'PhabricatorConfigManagementMigrateWorkflow' => 'applications/config/management/PhabricatorConfigManagementMigrateWorkflow.php',
'PhabricatorConfigManagementSetWorkflow' => 'applications/config/management/PhabricatorConfigManagementSetWorkflow.php',
'PhabricatorConfigManagementWorkflow' => 'applications/config/management/PhabricatorConfigManagementWorkflow.php',
'PhabricatorConfigManualActivity' => 'applications/config/storage/PhabricatorConfigManualActivity.php',
'PhabricatorConfigModule' => 'applications/config/module/PhabricatorConfigModule.php',
'PhabricatorConfigModuleController' => 'applications/config/controller/PhabricatorConfigModuleController.php',
'PhabricatorConfigOption' => 'applications/config/option/PhabricatorConfigOption.php',
'PhabricatorConfigOptionType' => 'applications/config/custom/PhabricatorConfigOptionType.php',
'PhabricatorConfigPHIDModule' => 'applications/config/module/PhabricatorConfigPHIDModule.php',
'PhabricatorConfigProxySource' => 'infrastructure/env/PhabricatorConfigProxySource.php',
'PhabricatorConfigPurgeCacheController' => 'applications/config/controller/PhabricatorConfigPurgeCacheController.php',
'PhabricatorConfigRegexOptionType' => 'applications/config/custom/PhabricatorConfigRegexOptionType.php',
+ 'PhabricatorConfigRemarkupRule' => 'infrastructure/markup/rule/PhabricatorConfigRemarkupRule.php',
'PhabricatorConfigRequestExceptionHandlerModule' => 'applications/config/module/PhabricatorConfigRequestExceptionHandlerModule.php',
'PhabricatorConfigResponse' => 'applications/config/response/PhabricatorConfigResponse.php',
'PhabricatorConfigSchemaQuery' => 'applications/config/schema/PhabricatorConfigSchemaQuery.php',
'PhabricatorConfigSchemaSpec' => 'applications/config/schema/PhabricatorConfigSchemaSpec.php',
'PhabricatorConfigServerSchema' => 'applications/config/schema/PhabricatorConfigServerSchema.php',
'PhabricatorConfigSetupCheckModule' => 'applications/config/module/PhabricatorConfigSetupCheckModule.php',
'PhabricatorConfigSiteModule' => 'applications/config/module/PhabricatorConfigSiteModule.php',
'PhabricatorConfigSiteSource' => 'infrastructure/env/PhabricatorConfigSiteSource.php',
'PhabricatorConfigSource' => 'infrastructure/env/PhabricatorConfigSource.php',
'PhabricatorConfigStackSource' => 'infrastructure/env/PhabricatorConfigStackSource.php',
'PhabricatorConfigStorageSchema' => 'applications/config/schema/PhabricatorConfigStorageSchema.php',
'PhabricatorConfigTableSchema' => 'applications/config/schema/PhabricatorConfigTableSchema.php',
'PhabricatorConfigTransaction' => 'applications/config/storage/PhabricatorConfigTransaction.php',
'PhabricatorConfigTransactionQuery' => 'applications/config/query/PhabricatorConfigTransactionQuery.php',
'PhabricatorConfigType' => 'applications/config/type/PhabricatorConfigType.php',
'PhabricatorConfigValidationException' => 'applications/config/exception/PhabricatorConfigValidationException.php',
'PhabricatorConfigVersionController' => 'applications/config/controller/PhabricatorConfigVersionController.php',
'PhabricatorConpherenceApplication' => 'applications/conpherence/application/PhabricatorConpherenceApplication.php',
'PhabricatorConpherenceColumnMinimizeSetting' => 'applications/settings/setting/PhabricatorConpherenceColumnMinimizeSetting.php',
'PhabricatorConpherenceColumnVisibleSetting' => 'applications/settings/setting/PhabricatorConpherenceColumnVisibleSetting.php',
'PhabricatorConpherenceNotificationsSetting' => 'applications/settings/setting/PhabricatorConpherenceNotificationsSetting.php',
'PhabricatorConpherencePreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorConpherencePreferencesSettingsPanel.php',
'PhabricatorConpherenceProfileMenuItem' => 'applications/search/menuitem/PhabricatorConpherenceProfileMenuItem.php',
'PhabricatorConpherenceRoomContextFreeGrammar' => 'applications/conpherence/lipsum/PhabricatorConpherenceRoomContextFreeGrammar.php',
'PhabricatorConpherenceRoomTestDataGenerator' => 'applications/conpherence/lipsum/PhabricatorConpherenceRoomTestDataGenerator.php',
'PhabricatorConpherenceSoundSetting' => 'applications/settings/setting/PhabricatorConpherenceSoundSetting.php',
'PhabricatorConpherenceThreadPHIDType' => 'applications/conpherence/phid/PhabricatorConpherenceThreadPHIDType.php',
'PhabricatorConpherenceWidgetVisibleSetting' => 'applications/settings/setting/PhabricatorConpherenceWidgetVisibleSetting.php',
'PhabricatorConsoleApplication' => 'applications/console/application/PhabricatorConsoleApplication.php',
'PhabricatorConsoleContentSource' => 'infrastructure/contentsource/PhabricatorConsoleContentSource.php',
+ 'PhabricatorContactNumbersSettingsPanel' => 'applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php',
'PhabricatorContentSource' => 'infrastructure/contentsource/PhabricatorContentSource.php',
'PhabricatorContentSourceModule' => 'infrastructure/contentsource/PhabricatorContentSourceModule.php',
'PhabricatorContentSourceView' => 'infrastructure/contentsource/PhabricatorContentSourceView.php',
'PhabricatorContributedToObjectEdgeType' => 'applications/transactions/edges/PhabricatorContributedToObjectEdgeType.php',
'PhabricatorController' => 'applications/base/controller/PhabricatorController.php',
'PhabricatorCookies' => 'applications/auth/constants/PhabricatorCookies.php',
'PhabricatorCoreConfigOptions' => 'applications/config/option/PhabricatorCoreConfigOptions.php',
'PhabricatorCoreCreateTransaction' => 'applications/transactions/xaction/PhabricatorCoreCreateTransaction.php',
'PhabricatorCoreTransactionType' => 'applications/transactions/xaction/PhabricatorCoreTransactionType.php',
'PhabricatorCoreVoidTransaction' => 'applications/transactions/xaction/PhabricatorCoreVoidTransaction.php',
'PhabricatorCountFact' => 'applications/fact/fact/PhabricatorCountFact.php',
'PhabricatorCountdown' => 'applications/countdown/storage/PhabricatorCountdown.php',
'PhabricatorCountdownApplication' => 'applications/countdown/application/PhabricatorCountdownApplication.php',
'PhabricatorCountdownController' => 'applications/countdown/controller/PhabricatorCountdownController.php',
'PhabricatorCountdownCountdownPHIDType' => 'applications/countdown/phid/PhabricatorCountdownCountdownPHIDType.php',
'PhabricatorCountdownDAO' => 'applications/countdown/storage/PhabricatorCountdownDAO.php',
'PhabricatorCountdownDefaultEditCapability' => 'applications/countdown/capability/PhabricatorCountdownDefaultEditCapability.php',
'PhabricatorCountdownDefaultViewCapability' => 'applications/countdown/capability/PhabricatorCountdownDefaultViewCapability.php',
'PhabricatorCountdownDescriptionTransaction' => 'applications/countdown/xaction/PhabricatorCountdownDescriptionTransaction.php',
'PhabricatorCountdownEditController' => 'applications/countdown/controller/PhabricatorCountdownEditController.php',
'PhabricatorCountdownEditEngine' => 'applications/countdown/editor/PhabricatorCountdownEditEngine.php',
'PhabricatorCountdownEditor' => 'applications/countdown/editor/PhabricatorCountdownEditor.php',
'PhabricatorCountdownEpochTransaction' => 'applications/countdown/xaction/PhabricatorCountdownEpochTransaction.php',
'PhabricatorCountdownListController' => 'applications/countdown/controller/PhabricatorCountdownListController.php',
'PhabricatorCountdownMailReceiver' => 'applications/countdown/mail/PhabricatorCountdownMailReceiver.php',
'PhabricatorCountdownQuery' => 'applications/countdown/query/PhabricatorCountdownQuery.php',
'PhabricatorCountdownRemarkupRule' => 'applications/countdown/remarkup/PhabricatorCountdownRemarkupRule.php',
'PhabricatorCountdownReplyHandler' => 'applications/countdown/mail/PhabricatorCountdownReplyHandler.php',
'PhabricatorCountdownSchemaSpec' => 'applications/countdown/storage/PhabricatorCountdownSchemaSpec.php',
'PhabricatorCountdownSearchEngine' => 'applications/countdown/query/PhabricatorCountdownSearchEngine.php',
'PhabricatorCountdownTitleTransaction' => 'applications/countdown/xaction/PhabricatorCountdownTitleTransaction.php',
'PhabricatorCountdownTransaction' => 'applications/countdown/storage/PhabricatorCountdownTransaction.php',
'PhabricatorCountdownTransactionComment' => 'applications/countdown/storage/PhabricatorCountdownTransactionComment.php',
'PhabricatorCountdownTransactionQuery' => 'applications/countdown/query/PhabricatorCountdownTransactionQuery.php',
'PhabricatorCountdownTransactionType' => 'applications/countdown/xaction/PhabricatorCountdownTransactionType.php',
'PhabricatorCountdownView' => 'applications/countdown/view/PhabricatorCountdownView.php',
'PhabricatorCountdownViewController' => 'applications/countdown/controller/PhabricatorCountdownViewController.php',
+ 'PhabricatorCredentialEditField' => 'applications/transactions/editfield/PhabricatorCredentialEditField.php',
'PhabricatorCursorPagedPolicyAwareQuery' => 'infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php',
'PhabricatorCustomField' => 'infrastructure/customfield/field/PhabricatorCustomField.php',
'PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource' => 'infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource.php',
'PhabricatorCustomFieldApplicationSearchDatasource' => 'infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchDatasource.php',
'PhabricatorCustomFieldApplicationSearchNoneFunctionDatasource' => 'infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchNoneFunctionDatasource.php',
'PhabricatorCustomFieldAttachment' => 'infrastructure/customfield/field/PhabricatorCustomFieldAttachment.php',
'PhabricatorCustomFieldConfigOptionType' => 'infrastructure/customfield/config/PhabricatorCustomFieldConfigOptionType.php',
'PhabricatorCustomFieldDataNotAvailableException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldDataNotAvailableException.php',
'PhabricatorCustomFieldEditEngineExtension' => 'infrastructure/customfield/engineextension/PhabricatorCustomFieldEditEngineExtension.php',
'PhabricatorCustomFieldEditField' => 'infrastructure/customfield/editor/PhabricatorCustomFieldEditField.php',
'PhabricatorCustomFieldEditType' => 'infrastructure/customfield/editor/PhabricatorCustomFieldEditType.php',
'PhabricatorCustomFieldExportEngineExtension' => 'infrastructure/export/engine/PhabricatorCustomFieldExportEngineExtension.php',
'PhabricatorCustomFieldFulltextEngineExtension' => 'infrastructure/customfield/engineextension/PhabricatorCustomFieldFulltextEngineExtension.php',
'PhabricatorCustomFieldHeraldAction' => 'infrastructure/customfield/herald/PhabricatorCustomFieldHeraldAction.php',
'PhabricatorCustomFieldHeraldActionGroup' => 'infrastructure/customfield/herald/PhabricatorCustomFieldHeraldActionGroup.php',
'PhabricatorCustomFieldHeraldField' => 'infrastructure/customfield/herald/PhabricatorCustomFieldHeraldField.php',
'PhabricatorCustomFieldHeraldFieldGroup' => 'infrastructure/customfield/herald/PhabricatorCustomFieldHeraldFieldGroup.php',
'PhabricatorCustomFieldImplementationIncompleteException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldImplementationIncompleteException.php',
'PhabricatorCustomFieldIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldIndexStorage.php',
'PhabricatorCustomFieldInterface' => 'infrastructure/customfield/interface/PhabricatorCustomFieldInterface.php',
'PhabricatorCustomFieldList' => 'infrastructure/customfield/field/PhabricatorCustomFieldList.php',
'PhabricatorCustomFieldMonogramParser' => 'infrastructure/customfield/parser/PhabricatorCustomFieldMonogramParser.php',
'PhabricatorCustomFieldNotAttachedException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldNotAttachedException.php',
'PhabricatorCustomFieldNotProxyException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldNotProxyException.php',
'PhabricatorCustomFieldNumericIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldNumericIndexStorage.php',
'PhabricatorCustomFieldSearchEngineExtension' => 'infrastructure/customfield/engineextension/PhabricatorCustomFieldSearchEngineExtension.php',
'PhabricatorCustomFieldStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldStorage.php',
'PhabricatorCustomFieldStorageQuery' => 'infrastructure/customfield/query/PhabricatorCustomFieldStorageQuery.php',
'PhabricatorCustomFieldStringIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldStringIndexStorage.php',
'PhabricatorCustomLogoConfigType' => 'applications/config/custom/PhabricatorCustomLogoConfigType.php',
'PhabricatorCustomUIFooterConfigType' => 'applications/config/custom/PhabricatorCustomUIFooterConfigType.php',
'PhabricatorDaemon' => 'infrastructure/daemon/PhabricatorDaemon.php',
'PhabricatorDaemonBulkJobController' => 'applications/daemon/controller/PhabricatorDaemonBulkJobController.php',
'PhabricatorDaemonBulkJobListController' => 'applications/daemon/controller/PhabricatorDaemonBulkJobListController.php',
'PhabricatorDaemonBulkJobMonitorController' => 'applications/daemon/controller/PhabricatorDaemonBulkJobMonitorController.php',
'PhabricatorDaemonBulkJobViewController' => 'applications/daemon/controller/PhabricatorDaemonBulkJobViewController.php',
'PhabricatorDaemonConsoleController' => 'applications/daemon/controller/PhabricatorDaemonConsoleController.php',
'PhabricatorDaemonContentSource' => 'infrastructure/daemon/contentsource/PhabricatorDaemonContentSource.php',
'PhabricatorDaemonController' => 'applications/daemon/controller/PhabricatorDaemonController.php',
'PhabricatorDaemonDAO' => 'applications/daemon/storage/PhabricatorDaemonDAO.php',
'PhabricatorDaemonEventListener' => 'applications/daemon/event/PhabricatorDaemonEventListener.php',
'PhabricatorDaemonLockLog' => 'applications/daemon/storage/PhabricatorDaemonLockLog.php',
'PhabricatorDaemonLockLogGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonLockLogGarbageCollector.php',
'PhabricatorDaemonLog' => 'applications/daemon/storage/PhabricatorDaemonLog.php',
'PhabricatorDaemonLogEvent' => 'applications/daemon/storage/PhabricatorDaemonLogEvent.php',
'PhabricatorDaemonLogEventGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonLogEventGarbageCollector.php',
'PhabricatorDaemonLogEventViewController' => 'applications/daemon/controller/PhabricatorDaemonLogEventViewController.php',
'PhabricatorDaemonLogEventsView' => 'applications/daemon/view/PhabricatorDaemonLogEventsView.php',
'PhabricatorDaemonLogGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonLogGarbageCollector.php',
'PhabricatorDaemonLogListController' => 'applications/daemon/controller/PhabricatorDaemonLogListController.php',
'PhabricatorDaemonLogListView' => 'applications/daemon/view/PhabricatorDaemonLogListView.php',
'PhabricatorDaemonLogQuery' => 'applications/daemon/query/PhabricatorDaemonLogQuery.php',
'PhabricatorDaemonLogViewController' => 'applications/daemon/controller/PhabricatorDaemonLogViewController.php',
'PhabricatorDaemonManagementDebugWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementDebugWorkflow.php',
'PhabricatorDaemonManagementLaunchWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementLaunchWorkflow.php',
'PhabricatorDaemonManagementListWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementListWorkflow.php',
'PhabricatorDaemonManagementLogWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementLogWorkflow.php',
'PhabricatorDaemonManagementReloadWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementReloadWorkflow.php',
'PhabricatorDaemonManagementRestartWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php',
'PhabricatorDaemonManagementStartWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementStartWorkflow.php',
'PhabricatorDaemonManagementStatusWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php',
'PhabricatorDaemonManagementStopWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php',
'PhabricatorDaemonManagementWorkflow' => 'applications/daemon/management/PhabricatorDaemonManagementWorkflow.php',
'PhabricatorDaemonOverseerModule' => 'infrastructure/daemon/overseer/PhabricatorDaemonOverseerModule.php',
'PhabricatorDaemonReference' => 'infrastructure/daemon/control/PhabricatorDaemonReference.php',
'PhabricatorDaemonTaskGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonTaskGarbageCollector.php',
'PhabricatorDaemonTasksTableView' => 'applications/daemon/view/PhabricatorDaemonTasksTableView.php',
'PhabricatorDaemonsApplication' => 'applications/daemon/application/PhabricatorDaemonsApplication.php',
'PhabricatorDaemonsSetupCheck' => 'applications/config/check/PhabricatorDaemonsSetupCheck.php',
'PhabricatorDailyRoutineTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorDailyRoutineTriggerClock.php',
'PhabricatorDarkConsoleSetting' => 'applications/settings/setting/PhabricatorDarkConsoleSetting.php',
'PhabricatorDarkConsoleTabSetting' => 'applications/settings/setting/PhabricatorDarkConsoleTabSetting.php',
'PhabricatorDarkConsoleVisibleSetting' => 'applications/settings/setting/PhabricatorDarkConsoleVisibleSetting.php',
'PhabricatorDashboard' => 'applications/dashboard/storage/PhabricatorDashboard.php',
'PhabricatorDashboardAddPanelController' => 'applications/dashboard/controller/PhabricatorDashboardAddPanelController.php',
'PhabricatorDashboardApplication' => 'applications/dashboard/application/PhabricatorDashboardApplication.php',
'PhabricatorDashboardArchiveController' => 'applications/dashboard/controller/PhabricatorDashboardArchiveController.php',
'PhabricatorDashboardArrangeController' => 'applications/dashboard/controller/PhabricatorDashboardArrangeController.php',
'PhabricatorDashboardController' => 'applications/dashboard/controller/PhabricatorDashboardController.php',
'PhabricatorDashboardDAO' => 'applications/dashboard/storage/PhabricatorDashboardDAO.php',
'PhabricatorDashboardDashboardHasPanelEdgeType' => 'applications/dashboard/edge/PhabricatorDashboardDashboardHasPanelEdgeType.php',
'PhabricatorDashboardDashboardPHIDType' => 'applications/dashboard/phid/PhabricatorDashboardDashboardPHIDType.php',
'PhabricatorDashboardDatasource' => 'applications/dashboard/typeahead/PhabricatorDashboardDatasource.php',
'PhabricatorDashboardEditController' => 'applications/dashboard/controller/PhabricatorDashboardEditController.php',
'PhabricatorDashboardIconSet' => 'applications/dashboard/icon/PhabricatorDashboardIconSet.php',
'PhabricatorDashboardInstall' => 'applications/dashboard/storage/PhabricatorDashboardInstall.php',
'PhabricatorDashboardInstallController' => 'applications/dashboard/controller/PhabricatorDashboardInstallController.php',
'PhabricatorDashboardLayoutConfig' => 'applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php',
'PhabricatorDashboardListController' => 'applications/dashboard/controller/PhabricatorDashboardListController.php',
'PhabricatorDashboardManageController' => 'applications/dashboard/controller/PhabricatorDashboardManageController.php',
'PhabricatorDashboardMovePanelController' => 'applications/dashboard/controller/PhabricatorDashboardMovePanelController.php',
'PhabricatorDashboardNgrams' => 'applications/dashboard/storage/PhabricatorDashboardNgrams.php',
'PhabricatorDashboardPanel' => 'applications/dashboard/storage/PhabricatorDashboardPanel.php',
'PhabricatorDashboardPanelArchiveController' => 'applications/dashboard/controller/PhabricatorDashboardPanelArchiveController.php',
'PhabricatorDashboardPanelCoreCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelCoreCustomField.php',
'PhabricatorDashboardPanelCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelCustomField.php',
'PhabricatorDashboardPanelDatasource' => 'applications/dashboard/typeahead/PhabricatorDashboardPanelDatasource.php',
'PhabricatorDashboardPanelEditConduitAPIMethod' => 'applications/dashboard/conduit/PhabricatorDashboardPanelEditConduitAPIMethod.php',
'PhabricatorDashboardPanelEditController' => 'applications/dashboard/controller/PhabricatorDashboardPanelEditController.php',
'PhabricatorDashboardPanelEditEngine' => 'applications/dashboard/editor/PhabricatorDashboardPanelEditEngine.php',
'PhabricatorDashboardPanelEditproController' => 'applications/dashboard/controller/PhabricatorDashboardPanelEditproController.php',
'PhabricatorDashboardPanelHasDashboardEdgeType' => 'applications/dashboard/edge/PhabricatorDashboardPanelHasDashboardEdgeType.php',
'PhabricatorDashboardPanelListController' => 'applications/dashboard/controller/PhabricatorDashboardPanelListController.php',
'PhabricatorDashboardPanelNgrams' => 'applications/dashboard/storage/PhabricatorDashboardPanelNgrams.php',
'PhabricatorDashboardPanelPHIDType' => 'applications/dashboard/phid/PhabricatorDashboardPanelPHIDType.php',
'PhabricatorDashboardPanelQuery' => 'applications/dashboard/query/PhabricatorDashboardPanelQuery.php',
'PhabricatorDashboardPanelRenderController' => 'applications/dashboard/controller/PhabricatorDashboardPanelRenderController.php',
'PhabricatorDashboardPanelRenderingEngine' => 'applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php',
'PhabricatorDashboardPanelSearchApplicationCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelSearchApplicationCustomField.php',
'PhabricatorDashboardPanelSearchEngine' => 'applications/dashboard/query/PhabricatorDashboardPanelSearchEngine.php',
'PhabricatorDashboardPanelSearchQueryCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelSearchQueryCustomField.php',
'PhabricatorDashboardPanelTabsCustomField' => 'applications/dashboard/customfield/PhabricatorDashboardPanelTabsCustomField.php',
'PhabricatorDashboardPanelTransaction' => 'applications/dashboard/storage/PhabricatorDashboardPanelTransaction.php',
'PhabricatorDashboardPanelTransactionEditor' => 'applications/dashboard/editor/PhabricatorDashboardPanelTransactionEditor.php',
'PhabricatorDashboardPanelTransactionQuery' => 'applications/dashboard/query/PhabricatorDashboardPanelTransactionQuery.php',
'PhabricatorDashboardPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardPanelType.php',
'PhabricatorDashboardPanelViewController' => 'applications/dashboard/controller/PhabricatorDashboardPanelViewController.php',
'PhabricatorDashboardProfileController' => 'applications/dashboard/controller/PhabricatorDashboardProfileController.php',
'PhabricatorDashboardProfileMenuItem' => 'applications/search/menuitem/PhabricatorDashboardProfileMenuItem.php',
'PhabricatorDashboardQuery' => 'applications/dashboard/query/PhabricatorDashboardQuery.php',
'PhabricatorDashboardQueryPanelInstallController' => 'applications/dashboard/controller/PhabricatorDashboardQueryPanelInstallController.php',
'PhabricatorDashboardQueryPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php',
'PhabricatorDashboardRemarkupRule' => 'applications/dashboard/remarkup/PhabricatorDashboardRemarkupRule.php',
'PhabricatorDashboardRemovePanelController' => 'applications/dashboard/controller/PhabricatorDashboardRemovePanelController.php',
'PhabricatorDashboardRenderingEngine' => 'applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php',
'PhabricatorDashboardSchemaSpec' => 'applications/dashboard/storage/PhabricatorDashboardSchemaSpec.php',
'PhabricatorDashboardSearchEngine' => 'applications/dashboard/query/PhabricatorDashboardSearchEngine.php',
'PhabricatorDashboardTabsPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardTabsPanelType.php',
'PhabricatorDashboardTextPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardTextPanelType.php',
'PhabricatorDashboardTransaction' => 'applications/dashboard/storage/PhabricatorDashboardTransaction.php',
'PhabricatorDashboardTransactionEditor' => 'applications/dashboard/editor/PhabricatorDashboardTransactionEditor.php',
'PhabricatorDashboardTransactionQuery' => 'applications/dashboard/query/PhabricatorDashboardTransactionQuery.php',
'PhabricatorDashboardViewController' => 'applications/dashboard/controller/PhabricatorDashboardViewController.php',
'PhabricatorDataCacheSpec' => 'applications/cache/spec/PhabricatorDataCacheSpec.php',
'PhabricatorDataNotAttachedException' => 'infrastructure/storage/lisk/PhabricatorDataNotAttachedException.php',
'PhabricatorDatabaseRef' => 'infrastructure/cluster/PhabricatorDatabaseRef.php',
'PhabricatorDatabaseRefParser' => 'infrastructure/cluster/PhabricatorDatabaseRefParser.php',
'PhabricatorDatabaseSetupCheck' => 'applications/config/check/PhabricatorDatabaseSetupCheck.php',
'PhabricatorDatasourceApplicationEngineExtension' => 'applications/meta/engineextension/PhabricatorDatasourceApplicationEngineExtension.php',
'PhabricatorDatasourceEditField' => 'applications/transactions/editfield/PhabricatorDatasourceEditField.php',
'PhabricatorDatasourceEditType' => 'applications/transactions/edittype/PhabricatorDatasourceEditType.php',
'PhabricatorDatasourceEngine' => 'applications/search/engine/PhabricatorDatasourceEngine.php',
'PhabricatorDatasourceEngineExtension' => 'applications/search/engineextension/PhabricatorDatasourceEngineExtension.php',
'PhabricatorDateFormatSetting' => 'applications/settings/setting/PhabricatorDateFormatSetting.php',
'PhabricatorDateTimeSettingsPanel' => 'applications/settings/panel/PhabricatorDateTimeSettingsPanel.php',
'PhabricatorDebugController' => 'applications/system/controller/PhabricatorDebugController.php',
'PhabricatorDefaultRequestExceptionHandler' => 'aphront/handler/PhabricatorDefaultRequestExceptionHandler.php',
'PhabricatorDefaultSyntaxStyle' => 'infrastructure/syntax/PhabricatorDefaultSyntaxStyle.php',
'PhabricatorDestructibleCodex' => 'applications/system/codex/PhabricatorDestructibleCodex.php',
'PhabricatorDestructibleCodexInterface' => 'applications/system/interface/PhabricatorDestructibleCodexInterface.php',
'PhabricatorDestructibleInterface' => 'applications/system/interface/PhabricatorDestructibleInterface.php',
'PhabricatorDestructionEngine' => 'applications/system/engine/PhabricatorDestructionEngine.php',
'PhabricatorDestructionEngineExtension' => 'applications/system/engine/PhabricatorDestructionEngineExtension.php',
'PhabricatorDestructionEngineExtensionModule' => 'applications/system/engine/PhabricatorDestructionEngineExtensionModule.php',
'PhabricatorDeveloperConfigOptions' => 'applications/config/option/PhabricatorDeveloperConfigOptions.php',
'PhabricatorDeveloperPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorDeveloperPreferencesSettingsPanel.php',
'PhabricatorDiffInlineCommentQuery' => 'infrastructure/diff/query/PhabricatorDiffInlineCommentQuery.php',
'PhabricatorDiffPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorDiffPreferencesSettingsPanel.php',
'PhabricatorDifferenceEngine' => 'infrastructure/diff/PhabricatorDifferenceEngine.php',
'PhabricatorDifferentialApplication' => 'applications/differential/application/PhabricatorDifferentialApplication.php',
'PhabricatorDifferentialAttachCommitWorkflow' => 'applications/differential/management/PhabricatorDifferentialAttachCommitWorkflow.php',
'PhabricatorDifferentialConfigOptions' => 'applications/differential/config/PhabricatorDifferentialConfigOptions.php',
'PhabricatorDifferentialExtractWorkflow' => 'applications/differential/management/PhabricatorDifferentialExtractWorkflow.php',
'PhabricatorDifferentialManagementWorkflow' => 'applications/differential/management/PhabricatorDifferentialManagementWorkflow.php',
'PhabricatorDifferentialMigrateHunkWorkflow' => 'applications/differential/management/PhabricatorDifferentialMigrateHunkWorkflow.php',
'PhabricatorDifferentialRebuildChangesetsWorkflow' => 'applications/differential/management/PhabricatorDifferentialRebuildChangesetsWorkflow.php',
'PhabricatorDifferentialRevisionTestDataGenerator' => 'applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php',
'PhabricatorDiffusionApplication' => 'applications/diffusion/application/PhabricatorDiffusionApplication.php',
'PhabricatorDiffusionBlameSetting' => 'applications/settings/setting/PhabricatorDiffusionBlameSetting.php',
'PhabricatorDiffusionConfigOptions' => 'applications/diffusion/config/PhabricatorDiffusionConfigOptions.php',
'PhabricatorDisabledUserController' => 'applications/auth/controller/PhabricatorDisabledUserController.php',
'PhabricatorDisplayPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorDisplayPreferencesSettingsPanel.php',
'PhabricatorDisqusAuthProvider' => 'applications/auth/provider/PhabricatorDisqusAuthProvider.php',
'PhabricatorDividerEditField' => 'applications/transactions/editfield/PhabricatorDividerEditField.php',
'PhabricatorDividerProfileMenuItem' => 'applications/search/menuitem/PhabricatorDividerProfileMenuItem.php',
'PhabricatorDivinerApplication' => 'applications/diviner/application/PhabricatorDivinerApplication.php',
'PhabricatorDocumentEngine' => 'applications/files/document/PhabricatorDocumentEngine.php',
'PhabricatorDocumentRef' => 'applications/files/document/PhabricatorDocumentRef.php',
'PhabricatorDocumentRenderingEngine' => 'applications/files/document/render/PhabricatorDocumentRenderingEngine.php',
'PhabricatorDoorkeeperApplication' => 'applications/doorkeeper/application/PhabricatorDoorkeeperApplication.php',
'PhabricatorDoubleExportField' => 'infrastructure/export/field/PhabricatorDoubleExportField.php',
'PhabricatorDraft' => 'applications/draft/storage/PhabricatorDraft.php',
'PhabricatorDraftDAO' => 'applications/draft/storage/PhabricatorDraftDAO.php',
'PhabricatorDraftEngine' => 'applications/transactions/draft/PhabricatorDraftEngine.php',
'PhabricatorDraftInterface' => 'applications/transactions/draft/PhabricatorDraftInterface.php',
'PhabricatorDrydockApplication' => 'applications/drydock/application/PhabricatorDrydockApplication.php',
+ 'PhabricatorDuoAuthFactor' => 'applications/auth/factor/PhabricatorDuoAuthFactor.php',
+ 'PhabricatorDuoFuture' => 'applications/auth/future/PhabricatorDuoFuture.php',
'PhabricatorEdgeChangeRecord' => 'infrastructure/edges/util/PhabricatorEdgeChangeRecord.php',
'PhabricatorEdgeChangeRecordTestCase' => 'infrastructure/edges/__tests__/PhabricatorEdgeChangeRecordTestCase.php',
'PhabricatorEdgeConfig' => 'infrastructure/edges/constants/PhabricatorEdgeConfig.php',
'PhabricatorEdgeConstants' => 'infrastructure/edges/constants/PhabricatorEdgeConstants.php',
'PhabricatorEdgeCycleException' => 'infrastructure/edges/exception/PhabricatorEdgeCycleException.php',
'PhabricatorEdgeEditType' => 'applications/transactions/edittype/PhabricatorEdgeEditType.php',
'PhabricatorEdgeEditor' => 'infrastructure/edges/editor/PhabricatorEdgeEditor.php',
'PhabricatorEdgeGraph' => 'infrastructure/edges/util/PhabricatorEdgeGraph.php',
'PhabricatorEdgeObject' => 'infrastructure/edges/conduit/PhabricatorEdgeObject.php',
'PhabricatorEdgeObjectQuery' => 'infrastructure/edges/query/PhabricatorEdgeObjectQuery.php',
'PhabricatorEdgeQuery' => 'infrastructure/edges/query/PhabricatorEdgeQuery.php',
'PhabricatorEdgeTestCase' => 'infrastructure/edges/__tests__/PhabricatorEdgeTestCase.php',
'PhabricatorEdgeType' => 'infrastructure/edges/type/PhabricatorEdgeType.php',
'PhabricatorEdgeTypeTestCase' => 'infrastructure/edges/type/__tests__/PhabricatorEdgeTypeTestCase.php',
'PhabricatorEdgesDestructionEngineExtension' => 'infrastructure/edges/engineextension/PhabricatorEdgesDestructionEngineExtension.php',
'PhabricatorEditEngine' => 'applications/transactions/editengine/PhabricatorEditEngine.php',
'PhabricatorEditEngineAPIMethod' => 'applications/transactions/editengine/PhabricatorEditEngineAPIMethod.php',
'PhabricatorEditEngineBulkJobType' => 'applications/transactions/bulk/PhabricatorEditEngineBulkJobType.php',
'PhabricatorEditEngineCheckboxesCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineCheckboxesCommentAction.php',
'PhabricatorEditEngineColumnsCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineColumnsCommentAction.php',
'PhabricatorEditEngineCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineCommentAction.php',
'PhabricatorEditEngineCommentActionGroup' => 'applications/transactions/commentaction/PhabricatorEditEngineCommentActionGroup.php',
'PhabricatorEditEngineConfiguration' => 'applications/transactions/storage/PhabricatorEditEngineConfiguration.php',
'PhabricatorEditEngineConfigurationDefaultCreateController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationDefaultCreateController.php',
'PhabricatorEditEngineConfigurationDefaultsController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationDefaultsController.php',
'PhabricatorEditEngineConfigurationDisableController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationDisableController.php',
'PhabricatorEditEngineConfigurationEditController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationEditController.php',
'PhabricatorEditEngineConfigurationEditEngine' => 'applications/transactions/editor/PhabricatorEditEngineConfigurationEditEngine.php',
'PhabricatorEditEngineConfigurationEditor' => 'applications/transactions/editor/PhabricatorEditEngineConfigurationEditor.php',
'PhabricatorEditEngineConfigurationIsEditController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationIsEditController.php',
'PhabricatorEditEngineConfigurationListController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationListController.php',
'PhabricatorEditEngineConfigurationLockController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationLockController.php',
'PhabricatorEditEngineConfigurationPHIDType' => 'applications/transactions/phid/PhabricatorEditEngineConfigurationPHIDType.php',
'PhabricatorEditEngineConfigurationQuery' => 'applications/transactions/query/PhabricatorEditEngineConfigurationQuery.php',
'PhabricatorEditEngineConfigurationReorderController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationReorderController.php',
'PhabricatorEditEngineConfigurationSaveController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationSaveController.php',
'PhabricatorEditEngineConfigurationSearchEngine' => 'applications/transactions/query/PhabricatorEditEngineConfigurationSearchEngine.php',
'PhabricatorEditEngineConfigurationSortController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationSortController.php',
'PhabricatorEditEngineConfigurationSubtypeController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationSubtypeController.php',
'PhabricatorEditEngineConfigurationTransaction' => 'applications/transactions/storage/PhabricatorEditEngineConfigurationTransaction.php',
'PhabricatorEditEngineConfigurationTransactionQuery' => 'applications/transactions/query/PhabricatorEditEngineConfigurationTransactionQuery.php',
'PhabricatorEditEngineConfigurationViewController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationViewController.php',
'PhabricatorEditEngineController' => 'applications/transactions/controller/PhabricatorEditEngineController.php',
'PhabricatorEditEngineDatasource' => 'applications/transactions/typeahead/PhabricatorEditEngineDatasource.php',
'PhabricatorEditEngineDefaultLock' => 'applications/transactions/editengine/PhabricatorEditEngineDefaultLock.php',
'PhabricatorEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorEditEngineExtension.php',
'PhabricatorEditEngineExtensionModule' => 'applications/transactions/engineextension/PhabricatorEditEngineExtensionModule.php',
'PhabricatorEditEngineListController' => 'applications/transactions/controller/PhabricatorEditEngineListController.php',
'PhabricatorEditEngineLock' => 'applications/transactions/editengine/PhabricatorEditEngineLock.php',
'PhabricatorEditEngineLockableInterface' => 'applications/transactions/editengine/PhabricatorEditEngineLockableInterface.php',
+ 'PhabricatorEditEngineMFAEngine' => 'applications/transactions/editengine/PhabricatorEditEngineMFAEngine.php',
+ 'PhabricatorEditEngineMFAInterface' => 'applications/transactions/editengine/PhabricatorEditEngineMFAInterface.php',
'PhabricatorEditEnginePointsCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEnginePointsCommentAction.php',
'PhabricatorEditEngineProfileMenuItem' => 'applications/search/menuitem/PhabricatorEditEngineProfileMenuItem.php',
'PhabricatorEditEngineQuery' => 'applications/transactions/query/PhabricatorEditEngineQuery.php',
'PhabricatorEditEngineSearchEngine' => 'applications/transactions/query/PhabricatorEditEngineSearchEngine.php',
'PhabricatorEditEngineSelectCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineSelectCommentAction.php',
'PhabricatorEditEngineSettingsPanel' => 'applications/settings/panel/PhabricatorEditEngineSettingsPanel.php',
'PhabricatorEditEngineStaticCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineStaticCommentAction.php',
'PhabricatorEditEngineSubtype' => 'applications/transactions/editengine/PhabricatorEditEngineSubtype.php',
'PhabricatorEditEngineSubtypeInterface' => 'applications/transactions/editengine/PhabricatorEditEngineSubtypeInterface.php',
'PhabricatorEditEngineSubtypeMap' => 'applications/transactions/editengine/PhabricatorEditEngineSubtypeMap.php',
'PhabricatorEditEngineSubtypeTestCase' => 'applications/transactions/editengine/__tests__/PhabricatorEditEngineSubtypeTestCase.php',
'PhabricatorEditEngineTokenizerCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineTokenizerCommentAction.php',
'PhabricatorEditField' => 'applications/transactions/editfield/PhabricatorEditField.php',
'PhabricatorEditPage' => 'applications/transactions/editengine/PhabricatorEditPage.php',
'PhabricatorEditType' => 'applications/transactions/edittype/PhabricatorEditType.php',
'PhabricatorEditor' => 'infrastructure/PhabricatorEditor.php',
+ 'PhabricatorEditorExtension' => 'applications/transactions/engineextension/PhabricatorEditorExtension.php',
+ 'PhabricatorEditorExtensionModule' => 'applications/transactions/engineextension/PhabricatorEditorExtensionModule.php',
'PhabricatorEditorMailEngineExtension' => 'applications/transactions/engineextension/PhabricatorEditorMailEngineExtension.php',
'PhabricatorEditorMultipleSetting' => 'applications/settings/setting/PhabricatorEditorMultipleSetting.php',
'PhabricatorEditorSetting' => 'applications/settings/setting/PhabricatorEditorSetting.php',
'PhabricatorElasticFulltextStorageEngine' => 'applications/search/fulltextstorage/PhabricatorElasticFulltextStorageEngine.php',
'PhabricatorElasticsearchHost' => 'infrastructure/cluster/search/PhabricatorElasticsearchHost.php',
'PhabricatorElasticsearchQueryBuilder' => 'applications/search/fulltextstorage/PhabricatorElasticsearchQueryBuilder.php',
'PhabricatorElasticsearchSetupCheck' => 'applications/config/check/PhabricatorElasticsearchSetupCheck.php',
'PhabricatorEmailAddressesSettingsPanel' => 'applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php',
'PhabricatorEmailContentSource' => 'applications/metamta/contentsource/PhabricatorEmailContentSource.php',
'PhabricatorEmailDeliverySettingsPanel' => 'applications/settings/panel/PhabricatorEmailDeliverySettingsPanel.php',
'PhabricatorEmailFormatSetting' => 'applications/settings/setting/PhabricatorEmailFormatSetting.php',
'PhabricatorEmailFormatSettingsPanel' => 'applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php',
'PhabricatorEmailLoginController' => 'applications/auth/controller/PhabricatorEmailLoginController.php',
'PhabricatorEmailNotificationsSetting' => 'applications/settings/setting/PhabricatorEmailNotificationsSetting.php',
'PhabricatorEmailPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorEmailPreferencesSettingsPanel.php',
'PhabricatorEmailRePrefixSetting' => 'applications/settings/setting/PhabricatorEmailRePrefixSetting.php',
'PhabricatorEmailSelfActionsSetting' => 'applications/settings/setting/PhabricatorEmailSelfActionsSetting.php',
'PhabricatorEmailStampsSetting' => 'applications/settings/setting/PhabricatorEmailStampsSetting.php',
'PhabricatorEmailTagsSetting' => 'applications/settings/setting/PhabricatorEmailTagsSetting.php',
'PhabricatorEmailVarySubjectsSetting' => 'applications/settings/setting/PhabricatorEmailVarySubjectsSetting.php',
'PhabricatorEmailVerificationController' => 'applications/auth/controller/PhabricatorEmailVerificationController.php',
'PhabricatorEmbedFileRemarkupRule' => 'applications/files/markup/PhabricatorEmbedFileRemarkupRule.php',
'PhabricatorEmojiDatasource' => 'applications/macro/typeahead/PhabricatorEmojiDatasource.php',
'PhabricatorEmojiRemarkupRule' => 'applications/macro/markup/PhabricatorEmojiRemarkupRule.php',
'PhabricatorEmojiTranslation' => 'infrastructure/internationalization/translation/PhabricatorEmojiTranslation.php',
'PhabricatorEmptyQueryException' => 'infrastructure/query/PhabricatorEmptyQueryException.php',
'PhabricatorEnumConfigType' => 'applications/config/type/PhabricatorEnumConfigType.php',
'PhabricatorEnv' => 'infrastructure/env/PhabricatorEnv.php',
'PhabricatorEnvTestCase' => 'infrastructure/env/__tests__/PhabricatorEnvTestCase.php',
'PhabricatorEpochEditField' => 'applications/transactions/editfield/PhabricatorEpochEditField.php',
'PhabricatorEpochExportField' => 'infrastructure/export/field/PhabricatorEpochExportField.php',
'PhabricatorEvent' => 'infrastructure/events/PhabricatorEvent.php',
'PhabricatorEventEngine' => 'infrastructure/events/PhabricatorEventEngine.php',
'PhabricatorEventListener' => 'infrastructure/events/PhabricatorEventListener.php',
'PhabricatorEventType' => 'infrastructure/events/constant/PhabricatorEventType.php',
'PhabricatorExampleEventListener' => 'infrastructure/events/PhabricatorExampleEventListener.php',
'PhabricatorExcelExportFormat' => 'infrastructure/export/format/PhabricatorExcelExportFormat.php',
'PhabricatorExecFutureFileUploadSource' => 'applications/files/uploadsource/PhabricatorExecFutureFileUploadSource.php',
'PhabricatorExportEngine' => 'infrastructure/export/engine/PhabricatorExportEngine.php',
'PhabricatorExportEngineBulkJobType' => 'infrastructure/export/engine/PhabricatorExportEngineBulkJobType.php',
'PhabricatorExportEngineExtension' => 'infrastructure/export/engine/PhabricatorExportEngineExtension.php',
'PhabricatorExportField' => 'infrastructure/export/field/PhabricatorExportField.php',
'PhabricatorExportFormat' => 'infrastructure/export/format/PhabricatorExportFormat.php',
'PhabricatorExportFormatSetting' => 'infrastructure/export/engine/PhabricatorExportFormatSetting.php',
'PhabricatorExtendedPolicyInterface' => 'applications/policy/interface/PhabricatorExtendedPolicyInterface.php',
'PhabricatorExtendingPhabricatorConfigOptions' => 'applications/config/option/PhabricatorExtendingPhabricatorConfigOptions.php',
'PhabricatorExtensionsSetupCheck' => 'applications/config/check/PhabricatorExtensionsSetupCheck.php',
'PhabricatorExternalAccount' => 'applications/people/storage/PhabricatorExternalAccount.php',
'PhabricatorExternalAccountQuery' => 'applications/auth/query/PhabricatorExternalAccountQuery.php',
'PhabricatorExternalAccountsSettingsPanel' => 'applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php',
'PhabricatorExtraConfigSetupCheck' => 'applications/config/check/PhabricatorExtraConfigSetupCheck.php',
'PhabricatorFacebookAuthProvider' => 'applications/auth/provider/PhabricatorFacebookAuthProvider.php',
'PhabricatorFact' => 'applications/fact/fact/PhabricatorFact.php',
'PhabricatorFactAggregate' => 'applications/fact/storage/PhabricatorFactAggregate.php',
'PhabricatorFactApplication' => 'applications/fact/application/PhabricatorFactApplication.php',
'PhabricatorFactChartController' => 'applications/fact/controller/PhabricatorFactChartController.php',
'PhabricatorFactController' => 'applications/fact/controller/PhabricatorFactController.php',
'PhabricatorFactCursor' => 'applications/fact/storage/PhabricatorFactCursor.php',
'PhabricatorFactDAO' => 'applications/fact/storage/PhabricatorFactDAO.php',
'PhabricatorFactDaemon' => 'applications/fact/daemon/PhabricatorFactDaemon.php',
'PhabricatorFactDatapointQuery' => 'applications/fact/query/PhabricatorFactDatapointQuery.php',
'PhabricatorFactDimension' => 'applications/fact/storage/PhabricatorFactDimension.php',
'PhabricatorFactEngine' => 'applications/fact/engine/PhabricatorFactEngine.php',
'PhabricatorFactEngineTestCase' => 'applications/fact/engine/__tests__/PhabricatorFactEngineTestCase.php',
'PhabricatorFactHomeController' => 'applications/fact/controller/PhabricatorFactHomeController.php',
'PhabricatorFactIntDatapoint' => 'applications/fact/storage/PhabricatorFactIntDatapoint.php',
'PhabricatorFactKeyDimension' => 'applications/fact/storage/PhabricatorFactKeyDimension.php',
'PhabricatorFactManagementAnalyzeWorkflow' => 'applications/fact/management/PhabricatorFactManagementAnalyzeWorkflow.php',
'PhabricatorFactManagementCursorsWorkflow' => 'applications/fact/management/PhabricatorFactManagementCursorsWorkflow.php',
'PhabricatorFactManagementDestroyWorkflow' => 'applications/fact/management/PhabricatorFactManagementDestroyWorkflow.php',
'PhabricatorFactManagementListWorkflow' => 'applications/fact/management/PhabricatorFactManagementListWorkflow.php',
'PhabricatorFactManagementWorkflow' => 'applications/fact/management/PhabricatorFactManagementWorkflow.php',
'PhabricatorFactObjectController' => 'applications/fact/controller/PhabricatorFactObjectController.php',
'PhabricatorFactObjectDimension' => 'applications/fact/storage/PhabricatorFactObjectDimension.php',
'PhabricatorFactRaw' => 'applications/fact/storage/PhabricatorFactRaw.php',
'PhabricatorFactUpdateIterator' => 'applications/fact/extract/PhabricatorFactUpdateIterator.php',
'PhabricatorFaviconRef' => 'applications/files/favicon/PhabricatorFaviconRef.php',
'PhabricatorFaviconRefQuery' => 'applications/files/favicon/PhabricatorFaviconRefQuery.php',
'PhabricatorFavoritesApplication' => 'applications/favorites/application/PhabricatorFavoritesApplication.php',
'PhabricatorFavoritesController' => 'applications/favorites/controller/PhabricatorFavoritesController.php',
'PhabricatorFavoritesMainMenuBarExtension' => 'applications/favorites/engineextension/PhabricatorFavoritesMainMenuBarExtension.php',
'PhabricatorFavoritesMenuItemController' => 'applications/favorites/controller/PhabricatorFavoritesMenuItemController.php',
'PhabricatorFavoritesProfileMenuEngine' => 'applications/favorites/engine/PhabricatorFavoritesProfileMenuEngine.php',
'PhabricatorFaxContentSource' => 'infrastructure/contentsource/PhabricatorFaxContentSource.php',
'PhabricatorFeedApplication' => 'applications/feed/application/PhabricatorFeedApplication.php',
'PhabricatorFeedBuilder' => 'applications/feed/builder/PhabricatorFeedBuilder.php',
'PhabricatorFeedConfigOptions' => 'applications/feed/config/PhabricatorFeedConfigOptions.php',
'PhabricatorFeedController' => 'applications/feed/controller/PhabricatorFeedController.php',
'PhabricatorFeedDAO' => 'applications/feed/storage/PhabricatorFeedDAO.php',
'PhabricatorFeedDetailController' => 'applications/feed/controller/PhabricatorFeedDetailController.php',
'PhabricatorFeedListController' => 'applications/feed/controller/PhabricatorFeedListController.php',
'PhabricatorFeedManagementRepublishWorkflow' => 'applications/feed/management/PhabricatorFeedManagementRepublishWorkflow.php',
'PhabricatorFeedManagementWorkflow' => 'applications/feed/management/PhabricatorFeedManagementWorkflow.php',
'PhabricatorFeedQuery' => 'applications/feed/query/PhabricatorFeedQuery.php',
'PhabricatorFeedSearchEngine' => 'applications/feed/query/PhabricatorFeedSearchEngine.php',
'PhabricatorFeedStory' => 'applications/feed/story/PhabricatorFeedStory.php',
'PhabricatorFeedStoryData' => 'applications/feed/storage/PhabricatorFeedStoryData.php',
'PhabricatorFeedStoryNotification' => 'applications/notification/storage/PhabricatorFeedStoryNotification.php',
'PhabricatorFeedStoryPublisher' => 'applications/feed/PhabricatorFeedStoryPublisher.php',
'PhabricatorFeedStoryReference' => 'applications/feed/storage/PhabricatorFeedStoryReference.php',
'PhabricatorFerretEngine' => 'applications/search/ferret/PhabricatorFerretEngine.php',
'PhabricatorFerretEngineTestCase' => 'applications/search/ferret/__tests__/PhabricatorFerretEngineTestCase.php',
'PhabricatorFerretFulltextEngineExtension' => 'applications/search/engineextension/PhabricatorFerretFulltextEngineExtension.php',
'PhabricatorFerretFulltextStorageEngine' => 'applications/search/fulltextstorage/PhabricatorFerretFulltextStorageEngine.php',
'PhabricatorFerretInterface' => 'applications/search/ferret/PhabricatorFerretInterface.php',
'PhabricatorFerretMetadata' => 'applications/search/ferret/PhabricatorFerretMetadata.php',
'PhabricatorFerretSearchEngineExtension' => 'applications/search/engineextension/PhabricatorFerretSearchEngineExtension.php',
'PhabricatorFile' => 'applications/files/storage/PhabricatorFile.php',
'PhabricatorFileAES256StorageFormat' => 'applications/files/format/PhabricatorFileAES256StorageFormat.php',
'PhabricatorFileBundleLoader' => 'applications/files/query/PhabricatorFileBundleLoader.php',
'PhabricatorFileChunk' => 'applications/files/storage/PhabricatorFileChunk.php',
'PhabricatorFileChunkIterator' => 'applications/files/engine/PhabricatorFileChunkIterator.php',
'PhabricatorFileChunkQuery' => 'applications/files/query/PhabricatorFileChunkQuery.php',
'PhabricatorFileComposeController' => 'applications/files/controller/PhabricatorFileComposeController.php',
'PhabricatorFileController' => 'applications/files/controller/PhabricatorFileController.php',
'PhabricatorFileDAO' => 'applications/files/storage/PhabricatorFileDAO.php',
'PhabricatorFileDataController' => 'applications/files/controller/PhabricatorFileDataController.php',
'PhabricatorFileDeleteController' => 'applications/files/controller/PhabricatorFileDeleteController.php',
'PhabricatorFileDeleteTransaction' => 'applications/files/xaction/PhabricatorFileDeleteTransaction.php',
'PhabricatorFileDocumentController' => 'applications/files/controller/PhabricatorFileDocumentController.php',
'PhabricatorFileDocumentRenderingEngine' => 'applications/files/document/render/PhabricatorFileDocumentRenderingEngine.php',
'PhabricatorFileDropUploadController' => 'applications/files/controller/PhabricatorFileDropUploadController.php',
'PhabricatorFileEditController' => 'applications/files/controller/PhabricatorFileEditController.php',
'PhabricatorFileEditEngine' => 'applications/files/editor/PhabricatorFileEditEngine.php',
'PhabricatorFileEditField' => 'applications/transactions/editfield/PhabricatorFileEditField.php',
'PhabricatorFileEditor' => 'applications/files/editor/PhabricatorFileEditor.php',
'PhabricatorFileExternalRequest' => 'applications/files/storage/PhabricatorFileExternalRequest.php',
'PhabricatorFileExternalRequestGarbageCollector' => 'applications/files/garbagecollector/PhabricatorFileExternalRequestGarbageCollector.php',
'PhabricatorFileFilePHIDType' => 'applications/files/phid/PhabricatorFileFilePHIDType.php',
'PhabricatorFileHasObjectEdgeType' => 'applications/files/edge/PhabricatorFileHasObjectEdgeType.php',
'PhabricatorFileIconSetSelectController' => 'applications/files/controller/PhabricatorFileIconSetSelectController.php',
'PhabricatorFileImageMacro' => 'applications/macro/storage/PhabricatorFileImageMacro.php',
'PhabricatorFileImageProxyController' => 'applications/files/controller/PhabricatorFileImageProxyController.php',
'PhabricatorFileImageTransform' => 'applications/files/transform/PhabricatorFileImageTransform.php',
'PhabricatorFileIntegrityException' => 'applications/files/exception/PhabricatorFileIntegrityException.php',
'PhabricatorFileLightboxController' => 'applications/files/controller/PhabricatorFileLightboxController.php',
'PhabricatorFileLinkView' => 'view/layout/PhabricatorFileLinkView.php',
'PhabricatorFileListController' => 'applications/files/controller/PhabricatorFileListController.php',
'PhabricatorFileNameNgrams' => 'applications/files/storage/PhabricatorFileNameNgrams.php',
'PhabricatorFileNameTransaction' => 'applications/files/xaction/PhabricatorFileNameTransaction.php',
'PhabricatorFileQuery' => 'applications/files/query/PhabricatorFileQuery.php',
'PhabricatorFileROT13StorageFormat' => 'applications/files/format/PhabricatorFileROT13StorageFormat.php',
'PhabricatorFileRawStorageFormat' => 'applications/files/format/PhabricatorFileRawStorageFormat.php',
'PhabricatorFileSchemaSpec' => 'applications/files/storage/PhabricatorFileSchemaSpec.php',
'PhabricatorFileSearchConduitAPIMethod' => 'applications/files/conduit/PhabricatorFileSearchConduitAPIMethod.php',
'PhabricatorFileSearchEngine' => 'applications/files/query/PhabricatorFileSearchEngine.php',
'PhabricatorFileStorageBlob' => 'applications/files/storage/PhabricatorFileStorageBlob.php',
'PhabricatorFileStorageConfigurationException' => 'applications/files/exception/PhabricatorFileStorageConfigurationException.php',
'PhabricatorFileStorageEngine' => 'applications/files/engine/PhabricatorFileStorageEngine.php',
'PhabricatorFileStorageEngineTestCase' => 'applications/files/engine/__tests__/PhabricatorFileStorageEngineTestCase.php',
'PhabricatorFileStorageFormat' => 'applications/files/format/PhabricatorFileStorageFormat.php',
'PhabricatorFileStorageFormatTestCase' => 'applications/files/format/__tests__/PhabricatorFileStorageFormatTestCase.php',
'PhabricatorFileTemporaryGarbageCollector' => 'applications/files/garbagecollector/PhabricatorFileTemporaryGarbageCollector.php',
'PhabricatorFileTestCase' => 'applications/files/storage/__tests__/PhabricatorFileTestCase.php',
'PhabricatorFileTestDataGenerator' => 'applications/files/lipsum/PhabricatorFileTestDataGenerator.php',
'PhabricatorFileThumbnailTransform' => 'applications/files/transform/PhabricatorFileThumbnailTransform.php',
'PhabricatorFileTransaction' => 'applications/files/storage/PhabricatorFileTransaction.php',
'PhabricatorFileTransactionComment' => 'applications/files/storage/PhabricatorFileTransactionComment.php',
'PhabricatorFileTransactionQuery' => 'applications/files/query/PhabricatorFileTransactionQuery.php',
'PhabricatorFileTransactionType' => 'applications/files/xaction/PhabricatorFileTransactionType.php',
'PhabricatorFileTransform' => 'applications/files/transform/PhabricatorFileTransform.php',
'PhabricatorFileTransformController' => 'applications/files/controller/PhabricatorFileTransformController.php',
'PhabricatorFileTransformListController' => 'applications/files/controller/PhabricatorFileTransformListController.php',
'PhabricatorFileTransformTestCase' => 'applications/files/transform/__tests__/PhabricatorFileTransformTestCase.php',
'PhabricatorFileUploadController' => 'applications/files/controller/PhabricatorFileUploadController.php',
'PhabricatorFileUploadDialogController' => 'applications/files/controller/PhabricatorFileUploadDialogController.php',
'PhabricatorFileUploadException' => 'applications/files/exception/PhabricatorFileUploadException.php',
'PhabricatorFileUploadSource' => 'applications/files/uploadsource/PhabricatorFileUploadSource.php',
'PhabricatorFileUploadSourceByteLimitException' => 'applications/files/uploadsource/PhabricatorFileUploadSourceByteLimitException.php',
'PhabricatorFileViewController' => 'applications/files/controller/PhabricatorFileViewController.php',
'PhabricatorFileinfoSetupCheck' => 'applications/config/check/PhabricatorFileinfoSetupCheck.php',
'PhabricatorFilesApplication' => 'applications/files/application/PhabricatorFilesApplication.php',
'PhabricatorFilesApplicationStorageEnginePanel' => 'applications/files/applicationpanel/PhabricatorFilesApplicationStorageEnginePanel.php',
'PhabricatorFilesBuiltinFile' => 'applications/files/builtin/PhabricatorFilesBuiltinFile.php',
'PhabricatorFilesComposeAvatarBuiltinFile' => 'applications/files/builtin/PhabricatorFilesComposeAvatarBuiltinFile.php',
'PhabricatorFilesComposeIconBuiltinFile' => 'applications/files/builtin/PhabricatorFilesComposeIconBuiltinFile.php',
'PhabricatorFilesConfigOptions' => 'applications/files/config/PhabricatorFilesConfigOptions.php',
'PhabricatorFilesManagementCatWorkflow' => 'applications/files/management/PhabricatorFilesManagementCatWorkflow.php',
'PhabricatorFilesManagementCompactWorkflow' => 'applications/files/management/PhabricatorFilesManagementCompactWorkflow.php',
'PhabricatorFilesManagementCycleWorkflow' => 'applications/files/management/PhabricatorFilesManagementCycleWorkflow.php',
'PhabricatorFilesManagementEncodeWorkflow' => 'applications/files/management/PhabricatorFilesManagementEncodeWorkflow.php',
'PhabricatorFilesManagementEnginesWorkflow' => 'applications/files/management/PhabricatorFilesManagementEnginesWorkflow.php',
'PhabricatorFilesManagementGenerateKeyWorkflow' => 'applications/files/management/PhabricatorFilesManagementGenerateKeyWorkflow.php',
'PhabricatorFilesManagementIntegrityWorkflow' => 'applications/files/management/PhabricatorFilesManagementIntegrityWorkflow.php',
'PhabricatorFilesManagementMigrateWorkflow' => 'applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php',
'PhabricatorFilesManagementRebuildWorkflow' => 'applications/files/management/PhabricatorFilesManagementRebuildWorkflow.php',
'PhabricatorFilesManagementWorkflow' => 'applications/files/management/PhabricatorFilesManagementWorkflow.php',
'PhabricatorFilesOnDiskBuiltinFile' => 'applications/files/builtin/PhabricatorFilesOnDiskBuiltinFile.php',
'PhabricatorFilesOutboundRequestAction' => 'applications/files/action/PhabricatorFilesOutboundRequestAction.php',
'PhabricatorFiletreeVisibleSetting' => 'applications/settings/setting/PhabricatorFiletreeVisibleSetting.php',
'PhabricatorFiletreeWidthSetting' => 'applications/settings/setting/PhabricatorFiletreeWidthSetting.php',
'PhabricatorFlag' => 'applications/flag/storage/PhabricatorFlag.php',
'PhabricatorFlagAddFlagHeraldAction' => 'applications/flag/herald/PhabricatorFlagAddFlagHeraldAction.php',
'PhabricatorFlagColor' => 'applications/flag/constants/PhabricatorFlagColor.php',
'PhabricatorFlagConstants' => 'applications/flag/constants/PhabricatorFlagConstants.php',
'PhabricatorFlagController' => 'applications/flag/controller/PhabricatorFlagController.php',
'PhabricatorFlagDAO' => 'applications/flag/storage/PhabricatorFlagDAO.php',
'PhabricatorFlagDeleteController' => 'applications/flag/controller/PhabricatorFlagDeleteController.php',
'PhabricatorFlagDestructionEngineExtension' => 'applications/flag/engineextension/PhabricatorFlagDestructionEngineExtension.php',
'PhabricatorFlagEditController' => 'applications/flag/controller/PhabricatorFlagEditController.php',
'PhabricatorFlagListController' => 'applications/flag/controller/PhabricatorFlagListController.php',
'PhabricatorFlagQuery' => 'applications/flag/query/PhabricatorFlagQuery.php',
'PhabricatorFlagSearchEngine' => 'applications/flag/query/PhabricatorFlagSearchEngine.php',
'PhabricatorFlagSelectControl' => 'applications/flag/view/PhabricatorFlagSelectControl.php',
'PhabricatorFlaggableInterface' => 'applications/flag/interface/PhabricatorFlaggableInterface.php',
'PhabricatorFlagsApplication' => 'applications/flag/application/PhabricatorFlagsApplication.php',
'PhabricatorFlagsUIEventListener' => 'applications/flag/events/PhabricatorFlagsUIEventListener.php',
'PhabricatorFulltextEngine' => 'applications/search/index/PhabricatorFulltextEngine.php',
'PhabricatorFulltextEngineExtension' => 'applications/search/index/PhabricatorFulltextEngineExtension.php',
'PhabricatorFulltextEngineExtensionModule' => 'applications/search/index/PhabricatorFulltextEngineExtensionModule.php',
'PhabricatorFulltextIndexEngineExtension' => 'applications/search/engineextension/PhabricatorFulltextIndexEngineExtension.php',
'PhabricatorFulltextInterface' => 'applications/search/interface/PhabricatorFulltextInterface.php',
'PhabricatorFulltextResultSet' => 'applications/search/query/PhabricatorFulltextResultSet.php',
'PhabricatorFulltextStorageEngine' => 'applications/search/fulltextstorage/PhabricatorFulltextStorageEngine.php',
'PhabricatorFulltextToken' => 'applications/search/query/PhabricatorFulltextToken.php',
'PhabricatorFundApplication' => 'applications/fund/application/PhabricatorFundApplication.php',
'PhabricatorGDSetupCheck' => 'applications/config/check/PhabricatorGDSetupCheck.php',
'PhabricatorGarbageCollector' => 'infrastructure/daemon/garbagecollector/PhabricatorGarbageCollector.php',
'PhabricatorGarbageCollectorManagementCollectWorkflow' => 'infrastructure/daemon/garbagecollector/management/PhabricatorGarbageCollectorManagementCollectWorkflow.php',
'PhabricatorGarbageCollectorManagementCompactEdgesWorkflow' => 'infrastructure/daemon/garbagecollector/management/PhabricatorGarbageCollectorManagementCompactEdgesWorkflow.php',
'PhabricatorGarbageCollectorManagementSetPolicyWorkflow' => 'infrastructure/daemon/garbagecollector/management/PhabricatorGarbageCollectorManagementSetPolicyWorkflow.php',
'PhabricatorGarbageCollectorManagementWorkflow' => 'infrastructure/daemon/garbagecollector/management/PhabricatorGarbageCollectorManagementWorkflow.php',
'PhabricatorGeneralCachePurger' => 'applications/cache/purger/PhabricatorGeneralCachePurger.php',
'PhabricatorGestureUIExample' => 'applications/uiexample/examples/PhabricatorGestureUIExample.php',
'PhabricatorGitGraphStream' => 'applications/repository/daemon/PhabricatorGitGraphStream.php',
'PhabricatorGitHubAuthProvider' => 'applications/auth/provider/PhabricatorGitHubAuthProvider.php',
'PhabricatorGlobalLock' => 'infrastructure/util/PhabricatorGlobalLock.php',
'PhabricatorGlobalUploadTargetView' => 'applications/files/view/PhabricatorGlobalUploadTargetView.php',
'PhabricatorGoogleAuthProvider' => 'applications/auth/provider/PhabricatorGoogleAuthProvider.php',
'PhabricatorGuidanceContext' => 'applications/guides/guidance/PhabricatorGuidanceContext.php',
'PhabricatorGuidanceEngine' => 'applications/guides/guidance/PhabricatorGuidanceEngine.php',
'PhabricatorGuidanceEngineExtension' => 'applications/guides/guidance/PhabricatorGuidanceEngineExtension.php',
'PhabricatorGuidanceMessage' => 'applications/guides/guidance/PhabricatorGuidanceMessage.php',
'PhabricatorGuideApplication' => 'applications/guides/application/PhabricatorGuideApplication.php',
'PhabricatorGuideController' => 'applications/guides/controller/PhabricatorGuideController.php',
'PhabricatorGuideInstallModule' => 'applications/guides/module/PhabricatorGuideInstallModule.php',
'PhabricatorGuideItemView' => 'applications/guides/view/PhabricatorGuideItemView.php',
'PhabricatorGuideListView' => 'applications/guides/view/PhabricatorGuideListView.php',
'PhabricatorGuideModule' => 'applications/guides/module/PhabricatorGuideModule.php',
'PhabricatorGuideModuleController' => 'applications/guides/controller/PhabricatorGuideModuleController.php',
'PhabricatorGuideQuickStartModule' => 'applications/guides/module/PhabricatorGuideQuickStartModule.php',
'PhabricatorHMACTestCase' => 'infrastructure/util/__tests__/PhabricatorHMACTestCase.php',
'PhabricatorHTTPParameterTypeTableView' => 'applications/config/view/PhabricatorHTTPParameterTypeTableView.php',
'PhabricatorHandleList' => 'applications/phid/handle/pool/PhabricatorHandleList.php',
'PhabricatorHandleObjectSelectorDataView' => 'applications/phid/handle/view/PhabricatorHandleObjectSelectorDataView.php',
'PhabricatorHandlePool' => 'applications/phid/handle/pool/PhabricatorHandlePool.php',
'PhabricatorHandlePoolTestCase' => 'applications/phid/handle/pool/__tests__/PhabricatorHandlePoolTestCase.php',
'PhabricatorHandleQuery' => 'applications/phid/query/PhabricatorHandleQuery.php',
'PhabricatorHandleRemarkupRule' => 'applications/phid/remarkup/PhabricatorHandleRemarkupRule.php',
'PhabricatorHandlesEditField' => 'applications/transactions/editfield/PhabricatorHandlesEditField.php',
'PhabricatorHarbormasterApplication' => 'applications/harbormaster/application/PhabricatorHarbormasterApplication.php',
'PhabricatorHash' => 'infrastructure/util/PhabricatorHash.php',
'PhabricatorHashTestCase' => 'infrastructure/util/__tests__/PhabricatorHashTestCase.php',
'PhabricatorHelpApplication' => 'applications/help/application/PhabricatorHelpApplication.php',
'PhabricatorHelpController' => 'applications/help/controller/PhabricatorHelpController.php',
'PhabricatorHelpDocumentationController' => 'applications/help/controller/PhabricatorHelpDocumentationController.php',
'PhabricatorHelpEditorProtocolController' => 'applications/help/controller/PhabricatorHelpEditorProtocolController.php',
'PhabricatorHelpKeyboardShortcutController' => 'applications/help/controller/PhabricatorHelpKeyboardShortcutController.php',
'PhabricatorHeraldApplication' => 'applications/herald/application/PhabricatorHeraldApplication.php',
'PhabricatorHeraldContentSource' => 'applications/herald/contentsource/PhabricatorHeraldContentSource.php',
'PhabricatorHexdumpDocumentEngine' => 'applications/files/document/PhabricatorHexdumpDocumentEngine.php',
'PhabricatorHighSecurityRequestExceptionHandler' => 'aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php',
'PhabricatorHomeApplication' => 'applications/home/application/PhabricatorHomeApplication.php',
'PhabricatorHomeConstants' => 'applications/home/constants/PhabricatorHomeConstants.php',
'PhabricatorHomeController' => 'applications/home/controller/PhabricatorHomeController.php',
'PhabricatorHomeLauncherProfileMenuItem' => 'applications/home/menuitem/PhabricatorHomeLauncherProfileMenuItem.php',
'PhabricatorHomeMenuItemController' => 'applications/home/controller/PhabricatorHomeMenuItemController.php',
'PhabricatorHomeProfileMenuEngine' => 'applications/home/engine/PhabricatorHomeProfileMenuEngine.php',
'PhabricatorHomeProfileMenuItem' => 'applications/home/menuitem/PhabricatorHomeProfileMenuItem.php',
'PhabricatorHovercardEngineExtension' => 'applications/search/engineextension/PhabricatorHovercardEngineExtension.php',
'PhabricatorHovercardEngineExtensionModule' => 'applications/search/engineextension/PhabricatorHovercardEngineExtensionModule.php',
'PhabricatorIDExportField' => 'infrastructure/export/field/PhabricatorIDExportField.php',
'PhabricatorIDsSearchEngineExtension' => 'applications/search/engineextension/PhabricatorIDsSearchEngineExtension.php',
'PhabricatorIDsSearchField' => 'applications/search/field/PhabricatorIDsSearchField.php',
'PhabricatorIconDatasource' => 'applications/files/typeahead/PhabricatorIconDatasource.php',
'PhabricatorIconRemarkupRule' => 'applications/macro/markup/PhabricatorIconRemarkupRule.php',
'PhabricatorIconSet' => 'applications/files/iconset/PhabricatorIconSet.php',
'PhabricatorIconSetEditField' => 'applications/transactions/editfield/PhabricatorIconSetEditField.php',
'PhabricatorIconSetIcon' => 'applications/files/iconset/PhabricatorIconSetIcon.php',
'PhabricatorImageDocumentEngine' => 'applications/files/document/PhabricatorImageDocumentEngine.php',
'PhabricatorImageMacroRemarkupRule' => 'applications/macro/markup/PhabricatorImageMacroRemarkupRule.php',
'PhabricatorImageRemarkupRule' => 'applications/files/markup/PhabricatorImageRemarkupRule.php',
'PhabricatorImageTransformer' => 'applications/files/PhabricatorImageTransformer.php',
'PhabricatorImagemagickSetupCheck' => 'applications/config/check/PhabricatorImagemagickSetupCheck.php',
'PhabricatorInFlightErrorView' => 'applications/config/view/PhabricatorInFlightErrorView.php',
'PhabricatorIndexEngine' => 'applications/search/index/PhabricatorIndexEngine.php',
'PhabricatorIndexEngineExtension' => 'applications/search/index/PhabricatorIndexEngineExtension.php',
'PhabricatorIndexEngineExtensionModule' => 'applications/search/index/PhabricatorIndexEngineExtensionModule.php',
'PhabricatorIndexableInterface' => 'applications/search/interface/PhabricatorIndexableInterface.php',
'PhabricatorInfrastructureTestCase' => '__tests__/PhabricatorInfrastructureTestCase.php',
'PhabricatorInlineCommentController' => 'infrastructure/diff/PhabricatorInlineCommentController.php',
'PhabricatorInlineCommentInterface' => 'infrastructure/diff/interface/PhabricatorInlineCommentInterface.php',
'PhabricatorInlineCommentPreviewController' => 'infrastructure/diff/PhabricatorInlineCommentPreviewController.php',
'PhabricatorInlineSummaryView' => 'infrastructure/diff/view/PhabricatorInlineSummaryView.php',
'PhabricatorInstructionsEditField' => 'applications/transactions/editfield/PhabricatorInstructionsEditField.php',
'PhabricatorIntConfigType' => 'applications/config/type/PhabricatorIntConfigType.php',
'PhabricatorIntEditField' => 'applications/transactions/editfield/PhabricatorIntEditField.php',
'PhabricatorIntExportField' => 'infrastructure/export/field/PhabricatorIntExportField.php',
'PhabricatorInternalSetting' => 'applications/settings/setting/PhabricatorInternalSetting.php',
'PhabricatorInternationalizationManagementExtractWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementExtractWorkflow.php',
'PhabricatorInternationalizationManagementWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementWorkflow.php',
'PhabricatorInvalidConfigSetupCheck' => 'applications/config/check/PhabricatorInvalidConfigSetupCheck.php',
'PhabricatorIteratedMD5PasswordHasher' => 'infrastructure/util/password/PhabricatorIteratedMD5PasswordHasher.php',
'PhabricatorIteratedMD5PasswordHasherTestCase' => 'infrastructure/util/password/__tests__/PhabricatorIteratedMD5PasswordHasherTestCase.php',
'PhabricatorIteratorFileUploadSource' => 'applications/files/uploadsource/PhabricatorIteratorFileUploadSource.php',
'PhabricatorJIRAAuthProvider' => 'applications/auth/provider/PhabricatorJIRAAuthProvider.php',
'PhabricatorJSONConfigType' => 'applications/config/type/PhabricatorJSONConfigType.php',
'PhabricatorJSONDocumentEngine' => 'applications/files/document/PhabricatorJSONDocumentEngine.php',
'PhabricatorJSONExportFormat' => 'infrastructure/export/format/PhabricatorJSONExportFormat.php',
'PhabricatorJavelinLinter' => 'infrastructure/lint/linter/PhabricatorJavelinLinter.php',
'PhabricatorJiraIssueHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorJiraIssueHasObjectEdgeType.php',
'PhabricatorJupyterDocumentEngine' => 'applications/files/document/PhabricatorJupyterDocumentEngine.php',
'PhabricatorKeyValueDatabaseCache' => 'applications/cache/PhabricatorKeyValueDatabaseCache.php',
'PhabricatorKeyValueSerializingCacheProxy' => 'applications/cache/PhabricatorKeyValueSerializingCacheProxy.php',
'PhabricatorKeyboardRemarkupRule' => 'infrastructure/markup/rule/PhabricatorKeyboardRemarkupRule.php',
'PhabricatorKeyring' => 'applications/files/keyring/PhabricatorKeyring.php',
'PhabricatorKeyringConfigOptionType' => 'applications/files/keyring/PhabricatorKeyringConfigOptionType.php',
'PhabricatorLDAPAuthProvider' => 'applications/auth/provider/PhabricatorLDAPAuthProvider.php',
'PhabricatorLabelProfileMenuItem' => 'applications/search/menuitem/PhabricatorLabelProfileMenuItem.php',
+ 'PhabricatorLanguageSettingsPanel' => 'applications/settings/panel/PhabricatorLanguageSettingsPanel.php',
'PhabricatorLegalpadApplication' => 'applications/legalpad/application/PhabricatorLegalpadApplication.php',
- 'PhabricatorLegalpadConfigOptions' => 'applications/legalpad/config/PhabricatorLegalpadConfigOptions.php',
'PhabricatorLegalpadDocumentPHIDType' => 'applications/legalpad/phid/PhabricatorLegalpadDocumentPHIDType.php',
'PhabricatorLegalpadSignaturePolicyRule' => 'applications/legalpad/policyrule/PhabricatorLegalpadSignaturePolicyRule.php',
'PhabricatorLibraryTestCase' => '__tests__/PhabricatorLibraryTestCase.php',
'PhabricatorLinkProfileMenuItem' => 'applications/search/menuitem/PhabricatorLinkProfileMenuItem.php',
'PhabricatorLipsumArtist' => 'applications/lipsum/image/PhabricatorLipsumArtist.php',
'PhabricatorLipsumContentSource' => 'infrastructure/contentsource/PhabricatorLipsumContentSource.php',
'PhabricatorLipsumGenerateWorkflow' => 'applications/lipsum/management/PhabricatorLipsumGenerateWorkflow.php',
'PhabricatorLipsumManagementWorkflow' => 'applications/lipsum/management/PhabricatorLipsumManagementWorkflow.php',
'PhabricatorLipsumMondrianArtist' => 'applications/lipsum/image/PhabricatorLipsumMondrianArtist.php',
'PhabricatorLiskDAO' => 'infrastructure/storage/lisk/PhabricatorLiskDAO.php',
'PhabricatorLiskExportEngineExtension' => 'infrastructure/export/engine/PhabricatorLiskExportEngineExtension.php',
'PhabricatorLiskFulltextEngineExtension' => 'applications/search/engineextension/PhabricatorLiskFulltextEngineExtension.php',
'PhabricatorLiskSearchEngineExtension' => 'applications/search/engineextension/PhabricatorLiskSearchEngineExtension.php',
'PhabricatorLiskSerializer' => 'infrastructure/storage/lisk/PhabricatorLiskSerializer.php',
'PhabricatorListExportField' => 'infrastructure/export/field/PhabricatorListExportField.php',
'PhabricatorLocalDiskFileStorageEngine' => 'applications/files/engine/PhabricatorLocalDiskFileStorageEngine.php',
'PhabricatorLocalTimeTestCase' => 'view/__tests__/PhabricatorLocalTimeTestCase.php',
'PhabricatorLocaleScopeGuard' => 'infrastructure/internationalization/scope/PhabricatorLocaleScopeGuard.php',
'PhabricatorLocaleScopeGuardTestCase' => 'infrastructure/internationalization/scope/__tests__/PhabricatorLocaleScopeGuardTestCase.php',
'PhabricatorLockLogManagementWorkflow' => 'applications/daemon/management/PhabricatorLockLogManagementWorkflow.php',
'PhabricatorLockManagementWorkflow' => 'applications/daemon/management/PhabricatorLockManagementWorkflow.php',
'PhabricatorLogTriggerAction' => 'infrastructure/daemon/workers/action/PhabricatorLogTriggerAction.php',
'PhabricatorLogoutController' => 'applications/auth/controller/PhabricatorLogoutController.php',
'PhabricatorLunarPhasePolicyRule' => 'applications/policy/rule/PhabricatorLunarPhasePolicyRule.php',
'PhabricatorMacroApplication' => 'applications/macro/application/PhabricatorMacroApplication.php',
'PhabricatorMacroAudioBehaviorTransaction' => 'applications/macro/xaction/PhabricatorMacroAudioBehaviorTransaction.php',
'PhabricatorMacroAudioController' => 'applications/macro/controller/PhabricatorMacroAudioController.php',
'PhabricatorMacroAudioTransaction' => 'applications/macro/xaction/PhabricatorMacroAudioTransaction.php',
- 'PhabricatorMacroConfigOptions' => 'applications/macro/config/PhabricatorMacroConfigOptions.php',
'PhabricatorMacroController' => 'applications/macro/controller/PhabricatorMacroController.php',
'PhabricatorMacroDatasource' => 'applications/macro/typeahead/PhabricatorMacroDatasource.php',
'PhabricatorMacroDisableController' => 'applications/macro/controller/PhabricatorMacroDisableController.php',
'PhabricatorMacroDisabledTransaction' => 'applications/macro/xaction/PhabricatorMacroDisabledTransaction.php',
'PhabricatorMacroEditController' => 'applications/macro/controller/PhabricatorMacroEditController.php',
'PhabricatorMacroEditEngine' => 'applications/macro/editor/PhabricatorMacroEditEngine.php',
'PhabricatorMacroEditor' => 'applications/macro/editor/PhabricatorMacroEditor.php',
'PhabricatorMacroFileTransaction' => 'applications/macro/xaction/PhabricatorMacroFileTransaction.php',
'PhabricatorMacroListController' => 'applications/macro/controller/PhabricatorMacroListController.php',
'PhabricatorMacroMacroPHIDType' => 'applications/macro/phid/PhabricatorMacroMacroPHIDType.php',
'PhabricatorMacroMailReceiver' => 'applications/macro/mail/PhabricatorMacroMailReceiver.php',
'PhabricatorMacroManageCapability' => 'applications/macro/capability/PhabricatorMacroManageCapability.php',
'PhabricatorMacroMemeController' => 'applications/macro/controller/PhabricatorMacroMemeController.php',
'PhabricatorMacroMemeDialogController' => 'applications/macro/controller/PhabricatorMacroMemeDialogController.php',
'PhabricatorMacroNameTransaction' => 'applications/macro/xaction/PhabricatorMacroNameTransaction.php',
'PhabricatorMacroQuery' => 'applications/macro/query/PhabricatorMacroQuery.php',
'PhabricatorMacroReplyHandler' => 'applications/macro/mail/PhabricatorMacroReplyHandler.php',
'PhabricatorMacroSearchEngine' => 'applications/macro/query/PhabricatorMacroSearchEngine.php',
'PhabricatorMacroTestCase' => 'applications/macro/xaction/__tests__/PhabricatorMacroTestCase.php',
'PhabricatorMacroTransaction' => 'applications/macro/storage/PhabricatorMacroTransaction.php',
'PhabricatorMacroTransactionComment' => 'applications/macro/storage/PhabricatorMacroTransactionComment.php',
'PhabricatorMacroTransactionQuery' => 'applications/macro/query/PhabricatorMacroTransactionQuery.php',
'PhabricatorMacroTransactionType' => 'applications/macro/xaction/PhabricatorMacroTransactionType.php',
'PhabricatorMacroViewController' => 'applications/macro/controller/PhabricatorMacroViewController.php',
+ 'PhabricatorMailAdapter' => 'applications/metamta/adapter/PhabricatorMailAdapter.php',
+ 'PhabricatorMailAmazonSESAdapter' => 'applications/metamta/adapter/PhabricatorMailAmazonSESAdapter.php',
+ 'PhabricatorMailAmazonSNSAdapter' => 'applications/metamta/adapter/PhabricatorMailAmazonSNSAdapter.php',
+ 'PhabricatorMailAttachment' => 'applications/metamta/message/PhabricatorMailAttachment.php',
'PhabricatorMailConfigTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMailConfigTestCase.php',
+ 'PhabricatorMailEmailEngine' => 'applications/metamta/engine/PhabricatorMailEmailEngine.php',
'PhabricatorMailEmailHeraldField' => 'applications/metamta/herald/PhabricatorMailEmailHeraldField.php',
'PhabricatorMailEmailHeraldFieldGroup' => 'applications/metamta/herald/PhabricatorMailEmailHeraldFieldGroup.php',
+ 'PhabricatorMailEmailMessage' => 'applications/metamta/message/PhabricatorMailEmailMessage.php',
'PhabricatorMailEmailSubjectHeraldField' => 'applications/metamta/herald/PhabricatorMailEmailSubjectHeraldField.php',
'PhabricatorMailEngineExtension' => 'applications/metamta/engine/PhabricatorMailEngineExtension.php',
- 'PhabricatorMailImplementationAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationAdapter.php',
- 'PhabricatorMailImplementationAmazonSESAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationAmazonSESAdapter.php',
- 'PhabricatorMailImplementationMailgunAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php',
- 'PhabricatorMailImplementationPHPMailerAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php',
- 'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php',
- 'PhabricatorMailImplementationPostmarkAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationPostmarkAdapter.php',
- 'PhabricatorMailImplementationSendGridAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php',
- 'PhabricatorMailImplementationTestAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php',
+ 'PhabricatorMailExternalMessage' => 'applications/metamta/message/PhabricatorMailExternalMessage.php',
+ 'PhabricatorMailHeader' => 'applications/metamta/message/PhabricatorMailHeader.php',
+ 'PhabricatorMailMailgunAdapter' => 'applications/metamta/adapter/PhabricatorMailMailgunAdapter.php',
'PhabricatorMailManagementListInboundWorkflow' => 'applications/metamta/management/PhabricatorMailManagementListInboundWorkflow.php',
'PhabricatorMailManagementListOutboundWorkflow' => 'applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php',
'PhabricatorMailManagementReceiveTestWorkflow' => 'applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php',
'PhabricatorMailManagementResendWorkflow' => 'applications/metamta/management/PhabricatorMailManagementResendWorkflow.php',
'PhabricatorMailManagementSendTestWorkflow' => 'applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php',
'PhabricatorMailManagementShowInboundWorkflow' => 'applications/metamta/management/PhabricatorMailManagementShowInboundWorkflow.php',
'PhabricatorMailManagementShowOutboundWorkflow' => 'applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php',
'PhabricatorMailManagementUnverifyWorkflow' => 'applications/metamta/management/PhabricatorMailManagementUnverifyWorkflow.php',
'PhabricatorMailManagementVolumeWorkflow' => 'applications/metamta/management/PhabricatorMailManagementVolumeWorkflow.php',
'PhabricatorMailManagementWorkflow' => 'applications/metamta/management/PhabricatorMailManagementWorkflow.php',
+ 'PhabricatorMailMessageEngine' => 'applications/metamta/engine/PhabricatorMailMessageEngine.php',
'PhabricatorMailMustEncryptHeraldAction' => 'applications/metamta/herald/PhabricatorMailMustEncryptHeraldAction.php',
'PhabricatorMailOutboundMailHeraldAdapter' => 'applications/metamta/herald/PhabricatorMailOutboundMailHeraldAdapter.php',
'PhabricatorMailOutboundRoutingHeraldAction' => 'applications/metamta/herald/PhabricatorMailOutboundRoutingHeraldAction.php',
'PhabricatorMailOutboundRoutingSelfEmailHeraldAction' => 'applications/metamta/herald/PhabricatorMailOutboundRoutingSelfEmailHeraldAction.php',
'PhabricatorMailOutboundRoutingSelfNotificationHeraldAction' => 'applications/metamta/herald/PhabricatorMailOutboundRoutingSelfNotificationHeraldAction.php',
'PhabricatorMailOutboundStatus' => 'applications/metamta/constants/PhabricatorMailOutboundStatus.php',
+ 'PhabricatorMailPostmarkAdapter' => 'applications/metamta/adapter/PhabricatorMailPostmarkAdapter.php',
'PhabricatorMailPropertiesDestructionEngineExtension' => 'applications/metamta/engineextension/PhabricatorMailPropertiesDestructionEngineExtension.php',
'PhabricatorMailReceiver' => 'applications/metamta/receiver/PhabricatorMailReceiver.php',
'PhabricatorMailReceiverTestCase' => 'applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php',
'PhabricatorMailReplyHandler' => 'applications/metamta/replyhandler/PhabricatorMailReplyHandler.php',
'PhabricatorMailRoutingRule' => 'applications/metamta/constants/PhabricatorMailRoutingRule.php',
+ 'PhabricatorMailSMSEngine' => 'applications/metamta/engine/PhabricatorMailSMSEngine.php',
+ 'PhabricatorMailSMSMessage' => 'applications/metamta/message/PhabricatorMailSMSMessage.php',
+ 'PhabricatorMailSMTPAdapter' => 'applications/metamta/adapter/PhabricatorMailSMTPAdapter.php',
+ 'PhabricatorMailSendGridAdapter' => 'applications/metamta/adapter/PhabricatorMailSendGridAdapter.php',
+ 'PhabricatorMailSendmailAdapter' => 'applications/metamta/adapter/PhabricatorMailSendmailAdapter.php',
'PhabricatorMailSetupCheck' => 'applications/config/check/PhabricatorMailSetupCheck.php',
'PhabricatorMailStamp' => 'applications/metamta/stamp/PhabricatorMailStamp.php',
'PhabricatorMailTarget' => 'applications/metamta/replyhandler/PhabricatorMailTarget.php',
- 'PhabricatorMailgunConfigOptions' => 'applications/config/option/PhabricatorMailgunConfigOptions.php',
+ 'PhabricatorMailTestAdapter' => 'applications/metamta/adapter/PhabricatorMailTestAdapter.php',
+ 'PhabricatorMailTwilioAdapter' => 'applications/metamta/adapter/PhabricatorMailTwilioAdapter.php',
+ 'PhabricatorMailUtil' => 'applications/metamta/util/PhabricatorMailUtil.php',
'PhabricatorMainMenuBarExtension' => 'view/page/menu/PhabricatorMainMenuBarExtension.php',
'PhabricatorMainMenuSearchView' => 'view/page/menu/PhabricatorMainMenuSearchView.php',
'PhabricatorMainMenuView' => 'view/page/menu/PhabricatorMainMenuView.php',
'PhabricatorManageProfileMenuItem' => 'applications/search/menuitem/PhabricatorManageProfileMenuItem.php',
'PhabricatorManagementWorkflow' => 'infrastructure/management/PhabricatorManagementWorkflow.php',
'PhabricatorManiphestApplication' => 'applications/maniphest/application/PhabricatorManiphestApplication.php',
'PhabricatorManiphestConfigOptions' => 'applications/maniphest/config/PhabricatorManiphestConfigOptions.php',
'PhabricatorManiphestTaskFactEngine' => 'applications/fact/engine/PhabricatorManiphestTaskFactEngine.php',
'PhabricatorManiphestTaskTestDataGenerator' => 'applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php',
'PhabricatorManualActivitySetupCheck' => 'applications/config/check/PhabricatorManualActivitySetupCheck.php',
'PhabricatorMarkupCache' => 'applications/cache/storage/PhabricatorMarkupCache.php',
'PhabricatorMarkupEngine' => 'infrastructure/markup/PhabricatorMarkupEngine.php',
'PhabricatorMarkupEngineTestCase' => 'infrastructure/markup/__tests__/PhabricatorMarkupEngineTestCase.php',
'PhabricatorMarkupInterface' => 'infrastructure/markup/PhabricatorMarkupInterface.php',
'PhabricatorMarkupOneOff' => 'infrastructure/markup/PhabricatorMarkupOneOff.php',
'PhabricatorMarkupPreviewController' => 'infrastructure/markup/PhabricatorMarkupPreviewController.php',
'PhabricatorMemeEngine' => 'applications/macro/engine/PhabricatorMemeEngine.php',
'PhabricatorMemeRemarkupRule' => 'applications/macro/markup/PhabricatorMemeRemarkupRule.php',
'PhabricatorMentionRemarkupRule' => 'applications/people/markup/PhabricatorMentionRemarkupRule.php',
'PhabricatorMentionableInterface' => 'applications/transactions/interface/PhabricatorMentionableInterface.php',
'PhabricatorMercurialGraphStream' => 'applications/repository/daemon/PhabricatorMercurialGraphStream.php',
'PhabricatorMetaMTAActor' => 'applications/metamta/query/PhabricatorMetaMTAActor.php',
'PhabricatorMetaMTAActorQuery' => 'applications/metamta/query/PhabricatorMetaMTAActorQuery.php',
'PhabricatorMetaMTAApplication' => 'applications/metamta/application/PhabricatorMetaMTAApplication.php',
'PhabricatorMetaMTAApplicationEmail' => 'applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php',
'PhabricatorMetaMTAApplicationEmailDatasource' => 'applications/metamta/typeahead/PhabricatorMetaMTAApplicationEmailDatasource.php',
'PhabricatorMetaMTAApplicationEmailEditor' => 'applications/metamta/editor/PhabricatorMetaMTAApplicationEmailEditor.php',
'PhabricatorMetaMTAApplicationEmailHeraldField' => 'applications/metamta/herald/PhabricatorMetaMTAApplicationEmailHeraldField.php',
'PhabricatorMetaMTAApplicationEmailPHIDType' => 'applications/phid/PhabricatorMetaMTAApplicationEmailPHIDType.php',
'PhabricatorMetaMTAApplicationEmailPanel' => 'applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php',
'PhabricatorMetaMTAApplicationEmailQuery' => 'applications/metamta/query/PhabricatorMetaMTAApplicationEmailQuery.php',
'PhabricatorMetaMTAApplicationEmailTransaction' => 'applications/metamta/storage/PhabricatorMetaMTAApplicationEmailTransaction.php',
'PhabricatorMetaMTAApplicationEmailTransactionQuery' => 'applications/metamta/query/PhabricatorMetaMTAApplicationEmailTransactionQuery.php',
- 'PhabricatorMetaMTAAttachment' => 'applications/metamta/storage/PhabricatorMetaMTAAttachment.php',
'PhabricatorMetaMTAConfigOptions' => 'applications/config/option/PhabricatorMetaMTAConfigOptions.php',
'PhabricatorMetaMTAController' => 'applications/metamta/controller/PhabricatorMetaMTAController.php',
'PhabricatorMetaMTADAO' => 'applications/metamta/storage/PhabricatorMetaMTADAO.php',
'PhabricatorMetaMTAEmailBodyParser' => 'applications/metamta/parser/PhabricatorMetaMTAEmailBodyParser.php',
'PhabricatorMetaMTAEmailBodyParserTestCase' => 'applications/metamta/parser/__tests__/PhabricatorMetaMTAEmailBodyParserTestCase.php',
'PhabricatorMetaMTAEmailHeraldAction' => 'applications/metamta/herald/PhabricatorMetaMTAEmailHeraldAction.php',
'PhabricatorMetaMTAEmailOthersHeraldAction' => 'applications/metamta/herald/PhabricatorMetaMTAEmailOthersHeraldAction.php',
'PhabricatorMetaMTAEmailSelfHeraldAction' => 'applications/metamta/herald/PhabricatorMetaMTAEmailSelfHeraldAction.php',
'PhabricatorMetaMTAErrorMailAction' => 'applications/metamta/action/PhabricatorMetaMTAErrorMailAction.php',
'PhabricatorMetaMTAMail' => 'applications/metamta/storage/PhabricatorMetaMTAMail.php',
'PhabricatorMetaMTAMailBody' => 'applications/metamta/view/PhabricatorMetaMTAMailBody.php',
'PhabricatorMetaMTAMailBodyTestCase' => 'applications/metamta/view/__tests__/PhabricatorMetaMTAMailBodyTestCase.php',
'PhabricatorMetaMTAMailHasRecipientEdgeType' => 'applications/metamta/edge/PhabricatorMetaMTAMailHasRecipientEdgeType.php',
'PhabricatorMetaMTAMailListController' => 'applications/metamta/controller/PhabricatorMetaMTAMailListController.php',
'PhabricatorMetaMTAMailPHIDType' => 'applications/metamta/phid/PhabricatorMetaMTAMailPHIDType.php',
'PhabricatorMetaMTAMailProperties' => 'applications/metamta/storage/PhabricatorMetaMTAMailProperties.php',
'PhabricatorMetaMTAMailPropertiesQuery' => 'applications/metamta/query/PhabricatorMetaMTAMailPropertiesQuery.php',
'PhabricatorMetaMTAMailQuery' => 'applications/metamta/query/PhabricatorMetaMTAMailQuery.php',
'PhabricatorMetaMTAMailSearchEngine' => 'applications/metamta/query/PhabricatorMetaMTAMailSearchEngine.php',
'PhabricatorMetaMTAMailSection' => 'applications/metamta/view/PhabricatorMetaMTAMailSection.php',
'PhabricatorMetaMTAMailTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php',
'PhabricatorMetaMTAMailViewController' => 'applications/metamta/controller/PhabricatorMetaMTAMailViewController.php',
'PhabricatorMetaMTAMailableDatasource' => 'applications/metamta/typeahead/PhabricatorMetaMTAMailableDatasource.php',
'PhabricatorMetaMTAMailableFunctionDatasource' => 'applications/metamta/typeahead/PhabricatorMetaMTAMailableFunctionDatasource.php',
'PhabricatorMetaMTAMailgunReceiveController' => 'applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php',
'PhabricatorMetaMTAMemberQuery' => 'applications/metamta/query/PhabricatorMetaMTAMemberQuery.php',
'PhabricatorMetaMTAPermanentFailureException' => 'applications/metamta/exception/PhabricatorMetaMTAPermanentFailureException.php',
'PhabricatorMetaMTAPostmarkReceiveController' => 'applications/metamta/controller/PhabricatorMetaMTAPostmarkReceiveController.php',
'PhabricatorMetaMTAReceivedMail' => 'applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php',
'PhabricatorMetaMTAReceivedMailProcessingException' => 'applications/metamta/exception/PhabricatorMetaMTAReceivedMailProcessingException.php',
'PhabricatorMetaMTAReceivedMailTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMetaMTAReceivedMailTestCase.php',
'PhabricatorMetaMTASchemaSpec' => 'applications/metamta/storage/PhabricatorMetaMTASchemaSpec.php',
'PhabricatorMetaMTASendGridReceiveController' => 'applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php',
'PhabricatorMetaMTAWorker' => 'applications/metamta/PhabricatorMetaMTAWorker.php',
'PhabricatorMetronomicTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorMetronomicTriggerClock.php',
'PhabricatorModularTransaction' => 'applications/transactions/storage/PhabricatorModularTransaction.php',
'PhabricatorModularTransactionType' => 'applications/transactions/storage/PhabricatorModularTransactionType.php',
'PhabricatorMonogramDatasourceEngineExtension' => 'applications/typeahead/engineextension/PhabricatorMonogramDatasourceEngineExtension.php',
'PhabricatorMonospacedFontSetting' => 'applications/settings/setting/PhabricatorMonospacedFontSetting.php',
'PhabricatorMonospacedTextareasSetting' => 'applications/settings/setting/PhabricatorMonospacedTextareasSetting.php',
'PhabricatorMotivatorProfileMenuItem' => 'applications/search/menuitem/PhabricatorMotivatorProfileMenuItem.php',
'PhabricatorMultiColumnUIExample' => 'applications/uiexample/examples/PhabricatorMultiColumnUIExample.php',
'PhabricatorMultiFactorSettingsPanel' => 'applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php',
'PhabricatorMultimeterApplication' => 'applications/multimeter/application/PhabricatorMultimeterApplication.php',
'PhabricatorMustVerifyEmailController' => 'applications/auth/controller/PhabricatorMustVerifyEmailController.php',
'PhabricatorMutedByEdgeType' => 'applications/transactions/edges/PhabricatorMutedByEdgeType.php',
'PhabricatorMutedEdgeType' => 'applications/transactions/edges/PhabricatorMutedEdgeType.php',
'PhabricatorMySQLConfigOptions' => 'applications/config/option/PhabricatorMySQLConfigOptions.php',
'PhabricatorMySQLFileStorageEngine' => 'applications/files/engine/PhabricatorMySQLFileStorageEngine.php',
'PhabricatorMySQLSearchHost' => 'infrastructure/cluster/search/PhabricatorMySQLSearchHost.php',
'PhabricatorMySQLSetupCheck' => 'applications/config/check/PhabricatorMySQLSetupCheck.php',
'PhabricatorNamedQuery' => 'applications/search/storage/PhabricatorNamedQuery.php',
'PhabricatorNamedQueryConfig' => 'applications/search/storage/PhabricatorNamedQueryConfig.php',
'PhabricatorNamedQueryConfigQuery' => 'applications/search/query/PhabricatorNamedQueryConfigQuery.php',
'PhabricatorNamedQueryQuery' => 'applications/search/query/PhabricatorNamedQueryQuery.php',
'PhabricatorNavigationRemarkupRule' => 'infrastructure/markup/rule/PhabricatorNavigationRemarkupRule.php',
'PhabricatorNeverTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorNeverTriggerClock.php',
'PhabricatorNgramsIndexEngineExtension' => 'applications/search/engineextension/PhabricatorNgramsIndexEngineExtension.php',
'PhabricatorNgramsInterface' => 'applications/search/interface/PhabricatorNgramsInterface.php',
'PhabricatorNotificationBuilder' => 'applications/notification/builder/PhabricatorNotificationBuilder.php',
'PhabricatorNotificationClearController' => 'applications/notification/controller/PhabricatorNotificationClearController.php',
'PhabricatorNotificationClient' => 'applications/notification/client/PhabricatorNotificationClient.php',
'PhabricatorNotificationConfigOptions' => 'applications/config/option/PhabricatorNotificationConfigOptions.php',
'PhabricatorNotificationController' => 'applications/notification/controller/PhabricatorNotificationController.php',
'PhabricatorNotificationDestructionEngineExtension' => 'applications/notification/engineextension/PhabricatorNotificationDestructionEngineExtension.php',
'PhabricatorNotificationIndividualController' => 'applications/notification/controller/PhabricatorNotificationIndividualController.php',
'PhabricatorNotificationListController' => 'applications/notification/controller/PhabricatorNotificationListController.php',
'PhabricatorNotificationPanelController' => 'applications/notification/controller/PhabricatorNotificationPanelController.php',
'PhabricatorNotificationQuery' => 'applications/notification/query/PhabricatorNotificationQuery.php',
'PhabricatorNotificationSearchEngine' => 'applications/notification/query/PhabricatorNotificationSearchEngine.php',
'PhabricatorNotificationServerRef' => 'applications/notification/client/PhabricatorNotificationServerRef.php',
'PhabricatorNotificationServersConfigType' => 'applications/notification/config/PhabricatorNotificationServersConfigType.php',
'PhabricatorNotificationStatusView' => 'applications/notification/view/PhabricatorNotificationStatusView.php',
'PhabricatorNotificationTestController' => 'applications/notification/controller/PhabricatorNotificationTestController.php',
'PhabricatorNotificationUIExample' => 'applications/uiexample/examples/PhabricatorNotificationUIExample.php',
'PhabricatorNotificationsApplication' => 'applications/notification/application/PhabricatorNotificationsApplication.php',
'PhabricatorNotificationsSetting' => 'applications/settings/setting/PhabricatorNotificationsSetting.php',
'PhabricatorNotificationsSettingsPanel' => 'applications/settings/panel/PhabricatorNotificationsSettingsPanel.php',
'PhabricatorNuanceApplication' => 'applications/nuance/application/PhabricatorNuanceApplication.php',
'PhabricatorOAuth1AuthProvider' => 'applications/auth/provider/PhabricatorOAuth1AuthProvider.php',
'PhabricatorOAuth1SecretTemporaryTokenType' => 'applications/auth/provider/PhabricatorOAuth1SecretTemporaryTokenType.php',
'PhabricatorOAuth2AuthProvider' => 'applications/auth/provider/PhabricatorOAuth2AuthProvider.php',
'PhabricatorOAuthAuthProvider' => 'applications/auth/provider/PhabricatorOAuthAuthProvider.php',
'PhabricatorOAuthClientAuthorization' => 'applications/oauthserver/storage/PhabricatorOAuthClientAuthorization.php',
'PhabricatorOAuthClientAuthorizationQuery' => 'applications/oauthserver/query/PhabricatorOAuthClientAuthorizationQuery.php',
'PhabricatorOAuthClientController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientController.php',
'PhabricatorOAuthClientDisableController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientDisableController.php',
'PhabricatorOAuthClientEditController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientEditController.php',
'PhabricatorOAuthClientListController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientListController.php',
'PhabricatorOAuthClientSecretController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientSecretController.php',
'PhabricatorOAuthClientTestController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientTestController.php',
'PhabricatorOAuthClientViewController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientViewController.php',
'PhabricatorOAuthResponse' => 'applications/oauthserver/PhabricatorOAuthResponse.php',
'PhabricatorOAuthServer' => 'applications/oauthserver/PhabricatorOAuthServer.php',
'PhabricatorOAuthServerAccessToken' => 'applications/oauthserver/storage/PhabricatorOAuthServerAccessToken.php',
'PhabricatorOAuthServerApplication' => 'applications/oauthserver/application/PhabricatorOAuthServerApplication.php',
'PhabricatorOAuthServerAuthController' => 'applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php',
'PhabricatorOAuthServerAuthorizationCode' => 'applications/oauthserver/storage/PhabricatorOAuthServerAuthorizationCode.php',
'PhabricatorOAuthServerAuthorizationsSettingsPanel' => 'applications/oauthserver/panel/PhabricatorOAuthServerAuthorizationsSettingsPanel.php',
'PhabricatorOAuthServerClient' => 'applications/oauthserver/storage/PhabricatorOAuthServerClient.php',
'PhabricatorOAuthServerClientAuthorizationPHIDType' => 'applications/oauthserver/phid/PhabricatorOAuthServerClientAuthorizationPHIDType.php',
'PhabricatorOAuthServerClientPHIDType' => 'applications/oauthserver/phid/PhabricatorOAuthServerClientPHIDType.php',
'PhabricatorOAuthServerClientQuery' => 'applications/oauthserver/query/PhabricatorOAuthServerClientQuery.php',
'PhabricatorOAuthServerClientSearchEngine' => 'applications/oauthserver/query/PhabricatorOAuthServerClientSearchEngine.php',
'PhabricatorOAuthServerController' => 'applications/oauthserver/controller/PhabricatorOAuthServerController.php',
'PhabricatorOAuthServerCreateClientsCapability' => 'applications/oauthserver/capability/PhabricatorOAuthServerCreateClientsCapability.php',
'PhabricatorOAuthServerDAO' => 'applications/oauthserver/storage/PhabricatorOAuthServerDAO.php',
'PhabricatorOAuthServerEditEngine' => 'applications/oauthserver/editor/PhabricatorOAuthServerEditEngine.php',
'PhabricatorOAuthServerEditor' => 'applications/oauthserver/editor/PhabricatorOAuthServerEditor.php',
'PhabricatorOAuthServerSchemaSpec' => 'applications/oauthserver/query/PhabricatorOAuthServerSchemaSpec.php',
'PhabricatorOAuthServerScope' => 'applications/oauthserver/PhabricatorOAuthServerScope.php',
'PhabricatorOAuthServerTestCase' => 'applications/oauthserver/__tests__/PhabricatorOAuthServerTestCase.php',
'PhabricatorOAuthServerTokenController' => 'applications/oauthserver/controller/PhabricatorOAuthServerTokenController.php',
'PhabricatorOAuthServerTransaction' => 'applications/oauthserver/storage/PhabricatorOAuthServerTransaction.php',
'PhabricatorOAuthServerTransactionQuery' => 'applications/oauthserver/query/PhabricatorOAuthServerTransactionQuery.php',
'PhabricatorObjectGraph' => 'infrastructure/graph/PhabricatorObjectGraph.php',
'PhabricatorObjectHandle' => 'applications/phid/PhabricatorObjectHandle.php',
'PhabricatorObjectHasAsanaSubtaskEdgeType' => 'applications/doorkeeper/edge/PhabricatorObjectHasAsanaSubtaskEdgeType.php',
'PhabricatorObjectHasAsanaTaskEdgeType' => 'applications/doorkeeper/edge/PhabricatorObjectHasAsanaTaskEdgeType.php',
'PhabricatorObjectHasContributorEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasContributorEdgeType.php',
'PhabricatorObjectHasDraftEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasDraftEdgeType.php',
'PhabricatorObjectHasFileEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasFileEdgeType.php',
'PhabricatorObjectHasJiraIssueEdgeType' => 'applications/doorkeeper/edge/PhabricatorObjectHasJiraIssueEdgeType.php',
'PhabricatorObjectHasSubscriberEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasSubscriberEdgeType.php',
'PhabricatorObjectHasUnsubscriberEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasUnsubscriberEdgeType.php',
'PhabricatorObjectHasWatcherEdgeType' => 'applications/transactions/edges/PhabricatorObjectHasWatcherEdgeType.php',
'PhabricatorObjectListQuery' => 'applications/phid/query/PhabricatorObjectListQuery.php',
'PhabricatorObjectListQueryTestCase' => 'applications/phid/query/__tests__/PhabricatorObjectListQueryTestCase.php',
'PhabricatorObjectMailReceiver' => 'applications/metamta/receiver/PhabricatorObjectMailReceiver.php',
'PhabricatorObjectMailReceiverTestCase' => 'applications/metamta/receiver/__tests__/PhabricatorObjectMailReceiverTestCase.php',
'PhabricatorObjectMentionedByObjectEdgeType' => 'applications/transactions/edges/PhabricatorObjectMentionedByObjectEdgeType.php',
'PhabricatorObjectMentionsObjectEdgeType' => 'applications/transactions/edges/PhabricatorObjectMentionsObjectEdgeType.php',
'PhabricatorObjectQuery' => 'applications/phid/query/PhabricatorObjectQuery.php',
'PhabricatorObjectRelationship' => 'applications/search/relationship/PhabricatorObjectRelationship.php',
'PhabricatorObjectRelationshipList' => 'applications/search/relationship/PhabricatorObjectRelationshipList.php',
'PhabricatorObjectRelationshipSource' => 'applications/search/relationship/PhabricatorObjectRelationshipSource.php',
'PhabricatorObjectRemarkupRule' => 'infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php',
'PhabricatorObjectSelectorDialog' => 'view/control/PhabricatorObjectSelectorDialog.php',
'PhabricatorObjectStatus' => 'infrastructure/status/PhabricatorObjectStatus.php',
'PhabricatorOffsetPagedQuery' => 'infrastructure/query/PhabricatorOffsetPagedQuery.php',
'PhabricatorOldWorldContentSource' => 'infrastructure/contentsource/PhabricatorOldWorldContentSource.php',
'PhabricatorOlderInlinesSetting' => 'applications/settings/setting/PhabricatorOlderInlinesSetting.php',
'PhabricatorOneTimeTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorOneTimeTriggerClock.php',
'PhabricatorOpcodeCacheSpec' => 'applications/cache/spec/PhabricatorOpcodeCacheSpec.php',
'PhabricatorOptionGroupSetting' => 'applications/settings/setting/PhabricatorOptionGroupSetting.php',
'PhabricatorOwnerPathQuery' => 'applications/owners/query/PhabricatorOwnerPathQuery.php',
'PhabricatorOwnersApplication' => 'applications/owners/application/PhabricatorOwnersApplication.php',
'PhabricatorOwnersArchiveController' => 'applications/owners/controller/PhabricatorOwnersArchiveController.php',
'PhabricatorOwnersConfigOptions' => 'applications/owners/config/PhabricatorOwnersConfigOptions.php',
'PhabricatorOwnersConfiguredCustomField' => 'applications/owners/customfield/PhabricatorOwnersConfiguredCustomField.php',
'PhabricatorOwnersController' => 'applications/owners/controller/PhabricatorOwnersController.php',
'PhabricatorOwnersCustomField' => 'applications/owners/customfield/PhabricatorOwnersCustomField.php',
'PhabricatorOwnersCustomFieldNumericIndex' => 'applications/owners/storage/PhabricatorOwnersCustomFieldNumericIndex.php',
'PhabricatorOwnersCustomFieldStorage' => 'applications/owners/storage/PhabricatorOwnersCustomFieldStorage.php',
'PhabricatorOwnersCustomFieldStringIndex' => 'applications/owners/storage/PhabricatorOwnersCustomFieldStringIndex.php',
'PhabricatorOwnersDAO' => 'applications/owners/storage/PhabricatorOwnersDAO.php',
'PhabricatorOwnersDefaultEditCapability' => 'applications/owners/capability/PhabricatorOwnersDefaultEditCapability.php',
'PhabricatorOwnersDefaultViewCapability' => 'applications/owners/capability/PhabricatorOwnersDefaultViewCapability.php',
'PhabricatorOwnersDetailController' => 'applications/owners/controller/PhabricatorOwnersDetailController.php',
'PhabricatorOwnersEditController' => 'applications/owners/controller/PhabricatorOwnersEditController.php',
'PhabricatorOwnersHovercardEngineExtension' => 'applications/owners/engineextension/PhabricatorOwnersHovercardEngineExtension.php',
'PhabricatorOwnersListController' => 'applications/owners/controller/PhabricatorOwnersListController.php',
'PhabricatorOwnersOwner' => 'applications/owners/storage/PhabricatorOwnersOwner.php',
'PhabricatorOwnersPackage' => 'applications/owners/storage/PhabricatorOwnersPackage.php',
'PhabricatorOwnersPackageAuditingTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageAuditingTransaction.php',
'PhabricatorOwnersPackageAutoreviewTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageAutoreviewTransaction.php',
'PhabricatorOwnersPackageContextFreeGrammar' => 'applications/owners/lipsum/PhabricatorOwnersPackageContextFreeGrammar.php',
'PhabricatorOwnersPackageDatasource' => 'applications/owners/typeahead/PhabricatorOwnersPackageDatasource.php',
'PhabricatorOwnersPackageDescriptionTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageDescriptionTransaction.php',
'PhabricatorOwnersPackageDominionTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageDominionTransaction.php',
'PhabricatorOwnersPackageEditEngine' => 'applications/owners/editor/PhabricatorOwnersPackageEditEngine.php',
'PhabricatorOwnersPackageFerretEngine' => 'applications/owners/search/PhabricatorOwnersPackageFerretEngine.php',
'PhabricatorOwnersPackageFulltextEngine' => 'applications/owners/search/PhabricatorOwnersPackageFulltextEngine.php',
'PhabricatorOwnersPackageFunctionDatasource' => 'applications/owners/typeahead/PhabricatorOwnersPackageFunctionDatasource.php',
'PhabricatorOwnersPackageIgnoredTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageIgnoredTransaction.php',
'PhabricatorOwnersPackageNameNgrams' => 'applications/owners/storage/PhabricatorOwnersPackageNameNgrams.php',
'PhabricatorOwnersPackageNameTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageNameTransaction.php',
'PhabricatorOwnersPackageOwnerDatasource' => 'applications/owners/typeahead/PhabricatorOwnersPackageOwnerDatasource.php',
'PhabricatorOwnersPackageOwnersTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageOwnersTransaction.php',
'PhabricatorOwnersPackagePHIDType' => 'applications/owners/phid/PhabricatorOwnersPackagePHIDType.php',
'PhabricatorOwnersPackagePathsTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackagePathsTransaction.php',
'PhabricatorOwnersPackagePrimaryTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackagePrimaryTransaction.php',
'PhabricatorOwnersPackageQuery' => 'applications/owners/query/PhabricatorOwnersPackageQuery.php',
'PhabricatorOwnersPackageRemarkupRule' => 'applications/owners/remarkup/PhabricatorOwnersPackageRemarkupRule.php',
'PhabricatorOwnersPackageSearchEngine' => 'applications/owners/query/PhabricatorOwnersPackageSearchEngine.php',
'PhabricatorOwnersPackageStatusTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageStatusTransaction.php',
'PhabricatorOwnersPackageTestCase' => 'applications/owners/storage/__tests__/PhabricatorOwnersPackageTestCase.php',
'PhabricatorOwnersPackageTestDataGenerator' => 'applications/owners/lipsum/PhabricatorOwnersPackageTestDataGenerator.php',
'PhabricatorOwnersPackageTransaction' => 'applications/owners/storage/PhabricatorOwnersPackageTransaction.php',
'PhabricatorOwnersPackageTransactionEditor' => 'applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php',
'PhabricatorOwnersPackageTransactionQuery' => 'applications/owners/query/PhabricatorOwnersPackageTransactionQuery.php',
'PhabricatorOwnersPackageTransactionType' => 'applications/owners/xaction/PhabricatorOwnersPackageTransactionType.php',
'PhabricatorOwnersPath' => 'applications/owners/storage/PhabricatorOwnersPath.php',
'PhabricatorOwnersPathContextFreeGrammar' => 'applications/owners/lipsum/PhabricatorOwnersPathContextFreeGrammar.php',
'PhabricatorOwnersPathsController' => 'applications/owners/controller/PhabricatorOwnersPathsController.php',
'PhabricatorOwnersPathsSearchEngineAttachment' => 'applications/owners/engineextension/PhabricatorOwnersPathsSearchEngineAttachment.php',
'PhabricatorOwnersSchemaSpec' => 'applications/owners/storage/PhabricatorOwnersSchemaSpec.php',
'PhabricatorOwnersSearchField' => 'applications/owners/searchfield/PhabricatorOwnersSearchField.php',
'PhabricatorPDFDocumentEngine' => 'applications/files/document/PhabricatorPDFDocumentEngine.php',
'PhabricatorPHDConfigOptions' => 'applications/config/option/PhabricatorPHDConfigOptions.php',
'PhabricatorPHID' => 'applications/phid/storage/PhabricatorPHID.php',
'PhabricatorPHIDConstants' => 'applications/phid/PhabricatorPHIDConstants.php',
'PhabricatorPHIDExportField' => 'infrastructure/export/field/PhabricatorPHIDExportField.php',
'PhabricatorPHIDInterface' => 'applications/phid/interface/PhabricatorPHIDInterface.php',
'PhabricatorPHIDListEditField' => 'applications/transactions/editfield/PhabricatorPHIDListEditField.php',
'PhabricatorPHIDListEditType' => 'applications/transactions/edittype/PhabricatorPHIDListEditType.php',
'PhabricatorPHIDListExportField' => 'infrastructure/export/field/PhabricatorPHIDListExportField.php',
'PhabricatorPHIDMailStamp' => 'applications/metamta/stamp/PhabricatorPHIDMailStamp.php',
'PhabricatorPHIDResolver' => 'applications/phid/resolver/PhabricatorPHIDResolver.php',
'PhabricatorPHIDType' => 'applications/phid/type/PhabricatorPHIDType.php',
'PhabricatorPHIDTypeTestCase' => 'applications/phid/type/__tests__/PhabricatorPHIDTypeTestCase.php',
'PhabricatorPHIDsSearchField' => 'applications/search/field/PhabricatorPHIDsSearchField.php',
'PhabricatorPHPASTApplication' => 'applications/phpast/application/PhabricatorPHPASTApplication.php',
'PhabricatorPHPConfigSetupCheck' => 'applications/config/check/PhabricatorPHPConfigSetupCheck.php',
- 'PhabricatorPHPMailerConfigOptions' => 'applications/config/option/PhabricatorPHPMailerConfigOptions.php',
'PhabricatorPHPPreflightSetupCheck' => 'applications/config/check/PhabricatorPHPPreflightSetupCheck.php',
'PhabricatorPackagesApplication' => 'applications/packages/application/PhabricatorPackagesApplication.php',
'PhabricatorPackagesController' => 'applications/packages/controller/PhabricatorPackagesController.php',
'PhabricatorPackagesCreatePublisherCapability' => 'applications/packages/capability/PhabricatorPackagesCreatePublisherCapability.php',
'PhabricatorPackagesDAO' => 'applications/packages/storage/PhabricatorPackagesDAO.php',
'PhabricatorPackagesEditEngine' => 'applications/packages/editor/PhabricatorPackagesEditEngine.php',
'PhabricatorPackagesEditor' => 'applications/packages/editor/PhabricatorPackagesEditor.php',
'PhabricatorPackagesNgrams' => 'applications/packages/storage/PhabricatorPackagesNgrams.php',
'PhabricatorPackagesPackage' => 'applications/packages/storage/PhabricatorPackagesPackage.php',
'PhabricatorPackagesPackageController' => 'applications/packages/controller/PhabricatorPackagesPackageController.php',
'PhabricatorPackagesPackageDatasource' => 'applications/packages/typeahead/PhabricatorPackagesPackageDatasource.php',
'PhabricatorPackagesPackageDefaultEditCapability' => 'applications/packages/capability/PhabricatorPackagesPackageDefaultEditCapability.php',
'PhabricatorPackagesPackageDefaultViewCapability' => 'applications/packages/capability/PhabricatorPackagesPackageDefaultViewCapability.php',
'PhabricatorPackagesPackageEditConduitAPIMethod' => 'applications/packages/conduit/PhabricatorPackagesPackageEditConduitAPIMethod.php',
'PhabricatorPackagesPackageEditController' => 'applications/packages/controller/PhabricatorPackagesPackageEditController.php',
'PhabricatorPackagesPackageEditEngine' => 'applications/packages/editor/PhabricatorPackagesPackageEditEngine.php',
'PhabricatorPackagesPackageEditor' => 'applications/packages/editor/PhabricatorPackagesPackageEditor.php',
'PhabricatorPackagesPackageKeyTransaction' => 'applications/packages/xaction/package/PhabricatorPackagesPackageKeyTransaction.php',
'PhabricatorPackagesPackageListController' => 'applications/packages/controller/PhabricatorPackagesPackageListController.php',
'PhabricatorPackagesPackageListView' => 'applications/packages/view/PhabricatorPackagesPackageListView.php',
'PhabricatorPackagesPackageNameNgrams' => 'applications/packages/storage/PhabricatorPackagesPackageNameNgrams.php',
'PhabricatorPackagesPackageNameTransaction' => 'applications/packages/xaction/package/PhabricatorPackagesPackageNameTransaction.php',
'PhabricatorPackagesPackagePHIDType' => 'applications/packages/phid/PhabricatorPackagesPackagePHIDType.php',
'PhabricatorPackagesPackagePublisherTransaction' => 'applications/packages/xaction/package/PhabricatorPackagesPackagePublisherTransaction.php',
'PhabricatorPackagesPackageQuery' => 'applications/packages/query/PhabricatorPackagesPackageQuery.php',
'PhabricatorPackagesPackageSearchConduitAPIMethod' => 'applications/packages/conduit/PhabricatorPackagesPackageSearchConduitAPIMethod.php',
'PhabricatorPackagesPackageSearchEngine' => 'applications/packages/query/PhabricatorPackagesPackageSearchEngine.php',
'PhabricatorPackagesPackageTransaction' => 'applications/packages/storage/PhabricatorPackagesPackageTransaction.php',
'PhabricatorPackagesPackageTransactionQuery' => 'applications/packages/query/PhabricatorPackagesPackageTransactionQuery.php',
'PhabricatorPackagesPackageTransactionType' => 'applications/packages/xaction/package/PhabricatorPackagesPackageTransactionType.php',
'PhabricatorPackagesPackageViewController' => 'applications/packages/controller/PhabricatorPackagesPackageViewController.php',
'PhabricatorPackagesPublisher' => 'applications/packages/storage/PhabricatorPackagesPublisher.php',
'PhabricatorPackagesPublisherController' => 'applications/packages/controller/PhabricatorPackagesPublisherController.php',
'PhabricatorPackagesPublisherDatasource' => 'applications/packages/typeahead/PhabricatorPackagesPublisherDatasource.php',
'PhabricatorPackagesPublisherDefaultEditCapability' => 'applications/packages/capability/PhabricatorPackagesPublisherDefaultEditCapability.php',
'PhabricatorPackagesPublisherEditConduitAPIMethod' => 'applications/packages/conduit/PhabricatorPackagesPublisherEditConduitAPIMethod.php',
'PhabricatorPackagesPublisherEditController' => 'applications/packages/controller/PhabricatorPackagesPublisherEditController.php',
'PhabricatorPackagesPublisherEditEngine' => 'applications/packages/editor/PhabricatorPackagesPublisherEditEngine.php',
'PhabricatorPackagesPublisherEditor' => 'applications/packages/editor/PhabricatorPackagesPublisherEditor.php',
'PhabricatorPackagesPublisherKeyTransaction' => 'applications/packages/xaction/publisher/PhabricatorPackagesPublisherKeyTransaction.php',
'PhabricatorPackagesPublisherListController' => 'applications/packages/controller/PhabricatorPackagesPublisherListController.php',
'PhabricatorPackagesPublisherListView' => 'applications/packages/view/PhabricatorPackagesPublisherListView.php',
'PhabricatorPackagesPublisherNameNgrams' => 'applications/packages/storage/PhabricatorPackagesPublisherNameNgrams.php',
'PhabricatorPackagesPublisherNameTransaction' => 'applications/packages/xaction/publisher/PhabricatorPackagesPublisherNameTransaction.php',
'PhabricatorPackagesPublisherPHIDType' => 'applications/packages/phid/PhabricatorPackagesPublisherPHIDType.php',
'PhabricatorPackagesPublisherQuery' => 'applications/packages/query/PhabricatorPackagesPublisherQuery.php',
'PhabricatorPackagesPublisherSearchConduitAPIMethod' => 'applications/packages/conduit/PhabricatorPackagesPublisherSearchConduitAPIMethod.php',
'PhabricatorPackagesPublisherSearchEngine' => 'applications/packages/query/PhabricatorPackagesPublisherSearchEngine.php',
'PhabricatorPackagesPublisherTransaction' => 'applications/packages/storage/PhabricatorPackagesPublisherTransaction.php',
'PhabricatorPackagesPublisherTransactionQuery' => 'applications/packages/query/PhabricatorPackagesPublisherTransactionQuery.php',
'PhabricatorPackagesPublisherTransactionType' => 'applications/packages/xaction/publisher/PhabricatorPackagesPublisherTransactionType.php',
'PhabricatorPackagesPublisherViewController' => 'applications/packages/controller/PhabricatorPackagesPublisherViewController.php',
'PhabricatorPackagesQuery' => 'applications/packages/query/PhabricatorPackagesQuery.php',
'PhabricatorPackagesSchemaSpec' => 'applications/packages/storage/PhabricatorPackagesSchemaSpec.php',
'PhabricatorPackagesTransactionType' => 'applications/packages/xaction/PhabricatorPackagesTransactionType.php',
'PhabricatorPackagesVersion' => 'applications/packages/storage/PhabricatorPackagesVersion.php',
'PhabricatorPackagesVersionController' => 'applications/packages/controller/PhabricatorPackagesVersionController.php',
'PhabricatorPackagesVersionEditConduitAPIMethod' => 'applications/packages/conduit/PhabricatorPackagesVersionEditConduitAPIMethod.php',
'PhabricatorPackagesVersionEditController' => 'applications/packages/controller/PhabricatorPackagesVersionEditController.php',
'PhabricatorPackagesVersionEditEngine' => 'applications/packages/editor/PhabricatorPackagesVersionEditEngine.php',
'PhabricatorPackagesVersionEditor' => 'applications/packages/editor/PhabricatorPackagesVersionEditor.php',
'PhabricatorPackagesVersionListController' => 'applications/packages/controller/PhabricatorPackagesVersionListController.php',
'PhabricatorPackagesVersionListView' => 'applications/packages/view/PhabricatorPackagesVersionListView.php',
'PhabricatorPackagesVersionNameNgrams' => 'applications/packages/storage/PhabricatorPackagesVersionNameNgrams.php',
'PhabricatorPackagesVersionNameTransaction' => 'applications/packages/xaction/version/PhabricatorPackagesVersionNameTransaction.php',
'PhabricatorPackagesVersionPHIDType' => 'applications/packages/phid/PhabricatorPackagesVersionPHIDType.php',
'PhabricatorPackagesVersionPackageTransaction' => 'applications/packages/xaction/version/PhabricatorPackagesVersionPackageTransaction.php',
'PhabricatorPackagesVersionQuery' => 'applications/packages/query/PhabricatorPackagesVersionQuery.php',
'PhabricatorPackagesVersionSearchConduitAPIMethod' => 'applications/packages/conduit/PhabricatorPackagesVersionSearchConduitAPIMethod.php',
'PhabricatorPackagesVersionSearchEngine' => 'applications/packages/query/PhabricatorPackagesVersionSearchEngine.php',
'PhabricatorPackagesVersionTransaction' => 'applications/packages/storage/PhabricatorPackagesVersionTransaction.php',
'PhabricatorPackagesVersionTransactionQuery' => 'applications/packages/query/PhabricatorPackagesVersionTransactionQuery.php',
'PhabricatorPackagesVersionTransactionType' => 'applications/packages/xaction/version/PhabricatorPackagesVersionTransactionType.php',
'PhabricatorPackagesVersionViewController' => 'applications/packages/controller/PhabricatorPackagesVersionViewController.php',
'PhabricatorPackagesView' => 'applications/packages/view/PhabricatorPackagesView.php',
'PhabricatorPagerUIExample' => 'applications/uiexample/examples/PhabricatorPagerUIExample.php',
'PhabricatorPassphraseApplication' => 'applications/passphrase/application/PhabricatorPassphraseApplication.php',
'PhabricatorPasswordAuthProvider' => 'applications/auth/provider/PhabricatorPasswordAuthProvider.php',
'PhabricatorPasswordDestructionEngineExtension' => 'applications/auth/extension/PhabricatorPasswordDestructionEngineExtension.php',
'PhabricatorPasswordHasher' => 'infrastructure/util/password/PhabricatorPasswordHasher.php',
'PhabricatorPasswordHasherTestCase' => 'infrastructure/util/password/__tests__/PhabricatorPasswordHasherTestCase.php',
'PhabricatorPasswordHasherUnavailableException' => 'infrastructure/util/password/PhabricatorPasswordHasherUnavailableException.php',
'PhabricatorPasswordSettingsPanel' => 'applications/settings/panel/PhabricatorPasswordSettingsPanel.php',
'PhabricatorPaste' => 'applications/paste/storage/PhabricatorPaste.php',
'PhabricatorPasteApplication' => 'applications/paste/application/PhabricatorPasteApplication.php',
'PhabricatorPasteArchiveController' => 'applications/paste/controller/PhabricatorPasteArchiveController.php',
- 'PhabricatorPasteConfigOptions' => 'applications/paste/config/PhabricatorPasteConfigOptions.php',
'PhabricatorPasteContentSearchEngineAttachment' => 'applications/paste/engineextension/PhabricatorPasteContentSearchEngineAttachment.php',
'PhabricatorPasteContentTransaction' => 'applications/paste/xaction/PhabricatorPasteContentTransaction.php',
'PhabricatorPasteController' => 'applications/paste/controller/PhabricatorPasteController.php',
'PhabricatorPasteDAO' => 'applications/paste/storage/PhabricatorPasteDAO.php',
'PhabricatorPasteEditController' => 'applications/paste/controller/PhabricatorPasteEditController.php',
'PhabricatorPasteEditEngine' => 'applications/paste/editor/PhabricatorPasteEditEngine.php',
'PhabricatorPasteEditor' => 'applications/paste/editor/PhabricatorPasteEditor.php',
'PhabricatorPasteFilenameContextFreeGrammar' => 'applications/paste/lipsum/PhabricatorPasteFilenameContextFreeGrammar.php',
'PhabricatorPasteLanguageTransaction' => 'applications/paste/xaction/PhabricatorPasteLanguageTransaction.php',
'PhabricatorPasteListController' => 'applications/paste/controller/PhabricatorPasteListController.php',
'PhabricatorPastePastePHIDType' => 'applications/paste/phid/PhabricatorPastePastePHIDType.php',
'PhabricatorPasteQuery' => 'applications/paste/query/PhabricatorPasteQuery.php',
'PhabricatorPasteRawController' => 'applications/paste/controller/PhabricatorPasteRawController.php',
'PhabricatorPasteRemarkupRule' => 'applications/paste/remarkup/PhabricatorPasteRemarkupRule.php',
'PhabricatorPasteSchemaSpec' => 'applications/paste/storage/PhabricatorPasteSchemaSpec.php',
'PhabricatorPasteSearchEngine' => 'applications/paste/query/PhabricatorPasteSearchEngine.php',
'PhabricatorPasteSnippet' => 'applications/paste/snippet/PhabricatorPasteSnippet.php',
'PhabricatorPasteStatusTransaction' => 'applications/paste/xaction/PhabricatorPasteStatusTransaction.php',
'PhabricatorPasteTestDataGenerator' => 'applications/paste/lipsum/PhabricatorPasteTestDataGenerator.php',
'PhabricatorPasteTitleTransaction' => 'applications/paste/xaction/PhabricatorPasteTitleTransaction.php',
'PhabricatorPasteTransaction' => 'applications/paste/storage/PhabricatorPasteTransaction.php',
'PhabricatorPasteTransactionComment' => 'applications/paste/storage/PhabricatorPasteTransactionComment.php',
'PhabricatorPasteTransactionQuery' => 'applications/paste/query/PhabricatorPasteTransactionQuery.php',
'PhabricatorPasteTransactionType' => 'applications/paste/xaction/PhabricatorPasteTransactionType.php',
'PhabricatorPasteViewController' => 'applications/paste/controller/PhabricatorPasteViewController.php',
'PhabricatorPathSetupCheck' => 'applications/config/check/PhabricatorPathSetupCheck.php',
'PhabricatorPeopleAnyOwnerDatasource' => 'applications/people/typeahead/PhabricatorPeopleAnyOwnerDatasource.php',
'PhabricatorPeopleApplication' => 'applications/people/application/PhabricatorPeopleApplication.php',
'PhabricatorPeopleApproveController' => 'applications/people/controller/PhabricatorPeopleApproveController.php',
'PhabricatorPeopleAvailabilitySearchEngineAttachment' => 'applications/people/engineextension/PhabricatorPeopleAvailabilitySearchEngineAttachment.php',
'PhabricatorPeopleBadgesProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleBadgesProfileMenuItem.php',
'PhabricatorPeopleCommitsProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleCommitsProfileMenuItem.php',
'PhabricatorPeopleController' => 'applications/people/controller/PhabricatorPeopleController.php',
'PhabricatorPeopleCreateController' => 'applications/people/controller/PhabricatorPeopleCreateController.php',
'PhabricatorPeopleCreateGuidanceContext' => 'applications/people/guidance/PhabricatorPeopleCreateGuidanceContext.php',
'PhabricatorPeopleDatasource' => 'applications/people/typeahead/PhabricatorPeopleDatasource.php',
'PhabricatorPeopleDatasourceEngineExtension' => 'applications/people/engineextension/PhabricatorPeopleDatasourceEngineExtension.php',
'PhabricatorPeopleDeleteController' => 'applications/people/controller/PhabricatorPeopleDeleteController.php',
'PhabricatorPeopleDetailsProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleDetailsProfileMenuItem.php',
'PhabricatorPeopleDisableController' => 'applications/people/controller/PhabricatorPeopleDisableController.php',
'PhabricatorPeopleEmpowerController' => 'applications/people/controller/PhabricatorPeopleEmpowerController.php',
'PhabricatorPeopleExternalPHIDType' => 'applications/people/phid/PhabricatorPeopleExternalPHIDType.php',
'PhabricatorPeopleIconSet' => 'applications/people/icon/PhabricatorPeopleIconSet.php',
'PhabricatorPeopleInviteController' => 'applications/people/controller/PhabricatorPeopleInviteController.php',
'PhabricatorPeopleInviteListController' => 'applications/people/controller/PhabricatorPeopleInviteListController.php',
'PhabricatorPeopleInviteSendController' => 'applications/people/controller/PhabricatorPeopleInviteSendController.php',
'PhabricatorPeopleLdapController' => 'applications/people/controller/PhabricatorPeopleLdapController.php',
'PhabricatorPeopleListController' => 'applications/people/controller/PhabricatorPeopleListController.php',
'PhabricatorPeopleLogQuery' => 'applications/people/query/PhabricatorPeopleLogQuery.php',
'PhabricatorPeopleLogSearchEngine' => 'applications/people/query/PhabricatorPeopleLogSearchEngine.php',
'PhabricatorPeopleLogsController' => 'applications/people/controller/PhabricatorPeopleLogsController.php',
+ 'PhabricatorPeopleMailEngine' => 'applications/people/mail/PhabricatorPeopleMailEngine.php',
+ 'PhabricatorPeopleMailEngineException' => 'applications/people/mail/PhabricatorPeopleMailEngineException.php',
'PhabricatorPeopleManageProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleManageProfileMenuItem.php',
'PhabricatorPeopleManagementWorkflow' => 'applications/people/management/PhabricatorPeopleManagementWorkflow.php',
'PhabricatorPeopleNewController' => 'applications/people/controller/PhabricatorPeopleNewController.php',
'PhabricatorPeopleNoOwnerDatasource' => 'applications/people/typeahead/PhabricatorPeopleNoOwnerDatasource.php',
'PhabricatorPeopleOwnerDatasource' => 'applications/people/typeahead/PhabricatorPeopleOwnerDatasource.php',
'PhabricatorPeoplePictureProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeoplePictureProfileMenuItem.php',
'PhabricatorPeopleProfileBadgesController' => 'applications/people/controller/PhabricatorPeopleProfileBadgesController.php',
'PhabricatorPeopleProfileCommitsController' => 'applications/people/controller/PhabricatorPeopleProfileCommitsController.php',
'PhabricatorPeopleProfileController' => 'applications/people/controller/PhabricatorPeopleProfileController.php',
'PhabricatorPeopleProfileEditController' => 'applications/people/controller/PhabricatorPeopleProfileEditController.php',
'PhabricatorPeopleProfileImageWorkflow' => 'applications/people/management/PhabricatorPeopleProfileImageWorkflow.php',
'PhabricatorPeopleProfileManageController' => 'applications/people/controller/PhabricatorPeopleProfileManageController.php',
'PhabricatorPeopleProfileMenuEngine' => 'applications/people/engine/PhabricatorPeopleProfileMenuEngine.php',
'PhabricatorPeopleProfilePictureController' => 'applications/people/controller/PhabricatorPeopleProfilePictureController.php',
'PhabricatorPeopleProfileRevisionsController' => 'applications/people/controller/PhabricatorPeopleProfileRevisionsController.php',
'PhabricatorPeopleProfileTasksController' => 'applications/people/controller/PhabricatorPeopleProfileTasksController.php',
'PhabricatorPeopleProfileViewController' => 'applications/people/controller/PhabricatorPeopleProfileViewController.php',
'PhabricatorPeopleQuery' => 'applications/people/query/PhabricatorPeopleQuery.php',
'PhabricatorPeopleRenameController' => 'applications/people/controller/PhabricatorPeopleRenameController.php',
'PhabricatorPeopleRevisionsProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleRevisionsProfileMenuItem.php',
'PhabricatorPeopleSearchEngine' => 'applications/people/query/PhabricatorPeopleSearchEngine.php',
'PhabricatorPeopleTasksProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleTasksProfileMenuItem.php',
'PhabricatorPeopleTestDataGenerator' => 'applications/people/lipsum/PhabricatorPeopleTestDataGenerator.php',
'PhabricatorPeopleTransactionQuery' => 'applications/people/query/PhabricatorPeopleTransactionQuery.php',
'PhabricatorPeopleUserFunctionDatasource' => 'applications/people/typeahead/PhabricatorPeopleUserFunctionDatasource.php',
'PhabricatorPeopleUserPHIDType' => 'applications/people/phid/PhabricatorPeopleUserPHIDType.php',
'PhabricatorPeopleWelcomeController' => 'applications/people/controller/PhabricatorPeopleWelcomeController.php',
+ 'PhabricatorPeopleWelcomeMailEngine' => 'applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php',
'PhabricatorPhabricatorAuthProvider' => 'applications/auth/provider/PhabricatorPhabricatorAuthProvider.php',
'PhabricatorPhameApplication' => 'applications/phame/application/PhabricatorPhameApplication.php',
'PhabricatorPhameBlogPHIDType' => 'applications/phame/phid/PhabricatorPhameBlogPHIDType.php',
'PhabricatorPhamePostPHIDType' => 'applications/phame/phid/PhabricatorPhamePostPHIDType.php',
'PhabricatorPhluxApplication' => 'applications/phlux/application/PhabricatorPhluxApplication.php',
'PhabricatorPholioApplication' => 'applications/pholio/application/PhabricatorPholioApplication.php',
- 'PhabricatorPholioConfigOptions' => 'applications/pholio/config/PhabricatorPholioConfigOptions.php',
'PhabricatorPholioMockTestDataGenerator' => 'applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php',
+ 'PhabricatorPhoneNumber' => 'applications/metamta/message/PhabricatorPhoneNumber.php',
+ 'PhabricatorPhoneNumberTestCase' => 'applications/metamta/message/__tests__/PhabricatorPhoneNumberTestCase.php',
'PhabricatorPhortuneApplication' => 'applications/phortune/application/PhabricatorPhortuneApplication.php',
'PhabricatorPhortuneContentSource' => 'applications/phortune/contentsource/PhabricatorPhortuneContentSource.php',
'PhabricatorPhortuneManagementInvoiceWorkflow' => 'applications/phortune/management/PhabricatorPhortuneManagementInvoiceWorkflow.php',
'PhabricatorPhortuneManagementWorkflow' => 'applications/phortune/management/PhabricatorPhortuneManagementWorkflow.php',
'PhabricatorPhortuneTestCase' => 'applications/phortune/__tests__/PhabricatorPhortuneTestCase.php',
'PhabricatorPhragmentApplication' => 'applications/phragment/application/PhabricatorPhragmentApplication.php',
'PhabricatorPhrequentApplication' => 'applications/phrequent/application/PhabricatorPhrequentApplication.php',
'PhabricatorPhrictionApplication' => 'applications/phriction/application/PhabricatorPhrictionApplication.php',
- 'PhabricatorPhrictionConfigOptions' => 'applications/phriction/config/PhabricatorPhrictionConfigOptions.php',
'PhabricatorPhurlApplication' => 'applications/phurl/application/PhabricatorPhurlApplication.php',
'PhabricatorPhurlConfigOptions' => 'applications/config/option/PhabricatorPhurlConfigOptions.php',
'PhabricatorPhurlController' => 'applications/phurl/controller/PhabricatorPhurlController.php',
'PhabricatorPhurlDAO' => 'applications/phurl/storage/PhabricatorPhurlDAO.php',
'PhabricatorPhurlLinkRemarkupRule' => 'applications/phurl/remarkup/PhabricatorPhurlLinkRemarkupRule.php',
'PhabricatorPhurlRemarkupRule' => 'applications/phurl/remarkup/PhabricatorPhurlRemarkupRule.php',
'PhabricatorPhurlSchemaSpec' => 'applications/phurl/storage/PhabricatorPhurlSchemaSpec.php',
'PhabricatorPhurlShortURLController' => 'applications/phurl/controller/PhabricatorPhurlShortURLController.php',
'PhabricatorPhurlShortURLDefaultController' => 'applications/phurl/controller/PhabricatorPhurlShortURLDefaultController.php',
'PhabricatorPhurlURL' => 'applications/phurl/storage/PhabricatorPhurlURL.php',
'PhabricatorPhurlURLAccessController' => 'applications/phurl/controller/PhabricatorPhurlURLAccessController.php',
'PhabricatorPhurlURLAliasTransaction' => 'applications/phurl/xaction/PhabricatorPhurlURLAliasTransaction.php',
'PhabricatorPhurlURLCreateCapability' => 'applications/phurl/capability/PhabricatorPhurlURLCreateCapability.php',
'PhabricatorPhurlURLDatasource' => 'applications/phurl/typeahead/PhabricatorPhurlURLDatasource.php',
'PhabricatorPhurlURLDescriptionTransaction' => 'applications/phurl/xaction/PhabricatorPhurlURLDescriptionTransaction.php',
'PhabricatorPhurlURLEditConduitAPIMethod' => 'applications/phurl/conduit/PhabricatorPhurlURLEditConduitAPIMethod.php',
'PhabricatorPhurlURLEditController' => 'applications/phurl/controller/PhabricatorPhurlURLEditController.php',
'PhabricatorPhurlURLEditEngine' => 'applications/phurl/editor/PhabricatorPhurlURLEditEngine.php',
'PhabricatorPhurlURLEditor' => 'applications/phurl/editor/PhabricatorPhurlURLEditor.php',
'PhabricatorPhurlURLListController' => 'applications/phurl/controller/PhabricatorPhurlURLListController.php',
'PhabricatorPhurlURLLongURLTransaction' => 'applications/phurl/xaction/PhabricatorPhurlURLLongURLTransaction.php',
'PhabricatorPhurlURLMailReceiver' => 'applications/phurl/mail/PhabricatorPhurlURLMailReceiver.php',
'PhabricatorPhurlURLNameNgrams' => 'applications/phurl/storage/PhabricatorPhurlURLNameNgrams.php',
'PhabricatorPhurlURLNameTransaction' => 'applications/phurl/xaction/PhabricatorPhurlURLNameTransaction.php',
'PhabricatorPhurlURLPHIDType' => 'applications/phurl/phid/PhabricatorPhurlURLPHIDType.php',
'PhabricatorPhurlURLQuery' => 'applications/phurl/query/PhabricatorPhurlURLQuery.php',
'PhabricatorPhurlURLReplyHandler' => 'applications/phurl/mail/PhabricatorPhurlURLReplyHandler.php',
'PhabricatorPhurlURLSearchConduitAPIMethod' => 'applications/phurl/conduit/PhabricatorPhurlURLSearchConduitAPIMethod.php',
'PhabricatorPhurlURLSearchEngine' => 'applications/phurl/query/PhabricatorPhurlURLSearchEngine.php',
'PhabricatorPhurlURLTransaction' => 'applications/phurl/storage/PhabricatorPhurlURLTransaction.php',
'PhabricatorPhurlURLTransactionComment' => 'applications/phurl/storage/PhabricatorPhurlURLTransactionComment.php',
'PhabricatorPhurlURLTransactionQuery' => 'applications/phurl/query/PhabricatorPhurlURLTransactionQuery.php',
'PhabricatorPhurlURLTransactionType' => 'applications/phurl/xaction/PhabricatorPhurlURLTransactionType.php',
'PhabricatorPhurlURLViewController' => 'applications/phurl/controller/PhabricatorPhurlURLViewController.php',
'PhabricatorPinnedApplicationsSetting' => 'applications/settings/setting/PhabricatorPinnedApplicationsSetting.php',
'PhabricatorPirateEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorPirateEnglishTranslation.php',
'PhabricatorPlatformSite' => 'aphront/site/PhabricatorPlatformSite.php',
'PhabricatorPointsEditField' => 'applications/transactions/editfield/PhabricatorPointsEditField.php',
'PhabricatorPointsFact' => 'applications/fact/fact/PhabricatorPointsFact.php',
'PhabricatorPolicies' => 'applications/policy/constants/PhabricatorPolicies.php',
'PhabricatorPolicy' => 'applications/policy/storage/PhabricatorPolicy.php',
'PhabricatorPolicyApplication' => 'applications/policy/application/PhabricatorPolicyApplication.php',
'PhabricatorPolicyAwareQuery' => 'infrastructure/query/policy/PhabricatorPolicyAwareQuery.php',
'PhabricatorPolicyAwareTestQuery' => 'applications/policy/__tests__/PhabricatorPolicyAwareTestQuery.php',
'PhabricatorPolicyCanEditCapability' => 'applications/policy/capability/PhabricatorPolicyCanEditCapability.php',
'PhabricatorPolicyCanInteractCapability' => 'applications/policy/capability/PhabricatorPolicyCanInteractCapability.php',
'PhabricatorPolicyCanJoinCapability' => 'applications/policy/capability/PhabricatorPolicyCanJoinCapability.php',
'PhabricatorPolicyCanViewCapability' => 'applications/policy/capability/PhabricatorPolicyCanViewCapability.php',
'PhabricatorPolicyCapability' => 'applications/policy/capability/PhabricatorPolicyCapability.php',
'PhabricatorPolicyCapabilityTestCase' => 'applications/policy/capability/__tests__/PhabricatorPolicyCapabilityTestCase.php',
'PhabricatorPolicyCodex' => 'applications/policy/codex/PhabricatorPolicyCodex.php',
'PhabricatorPolicyCodexInterface' => 'applications/policy/codex/PhabricatorPolicyCodexInterface.php',
'PhabricatorPolicyCodexRuleDescription' => 'applications/policy/codex/PhabricatorPolicyCodexRuleDescription.php',
'PhabricatorPolicyConfigOptions' => 'applications/policy/config/PhabricatorPolicyConfigOptions.php',
'PhabricatorPolicyConstants' => 'applications/policy/constants/PhabricatorPolicyConstants.php',
'PhabricatorPolicyController' => 'applications/policy/controller/PhabricatorPolicyController.php',
'PhabricatorPolicyDAO' => 'applications/policy/storage/PhabricatorPolicyDAO.php',
'PhabricatorPolicyDataTestCase' => 'applications/policy/__tests__/PhabricatorPolicyDataTestCase.php',
'PhabricatorPolicyEditController' => 'applications/policy/controller/PhabricatorPolicyEditController.php',
'PhabricatorPolicyEditEngineExtension' => 'applications/policy/editor/PhabricatorPolicyEditEngineExtension.php',
'PhabricatorPolicyEditField' => 'applications/transactions/editfield/PhabricatorPolicyEditField.php',
'PhabricatorPolicyException' => 'applications/policy/exception/PhabricatorPolicyException.php',
'PhabricatorPolicyExplainController' => 'applications/policy/controller/PhabricatorPolicyExplainController.php',
'PhabricatorPolicyFavoritesSetting' => 'applications/settings/setting/PhabricatorPolicyFavoritesSetting.php',
'PhabricatorPolicyFilter' => 'applications/policy/filter/PhabricatorPolicyFilter.php',
'PhabricatorPolicyInterface' => 'applications/policy/interface/PhabricatorPolicyInterface.php',
'PhabricatorPolicyManagementShowWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementShowWorkflow.php',
'PhabricatorPolicyManagementUnlockWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php',
'PhabricatorPolicyManagementWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementWorkflow.php',
'PhabricatorPolicyPHIDTypePolicy' => 'applications/policy/phid/PhabricatorPolicyPHIDTypePolicy.php',
'PhabricatorPolicyQuery' => 'applications/policy/query/PhabricatorPolicyQuery.php',
'PhabricatorPolicyRequestExceptionHandler' => 'aphront/handler/PhabricatorPolicyRequestExceptionHandler.php',
'PhabricatorPolicyRule' => 'applications/policy/rule/PhabricatorPolicyRule.php',
'PhabricatorPolicySearchEngineExtension' => 'applications/policy/engineextension/PhabricatorPolicySearchEngineExtension.php',
'PhabricatorPolicyStrengthConstants' => 'applications/policy/constants/PhabricatorPolicyStrengthConstants.php',
'PhabricatorPolicyTestCase' => 'applications/policy/__tests__/PhabricatorPolicyTestCase.php',
'PhabricatorPolicyTestObject' => 'applications/policy/__tests__/PhabricatorPolicyTestObject.php',
'PhabricatorPolicyType' => 'applications/policy/constants/PhabricatorPolicyType.php',
'PhabricatorPonderApplication' => 'applications/ponder/application/PhabricatorPonderApplication.php',
'PhabricatorProfileMenuEditEngine' => 'applications/search/editor/PhabricatorProfileMenuEditEngine.php',
'PhabricatorProfileMenuEditor' => 'applications/search/editor/PhabricatorProfileMenuEditor.php',
'PhabricatorProfileMenuEngine' => 'applications/search/engine/PhabricatorProfileMenuEngine.php',
'PhabricatorProfileMenuItem' => 'applications/search/menuitem/PhabricatorProfileMenuItem.php',
'PhabricatorProfileMenuItemConfiguration' => 'applications/search/storage/PhabricatorProfileMenuItemConfiguration.php',
'PhabricatorProfileMenuItemConfigurationQuery' => 'applications/search/query/PhabricatorProfileMenuItemConfigurationQuery.php',
'PhabricatorProfileMenuItemConfigurationTransaction' => 'applications/search/storage/PhabricatorProfileMenuItemConfigurationTransaction.php',
'PhabricatorProfileMenuItemConfigurationTransactionQuery' => 'applications/search/query/PhabricatorProfileMenuItemConfigurationTransactionQuery.php',
'PhabricatorProfileMenuItemIconSet' => 'applications/search/menuitem/PhabricatorProfileMenuItemIconSet.php',
'PhabricatorProfileMenuItemPHIDType' => 'applications/search/phidtype/PhabricatorProfileMenuItemPHIDType.php',
'PhabricatorProject' => 'applications/project/storage/PhabricatorProject.php',
'PhabricatorProjectAddHeraldAction' => 'applications/project/herald/PhabricatorProjectAddHeraldAction.php',
'PhabricatorProjectApplication' => 'applications/project/application/PhabricatorProjectApplication.php',
'PhabricatorProjectArchiveController' => 'applications/project/controller/PhabricatorProjectArchiveController.php',
'PhabricatorProjectBoardBackgroundController' => 'applications/project/controller/PhabricatorProjectBoardBackgroundController.php',
'PhabricatorProjectBoardController' => 'applications/project/controller/PhabricatorProjectBoardController.php',
'PhabricatorProjectBoardDisableController' => 'applications/project/controller/PhabricatorProjectBoardDisableController.php',
'PhabricatorProjectBoardImportController' => 'applications/project/controller/PhabricatorProjectBoardImportController.php',
'PhabricatorProjectBoardManageController' => 'applications/project/controller/PhabricatorProjectBoardManageController.php',
'PhabricatorProjectBoardReorderController' => 'applications/project/controller/PhabricatorProjectBoardReorderController.php',
'PhabricatorProjectBoardViewController' => 'applications/project/controller/PhabricatorProjectBoardViewController.php',
'PhabricatorProjectBuiltinsExample' => 'applications/uiexample/examples/PhabricatorProjectBuiltinsExample.php',
'PhabricatorProjectCardView' => 'applications/project/view/PhabricatorProjectCardView.php',
'PhabricatorProjectColorTransaction' => 'applications/project/xaction/PhabricatorProjectColorTransaction.php',
'PhabricatorProjectColorsConfigType' => 'applications/project/config/PhabricatorProjectColorsConfigType.php',
'PhabricatorProjectColumn' => 'applications/project/storage/PhabricatorProjectColumn.php',
'PhabricatorProjectColumnDetailController' => 'applications/project/controller/PhabricatorProjectColumnDetailController.php',
'PhabricatorProjectColumnEditController' => 'applications/project/controller/PhabricatorProjectColumnEditController.php',
'PhabricatorProjectColumnHideController' => 'applications/project/controller/PhabricatorProjectColumnHideController.php',
'PhabricatorProjectColumnPHIDType' => 'applications/project/phid/PhabricatorProjectColumnPHIDType.php',
'PhabricatorProjectColumnPosition' => 'applications/project/storage/PhabricatorProjectColumnPosition.php',
'PhabricatorProjectColumnPositionQuery' => 'applications/project/query/PhabricatorProjectColumnPositionQuery.php',
'PhabricatorProjectColumnQuery' => 'applications/project/query/PhabricatorProjectColumnQuery.php',
'PhabricatorProjectColumnSearchEngine' => 'applications/project/query/PhabricatorProjectColumnSearchEngine.php',
'PhabricatorProjectColumnTransaction' => 'applications/project/storage/PhabricatorProjectColumnTransaction.php',
'PhabricatorProjectColumnTransactionEditor' => 'applications/project/editor/PhabricatorProjectColumnTransactionEditor.php',
'PhabricatorProjectColumnTransactionQuery' => 'applications/project/query/PhabricatorProjectColumnTransactionQuery.php',
'PhabricatorProjectConfigOptions' => 'applications/project/config/PhabricatorProjectConfigOptions.php',
'PhabricatorProjectConfiguredCustomField' => 'applications/project/customfield/PhabricatorProjectConfiguredCustomField.php',
'PhabricatorProjectController' => 'applications/project/controller/PhabricatorProjectController.php',
'PhabricatorProjectCoreTestCase' => 'applications/project/__tests__/PhabricatorProjectCoreTestCase.php',
'PhabricatorProjectCoverController' => 'applications/project/controller/PhabricatorProjectCoverController.php',
'PhabricatorProjectCustomField' => 'applications/project/customfield/PhabricatorProjectCustomField.php',
'PhabricatorProjectCustomFieldNumericIndex' => 'applications/project/storage/PhabricatorProjectCustomFieldNumericIndex.php',
'PhabricatorProjectCustomFieldStorage' => 'applications/project/storage/PhabricatorProjectCustomFieldStorage.php',
'PhabricatorProjectCustomFieldStringIndex' => 'applications/project/storage/PhabricatorProjectCustomFieldStringIndex.php',
'PhabricatorProjectDAO' => 'applications/project/storage/PhabricatorProjectDAO.php',
'PhabricatorProjectDatasource' => 'applications/project/typeahead/PhabricatorProjectDatasource.php',
'PhabricatorProjectDefaultController' => 'applications/project/controller/PhabricatorProjectDefaultController.php',
'PhabricatorProjectDescriptionField' => 'applications/project/customfield/PhabricatorProjectDescriptionField.php',
'PhabricatorProjectDetailsProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectDetailsProfileMenuItem.php',
'PhabricatorProjectEditController' => 'applications/project/controller/PhabricatorProjectEditController.php',
'PhabricatorProjectEditEngine' => 'applications/project/engine/PhabricatorProjectEditEngine.php',
'PhabricatorProjectEditPictureController' => 'applications/project/controller/PhabricatorProjectEditPictureController.php',
'PhabricatorProjectFerretEngine' => 'applications/project/search/PhabricatorProjectFerretEngine.php',
'PhabricatorProjectFilterTransaction' => 'applications/project/xaction/PhabricatorProjectFilterTransaction.php',
'PhabricatorProjectFulltextEngine' => 'applications/project/search/PhabricatorProjectFulltextEngine.php',
'PhabricatorProjectHeraldAction' => 'applications/project/herald/PhabricatorProjectHeraldAction.php',
'PhabricatorProjectHeraldAdapter' => 'applications/project/herald/PhabricatorProjectHeraldAdapter.php',
'PhabricatorProjectHeraldFieldGroup' => 'applications/project/herald/PhabricatorProjectHeraldFieldGroup.php',
'PhabricatorProjectHovercardEngineExtension' => 'applications/project/engineextension/PhabricatorProjectHovercardEngineExtension.php',
'PhabricatorProjectIconSet' => 'applications/project/icon/PhabricatorProjectIconSet.php',
'PhabricatorProjectIconTransaction' => 'applications/project/xaction/PhabricatorProjectIconTransaction.php',
'PhabricatorProjectIconsConfigType' => 'applications/project/config/PhabricatorProjectIconsConfigType.php',
'PhabricatorProjectImageTransaction' => 'applications/project/xaction/PhabricatorProjectImageTransaction.php',
'PhabricatorProjectInterface' => 'applications/project/interface/PhabricatorProjectInterface.php',
'PhabricatorProjectListController' => 'applications/project/controller/PhabricatorProjectListController.php',
'PhabricatorProjectListView' => 'applications/project/view/PhabricatorProjectListView.php',
'PhabricatorProjectLockController' => 'applications/project/controller/PhabricatorProjectLockController.php',
'PhabricatorProjectLockTransaction' => 'applications/project/xaction/PhabricatorProjectLockTransaction.php',
'PhabricatorProjectLogicalAncestorDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalAncestorDatasource.php',
'PhabricatorProjectLogicalDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalDatasource.php',
'PhabricatorProjectLogicalOnlyDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalOnlyDatasource.php',
'PhabricatorProjectLogicalOrNotDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalOrNotDatasource.php',
'PhabricatorProjectLogicalUserDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalUserDatasource.php',
'PhabricatorProjectLogicalViewerDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalViewerDatasource.php',
'PhabricatorProjectManageController' => 'applications/project/controller/PhabricatorProjectManageController.php',
'PhabricatorProjectManageProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectManageProfileMenuItem.php',
'PhabricatorProjectMaterializedMemberEdgeType' => 'applications/project/edge/PhabricatorProjectMaterializedMemberEdgeType.php',
'PhabricatorProjectMemberListView' => 'applications/project/view/PhabricatorProjectMemberListView.php',
'PhabricatorProjectMemberOfProjectEdgeType' => 'applications/project/edge/PhabricatorProjectMemberOfProjectEdgeType.php',
'PhabricatorProjectMembersAddController' => 'applications/project/controller/PhabricatorProjectMembersAddController.php',
'PhabricatorProjectMembersDatasource' => 'applications/project/typeahead/PhabricatorProjectMembersDatasource.php',
'PhabricatorProjectMembersPolicyRule' => 'applications/project/policyrule/PhabricatorProjectMembersPolicyRule.php',
'PhabricatorProjectMembersProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectMembersProfileMenuItem.php',
'PhabricatorProjectMembersRemoveController' => 'applications/project/controller/PhabricatorProjectMembersRemoveController.php',
'PhabricatorProjectMembersViewController' => 'applications/project/controller/PhabricatorProjectMembersViewController.php',
'PhabricatorProjectMenuItemController' => 'applications/project/controller/PhabricatorProjectMenuItemController.php',
'PhabricatorProjectMilestoneTransaction' => 'applications/project/xaction/PhabricatorProjectMilestoneTransaction.php',
'PhabricatorProjectMoveController' => 'applications/project/controller/PhabricatorProjectMoveController.php',
'PhabricatorProjectNameContextFreeGrammar' => 'applications/project/lipsum/PhabricatorProjectNameContextFreeGrammar.php',
'PhabricatorProjectNameTransaction' => 'applications/project/xaction/PhabricatorProjectNameTransaction.php',
'PhabricatorProjectNoProjectsDatasource' => 'applications/project/typeahead/PhabricatorProjectNoProjectsDatasource.php',
'PhabricatorProjectObjectHasProjectEdgeType' => 'applications/project/edge/PhabricatorProjectObjectHasProjectEdgeType.php',
'PhabricatorProjectOrUserDatasource' => 'applications/project/typeahead/PhabricatorProjectOrUserDatasource.php',
'PhabricatorProjectOrUserFunctionDatasource' => 'applications/project/typeahead/PhabricatorProjectOrUserFunctionDatasource.php',
'PhabricatorProjectPHIDResolver' => 'applications/phid/resolver/PhabricatorProjectPHIDResolver.php',
'PhabricatorProjectParentTransaction' => 'applications/project/xaction/PhabricatorProjectParentTransaction.php',
'PhabricatorProjectPictureProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectPictureProfileMenuItem.php',
'PhabricatorProjectPointsProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectPointsProfileMenuItem.php',
'PhabricatorProjectProfileController' => 'applications/project/controller/PhabricatorProjectProfileController.php',
'PhabricatorProjectProfileMenuEngine' => 'applications/project/engine/PhabricatorProjectProfileMenuEngine.php',
'PhabricatorProjectProfileMenuItem' => 'applications/search/menuitem/PhabricatorProjectProfileMenuItem.php',
'PhabricatorProjectProjectHasMemberEdgeType' => 'applications/project/edge/PhabricatorProjectProjectHasMemberEdgeType.php',
'PhabricatorProjectProjectHasObjectEdgeType' => 'applications/project/edge/PhabricatorProjectProjectHasObjectEdgeType.php',
'PhabricatorProjectProjectPHIDType' => 'applications/project/phid/PhabricatorProjectProjectPHIDType.php',
'PhabricatorProjectQuery' => 'applications/project/query/PhabricatorProjectQuery.php',
'PhabricatorProjectRemoveHeraldAction' => 'applications/project/herald/PhabricatorProjectRemoveHeraldAction.php',
'PhabricatorProjectSchemaSpec' => 'applications/project/storage/PhabricatorProjectSchemaSpec.php',
'PhabricatorProjectSearchEngine' => 'applications/project/query/PhabricatorProjectSearchEngine.php',
'PhabricatorProjectSearchField' => 'applications/project/searchfield/PhabricatorProjectSearchField.php',
'PhabricatorProjectSilenceController' => 'applications/project/controller/PhabricatorProjectSilenceController.php',
'PhabricatorProjectSilencedEdgeType' => 'applications/project/edge/PhabricatorProjectSilencedEdgeType.php',
'PhabricatorProjectSlug' => 'applications/project/storage/PhabricatorProjectSlug.php',
'PhabricatorProjectSlugsTransaction' => 'applications/project/xaction/PhabricatorProjectSlugsTransaction.php',
'PhabricatorProjectSortTransaction' => 'applications/project/xaction/PhabricatorProjectSortTransaction.php',
'PhabricatorProjectStandardCustomField' => 'applications/project/customfield/PhabricatorProjectStandardCustomField.php',
'PhabricatorProjectStatus' => 'applications/project/constants/PhabricatorProjectStatus.php',
'PhabricatorProjectStatusTransaction' => 'applications/project/xaction/PhabricatorProjectStatusTransaction.php',
'PhabricatorProjectSubprojectWarningController' => 'applications/project/controller/PhabricatorProjectSubprojectWarningController.php',
'PhabricatorProjectSubprojectsController' => 'applications/project/controller/PhabricatorProjectSubprojectsController.php',
'PhabricatorProjectSubprojectsProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectSubprojectsProfileMenuItem.php',
+ 'PhabricatorProjectSubtypeDatasource' => 'applications/project/typeahead/PhabricatorProjectSubtypeDatasource.php',
+ 'PhabricatorProjectSubtypesConfigType' => 'applications/project/config/PhabricatorProjectSubtypesConfigType.php',
'PhabricatorProjectTestDataGenerator' => 'applications/project/lipsum/PhabricatorProjectTestDataGenerator.php',
'PhabricatorProjectTransaction' => 'applications/project/storage/PhabricatorProjectTransaction.php',
'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php',
'PhabricatorProjectTransactionQuery' => 'applications/project/query/PhabricatorProjectTransactionQuery.php',
'PhabricatorProjectTransactionType' => 'applications/project/xaction/PhabricatorProjectTransactionType.php',
'PhabricatorProjectTypeTransaction' => 'applications/project/xaction/PhabricatorProjectTypeTransaction.php',
'PhabricatorProjectUIEventListener' => 'applications/project/events/PhabricatorProjectUIEventListener.php',
'PhabricatorProjectUpdateController' => 'applications/project/controller/PhabricatorProjectUpdateController.php',
'PhabricatorProjectUserFunctionDatasource' => 'applications/project/typeahead/PhabricatorProjectUserFunctionDatasource.php',
'PhabricatorProjectUserListView' => 'applications/project/view/PhabricatorProjectUserListView.php',
'PhabricatorProjectViewController' => 'applications/project/controller/PhabricatorProjectViewController.php',
'PhabricatorProjectWatchController' => 'applications/project/controller/PhabricatorProjectWatchController.php',
'PhabricatorProjectWatcherListView' => 'applications/project/view/PhabricatorProjectWatcherListView.php',
'PhabricatorProjectWorkboardBackgroundColor' => 'applications/project/constants/PhabricatorProjectWorkboardBackgroundColor.php',
'PhabricatorProjectWorkboardBackgroundTransaction' => 'applications/project/xaction/PhabricatorProjectWorkboardBackgroundTransaction.php',
'PhabricatorProjectWorkboardProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectWorkboardProfileMenuItem.php',
'PhabricatorProjectWorkboardTransaction' => 'applications/project/xaction/PhabricatorProjectWorkboardTransaction.php',
'PhabricatorProjectsAllPolicyRule' => 'applications/project/policyrule/PhabricatorProjectsAllPolicyRule.php',
'PhabricatorProjectsAncestorsSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorProjectsAncestorsSearchEngineAttachment.php',
'PhabricatorProjectsBasePolicyRule' => 'applications/project/policyrule/PhabricatorProjectsBasePolicyRule.php',
'PhabricatorProjectsCurtainExtension' => 'applications/project/engineextension/PhabricatorProjectsCurtainExtension.php',
'PhabricatorProjectsEditEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsEditEngineExtension.php',
'PhabricatorProjectsEditField' => 'applications/transactions/editfield/PhabricatorProjectsEditField.php',
'PhabricatorProjectsExportEngineExtension' => 'infrastructure/export/engine/PhabricatorProjectsExportEngineExtension.php',
'PhabricatorProjectsFulltextEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsFulltextEngineExtension.php',
'PhabricatorProjectsMailEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsMailEngineExtension.php',
'PhabricatorProjectsMembersSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorProjectsMembersSearchEngineAttachment.php',
'PhabricatorProjectsMembershipIndexEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsMembershipIndexEngineExtension.php',
'PhabricatorProjectsPolicyRule' => 'applications/project/policyrule/PhabricatorProjectsPolicyRule.php',
'PhabricatorProjectsSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorProjectsSearchEngineAttachment.php',
'PhabricatorProjectsSearchEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsSearchEngineExtension.php',
'PhabricatorProjectsWatchersSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorProjectsWatchersSearchEngineAttachment.php',
'PhabricatorPronounSetting' => 'applications/settings/setting/PhabricatorPronounSetting.php',
'PhabricatorPygmentSetupCheck' => 'applications/config/check/PhabricatorPygmentSetupCheck.php',
'PhabricatorQuery' => 'infrastructure/query/PhabricatorQuery.php',
'PhabricatorQueryConstraint' => 'infrastructure/query/constraint/PhabricatorQueryConstraint.php',
'PhabricatorQueryIterator' => 'infrastructure/storage/lisk/PhabricatorQueryIterator.php',
'PhabricatorQueryOrderItem' => 'infrastructure/query/order/PhabricatorQueryOrderItem.php',
'PhabricatorQueryOrderTestCase' => 'infrastructure/query/order/__tests__/PhabricatorQueryOrderTestCase.php',
'PhabricatorQueryOrderVector' => 'infrastructure/query/order/PhabricatorQueryOrderVector.php',
'PhabricatorQuickSearchEngineExtension' => 'applications/search/engineextension/PhabricatorQuickSearchEngineExtension.php',
'PhabricatorRateLimitRequestExceptionHandler' => 'aphront/handler/PhabricatorRateLimitRequestExceptionHandler.php',
'PhabricatorRecaptchaConfigOptions' => 'applications/config/option/PhabricatorRecaptchaConfigOptions.php',
'PhabricatorRedirectController' => 'applications/base/controller/PhabricatorRedirectController.php',
'PhabricatorRefreshCSRFController' => 'applications/auth/controller/PhabricatorRefreshCSRFController.php',
'PhabricatorRegexListConfigType' => 'applications/config/type/PhabricatorRegexListConfigType.php',
'PhabricatorRegistrationProfile' => 'applications/people/storage/PhabricatorRegistrationProfile.php',
'PhabricatorReleephApplication' => 'applications/releeph/application/PhabricatorReleephApplication.php',
'PhabricatorReleephApplicationConfigOptions' => 'applications/releeph/config/PhabricatorReleephApplicationConfigOptions.php',
'PhabricatorRemarkupCachePurger' => 'applications/cache/purger/PhabricatorRemarkupCachePurger.php',
'PhabricatorRemarkupControl' => 'view/form/control/PhabricatorRemarkupControl.php',
'PhabricatorRemarkupCowsayBlockInterpreter' => 'infrastructure/markup/interpreter/PhabricatorRemarkupCowsayBlockInterpreter.php',
'PhabricatorRemarkupCustomBlockRule' => 'infrastructure/markup/rule/PhabricatorRemarkupCustomBlockRule.php',
'PhabricatorRemarkupCustomInlineRule' => 'infrastructure/markup/rule/PhabricatorRemarkupCustomInlineRule.php',
'PhabricatorRemarkupDocumentEngine' => 'applications/files/document/PhabricatorRemarkupDocumentEngine.php',
'PhabricatorRemarkupEditField' => 'applications/transactions/editfield/PhabricatorRemarkupEditField.php',
'PhabricatorRemarkupFigletBlockInterpreter' => 'infrastructure/markup/interpreter/PhabricatorRemarkupFigletBlockInterpreter.php',
'PhabricatorRemarkupUIExample' => 'applications/uiexample/examples/PhabricatorRemarkupUIExample.php',
'PhabricatorRepositoriesSetupCheck' => 'applications/config/check/PhabricatorRepositoriesSetupCheck.php',
'PhabricatorRepository' => 'applications/repository/storage/PhabricatorRepository.php',
'PhabricatorRepositoryActivateTransaction' => 'applications/repository/xaction/PhabricatorRepositoryActivateTransaction.php',
'PhabricatorRepositoryAuditRequest' => 'applications/repository/storage/PhabricatorRepositoryAuditRequest.php',
'PhabricatorRepositoryAutocloseOnlyTransaction' => 'applications/repository/xaction/PhabricatorRepositoryAutocloseOnlyTransaction.php',
'PhabricatorRepositoryAutocloseTransaction' => 'applications/repository/xaction/PhabricatorRepositoryAutocloseTransaction.php',
'PhabricatorRepositoryBlueprintsTransaction' => 'applications/repository/xaction/PhabricatorRepositoryBlueprintsTransaction.php',
'PhabricatorRepositoryBranch' => 'applications/repository/storage/PhabricatorRepositoryBranch.php',
'PhabricatorRepositoryCallsignTransaction' => 'applications/repository/xaction/PhabricatorRepositoryCallsignTransaction.php',
'PhabricatorRepositoryCommit' => 'applications/repository/storage/PhabricatorRepositoryCommit.php',
'PhabricatorRepositoryCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php',
'PhabricatorRepositoryCommitData' => 'applications/repository/storage/PhabricatorRepositoryCommitData.php',
'PhabricatorRepositoryCommitHeraldWorker' => 'applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php',
'PhabricatorRepositoryCommitHint' => 'applications/repository/storage/PhabricatorRepositoryCommitHint.php',
'PhabricatorRepositoryCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php',
'PhabricatorRepositoryCommitOwnersWorker' => 'applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php',
'PhabricatorRepositoryCommitPHIDType' => 'applications/repository/phid/PhabricatorRepositoryCommitPHIDType.php',
'PhabricatorRepositoryCommitParserWorker' => 'applications/repository/worker/PhabricatorRepositoryCommitParserWorker.php',
'PhabricatorRepositoryCommitRef' => 'applications/repository/engine/PhabricatorRepositoryCommitRef.php',
'PhabricatorRepositoryCommitTestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryCommitTestCase.php',
'PhabricatorRepositoryConfigOptions' => 'applications/repository/config/PhabricatorRepositoryConfigOptions.php',
'PhabricatorRepositoryCopyTimeLimitTransaction' => 'applications/repository/xaction/PhabricatorRepositoryCopyTimeLimitTransaction.php',
'PhabricatorRepositoryDAO' => 'applications/repository/storage/PhabricatorRepositoryDAO.php',
'PhabricatorRepositoryDangerousTransaction' => 'applications/repository/xaction/PhabricatorRepositoryDangerousTransaction.php',
'PhabricatorRepositoryDefaultBranchTransaction' => 'applications/repository/xaction/PhabricatorRepositoryDefaultBranchTransaction.php',
'PhabricatorRepositoryDescriptionTransaction' => 'applications/repository/xaction/PhabricatorRepositoryDescriptionTransaction.php',
'PhabricatorRepositoryDestructibleCodex' => 'applications/repository/codex/PhabricatorRepositoryDestructibleCodex.php',
'PhabricatorRepositoryDiscoveryEngine' => 'applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php',
'PhabricatorRepositoryEditor' => 'applications/repository/editor/PhabricatorRepositoryEditor.php',
'PhabricatorRepositoryEncodingTransaction' => 'applications/repository/xaction/PhabricatorRepositoryEncodingTransaction.php',
'PhabricatorRepositoryEngine' => 'applications/repository/engine/PhabricatorRepositoryEngine.php',
'PhabricatorRepositoryEnormousTransaction' => 'applications/repository/xaction/PhabricatorRepositoryEnormousTransaction.php',
'PhabricatorRepositoryFerretEngine' => 'applications/repository/search/PhabricatorRepositoryFerretEngine.php',
'PhabricatorRepositoryFilesizeLimitTransaction' => 'applications/repository/xaction/PhabricatorRepositoryFilesizeLimitTransaction.php',
'PhabricatorRepositoryFulltextEngine' => 'applications/repository/search/PhabricatorRepositoryFulltextEngine.php',
'PhabricatorRepositoryGitCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryGitCommitChangeParserWorker.php',
'PhabricatorRepositoryGitCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositoryGitCommitMessageParserWorker.php',
'PhabricatorRepositoryGitLFSRef' => 'applications/repository/storage/PhabricatorRepositoryGitLFSRef.php',
'PhabricatorRepositoryGitLFSRefQuery' => 'applications/repository/query/PhabricatorRepositoryGitLFSRefQuery.php',
'PhabricatorRepositoryGraphCache' => 'applications/repository/graphcache/PhabricatorRepositoryGraphCache.php',
'PhabricatorRepositoryGraphStream' => 'applications/repository/daemon/PhabricatorRepositoryGraphStream.php',
'PhabricatorRepositoryIdentity' => 'applications/repository/storage/PhabricatorRepositoryIdentity.php',
'PhabricatorRepositoryIdentityAssignTransaction' => 'applications/repository/xaction/PhabricatorRepositoryIdentityAssignTransaction.php',
'PhabricatorRepositoryIdentityChangeWorker' => 'applications/repository/worker/PhabricatorRepositoryIdentityChangeWorker.php',
'PhabricatorRepositoryIdentityEditEngine' => 'applications/repository/engine/PhabricatorRepositoryIdentityEditEngine.php',
'PhabricatorRepositoryIdentityFerretEngine' => 'applications/repository/search/PhabricatorRepositoryIdentityFerretEngine.php',
'PhabricatorRepositoryIdentityPHIDType' => 'applications/repository/phid/PhabricatorRepositoryIdentityPHIDType.php',
'PhabricatorRepositoryIdentityQuery' => 'applications/repository/query/PhabricatorRepositoryIdentityQuery.php',
'PhabricatorRepositoryIdentityTransaction' => 'applications/repository/storage/PhabricatorRepositoryIdentityTransaction.php',
'PhabricatorRepositoryIdentityTransactionQuery' => 'applications/repository/query/PhabricatorRepositoryIdentityTransactionQuery.php',
'PhabricatorRepositoryIdentityTransactionType' => 'applications/repository/xaction/PhabricatorRepositoryIdentityTransactionType.php',
'PhabricatorRepositoryManagementCacheWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementCacheWorkflow.php',
'PhabricatorRepositoryManagementClusterizeWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementClusterizeWorkflow.php',
'PhabricatorRepositoryManagementDiscoverWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementDiscoverWorkflow.php',
'PhabricatorRepositoryManagementHintWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementHintWorkflow.php',
'PhabricatorRepositoryManagementImportingWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementImportingWorkflow.php',
'PhabricatorRepositoryManagementListPathsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementListPathsWorkflow.php',
'PhabricatorRepositoryManagementListWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementListWorkflow.php',
'PhabricatorRepositoryManagementLookupUsersWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementLookupUsersWorkflow.php',
'PhabricatorRepositoryManagementMarkImportedWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMarkImportedWorkflow.php',
'PhabricatorRepositoryManagementMarkReachableWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMarkReachableWorkflow.php',
'PhabricatorRepositoryManagementMirrorWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMirrorWorkflow.php',
'PhabricatorRepositoryManagementMovePathsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMovePathsWorkflow.php',
'PhabricatorRepositoryManagementParentsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementParentsWorkflow.php',
'PhabricatorRepositoryManagementPullWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementPullWorkflow.php',
'PhabricatorRepositoryManagementRebuildIdentitiesWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php',
'PhabricatorRepositoryManagementRefsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementRefsWorkflow.php',
'PhabricatorRepositoryManagementReparseWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementReparseWorkflow.php',
'PhabricatorRepositoryManagementThawWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementThawWorkflow.php',
'PhabricatorRepositoryManagementUnpublishWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementUnpublishWorkflow.php',
'PhabricatorRepositoryManagementUpdateWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementUpdateWorkflow.php',
'PhabricatorRepositoryManagementWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementWorkflow.php',
'PhabricatorRepositoryMercurialCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryMercurialCommitChangeParserWorker.php',
'PhabricatorRepositoryMercurialCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositoryMercurialCommitMessageParserWorker.php',
'PhabricatorRepositoryMirror' => 'applications/repository/storage/PhabricatorRepositoryMirror.php',
'PhabricatorRepositoryMirrorEngine' => 'applications/repository/engine/PhabricatorRepositoryMirrorEngine.php',
'PhabricatorRepositoryNameTransaction' => 'applications/repository/xaction/PhabricatorRepositoryNameTransaction.php',
'PhabricatorRepositoryNotifyTransaction' => 'applications/repository/xaction/PhabricatorRepositoryNotifyTransaction.php',
'PhabricatorRepositoryOldRef' => 'applications/repository/storage/PhabricatorRepositoryOldRef.php',
'PhabricatorRepositoryParsedChange' => 'applications/repository/data/PhabricatorRepositoryParsedChange.php',
'PhabricatorRepositoryPullEngine' => 'applications/repository/engine/PhabricatorRepositoryPullEngine.php',
'PhabricatorRepositoryPullEvent' => 'applications/repository/storage/PhabricatorRepositoryPullEvent.php',
'PhabricatorRepositoryPullEventPHIDType' => 'applications/repository/phid/PhabricatorRepositoryPullEventPHIDType.php',
'PhabricatorRepositoryPullEventQuery' => 'applications/repository/query/PhabricatorRepositoryPullEventQuery.php',
'PhabricatorRepositoryPullLocalDaemon' => 'applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php',
'PhabricatorRepositoryPullLocalDaemonModule' => 'applications/repository/daemon/PhabricatorRepositoryPullLocalDaemonModule.php',
'PhabricatorRepositoryPushEvent' => 'applications/repository/storage/PhabricatorRepositoryPushEvent.php',
'PhabricatorRepositoryPushEventPHIDType' => 'applications/repository/phid/PhabricatorRepositoryPushEventPHIDType.php',
'PhabricatorRepositoryPushEventQuery' => 'applications/repository/query/PhabricatorRepositoryPushEventQuery.php',
'PhabricatorRepositoryPushLog' => 'applications/repository/storage/PhabricatorRepositoryPushLog.php',
'PhabricatorRepositoryPushLogPHIDType' => 'applications/repository/phid/PhabricatorRepositoryPushLogPHIDType.php',
'PhabricatorRepositoryPushLogQuery' => 'applications/repository/query/PhabricatorRepositoryPushLogQuery.php',
'PhabricatorRepositoryPushLogSearchEngine' => 'applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php',
'PhabricatorRepositoryPushMailWorker' => 'applications/repository/worker/PhabricatorRepositoryPushMailWorker.php',
'PhabricatorRepositoryPushPolicyTransaction' => 'applications/repository/xaction/PhabricatorRepositoryPushPolicyTransaction.php',
'PhabricatorRepositoryPushReplyHandler' => 'applications/repository/mail/PhabricatorRepositoryPushReplyHandler.php',
'PhabricatorRepositoryQuery' => 'applications/repository/query/PhabricatorRepositoryQuery.php',
'PhabricatorRepositoryRefCursor' => 'applications/repository/storage/PhabricatorRepositoryRefCursor.php',
'PhabricatorRepositoryRefCursorPHIDType' => 'applications/repository/phid/PhabricatorRepositoryRefCursorPHIDType.php',
'PhabricatorRepositoryRefCursorQuery' => 'applications/repository/query/PhabricatorRepositoryRefCursorQuery.php',
'PhabricatorRepositoryRefEngine' => 'applications/repository/engine/PhabricatorRepositoryRefEngine.php',
'PhabricatorRepositoryRefPosition' => 'applications/repository/storage/PhabricatorRepositoryRefPosition.php',
'PhabricatorRepositoryRepositoryPHIDType' => 'applications/repository/phid/PhabricatorRepositoryRepositoryPHIDType.php',
'PhabricatorRepositorySVNSubpathTransaction' => 'applications/repository/xaction/PhabricatorRepositorySVNSubpathTransaction.php',
'PhabricatorRepositorySchemaSpec' => 'applications/repository/storage/PhabricatorRepositorySchemaSpec.php',
'PhabricatorRepositorySearchEngine' => 'applications/repository/query/PhabricatorRepositorySearchEngine.php',
'PhabricatorRepositoryServiceTransaction' => 'applications/repository/xaction/PhabricatorRepositoryServiceTransaction.php',
'PhabricatorRepositorySlugTransaction' => 'applications/repository/xaction/PhabricatorRepositorySlugTransaction.php',
'PhabricatorRepositoryStagingURITransaction' => 'applications/repository/xaction/PhabricatorRepositoryStagingURITransaction.php',
'PhabricatorRepositoryStatusMessage' => 'applications/repository/storage/PhabricatorRepositoryStatusMessage.php',
'PhabricatorRepositorySvnCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositorySvnCommitChangeParserWorker.php',
'PhabricatorRepositorySvnCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositorySvnCommitMessageParserWorker.php',
'PhabricatorRepositorySymbol' => 'applications/repository/storage/PhabricatorRepositorySymbol.php',
'PhabricatorRepositorySymbolLanguagesTransaction' => 'applications/repository/xaction/PhabricatorRepositorySymbolLanguagesTransaction.php',
'PhabricatorRepositorySymbolSourcesTransaction' => 'applications/repository/xaction/PhabricatorRepositorySymbolSourcesTransaction.php',
'PhabricatorRepositorySyncEvent' => 'applications/repository/storage/PhabricatorRepositorySyncEvent.php',
'PhabricatorRepositorySyncEventPHIDType' => 'applications/repository/phid/PhabricatorRepositorySyncEventPHIDType.php',
'PhabricatorRepositorySyncEventQuery' => 'applications/repository/query/PhabricatorRepositorySyncEventQuery.php',
'PhabricatorRepositoryTestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php',
'PhabricatorRepositoryTouchLimitTransaction' => 'applications/repository/xaction/PhabricatorRepositoryTouchLimitTransaction.php',
'PhabricatorRepositoryTrackOnlyTransaction' => 'applications/repository/xaction/PhabricatorRepositoryTrackOnlyTransaction.php',
'PhabricatorRepositoryTransaction' => 'applications/repository/storage/PhabricatorRepositoryTransaction.php',
'PhabricatorRepositoryTransactionQuery' => 'applications/repository/query/PhabricatorRepositoryTransactionQuery.php',
'PhabricatorRepositoryTransactionType' => 'applications/repository/xaction/PhabricatorRepositoryTransactionType.php',
'PhabricatorRepositoryType' => 'applications/repository/constants/PhabricatorRepositoryType.php',
'PhabricatorRepositoryURI' => 'applications/repository/storage/PhabricatorRepositoryURI.php',
'PhabricatorRepositoryURIIndex' => 'applications/repository/storage/PhabricatorRepositoryURIIndex.php',
'PhabricatorRepositoryURINormalizer' => 'applications/repository/data/PhabricatorRepositoryURINormalizer.php',
'PhabricatorRepositoryURINormalizerTestCase' => 'applications/repository/data/__tests__/PhabricatorRepositoryURINormalizerTestCase.php',
'PhabricatorRepositoryURIPHIDType' => 'applications/repository/phid/PhabricatorRepositoryURIPHIDType.php',
'PhabricatorRepositoryURIQuery' => 'applications/repository/query/PhabricatorRepositoryURIQuery.php',
'PhabricatorRepositoryURITestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryURITestCase.php',
'PhabricatorRepositoryURITransaction' => 'applications/repository/storage/PhabricatorRepositoryURITransaction.php',
'PhabricatorRepositoryURITransactionQuery' => 'applications/repository/query/PhabricatorRepositoryURITransactionQuery.php',
'PhabricatorRepositoryVCSTransaction' => 'applications/repository/xaction/PhabricatorRepositoryVCSTransaction.php',
'PhabricatorRepositoryWorkingCopyVersion' => 'applications/repository/storage/PhabricatorRepositoryWorkingCopyVersion.php',
'PhabricatorRequestExceptionHandler' => 'aphront/handler/PhabricatorRequestExceptionHandler.php',
'PhabricatorResourceSite' => 'aphront/site/PhabricatorResourceSite.php',
'PhabricatorRobotsController' => 'applications/system/controller/PhabricatorRobotsController.php',
'PhabricatorS3FileStorageEngine' => 'applications/files/engine/PhabricatorS3FileStorageEngine.php',
- 'PhabricatorSMS' => 'infrastructure/sms/storage/PhabricatorSMS.php',
- 'PhabricatorSMSConfigOptions' => 'applications/config/option/PhabricatorSMSConfigOptions.php',
- 'PhabricatorSMSDAO' => 'infrastructure/sms/storage/PhabricatorSMSDAO.php',
- 'PhabricatorSMSDemultiplexWorker' => 'infrastructure/sms/worker/PhabricatorSMSDemultiplexWorker.php',
- 'PhabricatorSMSImplementationAdapter' => 'infrastructure/sms/adapter/PhabricatorSMSImplementationAdapter.php',
- 'PhabricatorSMSImplementationTestBlackholeAdapter' => 'infrastructure/sms/adapter/PhabricatorSMSImplementationTestBlackholeAdapter.php',
- 'PhabricatorSMSImplementationTwilioAdapter' => 'infrastructure/sms/adapter/PhabricatorSMSImplementationTwilioAdapter.php',
- 'PhabricatorSMSManagementListOutboundWorkflow' => 'infrastructure/sms/management/PhabricatorSMSManagementListOutboundWorkflow.php',
- 'PhabricatorSMSManagementSendTestWorkflow' => 'infrastructure/sms/management/PhabricatorSMSManagementSendTestWorkflow.php',
- 'PhabricatorSMSManagementShowOutboundWorkflow' => 'infrastructure/sms/management/PhabricatorSMSManagementShowOutboundWorkflow.php',
- 'PhabricatorSMSManagementWorkflow' => 'infrastructure/sms/management/PhabricatorSMSManagementWorkflow.php',
- 'PhabricatorSMSSendWorker' => 'infrastructure/sms/worker/PhabricatorSMSSendWorker.php',
- 'PhabricatorSMSWorker' => 'infrastructure/sms/worker/PhabricatorSMSWorker.php',
+ 'PhabricatorSMSAuthFactor' => 'applications/auth/factor/PhabricatorSMSAuthFactor.php',
'PhabricatorSQLPatchList' => 'infrastructure/storage/patch/PhabricatorSQLPatchList.php',
'PhabricatorSSHKeyGenerator' => 'infrastructure/util/PhabricatorSSHKeyGenerator.php',
'PhabricatorSSHKeysSettingsPanel' => 'applications/settings/panel/PhabricatorSSHKeysSettingsPanel.php',
'PhabricatorSSHLog' => 'infrastructure/log/PhabricatorSSHLog.php',
'PhabricatorSSHPassthruCommand' => 'infrastructure/ssh/PhabricatorSSHPassthruCommand.php',
'PhabricatorSSHPublicKeyInterface' => 'applications/auth/sshkey/PhabricatorSSHPublicKeyInterface.php',
'PhabricatorSSHWorkflow' => 'infrastructure/ssh/PhabricatorSSHWorkflow.php',
'PhabricatorSavedQuery' => 'applications/search/storage/PhabricatorSavedQuery.php',
'PhabricatorSavedQueryQuery' => 'applications/search/query/PhabricatorSavedQueryQuery.php',
'PhabricatorScheduleTaskTriggerAction' => 'infrastructure/daemon/workers/action/PhabricatorScheduleTaskTriggerAction.php',
'PhabricatorScopedEnv' => 'infrastructure/env/PhabricatorScopedEnv.php',
'PhabricatorSearchAbstractDocument' => 'applications/search/index/PhabricatorSearchAbstractDocument.php',
'PhabricatorSearchApplication' => 'applications/search/application/PhabricatorSearchApplication.php',
'PhabricatorSearchApplicationSearchEngine' => 'applications/search/query/PhabricatorSearchApplicationSearchEngine.php',
'PhabricatorSearchApplicationStorageEnginePanel' => 'applications/search/applicationpanel/PhabricatorSearchApplicationStorageEnginePanel.php',
'PhabricatorSearchBaseController' => 'applications/search/controller/PhabricatorSearchBaseController.php',
'PhabricatorSearchCheckboxesField' => 'applications/search/field/PhabricatorSearchCheckboxesField.php',
'PhabricatorSearchConstraintException' => 'applications/search/exception/PhabricatorSearchConstraintException.php',
'PhabricatorSearchController' => 'applications/search/controller/PhabricatorSearchController.php',
'PhabricatorSearchCustomFieldProxyField' => 'applications/search/field/PhabricatorSearchCustomFieldProxyField.php',
'PhabricatorSearchDAO' => 'applications/search/storage/PhabricatorSearchDAO.php',
'PhabricatorSearchDatasource' => 'applications/search/typeahead/PhabricatorSearchDatasource.php',
'PhabricatorSearchDatasourceField' => 'applications/search/field/PhabricatorSearchDatasourceField.php',
'PhabricatorSearchDateControlField' => 'applications/search/field/PhabricatorSearchDateControlField.php',
'PhabricatorSearchDateField' => 'applications/search/field/PhabricatorSearchDateField.php',
'PhabricatorSearchDefaultController' => 'applications/search/controller/PhabricatorSearchDefaultController.php',
'PhabricatorSearchDeleteController' => 'applications/search/controller/PhabricatorSearchDeleteController.php',
'PhabricatorSearchDocument' => 'applications/search/storage/document/PhabricatorSearchDocument.php',
'PhabricatorSearchDocumentField' => 'applications/search/storage/document/PhabricatorSearchDocumentField.php',
'PhabricatorSearchDocumentFieldType' => 'applications/search/constants/PhabricatorSearchDocumentFieldType.php',
'PhabricatorSearchDocumentQuery' => 'applications/search/query/PhabricatorSearchDocumentQuery.php',
'PhabricatorSearchDocumentRelationship' => 'applications/search/storage/document/PhabricatorSearchDocumentRelationship.php',
'PhabricatorSearchDocumentTypeDatasource' => 'applications/search/typeahead/PhabricatorSearchDocumentTypeDatasource.php',
'PhabricatorSearchEditController' => 'applications/search/controller/PhabricatorSearchEditController.php',
'PhabricatorSearchEngineAPIMethod' => 'applications/search/engine/PhabricatorSearchEngineAPIMethod.php',
'PhabricatorSearchEngineAttachment' => 'applications/search/engineextension/PhabricatorSearchEngineAttachment.php',
'PhabricatorSearchEngineExtension' => 'applications/search/engineextension/PhabricatorSearchEngineExtension.php',
'PhabricatorSearchEngineExtensionModule' => 'applications/search/engineextension/PhabricatorSearchEngineExtensionModule.php',
'PhabricatorSearchFerretNgramGarbageCollector' => 'applications/search/garbagecollector/PhabricatorSearchFerretNgramGarbageCollector.php',
'PhabricatorSearchField' => 'applications/search/field/PhabricatorSearchField.php',
'PhabricatorSearchHost' => 'infrastructure/cluster/search/PhabricatorSearchHost.php',
'PhabricatorSearchHovercardController' => 'applications/search/controller/PhabricatorSearchHovercardController.php',
'PhabricatorSearchIndexVersion' => 'applications/search/storage/PhabricatorSearchIndexVersion.php',
'PhabricatorSearchIndexVersionDestructionEngineExtension' => 'applications/search/engineextension/PhabricatorSearchIndexVersionDestructionEngineExtension.php',
'PhabricatorSearchManagementIndexWorkflow' => 'applications/search/management/PhabricatorSearchManagementIndexWorkflow.php',
'PhabricatorSearchManagementInitWorkflow' => 'applications/search/management/PhabricatorSearchManagementInitWorkflow.php',
'PhabricatorSearchManagementNgramsWorkflow' => 'applications/search/management/PhabricatorSearchManagementNgramsWorkflow.php',
'PhabricatorSearchManagementQueryWorkflow' => 'applications/search/management/PhabricatorSearchManagementQueryWorkflow.php',
'PhabricatorSearchManagementWorkflow' => 'applications/search/management/PhabricatorSearchManagementWorkflow.php',
'PhabricatorSearchNgrams' => 'applications/search/ngrams/PhabricatorSearchNgrams.php',
'PhabricatorSearchNgramsDestructionEngineExtension' => 'applications/search/engineextension/PhabricatorSearchNgramsDestructionEngineExtension.php',
'PhabricatorSearchOrderController' => 'applications/search/controller/PhabricatorSearchOrderController.php',
'PhabricatorSearchOrderField' => 'applications/search/field/PhabricatorSearchOrderField.php',
'PhabricatorSearchRelationship' => 'applications/search/constants/PhabricatorSearchRelationship.php',
'PhabricatorSearchRelationshipController' => 'applications/search/controller/PhabricatorSearchRelationshipController.php',
'PhabricatorSearchRelationshipSourceController' => 'applications/search/controller/PhabricatorSearchRelationshipSourceController.php',
'PhabricatorSearchResultBucket' => 'applications/search/buckets/PhabricatorSearchResultBucket.php',
'PhabricatorSearchResultBucketGroup' => 'applications/search/buckets/PhabricatorSearchResultBucketGroup.php',
'PhabricatorSearchResultView' => 'applications/search/view/PhabricatorSearchResultView.php',
'PhabricatorSearchSchemaSpec' => 'applications/search/storage/PhabricatorSearchSchemaSpec.php',
'PhabricatorSearchScopeSetting' => 'applications/settings/setting/PhabricatorSearchScopeSetting.php',
'PhabricatorSearchSelectField' => 'applications/search/field/PhabricatorSearchSelectField.php',
'PhabricatorSearchService' => 'infrastructure/cluster/search/PhabricatorSearchService.php',
'PhabricatorSearchStringListField' => 'applications/search/field/PhabricatorSearchStringListField.php',
'PhabricatorSearchSubscribersField' => 'applications/search/field/PhabricatorSearchSubscribersField.php',
'PhabricatorSearchTextField' => 'applications/search/field/PhabricatorSearchTextField.php',
'PhabricatorSearchThreeStateField' => 'applications/search/field/PhabricatorSearchThreeStateField.php',
'PhabricatorSearchTokenizerField' => 'applications/search/field/PhabricatorSearchTokenizerField.php',
'PhabricatorSearchWorker' => 'applications/search/worker/PhabricatorSearchWorker.php',
'PhabricatorSecurityConfigOptions' => 'applications/config/option/PhabricatorSecurityConfigOptions.php',
'PhabricatorSecuritySetupCheck' => 'applications/config/check/PhabricatorSecuritySetupCheck.php',
'PhabricatorSelectEditField' => 'applications/transactions/editfield/PhabricatorSelectEditField.php',
'PhabricatorSelectSetting' => 'applications/settings/setting/PhabricatorSelectSetting.php',
- 'PhabricatorSendGridConfigOptions' => 'applications/config/option/PhabricatorSendGridConfigOptions.php',
'PhabricatorSessionsSettingsPanel' => 'applications/settings/panel/PhabricatorSessionsSettingsPanel.php',
'PhabricatorSetConfigType' => 'applications/config/type/PhabricatorSetConfigType.php',
'PhabricatorSetting' => 'applications/settings/setting/PhabricatorSetting.php',
'PhabricatorSettingsAccountPanelGroup' => 'applications/settings/panelgroup/PhabricatorSettingsAccountPanelGroup.php',
'PhabricatorSettingsAddEmailAction' => 'applications/settings/action/PhabricatorSettingsAddEmailAction.php',
'PhabricatorSettingsAdjustController' => 'applications/settings/controller/PhabricatorSettingsAdjustController.php',
'PhabricatorSettingsApplication' => 'applications/settings/application/PhabricatorSettingsApplication.php',
'PhabricatorSettingsApplicationsPanelGroup' => 'applications/settings/panelgroup/PhabricatorSettingsApplicationsPanelGroup.php',
'PhabricatorSettingsAuthenticationPanelGroup' => 'applications/settings/panelgroup/PhabricatorSettingsAuthenticationPanelGroup.php',
'PhabricatorSettingsDeveloperPanelGroup' => 'applications/settings/panelgroup/PhabricatorSettingsDeveloperPanelGroup.php',
'PhabricatorSettingsEditEngine' => 'applications/settings/editor/PhabricatorSettingsEditEngine.php',
'PhabricatorSettingsEmailPanelGroup' => 'applications/settings/panelgroup/PhabricatorSettingsEmailPanelGroup.php',
'PhabricatorSettingsIssueController' => 'applications/settings/controller/PhabricatorSettingsIssueController.php',
'PhabricatorSettingsListController' => 'applications/settings/controller/PhabricatorSettingsListController.php',
'PhabricatorSettingsLogsPanelGroup' => 'applications/settings/panelgroup/PhabricatorSettingsLogsPanelGroup.php',
'PhabricatorSettingsMainController' => 'applications/settings/controller/PhabricatorSettingsMainController.php',
'PhabricatorSettingsPanel' => 'applications/settings/panel/PhabricatorSettingsPanel.php',
'PhabricatorSettingsPanelGroup' => 'applications/settings/panelgroup/PhabricatorSettingsPanelGroup.php',
'PhabricatorSettingsTimezoneController' => 'applications/settings/controller/PhabricatorSettingsTimezoneController.php',
'PhabricatorSetupCheck' => 'applications/config/check/PhabricatorSetupCheck.php',
'PhabricatorSetupCheckTestCase' => 'applications/config/check/__tests__/PhabricatorSetupCheckTestCase.php',
'PhabricatorSetupEngine' => 'applications/config/engine/PhabricatorSetupEngine.php',
'PhabricatorSetupIssue' => 'applications/config/issue/PhabricatorSetupIssue.php',
'PhabricatorSetupIssueUIExample' => 'applications/uiexample/examples/PhabricatorSetupIssueUIExample.php',
'PhabricatorSetupIssueView' => 'applications/config/view/PhabricatorSetupIssueView.php',
'PhabricatorShortSite' => 'aphront/site/PhabricatorShortSite.php',
'PhabricatorShowFiletreeSetting' => 'applications/settings/setting/PhabricatorShowFiletreeSetting.php',
'PhabricatorSimpleEditType' => 'applications/transactions/edittype/PhabricatorSimpleEditType.php',
'PhabricatorSite' => 'aphront/site/PhabricatorSite.php',
'PhabricatorSlackAuthProvider' => 'applications/auth/provider/PhabricatorSlackAuthProvider.php',
'PhabricatorSlowvoteApplication' => 'applications/slowvote/application/PhabricatorSlowvoteApplication.php',
'PhabricatorSlowvoteChoice' => 'applications/slowvote/storage/PhabricatorSlowvoteChoice.php',
'PhabricatorSlowvoteCloseController' => 'applications/slowvote/controller/PhabricatorSlowvoteCloseController.php',
'PhabricatorSlowvoteCloseTransaction' => 'applications/slowvote/xaction/PhabricatorSlowvoteCloseTransaction.php',
'PhabricatorSlowvoteCommentController' => 'applications/slowvote/controller/PhabricatorSlowvoteCommentController.php',
'PhabricatorSlowvoteController' => 'applications/slowvote/controller/PhabricatorSlowvoteController.php',
'PhabricatorSlowvoteDAO' => 'applications/slowvote/storage/PhabricatorSlowvoteDAO.php',
'PhabricatorSlowvoteDefaultViewCapability' => 'applications/slowvote/capability/PhabricatorSlowvoteDefaultViewCapability.php',
'PhabricatorSlowvoteDescriptionTransaction' => 'applications/slowvote/xaction/PhabricatorSlowvoteDescriptionTransaction.php',
'PhabricatorSlowvoteEditController' => 'applications/slowvote/controller/PhabricatorSlowvoteEditController.php',
'PhabricatorSlowvoteEditor' => 'applications/slowvote/editor/PhabricatorSlowvoteEditor.php',
'PhabricatorSlowvoteListController' => 'applications/slowvote/controller/PhabricatorSlowvoteListController.php',
'PhabricatorSlowvoteMailReceiver' => 'applications/slowvote/mail/PhabricatorSlowvoteMailReceiver.php',
'PhabricatorSlowvoteOption' => 'applications/slowvote/storage/PhabricatorSlowvoteOption.php',
'PhabricatorSlowvotePoll' => 'applications/slowvote/storage/PhabricatorSlowvotePoll.php',
'PhabricatorSlowvotePollController' => 'applications/slowvote/controller/PhabricatorSlowvotePollController.php',
'PhabricatorSlowvotePollPHIDType' => 'applications/slowvote/phid/PhabricatorSlowvotePollPHIDType.php',
'PhabricatorSlowvoteQuery' => 'applications/slowvote/query/PhabricatorSlowvoteQuery.php',
'PhabricatorSlowvoteQuestionTransaction' => 'applications/slowvote/xaction/PhabricatorSlowvoteQuestionTransaction.php',
'PhabricatorSlowvoteReplyHandler' => 'applications/slowvote/mail/PhabricatorSlowvoteReplyHandler.php',
'PhabricatorSlowvoteResponsesTransaction' => 'applications/slowvote/xaction/PhabricatorSlowvoteResponsesTransaction.php',
'PhabricatorSlowvoteSchemaSpec' => 'applications/slowvote/storage/PhabricatorSlowvoteSchemaSpec.php',
'PhabricatorSlowvoteSearchEngine' => 'applications/slowvote/query/PhabricatorSlowvoteSearchEngine.php',
'PhabricatorSlowvoteShuffleTransaction' => 'applications/slowvote/xaction/PhabricatorSlowvoteShuffleTransaction.php',
'PhabricatorSlowvoteTransaction' => 'applications/slowvote/storage/PhabricatorSlowvoteTransaction.php',
'PhabricatorSlowvoteTransactionComment' => 'applications/slowvote/storage/PhabricatorSlowvoteTransactionComment.php',
'PhabricatorSlowvoteTransactionQuery' => 'applications/slowvote/query/PhabricatorSlowvoteTransactionQuery.php',
'PhabricatorSlowvoteTransactionType' => 'applications/slowvote/xaction/PhabricatorSlowvoteTransactionType.php',
'PhabricatorSlowvoteVoteController' => 'applications/slowvote/controller/PhabricatorSlowvoteVoteController.php',
'PhabricatorSlug' => 'infrastructure/util/PhabricatorSlug.php',
'PhabricatorSlugTestCase' => 'infrastructure/util/__tests__/PhabricatorSlugTestCase.php',
'PhabricatorSourceCodeView' => 'view/layout/PhabricatorSourceCodeView.php',
'PhabricatorSourceDocumentEngine' => 'applications/files/document/PhabricatorSourceDocumentEngine.php',
'PhabricatorSpaceEditField' => 'applications/transactions/editfield/PhabricatorSpaceEditField.php',
'PhabricatorSpacesApplication' => 'applications/spaces/application/PhabricatorSpacesApplication.php',
'PhabricatorSpacesArchiveController' => 'applications/spaces/controller/PhabricatorSpacesArchiveController.php',
'PhabricatorSpacesCapabilityCreateSpaces' => 'applications/spaces/capability/PhabricatorSpacesCapabilityCreateSpaces.php',
'PhabricatorSpacesCapabilityDefaultEdit' => 'applications/spaces/capability/PhabricatorSpacesCapabilityDefaultEdit.php',
'PhabricatorSpacesCapabilityDefaultView' => 'applications/spaces/capability/PhabricatorSpacesCapabilityDefaultView.php',
'PhabricatorSpacesController' => 'applications/spaces/controller/PhabricatorSpacesController.php',
'PhabricatorSpacesDAO' => 'applications/spaces/storage/PhabricatorSpacesDAO.php',
'PhabricatorSpacesEditController' => 'applications/spaces/controller/PhabricatorSpacesEditController.php',
'PhabricatorSpacesExportEngineExtension' => 'infrastructure/export/engine/PhabricatorSpacesExportEngineExtension.php',
'PhabricatorSpacesInterface' => 'applications/spaces/interface/PhabricatorSpacesInterface.php',
'PhabricatorSpacesListController' => 'applications/spaces/controller/PhabricatorSpacesListController.php',
'PhabricatorSpacesMailEngineExtension' => 'applications/spaces/engineextension/PhabricatorSpacesMailEngineExtension.php',
'PhabricatorSpacesNamespace' => 'applications/spaces/storage/PhabricatorSpacesNamespace.php',
'PhabricatorSpacesNamespaceArchiveTransaction' => 'applications/spaces/xaction/PhabricatorSpacesNamespaceArchiveTransaction.php',
'PhabricatorSpacesNamespaceDatasource' => 'applications/spaces/typeahead/PhabricatorSpacesNamespaceDatasource.php',
'PhabricatorSpacesNamespaceDefaultTransaction' => 'applications/spaces/xaction/PhabricatorSpacesNamespaceDefaultTransaction.php',
'PhabricatorSpacesNamespaceDescriptionTransaction' => 'applications/spaces/xaction/PhabricatorSpacesNamespaceDescriptionTransaction.php',
'PhabricatorSpacesNamespaceEditor' => 'applications/spaces/editor/PhabricatorSpacesNamespaceEditor.php',
'PhabricatorSpacesNamespaceNameTransaction' => 'applications/spaces/xaction/PhabricatorSpacesNamespaceNameTransaction.php',
'PhabricatorSpacesNamespacePHIDType' => 'applications/spaces/phid/PhabricatorSpacesNamespacePHIDType.php',
'PhabricatorSpacesNamespaceQuery' => 'applications/spaces/query/PhabricatorSpacesNamespaceQuery.php',
'PhabricatorSpacesNamespaceSearchEngine' => 'applications/spaces/query/PhabricatorSpacesNamespaceSearchEngine.php',
'PhabricatorSpacesNamespaceTransaction' => 'applications/spaces/storage/PhabricatorSpacesNamespaceTransaction.php',
'PhabricatorSpacesNamespaceTransactionQuery' => 'applications/spaces/query/PhabricatorSpacesNamespaceTransactionQuery.php',
'PhabricatorSpacesNamespaceTransactionType' => 'applications/spaces/xaction/PhabricatorSpacesNamespaceTransactionType.php',
'PhabricatorSpacesNoAccessController' => 'applications/spaces/controller/PhabricatorSpacesNoAccessController.php',
'PhabricatorSpacesRemarkupRule' => 'applications/spaces/remarkup/PhabricatorSpacesRemarkupRule.php',
'PhabricatorSpacesSchemaSpec' => 'applications/spaces/storage/PhabricatorSpacesSchemaSpec.php',
'PhabricatorSpacesSearchEngineExtension' => 'applications/spaces/engineextension/PhabricatorSpacesSearchEngineExtension.php',
'PhabricatorSpacesSearchField' => 'applications/spaces/searchfield/PhabricatorSpacesSearchField.php',
'PhabricatorSpacesTestCase' => 'applications/spaces/__tests__/PhabricatorSpacesTestCase.php',
'PhabricatorSpacesViewController' => 'applications/spaces/controller/PhabricatorSpacesViewController.php',
'PhabricatorStandardCustomField' => 'infrastructure/customfield/standard/PhabricatorStandardCustomField.php',
'PhabricatorStandardCustomFieldBlueprints' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldBlueprints.php',
'PhabricatorStandardCustomFieldBool' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php',
'PhabricatorStandardCustomFieldCredential' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldCredential.php',
'PhabricatorStandardCustomFieldDatasource' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldDatasource.php',
'PhabricatorStandardCustomFieldDate' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldDate.php',
'PhabricatorStandardCustomFieldHeader' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldHeader.php',
'PhabricatorStandardCustomFieldInt' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldInt.php',
'PhabricatorStandardCustomFieldInterface' => 'infrastructure/customfield/interface/PhabricatorStandardCustomFieldInterface.php',
'PhabricatorStandardCustomFieldLink' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php',
'PhabricatorStandardCustomFieldPHIDs' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php',
'PhabricatorStandardCustomFieldRemarkup' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php',
'PhabricatorStandardCustomFieldSelect' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldSelect.php',
'PhabricatorStandardCustomFieldText' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php',
'PhabricatorStandardCustomFieldTokenizer' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php',
'PhabricatorStandardCustomFieldUsers' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldUsers.php',
'PhabricatorStandardPageView' => 'view/page/PhabricatorStandardPageView.php',
'PhabricatorStandardSelectCustomFieldDatasource' => 'infrastructure/customfield/datasource/PhabricatorStandardSelectCustomFieldDatasource.php',
+ 'PhabricatorStandardTimelineEngine' => 'applications/transactions/engine/PhabricatorStandardTimelineEngine.php',
'PhabricatorStaticEditField' => 'applications/transactions/editfield/PhabricatorStaticEditField.php',
'PhabricatorStatusController' => 'applications/system/controller/PhabricatorStatusController.php',
'PhabricatorStatusUIExample' => 'applications/uiexample/examples/PhabricatorStatusUIExample.php',
'PhabricatorStorageFixtureScopeGuard' => 'infrastructure/testing/fixture/PhabricatorStorageFixtureScopeGuard.php',
'PhabricatorStorageManagementAPI' => 'infrastructure/storage/management/PhabricatorStorageManagementAPI.php',
'PhabricatorStorageManagementAdjustWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementAdjustWorkflow.php',
'PhabricatorStorageManagementAnalyzeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementAnalyzeWorkflow.php',
'PhabricatorStorageManagementDatabasesWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementDatabasesWorkflow.php',
'PhabricatorStorageManagementDestroyWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php',
'PhabricatorStorageManagementDumpWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php',
'PhabricatorStorageManagementOptimizeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementOptimizeWorkflow.php',
'PhabricatorStorageManagementPartitionWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementPartitionWorkflow.php',
'PhabricatorStorageManagementProbeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementProbeWorkflow.php',
'PhabricatorStorageManagementQuickstartWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementQuickstartWorkflow.php',
'PhabricatorStorageManagementRenamespaceWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementRenamespaceWorkflow.php',
'PhabricatorStorageManagementShellWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementShellWorkflow.php',
'PhabricatorStorageManagementStatusWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementStatusWorkflow.php',
'PhabricatorStorageManagementUpgradeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php',
'PhabricatorStorageManagementWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php',
'PhabricatorStoragePatch' => 'infrastructure/storage/management/PhabricatorStoragePatch.php',
'PhabricatorStorageSchemaSpec' => 'infrastructure/storage/schema/PhabricatorStorageSchemaSpec.php',
'PhabricatorStorageSetupCheck' => 'applications/config/check/PhabricatorStorageSetupCheck.php',
'PhabricatorStringConfigType' => 'applications/config/type/PhabricatorStringConfigType.php',
'PhabricatorStringExportField' => 'infrastructure/export/field/PhabricatorStringExportField.php',
'PhabricatorStringListConfigType' => 'applications/config/type/PhabricatorStringListConfigType.php',
'PhabricatorStringListEditField' => 'applications/transactions/editfield/PhabricatorStringListEditField.php',
'PhabricatorStringListExportField' => 'infrastructure/export/field/PhabricatorStringListExportField.php',
'PhabricatorStringMailStamp' => 'applications/metamta/stamp/PhabricatorStringMailStamp.php',
'PhabricatorStringSetting' => 'applications/settings/setting/PhabricatorStringSetting.php',
'PhabricatorSubmitEditField' => 'applications/transactions/editfield/PhabricatorSubmitEditField.php',
'PhabricatorSubscribableInterface' => 'applications/subscriptions/interface/PhabricatorSubscribableInterface.php',
'PhabricatorSubscribedToObjectEdgeType' => 'applications/transactions/edges/PhabricatorSubscribedToObjectEdgeType.php',
'PhabricatorSubscribersEditField' => 'applications/transactions/editfield/PhabricatorSubscribersEditField.php',
'PhabricatorSubscribersQuery' => 'applications/subscriptions/query/PhabricatorSubscribersQuery.php',
'PhabricatorSubscriptionTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorSubscriptionTriggerClock.php',
'PhabricatorSubscriptionsAddSelfHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsAddSelfHeraldAction.php',
'PhabricatorSubscriptionsAddSubscribersHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsAddSubscribersHeraldAction.php',
'PhabricatorSubscriptionsApplication' => 'applications/subscriptions/application/PhabricatorSubscriptionsApplication.php',
'PhabricatorSubscriptionsCurtainExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsCurtainExtension.php',
'PhabricatorSubscriptionsEditController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php',
'PhabricatorSubscriptionsEditEngineExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsEditEngineExtension.php',
'PhabricatorSubscriptionsEditor' => 'applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php',
'PhabricatorSubscriptionsExportEngineExtension' => 'infrastructure/export/engine/PhabricatorSubscriptionsExportEngineExtension.php',
'PhabricatorSubscriptionsFulltextEngineExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsFulltextEngineExtension.php',
'PhabricatorSubscriptionsHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsHeraldAction.php',
'PhabricatorSubscriptionsListController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsListController.php',
'PhabricatorSubscriptionsMailEngineExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsMailEngineExtension.php',
'PhabricatorSubscriptionsMuteController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php',
'PhabricatorSubscriptionsRemoveSelfHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsRemoveSelfHeraldAction.php',
'PhabricatorSubscriptionsRemoveSubscribersHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsRemoveSubscribersHeraldAction.php',
'PhabricatorSubscriptionsSearchEngineAttachment' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsSearchEngineAttachment.php',
'PhabricatorSubscriptionsSearchEngineExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsSearchEngineExtension.php',
'PhabricatorSubscriptionsSubscribeEmailCommand' => 'applications/subscriptions/command/PhabricatorSubscriptionsSubscribeEmailCommand.php',
'PhabricatorSubscriptionsSubscribersPolicyRule' => 'applications/subscriptions/policyrule/PhabricatorSubscriptionsSubscribersPolicyRule.php',
'PhabricatorSubscriptionsTransactionController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsTransactionController.php',
'PhabricatorSubscriptionsUIEventListener' => 'applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php',
'PhabricatorSubscriptionsUnsubscribeEmailCommand' => 'applications/subscriptions/command/PhabricatorSubscriptionsUnsubscribeEmailCommand.php',
'PhabricatorSubtypeEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php',
'PhabricatorSupportApplication' => 'applications/support/application/PhabricatorSupportApplication.php',
'PhabricatorSyntaxHighlighter' => 'infrastructure/markup/PhabricatorSyntaxHighlighter.php',
'PhabricatorSyntaxHighlightingConfigOptions' => 'applications/config/option/PhabricatorSyntaxHighlightingConfigOptions.php',
'PhabricatorSyntaxStyle' => 'infrastructure/syntax/PhabricatorSyntaxStyle.php',
'PhabricatorSystemAction' => 'applications/system/action/PhabricatorSystemAction.php',
'PhabricatorSystemActionEngine' => 'applications/system/engine/PhabricatorSystemActionEngine.php',
'PhabricatorSystemActionGarbageCollector' => 'applications/system/garbagecollector/PhabricatorSystemActionGarbageCollector.php',
'PhabricatorSystemActionLog' => 'applications/system/storage/PhabricatorSystemActionLog.php',
'PhabricatorSystemActionRateLimitException' => 'applications/system/exception/PhabricatorSystemActionRateLimitException.php',
'PhabricatorSystemApplication' => 'applications/system/application/PhabricatorSystemApplication.php',
'PhabricatorSystemDAO' => 'applications/system/storage/PhabricatorSystemDAO.php',
'PhabricatorSystemDestructionGarbageCollector' => 'applications/system/garbagecollector/PhabricatorSystemDestructionGarbageCollector.php',
'PhabricatorSystemDestructionLog' => 'applications/system/storage/PhabricatorSystemDestructionLog.php',
'PhabricatorSystemObjectController' => 'applications/system/controller/PhabricatorSystemObjectController.php',
'PhabricatorSystemReadOnlyController' => 'applications/system/controller/PhabricatorSystemReadOnlyController.php',
'PhabricatorSystemRemoveDestroyWorkflow' => 'applications/system/management/PhabricatorSystemRemoveDestroyWorkflow.php',
'PhabricatorSystemRemoveLogWorkflow' => 'applications/system/management/PhabricatorSystemRemoveLogWorkflow.php',
'PhabricatorSystemRemoveWorkflow' => 'applications/system/management/PhabricatorSystemRemoveWorkflow.php',
'PhabricatorSystemSelectEncodingController' => 'applications/system/controller/PhabricatorSystemSelectEncodingController.php',
'PhabricatorSystemSelectHighlightController' => 'applications/system/controller/PhabricatorSystemSelectHighlightController.php',
'PhabricatorTOTPAuthFactor' => 'applications/auth/factor/PhabricatorTOTPAuthFactor.php',
'PhabricatorTOTPAuthFactorTestCase' => 'applications/auth/factor/__tests__/PhabricatorTOTPAuthFactorTestCase.php',
'PhabricatorTaskmasterDaemon' => 'infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php',
'PhabricatorTaskmasterDaemonModule' => 'infrastructure/daemon/workers/PhabricatorTaskmasterDaemonModule.php',
'PhabricatorTestApplication' => 'applications/base/controller/__tests__/PhabricatorTestApplication.php',
'PhabricatorTestCase' => 'infrastructure/testing/PhabricatorTestCase.php',
'PhabricatorTestController' => 'applications/base/controller/__tests__/PhabricatorTestController.php',
'PhabricatorTestDataGenerator' => 'applications/lipsum/generator/PhabricatorTestDataGenerator.php',
'PhabricatorTestNoCycleEdgeType' => 'applications/transactions/edges/PhabricatorTestNoCycleEdgeType.php',
'PhabricatorTestStorageEngine' => 'applications/files/engine/PhabricatorTestStorageEngine.php',
'PhabricatorTestWorker' => 'infrastructure/daemon/workers/__tests__/PhabricatorTestWorker.php',
'PhabricatorTextAreaEditField' => 'applications/transactions/editfield/PhabricatorTextAreaEditField.php',
'PhabricatorTextConfigType' => 'applications/config/type/PhabricatorTextConfigType.php',
'PhabricatorTextDocumentEngine' => 'applications/files/document/PhabricatorTextDocumentEngine.php',
'PhabricatorTextEditField' => 'applications/transactions/editfield/PhabricatorTextEditField.php',
'PhabricatorTextExportFormat' => 'infrastructure/export/format/PhabricatorTextExportFormat.php',
'PhabricatorTextListConfigType' => 'applications/config/type/PhabricatorTextListConfigType.php',
'PhabricatorTime' => 'infrastructure/time/PhabricatorTime.php',
'PhabricatorTimeFormatSetting' => 'applications/settings/setting/PhabricatorTimeFormatSetting.php',
'PhabricatorTimeGuard' => 'infrastructure/time/PhabricatorTimeGuard.php',
'PhabricatorTimeTestCase' => 'infrastructure/time/__tests__/PhabricatorTimeTestCase.php',
+ 'PhabricatorTimelineEngine' => 'applications/transactions/engine/PhabricatorTimelineEngine.php',
+ 'PhabricatorTimelineInterface' => 'applications/transactions/interface/PhabricatorTimelineInterface.php',
'PhabricatorTimezoneIgnoreOffsetSetting' => 'applications/settings/setting/PhabricatorTimezoneIgnoreOffsetSetting.php',
'PhabricatorTimezoneSetting' => 'applications/settings/setting/PhabricatorTimezoneSetting.php',
'PhabricatorTimezoneSetupCheck' => 'applications/config/check/PhabricatorTimezoneSetupCheck.php',
'PhabricatorTitleGlyphsSetting' => 'applications/settings/setting/PhabricatorTitleGlyphsSetting.php',
'PhabricatorToken' => 'applications/tokens/storage/PhabricatorToken.php',
'PhabricatorTokenController' => 'applications/tokens/controller/PhabricatorTokenController.php',
'PhabricatorTokenCount' => 'applications/tokens/storage/PhabricatorTokenCount.php',
'PhabricatorTokenCountQuery' => 'applications/tokens/query/PhabricatorTokenCountQuery.php',
'PhabricatorTokenDAO' => 'applications/tokens/storage/PhabricatorTokenDAO.php',
'PhabricatorTokenDestructionEngineExtension' => 'applications/tokens/engineextension/PhabricatorTokenDestructionEngineExtension.php',
'PhabricatorTokenGiveController' => 'applications/tokens/controller/PhabricatorTokenGiveController.php',
'PhabricatorTokenGiven' => 'applications/tokens/storage/PhabricatorTokenGiven.php',
'PhabricatorTokenGivenController' => 'applications/tokens/controller/PhabricatorTokenGivenController.php',
'PhabricatorTokenGivenEditor' => 'applications/tokens/editor/PhabricatorTokenGivenEditor.php',
'PhabricatorTokenGivenFeedStory' => 'applications/tokens/feed/PhabricatorTokenGivenFeedStory.php',
'PhabricatorTokenGivenQuery' => 'applications/tokens/query/PhabricatorTokenGivenQuery.php',
'PhabricatorTokenLeaderController' => 'applications/tokens/controller/PhabricatorTokenLeaderController.php',
'PhabricatorTokenQuery' => 'applications/tokens/query/PhabricatorTokenQuery.php',
'PhabricatorTokenReceiverInterface' => 'applications/tokens/interface/PhabricatorTokenReceiverInterface.php',
'PhabricatorTokenReceiverQuery' => 'applications/tokens/query/PhabricatorTokenReceiverQuery.php',
'PhabricatorTokenTokenPHIDType' => 'applications/tokens/phid/PhabricatorTokenTokenPHIDType.php',
'PhabricatorTokenUIEventListener' => 'applications/tokens/event/PhabricatorTokenUIEventListener.php',
'PhabricatorTokenizerEditField' => 'applications/transactions/editfield/PhabricatorTokenizerEditField.php',
'PhabricatorTokensApplication' => 'applications/tokens/application/PhabricatorTokensApplication.php',
'PhabricatorTokensCurtainExtension' => 'applications/tokens/engineextension/PhabricatorTokensCurtainExtension.php',
'PhabricatorTokensSettingsPanel' => 'applications/settings/panel/PhabricatorTokensSettingsPanel.php',
'PhabricatorTokensToken' => 'applications/tokens/storage/PhabricatorTokensToken.php',
'PhabricatorTransactionChange' => 'applications/transactions/data/PhabricatorTransactionChange.php',
'PhabricatorTransactionFactEngine' => 'applications/fact/engine/PhabricatorTransactionFactEngine.php',
'PhabricatorTransactionRemarkupChange' => 'applications/transactions/data/PhabricatorTransactionRemarkupChange.php',
'PhabricatorTransactions' => 'applications/transactions/constants/PhabricatorTransactions.php',
'PhabricatorTransactionsApplication' => 'applications/transactions/application/PhabricatorTransactionsApplication.php',
'PhabricatorTransactionsDestructionEngineExtension' => 'applications/transactions/engineextension/PhabricatorTransactionsDestructionEngineExtension.php',
'PhabricatorTransactionsFulltextEngineExtension' => 'applications/transactions/engineextension/PhabricatorTransactionsFulltextEngineExtension.php',
'PhabricatorTransformedFile' => 'applications/files/storage/PhabricatorTransformedFile.php',
'PhabricatorTranslationSetting' => 'applications/settings/setting/PhabricatorTranslationSetting.php',
'PhabricatorTranslationsConfigOptions' => 'applications/config/option/PhabricatorTranslationsConfigOptions.php',
'PhabricatorTriggerAction' => 'infrastructure/daemon/workers/action/PhabricatorTriggerAction.php',
'PhabricatorTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorTriggerClock.php',
'PhabricatorTriggerClockTestCase' => 'infrastructure/daemon/workers/clock/__tests__/PhabricatorTriggerClockTestCase.php',
'PhabricatorTriggerDaemon' => 'infrastructure/daemon/workers/PhabricatorTriggerDaemon.php',
'PhabricatorTrivialTestCase' => 'infrastructure/testing/__tests__/PhabricatorTrivialTestCase.php',
+ 'PhabricatorTwilioFuture' => 'applications/metamta/future/PhabricatorTwilioFuture.php',
'PhabricatorTwitchAuthProvider' => 'applications/auth/provider/PhabricatorTwitchAuthProvider.php',
'PhabricatorTwitterAuthProvider' => 'applications/auth/provider/PhabricatorTwitterAuthProvider.php',
'PhabricatorTypeaheadApplication' => 'applications/typeahead/application/PhabricatorTypeaheadApplication.php',
'PhabricatorTypeaheadCompositeDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php',
'PhabricatorTypeaheadDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php',
'PhabricatorTypeaheadDatasourceController' => 'applications/typeahead/controller/PhabricatorTypeaheadDatasourceController.php',
'PhabricatorTypeaheadDatasourceTestCase' => 'applications/typeahead/datasource/__tests__/PhabricatorTypeaheadDatasourceTestCase.php',
'PhabricatorTypeaheadFunctionHelpController' => 'applications/typeahead/controller/PhabricatorTypeaheadFunctionHelpController.php',
'PhabricatorTypeaheadInvalidTokenException' => 'applications/typeahead/exception/PhabricatorTypeaheadInvalidTokenException.php',
'PhabricatorTypeaheadModularDatasourceController' => 'applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php',
'PhabricatorTypeaheadMonogramDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadMonogramDatasource.php',
'PhabricatorTypeaheadProxyDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadProxyDatasource.php',
'PhabricatorTypeaheadResult' => 'applications/typeahead/storage/PhabricatorTypeaheadResult.php',
'PhabricatorTypeaheadRuntimeCompositeDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadRuntimeCompositeDatasource.php',
'PhabricatorTypeaheadTestNumbersDatasource' => 'applications/typeahead/datasource/__tests__/PhabricatorTypeaheadTestNumbersDatasource.php',
'PhabricatorTypeaheadTokenView' => 'applications/typeahead/view/PhabricatorTypeaheadTokenView.php',
'PhabricatorUIConfigOptions' => 'applications/config/option/PhabricatorUIConfigOptions.php',
'PhabricatorUIExample' => 'applications/uiexample/examples/PhabricatorUIExample.php',
'PhabricatorUIExampleRenderController' => 'applications/uiexample/controller/PhabricatorUIExampleRenderController.php',
'PhabricatorUIExamplesApplication' => 'applications/uiexample/application/PhabricatorUIExamplesApplication.php',
'PhabricatorURIExportField' => 'infrastructure/export/field/PhabricatorURIExportField.php',
'PhabricatorUSEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php',
'PhabricatorUnifiedDiffsSetting' => 'applications/settings/setting/PhabricatorUnifiedDiffsSetting.php',
'PhabricatorUnitTestContentSource' => 'infrastructure/contentsource/PhabricatorUnitTestContentSource.php',
'PhabricatorUnitsTestCase' => 'view/__tests__/PhabricatorUnitsTestCase.php',
'PhabricatorUnknownContentSource' => 'infrastructure/contentsource/PhabricatorUnknownContentSource.php',
'PhabricatorUnsubscribedFromObjectEdgeType' => 'applications/transactions/edges/PhabricatorUnsubscribedFromObjectEdgeType.php',
'PhabricatorUser' => 'applications/people/storage/PhabricatorUser.php',
'PhabricatorUserApproveTransaction' => 'applications/people/xaction/PhabricatorUserApproveTransaction.php',
'PhabricatorUserBadgesCacheType' => 'applications/people/cache/PhabricatorUserBadgesCacheType.php',
'PhabricatorUserBlurbField' => 'applications/people/customfield/PhabricatorUserBlurbField.php',
'PhabricatorUserCache' => 'applications/people/storage/PhabricatorUserCache.php',
'PhabricatorUserCachePurger' => 'applications/cache/purger/PhabricatorUserCachePurger.php',
'PhabricatorUserCacheType' => 'applications/people/cache/PhabricatorUserCacheType.php',
'PhabricatorUserCardView' => 'applications/people/view/PhabricatorUserCardView.php',
'PhabricatorUserConfigOptions' => 'applications/people/config/PhabricatorUserConfigOptions.php',
'PhabricatorUserConfiguredCustomField' => 'applications/people/customfield/PhabricatorUserConfiguredCustomField.php',
'PhabricatorUserConfiguredCustomFieldStorage' => 'applications/people/storage/PhabricatorUserConfiguredCustomFieldStorage.php',
'PhabricatorUserCustomField' => 'applications/people/customfield/PhabricatorUserCustomField.php',
'PhabricatorUserCustomFieldNumericIndex' => 'applications/people/storage/PhabricatorUserCustomFieldNumericIndex.php',
'PhabricatorUserCustomFieldStringIndex' => 'applications/people/storage/PhabricatorUserCustomFieldStringIndex.php',
'PhabricatorUserDAO' => 'applications/people/storage/PhabricatorUserDAO.php',
'PhabricatorUserDisableTransaction' => 'applications/people/xaction/PhabricatorUserDisableTransaction.php',
'PhabricatorUserEditEngine' => 'applications/people/editor/PhabricatorUserEditEngine.php',
'PhabricatorUserEditor' => 'applications/people/editor/PhabricatorUserEditor.php',
'PhabricatorUserEditorTestCase' => 'applications/people/editor/__tests__/PhabricatorUserEditorTestCase.php',
'PhabricatorUserEmail' => 'applications/people/storage/PhabricatorUserEmail.php',
'PhabricatorUserEmailTestCase' => 'applications/people/storage/__tests__/PhabricatorUserEmailTestCase.php',
+ 'PhabricatorUserEmpowerTransaction' => 'applications/people/xaction/PhabricatorUserEmpowerTransaction.php',
'PhabricatorUserFerretEngine' => 'applications/people/search/PhabricatorUserFerretEngine.php',
'PhabricatorUserFulltextEngine' => 'applications/people/search/PhabricatorUserFulltextEngine.php',
'PhabricatorUserIconField' => 'applications/people/customfield/PhabricatorUserIconField.php',
'PhabricatorUserLog' => 'applications/people/storage/PhabricatorUserLog.php',
'PhabricatorUserLogView' => 'applications/people/view/PhabricatorUserLogView.php',
'PhabricatorUserMessageCountCacheType' => 'applications/people/cache/PhabricatorUserMessageCountCacheType.php',
'PhabricatorUserNotificationCountCacheType' => 'applications/people/cache/PhabricatorUserNotificationCountCacheType.php',
'PhabricatorUserNotifyTransaction' => 'applications/people/xaction/PhabricatorUserNotifyTransaction.php',
'PhabricatorUserPHIDResolver' => 'applications/phid/resolver/PhabricatorUserPHIDResolver.php',
'PhabricatorUserPreferences' => 'applications/settings/storage/PhabricatorUserPreferences.php',
'PhabricatorUserPreferencesCacheType' => 'applications/people/cache/PhabricatorUserPreferencesCacheType.php',
'PhabricatorUserPreferencesEditor' => 'applications/settings/editor/PhabricatorUserPreferencesEditor.php',
'PhabricatorUserPreferencesPHIDType' => 'applications/settings/phid/PhabricatorUserPreferencesPHIDType.php',
'PhabricatorUserPreferencesQuery' => 'applications/settings/query/PhabricatorUserPreferencesQuery.php',
'PhabricatorUserPreferencesSearchEngine' => 'applications/settings/query/PhabricatorUserPreferencesSearchEngine.php',
'PhabricatorUserPreferencesTransaction' => 'applications/settings/storage/PhabricatorUserPreferencesTransaction.php',
'PhabricatorUserPreferencesTransactionQuery' => 'applications/settings/query/PhabricatorUserPreferencesTransactionQuery.php',
'PhabricatorUserProfile' => 'applications/people/storage/PhabricatorUserProfile.php',
'PhabricatorUserProfileImageCacheType' => 'applications/people/cache/PhabricatorUserProfileImageCacheType.php',
'PhabricatorUserRealNameField' => 'applications/people/customfield/PhabricatorUserRealNameField.php',
'PhabricatorUserRolesField' => 'applications/people/customfield/PhabricatorUserRolesField.php',
'PhabricatorUserSchemaSpec' => 'applications/people/storage/PhabricatorUserSchemaSpec.php',
'PhabricatorUserSinceField' => 'applications/people/customfield/PhabricatorUserSinceField.php',
'PhabricatorUserStatusField' => 'applications/people/customfield/PhabricatorUserStatusField.php',
'PhabricatorUserTestCase' => 'applications/people/storage/__tests__/PhabricatorUserTestCase.php',
'PhabricatorUserTitleField' => 'applications/people/customfield/PhabricatorUserTitleField.php',
'PhabricatorUserTransaction' => 'applications/people/storage/PhabricatorUserTransaction.php',
'PhabricatorUserTransactionEditor' => 'applications/people/editor/PhabricatorUserTransactionEditor.php',
'PhabricatorUserTransactionType' => 'applications/people/xaction/PhabricatorUserTransactionType.php',
'PhabricatorUserUsernameTransaction' => 'applications/people/xaction/PhabricatorUserUsernameTransaction.php',
'PhabricatorUsersEditField' => 'applications/transactions/editfield/PhabricatorUsersEditField.php',
'PhabricatorUsersPolicyRule' => 'applications/people/policyrule/PhabricatorUsersPolicyRule.php',
'PhabricatorUsersSearchField' => 'applications/people/searchfield/PhabricatorUsersSearchField.php',
'PhabricatorVCSResponse' => 'applications/repository/response/PhabricatorVCSResponse.php',
'PhabricatorVersionedDraft' => 'applications/draft/storage/PhabricatorVersionedDraft.php',
'PhabricatorVeryWowEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorVeryWowEnglishTranslation.php',
'PhabricatorVideoDocumentEngine' => 'applications/files/document/PhabricatorVideoDocumentEngine.php',
'PhabricatorViewerDatasource' => 'applications/people/typeahead/PhabricatorViewerDatasource.php',
'PhabricatorVoidDocumentEngine' => 'applications/files/document/PhabricatorVoidDocumentEngine.php',
'PhabricatorWatcherHasObjectEdgeType' => 'applications/transactions/edges/PhabricatorWatcherHasObjectEdgeType.php',
'PhabricatorWebContentSource' => 'infrastructure/contentsource/PhabricatorWebContentSource.php',
'PhabricatorWebServerSetupCheck' => 'applications/config/check/PhabricatorWebServerSetupCheck.php',
'PhabricatorWeekStartDaySetting' => 'applications/settings/setting/PhabricatorWeekStartDaySetting.php',
'PhabricatorWildConfigType' => 'applications/config/type/PhabricatorWildConfigType.php',
'PhabricatorWordPressAuthProvider' => 'applications/auth/provider/PhabricatorWordPressAuthProvider.php',
'PhabricatorWorker' => 'infrastructure/daemon/workers/PhabricatorWorker.php',
'PhabricatorWorkerActiveTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php',
'PhabricatorWorkerActiveTaskQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerActiveTaskQuery.php',
'PhabricatorWorkerArchiveTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php',
'PhabricatorWorkerArchiveTaskQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerArchiveTaskQuery.php',
'PhabricatorWorkerBulkJob' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php',
'PhabricatorWorkerBulkJobCreateWorker' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobCreateWorker.php',
'PhabricatorWorkerBulkJobEditor' => 'infrastructure/daemon/workers/editor/PhabricatorWorkerBulkJobEditor.php',
'PhabricatorWorkerBulkJobPHIDType' => 'infrastructure/daemon/workers/phid/PhabricatorWorkerBulkJobPHIDType.php',
'PhabricatorWorkerBulkJobQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerBulkJobQuery.php',
'PhabricatorWorkerBulkJobSearchEngine' => 'infrastructure/daemon/workers/query/PhabricatorWorkerBulkJobSearchEngine.php',
'PhabricatorWorkerBulkJobTaskWorker' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobTaskWorker.php',
'PhabricatorWorkerBulkJobTestCase' => 'infrastructure/daemon/workers/__tests__/PhabricatorWorkerBulkJobTestCase.php',
'PhabricatorWorkerBulkJobTransaction' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJobTransaction.php',
'PhabricatorWorkerBulkJobTransactionQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerBulkJobTransactionQuery.php',
'PhabricatorWorkerBulkJobType' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobType.php',
'PhabricatorWorkerBulkJobWorker' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobWorker.php',
'PhabricatorWorkerBulkTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerBulkTask.php',
'PhabricatorWorkerDAO' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerDAO.php',
'PhabricatorWorkerDestructionEngineExtension' => 'infrastructure/daemon/workers/engineextension/PhabricatorWorkerDestructionEngineExtension.php',
'PhabricatorWorkerLeaseQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php',
'PhabricatorWorkerManagementCancelWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementCancelWorkflow.php',
'PhabricatorWorkerManagementExecuteWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementExecuteWorkflow.php',
'PhabricatorWorkerManagementFloodWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementFloodWorkflow.php',
'PhabricatorWorkerManagementFreeWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementFreeWorkflow.php',
'PhabricatorWorkerManagementRetryWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementRetryWorkflow.php',
'PhabricatorWorkerManagementWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementWorkflow.php',
'PhabricatorWorkerPermanentFailureException' => 'infrastructure/daemon/workers/exception/PhabricatorWorkerPermanentFailureException.php',
'PhabricatorWorkerSchemaSpec' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerSchemaSpec.php',
'PhabricatorWorkerSingleBulkJobType' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerSingleBulkJobType.php',
'PhabricatorWorkerTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTask.php',
'PhabricatorWorkerTaskData' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTaskData.php',
'PhabricatorWorkerTaskDetailController' => 'applications/daemon/controller/PhabricatorWorkerTaskDetailController.php',
'PhabricatorWorkerTaskQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerTaskQuery.php',
'PhabricatorWorkerTestCase' => 'infrastructure/daemon/workers/__tests__/PhabricatorWorkerTestCase.php',
'PhabricatorWorkerTrigger' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTrigger.php',
'PhabricatorWorkerTriggerEvent' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTriggerEvent.php',
'PhabricatorWorkerTriggerManagementFireWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerTriggerManagementFireWorkflow.php',
'PhabricatorWorkerTriggerManagementWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerTriggerManagementWorkflow.php',
'PhabricatorWorkerTriggerPHIDType' => 'infrastructure/daemon/workers/phid/PhabricatorWorkerTriggerPHIDType.php',
'PhabricatorWorkerTriggerQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php',
'PhabricatorWorkerYieldException' => 'infrastructure/daemon/workers/exception/PhabricatorWorkerYieldException.php',
'PhabricatorWorkingCopyDiscoveryTestCase' => 'applications/repository/engine/__tests__/PhabricatorWorkingCopyDiscoveryTestCase.php',
'PhabricatorWorkingCopyPullTestCase' => 'applications/repository/engine/__tests__/PhabricatorWorkingCopyPullTestCase.php',
'PhabricatorWorkingCopyTestCase' => 'applications/repository/engine/__tests__/PhabricatorWorkingCopyTestCase.php',
'PhabricatorXHPASTDAO' => 'applications/phpast/storage/PhabricatorXHPASTDAO.php',
'PhabricatorXHPASTParseTree' => 'applications/phpast/storage/PhabricatorXHPASTParseTree.php',
'PhabricatorXHPASTViewController' => 'applications/phpast/controller/PhabricatorXHPASTViewController.php',
'PhabricatorXHPASTViewFrameController' => 'applications/phpast/controller/PhabricatorXHPASTViewFrameController.php',
'PhabricatorXHPASTViewFramesetController' => 'applications/phpast/controller/PhabricatorXHPASTViewFramesetController.php',
'PhabricatorXHPASTViewInputController' => 'applications/phpast/controller/PhabricatorXHPASTViewInputController.php',
'PhabricatorXHPASTViewPanelController' => 'applications/phpast/controller/PhabricatorXHPASTViewPanelController.php',
'PhabricatorXHPASTViewRunController' => 'applications/phpast/controller/PhabricatorXHPASTViewRunController.php',
'PhabricatorXHPASTViewStreamController' => 'applications/phpast/controller/PhabricatorXHPASTViewStreamController.php',
'PhabricatorXHPASTViewTreeController' => 'applications/phpast/controller/PhabricatorXHPASTViewTreeController.php',
'PhabricatorXHProfApplication' => 'applications/xhprof/application/PhabricatorXHProfApplication.php',
'PhabricatorXHProfController' => 'applications/xhprof/controller/PhabricatorXHProfController.php',
'PhabricatorXHProfDAO' => 'applications/xhprof/storage/PhabricatorXHProfDAO.php',
'PhabricatorXHProfDropController' => 'applications/xhprof/controller/PhabricatorXHProfDropController.php',
'PhabricatorXHProfProfileController' => 'applications/xhprof/controller/PhabricatorXHProfProfileController.php',
'PhabricatorXHProfProfileSymbolView' => 'applications/xhprof/view/PhabricatorXHProfProfileSymbolView.php',
'PhabricatorXHProfProfileTopLevelView' => 'applications/xhprof/view/PhabricatorXHProfProfileTopLevelView.php',
'PhabricatorXHProfProfileView' => 'applications/xhprof/view/PhabricatorXHProfProfileView.php',
'PhabricatorXHProfSample' => 'applications/xhprof/storage/PhabricatorXHProfSample.php',
'PhabricatorXHProfSampleListController' => 'applications/xhprof/controller/PhabricatorXHProfSampleListController.php',
'PhabricatorXHProfSampleQuery' => 'applications/xhprof/query/PhabricatorXHProfSampleQuery.php',
'PhabricatorXHProfSampleSearchEngine' => 'applications/xhprof/query/PhabricatorXHProfSampleSearchEngine.php',
'PhabricatorYoutubeRemarkupRule' => 'infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php',
'Phame404Response' => 'applications/phame/site/Phame404Response.php',
'PhameBlog' => 'applications/phame/storage/PhameBlog.php',
'PhameBlog404Controller' => 'applications/phame/controller/blog/PhameBlog404Controller.php',
'PhameBlogArchiveController' => 'applications/phame/controller/blog/PhameBlogArchiveController.php',
'PhameBlogController' => 'applications/phame/controller/blog/PhameBlogController.php',
'PhameBlogCreateCapability' => 'applications/phame/capability/PhameBlogCreateCapability.php',
'PhameBlogDatasource' => 'applications/phame/typeahead/PhameBlogDatasource.php',
'PhameBlogDescriptionTransaction' => 'applications/phame/xaction/PhameBlogDescriptionTransaction.php',
'PhameBlogEditConduitAPIMethod' => 'applications/phame/conduit/PhameBlogEditConduitAPIMethod.php',
'PhameBlogEditController' => 'applications/phame/controller/blog/PhameBlogEditController.php',
'PhameBlogEditEngine' => 'applications/phame/editor/PhameBlogEditEngine.php',
'PhameBlogEditor' => 'applications/phame/editor/PhameBlogEditor.php',
'PhameBlogFeedController' => 'applications/phame/controller/blog/PhameBlogFeedController.php',
'PhameBlogFerretEngine' => 'applications/phame/search/PhameBlogFerretEngine.php',
'PhameBlogFullDomainTransaction' => 'applications/phame/xaction/PhameBlogFullDomainTransaction.php',
'PhameBlogFulltextEngine' => 'applications/phame/search/PhameBlogFulltextEngine.php',
'PhameBlogHeaderImageTransaction' => 'applications/phame/xaction/PhameBlogHeaderImageTransaction.php',
'PhameBlogHeaderPictureController' => 'applications/phame/controller/blog/PhameBlogHeaderPictureController.php',
'PhameBlogListController' => 'applications/phame/controller/blog/PhameBlogListController.php',
'PhameBlogListView' => 'applications/phame/view/PhameBlogListView.php',
'PhameBlogManageController' => 'applications/phame/controller/blog/PhameBlogManageController.php',
'PhameBlogNameTransaction' => 'applications/phame/xaction/PhameBlogNameTransaction.php',
'PhameBlogParentDomainTransaction' => 'applications/phame/xaction/PhameBlogParentDomainTransaction.php',
'PhameBlogParentSiteTransaction' => 'applications/phame/xaction/PhameBlogParentSiteTransaction.php',
'PhameBlogProfileImageTransaction' => 'applications/phame/xaction/PhameBlogProfileImageTransaction.php',
'PhameBlogProfilePictureController' => 'applications/phame/controller/blog/PhameBlogProfilePictureController.php',
'PhameBlogQuery' => 'applications/phame/query/PhameBlogQuery.php',
'PhameBlogReplyHandler' => 'applications/phame/mail/PhameBlogReplyHandler.php',
'PhameBlogSearchConduitAPIMethod' => 'applications/phame/conduit/PhameBlogSearchConduitAPIMethod.php',
'PhameBlogSearchEngine' => 'applications/phame/query/PhameBlogSearchEngine.php',
'PhameBlogSite' => 'applications/phame/site/PhameBlogSite.php',
'PhameBlogStatusTransaction' => 'applications/phame/xaction/PhameBlogStatusTransaction.php',
'PhameBlogSubtitleTransaction' => 'applications/phame/xaction/PhameBlogSubtitleTransaction.php',
'PhameBlogTransaction' => 'applications/phame/storage/PhameBlogTransaction.php',
'PhameBlogTransactionQuery' => 'applications/phame/query/PhameBlogTransactionQuery.php',
'PhameBlogTransactionType' => 'applications/phame/xaction/PhameBlogTransactionType.php',
'PhameBlogViewController' => 'applications/phame/controller/blog/PhameBlogViewController.php',
'PhameConstants' => 'applications/phame/constants/PhameConstants.php',
'PhameController' => 'applications/phame/controller/PhameController.php',
'PhameDAO' => 'applications/phame/storage/PhameDAO.php',
'PhameDescriptionView' => 'applications/phame/view/PhameDescriptionView.php',
'PhameDraftListView' => 'applications/phame/view/PhameDraftListView.php',
'PhameHomeController' => 'applications/phame/controller/PhameHomeController.php',
'PhameLiveController' => 'applications/phame/controller/PhameLiveController.php',
'PhameNextPostView' => 'applications/phame/view/PhameNextPostView.php',
'PhamePost' => 'applications/phame/storage/PhamePost.php',
'PhamePostArchiveController' => 'applications/phame/controller/post/PhamePostArchiveController.php',
'PhamePostBlogTransaction' => 'applications/phame/xaction/PhamePostBlogTransaction.php',
'PhamePostBodyTransaction' => 'applications/phame/xaction/PhamePostBodyTransaction.php',
'PhamePostController' => 'applications/phame/controller/post/PhamePostController.php',
'PhamePostEditConduitAPIMethod' => 'applications/phame/conduit/PhamePostEditConduitAPIMethod.php',
'PhamePostEditController' => 'applications/phame/controller/post/PhamePostEditController.php',
'PhamePostEditEngine' => 'applications/phame/editor/PhamePostEditEngine.php',
'PhamePostEditor' => 'applications/phame/editor/PhamePostEditor.php',
'PhamePostFerretEngine' => 'applications/phame/search/PhamePostFerretEngine.php',
'PhamePostFulltextEngine' => 'applications/phame/search/PhamePostFulltextEngine.php',
'PhamePostHeaderImageTransaction' => 'applications/phame/xaction/PhamePostHeaderImageTransaction.php',
'PhamePostHeaderPictureController' => 'applications/phame/controller/post/PhamePostHeaderPictureController.php',
'PhamePostHistoryController' => 'applications/phame/controller/post/PhamePostHistoryController.php',
'PhamePostListController' => 'applications/phame/controller/post/PhamePostListController.php',
'PhamePostListView' => 'applications/phame/view/PhamePostListView.php',
'PhamePostMailReceiver' => 'applications/phame/mail/PhamePostMailReceiver.php',
'PhamePostMoveController' => 'applications/phame/controller/post/PhamePostMoveController.php',
'PhamePostPublishController' => 'applications/phame/controller/post/PhamePostPublishController.php',
'PhamePostQuery' => 'applications/phame/query/PhamePostQuery.php',
'PhamePostRemarkupRule' => 'applications/phame/remarkup/PhamePostRemarkupRule.php',
'PhamePostReplyHandler' => 'applications/phame/mail/PhamePostReplyHandler.php',
'PhamePostSearchConduitAPIMethod' => 'applications/phame/conduit/PhamePostSearchConduitAPIMethod.php',
'PhamePostSearchEngine' => 'applications/phame/query/PhamePostSearchEngine.php',
'PhamePostSubtitleTransaction' => 'applications/phame/xaction/PhamePostSubtitleTransaction.php',
'PhamePostTitleTransaction' => 'applications/phame/xaction/PhamePostTitleTransaction.php',
'PhamePostTransaction' => 'applications/phame/storage/PhamePostTransaction.php',
'PhamePostTransactionComment' => 'applications/phame/storage/PhamePostTransactionComment.php',
'PhamePostTransactionQuery' => 'applications/phame/query/PhamePostTransactionQuery.php',
'PhamePostTransactionType' => 'applications/phame/xaction/PhamePostTransactionType.php',
'PhamePostViewController' => 'applications/phame/controller/post/PhamePostViewController.php',
'PhamePostVisibilityTransaction' => 'applications/phame/xaction/PhamePostVisibilityTransaction.php',
'PhameSchemaSpec' => 'applications/phame/storage/PhameSchemaSpec.php',
'PhameSite' => 'applications/phame/site/PhameSite.php',
'PhluxController' => 'applications/phlux/controller/PhluxController.php',
'PhluxDAO' => 'applications/phlux/storage/PhluxDAO.php',
'PhluxEditController' => 'applications/phlux/controller/PhluxEditController.php',
'PhluxListController' => 'applications/phlux/controller/PhluxListController.php',
'PhluxSchemaSpec' => 'applications/phlux/storage/PhluxSchemaSpec.php',
'PhluxTransaction' => 'applications/phlux/storage/PhluxTransaction.php',
'PhluxTransactionQuery' => 'applications/phlux/query/PhluxTransactionQuery.php',
'PhluxVariable' => 'applications/phlux/storage/PhluxVariable.php',
'PhluxVariableEditor' => 'applications/phlux/editor/PhluxVariableEditor.php',
'PhluxVariablePHIDType' => 'applications/phlux/phid/PhluxVariablePHIDType.php',
'PhluxVariableQuery' => 'applications/phlux/query/PhluxVariableQuery.php',
'PhluxViewController' => 'applications/phlux/controller/PhluxViewController.php',
'PholioController' => 'applications/pholio/controller/PholioController.php',
'PholioDAO' => 'applications/pholio/storage/PholioDAO.php',
'PholioDefaultEditCapability' => 'applications/pholio/capability/PholioDefaultEditCapability.php',
'PholioDefaultViewCapability' => 'applications/pholio/capability/PholioDefaultViewCapability.php',
'PholioImage' => 'applications/pholio/storage/PholioImage.php',
'PholioImageDescriptionTransaction' => 'applications/pholio/xaction/PholioImageDescriptionTransaction.php',
'PholioImageFileTransaction' => 'applications/pholio/xaction/PholioImageFileTransaction.php',
'PholioImageNameTransaction' => 'applications/pholio/xaction/PholioImageNameTransaction.php',
'PholioImagePHIDType' => 'applications/pholio/phid/PholioImagePHIDType.php',
'PholioImageQuery' => 'applications/pholio/query/PholioImageQuery.php',
'PholioImageReplaceTransaction' => 'applications/pholio/xaction/PholioImageReplaceTransaction.php',
'PholioImageSequenceTransaction' => 'applications/pholio/xaction/PholioImageSequenceTransaction.php',
'PholioImageTransactionType' => 'applications/pholio/xaction/PholioImageTransactionType.php',
'PholioImageUploadController' => 'applications/pholio/controller/PholioImageUploadController.php',
'PholioInlineController' => 'applications/pholio/controller/PholioInlineController.php',
'PholioInlineListController' => 'applications/pholio/controller/PholioInlineListController.php',
'PholioMock' => 'applications/pholio/storage/PholioMock.php',
'PholioMockArchiveController' => 'applications/pholio/controller/PholioMockArchiveController.php',
'PholioMockAuthorHeraldField' => 'applications/pholio/herald/PholioMockAuthorHeraldField.php',
'PholioMockCommentController' => 'applications/pholio/controller/PholioMockCommentController.php',
'PholioMockDescriptionHeraldField' => 'applications/pholio/herald/PholioMockDescriptionHeraldField.php',
'PholioMockDescriptionTransaction' => 'applications/pholio/xaction/PholioMockDescriptionTransaction.php',
'PholioMockEditController' => 'applications/pholio/controller/PholioMockEditController.php',
'PholioMockEditor' => 'applications/pholio/editor/PholioMockEditor.php',
'PholioMockEmbedView' => 'applications/pholio/view/PholioMockEmbedView.php',
'PholioMockFerretEngine' => 'applications/pholio/search/PholioMockFerretEngine.php',
'PholioMockFulltextEngine' => 'applications/pholio/search/PholioMockFulltextEngine.php',
'PholioMockHasTaskEdgeType' => 'applications/pholio/edge/PholioMockHasTaskEdgeType.php',
'PholioMockHasTaskRelationship' => 'applications/pholio/relationships/PholioMockHasTaskRelationship.php',
'PholioMockHeraldField' => 'applications/pholio/herald/PholioMockHeraldField.php',
'PholioMockHeraldFieldGroup' => 'applications/pholio/herald/PholioMockHeraldFieldGroup.php',
'PholioMockImagesView' => 'applications/pholio/view/PholioMockImagesView.php',
'PholioMockInlineTransaction' => 'applications/pholio/xaction/PholioMockInlineTransaction.php',
'PholioMockListController' => 'applications/pholio/controller/PholioMockListController.php',
'PholioMockMailReceiver' => 'applications/pholio/mail/PholioMockMailReceiver.php',
'PholioMockNameHeraldField' => 'applications/pholio/herald/PholioMockNameHeraldField.php',
'PholioMockNameTransaction' => 'applications/pholio/xaction/PholioMockNameTransaction.php',
'PholioMockPHIDType' => 'applications/pholio/phid/PholioMockPHIDType.php',
'PholioMockQuery' => 'applications/pholio/query/PholioMockQuery.php',
'PholioMockRelationship' => 'applications/pholio/relationships/PholioMockRelationship.php',
'PholioMockRelationshipSource' => 'applications/search/relationship/PholioMockRelationshipSource.php',
'PholioMockSearchEngine' => 'applications/pholio/query/PholioMockSearchEngine.php',
'PholioMockStatusTransaction' => 'applications/pholio/xaction/PholioMockStatusTransaction.php',
'PholioMockThumbGridView' => 'applications/pholio/view/PholioMockThumbGridView.php',
+ 'PholioMockTimelineEngine' => 'applications/pholio/engine/PholioMockTimelineEngine.php',
'PholioMockTransactionType' => 'applications/pholio/xaction/PholioMockTransactionType.php',
'PholioMockViewController' => 'applications/pholio/controller/PholioMockViewController.php',
'PholioRemarkupRule' => 'applications/pholio/remarkup/PholioRemarkupRule.php',
'PholioReplyHandler' => 'applications/pholio/mail/PholioReplyHandler.php',
'PholioSchemaSpec' => 'applications/pholio/storage/PholioSchemaSpec.php',
'PholioTransaction' => 'applications/pholio/storage/PholioTransaction.php',
'PholioTransactionComment' => 'applications/pholio/storage/PholioTransactionComment.php',
'PholioTransactionQuery' => 'applications/pholio/query/PholioTransactionQuery.php',
'PholioTransactionType' => 'applications/pholio/xaction/PholioTransactionType.php',
'PholioTransactionView' => 'applications/pholio/view/PholioTransactionView.php',
'PholioUploadedImageView' => 'applications/pholio/view/PholioUploadedImageView.php',
'PhortuneAccount' => 'applications/phortune/storage/PhortuneAccount.php',
'PhortuneAccountAddManagerController' => 'applications/phortune/controller/account/PhortuneAccountAddManagerController.php',
+ 'PhortuneAccountBillingAddressTransaction' => 'applications/phortune/xaction/PhortuneAccountBillingAddressTransaction.php',
'PhortuneAccountBillingController' => 'applications/phortune/controller/account/PhortuneAccountBillingController.php',
+ 'PhortuneAccountBillingNameTransaction' => 'applications/phortune/xaction/PhortuneAccountBillingNameTransaction.php',
'PhortuneAccountChargeListController' => 'applications/phortune/controller/account/PhortuneAccountChargeListController.php',
'PhortuneAccountController' => 'applications/phortune/controller/account/PhortuneAccountController.php',
'PhortuneAccountEditController' => 'applications/phortune/controller/account/PhortuneAccountEditController.php',
'PhortuneAccountEditEngine' => 'applications/phortune/editor/PhortuneAccountEditEngine.php',
'PhortuneAccountEditor' => 'applications/phortune/editor/PhortuneAccountEditor.php',
'PhortuneAccountHasMemberEdgeType' => 'applications/phortune/edge/PhortuneAccountHasMemberEdgeType.php',
'PhortuneAccountListController' => 'applications/phortune/controller/account/PhortuneAccountListController.php',
'PhortuneAccountManagerController' => 'applications/phortune/controller/account/PhortuneAccountManagerController.php',
'PhortuneAccountNameTransaction' => 'applications/phortune/xaction/PhortuneAccountNameTransaction.php',
'PhortuneAccountPHIDType' => 'applications/phortune/phid/PhortuneAccountPHIDType.php',
'PhortuneAccountProfileController' => 'applications/phortune/controller/account/PhortuneAccountProfileController.php',
'PhortuneAccountQuery' => 'applications/phortune/query/PhortuneAccountQuery.php',
'PhortuneAccountSubscriptionController' => 'applications/phortune/controller/account/PhortuneAccountSubscriptionController.php',
'PhortuneAccountTransaction' => 'applications/phortune/storage/PhortuneAccountTransaction.php',
'PhortuneAccountTransactionQuery' => 'applications/phortune/query/PhortuneAccountTransactionQuery.php',
'PhortuneAccountTransactionType' => 'applications/phortune/xaction/PhortuneAccountTransactionType.php',
'PhortuneAccountViewController' => 'applications/phortune/controller/account/PhortuneAccountViewController.php',
'PhortuneAdHocCart' => 'applications/phortune/cart/PhortuneAdHocCart.php',
'PhortuneAdHocProduct' => 'applications/phortune/product/PhortuneAdHocProduct.php',
'PhortuneCart' => 'applications/phortune/storage/PhortuneCart.php',
'PhortuneCartAcceptController' => 'applications/phortune/controller/cart/PhortuneCartAcceptController.php',
'PhortuneCartCancelController' => 'applications/phortune/controller/cart/PhortuneCartCancelController.php',
'PhortuneCartCheckoutController' => 'applications/phortune/controller/cart/PhortuneCartCheckoutController.php',
'PhortuneCartController' => 'applications/phortune/controller/cart/PhortuneCartController.php',
'PhortuneCartEditor' => 'applications/phortune/editor/PhortuneCartEditor.php',
'PhortuneCartImplementation' => 'applications/phortune/cart/PhortuneCartImplementation.php',
'PhortuneCartListController' => 'applications/phortune/controller/cart/PhortuneCartListController.php',
'PhortuneCartPHIDType' => 'applications/phortune/phid/PhortuneCartPHIDType.php',
'PhortuneCartQuery' => 'applications/phortune/query/PhortuneCartQuery.php',
'PhortuneCartReplyHandler' => 'applications/phortune/mail/PhortuneCartReplyHandler.php',
'PhortuneCartSearchEngine' => 'applications/phortune/query/PhortuneCartSearchEngine.php',
'PhortuneCartTransaction' => 'applications/phortune/storage/PhortuneCartTransaction.php',
'PhortuneCartTransactionQuery' => 'applications/phortune/query/PhortuneCartTransactionQuery.php',
'PhortuneCartUpdateController' => 'applications/phortune/controller/cart/PhortuneCartUpdateController.php',
'PhortuneCartViewController' => 'applications/phortune/controller/cart/PhortuneCartViewController.php',
'PhortuneCharge' => 'applications/phortune/storage/PhortuneCharge.php',
'PhortuneChargePHIDType' => 'applications/phortune/phid/PhortuneChargePHIDType.php',
'PhortuneChargeQuery' => 'applications/phortune/query/PhortuneChargeQuery.php',
'PhortuneChargeSearchEngine' => 'applications/phortune/query/PhortuneChargeSearchEngine.php',
'PhortuneChargeTableView' => 'applications/phortune/view/PhortuneChargeTableView.php',
'PhortuneConstants' => 'applications/phortune/constants/PhortuneConstants.php',
'PhortuneController' => 'applications/phortune/controller/PhortuneController.php',
'PhortuneCreditCardForm' => 'applications/phortune/view/PhortuneCreditCardForm.php',
'PhortuneCurrency' => 'applications/phortune/currency/PhortuneCurrency.php',
'PhortuneCurrencySerializer' => 'applications/phortune/currency/PhortuneCurrencySerializer.php',
'PhortuneCurrencyTestCase' => 'applications/phortune/currency/__tests__/PhortuneCurrencyTestCase.php',
'PhortuneDAO' => 'applications/phortune/storage/PhortuneDAO.php',
'PhortuneErrCode' => 'applications/phortune/constants/PhortuneErrCode.php',
'PhortuneInvoiceView' => 'applications/phortune/view/PhortuneInvoiceView.php',
'PhortuneLandingController' => 'applications/phortune/controller/PhortuneLandingController.php',
'PhortuneMemberHasAccountEdgeType' => 'applications/phortune/edge/PhortuneMemberHasAccountEdgeType.php',
'PhortuneMemberHasMerchantEdgeType' => 'applications/phortune/edge/PhortuneMemberHasMerchantEdgeType.php',
'PhortuneMerchant' => 'applications/phortune/storage/PhortuneMerchant.php',
'PhortuneMerchantAddManagerController' => 'applications/phortune/controller/merchant/PhortuneMerchantAddManagerController.php',
'PhortuneMerchantCapability' => 'applications/phortune/capability/PhortuneMerchantCapability.php',
'PhortuneMerchantContactInfoTransaction' => 'applications/phortune/xaction/PhortuneMerchantContactInfoTransaction.php',
'PhortuneMerchantController' => 'applications/phortune/controller/merchant/PhortuneMerchantController.php',
'PhortuneMerchantDescriptionTransaction' => 'applications/phortune/xaction/PhortuneMerchantDescriptionTransaction.php',
'PhortuneMerchantEditController' => 'applications/phortune/controller/merchant/PhortuneMerchantEditController.php',
'PhortuneMerchantEditEngine' => 'applications/phortune/editor/PhortuneMerchantEditEngine.php',
'PhortuneMerchantEditor' => 'applications/phortune/editor/PhortuneMerchantEditor.php',
'PhortuneMerchantHasMemberEdgeType' => 'applications/phortune/edge/PhortuneMerchantHasMemberEdgeType.php',
'PhortuneMerchantInvoiceCreateController' => 'applications/phortune/controller/merchant/PhortuneMerchantInvoiceCreateController.php',
'PhortuneMerchantInvoiceEmailTransaction' => 'applications/phortune/xaction/PhortuneMerchantInvoiceEmailTransaction.php',
'PhortuneMerchantInvoiceFooterTransaction' => 'applications/phortune/xaction/PhortuneMerchantInvoiceFooterTransaction.php',
'PhortuneMerchantListController' => 'applications/phortune/controller/merchant/PhortuneMerchantListController.php',
'PhortuneMerchantManagerController' => 'applications/phortune/controller/merchant/PhortuneMerchantManagerController.php',
'PhortuneMerchantNameTransaction' => 'applications/phortune/xaction/PhortuneMerchantNameTransaction.php',
'PhortuneMerchantPHIDType' => 'applications/phortune/phid/PhortuneMerchantPHIDType.php',
'PhortuneMerchantPictureController' => 'applications/phortune/controller/merchant/PhortuneMerchantPictureController.php',
'PhortuneMerchantPictureTransaction' => 'applications/phortune/xaction/PhortuneMerchantPictureTransaction.php',
'PhortuneMerchantProfileController' => 'applications/phortune/controller/merchant/PhortuneMerchantProfileController.php',
'PhortuneMerchantQuery' => 'applications/phortune/query/PhortuneMerchantQuery.php',
'PhortuneMerchantSearchEngine' => 'applications/phortune/query/PhortuneMerchantSearchEngine.php',
'PhortuneMerchantTransaction' => 'applications/phortune/storage/PhortuneMerchantTransaction.php',
'PhortuneMerchantTransactionQuery' => 'applications/phortune/query/PhortuneMerchantTransactionQuery.php',
'PhortuneMerchantTransactionType' => 'applications/phortune/xaction/PhortuneMerchantTransactionType.php',
'PhortuneMerchantViewController' => 'applications/phortune/controller/merchant/PhortuneMerchantViewController.php',
'PhortuneMonthYearExpiryControl' => 'applications/phortune/control/PhortuneMonthYearExpiryControl.php',
'PhortuneOrderTableView' => 'applications/phortune/view/PhortuneOrderTableView.php',
'PhortunePayPalPaymentProvider' => 'applications/phortune/provider/PhortunePayPalPaymentProvider.php',
'PhortunePaymentMethod' => 'applications/phortune/storage/PhortunePaymentMethod.php',
'PhortunePaymentMethodCreateController' => 'applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php',
'PhortunePaymentMethodDisableController' => 'applications/phortune/controller/payment/PhortunePaymentMethodDisableController.php',
'PhortunePaymentMethodEditController' => 'applications/phortune/controller/payment/PhortunePaymentMethodEditController.php',
'PhortunePaymentMethodPHIDType' => 'applications/phortune/phid/PhortunePaymentMethodPHIDType.php',
'PhortunePaymentMethodQuery' => 'applications/phortune/query/PhortunePaymentMethodQuery.php',
'PhortunePaymentProvider' => 'applications/phortune/provider/PhortunePaymentProvider.php',
'PhortunePaymentProviderConfig' => 'applications/phortune/storage/PhortunePaymentProviderConfig.php',
'PhortunePaymentProviderConfigEditor' => 'applications/phortune/editor/PhortunePaymentProviderConfigEditor.php',
'PhortunePaymentProviderConfigQuery' => 'applications/phortune/query/PhortunePaymentProviderConfigQuery.php',
'PhortunePaymentProviderConfigTransaction' => 'applications/phortune/storage/PhortunePaymentProviderConfigTransaction.php',
'PhortunePaymentProviderConfigTransactionQuery' => 'applications/phortune/query/PhortunePaymentProviderConfigTransactionQuery.php',
'PhortunePaymentProviderPHIDType' => 'applications/phortune/phid/PhortunePaymentProviderPHIDType.php',
'PhortunePaymentProviderTestCase' => 'applications/phortune/provider/__tests__/PhortunePaymentProviderTestCase.php',
'PhortuneProduct' => 'applications/phortune/storage/PhortuneProduct.php',
'PhortuneProductImplementation' => 'applications/phortune/product/PhortuneProductImplementation.php',
'PhortuneProductListController' => 'applications/phortune/controller/product/PhortuneProductListController.php',
'PhortuneProductPHIDType' => 'applications/phortune/phid/PhortuneProductPHIDType.php',
'PhortuneProductQuery' => 'applications/phortune/query/PhortuneProductQuery.php',
'PhortuneProductViewController' => 'applications/phortune/controller/product/PhortuneProductViewController.php',
'PhortuneProviderActionController' => 'applications/phortune/controller/provider/PhortuneProviderActionController.php',
'PhortuneProviderDisableController' => 'applications/phortune/controller/provider/PhortuneProviderDisableController.php',
'PhortuneProviderEditController' => 'applications/phortune/controller/provider/PhortuneProviderEditController.php',
'PhortunePurchase' => 'applications/phortune/storage/PhortunePurchase.php',
'PhortunePurchasePHIDType' => 'applications/phortune/phid/PhortunePurchasePHIDType.php',
'PhortunePurchaseQuery' => 'applications/phortune/query/PhortunePurchaseQuery.php',
'PhortuneSchemaSpec' => 'applications/phortune/storage/PhortuneSchemaSpec.php',
'PhortuneStripePaymentProvider' => 'applications/phortune/provider/PhortuneStripePaymentProvider.php',
'PhortuneSubscription' => 'applications/phortune/storage/PhortuneSubscription.php',
'PhortuneSubscriptionCart' => 'applications/phortune/cart/PhortuneSubscriptionCart.php',
'PhortuneSubscriptionEditController' => 'applications/phortune/controller/subscription/PhortuneSubscriptionEditController.php',
'PhortuneSubscriptionImplementation' => 'applications/phortune/subscription/PhortuneSubscriptionImplementation.php',
'PhortuneSubscriptionListController' => 'applications/phortune/controller/subscription/PhortuneSubscriptionListController.php',
'PhortuneSubscriptionPHIDType' => 'applications/phortune/phid/PhortuneSubscriptionPHIDType.php',
'PhortuneSubscriptionProduct' => 'applications/phortune/product/PhortuneSubscriptionProduct.php',
'PhortuneSubscriptionQuery' => 'applications/phortune/query/PhortuneSubscriptionQuery.php',
'PhortuneSubscriptionSearchEngine' => 'applications/phortune/query/PhortuneSubscriptionSearchEngine.php',
'PhortuneSubscriptionTableView' => 'applications/phortune/view/PhortuneSubscriptionTableView.php',
'PhortuneSubscriptionViewController' => 'applications/phortune/controller/subscription/PhortuneSubscriptionViewController.php',
'PhortuneSubscriptionWorker' => 'applications/phortune/worker/PhortuneSubscriptionWorker.php',
'PhortuneTestPaymentProvider' => 'applications/phortune/provider/PhortuneTestPaymentProvider.php',
'PhortuneWePayPaymentProvider' => 'applications/phortune/provider/PhortuneWePayPaymentProvider.php',
'PhragmentBrowseController' => 'applications/phragment/controller/PhragmentBrowseController.php',
'PhragmentCanCreateCapability' => 'applications/phragment/capability/PhragmentCanCreateCapability.php',
'PhragmentConduitAPIMethod' => 'applications/phragment/conduit/PhragmentConduitAPIMethod.php',
'PhragmentController' => 'applications/phragment/controller/PhragmentController.php',
'PhragmentCreateController' => 'applications/phragment/controller/PhragmentCreateController.php',
'PhragmentDAO' => 'applications/phragment/storage/PhragmentDAO.php',
'PhragmentFragment' => 'applications/phragment/storage/PhragmentFragment.php',
'PhragmentFragmentPHIDType' => 'applications/phragment/phid/PhragmentFragmentPHIDType.php',
'PhragmentFragmentQuery' => 'applications/phragment/query/PhragmentFragmentQuery.php',
'PhragmentFragmentVersion' => 'applications/phragment/storage/PhragmentFragmentVersion.php',
'PhragmentFragmentVersionPHIDType' => 'applications/phragment/phid/PhragmentFragmentVersionPHIDType.php',
'PhragmentFragmentVersionQuery' => 'applications/phragment/query/PhragmentFragmentVersionQuery.php',
'PhragmentGetPatchConduitAPIMethod' => 'applications/phragment/conduit/PhragmentGetPatchConduitAPIMethod.php',
'PhragmentHistoryController' => 'applications/phragment/controller/PhragmentHistoryController.php',
'PhragmentPatchController' => 'applications/phragment/controller/PhragmentPatchController.php',
'PhragmentPatchUtil' => 'applications/phragment/util/PhragmentPatchUtil.php',
'PhragmentPolicyController' => 'applications/phragment/controller/PhragmentPolicyController.php',
'PhragmentQueryFragmentsConduitAPIMethod' => 'applications/phragment/conduit/PhragmentQueryFragmentsConduitAPIMethod.php',
'PhragmentRevertController' => 'applications/phragment/controller/PhragmentRevertController.php',
'PhragmentSchemaSpec' => 'applications/phragment/storage/PhragmentSchemaSpec.php',
'PhragmentSnapshot' => 'applications/phragment/storage/PhragmentSnapshot.php',
'PhragmentSnapshotChild' => 'applications/phragment/storage/PhragmentSnapshotChild.php',
'PhragmentSnapshotChildQuery' => 'applications/phragment/query/PhragmentSnapshotChildQuery.php',
'PhragmentSnapshotCreateController' => 'applications/phragment/controller/PhragmentSnapshotCreateController.php',
'PhragmentSnapshotDeleteController' => 'applications/phragment/controller/PhragmentSnapshotDeleteController.php',
'PhragmentSnapshotPHIDType' => 'applications/phragment/phid/PhragmentSnapshotPHIDType.php',
'PhragmentSnapshotPromoteController' => 'applications/phragment/controller/PhragmentSnapshotPromoteController.php',
'PhragmentSnapshotQuery' => 'applications/phragment/query/PhragmentSnapshotQuery.php',
'PhragmentSnapshotViewController' => 'applications/phragment/controller/PhragmentSnapshotViewController.php',
'PhragmentUpdateController' => 'applications/phragment/controller/PhragmentUpdateController.php',
'PhragmentVersionController' => 'applications/phragment/controller/PhragmentVersionController.php',
'PhragmentZIPController' => 'applications/phragment/controller/PhragmentZIPController.php',
'PhrequentConduitAPIMethod' => 'applications/phrequent/conduit/PhrequentConduitAPIMethod.php',
'PhrequentController' => 'applications/phrequent/controller/PhrequentController.php',
'PhrequentCurtainExtension' => 'applications/phrequent/engineextension/PhrequentCurtainExtension.php',
'PhrequentDAO' => 'applications/phrequent/storage/PhrequentDAO.php',
'PhrequentListController' => 'applications/phrequent/controller/PhrequentListController.php',
'PhrequentPopConduitAPIMethod' => 'applications/phrequent/conduit/PhrequentPopConduitAPIMethod.php',
'PhrequentPushConduitAPIMethod' => 'applications/phrequent/conduit/PhrequentPushConduitAPIMethod.php',
'PhrequentSearchEngine' => 'applications/phrequent/query/PhrequentSearchEngine.php',
'PhrequentTimeBlock' => 'applications/phrequent/storage/PhrequentTimeBlock.php',
'PhrequentTimeBlockTestCase' => 'applications/phrequent/storage/__tests__/PhrequentTimeBlockTestCase.php',
'PhrequentTimeSlices' => 'applications/phrequent/storage/PhrequentTimeSlices.php',
'PhrequentTrackController' => 'applications/phrequent/controller/PhrequentTrackController.php',
'PhrequentTrackableInterface' => 'applications/phrequent/interface/PhrequentTrackableInterface.php',
'PhrequentTrackingConduitAPIMethod' => 'applications/phrequent/conduit/PhrequentTrackingConduitAPIMethod.php',
'PhrequentTrackingEditor' => 'applications/phrequent/editor/PhrequentTrackingEditor.php',
'PhrequentUIEventListener' => 'applications/phrequent/event/PhrequentUIEventListener.php',
'PhrequentUserTime' => 'applications/phrequent/storage/PhrequentUserTime.php',
'PhrequentUserTimeQuery' => 'applications/phrequent/query/PhrequentUserTimeQuery.php',
'PhrictionChangeType' => 'applications/phriction/constants/PhrictionChangeType.php',
'PhrictionConduitAPIMethod' => 'applications/phriction/conduit/PhrictionConduitAPIMethod.php',
'PhrictionConstants' => 'applications/phriction/constants/PhrictionConstants.php',
'PhrictionContent' => 'applications/phriction/storage/PhrictionContent.php',
'PhrictionContentPHIDType' => 'applications/phriction/phid/PhrictionContentPHIDType.php',
'PhrictionContentQuery' => 'applications/phriction/query/PhrictionContentQuery.php',
'PhrictionContentSearchConduitAPIMethod' => 'applications/phriction/conduit/PhrictionContentSearchConduitAPIMethod.php',
'PhrictionContentSearchEngine' => 'applications/phriction/query/PhrictionContentSearchEngine.php',
'PhrictionContentSearchEngineAttachment' => 'applications/phriction/engineextension/PhrictionContentSearchEngineAttachment.php',
'PhrictionController' => 'applications/phriction/controller/PhrictionController.php',
'PhrictionCreateConduitAPIMethod' => 'applications/phriction/conduit/PhrictionCreateConduitAPIMethod.php',
'PhrictionDAO' => 'applications/phriction/storage/PhrictionDAO.php',
'PhrictionDatasourceEngineExtension' => 'applications/phriction/engineextension/PhrictionDatasourceEngineExtension.php',
'PhrictionDeleteController' => 'applications/phriction/controller/PhrictionDeleteController.php',
'PhrictionDiffController' => 'applications/phriction/controller/PhrictionDiffController.php',
'PhrictionDocument' => 'applications/phriction/storage/PhrictionDocument.php',
'PhrictionDocumentAuthorHeraldField' => 'applications/phriction/herald/PhrictionDocumentAuthorHeraldField.php',
'PhrictionDocumentContentHeraldField' => 'applications/phriction/herald/PhrictionDocumentContentHeraldField.php',
'PhrictionDocumentContentTransaction' => 'applications/phriction/xaction/PhrictionDocumentContentTransaction.php',
'PhrictionDocumentController' => 'applications/phriction/controller/PhrictionDocumentController.php',
'PhrictionDocumentDatasource' => 'applications/phriction/typeahead/PhrictionDocumentDatasource.php',
'PhrictionDocumentDeleteTransaction' => 'applications/phriction/xaction/PhrictionDocumentDeleteTransaction.php',
'PhrictionDocumentDraftTransaction' => 'applications/phriction/xaction/PhrictionDocumentDraftTransaction.php',
'PhrictionDocumentEditEngine' => 'applications/phriction/editor/PhrictionDocumentEditEngine.php',
'PhrictionDocumentEditTransaction' => 'applications/phriction/xaction/PhrictionDocumentEditTransaction.php',
'PhrictionDocumentFerretEngine' => 'applications/phriction/search/PhrictionDocumentFerretEngine.php',
'PhrictionDocumentFulltextEngine' => 'applications/phriction/search/PhrictionDocumentFulltextEngine.php',
'PhrictionDocumentHeraldAdapter' => 'applications/phriction/herald/PhrictionDocumentHeraldAdapter.php',
'PhrictionDocumentHeraldField' => 'applications/phriction/herald/PhrictionDocumentHeraldField.php',
'PhrictionDocumentHeraldFieldGroup' => 'applications/phriction/herald/PhrictionDocumentHeraldFieldGroup.php',
'PhrictionDocumentMoveAwayTransaction' => 'applications/phriction/xaction/PhrictionDocumentMoveAwayTransaction.php',
'PhrictionDocumentMoveToTransaction' => 'applications/phriction/xaction/PhrictionDocumentMoveToTransaction.php',
'PhrictionDocumentPHIDType' => 'applications/phriction/phid/PhrictionDocumentPHIDType.php',
'PhrictionDocumentPathHeraldField' => 'applications/phriction/herald/PhrictionDocumentPathHeraldField.php',
'PhrictionDocumentPolicyCodex' => 'applications/phriction/codex/PhrictionDocumentPolicyCodex.php',
'PhrictionDocumentPublishTransaction' => 'applications/phriction/xaction/PhrictionDocumentPublishTransaction.php',
'PhrictionDocumentQuery' => 'applications/phriction/query/PhrictionDocumentQuery.php',
'PhrictionDocumentSearchConduitAPIMethod' => 'applications/phriction/conduit/PhrictionDocumentSearchConduitAPIMethod.php',
'PhrictionDocumentSearchEngine' => 'applications/phriction/query/PhrictionDocumentSearchEngine.php',
'PhrictionDocumentStatus' => 'applications/phriction/constants/PhrictionDocumentStatus.php',
'PhrictionDocumentTitleHeraldField' => 'applications/phriction/herald/PhrictionDocumentTitleHeraldField.php',
'PhrictionDocumentTitleTransaction' => 'applications/phriction/xaction/PhrictionDocumentTitleTransaction.php',
'PhrictionDocumentTransactionType' => 'applications/phriction/xaction/PhrictionDocumentTransactionType.php',
'PhrictionDocumentVersionTransaction' => 'applications/phriction/xaction/PhrictionDocumentVersionTransaction.php',
'PhrictionEditConduitAPIMethod' => 'applications/phriction/conduit/PhrictionEditConduitAPIMethod.php',
'PhrictionEditController' => 'applications/phriction/controller/PhrictionEditController.php',
'PhrictionEditEngineController' => 'applications/phriction/controller/PhrictionEditEngineController.php',
'PhrictionHistoryConduitAPIMethod' => 'applications/phriction/conduit/PhrictionHistoryConduitAPIMethod.php',
'PhrictionHistoryController' => 'applications/phriction/controller/PhrictionHistoryController.php',
'PhrictionInfoConduitAPIMethod' => 'applications/phriction/conduit/PhrictionInfoConduitAPIMethod.php',
'PhrictionListController' => 'applications/phriction/controller/PhrictionListController.php',
'PhrictionMarkupPreviewController' => 'applications/phriction/controller/PhrictionMarkupPreviewController.php',
'PhrictionMoveController' => 'applications/phriction/controller/PhrictionMoveController.php',
'PhrictionNewController' => 'applications/phriction/controller/PhrictionNewController.php',
'PhrictionPublishController' => 'applications/phriction/controller/PhrictionPublishController.php',
'PhrictionRemarkupRule' => 'applications/phriction/markup/PhrictionRemarkupRule.php',
'PhrictionReplyHandler' => 'applications/phriction/mail/PhrictionReplyHandler.php',
'PhrictionSchemaSpec' => 'applications/phriction/storage/PhrictionSchemaSpec.php',
'PhrictionTransaction' => 'applications/phriction/storage/PhrictionTransaction.php',
'PhrictionTransactionComment' => 'applications/phriction/storage/PhrictionTransactionComment.php',
'PhrictionTransactionEditor' => 'applications/phriction/editor/PhrictionTransactionEditor.php',
'PhrictionTransactionQuery' => 'applications/phriction/query/PhrictionTransactionQuery.php',
'PolicyLockOptionType' => 'applications/policy/config/PolicyLockOptionType.php',
'PonderAddAnswerView' => 'applications/ponder/view/PonderAddAnswerView.php',
'PonderAnswer' => 'applications/ponder/storage/PonderAnswer.php',
'PonderAnswerCommentController' => 'applications/ponder/controller/PonderAnswerCommentController.php',
'PonderAnswerContentTransaction' => 'applications/ponder/xaction/PonderAnswerContentTransaction.php',
'PonderAnswerEditController' => 'applications/ponder/controller/PonderAnswerEditController.php',
'PonderAnswerEditor' => 'applications/ponder/editor/PonderAnswerEditor.php',
'PonderAnswerHistoryController' => 'applications/ponder/controller/PonderAnswerHistoryController.php',
'PonderAnswerMailReceiver' => 'applications/ponder/mail/PonderAnswerMailReceiver.php',
'PonderAnswerPHIDType' => 'applications/ponder/phid/PonderAnswerPHIDType.php',
'PonderAnswerQuery' => 'applications/ponder/query/PonderAnswerQuery.php',
'PonderAnswerQuestionIDTransaction' => 'applications/ponder/xaction/PonderAnswerQuestionIDTransaction.php',
'PonderAnswerReplyHandler' => 'applications/ponder/mail/PonderAnswerReplyHandler.php',
'PonderAnswerSaveController' => 'applications/ponder/controller/PonderAnswerSaveController.php',
'PonderAnswerStatus' => 'applications/ponder/constants/PonderAnswerStatus.php',
'PonderAnswerStatusTransaction' => 'applications/ponder/xaction/PonderAnswerStatusTransaction.php',
'PonderAnswerTransaction' => 'applications/ponder/storage/PonderAnswerTransaction.php',
'PonderAnswerTransactionComment' => 'applications/ponder/storage/PonderAnswerTransactionComment.php',
'PonderAnswerTransactionQuery' => 'applications/ponder/query/PonderAnswerTransactionQuery.php',
'PonderAnswerTransactionType' => 'applications/ponder/xaction/PonderAnswerTransactionType.php',
'PonderAnswerView' => 'applications/ponder/view/PonderAnswerView.php',
'PonderConstants' => 'applications/ponder/constants/PonderConstants.php',
'PonderController' => 'applications/ponder/controller/PonderController.php',
'PonderDAO' => 'applications/ponder/storage/PonderDAO.php',
'PonderDefaultViewCapability' => 'applications/ponder/capability/PonderDefaultViewCapability.php',
'PonderEditor' => 'applications/ponder/editor/PonderEditor.php',
'PonderFooterView' => 'applications/ponder/view/PonderFooterView.php',
'PonderModerateCapability' => 'applications/ponder/capability/PonderModerateCapability.php',
'PonderQuestion' => 'applications/ponder/storage/PonderQuestion.php',
'PonderQuestionAnswerTransaction' => 'applications/ponder/xaction/PonderQuestionAnswerTransaction.php',
'PonderQuestionAnswerWikiTransaction' => 'applications/ponder/xaction/PonderQuestionAnswerWikiTransaction.php',
'PonderQuestionCommentController' => 'applications/ponder/controller/PonderQuestionCommentController.php',
'PonderQuestionContentTransaction' => 'applications/ponder/xaction/PonderQuestionContentTransaction.php',
'PonderQuestionCreateMailReceiver' => 'applications/ponder/mail/PonderQuestionCreateMailReceiver.php',
'PonderQuestionEditController' => 'applications/ponder/controller/PonderQuestionEditController.php',
'PonderQuestionEditEngine' => 'applications/ponder/editor/PonderQuestionEditEngine.php',
'PonderQuestionEditor' => 'applications/ponder/editor/PonderQuestionEditor.php',
'PonderQuestionFerretEngine' => 'applications/ponder/search/PonderQuestionFerretEngine.php',
'PonderQuestionFulltextEngine' => 'applications/ponder/search/PonderQuestionFulltextEngine.php',
'PonderQuestionHistoryController' => 'applications/ponder/controller/PonderQuestionHistoryController.php',
'PonderQuestionListController' => 'applications/ponder/controller/PonderQuestionListController.php',
'PonderQuestionMailReceiver' => 'applications/ponder/mail/PonderQuestionMailReceiver.php',
'PonderQuestionPHIDType' => 'applications/ponder/phid/PonderQuestionPHIDType.php',
'PonderQuestionQuery' => 'applications/ponder/query/PonderQuestionQuery.php',
'PonderQuestionReplyHandler' => 'applications/ponder/mail/PonderQuestionReplyHandler.php',
'PonderQuestionSearchEngine' => 'applications/ponder/query/PonderQuestionSearchEngine.php',
'PonderQuestionStatus' => 'applications/ponder/constants/PonderQuestionStatus.php',
'PonderQuestionStatusController' => 'applications/ponder/controller/PonderQuestionStatusController.php',
'PonderQuestionStatusTransaction' => 'applications/ponder/xaction/PonderQuestionStatusTransaction.php',
'PonderQuestionTitleTransaction' => 'applications/ponder/xaction/PonderQuestionTitleTransaction.php',
'PonderQuestionTransaction' => 'applications/ponder/storage/PonderQuestionTransaction.php',
'PonderQuestionTransactionComment' => 'applications/ponder/storage/PonderQuestionTransactionComment.php',
'PonderQuestionTransactionQuery' => 'applications/ponder/query/PonderQuestionTransactionQuery.php',
'PonderQuestionTransactionType' => 'applications/ponder/xaction/PonderQuestionTransactionType.php',
'PonderQuestionViewController' => 'applications/ponder/controller/PonderQuestionViewController.php',
'PonderRemarkupRule' => 'applications/ponder/remarkup/PonderRemarkupRule.php',
'PonderSchemaSpec' => 'applications/ponder/storage/PonderSchemaSpec.php',
'ProjectAddProjectsEmailCommand' => 'applications/project/command/ProjectAddProjectsEmailCommand.php',
'ProjectBoardTaskCard' => 'applications/project/view/ProjectBoardTaskCard.php',
'ProjectCanLockProjectsCapability' => 'applications/project/capability/ProjectCanLockProjectsCapability.php',
'ProjectColumnSearchConduitAPIMethod' => 'applications/project/conduit/ProjectColumnSearchConduitAPIMethod.php',
'ProjectConduitAPIMethod' => 'applications/project/conduit/ProjectConduitAPIMethod.php',
'ProjectCreateConduitAPIMethod' => 'applications/project/conduit/ProjectCreateConduitAPIMethod.php',
'ProjectCreateProjectsCapability' => 'applications/project/capability/ProjectCreateProjectsCapability.php',
'ProjectDatasourceEngineExtension' => 'applications/project/engineextension/ProjectDatasourceEngineExtension.php',
'ProjectDefaultEditCapability' => 'applications/project/capability/ProjectDefaultEditCapability.php',
'ProjectDefaultJoinCapability' => 'applications/project/capability/ProjectDefaultJoinCapability.php',
'ProjectDefaultViewCapability' => 'applications/project/capability/ProjectDefaultViewCapability.php',
'ProjectEditConduitAPIMethod' => 'applications/project/conduit/ProjectEditConduitAPIMethod.php',
'ProjectQueryConduitAPIMethod' => 'applications/project/conduit/ProjectQueryConduitAPIMethod.php',
'ProjectRemarkupRule' => 'applications/project/remarkup/ProjectRemarkupRule.php',
'ProjectRemarkupRuleTestCase' => 'applications/project/remarkup/__tests__/ProjectRemarkupRuleTestCase.php',
'ProjectReplyHandler' => 'applications/project/mail/ProjectReplyHandler.php',
'ProjectSearchConduitAPIMethod' => 'applications/project/conduit/ProjectSearchConduitAPIMethod.php',
'QueryFormattingTestCase' => 'infrastructure/storage/__tests__/QueryFormattingTestCase.php',
'ReleephAuthorFieldSpecification' => 'applications/releeph/field/specification/ReleephAuthorFieldSpecification.php',
'ReleephBranch' => 'applications/releeph/storage/ReleephBranch.php',
'ReleephBranchAccessController' => 'applications/releeph/controller/branch/ReleephBranchAccessController.php',
'ReleephBranchCommitFieldSpecification' => 'applications/releeph/field/specification/ReleephBranchCommitFieldSpecification.php',
'ReleephBranchController' => 'applications/releeph/controller/branch/ReleephBranchController.php',
'ReleephBranchCreateController' => 'applications/releeph/controller/branch/ReleephBranchCreateController.php',
'ReleephBranchEditController' => 'applications/releeph/controller/branch/ReleephBranchEditController.php',
'ReleephBranchEditor' => 'applications/releeph/editor/ReleephBranchEditor.php',
'ReleephBranchHistoryController' => 'applications/releeph/controller/branch/ReleephBranchHistoryController.php',
'ReleephBranchNamePreviewController' => 'applications/releeph/controller/branch/ReleephBranchNamePreviewController.php',
'ReleephBranchPHIDType' => 'applications/releeph/phid/ReleephBranchPHIDType.php',
'ReleephBranchPreviewView' => 'applications/releeph/view/branch/ReleephBranchPreviewView.php',
'ReleephBranchQuery' => 'applications/releeph/query/ReleephBranchQuery.php',
'ReleephBranchSearchEngine' => 'applications/releeph/query/ReleephBranchSearchEngine.php',
'ReleephBranchTemplate' => 'applications/releeph/view/branch/ReleephBranchTemplate.php',
'ReleephBranchTransaction' => 'applications/releeph/storage/ReleephBranchTransaction.php',
'ReleephBranchTransactionQuery' => 'applications/releeph/query/ReleephBranchTransactionQuery.php',
'ReleephBranchViewController' => 'applications/releeph/controller/branch/ReleephBranchViewController.php',
'ReleephCommitFinder' => 'applications/releeph/commitfinder/ReleephCommitFinder.php',
'ReleephCommitFinderException' => 'applications/releeph/commitfinder/ReleephCommitFinderException.php',
'ReleephCommitMessageFieldSpecification' => 'applications/releeph/field/specification/ReleephCommitMessageFieldSpecification.php',
'ReleephConduitAPIMethod' => 'applications/releeph/conduit/ReleephConduitAPIMethod.php',
'ReleephController' => 'applications/releeph/controller/ReleephController.php',
'ReleephDAO' => 'applications/releeph/storage/ReleephDAO.php',
'ReleephDefaultFieldSelector' => 'applications/releeph/field/selector/ReleephDefaultFieldSelector.php',
'ReleephDependsOnFieldSpecification' => 'applications/releeph/field/specification/ReleephDependsOnFieldSpecification.php',
'ReleephDiffChurnFieldSpecification' => 'applications/releeph/field/specification/ReleephDiffChurnFieldSpecification.php',
'ReleephDiffMessageFieldSpecification' => 'applications/releeph/field/specification/ReleephDiffMessageFieldSpecification.php',
'ReleephDiffSizeFieldSpecification' => 'applications/releeph/field/specification/ReleephDiffSizeFieldSpecification.php',
'ReleephFieldParseException' => 'applications/releeph/field/exception/ReleephFieldParseException.php',
'ReleephFieldSelector' => 'applications/releeph/field/selector/ReleephFieldSelector.php',
'ReleephFieldSpecification' => 'applications/releeph/field/specification/ReleephFieldSpecification.php',
'ReleephGetBranchesConduitAPIMethod' => 'applications/releeph/conduit/ReleephGetBranchesConduitAPIMethod.php',
'ReleephIntentFieldSpecification' => 'applications/releeph/field/specification/ReleephIntentFieldSpecification.php',
'ReleephLevelFieldSpecification' => 'applications/releeph/field/specification/ReleephLevelFieldSpecification.php',
'ReleephOriginalCommitFieldSpecification' => 'applications/releeph/field/specification/ReleephOriginalCommitFieldSpecification.php',
'ReleephProductActionController' => 'applications/releeph/controller/product/ReleephProductActionController.php',
'ReleephProductController' => 'applications/releeph/controller/product/ReleephProductController.php',
'ReleephProductCreateController' => 'applications/releeph/controller/product/ReleephProductCreateController.php',
'ReleephProductEditController' => 'applications/releeph/controller/product/ReleephProductEditController.php',
'ReleephProductEditor' => 'applications/releeph/editor/ReleephProductEditor.php',
'ReleephProductHistoryController' => 'applications/releeph/controller/product/ReleephProductHistoryController.php',
'ReleephProductListController' => 'applications/releeph/controller/product/ReleephProductListController.php',
'ReleephProductPHIDType' => 'applications/releeph/phid/ReleephProductPHIDType.php',
'ReleephProductQuery' => 'applications/releeph/query/ReleephProductQuery.php',
'ReleephProductSearchEngine' => 'applications/releeph/query/ReleephProductSearchEngine.php',
'ReleephProductTransaction' => 'applications/releeph/storage/ReleephProductTransaction.php',
'ReleephProductTransactionQuery' => 'applications/releeph/query/ReleephProductTransactionQuery.php',
'ReleephProductViewController' => 'applications/releeph/controller/product/ReleephProductViewController.php',
'ReleephProject' => 'applications/releeph/storage/ReleephProject.php',
'ReleephQueryBranchesConduitAPIMethod' => 'applications/releeph/conduit/ReleephQueryBranchesConduitAPIMethod.php',
'ReleephQueryProductsConduitAPIMethod' => 'applications/releeph/conduit/ReleephQueryProductsConduitAPIMethod.php',
'ReleephQueryRequestsConduitAPIMethod' => 'applications/releeph/conduit/ReleephQueryRequestsConduitAPIMethod.php',
'ReleephReasonFieldSpecification' => 'applications/releeph/field/specification/ReleephReasonFieldSpecification.php',
'ReleephRequest' => 'applications/releeph/storage/ReleephRequest.php',
'ReleephRequestActionController' => 'applications/releeph/controller/request/ReleephRequestActionController.php',
'ReleephRequestCommentController' => 'applications/releeph/controller/request/ReleephRequestCommentController.php',
'ReleephRequestConduitAPIMethod' => 'applications/releeph/conduit/ReleephRequestConduitAPIMethod.php',
'ReleephRequestController' => 'applications/releeph/controller/request/ReleephRequestController.php',
'ReleephRequestDifferentialCreateController' => 'applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php',
'ReleephRequestEditController' => 'applications/releeph/controller/request/ReleephRequestEditController.php',
'ReleephRequestMailReceiver' => 'applications/releeph/mail/ReleephRequestMailReceiver.php',
'ReleephRequestPHIDType' => 'applications/releeph/phid/ReleephRequestPHIDType.php',
'ReleephRequestQuery' => 'applications/releeph/query/ReleephRequestQuery.php',
'ReleephRequestReplyHandler' => 'applications/releeph/mail/ReleephRequestReplyHandler.php',
'ReleephRequestSearchEngine' => 'applications/releeph/query/ReleephRequestSearchEngine.php',
'ReleephRequestStatus' => 'applications/releeph/constants/ReleephRequestStatus.php',
'ReleephRequestTransaction' => 'applications/releeph/storage/ReleephRequestTransaction.php',
'ReleephRequestTransactionComment' => 'applications/releeph/storage/ReleephRequestTransactionComment.php',
'ReleephRequestTransactionQuery' => 'applications/releeph/query/ReleephRequestTransactionQuery.php',
'ReleephRequestTransactionalEditor' => 'applications/releeph/editor/ReleephRequestTransactionalEditor.php',
'ReleephRequestTypeaheadControl' => 'applications/releeph/view/request/ReleephRequestTypeaheadControl.php',
'ReleephRequestTypeaheadController' => 'applications/releeph/controller/request/ReleephRequestTypeaheadController.php',
'ReleephRequestView' => 'applications/releeph/view/ReleephRequestView.php',
'ReleephRequestViewController' => 'applications/releeph/controller/request/ReleephRequestViewController.php',
'ReleephRequestorFieldSpecification' => 'applications/releeph/field/specification/ReleephRequestorFieldSpecification.php',
'ReleephRevisionFieldSpecification' => 'applications/releeph/field/specification/ReleephRevisionFieldSpecification.php',
'ReleephSeverityFieldSpecification' => 'applications/releeph/field/specification/ReleephSeverityFieldSpecification.php',
'ReleephSummaryFieldSpecification' => 'applications/releeph/field/specification/ReleephSummaryFieldSpecification.php',
'ReleephWorkCanPushConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkCanPushConduitAPIMethod.php',
'ReleephWorkGetAuthorInfoConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkGetAuthorInfoConduitAPIMethod.php',
'ReleephWorkGetBranchCommitMessageConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkGetBranchCommitMessageConduitAPIMethod.php',
'ReleephWorkGetBranchConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkGetBranchConduitAPIMethod.php',
'ReleephWorkGetCommitMessageConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkGetCommitMessageConduitAPIMethod.php',
'ReleephWorkNextRequestConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkNextRequestConduitAPIMethod.php',
'ReleephWorkRecordConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkRecordConduitAPIMethod.php',
'ReleephWorkRecordPickStatusConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkRecordPickStatusConduitAPIMethod.php',
'RemarkupProcessConduitAPIMethod' => 'applications/remarkup/conduit/RemarkupProcessConduitAPIMethod.php',
'RepositoryConduitAPIMethod' => 'applications/repository/conduit/RepositoryConduitAPIMethod.php',
'RepositoryQueryConduitAPIMethod' => 'applications/repository/conduit/RepositoryQueryConduitAPIMethod.php',
'ShellLogView' => 'applications/harbormaster/view/ShellLogView.php',
'SlowvoteConduitAPIMethod' => 'applications/slowvote/conduit/SlowvoteConduitAPIMethod.php',
'SlowvoteEmbedView' => 'applications/slowvote/view/SlowvoteEmbedView.php',
'SlowvoteInfoConduitAPIMethod' => 'applications/slowvote/conduit/SlowvoteInfoConduitAPIMethod.php',
'SlowvoteRemarkupRule' => 'applications/slowvote/remarkup/SlowvoteRemarkupRule.php',
'SubscriptionListDialogBuilder' => 'applications/subscriptions/view/SubscriptionListDialogBuilder.php',
'SubscriptionListStringBuilder' => 'applications/subscriptions/view/SubscriptionListStringBuilder.php',
'TokenConduitAPIMethod' => 'applications/tokens/conduit/TokenConduitAPIMethod.php',
'TokenGiveConduitAPIMethod' => 'applications/tokens/conduit/TokenGiveConduitAPIMethod.php',
'TokenGivenConduitAPIMethod' => 'applications/tokens/conduit/TokenGivenConduitAPIMethod.php',
'TokenQueryConduitAPIMethod' => 'applications/tokens/conduit/TokenQueryConduitAPIMethod.php',
'TransactionSearchConduitAPIMethod' => 'applications/transactions/conduit/TransactionSearchConduitAPIMethod.php',
'UserConduitAPIMethod' => 'applications/people/conduit/UserConduitAPIMethod.php',
'UserDisableConduitAPIMethod' => 'applications/people/conduit/UserDisableConduitAPIMethod.php',
'UserEditConduitAPIMethod' => 'applications/people/conduit/UserEditConduitAPIMethod.php',
'UserEnableConduitAPIMethod' => 'applications/people/conduit/UserEnableConduitAPIMethod.php',
'UserFindConduitAPIMethod' => 'applications/people/conduit/UserFindConduitAPIMethod.php',
'UserQueryConduitAPIMethod' => 'applications/people/conduit/UserQueryConduitAPIMethod.php',
'UserSearchConduitAPIMethod' => 'applications/people/conduit/UserSearchConduitAPIMethod.php',
'UserWhoAmIConduitAPIMethod' => 'applications/people/conduit/UserWhoAmIConduitAPIMethod.php',
),
'function' => array(
'celerity_generate_unique_node_id' => 'applications/celerity/api.php',
'celerity_get_resource_uri' => 'applications/celerity/api.php',
'javelin_tag' => 'infrastructure/javelin/markup.php',
'phabricator_date' => 'view/viewutils.php',
'phabricator_datetime' => 'view/viewutils.php',
'phabricator_datetimezone' => 'view/viewutils.php',
'phabricator_form' => 'infrastructure/javelin/markup.php',
'phabricator_format_local_time' => 'view/viewutils.php',
'phabricator_relative_date' => 'view/viewutils.php',
'phabricator_time' => 'view/viewutils.php',
'phid_get_subtype' => 'applications/phid/utils.php',
'phid_get_type' => 'applications/phid/utils.php',
'phid_group_by_type' => 'applications/phid/utils.php',
'require_celerity_resource' => 'applications/celerity/api.php',
),
'xmap' => array(
'AlmanacAddress' => 'Phobject',
'AlmanacBinding' => array(
'AlmanacDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'AlmanacPropertyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorExtendedPolicyInterface',
'PhabricatorConduitResultInterface',
),
'AlmanacBindingDeletePropertyTransaction' => 'AlmanacBindingTransactionType',
'AlmanacBindingDisableController' => 'AlmanacServiceController',
'AlmanacBindingDisableTransaction' => 'AlmanacBindingTransactionType',
'AlmanacBindingEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'AlmanacBindingEditController' => 'AlmanacServiceController',
'AlmanacBindingEditEngine' => 'PhabricatorEditEngine',
'AlmanacBindingEditor' => 'AlmanacEditor',
'AlmanacBindingInterfaceTransaction' => 'AlmanacBindingTransactionType',
'AlmanacBindingPHIDType' => 'PhabricatorPHIDType',
'AlmanacBindingPropertyEditEngine' => 'AlmanacPropertyEditEngine',
'AlmanacBindingQuery' => 'AlmanacQuery',
'AlmanacBindingSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'AlmanacBindingSearchEngine' => 'PhabricatorApplicationSearchEngine',
'AlmanacBindingServiceTransaction' => 'AlmanacBindingTransactionType',
'AlmanacBindingSetPropertyTransaction' => 'AlmanacBindingTransactionType',
'AlmanacBindingTableView' => 'AphrontView',
'AlmanacBindingTransaction' => 'AlmanacModularTransaction',
'AlmanacBindingTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'AlmanacBindingTransactionType' => 'AlmanacTransactionType',
'AlmanacBindingViewController' => 'AlmanacServiceController',
'AlmanacBindingsSearchEngineAttachment' => 'AlmanacSearchEngineAttachment',
'AlmanacCacheEngineExtension' => 'PhabricatorCacheEngineExtension',
'AlmanacClusterDatabaseServiceType' => 'AlmanacClusterServiceType',
'AlmanacClusterRepositoryServiceType' => 'AlmanacClusterServiceType',
'AlmanacClusterServiceType' => 'AlmanacServiceType',
'AlmanacConsoleController' => 'AlmanacController',
'AlmanacController' => 'PhabricatorController',
'AlmanacCreateDevicesCapability' => 'PhabricatorPolicyCapability',
'AlmanacCreateNamespacesCapability' => 'PhabricatorPolicyCapability',
'AlmanacCreateNetworksCapability' => 'PhabricatorPolicyCapability',
'AlmanacCreateServicesCapability' => 'PhabricatorPolicyCapability',
'AlmanacCustomServiceType' => 'AlmanacServiceType',
'AlmanacDAO' => 'PhabricatorLiskDAO',
'AlmanacDeletePropertyEditField' => 'PhabricatorEditField',
'AlmanacDeletePropertyEditType' => 'PhabricatorEditType',
'AlmanacDevice' => array(
'AlmanacDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorProjectInterface',
'PhabricatorSSHPublicKeyInterface',
'AlmanacPropertyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorNgramsInterface',
'PhabricatorConduitResultInterface',
'PhabricatorExtendedPolicyInterface',
),
'AlmanacDeviceController' => 'AlmanacController',
'AlmanacDeviceDeletePropertyTransaction' => 'AlmanacDeviceTransactionType',
'AlmanacDeviceEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'AlmanacDeviceEditController' => 'AlmanacDeviceController',
'AlmanacDeviceEditEngine' => 'PhabricatorEditEngine',
'AlmanacDeviceEditor' => 'AlmanacEditor',
'AlmanacDeviceListController' => 'AlmanacDeviceController',
'AlmanacDeviceNameNgrams' => 'PhabricatorSearchNgrams',
'AlmanacDeviceNameTransaction' => 'AlmanacDeviceTransactionType',
'AlmanacDevicePHIDType' => 'PhabricatorPHIDType',
'AlmanacDevicePropertyEditEngine' => 'AlmanacPropertyEditEngine',
'AlmanacDeviceQuery' => 'AlmanacQuery',
'AlmanacDeviceSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'AlmanacDeviceSearchEngine' => 'PhabricatorApplicationSearchEngine',
'AlmanacDeviceSetPropertyTransaction' => 'AlmanacDeviceTransactionType',
'AlmanacDeviceTransaction' => 'AlmanacModularTransaction',
'AlmanacDeviceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'AlmanacDeviceTransactionType' => 'AlmanacTransactionType',
'AlmanacDeviceViewController' => 'AlmanacDeviceController',
'AlmanacDrydockPoolServiceType' => 'AlmanacServiceType',
'AlmanacEditor' => 'PhabricatorApplicationTransactionEditor',
'AlmanacInterface' => array(
'AlmanacDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorExtendedPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorConduitResultInterface',
),
'AlmanacInterfaceAddressTransaction' => 'AlmanacInterfaceTransactionType',
'AlmanacInterfaceDatasource' => 'PhabricatorTypeaheadDatasource',
'AlmanacInterfaceDeleteController' => 'AlmanacDeviceController',
'AlmanacInterfaceDestroyTransaction' => 'AlmanacInterfaceTransactionType',
'AlmanacInterfaceDeviceTransaction' => 'AlmanacInterfaceTransactionType',
'AlmanacInterfaceEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'AlmanacInterfaceEditController' => 'AlmanacDeviceController',
'AlmanacInterfaceEditEngine' => 'PhabricatorEditEngine',
'AlmanacInterfaceEditor' => 'AlmanacEditor',
'AlmanacInterfaceNetworkTransaction' => 'AlmanacInterfaceTransactionType',
'AlmanacInterfacePHIDType' => 'PhabricatorPHIDType',
'AlmanacInterfacePortTransaction' => 'AlmanacInterfaceTransactionType',
'AlmanacInterfaceQuery' => 'AlmanacQuery',
'AlmanacInterfaceSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'AlmanacInterfaceSearchEngine' => 'PhabricatorApplicationSearchEngine',
'AlmanacInterfaceTableView' => 'AphrontView',
'AlmanacInterfaceTransaction' => 'AlmanacModularTransaction',
'AlmanacInterfaceTransactionType' => 'AlmanacTransactionType',
'AlmanacKeys' => 'Phobject',
'AlmanacManageClusterServicesCapability' => 'PhabricatorPolicyCapability',
'AlmanacManagementRegisterWorkflow' => 'AlmanacManagementWorkflow',
'AlmanacManagementTrustKeyWorkflow' => 'AlmanacManagementWorkflow',
'AlmanacManagementUntrustKeyWorkflow' => 'AlmanacManagementWorkflow',
'AlmanacManagementWorkflow' => 'PhabricatorManagementWorkflow',
'AlmanacModularTransaction' => 'PhabricatorModularTransaction',
'AlmanacNames' => 'Phobject',
'AlmanacNamesTestCase' => 'PhabricatorTestCase',
'AlmanacNamespace' => array(
'AlmanacDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorProjectInterface',
'AlmanacPropertyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorNgramsInterface',
'PhabricatorConduitResultInterface',
),
'AlmanacNamespaceController' => 'AlmanacController',
'AlmanacNamespaceEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'AlmanacNamespaceEditController' => 'AlmanacNamespaceController',
'AlmanacNamespaceEditEngine' => 'PhabricatorEditEngine',
'AlmanacNamespaceEditor' => 'AlmanacEditor',
'AlmanacNamespaceListController' => 'AlmanacNamespaceController',
'AlmanacNamespaceNameNgrams' => 'PhabricatorSearchNgrams',
'AlmanacNamespaceNameTransaction' => 'AlmanacNamespaceTransactionType',
'AlmanacNamespacePHIDType' => 'PhabricatorPHIDType',
'AlmanacNamespaceQuery' => 'AlmanacQuery',
'AlmanacNamespaceSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'AlmanacNamespaceSearchEngine' => 'PhabricatorApplicationSearchEngine',
'AlmanacNamespaceTransaction' => 'AlmanacModularTransaction',
'AlmanacNamespaceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'AlmanacNamespaceTransactionType' => 'AlmanacTransactionType',
'AlmanacNamespaceViewController' => 'AlmanacNamespaceController',
'AlmanacNetwork' => array(
'AlmanacDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorNgramsInterface',
'PhabricatorConduitResultInterface',
),
'AlmanacNetworkController' => 'AlmanacController',
'AlmanacNetworkEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'AlmanacNetworkEditController' => 'AlmanacNetworkController',
'AlmanacNetworkEditEngine' => 'PhabricatorEditEngine',
'AlmanacNetworkEditor' => 'AlmanacEditor',
'AlmanacNetworkListController' => 'AlmanacNetworkController',
'AlmanacNetworkNameNgrams' => 'PhabricatorSearchNgrams',
'AlmanacNetworkNameTransaction' => 'AlmanacNetworkTransactionType',
'AlmanacNetworkPHIDType' => 'PhabricatorPHIDType',
'AlmanacNetworkQuery' => 'AlmanacQuery',
'AlmanacNetworkSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'AlmanacNetworkSearchEngine' => 'PhabricatorApplicationSearchEngine',
'AlmanacNetworkTransaction' => 'AlmanacModularTransaction',
'AlmanacNetworkTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'AlmanacNetworkTransactionType' => 'AlmanacTransactionType',
'AlmanacNetworkViewController' => 'AlmanacNetworkController',
'AlmanacPropertiesDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'AlmanacPropertiesEditEngineExtension' => 'PhabricatorEditEngineExtension',
'AlmanacPropertiesSearchEngineAttachment' => 'AlmanacSearchEngineAttachment',
'AlmanacProperty' => array(
'AlmanacDAO',
'PhabricatorPolicyInterface',
),
'AlmanacPropertyController' => 'AlmanacController',
'AlmanacPropertyDeleteController' => 'AlmanacPropertyController',
'AlmanacPropertyEditController' => 'AlmanacPropertyController',
'AlmanacPropertyEditEngine' => 'PhabricatorEditEngine',
'AlmanacPropertyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'AlmanacQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'AlmanacSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'AlmanacSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'AlmanacService' => array(
'AlmanacDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorProjectInterface',
'AlmanacPropertyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorNgramsInterface',
'PhabricatorConduitResultInterface',
'PhabricatorExtendedPolicyInterface',
),
'AlmanacServiceController' => 'AlmanacController',
'AlmanacServiceDatasource' => 'PhabricatorTypeaheadDatasource',
'AlmanacServiceDeletePropertyTransaction' => 'AlmanacServiceTransactionType',
'AlmanacServiceEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'AlmanacServiceEditController' => 'AlmanacServiceController',
'AlmanacServiceEditEngine' => 'PhabricatorEditEngine',
'AlmanacServiceEditor' => 'AlmanacEditor',
'AlmanacServiceListController' => 'AlmanacServiceController',
'AlmanacServiceNameNgrams' => 'PhabricatorSearchNgrams',
'AlmanacServiceNameTransaction' => 'AlmanacServiceTransactionType',
'AlmanacServicePHIDType' => 'PhabricatorPHIDType',
'AlmanacServicePropertyEditEngine' => 'AlmanacPropertyEditEngine',
'AlmanacServiceQuery' => 'AlmanacQuery',
'AlmanacServiceSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'AlmanacServiceSearchEngine' => 'PhabricatorApplicationSearchEngine',
'AlmanacServiceSetPropertyTransaction' => 'AlmanacServiceTransactionType',
'AlmanacServiceTransaction' => 'AlmanacModularTransaction',
'AlmanacServiceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'AlmanacServiceTransactionType' => 'AlmanacTransactionType',
'AlmanacServiceType' => 'Phobject',
'AlmanacServiceTypeDatasource' => 'PhabricatorTypeaheadDatasource',
'AlmanacServiceTypeTestCase' => 'PhabricatorTestCase',
'AlmanacServiceTypeTransaction' => 'AlmanacServiceTransactionType',
'AlmanacServiceViewController' => 'AlmanacServiceController',
'AlmanacSetPropertyEditField' => 'PhabricatorEditField',
'AlmanacSetPropertyEditType' => 'PhabricatorEditType',
'AlmanacTransactionType' => 'PhabricatorModularTransactionType',
'AphlictDropdownDataQuery' => 'Phobject',
'Aphront304Response' => 'AphrontResponse',
'Aphront400Response' => 'AphrontResponse',
'Aphront403Response' => 'AphrontHTMLResponse',
'Aphront404Response' => 'AphrontHTMLResponse',
'AphrontAjaxResponse' => 'AphrontResponse',
'AphrontApplicationConfiguration' => 'Phobject',
'AphrontBarView' => 'AphrontView',
'AphrontBoolHTTPParameterType' => 'AphrontHTTPParameterType',
'AphrontCalendarEventView' => 'AphrontView',
'AphrontController' => 'Phobject',
'AphrontCursorPagerView' => 'AphrontView',
- 'AphrontDefaultApplicationConfiguration' => 'AphrontApplicationConfiguration',
'AphrontDialogResponse' => 'AphrontResponse',
'AphrontDialogView' => array(
'AphrontView',
'AphrontResponseProducerInterface',
),
'AphrontEpochHTTPParameterType' => 'AphrontHTTPParameterType',
'AphrontException' => 'Exception',
'AphrontFileHTTPParameterType' => 'AphrontHTTPParameterType',
'AphrontFileResponse' => 'AphrontResponse',
'AphrontFormCheckboxControl' => 'AphrontFormControl',
'AphrontFormControl' => 'AphrontView',
'AphrontFormDateControl' => 'AphrontFormControl',
'AphrontFormDateControlValue' => 'Phobject',
'AphrontFormDividerControl' => 'AphrontFormControl',
'AphrontFormFileControl' => 'AphrontFormControl',
'AphrontFormHandlesControl' => 'AphrontFormControl',
'AphrontFormMarkupControl' => 'AphrontFormControl',
'AphrontFormPasswordControl' => 'AphrontFormControl',
'AphrontFormPolicyControl' => 'AphrontFormControl',
'AphrontFormRadioButtonControl' => 'AphrontFormControl',
'AphrontFormRecaptchaControl' => 'AphrontFormControl',
'AphrontFormSelectControl' => 'AphrontFormControl',
'AphrontFormStaticControl' => 'AphrontFormControl',
'AphrontFormSubmitControl' => 'AphrontFormControl',
'AphrontFormTextAreaControl' => 'AphrontFormControl',
'AphrontFormTextControl' => 'AphrontFormControl',
'AphrontFormTextWithSubmitControl' => 'AphrontFormControl',
'AphrontFormTokenizerControl' => 'AphrontFormControl',
'AphrontFormTypeaheadControl' => 'AphrontFormControl',
'AphrontFormView' => 'AphrontView',
'AphrontGlyphBarView' => 'AphrontBarView',
'AphrontHTMLResponse' => 'AphrontResponse',
'AphrontHTTPParameterType' => 'Phobject',
'AphrontHTTPProxyResponse' => 'AphrontResponse',
'AphrontHTTPSink' => 'Phobject',
'AphrontHTTPSinkTestCase' => 'PhabricatorTestCase',
'AphrontIntHTTPParameterType' => 'AphrontHTTPParameterType',
'AphrontIsolatedDatabaseConnectionTestCase' => 'PhabricatorTestCase',
'AphrontIsolatedHTTPSink' => 'AphrontHTTPSink',
'AphrontJSONResponse' => 'AphrontResponse',
'AphrontJavelinView' => 'AphrontView',
'AphrontKeyboardShortcutsAvailableView' => 'AphrontView',
'AphrontListFilterView' => 'AphrontView',
'AphrontListHTTPParameterType' => 'AphrontHTTPParameterType',
'AphrontMalformedRequestException' => 'AphrontException',
'AphrontMoreView' => 'AphrontView',
'AphrontMultiColumnView' => 'AphrontView',
'AphrontMySQLDatabaseConnectionTestCase' => 'PhabricatorTestCase',
'AphrontNullView' => 'AphrontView',
'AphrontPHIDHTTPParameterType' => 'AphrontHTTPParameterType',
'AphrontPHIDListHTTPParameterType' => 'AphrontListHTTPParameterType',
'AphrontPHPHTTPSink' => 'AphrontHTTPSink',
'AphrontPageView' => 'AphrontView',
'AphrontPlainTextResponse' => 'AphrontResponse',
'AphrontProgressBarView' => 'AphrontBarView',
'AphrontProjectListHTTPParameterType' => 'AphrontListHTTPParameterType',
'AphrontProxyResponse' => array(
'AphrontResponse',
'AphrontResponseProducerInterface',
),
'AphrontRedirectResponse' => 'AphrontResponse',
'AphrontRedirectResponseTestCase' => 'PhabricatorTestCase',
'AphrontReloadResponse' => 'AphrontRedirectResponse',
'AphrontRequest' => 'Phobject',
'AphrontRequestExceptionHandler' => 'Phobject',
'AphrontRequestTestCase' => 'PhabricatorTestCase',
'AphrontResponse' => 'Phobject',
'AphrontRoutingMap' => 'Phobject',
'AphrontRoutingResult' => 'Phobject',
'AphrontSelectHTTPParameterType' => 'AphrontHTTPParameterType',
'AphrontSideNavFilterView' => 'AphrontView',
'AphrontSite' => 'Phobject',
'AphrontStackTraceView' => 'AphrontView',
'AphrontStandaloneHTMLResponse' => 'AphrontHTMLResponse',
'AphrontStringHTTPParameterType' => 'AphrontHTTPParameterType',
'AphrontStringListHTTPParameterType' => 'AphrontListHTTPParameterType',
'AphrontTableView' => 'AphrontView',
'AphrontTagView' => 'AphrontView',
'AphrontTokenizerTemplateView' => 'AphrontView',
'AphrontTypeaheadTemplateView' => 'AphrontView',
'AphrontUnhandledExceptionResponse' => 'AphrontStandaloneHTMLResponse',
'AphrontUserListHTTPParameterType' => 'AphrontListHTTPParameterType',
'AphrontView' => array(
'Phobject',
'PhutilSafeHTMLProducerInterface',
),
'AphrontWebpageResponse' => 'AphrontHTMLResponse',
'ArcanistConduitAPIMethod' => 'ConduitAPIMethod',
'AuditConduitAPIMethod' => 'ConduitAPIMethod',
'AuditQueryConduitAPIMethod' => 'AuditConduitAPIMethod',
'AuthManageProvidersCapability' => 'PhabricatorPolicyCapability',
'BulkParameterType' => 'Phobject',
'BulkPointsParameterType' => 'BulkParameterType',
'BulkRemarkupParameterType' => 'BulkParameterType',
'BulkSelectParameterType' => 'BulkParameterType',
'BulkStringParameterType' => 'BulkParameterType',
'BulkTokenizerParameterType' => 'BulkParameterType',
'CalendarTimeUtil' => 'Phobject',
'CalendarTimeUtilTestCase' => 'PhabricatorTestCase',
'CelerityAPI' => 'Phobject',
'CelerityDarkModePostprocessor' => 'CelerityPostprocessor',
'CelerityDefaultPostprocessor' => 'CelerityPostprocessor',
'CelerityHighContrastPostprocessor' => 'CelerityPostprocessor',
'CelerityLargeFontPostprocessor' => 'CelerityPostprocessor',
'CelerityManagementMapWorkflow' => 'CelerityManagementWorkflow',
'CelerityManagementSyntaxWorkflow' => 'CelerityManagementWorkflow',
'CelerityManagementWorkflow' => 'PhabricatorManagementWorkflow',
'CelerityPhabricatorResourceController' => 'CelerityResourceController',
'CelerityPhabricatorResources' => 'CelerityResourcesOnDisk',
'CelerityPhysicalResources' => 'CelerityResources',
'CelerityPhysicalResourcesTestCase' => 'PhabricatorTestCase',
'CelerityPostprocessor' => 'Phobject',
'CelerityPostprocessorTestCase' => 'PhabricatorTestCase',
'CelerityRedGreenPostprocessor' => 'CelerityPostprocessor',
'CelerityResourceController' => 'PhabricatorController',
'CelerityResourceGraph' => 'AbstractDirectedGraph',
'CelerityResourceMap' => 'Phobject',
'CelerityResourceMapGenerator' => 'Phobject',
'CelerityResourceTransformer' => 'Phobject',
'CelerityResourceTransformerTestCase' => 'PhabricatorTestCase',
'CelerityResources' => 'Phobject',
'CelerityResourcesOnDisk' => 'CelerityPhysicalResources',
'CeleritySpriteGenerator' => 'Phobject',
'CelerityStaticResourceResponse' => 'Phobject',
'ChatLogConduitAPIMethod' => 'ConduitAPIMethod',
'ChatLogQueryConduitAPIMethod' => 'ChatLogConduitAPIMethod',
'ChatLogRecordConduitAPIMethod' => 'ChatLogConduitAPIMethod',
'ConduitAPIMethod' => array(
'Phobject',
'PhabricatorPolicyInterface',
),
'ConduitAPIMethodTestCase' => 'PhabricatorTestCase',
'ConduitAPIRequest' => 'Phobject',
'ConduitAPIResponse' => 'Phobject',
'ConduitApplicationNotInstalledException' => 'ConduitMethodNotFoundException',
'ConduitBoolParameterType' => 'ConduitParameterType',
'ConduitCall' => 'Phobject',
'ConduitCallTestCase' => 'PhabricatorTestCase',
'ConduitColumnsParameterType' => 'ConduitParameterType',
'ConduitConnectConduitAPIMethod' => 'ConduitAPIMethod',
'ConduitConstantDescription' => 'Phobject',
'ConduitEpochParameterType' => 'ConduitParameterType',
'ConduitException' => 'Exception',
'ConduitGetCapabilitiesConduitAPIMethod' => 'ConduitAPIMethod',
'ConduitGetCertificateConduitAPIMethod' => 'ConduitAPIMethod',
'ConduitIntListParameterType' => 'ConduitListParameterType',
'ConduitIntParameterType' => 'ConduitParameterType',
'ConduitListParameterType' => 'ConduitParameterType',
'ConduitLogGarbageCollector' => 'PhabricatorGarbageCollector',
'ConduitMethodDoesNotExistException' => 'ConduitMethodNotFoundException',
'ConduitMethodNotFoundException' => 'ConduitException',
'ConduitPHIDListParameterType' => 'ConduitListParameterType',
'ConduitPHIDParameterType' => 'ConduitParameterType',
'ConduitParameterType' => 'Phobject',
'ConduitPingConduitAPIMethod' => 'ConduitAPIMethod',
'ConduitPointsParameterType' => 'ConduitParameterType',
'ConduitProjectListParameterType' => 'ConduitListParameterType',
'ConduitQueryConduitAPIMethod' => 'ConduitAPIMethod',
'ConduitResultSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
'ConduitSSHWorkflow' => 'PhabricatorSSHWorkflow',
'ConduitStringListParameterType' => 'ConduitListParameterType',
'ConduitStringParameterType' => 'ConduitParameterType',
'ConduitTokenGarbageCollector' => 'PhabricatorGarbageCollector',
'ConduitUserListParameterType' => 'ConduitListParameterType',
'ConduitUserParameterType' => 'ConduitParameterType',
'ConduitWildParameterType' => 'ConduitParameterType',
'ConpherenceColumnViewController' => 'ConpherenceController',
'ConpherenceConduitAPIMethod' => 'ConduitAPIMethod',
- 'ConpherenceConfigOptions' => 'PhabricatorApplicationConfigOptions',
'ConpherenceConstants' => 'Phobject',
'ConpherenceController' => 'PhabricatorController',
'ConpherenceCreateThreadConduitAPIMethod' => 'ConpherenceConduitAPIMethod',
'ConpherenceDAO' => 'PhabricatorLiskDAO',
'ConpherenceDurableColumnView' => 'AphrontTagView',
'ConpherenceEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'ConpherenceEditEngine' => 'PhabricatorEditEngine',
'ConpherenceEditor' => 'PhabricatorApplicationTransactionEditor',
'ConpherenceFulltextQuery' => 'PhabricatorOffsetPagedQuery',
'ConpherenceIndex' => 'ConpherenceDAO',
'ConpherenceLayoutView' => 'AphrontTagView',
'ConpherenceListController' => 'ConpherenceController',
'ConpherenceMenuItemView' => 'AphrontTagView',
'ConpherenceNotificationPanelController' => 'ConpherenceController',
'ConpherenceParticipant' => 'ConpherenceDAO',
'ConpherenceParticipantController' => 'ConpherenceController',
'ConpherenceParticipantCountQuery' => 'PhabricatorOffsetPagedQuery',
'ConpherenceParticipantQuery' => 'PhabricatorOffsetPagedQuery',
'ConpherenceParticipantView' => 'AphrontView',
'ConpherenceQueryThreadConduitAPIMethod' => 'ConpherenceConduitAPIMethod',
'ConpherenceQueryTransactionConduitAPIMethod' => 'ConpherenceConduitAPIMethod',
'ConpherenceReplyHandler' => 'PhabricatorMailReplyHandler',
'ConpherenceRoomEditController' => 'ConpherenceController',
'ConpherenceRoomListController' => 'ConpherenceController',
'ConpherenceRoomPictureController' => 'ConpherenceController',
'ConpherenceRoomPreferencesController' => 'ConpherenceController',
'ConpherenceRoomSettings' => 'ConpherenceConstants',
'ConpherenceRoomTestCase' => 'ConpherenceTestCase',
'ConpherenceSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'ConpherenceTestCase' => 'PhabricatorTestCase',
'ConpherenceThread' => array(
'ConpherenceDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorMentionableInterface',
'PhabricatorDestructibleInterface',
'PhabricatorNgramsInterface',
),
'ConpherenceThreadDatasource' => 'PhabricatorTypeaheadDatasource',
'ConpherenceThreadDateMarkerTransaction' => 'ConpherenceThreadTransactionType',
'ConpherenceThreadIndexEngineExtension' => 'PhabricatorIndexEngineExtension',
'ConpherenceThreadListView' => 'AphrontView',
'ConpherenceThreadMailReceiver' => 'PhabricatorObjectMailReceiver',
'ConpherenceThreadMembersPolicyRule' => 'PhabricatorPolicyRule',
'ConpherenceThreadParticipantsTransaction' => 'ConpherenceThreadTransactionType',
'ConpherenceThreadPictureTransaction' => 'ConpherenceThreadTransactionType',
'ConpherenceThreadQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'ConpherenceThreadRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'ConpherenceThreadSearchController' => 'ConpherenceController',
'ConpherenceThreadSearchEngine' => 'PhabricatorApplicationSearchEngine',
'ConpherenceThreadTitleNgrams' => 'PhabricatorSearchNgrams',
'ConpherenceThreadTitleTransaction' => 'ConpherenceThreadTransactionType',
'ConpherenceThreadTopicTransaction' => 'ConpherenceThreadTransactionType',
'ConpherenceThreadTransactionType' => 'PhabricatorModularTransactionType',
'ConpherenceTransaction' => 'PhabricatorModularTransaction',
'ConpherenceTransactionComment' => 'PhabricatorApplicationTransactionComment',
'ConpherenceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'ConpherenceTransactionRenderer' => 'Phobject',
'ConpherenceTransactionView' => 'AphrontView',
'ConpherenceUpdateActions' => 'ConpherenceConstants',
'ConpherenceUpdateController' => 'ConpherenceController',
'ConpherenceUpdateThreadConduitAPIMethod' => 'ConpherenceConduitAPIMethod',
'ConpherenceViewController' => 'ConpherenceController',
'CountdownEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'CountdownSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'DarkConsoleController' => 'PhabricatorController',
'DarkConsoleCore' => 'Phobject',
'DarkConsoleDataController' => 'PhabricatorController',
'DarkConsoleErrorLogPlugin' => 'DarkConsolePlugin',
'DarkConsoleErrorLogPluginAPI' => 'Phobject',
'DarkConsoleEventPlugin' => 'DarkConsolePlugin',
'DarkConsoleEventPluginAPI' => 'PhabricatorEventListener',
'DarkConsolePlugin' => 'Phobject',
'DarkConsoleRealtimePlugin' => 'DarkConsolePlugin',
'DarkConsoleRequestPlugin' => 'DarkConsolePlugin',
'DarkConsoleServicesPlugin' => 'DarkConsolePlugin',
'DarkConsoleStartupPlugin' => 'DarkConsolePlugin',
'DarkConsoleXHProfPlugin' => 'DarkConsolePlugin',
'DarkConsoleXHProfPluginAPI' => 'Phobject',
'DifferentialAction' => 'Phobject',
'DifferentialActionEmailCommand' => 'MetaMTAEmailTransactionCommand',
'DifferentialAdjustmentMapTestCase' => 'PhutilTestCase',
'DifferentialAffectedPath' => 'DifferentialDAO',
'DifferentialAsanaRepresentationField' => 'DifferentialCustomField',
'DifferentialAuditorsCommitMessageField' => 'DifferentialCommitMessageCustomField',
'DifferentialAuditorsField' => 'DifferentialStoredCustomField',
'DifferentialBlameRevisionCommitMessageField' => 'DifferentialCommitMessageCustomField',
'DifferentialBlameRevisionField' => 'DifferentialStoredCustomField',
'DifferentialBlockHeraldAction' => 'HeraldAction',
'DifferentialBlockingReviewerDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DifferentialBranchField' => 'DifferentialCustomField',
'DifferentialBuildableEngine' => 'HarbormasterBuildableEngine',
'DifferentialChangeDetailMailView' => 'DifferentialMailView',
'DifferentialChangeHeraldFieldGroup' => 'HeraldFieldGroup',
'DifferentialChangeType' => 'Phobject',
'DifferentialChangesSinceLastUpdateField' => 'DifferentialCustomField',
'DifferentialChangeset' => array(
'DifferentialDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'DifferentialChangesetDetailView' => 'AphrontView',
'DifferentialChangesetEngine' => 'Phobject',
'DifferentialChangesetFileTreeSideNavBuilder' => 'Phobject',
'DifferentialChangesetHTMLRenderer' => 'DifferentialChangesetRenderer',
'DifferentialChangesetListController' => 'DifferentialController',
'DifferentialChangesetListView' => 'AphrontView',
'DifferentialChangesetOneUpMailRenderer' => 'DifferentialChangesetRenderer',
'DifferentialChangesetOneUpRenderer' => 'DifferentialChangesetHTMLRenderer',
'DifferentialChangesetOneUpTestRenderer' => 'DifferentialChangesetTestRenderer',
'DifferentialChangesetParser' => 'Phobject',
'DifferentialChangesetParserTestCase' => 'PhabricatorTestCase',
'DifferentialChangesetQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DifferentialChangesetRenderer' => 'Phobject',
'DifferentialChangesetSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DifferentialChangesetTestRenderer' => 'DifferentialChangesetRenderer',
'DifferentialChangesetTwoUpRenderer' => 'DifferentialChangesetHTMLRenderer',
'DifferentialChangesetTwoUpTestRenderer' => 'DifferentialChangesetTestRenderer',
'DifferentialChangesetViewController' => 'DifferentialController',
'DifferentialCloseConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialCommitMessageCustomField' => 'DifferentialCommitMessageField',
'DifferentialCommitMessageField' => 'Phobject',
'DifferentialCommitMessageFieldTestCase' => 'PhabricatorTestCase',
'DifferentialCommitMessageParser' => 'Phobject',
'DifferentialCommitMessageParserTestCase' => 'PhabricatorTestCase',
'DifferentialCommitsField' => 'DifferentialCustomField',
'DifferentialCommitsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'DifferentialConduitAPIMethod' => 'ConduitAPIMethod',
'DifferentialConflictsCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialController' => 'PhabricatorController',
'DifferentialCoreCustomField' => 'DifferentialCustomField',
'DifferentialCreateCommentConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialCreateDiffConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialCreateInlineConduitAPIMethod' => 'DifferentialConduitAPIMethod',
- 'DifferentialCreateMailReceiver' => 'PhabricatorMailReceiver',
+ 'DifferentialCreateMailReceiver' => 'PhabricatorApplicationMailReceiver',
'DifferentialCreateRawDiffConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialCreateRevisionConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialCustomField' => 'PhabricatorCustomField',
'DifferentialCustomFieldDependsOnParser' => 'PhabricatorCustomFieldMonogramParser',
'DifferentialCustomFieldDependsOnParserTestCase' => 'PhabricatorTestCase',
'DifferentialCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage',
'DifferentialCustomFieldRevertsParser' => 'PhabricatorCustomFieldMonogramParser',
'DifferentialCustomFieldRevertsParserTestCase' => 'PhabricatorTestCase',
'DifferentialCustomFieldStorage' => 'PhabricatorCustomFieldStorage',
'DifferentialCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage',
'DifferentialDAO' => 'PhabricatorLiskDAO',
'DifferentialDefaultViewCapability' => 'PhabricatorPolicyCapability',
'DifferentialDiff' => array(
'DifferentialDAO',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
'HarbormasterBuildableInterface',
'HarbormasterCircleCIBuildableInterface',
'HarbormasterBuildkiteBuildableInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
'PhabricatorConduitResultInterface',
),
'DifferentialDiffAffectedFilesHeraldField' => 'DifferentialDiffHeraldField',
'DifferentialDiffAuthorHeraldField' => 'DifferentialDiffHeraldField',
'DifferentialDiffAuthorProjectsHeraldField' => 'DifferentialDiffHeraldField',
'DifferentialDiffContentAddedHeraldField' => 'DifferentialDiffHeraldField',
'DifferentialDiffContentHeraldField' => 'DifferentialDiffHeraldField',
'DifferentialDiffContentRemovedHeraldField' => 'DifferentialDiffHeraldField',
'DifferentialDiffCreateController' => 'DifferentialController',
'DifferentialDiffEditor' => 'PhabricatorApplicationTransactionEditor',
'DifferentialDiffExtractionEngine' => 'Phobject',
'DifferentialDiffHeraldField' => 'HeraldField',
'DifferentialDiffHeraldFieldGroup' => 'HeraldFieldGroup',
'DifferentialDiffInlineCommentQuery' => 'PhabricatorDiffInlineCommentQuery',
'DifferentialDiffPHIDType' => 'PhabricatorPHIDType',
'DifferentialDiffProperty' => 'DifferentialDAO',
'DifferentialDiffQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DifferentialDiffRepositoryHeraldField' => 'DifferentialDiffHeraldField',
'DifferentialDiffRepositoryProjectsHeraldField' => 'DifferentialDiffHeraldField',
'DifferentialDiffSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'DifferentialDiffSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DifferentialDiffTestCase' => 'PhutilTestCase',
'DifferentialDiffTransaction' => 'PhabricatorApplicationTransaction',
'DifferentialDiffTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'DifferentialDiffViewController' => 'DifferentialController',
'DifferentialDoorkeeperRevisionFeedStoryPublisher' => 'DoorkeeperFeedStoryPublisher',
'DifferentialDraftField' => 'DifferentialCoreCustomField',
'DifferentialExactUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DifferentialFieldParseException' => 'Exception',
'DifferentialFieldValidationException' => 'Exception',
'DifferentialGetAllDiffsConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialGetCommitMessageConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialGetCommitPathsConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialGetDiffConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialGetRawDiffConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialGetRevisionCommentsConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialGetRevisionConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialGetWorkingCopy' => 'Phobject',
'DifferentialGitSVNIDCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialHarbormasterField' => 'DifferentialCustomField',
'DifferentialHeraldStateReasons' => 'HeraldStateReasons',
'DifferentialHiddenComment' => 'DifferentialDAO',
'DifferentialHostField' => 'DifferentialCustomField',
'DifferentialHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension',
'DifferentialHunk' => array(
'DifferentialDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'DifferentialHunkParser' => 'Phobject',
'DifferentialHunkParserTestCase' => 'PhabricatorTestCase',
'DifferentialHunkQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DifferentialHunkTestCase' => 'PhutilTestCase',
'DifferentialInlineComment' => array(
'Phobject',
'PhabricatorInlineCommentInterface',
),
'DifferentialInlineCommentEditController' => 'PhabricatorInlineCommentController',
'DifferentialInlineCommentMailView' => 'DifferentialMailView',
'DifferentialInlineCommentQuery' => 'PhabricatorOffsetPagedQuery',
'DifferentialJIRAIssuesCommitMessageField' => 'DifferentialCommitMessageCustomField',
'DifferentialJIRAIssuesField' => 'DifferentialStoredCustomField',
'DifferentialLegacyQuery' => 'Phobject',
'DifferentialLineAdjustmentMap' => 'Phobject',
'DifferentialLintField' => 'DifferentialHarbormasterField',
'DifferentialLintStatus' => 'Phobject',
'DifferentialLocalCommitsView' => 'AphrontView',
'DifferentialMailEngineExtension' => 'PhabricatorMailEngineExtension',
'DifferentialMailView' => 'Phobject',
'DifferentialManiphestTasksField' => 'DifferentialCoreCustomField',
'DifferentialParseCacheGarbageCollector' => 'PhabricatorGarbageCollector',
'DifferentialParseCommitMessageConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialParseRenderTestCase' => 'PhabricatorTestCase',
'DifferentialPathField' => 'DifferentialCustomField',
'DifferentialProjectReviewersField' => 'DifferentialCustomField',
'DifferentialQueryConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialQueryDiffsConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialRawDiffRenderer' => 'Phobject',
'DifferentialReleephRequestFieldSpecification' => 'Phobject',
'DifferentialRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'DifferentialReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'DifferentialRepositoryField' => 'DifferentialCoreCustomField',
'DifferentialRepositoryLookup' => 'Phobject',
'DifferentialRequiredSignaturesField' => 'DifferentialCoreCustomField',
'DifferentialResponsibleDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DifferentialResponsibleUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DifferentialResponsibleViewerFunctionDatasource' => 'PhabricatorTypeaheadDatasource',
'DifferentialRevertPlanCommitMessageField' => 'DifferentialCommitMessageCustomField',
'DifferentialRevertPlanField' => 'DifferentialStoredCustomField',
'DifferentialReviewedByCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialReviewer' => 'DifferentialDAO',
'DifferentialReviewerDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DifferentialReviewerForRevisionEdgeType' => 'PhabricatorEdgeType',
'DifferentialReviewerStatus' => 'Phobject',
'DifferentialReviewersAddBlockingReviewersHeraldAction' => 'DifferentialReviewersHeraldAction',
'DifferentialReviewersAddBlockingSelfHeraldAction' => 'DifferentialReviewersHeraldAction',
'DifferentialReviewersAddReviewersHeraldAction' => 'DifferentialReviewersHeraldAction',
'DifferentialReviewersAddSelfHeraldAction' => 'DifferentialReviewersHeraldAction',
'DifferentialReviewersCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialReviewersField' => 'DifferentialCoreCustomField',
'DifferentialReviewersHeraldAction' => 'HeraldAction',
'DifferentialReviewersSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'DifferentialReviewersView' => 'AphrontView',
'DifferentialRevision' => array(
'DifferentialDAO',
'PhabricatorTokenReceiverInterface',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
'PhabricatorFlaggableInterface',
'PhrequentTrackableInterface',
'HarbormasterBuildableInterface',
'PhabricatorSubscribableInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorApplicationTransactionInterface',
+ 'PhabricatorTimelineInterface',
'PhabricatorMentionableInterface',
'PhabricatorDestructibleInterface',
'PhabricatorProjectInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
'PhabricatorConduitResultInterface',
'PhabricatorDraftInterface',
),
'DifferentialRevisionAbandonTransaction' => 'DifferentialRevisionActionTransaction',
'DifferentialRevisionAcceptTransaction' => 'DifferentialRevisionReviewTransaction',
'DifferentialRevisionActionTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionAffectedFilesHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionAuthorHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionAuthorProjectsHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionBuildableTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionCloseDetailsController' => 'DifferentialController',
'DifferentialRevisionCloseTransaction' => 'DifferentialRevisionActionTransaction',
'DifferentialRevisionClosedStatusDatasource' => 'PhabricatorTypeaheadDatasource',
'DifferentialRevisionCommandeerTransaction' => 'DifferentialRevisionActionTransaction',
'DifferentialRevisionContentAddedHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionContentHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionContentRemovedHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionControlSystem' => 'Phobject',
'DifferentialRevisionDependedOnByRevisionEdgeType' => 'PhabricatorEdgeType',
'DifferentialRevisionDependsOnRevisionEdgeType' => 'PhabricatorEdgeType',
'DifferentialRevisionDraftEngine' => 'PhabricatorDraftEngine',
'DifferentialRevisionEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'DifferentialRevisionEditController' => 'DifferentialController',
'DifferentialRevisionEditEngine' => 'PhabricatorEditEngine',
'DifferentialRevisionFerretEngine' => 'PhabricatorFerretEngine',
'DifferentialRevisionFulltextEngine' => 'PhabricatorFulltextEngine',
'DifferentialRevisionGraph' => 'PhabricatorObjectGraph',
'DifferentialRevisionHasChildRelationship' => 'DifferentialRevisionRelationship',
'DifferentialRevisionHasCommitEdgeType' => 'PhabricatorEdgeType',
'DifferentialRevisionHasCommitRelationship' => 'DifferentialRevisionRelationship',
'DifferentialRevisionHasParentRelationship' => 'DifferentialRevisionRelationship',
'DifferentialRevisionHasReviewerEdgeType' => 'PhabricatorEdgeType',
'DifferentialRevisionHasTaskEdgeType' => 'PhabricatorEdgeType',
'DifferentialRevisionHasTaskRelationship' => 'DifferentialRevisionRelationship',
'DifferentialRevisionHeraldField' => 'HeraldField',
'DifferentialRevisionHeraldFieldGroup' => 'HeraldFieldGroup',
'DifferentialRevisionHoldDraftTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionIDCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialRevisionInlineTransaction' => 'PhabricatorModularTransactionType',
'DifferentialRevisionInlinesController' => 'DifferentialController',
'DifferentialRevisionListController' => 'DifferentialController',
'DifferentialRevisionListView' => 'AphrontView',
'DifferentialRevisionMailReceiver' => 'PhabricatorObjectMailReceiver',
'DifferentialRevisionOpenStatusDatasource' => 'PhabricatorTypeaheadDatasource',
'DifferentialRevisionOperationController' => 'DifferentialController',
'DifferentialRevisionPHIDType' => 'PhabricatorPHIDType',
'DifferentialRevisionPackageHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionPackageOwnerHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionPlanChangesTransaction' => 'DifferentialRevisionActionTransaction',
'DifferentialRevisionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DifferentialRevisionReclaimTransaction' => 'DifferentialRevisionActionTransaction',
'DifferentialRevisionRejectTransaction' => 'DifferentialRevisionReviewTransaction',
'DifferentialRevisionRelationship' => 'PhabricatorObjectRelationship',
'DifferentialRevisionRelationshipSource' => 'PhabricatorObjectRelationshipSource',
'DifferentialRevisionReopenTransaction' => 'DifferentialRevisionActionTransaction',
'DifferentialRevisionRepositoryHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionRepositoryProjectsHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionRepositoryTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionRequestReviewTransaction' => 'DifferentialRevisionActionTransaction',
'DifferentialRevisionRequiredActionResultBucket' => 'DifferentialRevisionResultBucket',
'DifferentialRevisionResignTransaction' => 'DifferentialRevisionReviewTransaction',
'DifferentialRevisionResultBucket' => 'PhabricatorSearchResultBucket',
'DifferentialRevisionReviewTransaction' => 'DifferentialRevisionActionTransaction',
'DifferentialRevisionReviewersHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionReviewersTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'DifferentialRevisionSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DifferentialRevisionStatus' => 'Phobject',
'DifferentialRevisionStatusDatasource' => 'PhabricatorTypeaheadDatasource',
'DifferentialRevisionStatusFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DifferentialRevisionStatusHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionStatusTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionSummaryHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionSummaryTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionTestPlanHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionTestPlanTransaction' => 'DifferentialRevisionTransactionType',
+ 'DifferentialRevisionTimelineEngine' => 'PhabricatorTimelineEngine',
'DifferentialRevisionTitleHeraldField' => 'DifferentialRevisionHeraldField',
'DifferentialRevisionTitleTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionTransactionType' => 'PhabricatorModularTransactionType',
'DifferentialRevisionUpdateHistoryView' => 'AphrontView',
'DifferentialRevisionUpdateTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionViewController' => 'DifferentialController',
'DifferentialRevisionVoidTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialRevisionWrongStateTransaction' => 'DifferentialRevisionTransactionType',
'DifferentialSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'DifferentialSetDiffPropertyConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DifferentialStoredCustomField' => 'DifferentialCustomField',
'DifferentialSubscribersCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialSummaryCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialSummaryField' => 'DifferentialCoreCustomField',
'DifferentialTagsCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialTasksCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialTestPlanCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialTestPlanField' => 'DifferentialCoreCustomField',
'DifferentialTitleCommitMessageField' => 'DifferentialCommitMessageField',
'DifferentialTransaction' => 'PhabricatorModularTransaction',
'DifferentialTransactionComment' => 'PhabricatorApplicationTransactionComment',
'DifferentialTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'DifferentialTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'DifferentialTransactionView' => 'PhabricatorApplicationTransactionView',
'DifferentialUnitField' => 'DifferentialCustomField',
'DifferentialUnitStatus' => 'Phobject',
'DifferentialUnitTestResult' => 'Phobject',
'DifferentialUpdateRevisionConduitAPIMethod' => 'DifferentialConduitAPIMethod',
'DiffusionAuditorDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DiffusionAuditorFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DiffusionAuditorsAddAuditorsHeraldAction' => 'DiffusionAuditorsHeraldAction',
'DiffusionAuditorsAddSelfHeraldAction' => 'DiffusionAuditorsHeraldAction',
'DiffusionAuditorsHeraldAction' => 'HeraldAction',
'DiffusionBlameConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionBlameController' => 'DiffusionController',
'DiffusionBlameQuery' => 'DiffusionQuery',
'DiffusionBlockHeraldAction' => 'HeraldAction',
'DiffusionBranchListView' => 'DiffusionView',
'DiffusionBranchQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionBranchTableController' => 'DiffusionController',
'DiffusionBranchTableView' => 'DiffusionView',
'DiffusionBrowseController' => 'DiffusionController',
'DiffusionBrowseQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionBrowseResultSet' => 'Phobject',
'DiffusionBrowseTableView' => 'DiffusionView',
'DiffusionBuildableEngine' => 'HarbormasterBuildableEngine',
'DiffusionCacheEngineExtension' => 'PhabricatorCacheEngineExtension',
'DiffusionCachedResolveRefsQuery' => 'DiffusionLowLevelQuery',
'DiffusionChangeController' => 'DiffusionController',
'DiffusionChangeHeraldFieldGroup' => 'HeraldFieldGroup',
'DiffusionCloneController' => 'DiffusionController',
'DiffusionCloneURIView' => 'AphrontView',
'DiffusionCommandEngine' => 'Phobject',
'DiffusionCommandEngineTestCase' => 'PhabricatorTestCase',
'DiffusionCommitAcceptTransaction' => 'DiffusionCommitAuditTransaction',
'DiffusionCommitActionTransaction' => 'DiffusionCommitTransactionType',
'DiffusionCommitAffectedFilesHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitAuditStatus' => 'Phobject',
'DiffusionCommitAuditTransaction' => 'DiffusionCommitActionTransaction',
'DiffusionCommitAuditorsHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitAuditorsTransaction' => 'DiffusionCommitTransactionType',
'DiffusionCommitAuthorHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitAuthorProjectsHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitAutocloseHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitBranchesController' => 'DiffusionController',
'DiffusionCommitBranchesHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitBuildableTransaction' => 'DiffusionCommitTransactionType',
'DiffusionCommitCommitterHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitCommitterProjectsHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitConcernTransaction' => 'DiffusionCommitAuditTransaction',
'DiffusionCommitController' => 'DiffusionController',
'DiffusionCommitDiffContentAddedHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitDiffContentHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitDiffContentRemovedHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitDiffEnormousHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitDraftEngine' => 'PhabricatorDraftEngine',
'DiffusionCommitEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'DiffusionCommitEditController' => 'DiffusionController',
'DiffusionCommitEditEngine' => 'PhabricatorEditEngine',
'DiffusionCommitFerretEngine' => 'PhabricatorFerretEngine',
'DiffusionCommitFulltextEngine' => 'PhabricatorFulltextEngine',
'DiffusionCommitHasPackageEdgeType' => 'PhabricatorEdgeType',
'DiffusionCommitHasRevisionEdgeType' => 'PhabricatorEdgeType',
'DiffusionCommitHasRevisionRelationship' => 'DiffusionCommitRelationship',
'DiffusionCommitHasTaskEdgeType' => 'PhabricatorEdgeType',
'DiffusionCommitHasTaskRelationship' => 'DiffusionCommitRelationship',
'DiffusionCommitHash' => 'Phobject',
'DiffusionCommitHeraldField' => 'HeraldField',
'DiffusionCommitHeraldFieldGroup' => 'HeraldFieldGroup',
'DiffusionCommitHintQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DiffusionCommitHookEngine' => 'Phobject',
'DiffusionCommitHookRejectException' => 'Exception',
'DiffusionCommitListController' => 'DiffusionController',
'DiffusionCommitListView' => 'AphrontView',
'DiffusionCommitMergeHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitMessageHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitPackageAuditHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitPackageHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitPackageOwnerHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitParentsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionCommitQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DiffusionCommitRef' => 'Phobject',
'DiffusionCommitRelationship' => 'PhabricatorObjectRelationship',
'DiffusionCommitRelationshipSource' => 'PhabricatorObjectRelationshipSource',
'DiffusionCommitRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'DiffusionCommitRemarkupRuleTestCase' => 'PhabricatorTestCase',
'DiffusionCommitRepositoryHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitRepositoryProjectsHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitRequiredActionResultBucket' => 'DiffusionCommitResultBucket',
'DiffusionCommitResignTransaction' => 'DiffusionCommitAuditTransaction',
'DiffusionCommitResultBucket' => 'PhabricatorSearchResultBucket',
'DiffusionCommitRevertedByCommitEdgeType' => 'PhabricatorEdgeType',
'DiffusionCommitRevertsCommitEdgeType' => 'PhabricatorEdgeType',
'DiffusionCommitReviewerHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitRevisionAcceptedHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitRevisionAcceptingReviewersHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitRevisionHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitRevisionReviewersHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitRevisionSubscribersHeraldField' => 'DiffusionCommitHeraldField',
'DiffusionCommitSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'DiffusionCommitStateTransaction' => 'DiffusionCommitTransactionType',
'DiffusionCommitTagsController' => 'DiffusionController',
+ 'DiffusionCommitTimelineEngine' => 'PhabricatorTimelineEngine',
'DiffusionCommitTransactionType' => 'PhabricatorModularTransactionType',
'DiffusionCommitVerifyTransaction' => 'DiffusionCommitAuditTransaction',
'DiffusionCompareController' => 'DiffusionController',
'DiffusionConduitAPIMethod' => 'ConduitAPIMethod',
'DiffusionController' => 'PhabricatorController',
'DiffusionCreateRepositoriesCapability' => 'PhabricatorPolicyCapability',
'DiffusionDaemonLockException' => 'Exception',
'DiffusionDatasourceEngineExtension' => 'PhabricatorDatasourceEngineExtension',
'DiffusionDefaultEditCapability' => 'PhabricatorPolicyCapability',
'DiffusionDefaultPushCapability' => 'PhabricatorPolicyCapability',
'DiffusionDefaultViewCapability' => 'PhabricatorPolicyCapability',
'DiffusionDiffController' => 'DiffusionController',
'DiffusionDiffInlineCommentQuery' => 'PhabricatorDiffInlineCommentQuery',
'DiffusionDiffQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionDocumentController' => 'DiffusionController',
'DiffusionDocumentRenderingEngine' => 'PhabricatorDocumentRenderingEngine',
'DiffusionDoorkeeperCommitFeedStoryPublisher' => 'DoorkeeperFeedStoryPublisher',
'DiffusionEmptyResultView' => 'DiffusionView',
'DiffusionExistsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionExternalController' => 'DiffusionController',
'DiffusionExternalSymbolQuery' => 'Phobject',
'DiffusionExternalSymbolsSource' => 'Phobject',
'DiffusionFileContentQuery' => 'DiffusionFileFutureQuery',
'DiffusionFileContentQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionFileFutureQuery' => 'DiffusionQuery',
'DiffusionFindSymbolsConduitAPIMethod' => 'DiffusionConduitAPIMethod',
'DiffusionGetLintMessagesConduitAPIMethod' => 'DiffusionConduitAPIMethod',
'DiffusionGetRecentCommitsByPathConduitAPIMethod' => 'DiffusionConduitAPIMethod',
'DiffusionGitBlameQuery' => 'DiffusionBlameQuery',
'DiffusionGitBranch' => 'Phobject',
'DiffusionGitBranchTestCase' => 'PhabricatorTestCase',
'DiffusionGitCommandEngine' => 'DiffusionCommandEngine',
'DiffusionGitFileContentQuery' => 'DiffusionFileContentQuery',
'DiffusionGitLFSAuthenticateWorkflow' => 'DiffusionGitSSHWorkflow',
'DiffusionGitLFSResponse' => 'AphrontResponse',
'DiffusionGitLFSTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType',
'DiffusionGitRawDiffQuery' => 'DiffusionRawDiffQuery',
'DiffusionGitReceivePackSSHWorkflow' => 'DiffusionGitSSHWorkflow',
'DiffusionGitRequest' => 'DiffusionRequest',
'DiffusionGitResponse' => 'AphrontResponse',
'DiffusionGitSSHWorkflow' => array(
'DiffusionSSHWorkflow',
'DiffusionRepositoryClusterEngineLogInterface',
),
'DiffusionGitUploadPackSSHWorkflow' => 'DiffusionGitSSHWorkflow',
'DiffusionGraphController' => 'DiffusionController',
'DiffusionHistoryController' => 'DiffusionController',
'DiffusionHistoryListView' => 'DiffusionHistoryView',
'DiffusionHistoryQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionHistoryTableView' => 'DiffusionHistoryView',
'DiffusionHistoryView' => 'DiffusionView',
'DiffusionHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension',
'DiffusionIdentityAssigneeDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DiffusionIdentityAssigneeEditField' => 'PhabricatorTokenizerEditField',
'DiffusionIdentityAssigneeSearchField' => 'PhabricatorSearchTokenizerField',
'DiffusionIdentityEditController' => 'DiffusionController',
'DiffusionIdentityListController' => 'DiffusionController',
'DiffusionIdentityUnassignedDatasource' => 'PhabricatorTypeaheadDatasource',
'DiffusionIdentityViewController' => 'DiffusionController',
'DiffusionInlineCommentController' => 'PhabricatorInlineCommentController',
'DiffusionInlineCommentPreviewController' => 'PhabricatorInlineCommentPreviewController',
'DiffusionInternalAncestorsConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionInternalGitRawDiffQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionLastModifiedController' => 'DiffusionController',
'DiffusionLastModifiedQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionLintController' => 'DiffusionController',
'DiffusionLintCountQuery' => 'PhabricatorQuery',
'DiffusionLintSaveRunner' => 'Phobject',
'DiffusionLocalRepositoryFilter' => 'Phobject',
'DiffusionLogController' => 'DiffusionController',
'DiffusionLookSoonConduitAPIMethod' => 'DiffusionConduitAPIMethod',
'DiffusionLowLevelCommitFieldsQuery' => 'DiffusionLowLevelQuery',
'DiffusionLowLevelCommitQuery' => 'DiffusionLowLevelQuery',
'DiffusionLowLevelFilesizeQuery' => 'DiffusionLowLevelQuery',
'DiffusionLowLevelGitRefQuery' => 'DiffusionLowLevelQuery',
'DiffusionLowLevelMercurialBranchesQuery' => 'DiffusionLowLevelQuery',
'DiffusionLowLevelMercurialPathsQuery' => 'DiffusionLowLevelQuery',
'DiffusionLowLevelParentsQuery' => 'DiffusionLowLevelQuery',
'DiffusionLowLevelQuery' => 'Phobject',
'DiffusionLowLevelResolveRefsQuery' => 'DiffusionLowLevelQuery',
'DiffusionMercurialBlameQuery' => 'DiffusionBlameQuery',
'DiffusionMercurialCommandEngine' => 'DiffusionCommandEngine',
'DiffusionMercurialFileContentQuery' => 'DiffusionFileContentQuery',
'DiffusionMercurialFlagInjectionException' => 'Exception',
'DiffusionMercurialRawDiffQuery' => 'DiffusionRawDiffQuery',
'DiffusionMercurialRequest' => 'DiffusionRequest',
'DiffusionMercurialResponse' => 'AphrontResponse',
'DiffusionMercurialSSHWorkflow' => 'DiffusionSSHWorkflow',
'DiffusionMercurialServeSSHWorkflow' => 'DiffusionMercurialSSHWorkflow',
'DiffusionMercurialWireClientSSHProtocolChannel' => 'PhutilProtocolChannel',
'DiffusionMercurialWireProtocol' => 'Phobject',
'DiffusionMercurialWireProtocolTests' => 'PhabricatorTestCase',
'DiffusionMercurialWireSSHTestCase' => 'PhabricatorTestCase',
'DiffusionMergedCommitsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionPathChange' => 'Phobject',
'DiffusionPathChangeQuery' => 'Phobject',
'DiffusionPathCompleteController' => 'DiffusionController',
'DiffusionPathIDQuery' => 'Phobject',
'DiffusionPathQuery' => 'Phobject',
'DiffusionPathQueryTestCase' => 'PhabricatorTestCase',
'DiffusionPathTreeController' => 'DiffusionController',
'DiffusionPathValidateController' => 'DiffusionController',
'DiffusionPatternSearchView' => 'DiffusionView',
'DiffusionPhpExternalSymbolsSource' => 'DiffusionExternalSymbolsSource',
'DiffusionPreCommitContentAffectedFilesHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentAuthorHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentAuthorProjectsHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentAuthorRawHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentBranchesHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentCommitterHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentCommitterProjectsHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentCommitterRawHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentDiffContentAddedHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentDiffContentHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentDiffContentRemovedHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentDiffEnormousHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentHeraldField' => 'HeraldField',
'DiffusionPreCommitContentMergeHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentMessageHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentPackageHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentPackageOwnerHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentPusherHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentPusherIsCommitterHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentPusherProjectsHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentRepositoryHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentRepositoryProjectsHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentRevisionAcceptedHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentRevisionAcceptingReviewersHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentRevisionHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentRevisionReviewersHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitContentRevisionSubscribersHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPreCommitRefChangeHeraldField' => 'DiffusionPreCommitRefHeraldField',
'DiffusionPreCommitRefHeraldField' => 'HeraldField',
'DiffusionPreCommitRefHeraldFieldGroup' => 'HeraldFieldGroup',
'DiffusionPreCommitRefNameHeraldField' => 'DiffusionPreCommitRefHeraldField',
'DiffusionPreCommitRefPusherHeraldField' => 'DiffusionPreCommitRefHeraldField',
'DiffusionPreCommitRefPusherProjectsHeraldField' => 'DiffusionPreCommitRefHeraldField',
'DiffusionPreCommitRefRepositoryHeraldField' => 'DiffusionPreCommitRefHeraldField',
'DiffusionPreCommitRefRepositoryProjectsHeraldField' => 'DiffusionPreCommitRefHeraldField',
'DiffusionPreCommitRefTypeHeraldField' => 'DiffusionPreCommitRefHeraldField',
'DiffusionPreCommitUsesGitLFSHeraldField' => 'DiffusionPreCommitContentHeraldField',
'DiffusionPullEventGarbageCollector' => 'PhabricatorGarbageCollector',
'DiffusionPullLogListController' => 'DiffusionLogController',
'DiffusionPullLogListView' => 'AphrontView',
'DiffusionPullLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DiffusionPushCapability' => 'PhabricatorPolicyCapability',
'DiffusionPushEventViewController' => 'DiffusionLogController',
'DiffusionPushLogListController' => 'DiffusionLogController',
'DiffusionPushLogListView' => 'AphrontView',
'DiffusionPythonExternalSymbolsSource' => 'DiffusionExternalSymbolsSource',
'DiffusionQuery' => 'PhabricatorQuery',
'DiffusionQueryCommitsConduitAPIMethod' => 'DiffusionConduitAPIMethod',
'DiffusionQueryConduitAPIMethod' => 'DiffusionConduitAPIMethod',
'DiffusionQueryPathsConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionRawDiffQuery' => 'DiffusionFileFutureQuery',
'DiffusionRawDiffQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionReadmeView' => 'DiffusionView',
'DiffusionRefDatasource' => 'PhabricatorTypeaheadDatasource',
'DiffusionRefNotFoundException' => 'Exception',
'DiffusionRefTableController' => 'DiffusionController',
'DiffusionRefsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionRenameHistoryQuery' => 'Phobject',
'DiffusionRepositoryActionsManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryAutomationManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryBasicsManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryBranchesManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryByIDRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'DiffusionRepositoryClusterEngine' => 'Phobject',
'DiffusionRepositoryController' => 'DiffusionController',
'DiffusionRepositoryDatasource' => 'PhabricatorTypeaheadDatasource',
'DiffusionRepositoryDefaultController' => 'DiffusionController',
'DiffusionRepositoryEditActivateController' => 'DiffusionRepositoryManageController',
'DiffusionRepositoryEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'DiffusionRepositoryEditController' => 'DiffusionRepositoryManageController',
'DiffusionRepositoryEditDangerousController' => 'DiffusionRepositoryManageController',
'DiffusionRepositoryEditDeleteController' => 'DiffusionRepositoryManageController',
'DiffusionRepositoryEditEngine' => 'PhabricatorEditEngine',
'DiffusionRepositoryEditEnormousController' => 'DiffusionRepositoryManageController',
'DiffusionRepositoryEditUpdateController' => 'DiffusionRepositoryManageController',
'DiffusionRepositoryFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DiffusionRepositoryHistoryManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryIdentityEditor' => 'PhabricatorApplicationTransactionEditor',
'DiffusionRepositoryIdentitySearchEngine' => 'PhabricatorApplicationSearchEngine',
'DiffusionRepositoryLimitsManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryListController' => 'DiffusionController',
'DiffusionRepositoryManageController' => 'DiffusionController',
'DiffusionRepositoryManagePanelsController' => 'DiffusionRepositoryManageController',
'DiffusionRepositoryManagementBuildsPanelGroup' => 'DiffusionRepositoryManagementPanelGroup',
'DiffusionRepositoryManagementIntegrationsPanelGroup' => 'DiffusionRepositoryManagementPanelGroup',
'DiffusionRepositoryManagementMainPanelGroup' => 'DiffusionRepositoryManagementPanelGroup',
'DiffusionRepositoryManagementOtherPanelGroup' => 'DiffusionRepositoryManagementPanelGroup',
'DiffusionRepositoryManagementPanel' => 'Phobject',
'DiffusionRepositoryManagementPanelGroup' => 'Phobject',
'DiffusionRepositoryPath' => 'Phobject',
'DiffusionRepositoryPoliciesManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryProfilePictureController' => 'DiffusionController',
'DiffusionRepositoryRef' => 'Phobject',
'DiffusionRepositoryRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'DiffusionRepositorySearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'DiffusionRepositoryStagingManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryStorageManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositorySubversionManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositorySymbolsManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryTag' => 'Phobject',
'DiffusionRepositoryTestAutomationController' => 'DiffusionRepositoryManageController',
'DiffusionRepositoryURICredentialController' => 'DiffusionController',
'DiffusionRepositoryURIDisableController' => 'DiffusionController',
'DiffusionRepositoryURIEditController' => 'DiffusionController',
'DiffusionRepositoryURIViewController' => 'DiffusionController',
'DiffusionRepositoryURIsIndexEngineExtension' => 'PhabricatorIndexEngineExtension',
'DiffusionRepositoryURIsManagementPanel' => 'DiffusionRepositoryManagementPanel',
'DiffusionRepositoryURIsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'DiffusionRequest' => 'Phobject',
'DiffusionResolveRefsConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionResolveUserQuery' => 'Phobject',
'DiffusionSSHWorkflow' => 'PhabricatorSSHWorkflow',
'DiffusionSearchQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionServeController' => 'DiffusionController',
'DiffusionSetPasswordSettingsPanel' => 'PhabricatorSettingsPanel',
'DiffusionSetupException' => 'Exception',
'DiffusionSubversionCommandEngine' => 'DiffusionCommandEngine',
'DiffusionSubversionSSHWorkflow' => 'DiffusionSSHWorkflow',
'DiffusionSubversionServeSSHWorkflow' => 'DiffusionSubversionSSHWorkflow',
'DiffusionSubversionWireProtocol' => 'Phobject',
'DiffusionSubversionWireProtocolTestCase' => 'PhabricatorTestCase',
'DiffusionSvnBlameQuery' => 'DiffusionBlameQuery',
'DiffusionSvnFileContentQuery' => 'DiffusionFileContentQuery',
'DiffusionSvnRawDiffQuery' => 'DiffusionRawDiffQuery',
'DiffusionSvnRequest' => 'DiffusionRequest',
'DiffusionSymbolController' => 'DiffusionController',
'DiffusionSymbolDatasource' => 'PhabricatorTypeaheadDatasource',
'DiffusionSymbolQuery' => 'PhabricatorOffsetPagedQuery',
'DiffusionSyncLogListController' => 'DiffusionLogController',
'DiffusionSyncLogListView' => 'AphrontView',
'DiffusionSyncLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DiffusionTagListController' => 'DiffusionController',
'DiffusionTagListView' => 'DiffusionView',
'DiffusionTagTableView' => 'DiffusionView',
'DiffusionTaggedRepositoriesFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'DiffusionTagsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod',
'DiffusionURIEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'DiffusionURIEditEngine' => 'PhabricatorEditEngine',
'DiffusionURIEditor' => 'PhabricatorApplicationTransactionEditor',
'DiffusionURITestCase' => 'PhutilTestCase',
'DiffusionUpdateCoverageConduitAPIMethod' => 'DiffusionConduitAPIMethod',
'DiffusionView' => 'AphrontView',
'DivinerArticleAtomizer' => 'DivinerAtomizer',
'DivinerAtom' => 'Phobject',
'DivinerAtomCache' => 'DivinerDiskCache',
'DivinerAtomController' => 'DivinerController',
'DivinerAtomListController' => 'DivinerController',
'DivinerAtomPHIDType' => 'PhabricatorPHIDType',
'DivinerAtomQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DivinerAtomRef' => 'Phobject',
'DivinerAtomSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DivinerAtomizeWorkflow' => 'DivinerWorkflow',
'DivinerAtomizer' => 'Phobject',
'DivinerBookController' => 'DivinerController',
'DivinerBookDatasource' => 'PhabricatorTypeaheadDatasource',
'DivinerBookEditController' => 'DivinerController',
'DivinerBookItemView' => 'AphrontTagView',
'DivinerBookPHIDType' => 'PhabricatorPHIDType',
'DivinerBookQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DivinerController' => 'PhabricatorController',
'DivinerDAO' => 'PhabricatorLiskDAO',
'DivinerDefaultEditCapability' => 'PhabricatorPolicyCapability',
'DivinerDefaultRenderer' => 'DivinerRenderer',
'DivinerDefaultViewCapability' => 'PhabricatorPolicyCapability',
'DivinerDiskCache' => 'Phobject',
'DivinerFileAtomizer' => 'DivinerAtomizer',
'DivinerFindController' => 'DivinerController',
'DivinerGenerateWorkflow' => 'DivinerWorkflow',
'DivinerLiveAtom' => 'DivinerDAO',
'DivinerLiveBook' => array(
'DivinerDAO',
'PhabricatorPolicyInterface',
'PhabricatorProjectInterface',
'PhabricatorDestructibleInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorFulltextInterface',
),
'DivinerLiveBookEditor' => 'PhabricatorApplicationTransactionEditor',
'DivinerLiveBookFulltextEngine' => 'PhabricatorFulltextEngine',
'DivinerLiveBookTransaction' => 'PhabricatorApplicationTransaction',
'DivinerLiveBookTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'DivinerLivePublisher' => 'DivinerPublisher',
'DivinerLiveSymbol' => array(
'DivinerDAO',
'PhabricatorPolicyInterface',
'PhabricatorMarkupInterface',
'PhabricatorDestructibleInterface',
'PhabricatorFulltextInterface',
),
'DivinerLiveSymbolFulltextEngine' => 'PhabricatorFulltextEngine',
'DivinerMainController' => 'DivinerController',
'DivinerPHPAtomizer' => 'DivinerAtomizer',
'DivinerParameterTableView' => 'AphrontTagView',
'DivinerPublishCache' => 'DivinerDiskCache',
'DivinerPublisher' => 'Phobject',
'DivinerRenderer' => 'Phobject',
'DivinerReturnTableView' => 'AphrontTagView',
'DivinerSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'DivinerSectionView' => 'AphrontTagView',
'DivinerStaticPublisher' => 'DivinerPublisher',
'DivinerSymbolRemarkupRule' => 'PhutilRemarkupRule',
'DivinerWorkflow' => 'PhabricatorManagementWorkflow',
'DoorkeeperAsanaFeedWorker' => 'DoorkeeperFeedWorker',
'DoorkeeperAsanaRemarkupRule' => 'DoorkeeperRemarkupRule',
'DoorkeeperBridge' => 'Phobject',
'DoorkeeperBridgeAsana' => 'DoorkeeperBridge',
'DoorkeeperBridgeGitHub' => 'DoorkeeperBridge',
'DoorkeeperBridgeGitHubIssue' => 'DoorkeeperBridgeGitHub',
'DoorkeeperBridgeGitHubUser' => 'DoorkeeperBridgeGitHub',
'DoorkeeperBridgeJIRA' => 'DoorkeeperBridge',
'DoorkeeperBridgeJIRATestCase' => 'PhabricatorTestCase',
'DoorkeeperBridgedObjectCurtainExtension' => 'PHUICurtainExtension',
'DoorkeeperDAO' => 'PhabricatorLiskDAO',
'DoorkeeperExternalObject' => array(
'DoorkeeperDAO',
'PhabricatorPolicyInterface',
),
'DoorkeeperExternalObjectPHIDType' => 'PhabricatorPHIDType',
'DoorkeeperExternalObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DoorkeeperFeedStoryPublisher' => 'Phobject',
'DoorkeeperFeedWorker' => 'FeedPushWorker',
'DoorkeeperImportEngine' => 'Phobject',
'DoorkeeperJIRAFeedWorker' => 'DoorkeeperFeedWorker',
'DoorkeeperJIRARemarkupRule' => 'DoorkeeperRemarkupRule',
'DoorkeeperMissingLinkException' => 'Exception',
'DoorkeeperObjectRef' => 'Phobject',
'DoorkeeperRemarkupRule' => 'PhutilRemarkupRule',
'DoorkeeperSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'DoorkeeperTagView' => 'AphrontView',
'DoorkeeperTagsController' => 'PhabricatorController',
'DrydockAcquiredBrokenResourceException' => 'Exception',
'DrydockAlmanacServiceHostBlueprintImplementation' => 'DrydockBlueprintImplementation',
'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface',
'DrydockAuthorization' => array(
'DrydockDAO',
'PhabricatorPolicyInterface',
'PhabricatorConduitResultInterface',
),
'DrydockAuthorizationAuthorizeController' => 'DrydockController',
'DrydockAuthorizationListController' => 'DrydockController',
'DrydockAuthorizationListView' => 'AphrontView',
'DrydockAuthorizationPHIDType' => 'PhabricatorPHIDType',
'DrydockAuthorizationQuery' => 'DrydockQuery',
'DrydockAuthorizationSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'DrydockAuthorizationSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DrydockAuthorizationViewController' => 'DrydockController',
'DrydockBlueprint' => array(
'DrydockDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorNgramsInterface',
'PhabricatorProjectInterface',
'PhabricatorConduitResultInterface',
),
'DrydockBlueprintController' => 'DrydockController',
'DrydockBlueprintCoreCustomField' => array(
'DrydockBlueprintCustomField',
'PhabricatorStandardCustomFieldInterface',
),
'DrydockBlueprintCustomField' => 'PhabricatorCustomField',
'DrydockBlueprintDatasource' => 'PhabricatorTypeaheadDatasource',
'DrydockBlueprintDisableController' => 'DrydockBlueprintController',
'DrydockBlueprintDisableTransaction' => 'DrydockBlueprintTransactionType',
'DrydockBlueprintEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'DrydockBlueprintEditController' => 'DrydockBlueprintController',
'DrydockBlueprintEditEngine' => 'PhabricatorEditEngine',
'DrydockBlueprintEditor' => 'PhabricatorApplicationTransactionEditor',
'DrydockBlueprintImplementation' => 'Phobject',
'DrydockBlueprintImplementationTestCase' => 'PhabricatorTestCase',
'DrydockBlueprintListController' => 'DrydockBlueprintController',
'DrydockBlueprintNameNgrams' => 'PhabricatorSearchNgrams',
'DrydockBlueprintNameTransaction' => 'DrydockBlueprintTransactionType',
'DrydockBlueprintPHIDType' => 'PhabricatorPHIDType',
'DrydockBlueprintQuery' => 'DrydockQuery',
'DrydockBlueprintSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'DrydockBlueprintSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DrydockBlueprintTransaction' => 'PhabricatorModularTransaction',
'DrydockBlueprintTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'DrydockBlueprintTransactionType' => 'PhabricatorModularTransactionType',
'DrydockBlueprintTypeTransaction' => 'DrydockBlueprintTransactionType',
'DrydockBlueprintViewController' => 'DrydockBlueprintController',
'DrydockCommand' => array(
'DrydockDAO',
'PhabricatorPolicyInterface',
),
'DrydockCommandError' => 'Phobject',
'DrydockCommandInterface' => 'DrydockInterface',
'DrydockCommandQuery' => 'DrydockQuery',
'DrydockConsoleController' => 'DrydockController',
'DrydockController' => 'PhabricatorController',
'DrydockCreateBlueprintsCapability' => 'PhabricatorPolicyCapability',
'DrydockDAO' => 'PhabricatorLiskDAO',
'DrydockDefaultEditCapability' => 'PhabricatorPolicyCapability',
'DrydockDefaultViewCapability' => 'PhabricatorPolicyCapability',
'DrydockFilesystemInterface' => 'DrydockInterface',
'DrydockInterface' => 'Phobject',
'DrydockLandRepositoryOperation' => 'DrydockRepositoryOperationType',
'DrydockLease' => array(
'DrydockDAO',
'PhabricatorPolicyInterface',
'PhabricatorConduitResultInterface',
),
'DrydockLeaseAcquiredLogType' => 'DrydockLogType',
'DrydockLeaseActivatedLogType' => 'DrydockLogType',
'DrydockLeaseActivationFailureLogType' => 'DrydockLogType',
'DrydockLeaseActivationYieldLogType' => 'DrydockLogType',
'DrydockLeaseAllocationFailureLogType' => 'DrydockLogType',
'DrydockLeaseController' => 'DrydockController',
'DrydockLeaseDatasource' => 'PhabricatorTypeaheadDatasource',
'DrydockLeaseDestroyedLogType' => 'DrydockLogType',
'DrydockLeaseListController' => 'DrydockLeaseController',
'DrydockLeaseListView' => 'AphrontView',
'DrydockLeaseNoAuthorizationsLogType' => 'DrydockLogType',
'DrydockLeaseNoBlueprintsLogType' => 'DrydockLogType',
'DrydockLeasePHIDType' => 'PhabricatorPHIDType',
'DrydockLeaseQuery' => 'DrydockQuery',
'DrydockLeaseQueuedLogType' => 'DrydockLogType',
'DrydockLeaseReacquireLogType' => 'DrydockLogType',
'DrydockLeaseReclaimLogType' => 'DrydockLogType',
'DrydockLeaseReleaseController' => 'DrydockLeaseController',
'DrydockLeaseReleasedLogType' => 'DrydockLogType',
'DrydockLeaseSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'DrydockLeaseSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DrydockLeaseStatus' => 'PhabricatorObjectStatus',
'DrydockLeaseUpdateWorker' => 'DrydockWorker',
'DrydockLeaseViewController' => 'DrydockLeaseController',
'DrydockLeaseWaitingForResourcesLogType' => 'DrydockLogType',
'DrydockLog' => array(
'DrydockDAO',
'PhabricatorPolicyInterface',
),
'DrydockLogController' => 'DrydockController',
'DrydockLogGarbageCollector' => 'PhabricatorGarbageCollector',
'DrydockLogListController' => 'DrydockLogController',
'DrydockLogListView' => 'AphrontView',
'DrydockLogQuery' => 'DrydockQuery',
'DrydockLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DrydockLogType' => 'Phobject',
'DrydockManagementCommandWorkflow' => 'DrydockManagementWorkflow',
'DrydockManagementLeaseWorkflow' => 'DrydockManagementWorkflow',
'DrydockManagementReclaimWorkflow' => 'DrydockManagementWorkflow',
'DrydockManagementReleaseLeaseWorkflow' => 'DrydockManagementWorkflow',
'DrydockManagementReleaseResourceWorkflow' => 'DrydockManagementWorkflow',
'DrydockManagementUpdateLeaseWorkflow' => 'DrydockManagementWorkflow',
'DrydockManagementUpdateResourceWorkflow' => 'DrydockManagementWorkflow',
'DrydockManagementWorkflow' => 'PhabricatorManagementWorkflow',
'DrydockObjectAuthorizationView' => 'AphrontView',
'DrydockOperationWorkLogType' => 'DrydockLogType',
'DrydockQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DrydockRepositoryOperation' => array(
'DrydockDAO',
'PhabricatorPolicyInterface',
),
'DrydockRepositoryOperationController' => 'DrydockController',
'DrydockRepositoryOperationDismissController' => 'DrydockRepositoryOperationController',
'DrydockRepositoryOperationListController' => 'DrydockRepositoryOperationController',
'DrydockRepositoryOperationPHIDType' => 'PhabricatorPHIDType',
'DrydockRepositoryOperationQuery' => 'DrydockQuery',
'DrydockRepositoryOperationSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DrydockRepositoryOperationStatusController' => 'DrydockRepositoryOperationController',
'DrydockRepositoryOperationStatusView' => 'AphrontView',
'DrydockRepositoryOperationType' => 'Phobject',
'DrydockRepositoryOperationUpdateWorker' => 'DrydockWorker',
'DrydockRepositoryOperationViewController' => 'DrydockRepositoryOperationController',
'DrydockResource' => array(
'DrydockDAO',
'PhabricatorPolicyInterface',
),
'DrydockResourceActivationFailureLogType' => 'DrydockLogType',
'DrydockResourceActivationYieldLogType' => 'DrydockLogType',
'DrydockResourceAllocationFailureLogType' => 'DrydockLogType',
'DrydockResourceController' => 'DrydockController',
'DrydockResourceDatasource' => 'PhabricatorTypeaheadDatasource',
'DrydockResourceListController' => 'DrydockResourceController',
'DrydockResourceListView' => 'AphrontView',
'DrydockResourceLockException' => 'Exception',
'DrydockResourcePHIDType' => 'PhabricatorPHIDType',
'DrydockResourceQuery' => 'DrydockQuery',
'DrydockResourceReclaimLogType' => 'DrydockLogType',
'DrydockResourceReleaseController' => 'DrydockResourceController',
'DrydockResourceSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DrydockResourceStatus' => 'PhabricatorObjectStatus',
'DrydockResourceUpdateWorker' => 'DrydockWorker',
'DrydockResourceViewController' => 'DrydockResourceController',
'DrydockSFTPFilesystemInterface' => 'DrydockFilesystemInterface',
'DrydockSSHCommandInterface' => 'DrydockCommandInterface',
'DrydockSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'DrydockSlotLock' => 'DrydockDAO',
'DrydockSlotLockException' => 'Exception',
'DrydockSlotLockFailureLogType' => 'DrydockLogType',
'DrydockTestRepositoryOperation' => 'DrydockRepositoryOperationType',
'DrydockTextLogType' => 'DrydockLogType',
'DrydockWebrootInterface' => 'DrydockInterface',
'DrydockWorker' => 'PhabricatorWorker',
'DrydockWorkingCopyBlueprintImplementation' => 'DrydockBlueprintImplementation',
'EdgeSearchConduitAPIMethod' => 'ConduitAPIMethod',
'FeedConduitAPIMethod' => 'ConduitAPIMethod',
'FeedPublishConduitAPIMethod' => 'FeedConduitAPIMethod',
'FeedPublisherHTTPWorker' => 'FeedPushWorker',
'FeedPublisherWorker' => 'FeedPushWorker',
'FeedPushWorker' => 'PhabricatorWorker',
'FeedQueryConduitAPIMethod' => 'FeedConduitAPIMethod',
'FeedStoryNotificationGarbageCollector' => 'PhabricatorGarbageCollector',
'FileAllocateConduitAPIMethod' => 'FileConduitAPIMethod',
'FileConduitAPIMethod' => 'ConduitAPIMethod',
- 'FileCreateMailReceiver' => 'PhabricatorMailReceiver',
+ 'FileCreateMailReceiver' => 'PhabricatorApplicationMailReceiver',
'FileDeletionWorker' => 'PhabricatorWorker',
'FileDownloadConduitAPIMethod' => 'FileConduitAPIMethod',
'FileInfoConduitAPIMethod' => 'FileConduitAPIMethod',
'FileMailReceiver' => 'PhabricatorObjectMailReceiver',
'FileQueryChunksConduitAPIMethod' => 'FileConduitAPIMethod',
'FileReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'FileTypeIcon' => 'Phobject',
'FileUploadChunkConduitAPIMethod' => 'FileConduitAPIMethod',
'FileUploadConduitAPIMethod' => 'FileConduitAPIMethod',
'FileUploadHashConduitAPIMethod' => 'FileConduitAPIMethod',
'FilesDefaultViewCapability' => 'PhabricatorPolicyCapability',
'FlagConduitAPIMethod' => 'ConduitAPIMethod',
'FlagDeleteConduitAPIMethod' => 'FlagConduitAPIMethod',
'FlagEditConduitAPIMethod' => 'FlagConduitAPIMethod',
'FlagQueryConduitAPIMethod' => 'FlagConduitAPIMethod',
'FundBacker' => array(
'FundDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
),
'FundBackerCart' => 'PhortuneCartImplementation',
'FundBackerEditor' => 'PhabricatorApplicationTransactionEditor',
'FundBackerListController' => 'FundController',
'FundBackerPHIDType' => 'PhabricatorPHIDType',
'FundBackerProduct' => 'PhortuneProductImplementation',
'FundBackerQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'FundBackerRefundTransaction' => 'FundBackerTransactionType',
'FundBackerSearchEngine' => 'PhabricatorApplicationSearchEngine',
'FundBackerStatusTransaction' => 'FundBackerTransactionType',
'FundBackerTransaction' => 'PhabricatorModularTransaction',
'FundBackerTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'FundBackerTransactionType' => 'PhabricatorModularTransactionType',
'FundController' => 'PhabricatorController',
'FundCreateInitiativesCapability' => 'PhabricatorPolicyCapability',
'FundDAO' => 'PhabricatorLiskDAO',
'FundDefaultViewCapability' => 'PhabricatorPolicyCapability',
'FundInitiative' => array(
'FundDAO',
'PhabricatorPolicyInterface',
'PhabricatorProjectInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorSubscribableInterface',
'PhabricatorMentionableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorDestructibleInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
),
'FundInitiativeBackController' => 'FundController',
'FundInitiativeBackerTransaction' => 'FundInitiativeTransactionType',
'FundInitiativeCloseController' => 'FundController',
'FundInitiativeDescriptionTransaction' => 'FundInitiativeTransactionType',
'FundInitiativeEditController' => 'FundController',
'FundInitiativeEditEngine' => 'PhabricatorEditEngine',
'FundInitiativeEditor' => 'PhabricatorApplicationTransactionEditor',
'FundInitiativeFerretEngine' => 'PhabricatorFerretEngine',
'FundInitiativeFulltextEngine' => 'PhabricatorFulltextEngine',
'FundInitiativeListController' => 'FundController',
'FundInitiativeMerchantTransaction' => 'FundInitiativeTransactionType',
'FundInitiativeNameTransaction' => 'FundInitiativeTransactionType',
'FundInitiativePHIDType' => 'PhabricatorPHIDType',
'FundInitiativeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'FundInitiativeRefundTransaction' => 'FundInitiativeTransactionType',
'FundInitiativeRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'FundInitiativeReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'FundInitiativeRisksTransaction' => 'FundInitiativeTransactionType',
'FundInitiativeSearchEngine' => 'PhabricatorApplicationSearchEngine',
'FundInitiativeStatusTransaction' => 'FundInitiativeTransactionType',
'FundInitiativeTransaction' => 'PhabricatorModularTransaction',
'FundInitiativeTransactionComment' => 'PhabricatorApplicationTransactionComment',
'FundInitiativeTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'FundInitiativeTransactionType' => 'PhabricatorModularTransactionType',
'FundInitiativeViewController' => 'FundController',
'FundSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'HarbormasterAbortOlderBuildsBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterArcLintBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterArcUnitBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterArtifact' => 'Phobject',
'HarbormasterAutotargetsTestCase' => 'PhabricatorTestCase',
'HarbormasterBuild' => array(
'HarbormasterDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorConduitResultInterface',
'PhabricatorDestructibleInterface',
),
'HarbormasterBuildAbortedException' => 'Exception',
'HarbormasterBuildActionController' => 'HarbormasterController',
'HarbormasterBuildArcanistAutoplan' => 'HarbormasterBuildAutoplan',
'HarbormasterBuildArtifact' => array(
'HarbormasterDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'HarbormasterBuildArtifactPHIDType' => 'PhabricatorPHIDType',
'HarbormasterBuildArtifactQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildAutoplan' => 'Phobject',
'HarbormasterBuildCommand' => 'HarbormasterDAO',
'HarbormasterBuildDependencyDatasource' => 'PhabricatorTypeaheadDatasource',
'HarbormasterBuildEngine' => 'Phobject',
'HarbormasterBuildFailureException' => 'Exception',
'HarbormasterBuildGraph' => 'AbstractDirectedGraph',
'HarbormasterBuildInitiatorDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'HarbormasterBuildLintMessage' => 'HarbormasterDAO',
'HarbormasterBuildListController' => 'HarbormasterController',
'HarbormasterBuildLog' => array(
'HarbormasterDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorConduitResultInterface',
),
'HarbormasterBuildLogChunk' => 'HarbormasterDAO',
'HarbormasterBuildLogChunkIterator' => 'PhutilBufferedIterator',
'HarbormasterBuildLogDownloadController' => 'HarbormasterController',
'HarbormasterBuildLogPHIDType' => 'PhabricatorPHIDType',
'HarbormasterBuildLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildLogRenderController' => 'HarbormasterController',
'HarbormasterBuildLogSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'HarbormasterBuildLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
'HarbormasterBuildLogTestCase' => 'PhabricatorTestCase',
'HarbormasterBuildLogView' => 'AphrontView',
'HarbormasterBuildLogViewController' => 'HarbormasterController',
'HarbormasterBuildMessage' => array(
'HarbormasterDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'HarbormasterBuildMessageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildPHIDType' => 'PhabricatorPHIDType',
'HarbormasterBuildPlan' => array(
'HarbormasterDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorSubscribableInterface',
'PhabricatorNgramsInterface',
'PhabricatorConduitResultInterface',
'PhabricatorProjectInterface',
),
'HarbormasterBuildPlanDatasource' => 'PhabricatorTypeaheadDatasource',
'HarbormasterBuildPlanDefaultEditCapability' => 'PhabricatorPolicyCapability',
'HarbormasterBuildPlanDefaultViewCapability' => 'PhabricatorPolicyCapability',
'HarbormasterBuildPlanEditEngine' => 'PhabricatorEditEngine',
'HarbormasterBuildPlanEditor' => 'PhabricatorApplicationTransactionEditor',
'HarbormasterBuildPlanNameNgrams' => 'PhabricatorSearchNgrams',
'HarbormasterBuildPlanPHIDType' => 'PhabricatorPHIDType',
'HarbormasterBuildPlanQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildPlanSearchAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'HarbormasterBuildPlanSearchEngine' => 'PhabricatorApplicationSearchEngine',
'HarbormasterBuildPlanTransaction' => 'PhabricatorApplicationTransaction',
'HarbormasterBuildPlanTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'HarbormasterBuildQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildRequest' => 'Phobject',
'HarbormasterBuildSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'HarbormasterBuildSearchEngine' => 'PhabricatorApplicationSearchEngine',
'HarbormasterBuildStatus' => 'Phobject',
'HarbormasterBuildStatusDatasource' => 'PhabricatorTypeaheadDatasource',
'HarbormasterBuildStep' => array(
'HarbormasterDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorCustomFieldInterface',
),
'HarbormasterBuildStepCoreCustomField' => array(
'HarbormasterBuildStepCustomField',
'PhabricatorStandardCustomFieldInterface',
),
'HarbormasterBuildStepCustomField' => 'PhabricatorCustomField',
'HarbormasterBuildStepEditor' => 'PhabricatorApplicationTransactionEditor',
'HarbormasterBuildStepGroup' => 'Phobject',
'HarbormasterBuildStepImplementation' => 'Phobject',
'HarbormasterBuildStepImplementationTestCase' => 'PhabricatorTestCase',
'HarbormasterBuildStepPHIDType' => 'PhabricatorPHIDType',
'HarbormasterBuildStepQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildStepTransaction' => 'PhabricatorApplicationTransaction',
'HarbormasterBuildStepTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'HarbormasterBuildTarget' => array(
'HarbormasterDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorConduitResultInterface',
),
'HarbormasterBuildTargetPHIDType' => 'PhabricatorPHIDType',
'HarbormasterBuildTargetQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildTargetSearchEngine' => 'PhabricatorApplicationSearchEngine',
'HarbormasterBuildTransaction' => 'PhabricatorApplicationTransaction',
'HarbormasterBuildTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'HarbormasterBuildTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'HarbormasterBuildUnitMessage' => 'HarbormasterDAO',
'HarbormasterBuildViewController' => 'HarbormasterController',
'HarbormasterBuildWorker' => 'HarbormasterWorker',
'HarbormasterBuildable' => array(
'HarbormasterDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'HarbormasterBuildableInterface',
'PhabricatorConduitResultInterface',
'PhabricatorDestructibleInterface',
),
'HarbormasterBuildableActionController' => 'HarbormasterController',
'HarbormasterBuildableEngine' => 'Phobject',
'HarbormasterBuildableListController' => 'HarbormasterController',
'HarbormasterBuildablePHIDType' => 'PhabricatorPHIDType',
'HarbormasterBuildableQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HarbormasterBuildableSearchAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'HarbormasterBuildableSearchEngine' => 'PhabricatorApplicationSearchEngine',
'HarbormasterBuildableStatus' => 'Phobject',
'HarbormasterBuildableTransaction' => 'PhabricatorApplicationTransaction',
'HarbormasterBuildableTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'HarbormasterBuildableTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'HarbormasterBuildableViewController' => 'HarbormasterController',
'HarbormasterBuildkiteBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterBuildkiteHookController' => 'HarbormasterController',
'HarbormasterBuiltinBuildStepGroup' => 'HarbormasterBuildStepGroup',
'HarbormasterCircleCIBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterCircleCIHookController' => 'HarbormasterController',
'HarbormasterConduitAPIMethod' => 'ConduitAPIMethod',
'HarbormasterControlBuildStepGroup' => 'HarbormasterBuildStepGroup',
'HarbormasterController' => 'PhabricatorController',
'HarbormasterCreateArtifactConduitAPIMethod' => 'HarbormasterConduitAPIMethod',
'HarbormasterCreatePlansCapability' => 'PhabricatorPolicyCapability',
'HarbormasterDAO' => 'PhabricatorLiskDAO',
'HarbormasterDrydockBuildStepGroup' => 'HarbormasterBuildStepGroup',
'HarbormasterDrydockCommandBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterDrydockLeaseArtifact' => 'HarbormasterArtifact',
'HarbormasterExecFuture' => 'Future',
'HarbormasterExternalBuildStepGroup' => 'HarbormasterBuildStepGroup',
'HarbormasterFileArtifact' => 'HarbormasterArtifact',
'HarbormasterHTTPRequestBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterHostArtifact' => 'HarbormasterDrydockLeaseArtifact',
'HarbormasterLeaseWorkingCopyBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterLintMessagesController' => 'HarbormasterController',
'HarbormasterLintPropertyView' => 'AphrontView',
'HarbormasterLogWorker' => 'HarbormasterWorker',
'HarbormasterManagementArchiveLogsWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterManagementBuildWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterManagementPublishWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterManagementRebuildLogWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterManagementRestartWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterManagementUpdateWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterManagementWorkflow' => 'PhabricatorManagementWorkflow',
'HarbormasterManagementWriteLogWorkflow' => 'HarbormasterManagementWorkflow',
'HarbormasterMessageType' => 'Phobject',
'HarbormasterObject' => 'HarbormasterDAO',
'HarbormasterOtherBuildStepGroup' => 'HarbormasterBuildStepGroup',
'HarbormasterPlanController' => 'HarbormasterController',
'HarbormasterPlanDisableController' => 'HarbormasterPlanController',
'HarbormasterPlanEditController' => 'HarbormasterPlanController',
'HarbormasterPlanListController' => 'HarbormasterPlanController',
'HarbormasterPlanRunController' => 'HarbormasterPlanController',
'HarbormasterPlanViewController' => 'HarbormasterPlanController',
'HarbormasterPrototypeBuildStepGroup' => 'HarbormasterBuildStepGroup',
'HarbormasterPublishFragmentBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterQueryAutotargetsConduitAPIMethod' => 'HarbormasterConduitAPIMethod',
'HarbormasterQueryBuildablesConduitAPIMethod' => 'HarbormasterConduitAPIMethod',
'HarbormasterQueryBuildsConduitAPIMethod' => 'HarbormasterConduitAPIMethod',
'HarbormasterQueryBuildsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'HarbormasterRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'HarbormasterRunBuildPlansHeraldAction' => 'HeraldAction',
'HarbormasterSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'HarbormasterScratchTable' => 'HarbormasterDAO',
'HarbormasterSendMessageConduitAPIMethod' => 'HarbormasterConduitAPIMethod',
'HarbormasterSleepBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterStepAddController' => 'HarbormasterPlanController',
'HarbormasterStepDeleteController' => 'HarbormasterPlanController',
'HarbormasterStepEditController' => 'HarbormasterPlanController',
'HarbormasterStepViewController' => 'HarbormasterPlanController',
'HarbormasterTargetEngine' => 'Phobject',
'HarbormasterTargetSearchAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'HarbormasterTargetWorker' => 'HarbormasterWorker',
'HarbormasterTestBuildStepGroup' => 'HarbormasterBuildStepGroup',
'HarbormasterThrowExceptionBuildStep' => 'HarbormasterBuildStepImplementation',
'HarbormasterUIEventListener' => 'PhabricatorEventListener',
'HarbormasterURIArtifact' => 'HarbormasterArtifact',
'HarbormasterUnitMessageListController' => 'HarbormasterController',
'HarbormasterUnitMessageViewController' => 'HarbormasterController',
'HarbormasterUnitPropertyView' => 'AphrontView',
'HarbormasterUnitStatus' => 'Phobject',
'HarbormasterUnitSummaryView' => 'AphrontView',
'HarbormasterUploadArtifactBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterWaitForPreviousBuildStepImplementation' => 'HarbormasterBuildStepImplementation',
'HarbormasterWorker' => 'PhabricatorWorker',
'HarbormasterWorkingCopyArtifact' => 'HarbormasterDrydockLeaseArtifact',
'HeraldActingUserField' => 'HeraldField',
'HeraldAction' => 'Phobject',
'HeraldActionGroup' => 'HeraldGroup',
'HeraldActionRecord' => 'HeraldDAO',
'HeraldAdapter' => 'Phobject',
'HeraldAdapterDatasource' => 'PhabricatorTypeaheadDatasource',
'HeraldAlwaysField' => 'HeraldField',
'HeraldAnotherRuleField' => 'HeraldField',
'HeraldApplicationActionGroup' => 'HeraldActionGroup',
'HeraldApplyTranscript' => 'Phobject',
'HeraldBasicFieldGroup' => 'HeraldFieldGroup',
'HeraldBuildableState' => 'HeraldState',
'HeraldCallWebhookAction' => 'HeraldAction',
'HeraldCommentAction' => 'HeraldAction',
'HeraldCommitAdapter' => array(
'HeraldAdapter',
'HarbormasterBuildableAdapterInterface',
),
'HeraldCondition' => 'HeraldDAO',
'HeraldConditionTranscript' => 'Phobject',
'HeraldContentSourceField' => 'HeraldField',
'HeraldController' => 'PhabricatorController',
'HeraldCoreStateReasons' => 'HeraldStateReasons',
'HeraldCreateWebhooksCapability' => 'PhabricatorPolicyCapability',
'HeraldDAO' => 'PhabricatorLiskDAO',
'HeraldDeprecatedFieldGroup' => 'HeraldFieldGroup',
'HeraldDifferentialAdapter' => 'HeraldAdapter',
'HeraldDifferentialDiffAdapter' => 'HeraldDifferentialAdapter',
'HeraldDifferentialRevisionAdapter' => array(
'HeraldDifferentialAdapter',
'HarbormasterBuildableAdapterInterface',
),
'HeraldDisableController' => 'HeraldController',
'HeraldDoNothingAction' => 'HeraldAction',
'HeraldEditFieldGroup' => 'HeraldFieldGroup',
'HeraldEffect' => 'Phobject',
'HeraldEmptyFieldValue' => 'HeraldFieldValue',
'HeraldEngine' => 'Phobject',
'HeraldExactProjectsField' => 'HeraldField',
'HeraldField' => 'Phobject',
'HeraldFieldGroup' => 'HeraldGroup',
'HeraldFieldTestCase' => 'PhutilTestCase',
'HeraldFieldValue' => 'Phobject',
'HeraldGroup' => 'Phobject',
'HeraldInvalidActionException' => 'Exception',
'HeraldInvalidConditionException' => 'Exception',
'HeraldMailableState' => 'HeraldState',
'HeraldManageGlobalRulesCapability' => 'PhabricatorPolicyCapability',
'HeraldManagementWorkflow' => 'PhabricatorManagementWorkflow',
'HeraldManiphestTaskAdapter' => 'HeraldAdapter',
'HeraldNewController' => 'HeraldController',
'HeraldNewObjectField' => 'HeraldField',
'HeraldNotifyActionGroup' => 'HeraldActionGroup',
'HeraldObjectTranscript' => 'Phobject',
'HeraldPhameBlogAdapter' => 'HeraldAdapter',
'HeraldPhamePostAdapter' => 'HeraldAdapter',
'HeraldPholioMockAdapter' => 'HeraldAdapter',
'HeraldPonderQuestionAdapter' => 'HeraldAdapter',
'HeraldPreCommitAdapter' => 'HeraldAdapter',
'HeraldPreCommitContentAdapter' => 'HeraldPreCommitAdapter',
'HeraldPreCommitRefAdapter' => 'HeraldPreCommitAdapter',
'HeraldPreventActionGroup' => 'HeraldActionGroup',
'HeraldProjectsField' => 'HeraldField',
'HeraldRecursiveConditionsException' => 'Exception',
'HeraldRelatedFieldGroup' => 'HeraldFieldGroup',
'HeraldRemarkupFieldValue' => 'HeraldFieldValue',
'HeraldRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'HeraldRule' => array(
'HeraldDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorFlaggableInterface',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSubscribableInterface',
),
'HeraldRuleAdapter' => 'HeraldAdapter',
'HeraldRuleAdapterField' => 'HeraldRuleField',
'HeraldRuleController' => 'HeraldController',
'HeraldRuleDatasource' => 'PhabricatorTypeaheadDatasource',
'HeraldRuleEditor' => 'PhabricatorApplicationTransactionEditor',
'HeraldRuleField' => 'HeraldField',
'HeraldRuleFieldGroup' => 'HeraldFieldGroup',
'HeraldRuleListController' => 'HeraldController',
'HeraldRulePHIDType' => 'PhabricatorPHIDType',
'HeraldRuleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HeraldRuleReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'HeraldRuleSearchEngine' => 'PhabricatorApplicationSearchEngine',
'HeraldRuleSerializer' => 'Phobject',
'HeraldRuleTestCase' => 'PhabricatorTestCase',
'HeraldRuleTransaction' => 'PhabricatorApplicationTransaction',
'HeraldRuleTransactionComment' => 'PhabricatorApplicationTransactionComment',
'HeraldRuleTranscript' => 'Phobject',
'HeraldRuleTypeConfig' => 'Phobject',
'HeraldRuleTypeDatasource' => 'PhabricatorTypeaheadDatasource',
'HeraldRuleTypeField' => 'HeraldRuleField',
'HeraldRuleViewController' => 'HeraldController',
'HeraldSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'HeraldSelectFieldValue' => 'HeraldFieldValue',
'HeraldSpaceField' => 'HeraldField',
'HeraldState' => 'Phobject',
'HeraldStateReasons' => 'Phobject',
'HeraldSubscribersField' => 'HeraldField',
'HeraldSupportActionGroup' => 'HeraldActionGroup',
'HeraldSupportFieldGroup' => 'HeraldFieldGroup',
'HeraldTestConsoleController' => 'HeraldController',
'HeraldTestManagementWorkflow' => 'HeraldManagementWorkflow',
'HeraldTextFieldValue' => 'HeraldFieldValue',
'HeraldTokenizerFieldValue' => 'HeraldFieldValue',
'HeraldTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'HeraldTranscript' => array(
'HeraldDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'HeraldTranscriptController' => 'HeraldController',
'HeraldTranscriptDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'HeraldTranscriptGarbageCollector' => 'PhabricatorGarbageCollector',
'HeraldTranscriptListController' => 'HeraldController',
'HeraldTranscriptPHIDType' => 'PhabricatorPHIDType',
'HeraldTranscriptQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HeraldTranscriptSearchEngine' => 'PhabricatorApplicationSearchEngine',
'HeraldTranscriptTestCase' => 'PhabricatorTestCase',
'HeraldUtilityActionGroup' => 'HeraldActionGroup',
'HeraldWebhook' => array(
'HeraldDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
'PhabricatorProjectInterface',
),
'HeraldWebhookCallManagementWorkflow' => 'HeraldWebhookManagementWorkflow',
'HeraldWebhookController' => 'HeraldController',
'HeraldWebhookDatasource' => 'PhabricatorTypeaheadDatasource',
'HeraldWebhookEditController' => 'HeraldWebhookController',
'HeraldWebhookEditEngine' => 'PhabricatorEditEngine',
'HeraldWebhookEditor' => 'PhabricatorApplicationTransactionEditor',
'HeraldWebhookKeyController' => 'HeraldWebhookController',
'HeraldWebhookListController' => 'HeraldWebhookController',
'HeraldWebhookManagementWorkflow' => 'PhabricatorManagementWorkflow',
'HeraldWebhookNameTransaction' => 'HeraldWebhookTransactionType',
'HeraldWebhookPHIDType' => 'PhabricatorPHIDType',
'HeraldWebhookQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HeraldWebhookRequest' => array(
'HeraldDAO',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
),
'HeraldWebhookRequestGarbageCollector' => 'PhabricatorGarbageCollector',
'HeraldWebhookRequestListView' => 'AphrontView',
'HeraldWebhookRequestPHIDType' => 'PhabricatorPHIDType',
'HeraldWebhookRequestQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'HeraldWebhookSearchEngine' => 'PhabricatorApplicationSearchEngine',
'HeraldWebhookStatusTransaction' => 'HeraldWebhookTransactionType',
'HeraldWebhookTestController' => 'HeraldWebhookController',
'HeraldWebhookTransaction' => 'PhabricatorModularTransaction',
'HeraldWebhookTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'HeraldWebhookTransactionType' => 'PhabricatorModularTransactionType',
'HeraldWebhookURITransaction' => 'HeraldWebhookTransactionType',
'HeraldWebhookViewController' => 'HeraldWebhookController',
'HeraldWebhookWorker' => 'PhabricatorWorker',
'Javelin' => 'Phobject',
'LegalpadController' => 'PhabricatorController',
'LegalpadCreateDocumentsCapability' => 'PhabricatorPolicyCapability',
'LegalpadDAO' => 'PhabricatorLiskDAO',
'LegalpadDefaultEditCapability' => 'PhabricatorPolicyCapability',
'LegalpadDefaultViewCapability' => 'PhabricatorPolicyCapability',
'LegalpadDocument' => array(
'LegalpadDAO',
'PhabricatorPolicyInterface',
'PhabricatorSubscribableInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
),
'LegalpadDocumentBody' => array(
'LegalpadDAO',
'PhabricatorMarkupInterface',
),
'LegalpadDocumentDatasource' => 'PhabricatorTypeaheadDatasource',
'LegalpadDocumentDoneController' => 'LegalpadController',
'LegalpadDocumentEditController' => 'LegalpadController',
'LegalpadDocumentEditEngine' => 'PhabricatorEditEngine',
'LegalpadDocumentEditor' => 'PhabricatorApplicationTransactionEditor',
'LegalpadDocumentListController' => 'LegalpadController',
'LegalpadDocumentManageController' => 'LegalpadController',
'LegalpadDocumentPreambleTransaction' => 'LegalpadDocumentTransactionType',
'LegalpadDocumentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'LegalpadDocumentRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'LegalpadDocumentRequireSignatureTransaction' => 'LegalpadDocumentTransactionType',
'LegalpadDocumentSearchEngine' => 'PhabricatorApplicationSearchEngine',
'LegalpadDocumentSignController' => 'LegalpadController',
'LegalpadDocumentSignature' => array(
'LegalpadDAO',
'PhabricatorPolicyInterface',
),
'LegalpadDocumentSignatureAddController' => 'LegalpadController',
'LegalpadDocumentSignatureListController' => 'LegalpadController',
'LegalpadDocumentSignatureQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'LegalpadDocumentSignatureSearchEngine' => 'PhabricatorApplicationSearchEngine',
'LegalpadDocumentSignatureTypeTransaction' => 'LegalpadDocumentTransactionType',
'LegalpadDocumentSignatureVerificationController' => 'LegalpadController',
'LegalpadDocumentSignatureViewController' => 'LegalpadController',
'LegalpadDocumentTextTransaction' => 'LegalpadDocumentTransactionType',
'LegalpadDocumentTitleTransaction' => 'LegalpadDocumentTransactionType',
'LegalpadDocumentTransactionType' => 'PhabricatorModularTransactionType',
'LegalpadMailReceiver' => 'PhabricatorObjectMailReceiver',
'LegalpadObjectNeedsSignatureEdgeType' => 'PhabricatorEdgeType',
'LegalpadReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'LegalpadRequireSignatureHeraldAction' => 'HeraldAction',
'LegalpadSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'LegalpadSignatureNeededByObjectEdgeType' => 'PhabricatorEdgeType',
'LegalpadTransaction' => 'PhabricatorModularTransaction',
'LegalpadTransactionComment' => 'PhabricatorApplicationTransactionComment',
'LegalpadTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
- 'LegalpadTransactionView' => 'PhabricatorApplicationTransactionView',
'LiskChunkTestCase' => 'PhabricatorTestCase',
'LiskDAO' => array(
'Phobject',
'AphrontDatabaseTableRefInterface',
),
'LiskDAOTestCase' => 'PhabricatorTestCase',
'LiskEphemeralObjectException' => 'Exception',
'LiskFixtureTestCase' => 'PhabricatorTestCase',
'LiskIsolationTestCase' => 'PhabricatorTestCase',
'LiskIsolationTestDAO' => 'LiskDAO',
'LiskIsolationTestDAOException' => 'Exception',
'LiskMigrationIterator' => 'PhutilBufferedIterator',
'LiskRawMigrationIterator' => 'PhutilBufferedIterator',
'MacroConduitAPIMethod' => 'ConduitAPIMethod',
'MacroCreateMemeConduitAPIMethod' => 'MacroConduitAPIMethod',
'MacroEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'MacroEmojiExample' => 'PhabricatorUIExample',
'MacroQueryConduitAPIMethod' => 'MacroConduitAPIMethod',
'ManiphestAssignEmailCommand' => 'ManiphestEmailCommand',
'ManiphestAssigneeDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'ManiphestBulkEditCapability' => 'PhabricatorPolicyCapability',
'ManiphestBulkEditController' => 'ManiphestController',
'ManiphestClaimEmailCommand' => 'ManiphestEmailCommand',
'ManiphestCloseEmailCommand' => 'ManiphestEmailCommand',
'ManiphestConduitAPIMethod' => 'ConduitAPIMethod',
'ManiphestConfiguredCustomField' => array(
'ManiphestCustomField',
'PhabricatorStandardCustomFieldInterface',
),
'ManiphestConstants' => 'Phobject',
'ManiphestController' => 'PhabricatorController',
- 'ManiphestCreateMailReceiver' => 'PhabricatorMailReceiver',
+ 'ManiphestCreateMailReceiver' => 'PhabricatorApplicationMailReceiver',
'ManiphestCreateTaskConduitAPIMethod' => 'ManiphestConduitAPIMethod',
'ManiphestCustomField' => 'PhabricatorCustomField',
'ManiphestCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage',
'ManiphestCustomFieldStatusParser' => 'PhabricatorCustomFieldMonogramParser',
'ManiphestCustomFieldStatusParserTestCase' => 'PhabricatorTestCase',
'ManiphestCustomFieldStorage' => 'PhabricatorCustomFieldStorage',
'ManiphestCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage',
'ManiphestDAO' => 'PhabricatorLiskDAO',
'ManiphestDefaultEditCapability' => 'PhabricatorPolicyCapability',
'ManiphestDefaultViewCapability' => 'PhabricatorPolicyCapability',
'ManiphestEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'ManiphestEditEngine' => 'PhabricatorEditEngine',
'ManiphestEmailCommand' => 'MetaMTAEmailTransactionCommand',
'ManiphestGetTaskTransactionsConduitAPIMethod' => 'ManiphestConduitAPIMethod',
'ManiphestHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension',
'ManiphestInfoConduitAPIMethod' => 'ManiphestConduitAPIMethod',
'ManiphestMailEngineExtension' => 'PhabricatorMailEngineExtension',
'ManiphestNameIndex' => 'ManiphestDAO',
'ManiphestPointsConfigType' => 'PhabricatorJSONConfigType',
'ManiphestPrioritiesConfigType' => 'PhabricatorJSONConfigType',
'ManiphestPriorityEmailCommand' => 'ManiphestEmailCommand',
'ManiphestPrioritySearchConduitAPIMethod' => 'ManiphestConduitAPIMethod',
'ManiphestProjectNameFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension',
'ManiphestQueryConduitAPIMethod' => 'ManiphestConduitAPIMethod',
'ManiphestQueryStatusesConduitAPIMethod' => 'ManiphestConduitAPIMethod',
'ManiphestRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'ManiphestReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'ManiphestReportController' => 'ManiphestController',
'ManiphestSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'ManiphestSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'ManiphestStatusEmailCommand' => 'ManiphestEmailCommand',
'ManiphestStatusSearchConduitAPIMethod' => 'ManiphestConduitAPIMethod',
'ManiphestStatusesConfigType' => 'PhabricatorJSONConfigType',
'ManiphestSubpriorityController' => 'ManiphestController',
'ManiphestSubtypesConfigType' => 'PhabricatorJSONConfigType',
'ManiphestTask' => array(
'ManiphestDAO',
'PhabricatorSubscribableInterface',
'PhabricatorMarkupInterface',
'PhabricatorPolicyInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorFlaggableInterface',
'PhabricatorMentionableInterface',
'PhrequentTrackableInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorDestructibleInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorProjectInterface',
'PhabricatorSpacesInterface',
'PhabricatorConduitResultInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
'DoorkeeperBridgedObjectInterface',
'PhabricatorEditEngineSubtypeInterface',
'PhabricatorEditEngineLockableInterface',
+ 'PhabricatorEditEngineMFAInterface',
),
'ManiphestTaskAssignHeraldAction' => 'HeraldAction',
'ManiphestTaskAssignOtherHeraldAction' => 'ManiphestTaskAssignHeraldAction',
'ManiphestTaskAssignSelfHeraldAction' => 'ManiphestTaskAssignHeraldAction',
'ManiphestTaskAssigneeHeraldField' => 'ManiphestTaskHeraldField',
'ManiphestTaskAttachTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskAuthorHeraldField' => 'ManiphestTaskHeraldField',
'ManiphestTaskAuthorPolicyRule' => 'PhabricatorPolicyRule',
'ManiphestTaskBulkEngine' => 'PhabricatorBulkEngine',
'ManiphestTaskCloseAsDuplicateRelationship' => 'ManiphestTaskRelationship',
'ManiphestTaskClosedStatusDatasource' => 'PhabricatorTypeaheadDatasource',
'ManiphestTaskCoverImageTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskDependedOnByTaskEdgeType' => 'PhabricatorEdgeType',
'ManiphestTaskDependsOnTaskEdgeType' => 'PhabricatorEdgeType',
'ManiphestTaskDescriptionHeraldField' => 'ManiphestTaskHeraldField',
'ManiphestTaskDescriptionTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskDetailController' => 'ManiphestController',
'ManiphestTaskEdgeTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskEditController' => 'ManiphestController',
'ManiphestTaskEditEngineLock' => 'PhabricatorEditEngineLock',
'ManiphestTaskFerretEngine' => 'PhabricatorFerretEngine',
'ManiphestTaskFulltextEngine' => 'PhabricatorFulltextEngine',
'ManiphestTaskGraph' => 'PhabricatorObjectGraph',
'ManiphestTaskHasCommitEdgeType' => 'PhabricatorEdgeType',
'ManiphestTaskHasCommitRelationship' => 'ManiphestTaskRelationship',
'ManiphestTaskHasDuplicateTaskEdgeType' => 'PhabricatorEdgeType',
'ManiphestTaskHasMockEdgeType' => 'PhabricatorEdgeType',
'ManiphestTaskHasMockRelationship' => 'ManiphestTaskRelationship',
'ManiphestTaskHasParentRelationship' => 'ManiphestTaskRelationship',
'ManiphestTaskHasRevisionEdgeType' => 'PhabricatorEdgeType',
'ManiphestTaskHasRevisionRelationship' => 'ManiphestTaskRelationship',
'ManiphestTaskHasSubtaskRelationship' => 'ManiphestTaskRelationship',
'ManiphestTaskHeraldField' => 'HeraldField',
'ManiphestTaskHeraldFieldGroup' => 'HeraldFieldGroup',
'ManiphestTaskIsDuplicateOfTaskEdgeType' => 'PhabricatorEdgeType',
'ManiphestTaskListController' => 'ManiphestController',
'ManiphestTaskListHTTPParameterType' => 'AphrontListHTTPParameterType',
'ManiphestTaskListView' => 'ManiphestView',
+ 'ManiphestTaskMFAEngine' => 'PhabricatorEditEngineMFAEngine',
'ManiphestTaskMailReceiver' => 'PhabricatorObjectMailReceiver',
'ManiphestTaskMergeInRelationship' => 'ManiphestTaskRelationship',
'ManiphestTaskMergedFromTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskMergedIntoTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskOpenStatusDatasource' => 'PhabricatorTypeaheadDatasource',
'ManiphestTaskOwnerTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskPHIDResolver' => 'PhabricatorPHIDResolver',
'ManiphestTaskPHIDType' => 'PhabricatorPHIDType',
'ManiphestTaskParentTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskPoints' => 'Phobject',
'ManiphestTaskPointsTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskPriority' => 'ManiphestConstants',
'ManiphestTaskPriorityDatasource' => 'PhabricatorTypeaheadDatasource',
'ManiphestTaskPriorityHeraldAction' => 'HeraldAction',
'ManiphestTaskPriorityHeraldField' => 'ManiphestTaskHeraldField',
'ManiphestTaskPriorityTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'ManiphestTaskRelationship' => 'PhabricatorObjectRelationship',
'ManiphestTaskRelationshipSource' => 'PhabricatorObjectRelationshipSource',
'ManiphestTaskResultListView' => 'ManiphestView',
'ManiphestTaskSearchEngine' => 'PhabricatorApplicationSearchEngine',
'ManiphestTaskStatus' => 'ManiphestConstants',
'ManiphestTaskStatusDatasource' => 'PhabricatorTypeaheadDatasource',
'ManiphestTaskStatusFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'ManiphestTaskStatusHeraldAction' => 'HeraldAction',
'ManiphestTaskStatusHeraldField' => 'ManiphestTaskHeraldField',
'ManiphestTaskStatusTestCase' => 'PhabricatorTestCase',
'ManiphestTaskStatusTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskSubpriorityTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskSubtaskController' => 'ManiphestController',
'ManiphestTaskSubtypeDatasource' => 'PhabricatorTypeaheadDatasource',
'ManiphestTaskTestCase' => 'PhabricatorTestCase',
'ManiphestTaskTitleHeraldField' => 'ManiphestTaskHeraldField',
'ManiphestTaskTitleTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTaskTransactionType' => 'PhabricatorModularTransactionType',
'ManiphestTaskUnblockTransaction' => 'ManiphestTaskTransactionType',
'ManiphestTransaction' => 'PhabricatorModularTransaction',
'ManiphestTransactionComment' => 'PhabricatorApplicationTransactionComment',
'ManiphestTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'ManiphestTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'ManiphestUpdateConduitAPIMethod' => 'ManiphestConduitAPIMethod',
'ManiphestView' => 'AphrontView',
'MetaMTAEmailTransactionCommand' => 'Phobject',
'MetaMTAEmailTransactionCommandTestCase' => 'PhabricatorTestCase',
'MetaMTAMailReceivedGarbageCollector' => 'PhabricatorGarbageCollector',
'MetaMTAMailSentGarbageCollector' => 'PhabricatorGarbageCollector',
'MetaMTAReceivedMailStatus' => 'Phobject',
'MultimeterContext' => 'MultimeterDimension',
'MultimeterControl' => 'Phobject',
'MultimeterController' => 'PhabricatorController',
'MultimeterDAO' => 'PhabricatorLiskDAO',
'MultimeterDimension' => 'MultimeterDAO',
'MultimeterEvent' => 'MultimeterDAO',
'MultimeterEventGarbageCollector' => 'PhabricatorGarbageCollector',
'MultimeterHost' => 'MultimeterDimension',
'MultimeterLabel' => 'MultimeterDimension',
'MultimeterSampleController' => 'MultimeterController',
'MultimeterViewer' => 'MultimeterDimension',
'NuanceCommandImplementation' => 'Phobject',
'NuanceConduitAPIMethod' => 'ConduitAPIMethod',
'NuanceConsoleController' => 'NuanceController',
'NuanceContentSource' => 'PhabricatorContentSource',
'NuanceController' => 'PhabricatorController',
'NuanceDAO' => 'PhabricatorLiskDAO',
'NuanceFormItemType' => 'NuanceItemType',
'NuanceGitHubEventItemType' => 'NuanceItemType',
'NuanceGitHubImportCursor' => 'NuanceImportCursor',
'NuanceGitHubIssuesImportCursor' => 'NuanceGitHubImportCursor',
'NuanceGitHubRawEvent' => 'Phobject',
'NuanceGitHubRawEventTestCase' => 'PhabricatorTestCase',
'NuanceGitHubRepositoryImportCursor' => 'NuanceGitHubImportCursor',
'NuanceGitHubRepositorySourceDefinition' => 'NuanceSourceDefinition',
'NuanceImportCursor' => 'Phobject',
'NuanceImportCursorData' => array(
'NuanceDAO',
'PhabricatorPolicyInterface',
),
'NuanceImportCursorDataQuery' => 'NuanceQuery',
'NuanceImportCursorPHIDType' => 'PhabricatorPHIDType',
'NuanceItem' => array(
'NuanceDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
),
'NuanceItemActionController' => 'NuanceController',
'NuanceItemCommand' => array(
'NuanceDAO',
'PhabricatorPolicyInterface',
),
'NuanceItemCommandQuery' => 'NuanceQuery',
'NuanceItemCommandSpec' => 'Phobject',
'NuanceItemCommandTransaction' => 'NuanceItemTransactionType',
'NuanceItemController' => 'NuanceController',
'NuanceItemEditor' => 'PhabricatorApplicationTransactionEditor',
'NuanceItemListController' => 'NuanceItemController',
'NuanceItemManageController' => 'NuanceController',
'NuanceItemOwnerTransaction' => 'NuanceItemTransactionType',
'NuanceItemPHIDType' => 'PhabricatorPHIDType',
'NuanceItemPropertyTransaction' => 'NuanceItemTransactionType',
'NuanceItemQuery' => 'NuanceQuery',
'NuanceItemQueueTransaction' => 'NuanceItemTransactionType',
'NuanceItemRequestorTransaction' => 'NuanceItemTransactionType',
'NuanceItemSearchEngine' => 'PhabricatorApplicationSearchEngine',
'NuanceItemSourceTransaction' => 'NuanceItemTransactionType',
'NuanceItemStatusTransaction' => 'NuanceItemTransactionType',
'NuanceItemTransaction' => 'NuanceTransaction',
'NuanceItemTransactionComment' => 'PhabricatorApplicationTransactionComment',
'NuanceItemTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'NuanceItemTransactionType' => 'PhabricatorModularTransactionType',
'NuanceItemType' => 'Phobject',
'NuanceItemUpdateWorker' => 'NuanceWorker',
'NuanceItemViewController' => 'NuanceController',
'NuanceManagementImportWorkflow' => 'NuanceManagementWorkflow',
'NuanceManagementUpdateWorkflow' => 'NuanceManagementWorkflow',
'NuanceManagementWorkflow' => 'PhabricatorManagementWorkflow',
'NuancePhabricatorFormSourceDefinition' => 'NuanceSourceDefinition',
'NuanceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'NuanceQueue' => array(
'NuanceDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
),
'NuanceQueueController' => 'NuanceController',
'NuanceQueueDatasource' => 'PhabricatorTypeaheadDatasource',
'NuanceQueueEditController' => 'NuanceQueueController',
'NuanceQueueEditEngine' => 'PhabricatorEditEngine',
'NuanceQueueEditor' => 'PhabricatorApplicationTransactionEditor',
'NuanceQueueListController' => 'NuanceQueueController',
'NuanceQueueNameTransaction' => 'NuanceQueueTransactionType',
'NuanceQueuePHIDType' => 'PhabricatorPHIDType',
'NuanceQueueQuery' => 'NuanceQuery',
'NuanceQueueSearchEngine' => 'PhabricatorApplicationSearchEngine',
'NuanceQueueTransaction' => 'NuanceTransaction',
'NuanceQueueTransactionComment' => 'PhabricatorApplicationTransactionComment',
'NuanceQueueTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'NuanceQueueTransactionType' => 'PhabricatorModularTransactionType',
'NuanceQueueViewController' => 'NuanceQueueController',
'NuanceQueueWorkController' => 'NuanceQueueController',
'NuanceSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'NuanceSource' => array(
'NuanceDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorNgramsInterface',
),
'NuanceSourceActionController' => 'NuanceController',
'NuanceSourceController' => 'NuanceController',
'NuanceSourceDefaultEditCapability' => 'PhabricatorPolicyCapability',
'NuanceSourceDefaultQueueTransaction' => 'NuanceSourceTransactionType',
'NuanceSourceDefaultViewCapability' => 'PhabricatorPolicyCapability',
'NuanceSourceDefinition' => 'Phobject',
'NuanceSourceDefinitionTestCase' => 'PhabricatorTestCase',
'NuanceSourceEditController' => 'NuanceSourceController',
'NuanceSourceEditEngine' => 'PhabricatorEditEngine',
'NuanceSourceEditor' => 'PhabricatorApplicationTransactionEditor',
'NuanceSourceListController' => 'NuanceSourceController',
'NuanceSourceManageCapability' => 'PhabricatorPolicyCapability',
'NuanceSourceNameNgrams' => 'PhabricatorSearchNgrams',
'NuanceSourceNameTransaction' => 'NuanceSourceTransactionType',
'NuanceSourcePHIDType' => 'PhabricatorPHIDType',
'NuanceSourceQuery' => 'NuanceQuery',
'NuanceSourceSearchEngine' => 'PhabricatorApplicationSearchEngine',
'NuanceSourceTransaction' => 'NuanceTransaction',
'NuanceSourceTransactionComment' => 'PhabricatorApplicationTransactionComment',
'NuanceSourceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'NuanceSourceTransactionType' => 'PhabricatorModularTransactionType',
'NuanceSourceViewController' => 'NuanceSourceController',
'NuanceTransaction' => 'PhabricatorModularTransaction',
'NuanceTrashCommand' => 'NuanceCommandImplementation',
'NuanceWorker' => 'PhabricatorWorker',
'OwnersConduitAPIMethod' => 'ConduitAPIMethod',
'OwnersEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'OwnersPackageReplyHandler' => 'PhabricatorMailReplyHandler',
'OwnersQueryConduitAPIMethod' => 'OwnersConduitAPIMethod',
'OwnersSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PHIDConduitAPIMethod' => 'ConduitAPIMethod',
'PHIDInfoConduitAPIMethod' => 'PHIDConduitAPIMethod',
'PHIDLookupConduitAPIMethod' => 'PHIDConduitAPIMethod',
'PHIDQueryConduitAPIMethod' => 'PHIDConduitAPIMethod',
'PHUI' => 'Phobject',
'PHUIActionPanelExample' => 'PhabricatorUIExample',
'PHUIActionPanelView' => 'AphrontTagView',
'PHUIApplicationMenuView' => 'Phobject',
'PHUIBadgeBoxView' => 'AphrontTagView',
'PHUIBadgeExample' => 'PhabricatorUIExample',
'PHUIBadgeMiniView' => 'AphrontTagView',
'PHUIBadgeView' => 'AphrontTagView',
'PHUIBigInfoExample' => 'PhabricatorUIExample',
'PHUIBigInfoView' => 'AphrontTagView',
'PHUIBoxExample' => 'PhabricatorUIExample',
'PHUIBoxView' => 'AphrontTagView',
'PHUIButtonBarExample' => 'PhabricatorUIExample',
'PHUIButtonBarView' => 'AphrontTagView',
'PHUIButtonExample' => 'PhabricatorUIExample',
'PHUIButtonView' => 'AphrontTagView',
'PHUICMSView' => 'AphrontTagView',
'PHUICalendarDayView' => 'AphrontView',
'PHUICalendarListView' => 'AphrontTagView',
'PHUICalendarMonthView' => 'AphrontView',
'PHUICalendarWeekView' => 'AphrontView',
'PHUICalendarWidgetView' => 'AphrontTagView',
'PHUIColorPalletteExample' => 'PhabricatorUIExample',
'PHUICrumbView' => 'AphrontView',
'PHUICrumbsView' => 'AphrontView',
'PHUICurtainExtension' => 'Phobject',
'PHUICurtainPanelView' => 'AphrontTagView',
'PHUICurtainView' => 'AphrontTagView',
'PHUIDiffGraphView' => 'Phobject',
'PHUIDiffGraphViewTestCase' => 'PhabricatorTestCase',
'PHUIDiffInlineCommentDetailView' => 'PHUIDiffInlineCommentView',
'PHUIDiffInlineCommentEditView' => 'PHUIDiffInlineCommentView',
'PHUIDiffInlineCommentPreviewListView' => 'AphrontView',
'PHUIDiffInlineCommentRowScaffold' => 'AphrontView',
'PHUIDiffInlineCommentTableScaffold' => 'AphrontView',
'PHUIDiffInlineCommentUndoView' => 'PHUIDiffInlineCommentView',
'PHUIDiffInlineCommentView' => 'AphrontView',
'PHUIDiffInlineThreader' => 'Phobject',
'PHUIDiffOneUpInlineCommentRowScaffold' => 'PHUIDiffInlineCommentRowScaffold',
'PHUIDiffRevealIconView' => 'AphrontView',
'PHUIDiffTableOfContentsItemView' => 'AphrontView',
'PHUIDiffTableOfContentsListView' => 'AphrontView',
'PHUIDiffTwoUpInlineCommentRowScaffold' => 'PHUIDiffInlineCommentRowScaffold',
'PHUIDocumentSummaryView' => 'AphrontTagView',
'PHUIDocumentView' => 'AphrontTagView',
'PHUIFeedStoryExample' => 'PhabricatorUIExample',
'PHUIFeedStoryView' => 'AphrontView',
'PHUIFormDividerControl' => 'AphrontFormControl',
'PHUIFormFileControl' => 'AphrontFormControl',
'PHUIFormFreeformDateControl' => 'AphrontFormControl',
'PHUIFormIconSetControl' => 'AphrontFormControl',
'PHUIFormInsetView' => 'AphrontView',
'PHUIFormLayoutView' => 'AphrontView',
'PHUIFormNumberControl' => 'AphrontFormControl',
+ 'PHUIFormTimerControl' => 'AphrontFormControl',
'PHUIHandleListView' => 'AphrontTagView',
'PHUIHandleTagListView' => 'AphrontTagView',
'PHUIHandleView' => 'AphrontView',
'PHUIHeadThingView' => 'AphrontTagView',
'PHUIHeaderView' => 'AphrontTagView',
'PHUIHomeView' => 'AphrontTagView',
'PHUIHovercardUIExample' => 'PhabricatorUIExample',
'PHUIHovercardView' => 'AphrontTagView',
'PHUIIconCircleView' => 'AphrontTagView',
'PHUIIconExample' => 'PhabricatorUIExample',
'PHUIIconView' => 'AphrontTagView',
'PHUIImageMaskExample' => 'PhabricatorUIExample',
'PHUIImageMaskView' => 'AphrontTagView',
'PHUIInfoExample' => 'PhabricatorUIExample',
'PHUIInfoView' => 'AphrontTagView',
'PHUIInvisibleCharacterTestCase' => 'PhabricatorTestCase',
'PHUIInvisibleCharacterView' => 'AphrontView',
'PHUILeftRightExample' => 'PhabricatorUIExample',
'PHUILeftRightView' => 'AphrontTagView',
'PHUIListExample' => 'PhabricatorUIExample',
'PHUIListItemView' => 'AphrontTagView',
'PHUIListView' => 'AphrontTagView',
'PHUIListViewTestCase' => 'PhabricatorTestCase',
'PHUIObjectBoxView' => 'AphrontTagView',
'PHUIObjectItemListExample' => 'PhabricatorUIExample',
'PHUIObjectItemListView' => 'AphrontTagView',
'PHUIObjectItemView' => 'AphrontTagView',
'PHUIPagerView' => 'AphrontView',
'PHUIPinboardItemView' => 'AphrontView',
'PHUIPinboardView' => 'AphrontView',
'PHUIPolicySectionView' => 'AphrontTagView',
'PHUIPropertyGroupView' => 'AphrontTagView',
'PHUIPropertyListExample' => 'PhabricatorUIExample',
'PHUIPropertyListView' => 'AphrontView',
'PHUIRemarkupImageView' => 'AphrontView',
'PHUIRemarkupPreviewPanel' => 'AphrontTagView',
'PHUIRemarkupView' => 'AphrontView',
'PHUISegmentBarSegmentView' => 'AphrontTagView',
'PHUISegmentBarView' => 'AphrontTagView',
'PHUISpacesNamespaceContextView' => 'AphrontView',
'PHUIStatusItemView' => 'AphrontTagView',
'PHUIStatusListView' => 'AphrontTagView',
'PHUITabGroupView' => 'AphrontTagView',
'PHUITabView' => 'AphrontTagView',
'PHUITagExample' => 'PhabricatorUIExample',
'PHUITagView' => 'AphrontTagView',
'PHUITimelineEventView' => 'AphrontView',
'PHUITimelineExample' => 'PhabricatorUIExample',
'PHUITimelineView' => 'AphrontView',
'PHUITwoColumnView' => 'AphrontTagView',
'PHUITypeaheadExample' => 'PhabricatorUIExample',
'PHUIUserAvailabilityView' => 'AphrontTagView',
'PHUIWorkboardView' => 'AphrontTagView',
'PHUIWorkpanelView' => 'AphrontTagView',
'PHUIXComponentsExample' => 'PhabricatorUIExample',
'PassphraseAbstractKey' => 'Phobject',
'PassphraseConduitAPIMethod' => 'ConduitAPIMethod',
'PassphraseController' => 'PhabricatorController',
'PassphraseCredential' => array(
'PassphraseDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorFlaggableInterface',
'PhabricatorSubscribableInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSpacesInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
),
'PassphraseCredentialAuthorPolicyRule' => 'PhabricatorPolicyRule',
'PassphraseCredentialConduitController' => 'PassphraseController',
'PassphraseCredentialConduitTransaction' => 'PassphraseCredentialTransactionType',
'PassphraseCredentialControl' => 'AphrontFormControl',
'PassphraseCredentialCreateController' => 'PassphraseController',
'PassphraseCredentialDescriptionTransaction' => 'PassphraseCredentialTransactionType',
'PassphraseCredentialDestroyController' => 'PassphraseController',
'PassphraseCredentialDestroyTransaction' => 'PassphraseCredentialTransactionType',
'PassphraseCredentialEditController' => 'PassphraseController',
'PassphraseCredentialFerretEngine' => 'PhabricatorFerretEngine',
'PassphraseCredentialFulltextEngine' => 'PhabricatorFulltextEngine',
'PassphraseCredentialListController' => 'PassphraseController',
'PassphraseCredentialLockController' => 'PassphraseController',
'PassphraseCredentialLockTransaction' => 'PassphraseCredentialTransactionType',
'PassphraseCredentialLookedAtTransaction' => 'PassphraseCredentialTransactionType',
'PassphraseCredentialNameTransaction' => 'PassphraseCredentialTransactionType',
'PassphraseCredentialPHIDType' => 'PhabricatorPHIDType',
'PassphraseCredentialPublicController' => 'PassphraseController',
'PassphraseCredentialQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PassphraseCredentialRevealController' => 'PassphraseController',
'PassphraseCredentialSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PassphraseCredentialSecretIDTransaction' => 'PassphraseCredentialTransactionType',
'PassphraseCredentialTransaction' => 'PhabricatorModularTransaction',
'PassphraseCredentialTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'PassphraseCredentialTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PassphraseCredentialTransactionType' => 'PhabricatorModularTransactionType',
'PassphraseCredentialType' => 'Phobject',
'PassphraseCredentialTypeTestCase' => 'PhabricatorTestCase',
'PassphraseCredentialUsernameTransaction' => 'PassphraseCredentialTransactionType',
'PassphraseCredentialViewController' => 'PassphraseController',
'PassphraseDAO' => 'PhabricatorLiskDAO',
'PassphraseDefaultEditCapability' => 'PhabricatorPolicyCapability',
'PassphraseDefaultViewCapability' => 'PhabricatorPolicyCapability',
'PassphraseNoteCredentialType' => 'PassphraseCredentialType',
'PassphrasePasswordCredentialType' => 'PassphraseCredentialType',
'PassphrasePasswordKey' => 'PassphraseAbstractKey',
'PassphraseQueryConduitAPIMethod' => 'PassphraseConduitAPIMethod',
'PassphraseRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PassphraseSSHGeneratedKeyCredentialType' => 'PassphraseSSHPrivateKeyCredentialType',
'PassphraseSSHKey' => 'PassphraseAbstractKey',
'PassphraseSSHPrivateKeyCredentialType' => 'PassphraseCredentialType',
'PassphraseSSHPrivateKeyFileCredentialType' => 'PassphraseSSHPrivateKeyCredentialType',
'PassphraseSSHPrivateKeyTextCredentialType' => 'PassphraseSSHPrivateKeyCredentialType',
'PassphraseSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PassphraseSecret' => 'PassphraseDAO',
'PassphraseTokenCredentialType' => 'PassphraseCredentialType',
'PasteConduitAPIMethod' => 'ConduitAPIMethod',
'PasteCreateConduitAPIMethod' => 'PasteConduitAPIMethod',
- 'PasteCreateMailReceiver' => 'PhabricatorMailReceiver',
+ 'PasteCreateMailReceiver' => 'PhabricatorApplicationMailReceiver',
'PasteDefaultEditCapability' => 'PhabricatorPolicyCapability',
'PasteDefaultViewCapability' => 'PhabricatorPolicyCapability',
'PasteEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PasteEmbedView' => 'AphrontView',
'PasteInfoConduitAPIMethod' => 'PasteConduitAPIMethod',
'PasteLanguageSelectDatasource' => 'PhabricatorTypeaheadDatasource',
'PasteMailReceiver' => 'PhabricatorObjectMailReceiver',
'PasteQueryConduitAPIMethod' => 'PasteConduitAPIMethod',
'PasteReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PasteSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PeopleBrowseUserDirectoryCapability' => 'PhabricatorPolicyCapability',
'PeopleCreateUsersCapability' => 'PhabricatorPolicyCapability',
'PeopleDisableUsersCapability' => 'PhabricatorPolicyCapability',
'PeopleHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension',
'PeopleMainMenuBarExtension' => 'PhabricatorMainMenuBarExtension',
'PeopleUserLogGarbageCollector' => 'PhabricatorGarbageCollector',
'Phabricator404Controller' => 'PhabricatorController',
'PhabricatorAWSConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorAccessControlTestCase' => 'PhabricatorTestCase',
'PhabricatorAccessLog' => 'Phobject',
'PhabricatorAccessLogConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorAccessibilitySetting' => 'PhabricatorSelectSetting',
- 'PhabricatorAccountSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorActionListView' => 'AphrontTagView',
'PhabricatorActionView' => 'AphrontView',
'PhabricatorActivitySettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorAdministratorsPolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorAjaxRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorAlmanacApplication' => 'PhabricatorApplication',
'PhabricatorAmazonAuthProvider' => 'PhabricatorOAuth2AuthProvider',
+ 'PhabricatorAmazonSNSFuture' => 'PhutilAWSFuture',
'PhabricatorAnchorView' => 'AphrontView',
'PhabricatorAphlictManagementDebugWorkflow' => 'PhabricatorAphlictManagementWorkflow',
'PhabricatorAphlictManagementNotifyWorkflow' => 'PhabricatorAphlictManagementWorkflow',
'PhabricatorAphlictManagementRestartWorkflow' => 'PhabricatorAphlictManagementWorkflow',
'PhabricatorAphlictManagementStartWorkflow' => 'PhabricatorAphlictManagementWorkflow',
'PhabricatorAphlictManagementStatusWorkflow' => 'PhabricatorAphlictManagementWorkflow',
'PhabricatorAphlictManagementStopWorkflow' => 'PhabricatorAphlictManagementWorkflow',
'PhabricatorAphlictManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorAphlictSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorAphrontBarUIExample' => 'PhabricatorUIExample',
'PhabricatorAphrontViewTestCase' => 'PhabricatorTestCase',
'PhabricatorAppSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorApplication' => array(
'PhabricatorLiskDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
),
'PhabricatorApplicationApplicationPHIDType' => 'PhabricatorPHIDType',
'PhabricatorApplicationApplicationTransaction' => 'PhabricatorModularTransaction',
'PhabricatorApplicationApplicationTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorApplicationConfigOptions' => 'Phobject',
'PhabricatorApplicationConfigurationPanel' => 'Phobject',
'PhabricatorApplicationConfigurationPanelTestCase' => 'PhabricatorTestCase',
'PhabricatorApplicationDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorApplicationDetailViewController' => 'PhabricatorApplicationsController',
'PhabricatorApplicationEditController' => 'PhabricatorApplicationsController',
'PhabricatorApplicationEditEngine' => 'PhabricatorEditEngine',
'PhabricatorApplicationEditHTTPParameterHelpView' => 'AphrontView',
'PhabricatorApplicationEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorApplicationEmailCommandsController' => 'PhabricatorApplicationsController',
+ 'PhabricatorApplicationMailReceiver' => 'PhabricatorMailReceiver',
'PhabricatorApplicationObjectMailEngineExtension' => 'PhabricatorMailEngineExtension',
'PhabricatorApplicationPanelController' => 'PhabricatorApplicationsController',
'PhabricatorApplicationPolicyChangeTransaction' => 'PhabricatorApplicationTransactionType',
'PhabricatorApplicationProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorApplicationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorApplicationSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorApplicationSearchController' => 'PhabricatorSearchBaseController',
'PhabricatorApplicationSearchEngine' => 'Phobject',
'PhabricatorApplicationSearchEngineTestCase' => 'PhabricatorTestCase',
'PhabricatorApplicationSearchResultView' => 'Phobject',
'PhabricatorApplicationTestCase' => 'PhabricatorTestCase',
'PhabricatorApplicationTransaction' => array(
'PhabricatorLiskDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorApplicationTransactionComment' => array(
'PhabricatorLiskDAO',
'PhabricatorMarkupInterface',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorApplicationTransactionCommentEditController' => 'PhabricatorApplicationTransactionController',
'PhabricatorApplicationTransactionCommentEditor' => 'PhabricatorEditor',
'PhabricatorApplicationTransactionCommentHistoryController' => 'PhabricatorApplicationTransactionController',
'PhabricatorApplicationTransactionCommentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorApplicationTransactionCommentQuoteController' => 'PhabricatorApplicationTransactionController',
'PhabricatorApplicationTransactionCommentRawController' => 'PhabricatorApplicationTransactionController',
'PhabricatorApplicationTransactionCommentRemoveController' => 'PhabricatorApplicationTransactionController',
'PhabricatorApplicationTransactionCommentView' => 'AphrontView',
'PhabricatorApplicationTransactionController' => 'PhabricatorController',
'PhabricatorApplicationTransactionDetailController' => 'PhabricatorApplicationTransactionController',
'PhabricatorApplicationTransactionEditor' => 'PhabricatorEditor',
'PhabricatorApplicationTransactionFeedStory' => 'PhabricatorFeedStory',
'PhabricatorApplicationTransactionNoEffectException' => 'Exception',
'PhabricatorApplicationTransactionNoEffectResponse' => 'AphrontProxyResponse',
'PhabricatorApplicationTransactionPublishWorker' => 'PhabricatorWorker',
'PhabricatorApplicationTransactionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorApplicationTransactionRemarkupPreviewController' => 'PhabricatorApplicationTransactionController',
'PhabricatorApplicationTransactionReplyHandler' => 'PhabricatorMailReplyHandler',
'PhabricatorApplicationTransactionResponse' => 'AphrontProxyResponse',
'PhabricatorApplicationTransactionShowOlderController' => 'PhabricatorApplicationTransactionController',
'PhabricatorApplicationTransactionStructureException' => 'Exception',
'PhabricatorApplicationTransactionTemplatedCommentQuery' => 'PhabricatorApplicationTransactionCommentQuery',
'PhabricatorApplicationTransactionTextDiffDetailView' => 'AphrontView',
'PhabricatorApplicationTransactionTransactionPHIDType' => 'PhabricatorPHIDType',
'PhabricatorApplicationTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorApplicationTransactionValidationError' => 'Phobject',
'PhabricatorApplicationTransactionValidationException' => 'Exception',
'PhabricatorApplicationTransactionValidationResponse' => 'AphrontProxyResponse',
'PhabricatorApplicationTransactionValueController' => 'PhabricatorApplicationTransactionController',
'PhabricatorApplicationTransactionView' => 'AphrontView',
'PhabricatorApplicationTransactionWarningException' => 'Exception',
'PhabricatorApplicationTransactionWarningResponse' => 'AphrontProxyResponse',
'PhabricatorApplicationUninstallController' => 'PhabricatorApplicationsController',
'PhabricatorApplicationUninstallTransaction' => 'PhabricatorApplicationTransactionType',
'PhabricatorApplicationsApplication' => 'PhabricatorApplication',
'PhabricatorApplicationsController' => 'PhabricatorController',
'PhabricatorApplicationsListController' => 'PhabricatorApplicationsController',
'PhabricatorApplyEditField' => 'PhabricatorEditField',
'PhabricatorAsanaAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorAsanaConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorAsanaSubtaskHasObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorAsanaTaskHasObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorAudioDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorAuditActionConstants' => 'Phobject',
'PhabricatorAuditApplication' => 'PhabricatorApplication',
'PhabricatorAuditCommentEditor' => 'PhabricatorEditor',
'PhabricatorAuditController' => 'PhabricatorController',
'PhabricatorAuditEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorAuditInlineComment' => array(
'Phobject',
'PhabricatorInlineCommentInterface',
),
'PhabricatorAuditListView' => 'AphrontView',
'PhabricatorAuditMailReceiver' => 'PhabricatorObjectMailReceiver',
'PhabricatorAuditManagementDeleteWorkflow' => 'PhabricatorAuditManagementWorkflow',
'PhabricatorAuditManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorAuditReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhabricatorAuditStatusConstants' => 'Phobject',
'PhabricatorAuditSynchronizeManagementWorkflow' => 'PhabricatorAuditManagementWorkflow',
'PhabricatorAuditTransaction' => 'PhabricatorModularTransaction',
'PhabricatorAuditTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhabricatorAuditTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorAuditTransactionView' => 'PhabricatorApplicationTransactionView',
'PhabricatorAuditUpdateOwnersManagementWorkflow' => 'PhabricatorAuditManagementWorkflow',
'PhabricatorAuthAccountView' => 'AphrontView',
'PhabricatorAuthApplication' => 'PhabricatorApplication',
'PhabricatorAuthAuthFactorPHIDType' => 'PhabricatorPHIDType',
+ 'PhabricatorAuthAuthFactorProviderPHIDType' => 'PhabricatorPHIDType',
'PhabricatorAuthAuthProviderPHIDType' => 'PhabricatorPHIDType',
+ 'PhabricatorAuthCSRFEngine' => 'Phobject',
+ 'PhabricatorAuthChallenge' => array(
+ 'PhabricatorAuthDAO',
+ 'PhabricatorPolicyInterface',
+ ),
+ 'PhabricatorAuthChallengeGarbageCollector' => 'PhabricatorGarbageCollector',
+ 'PhabricatorAuthChallengePHIDType' => 'PhabricatorPHIDType',
+ 'PhabricatorAuthChallengeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthChangePasswordAction' => 'PhabricatorSystemAction',
'PhabricatorAuthConduitAPIMethod' => 'ConduitAPIMethod',
'PhabricatorAuthConduitTokenRevoker' => 'PhabricatorAuthRevoker',
'PhabricatorAuthConfirmLinkController' => 'PhabricatorAuthController',
+ 'PhabricatorAuthContactNumber' => array(
+ 'PhabricatorAuthDAO',
+ 'PhabricatorApplicationTransactionInterface',
+ 'PhabricatorPolicyInterface',
+ 'PhabricatorDestructibleInterface',
+ 'PhabricatorEditEngineMFAInterface',
+ ),
+ 'PhabricatorAuthContactNumberController' => 'PhabricatorAuthController',
+ 'PhabricatorAuthContactNumberDisableController' => 'PhabricatorAuthContactNumberController',
+ 'PhabricatorAuthContactNumberEditController' => 'PhabricatorAuthContactNumberController',
+ 'PhabricatorAuthContactNumberEditEngine' => 'PhabricatorEditEngine',
+ 'PhabricatorAuthContactNumberEditor' => 'PhabricatorApplicationTransactionEditor',
+ 'PhabricatorAuthContactNumberMFAEngine' => 'PhabricatorEditEngineMFAEngine',
+ 'PhabricatorAuthContactNumberNumberTransaction' => 'PhabricatorAuthContactNumberTransactionType',
+ 'PhabricatorAuthContactNumberPHIDType' => 'PhabricatorPHIDType',
+ 'PhabricatorAuthContactNumberPrimaryController' => 'PhabricatorAuthContactNumberController',
+ 'PhabricatorAuthContactNumberPrimaryTransaction' => 'PhabricatorAuthContactNumberTransactionType',
+ 'PhabricatorAuthContactNumberQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+ 'PhabricatorAuthContactNumberStatusTransaction' => 'PhabricatorAuthContactNumberTransactionType',
+ 'PhabricatorAuthContactNumberTestController' => 'PhabricatorAuthContactNumberController',
+ 'PhabricatorAuthContactNumberTransaction' => 'PhabricatorModularTransaction',
+ 'PhabricatorAuthContactNumberTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
+ 'PhabricatorAuthContactNumberTransactionType' => 'PhabricatorModularTransactionType',
+ 'PhabricatorAuthContactNumberViewController' => 'PhabricatorAuthContactNumberController',
'PhabricatorAuthController' => 'PhabricatorController',
'PhabricatorAuthDAO' => 'PhabricatorLiskDAO',
'PhabricatorAuthDisableController' => 'PhabricatorAuthProviderConfigController',
'PhabricatorAuthDowngradeSessionController' => 'PhabricatorAuthController',
'PhabricatorAuthEditController' => 'PhabricatorAuthProviderConfigController',
'PhabricatorAuthFactor' => 'Phobject',
- 'PhabricatorAuthFactorConfig' => 'PhabricatorAuthDAO',
+ 'PhabricatorAuthFactorConfig' => array(
+ 'PhabricatorAuthDAO',
+ 'PhabricatorPolicyInterface',
+ 'PhabricatorDestructibleInterface',
+ ),
+ 'PhabricatorAuthFactorConfigQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+ 'PhabricatorAuthFactorProvider' => array(
+ 'PhabricatorAuthDAO',
+ 'PhabricatorApplicationTransactionInterface',
+ 'PhabricatorPolicyInterface',
+ 'PhabricatorExtendedPolicyInterface',
+ 'PhabricatorEditEngineMFAInterface',
+ ),
+ 'PhabricatorAuthFactorProviderController' => 'PhabricatorAuthProviderController',
+ 'PhabricatorAuthFactorProviderDuoCredentialTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
+ 'PhabricatorAuthFactorProviderDuoEnrollTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
+ 'PhabricatorAuthFactorProviderDuoHostnameTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
+ 'PhabricatorAuthFactorProviderDuoUsernamesTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
+ 'PhabricatorAuthFactorProviderEditController' => 'PhabricatorAuthFactorProviderController',
+ 'PhabricatorAuthFactorProviderEditEngine' => 'PhabricatorEditEngine',
+ 'PhabricatorAuthFactorProviderEditor' => 'PhabricatorApplicationTransactionEditor',
+ 'PhabricatorAuthFactorProviderEnrollMessageTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
+ 'PhabricatorAuthFactorProviderListController' => 'PhabricatorAuthProviderController',
+ 'PhabricatorAuthFactorProviderMFAEngine' => 'PhabricatorEditEngineMFAEngine',
+ 'PhabricatorAuthFactorProviderMessageController' => 'PhabricatorAuthFactorProviderController',
+ 'PhabricatorAuthFactorProviderNameTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
+ 'PhabricatorAuthFactorProviderQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+ 'PhabricatorAuthFactorProviderStatus' => 'Phobject',
+ 'PhabricatorAuthFactorProviderStatusTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
+ 'PhabricatorAuthFactorProviderTransaction' => 'PhabricatorModularTransaction',
+ 'PhabricatorAuthFactorProviderTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
+ 'PhabricatorAuthFactorProviderTransactionType' => 'PhabricatorModularTransactionType',
+ 'PhabricatorAuthFactorProviderViewController' => 'PhabricatorAuthFactorProviderController',
+ 'PhabricatorAuthFactorResult' => 'Phobject',
'PhabricatorAuthFactorTestCase' => 'PhabricatorTestCase',
'PhabricatorAuthFinishController' => 'PhabricatorAuthController',
'PhabricatorAuthHMACKey' => 'PhabricatorAuthDAO',
'PhabricatorAuthHighSecurityRequiredException' => 'Exception',
'PhabricatorAuthHighSecurityToken' => 'Phobject',
'PhabricatorAuthInvite' => array(
'PhabricatorUserDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorAuthInviteAccountException' => 'PhabricatorAuthInviteDialogException',
'PhabricatorAuthInviteAction' => 'Phobject',
'PhabricatorAuthInviteActionTableView' => 'AphrontView',
'PhabricatorAuthInviteController' => 'PhabricatorAuthController',
'PhabricatorAuthInviteDialogException' => 'PhabricatorAuthInviteException',
'PhabricatorAuthInviteEngine' => 'Phobject',
'PhabricatorAuthInviteException' => 'Exception',
'PhabricatorAuthInviteInvalidException' => 'PhabricatorAuthInviteDialogException',
'PhabricatorAuthInviteLoginException' => 'PhabricatorAuthInviteDialogException',
'PhabricatorAuthInvitePHIDType' => 'PhabricatorPHIDType',
'PhabricatorAuthInviteQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthInviteRegisteredException' => 'PhabricatorAuthInviteException',
'PhabricatorAuthInviteSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorAuthInviteTestCase' => 'PhabricatorTestCase',
'PhabricatorAuthInviteVerifyException' => 'PhabricatorAuthInviteDialogException',
'PhabricatorAuthInviteWorker' => 'PhabricatorWorker',
'PhabricatorAuthLinkController' => 'PhabricatorAuthController',
'PhabricatorAuthListController' => 'PhabricatorAuthProviderConfigController',
'PhabricatorAuthLoginController' => 'PhabricatorAuthController',
'PhabricatorAuthLoginHandler' => 'Phobject',
+ 'PhabricatorAuthLoginMessageType' => 'PhabricatorAuthMessageType',
'PhabricatorAuthLogoutConduitAPIMethod' => 'PhabricatorAuthConduitAPIMethod',
+ 'PhabricatorAuthMFAEditEngineExtension' => 'PhabricatorEditEngineExtension',
+ 'PhabricatorAuthMFASyncTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType',
'PhabricatorAuthMainMenuBarExtension' => 'PhabricatorMainMenuBarExtension',
'PhabricatorAuthManagementCachePKCS8Workflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementLDAPWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementListFactorsWorkflow' => 'PhabricatorAuthManagementWorkflow',
+ 'PhabricatorAuthManagementListMFAProvidersWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementRecoverWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementRefreshWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementRevokeWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementStripWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementTrustOAuthClientWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementUnlimitWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementUntrustOAuthClientWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementVerifyWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementWorkflow' => 'PhabricatorManagementWorkflow',
+ 'PhabricatorAuthMessage' => array(
+ 'PhabricatorAuthDAO',
+ 'PhabricatorApplicationTransactionInterface',
+ 'PhabricatorPolicyInterface',
+ 'PhabricatorDestructibleInterface',
+ ),
+ 'PhabricatorAuthMessageController' => 'PhabricatorAuthProviderController',
+ 'PhabricatorAuthMessageEditController' => 'PhabricatorAuthMessageController',
+ 'PhabricatorAuthMessageEditEngine' => 'PhabricatorEditEngine',
+ 'PhabricatorAuthMessageEditor' => 'PhabricatorApplicationTransactionEditor',
+ 'PhabricatorAuthMessageListController' => 'PhabricatorAuthProviderController',
+ 'PhabricatorAuthMessagePHIDType' => 'PhabricatorPHIDType',
+ 'PhabricatorAuthMessageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+ 'PhabricatorAuthMessageTextTransaction' => 'PhabricatorAuthMessageTransactionType',
+ 'PhabricatorAuthMessageTransaction' => 'PhabricatorModularTransaction',
+ 'PhabricatorAuthMessageTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
+ 'PhabricatorAuthMessageTransactionType' => 'PhabricatorModularTransactionType',
+ 'PhabricatorAuthMessageType' => 'Phobject',
+ 'PhabricatorAuthMessageViewController' => 'PhabricatorAuthMessageController',
'PhabricatorAuthNeedsApprovalController' => 'PhabricatorAuthController',
'PhabricatorAuthNeedsMultiFactorController' => 'PhabricatorAuthController',
'PhabricatorAuthNewController' => 'PhabricatorAuthProviderConfigController',
+ 'PhabricatorAuthNewFactorAction' => 'PhabricatorSystemAction',
'PhabricatorAuthOldOAuthRedirectController' => 'PhabricatorAuthController',
'PhabricatorAuthOneTimeLoginController' => 'PhabricatorAuthController',
'PhabricatorAuthOneTimeLoginTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType',
'PhabricatorAuthPassword' => array(
'PhabricatorAuthDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorApplicationTransactionInterface',
),
'PhabricatorAuthPasswordEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorAuthPasswordEngine' => 'Phobject',
'PhabricatorAuthPasswordException' => 'Exception',
'PhabricatorAuthPasswordPHIDType' => 'PhabricatorPHIDType',
'PhabricatorAuthPasswordQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthPasswordResetTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType',
'PhabricatorAuthPasswordRevokeTransaction' => 'PhabricatorAuthPasswordTransactionType',
'PhabricatorAuthPasswordRevoker' => 'PhabricatorAuthRevoker',
'PhabricatorAuthPasswordTestCase' => 'PhabricatorTestCase',
'PhabricatorAuthPasswordTransaction' => 'PhabricatorModularTransaction',
'PhabricatorAuthPasswordTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorAuthPasswordTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorAuthPasswordUpgradeTransaction' => 'PhabricatorAuthPasswordTransactionType',
'PhabricatorAuthProvider' => 'Phobject',
'PhabricatorAuthProviderConfig' => array(
'PhabricatorAuthDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
),
- 'PhabricatorAuthProviderConfigController' => 'PhabricatorAuthController',
+ 'PhabricatorAuthProviderConfigController' => 'PhabricatorAuthProviderController',
'PhabricatorAuthProviderConfigEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorAuthProviderConfigQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthProviderConfigTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorAuthProviderConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
+ 'PhabricatorAuthProviderController' => 'PhabricatorAuthController',
'PhabricatorAuthProvidersGuidanceContext' => 'PhabricatorGuidanceContext',
'PhabricatorAuthProvidersGuidanceEngineExtension' => 'PhabricatorGuidanceEngineExtension',
'PhabricatorAuthQueryPublicKeysConduitAPIMethod' => 'PhabricatorAuthConduitAPIMethod',
'PhabricatorAuthRegisterController' => 'PhabricatorAuthController',
'PhabricatorAuthRevokeTokenController' => 'PhabricatorAuthController',
'PhabricatorAuthRevoker' => 'Phobject',
'PhabricatorAuthSSHKey' => array(
'PhabricatorAuthDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorApplicationTransactionInterface',
),
'PhabricatorAuthSSHKeyController' => 'PhabricatorAuthController',
'PhabricatorAuthSSHKeyEditController' => 'PhabricatorAuthSSHKeyController',
'PhabricatorAuthSSHKeyEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorAuthSSHKeyGenerateController' => 'PhabricatorAuthSSHKeyController',
'PhabricatorAuthSSHKeyListController' => 'PhabricatorAuthSSHKeyController',
'PhabricatorAuthSSHKeyPHIDType' => 'PhabricatorPHIDType',
'PhabricatorAuthSSHKeyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthSSHKeyReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhabricatorAuthSSHKeyRevokeController' => 'PhabricatorAuthSSHKeyController',
'PhabricatorAuthSSHKeySearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorAuthSSHKeyTableView' => 'AphrontView',
'PhabricatorAuthSSHKeyTestCase' => 'PhabricatorTestCase',
'PhabricatorAuthSSHKeyTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorAuthSSHKeyTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorAuthSSHKeyViewController' => 'PhabricatorAuthSSHKeyController',
'PhabricatorAuthSSHPublicKey' => 'Phobject',
'PhabricatorAuthSSHRevoker' => 'PhabricatorAuthRevoker',
'PhabricatorAuthSession' => array(
'PhabricatorAuthDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorAuthSessionEngine' => 'Phobject',
'PhabricatorAuthSessionEngineExtension' => 'Phobject',
'PhabricatorAuthSessionEngineExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorAuthSessionGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorAuthSessionInfo' => 'Phobject',
'PhabricatorAuthSessionPHIDType' => 'PhabricatorPHIDType',
'PhabricatorAuthSessionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthSessionRevoker' => 'PhabricatorAuthRevoker',
'PhabricatorAuthSetPasswordController' => 'PhabricatorAuthController',
'PhabricatorAuthSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorAuthStartController' => 'PhabricatorAuthController',
- 'PhabricatorAuthTOTPKeyTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType',
'PhabricatorAuthTemporaryToken' => array(
'PhabricatorAuthDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorAuthTemporaryTokenGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorAuthTemporaryTokenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthTemporaryTokenRevoker' => 'PhabricatorAuthRevoker',
'PhabricatorAuthTemporaryTokenType' => 'Phobject',
'PhabricatorAuthTemporaryTokenTypeModule' => 'PhabricatorConfigModule',
'PhabricatorAuthTerminateSessionController' => 'PhabricatorAuthController',
+ 'PhabricatorAuthTestSMSAction' => 'PhabricatorSystemAction',
'PhabricatorAuthTryFactorAction' => 'PhabricatorSystemAction',
'PhabricatorAuthUnlinkController' => 'PhabricatorAuthController',
'PhabricatorAuthValidateController' => 'PhabricatorAuthController',
+ 'PhabricatorAuthWelcomeMailMessageType' => 'PhabricatorAuthMessageType',
'PhabricatorAuthenticationConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorAutoEventListener' => 'PhabricatorEventListener',
'PhabricatorBadgesApplication' => 'PhabricatorApplication',
'PhabricatorBadgesArchiveController' => 'PhabricatorBadgesController',
'PhabricatorBadgesAward' => array(
'PhabricatorBadgesDAO',
'PhabricatorDestructibleInterface',
'PhabricatorPolicyInterface',
),
'PhabricatorBadgesAwardController' => 'PhabricatorBadgesController',
'PhabricatorBadgesAwardQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorBadgesAwardTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorBadgesBadge' => array(
'PhabricatorBadgesDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorSubscribableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorDestructibleInterface',
'PhabricatorConduitResultInterface',
'PhabricatorNgramsInterface',
),
'PhabricatorBadgesBadgeAwardTransaction' => 'PhabricatorBadgesBadgeTransactionType',
'PhabricatorBadgesBadgeDescriptionTransaction' => 'PhabricatorBadgesBadgeTransactionType',
'PhabricatorBadgesBadgeFlavorTransaction' => 'PhabricatorBadgesBadgeTransactionType',
'PhabricatorBadgesBadgeIconTransaction' => 'PhabricatorBadgesBadgeTransactionType',
'PhabricatorBadgesBadgeNameNgrams' => 'PhabricatorSearchNgrams',
'PhabricatorBadgesBadgeNameTransaction' => 'PhabricatorBadgesBadgeTransactionType',
'PhabricatorBadgesBadgeQualityTransaction' => 'PhabricatorBadgesBadgeTransactionType',
'PhabricatorBadgesBadgeRevokeTransaction' => 'PhabricatorBadgesBadgeTransactionType',
'PhabricatorBadgesBadgeStatusTransaction' => 'PhabricatorBadgesBadgeTransactionType',
'PhabricatorBadgesBadgeTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorBadgesBadgeTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorBadgesCommentController' => 'PhabricatorBadgesController',
'PhabricatorBadgesController' => 'PhabricatorController',
'PhabricatorBadgesCreateCapability' => 'PhabricatorPolicyCapability',
'PhabricatorBadgesDAO' => 'PhabricatorLiskDAO',
'PhabricatorBadgesDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorBadgesDefaultEditCapability' => 'PhabricatorPolicyCapability',
'PhabricatorBadgesEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PhabricatorBadgesEditController' => 'PhabricatorBadgesController',
'PhabricatorBadgesEditEngine' => 'PhabricatorEditEngine',
'PhabricatorBadgesEditRecipientsController' => 'PhabricatorBadgesController',
'PhabricatorBadgesEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorBadgesIconSet' => 'PhabricatorIconSet',
'PhabricatorBadgesListController' => 'PhabricatorBadgesController',
'PhabricatorBadgesLootContextFreeGrammar' => 'PhutilContextFreeGrammar',
'PhabricatorBadgesMailReceiver' => 'PhabricatorObjectMailReceiver',
'PhabricatorBadgesPHIDType' => 'PhabricatorPHIDType',
'PhabricatorBadgesProfileController' => 'PhabricatorController',
'PhabricatorBadgesQuality' => 'Phobject',
'PhabricatorBadgesQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorBadgesRecipientsController' => 'PhabricatorBadgesProfileController',
'PhabricatorBadgesRecipientsListView' => 'AphrontView',
'PhabricatorBadgesRemoveRecipientsController' => 'PhabricatorBadgesController',
'PhabricatorBadgesReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhabricatorBadgesSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorBadgesSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhabricatorBadgesSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorBadgesTransaction' => 'PhabricatorModularTransaction',
'PhabricatorBadgesTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhabricatorBadgesTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorBadgesViewController' => 'PhabricatorBadgesProfileController',
'PhabricatorBarePageView' => 'AphrontPageView',
'PhabricatorBaseURISetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorBcryptPasswordHasher' => 'PhabricatorPasswordHasher',
'PhabricatorBinariesSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorBitbucketAuthProvider' => 'PhabricatorOAuth1AuthProvider',
'PhabricatorBoardColumnsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorBoardLayoutEngine' => 'Phobject',
'PhabricatorBoardRenderingEngine' => 'Phobject',
'PhabricatorBoardResponseEngine' => 'Phobject',
'PhabricatorBoolConfigType' => 'PhabricatorTextConfigType',
'PhabricatorBoolEditField' => 'PhabricatorEditField',
'PhabricatorBoolMailStamp' => 'PhabricatorMailStamp',
'PhabricatorBritishEnglishTranslation' => 'PhutilTranslation',
'PhabricatorBuiltinDraftEngine' => 'PhabricatorDraftEngine',
'PhabricatorBuiltinFileCachePurger' => 'PhabricatorCachePurger',
'PhabricatorBuiltinPatchList' => 'PhabricatorSQLPatchList',
'PhabricatorBulkContentSource' => 'PhabricatorContentSource',
'PhabricatorBulkEditGroup' => 'Phobject',
'PhabricatorBulkEngine' => 'Phobject',
'PhabricatorBulkManagementExportWorkflow' => 'PhabricatorBulkManagementWorkflow',
'PhabricatorBulkManagementMakeSilentWorkflow' => 'PhabricatorBulkManagementWorkflow',
'PhabricatorBulkManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorCSVExportFormat' => 'PhabricatorExportFormat',
'PhabricatorCacheDAO' => 'PhabricatorLiskDAO',
'PhabricatorCacheEngine' => 'Phobject',
'PhabricatorCacheEngineExtension' => 'Phobject',
'PhabricatorCacheGeneralGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorCacheManagementPurgeWorkflow' => 'PhabricatorCacheManagementWorkflow',
'PhabricatorCacheManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorCacheMarkupGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorCachePurger' => 'Phobject',
'PhabricatorCacheSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorCacheSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorCacheSpec' => 'Phobject',
'PhabricatorCacheTTLGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorCachedClassMapQuery' => 'Phobject',
'PhabricatorCaches' => 'Phobject',
'PhabricatorCachesTestCase' => 'PhabricatorTestCase',
'PhabricatorCalendarApplication' => 'PhabricatorApplication',
'PhabricatorCalendarController' => 'PhabricatorController',
'PhabricatorCalendarDAO' => 'PhabricatorLiskDAO',
'PhabricatorCalendarEvent' => array(
'PhabricatorCalendarDAO',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
'PhabricatorPolicyCodexInterface',
'PhabricatorProjectInterface',
'PhabricatorMarkupInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorSubscribableInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorDestructibleInterface',
'PhabricatorMentionableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorSpacesInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
'PhabricatorConduitResultInterface',
),
'PhabricatorCalendarEventAcceptTransaction' => 'PhabricatorCalendarEventReplyTransaction',
'PhabricatorCalendarEventAllDayTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventAvailabilityController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventCancelController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventCancelTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventDateTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventDeclineTransaction' => 'PhabricatorCalendarEventReplyTransaction',
'PhabricatorCalendarEventDefaultEditCapability' => 'PhabricatorPolicyCapability',
'PhabricatorCalendarEventDefaultViewCapability' => 'PhabricatorPolicyCapability',
'PhabricatorCalendarEventDescriptionTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventDragController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PhabricatorCalendarEventEditController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventEditEngine' => 'PhabricatorEditEngine',
'PhabricatorCalendarEventEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorCalendarEventEmailCommand' => 'MetaMTAEmailTransactionCommand',
'PhabricatorCalendarEventEndDateTransaction' => 'PhabricatorCalendarEventDateTransaction',
'PhabricatorCalendarEventExportController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventFerretEngine' => 'PhabricatorFerretEngine',
'PhabricatorCalendarEventForkTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventFrequencyTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventFulltextEngine' => 'PhabricatorFulltextEngine',
'PhabricatorCalendarEventHeraldAdapter' => 'HeraldAdapter',
'PhabricatorCalendarEventHeraldField' => 'HeraldField',
'PhabricatorCalendarEventHeraldFieldGroup' => 'HeraldFieldGroup',
'PhabricatorCalendarEventHostPolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorCalendarEventHostTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventIconTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventInviteTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventInvitee' => array(
'PhabricatorCalendarDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorCalendarEventInviteeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorCalendarEventInviteesPolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorCalendarEventJoinController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventListController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventMailReceiver' => 'PhabricatorObjectMailReceiver',
'PhabricatorCalendarEventNameHeraldField' => 'PhabricatorCalendarEventHeraldField',
'PhabricatorCalendarEventNameTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventNotificationView' => 'Phobject',
'PhabricatorCalendarEventPHIDType' => 'PhabricatorPHIDType',
'PhabricatorCalendarEventPolicyCodex' => 'PhabricatorPolicyCodex',
'PhabricatorCalendarEventQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorCalendarEventRSVPEmailCommand' => 'PhabricatorCalendarEventEmailCommand',
'PhabricatorCalendarEventRecurringTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventReplyTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhabricatorCalendarEventSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorCalendarEventStartDateTransaction' => 'PhabricatorCalendarEventDateTransaction',
'PhabricatorCalendarEventTransaction' => 'PhabricatorModularTransaction',
'PhabricatorCalendarEventTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhabricatorCalendarEventTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorCalendarEventTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorCalendarEventUntilDateTransaction' => 'PhabricatorCalendarEventDateTransaction',
'PhabricatorCalendarEventViewController' => 'PhabricatorCalendarController',
'PhabricatorCalendarExport' => array(
'PhabricatorCalendarDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorCalendarExportDisableController' => 'PhabricatorCalendarController',
'PhabricatorCalendarExportDisableTransaction' => 'PhabricatorCalendarExportTransactionType',
'PhabricatorCalendarExportEditController' => 'PhabricatorCalendarController',
'PhabricatorCalendarExportEditEngine' => 'PhabricatorEditEngine',
'PhabricatorCalendarExportEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorCalendarExportICSController' => 'PhabricatorCalendarController',
'PhabricatorCalendarExportListController' => 'PhabricatorCalendarController',
'PhabricatorCalendarExportModeTransaction' => 'PhabricatorCalendarExportTransactionType',
'PhabricatorCalendarExportNameTransaction' => 'PhabricatorCalendarExportTransactionType',
'PhabricatorCalendarExportPHIDType' => 'PhabricatorPHIDType',
'PhabricatorCalendarExportQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorCalendarExportQueryKeyTransaction' => 'PhabricatorCalendarExportTransactionType',
'PhabricatorCalendarExportSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorCalendarExportTransaction' => 'PhabricatorModularTransaction',
'PhabricatorCalendarExportTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorCalendarExportTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorCalendarExportViewController' => 'PhabricatorCalendarController',
'PhabricatorCalendarExternalInvitee' => array(
'PhabricatorCalendarDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorCalendarExternalInviteePHIDType' => 'PhabricatorPHIDType',
'PhabricatorCalendarExternalInviteeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorCalendarICSFileImportEngine' => 'PhabricatorCalendarICSImportEngine',
'PhabricatorCalendarICSImportEngine' => 'PhabricatorCalendarImportEngine',
'PhabricatorCalendarICSURIImportEngine' => 'PhabricatorCalendarICSImportEngine',
'PhabricatorCalendarICSWriter' => 'Phobject',
'PhabricatorCalendarIconSet' => 'PhabricatorIconSet',
'PhabricatorCalendarImport' => array(
'PhabricatorCalendarDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorCalendarImportDefaultLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportDeleteController' => 'PhabricatorCalendarController',
'PhabricatorCalendarImportDeleteLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportDeleteTransaction' => 'PhabricatorCalendarImportTransactionType',
'PhabricatorCalendarImportDisableController' => 'PhabricatorCalendarController',
'PhabricatorCalendarImportDisableTransaction' => 'PhabricatorCalendarImportTransactionType',
'PhabricatorCalendarImportDropController' => 'PhabricatorCalendarController',
'PhabricatorCalendarImportDuplicateLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportEditController' => 'PhabricatorCalendarController',
'PhabricatorCalendarImportEditEngine' => 'PhabricatorEditEngine',
'PhabricatorCalendarImportEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorCalendarImportEmptyLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportEngine' => 'Phobject',
'PhabricatorCalendarImportEpochLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportFetchLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportFrequencyLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportFrequencyTransaction' => 'PhabricatorCalendarImportTransactionType',
'PhabricatorCalendarImportICSFileTransaction' => 'PhabricatorCalendarImportTransactionType',
'PhabricatorCalendarImportICSLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportICSURITransaction' => 'PhabricatorCalendarImportTransactionType',
'PhabricatorCalendarImportICSWarningLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportIgnoredNodeLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportListController' => 'PhabricatorCalendarController',
'PhabricatorCalendarImportLog' => array(
'PhabricatorCalendarDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorCalendarImportLogListController' => 'PhabricatorCalendarController',
'PhabricatorCalendarImportLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorCalendarImportLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorCalendarImportLogType' => 'Phobject',
'PhabricatorCalendarImportLogView' => 'AphrontView',
'PhabricatorCalendarImportNameTransaction' => 'PhabricatorCalendarImportTransactionType',
'PhabricatorCalendarImportOriginalLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportOrphanLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportPHIDType' => 'PhabricatorPHIDType',
'PhabricatorCalendarImportQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorCalendarImportQueueLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportReloadController' => 'PhabricatorCalendarController',
'PhabricatorCalendarImportReloadTransaction' => 'PhabricatorCalendarImportTransactionType',
'PhabricatorCalendarImportReloadWorker' => 'PhabricatorWorker',
'PhabricatorCalendarImportSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorCalendarImportTransaction' => 'PhabricatorModularTransaction',
'PhabricatorCalendarImportTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorCalendarImportTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorCalendarImportTriggerLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportUpdateLogType' => 'PhabricatorCalendarImportLogType',
'PhabricatorCalendarImportViewController' => 'PhabricatorCalendarController',
'PhabricatorCalendarInviteeDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorCalendarInviteeUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorCalendarInviteeViewerFunctionDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorCalendarManagementNotifyWorkflow' => 'PhabricatorCalendarManagementWorkflow',
'PhabricatorCalendarManagementReloadWorkflow' => 'PhabricatorCalendarManagementWorkflow',
'PhabricatorCalendarManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorCalendarNotification' => 'PhabricatorCalendarDAO',
'PhabricatorCalendarNotificationEngine' => 'Phobject',
'PhabricatorCalendarRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhabricatorCalendarReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhabricatorCalendarSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorCelerityApplication' => 'PhabricatorApplication',
'PhabricatorCelerityTestCase' => 'PhabricatorTestCase',
'PhabricatorChangeParserTestCase' => 'PhabricatorWorkingCopyTestCase',
'PhabricatorChangesetCachePurger' => 'PhabricatorCachePurger',
'PhabricatorChangesetResponse' => 'AphrontProxyResponse',
'PhabricatorChatLogApplication' => 'PhabricatorApplication',
'PhabricatorChatLogChannel' => array(
'PhabricatorChatLogDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorChatLogChannelListController' => 'PhabricatorChatLogController',
'PhabricatorChatLogChannelLogController' => 'PhabricatorChatLogController',
'PhabricatorChatLogChannelQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorChatLogController' => 'PhabricatorController',
'PhabricatorChatLogDAO' => 'PhabricatorLiskDAO',
'PhabricatorChatLogEvent' => array(
'PhabricatorChatLogDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorChatLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorCheckboxesEditField' => 'PhabricatorEditField',
'PhabricatorChunkedFileStorageEngine' => 'PhabricatorFileStorageEngine',
'PhabricatorClassConfigType' => 'PhabricatorTextConfigType',
'PhabricatorClusterConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorClusterDatabasesConfigType' => 'PhabricatorJSONConfigType',
'PhabricatorClusterException' => 'Exception',
'PhabricatorClusterExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorClusterImpossibleWriteException' => 'PhabricatorClusterException',
'PhabricatorClusterImproperWriteException' => 'PhabricatorClusterException',
'PhabricatorClusterMailersConfigType' => 'PhabricatorJSONConfigType',
'PhabricatorClusterNoHostForRoleException' => 'Exception',
'PhabricatorClusterSearchConfigType' => 'PhabricatorJSONConfigType',
'PhabricatorClusterServiceHealthRecord' => 'Phobject',
'PhabricatorClusterStrandedException' => 'PhabricatorClusterException',
'PhabricatorColumnsEditField' => 'PhabricatorPHIDListEditField',
'PhabricatorCommentEditEngineExtension' => 'PhabricatorEditEngineExtension',
'PhabricatorCommentEditField' => 'PhabricatorEditField',
'PhabricatorCommentEditType' => 'PhabricatorEditType',
'PhabricatorCommitBranchesField' => 'PhabricatorCommitCustomField',
'PhabricatorCommitCustomField' => 'PhabricatorCustomField',
'PhabricatorCommitMergedCommitsField' => 'PhabricatorCommitCustomField',
'PhabricatorCommitRepositoryField' => 'PhabricatorCommitCustomField',
'PhabricatorCommitSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorCommitTagsField' => 'PhabricatorCommitCustomField',
'PhabricatorCommonPasswords' => 'Phobject',
'PhabricatorConduitAPIController' => 'PhabricatorConduitController',
'PhabricatorConduitApplication' => 'PhabricatorApplication',
'PhabricatorConduitCallManagementWorkflow' => 'PhabricatorConduitManagementWorkflow',
'PhabricatorConduitCertificateToken' => 'PhabricatorConduitDAO',
'PhabricatorConduitConsoleController' => 'PhabricatorConduitController',
'PhabricatorConduitContentSource' => 'PhabricatorContentSource',
'PhabricatorConduitController' => 'PhabricatorController',
'PhabricatorConduitDAO' => 'PhabricatorLiskDAO',
'PhabricatorConduitEditField' => 'PhabricatorEditField',
'PhabricatorConduitListController' => 'PhabricatorConduitController',
'PhabricatorConduitLogController' => 'PhabricatorConduitController',
'PhabricatorConduitLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorConduitLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorConduitManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorConduitMethodCallLog' => array(
'PhabricatorConduitDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorConduitMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorConduitRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorConduitResultInterface' => 'PhabricatorPHIDInterface',
'PhabricatorConduitSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorConduitSearchFieldSpecification' => 'Phobject',
'PhabricatorConduitTestCase' => 'PhabricatorTestCase',
'PhabricatorConduitToken' => array(
'PhabricatorConduitDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorConduitTokenController' => 'PhabricatorConduitController',
'PhabricatorConduitTokenEditController' => 'PhabricatorConduitController',
'PhabricatorConduitTokenHandshakeController' => 'PhabricatorConduitController',
'PhabricatorConduitTokenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorConduitTokenTerminateController' => 'PhabricatorConduitController',
'PhabricatorConduitTokensSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorConfigAllController' => 'PhabricatorConfigController',
'PhabricatorConfigApplication' => 'PhabricatorApplication',
'PhabricatorConfigApplicationController' => 'PhabricatorConfigController',
'PhabricatorConfigCacheController' => 'PhabricatorConfigController',
'PhabricatorConfigClusterDatabasesController' => 'PhabricatorConfigController',
'PhabricatorConfigClusterNotificationsController' => 'PhabricatorConfigController',
'PhabricatorConfigClusterRepositoriesController' => 'PhabricatorConfigController',
'PhabricatorConfigClusterSearchController' => 'PhabricatorConfigController',
'PhabricatorConfigCollectorsModule' => 'PhabricatorConfigModule',
'PhabricatorConfigColumnSchema' => 'PhabricatorConfigStorageSchema',
'PhabricatorConfigConfigPHIDType' => 'PhabricatorPHIDType',
'PhabricatorConfigConstants' => 'Phobject',
'PhabricatorConfigController' => 'PhabricatorController',
'PhabricatorConfigCoreSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorConfigDatabaseController' => 'PhabricatorConfigController',
'PhabricatorConfigDatabaseIssueController' => 'PhabricatorConfigDatabaseController',
'PhabricatorConfigDatabaseSchema' => 'PhabricatorConfigStorageSchema',
'PhabricatorConfigDatabaseSource' => 'PhabricatorConfigProxySource',
'PhabricatorConfigDatabaseStatusController' => 'PhabricatorConfigDatabaseController',
'PhabricatorConfigDefaultSource' => 'PhabricatorConfigProxySource',
'PhabricatorConfigDictionarySource' => 'PhabricatorConfigSource',
'PhabricatorConfigEdgeModule' => 'PhabricatorConfigModule',
'PhabricatorConfigEditController' => 'PhabricatorConfigController',
'PhabricatorConfigEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorConfigEntry' => array(
'PhabricatorConfigEntryDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
),
'PhabricatorConfigEntryDAO' => 'PhabricatorLiskDAO',
'PhabricatorConfigEntryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorConfigFileSource' => 'PhabricatorConfigProxySource',
'PhabricatorConfigGroupConstants' => 'PhabricatorConfigConstants',
'PhabricatorConfigGroupController' => 'PhabricatorConfigController',
'PhabricatorConfigHTTPParameterTypesModule' => 'PhabricatorConfigModule',
'PhabricatorConfigHistoryController' => 'PhabricatorConfigController',
'PhabricatorConfigIgnoreController' => 'PhabricatorConfigController',
'PhabricatorConfigIssueListController' => 'PhabricatorConfigController',
'PhabricatorConfigIssuePanelController' => 'PhabricatorConfigController',
'PhabricatorConfigIssueViewController' => 'PhabricatorConfigController',
'PhabricatorConfigJSON' => 'Phobject',
'PhabricatorConfigJSONOptionType' => 'PhabricatorConfigOptionType',
'PhabricatorConfigKeySchema' => 'PhabricatorConfigStorageSchema',
'PhabricatorConfigListController' => 'PhabricatorConfigController',
'PhabricatorConfigLocalSource' => 'PhabricatorConfigProxySource',
'PhabricatorConfigManagementDeleteWorkflow' => 'PhabricatorConfigManagementWorkflow',
'PhabricatorConfigManagementDoneWorkflow' => 'PhabricatorConfigManagementWorkflow',
'PhabricatorConfigManagementGetWorkflow' => 'PhabricatorConfigManagementWorkflow',
'PhabricatorConfigManagementListWorkflow' => 'PhabricatorConfigManagementWorkflow',
'PhabricatorConfigManagementMigrateWorkflow' => 'PhabricatorConfigManagementWorkflow',
'PhabricatorConfigManagementSetWorkflow' => 'PhabricatorConfigManagementWorkflow',
'PhabricatorConfigManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorConfigManualActivity' => 'PhabricatorConfigEntryDAO',
'PhabricatorConfigModule' => 'Phobject',
'PhabricatorConfigModuleController' => 'PhabricatorConfigController',
'PhabricatorConfigOption' => 'Phobject',
'PhabricatorConfigOptionType' => 'Phobject',
'PhabricatorConfigPHIDModule' => 'PhabricatorConfigModule',
'PhabricatorConfigProxySource' => 'PhabricatorConfigSource',
'PhabricatorConfigPurgeCacheController' => 'PhabricatorConfigController',
'PhabricatorConfigRegexOptionType' => 'PhabricatorConfigJSONOptionType',
+ 'PhabricatorConfigRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorConfigRequestExceptionHandlerModule' => 'PhabricatorConfigModule',
'PhabricatorConfigResponse' => 'AphrontStandaloneHTMLResponse',
'PhabricatorConfigSchemaQuery' => 'Phobject',
'PhabricatorConfigSchemaSpec' => 'Phobject',
'PhabricatorConfigServerSchema' => 'PhabricatorConfigStorageSchema',
'PhabricatorConfigSetupCheckModule' => 'PhabricatorConfigModule',
'PhabricatorConfigSiteModule' => 'PhabricatorConfigModule',
'PhabricatorConfigSiteSource' => 'PhabricatorConfigProxySource',
'PhabricatorConfigSource' => 'Phobject',
'PhabricatorConfigStackSource' => 'PhabricatorConfigSource',
'PhabricatorConfigStorageSchema' => 'Phobject',
'PhabricatorConfigTableSchema' => 'PhabricatorConfigStorageSchema',
'PhabricatorConfigTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorConfigType' => 'Phobject',
'PhabricatorConfigValidationException' => 'Exception',
'PhabricatorConfigVersionController' => 'PhabricatorConfigController',
'PhabricatorConpherenceApplication' => 'PhabricatorApplication',
'PhabricatorConpherenceColumnMinimizeSetting' => 'PhabricatorInternalSetting',
'PhabricatorConpherenceColumnVisibleSetting' => 'PhabricatorInternalSetting',
'PhabricatorConpherenceNotificationsSetting' => 'PhabricatorSelectSetting',
'PhabricatorConpherencePreferencesSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorConpherenceProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorConpherenceRoomContextFreeGrammar' => 'PhutilContextFreeGrammar',
'PhabricatorConpherenceRoomTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorConpherenceSoundSetting' => 'PhabricatorSelectSetting',
'PhabricatorConpherenceThreadPHIDType' => 'PhabricatorPHIDType',
'PhabricatorConpherenceWidgetVisibleSetting' => 'PhabricatorInternalSetting',
'PhabricatorConsoleApplication' => 'PhabricatorApplication',
'PhabricatorConsoleContentSource' => 'PhabricatorContentSource',
+ 'PhabricatorContactNumbersSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorContentSource' => 'Phobject',
'PhabricatorContentSourceModule' => 'PhabricatorConfigModule',
'PhabricatorContentSourceView' => 'AphrontView',
'PhabricatorContributedToObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorController' => 'AphrontController',
'PhabricatorCookies' => 'Phobject',
'PhabricatorCoreConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorCoreCreateTransaction' => 'PhabricatorCoreTransactionType',
'PhabricatorCoreTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorCoreVoidTransaction' => 'PhabricatorModularTransactionType',
'PhabricatorCountFact' => 'PhabricatorFact',
'PhabricatorCountdown' => array(
'PhabricatorCountdownDAO',
'PhabricatorPolicyInterface',
'PhabricatorFlaggableInterface',
'PhabricatorSubscribableInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorSpacesInterface',
'PhabricatorProjectInterface',
'PhabricatorDestructibleInterface',
'PhabricatorConduitResultInterface',
),
'PhabricatorCountdownApplication' => 'PhabricatorApplication',
'PhabricatorCountdownController' => 'PhabricatorController',
'PhabricatorCountdownCountdownPHIDType' => 'PhabricatorPHIDType',
'PhabricatorCountdownDAO' => 'PhabricatorLiskDAO',
'PhabricatorCountdownDefaultEditCapability' => 'PhabricatorPolicyCapability',
'PhabricatorCountdownDefaultViewCapability' => 'PhabricatorPolicyCapability',
'PhabricatorCountdownDescriptionTransaction' => 'PhabricatorCountdownTransactionType',
'PhabricatorCountdownEditController' => 'PhabricatorCountdownController',
'PhabricatorCountdownEditEngine' => 'PhabricatorEditEngine',
'PhabricatorCountdownEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorCountdownEpochTransaction' => 'PhabricatorCountdownTransactionType',
'PhabricatorCountdownListController' => 'PhabricatorCountdownController',
'PhabricatorCountdownMailReceiver' => 'PhabricatorObjectMailReceiver',
'PhabricatorCountdownQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorCountdownRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhabricatorCountdownReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhabricatorCountdownSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorCountdownSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorCountdownTitleTransaction' => 'PhabricatorCountdownTransactionType',
'PhabricatorCountdownTransaction' => 'PhabricatorModularTransaction',
'PhabricatorCountdownTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhabricatorCountdownTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorCountdownTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorCountdownView' => 'AphrontView',
'PhabricatorCountdownViewController' => 'PhabricatorCountdownController',
+ 'PhabricatorCredentialEditField' => 'PhabricatorEditField',
'PhabricatorCursorPagedPolicyAwareQuery' => 'PhabricatorPolicyAwareQuery',
'PhabricatorCustomField' => 'Phobject',
'PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorCustomFieldApplicationSearchDatasource' => 'PhabricatorTypeaheadProxyDatasource',
'PhabricatorCustomFieldApplicationSearchNoneFunctionDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorCustomFieldAttachment' => 'Phobject',
'PhabricatorCustomFieldConfigOptionType' => 'PhabricatorConfigOptionType',
'PhabricatorCustomFieldDataNotAvailableException' => 'Exception',
'PhabricatorCustomFieldEditEngineExtension' => 'PhabricatorEditEngineExtension',
'PhabricatorCustomFieldEditField' => 'PhabricatorEditField',
'PhabricatorCustomFieldEditType' => 'PhabricatorEditType',
'PhabricatorCustomFieldExportEngineExtension' => 'PhabricatorExportEngineExtension',
'PhabricatorCustomFieldFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension',
'PhabricatorCustomFieldHeraldAction' => 'HeraldAction',
'PhabricatorCustomFieldHeraldActionGroup' => 'HeraldActionGroup',
'PhabricatorCustomFieldHeraldField' => 'HeraldField',
'PhabricatorCustomFieldHeraldFieldGroup' => 'HeraldFieldGroup',
'PhabricatorCustomFieldImplementationIncompleteException' => 'Exception',
'PhabricatorCustomFieldIndexStorage' => 'PhabricatorLiskDAO',
'PhabricatorCustomFieldList' => 'Phobject',
'PhabricatorCustomFieldMonogramParser' => 'Phobject',
'PhabricatorCustomFieldNotAttachedException' => 'Exception',
'PhabricatorCustomFieldNotProxyException' => 'Exception',
'PhabricatorCustomFieldNumericIndexStorage' => 'PhabricatorCustomFieldIndexStorage',
'PhabricatorCustomFieldSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
'PhabricatorCustomFieldStorage' => 'PhabricatorLiskDAO',
'PhabricatorCustomFieldStorageQuery' => 'Phobject',
'PhabricatorCustomFieldStringIndexStorage' => 'PhabricatorCustomFieldIndexStorage',
'PhabricatorCustomLogoConfigType' => 'PhabricatorConfigOptionType',
'PhabricatorCustomUIFooterConfigType' => 'PhabricatorConfigJSONOptionType',
'PhabricatorDaemon' => 'PhutilDaemon',
'PhabricatorDaemonBulkJobController' => 'PhabricatorDaemonController',
'PhabricatorDaemonBulkJobListController' => 'PhabricatorDaemonBulkJobController',
'PhabricatorDaemonBulkJobMonitorController' => 'PhabricatorDaemonBulkJobController',
'PhabricatorDaemonBulkJobViewController' => 'PhabricatorDaemonBulkJobController',
'PhabricatorDaemonConsoleController' => 'PhabricatorDaemonController',
'PhabricatorDaemonContentSource' => 'PhabricatorContentSource',
'PhabricatorDaemonController' => 'PhabricatorController',
'PhabricatorDaemonDAO' => 'PhabricatorLiskDAO',
'PhabricatorDaemonEventListener' => 'PhabricatorEventListener',
'PhabricatorDaemonLockLog' => 'PhabricatorDaemonDAO',
'PhabricatorDaemonLockLogGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorDaemonLog' => array(
'PhabricatorDaemonDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorDaemonLogEvent' => 'PhabricatorDaemonDAO',
'PhabricatorDaemonLogEventGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorDaemonLogEventViewController' => 'PhabricatorDaemonController',
'PhabricatorDaemonLogEventsView' => 'AphrontView',
'PhabricatorDaemonLogGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorDaemonLogListController' => 'PhabricatorDaemonController',
'PhabricatorDaemonLogListView' => 'AphrontView',
'PhabricatorDaemonLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorDaemonLogViewController' => 'PhabricatorDaemonController',
'PhabricatorDaemonManagementDebugWorkflow' => 'PhabricatorDaemonManagementWorkflow',
'PhabricatorDaemonManagementLaunchWorkflow' => 'PhabricatorDaemonManagementWorkflow',
'PhabricatorDaemonManagementListWorkflow' => 'PhabricatorDaemonManagementWorkflow',
'PhabricatorDaemonManagementLogWorkflow' => 'PhabricatorDaemonManagementWorkflow',
'PhabricatorDaemonManagementReloadWorkflow' => 'PhabricatorDaemonManagementWorkflow',
'PhabricatorDaemonManagementRestartWorkflow' => 'PhabricatorDaemonManagementWorkflow',
'PhabricatorDaemonManagementStartWorkflow' => 'PhabricatorDaemonManagementWorkflow',
'PhabricatorDaemonManagementStatusWorkflow' => 'PhabricatorDaemonManagementWorkflow',
'PhabricatorDaemonManagementStopWorkflow' => 'PhabricatorDaemonManagementWorkflow',
'PhabricatorDaemonManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorDaemonOverseerModule' => 'PhutilDaemonOverseerModule',
'PhabricatorDaemonReference' => 'Phobject',
'PhabricatorDaemonTaskGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorDaemonTasksTableView' => 'AphrontView',
'PhabricatorDaemonsApplication' => 'PhabricatorApplication',
'PhabricatorDaemonsSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorDailyRoutineTriggerClock' => 'PhabricatorTriggerClock',
'PhabricatorDarkConsoleSetting' => 'PhabricatorSelectSetting',
'PhabricatorDarkConsoleTabSetting' => 'PhabricatorInternalSetting',
'PhabricatorDarkConsoleVisibleSetting' => 'PhabricatorInternalSetting',
'PhabricatorDashboard' => array(
'PhabricatorDashboardDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorFlaggableInterface',
'PhabricatorDestructibleInterface',
'PhabricatorProjectInterface',
'PhabricatorNgramsInterface',
),
'PhabricatorDashboardAddPanelController' => 'PhabricatorDashboardController',
'PhabricatorDashboardApplication' => 'PhabricatorApplication',
'PhabricatorDashboardArchiveController' => 'PhabricatorDashboardController',
'PhabricatorDashboardArrangeController' => 'PhabricatorDashboardProfileController',
'PhabricatorDashboardController' => 'PhabricatorController',
'PhabricatorDashboardDAO' => 'PhabricatorLiskDAO',
'PhabricatorDashboardDashboardHasPanelEdgeType' => 'PhabricatorEdgeType',
'PhabricatorDashboardDashboardPHIDType' => 'PhabricatorPHIDType',
'PhabricatorDashboardDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorDashboardEditController' => 'PhabricatorDashboardController',
'PhabricatorDashboardIconSet' => 'PhabricatorIconSet',
'PhabricatorDashboardInstall' => 'PhabricatorDashboardDAO',
'PhabricatorDashboardInstallController' => 'PhabricatorDashboardController',
'PhabricatorDashboardLayoutConfig' => 'Phobject',
'PhabricatorDashboardListController' => 'PhabricatorDashboardController',
'PhabricatorDashboardManageController' => 'PhabricatorDashboardProfileController',
'PhabricatorDashboardMovePanelController' => 'PhabricatorDashboardController',
'PhabricatorDashboardNgrams' => 'PhabricatorSearchNgrams',
'PhabricatorDashboardPanel' => array(
'PhabricatorDashboardDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorFlaggableInterface',
'PhabricatorDestructibleInterface',
'PhabricatorNgramsInterface',
),
'PhabricatorDashboardPanelArchiveController' => 'PhabricatorDashboardController',
'PhabricatorDashboardPanelCoreCustomField' => array(
'PhabricatorDashboardPanelCustomField',
'PhabricatorStandardCustomFieldInterface',
),
'PhabricatorDashboardPanelCustomField' => 'PhabricatorCustomField',
'PhabricatorDashboardPanelDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorDashboardPanelEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PhabricatorDashboardPanelEditController' => 'PhabricatorDashboardController',
'PhabricatorDashboardPanelEditEngine' => 'PhabricatorEditEngine',
'PhabricatorDashboardPanelEditproController' => 'PhabricatorDashboardController',
'PhabricatorDashboardPanelHasDashboardEdgeType' => 'PhabricatorEdgeType',
'PhabricatorDashboardPanelListController' => 'PhabricatorDashboardController',
'PhabricatorDashboardPanelNgrams' => 'PhabricatorSearchNgrams',
'PhabricatorDashboardPanelPHIDType' => 'PhabricatorPHIDType',
'PhabricatorDashboardPanelQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorDashboardPanelRenderController' => 'PhabricatorDashboardController',
'PhabricatorDashboardPanelRenderingEngine' => 'Phobject',
'PhabricatorDashboardPanelSearchApplicationCustomField' => 'PhabricatorStandardCustomField',
'PhabricatorDashboardPanelSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorDashboardPanelSearchQueryCustomField' => 'PhabricatorStandardCustomField',
'PhabricatorDashboardPanelTabsCustomField' => 'PhabricatorStandardCustomField',
'PhabricatorDashboardPanelTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorDashboardPanelTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorDashboardPanelTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorDashboardPanelType' => 'Phobject',
'PhabricatorDashboardPanelViewController' => 'PhabricatorDashboardController',
'PhabricatorDashboardProfileController' => 'PhabricatorController',
'PhabricatorDashboardProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorDashboardQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorDashboardQueryPanelInstallController' => 'PhabricatorDashboardController',
'PhabricatorDashboardQueryPanelType' => 'PhabricatorDashboardPanelType',
'PhabricatorDashboardRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhabricatorDashboardRemovePanelController' => 'PhabricatorDashboardController',
'PhabricatorDashboardRenderingEngine' => 'Phobject',
'PhabricatorDashboardSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorDashboardSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorDashboardTabsPanelType' => 'PhabricatorDashboardPanelType',
'PhabricatorDashboardTextPanelType' => 'PhabricatorDashboardPanelType',
'PhabricatorDashboardTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorDashboardTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorDashboardTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorDashboardViewController' => 'PhabricatorDashboardProfileController',
'PhabricatorDataCacheSpec' => 'PhabricatorCacheSpec',
'PhabricatorDataNotAttachedException' => 'Exception',
'PhabricatorDatabaseRef' => 'Phobject',
'PhabricatorDatabaseRefParser' => 'Phobject',
'PhabricatorDatabaseSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorDatasourceApplicationEngineExtension' => 'PhabricatorDatasourceEngineExtension',
'PhabricatorDatasourceEditField' => 'PhabricatorTokenizerEditField',
'PhabricatorDatasourceEditType' => 'PhabricatorPHIDListEditType',
'PhabricatorDatasourceEngine' => 'Phobject',
'PhabricatorDatasourceEngineExtension' => 'Phobject',
'PhabricatorDateFormatSetting' => 'PhabricatorSelectSetting',
'PhabricatorDateTimeSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorDebugController' => 'PhabricatorController',
'PhabricatorDefaultRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorDefaultSyntaxStyle' => 'PhabricatorSyntaxStyle',
'PhabricatorDestructibleCodex' => 'Phobject',
'PhabricatorDestructionEngine' => 'Phobject',
'PhabricatorDestructionEngineExtension' => 'Phobject',
'PhabricatorDestructionEngineExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorDeveloperConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorDeveloperPreferencesSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorDiffInlineCommentQuery' => 'PhabricatorApplicationTransactionCommentQuery',
'PhabricatorDiffPreferencesSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorDifferenceEngine' => 'Phobject',
'PhabricatorDifferentialApplication' => 'PhabricatorApplication',
'PhabricatorDifferentialAttachCommitWorkflow' => 'PhabricatorDifferentialManagementWorkflow',
'PhabricatorDifferentialConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorDifferentialExtractWorkflow' => 'PhabricatorDifferentialManagementWorkflow',
'PhabricatorDifferentialManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorDifferentialMigrateHunkWorkflow' => 'PhabricatorDifferentialManagementWorkflow',
'PhabricatorDifferentialRebuildChangesetsWorkflow' => 'PhabricatorDifferentialManagementWorkflow',
'PhabricatorDifferentialRevisionTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorDiffusionApplication' => 'PhabricatorApplication',
'PhabricatorDiffusionBlameSetting' => 'PhabricatorInternalSetting',
'PhabricatorDiffusionConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorDisabledUserController' => 'PhabricatorAuthController',
'PhabricatorDisplayPreferencesSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorDisqusAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorDividerEditField' => 'PhabricatorEditField',
'PhabricatorDividerProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorDivinerApplication' => 'PhabricatorApplication',
'PhabricatorDocumentEngine' => 'Phobject',
'PhabricatorDocumentRef' => 'Phobject',
'PhabricatorDocumentRenderingEngine' => 'Phobject',
'PhabricatorDoorkeeperApplication' => 'PhabricatorApplication',
'PhabricatorDoubleExportField' => 'PhabricatorExportField',
'PhabricatorDraft' => 'PhabricatorDraftDAO',
'PhabricatorDraftDAO' => 'PhabricatorLiskDAO',
'PhabricatorDraftEngine' => 'Phobject',
'PhabricatorDrydockApplication' => 'PhabricatorApplication',
+ 'PhabricatorDuoAuthFactor' => 'PhabricatorAuthFactor',
+ 'PhabricatorDuoFuture' => 'FutureProxy',
'PhabricatorEdgeChangeRecord' => 'Phobject',
'PhabricatorEdgeChangeRecordTestCase' => 'PhabricatorTestCase',
'PhabricatorEdgeConfig' => 'PhabricatorEdgeConstants',
'PhabricatorEdgeConstants' => 'Phobject',
'PhabricatorEdgeCycleException' => 'Exception',
'PhabricatorEdgeEditType' => 'PhabricatorPHIDListEditType',
'PhabricatorEdgeEditor' => 'Phobject',
'PhabricatorEdgeGraph' => 'AbstractDirectedGraph',
'PhabricatorEdgeObject' => array(
'Phobject',
'PhabricatorPolicyInterface',
),
'PhabricatorEdgeObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorEdgeQuery' => 'PhabricatorQuery',
'PhabricatorEdgeTestCase' => 'PhabricatorTestCase',
'PhabricatorEdgeType' => 'Phobject',
'PhabricatorEdgeTypeTestCase' => 'PhabricatorTestCase',
'PhabricatorEdgesDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorEditEngine' => array(
'Phobject',
'PhabricatorPolicyInterface',
),
'PhabricatorEditEngineAPIMethod' => 'ConduitAPIMethod',
'PhabricatorEditEngineBulkJobType' => 'PhabricatorWorkerBulkJobType',
'PhabricatorEditEngineCheckboxesCommentAction' => 'PhabricatorEditEngineCommentAction',
'PhabricatorEditEngineColumnsCommentAction' => 'PhabricatorEditEngineCommentAction',
'PhabricatorEditEngineCommentAction' => 'Phobject',
'PhabricatorEditEngineCommentActionGroup' => 'Phobject',
'PhabricatorEditEngineConfiguration' => array(
'PhabricatorSearchDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
),
'PhabricatorEditEngineConfigurationDefaultCreateController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationDefaultsController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationDisableController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationEditController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationEditEngine' => 'PhabricatorEditEngine',
'PhabricatorEditEngineConfigurationEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorEditEngineConfigurationIsEditController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationListController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationLockController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationPHIDType' => 'PhabricatorPHIDType',
'PhabricatorEditEngineConfigurationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorEditEngineConfigurationReorderController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationSaveController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorEditEngineConfigurationSortController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationSubtypeController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineConfigurationTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorEditEngineConfigurationTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorEditEngineConfigurationViewController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineController' => 'PhabricatorApplicationTransactionController',
'PhabricatorEditEngineDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorEditEngineDefaultLock' => 'PhabricatorEditEngineLock',
'PhabricatorEditEngineExtension' => 'Phobject',
'PhabricatorEditEngineExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorEditEngineListController' => 'PhabricatorEditEngineController',
'PhabricatorEditEngineLock' => 'Phobject',
+ 'PhabricatorEditEngineMFAEngine' => 'Phobject',
'PhabricatorEditEnginePointsCommentAction' => 'PhabricatorEditEngineCommentAction',
'PhabricatorEditEngineProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorEditEngineQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorEditEngineSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorEditEngineSelectCommentAction' => 'PhabricatorEditEngineCommentAction',
'PhabricatorEditEngineSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorEditEngineStaticCommentAction' => 'PhabricatorEditEngineCommentAction',
'PhabricatorEditEngineSubtype' => 'Phobject',
'PhabricatorEditEngineSubtypeMap' => 'Phobject',
'PhabricatorEditEngineSubtypeTestCase' => 'PhabricatorTestCase',
'PhabricatorEditEngineTokenizerCommentAction' => 'PhabricatorEditEngineCommentAction',
'PhabricatorEditField' => 'Phobject',
'PhabricatorEditPage' => 'Phobject',
'PhabricatorEditType' => 'Phobject',
'PhabricatorEditor' => 'Phobject',
+ 'PhabricatorEditorExtension' => 'Phobject',
+ 'PhabricatorEditorExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorEditorMailEngineExtension' => 'PhabricatorMailEngineExtension',
'PhabricatorEditorMultipleSetting' => 'PhabricatorSelectSetting',
'PhabricatorEditorSetting' => 'PhabricatorStringSetting',
'PhabricatorElasticFulltextStorageEngine' => 'PhabricatorFulltextStorageEngine',
'PhabricatorElasticsearchHost' => 'PhabricatorSearchHost',
'PhabricatorElasticsearchSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorEmailAddressesSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorEmailContentSource' => 'PhabricatorContentSource',
'PhabricatorEmailDeliverySettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorEmailFormatSetting' => 'PhabricatorSelectSetting',
'PhabricatorEmailFormatSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorEmailLoginController' => 'PhabricatorAuthController',
'PhabricatorEmailNotificationsSetting' => 'PhabricatorSelectSetting',
'PhabricatorEmailPreferencesSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorEmailRePrefixSetting' => 'PhabricatorSelectSetting',
'PhabricatorEmailSelfActionsSetting' => 'PhabricatorSelectSetting',
'PhabricatorEmailStampsSetting' => 'PhabricatorSelectSetting',
'PhabricatorEmailTagsSetting' => 'PhabricatorInternalSetting',
'PhabricatorEmailVarySubjectsSetting' => 'PhabricatorSelectSetting',
'PhabricatorEmailVerificationController' => 'PhabricatorAuthController',
'PhabricatorEmbedFileRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhabricatorEmojiDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorEmojiRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorEmojiTranslation' => 'PhutilTranslation',
'PhabricatorEmptyQueryException' => 'Exception',
'PhabricatorEnumConfigType' => 'PhabricatorTextConfigType',
'PhabricatorEnv' => 'Phobject',
'PhabricatorEnvTestCase' => 'PhabricatorTestCase',
'PhabricatorEpochEditField' => 'PhabricatorEditField',
'PhabricatorEpochExportField' => 'PhabricatorExportField',
'PhabricatorEvent' => 'PhutilEvent',
'PhabricatorEventEngine' => 'Phobject',
'PhabricatorEventListener' => 'PhutilEventListener',
'PhabricatorEventType' => 'PhutilEventType',
'PhabricatorExampleEventListener' => 'PhabricatorEventListener',
'PhabricatorExcelExportFormat' => 'PhabricatorExportFormat',
'PhabricatorExecFutureFileUploadSource' => 'PhabricatorFileUploadSource',
'PhabricatorExportEngine' => 'Phobject',
'PhabricatorExportEngineBulkJobType' => 'PhabricatorWorkerSingleBulkJobType',
'PhabricatorExportEngineExtension' => 'Phobject',
'PhabricatorExportField' => 'Phobject',
'PhabricatorExportFormat' => 'Phobject',
'PhabricatorExportFormatSetting' => 'PhabricatorInternalSetting',
'PhabricatorExtendingPhabricatorConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorExtensionsSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorExternalAccount' => array(
'PhabricatorUserDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorExternalAccountQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorExternalAccountsSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorExtraConfigSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorFacebookAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorFact' => 'Phobject',
'PhabricatorFactAggregate' => 'PhabricatorFactDAO',
'PhabricatorFactApplication' => 'PhabricatorApplication',
'PhabricatorFactChartController' => 'PhabricatorFactController',
'PhabricatorFactController' => 'PhabricatorController',
'PhabricatorFactCursor' => 'PhabricatorFactDAO',
'PhabricatorFactDAO' => 'PhabricatorLiskDAO',
'PhabricatorFactDaemon' => 'PhabricatorDaemon',
'PhabricatorFactDatapointQuery' => 'Phobject',
'PhabricatorFactDimension' => 'PhabricatorFactDAO',
'PhabricatorFactEngine' => 'Phobject',
'PhabricatorFactEngineTestCase' => 'PhabricatorTestCase',
'PhabricatorFactHomeController' => 'PhabricatorFactController',
'PhabricatorFactIntDatapoint' => 'PhabricatorFactDAO',
'PhabricatorFactKeyDimension' => 'PhabricatorFactDimension',
'PhabricatorFactManagementAnalyzeWorkflow' => 'PhabricatorFactManagementWorkflow',
'PhabricatorFactManagementCursorsWorkflow' => 'PhabricatorFactManagementWorkflow',
'PhabricatorFactManagementDestroyWorkflow' => 'PhabricatorFactManagementWorkflow',
'PhabricatorFactManagementListWorkflow' => 'PhabricatorFactManagementWorkflow',
'PhabricatorFactManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorFactObjectController' => 'PhabricatorFactController',
'PhabricatorFactObjectDimension' => 'PhabricatorFactDimension',
'PhabricatorFactRaw' => 'PhabricatorFactDAO',
'PhabricatorFactUpdateIterator' => 'PhutilBufferedIterator',
'PhabricatorFaviconRef' => 'Phobject',
'PhabricatorFaviconRefQuery' => 'Phobject',
'PhabricatorFavoritesApplication' => 'PhabricatorApplication',
'PhabricatorFavoritesController' => 'PhabricatorController',
'PhabricatorFavoritesMainMenuBarExtension' => 'PhabricatorMainMenuBarExtension',
'PhabricatorFavoritesMenuItemController' => 'PhabricatorFavoritesController',
'PhabricatorFavoritesProfileMenuEngine' => 'PhabricatorProfileMenuEngine',
'PhabricatorFaxContentSource' => 'PhabricatorContentSource',
'PhabricatorFeedApplication' => 'PhabricatorApplication',
'PhabricatorFeedBuilder' => 'Phobject',
'PhabricatorFeedConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorFeedController' => 'PhabricatorController',
'PhabricatorFeedDAO' => 'PhabricatorLiskDAO',
'PhabricatorFeedDetailController' => 'PhabricatorFeedController',
'PhabricatorFeedListController' => 'PhabricatorFeedController',
'PhabricatorFeedManagementRepublishWorkflow' => 'PhabricatorFeedManagementWorkflow',
'PhabricatorFeedManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorFeedQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorFeedSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorFeedStory' => array(
'Phobject',
'PhabricatorPolicyInterface',
'PhabricatorMarkupInterface',
),
'PhabricatorFeedStoryData' => array(
'PhabricatorFeedDAO',
'PhabricatorDestructibleInterface',
),
'PhabricatorFeedStoryNotification' => 'PhabricatorFeedDAO',
'PhabricatorFeedStoryPublisher' => 'Phobject',
'PhabricatorFeedStoryReference' => 'PhabricatorFeedDAO',
'PhabricatorFerretEngine' => 'Phobject',
'PhabricatorFerretEngineTestCase' => 'PhabricatorTestCase',
'PhabricatorFerretFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension',
'PhabricatorFerretFulltextStorageEngine' => 'PhabricatorFulltextStorageEngine',
'PhabricatorFerretMetadata' => 'Phobject',
'PhabricatorFerretSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
'PhabricatorFile' => array(
'PhabricatorFileDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorSubscribableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorConduitResultInterface',
'PhabricatorIndexableInterface',
'PhabricatorNgramsInterface',
),
'PhabricatorFileAES256StorageFormat' => 'PhabricatorFileStorageFormat',
'PhabricatorFileBundleLoader' => 'Phobject',
'PhabricatorFileChunk' => array(
'PhabricatorFileDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorFileChunkIterator' => array(
'Phobject',
'Iterator',
),
'PhabricatorFileChunkQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorFileComposeController' => 'PhabricatorFileController',
'PhabricatorFileController' => 'PhabricatorController',
'PhabricatorFileDAO' => 'PhabricatorLiskDAO',
'PhabricatorFileDataController' => 'PhabricatorFileController',
'PhabricatorFileDeleteController' => 'PhabricatorFileController',
'PhabricatorFileDeleteTransaction' => 'PhabricatorFileTransactionType',
'PhabricatorFileDocumentController' => 'PhabricatorFileController',
'PhabricatorFileDocumentRenderingEngine' => 'PhabricatorDocumentRenderingEngine',
'PhabricatorFileDropUploadController' => 'PhabricatorFileController',
'PhabricatorFileEditController' => 'PhabricatorFileController',
'PhabricatorFileEditEngine' => 'PhabricatorEditEngine',
'PhabricatorFileEditField' => 'PhabricatorEditField',
'PhabricatorFileEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorFileExternalRequest' => array(
'PhabricatorFileDAO',
'PhabricatorDestructibleInterface',
),
'PhabricatorFileExternalRequestGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorFileFilePHIDType' => 'PhabricatorPHIDType',
'PhabricatorFileHasObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorFileIconSetSelectController' => 'PhabricatorFileController',
'PhabricatorFileImageMacro' => array(
'PhabricatorFileDAO',
'PhabricatorSubscribableInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorFlaggableInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorPolicyInterface',
),
'PhabricatorFileImageProxyController' => 'PhabricatorFileController',
'PhabricatorFileImageTransform' => 'PhabricatorFileTransform',
'PhabricatorFileIntegrityException' => 'Exception',
'PhabricatorFileLightboxController' => 'PhabricatorFileController',
'PhabricatorFileLinkView' => 'AphrontTagView',
'PhabricatorFileListController' => 'PhabricatorFileController',
'PhabricatorFileNameNgrams' => 'PhabricatorSearchNgrams',
'PhabricatorFileNameTransaction' => 'PhabricatorFileTransactionType',
'PhabricatorFileQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorFileROT13StorageFormat' => 'PhabricatorFileStorageFormat',
'PhabricatorFileRawStorageFormat' => 'PhabricatorFileStorageFormat',
'PhabricatorFileSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorFileSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhabricatorFileSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorFileStorageBlob' => 'PhabricatorFileDAO',
'PhabricatorFileStorageConfigurationException' => 'Exception',
'PhabricatorFileStorageEngine' => 'Phobject',
'PhabricatorFileStorageEngineTestCase' => 'PhabricatorTestCase',
'PhabricatorFileStorageFormat' => 'Phobject',
'PhabricatorFileStorageFormatTestCase' => 'PhabricatorTestCase',
'PhabricatorFileTemporaryGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorFileTestCase' => 'PhabricatorTestCase',
'PhabricatorFileTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorFileThumbnailTransform' => 'PhabricatorFileImageTransform',
'PhabricatorFileTransaction' => 'PhabricatorModularTransaction',
'PhabricatorFileTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhabricatorFileTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorFileTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorFileTransform' => 'Phobject',
'PhabricatorFileTransformController' => 'PhabricatorFileController',
'PhabricatorFileTransformListController' => 'PhabricatorFileController',
'PhabricatorFileTransformTestCase' => 'PhabricatorTestCase',
'PhabricatorFileUploadController' => 'PhabricatorFileController',
'PhabricatorFileUploadDialogController' => 'PhabricatorFileController',
'PhabricatorFileUploadException' => 'Exception',
'PhabricatorFileUploadSource' => 'Phobject',
'PhabricatorFileUploadSourceByteLimitException' => 'Exception',
'PhabricatorFileViewController' => 'PhabricatorFileController',
'PhabricatorFileinfoSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorFilesApplication' => 'PhabricatorApplication',
'PhabricatorFilesApplicationStorageEnginePanel' => 'PhabricatorApplicationConfigurationPanel',
'PhabricatorFilesBuiltinFile' => 'Phobject',
'PhabricatorFilesComposeAvatarBuiltinFile' => 'PhabricatorFilesBuiltinFile',
'PhabricatorFilesComposeIconBuiltinFile' => 'PhabricatorFilesBuiltinFile',
'PhabricatorFilesConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorFilesManagementCatWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementCompactWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementCycleWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementEncodeWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementEnginesWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementGenerateKeyWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementIntegrityWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementMigrateWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementRebuildWorkflow' => 'PhabricatorFilesManagementWorkflow',
'PhabricatorFilesManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorFilesOnDiskBuiltinFile' => 'PhabricatorFilesBuiltinFile',
'PhabricatorFilesOutboundRequestAction' => 'PhabricatorSystemAction',
'PhabricatorFiletreeVisibleSetting' => 'PhabricatorInternalSetting',
'PhabricatorFiletreeWidthSetting' => 'PhabricatorInternalSetting',
'PhabricatorFlag' => array(
'PhabricatorFlagDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorFlagAddFlagHeraldAction' => 'HeraldAction',
'PhabricatorFlagColor' => 'PhabricatorFlagConstants',
'PhabricatorFlagConstants' => 'Phobject',
'PhabricatorFlagController' => 'PhabricatorController',
'PhabricatorFlagDAO' => 'PhabricatorLiskDAO',
'PhabricatorFlagDeleteController' => 'PhabricatorFlagController',
'PhabricatorFlagDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorFlagEditController' => 'PhabricatorFlagController',
'PhabricatorFlagListController' => 'PhabricatorFlagController',
'PhabricatorFlagQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorFlagSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorFlagSelectControl' => 'AphrontFormControl',
'PhabricatorFlaggableInterface' => 'PhabricatorPHIDInterface',
'PhabricatorFlagsApplication' => 'PhabricatorApplication',
'PhabricatorFlagsUIEventListener' => 'PhabricatorEventListener',
'PhabricatorFulltextEngine' => 'Phobject',
'PhabricatorFulltextEngineExtension' => 'Phobject',
'PhabricatorFulltextEngineExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorFulltextIndexEngineExtension' => 'PhabricatorIndexEngineExtension',
'PhabricatorFulltextInterface' => 'PhabricatorIndexableInterface',
'PhabricatorFulltextResultSet' => 'Phobject',
'PhabricatorFulltextStorageEngine' => 'Phobject',
'PhabricatorFulltextToken' => 'Phobject',
'PhabricatorFundApplication' => 'PhabricatorApplication',
'PhabricatorGDSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorGarbageCollector' => 'Phobject',
'PhabricatorGarbageCollectorManagementCollectWorkflow' => 'PhabricatorGarbageCollectorManagementWorkflow',
'PhabricatorGarbageCollectorManagementCompactEdgesWorkflow' => 'PhabricatorGarbageCollectorManagementWorkflow',
'PhabricatorGarbageCollectorManagementSetPolicyWorkflow' => 'PhabricatorGarbageCollectorManagementWorkflow',
'PhabricatorGarbageCollectorManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorGeneralCachePurger' => 'PhabricatorCachePurger',
'PhabricatorGestureUIExample' => 'PhabricatorUIExample',
'PhabricatorGitGraphStream' => 'PhabricatorRepositoryGraphStream',
'PhabricatorGitHubAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorGlobalLock' => 'PhutilLock',
'PhabricatorGlobalUploadTargetView' => 'AphrontView',
'PhabricatorGoogleAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorGuidanceContext' => 'Phobject',
'PhabricatorGuidanceEngine' => 'Phobject',
'PhabricatorGuidanceEngineExtension' => 'Phobject',
'PhabricatorGuidanceMessage' => 'Phobject',
'PhabricatorGuideApplication' => 'PhabricatorApplication',
'PhabricatorGuideController' => 'PhabricatorController',
'PhabricatorGuideInstallModule' => 'PhabricatorGuideModule',
'PhabricatorGuideItemView' => 'Phobject',
'PhabricatorGuideListView' => 'AphrontView',
'PhabricatorGuideModule' => 'Phobject',
'PhabricatorGuideModuleController' => 'PhabricatorGuideController',
'PhabricatorGuideQuickStartModule' => 'PhabricatorGuideModule',
'PhabricatorHMACTestCase' => 'PhabricatorTestCase',
'PhabricatorHTTPParameterTypeTableView' => 'AphrontView',
'PhabricatorHandleList' => array(
'Phobject',
'Iterator',
'ArrayAccess',
'Countable',
),
'PhabricatorHandleObjectSelectorDataView' => 'Phobject',
'PhabricatorHandlePool' => 'Phobject',
'PhabricatorHandlePoolTestCase' => 'PhabricatorTestCase',
'PhabricatorHandleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorHandleRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorHandlesEditField' => 'PhabricatorPHIDListEditField',
'PhabricatorHarbormasterApplication' => 'PhabricatorApplication',
'PhabricatorHash' => 'Phobject',
'PhabricatorHashTestCase' => 'PhabricatorTestCase',
'PhabricatorHelpApplication' => 'PhabricatorApplication',
'PhabricatorHelpController' => 'PhabricatorController',
'PhabricatorHelpDocumentationController' => 'PhabricatorHelpController',
'PhabricatorHelpEditorProtocolController' => 'PhabricatorHelpController',
'PhabricatorHelpKeyboardShortcutController' => 'PhabricatorHelpController',
'PhabricatorHeraldApplication' => 'PhabricatorApplication',
'PhabricatorHeraldContentSource' => 'PhabricatorContentSource',
'PhabricatorHexdumpDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorHighSecurityRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorHomeApplication' => 'PhabricatorApplication',
'PhabricatorHomeConstants' => 'PhabricatorHomeController',
'PhabricatorHomeController' => 'PhabricatorController',
'PhabricatorHomeLauncherProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorHomeMenuItemController' => 'PhabricatorHomeController',
'PhabricatorHomeProfileMenuEngine' => 'PhabricatorProfileMenuEngine',
'PhabricatorHomeProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorHovercardEngineExtension' => 'Phobject',
'PhabricatorHovercardEngineExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorIDExportField' => 'PhabricatorExportField',
'PhabricatorIDsSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
'PhabricatorIDsSearchField' => 'PhabricatorSearchField',
'PhabricatorIconDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorIconRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorIconSet' => 'Phobject',
'PhabricatorIconSetEditField' => 'PhabricatorEditField',
'PhabricatorIconSetIcon' => 'Phobject',
'PhabricatorImageDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorImageMacroRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorImageRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorImageTransformer' => 'Phobject',
'PhabricatorImagemagickSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorInFlightErrorView' => 'AphrontView',
'PhabricatorIndexEngine' => 'Phobject',
'PhabricatorIndexEngineExtension' => 'Phobject',
'PhabricatorIndexEngineExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorInfrastructureTestCase' => 'PhabricatorTestCase',
'PhabricatorInlineCommentController' => 'PhabricatorController',
'PhabricatorInlineCommentInterface' => 'PhabricatorMarkupInterface',
'PhabricatorInlineCommentPreviewController' => 'PhabricatorController',
'PhabricatorInlineSummaryView' => 'AphrontView',
'PhabricatorInstructionsEditField' => 'PhabricatorEditField',
'PhabricatorIntConfigType' => 'PhabricatorTextConfigType',
'PhabricatorIntEditField' => 'PhabricatorEditField',
'PhabricatorIntExportField' => 'PhabricatorExportField',
'PhabricatorInternalSetting' => 'PhabricatorSetting',
'PhabricatorInternationalizationManagementExtractWorkflow' => 'PhabricatorInternationalizationManagementWorkflow',
'PhabricatorInternationalizationManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorInvalidConfigSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorIteratedMD5PasswordHasher' => 'PhabricatorPasswordHasher',
'PhabricatorIteratedMD5PasswordHasherTestCase' => 'PhabricatorTestCase',
'PhabricatorIteratorFileUploadSource' => 'PhabricatorFileUploadSource',
'PhabricatorJIRAAuthProvider' => 'PhabricatorOAuth1AuthProvider',
'PhabricatorJSONConfigType' => 'PhabricatorTextConfigType',
'PhabricatorJSONDocumentEngine' => 'PhabricatorTextDocumentEngine',
'PhabricatorJSONExportFormat' => 'PhabricatorExportFormat',
'PhabricatorJavelinLinter' => 'ArcanistLinter',
'PhabricatorJiraIssueHasObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorJupyterDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorKeyValueDatabaseCache' => 'PhutilKeyValueCache',
'PhabricatorKeyValueSerializingCacheProxy' => 'PhutilKeyValueCacheProxy',
'PhabricatorKeyboardRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorKeyring' => 'Phobject',
'PhabricatorKeyringConfigOptionType' => 'PhabricatorConfigJSONOptionType',
'PhabricatorLDAPAuthProvider' => 'PhabricatorAuthProvider',
'PhabricatorLabelProfileMenuItem' => 'PhabricatorProfileMenuItem',
+ 'PhabricatorLanguageSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorLegalpadApplication' => 'PhabricatorApplication',
- 'PhabricatorLegalpadConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorLegalpadDocumentPHIDType' => 'PhabricatorPHIDType',
'PhabricatorLegalpadSignaturePolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorLibraryTestCase' => 'PhutilLibraryTestCase',
'PhabricatorLinkProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorLipsumArtist' => 'Phobject',
'PhabricatorLipsumContentSource' => 'PhabricatorContentSource',
'PhabricatorLipsumGenerateWorkflow' => 'PhabricatorLipsumManagementWorkflow',
'PhabricatorLipsumManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorLipsumMondrianArtist' => 'PhabricatorLipsumArtist',
'PhabricatorLiskDAO' => 'LiskDAO',
'PhabricatorLiskExportEngineExtension' => 'PhabricatorExportEngineExtension',
'PhabricatorLiskFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension',
'PhabricatorLiskSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
'PhabricatorLiskSerializer' => 'Phobject',
'PhabricatorListExportField' => 'PhabricatorExportField',
'PhabricatorLocalDiskFileStorageEngine' => 'PhabricatorFileStorageEngine',
'PhabricatorLocalTimeTestCase' => 'PhabricatorTestCase',
'PhabricatorLocaleScopeGuard' => 'Phobject',
'PhabricatorLocaleScopeGuardTestCase' => 'PhabricatorTestCase',
'PhabricatorLockLogManagementWorkflow' => 'PhabricatorLockManagementWorkflow',
'PhabricatorLockManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorLogTriggerAction' => 'PhabricatorTriggerAction',
'PhabricatorLogoutController' => 'PhabricatorAuthController',
'PhabricatorLunarPhasePolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorMacroApplication' => 'PhabricatorApplication',
'PhabricatorMacroAudioBehaviorTransaction' => 'PhabricatorMacroTransactionType',
'PhabricatorMacroAudioController' => 'PhabricatorMacroController',
'PhabricatorMacroAudioTransaction' => 'PhabricatorMacroTransactionType',
- 'PhabricatorMacroConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorMacroController' => 'PhabricatorController',
'PhabricatorMacroDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorMacroDisableController' => 'PhabricatorMacroController',
'PhabricatorMacroDisabledTransaction' => 'PhabricatorMacroTransactionType',
'PhabricatorMacroEditController' => 'PhameBlogController',
'PhabricatorMacroEditEngine' => 'PhabricatorEditEngine',
'PhabricatorMacroEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorMacroFileTransaction' => 'PhabricatorMacroTransactionType',
'PhabricatorMacroListController' => 'PhabricatorMacroController',
'PhabricatorMacroMacroPHIDType' => 'PhabricatorPHIDType',
'PhabricatorMacroMailReceiver' => 'PhabricatorObjectMailReceiver',
'PhabricatorMacroManageCapability' => 'PhabricatorPolicyCapability',
'PhabricatorMacroMemeController' => 'PhabricatorMacroController',
'PhabricatorMacroMemeDialogController' => 'PhabricatorMacroController',
'PhabricatorMacroNameTransaction' => 'PhabricatorMacroTransactionType',
'PhabricatorMacroQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorMacroReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhabricatorMacroSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorMacroTestCase' => 'PhabricatorTestCase',
'PhabricatorMacroTransaction' => 'PhabricatorModularTransaction',
'PhabricatorMacroTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhabricatorMacroTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorMacroTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorMacroViewController' => 'PhabricatorMacroController',
+ 'PhabricatorMailAdapter' => 'Phobject',
+ 'PhabricatorMailAmazonSESAdapter' => 'PhabricatorMailAdapter',
+ 'PhabricatorMailAmazonSNSAdapter' => 'PhabricatorMailAdapter',
+ 'PhabricatorMailAttachment' => 'Phobject',
'PhabricatorMailConfigTestCase' => 'PhabricatorTestCase',
+ 'PhabricatorMailEmailEngine' => 'PhabricatorMailMessageEngine',
'PhabricatorMailEmailHeraldField' => 'HeraldField',
'PhabricatorMailEmailHeraldFieldGroup' => 'HeraldFieldGroup',
+ 'PhabricatorMailEmailMessage' => 'PhabricatorMailExternalMessage',
'PhabricatorMailEmailSubjectHeraldField' => 'PhabricatorMailEmailHeraldField',
'PhabricatorMailEngineExtension' => 'Phobject',
- 'PhabricatorMailImplementationAdapter' => 'Phobject',
- 'PhabricatorMailImplementationAmazonSESAdapter' => 'PhabricatorMailImplementationPHPMailerLiteAdapter',
- 'PhabricatorMailImplementationMailgunAdapter' => 'PhabricatorMailImplementationAdapter',
- 'PhabricatorMailImplementationPHPMailerAdapter' => 'PhabricatorMailImplementationAdapter',
- 'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'PhabricatorMailImplementationAdapter',
- 'PhabricatorMailImplementationPostmarkAdapter' => 'PhabricatorMailImplementationAdapter',
- 'PhabricatorMailImplementationSendGridAdapter' => 'PhabricatorMailImplementationAdapter',
- 'PhabricatorMailImplementationTestAdapter' => 'PhabricatorMailImplementationAdapter',
+ 'PhabricatorMailExternalMessage' => 'Phobject',
+ 'PhabricatorMailHeader' => 'Phobject',
+ 'PhabricatorMailMailgunAdapter' => 'PhabricatorMailAdapter',
'PhabricatorMailManagementListInboundWorkflow' => 'PhabricatorMailManagementWorkflow',
'PhabricatorMailManagementListOutboundWorkflow' => 'PhabricatorMailManagementWorkflow',
'PhabricatorMailManagementReceiveTestWorkflow' => 'PhabricatorMailManagementWorkflow',
'PhabricatorMailManagementResendWorkflow' => 'PhabricatorMailManagementWorkflow',
'PhabricatorMailManagementSendTestWorkflow' => 'PhabricatorMailManagementWorkflow',
'PhabricatorMailManagementShowInboundWorkflow' => 'PhabricatorMailManagementWorkflow',
'PhabricatorMailManagementShowOutboundWorkflow' => 'PhabricatorMailManagementWorkflow',
'PhabricatorMailManagementUnverifyWorkflow' => 'PhabricatorMailManagementWorkflow',
'PhabricatorMailManagementVolumeWorkflow' => 'PhabricatorMailManagementWorkflow',
'PhabricatorMailManagementWorkflow' => 'PhabricatorManagementWorkflow',
+ 'PhabricatorMailMessageEngine' => 'Phobject',
'PhabricatorMailMustEncryptHeraldAction' => 'HeraldAction',
'PhabricatorMailOutboundMailHeraldAdapter' => 'HeraldAdapter',
'PhabricatorMailOutboundRoutingHeraldAction' => 'HeraldAction',
'PhabricatorMailOutboundRoutingSelfEmailHeraldAction' => 'PhabricatorMailOutboundRoutingHeraldAction',
'PhabricatorMailOutboundRoutingSelfNotificationHeraldAction' => 'PhabricatorMailOutboundRoutingHeraldAction',
'PhabricatorMailOutboundStatus' => 'Phobject',
+ 'PhabricatorMailPostmarkAdapter' => 'PhabricatorMailAdapter',
'PhabricatorMailPropertiesDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorMailReceiver' => 'Phobject',
'PhabricatorMailReceiverTestCase' => 'PhabricatorTestCase',
'PhabricatorMailReplyHandler' => 'Phobject',
'PhabricatorMailRoutingRule' => 'Phobject',
+ 'PhabricatorMailSMSEngine' => 'PhabricatorMailMessageEngine',
+ 'PhabricatorMailSMSMessage' => 'PhabricatorMailExternalMessage',
+ 'PhabricatorMailSMTPAdapter' => 'PhabricatorMailAdapter',
+ 'PhabricatorMailSendGridAdapter' => 'PhabricatorMailAdapter',
+ 'PhabricatorMailSendmailAdapter' => 'PhabricatorMailAdapter',
'PhabricatorMailSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorMailStamp' => 'Phobject',
'PhabricatorMailTarget' => 'Phobject',
- 'PhabricatorMailgunConfigOptions' => 'PhabricatorApplicationConfigOptions',
+ 'PhabricatorMailTestAdapter' => 'PhabricatorMailAdapter',
+ 'PhabricatorMailTwilioAdapter' => 'PhabricatorMailAdapter',
+ 'PhabricatorMailUtil' => 'Phobject',
'PhabricatorMainMenuBarExtension' => 'Phobject',
'PhabricatorMainMenuSearchView' => 'AphrontView',
'PhabricatorMainMenuView' => 'AphrontView',
'PhabricatorManageProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorManagementWorkflow' => 'PhutilArgumentWorkflow',
'PhabricatorManiphestApplication' => 'PhabricatorApplication',
'PhabricatorManiphestConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorManiphestTaskFactEngine' => 'PhabricatorTransactionFactEngine',
'PhabricatorManiphestTaskTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorManualActivitySetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorMarkupCache' => 'PhabricatorCacheDAO',
'PhabricatorMarkupEngine' => 'Phobject',
'PhabricatorMarkupEngineTestCase' => 'PhabricatorTestCase',
'PhabricatorMarkupOneOff' => array(
'Phobject',
'PhabricatorMarkupInterface',
),
'PhabricatorMarkupPreviewController' => 'PhabricatorController',
'PhabricatorMemeEngine' => 'Phobject',
'PhabricatorMemeRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorMentionRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorMercurialGraphStream' => 'PhabricatorRepositoryGraphStream',
'PhabricatorMetaMTAActor' => 'Phobject',
'PhabricatorMetaMTAActorQuery' => 'PhabricatorQuery',
'PhabricatorMetaMTAApplication' => 'PhabricatorApplication',
'PhabricatorMetaMTAApplicationEmail' => array(
'PhabricatorMetaMTADAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSpacesInterface',
),
'PhabricatorMetaMTAApplicationEmailDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorMetaMTAApplicationEmailEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorMetaMTAApplicationEmailHeraldField' => 'HeraldField',
'PhabricatorMetaMTAApplicationEmailPHIDType' => 'PhabricatorPHIDType',
'PhabricatorMetaMTAApplicationEmailPanel' => 'PhabricatorApplicationConfigurationPanel',
'PhabricatorMetaMTAApplicationEmailQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorMetaMTAApplicationEmailTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorMetaMTAApplicationEmailTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
- 'PhabricatorMetaMTAAttachment' => 'Phobject',
'PhabricatorMetaMTAConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorMetaMTAController' => 'PhabricatorController',
'PhabricatorMetaMTADAO' => 'PhabricatorLiskDAO',
'PhabricatorMetaMTAEmailBodyParser' => 'Phobject',
'PhabricatorMetaMTAEmailBodyParserTestCase' => 'PhabricatorTestCase',
'PhabricatorMetaMTAEmailHeraldAction' => 'HeraldAction',
'PhabricatorMetaMTAEmailOthersHeraldAction' => 'PhabricatorMetaMTAEmailHeraldAction',
'PhabricatorMetaMTAEmailSelfHeraldAction' => 'PhabricatorMetaMTAEmailHeraldAction',
'PhabricatorMetaMTAErrorMailAction' => 'PhabricatorSystemAction',
'PhabricatorMetaMTAMail' => array(
'PhabricatorMetaMTADAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorMetaMTAMailBody' => 'Phobject',
'PhabricatorMetaMTAMailBodyTestCase' => 'PhabricatorTestCase',
'PhabricatorMetaMTAMailHasRecipientEdgeType' => 'PhabricatorEdgeType',
'PhabricatorMetaMTAMailListController' => 'PhabricatorMetaMTAController',
'PhabricatorMetaMTAMailPHIDType' => 'PhabricatorPHIDType',
'PhabricatorMetaMTAMailProperties' => array(
'PhabricatorMetaMTADAO',
'PhabricatorPolicyInterface',
),
'PhabricatorMetaMTAMailPropertiesQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorMetaMTAMailQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorMetaMTAMailSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorMetaMTAMailSection' => 'Phobject',
'PhabricatorMetaMTAMailTestCase' => 'PhabricatorTestCase',
'PhabricatorMetaMTAMailViewController' => 'PhabricatorMetaMTAController',
'PhabricatorMetaMTAMailableDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorMetaMTAMailableFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorMetaMTAMailgunReceiveController' => 'PhabricatorMetaMTAController',
'PhabricatorMetaMTAMemberQuery' => 'PhabricatorQuery',
'PhabricatorMetaMTAPermanentFailureException' => 'Exception',
'PhabricatorMetaMTAPostmarkReceiveController' => 'PhabricatorMetaMTAController',
'PhabricatorMetaMTAReceivedMail' => 'PhabricatorMetaMTADAO',
'PhabricatorMetaMTAReceivedMailProcessingException' => 'Exception',
'PhabricatorMetaMTAReceivedMailTestCase' => 'PhabricatorTestCase',
'PhabricatorMetaMTASchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorMetaMTASendGridReceiveController' => 'PhabricatorMetaMTAController',
'PhabricatorMetaMTAWorker' => 'PhabricatorWorker',
'PhabricatorMetronomicTriggerClock' => 'PhabricatorTriggerClock',
'PhabricatorModularTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorModularTransactionType' => 'Phobject',
'PhabricatorMonogramDatasourceEngineExtension' => 'PhabricatorDatasourceEngineExtension',
'PhabricatorMonospacedFontSetting' => 'PhabricatorStringSetting',
'PhabricatorMonospacedTextareasSetting' => 'PhabricatorSelectSetting',
'PhabricatorMotivatorProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorMultiColumnUIExample' => 'PhabricatorUIExample',
'PhabricatorMultiFactorSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorMultimeterApplication' => 'PhabricatorApplication',
'PhabricatorMustVerifyEmailController' => 'PhabricatorAuthController',
'PhabricatorMutedByEdgeType' => 'PhabricatorEdgeType',
'PhabricatorMutedEdgeType' => 'PhabricatorEdgeType',
'PhabricatorMySQLConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorMySQLFileStorageEngine' => 'PhabricatorFileStorageEngine',
'PhabricatorMySQLSearchHost' => 'PhabricatorSearchHost',
'PhabricatorMySQLSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorNamedQuery' => array(
'PhabricatorSearchDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorNamedQueryConfig' => array(
'PhabricatorSearchDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorNamedQueryConfigQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorNamedQueryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorNavigationRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorNeverTriggerClock' => 'PhabricatorTriggerClock',
'PhabricatorNgramsIndexEngineExtension' => 'PhabricatorIndexEngineExtension',
'PhabricatorNgramsInterface' => 'PhabricatorIndexableInterface',
'PhabricatorNotificationBuilder' => 'Phobject',
'PhabricatorNotificationClearController' => 'PhabricatorNotificationController',
'PhabricatorNotificationClient' => 'Phobject',
'PhabricatorNotificationConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorNotificationController' => 'PhabricatorController',
'PhabricatorNotificationDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorNotificationIndividualController' => 'PhabricatorNotificationController',
'PhabricatorNotificationListController' => 'PhabricatorNotificationController',
'PhabricatorNotificationPanelController' => 'PhabricatorNotificationController',
'PhabricatorNotificationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorNotificationSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorNotificationServerRef' => 'Phobject',
'PhabricatorNotificationServersConfigType' => 'PhabricatorJSONConfigType',
'PhabricatorNotificationStatusView' => 'AphrontTagView',
'PhabricatorNotificationTestController' => 'PhabricatorNotificationController',
'PhabricatorNotificationUIExample' => 'PhabricatorUIExample',
'PhabricatorNotificationsApplication' => 'PhabricatorApplication',
'PhabricatorNotificationsSetting' => 'PhabricatorInternalSetting',
'PhabricatorNotificationsSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorNuanceApplication' => 'PhabricatorApplication',
'PhabricatorOAuth1AuthProvider' => 'PhabricatorOAuthAuthProvider',
'PhabricatorOAuth1SecretTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType',
'PhabricatorOAuth2AuthProvider' => 'PhabricatorOAuthAuthProvider',
'PhabricatorOAuthAuthProvider' => 'PhabricatorAuthProvider',
'PhabricatorOAuthClientAuthorization' => array(
'PhabricatorOAuthServerDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorOAuthClientAuthorizationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorOAuthClientController' => 'PhabricatorOAuthServerController',
'PhabricatorOAuthClientDisableController' => 'PhabricatorOAuthClientController',
'PhabricatorOAuthClientEditController' => 'PhabricatorOAuthClientController',
'PhabricatorOAuthClientListController' => 'PhabricatorOAuthClientController',
'PhabricatorOAuthClientSecretController' => 'PhabricatorOAuthClientController',
'PhabricatorOAuthClientTestController' => 'PhabricatorOAuthClientController',
'PhabricatorOAuthClientViewController' => 'PhabricatorOAuthClientController',
'PhabricatorOAuthResponse' => 'AphrontResponse',
'PhabricatorOAuthServer' => 'Phobject',
'PhabricatorOAuthServerAccessToken' => 'PhabricatorOAuthServerDAO',
'PhabricatorOAuthServerApplication' => 'PhabricatorApplication',
'PhabricatorOAuthServerAuthController' => 'PhabricatorOAuthServerController',
'PhabricatorOAuthServerAuthorizationCode' => 'PhabricatorOAuthServerDAO',
'PhabricatorOAuthServerAuthorizationsSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorOAuthServerClient' => array(
'PhabricatorOAuthServerDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorOAuthServerClientAuthorizationPHIDType' => 'PhabricatorPHIDType',
'PhabricatorOAuthServerClientPHIDType' => 'PhabricatorPHIDType',
'PhabricatorOAuthServerClientQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorOAuthServerClientSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorOAuthServerController' => 'PhabricatorController',
'PhabricatorOAuthServerCreateClientsCapability' => 'PhabricatorPolicyCapability',
'PhabricatorOAuthServerDAO' => 'PhabricatorLiskDAO',
'PhabricatorOAuthServerEditEngine' => 'PhabricatorEditEngine',
'PhabricatorOAuthServerEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorOAuthServerSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorOAuthServerScope' => 'Phobject',
'PhabricatorOAuthServerTestCase' => 'PhabricatorTestCase',
'PhabricatorOAuthServerTokenController' => 'PhabricatorOAuthServerController',
'PhabricatorOAuthServerTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorOAuthServerTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorObjectGraph' => 'AbstractDirectedGraph',
'PhabricatorObjectHandle' => array(
'Phobject',
'PhabricatorPolicyInterface',
),
'PhabricatorObjectHasAsanaSubtaskEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectHasAsanaTaskEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectHasContributorEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectHasDraftEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectHasFileEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectHasJiraIssueEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectHasSubscriberEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectHasUnsubscriberEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectHasWatcherEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectListQuery' => 'Phobject',
'PhabricatorObjectListQueryTestCase' => 'PhabricatorTestCase',
'PhabricatorObjectMailReceiver' => 'PhabricatorMailReceiver',
'PhabricatorObjectMailReceiverTestCase' => 'PhabricatorTestCase',
'PhabricatorObjectMentionedByObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectMentionsObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorObjectRelationship' => 'Phobject',
'PhabricatorObjectRelationshipList' => 'Phobject',
'PhabricatorObjectRelationshipSource' => 'Phobject',
'PhabricatorObjectRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorObjectSelectorDialog' => 'Phobject',
'PhabricatorObjectStatus' => 'Phobject',
'PhabricatorOffsetPagedQuery' => 'PhabricatorQuery',
'PhabricatorOldWorldContentSource' => 'PhabricatorContentSource',
'PhabricatorOlderInlinesSetting' => 'PhabricatorSelectSetting',
'PhabricatorOneTimeTriggerClock' => 'PhabricatorTriggerClock',
'PhabricatorOpcodeCacheSpec' => 'PhabricatorCacheSpec',
'PhabricatorOptionGroupSetting' => 'PhabricatorSetting',
'PhabricatorOwnerPathQuery' => 'Phobject',
'PhabricatorOwnersApplication' => 'PhabricatorApplication',
'PhabricatorOwnersArchiveController' => 'PhabricatorOwnersController',
'PhabricatorOwnersConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorOwnersConfiguredCustomField' => array(
'PhabricatorOwnersCustomField',
'PhabricatorStandardCustomFieldInterface',
),
'PhabricatorOwnersController' => 'PhabricatorController',
'PhabricatorOwnersCustomField' => 'PhabricatorCustomField',
'PhabricatorOwnersCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage',
'PhabricatorOwnersCustomFieldStorage' => 'PhabricatorCustomFieldStorage',
'PhabricatorOwnersCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage',
'PhabricatorOwnersDAO' => 'PhabricatorLiskDAO',
'PhabricatorOwnersDefaultEditCapability' => 'PhabricatorPolicyCapability',
'PhabricatorOwnersDefaultViewCapability' => 'PhabricatorPolicyCapability',
'PhabricatorOwnersDetailController' => 'PhabricatorOwnersController',
'PhabricatorOwnersEditController' => 'PhabricatorOwnersController',
'PhabricatorOwnersHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension',
'PhabricatorOwnersListController' => 'PhabricatorOwnersController',
'PhabricatorOwnersOwner' => 'PhabricatorOwnersDAO',
'PhabricatorOwnersPackage' => array(
'PhabricatorOwnersDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorDestructibleInterface',
'PhabricatorConduitResultInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
'PhabricatorNgramsInterface',
),
'PhabricatorOwnersPackageAuditingTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackageAutoreviewTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackageContextFreeGrammar' => 'PhutilContextFreeGrammar',
'PhabricatorOwnersPackageDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorOwnersPackageDescriptionTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackageDominionTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackageEditEngine' => 'PhabricatorEditEngine',
'PhabricatorOwnersPackageFerretEngine' => 'PhabricatorFerretEngine',
'PhabricatorOwnersPackageFulltextEngine' => 'PhabricatorFulltextEngine',
'PhabricatorOwnersPackageFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorOwnersPackageIgnoredTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackageNameNgrams' => 'PhabricatorSearchNgrams',
'PhabricatorOwnersPackageNameTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackageOwnerDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorOwnersPackageOwnersTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackagePHIDType' => 'PhabricatorPHIDType',
'PhabricatorOwnersPackagePathsTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackagePrimaryTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorOwnersPackageRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhabricatorOwnersPackageSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorOwnersPackageStatusTransaction' => 'PhabricatorOwnersPackageTransactionType',
'PhabricatorOwnersPackageTestCase' => 'PhabricatorTestCase',
'PhabricatorOwnersPackageTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorOwnersPackageTransaction' => 'PhabricatorModularTransaction',
'PhabricatorOwnersPackageTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorOwnersPackageTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorOwnersPackageTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorOwnersPath' => 'PhabricatorOwnersDAO',
'PhabricatorOwnersPathContextFreeGrammar' => 'PhutilContextFreeGrammar',
'PhabricatorOwnersPathsController' => 'PhabricatorOwnersController',
'PhabricatorOwnersPathsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorOwnersSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorOwnersSearchField' => 'PhabricatorSearchTokenizerField',
'PhabricatorPDFDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorPHDConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorPHID' => 'Phobject',
'PhabricatorPHIDConstants' => 'Phobject',
'PhabricatorPHIDExportField' => 'PhabricatorExportField',
'PhabricatorPHIDListEditField' => 'PhabricatorEditField',
'PhabricatorPHIDListEditType' => 'PhabricatorEditType',
'PhabricatorPHIDListExportField' => 'PhabricatorListExportField',
'PhabricatorPHIDMailStamp' => 'PhabricatorMailStamp',
'PhabricatorPHIDResolver' => 'Phobject',
'PhabricatorPHIDType' => 'Phobject',
'PhabricatorPHIDTypeTestCase' => 'PhutilTestCase',
'PhabricatorPHIDsSearchField' => 'PhabricatorSearchField',
'PhabricatorPHPASTApplication' => 'PhabricatorApplication',
'PhabricatorPHPConfigSetupCheck' => 'PhabricatorSetupCheck',
- 'PhabricatorPHPMailerConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorPHPPreflightSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorPackagesApplication' => 'PhabricatorApplication',
'PhabricatorPackagesController' => 'PhabricatorController',
'PhabricatorPackagesCreatePublisherCapability' => 'PhabricatorPolicyCapability',
'PhabricatorPackagesDAO' => 'PhabricatorLiskDAO',
'PhabricatorPackagesEditEngine' => 'PhabricatorEditEngine',
'PhabricatorPackagesEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorPackagesNgrams' => 'PhabricatorSearchNgrams',
'PhabricatorPackagesPackage' => array(
'PhabricatorPackagesDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSubscribableInterface',
'PhabricatorProjectInterface',
'PhabricatorConduitResultInterface',
'PhabricatorNgramsInterface',
),
'PhabricatorPackagesPackageController' => 'PhabricatorPackagesController',
'PhabricatorPackagesPackageDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorPackagesPackageDefaultEditCapability' => 'PhabricatorPolicyCapability',
'PhabricatorPackagesPackageDefaultViewCapability' => 'PhabricatorPolicyCapability',
'PhabricatorPackagesPackageEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PhabricatorPackagesPackageEditController' => 'PhabricatorPackagesPackageController',
'PhabricatorPackagesPackageEditEngine' => 'PhabricatorPackagesEditEngine',
'PhabricatorPackagesPackageEditor' => 'PhabricatorPackagesEditor',
'PhabricatorPackagesPackageKeyTransaction' => 'PhabricatorPackagesPackageTransactionType',
'PhabricatorPackagesPackageListController' => 'PhabricatorPackagesPackageController',
'PhabricatorPackagesPackageListView' => 'PhabricatorPackagesView',
'PhabricatorPackagesPackageNameNgrams' => 'PhabricatorPackagesNgrams',
'PhabricatorPackagesPackageNameTransaction' => 'PhabricatorPackagesPackageTransactionType',
'PhabricatorPackagesPackagePHIDType' => 'PhabricatorPHIDType',
'PhabricatorPackagesPackagePublisherTransaction' => 'PhabricatorPackagesPackageTransactionType',
'PhabricatorPackagesPackageQuery' => 'PhabricatorPackagesQuery',
'PhabricatorPackagesPackageSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhabricatorPackagesPackageSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorPackagesPackageTransaction' => 'PhabricatorModularTransaction',
'PhabricatorPackagesPackageTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorPackagesPackageTransactionType' => 'PhabricatorPackagesTransactionType',
'PhabricatorPackagesPackageViewController' => 'PhabricatorPackagesPackageController',
'PhabricatorPackagesPublisher' => array(
'PhabricatorPackagesDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSubscribableInterface',
'PhabricatorProjectInterface',
'PhabricatorConduitResultInterface',
'PhabricatorNgramsInterface',
),
'PhabricatorPackagesPublisherController' => 'PhabricatorPackagesController',
'PhabricatorPackagesPublisherDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorPackagesPublisherDefaultEditCapability' => 'PhabricatorPolicyCapability',
'PhabricatorPackagesPublisherEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PhabricatorPackagesPublisherEditController' => 'PhabricatorPackagesPublisherController',
'PhabricatorPackagesPublisherEditEngine' => 'PhabricatorPackagesEditEngine',
'PhabricatorPackagesPublisherEditor' => 'PhabricatorPackagesEditor',
'PhabricatorPackagesPublisherKeyTransaction' => 'PhabricatorPackagesPublisherTransactionType',
'PhabricatorPackagesPublisherListController' => 'PhabricatorPackagesPublisherController',
'PhabricatorPackagesPublisherListView' => 'PhabricatorPackagesView',
'PhabricatorPackagesPublisherNameNgrams' => 'PhabricatorPackagesNgrams',
'PhabricatorPackagesPublisherNameTransaction' => 'PhabricatorPackagesPublisherTransactionType',
'PhabricatorPackagesPublisherPHIDType' => 'PhabricatorPHIDType',
'PhabricatorPackagesPublisherQuery' => 'PhabricatorPackagesQuery',
'PhabricatorPackagesPublisherSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhabricatorPackagesPublisherSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorPackagesPublisherTransaction' => 'PhabricatorModularTransaction',
'PhabricatorPackagesPublisherTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorPackagesPublisherTransactionType' => 'PhabricatorPackagesTransactionType',
'PhabricatorPackagesPublisherViewController' => 'PhabricatorPackagesPublisherController',
'PhabricatorPackagesQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorPackagesSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorPackagesTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorPackagesVersion' => array(
'PhabricatorPackagesDAO',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSubscribableInterface',
'PhabricatorProjectInterface',
'PhabricatorConduitResultInterface',
'PhabricatorNgramsInterface',
),
'PhabricatorPackagesVersionController' => 'PhabricatorPackagesController',
'PhabricatorPackagesVersionEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PhabricatorPackagesVersionEditController' => 'PhabricatorPackagesVersionController',
'PhabricatorPackagesVersionEditEngine' => 'PhabricatorPackagesEditEngine',
'PhabricatorPackagesVersionEditor' => 'PhabricatorPackagesEditor',
'PhabricatorPackagesVersionListController' => 'PhabricatorPackagesVersionController',
'PhabricatorPackagesVersionListView' => 'PhabricatorPackagesView',
'PhabricatorPackagesVersionNameNgrams' => 'PhabricatorPackagesNgrams',
'PhabricatorPackagesVersionNameTransaction' => 'PhabricatorPackagesVersionTransactionType',
'PhabricatorPackagesVersionPHIDType' => 'PhabricatorPHIDType',
'PhabricatorPackagesVersionPackageTransaction' => 'PhabricatorPackagesVersionTransactionType',
'PhabricatorPackagesVersionQuery' => 'PhabricatorPackagesQuery',
'PhabricatorPackagesVersionSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhabricatorPackagesVersionSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorPackagesVersionTransaction' => 'PhabricatorModularTransaction',
'PhabricatorPackagesVersionTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorPackagesVersionTransactionType' => 'PhabricatorPackagesTransactionType',
'PhabricatorPackagesVersionViewController' => 'PhabricatorPackagesVersionController',
'PhabricatorPackagesView' => 'AphrontView',
'PhabricatorPagerUIExample' => 'PhabricatorUIExample',
'PhabricatorPassphraseApplication' => 'PhabricatorApplication',
'PhabricatorPasswordAuthProvider' => 'PhabricatorAuthProvider',
'PhabricatorPasswordDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorPasswordHasher' => 'Phobject',
'PhabricatorPasswordHasherTestCase' => 'PhabricatorTestCase',
'PhabricatorPasswordHasherUnavailableException' => 'Exception',
'PhabricatorPasswordSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorPaste' => array(
'PhabricatorPasteDAO',
'PhabricatorSubscribableInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorFlaggableInterface',
'PhabricatorMentionableInterface',
'PhabricatorPolicyInterface',
'PhabricatorProjectInterface',
'PhabricatorDestructibleInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorSpacesInterface',
'PhabricatorConduitResultInterface',
),
'PhabricatorPasteApplication' => 'PhabricatorApplication',
'PhabricatorPasteArchiveController' => 'PhabricatorPasteController',
- 'PhabricatorPasteConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorPasteContentSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorPasteContentTransaction' => 'PhabricatorPasteTransactionType',
'PhabricatorPasteController' => 'PhabricatorController',
'PhabricatorPasteDAO' => 'PhabricatorLiskDAO',
'PhabricatorPasteEditController' => 'PhabricatorPasteController',
'PhabricatorPasteEditEngine' => 'PhabricatorEditEngine',
'PhabricatorPasteEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorPasteFilenameContextFreeGrammar' => 'PhutilContextFreeGrammar',
'PhabricatorPasteLanguageTransaction' => 'PhabricatorPasteTransactionType',
'PhabricatorPasteListController' => 'PhabricatorPasteController',
'PhabricatorPastePastePHIDType' => 'PhabricatorPHIDType',
'PhabricatorPasteQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorPasteRawController' => 'PhabricatorPasteController',
'PhabricatorPasteRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhabricatorPasteSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorPasteSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorPasteSnippet' => 'Phobject',
'PhabricatorPasteStatusTransaction' => 'PhabricatorPasteTransactionType',
'PhabricatorPasteTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorPasteTitleTransaction' => 'PhabricatorPasteTransactionType',
'PhabricatorPasteTransaction' => 'PhabricatorModularTransaction',
'PhabricatorPasteTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhabricatorPasteTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorPasteTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorPasteViewController' => 'PhabricatorPasteController',
'PhabricatorPathSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorPeopleAnyOwnerDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorPeopleApplication' => 'PhabricatorApplication',
'PhabricatorPeopleApproveController' => 'PhabricatorPeopleController',
'PhabricatorPeopleAvailabilitySearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorPeopleBadgesProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorPeopleCommitsProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorPeopleController' => 'PhabricatorController',
'PhabricatorPeopleCreateController' => 'PhabricatorPeopleController',
'PhabricatorPeopleCreateGuidanceContext' => 'PhabricatorGuidanceContext',
'PhabricatorPeopleDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorPeopleDatasourceEngineExtension' => 'PhabricatorDatasourceEngineExtension',
'PhabricatorPeopleDeleteController' => 'PhabricatorPeopleController',
'PhabricatorPeopleDetailsProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorPeopleDisableController' => 'PhabricatorPeopleController',
'PhabricatorPeopleEmpowerController' => 'PhabricatorPeopleController',
'PhabricatorPeopleExternalPHIDType' => 'PhabricatorPHIDType',
'PhabricatorPeopleIconSet' => 'PhabricatorIconSet',
'PhabricatorPeopleInviteController' => 'PhabricatorPeopleController',
'PhabricatorPeopleInviteListController' => 'PhabricatorPeopleInviteController',
'PhabricatorPeopleInviteSendController' => 'PhabricatorPeopleInviteController',
'PhabricatorPeopleLdapController' => 'PhabricatorPeopleController',
'PhabricatorPeopleListController' => 'PhabricatorPeopleController',
'PhabricatorPeopleLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorPeopleLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorPeopleLogsController' => 'PhabricatorPeopleController',
+ 'PhabricatorPeopleMailEngine' => 'Phobject',
+ 'PhabricatorPeopleMailEngineException' => 'Exception',
'PhabricatorPeopleManageProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorPeopleManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorPeopleNewController' => 'PhabricatorPeopleController',
'PhabricatorPeopleNoOwnerDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorPeopleOwnerDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorPeoplePictureProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorPeopleProfileBadgesController' => 'PhabricatorPeopleProfileController',
'PhabricatorPeopleProfileCommitsController' => 'PhabricatorPeopleProfileController',
'PhabricatorPeopleProfileController' => 'PhabricatorPeopleController',
'PhabricatorPeopleProfileEditController' => 'PhabricatorPeopleProfileController',
'PhabricatorPeopleProfileImageWorkflow' => 'PhabricatorPeopleManagementWorkflow',
'PhabricatorPeopleProfileManageController' => 'PhabricatorPeopleProfileController',
'PhabricatorPeopleProfileMenuEngine' => 'PhabricatorProfileMenuEngine',
'PhabricatorPeopleProfilePictureController' => 'PhabricatorPeopleProfileController',
'PhabricatorPeopleProfileRevisionsController' => 'PhabricatorPeopleProfileController',
'PhabricatorPeopleProfileTasksController' => 'PhabricatorPeopleProfileController',
'PhabricatorPeopleProfileViewController' => 'PhabricatorPeopleProfileController',
'PhabricatorPeopleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorPeopleRenameController' => 'PhabricatorPeopleController',
'PhabricatorPeopleRevisionsProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorPeopleSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorPeopleTasksProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorPeopleTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorPeopleTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorPeopleUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorPeopleUserPHIDType' => 'PhabricatorPHIDType',
'PhabricatorPeopleWelcomeController' => 'PhabricatorPeopleController',
+ 'PhabricatorPeopleWelcomeMailEngine' => 'PhabricatorPeopleMailEngine',
'PhabricatorPhabricatorAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorPhameApplication' => 'PhabricatorApplication',
'PhabricatorPhameBlogPHIDType' => 'PhabricatorPHIDType',
'PhabricatorPhamePostPHIDType' => 'PhabricatorPHIDType',
'PhabricatorPhluxApplication' => 'PhabricatorApplication',
'PhabricatorPholioApplication' => 'PhabricatorApplication',
- 'PhabricatorPholioConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorPholioMockTestDataGenerator' => 'PhabricatorTestDataGenerator',
+ 'PhabricatorPhoneNumber' => 'Phobject',
+ 'PhabricatorPhoneNumberTestCase' => 'PhabricatorTestCase',
'PhabricatorPhortuneApplication' => 'PhabricatorApplication',
'PhabricatorPhortuneContentSource' => 'PhabricatorContentSource',
'PhabricatorPhortuneManagementInvoiceWorkflow' => 'PhabricatorPhortuneManagementWorkflow',
'PhabricatorPhortuneManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorPhortuneTestCase' => 'PhabricatorTestCase',
'PhabricatorPhragmentApplication' => 'PhabricatorApplication',
'PhabricatorPhrequentApplication' => 'PhabricatorApplication',
'PhabricatorPhrictionApplication' => 'PhabricatorApplication',
- 'PhabricatorPhrictionConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorPhurlApplication' => 'PhabricatorApplication',
'PhabricatorPhurlConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorPhurlController' => 'PhabricatorController',
'PhabricatorPhurlDAO' => 'PhabricatorLiskDAO',
'PhabricatorPhurlLinkRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorPhurlRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhabricatorPhurlSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorPhurlShortURLController' => 'PhabricatorPhurlController',
'PhabricatorPhurlShortURLDefaultController' => 'PhabricatorPhurlController',
'PhabricatorPhurlURL' => array(
'PhabricatorPhurlDAO',
'PhabricatorPolicyInterface',
'PhabricatorProjectInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorSubscribableInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorDestructibleInterface',
'PhabricatorMentionableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorSpacesInterface',
'PhabricatorConduitResultInterface',
'PhabricatorNgramsInterface',
),
'PhabricatorPhurlURLAccessController' => 'PhabricatorPhurlController',
'PhabricatorPhurlURLAliasTransaction' => 'PhabricatorPhurlURLTransactionType',
'PhabricatorPhurlURLCreateCapability' => 'PhabricatorPolicyCapability',
'PhabricatorPhurlURLDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorPhurlURLDescriptionTransaction' => 'PhabricatorPhurlURLTransactionType',
'PhabricatorPhurlURLEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PhabricatorPhurlURLEditController' => 'PhabricatorPhurlController',
'PhabricatorPhurlURLEditEngine' => 'PhabricatorEditEngine',
'PhabricatorPhurlURLEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorPhurlURLListController' => 'PhabricatorPhurlController',
'PhabricatorPhurlURLLongURLTransaction' => 'PhabricatorPhurlURLTransactionType',
'PhabricatorPhurlURLMailReceiver' => 'PhabricatorObjectMailReceiver',
'PhabricatorPhurlURLNameNgrams' => 'PhabricatorSearchNgrams',
'PhabricatorPhurlURLNameTransaction' => 'PhabricatorPhurlURLTransactionType',
'PhabricatorPhurlURLPHIDType' => 'PhabricatorPHIDType',
'PhabricatorPhurlURLQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorPhurlURLReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhabricatorPhurlURLSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhabricatorPhurlURLSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorPhurlURLTransaction' => 'PhabricatorModularTransaction',
'PhabricatorPhurlURLTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhabricatorPhurlURLTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorPhurlURLTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorPhurlURLViewController' => 'PhabricatorPhurlController',
'PhabricatorPinnedApplicationsSetting' => 'PhabricatorInternalSetting',
'PhabricatorPirateEnglishTranslation' => 'PhutilTranslation',
'PhabricatorPlatformSite' => 'PhabricatorSite',
'PhabricatorPointsEditField' => 'PhabricatorEditField',
'PhabricatorPointsFact' => 'PhabricatorFact',
'PhabricatorPolicies' => 'PhabricatorPolicyConstants',
'PhabricatorPolicy' => array(
'PhabricatorPolicyDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorPolicyApplication' => 'PhabricatorApplication',
'PhabricatorPolicyAwareQuery' => 'PhabricatorOffsetPagedQuery',
'PhabricatorPolicyAwareTestQuery' => 'PhabricatorPolicyAwareQuery',
'PhabricatorPolicyCanEditCapability' => 'PhabricatorPolicyCapability',
'PhabricatorPolicyCanInteractCapability' => 'PhabricatorPolicyCapability',
'PhabricatorPolicyCanJoinCapability' => 'PhabricatorPolicyCapability',
'PhabricatorPolicyCanViewCapability' => 'PhabricatorPolicyCapability',
'PhabricatorPolicyCapability' => 'Phobject',
'PhabricatorPolicyCapabilityTestCase' => 'PhabricatorTestCase',
'PhabricatorPolicyCodex' => 'Phobject',
'PhabricatorPolicyCodexRuleDescription' => 'Phobject',
'PhabricatorPolicyConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorPolicyConstants' => 'Phobject',
'PhabricatorPolicyController' => 'PhabricatorController',
'PhabricatorPolicyDAO' => 'PhabricatorLiskDAO',
'PhabricatorPolicyDataTestCase' => 'PhabricatorTestCase',
'PhabricatorPolicyEditController' => 'PhabricatorPolicyController',
'PhabricatorPolicyEditEngineExtension' => 'PhabricatorEditEngineExtension',
'PhabricatorPolicyEditField' => 'PhabricatorEditField',
'PhabricatorPolicyException' => 'Exception',
'PhabricatorPolicyExplainController' => 'PhabricatorPolicyController',
'PhabricatorPolicyFavoritesSetting' => 'PhabricatorInternalSetting',
'PhabricatorPolicyFilter' => 'Phobject',
'PhabricatorPolicyInterface' => 'PhabricatorPHIDInterface',
'PhabricatorPolicyManagementShowWorkflow' => 'PhabricatorPolicyManagementWorkflow',
'PhabricatorPolicyManagementUnlockWorkflow' => 'PhabricatorPolicyManagementWorkflow',
'PhabricatorPolicyManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorPolicyPHIDTypePolicy' => 'PhabricatorPHIDType',
'PhabricatorPolicyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorPolicyRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorPolicyRule' => 'Phobject',
'PhabricatorPolicySearchEngineExtension' => 'PhabricatorSearchEngineExtension',
'PhabricatorPolicyStrengthConstants' => 'PhabricatorPolicyConstants',
'PhabricatorPolicyTestCase' => 'PhabricatorTestCase',
'PhabricatorPolicyTestObject' => array(
'Phobject',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
),
'PhabricatorPolicyType' => 'PhabricatorPolicyConstants',
'PhabricatorPonderApplication' => 'PhabricatorApplication',
'PhabricatorProfileMenuEditEngine' => 'PhabricatorEditEngine',
'PhabricatorProfileMenuEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorProfileMenuEngine' => 'Phobject',
'PhabricatorProfileMenuItem' => 'Phobject',
'PhabricatorProfileMenuItemConfiguration' => array(
'PhabricatorSearchDAO',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
'PhabricatorApplicationTransactionInterface',
),
'PhabricatorProfileMenuItemConfigurationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorProfileMenuItemConfigurationTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorProfileMenuItemConfigurationTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorProfileMenuItemIconSet' => 'PhabricatorIconSet',
'PhabricatorProfileMenuItemPHIDType' => 'PhabricatorPHIDType',
'PhabricatorProject' => array(
'PhabricatorProjectDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorFlaggableInterface',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorDestructibleInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
'PhabricatorConduitResultInterface',
'PhabricatorColumnProxyInterface',
'PhabricatorSpacesInterface',
+ 'PhabricatorEditEngineSubtypeInterface',
),
'PhabricatorProjectAddHeraldAction' => 'PhabricatorProjectHeraldAction',
'PhabricatorProjectApplication' => 'PhabricatorApplication',
'PhabricatorProjectArchiveController' => 'PhabricatorProjectController',
'PhabricatorProjectBoardBackgroundController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectBoardController' => 'PhabricatorProjectController',
'PhabricatorProjectBoardDisableController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectBoardImportController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectBoardManageController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectBoardReorderController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectBoardViewController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectBuiltinsExample' => 'PhabricatorUIExample',
'PhabricatorProjectCardView' => 'AphrontTagView',
'PhabricatorProjectColorTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectColorsConfigType' => 'PhabricatorJSONConfigType',
'PhabricatorProjectColumn' => array(
'PhabricatorProjectDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorExtendedPolicyInterface',
'PhabricatorConduitResultInterface',
),
'PhabricatorProjectColumnDetailController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectColumnEditController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectColumnHideController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectColumnPHIDType' => 'PhabricatorPHIDType',
'PhabricatorProjectColumnPosition' => array(
'PhabricatorProjectDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorProjectColumnPositionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorProjectColumnQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorProjectColumnSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorProjectColumnTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorProjectColumnTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorProjectColumnTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorProjectConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorProjectConfiguredCustomField' => array(
'PhabricatorProjectStandardCustomField',
'PhabricatorStandardCustomFieldInterface',
),
'PhabricatorProjectController' => 'PhabricatorController',
'PhabricatorProjectCoreTestCase' => 'PhabricatorTestCase',
'PhabricatorProjectCoverController' => 'PhabricatorProjectController',
'PhabricatorProjectCustomField' => 'PhabricatorCustomField',
'PhabricatorProjectCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage',
'PhabricatorProjectCustomFieldStorage' => 'PhabricatorCustomFieldStorage',
'PhabricatorProjectCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage',
'PhabricatorProjectDAO' => 'PhabricatorLiskDAO',
'PhabricatorProjectDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorProjectDefaultController' => 'PhabricatorProjectBoardController',
'PhabricatorProjectDescriptionField' => 'PhabricatorProjectStandardCustomField',
'PhabricatorProjectDetailsProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorProjectEditController' => 'PhabricatorProjectController',
'PhabricatorProjectEditEngine' => 'PhabricatorEditEngine',
'PhabricatorProjectEditPictureController' => 'PhabricatorProjectController',
'PhabricatorProjectFerretEngine' => 'PhabricatorFerretEngine',
'PhabricatorProjectFilterTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectFulltextEngine' => 'PhabricatorFulltextEngine',
'PhabricatorProjectHeraldAction' => 'HeraldAction',
'PhabricatorProjectHeraldAdapter' => 'HeraldAdapter',
'PhabricatorProjectHeraldFieldGroup' => 'HeraldFieldGroup',
'PhabricatorProjectHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension',
'PhabricatorProjectIconSet' => 'PhabricatorIconSet',
'PhabricatorProjectIconTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectIconsConfigType' => 'PhabricatorJSONConfigType',
'PhabricatorProjectImageTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectListController' => 'PhabricatorProjectController',
'PhabricatorProjectListView' => 'AphrontView',
'PhabricatorProjectLockController' => 'PhabricatorProjectController',
'PhabricatorProjectLockTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectLogicalAncestorDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorProjectLogicalDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorProjectLogicalOnlyDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorProjectLogicalOrNotDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorProjectLogicalUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorProjectLogicalViewerDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorProjectManageController' => 'PhabricatorProjectController',
'PhabricatorProjectManageProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorProjectMaterializedMemberEdgeType' => 'PhabricatorEdgeType',
'PhabricatorProjectMemberListView' => 'PhabricatorProjectUserListView',
'PhabricatorProjectMemberOfProjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorProjectMembersAddController' => 'PhabricatorProjectController',
'PhabricatorProjectMembersDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorProjectMembersPolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorProjectMembersProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorProjectMembersRemoveController' => 'PhabricatorProjectController',
'PhabricatorProjectMembersViewController' => 'PhabricatorProjectController',
'PhabricatorProjectMenuItemController' => 'PhabricatorProjectController',
'PhabricatorProjectMilestoneTransaction' => 'PhabricatorProjectTypeTransaction',
'PhabricatorProjectMoveController' => 'PhabricatorProjectController',
'PhabricatorProjectNameContextFreeGrammar' => 'PhutilContextFreeGrammar',
'PhabricatorProjectNameTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectNoProjectsDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorProjectObjectHasProjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorProjectOrUserDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorProjectOrUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorProjectPHIDResolver' => 'PhabricatorPHIDResolver',
'PhabricatorProjectParentTransaction' => 'PhabricatorProjectTypeTransaction',
'PhabricatorProjectPictureProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorProjectPointsProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorProjectProfileController' => 'PhabricatorProjectController',
'PhabricatorProjectProfileMenuEngine' => 'PhabricatorProfileMenuEngine',
'PhabricatorProjectProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorProjectProjectHasMemberEdgeType' => 'PhabricatorEdgeType',
'PhabricatorProjectProjectHasObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorProjectProjectPHIDType' => 'PhabricatorPHIDType',
'PhabricatorProjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorProjectRemoveHeraldAction' => 'PhabricatorProjectHeraldAction',
'PhabricatorProjectSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorProjectSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorProjectSearchField' => 'PhabricatorSearchTokenizerField',
'PhabricatorProjectSilenceController' => 'PhabricatorProjectController',
'PhabricatorProjectSilencedEdgeType' => 'PhabricatorEdgeType',
'PhabricatorProjectSlug' => 'PhabricatorProjectDAO',
'PhabricatorProjectSlugsTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectSortTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectStandardCustomField' => array(
'PhabricatorProjectCustomField',
'PhabricatorStandardCustomFieldInterface',
),
'PhabricatorProjectStatus' => 'Phobject',
'PhabricatorProjectStatusTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectSubprojectWarningController' => 'PhabricatorProjectController',
'PhabricatorProjectSubprojectsController' => 'PhabricatorProjectController',
'PhabricatorProjectSubprojectsProfileMenuItem' => 'PhabricatorProfileMenuItem',
+ 'PhabricatorProjectSubtypeDatasource' => 'PhabricatorTypeaheadDatasource',
+ 'PhabricatorProjectSubtypesConfigType' => 'PhabricatorJSONConfigType',
'PhabricatorProjectTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorProjectTransaction' => 'PhabricatorModularTransaction',
'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorProjectTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorProjectTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorProjectTypeTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectUIEventListener' => 'PhabricatorEventListener',
'PhabricatorProjectUpdateController' => 'PhabricatorProjectController',
'PhabricatorProjectUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorProjectUserListView' => 'AphrontView',
'PhabricatorProjectViewController' => 'PhabricatorProjectController',
'PhabricatorProjectWatchController' => 'PhabricatorProjectController',
'PhabricatorProjectWatcherListView' => 'PhabricatorProjectUserListView',
'PhabricatorProjectWorkboardBackgroundColor' => 'Phobject',
'PhabricatorProjectWorkboardBackgroundTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectWorkboardProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorProjectWorkboardTransaction' => 'PhabricatorProjectTransactionType',
'PhabricatorProjectsAllPolicyRule' => 'PhabricatorProjectsBasePolicyRule',
'PhabricatorProjectsAncestorsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorProjectsBasePolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorProjectsCurtainExtension' => 'PHUICurtainExtension',
'PhabricatorProjectsEditEngineExtension' => 'PhabricatorEditEngineExtension',
'PhabricatorProjectsEditField' => 'PhabricatorTokenizerEditField',
'PhabricatorProjectsExportEngineExtension' => 'PhabricatorExportEngineExtension',
'PhabricatorProjectsFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension',
'PhabricatorProjectsMailEngineExtension' => 'PhabricatorMailEngineExtension',
'PhabricatorProjectsMembersSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorProjectsMembershipIndexEngineExtension' => 'PhabricatorIndexEngineExtension',
'PhabricatorProjectsPolicyRule' => 'PhabricatorProjectsBasePolicyRule',
'PhabricatorProjectsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorProjectsSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
'PhabricatorProjectsWatchersSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorPronounSetting' => 'PhabricatorSelectSetting',
'PhabricatorPygmentSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorQuery' => 'Phobject',
'PhabricatorQueryConstraint' => 'Phobject',
'PhabricatorQueryIterator' => 'PhutilBufferedIterator',
'PhabricatorQueryOrderItem' => 'Phobject',
'PhabricatorQueryOrderTestCase' => 'PhabricatorTestCase',
'PhabricatorQueryOrderVector' => array(
'Phobject',
'Iterator',
),
'PhabricatorQuickSearchEngineExtension' => 'PhabricatorDatasourceEngineExtension',
'PhabricatorRateLimitRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorRecaptchaConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorRedirectController' => 'PhabricatorController',
'PhabricatorRefreshCSRFController' => 'PhabricatorAuthController',
'PhabricatorRegexListConfigType' => 'PhabricatorTextListConfigType',
'PhabricatorRegistrationProfile' => 'Phobject',
'PhabricatorReleephApplication' => 'PhabricatorApplication',
'PhabricatorReleephApplicationConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorRemarkupCachePurger' => 'PhabricatorCachePurger',
'PhabricatorRemarkupControl' => 'AphrontFormTextAreaControl',
'PhabricatorRemarkupCowsayBlockInterpreter' => 'PhutilRemarkupBlockInterpreter',
'PhabricatorRemarkupCustomBlockRule' => 'PhutilRemarkupBlockRule',
'PhabricatorRemarkupCustomInlineRule' => 'PhutilRemarkupRule',
'PhabricatorRemarkupDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorRemarkupEditField' => 'PhabricatorEditField',
'PhabricatorRemarkupFigletBlockInterpreter' => 'PhutilRemarkupBlockInterpreter',
'PhabricatorRemarkupUIExample' => 'PhabricatorUIExample',
'PhabricatorRepositoriesSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorRepository' => array(
'PhabricatorRepositoryDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorFlaggableInterface',
'PhabricatorMarkupInterface',
'PhabricatorDestructibleInterface',
'PhabricatorDestructibleCodexInterface',
'PhabricatorProjectInterface',
'PhabricatorSpacesInterface',
'PhabricatorConduitResultInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
),
'PhabricatorRepositoryActivateTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryAuditRequest' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorRepositoryAutocloseOnlyTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryAutocloseTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryBlueprintsTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryBranch' => 'PhabricatorRepositoryDAO',
'PhabricatorRepositoryCallsignTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryCommit' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
'PhabricatorFlaggableInterface',
'PhabricatorProjectInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorSubscribableInterface',
'PhabricatorMentionableInterface',
'HarbormasterBuildableInterface',
'HarbormasterCircleCIBuildableInterface',
'HarbormasterBuildkiteBuildableInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorApplicationTransactionInterface',
+ 'PhabricatorTimelineInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
'PhabricatorConduitResultInterface',
'PhabricatorDraftInterface',
),
'PhabricatorRepositoryCommitChangeParserWorker' => 'PhabricatorRepositoryCommitParserWorker',
'PhabricatorRepositoryCommitData' => 'PhabricatorRepositoryDAO',
'PhabricatorRepositoryCommitHeraldWorker' => 'PhabricatorRepositoryCommitParserWorker',
'PhabricatorRepositoryCommitHint' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorRepositoryCommitMessageParserWorker' => 'PhabricatorRepositoryCommitParserWorker',
'PhabricatorRepositoryCommitOwnersWorker' => 'PhabricatorRepositoryCommitParserWorker',
'PhabricatorRepositoryCommitPHIDType' => 'PhabricatorPHIDType',
'PhabricatorRepositoryCommitParserWorker' => 'PhabricatorWorker',
'PhabricatorRepositoryCommitRef' => 'Phobject',
'PhabricatorRepositoryCommitTestCase' => 'PhabricatorTestCase',
'PhabricatorRepositoryConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorRepositoryCopyTimeLimitTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryDAO' => 'PhabricatorLiskDAO',
'PhabricatorRepositoryDangerousTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryDefaultBranchTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryDescriptionTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryDestructibleCodex' => 'PhabricatorDestructibleCodex',
'PhabricatorRepositoryDiscoveryEngine' => 'PhabricatorRepositoryEngine',
'PhabricatorRepositoryEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorRepositoryEncodingTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryEngine' => 'Phobject',
'PhabricatorRepositoryEnormousTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryFerretEngine' => 'PhabricatorFerretEngine',
'PhabricatorRepositoryFilesizeLimitTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryFulltextEngine' => 'PhabricatorFulltextEngine',
'PhabricatorRepositoryGitCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker',
'PhabricatorRepositoryGitCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker',
'PhabricatorRepositoryGitLFSRef' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorRepositoryGitLFSRefQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorRepositoryGraphCache' => 'Phobject',
'PhabricatorRepositoryGraphStream' => 'Phobject',
'PhabricatorRepositoryIdentity' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
),
'PhabricatorRepositoryIdentityAssignTransaction' => 'PhabricatorRepositoryIdentityTransactionType',
'PhabricatorRepositoryIdentityChangeWorker' => 'PhabricatorWorker',
'PhabricatorRepositoryIdentityEditEngine' => 'PhabricatorEditEngine',
'PhabricatorRepositoryIdentityFerretEngine' => 'PhabricatorFerretEngine',
'PhabricatorRepositoryIdentityPHIDType' => 'PhabricatorPHIDType',
'PhabricatorRepositoryIdentityQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorRepositoryIdentityTransaction' => 'PhabricatorModularTransaction',
'PhabricatorRepositoryIdentityTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorRepositoryIdentityTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorRepositoryManagementCacheWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementClusterizeWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementDiscoverWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementHintWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementImportingWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementListPathsWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementListWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementLookupUsersWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementMarkImportedWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementMarkReachableWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementMirrorWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementMovePathsWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementParentsWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementPullWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementRebuildIdentitiesWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementRefsWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementReparseWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementThawWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementUnpublishWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementUpdateWorkflow' => 'PhabricatorRepositoryManagementWorkflow',
'PhabricatorRepositoryManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorRepositoryMercurialCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker',
'PhabricatorRepositoryMercurialCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker',
'PhabricatorRepositoryMirror' => 'PhabricatorRepositoryDAO',
'PhabricatorRepositoryMirrorEngine' => 'PhabricatorRepositoryEngine',
'PhabricatorRepositoryNameTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryNotifyTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryOldRef' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorRepositoryParsedChange' => 'Phobject',
'PhabricatorRepositoryPullEngine' => 'PhabricatorRepositoryEngine',
'PhabricatorRepositoryPullEvent' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorRepositoryPullEventPHIDType' => 'PhabricatorPHIDType',
'PhabricatorRepositoryPullEventQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorRepositoryPullLocalDaemon' => 'PhabricatorDaemon',
'PhabricatorRepositoryPullLocalDaemonModule' => 'PhutilDaemonOverseerModule',
'PhabricatorRepositoryPushEvent' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorRepositoryPushEventPHIDType' => 'PhabricatorPHIDType',
'PhabricatorRepositoryPushEventQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorRepositoryPushLog' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorRepositoryPushLogPHIDType' => 'PhabricatorPHIDType',
'PhabricatorRepositoryPushLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorRepositoryPushLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorRepositoryPushMailWorker' => 'PhabricatorWorker',
'PhabricatorRepositoryPushPolicyTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryPushReplyHandler' => 'PhabricatorMailReplyHandler',
'PhabricatorRepositoryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorRepositoryRefCursor' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorRepositoryRefCursorPHIDType' => 'PhabricatorPHIDType',
'PhabricatorRepositoryRefCursorQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorRepositoryRefEngine' => 'PhabricatorRepositoryEngine',
'PhabricatorRepositoryRefPosition' => 'PhabricatorRepositoryDAO',
'PhabricatorRepositoryRepositoryPHIDType' => 'PhabricatorPHIDType',
'PhabricatorRepositorySVNSubpathTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositorySchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorRepositorySearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorRepositoryServiceTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositorySlugTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryStagingURITransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryStatusMessage' => 'PhabricatorRepositoryDAO',
'PhabricatorRepositorySvnCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker',
'PhabricatorRepositorySvnCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker',
'PhabricatorRepositorySymbol' => 'PhabricatorRepositoryDAO',
'PhabricatorRepositorySymbolLanguagesTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositorySymbolSourcesTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositorySyncEvent' => array(
'PhabricatorRepositoryDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorRepositorySyncEventPHIDType' => 'PhabricatorPHIDType',
'PhabricatorRepositorySyncEventQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorRepositoryTestCase' => 'PhabricatorTestCase',
'PhabricatorRepositoryTouchLimitTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryTrackOnlyTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryTransaction' => 'PhabricatorModularTransaction',
'PhabricatorRepositoryTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorRepositoryTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorRepositoryType' => 'Phobject',
'PhabricatorRepositoryURI' => array(
'PhabricatorRepositoryDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
'PhabricatorConduitResultInterface',
),
'PhabricatorRepositoryURIIndex' => 'PhabricatorRepositoryDAO',
'PhabricatorRepositoryURINormalizer' => 'Phobject',
'PhabricatorRepositoryURINormalizerTestCase' => 'PhabricatorTestCase',
'PhabricatorRepositoryURIPHIDType' => 'PhabricatorPHIDType',
'PhabricatorRepositoryURIQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorRepositoryURITestCase' => 'PhabricatorTestCase',
'PhabricatorRepositoryURITransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorRepositoryURITransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorRepositoryVCSTransaction' => 'PhabricatorRepositoryTransactionType',
'PhabricatorRepositoryWorkingCopyVersion' => 'PhabricatorRepositoryDAO',
'PhabricatorRequestExceptionHandler' => 'AphrontRequestExceptionHandler',
'PhabricatorResourceSite' => 'PhabricatorSite',
'PhabricatorRobotsController' => 'PhabricatorController',
'PhabricatorS3FileStorageEngine' => 'PhabricatorFileStorageEngine',
- 'PhabricatorSMS' => 'PhabricatorSMSDAO',
- 'PhabricatorSMSConfigOptions' => 'PhabricatorApplicationConfigOptions',
- 'PhabricatorSMSDAO' => 'PhabricatorLiskDAO',
- 'PhabricatorSMSDemultiplexWorker' => 'PhabricatorSMSWorker',
- 'PhabricatorSMSImplementationAdapter' => 'Phobject',
- 'PhabricatorSMSImplementationTestBlackholeAdapter' => 'PhabricatorSMSImplementationAdapter',
- 'PhabricatorSMSImplementationTwilioAdapter' => 'PhabricatorSMSImplementationAdapter',
- 'PhabricatorSMSManagementListOutboundWorkflow' => 'PhabricatorSMSManagementWorkflow',
- 'PhabricatorSMSManagementSendTestWorkflow' => 'PhabricatorSMSManagementWorkflow',
- 'PhabricatorSMSManagementShowOutboundWorkflow' => 'PhabricatorSMSManagementWorkflow',
- 'PhabricatorSMSManagementWorkflow' => 'PhabricatorManagementWorkflow',
- 'PhabricatorSMSSendWorker' => 'PhabricatorSMSWorker',
- 'PhabricatorSMSWorker' => 'PhabricatorWorker',
+ 'PhabricatorSMSAuthFactor' => 'PhabricatorAuthFactor',
'PhabricatorSQLPatchList' => 'Phobject',
'PhabricatorSSHKeyGenerator' => 'Phobject',
'PhabricatorSSHKeysSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorSSHLog' => 'Phobject',
'PhabricatorSSHPassthruCommand' => 'Phobject',
'PhabricatorSSHWorkflow' => 'PhutilArgumentWorkflow',
'PhabricatorSavedQuery' => array(
'PhabricatorSearchDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorSavedQueryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorScheduleTaskTriggerAction' => 'PhabricatorTriggerAction',
'PhabricatorScopedEnv' => 'Phobject',
'PhabricatorSearchAbstractDocument' => 'Phobject',
'PhabricatorSearchApplication' => 'PhabricatorApplication',
'PhabricatorSearchApplicationSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorSearchApplicationStorageEnginePanel' => 'PhabricatorApplicationConfigurationPanel',
'PhabricatorSearchBaseController' => 'PhabricatorController',
'PhabricatorSearchCheckboxesField' => 'PhabricatorSearchField',
'PhabricatorSearchConstraintException' => 'Exception',
'PhabricatorSearchController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchCustomFieldProxyField' => 'PhabricatorSearchField',
'PhabricatorSearchDAO' => 'PhabricatorLiskDAO',
'PhabricatorSearchDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorSearchDatasourceField' => 'PhabricatorSearchTokenizerField',
'PhabricatorSearchDateControlField' => 'PhabricatorSearchField',
'PhabricatorSearchDateField' => 'PhabricatorSearchField',
'PhabricatorSearchDefaultController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchDeleteController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchDocument' => 'PhabricatorSearchDAO',
'PhabricatorSearchDocumentField' => 'PhabricatorSearchDAO',
'PhabricatorSearchDocumentFieldType' => 'Phobject',
'PhabricatorSearchDocumentQuery' => 'PhabricatorPolicyAwareQuery',
'PhabricatorSearchDocumentRelationship' => 'PhabricatorSearchDAO',
'PhabricatorSearchDocumentTypeDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorSearchEditController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchEngineAPIMethod' => 'ConduitAPIMethod',
'PhabricatorSearchEngineAttachment' => 'Phobject',
'PhabricatorSearchEngineExtension' => 'Phobject',
'PhabricatorSearchEngineExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorSearchFerretNgramGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorSearchField' => 'Phobject',
'PhabricatorSearchHost' => 'Phobject',
'PhabricatorSearchHovercardController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchIndexVersion' => 'PhabricatorSearchDAO',
'PhabricatorSearchIndexVersionDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorSearchManagementIndexWorkflow' => 'PhabricatorSearchManagementWorkflow',
'PhabricatorSearchManagementInitWorkflow' => 'PhabricatorSearchManagementWorkflow',
'PhabricatorSearchManagementNgramsWorkflow' => 'PhabricatorSearchManagementWorkflow',
'PhabricatorSearchManagementQueryWorkflow' => 'PhabricatorSearchManagementWorkflow',
'PhabricatorSearchManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorSearchNgrams' => 'PhabricatorSearchDAO',
'PhabricatorSearchNgramsDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorSearchOrderController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchOrderField' => 'PhabricatorSearchField',
'PhabricatorSearchRelationship' => 'Phobject',
'PhabricatorSearchRelationshipController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchRelationshipSourceController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchResultBucket' => 'Phobject',
'PhabricatorSearchResultBucketGroup' => 'Phobject',
'PhabricatorSearchResultView' => 'AphrontView',
'PhabricatorSearchSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorSearchScopeSetting' => 'PhabricatorInternalSetting',
'PhabricatorSearchSelectField' => 'PhabricatorSearchField',
'PhabricatorSearchService' => 'Phobject',
'PhabricatorSearchStringListField' => 'PhabricatorSearchField',
'PhabricatorSearchSubscribersField' => 'PhabricatorSearchTokenizerField',
'PhabricatorSearchTextField' => 'PhabricatorSearchField',
'PhabricatorSearchThreeStateField' => 'PhabricatorSearchField',
'PhabricatorSearchTokenizerField' => 'PhabricatorSearchField',
'PhabricatorSearchWorker' => 'PhabricatorWorker',
'PhabricatorSecurityConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorSecuritySetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorSelectEditField' => 'PhabricatorEditField',
'PhabricatorSelectSetting' => 'PhabricatorSetting',
- 'PhabricatorSendGridConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorSessionsSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorSetConfigType' => 'PhabricatorTextConfigType',
'PhabricatorSetting' => 'Phobject',
'PhabricatorSettingsAccountPanelGroup' => 'PhabricatorSettingsPanelGroup',
'PhabricatorSettingsAddEmailAction' => 'PhabricatorSystemAction',
'PhabricatorSettingsAdjustController' => 'PhabricatorController',
'PhabricatorSettingsApplication' => 'PhabricatorApplication',
'PhabricatorSettingsApplicationsPanelGroup' => 'PhabricatorSettingsPanelGroup',
'PhabricatorSettingsAuthenticationPanelGroup' => 'PhabricatorSettingsPanelGroup',
'PhabricatorSettingsDeveloperPanelGroup' => 'PhabricatorSettingsPanelGroup',
'PhabricatorSettingsEditEngine' => 'PhabricatorEditEngine',
'PhabricatorSettingsEmailPanelGroup' => 'PhabricatorSettingsPanelGroup',
'PhabricatorSettingsIssueController' => 'PhabricatorController',
'PhabricatorSettingsListController' => 'PhabricatorController',
'PhabricatorSettingsLogsPanelGroup' => 'PhabricatorSettingsPanelGroup',
'PhabricatorSettingsMainController' => 'PhabricatorController',
'PhabricatorSettingsPanel' => 'Phobject',
'PhabricatorSettingsPanelGroup' => 'Phobject',
'PhabricatorSettingsTimezoneController' => 'PhabricatorController',
'PhabricatorSetupCheck' => 'Phobject',
'PhabricatorSetupCheckTestCase' => 'PhabricatorTestCase',
'PhabricatorSetupEngine' => 'Phobject',
'PhabricatorSetupIssue' => 'Phobject',
'PhabricatorSetupIssueUIExample' => 'PhabricatorUIExample',
'PhabricatorSetupIssueView' => 'AphrontView',
'PhabricatorShortSite' => 'PhabricatorSite',
'PhabricatorShowFiletreeSetting' => 'PhabricatorSelectSetting',
'PhabricatorSimpleEditType' => 'PhabricatorEditType',
'PhabricatorSite' => 'AphrontSite',
'PhabricatorSlackAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorSlowvoteApplication' => 'PhabricatorApplication',
'PhabricatorSlowvoteChoice' => 'PhabricatorSlowvoteDAO',
'PhabricatorSlowvoteCloseController' => 'PhabricatorSlowvoteController',
'PhabricatorSlowvoteCloseTransaction' => 'PhabricatorSlowvoteTransactionType',
'PhabricatorSlowvoteCommentController' => 'PhabricatorSlowvoteController',
'PhabricatorSlowvoteController' => 'PhabricatorController',
'PhabricatorSlowvoteDAO' => 'PhabricatorLiskDAO',
'PhabricatorSlowvoteDefaultViewCapability' => 'PhabricatorPolicyCapability',
'PhabricatorSlowvoteDescriptionTransaction' => 'PhabricatorSlowvoteTransactionType',
'PhabricatorSlowvoteEditController' => 'PhabricatorSlowvoteController',
'PhabricatorSlowvoteEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorSlowvoteListController' => 'PhabricatorSlowvoteController',
'PhabricatorSlowvoteMailReceiver' => 'PhabricatorObjectMailReceiver',
'PhabricatorSlowvoteOption' => 'PhabricatorSlowvoteDAO',
'PhabricatorSlowvotePoll' => array(
'PhabricatorSlowvoteDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorSubscribableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorProjectInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSpacesInterface',
),
'PhabricatorSlowvotePollController' => 'PhabricatorSlowvoteController',
'PhabricatorSlowvotePollPHIDType' => 'PhabricatorPHIDType',
'PhabricatorSlowvoteQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorSlowvoteQuestionTransaction' => 'PhabricatorSlowvoteTransactionType',
'PhabricatorSlowvoteReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhabricatorSlowvoteResponsesTransaction' => 'PhabricatorSlowvoteTransactionType',
'PhabricatorSlowvoteSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorSlowvoteSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorSlowvoteShuffleTransaction' => 'PhabricatorSlowvoteTransactionType',
'PhabricatorSlowvoteTransaction' => 'PhabricatorModularTransaction',
'PhabricatorSlowvoteTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhabricatorSlowvoteTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorSlowvoteTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorSlowvoteVoteController' => 'PhabricatorSlowvoteController',
'PhabricatorSlug' => 'Phobject',
'PhabricatorSlugTestCase' => 'PhabricatorTestCase',
'PhabricatorSourceCodeView' => 'AphrontView',
'PhabricatorSourceDocumentEngine' => 'PhabricatorTextDocumentEngine',
'PhabricatorSpaceEditField' => 'PhabricatorEditField',
'PhabricatorSpacesApplication' => 'PhabricatorApplication',
'PhabricatorSpacesArchiveController' => 'PhabricatorSpacesController',
'PhabricatorSpacesCapabilityCreateSpaces' => 'PhabricatorPolicyCapability',
'PhabricatorSpacesCapabilityDefaultEdit' => 'PhabricatorPolicyCapability',
'PhabricatorSpacesCapabilityDefaultView' => 'PhabricatorPolicyCapability',
'PhabricatorSpacesController' => 'PhabricatorController',
'PhabricatorSpacesDAO' => 'PhabricatorLiskDAO',
'PhabricatorSpacesEditController' => 'PhabricatorSpacesController',
'PhabricatorSpacesExportEngineExtension' => 'PhabricatorExportEngineExtension',
'PhabricatorSpacesInterface' => 'PhabricatorPHIDInterface',
'PhabricatorSpacesListController' => 'PhabricatorSpacesController',
'PhabricatorSpacesMailEngineExtension' => 'PhabricatorMailEngineExtension',
'PhabricatorSpacesNamespace' => array(
'PhabricatorSpacesDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorSpacesNamespaceArchiveTransaction' => 'PhabricatorSpacesNamespaceTransactionType',
'PhabricatorSpacesNamespaceDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorSpacesNamespaceDefaultTransaction' => 'PhabricatorSpacesNamespaceTransactionType',
'PhabricatorSpacesNamespaceDescriptionTransaction' => 'PhabricatorSpacesNamespaceTransactionType',
'PhabricatorSpacesNamespaceEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorSpacesNamespaceNameTransaction' => 'PhabricatorSpacesNamespaceTransactionType',
'PhabricatorSpacesNamespacePHIDType' => 'PhabricatorPHIDType',
'PhabricatorSpacesNamespaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorSpacesNamespaceSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorSpacesNamespaceTransaction' => 'PhabricatorModularTransaction',
'PhabricatorSpacesNamespaceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorSpacesNamespaceTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorSpacesNoAccessController' => 'PhabricatorSpacesController',
'PhabricatorSpacesRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhabricatorSpacesSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorSpacesSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
'PhabricatorSpacesSearchField' => 'PhabricatorSearchTokenizerField',
'PhabricatorSpacesTestCase' => 'PhabricatorTestCase',
'PhabricatorSpacesViewController' => 'PhabricatorSpacesController',
'PhabricatorStandardCustomField' => 'PhabricatorCustomField',
'PhabricatorStandardCustomFieldBlueprints' => 'PhabricatorStandardCustomFieldTokenizer',
'PhabricatorStandardCustomFieldBool' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldCredential' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldDatasource' => 'PhabricatorStandardCustomFieldTokenizer',
'PhabricatorStandardCustomFieldDate' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldHeader' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldInt' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldLink' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldPHIDs' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldRemarkup' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldSelect' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldText' => 'PhabricatorStandardCustomField',
'PhabricatorStandardCustomFieldTokenizer' => 'PhabricatorStandardCustomFieldPHIDs',
'PhabricatorStandardCustomFieldUsers' => 'PhabricatorStandardCustomFieldTokenizer',
'PhabricatorStandardPageView' => array(
'PhabricatorBarePageView',
'AphrontResponseProducerInterface',
),
'PhabricatorStandardSelectCustomFieldDatasource' => 'PhabricatorTypeaheadDatasource',
+ 'PhabricatorStandardTimelineEngine' => 'PhabricatorTimelineEngine',
'PhabricatorStaticEditField' => 'PhabricatorEditField',
'PhabricatorStatusController' => 'PhabricatorController',
'PhabricatorStatusUIExample' => 'PhabricatorUIExample',
'PhabricatorStorageFixtureScopeGuard' => 'Phobject',
'PhabricatorStorageManagementAPI' => 'Phobject',
'PhabricatorStorageManagementAdjustWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementAnalyzeWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementDatabasesWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementDestroyWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementDumpWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementOptimizeWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementPartitionWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementProbeWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementQuickstartWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementRenamespaceWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementShellWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementStatusWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementUpgradeWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorStoragePatch' => 'Phobject',
'PhabricatorStorageSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorStorageSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorStringConfigType' => 'PhabricatorTextConfigType',
'PhabricatorStringExportField' => 'PhabricatorExportField',
'PhabricatorStringListConfigType' => 'PhabricatorTextListConfigType',
'PhabricatorStringListEditField' => 'PhabricatorEditField',
'PhabricatorStringListExportField' => 'PhabricatorListExportField',
'PhabricatorStringMailStamp' => 'PhabricatorMailStamp',
'PhabricatorStringSetting' => 'PhabricatorSetting',
'PhabricatorSubmitEditField' => 'PhabricatorEditField',
'PhabricatorSubscribedToObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorSubscribersEditField' => 'PhabricatorTokenizerEditField',
'PhabricatorSubscribersQuery' => 'PhabricatorQuery',
'PhabricatorSubscriptionTriggerClock' => 'PhabricatorTriggerClock',
'PhabricatorSubscriptionsAddSelfHeraldAction' => 'PhabricatorSubscriptionsHeraldAction',
'PhabricatorSubscriptionsAddSubscribersHeraldAction' => 'PhabricatorSubscriptionsHeraldAction',
'PhabricatorSubscriptionsApplication' => 'PhabricatorApplication',
'PhabricatorSubscriptionsCurtainExtension' => 'PHUICurtainExtension',
'PhabricatorSubscriptionsEditController' => 'PhabricatorController',
'PhabricatorSubscriptionsEditEngineExtension' => 'PhabricatorEditEngineExtension',
'PhabricatorSubscriptionsEditor' => 'PhabricatorEditor',
'PhabricatorSubscriptionsExportEngineExtension' => 'PhabricatorExportEngineExtension',
'PhabricatorSubscriptionsFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension',
'PhabricatorSubscriptionsHeraldAction' => 'HeraldAction',
'PhabricatorSubscriptionsListController' => 'PhabricatorController',
'PhabricatorSubscriptionsMailEngineExtension' => 'PhabricatorMailEngineExtension',
'PhabricatorSubscriptionsMuteController' => 'PhabricatorController',
'PhabricatorSubscriptionsRemoveSelfHeraldAction' => 'PhabricatorSubscriptionsHeraldAction',
'PhabricatorSubscriptionsRemoveSubscribersHeraldAction' => 'PhabricatorSubscriptionsHeraldAction',
'PhabricatorSubscriptionsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorSubscriptionsSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
'PhabricatorSubscriptionsSubscribeEmailCommand' => 'MetaMTAEmailTransactionCommand',
'PhabricatorSubscriptionsSubscribersPolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorSubscriptionsTransactionController' => 'PhabricatorController',
'PhabricatorSubscriptionsUIEventListener' => 'PhabricatorEventListener',
'PhabricatorSubscriptionsUnsubscribeEmailCommand' => 'MetaMTAEmailTransactionCommand',
'PhabricatorSubtypeEditEngineExtension' => 'PhabricatorEditEngineExtension',
'PhabricatorSupportApplication' => 'PhabricatorApplication',
'PhabricatorSyntaxHighlighter' => 'Phobject',
'PhabricatorSyntaxHighlightingConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorSyntaxStyle' => 'Phobject',
'PhabricatorSystemAction' => 'Phobject',
'PhabricatorSystemActionEngine' => 'Phobject',
'PhabricatorSystemActionGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorSystemActionLog' => 'PhabricatorSystemDAO',
'PhabricatorSystemActionRateLimitException' => 'Exception',
'PhabricatorSystemApplication' => 'PhabricatorApplication',
'PhabricatorSystemDAO' => 'PhabricatorLiskDAO',
'PhabricatorSystemDestructionGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorSystemDestructionLog' => 'PhabricatorSystemDAO',
'PhabricatorSystemObjectController' => 'PhabricatorController',
'PhabricatorSystemReadOnlyController' => 'PhabricatorController',
'PhabricatorSystemRemoveDestroyWorkflow' => 'PhabricatorSystemRemoveWorkflow',
'PhabricatorSystemRemoveLogWorkflow' => 'PhabricatorSystemRemoveWorkflow',
'PhabricatorSystemRemoveWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorSystemSelectEncodingController' => 'PhabricatorController',
'PhabricatorSystemSelectHighlightController' => 'PhabricatorController',
'PhabricatorTOTPAuthFactor' => 'PhabricatorAuthFactor',
'PhabricatorTOTPAuthFactorTestCase' => 'PhabricatorTestCase',
'PhabricatorTaskmasterDaemon' => 'PhabricatorDaemon',
'PhabricatorTaskmasterDaemonModule' => 'PhutilDaemonOverseerModule',
'PhabricatorTestApplication' => 'PhabricatorApplication',
'PhabricatorTestCase' => 'PhutilTestCase',
'PhabricatorTestController' => 'PhabricatorController',
'PhabricatorTestDataGenerator' => 'Phobject',
'PhabricatorTestNoCycleEdgeType' => 'PhabricatorEdgeType',
'PhabricatorTestStorageEngine' => 'PhabricatorFileStorageEngine',
'PhabricatorTestWorker' => 'PhabricatorWorker',
'PhabricatorTextAreaEditField' => 'PhabricatorEditField',
'PhabricatorTextConfigType' => 'PhabricatorConfigType',
'PhabricatorTextDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorTextEditField' => 'PhabricatorEditField',
'PhabricatorTextExportFormat' => 'PhabricatorExportFormat',
'PhabricatorTextListConfigType' => 'PhabricatorTextConfigType',
'PhabricatorTime' => 'Phobject',
'PhabricatorTimeFormatSetting' => 'PhabricatorSelectSetting',
'PhabricatorTimeGuard' => 'Phobject',
'PhabricatorTimeTestCase' => 'PhabricatorTestCase',
+ 'PhabricatorTimelineEngine' => 'Phobject',
'PhabricatorTimezoneIgnoreOffsetSetting' => 'PhabricatorInternalSetting',
'PhabricatorTimezoneSetting' => 'PhabricatorOptionGroupSetting',
'PhabricatorTimezoneSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorTitleGlyphsSetting' => 'PhabricatorSelectSetting',
'PhabricatorToken' => array(
'PhabricatorTokenDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorTokenController' => 'PhabricatorController',
'PhabricatorTokenCount' => 'PhabricatorTokenDAO',
'PhabricatorTokenCountQuery' => 'PhabricatorOffsetPagedQuery',
'PhabricatorTokenDAO' => 'PhabricatorLiskDAO',
'PhabricatorTokenDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorTokenGiveController' => 'PhabricatorTokenController',
'PhabricatorTokenGiven' => array(
'PhabricatorTokenDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorTokenGivenController' => 'PhabricatorTokenController',
'PhabricatorTokenGivenEditor' => 'PhabricatorEditor',
'PhabricatorTokenGivenFeedStory' => 'PhabricatorFeedStory',
'PhabricatorTokenGivenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorTokenLeaderController' => 'PhabricatorTokenController',
'PhabricatorTokenQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorTokenReceiverQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorTokenTokenPHIDType' => 'PhabricatorPHIDType',
'PhabricatorTokenUIEventListener' => 'PhabricatorEventListener',
'PhabricatorTokenizerEditField' => 'PhabricatorPHIDListEditField',
'PhabricatorTokensApplication' => 'PhabricatorApplication',
'PhabricatorTokensCurtainExtension' => 'PHUICurtainExtension',
'PhabricatorTokensSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorTokensToken' => array(
'PhabricatorTokenDAO',
'PhabricatorDestructibleInterface',
'PhabricatorSubscribableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorConduitResultInterface',
),
'PhabricatorTransactionChange' => 'Phobject',
'PhabricatorTransactionFactEngine' => 'PhabricatorFactEngine',
'PhabricatorTransactionRemarkupChange' => 'PhabricatorTransactionChange',
'PhabricatorTransactions' => 'Phobject',
'PhabricatorTransactionsApplication' => 'PhabricatorApplication',
'PhabricatorTransactionsDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorTransactionsFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension',
'PhabricatorTransformedFile' => 'PhabricatorFileDAO',
'PhabricatorTranslationSetting' => 'PhabricatorOptionGroupSetting',
'PhabricatorTranslationsConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorTriggerAction' => 'Phobject',
'PhabricatorTriggerClock' => 'Phobject',
'PhabricatorTriggerClockTestCase' => 'PhabricatorTestCase',
'PhabricatorTriggerDaemon' => 'PhabricatorDaemon',
'PhabricatorTrivialTestCase' => 'PhabricatorTestCase',
+ 'PhabricatorTwilioFuture' => 'FutureProxy',
'PhabricatorTwitchAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorTwitterAuthProvider' => 'PhabricatorOAuth1AuthProvider',
'PhabricatorTypeaheadApplication' => 'PhabricatorApplication',
'PhabricatorTypeaheadCompositeDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorTypeaheadDatasource' => 'Phobject',
'PhabricatorTypeaheadDatasourceController' => 'PhabricatorController',
'PhabricatorTypeaheadDatasourceTestCase' => 'PhabricatorTestCase',
'PhabricatorTypeaheadFunctionHelpController' => 'PhabricatorTypeaheadDatasourceController',
'PhabricatorTypeaheadInvalidTokenException' => 'Exception',
'PhabricatorTypeaheadModularDatasourceController' => 'PhabricatorTypeaheadDatasourceController',
'PhabricatorTypeaheadMonogramDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorTypeaheadProxyDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorTypeaheadResult' => 'Phobject',
'PhabricatorTypeaheadRuntimeCompositeDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
'PhabricatorTypeaheadTestNumbersDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorTypeaheadTokenView' => 'AphrontTagView',
'PhabricatorUIConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorUIExample' => 'Phobject',
'PhabricatorUIExampleRenderController' => 'PhabricatorController',
'PhabricatorUIExamplesApplication' => 'PhabricatorApplication',
'PhabricatorURIExportField' => 'PhabricatorExportField',
'PhabricatorUSEnglishTranslation' => 'PhutilTranslation',
'PhabricatorUnifiedDiffsSetting' => 'PhabricatorSelectSetting',
'PhabricatorUnitTestContentSource' => 'PhabricatorContentSource',
'PhabricatorUnitsTestCase' => 'PhabricatorTestCase',
'PhabricatorUnknownContentSource' => 'PhabricatorContentSource',
'PhabricatorUnsubscribedFromObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorUser' => array(
'PhabricatorUserDAO',
'PhutilPerson',
'PhabricatorPolicyInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSSHPublicKeyInterface',
'PhabricatorFlaggableInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
'PhabricatorConduitResultInterface',
'PhabricatorAuthPasswordHashInterface',
),
'PhabricatorUserApproveTransaction' => 'PhabricatorUserTransactionType',
'PhabricatorUserBadgesCacheType' => 'PhabricatorUserCacheType',
'PhabricatorUserBlurbField' => 'PhabricatorUserCustomField',
'PhabricatorUserCache' => 'PhabricatorUserDAO',
'PhabricatorUserCachePurger' => 'PhabricatorCachePurger',
'PhabricatorUserCacheType' => 'Phobject',
'PhabricatorUserCardView' => 'AphrontTagView',
'PhabricatorUserConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorUserConfiguredCustomField' => array(
'PhabricatorUserCustomField',
'PhabricatorStandardCustomFieldInterface',
),
'PhabricatorUserConfiguredCustomFieldStorage' => 'PhabricatorCustomFieldStorage',
'PhabricatorUserCustomField' => 'PhabricatorCustomField',
'PhabricatorUserCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage',
'PhabricatorUserCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage',
'PhabricatorUserDAO' => 'PhabricatorLiskDAO',
'PhabricatorUserDisableTransaction' => 'PhabricatorUserTransactionType',
'PhabricatorUserEditEngine' => 'PhabricatorEditEngine',
'PhabricatorUserEditor' => 'PhabricatorEditor',
'PhabricatorUserEditorTestCase' => 'PhabricatorTestCase',
'PhabricatorUserEmail' => 'PhabricatorUserDAO',
'PhabricatorUserEmailTestCase' => 'PhabricatorTestCase',
+ 'PhabricatorUserEmpowerTransaction' => 'PhabricatorUserTransactionType',
'PhabricatorUserFerretEngine' => 'PhabricatorFerretEngine',
'PhabricatorUserFulltextEngine' => 'PhabricatorFulltextEngine',
'PhabricatorUserIconField' => 'PhabricatorUserCustomField',
'PhabricatorUserLog' => array(
'PhabricatorUserDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorUserLogView' => 'AphrontView',
'PhabricatorUserMessageCountCacheType' => 'PhabricatorUserCacheType',
'PhabricatorUserNotificationCountCacheType' => 'PhabricatorUserCacheType',
'PhabricatorUserNotifyTransaction' => 'PhabricatorUserTransactionType',
'PhabricatorUserPHIDResolver' => 'PhabricatorPHIDResolver',
'PhabricatorUserPreferences' => array(
'PhabricatorUserDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorApplicationTransactionInterface',
),
'PhabricatorUserPreferencesCacheType' => 'PhabricatorUserCacheType',
'PhabricatorUserPreferencesEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorUserPreferencesPHIDType' => 'PhabricatorPHIDType',
'PhabricatorUserPreferencesQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorUserPreferencesSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorUserPreferencesTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorUserPreferencesTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorUserProfile' => 'PhabricatorUserDAO',
'PhabricatorUserProfileImageCacheType' => 'PhabricatorUserCacheType',
'PhabricatorUserRealNameField' => 'PhabricatorUserCustomField',
'PhabricatorUserRolesField' => 'PhabricatorUserCustomField',
'PhabricatorUserSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorUserSinceField' => 'PhabricatorUserCustomField',
'PhabricatorUserStatusField' => 'PhabricatorUserCustomField',
'PhabricatorUserTestCase' => 'PhabricatorTestCase',
'PhabricatorUserTitleField' => 'PhabricatorUserCustomField',
'PhabricatorUserTransaction' => 'PhabricatorModularTransaction',
'PhabricatorUserTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorUserTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorUserUsernameTransaction' => 'PhabricatorUserTransactionType',
'PhabricatorUsersEditField' => 'PhabricatorTokenizerEditField',
'PhabricatorUsersPolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorUsersSearchField' => 'PhabricatorSearchTokenizerField',
'PhabricatorVCSResponse' => 'AphrontResponse',
'PhabricatorVersionedDraft' => 'PhabricatorDraftDAO',
'PhabricatorVeryWowEnglishTranslation' => 'PhutilTranslation',
'PhabricatorVideoDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorViewerDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorVoidDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorWatcherHasObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorWebContentSource' => 'PhabricatorContentSource',
'PhabricatorWebServerSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorWeekStartDaySetting' => 'PhabricatorSelectSetting',
'PhabricatorWildConfigType' => 'PhabricatorJSONConfigType',
'PhabricatorWordPressAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorWorker' => 'Phobject',
'PhabricatorWorkerActiveTask' => 'PhabricatorWorkerTask',
'PhabricatorWorkerActiveTaskQuery' => 'PhabricatorWorkerTaskQuery',
'PhabricatorWorkerArchiveTask' => 'PhabricatorWorkerTask',
'PhabricatorWorkerArchiveTaskQuery' => 'PhabricatorWorkerTaskQuery',
'PhabricatorWorkerBulkJob' => array(
'PhabricatorWorkerDAO',
'PhabricatorPolicyInterface',
'PhabricatorSubscribableInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorWorkerBulkJobCreateWorker' => 'PhabricatorWorkerBulkJobWorker',
'PhabricatorWorkerBulkJobEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorWorkerBulkJobPHIDType' => 'PhabricatorPHIDType',
'PhabricatorWorkerBulkJobQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorWorkerBulkJobSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorWorkerBulkJobTaskWorker' => 'PhabricatorWorkerBulkJobWorker',
'PhabricatorWorkerBulkJobTestCase' => 'PhabricatorTestCase',
'PhabricatorWorkerBulkJobTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorWorkerBulkJobTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorWorkerBulkJobType' => 'Phobject',
'PhabricatorWorkerBulkJobWorker' => 'PhabricatorWorker',
'PhabricatorWorkerBulkTask' => 'PhabricatorWorkerDAO',
'PhabricatorWorkerDAO' => 'PhabricatorLiskDAO',
'PhabricatorWorkerDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension',
'PhabricatorWorkerLeaseQuery' => 'PhabricatorQuery',
'PhabricatorWorkerManagementCancelWorkflow' => 'PhabricatorWorkerManagementWorkflow',
'PhabricatorWorkerManagementExecuteWorkflow' => 'PhabricatorWorkerManagementWorkflow',
'PhabricatorWorkerManagementFloodWorkflow' => 'PhabricatorWorkerManagementWorkflow',
'PhabricatorWorkerManagementFreeWorkflow' => 'PhabricatorWorkerManagementWorkflow',
'PhabricatorWorkerManagementRetryWorkflow' => 'PhabricatorWorkerManagementWorkflow',
'PhabricatorWorkerManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorWorkerPermanentFailureException' => 'Exception',
'PhabricatorWorkerSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorWorkerSingleBulkJobType' => 'PhabricatorWorkerBulkJobType',
'PhabricatorWorkerTask' => 'PhabricatorWorkerDAO',
'PhabricatorWorkerTaskData' => 'PhabricatorWorkerDAO',
'PhabricatorWorkerTaskDetailController' => 'PhabricatorDaemonController',
'PhabricatorWorkerTaskQuery' => 'PhabricatorQuery',
'PhabricatorWorkerTestCase' => 'PhabricatorTestCase',
'PhabricatorWorkerTrigger' => array(
'PhabricatorWorkerDAO',
'PhabricatorDestructibleInterface',
'PhabricatorPolicyInterface',
),
'PhabricatorWorkerTriggerEvent' => 'PhabricatorWorkerDAO',
'PhabricatorWorkerTriggerManagementFireWorkflow' => 'PhabricatorWorkerTriggerManagementWorkflow',
'PhabricatorWorkerTriggerManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorWorkerTriggerPHIDType' => 'PhabricatorPHIDType',
'PhabricatorWorkerTriggerQuery' => 'PhabricatorPolicyAwareQuery',
'PhabricatorWorkerYieldException' => 'Exception',
'PhabricatorWorkingCopyDiscoveryTestCase' => 'PhabricatorWorkingCopyTestCase',
'PhabricatorWorkingCopyPullTestCase' => 'PhabricatorWorkingCopyTestCase',
'PhabricatorWorkingCopyTestCase' => 'PhabricatorTestCase',
'PhabricatorXHPASTDAO' => 'PhabricatorLiskDAO',
'PhabricatorXHPASTParseTree' => 'PhabricatorXHPASTDAO',
'PhabricatorXHPASTViewController' => 'PhabricatorController',
'PhabricatorXHPASTViewFrameController' => 'PhabricatorXHPASTViewController',
'PhabricatorXHPASTViewFramesetController' => 'PhabricatorXHPASTViewController',
'PhabricatorXHPASTViewInputController' => 'PhabricatorXHPASTViewPanelController',
'PhabricatorXHPASTViewPanelController' => 'PhabricatorXHPASTViewController',
'PhabricatorXHPASTViewRunController' => 'PhabricatorXHPASTViewController',
'PhabricatorXHPASTViewStreamController' => 'PhabricatorXHPASTViewPanelController',
'PhabricatorXHPASTViewTreeController' => 'PhabricatorXHPASTViewPanelController',
'PhabricatorXHProfApplication' => 'PhabricatorApplication',
'PhabricatorXHProfController' => 'PhabricatorController',
'PhabricatorXHProfDAO' => 'PhabricatorLiskDAO',
'PhabricatorXHProfDropController' => 'PhabricatorXHProfController',
'PhabricatorXHProfProfileController' => 'PhabricatorXHProfController',
'PhabricatorXHProfProfileSymbolView' => 'PhabricatorXHProfProfileView',
'PhabricatorXHProfProfileTopLevelView' => 'PhabricatorXHProfProfileView',
'PhabricatorXHProfProfileView' => 'AphrontView',
'PhabricatorXHProfSample' => array(
'PhabricatorXHProfDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorXHProfSampleListController' => 'PhabricatorXHProfController',
'PhabricatorXHProfSampleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorXHProfSampleSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorYoutubeRemarkupRule' => 'PhutilRemarkupRule',
'Phame404Response' => 'AphrontHTMLResponse',
'PhameBlog' => array(
'PhameDAO',
'PhabricatorPolicyInterface',
'PhabricatorMarkupInterface',
'PhabricatorSubscribableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorProjectInterface',
'PhabricatorDestructibleInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorConduitResultInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
),
'PhameBlog404Controller' => 'PhameLiveController',
'PhameBlogArchiveController' => 'PhameBlogController',
'PhameBlogController' => 'PhameController',
'PhameBlogCreateCapability' => 'PhabricatorPolicyCapability',
'PhameBlogDatasource' => 'PhabricatorTypeaheadDatasource',
'PhameBlogDescriptionTransaction' => 'PhameBlogTransactionType',
'PhameBlogEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PhameBlogEditController' => 'PhameBlogController',
'PhameBlogEditEngine' => 'PhabricatorEditEngine',
'PhameBlogEditor' => 'PhabricatorApplicationTransactionEditor',
'PhameBlogFeedController' => 'PhameBlogController',
'PhameBlogFerretEngine' => 'PhabricatorFerretEngine',
'PhameBlogFullDomainTransaction' => 'PhameBlogTransactionType',
'PhameBlogFulltextEngine' => 'PhabricatorFulltextEngine',
'PhameBlogHeaderImageTransaction' => 'PhameBlogTransactionType',
'PhameBlogHeaderPictureController' => 'PhameBlogController',
'PhameBlogListController' => 'PhameBlogController',
'PhameBlogListView' => 'AphrontTagView',
'PhameBlogManageController' => 'PhameBlogController',
'PhameBlogNameTransaction' => 'PhameBlogTransactionType',
'PhameBlogParentDomainTransaction' => 'PhameBlogTransactionType',
'PhameBlogParentSiteTransaction' => 'PhameBlogTransactionType',
'PhameBlogProfileImageTransaction' => 'PhameBlogTransactionType',
'PhameBlogProfilePictureController' => 'PhameBlogController',
'PhameBlogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhameBlogReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhameBlogSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhameBlogSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhameBlogSite' => 'PhameSite',
'PhameBlogStatusTransaction' => 'PhameBlogTransactionType',
'PhameBlogSubtitleTransaction' => 'PhameBlogTransactionType',
'PhameBlogTransaction' => 'PhabricatorModularTransaction',
'PhameBlogTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhameBlogTransactionType' => 'PhabricatorModularTransactionType',
'PhameBlogViewController' => 'PhameLiveController',
'PhameConstants' => 'Phobject',
'PhameController' => 'PhabricatorController',
'PhameDAO' => 'PhabricatorLiskDAO',
'PhameDescriptionView' => 'AphrontTagView',
'PhameDraftListView' => 'AphrontTagView',
'PhameHomeController' => 'PhamePostController',
'PhameLiveController' => 'PhameController',
'PhameNextPostView' => 'AphrontTagView',
'PhamePost' => array(
'PhameDAO',
'PhabricatorPolicyInterface',
'PhabricatorMarkupInterface',
'PhabricatorFlaggableInterface',
'PhabricatorProjectInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorSubscribableInterface',
'PhabricatorDestructibleInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorConduitResultInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
),
'PhamePostArchiveController' => 'PhamePostController',
'PhamePostBlogTransaction' => 'PhamePostTransactionType',
'PhamePostBodyTransaction' => 'PhamePostTransactionType',
'PhamePostController' => 'PhameController',
'PhamePostEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'PhamePostEditController' => 'PhamePostController',
'PhamePostEditEngine' => 'PhabricatorEditEngine',
'PhamePostEditor' => 'PhabricatorApplicationTransactionEditor',
'PhamePostFerretEngine' => 'PhabricatorFerretEngine',
'PhamePostFulltextEngine' => 'PhabricatorFulltextEngine',
'PhamePostHeaderImageTransaction' => 'PhamePostTransactionType',
'PhamePostHeaderPictureController' => 'PhamePostController',
'PhamePostHistoryController' => 'PhamePostController',
'PhamePostListController' => 'PhamePostController',
'PhamePostListView' => 'AphrontTagView',
'PhamePostMailReceiver' => 'PhabricatorObjectMailReceiver',
'PhamePostMoveController' => 'PhamePostController',
'PhamePostPublishController' => 'PhamePostController',
'PhamePostQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhamePostRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhamePostReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhamePostSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhamePostSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhamePostSubtitleTransaction' => 'PhamePostTransactionType',
'PhamePostTitleTransaction' => 'PhamePostTransactionType',
'PhamePostTransaction' => 'PhabricatorModularTransaction',
'PhamePostTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhamePostTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhamePostTransactionType' => 'PhabricatorModularTransactionType',
'PhamePostViewController' => 'PhameLiveController',
'PhamePostVisibilityTransaction' => 'PhamePostTransactionType',
'PhameSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhameSite' => 'PhabricatorSite',
'PhluxController' => 'PhabricatorController',
'PhluxDAO' => 'PhabricatorLiskDAO',
'PhluxEditController' => 'PhluxController',
'PhluxListController' => 'PhluxController',
'PhluxSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhluxTransaction' => 'PhabricatorApplicationTransaction',
'PhluxTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhluxVariable' => array(
'PhluxDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorFlaggableInterface',
'PhabricatorPolicyInterface',
),
'PhluxVariableEditor' => 'PhabricatorApplicationTransactionEditor',
'PhluxVariablePHIDType' => 'PhabricatorPHIDType',
'PhluxVariableQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhluxViewController' => 'PhluxController',
'PholioController' => 'PhabricatorController',
'PholioDAO' => 'PhabricatorLiskDAO',
'PholioDefaultEditCapability' => 'PhabricatorPolicyCapability',
'PholioDefaultViewCapability' => 'PhabricatorPolicyCapability',
'PholioImage' => array(
'PholioDAO',
- 'PhabricatorMarkupInterface',
'PhabricatorPolicyInterface',
+ 'PhabricatorExtendedPolicyInterface',
),
'PholioImageDescriptionTransaction' => 'PholioImageTransactionType',
'PholioImageFileTransaction' => 'PholioImageTransactionType',
'PholioImageNameTransaction' => 'PholioImageTransactionType',
'PholioImagePHIDType' => 'PhabricatorPHIDType',
'PholioImageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PholioImageReplaceTransaction' => 'PholioImageTransactionType',
'PholioImageSequenceTransaction' => 'PholioImageTransactionType',
'PholioImageTransactionType' => 'PholioTransactionType',
'PholioImageUploadController' => 'PholioController',
'PholioInlineController' => 'PholioController',
'PholioInlineListController' => 'PholioController',
'PholioMock' => array(
'PholioDAO',
- 'PhabricatorMarkupInterface',
'PhabricatorPolicyInterface',
'PhabricatorSubscribableInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorFlaggableInterface',
'PhabricatorApplicationTransactionInterface',
+ 'PhabricatorTimelineInterface',
'PhabricatorProjectInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSpacesInterface',
'PhabricatorMentionableInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
),
'PholioMockArchiveController' => 'PholioController',
'PholioMockAuthorHeraldField' => 'PholioMockHeraldField',
'PholioMockCommentController' => 'PholioController',
'PholioMockDescriptionHeraldField' => 'PholioMockHeraldField',
'PholioMockDescriptionTransaction' => 'PholioMockTransactionType',
'PholioMockEditController' => 'PholioController',
'PholioMockEditor' => 'PhabricatorApplicationTransactionEditor',
'PholioMockEmbedView' => 'AphrontView',
'PholioMockFerretEngine' => 'PhabricatorFerretEngine',
'PholioMockFulltextEngine' => 'PhabricatorFulltextEngine',
'PholioMockHasTaskEdgeType' => 'PhabricatorEdgeType',
'PholioMockHasTaskRelationship' => 'PholioMockRelationship',
'PholioMockHeraldField' => 'HeraldField',
'PholioMockHeraldFieldGroup' => 'HeraldFieldGroup',
'PholioMockImagesView' => 'AphrontView',
'PholioMockInlineTransaction' => 'PholioMockTransactionType',
'PholioMockListController' => 'PholioController',
'PholioMockMailReceiver' => 'PhabricatorObjectMailReceiver',
'PholioMockNameHeraldField' => 'PholioMockHeraldField',
'PholioMockNameTransaction' => 'PholioMockTransactionType',
'PholioMockPHIDType' => 'PhabricatorPHIDType',
'PholioMockQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PholioMockRelationship' => 'PhabricatorObjectRelationship',
'PholioMockRelationshipSource' => 'PhabricatorObjectRelationshipSource',
'PholioMockSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PholioMockStatusTransaction' => 'PholioMockTransactionType',
'PholioMockThumbGridView' => 'AphrontView',
+ 'PholioMockTimelineEngine' => 'PhabricatorTimelineEngine',
'PholioMockTransactionType' => 'PholioTransactionType',
'PholioMockViewController' => 'PholioController',
'PholioRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PholioReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PholioSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PholioTransaction' => 'PhabricatorModularTransaction',
'PholioTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PholioTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PholioTransactionType' => 'PhabricatorModularTransactionType',
'PholioTransactionView' => 'PhabricatorApplicationTransactionView',
'PholioUploadedImageView' => 'AphrontView',
'PhortuneAccount' => array(
'PhortuneDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
),
'PhortuneAccountAddManagerController' => 'PhortuneController',
+ 'PhortuneAccountBillingAddressTransaction' => 'PhortuneAccountTransactionType',
'PhortuneAccountBillingController' => 'PhortuneAccountProfileController',
+ 'PhortuneAccountBillingNameTransaction' => 'PhortuneAccountTransactionType',
'PhortuneAccountChargeListController' => 'PhortuneController',
'PhortuneAccountController' => 'PhortuneController',
'PhortuneAccountEditController' => 'PhortuneController',
'PhortuneAccountEditEngine' => 'PhabricatorEditEngine',
'PhortuneAccountEditor' => 'PhabricatorApplicationTransactionEditor',
'PhortuneAccountHasMemberEdgeType' => 'PhabricatorEdgeType',
'PhortuneAccountListController' => 'PhortuneController',
'PhortuneAccountManagerController' => 'PhortuneAccountProfileController',
'PhortuneAccountNameTransaction' => 'PhortuneAccountTransactionType',
'PhortuneAccountPHIDType' => 'PhabricatorPHIDType',
'PhortuneAccountProfileController' => 'PhortuneAccountController',
'PhortuneAccountQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneAccountSubscriptionController' => 'PhortuneAccountProfileController',
'PhortuneAccountTransaction' => 'PhabricatorModularTransaction',
'PhortuneAccountTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhortuneAccountTransactionType' => 'PhabricatorModularTransactionType',
'PhortuneAccountViewController' => 'PhortuneAccountProfileController',
'PhortuneAdHocCart' => 'PhortuneCartImplementation',
'PhortuneAdHocProduct' => 'PhortuneProductImplementation',
'PhortuneCart' => array(
'PhortuneDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
),
'PhortuneCartAcceptController' => 'PhortuneCartController',
'PhortuneCartCancelController' => 'PhortuneCartController',
'PhortuneCartCheckoutController' => 'PhortuneCartController',
'PhortuneCartController' => 'PhortuneController',
'PhortuneCartEditor' => 'PhabricatorApplicationTransactionEditor',
'PhortuneCartImplementation' => 'Phobject',
'PhortuneCartListController' => 'PhortuneController',
'PhortuneCartPHIDType' => 'PhabricatorPHIDType',
'PhortuneCartQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneCartReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhortuneCartSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhortuneCartTransaction' => 'PhabricatorApplicationTransaction',
'PhortuneCartTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhortuneCartUpdateController' => 'PhortuneCartController',
'PhortuneCartViewController' => 'PhortuneCartController',
'PhortuneCharge' => array(
'PhortuneDAO',
'PhabricatorPolicyInterface',
),
'PhortuneChargePHIDType' => 'PhabricatorPHIDType',
'PhortuneChargeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneChargeSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhortuneChargeTableView' => 'AphrontView',
'PhortuneConstants' => 'Phobject',
'PhortuneController' => 'PhabricatorController',
'PhortuneCreditCardForm' => 'Phobject',
'PhortuneCurrency' => 'Phobject',
'PhortuneCurrencySerializer' => 'PhabricatorLiskSerializer',
'PhortuneCurrencyTestCase' => 'PhabricatorTestCase',
'PhortuneDAO' => 'PhabricatorLiskDAO',
'PhortuneErrCode' => 'PhortuneConstants',
'PhortuneInvoiceView' => 'AphrontTagView',
'PhortuneLandingController' => 'PhortuneController',
'PhortuneMemberHasAccountEdgeType' => 'PhabricatorEdgeType',
'PhortuneMemberHasMerchantEdgeType' => 'PhabricatorEdgeType',
'PhortuneMerchant' => array(
'PhortuneDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
),
'PhortuneMerchantAddManagerController' => 'PhortuneController',
'PhortuneMerchantCapability' => 'PhabricatorPolicyCapability',
'PhortuneMerchantContactInfoTransaction' => 'PhortuneMerchantTransactionType',
'PhortuneMerchantController' => 'PhortuneController',
'PhortuneMerchantDescriptionTransaction' => 'PhortuneMerchantTransactionType',
'PhortuneMerchantEditController' => 'PhortuneMerchantController',
'PhortuneMerchantEditEngine' => 'PhabricatorEditEngine',
'PhortuneMerchantEditor' => 'PhabricatorApplicationTransactionEditor',
'PhortuneMerchantHasMemberEdgeType' => 'PhabricatorEdgeType',
'PhortuneMerchantInvoiceCreateController' => 'PhortuneMerchantProfileController',
'PhortuneMerchantInvoiceEmailTransaction' => 'PhortuneMerchantTransactionType',
'PhortuneMerchantInvoiceFooterTransaction' => 'PhortuneMerchantTransactionType',
'PhortuneMerchantListController' => 'PhortuneMerchantController',
'PhortuneMerchantManagerController' => 'PhortuneMerchantProfileController',
'PhortuneMerchantNameTransaction' => 'PhortuneMerchantTransactionType',
'PhortuneMerchantPHIDType' => 'PhabricatorPHIDType',
'PhortuneMerchantPictureController' => 'PhortuneMerchantProfileController',
'PhortuneMerchantPictureTransaction' => 'PhortuneMerchantTransactionType',
'PhortuneMerchantProfileController' => 'PhortuneController',
'PhortuneMerchantQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneMerchantSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhortuneMerchantTransaction' => 'PhabricatorModularTransaction',
'PhortuneMerchantTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhortuneMerchantTransactionType' => 'PhabricatorModularTransactionType',
'PhortuneMerchantViewController' => 'PhortuneMerchantProfileController',
'PhortuneMonthYearExpiryControl' => 'AphrontFormControl',
'PhortuneOrderTableView' => 'AphrontView',
'PhortunePayPalPaymentProvider' => 'PhortunePaymentProvider',
'PhortunePaymentMethod' => array(
'PhortuneDAO',
'PhabricatorPolicyInterface',
),
'PhortunePaymentMethodCreateController' => 'PhortuneController',
'PhortunePaymentMethodDisableController' => 'PhortuneController',
'PhortunePaymentMethodEditController' => 'PhortuneController',
'PhortunePaymentMethodPHIDType' => 'PhabricatorPHIDType',
'PhortunePaymentMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortunePaymentProvider' => 'Phobject',
'PhortunePaymentProviderConfig' => array(
'PhortuneDAO',
'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface',
),
'PhortunePaymentProviderConfigEditor' => 'PhabricatorApplicationTransactionEditor',
'PhortunePaymentProviderConfigQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortunePaymentProviderConfigTransaction' => 'PhabricatorApplicationTransaction',
'PhortunePaymentProviderConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhortunePaymentProviderPHIDType' => 'PhabricatorPHIDType',
'PhortunePaymentProviderTestCase' => 'PhabricatorTestCase',
'PhortuneProduct' => array(
'PhortuneDAO',
'PhabricatorPolicyInterface',
),
'PhortuneProductImplementation' => 'Phobject',
'PhortuneProductListController' => 'PhabricatorController',
'PhortuneProductPHIDType' => 'PhabricatorPHIDType',
'PhortuneProductQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneProductViewController' => 'PhortuneController',
'PhortuneProviderActionController' => 'PhortuneController',
'PhortuneProviderDisableController' => 'PhortuneMerchantController',
'PhortuneProviderEditController' => 'PhortuneMerchantController',
'PhortunePurchase' => array(
'PhortuneDAO',
'PhabricatorPolicyInterface',
),
'PhortunePurchasePHIDType' => 'PhabricatorPHIDType',
'PhortunePurchaseQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhortuneStripePaymentProvider' => 'PhortunePaymentProvider',
'PhortuneSubscription' => array(
'PhortuneDAO',
'PhabricatorPolicyInterface',
),
'PhortuneSubscriptionCart' => 'PhortuneCartImplementation',
'PhortuneSubscriptionEditController' => 'PhortuneController',
'PhortuneSubscriptionImplementation' => 'Phobject',
'PhortuneSubscriptionListController' => 'PhortuneController',
'PhortuneSubscriptionPHIDType' => 'PhabricatorPHIDType',
'PhortuneSubscriptionProduct' => 'PhortuneProductImplementation',
'PhortuneSubscriptionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneSubscriptionSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhortuneSubscriptionTableView' => 'AphrontView',
'PhortuneSubscriptionViewController' => 'PhortuneController',
'PhortuneSubscriptionWorker' => 'PhabricatorWorker',
'PhortuneTestPaymentProvider' => 'PhortunePaymentProvider',
'PhortuneWePayPaymentProvider' => 'PhortunePaymentProvider',
'PhragmentBrowseController' => 'PhragmentController',
'PhragmentCanCreateCapability' => 'PhabricatorPolicyCapability',
'PhragmentConduitAPIMethod' => 'ConduitAPIMethod',
'PhragmentController' => 'PhabricatorController',
'PhragmentCreateController' => 'PhragmentController',
'PhragmentDAO' => 'PhabricatorLiskDAO',
'PhragmentFragment' => array(
'PhragmentDAO',
'PhabricatorPolicyInterface',
),
'PhragmentFragmentPHIDType' => 'PhabricatorPHIDType',
'PhragmentFragmentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhragmentFragmentVersion' => array(
'PhragmentDAO',
'PhabricatorPolicyInterface',
),
'PhragmentFragmentVersionPHIDType' => 'PhabricatorPHIDType',
'PhragmentFragmentVersionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhragmentGetPatchConduitAPIMethod' => 'PhragmentConduitAPIMethod',
'PhragmentHistoryController' => 'PhragmentController',
'PhragmentPatchController' => 'PhragmentController',
'PhragmentPatchUtil' => 'Phobject',
'PhragmentPolicyController' => 'PhragmentController',
'PhragmentQueryFragmentsConduitAPIMethod' => 'PhragmentConduitAPIMethod',
'PhragmentRevertController' => 'PhragmentController',
'PhragmentSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhragmentSnapshot' => array(
'PhragmentDAO',
'PhabricatorPolicyInterface',
),
'PhragmentSnapshotChild' => array(
'PhragmentDAO',
'PhabricatorPolicyInterface',
),
'PhragmentSnapshotChildQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhragmentSnapshotCreateController' => 'PhragmentController',
'PhragmentSnapshotDeleteController' => 'PhragmentController',
'PhragmentSnapshotPHIDType' => 'PhabricatorPHIDType',
'PhragmentSnapshotPromoteController' => 'PhragmentController',
'PhragmentSnapshotQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhragmentSnapshotViewController' => 'PhragmentController',
'PhragmentUpdateController' => 'PhragmentController',
'PhragmentVersionController' => 'PhragmentController',
'PhragmentZIPController' => 'PhragmentController',
'PhrequentConduitAPIMethod' => 'ConduitAPIMethod',
'PhrequentController' => 'PhabricatorController',
'PhrequentCurtainExtension' => 'PHUICurtainExtension',
'PhrequentDAO' => 'PhabricatorLiskDAO',
'PhrequentListController' => 'PhrequentController',
'PhrequentPopConduitAPIMethod' => 'PhrequentConduitAPIMethod',
'PhrequentPushConduitAPIMethod' => 'PhrequentConduitAPIMethod',
'PhrequentSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhrequentTimeBlock' => 'Phobject',
'PhrequentTimeBlockTestCase' => 'PhabricatorTestCase',
'PhrequentTimeSlices' => 'Phobject',
'PhrequentTrackController' => 'PhrequentController',
'PhrequentTrackingConduitAPIMethod' => 'PhrequentConduitAPIMethod',
'PhrequentTrackingEditor' => 'PhabricatorEditor',
'PhrequentUIEventListener' => 'PhabricatorEventListener',
'PhrequentUserTime' => array(
'PhrequentDAO',
'PhabricatorPolicyInterface',
),
'PhrequentUserTimeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhrictionChangeType' => 'PhrictionConstants',
'PhrictionConduitAPIMethod' => 'ConduitAPIMethod',
'PhrictionConstants' => 'Phobject',
'PhrictionContent' => array(
'PhrictionDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorConduitResultInterface',
),
'PhrictionContentPHIDType' => 'PhabricatorPHIDType',
'PhrictionContentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhrictionContentSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhrictionContentSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhrictionContentSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhrictionController' => 'PhabricatorController',
'PhrictionCreateConduitAPIMethod' => 'PhrictionConduitAPIMethod',
'PhrictionDAO' => 'PhabricatorLiskDAO',
'PhrictionDatasourceEngineExtension' => 'PhabricatorDatasourceEngineExtension',
'PhrictionDeleteController' => 'PhrictionController',
'PhrictionDiffController' => 'PhrictionController',
'PhrictionDocument' => array(
'PhrictionDAO',
'PhabricatorPolicyInterface',
'PhabricatorSubscribableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorDestructibleInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
'PhabricatorProjectInterface',
'PhabricatorApplicationTransactionInterface',
'PhabricatorConduitResultInterface',
'PhabricatorPolicyCodexInterface',
'PhabricatorSpacesInterface',
),
'PhrictionDocumentAuthorHeraldField' => 'PhrictionDocumentHeraldField',
'PhrictionDocumentContentHeraldField' => 'PhrictionDocumentHeraldField',
'PhrictionDocumentContentTransaction' => 'PhrictionDocumentEditTransaction',
'PhrictionDocumentController' => 'PhrictionController',
'PhrictionDocumentDatasource' => 'PhabricatorTypeaheadDatasource',
'PhrictionDocumentDeleteTransaction' => 'PhrictionDocumentVersionTransaction',
'PhrictionDocumentDraftTransaction' => 'PhrictionDocumentEditTransaction',
'PhrictionDocumentEditEngine' => 'PhabricatorEditEngine',
'PhrictionDocumentEditTransaction' => 'PhrictionDocumentVersionTransaction',
'PhrictionDocumentFerretEngine' => 'PhabricatorFerretEngine',
'PhrictionDocumentFulltextEngine' => 'PhabricatorFulltextEngine',
'PhrictionDocumentHeraldAdapter' => 'HeraldAdapter',
'PhrictionDocumentHeraldField' => 'HeraldField',
'PhrictionDocumentHeraldFieldGroup' => 'HeraldFieldGroup',
'PhrictionDocumentMoveAwayTransaction' => 'PhrictionDocumentVersionTransaction',
'PhrictionDocumentMoveToTransaction' => 'PhrictionDocumentVersionTransaction',
'PhrictionDocumentPHIDType' => 'PhabricatorPHIDType',
'PhrictionDocumentPathHeraldField' => 'PhrictionDocumentHeraldField',
'PhrictionDocumentPolicyCodex' => 'PhabricatorPolicyCodex',
'PhrictionDocumentPublishTransaction' => 'PhrictionDocumentTransactionType',
'PhrictionDocumentQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhrictionDocumentSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'PhrictionDocumentSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhrictionDocumentStatus' => 'PhabricatorObjectStatus',
'PhrictionDocumentTitleHeraldField' => 'PhrictionDocumentHeraldField',
'PhrictionDocumentTitleTransaction' => 'PhrictionDocumentVersionTransaction',
'PhrictionDocumentTransactionType' => 'PhabricatorModularTransactionType',
'PhrictionDocumentVersionTransaction' => 'PhrictionDocumentTransactionType',
'PhrictionEditConduitAPIMethod' => 'PhrictionConduitAPIMethod',
'PhrictionEditController' => 'PhrictionController',
'PhrictionEditEngineController' => 'PhrictionController',
'PhrictionHistoryConduitAPIMethod' => 'PhrictionConduitAPIMethod',
'PhrictionHistoryController' => 'PhrictionController',
'PhrictionInfoConduitAPIMethod' => 'PhrictionConduitAPIMethod',
'PhrictionListController' => 'PhrictionController',
'PhrictionMarkupPreviewController' => 'PhabricatorController',
'PhrictionMoveController' => 'PhrictionController',
'PhrictionNewController' => 'PhrictionController',
'PhrictionPublishController' => 'PhrictionController',
'PhrictionRemarkupRule' => 'PhutilRemarkupRule',
'PhrictionReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhrictionSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhrictionTransaction' => 'PhabricatorModularTransaction',
'PhrictionTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PhrictionTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
'PhrictionTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PolicyLockOptionType' => 'PhabricatorConfigJSONOptionType',
'PonderAddAnswerView' => 'AphrontView',
'PonderAnswer' => array(
'PonderDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorMarkupInterface',
'PhabricatorPolicyInterface',
'PhabricatorFlaggableInterface',
'PhabricatorSubscribableInterface',
'PhabricatorDestructibleInterface',
),
'PonderAnswerCommentController' => 'PonderController',
'PonderAnswerContentTransaction' => 'PonderAnswerTransactionType',
'PonderAnswerEditController' => 'PonderController',
'PonderAnswerEditor' => 'PonderEditor',
'PonderAnswerHistoryController' => 'PonderController',
'PonderAnswerMailReceiver' => 'PhabricatorObjectMailReceiver',
'PonderAnswerPHIDType' => 'PhabricatorPHIDType',
'PonderAnswerQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PonderAnswerQuestionIDTransaction' => 'PonderAnswerTransactionType',
'PonderAnswerReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PonderAnswerSaveController' => 'PonderController',
'PonderAnswerStatus' => 'PonderConstants',
'PonderAnswerStatusTransaction' => 'PonderAnswerTransactionType',
'PonderAnswerTransaction' => 'PhabricatorModularTransaction',
'PonderAnswerTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PonderAnswerTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PonderAnswerTransactionType' => 'PhabricatorModularTransactionType',
'PonderAnswerView' => 'AphrontTagView',
'PonderConstants' => 'Phobject',
'PonderController' => 'PhabricatorController',
'PonderDAO' => 'PhabricatorLiskDAO',
'PonderDefaultViewCapability' => 'PhabricatorPolicyCapability',
'PonderEditor' => 'PhabricatorApplicationTransactionEditor',
'PonderFooterView' => 'AphrontTagView',
'PonderModerateCapability' => 'PhabricatorPolicyCapability',
'PonderQuestion' => array(
'PonderDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorMarkupInterface',
'PhabricatorSubscribableInterface',
'PhabricatorFlaggableInterface',
'PhabricatorPolicyInterface',
'PhabricatorTokenReceiverInterface',
'PhabricatorProjectInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSpacesInterface',
'PhabricatorFulltextInterface',
'PhabricatorFerretInterface',
),
'PonderQuestionAnswerTransaction' => 'PonderQuestionTransactionType',
'PonderQuestionAnswerWikiTransaction' => 'PonderQuestionTransactionType',
'PonderQuestionCommentController' => 'PonderController',
'PonderQuestionContentTransaction' => 'PonderQuestionTransactionType',
- 'PonderQuestionCreateMailReceiver' => 'PhabricatorMailReceiver',
+ 'PonderQuestionCreateMailReceiver' => 'PhabricatorApplicationMailReceiver',
'PonderQuestionEditController' => 'PonderController',
'PonderQuestionEditEngine' => 'PhabricatorEditEngine',
'PonderQuestionEditor' => 'PonderEditor',
'PonderQuestionFerretEngine' => 'PhabricatorFerretEngine',
'PonderQuestionFulltextEngine' => 'PhabricatorFulltextEngine',
'PonderQuestionHistoryController' => 'PonderController',
'PonderQuestionListController' => 'PonderController',
'PonderQuestionMailReceiver' => 'PhabricatorObjectMailReceiver',
'PonderQuestionPHIDType' => 'PhabricatorPHIDType',
'PonderQuestionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PonderQuestionReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PonderQuestionSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PonderQuestionStatus' => 'PonderConstants',
'PonderQuestionStatusController' => 'PonderController',
'PonderQuestionStatusTransaction' => 'PonderQuestionTransactionType',
'PonderQuestionTitleTransaction' => 'PonderQuestionTransactionType',
'PonderQuestionTransaction' => 'PhabricatorModularTransaction',
'PonderQuestionTransactionComment' => 'PhabricatorApplicationTransactionComment',
'PonderQuestionTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PonderQuestionTransactionType' => 'PhabricatorModularTransactionType',
'PonderQuestionViewController' => 'PonderController',
'PonderRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PonderSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'ProjectAddProjectsEmailCommand' => 'MetaMTAEmailTransactionCommand',
'ProjectBoardTaskCard' => 'Phobject',
'ProjectCanLockProjectsCapability' => 'PhabricatorPolicyCapability',
'ProjectColumnSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'ProjectConduitAPIMethod' => 'ConduitAPIMethod',
'ProjectCreateConduitAPIMethod' => 'ProjectConduitAPIMethod',
'ProjectCreateProjectsCapability' => 'PhabricatorPolicyCapability',
'ProjectDatasourceEngineExtension' => 'PhabricatorDatasourceEngineExtension',
'ProjectDefaultEditCapability' => 'PhabricatorPolicyCapability',
'ProjectDefaultJoinCapability' => 'PhabricatorPolicyCapability',
'ProjectDefaultViewCapability' => 'PhabricatorPolicyCapability',
'ProjectEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'ProjectQueryConduitAPIMethod' => 'ProjectConduitAPIMethod',
'ProjectRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'ProjectRemarkupRuleTestCase' => 'PhabricatorTestCase',
'ProjectReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'ProjectSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'QueryFormattingTestCase' => 'PhabricatorTestCase',
'ReleephAuthorFieldSpecification' => 'ReleephFieldSpecification',
'ReleephBranch' => array(
'ReleephDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
),
'ReleephBranchAccessController' => 'ReleephBranchController',
'ReleephBranchCommitFieldSpecification' => 'ReleephFieldSpecification',
'ReleephBranchController' => 'ReleephController',
'ReleephBranchCreateController' => 'ReleephProductController',
'ReleephBranchEditController' => 'ReleephBranchController',
'ReleephBranchEditor' => 'PhabricatorEditor',
'ReleephBranchHistoryController' => 'ReleephBranchController',
'ReleephBranchNamePreviewController' => 'ReleephController',
'ReleephBranchPHIDType' => 'PhabricatorPHIDType',
'ReleephBranchPreviewView' => 'AphrontFormControl',
'ReleephBranchQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'ReleephBranchSearchEngine' => 'PhabricatorApplicationSearchEngine',
'ReleephBranchTemplate' => 'Phobject',
'ReleephBranchTransaction' => 'PhabricatorApplicationTransaction',
'ReleephBranchTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'ReleephBranchViewController' => 'ReleephBranchController',
'ReleephCommitFinder' => 'Phobject',
'ReleephCommitFinderException' => 'Exception',
'ReleephCommitMessageFieldSpecification' => 'ReleephFieldSpecification',
'ReleephConduitAPIMethod' => 'ConduitAPIMethod',
'ReleephController' => 'PhabricatorController',
'ReleephDAO' => 'PhabricatorLiskDAO',
'ReleephDefaultFieldSelector' => 'ReleephFieldSelector',
'ReleephDependsOnFieldSpecification' => 'ReleephFieldSpecification',
'ReleephDiffChurnFieldSpecification' => 'ReleephFieldSpecification',
'ReleephDiffMessageFieldSpecification' => 'ReleephFieldSpecification',
'ReleephDiffSizeFieldSpecification' => 'ReleephFieldSpecification',
'ReleephFieldParseException' => 'Exception',
'ReleephFieldSelector' => 'Phobject',
'ReleephFieldSpecification' => array(
'PhabricatorCustomField',
'PhabricatorMarkupInterface',
),
'ReleephGetBranchesConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephIntentFieldSpecification' => 'ReleephFieldSpecification',
'ReleephLevelFieldSpecification' => 'ReleephFieldSpecification',
'ReleephOriginalCommitFieldSpecification' => 'ReleephFieldSpecification',
'ReleephProductActionController' => 'ReleephProductController',
'ReleephProductController' => 'ReleephController',
'ReleephProductCreateController' => 'ReleephProductController',
'ReleephProductEditController' => 'ReleephProductController',
'ReleephProductEditor' => 'PhabricatorApplicationTransactionEditor',
'ReleephProductHistoryController' => 'ReleephProductController',
'ReleephProductListController' => 'ReleephController',
'ReleephProductPHIDType' => 'PhabricatorPHIDType',
'ReleephProductQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'ReleephProductSearchEngine' => 'PhabricatorApplicationSearchEngine',
'ReleephProductTransaction' => 'PhabricatorApplicationTransaction',
'ReleephProductTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'ReleephProductViewController' => 'ReleephProductController',
'ReleephProject' => array(
'ReleephDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
),
'ReleephQueryBranchesConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephQueryProductsConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephQueryRequestsConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephReasonFieldSpecification' => 'ReleephFieldSpecification',
'ReleephRequest' => array(
'ReleephDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorCustomFieldInterface',
),
'ReleephRequestActionController' => 'ReleephRequestController',
'ReleephRequestCommentController' => 'ReleephRequestController',
'ReleephRequestConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephRequestController' => 'ReleephController',
'ReleephRequestDifferentialCreateController' => 'ReleephController',
'ReleephRequestEditController' => 'ReleephBranchController',
'ReleephRequestMailReceiver' => 'PhabricatorObjectMailReceiver',
'ReleephRequestPHIDType' => 'PhabricatorPHIDType',
'ReleephRequestQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'ReleephRequestReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'ReleephRequestSearchEngine' => 'PhabricatorApplicationSearchEngine',
'ReleephRequestStatus' => 'Phobject',
'ReleephRequestTransaction' => 'PhabricatorApplicationTransaction',
'ReleephRequestTransactionComment' => 'PhabricatorApplicationTransactionComment',
'ReleephRequestTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'ReleephRequestTransactionalEditor' => 'PhabricatorApplicationTransactionEditor',
'ReleephRequestTypeaheadControl' => 'AphrontFormControl',
'ReleephRequestTypeaheadController' => 'PhabricatorTypeaheadDatasourceController',
'ReleephRequestView' => 'AphrontView',
'ReleephRequestViewController' => 'ReleephBranchController',
'ReleephRequestorFieldSpecification' => 'ReleephFieldSpecification',
'ReleephRevisionFieldSpecification' => 'ReleephFieldSpecification',
'ReleephSeverityFieldSpecification' => 'ReleephLevelFieldSpecification',
'ReleephSummaryFieldSpecification' => 'ReleephFieldSpecification',
'ReleephWorkCanPushConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephWorkGetAuthorInfoConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephWorkGetBranchCommitMessageConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephWorkGetBranchConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephWorkGetCommitMessageConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephWorkNextRequestConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephWorkRecordConduitAPIMethod' => 'ReleephConduitAPIMethod',
'ReleephWorkRecordPickStatusConduitAPIMethod' => 'ReleephConduitAPIMethod',
'RemarkupProcessConduitAPIMethod' => 'ConduitAPIMethod',
'RepositoryConduitAPIMethod' => 'ConduitAPIMethod',
'RepositoryQueryConduitAPIMethod' => 'RepositoryConduitAPIMethod',
'ShellLogView' => 'AphrontView',
'SlowvoteConduitAPIMethod' => 'ConduitAPIMethod',
'SlowvoteEmbedView' => 'AphrontView',
'SlowvoteInfoConduitAPIMethod' => 'SlowvoteConduitAPIMethod',
'SlowvoteRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'SubscriptionListDialogBuilder' => 'Phobject',
'SubscriptionListStringBuilder' => 'Phobject',
'TokenConduitAPIMethod' => 'ConduitAPIMethod',
'TokenGiveConduitAPIMethod' => 'TokenConduitAPIMethod',
'TokenGivenConduitAPIMethod' => 'TokenConduitAPIMethod',
'TokenQueryConduitAPIMethod' => 'TokenConduitAPIMethod',
'TransactionSearchConduitAPIMethod' => 'ConduitAPIMethod',
'UserConduitAPIMethod' => 'ConduitAPIMethod',
'UserDisableConduitAPIMethod' => 'UserConduitAPIMethod',
'UserEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
'UserEnableConduitAPIMethod' => 'UserConduitAPIMethod',
'UserFindConduitAPIMethod' => 'UserConduitAPIMethod',
'UserQueryConduitAPIMethod' => 'UserConduitAPIMethod',
'UserSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'UserWhoAmIConduitAPIMethod' => 'UserConduitAPIMethod',
),
));
diff --git a/src/aphront/configuration/AphrontApplicationConfiguration.php b/src/aphront/configuration/AphrontApplicationConfiguration.php
index 60b12557c..8d36bbc88 100644
--- a/src/aphront/configuration/AphrontApplicationConfiguration.php
+++ b/src/aphront/configuration/AphrontApplicationConfiguration.php
@@ -1,711 +1,817 @@
<?php
/**
* @task routing URI Routing
* @task response Response Handling
* @task exception Exception Handling
*/
-abstract class AphrontApplicationConfiguration extends Phobject {
+final class AphrontApplicationConfiguration
+ extends Phobject {
private $request;
private $host;
private $path;
private $console;
- abstract public function buildRequest();
- abstract public function build404Controller();
- abstract public function buildRedirectController($uri, $external);
+ public function buildRequest() {
+ $parser = new PhutilQueryStringParser();
- final public function setRequest(AphrontRequest $request) {
+ $data = array();
+ $data += $_POST;
+ $data += $parser->parseQueryString(idx($_SERVER, 'QUERY_STRING', ''));
+
+ $cookie_prefix = PhabricatorEnv::getEnvConfig('phabricator.cookie-prefix');
+
+ $request = new AphrontRequest($this->getHost(), $this->getPath());
+ $request->setRequestData($data);
+ $request->setApplicationConfiguration($this);
+ $request->setCookiePrefix($cookie_prefix);
+
+ return $request;
+ }
+
+ public function build404Controller() {
+ return array(new Phabricator404Controller(), array());
+ }
+
+ public function buildRedirectController($uri, $external) {
+ return array(
+ new PhabricatorRedirectController(),
+ array(
+ 'uri' => $uri,
+ 'external' => $external,
+ ),
+ );
+ }
+
+ public function setRequest(AphrontRequest $request) {
$this->request = $request;
return $this;
}
- final public function getRequest() {
+ public function getRequest() {
return $this->request;
}
- final public function getConsole() {
+ public function getConsole() {
return $this->console;
}
- final public function setConsole($console) {
+ public function setConsole($console) {
$this->console = $console;
return $this;
}
- final public function setHost($host) {
+ public function setHost($host) {
$this->host = $host;
return $this;
}
- final public function getHost() {
+ public function getHost() {
return $this->host;
}
- final public function setPath($path) {
+ public function setPath($path) {
$this->path = $path;
return $this;
}
- final public function getPath() {
+ public function getPath() {
return $this->path;
}
- public function willBuildRequest() {}
-
/**
* @phutil-external-symbol class PhabricatorStartup
*/
public static function runHTTPRequest(AphrontHTTPSink $sink) {
if (isset($_SERVER['HTTP_X_PHABRICATOR_SELFCHECK'])) {
$response = self::newSelfCheckResponse();
return self::writeResponse($sink, $response);
}
PhabricatorStartup::beginStartupPhase('multimeter');
$multimeter = MultimeterControl::newInstance();
$multimeter->setEventContext('<http-init>');
$multimeter->setEventViewer('<none>');
// Build a no-op write guard for the setup phase. We'll replace this with a
// real write guard later on, but we need to survive setup and build a
// request object first.
$write_guard = new AphrontWriteGuard('id');
PhabricatorStartup::beginStartupPhase('preflight');
$response = PhabricatorSetupCheck::willPreflightRequest();
if ($response) {
return self::writeResponse($sink, $response);
}
PhabricatorStartup::beginStartupPhase('env.init');
+ self::readHTTPPOSTData();
+
try {
PhabricatorEnv::initializeWebEnvironment();
$database_exception = null;
} catch (PhabricatorClusterStrandedException $ex) {
$database_exception = $ex;
}
if ($database_exception) {
$issue = PhabricatorSetupIssue::newDatabaseConnectionIssue(
$database_exception,
true);
$response = PhabricatorSetupCheck::newIssueResponse($issue);
return self::writeResponse($sink, $response);
}
$multimeter->setSampleRate(
PhabricatorEnv::getEnvConfig('debug.sample-rate'));
$debug_time_limit = PhabricatorEnv::getEnvConfig('debug.time-limit');
if ($debug_time_limit) {
PhabricatorStartup::setDebugTimeLimit($debug_time_limit);
}
// This is the earliest we can get away with this, we need env config first.
PhabricatorStartup::beginStartupPhase('log.access');
PhabricatorAccessLog::init();
$access_log = PhabricatorAccessLog::getLog();
PhabricatorStartup::setAccessLog($access_log);
$address = PhabricatorEnv::getRemoteAddress();
if ($address) {
$address_string = $address->getAddress();
} else {
$address_string = '-';
}
$access_log->setData(
array(
'R' => AphrontRequest::getHTTPHeader('Referer', '-'),
'r' => $address_string,
'M' => idx($_SERVER, 'REQUEST_METHOD', '-'),
));
DarkConsoleXHProfPluginAPI::hookProfiler();
// We just activated the profiler, so we don't need to keep track of
// startup phases anymore: it can take over from here.
PhabricatorStartup::beginStartupPhase('startup.done');
DarkConsoleErrorLogPluginAPI::registerErrorHandler();
$response = PhabricatorSetupCheck::willProcessRequest();
if ($response) {
return self::writeResponse($sink, $response);
}
$host = AphrontRequest::getHTTPHeader('Host');
$path = $_REQUEST['__path__'];
- switch ($host) {
- default:
- $config_key = 'aphront.default-application-configuration-class';
- $application = PhabricatorEnv::newObjectFromConfig($config_key);
- break;
- }
+ $application = new self();
$application->setHost($host);
$application->setPath($path);
- $application->willBuildRequest();
$request = $application->buildRequest();
// Now that we have a request, convert the write guard into one which
// actually checks CSRF tokens.
$write_guard->dispose();
$write_guard = new AphrontWriteGuard(array($request, 'validateCSRF'));
// Build the server URI implied by the request headers. If an administrator
// has not configured "phabricator.base-uri" yet, we'll use this to generate
// links.
$request_protocol = ($request->isHTTPS() ? 'https' : 'http');
$request_base_uri = "{$request_protocol}://{$host}/";
PhabricatorEnv::setRequestBaseURI($request_base_uri);
$access_log->setData(
array(
'U' => (string)$request->getRequestURI()->getPath(),
));
$processing_exception = null;
try {
$response = $application->processRequest(
$request,
$access_log,
$sink,
$multimeter);
$response_code = $response->getHTTPResponseCode();
} catch (Exception $ex) {
$processing_exception = $ex;
$response_code = 500;
}
$write_guard->dispose();
$access_log->setData(
array(
'c' => $response_code,
'T' => PhabricatorStartup::getMicrosecondsSinceStart(),
));
$multimeter->newEvent(
MultimeterEvent::TYPE_REQUEST_TIME,
$multimeter->getEventContext(),
PhabricatorStartup::getMicrosecondsSinceStart());
$access_log->write();
$multimeter->saveEvents();
DarkConsoleXHProfPluginAPI::saveProfilerSample($access_log);
PhabricatorStartup::disconnectRateLimits(
array(
'viewer' => $request->getUser(),
));
if ($processing_exception) {
throw $processing_exception;
}
}
public function processRequest(
AphrontRequest $request,
PhutilDeferredLog $access_log,
AphrontHTTPSink $sink,
MultimeterControl $multimeter) {
$this->setRequest($request);
list($controller, $uri_data) = $this->buildController();
$controller_class = get_class($controller);
$access_log->setData(
array(
'C' => $controller_class,
));
$multimeter->setEventContext('web.'.$controller_class);
$request->setController($controller);
$request->setURIMap($uri_data);
$controller->setRequest($request);
// If execution throws an exception and then trying to render that
// exception throws another exception, we want to show the original
// exception, as it is likely the root cause of the rendering exception.
$original_exception = null;
try {
$response = $controller->willBeginExecution();
if ($request->getUser() && $request->getUser()->getPHID()) {
$access_log->setData(
array(
'u' => $request->getUser()->getUserName(),
'P' => $request->getUser()->getPHID(),
));
$multimeter->setEventViewer('user.'.$request->getUser()->getPHID());
}
if (!$response) {
$controller->willProcessRequest($uri_data);
$response = $controller->handleRequest($request);
$this->validateControllerResponse($controller, $response);
}
} catch (Exception $ex) {
$original_exception = $ex;
$response = $this->handleThrowable($ex);
} catch (Throwable $ex) {
$original_exception = $ex;
$response = $this->handleThrowable($ex);
}
try {
$response = $this->produceResponse($request, $response);
$response = $controller->willSendResponse($response);
$response->setRequest($request);
self::writeResponse($sink, $response);
} catch (Exception $ex) {
if ($original_exception) {
throw $original_exception;
}
throw $ex;
}
return $response;
}
private static function writeResponse(
AphrontHTTPSink $sink,
AphrontResponse $response) {
$unexpected_output = PhabricatorStartup::endOutputCapture();
if ($unexpected_output) {
$unexpected_output = pht(
"Unexpected output:\n\n%s",
$unexpected_output);
phlog($unexpected_output);
if ($response instanceof AphrontWebpageResponse) {
$response->setUnexpectedOutput($unexpected_output);
}
}
$sink->writeResponse($response);
}
/* -( URI Routing )-------------------------------------------------------- */
/**
* Build a controller to respond to the request.
*
* @return pair<AphrontController,dict> Controller and dictionary of request
* parameters.
* @task routing
*/
- final private function buildController() {
+ private function buildController() {
$request = $this->getRequest();
// If we're configured to operate in cluster mode, reject requests which
// were not received on a cluster interface.
//
// For example, a host may have an internal address like "170.0.0.1", and
// also have a public address like "51.23.95.16". Assuming the cluster
// is configured on a range like "170.0.0.0/16", we want to reject the
// requests received on the public interface.
//
// Ideally, nodes in a cluster should only be listening on internal
// interfaces, but they may be configured in such a way that they also
// listen on external interfaces, since this is easy to forget about or
// get wrong. As a broad security measure, reject requests received on any
// interfaces which aren't on the whitelist.
$cluster_addresses = PhabricatorEnv::getEnvConfig('cluster.addresses');
if ($cluster_addresses) {
$server_addr = idx($_SERVER, 'SERVER_ADDR');
if (!$server_addr) {
if (php_sapi_name() == 'cli') {
// This is a command line script (probably something like a unit
// test) so it's fine that we don't have SERVER_ADDR defined.
} else {
throw new AphrontMalformedRequestException(
pht('No %s', 'SERVER_ADDR'),
pht(
'Phabricator is configured to operate in cluster mode, but '.
'%s is not defined in the request context. Your webserver '.
'configuration needs to forward %s to PHP so Phabricator can '.
'reject requests received on external interfaces.',
'SERVER_ADDR',
'SERVER_ADDR'));
}
} else {
if (!PhabricatorEnv::isClusterAddress($server_addr)) {
throw new AphrontMalformedRequestException(
pht('External Interface'),
pht(
'Phabricator is configured in cluster mode and the address '.
'this request was received on ("%s") is not whitelisted as '.
'a cluster address.',
$server_addr));
}
}
}
$site = $this->buildSiteForRequest($request);
if ($site->shouldRequireHTTPS()) {
if (!$request->isHTTPS()) {
// Don't redirect intracluster requests: doing so drops headers and
// parameters, imposes a performance penalty, and indicates a
// misconfiguration.
if ($request->isProxiedClusterRequest()) {
throw new AphrontMalformedRequestException(
pht('HTTPS Required'),
pht(
'This request reached a site which requires HTTPS, but the '.
'request is not marked as HTTPS.'));
}
$https_uri = $request->getRequestURI();
$https_uri->setDomain($request->getHost());
$https_uri->setProtocol('https');
// In this scenario, we'll be redirecting to HTTPS using an absolute
// URI, so we need to permit an external redirect.
return $this->buildRedirectController($https_uri, true);
}
}
$maps = $site->getRoutingMaps();
$path = $request->getPath();
$result = $this->routePath($maps, $path);
if ($result) {
return $result;
}
// If we failed to match anything but don't have a trailing slash, try
// to add a trailing slash and issue a redirect if that resolves.
// NOTE: We only do this for GET, since redirects switch to GET and drop
// data like POST parameters.
if (!preg_match('@/$@', $path) && $request->isHTTPGet()) {
$result = $this->routePath($maps, $path.'/');
if ($result) {
$target_uri = $request->getAbsoluteRequestURI();
// We need to restore URI encoding because the webserver has
// interpreted it. For example, this allows us to redirect a path
// like `/tag/aa%20bb` to `/tag/aa%20bb/`, which may eventually be
// resolved meaningfully by an application.
$target_path = phutil_escape_uri($path.'/');
$target_uri->setPath($target_path);
$target_uri = (string)$target_uri;
return $this->buildRedirectController($target_uri, true);
}
}
$result = $site->new404Controller($request);
if ($result) {
return array($result, array());
}
return $this->build404Controller();
}
/**
* Map a specific path to the corresponding controller. For a description
* of routing, see @{method:buildController}.
*
* @param list<AphrontRoutingMap> List of routing maps.
* @param string Path to route.
* @return pair<AphrontController,dict> Controller and dictionary of request
* parameters.
* @task routing
*/
private function routePath(array $maps, $path) {
foreach ($maps as $map) {
$result = $map->routePath($path);
if ($result) {
return array($result->getController(), $result->getURIData());
}
}
}
private function buildSiteForRequest(AphrontRequest $request) {
$sites = PhabricatorSite::getAllSites();
$site = null;
foreach ($sites as $candidate) {
$site = $candidate->newSiteForRequest($request);
if ($site) {
break;
}
}
if (!$site) {
$path = $request->getPath();
$host = $request->getHost();
throw new AphrontMalformedRequestException(
pht('Site Not Found'),
pht(
'This request asked for "%s" on host "%s", but no site is '.
'configured which can serve this request.',
$path,
$host),
true);
}
$request->setSite($site);
return $site;
}
/* -( Response Handling )-------------------------------------------------- */
/**
* Tests if a response is of a valid type.
*
* @param wild Supposedly valid response.
* @return bool True if the object is of a valid type.
* @task response
*/
private function isValidResponseObject($response) {
if ($response instanceof AphrontResponse) {
return true;
}
if ($response instanceof AphrontResponseProducerInterface) {
return true;
}
return false;
}
/**
* Verifies that the return value from an @{class:AphrontController} is
* of an allowed type.
*
* @param AphrontController Controller which returned the response.
* @param wild Supposedly valid response.
* @return void
* @task response
*/
private function validateControllerResponse(
AphrontController $controller,
$response) {
if ($this->isValidResponseObject($response)) {
return;
}
throw new Exception(
pht(
'Controller "%s" returned an invalid response from call to "%s". '.
'This method must return an object of class "%s", or an object '.
'which implements the "%s" interface.',
get_class($controller),
'handleRequest()',
'AphrontResponse',
'AphrontResponseProducerInterface'));
}
/**
* Verifies that the return value from an
* @{class:AphrontResponseProducerInterface} is of an allowed type.
*
* @param AphrontResponseProducerInterface Object which produced
* this response.
* @param wild Supposedly valid response.
* @return void
* @task response
*/
private function validateProducerResponse(
AphrontResponseProducerInterface $producer,
$response) {
if ($this->isValidResponseObject($response)) {
return;
}
throw new Exception(
pht(
'Producer "%s" returned an invalid response from call to "%s". '.
'This method must return an object of class "%s", or an object '.
'which implements the "%s" interface.',
get_class($producer),
'produceAphrontResponse()',
'AphrontResponse',
'AphrontResponseProducerInterface'));
}
/**
* Verifies that the return value from an
* @{class:AphrontRequestExceptionHandler} is of an allowed type.
*
* @param AphrontRequestExceptionHandler Object which produced this
* response.
* @param wild Supposedly valid response.
* @return void
* @task response
*/
private function validateErrorHandlerResponse(
AphrontRequestExceptionHandler $handler,
$response) {
if ($this->isValidResponseObject($response)) {
return;
}
throw new Exception(
pht(
'Exception handler "%s" returned an invalid response from call to '.
'"%s". This method must return an object of class "%s", or an object '.
'which implements the "%s" interface.',
get_class($handler),
'handleRequestException()',
'AphrontResponse',
'AphrontResponseProducerInterface'));
}
/**
* Resolves a response object into an @{class:AphrontResponse}.
*
* Controllers are permitted to return actual responses of class
* @{class:AphrontResponse}, or other objects which implement
* @{interface:AphrontResponseProducerInterface} and can produce a response.
*
* If a controller returns a response producer, invoke it now and produce
* the real response.
*
* @param AphrontRequest Request being handled.
* @param AphrontResponse|AphrontResponseProducerInterface Response, or
* response producer.
* @return AphrontResponse Response after any required production.
* @task response
*/
private function produceResponse(AphrontRequest $request, $response) {
$original = $response;
// Detect cycles on the exact same objects. It's still possible to produce
// infinite responses as long as they're all unique, but we can only
// reasonably detect cycles, not guarantee that response production halts.
$seen = array();
while (true) {
// NOTE: It is permissible for an object to be both a response and a
// response producer. If so, being a producer is "stronger". This is
// used by AphrontProxyResponse.
// If this response is a valid response, hand over the request first.
if ($response instanceof AphrontResponse) {
$response->setRequest($request);
}
// If this isn't a producer, we're all done.
if (!($response instanceof AphrontResponseProducerInterface)) {
break;
}
$hash = spl_object_hash($response);
if (isset($seen[$hash])) {
throw new Exception(
pht(
'Failure while producing response for object of class "%s": '.
'encountered production cycle (identical object, of class "%s", '.
'was produced twice).',
get_class($original),
get_class($response)));
}
$seen[$hash] = true;
$new_response = $response->produceAphrontResponse();
$this->validateProducerResponse($response, $new_response);
$response = $new_response;
}
return $response;
}
/* -( Error Handling )----------------------------------------------------- */
/**
* Convert an exception which has escaped the controller into a response.
*
* This method delegates exception handling to available subclasses of
* @{class:AphrontRequestExceptionHandler}.
*
* @param Throwable Exception which needs to be handled.
* @return wild Response or response producer, or null if no available
* handler can produce a response.
* @task exception
*/
private function handleThrowable($throwable) {
$handlers = AphrontRequestExceptionHandler::getAllHandlers();
$request = $this->getRequest();
foreach ($handlers as $handler) {
if ($handler->canHandleRequestThrowable($request, $throwable)) {
$response = $handler->handleRequestThrowable($request, $throwable);
$this->validateErrorHandlerResponse($handler, $response);
return $response;
}
}
throw $throwable;
}
private static function newSelfCheckResponse() {
$path = idx($_REQUEST, '__path__', '');
$query = idx($_SERVER, 'QUERY_STRING', '');
$pairs = id(new PhutilQueryStringParser())
->parseQueryStringToPairList($query);
$params = array();
foreach ($pairs as $v) {
$params[] = array(
'name' => $v[0],
'value' => $v[1],
);
}
$result = array(
'path' => $path,
'params' => $params,
'user' => idx($_SERVER, 'PHP_AUTH_USER'),
'pass' => idx($_SERVER, 'PHP_AUTH_PW'),
// This just makes sure that the response compresses well, so reasonable
// algorithms should want to gzip or deflate it.
'filler' => str_repeat('Q', 1024 * 16),
);
return id(new AphrontJSONResponse())
->setAddJSONShield(false)
->setContent($result);
}
+ private static function readHTTPPOSTData() {
+ $request_method = idx($_SERVER, 'REQUEST_METHOD');
+ if ($request_method === 'PUT') {
+ // For PUT requests, do nothing: in particular, do NOT read input. This
+ // allows us to stream input later and process very large PUT requests,
+ // like those coming from Git LFS.
+ return;
+ }
+
+
+ // For POST requests, we're going to read the raw input ourselves here
+ // if we can. Among other things, this corrects variable names with
+ // the "." character in them, which PHP normally converts into "_".
+
+ // There are two major considerations here: whether the
+ // `enable_post_data_reading` option is set, and whether the content
+ // type is "multipart/form-data" or not.
+
+ // If `enable_post_data_reading` is off, we're free to read the entire
+ // raw request body and parse it -- and we must, because $_POST and
+ // $_FILES are not built for us. If `enable_post_data_reading` is on,
+ // which is the default, we may not be able to read the body (the
+ // documentation says we can't, but empirically we can at least some
+ // of the time).
+
+ // If the content type is "multipart/form-data", we need to build both
+ // $_POST and $_FILES, which is involved. The body itself is also more
+ // difficult to parse than other requests.
+ $raw_input = PhabricatorStartup::getRawInput();
+ $parser = new PhutilQueryStringParser();
+
+ if (strlen($raw_input)) {
+ $content_type = idx($_SERVER, 'CONTENT_TYPE');
+ $is_multipart = preg_match('@^multipart/form-data@i', $content_type);
+ if ($is_multipart && !ini_get('enable_post_data_reading')) {
+ $multipart_parser = id(new AphrontMultipartParser())
+ ->setContentType($content_type);
+
+ $multipart_parser->beginParse();
+ $multipart_parser->continueParse($raw_input);
+ $parts = $multipart_parser->endParse();
+
+ // We're building and then parsing a query string so that requests
+ // with arrays (like "x[]=apple&x[]=banana") work correctly. This also
+ // means we can't use "phutil_build_http_querystring()", since it
+ // can't build a query string with duplicate names.
+
+ $query_string = array();
+ foreach ($parts as $part) {
+ if (!$part->isVariable()) {
+ continue;
+ }
+
+ $name = $part->getName();
+ $value = $part->getVariableValue();
+ $query_string[] = rawurlencode($name).'='.rawurlencode($value);
+ }
+ $query_string = implode('&', $query_string);
+ $post = $parser->parseQueryString($query_string);
+
+ $files = array();
+ foreach ($parts as $part) {
+ if ($part->isVariable()) {
+ continue;
+ }
+
+ $files[$part->getName()] = $part->getPHPFileDictionary();
+ }
+ $_FILES = $files;
+ } else {
+ $post = $parser->parseQueryString($raw_input);
+ }
+
+ $_POST = $post;
+ PhabricatorStartup::rebuildRequest();
+ } else if ($_POST) {
+ $post = filter_input_array(INPUT_POST, FILTER_UNSAFE_RAW);
+ if (is_array($post)) {
+ $_POST = $post;
+ PhabricatorStartup::rebuildRequest();
+ }
+ }
+ }
+
}
diff --git a/src/aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php b/src/aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php
index f8d522711..7f4eddad4 100644
--- a/src/aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php
+++ b/src/aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php
@@ -1,76 +1,126 @@
<?php
final class PhabricatorHighSecurityRequestExceptionHandler
extends PhabricatorRequestExceptionHandler {
public function getRequestExceptionHandlerPriority() {
return 310000;
}
public function getRequestExceptionHandlerDescription() {
return pht(
'Handles high security exceptions which occur when a user needs '.
'to present MFA credentials to take an action.');
}
public function canHandleRequestThrowable(
AphrontRequest $request,
$throwable) {
if (!$this->isPhabricatorSite($request)) {
return false;
}
return ($throwable instanceof PhabricatorAuthHighSecurityRequiredException);
}
public function handleRequestThrowable(
AphrontRequest $request,
$throwable) {
$viewer = $this->getViewer($request);
+ $results = $throwable->getFactorValidationResults();
$form = id(new PhabricatorAuthSessionEngine())->renderHighSecurityForm(
$throwable->getFactors(),
- $throwable->getFactorValidationResults(),
+ $results,
$viewer,
$request);
+ $is_wait = false;
+ $is_continue = false;
+ foreach ($results as $result) {
+ if ($result->getIsWait()) {
+ $is_wait = true;
+ }
+
+ if ($result->getIsContinue()) {
+ $is_continue = true;
+ }
+ }
+
+ $is_upgrade = $throwable->getIsSessionUpgrade();
+
+ if ($is_upgrade) {
+ $title = pht('Enter High Security');
+ } else {
+ $title = pht('Provide MFA Credentials');
+ }
+
+ if ($is_wait) {
+ $submit = pht('Wait Patiently');
+ } else if ($is_upgrade && !$is_continue) {
+ $submit = pht('Enter High Security');
+ } else {
+ $submit = pht('Continue');
+ }
+
$dialog = id(new AphrontDialogView())
->setUser($viewer)
- ->setTitle(pht('Entering High Security'))
+ ->setTitle($title)
->setShortTitle(pht('Security Checkpoint'))
->setWidth(AphrontDialogView::WIDTH_FORM)
->addHiddenInput(AphrontRequest::TYPE_HISEC, true)
- ->setErrors(
- array(
- pht(
- 'You are taking an action which requires you to enter '.
- 'high security.'),
- ))
- ->appendParagraph(
- pht(
- 'High security mode helps protect your account from security '.
- 'threats, like session theft or someone messing with your stuff '.
- 'while you\'re grabbing a coffee. To enter high security mode, '.
- 'confirm your credentials.'))
- ->appendChild($form->buildLayoutView())
- ->appendParagraph(
- pht(
- 'Your account will remain in high security mode for a short '.
- 'period of time. When you are finished taking sensitive '.
- 'actions, you should leave high security.'))
->setSubmitURI($request->getPath())
->addCancelButton($throwable->getCancelURI())
- ->addSubmitButton(pht('Enter High Security'));
+ ->addSubmitButton($submit);
+
+ $form_layout = $form->buildLayoutView();
+
+ if ($is_upgrade) {
+ $message = pht(
+ 'You are taking an action which requires you to enter '.
+ 'high security.');
+
+ $info_view = id(new PHUIInfoView())
+ ->setSeverity(PHUIInfoView::SEVERITY_MFA)
+ ->setErrors(array($message));
+
+ $dialog
+ ->appendChild($info_view)
+ ->appendParagraph(
+ pht(
+ 'To enter high security mode, confirm your credentials:'))
+ ->appendChild($form_layout)
+ ->appendParagraph(
+ pht(
+ 'Your account will remain in high security mode for a short '.
+ 'period of time. When you are finished taking sensitive '.
+ 'actions, you should leave high security.'));
+ } else {
+ $message = pht(
+ 'You are taking an action which requires you to provide '.
+ 'multi-factor credentials.');
+
+ $info_view = id(new PHUIInfoView())
+ ->setSeverity(PHUIInfoView::SEVERITY_MFA)
+ ->setErrors(array($message));
+
+ $dialog
+ ->appendChild($info_view)
+ ->setErrors(
+ array(
+ ))
+ ->appendChild($form_layout);
+ }
$request_parameters = $request->getPassthroughRequestParameters(
$respect_quicksand = true);
foreach ($request_parameters as $key => $value) {
$dialog->addHiddenInput($key, $value);
}
return $dialog;
}
}
diff --git a/src/applications/almanac/engineextension/AlmanacCacheEngineExtension.php b/src/applications/almanac/engineextension/AlmanacCacheEngineExtension.php
index 20c6bbcd7..d00926232 100644
--- a/src/applications/almanac/engineextension/AlmanacCacheEngineExtension.php
+++ b/src/applications/almanac/engineextension/AlmanacCacheEngineExtension.php
@@ -1,53 +1,61 @@
<?php
final class AlmanacCacheEngineExtension
extends PhabricatorCacheEngineExtension {
const EXTENSIONKEY = 'almanac';
public function getExtensionName() {
return pht('Almanac Core Objects');
}
public function discoverLinkedObjects(
PhabricatorCacheEngine $engine,
array $objects) {
$viewer = $engine->getViewer();
$results = array();
foreach ($this->selectObjects($objects, 'AlmanacBinding') as $object) {
$results[] = $object->getServicePHID();
$results[] = $object->getDevicePHID();
$results[] = $object->getInterfacePHID();
}
$devices = $this->selectObjects($objects, 'AlmanacDevice');
if ($devices) {
$interfaces = id(new AlmanacInterfaceQuery())
->setViewer($viewer)
->withDevicePHIDs(mpull($devices, 'getPHID'))
->execute();
foreach ($interfaces as $interface) {
$results[] = $interface;
}
+
+ $bindings = id(new AlmanacBindingQuery())
+ ->setViewer($viewer)
+ ->withDevicePHIDs(mpull($devices, 'getPHID'))
+ ->execute();
+ foreach ($bindings as $binding) {
+ $results[] = $binding;
+ }
}
foreach ($this->selectObjects($objects, 'AlmanacInterface') as $iface) {
$results[] = $iface->getDevicePHID();
$results[] = $iface->getNetworkPHID();
}
foreach ($this->selectObjects($objects, 'AlmanacProperty') as $object) {
$results[] = $object->getObjectPHID();
}
return $results;
}
public function deleteCaches(
PhabricatorCacheEngine $engine,
array $objects) {
return;
}
}
diff --git a/src/applications/almanac/storage/AlmanacBinding.php b/src/applications/almanac/storage/AlmanacBinding.php
index c593e40fa..a7096fc51 100644
--- a/src/applications/almanac/storage/AlmanacBinding.php
+++ b/src/applications/almanac/storage/AlmanacBinding.php
@@ -1,274 +1,264 @@
<?php
final class AlmanacBinding
extends AlmanacDAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface,
AlmanacPropertyInterface,
PhabricatorDestructibleInterface,
PhabricatorExtendedPolicyInterface,
PhabricatorConduitResultInterface {
protected $servicePHID;
protected $devicePHID;
protected $interfacePHID;
protected $mailKey;
protected $isDisabled;
private $service = self::ATTACHABLE;
private $device = self::ATTACHABLE;
private $interface = self::ATTACHABLE;
private $almanacProperties = self::ATTACHABLE;
public static function initializeNewBinding(AlmanacService $service) {
return id(new AlmanacBinding())
->setServicePHID($service->getPHID())
->attachService($service)
->attachAlmanacProperties(array())
->setIsDisabled(0);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'mailKey' => 'bytes20',
'isDisabled' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_service' => array(
'columns' => array('servicePHID', 'interfacePHID'),
'unique' => true,
),
'key_device' => array(
'columns' => array('devicePHID'),
),
'key_interface' => array(
'columns' => array('interfacePHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(AlmanacBindingPHIDType::TYPECONST);
}
public function save() {
if (!$this->mailKey) {
$this->mailKey = Filesystem::readRandomCharacters(20);
}
return parent::save();
}
public function getName() {
return pht('Binding %s', $this->getID());
}
public function getURI() {
return '/almanac/binding/'.$this->getID().'/';
}
public function getService() {
return $this->assertAttached($this->service);
}
public function attachService(AlmanacService $service) {
$this->service = $service;
return $this;
}
public function getDevice() {
return $this->assertAttached($this->device);
}
public function attachDevice(AlmanacDevice $device) {
$this->device = $device;
return $this;
}
public function hasInterface() {
return ($this->interface !== self::ATTACHABLE);
}
public function getInterface() {
return $this->assertAttached($this->interface);
}
public function attachInterface(AlmanacInterface $interface) {
$this->interface = $interface;
return $this;
}
/* -( AlmanacPropertyInterface )------------------------------------------- */
public function attachAlmanacProperties(array $properties) {
assert_instances_of($properties, 'AlmanacProperty');
$this->almanacProperties = mpull($properties, null, 'getFieldName');
return $this;
}
public function getAlmanacProperties() {
return $this->assertAttached($this->almanacProperties);
}
public function hasAlmanacProperty($key) {
$this->assertAttached($this->almanacProperties);
return isset($this->almanacProperties[$key]);
}
public function getAlmanacProperty($key) {
return $this->assertAttachedKey($this->almanacProperties, $key);
}
public function getAlmanacPropertyValue($key, $default = null) {
if ($this->hasAlmanacProperty($key)) {
return $this->getAlmanacProperty($key)->getFieldValue();
} else {
return $default;
}
}
public function getAlmanacPropertyFieldSpecifications() {
return $this->getService()->getBindingFieldSpecifications($this);
}
public function newAlmanacPropertyEditEngine() {
return new AlmanacBindingPropertyEditEngine();
}
public function getAlmanacPropertySetTransactionType() {
return AlmanacBindingSetPropertyTransaction::TRANSACTIONTYPE;
}
public function getAlmanacPropertyDeleteTransactionType() {
return AlmanacBindingDeletePropertyTransaction::TRANSACTIONTYPE;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return $this->getService()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getService()->hasAutomaticCapability($capability, $viewer);
}
public function describeAutomaticCapability($capability) {
$notes = array(
pht('A binding inherits the policies of its service.'),
pht(
'To view a binding, you must also be able to view its device and '.
'interface.'),
);
return $notes;
}
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_EDIT:
if ($this->getService()->isClusterService()) {
return array(
array(
new PhabricatorAlmanacApplication(),
AlmanacManageClusterServicesCapability::CAPABILITY,
),
);
}
break;
}
return array();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new AlmanacBindingEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new AlmanacBindingTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->delete();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('servicePHID')
->setType('phid')
->setDescription(pht('The bound service.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('devicePHID')
->setType('phid')
->setDescription(pht('The device the service is bound to.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('interfacePHID')
->setType('phid')
->setDescription(pht('The interface the service is bound to.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('disabled')
->setType('bool')
->setDescription(pht('Interface status.')),
);
}
public function getFieldValuesForConduit() {
return array(
'servicePHID' => $this->getServicePHID(),
'devicePHID' => $this->getDevicePHID(),
'interfacePHID' => $this->getInterfacePHID(),
'disabled' => (bool)$this->getIsDisabled(),
);
}
public function getConduitSearchAttachments() {
return array(
id(new AlmanacPropertiesSearchEngineAttachment())
->setAttachmentKey('properties'),
);
}
}
diff --git a/src/applications/almanac/storage/AlmanacDevice.php b/src/applications/almanac/storage/AlmanacDevice.php
index a1ebdfffa..1d1010733 100644
--- a/src/applications/almanac/storage/AlmanacDevice.php
+++ b/src/applications/almanac/storage/AlmanacDevice.php
@@ -1,297 +1,286 @@
<?php
final class AlmanacDevice
extends AlmanacDAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorProjectInterface,
PhabricatorSSHPublicKeyInterface,
AlmanacPropertyInterface,
PhabricatorDestructibleInterface,
PhabricatorNgramsInterface,
PhabricatorConduitResultInterface,
PhabricatorExtendedPolicyInterface {
protected $name;
protected $nameIndex;
protected $mailKey;
protected $viewPolicy;
protected $editPolicy;
protected $isBoundToClusterService;
private $almanacProperties = self::ATTACHABLE;
public static function initializeNewDevice() {
return id(new AlmanacDevice())
->setViewPolicy(PhabricatorPolicies::POLICY_USER)
->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN)
->attachAlmanacProperties(array())
->setIsBoundToClusterService(0);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text128',
'nameIndex' => 'bytes12',
'mailKey' => 'bytes20',
'isBoundToClusterService' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_name' => array(
'columns' => array('nameIndex'),
'unique' => true,
),
'key_nametext' => array(
'columns' => array('name'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(AlmanacDevicePHIDType::TYPECONST);
}
public function save() {
AlmanacNames::validateName($this->getName());
$this->nameIndex = PhabricatorHash::digestForIndex($this->getName());
if (!$this->mailKey) {
$this->mailKey = Filesystem::readRandomCharacters(20);
}
return parent::save();
}
public function getURI() {
return '/almanac/device/view/'.$this->getName().'/';
}
public function rebuildClusterBindingStatus() {
$services = id(new AlmanacServiceQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withDevicePHIDs(array($this->getPHID()))
->execute();
$is_cluster = false;
foreach ($services as $service) {
if ($service->isClusterService()) {
$is_cluster = true;
break;
}
}
if ($is_cluster != $this->getIsBoundToClusterService()) {
$this->setIsBoundToClusterService((int)$is_cluster);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
queryfx(
$this->establishConnection('w'),
'UPDATE %T SET isBoundToClusterService = %d WHERE id = %d',
$this->getTableName(),
$this->getIsBoundToClusterService(),
$this->getID());
unset($unguarded);
}
return $this;
}
public function isClusterDevice() {
return $this->getIsBoundToClusterService();
}
/* -( AlmanacPropertyInterface )------------------------------------------- */
public function attachAlmanacProperties(array $properties) {
assert_instances_of($properties, 'AlmanacProperty');
$this->almanacProperties = mpull($properties, null, 'getFieldName');
return $this;
}
public function getAlmanacProperties() {
return $this->assertAttached($this->almanacProperties);
}
public function hasAlmanacProperty($key) {
$this->assertAttached($this->almanacProperties);
return isset($this->almanacProperties[$key]);
}
public function getAlmanacProperty($key) {
return $this->assertAttachedKey($this->almanacProperties, $key);
}
public function getAlmanacPropertyValue($key, $default = null) {
if ($this->hasAlmanacProperty($key)) {
return $this->getAlmanacProperty($key)->getFieldValue();
} else {
return $default;
}
}
public function getAlmanacPropertyFieldSpecifications() {
return array();
}
public function newAlmanacPropertyEditEngine() {
return new AlmanacDevicePropertyEditEngine();
}
public function getAlmanacPropertySetTransactionType() {
return AlmanacDeviceSetPropertyTransaction::TRANSACTIONTYPE;
}
public function getAlmanacPropertyDeleteTransactionType() {
return AlmanacDeviceDeletePropertyTransaction::TRANSACTIONTYPE;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
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 $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_EDIT:
if ($this->isClusterDevice()) {
return array(
array(
new PhabricatorAlmanacApplication(),
AlmanacManageClusterServicesCapability::CAPABILITY,
),
);
}
break;
}
return array();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new AlmanacDeviceEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new AlmanacDeviceTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorSSHPublicKeyInterface )----------------------------------- */
public function getSSHPublicKeyManagementURI(PhabricatorUser $viewer) {
return $this->getURI();
}
public function getSSHKeyDefaultName() {
return $this->getName();
}
public function getSSHKeyNotifyPHIDs() {
// Devices don't currently have anyone useful to notify about SSH key
// edits, and they're usually a difficult vector to attack since you need
// access to a cluster host. However, it would be nice to make them
// subscribable at some point.
return array();
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$interfaces = id(new AlmanacInterfaceQuery())
->setViewer($engine->getViewer())
->withDevicePHIDs(array($this->getPHID()))
->execute();
foreach ($interfaces as $interface) {
$engine->destroyObject($interface);
}
$this->delete();
}
/* -( PhabricatorNgramsInterface )----------------------------------------- */
public function newNgrams() {
return array(
id(new AlmanacDeviceNameNgrams())
->setValue($this->getName()),
);
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of the device.')),
);
}
public function getFieldValuesForConduit() {
return array(
'name' => $this->getName(),
);
}
public function getConduitSearchAttachments() {
return array(
id(new AlmanacPropertiesSearchEngineAttachment())
->setAttachmentKey('properties'),
);
}
}
diff --git a/src/applications/almanac/storage/AlmanacInterface.php b/src/applications/almanac/storage/AlmanacInterface.php
index 5c7f65ddd..6cd318186 100644
--- a/src/applications/almanac/storage/AlmanacInterface.php
+++ b/src/applications/almanac/storage/AlmanacInterface.php
@@ -1,223 +1,213 @@
<?php
final class AlmanacInterface
extends AlmanacDAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface,
PhabricatorExtendedPolicyInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorConduitResultInterface {
protected $devicePHID;
protected $networkPHID;
protected $address;
protected $port;
private $device = self::ATTACHABLE;
private $network = self::ATTACHABLE;
public static function initializeNewInterface() {
return id(new AlmanacInterface());
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'address' => 'text64',
'port' => 'uint32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_location' => array(
'columns' => array('networkPHID', 'address', 'port'),
),
'key_device' => array(
'columns' => array('devicePHID'),
),
'key_unique' => array(
'columns' => array('devicePHID', 'networkPHID', 'address', 'port'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
AlmanacInterfacePHIDType::TYPECONST);
}
public function getDevice() {
return $this->assertAttached($this->device);
}
public function attachDevice(AlmanacDevice $device) {
$this->device = $device;
return $this;
}
public function getNetwork() {
return $this->assertAttached($this->network);
}
public function attachNetwork(AlmanacNetwork $network) {
$this->network = $network;
return $this;
}
public function toAddress() {
return AlmanacAddress::newFromParts(
$this->getNetworkPHID(),
$this->getAddress(),
$this->getPort());
}
public function getAddressHash() {
return $this->toAddress()->toHash();
}
public function renderDisplayAddress() {
return $this->getAddress().':'.$this->getPort();
}
public function loadIsInUse() {
$binding = id(new AlmanacBindingQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withInterfacePHIDs(array($this->getPHID()))
->setLimit(1)
->executeOne();
return (bool)$binding;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return $this->getDevice()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getDevice()->hasAutomaticCapability($capability, $viewer);
}
public function describeAutomaticCapability($capability) {
$notes = array(
pht('An interface inherits the policies of the device it belongs to.'),
pht(
'You must be able to view the network an interface resides on to '.
'view the interface.'),
);
return $notes;
}
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_EDIT:
if ($this->getDevice()->isClusterDevice()) {
return array(
array(
new PhabricatorAlmanacApplication(),
AlmanacManageClusterServicesCapability::CAPABILITY,
),
);
}
break;
}
return array();
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$bindings = id(new AlmanacBindingQuery())
->setViewer($engine->getViewer())
->withInterfacePHIDs(array($this->getPHID()))
->execute();
foreach ($bindings as $binding) {
$engine->destroyObject($binding);
}
$this->delete();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new AlmanacInterfaceEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new AlmanacInterfaceTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
- return $timeline;
- }
-
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('devicePHID')
->setType('phid')
->setDescription(pht('The device the interface is on.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('networkPHID')
->setType('phid')
->setDescription(pht('The network the interface is part of.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('address')
->setType('string')
->setDescription(pht('The address of the interface.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('port')
->setType('int')
->setDescription(pht('The port number of the interface.')),
);
}
public function getFieldValuesForConduit() {
return array(
'devicePHID' => $this->getDevicePHID(),
'networkPHID' => $this->getNetworkPHID(),
'address' => (string)$this->getAddress(),
'port' => (int)$this->getPort(),
);
}
public function getConduitSearchAttachments() {
return array();
}
}
diff --git a/src/applications/almanac/storage/AlmanacNamespace.php b/src/applications/almanac/storage/AlmanacNamespace.php
index 238a7b628..128cfdb72 100644
--- a/src/applications/almanac/storage/AlmanacNamespace.php
+++ b/src/applications/almanac/storage/AlmanacNamespace.php
@@ -1,252 +1,242 @@
<?php
final class AlmanacNamespace
extends AlmanacDAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorProjectInterface,
AlmanacPropertyInterface,
PhabricatorDestructibleInterface,
PhabricatorNgramsInterface,
PhabricatorConduitResultInterface {
protected $name;
protected $nameIndex;
protected $mailKey;
protected $viewPolicy;
protected $editPolicy;
private $almanacProperties = self::ATTACHABLE;
public static function initializeNewNamespace() {
return id(new self())
->setViewPolicy(PhabricatorPolicies::POLICY_USER)
->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN)
->attachAlmanacProperties(array());
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text128',
'nameIndex' => 'bytes12',
'mailKey' => 'bytes20',
),
self::CONFIG_KEY_SCHEMA => array(
'key_nameindex' => array(
'columns' => array('nameIndex'),
'unique' => true,
),
'key_name' => array(
'columns' => array('name'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
AlmanacNamespacePHIDType::TYPECONST);
}
public function save() {
AlmanacNames::validateName($this->getName());
$this->nameIndex = PhabricatorHash::digestForIndex($this->getName());
if (!$this->mailKey) {
$this->mailKey = Filesystem::readRandomCharacters(20);
}
return parent::save();
}
public function getURI() {
return '/almanac/namespace/view/'.$this->getName().'/';
}
public function getNameLength() {
return strlen($this->getName());
}
/**
* Load the namespace which prevents use of an Almanac name, if one exists.
*/
public static function loadRestrictedNamespace(
PhabricatorUser $viewer,
$name) {
// For a name like "x.y.z", produce a list of controlling namespaces like
// ("z", "y.x", "x.y.z").
$names = array();
$parts = explode('.', $name);
for ($ii = 0; $ii < count($parts); $ii++) {
$names[] = implode('.', array_slice($parts, -($ii + 1)));
}
// Load all the possible controlling namespaces.
$namespaces = id(new AlmanacNamespaceQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withNames($names)
->execute();
if (!$namespaces) {
return null;
}
// Find the "nearest" (longest) namespace that exists. If both
// "sub.domain.com" and "domain.com" exist, we only care about the policy
// on the former.
$namespaces = msort($namespaces, 'getNameLength');
$namespace = last($namespaces);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$namespace,
PhabricatorPolicyCapability::CAN_EDIT);
if ($can_edit) {
return null;
}
return $namespace;
}
/* -( AlmanacPropertyInterface )------------------------------------------- */
public function attachAlmanacProperties(array $properties) {
assert_instances_of($properties, 'AlmanacProperty');
$this->almanacProperties = mpull($properties, null, 'getFieldName');
return $this;
}
public function getAlmanacProperties() {
return $this->assertAttached($this->almanacProperties);
}
public function hasAlmanacProperty($key) {
$this->assertAttached($this->almanacProperties);
return isset($this->almanacProperties[$key]);
}
public function getAlmanacProperty($key) {
return $this->assertAttachedKey($this->almanacProperties, $key);
}
public function getAlmanacPropertyValue($key, $default = null) {
if ($this->hasAlmanacProperty($key)) {
return $this->getAlmanacProperty($key)->getFieldValue();
} else {
return $default;
}
}
public function getAlmanacPropertyFieldSpecifications() {
return array();
}
public function newAlmanacPropertyEditEngine() {
throw new PhutilMethodNotImplementedException();
}
public function getAlmanacPropertySetTransactionType() {
throw new PhutilMethodNotImplementedException();
}
public function getAlmanacPropertyDeleteTransactionType() {
throw new PhutilMethodNotImplementedException();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
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 $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new AlmanacNamespaceEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new AlmanacNamespaceTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
- return $timeline;
- }
-
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->delete();
}
/* -( PhabricatorNgramsInterface )----------------------------------------- */
public function newNgrams() {
return array(
id(new AlmanacNamespaceNameNgrams())
->setValue($this->getName()),
);
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of the namespace.')),
);
}
public function getFieldValuesForConduit() {
return array(
'name' => $this->getName(),
);
}
public function getConduitSearchAttachments() {
return array();
}
}
diff --git a/src/applications/almanac/storage/AlmanacNetwork.php b/src/applications/almanac/storage/AlmanacNetwork.php
index 6d2f23032..78313fad7 100644
--- a/src/applications/almanac/storage/AlmanacNetwork.php
+++ b/src/applications/almanac/storage/AlmanacNetwork.php
@@ -1,156 +1,145 @@
<?php
final class AlmanacNetwork
extends AlmanacDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface,
PhabricatorNgramsInterface,
PhabricatorConduitResultInterface {
protected $name;
protected $mailKey;
protected $viewPolicy;
protected $editPolicy;
public static function initializeNewNetwork() {
return id(new AlmanacNetwork())
->setViewPolicy(PhabricatorPolicies::POLICY_USER)
->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort128',
'mailKey' => 'bytes20',
),
self::CONFIG_KEY_SCHEMA => array(
'key_name' => array(
'columns' => array('name'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(AlmanacNetworkPHIDType::TYPECONST);
}
public function save() {
if (!$this->mailKey) {
$this->mailKey = Filesystem::readRandomCharacters(20);
}
return parent::save();
}
public function getURI() {
return '/almanac/network/'.$this->getID().'/';
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new AlmanacNetworkEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new AlmanacNetworkTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorPolicyInterface )----------------------------------------- */
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 $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$interfaces = id(new AlmanacInterfaceQuery())
->setViewer($engine->getViewer())
->withNetworkPHIDs(array($this->getPHID()))
->execute();
foreach ($interfaces as $interface) {
$engine->destroyObject($interface);
}
$this->delete();
}
/* -( PhabricatorNgramsInterface )----------------------------------------- */
public function newNgrams() {
return array(
id(new AlmanacNetworkNameNgrams())
->setValue($this->getName()),
);
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of the network.')),
);
}
public function getFieldValuesForConduit() {
return array(
'name' => $this->getName(),
);
}
public function getConduitSearchAttachments() {
return array();
}
}
diff --git a/src/applications/almanac/storage/AlmanacService.php b/src/applications/almanac/storage/AlmanacService.php
index ee40d8340..2979b7436 100644
--- a/src/applications/almanac/storage/AlmanacService.php
+++ b/src/applications/almanac/storage/AlmanacService.php
@@ -1,306 +1,295 @@
<?php
final class AlmanacService
extends AlmanacDAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorProjectInterface,
AlmanacPropertyInterface,
PhabricatorDestructibleInterface,
PhabricatorNgramsInterface,
PhabricatorConduitResultInterface,
PhabricatorExtendedPolicyInterface {
protected $name;
protected $nameIndex;
protected $mailKey;
protected $viewPolicy;
protected $editPolicy;
protected $serviceType;
private $almanacProperties = self::ATTACHABLE;
private $bindings = self::ATTACHABLE;
private $serviceImplementation = self::ATTACHABLE;
public static function initializeNewService($type) {
$type_map = AlmanacServiceType::getAllServiceTypes();
$implementation = idx($type_map, $type);
if (!$implementation) {
throw new Exception(
pht(
'No Almanac service type "%s" exists!',
$type));
}
return id(new AlmanacService())
->setViewPolicy(PhabricatorPolicies::POLICY_USER)
->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN)
->attachAlmanacProperties(array())
->setServiceType($type)
->attachServiceImplementation($implementation);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text128',
'nameIndex' => 'bytes12',
'mailKey' => 'bytes20',
'serviceType' => 'text64',
),
self::CONFIG_KEY_SCHEMA => array(
'key_name' => array(
'columns' => array('nameIndex'),
'unique' => true,
),
'key_nametext' => array(
'columns' => array('name'),
),
'key_servicetype' => array(
'columns' => array('serviceType'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(AlmanacServicePHIDType::TYPECONST);
}
public function save() {
AlmanacNames::validateName($this->getName());
$this->nameIndex = PhabricatorHash::digestForIndex($this->getName());
if (!$this->mailKey) {
$this->mailKey = Filesystem::readRandomCharacters(20);
}
return parent::save();
}
public function getURI() {
return '/almanac/service/view/'.$this->getName().'/';
}
public function getBindings() {
return $this->assertAttached($this->bindings);
}
public function getActiveBindings() {
$bindings = $this->getBindings();
// Filter out disabled bindings.
foreach ($bindings as $key => $binding) {
if ($binding->getIsDisabled()) {
unset($bindings[$key]);
}
}
return $bindings;
}
public function attachBindings(array $bindings) {
$this->bindings = $bindings;
return $this;
}
public function getServiceImplementation() {
return $this->assertAttached($this->serviceImplementation);
}
public function attachServiceImplementation(AlmanacServiceType $type) {
$this->serviceImplementation = $type;
return $this;
}
public function isClusterService() {
return $this->getServiceImplementation()->isClusterServiceType();
}
/* -( AlmanacPropertyInterface )------------------------------------------- */
public function attachAlmanacProperties(array $properties) {
assert_instances_of($properties, 'AlmanacProperty');
$this->almanacProperties = mpull($properties, null, 'getFieldName');
return $this;
}
public function getAlmanacProperties() {
return $this->assertAttached($this->almanacProperties);
}
public function hasAlmanacProperty($key) {
$this->assertAttached($this->almanacProperties);
return isset($this->almanacProperties[$key]);
}
public function getAlmanacProperty($key) {
return $this->assertAttachedKey($this->almanacProperties, $key);
}
public function getAlmanacPropertyValue($key, $default = null) {
if ($this->hasAlmanacProperty($key)) {
return $this->getAlmanacProperty($key)->getFieldValue();
} else {
return $default;
}
}
public function getAlmanacPropertyFieldSpecifications() {
return $this->getServiceImplementation()->getFieldSpecifications();
}
public function getBindingFieldSpecifications(AlmanacBinding $binding) {
$impl = $this->getServiceImplementation();
return $impl->getBindingFieldSpecifications($binding);
}
public function newAlmanacPropertyEditEngine() {
return new AlmanacServicePropertyEditEngine();
}
public function getAlmanacPropertySetTransactionType() {
return AlmanacServiceSetPropertyTransaction::TRANSACTIONTYPE;
}
public function getAlmanacPropertyDeleteTransactionType() {
return AlmanacServiceDeletePropertyTransaction::TRANSACTIONTYPE;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
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 $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_EDIT:
if ($this->isClusterService()) {
return array(
array(
new PhabricatorAlmanacApplication(),
AlmanacManageClusterServicesCapability::CAPABILITY,
),
);
}
break;
}
return array();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new AlmanacServiceEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new AlmanacServiceTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$bindings = id(new AlmanacBindingQuery())
->setViewer($engine->getViewer())
->withServicePHIDs(array($this->getPHID()))
->execute();
foreach ($bindings as $binding) {
$engine->destroyObject($binding);
}
$this->delete();
}
/* -( PhabricatorNgramsInterface )----------------------------------------- */
public function newNgrams() {
return array(
id(new AlmanacServiceNameNgrams())
->setValue($this->getName()),
);
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of the service.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('serviceType')
->setType('string')
->setDescription(pht('The service type constant.')),
);
}
public function getFieldValuesForConduit() {
return array(
'name' => $this->getName(),
'serviceType' => $this->getServiceType(),
);
}
public function getConduitSearchAttachments() {
return array(
id(new AlmanacPropertiesSearchEngineAttachment())
->setAttachmentKey('properties'),
id(new AlmanacBindingsSearchEngineAttachment())
->setAttachmentKey('bindings'),
);
}
}
diff --git a/src/applications/almanac/xaction/AlmanacBindingInterfaceTransaction.php b/src/applications/almanac/xaction/AlmanacBindingInterfaceTransaction.php
index f43fcdfa8..03effc028 100644
--- a/src/applications/almanac/xaction/AlmanacBindingInterfaceTransaction.php
+++ b/src/applications/almanac/xaction/AlmanacBindingInterfaceTransaction.php
@@ -1,111 +1,122 @@
<?php
final class AlmanacBindingInterfaceTransaction
extends AlmanacBindingTransactionType {
const TRANSACTIONTYPE = 'almanac:binding:interface';
public function generateOldValue($object) {
return $object->getInterfacePHID();
}
public function applyInternalEffects($object, $value) {
$interface = $this->loadInterface($value);
$object
->setDevicePHID($interface->getDevicePHID())
->setInterfacePHID($interface->getPHID());
}
public function applyExternalEffects($object, $value) {
// When we change which services a device is bound to, we need to
// recalculate whether it is a cluster device or not so we can tell if
// the "Can Manage Cluster Services" permission applies to it.
$viewer = PhabricatorUser::getOmnipotentUser();
$interface_phids = array();
$interface_phids[] = $this->getOldValue();
$interface_phids[] = $this->getNewValue();
$interface_phids = array_filter($interface_phids);
$interface_phids = array_unique($interface_phids);
$interfaces = id(new AlmanacInterfaceQuery())
->setViewer($viewer)
->withPHIDs($interface_phids)
->execute();
$device_phids = array();
foreach ($interfaces as $interface) {
$device_phids[] = $interface->getDevicePHID();
}
$device_phids = array_unique($device_phids);
$devices = id(new AlmanacDeviceQuery())
->setViewer($viewer)
->withPHIDs($device_phids)
->execute();
foreach ($devices as $device) {
$device->rebuildClusterBindingStatus();
}
}
public function getTitle() {
- return pht(
- '%s changed the interface for this binding from %s to %s.',
- $this->renderAuthor(),
- $this->renderOldHandle(),
- $this->renderNewHandle());
+ if ($this->getOldValue() === null) {
+ return pht(
+ '%s set the interface for this binding to %s.',
+ $this->renderAuthor(),
+ $this->renderNewHandle());
+ } else if ($this->getNewValue() == null) {
+ return pht(
+ '%s removed the interface for this binding.',
+ $this->renderAuthor());
+ } else {
+ return pht(
+ '%s changed the interface for this binding from %s to %s.',
+ $this->renderAuthor(),
+ $this->renderOldHandle(),
+ $this->renderNewHandle());
+ }
}
public function validateTransactions($object, array $xactions) {
$errors = array();
$interface_phid = $object->getInterfacePHID();
if ($this->isEmptyTextTransaction($interface_phid, $xactions)) {
$errors[] = $this->newRequiredError(
pht('Bindings must specify an interface.'));
}
foreach ($xactions as $xaction) {
$interface_phid = $xaction->getNewValue();
$interface = $this->loadInterface($interface_phid);
if (!$interface) {
$errors[] = $this->newInvalidError(
pht(
'You can not bind a service to an invalid or restricted '.
'interface.'),
$xaction);
continue;
}
$binding = id(new AlmanacBindingQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withServicePHIDs(array($object->getServicePHID()))
->withInterfacePHIDs(array($interface_phid))
->executeOne();
if ($binding && ($binding->getID() != $object->getID())) {
$errors[] = $this->newInvalidError(
pht(
'You can not bind a service to the same interface multiple '.
'times.'),
$xaction);
continue;
}
}
return $errors;
}
private function loadInterface($phid) {
return id(new AlmanacInterfaceQuery())
->setViewer($this->getActor())
->withPHIDs(array($phid))
->executeOne();
}
}
diff --git a/src/applications/audit/editor/PhabricatorAuditEditor.php b/src/applications/audit/editor/PhabricatorAuditEditor.php
index 165f8ef84..d4fa1c32f 100644
--- a/src/applications/audit/editor/PhabricatorAuditEditor.php
+++ b/src/applications/audit/editor/PhabricatorAuditEditor.php
@@ -1,858 +1,858 @@
<?php
final class PhabricatorAuditEditor
extends PhabricatorApplicationTransactionEditor {
const MAX_FILES_SHOWN_IN_EMAIL = 1000;
private $affectedFiles;
private $rawPatch;
private $auditorPHIDs = array();
private $didExpandInlineState = false;
private $oldAuditStatus = null;
public function setRawPatch($patch) {
$this->rawPatch = $patch;
return $this;
}
public function getRawPatch() {
return $this->rawPatch;
}
public function getEditorApplicationClass() {
return 'PhabricatorDiffusionApplication';
}
public function getEditorObjectsDescription() {
return pht('Audits');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = PhabricatorTransactions::TYPE_INLINESTATE;
$types[] = PhabricatorAuditTransaction::TYPE_COMMIT;
// TODO: These will get modernized eventually, but that can happen one
// at a time later on.
$types[] = PhabricatorAuditActionConstants::INLINE;
return $types;
}
protected function expandTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_INLINESTATE:
$this->didExpandInlineState = true;
break;
}
}
$this->oldAuditStatus = $object->getAuditStatus();
return parent::expandTransactions($object, $xactions);
}
protected function transactionHasEffect(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorAuditActionConstants::INLINE:
return $xaction->hasComment();
}
return parent::transactionHasEffect($object, $xaction);
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorAuditActionConstants::INLINE:
case PhabricatorAuditTransaction::TYPE_COMMIT:
return null;
}
return parent::getCustomTransactionOldValue($object, $xaction);
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorAuditActionConstants::INLINE:
case PhabricatorAuditTransaction::TYPE_COMMIT:
return $xaction->getNewValue();
}
return parent::getCustomTransactionNewValue($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorAuditActionConstants::INLINE:
case PhabricatorAuditTransaction::TYPE_COMMIT:
return;
}
return parent::applyCustomInternalTransaction($object, $xaction);
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorAuditTransaction::TYPE_COMMIT:
return;
case PhabricatorAuditActionConstants::INLINE:
$reply = $xaction->getComment()->getReplyToComment();
if ($reply && !$reply->getHasReplies()) {
$reply->setHasReplies(1)->save();
}
return;
}
return parent::applyCustomExternalTransaction($object, $xaction);
}
protected function applyBuiltinExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_INLINESTATE:
$table = new PhabricatorAuditTransactionComment();
$conn_w = $table->establishConnection('w');
foreach ($xaction->getNewValue() as $phid => $state) {
queryfx(
$conn_w,
'UPDATE %T SET fixedState = %s WHERE phid = %s',
$table->getTableName(),
$state,
$phid);
}
break;
}
return parent::applyBuiltinExternalTransaction($object, $xaction);
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
// Load auditors explicitly; we may not have them if the caller was a
// generic piece of infrastructure.
$commit = id(new DiffusionCommitQuery())
->setViewer($this->requireActor())
->withIDs(array($object->getID()))
->needAuditRequests(true)
->executeOne();
if (!$commit) {
throw new Exception(
pht('Failed to load commit during transaction finalization!'));
}
$object->attachAudits($commit->getAudits());
$status_concerned = PhabricatorAuditStatusConstants::CONCERNED;
$status_closed = PhabricatorAuditStatusConstants::CLOSED;
$status_resigned = PhabricatorAuditStatusConstants::RESIGNED;
$status_accepted = PhabricatorAuditStatusConstants::ACCEPTED;
$status_concerned = PhabricatorAuditStatusConstants::CONCERNED;
$actor_phid = $this->getActingAsPHID();
$actor_is_author = ($object->getAuthorPHID()) &&
($actor_phid == $object->getAuthorPHID());
$import_status_flag = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorAuditTransaction::TYPE_COMMIT:
$import_status_flag = PhabricatorRepositoryCommit::IMPORTED_HERALD;
break;
}
}
$old_status = $this->oldAuditStatus;
$requests = $object->getAudits();
$object->updateAuditStatus($requests);
$new_status = $object->getAuditStatus();
$object->save();
if ($import_status_flag) {
$object->writeImportStatusFlag($import_status_flag);
}
// If the commit has changed state after this edit, add an informational
// transaction about the state change.
if ($old_status != $new_status) {
if ($object->isAuditStatusPartiallyAudited()) {
// This state isn't interesting enough to get a transaction. The
// best way we could lead the user forward is something like "This
// commit still requires additional audits." but that's redundant and
// probably not very useful.
} else {
$xaction = $object->getApplicationTransactionTemplate()
->setTransactionType(DiffusionCommitStateTransaction::TRANSACTIONTYPE)
->setOldValue($old_status)
->setNewValue($new_status);
$xaction = $this->populateTransaction($object, $xaction);
$xaction->save();
}
}
// Collect auditor PHIDs for building mail.
$this->auditorPHIDs = mpull($object->getAudits(), 'getAuditorPHID');
return $xactions;
}
protected function expandTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$xactions = parent::expandTransaction($object, $xaction);
switch ($xaction->getTransactionType()) {
case PhabricatorAuditTransaction::TYPE_COMMIT:
$request = $this->createAuditRequestTransactionFromCommitMessage(
$object);
if ($request) {
$xactions[] = $request;
$this->setUnmentionablePHIDMap($request->getNewValue());
}
break;
default:
break;
}
if (!$this->didExpandInlineState) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
$this->didExpandInlineState = true;
$query_template = id(new DiffusionDiffInlineCommentQuery())
->withCommitPHIDs(array($object->getPHID()));
$state_xaction = $this->newInlineStateTransaction(
$object,
$query_template);
if ($state_xaction) {
$xactions[] = $state_xaction;
}
break;
}
}
return $xactions;
}
private function createAuditRequestTransactionFromCommitMessage(
PhabricatorRepositoryCommit $commit) {
$actor = $this->getActor();
$data = $commit->getCommitData();
$message = $data->getCommitMessage();
$result = DifferentialCommitMessageParser::newStandardParser($actor)
->setRaiseMissingFieldErrors(false)
->parseFields($message);
$field_key = DifferentialAuditorsCommitMessageField::FIELDKEY;
$phids = idx($result, $field_key, null);
if (!$phids) {
return array();
}
// If a commit lists its author as an auditor, just pretend it does not.
foreach ($phids as $key => $phid) {
if ($phid == $commit->getAuthorPHID()) {
unset($phids[$key]);
}
}
if (!$phids) {
return array();
}
return $commit->getApplicationTransactionTemplate()
->setTransactionType(DiffusionCommitAuditorsTransaction::TRANSACTIONTYPE)
->setNewValue(
array(
'+' => array_fuse($phids),
));
}
protected function sortTransactions(array $xactions) {
$xactions = parent::sortTransactions($xactions);
$head = array();
$tail = array();
foreach ($xactions as $xaction) {
$type = $xaction->getTransactionType();
if ($type == PhabricatorAuditActionConstants::INLINE) {
$tail[] = $xaction;
} else {
$head[] = $xaction;
}
}
return array_values(array_merge($head, $tail));
}
protected function supportsSearch() {
return true;
}
protected function expandCustomRemarkupBlockTransactions(
PhabricatorLiskDAO $object,
array $xactions,
array $changes,
PhutilMarkupEngine $engine) {
$actor = $this->getActor();
$result = array();
// Some interactions (like "Fixes Txxx" interacting with Maniphest) have
// already been processed, so we're only re-parsing them here to avoid
// generating an extra redundant mention. Other interactions are being
// processed for the first time.
// We're only recognizing magic in the commit message itself, not in
// audit comments.
$is_commit = false;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorAuditTransaction::TYPE_COMMIT:
$is_commit = true;
break;
}
}
if (!$is_commit) {
return $result;
}
$flat_blocks = mpull($changes, 'getNewValue');
$huge_block = implode("\n\n", $flat_blocks);
$phid_map = array();
$phid_map[] = $this->getUnmentionablePHIDMap();
$monograms = array();
$task_refs = id(new ManiphestCustomFieldStatusParser())
->parseCorpus($huge_block);
foreach ($task_refs as $match) {
foreach ($match['monograms'] as $monogram) {
$monograms[] = $monogram;
}
}
$rev_refs = id(new DifferentialCustomFieldDependsOnParser())
->parseCorpus($huge_block);
foreach ($rev_refs as $match) {
foreach ($match['monograms'] as $monogram) {
$monograms[] = $monogram;
}
}
$objects = id(new PhabricatorObjectQuery())
->setViewer($this->getActor())
->withNames($monograms)
->execute();
$phid_map[] = mpull($objects, 'getPHID', 'getPHID');
$reverts_refs = id(new DifferentialCustomFieldRevertsParser())
->parseCorpus($huge_block);
$reverts = array_mergev(ipull($reverts_refs, 'monograms'));
if ($reverts) {
// Only allow commits to revert other commits in the same repository.
$reverted_commits = id(new DiffusionCommitQuery())
->setViewer($actor)
->withRepository($object->getRepository())
->withIdentifiers($reverts)
->execute();
$reverted_revisions = id(new PhabricatorObjectQuery())
->setViewer($actor)
->withNames($reverts)
->withTypes(
array(
DifferentialRevisionPHIDType::TYPECONST,
))
->execute();
$reverted_phids =
mpull($reverted_commits, 'getPHID', 'getPHID') +
mpull($reverted_revisions, 'getPHID', 'getPHID');
// NOTE: Skip any write attempts if a user cleverly implies a commit
// reverts itself, although this would be exceptionally clever in Git
// or Mercurial.
unset($reverted_phids[$object->getPHID()]);
$reverts_edge = DiffusionCommitRevertsCommitEdgeType::EDGECONST;
$result[] = id(new PhabricatorAuditTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $reverts_edge)
->setNewValue(array('+' => $reverted_phids));
$phid_map[] = $reverted_phids;
}
$phid_map = array_mergev($phid_map);
$this->setUnmentionablePHIDMap($phid_map);
return $result;
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
$reply_handler = new PhabricatorAuditReplyHandler();
$reply_handler->setMailReceiver($object);
return $reply_handler;
}
protected function getMailSubjectPrefix() {
- return PhabricatorEnv::getEnvConfig('metamta.diffusion.subject-prefix');
+ return pht('[Diffusion]');
}
protected function getMailThreadID(PhabricatorLiskDAO $object) {
// For backward compatibility, use this legacy thread ID.
return 'diffusion-audit-'.$object->getPHID();
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$identifier = $object->getCommitIdentifier();
$repository = $object->getRepository();
$summary = $object->getSummary();
$name = $repository->formatCommitName($identifier);
$subject = "{$name}: {$summary}";
$template = id(new PhabricatorMetaMTAMail())
->setSubject($subject);
$this->attachPatch(
$template,
$object);
return $template;
}
protected function getMailTo(PhabricatorLiskDAO $object) {
$this->requireAuditors($object);
$phids = array();
if ($object->getAuthorPHID()) {
$phids[] = $object->getAuthorPHID();
}
foreach ($object->getAudits() as $audit) {
if (!$audit->isInteresting()) {
// Don't send mail to uninteresting auditors, like packages which
// own this code but which audits have not triggered for.
continue;
}
if (!$audit->isResigned()) {
$phids[] = $audit->getAuditorPHID();
}
}
$phids[] = $this->getActingAsPHID();
return $phids;
}
protected function newMailUnexpandablePHIDs(PhabricatorLiskDAO $object) {
$this->requireAuditors($object);
$phids = array();
foreach ($object->getAudits() as $auditor) {
if ($auditor->isResigned()) {
$phids[] = $auditor->getAuditorPHID();
}
}
return $phids;
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$type_inline = PhabricatorAuditActionConstants::INLINE;
$type_push = PhabricatorAuditTransaction::TYPE_COMMIT;
$is_commit = false;
$inlines = array();
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == $type_inline) {
$inlines[] = $xaction;
}
if ($xaction->getTransactionType() == $type_push) {
$is_commit = true;
}
}
if ($inlines) {
$body->addTextSection(
pht('INLINE COMMENTS'),
$this->renderInlineCommentsForMail($object, $inlines));
}
if ($is_commit) {
$data = $object->getCommitData();
$body->addTextSection(pht('AFFECTED FILES'), $this->affectedFiles);
$this->inlinePatch(
$body,
$object);
}
$data = $object->getCommitData();
$user_phids = array();
$author_phid = $object->getAuthorPHID();
if ($author_phid) {
$user_phids[$author_phid][] = pht('Author');
}
$committer_phid = $data->getCommitDetail('committerPHID');
if ($committer_phid && ($committer_phid != $author_phid)) {
$user_phids[$committer_phid][] = pht('Committer');
}
foreach ($this->auditorPHIDs as $auditor_phid) {
$user_phids[$auditor_phid][] = pht('Auditor');
}
// TODO: It would be nice to show pusher here too, but that information
// is a little tricky to get at right now.
if ($user_phids) {
$handle_phids = array_keys($user_phids);
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->requireActor())
->withPHIDs($handle_phids)
->execute();
$user_info = array();
foreach ($user_phids as $phid => $roles) {
$user_info[] = pht(
'%s (%s)',
$handles[$phid]->getName(),
implode(', ', $roles));
}
$body->addTextSection(
pht('USERS'),
implode("\n", $user_info));
}
$monogram = $object->getRepository()->formatCommitName(
$object->getCommitIdentifier());
$body->addLinkSection(
pht('COMMIT'),
PhabricatorEnv::getProductionURI('/'.$monogram));
return $body;
}
private function attachPatch(
PhabricatorMetaMTAMail $template,
PhabricatorRepositoryCommit $commit) {
if (!$this->getRawPatch()) {
return;
}
$attach_key = 'metamta.diffusion.attach-patches';
$attach_patches = PhabricatorEnv::getEnvConfig($attach_key);
if (!$attach_patches) {
return;
}
$repository = $commit->getRepository();
$encoding = $repository->getDetail('encoding', 'UTF-8');
$raw_patch = $this->getRawPatch();
$commit_name = $repository->formatCommitName(
$commit->getCommitIdentifier());
$template->addAttachment(
- new PhabricatorMetaMTAAttachment(
+ new PhabricatorMailAttachment(
$raw_patch,
$commit_name.'.patch',
'text/x-patch; charset='.$encoding));
}
private function inlinePatch(
PhabricatorMetaMTAMailBody $body,
PhabricatorRepositoryCommit $commit) {
if (!$this->getRawPatch()) {
return;
}
$inline_key = 'metamta.diffusion.inline-patches';
$inline_patches = PhabricatorEnv::getEnvConfig($inline_key);
if (!$inline_patches) {
return;
}
$repository = $commit->getRepository();
$raw_patch = $this->getRawPatch();
$result = null;
$len = substr_count($raw_patch, "\n");
if ($len <= $inline_patches) {
// We send email as utf8, so we need to convert the text to utf8 if
// we can.
$encoding = $repository->getDetail('encoding', 'UTF-8');
if ($encoding) {
$raw_patch = phutil_utf8_convert($raw_patch, 'UTF-8', $encoding);
}
$result = phutil_utf8ize($raw_patch);
}
if ($result) {
$result = "PATCH\n\n{$result}\n";
}
$body->addRawSection($result);
}
private function renderInlineCommentsForMail(
PhabricatorLiskDAO $object,
array $inline_xactions) {
$inlines = mpull($inline_xactions, 'getComment');
$block = array();
$path_map = id(new DiffusionPathQuery())
->withPathIDs(mpull($inlines, 'getPathID'))
->execute();
$path_map = ipull($path_map, 'path', 'id');
foreach ($inlines as $inline) {
$path = idx($path_map, $inline->getPathID());
if ($path === null) {
continue;
}
$start = $inline->getLineNumber();
$len = $inline->getLineLength();
if ($len) {
$range = $start.'-'.($start + $len);
} else {
$range = $start;
}
$content = $inline->getContent();
$block[] = "{$path}:{$range} {$content}";
}
return implode("\n", $block);
}
public function getMailTagsMap() {
return array(
PhabricatorAuditTransaction::MAILTAG_COMMIT =>
pht('A commit is created.'),
PhabricatorAuditTransaction::MAILTAG_ACTION_CONCERN =>
pht('A commit has a concerned raised against it.'),
PhabricatorAuditTransaction::MAILTAG_ACTION_ACCEPT =>
pht('A commit is accepted.'),
PhabricatorAuditTransaction::MAILTAG_ACTION_RESIGN =>
pht('A commit has an auditor resign.'),
PhabricatorAuditTransaction::MAILTAG_ACTION_CLOSE =>
pht('A commit is closed.'),
PhabricatorAuditTransaction::MAILTAG_ADD_AUDITORS =>
pht('A commit has auditors added.'),
PhabricatorAuditTransaction::MAILTAG_ADD_CCS =>
pht("A commit's subscribers change."),
PhabricatorAuditTransaction::MAILTAG_PROJECTS =>
pht("A commit's projects change."),
PhabricatorAuditTransaction::MAILTAG_COMMENT =>
pht('Someone comments on a commit.'),
PhabricatorAuditTransaction::MAILTAG_OTHER =>
pht('Other commit activity not listed above occurs.'),
);
}
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorAuditTransaction::TYPE_COMMIT:
$repository = $object->getRepository();
if (!$repository->shouldPublish()) {
return false;
}
return true;
default:
break;
}
}
return parent::shouldApplyHeraldRules($object, $xactions);
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
return id(new HeraldCommitAdapter())
->setObject($object);
}
protected function didApplyHeraldRules(
PhabricatorLiskDAO $object,
HeraldAdapter $adapter,
HeraldTranscript $transcript) {
$limit = self::MAX_FILES_SHOWN_IN_EMAIL;
$files = $adapter->loadAffectedPaths();
sort($files);
if (count($files) > $limit) {
array_splice($files, $limit);
$files[] = pht(
'(This commit affected more than %d files. Only %d are shown here '.
'and additional ones are truncated.)',
$limit,
$limit);
}
$this->affectedFiles = implode("\n", $files);
return array();
}
private function isCommitMostlyImported(PhabricatorLiskDAO $object) {
$has_message = PhabricatorRepositoryCommit::IMPORTED_MESSAGE;
$has_changes = PhabricatorRepositoryCommit::IMPORTED_CHANGE;
// Don't publish feed stories or email about events which occur during
// import. In particular, this affects tasks being attached when they are
// closed by "Fixes Txxxx" in a commit message. See T5851.
$mask = ($has_message | $has_changes);
return $object->isPartiallyImported($mask);
}
private function shouldPublishRepositoryActivity(
PhabricatorLiskDAO $object,
array $xactions) {
// not every code path loads the repository so tread carefully
// TODO: They should, and then we should simplify this.
$repository = $object->getRepository($assert_attached = false);
if ($repository != PhabricatorLiskDAO::ATTACHABLE) {
if (!$repository->shouldPublish()) {
return false;
}
}
return $this->isCommitMostlyImported($object);
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return $this->shouldPublishRepositoryActivity($object, $xactions);
}
protected function shouldEnableMentions(
PhabricatorLiskDAO $object,
array $xactions) {
return $this->shouldPublishRepositoryActivity($object, $xactions);
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return $this->shouldPublishRepositoryActivity($object, $xactions);
}
protected function getCustomWorkerState() {
return array(
'rawPatch' => $this->rawPatch,
'affectedFiles' => $this->affectedFiles,
'auditorPHIDs' => $this->auditorPHIDs,
);
}
protected function getCustomWorkerStateEncoding() {
return array(
'rawPatch' => self::STORAGE_ENCODING_BINARY,
);
}
protected function loadCustomWorkerState(array $state) {
$this->rawPatch = idx($state, 'rawPatch');
$this->affectedFiles = idx($state, 'affectedFiles');
$this->auditorPHIDs = idx($state, 'auditorPHIDs');
return $this;
}
protected function willPublish(PhabricatorLiskDAO $object, array $xactions) {
return id(new DiffusionCommitQuery())
->setViewer($this->requireActor())
->withIDs(array($object->getID()))
->needAuditRequests(true)
->needCommitData(true)
->executeOne();
}
private function requireAuditors(PhabricatorRepositoryCommit $commit) {
if ($commit->hasAttachedAudits()) {
return;
}
$with_auditors = id(new DiffusionCommitQuery())
->setViewer($this->getActor())
->needAuditRequests(true)
->withPHIDs(array($commit->getPHID()))
->executeOne();
if (!$with_auditors) {
throw new Exception(
pht(
'Failed to reload commit ("%s").',
$commit->getPHID()));
}
$commit->attachAudits($with_auditors->getAudits());
}
}
diff --git a/src/applications/audit/mail/PhabricatorAuditMailReceiver.php b/src/applications/audit/mail/PhabricatorAuditMailReceiver.php
index 9dc70e9d8..9b5515174 100644
--- a/src/applications/audit/mail/PhabricatorAuditMailReceiver.php
+++ b/src/applications/audit/mail/PhabricatorAuditMailReceiver.php
@@ -1,28 +1,28 @@
<?php
final class PhabricatorAuditMailReceiver extends PhabricatorObjectMailReceiver {
public function isEnabled() {
return PhabricatorApplication::isClassInstalled(
'PhabricatorDiffusionApplication');
}
protected function getObjectPattern() {
return 'COMMIT[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
- $id = (int)preg_replace('/^COMMIT/', '', $pattern);
+ $id = (int)preg_replace('/^COMMIT/i', '', $pattern);
return id(new DiffusionCommitQuery())
->setViewer($viewer)
->withIDs(array($id))
->needAuditRequests(true)
->executeOne();
}
protected function getTransactionReplyHandler() {
return new PhabricatorAuditReplyHandler();
}
}
diff --git a/src/applications/audit/storage/PhabricatorAuditTransaction.php b/src/applications/audit/storage/PhabricatorAuditTransaction.php
index ad96edb3a..da312e626 100644
--- a/src/applications/audit/storage/PhabricatorAuditTransaction.php
+++ b/src/applications/audit/storage/PhabricatorAuditTransaction.php
@@ -1,530 +1,526 @@
<?php
final class PhabricatorAuditTransaction
extends PhabricatorModularTransaction {
const TYPE_COMMIT = 'audit:commit';
const MAILTAG_ACTION_CONCERN = 'audit-action-concern';
const MAILTAG_ACTION_ACCEPT = 'audit-action-accept';
const MAILTAG_ACTION_RESIGN = 'audit-action-resign';
const MAILTAG_ACTION_CLOSE = 'audit-action-close';
const MAILTAG_ADD_AUDITORS = 'audit-add-auditors';
const MAILTAG_ADD_CCS = 'audit-add-ccs';
const MAILTAG_COMMENT = 'audit-comment';
const MAILTAG_COMMIT = 'audit-commit';
const MAILTAG_PROJECTS = 'audit-projects';
const MAILTAG_OTHER = 'audit-other';
public function getApplicationName() {
return 'audit';
}
public function getBaseTransactionClass() {
return 'DiffusionCommitTransactionType';
}
public function getApplicationTransactionType() {
return PhabricatorRepositoryCommitPHIDType::TYPECONST;
}
public function getApplicationTransactionCommentObject() {
return new PhabricatorAuditTransactionComment();
}
- public function getApplicationTransactionViewObject() {
- return new PhabricatorAuditTransactionView();
- }
-
public function getRemarkupBlocks() {
$blocks = parent::getRemarkupBlocks();
switch ($this->getTransactionType()) {
case self::TYPE_COMMIT:
$data = $this->getNewValue();
$blocks[] = $data['description'];
break;
}
return $blocks;
}
public function getActionStrength() {
$type = $this->getTransactionType();
switch ($type) {
case self::TYPE_COMMIT:
return 3.0;
}
return parent::getActionStrength();
}
public function getRequiredHandlePHIDs() {
$phids = parent::getRequiredHandlePHIDs();
$type = $this->getTransactionType();
switch ($type) {
case self::TYPE_COMMIT:
$phids[] = $this->getObjectPHID();
$data = $this->getNewValue();
if ($data['authorPHID']) {
$phids[] = $data['authorPHID'];
}
if ($data['committerPHID']) {
$phids[] = $data['committerPHID'];
}
break;
case PhabricatorAuditActionConstants::ADD_CCS:
case PhabricatorAuditActionConstants::ADD_AUDITORS:
$old = $this->getOldValue();
$new = $this->getNewValue();
if (!is_array($old)) {
$old = array();
}
if (!is_array($new)) {
$new = array();
}
foreach (array_keys($old + $new) as $phid) {
$phids[] = $phid;
}
break;
}
return $phids;
}
public function getActionName() {
switch ($this->getTransactionType()) {
case PhabricatorAuditActionConstants::ACTION:
switch ($this->getNewValue()) {
case PhabricatorAuditActionConstants::CONCERN:
return pht('Raised Concern');
case PhabricatorAuditActionConstants::ACCEPT:
return pht('Accepted');
case PhabricatorAuditActionConstants::RESIGN:
return pht('Resigned');
case PhabricatorAuditActionConstants::CLOSE:
return pht('Closed');
}
break;
case PhabricatorAuditActionConstants::ADD_AUDITORS:
return pht('Added Auditors');
case self::TYPE_COMMIT:
return pht('Committed');
}
return parent::getActionName();
}
public function getColor() {
$type = $this->getTransactionType();
switch ($type) {
case PhabricatorAuditActionConstants::ACTION:
switch ($this->getNewValue()) {
case PhabricatorAuditActionConstants::CONCERN:
return 'red';
case PhabricatorAuditActionConstants::ACCEPT:
return 'green';
case PhabricatorAuditActionConstants::RESIGN:
return 'black';
case PhabricatorAuditActionConstants::CLOSE:
return 'indigo';
}
}
return parent::getColor();
}
public function getIcon() {
$type = $this->getTransactionType();
switch ($type) {
case PhabricatorAuditActionConstants::ACTION:
switch ($this->getNewValue()) {
case PhabricatorAuditActionConstants::CONCERN:
return 'fa-exclamation-circle';
case PhabricatorAuditActionConstants::ACCEPT:
return 'fa-check';
case PhabricatorAuditActionConstants::RESIGN:
return 'fa-plane';
case PhabricatorAuditActionConstants::CLOSE:
return 'fa-check';
}
}
return parent::getIcon();
}
public function getTitle() {
$old = $this->getOldValue();
$new = $this->getNewValue();
$author_handle = $this->renderHandleLink($this->getAuthorPHID());
$type = $this->getTransactionType();
switch ($type) {
case PhabricatorAuditActionConstants::ADD_CCS:
case PhabricatorAuditActionConstants::ADD_AUDITORS:
if (!is_array($old)) {
$old = array();
}
if (!is_array($new)) {
$new = array();
}
$add = array_keys(array_diff_key($new, $old));
$rem = array_keys(array_diff_key($old, $new));
break;
}
switch ($type) {
case self::TYPE_COMMIT:
$author = null;
if ($new['authorPHID']) {
$author = $this->renderHandleLink($new['authorPHID']);
} else {
$author = $new['authorName'];
}
$committer = null;
if ($new['committerPHID']) {
$committer = $this->renderHandleLink($new['committerPHID']);
} else if ($new['committerName']) {
$committer = $new['committerName'];
}
$commit = $this->renderHandleLink($this->getObjectPHID());
if (!$committer) {
$committer = $author;
$author = null;
}
if ($author) {
$title = pht(
'%s committed %s (authored by %s).',
$committer,
$commit,
$author);
} else {
$title = pht(
'%s committed %s.',
$committer,
$commit);
}
return $title;
case PhabricatorAuditActionConstants::INLINE:
return pht(
'%s added inline comments.',
$author_handle);
case PhabricatorAuditActionConstants::ADD_CCS:
if ($add && $rem) {
return pht(
'%s edited subscribers; added: %s, removed: %s.',
$author_handle,
$this->renderHandleList($add),
$this->renderHandleList($rem));
} else if ($add) {
return pht(
'%s added subscribers: %s.',
$author_handle,
$this->renderHandleList($add));
} else if ($rem) {
return pht(
'%s removed subscribers: %s.',
$author_handle,
$this->renderHandleList($rem));
} else {
return pht(
'%s added subscribers...',
$author_handle);
}
case PhabricatorAuditActionConstants::ADD_AUDITORS:
if ($add && $rem) {
return pht(
'%s edited auditors; added: %s, removed: %s.',
$author_handle,
$this->renderHandleList($add),
$this->renderHandleList($rem));
} else if ($add) {
return pht(
'%s added auditors: %s.',
$author_handle,
$this->renderHandleList($add));
} else if ($rem) {
return pht(
'%s removed auditors: %s.',
$author_handle,
$this->renderHandleList($rem));
} else {
return pht(
'%s added auditors...',
$author_handle);
}
case PhabricatorAuditActionConstants::ACTION:
switch ($new) {
case PhabricatorAuditActionConstants::ACCEPT:
return pht(
'%s accepted this commit.',
$author_handle);
case PhabricatorAuditActionConstants::CONCERN:
return pht(
'%s raised a concern with this commit.',
$author_handle);
case PhabricatorAuditActionConstants::RESIGN:
return pht(
'%s resigned from this audit.',
$author_handle);
case PhabricatorAuditActionConstants::CLOSE:
return pht(
'%s closed this audit.',
$author_handle);
}
}
return parent::getTitle();
}
public function getTitleForFeed() {
$old = $this->getOldValue();
$new = $this->getNewValue();
$author_handle = $this->renderHandleLink($this->getAuthorPHID());
$object_handle = $this->renderHandleLink($this->getObjectPHID());
$type = $this->getTransactionType();
switch ($type) {
case PhabricatorAuditActionConstants::ADD_CCS:
case PhabricatorAuditActionConstants::ADD_AUDITORS:
if (!is_array($old)) {
$old = array();
}
if (!is_array($new)) {
$new = array();
}
$add = array_keys(array_diff_key($new, $old));
$rem = array_keys(array_diff_key($old, $new));
break;
}
switch ($type) {
case self::TYPE_COMMIT:
$author = null;
if ($new['authorPHID']) {
$author = $this->renderHandleLink($new['authorPHID']);
} else {
$author = $new['authorName'];
}
$committer = null;
if ($new['committerPHID']) {
$committer = $this->renderHandleLink($new['committerPHID']);
} else if ($new['committerName']) {
$committer = $new['committerName'];
}
if (!$committer) {
$committer = $author;
$author = null;
}
if ($author) {
$title = pht(
'%s committed %s (authored by %s).',
$committer,
$object_handle,
$author);
} else {
$title = pht(
'%s committed %s.',
$committer,
$object_handle);
}
return $title;
case PhabricatorAuditActionConstants::INLINE:
return pht(
'%s added inline comments to %s.',
$author_handle,
$object_handle);
case PhabricatorAuditActionConstants::ADD_AUDITORS:
if ($add && $rem) {
return pht(
'%s edited auditors for %s; added: %s, removed: %s.',
$author_handle,
$object_handle,
$this->renderHandleList($add),
$this->renderHandleList($rem));
} else if ($add) {
return pht(
'%s added auditors to %s: %s.',
$author_handle,
$object_handle,
$this->renderHandleList($add));
} else if ($rem) {
return pht(
'%s removed auditors from %s: %s.',
$author_handle,
$object_handle,
$this->renderHandleList($rem));
} else {
return pht(
'%s added auditors to %s...',
$author_handle,
$object_handle);
}
case PhabricatorAuditActionConstants::ACTION:
switch ($new) {
case PhabricatorAuditActionConstants::ACCEPT:
return pht(
'%s accepted %s.',
$author_handle,
$object_handle);
case PhabricatorAuditActionConstants::CONCERN:
return pht(
'%s raised a concern with %s.',
$author_handle,
$object_handle);
case PhabricatorAuditActionConstants::RESIGN:
return pht(
'%s resigned from auditing %s.',
$author_handle,
$object_handle);
case PhabricatorAuditActionConstants::CLOSE:
return pht(
'%s closed the audit of %s.',
$author_handle,
$object_handle);
}
}
return parent::getTitleForFeed();
}
public function getBodyForFeed(PhabricatorFeedStory $story) {
switch ($this->getTransactionType()) {
case self::TYPE_COMMIT:
$data = $this->getNewValue();
return $story->renderSummary($data['summary']);
}
return parent::getBodyForFeed($story);
}
public function isInlineCommentTransaction() {
switch ($this->getTransactionType()) {
case PhabricatorAuditActionConstants::INLINE:
return true;
}
return parent::isInlineCommentTransaction();
}
public function getBodyForMail() {
switch ($this->getTransactionType()) {
case self::TYPE_COMMIT:
$data = $this->getNewValue();
return $data['description'];
}
return parent::getBodyForMail();
}
public function getMailTags() {
$tags = array();
switch ($this->getTransactionType()) {
case DiffusionCommitAcceptTransaction::TRANSACTIONTYPE:
$tags[] = self::MAILTAG_ACTION_ACCEPT;
break;
case DiffusionCommitConcernTransaction::TRANSACTIONTYPE:
$tags[] = self::MAILTAG_ACTION_CONCERN;
break;
case DiffusionCommitResignTransaction::TRANSACTIONTYPE:
$tags[] = self::MAILTAG_ACTION_RESIGN;
break;
case DiffusionCommitAuditorsTransaction::TRANSACTIONTYPE:
$tags[] = self::MAILTAG_ADD_AUDITORS;
break;
case PhabricatorAuditActionConstants::ACTION:
switch ($this->getNewValue()) {
case PhabricatorAuditActionConstants::CONCERN:
$tags[] = self::MAILTAG_ACTION_CONCERN;
break;
case PhabricatorAuditActionConstants::ACCEPT:
$tags[] = self::MAILTAG_ACTION_ACCEPT;
break;
case PhabricatorAuditActionConstants::RESIGN:
$tags[] = self::MAILTAG_ACTION_RESIGN;
break;
case PhabricatorAuditActionConstants::CLOSE:
$tags[] = self::MAILTAG_ACTION_CLOSE;
break;
}
break;
case PhabricatorAuditActionConstants::ADD_AUDITORS:
$tags[] = self::MAILTAG_ADD_AUDITORS;
break;
case PhabricatorAuditActionConstants::ADD_CCS:
$tags[] = self::MAILTAG_ADD_CCS;
break;
case PhabricatorAuditActionConstants::INLINE:
case PhabricatorTransactions::TYPE_COMMENT:
$tags[] = self::MAILTAG_COMMENT;
break;
case self::TYPE_COMMIT:
$tags[] = self::MAILTAG_COMMIT;
break;
case PhabricatorTransactions::TYPE_EDGE:
switch ($this->getMetadataValue('edge:type')) {
case PhabricatorProjectObjectHasProjectEdgeType::EDGECONST:
$tags[] = self::MAILTAG_PROJECTS;
break;
case PhabricatorObjectHasSubscriberEdgeType::EDGECONST:
$tags[] = self::MAILTAG_ADD_CCS;
break;
default:
$tags[] = self::MAILTAG_OTHER;
break;
}
break;
default:
$tags[] = self::MAILTAG_OTHER;
break;
}
return $tags;
}
public function shouldDisplayGroupWith(array $group) {
// Make the "This commit now requires audit." state message stand alone.
$type_state = DiffusionCommitStateTransaction::TRANSACTIONTYPE;
if ($this->getTransactionType() == $type_state) {
return false;
}
foreach ($group as $xaction) {
if ($xaction->getTransactionType() == $type_state) {
return false;
}
}
return parent::shouldDisplayGroupWith($group);
}
}
diff --git a/src/applications/auth/action/PhabricatorAuthNewFactorAction.php b/src/applications/auth/action/PhabricatorAuthNewFactorAction.php
new file mode 100644
index 000000000..c1244587f
--- /dev/null
+++ b/src/applications/auth/action/PhabricatorAuthNewFactorAction.php
@@ -0,0 +1,21 @@
+<?php
+
+final class PhabricatorAuthNewFactorAction extends PhabricatorSystemAction {
+
+ const TYPECONST = 'auth.factor.new';
+
+ public function getActionConstant() {
+ return self::TYPECONST;
+ }
+
+ public function getScoreThreshold() {
+ return 60 / phutil_units('1 hour in seconds');
+ }
+
+ public function getLimitExplanation() {
+ return pht(
+ 'You have failed too many attempts to synchronize new multi-factor '.
+ 'authentication methods in a short period of time.');
+ }
+
+}
diff --git a/src/applications/auth/action/PhabricatorAuthTestSMSAction.php b/src/applications/auth/action/PhabricatorAuthTestSMSAction.php
new file mode 100644
index 000000000..d0f4a6bb7
--- /dev/null
+++ b/src/applications/auth/action/PhabricatorAuthTestSMSAction.php
@@ -0,0 +1,22 @@
+<?php
+
+final class PhabricatorAuthTestSMSAction extends PhabricatorSystemAction {
+
+ const TYPECONST = 'auth.sms.test';
+
+ public function getActionConstant() {
+ return self::TYPECONST;
+ }
+
+ public function getScoreThreshold() {
+ return 60 / phutil_units('1 hour in seconds');
+ }
+
+ public function getLimitExplanation() {
+ return pht(
+ 'You and other users on this install are collectively sending too '.
+ 'many test text messages too quickly. Wait a few minutes to continue '.
+ 'texting tests.');
+ }
+
+}
diff --git a/src/applications/auth/application/PhabricatorAuthApplication.php b/src/applications/auth/application/PhabricatorAuthApplication.php
index ff4ed1f13..df86595b4 100644
--- a/src/applications/auth/application/PhabricatorAuthApplication.php
+++ b/src/applications/auth/application/PhabricatorAuthApplication.php
@@ -1,119 +1,154 @@
<?php
final class PhabricatorAuthApplication extends PhabricatorApplication {
public function canUninstall() {
return false;
}
public function getBaseURI() {
return '/auth/';
}
public function getIcon() {
return 'fa-key';
}
public function isPinnedByDefault(PhabricatorUser $viewer) {
return $viewer->getIsAdmin();
}
public function getName() {
return pht('Auth');
}
public function getShortDescription() {
return pht('Login/Registration');
}
public function getHelpDocumentationArticles(PhabricatorUser $viewer) {
// NOTE: Although reasonable help exists for this in "Configuring Accounts
// and Registration", specifying help items here means we get the menu
// item in all the login/link interfaces, which is confusing and not
// helpful.
// TODO: Special case this, or split the auth and auth administration
// applications?
return array();
}
public function getApplicationGroup() {
return self::GROUP_ADMIN;
}
public function getRoutes() {
return array(
'/auth/' => array(
'' => 'PhabricatorAuthListController',
'config/' => array(
'new/' => 'PhabricatorAuthNewController',
'new/(?P<className>[^/]+)/' => 'PhabricatorAuthEditController',
'edit/(?P<id>\d+)/' => 'PhabricatorAuthEditController',
'(?P<action>enable|disable)/(?P<id>\d+)/'
=> 'PhabricatorAuthDisableController',
),
'login/(?P<pkey>[^/]+)/(?:(?P<extra>[^/]+)/)?'
=> 'PhabricatorAuthLoginController',
'(?P<loggedout>loggedout)/' => 'PhabricatorAuthStartController',
'invite/(?P<code>[^/]+)/' => 'PhabricatorAuthInviteController',
'register/(?:(?P<akey>[^/]+)/)?' => 'PhabricatorAuthRegisterController',
'start/' => 'PhabricatorAuthStartController',
'validate/' => 'PhabricatorAuthValidateController',
'finish/' => 'PhabricatorAuthFinishController',
'unlink/(?P<pkey>[^/]+)/' => 'PhabricatorAuthUnlinkController',
'(?P<action>link|refresh)/(?P<pkey>[^/]+)/'
=> 'PhabricatorAuthLinkController',
'confirmlink/(?P<akey>[^/]+)/'
=> 'PhabricatorAuthConfirmLinkController',
'session/terminate/(?P<id>[^/]+)/'
=> 'PhabricatorAuthTerminateSessionController',
'token/revoke/(?P<id>[^/]+)/'
=> 'PhabricatorAuthRevokeTokenController',
'session/downgrade/'
=> 'PhabricatorAuthDowngradeSessionController',
- 'multifactor/'
- => 'PhabricatorAuthNeedsMultiFactorController',
+ 'enroll/' => array(
+ '(?:(?P<pageKey>[^/]+)/)?(?:(?P<formSaved>saved)/)?'
+ => 'PhabricatorAuthNeedsMultiFactorController',
+ ),
'sshkey/' => array(
$this->getQueryRoutePattern('for/(?P<forPHID>[^/]+)/')
=> 'PhabricatorAuthSSHKeyListController',
'generate/' => 'PhabricatorAuthSSHKeyGenerateController',
'upload/' => 'PhabricatorAuthSSHKeyEditController',
'edit/(?P<id>\d+)/' => 'PhabricatorAuthSSHKeyEditController',
'revoke/(?P<id>\d+)/'
=> 'PhabricatorAuthSSHKeyRevokeController',
'view/(?P<id>\d+)/' => 'PhabricatorAuthSSHKeyViewController',
),
'password/' => 'PhabricatorAuthSetPasswordController',
+
+ 'mfa/' => array(
+ $this->getQueryRoutePattern() =>
+ 'PhabricatorAuthFactorProviderListController',
+ $this->getEditRoutePattern('edit/') =>
+ 'PhabricatorAuthFactorProviderEditController',
+ '(?P<id>[1-9]\d*)/' =>
+ 'PhabricatorAuthFactorProviderViewController',
+ 'message/(?P<id>[1-9]\d*)/' =>
+ 'PhabricatorAuthFactorProviderMessageController',
+ ),
+
+ 'message/' => array(
+ $this->getQueryRoutePattern() =>
+ 'PhabricatorAuthMessageListController',
+ $this->getEditRoutePattern('edit/') =>
+ 'PhabricatorAuthMessageEditController',
+ '(?P<id>[1-9]\d*)/' =>
+ 'PhabricatorAuthMessageViewController',
+ ),
+
+ 'contact/' => array(
+ $this->getEditRoutePattern('edit/') =>
+ 'PhabricatorAuthContactNumberEditController',
+ '(?P<id>[1-9]\d*)/' =>
+ 'PhabricatorAuthContactNumberViewController',
+ '(?P<action>disable|enable)/(?P<id>[1-9]\d*)/' =>
+ 'PhabricatorAuthContactNumberDisableController',
+ 'primary/(?P<id>[1-9]\d*)/' =>
+ 'PhabricatorAuthContactNumberPrimaryController',
+ 'test/(?P<id>[1-9]\d*)/' =>
+ 'PhabricatorAuthContactNumberTestController',
+ ),
),
'/oauth/(?P<provider>\w+)/login/'
=> 'PhabricatorAuthOldOAuthRedirectController',
'/login/' => array(
'' => 'PhabricatorAuthStartController',
'email/' => 'PhabricatorEmailLoginController',
'once/'.
'(?P<type>[^/]+)/'.
'(?P<id>\d+)/'.
'(?P<key>[^/]+)/'.
'(?:(?P<emailID>\d+)/)?' => 'PhabricatorAuthOneTimeLoginController',
'refresh/' => 'PhabricatorRefreshCSRFController',
'mustverify/' => 'PhabricatorMustVerifyEmailController',
),
'/emailverify/(?P<code>[^/]+)/'
=> 'PhabricatorEmailVerificationController',
'/logout/' => 'PhabricatorLogoutController',
);
}
protected function getCustomCapabilities() {
return array(
AuthManageProvidersCapability::CAPABILITY => array(
'default' => PhabricatorPolicies::POLICY_ADMIN,
),
);
}
}
diff --git a/src/applications/auth/constants/PhabricatorAuthFactorProviderStatus.php b/src/applications/auth/constants/PhabricatorAuthFactorProviderStatus.php
new file mode 100644
index 000000000..61d4a1257
--- /dev/null
+++ b/src/applications/auth/constants/PhabricatorAuthFactorProviderStatus.php
@@ -0,0 +1,103 @@
+<?php
+
+final class PhabricatorAuthFactorProviderStatus
+ extends Phobject {
+
+ private $key;
+ private $spec = array();
+
+ const STATUS_ACTIVE = 'active';
+ const STATUS_DEPRECATED = 'deprecated';
+ const STATUS_DISABLED = 'disabled';
+
+ public static function newForStatus($status) {
+ $result = new self();
+
+ $result->key = $status;
+ $result->spec = self::newSpecification($status);
+
+ return $result;
+ }
+
+ public function getName() {
+ return idx($this->spec, 'name', $this->key);
+ }
+
+ public function getStatusHeaderIcon() {
+ return idx($this->spec, 'header.icon');
+ }
+
+ public function getStatusHeaderColor() {
+ return idx($this->spec, 'header.color');
+ }
+
+ public function isActive() {
+ return ($this->key === self::STATUS_ACTIVE);
+ }
+
+ public function getListIcon() {
+ return idx($this->spec, 'list.icon');
+ }
+
+ public function getListColor() {
+ return idx($this->spec, 'list.color');
+ }
+
+ public function getFactorIcon() {
+ return idx($this->spec, 'factor.icon');
+ }
+
+ public function getFactorColor() {
+ return idx($this->spec, 'factor.color');
+ }
+
+ public function getOrder() {
+ return idx($this->spec, 'order', 0);
+ }
+
+ public static function getMap() {
+ $specs = self::newSpecifications();
+ return ipull($specs, 'name');
+ }
+
+ private static function newSpecification($key) {
+ $specs = self::newSpecifications();
+ return idx($specs, $key, array());
+ }
+
+ private static function newSpecifications() {
+ return array(
+ self::STATUS_ACTIVE => array(
+ 'name' => pht('Active'),
+ 'header.icon' => 'fa-check',
+ 'header.color' => null,
+ 'list.icon' => null,
+ 'list.color' => null,
+ 'factor.icon' => 'fa-check',
+ 'factor.color' => 'green',
+ 'order' => 1,
+ ),
+ self::STATUS_DEPRECATED => array(
+ 'name' => pht('Deprecated'),
+ 'header.icon' => 'fa-ban',
+ 'header.color' => 'indigo',
+ 'list.icon' => 'fa-ban',
+ 'list.color' => 'indigo',
+ 'factor.icon' => 'fa-ban',
+ 'factor.color' => 'indigo',
+ 'order' => 2,
+ ),
+ self::STATUS_DISABLED => array(
+ 'name' => pht('Disabled'),
+ 'header.icon' => 'fa-times',
+ 'header.color' => 'red',
+ 'list.icon' => 'fa-times',
+ 'list.color' => 'red',
+ 'factor.icon' => 'fa-times',
+ 'factor.color' => 'grey',
+ 'order' => 3,
+ ),
+ );
+ }
+
+}
diff --git a/src/applications/auth/controller/PhabricatorAuthController.php b/src/applications/auth/controller/PhabricatorAuthController.php
index 0ed86e305..9b7267ec9 100644
--- a/src/applications/auth/controller/PhabricatorAuthController.php
+++ b/src/applications/auth/controller/PhabricatorAuthController.php
@@ -1,295 +1,289 @@
<?php
abstract class PhabricatorAuthController extends PhabricatorController {
protected function renderErrorPage($title, array $messages) {
$view = new PHUIInfoView();
$view->setTitle($title);
$view->setErrors($messages);
return $this->newPage()
->setTitle($title)
->appendChild($view);
}
/**
* Returns true if this install is newly setup (i.e., there are no user
* accounts yet). In this case, we enter a special mode to permit creation
* of the first account form the web UI.
*/
protected function isFirstTimeSetup() {
// If there are any auth providers, this isn't first time setup, even if
// we don't have accounts.
if (PhabricatorAuthProvider::getAllEnabledProviders()) {
return false;
}
// Otherwise, check if there are any user accounts. If not, we're in first
// time setup.
$any_users = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->setLimit(1)
->execute();
return !$any_users;
}
/**
* Log a user into a web session and return an @{class:AphrontResponse} which
* corresponds to continuing the login process.
*
* Normally, this is a redirect to the validation controller which makes sure
* the user's cookies are set. However, event listeners can intercept this
* event and do something else if they prefer.
*
* @param PhabricatorUser User to log the viewer in as.
+ * @param bool True to issue a full session immediately, bypassing MFA.
* @return AphrontResponse Response which continues the login process.
*/
- protected function loginUser(PhabricatorUser $user) {
+ protected function loginUser(
+ PhabricatorUser $user,
+ $force_full_session = false) {
$response = $this->buildLoginValidateResponse($user);
$session_type = PhabricatorAuthSession::TYPE_WEB;
- $event_type = PhabricatorEventType::TYPE_AUTH_WILLLOGINUSER;
- $event_data = array(
- 'user' => $user,
- 'type' => $session_type,
- 'response' => $response,
- 'shouldLogin' => true,
- );
-
- $event = id(new PhabricatorEvent($event_type, $event_data))
- ->setUser($user);
- PhutilEventEngine::dispatchEvent($event);
-
- $should_login = $event->getValue('shouldLogin');
- if ($should_login) {
- $session_key = id(new PhabricatorAuthSessionEngine())
- ->establishSession($session_type, $user->getPHID(), $partial = true);
-
- // NOTE: We allow disabled users to login and roadblock them later, so
- // there's no check for users being disabled here.
-
- $request = $this->getRequest();
- $request->setCookie(
- PhabricatorCookies::COOKIE_USERNAME,
- $user->getUsername());
- $request->setCookie(
- PhabricatorCookies::COOKIE_SESSION,
- $session_key);
-
- $this->clearRegistrationCookies();
+ if ($force_full_session) {
+ $partial_session = false;
+ } else {
+ $partial_session = true;
}
- return $event->getValue('response');
+ $session_key = id(new PhabricatorAuthSessionEngine())
+ ->establishSession($session_type, $user->getPHID(), $partial_session);
+
+ // NOTE: We allow disabled users to login and roadblock them later, so
+ // there's no check for users being disabled here.
+
+ $request = $this->getRequest();
+ $request->setCookie(
+ PhabricatorCookies::COOKIE_USERNAME,
+ $user->getUsername());
+ $request->setCookie(
+ PhabricatorCookies::COOKIE_SESSION,
+ $session_key);
+
+ $this->clearRegistrationCookies();
+
+ return $response;
}
protected function clearRegistrationCookies() {
$request = $this->getRequest();
// Clear the registration key.
$request->clearCookie(PhabricatorCookies::COOKIE_REGISTRATION);
// Clear the client ID / OAuth state key.
$request->clearCookie(PhabricatorCookies::COOKIE_CLIENTID);
// Clear the invite cookie.
$request->clearCookie(PhabricatorCookies::COOKIE_INVITE);
}
private function buildLoginValidateResponse(PhabricatorUser $user) {
$validate_uri = new PhutilURI($this->getApplicationURI('validate/'));
$validate_uri->setQueryParam('expect', $user->getUsername());
return id(new AphrontRedirectResponse())->setURI((string)$validate_uri);
}
protected function renderError($message) {
return $this->renderErrorPage(
pht('Authentication Error'),
array(
$message,
));
}
protected function loadAccountForRegistrationOrLinking($account_key) {
$request = $this->getRequest();
$viewer = $request->getUser();
$account = null;
$provider = null;
$response = null;
if (!$account_key) {
$response = $this->renderError(
pht('Request did not include account key.'));
return array($account, $provider, $response);
}
// NOTE: We're using the omnipotent user because the actual user may not
// be logged in yet, and because we want to tailor an error message to
// distinguish between "not usable" and "does not exist". We do explicit
// checks later on to make sure this account is valid for the intended
// operation. This requires edit permission for completeness and consistency
// but it won't actually be meaningfully checked because we're using the
// omnipotent user.
$account = id(new PhabricatorExternalAccountQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withAccountSecrets(array($account_key))
->needImages(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$account) {
$response = $this->renderError(pht('No valid linkable account.'));
return array($account, $provider, $response);
}
if ($account->getUserPHID()) {
if ($account->getUserPHID() != $viewer->getPHID()) {
$response = $this->renderError(
pht(
'The account you are attempting to register or link is already '.
'linked to another user.'));
} else {
$response = $this->renderError(
pht(
'The account you are attempting to link is already linked '.
'to your account.'));
}
return array($account, $provider, $response);
}
$registration_key = $request->getCookie(
PhabricatorCookies::COOKIE_REGISTRATION);
// NOTE: This registration key check is not strictly necessary, because
// we're only creating new accounts, not linking existing accounts. It
// might be more hassle than it is worth, especially for email.
//
// The attack this prevents is getting to the registration screen, then
// copy/pasting the URL and getting someone else to click it and complete
// the process. They end up with an account bound to credentials you
// control. This doesn't really let you do anything meaningful, though,
// since you could have simply completed the process yourself.
if (!$registration_key) {
$response = $this->renderError(
pht(
'Your browser did not submit a registration key with the request. '.
'You must use the same browser to begin and complete registration. '.
'Check that cookies are enabled and try again.'));
return array($account, $provider, $response);
}
// We store the digest of the key rather than the key itself to prevent a
// theoretical attacker with read-only access to the database from
// hijacking registration sessions.
$actual = $account->getProperty('registrationKey');
$expect = PhabricatorHash::weakDigest($registration_key);
if (!phutil_hashes_are_identical($actual, $expect)) {
$response = $this->renderError(
pht(
'Your browser submitted a different registration key than the one '.
'associated with this account. You may need to clear your cookies.'));
return array($account, $provider, $response);
}
$other_account = id(new PhabricatorExternalAccount())->loadAllWhere(
'accountType = %s AND accountDomain = %s AND accountID = %s
AND id != %d',
$account->getAccountType(),
$account->getAccountDomain(),
$account->getAccountID(),
$account->getID());
if ($other_account) {
$response = $this->renderError(
pht(
'The account you are attempting to register with already belongs '.
'to another user.'));
return array($account, $provider, $response);
}
$provider = PhabricatorAuthProvider::getEnabledProviderByKey(
$account->getProviderKey());
if (!$provider) {
$response = $this->renderError(
pht(
'The account you are attempting to register with uses a nonexistent '.
'or disabled authentication provider (with key "%s"). An '.
'administrator may have recently disabled this provider.',
$account->getProviderKey()));
return array($account, $provider, $response);
}
return array($account, $provider, null);
}
protected function loadInvite() {
$invite_cookie = PhabricatorCookies::COOKIE_INVITE;
$invite_code = $this->getRequest()->getCookie($invite_cookie);
if (!$invite_code) {
return null;
}
$engine = id(new PhabricatorAuthInviteEngine())
->setViewer($this->getViewer())
->setUserHasConfirmedVerify(true);
try {
return $engine->processInviteCode($invite_code);
} catch (Exception $ex) {
// If this fails for any reason, just drop the invite. In normal
// circumstances, we gave them a detailed explanation of any error
// before they jumped into this workflow.
return null;
}
}
protected function renderInviteHeader(PhabricatorAuthInvite $invite) {
$viewer = $this->getViewer();
// Since the user hasn't registered yet, they may not be able to see other
// user accounts. Load the inviting user with the omnipotent viewer.
$omnipotent_viewer = PhabricatorUser::getOmnipotentUser();
$invite_author = id(new PhabricatorPeopleQuery())
->setViewer($omnipotent_viewer)
->withPHIDs(array($invite->getAuthorPHID()))
->needProfileImage(true)
->executeOne();
// If we can't load the author for some reason, just drop this message.
// We lose the value of contextualizing things without author details.
if (!$invite_author) {
return null;
}
$invite_item = id(new PHUIObjectItemView())
->setHeader(pht('Welcome to Phabricator!'))
->setImageURI($invite_author->getProfileImageURI())
->addAttribute(
pht(
'%s has invited you to join Phabricator.',
$invite_author->getFullName()));
$invite_list = id(new PHUIObjectItemListView())
->addItem($invite_item)
->setFlush(true);
return id(new PHUIBoxView())
->addMargin(PHUI::MARGIN_LARGE)
->appendChild($invite_list);
}
}
diff --git a/src/applications/auth/controller/PhabricatorAuthLoginController.php b/src/applications/auth/controller/PhabricatorAuthLoginController.php
index 39b631848..54649a6a6 100644
--- a/src/applications/auth/controller/PhabricatorAuthLoginController.php
+++ b/src/applications/auth/controller/PhabricatorAuthLoginController.php
@@ -1,266 +1,267 @@
<?php
final class PhabricatorAuthLoginController
extends PhabricatorAuthController {
private $providerKey;
private $extraURIData;
private $provider;
public function shouldRequireLogin() {
return false;
}
public function shouldAllowRestrictedParameter($parameter_name) {
// Whitelist the OAuth 'code' parameter.
if ($parameter_name == 'code') {
return true;
}
+
return parent::shouldAllowRestrictedParameter($parameter_name);
}
public function getExtraURIData() {
return $this->extraURIData;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$this->providerKey = $request->getURIData('pkey');
$this->extraURIData = $request->getURIData('extra');
$response = $this->loadProvider();
if ($response) {
return $response;
}
$provider = $this->provider;
try {
list($account, $response) = $provider->processLoginRequest($this);
} catch (PhutilAuthUserAbortedException $ex) {
if ($viewer->isLoggedIn()) {
// If a logged-in user cancels, take them back to the external accounts
// panel.
$next_uri = '/settings/panel/external/';
} else {
// If a logged-out user cancels, take them back to the auth start page.
$next_uri = '/';
}
// User explicitly hit "Cancel".
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle(pht('Authentication Canceled'))
->appendChild(
pht('You canceled authentication.'))
->addCancelButton($next_uri, pht('Continue'));
return id(new AphrontDialogResponse())->setDialog($dialog);
}
if ($response) {
return $response;
}
if (!$account) {
throw new Exception(
pht(
'Auth provider failed to load an account from %s!',
'processLoginRequest()'));
}
if ($account->getUserPHID()) {
// The account is already attached to a Phabricator user, so this is
// either a login or a bad account link request.
if (!$viewer->isLoggedIn()) {
if ($provider->shouldAllowLogin()) {
return $this->processLoginUser($account);
} else {
return $this->renderError(
pht(
'The external account ("%s") you just authenticated with is '.
'not configured to allow logins on this Phabricator install. '.
'An administrator may have recently disabled it.',
$provider->getProviderName()));
}
} else if ($viewer->getPHID() == $account->getUserPHID()) {
// This is either an attempt to re-link an existing and already
// linked account (which is silly) or a refresh of an external account
// (e.g., an OAuth account).
return id(new AphrontRedirectResponse())
->setURI('/settings/panel/external/');
} else {
return $this->renderError(
pht(
'The external account ("%s") you just used to log in is already '.
'associated with another Phabricator user account. Log in to the '.
'other Phabricator account and unlink the external account before '.
'linking it to a new Phabricator account.',
$provider->getProviderName()));
}
} else {
// The account is not yet attached to a Phabricator user, so this is
// either a registration or an account link request.
if (!$viewer->isLoggedIn()) {
if ($provider->shouldAllowRegistration()) {
return $this->processRegisterUser($account);
} else {
return $this->renderError(
pht(
'The external account ("%s") you just authenticated with is '.
'not configured to allow registration on this Phabricator '.
'install. An administrator may have recently disabled it.',
$provider->getProviderName()));
}
} else {
// If the user already has a linked account of this type, prevent them
// from linking a second account. This can happen if they swap logins
// and then refresh the account link. See T6707. We will eventually
// allow this after T2549.
$existing_accounts = id(new PhabricatorExternalAccountQuery())
->setViewer($viewer)
->withUserPHIDs(array($viewer->getPHID()))
->withAccountTypes(array($account->getAccountType()))
->execute();
if ($existing_accounts) {
return $this->renderError(
pht(
'Your Phabricator account is already connected to an external '.
'account on this provider ("%s"), but you are currently logged '.
'in to the provider with a different account. Log out of the '.
'external service, then log back in with the correct account '.
'before refreshing the account link.',
$provider->getProviderName()));
}
if ($provider->shouldAllowAccountLink()) {
return $this->processLinkUser($account);
} else {
return $this->renderError(
pht(
'The external account ("%s") you just authenticated with is '.
'not configured to allow account linking on this Phabricator '.
'install. An administrator may have recently disabled it.',
$provider->getProviderName()));
}
}
}
// This should be unreachable, but fail explicitly if we get here somehow.
return new Aphront400Response();
}
private function processLoginUser(PhabricatorExternalAccount $account) {
$user = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$account->getUserPHID());
if (!$user) {
return $this->renderError(
pht(
'The external account you just logged in with is not associated '.
'with a valid Phabricator user.'));
}
return $this->loginUser($user);
}
private function processRegisterUser(PhabricatorExternalAccount $account) {
$account_secret = $account->getAccountSecret();
$register_uri = $this->getApplicationURI('register/'.$account_secret.'/');
return $this->setAccountKeyAndContinue($account, $register_uri);
}
private function processLinkUser(PhabricatorExternalAccount $account) {
$account_secret = $account->getAccountSecret();
$confirm_uri = $this->getApplicationURI('confirmlink/'.$account_secret.'/');
return $this->setAccountKeyAndContinue($account, $confirm_uri);
}
private function setAccountKeyAndContinue(
PhabricatorExternalAccount $account,
$next_uri) {
if ($account->getUserPHID()) {
throw new Exception(pht('Account is already registered or linked.'));
}
// Regenerate the registration secret key, set it on the external account,
// set a cookie on the user's machine, and redirect them to registration.
// See PhabricatorAuthRegisterController for discussion of the registration
// key.
$registration_key = Filesystem::readRandomCharacters(32);
$account->setProperty(
'registrationKey',
PhabricatorHash::weakDigest($registration_key));
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$account->save();
unset($unguarded);
$this->getRequest()->setTemporaryCookie(
PhabricatorCookies::COOKIE_REGISTRATION,
$registration_key);
return id(new AphrontRedirectResponse())->setURI($next_uri);
}
private function loadProvider() {
$provider = PhabricatorAuthProvider::getEnabledProviderByKey(
$this->providerKey);
if (!$provider) {
return $this->renderError(
pht(
'The account you are attempting to log in with uses a nonexistent '.
'or disabled authentication provider (with key "%s"). An '.
'administrator may have recently disabled this provider.',
$this->providerKey));
}
$this->provider = $provider;
return null;
}
protected function renderError($message) {
return $this->renderErrorPage(
pht('Login Failed'),
array($message));
}
public function buildProviderPageResponse(
PhabricatorAuthProvider $provider,
$content) {
$crumbs = $this->buildApplicationCrumbs();
if ($this->getRequest()->getUser()->isLoggedIn()) {
$crumbs->addTextCrumb(pht('Link Account'), $provider->getSettingsURI());
} else {
$crumbs->addTextCrumb(pht('Log In'), $this->getApplicationURI('start/'));
}
$crumbs->addTextCrumb($provider->getProviderName());
$crumbs->setBorder(true);
return $this->newPage()
->setTitle(pht('Log In'))
->setCrumbs($crumbs)
->appendChild($content);
}
public function buildProviderErrorResponse(
PhabricatorAuthProvider $provider,
$message) {
$message = pht(
'Authentication provider ("%s") encountered an error while attempting '.
'to log in. %s', $provider->getProviderName(), $message);
return $this->renderError($message);
}
}
diff --git a/src/applications/auth/controller/PhabricatorAuthNeedsMultiFactorController.php b/src/applications/auth/controller/PhabricatorAuthNeedsMultiFactorController.php
index 27e03485c..259e4c674 100644
--- a/src/applications/auth/controller/PhabricatorAuthNeedsMultiFactorController.php
+++ b/src/applications/auth/controller/PhabricatorAuthNeedsMultiFactorController.php
@@ -1,109 +1,250 @@
<?php
final class PhabricatorAuthNeedsMultiFactorController
extends PhabricatorAuthController {
public function shouldRequireMultiFactorEnrollment() {
// Users need access to this controller in order to enroll in multi-factor
// auth.
return false;
}
public function shouldRequireEnabledUser() {
// Users who haven't been approved yet are allowed to enroll in MFA. We'll
// kick disabled users out later.
return false;
}
public function shouldRequireEmailVerification() {
// Users who haven't verified their email addresses yet can still enroll
// in MFA.
return false;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
if ($viewer->getIsDisabled()) {
// We allowed unapproved and disabled users to hit this controller, but
// want to kick out disabled users now.
return new Aphront400Response();
}
- $panel = id(new PhabricatorMultiFactorSettingsPanel())
- ->setUser($viewer)
- ->setViewer($viewer)
- ->setOverrideURI($this->getApplicationURI('/multifactor/'))
- ->processRequest($request);
+ $panels = $this->loadPanels();
+
+ $multifactor_key = id(new PhabricatorMultiFactorSettingsPanel())
+ ->getPanelKey();
+
+ $panel_key = $request->getURIData('pageKey');
+ if (!strlen($panel_key)) {
+ $panel_key = $multifactor_key;
+ }
- if ($panel instanceof AphrontResponse) {
- return $panel;
+ if (!isset($panels[$panel_key])) {
+ return new Aphront404Response();
}
- $crumbs = $this->buildApplicationCrumbs();
- $crumbs->addTextCrumb(pht('Add Multi-Factor Auth'));
+ $nav = $this->newNavigation();
+ $nav->selectFilter($panel_key);
+
+ $panel = $panels[$panel_key];
$viewer->updateMultiFactorEnrollment();
- if (!$viewer->getIsEnrolledInMultiFactor()) {
- $help = id(new PHUIInfoView())
- ->setTitle(pht('Add Multi-Factor Authentication To Your Account'))
- ->setSeverity(PHUIInfoView::SEVERITY_WARNING)
- ->setErrors(
- array(
- pht(
- 'Before you can use Phabricator, you need to add multi-factor '.
- 'authentication to your account.'),
- pht(
- 'Multi-factor authentication helps secure your account by '.
- 'making it more difficult for attackers to gain access or '.
- 'take sensitive actions.'),
- pht(
- 'To learn more about multi-factor authentication, click the '.
- '%s button below.',
- phutil_tag('strong', array(), pht('Help'))),
- pht(
- 'To add an authentication factor, click the %s button below.',
- phutil_tag('strong', array(), pht('Add Authentication Factor'))),
- pht(
- 'To continue, add at least one authentication factor to your '.
- 'account.'),
- ));
+ if ($panel_key === $multifactor_key) {
+ $header_text = pht('Add Multi-Factor Auth');
+ $help = $this->newGuidance();
+ $panel->setIsEnrollment(true);
} else {
- $help = id(new PHUIInfoView())
- ->setTitle(pht('Multi-Factor Authentication Configured'))
- ->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
- ->setErrors(
- array(
- pht(
- 'You have successfully configured multi-factor authentication '.
- 'for your account.'),
- pht(
- 'You can make adjustments from the Settings panel later.'),
- pht(
- 'When you are ready, %s.',
- phutil_tag(
- 'strong',
- array(),
- phutil_tag(
- 'a',
- array(
- 'href' => '/',
- ),
- pht('continue to Phabricator')))),
- ));
+ $header_text = $panel->getPanelName();
+ $help = null;
+ }
+
+ $response = $panel
+ ->setController($this)
+ ->setNavigation($nav)
+ ->processRequest($request);
+
+ if (($response instanceof AphrontResponse) ||
+ ($response instanceof AphrontResponseProducerInterface)) {
+ return $response;
}
- $view = array(
- $help,
- $panel,
- );
+ $crumbs = $this->buildApplicationCrumbs()
+ ->addTextCrumb(pht('Add Multi-Factor Auth'))
+ ->setBorder(true);
+
+ $header = id(new PHUIHeaderView())
+ ->setHeader($header_text);
+
+ $view = id(new PHUITwoColumnView())
+ ->setHeader($header)
+ ->setFooter(
+ array(
+ $help,
+ $response,
+ ));
return $this->newPage()
->setTitle(pht('Add Multi-Factor Authentication'))
->setCrumbs($crumbs)
+ ->setNavigation($nav)
->appendChild($view);
}
+ private function loadPanels() {
+ $viewer = $this->getViewer();
+ $preferences = PhabricatorUserPreferences::loadUserPreferences($viewer);
+
+ $panels = PhabricatorSettingsPanel::getAllDisplayPanels();
+ $base_uri = $this->newEnrollBaseURI();
+
+ $result = array();
+ foreach ($panels as $key => $panel) {
+ $panel
+ ->setPreferences($preferences)
+ ->setViewer($viewer)
+ ->setUser($viewer)
+ ->setOverrideURI(urisprintf('%s%s/', $base_uri, $key));
+
+ if (!$panel->isEnabled()) {
+ continue;
+ }
+
+ if (!$panel->isUserPanel()) {
+ continue;
+ }
+
+ if (!$panel->isMultiFactorEnrollmentPanel()) {
+ continue;
+ }
+
+ if (!empty($result[$key])) {
+ throw new Exception(pht(
+ "Two settings panels share the same panel key ('%s'): %s, %s.",
+ $key,
+ get_class($panel),
+ get_class($result[$key])));
+ }
+
+ $result[$key] = $panel;
+ }
+
+ return $result;
+ }
+
+
+ private function newNavigation() {
+ $viewer = $this->getViewer();
+
+ $enroll_uri = $this->newEnrollBaseURI();
+
+ $nav = id(new AphrontSideNavFilterView())
+ ->setBaseURI(new PhutilURI($enroll_uri));
+
+ $multifactor_key = id(new PhabricatorMultiFactorSettingsPanel())
+ ->getPanelKey();
+
+ $nav->addFilter(
+ $multifactor_key,
+ pht('Enroll in MFA'),
+ null,
+ 'fa-exclamation-triangle blue');
+
+ $panels = $this->loadPanels();
+
+ if ($panels) {
+ $nav->addLabel(pht('Settings'));
+ }
+
+ foreach ($panels as $panel_key => $panel) {
+ if ($panel_key === $multifactor_key) {
+ continue;
+ }
+
+ $nav->addFilter(
+ $panel->getPanelKey(),
+ $panel->getPanelName(),
+ null,
+ $panel->getPanelMenuIcon());
+ }
+
+ return $nav;
+ }
+
+ private function newEnrollBaseURI() {
+ return $this->getApplicationURI('enroll/');
+ }
+
+ private function newGuidance() {
+ $viewer = $this->getViewer();
+
+ if ($viewer->getIsEnrolledInMultiFactor()) {
+ $guidance = pht(
+ '{icon check, color="green"} **Setup Complete!**'.
+ "\n\n".
+ 'You have successfully configured multi-factor authentication '.
+ 'for your account.'.
+ "\n\n".
+ 'You can make adjustments from the [[ /settings/ | Settings ]] panel '.
+ 'later.');
+
+ return $this->newDialog()
+ ->setTitle(pht('Multi-Factor Authentication Setup Complete'))
+ ->setWidth(AphrontDialogView::WIDTH_FULL)
+ ->appendChild(new PHUIRemarkupView($viewer, $guidance))
+ ->addCancelButton('/', pht('Continue'));
+ }
+
+ $views = array();
+
+ $messages = array();
+
+ $messages[] = pht(
+ 'Before you can use Phabricator, you need to add multi-factor '.
+ 'authentication to your account. Multi-factor authentication helps '.
+ 'secure your account by making it more difficult for attackers to '.
+ 'gain access or take sensitive actions.');
+
+ $view = id(new PHUIInfoView())
+ ->setTitle(pht('Add Multi-Factor Authentication To Your Account'))
+ ->setSeverity(PHUIInfoView::SEVERITY_WARNING)
+ ->setErrors($messages);
+
+ $views[] = $view;
+
+
+ $providers = id(new PhabricatorAuthFactorProviderQuery())
+ ->setViewer($viewer)
+ ->withStatuses(
+ array(
+ PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE,
+ ))
+ ->execute();
+ if (!$providers) {
+ $messages = array();
+
+ $required_key = 'security.require-multi-factor-auth';
+
+ $messages[] = pht(
+ 'This install has the configuration option "%s" enabled, but does '.
+ 'not have any active multifactor providers configured. This means '.
+ 'you are required to add MFA, but are also prevented from doing so. '.
+ 'An administrator must disable "%s" or enable an MFA provider to '.
+ 'allow you to continue.',
+ $required_key,
+ $required_key);
+
+ $view = id(new PHUIInfoView())
+ ->setTitle(pht('Multi-Factor Authentication is Misconfigured'))
+ ->setSeverity(PHUIInfoView::SEVERITY_ERROR)
+ ->setErrors($messages);
+
+ $views[] = $view;
+ }
+
+ return $views;
+ }
+
}
diff --git a/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php b/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php
index 9f74d5076..0cac95f53 100644
--- a/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php
+++ b/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php
@@ -1,204 +1,209 @@
<?php
final class PhabricatorAuthOneTimeLoginController
extends PhabricatorAuthController {
public function shouldRequireLogin() {
return false;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$link_type = $request->getURIData('type');
$key = $request->getURIData('key');
$email_id = $request->getURIData('emailID');
if ($request->getUser()->isLoggedIn()) {
return $this->renderError(
pht('You are already logged in.'));
}
$target_user = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIDs(array($id))
->executeOne();
if (!$target_user) {
return new Aphront404Response();
}
// NOTE: As a convenience to users, these one-time login URIs may also
// be associated with an email address which will be verified when the
// URI is used.
// This improves the new user experience for users receiving "Welcome"
// emails on installs that require verification: if we did not verify the
// email, they'd immediately get roadblocked with a "Verify Your Email"
// error and have to go back to their email account, wait for a
// "Verification" email, and then click that link to actually get access to
// their account. This is hugely unwieldy, and if the link was only sent
// to the user's email in the first place we can safely verify it as a
// side effect of login.
// The email hashed into the URI so users can't verify some email they
// do not own by doing this:
//
// - Add some address you do not own;
// - request a password reset;
// - change the URI in the email to the address you don't own;
// - login via the email link; and
// - get a "verified" address you don't control.
$target_email = null;
if ($email_id) {
$target_email = id(new PhabricatorUserEmail())->loadOneWhere(
'userPHID = %s AND id = %d',
$target_user->getPHID(),
$email_id);
if (!$target_email) {
return new Aphront404Response();
}
}
$engine = new PhabricatorAuthSessionEngine();
$token = $engine->loadOneTimeLoginKey(
$target_user,
$target_email,
$key);
if (!$token) {
return $this->newDialog()
->setTitle(pht('Unable to Log In'))
->setShortTitle(pht('Login Failure'))
->appendParagraph(
pht(
'The login link you clicked is invalid, out of date, or has '.
'already been used.'))
->appendParagraph(
pht(
'Make sure you are copy-and-pasting the entire link into '.
'your browser. Login links are only valid for 24 hours, and '.
'can only be used once.'))
->appendParagraph(
pht('You can try again, or request a new link via email.'))
->addCancelButton('/login/email/', pht('Send Another Email'));
}
if (!$target_user->canEstablishWebSessions()) {
return $this->newDialog()
->setTitle(pht('Unable to Establish Web Session'))
->setShortTitle(pht('Login Failure'))
->appendParagraph(
pht(
'You are trying to gain access to an account ("%s") that can not '.
'establish a web session.',
$target_user->getUsername()))
->appendParagraph(
pht(
'Special users like daemons and mailing lists are not permitted '.
'to log in via the web. Log in as a normal user instead.'))
->addCancelButton('/');
}
if ($request->isFormPost()) {
// If we have an email bound into this URI, verify email so that clicking
// the link in the "Welcome" email is good enough, without requiring users
// to go through a second round of email verification.
$editor = id(new PhabricatorUserEditor())
->setActor($target_user);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
// Nuke the token and all other outstanding password reset tokens.
// There is no particular security benefit to destroying them all, but
// it should reduce HackerOne reports of nebulous harm.
$editor->revokePasswordResetLinks($target_user);
if ($target_email) {
$editor->verifyEmail($target_user, $target_email);
}
unset($unguarded);
$next = '/';
if (!PhabricatorPasswordAuthProvider::getPasswordProvider()) {
$next = '/settings/panel/external/';
} else {
// We're going to let the user reset their password without knowing
// the old one. Generate a one-time token for that.
$key = Filesystem::readRandomCharacters(16);
$password_type =
PhabricatorAuthPasswordResetTemporaryTokenType::TOKENTYPE;
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
id(new PhabricatorAuthTemporaryToken())
->setTokenResource($target_user->getPHID())
->setTokenType($password_type)
->setTokenExpires(time() + phutil_units('1 hour in seconds'))
->setTokenCode(PhabricatorHash::weakDigest($key))
->save();
unset($unguarded);
$panel_uri = '/auth/password/';
$next = (string)id(new PhutilURI($panel_uri))
->setQueryParams(
array(
'key' => $key,
));
$request->setTemporaryCookie(PhabricatorCookies::COOKIE_HISEC, 'yes');
}
PhabricatorCookies::setNextURICookie($request, $next, $force = true);
- return $this->loginUser($target_user);
+ $force_full_session = false;
+ if ($link_type === PhabricatorAuthSessionEngine::ONETIME_RECOVER) {
+ $force_full_session = $token->getShouldForceFullSession();
+ }
+
+ return $this->loginUser($target_user, $force_full_session);
}
// NOTE: We need to CSRF here so attackers can't generate an email link,
// then log a user in to an account they control via sneaky invisible
// form submissions.
switch ($link_type) {
case PhabricatorAuthSessionEngine::ONETIME_WELCOME:
$title = pht('Welcome to Phabricator');
break;
case PhabricatorAuthSessionEngine::ONETIME_RECOVER:
$title = pht('Account Recovery');
break;
case PhabricatorAuthSessionEngine::ONETIME_USERNAME:
case PhabricatorAuthSessionEngine::ONETIME_RESET:
default:
$title = pht('Log in to Phabricator');
break;
}
$body = array();
$body[] = pht(
'Use the button below to log in as: %s',
phutil_tag('strong', array(), $target_user->getUsername()));
if ($target_email && !$target_email->getIsVerified()) {
$body[] = pht(
'Logging in will verify %s as an email address you own.',
phutil_tag('strong', array(), $target_email->getAddress()));
}
$body[] = pht(
'After logging in you should set a password for your account, or '.
'link your account to an external account that you can use to '.
'authenticate in the future.');
$dialog = $this->newDialog()
->setTitle($title)
->addSubmitButton(pht('Log In (%s)', $target_user->getUsername()))
->addCancelButton('/');
foreach ($body as $paragraph) {
$dialog->appendParagraph($paragraph);
}
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/auth/controller/PhabricatorAuthRegisterController.php b/src/applications/auth/controller/PhabricatorAuthRegisterController.php
index 1138d52b3..9e1aef592 100644
--- a/src/applications/auth/controller/PhabricatorAuthRegisterController.php
+++ b/src/applications/auth/controller/PhabricatorAuthRegisterController.php
@@ -1,724 +1,743 @@
<?php
final class PhabricatorAuthRegisterController
extends PhabricatorAuthController {
public function shouldRequireLogin() {
return false;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$account_key = $request->getURIData('akey');
if ($request->getUser()->isLoggedIn()) {
return id(new AphrontRedirectResponse())->setURI('/');
}
$is_setup = false;
if (strlen($account_key)) {
$result = $this->loadAccountForRegistrationOrLinking($account_key);
list($account, $provider, $response) = $result;
$is_default = false;
} else if ($this->isFirstTimeSetup()) {
list($account, $provider, $response) = $this->loadSetupAccount();
$is_default = true;
$is_setup = true;
} else {
list($account, $provider, $response) = $this->loadDefaultAccount();
$is_default = true;
}
if ($response) {
return $response;
}
$invite = $this->loadInvite();
if (!$provider->shouldAllowRegistration()) {
if ($invite) {
// If the user has an invite, we allow them to register with any
// provider, even a login-only provider.
} else {
// TODO: This is a routine error if you click "Login" on an external
// auth source which doesn't allow registration. The error should be
// more tailored.
return $this->renderError(
pht(
'The account you are attempting to register with uses an '.
'authentication provider ("%s") which does not allow '.
'registration. An administrator may have recently disabled '.
'registration with this provider.',
$provider->getProviderName()));
}
}
$errors = array();
$user = new PhabricatorUser();
$default_username = $account->getUsername();
$default_realname = $account->getRealName();
$account_type = PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT;
$content_source = PhabricatorContentSource::newFromRequest($request);
$default_email = $account->getEmail();
if ($invite) {
$default_email = $invite->getEmailAddress();
}
if ($default_email !== null) {
if (!PhabricatorUserEmail::isValidAddress($default_email)) {
$errors[] = pht(
'The email address associated with this external account ("%s") is '.
'not a valid email address and can not be used to register a '.
'Phabricator account. Choose a different, valid address.',
phutil_tag('strong', array(), $default_email));
$default_email = null;
}
}
if ($default_email !== null) {
// We should bypass policy here because e.g. limiting an application use
// to a subset of users should not allow the others to overwrite
// configured application emails.
$application_email = id(new PhabricatorMetaMTAApplicationEmailQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withAddresses(array($default_email))
->executeOne();
if ($application_email) {
$errors[] = pht(
'The email address associated with this account ("%s") is '.
'already in use by an application and can not be used to '.
'register a new Phabricator account. Choose a different, valid '.
'address.',
phutil_tag('strong', array(), $default_email));
$default_email = null;
}
}
$show_existing = null;
if ($default_email !== null) {
// If the account source provided an email, but it's not allowed by
// the configuration, roadblock the user. Previously, we let the user
// pick a valid email address instead, but this does not align well with
// user expectation and it's not clear the cases it enables are valuable.
// See discussion in T3472.
if (!PhabricatorUserEmail::isAllowedAddress($default_email)) {
$debug_email = new PHUIInvisibleCharacterView($default_email);
return $this->renderError(
array(
pht(
'The account you are attempting to register with has an invalid '.
'email address (%s). This Phabricator install only allows '.
'registration with specific email addresses:',
$debug_email),
phutil_tag('br'),
phutil_tag('br'),
PhabricatorUserEmail::describeAllowedAddresses(),
));
}
// If the account source provided an email, but another account already
// has that email, just pretend we didn't get an email.
if ($default_email !== null) {
$same_email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$default_email);
if ($same_email) {
if ($invite) {
// We're allowing this to continue. The fact that we loaded the
// invite means that the address is nonprimary and unverified and
// we're OK to steal it.
} else {
$show_existing = $default_email;
$default_email = null;
}
}
}
}
if ($show_existing !== null) {
if (!$request->getInt('phase')) {
return $this->newDialog()
->setTitle(pht('Email Address Already in Use'))
->addHiddenInput('phase', 1)
->appendParagraph(
pht(
'You are creating a new Phabricator account linked to an '.
'existing external account from outside Phabricator.'))
->appendParagraph(
pht(
'The email address ("%s") associated with the external account '.
'is already in use by an existing Phabricator account. Multiple '.
'Phabricator accounts may not have the same email address, so '.
'you can not use this email address to register a new '.
'Phabricator account.',
phutil_tag('strong', array(), $show_existing)))
->appendParagraph(
pht(
'If you want to register a new account, continue with this '.
'registration workflow and choose a new, unique email address '.
'for the new account.'))
->appendParagraph(
pht(
'If you want to link an existing Phabricator account to this '.
'external account, do not continue. Instead: log in to your '.
'existing account, then go to "Settings" and link the account '.
'in the "External Accounts" panel.'))
->appendParagraph(
pht(
'If you continue, you will create a new account. You will not '.
'be able to link this external account to an existing account.'))
->addCancelButton('/auth/login/', pht('Cancel'))
->addSubmitButton(pht('Create New Account'));
} else {
$errors[] = pht(
'The external account you are registering with has an email address '.
'that is already in use ("%s") by an existing Phabricator account. '.
'Choose a new, valid email address to register a new Phabricator '.
'account.',
phutil_tag('strong', array(), $show_existing));
}
}
$profile = id(new PhabricatorRegistrationProfile())
->setDefaultUsername($default_username)
->setDefaultEmail($default_email)
->setDefaultRealName($default_realname)
->setCanEditUsername(true)
->setCanEditEmail(($default_email === null))
->setCanEditRealName(true)
->setShouldVerifyEmail(false);
$event_type = PhabricatorEventType::TYPE_AUTH_WILLREGISTERUSER;
$event_data = array(
'account' => $account,
'profile' => $profile,
);
$event = id(new PhabricatorEvent($event_type, $event_data))
->setUser($user);
PhutilEventEngine::dispatchEvent($event);
$default_username = $profile->getDefaultUsername();
$default_email = $profile->getDefaultEmail();
$default_realname = $profile->getDefaultRealName();
$can_edit_username = $profile->getCanEditUsername();
$can_edit_email = $profile->getCanEditEmail();
$can_edit_realname = $profile->getCanEditRealName();
$must_set_password = $provider->shouldRequireRegistrationPassword();
$can_edit_anything = $profile->getCanEditAnything() || $must_set_password;
$force_verify = $profile->getShouldVerifyEmail();
// Automatically verify the administrator's email address during first-time
// setup.
if ($is_setup) {
$force_verify = true;
}
$value_username = $default_username;
$value_realname = $default_realname;
$value_email = $default_email;
$value_password = null;
$require_real_name = PhabricatorEnv::getEnvConfig('user.require-real-name');
$e_username = strlen($value_username) ? null : true;
$e_realname = $require_real_name ? true : null;
$e_email = strlen($value_email) ? null : true;
$e_password = true;
$e_captcha = true;
$skip_captcha = false;
if ($invite) {
// If the user is accepting an invite, assume they're trustworthy enough
// that we don't need to CAPTCHA them.
$skip_captcha = true;
}
$min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length');
$min_len = (int)$min_len;
$from_invite = $request->getStr('invite');
if ($from_invite && $can_edit_username) {
$value_username = $request->getStr('username');
$e_username = null;
}
$try_register =
($request->isFormPost() || !$can_edit_anything) &&
!$from_invite &&
($request->getInt('phase') != 1);
if ($try_register) {
$errors = array();
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
if ($must_set_password && !$skip_captcha) {
$e_captcha = pht('Again');
$captcha_ok = AphrontFormRecaptchaControl::processCaptcha($request);
if (!$captcha_ok) {
$errors[] = pht('Captcha response is incorrect, try again.');
$e_captcha = pht('Invalid');
}
}
if ($can_edit_username) {
$value_username = $request->getStr('username');
if (!strlen($value_username)) {
$e_username = pht('Required');
$errors[] = pht('Username is required.');
} else if (!PhabricatorUser::validateUsername($value_username)) {
$e_username = pht('Invalid');
$errors[] = PhabricatorUser::describeValidUsername();
} else {
$e_username = null;
}
}
if ($must_set_password) {
$value_password = $request->getStr('password');
$value_confirm = $request->getStr('confirm');
$password_envelope = new PhutilOpaqueEnvelope($value_password);
$confirm_envelope = new PhutilOpaqueEnvelope($value_confirm);
$engine = id(new PhabricatorAuthPasswordEngine())
->setViewer($user)
->setContentSource($content_source)
->setPasswordType($account_type)
->setObject($user);
try {
$engine->checkNewPassword($password_envelope, $confirm_envelope);
$e_password = null;
} catch (PhabricatorAuthPasswordException $ex) {
$errors[] = $ex->getMessage();
$e_password = $ex->getPasswordError();
}
}
if ($can_edit_email) {
$value_email = $request->getStr('email');
if (!strlen($value_email)) {
$e_email = pht('Required');
$errors[] = pht('Email is required.');
} else if (!PhabricatorUserEmail::isValidAddress($value_email)) {
$e_email = pht('Invalid');
$errors[] = PhabricatorUserEmail::describeValidAddresses();
} else if (!PhabricatorUserEmail::isAllowedAddress($value_email)) {
$e_email = pht('Disallowed');
$errors[] = PhabricatorUserEmail::describeAllowedAddresses();
} else {
$e_email = null;
}
}
if ($can_edit_realname) {
$value_realname = $request->getStr('realName');
if (!strlen($value_realname) && $require_real_name) {
$e_realname = pht('Required');
$errors[] = pht('Real name is required.');
} else {
$e_realname = null;
}
}
if (!$errors) {
$image = $this->loadProfilePicture($account);
if ($image) {
$user->setProfileImagePHID($image->getPHID());
}
try {
$verify_email = false;
if ($force_verify) {
$verify_email = true;
}
if ($value_email === $default_email) {
if ($account->getEmailVerified()) {
$verify_email = true;
}
if ($provider->shouldTrustEmails()) {
$verify_email = true;
}
if ($invite) {
$verify_email = true;
}
}
$email_obj = null;
if ($invite) {
// If we have a valid invite, this email may exist but be
// nonprimary and unverified, so we'll reassign it.
$email_obj = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$value_email);
}
if (!$email_obj) {
$email_obj = id(new PhabricatorUserEmail())
->setAddress($value_email);
}
$email_obj->setIsVerified((int)$verify_email);
$user->setUsername($value_username);
$user->setRealname($value_realname);
if ($is_setup) {
$must_approve = false;
} else if ($invite) {
$must_approve = false;
} else {
$must_approve = PhabricatorEnv::getEnvConfig(
'auth.require-approval');
}
if ($must_approve) {
$user->setIsApproved(0);
} else {
$user->setIsApproved(1);
}
if ($invite) {
$allow_reassign_email = true;
} else {
$allow_reassign_email = false;
}
$user->openTransaction();
$editor = id(new PhabricatorUserEditor())
->setActor($user);
$editor->createNewUser($user, $email_obj, $allow_reassign_email);
if ($must_set_password) {
$password_object = PhabricatorAuthPassword::initializeNewPassword(
$user,
$account_type);
$password_object
->setPassword($password_envelope, $user)
->save();
}
if ($is_setup) {
- $editor->makeAdminUser($user, true);
+ $xactions = array();
+ $xactions[] = id(new PhabricatorUserTransaction())
+ ->setTransactionType(
+ PhabricatorUserEmpowerTransaction::TRANSACTIONTYPE)
+ ->setNewValue(true);
+
+ $actor = PhabricatorUser::getOmnipotentUser();
+ $content_source = PhabricatorContentSource::newFromRequest(
+ $request);
+
+ $people_application_phid = id(new PhabricatorPeopleApplication())
+ ->getPHID();
+
+ $transaction_editor = id(new PhabricatorUserTransactionEditor())
+ ->setActor($actor)
+ ->setActingAsPHID($people_application_phid)
+ ->setContentSource($content_source)
+ ->setContinueOnMissingFields(true);
+
+ $transaction_editor->applyTransactions($user, $xactions);
}
$account->setUserPHID($user->getPHID());
$provider->willRegisterAccount($account);
$account->save();
$user->saveTransaction();
if (!$email_obj->getIsVerified()) {
$email_obj->sendVerificationEmail($user);
}
if ($must_approve) {
$this->sendWaitingForApprovalEmail($user);
}
if ($invite) {
$invite->setAcceptedByPHID($user->getPHID())->save();
}
return $this->loginUser($user);
} catch (AphrontDuplicateKeyQueryException $exception) {
$same_username = id(new PhabricatorUser())->loadOneWhere(
'userName = %s',
$user->getUserName());
$same_email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$value_email);
if ($same_username) {
$e_username = pht('Duplicate');
$errors[] = pht('Another user already has that username.');
}
if ($same_email) {
// TODO: See T3340.
$e_email = pht('Duplicate');
$errors[] = pht('Another user already has that email.');
}
if (!$same_username && !$same_email) {
throw $exception;
}
}
}
unset($unguarded);
}
$form = id(new AphrontFormView())
->setUser($request->getUser())
->addHiddenInput('phase', 2);
if (!$is_default) {
$form->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('External Account'))
->setValue(
id(new PhabricatorAuthAccountView())
->setUser($request->getUser())
->setExternalAccount($account)
->setAuthProvider($provider)));
}
if ($can_edit_username) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Username'))
->setName('username')
->setValue($value_username)
->setError($e_username));
} else {
$form->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Username'))
->setValue($value_username)
->setError($e_username));
}
if ($can_edit_realname) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Real Name'))
->setName('realName')
->setValue($value_realname)
->setError($e_realname));
}
if ($must_set_password) {
$form->appendChild(
id(new AphrontFormPasswordControl())
->setLabel(pht('Password'))
->setName('password')
->setError($e_password));
$form->appendChild(
id(new AphrontFormPasswordControl())
->setLabel(pht('Confirm Password'))
->setName('confirm')
->setError($e_password)
->setCaption(
$min_len
? pht('Minimum length of %d characters.', $min_len)
: null));
}
if ($can_edit_email) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Email'))
->setName('email')
->setValue($value_email)
->setCaption(PhabricatorUserEmail::describeAllowedAddresses())
->setError($e_email));
}
if ($must_set_password && !$skip_captcha) {
$form->appendChild(
id(new AphrontFormRecaptchaControl())
->setLabel(pht('Captcha'))
->setError($e_captcha));
}
$submit = id(new AphrontFormSubmitControl());
if ($is_setup) {
$submit
->setValue(pht('Create Admin Account'));
} else {
$submit
->addCancelButton($this->getApplicationURI('start/'))
->setValue(pht('Register Account'));
}
$form->appendChild($submit);
$crumbs = $this->buildApplicationCrumbs();
if ($is_setup) {
$crumbs->addTextCrumb(pht('Setup Admin Account'));
$title = pht('Welcome to Phabricator');
} else {
$crumbs->addTextCrumb(pht('Register'));
$crumbs->addTextCrumb($provider->getProviderName());
$title = pht('Create a New Account');
}
$crumbs->setBorder(true);
$welcome_view = null;
if ($is_setup) {
$welcome_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->setTitle(pht('Welcome to Phabricator'))
->appendChild(
pht(
'Installation is complete. Register your administrator account '.
'below to log in. You will be able to configure options and add '.
'other authentication mechanisms (like LDAP or OAuth) later on.'));
}
$object_box = id(new PHUIObjectBoxView())
->setForm($form)
->setFormErrors($errors);
$invite_header = null;
if ($invite) {
$invite_header = $this->renderInviteHeader($invite);
}
$header = id(new PHUIHeaderView())
->setHeader($title);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter(array(
$welcome_view,
$invite_header,
$object_box,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
private function loadDefaultAccount() {
$providers = PhabricatorAuthProvider::getAllEnabledProviders();
$account = null;
$provider = null;
$response = null;
foreach ($providers as $key => $candidate_provider) {
if (!$candidate_provider->shouldAllowRegistration()) {
unset($providers[$key]);
continue;
}
if (!$candidate_provider->isDefaultRegistrationProvider()) {
unset($providers[$key]);
}
}
if (!$providers) {
$response = $this->renderError(
pht(
'There are no configured default registration providers.'));
return array($account, $provider, $response);
} else if (count($providers) > 1) {
$response = $this->renderError(
pht('There are too many configured default registration providers.'));
return array($account, $provider, $response);
}
$provider = head($providers);
$account = $provider->getDefaultExternalAccount();
return array($account, $provider, $response);
}
private function loadSetupAccount() {
$provider = new PhabricatorPasswordAuthProvider();
$provider->attachProviderConfig(
id(new PhabricatorAuthProviderConfig())
->setShouldAllowRegistration(1)
->setShouldAllowLogin(1)
->setIsEnabled(true));
$account = $provider->getDefaultExternalAccount();
$response = null;
return array($account, $provider, $response);
}
private function loadProfilePicture(PhabricatorExternalAccount $account) {
$phid = $account->getProfileImagePHID();
if (!$phid) {
return null;
}
// NOTE: Use of omnipotent user is okay here because the registering user
// can not control the field value, and we can't use their user object to
// do meaningful policy checks anyway since they have not registered yet.
// Reaching this means the user holds the account secret key and the
// registration secret key, and thus has permission to view the image.
$file = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($phid))
->executeOne();
if (!$file) {
return null;
}
$xform = PhabricatorFileTransform::getTransformByKey(
PhabricatorFileThumbnailTransform::TRANSFORM_PROFILE);
return $xform->executeTransform($file);
}
protected function renderError($message) {
return $this->renderErrorPage(
pht('Registration Failed'),
array($message));
}
private function sendWaitingForApprovalEmail(PhabricatorUser $user) {
$title = '[Phabricator] '.pht(
'New User "%s" Awaiting Approval',
$user->getUsername());
$body = new PhabricatorMetaMTAMailBody();
$body->addRawSection(
pht(
'Newly registered user "%s" is awaiting account approval by an '.
'administrator.',
$user->getUsername()));
$body->addLinkSection(
pht('APPROVAL QUEUE'),
PhabricatorEnv::getProductionURI(
'/people/query/approval/'));
$body->addLinkSection(
pht('DISABLE APPROVAL QUEUE'),
PhabricatorEnv::getProductionURI(
'/config/edit/auth.require-approval/'));
$admins = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIsAdmin(true)
->execute();
if (!$admins) {
return;
}
$mail = id(new PhabricatorMetaMTAMail())
->addTos(mpull($admins, 'getPHID'))
->setSubject($title)
->setBody($body->render())
->saveAndSend();
}
}
diff --git a/src/applications/auth/controller/PhabricatorAuthStartController.php b/src/applications/auth/controller/PhabricatorAuthStartController.php
index 9af8f25bc..29fa7e0b9 100644
--- a/src/applications/auth/controller/PhabricatorAuthStartController.php
+++ b/src/applications/auth/controller/PhabricatorAuthStartController.php
@@ -1,308 +1,332 @@
<?php
final class PhabricatorAuthStartController
extends PhabricatorAuthController {
public function shouldRequireLogin() {
return false;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getUser();
if ($viewer->isLoggedIn()) {
// Kick the user home if they are already logged in.
return id(new AphrontRedirectResponse())->setURI('/');
}
if ($request->isAjax()) {
return $this->processAjaxRequest();
}
if ($request->isConduit()) {
return $this->processConduitRequest();
}
// If the user gets this far, they aren't logged in, so if they have a
// user session token we can conclude that it's invalid: if it was valid,
// they'd have been logged in above and never made it here. Try to clear
// it and warn the user they may need to nuke their cookies.
$session_token = $request->getCookie(PhabricatorCookies::COOKIE_SESSION);
$did_clear = $request->getStr('cleared');
if (strlen($session_token)) {
$kind = PhabricatorAuthSessionEngine::getSessionKindFromToken(
$session_token);
switch ($kind) {
case PhabricatorAuthSessionEngine::KIND_ANONYMOUS:
// If this is an anonymous session. It's expected that they won't
// be logged in, so we can just continue.
break;
default:
// The session cookie is invalid, so try to clear it.
$request->clearCookie(PhabricatorCookies::COOKIE_USERNAME);
$request->clearCookie(PhabricatorCookies::COOKIE_SESSION);
// We've previously tried to clear the cookie but we ended up back
// here, so it didn't work. Hard fatal instead of trying again.
if ($did_clear) {
return $this->renderError(
pht(
'Your login session is invalid, and clearing the session '.
'cookie was unsuccessful. Try clearing your browser cookies.'));
}
$redirect_uri = $request->getRequestURI();
$redirect_uri->setQueryParam('cleared', 1);
return id(new AphrontRedirectResponse())->setURI($redirect_uri);
}
}
// If we just cleared the session cookie and it worked, clean up after
// ourselves by redirecting to get rid of the "cleared" parameter. The
// the workflow will continue normally.
if ($did_clear) {
$redirect_uri = $request->getRequestURI();
$redirect_uri->setQueryParam('cleared', null);
return id(new AphrontRedirectResponse())->setURI($redirect_uri);
}
$providers = PhabricatorAuthProvider::getAllEnabledProviders();
foreach ($providers as $key => $provider) {
if (!$provider->shouldAllowLogin()) {
unset($providers[$key]);
}
}
if (!$providers) {
if ($this->isFirstTimeSetup()) {
// If this is a fresh install, let the user register their admin
// account.
return id(new AphrontRedirectResponse())
->setURI($this->getApplicationURI('/register/'));
}
return $this->renderError(
pht(
'This Phabricator install is not configured with any enabled '.
'authentication providers which can be used to log in. If you '.
'have accidentally locked yourself out by disabling all providers, '.
- 'you can use `%s` to recover access to an administrative account.',
+ 'you can use `%s` to recover access to an account.',
'phabricator/bin/auth recover <username>'));
}
$next_uri = $request->getStr('next');
if (!strlen($next_uri)) {
if ($this->getDelegatingController()) {
// Only set a next URI from the request path if this controller was
// delegated to, which happens when a user tries to view a page which
// requires them to login.
// If this controller handled the request directly, we're on the main
// login page, and never want to redirect the user back here after they
// login.
$next_uri = (string)$this->getRequest()->getRequestURI();
}
}
if (!$request->isFormPost()) {
if (strlen($next_uri)) {
PhabricatorCookies::setNextURICookie($request, $next_uri);
}
PhabricatorCookies::setClientIDCookie($request);
}
$auto_response = $this->tryAutoLogin($providers);
if ($auto_response) {
return $auto_response;
}
$invite = $this->loadInvite();
$not_buttons = array();
$are_buttons = array();
$providers = msort($providers, 'getLoginOrder');
foreach ($providers as $provider) {
if ($invite) {
$form = $provider->buildInviteForm($this);
} else {
$form = $provider->buildLoginForm($this);
}
if ($provider->isLoginFormAButton()) {
$are_buttons[] = $form;
} else {
$not_buttons[] = $form;
}
}
$out = array();
$out[] = $not_buttons;
if ($are_buttons) {
require_celerity_resource('auth-css');
foreach ($are_buttons as $key => $button) {
$are_buttons[$key] = phutil_tag(
'div',
array(
'class' => 'phabricator-login-button mmb',
),
$button);
}
// If we only have one button, add a second pretend button so that we
// always have two columns. This makes it easier to get the alignments
// looking reasonable.
if (count($are_buttons) == 1) {
$are_buttons[] = null;
}
$button_columns = id(new AphrontMultiColumnView())
->setFluidLayout(true);
$are_buttons = array_chunk($are_buttons, ceil(count($are_buttons) / 2));
foreach ($are_buttons as $column) {
$button_columns->addColumn($column);
}
$out[] = phutil_tag(
'div',
array(
'class' => 'phabricator-login-buttons',
),
$button_columns);
}
$handlers = PhabricatorAuthLoginHandler::getAllHandlers();
$delegating_controller = $this->getDelegatingController();
$header = array();
foreach ($handlers as $handler) {
$handler = clone $handler;
$handler->setRequest($request);
if ($delegating_controller) {
$handler->setDelegatingController($delegating_controller);
}
$header[] = $handler->getAuthLoginHeaderContent();
}
$invite_message = null;
if ($invite) {
$invite_message = $this->renderInviteHeader($invite);
}
+ $custom_message = $this->newCustomStartMessage();
+
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Login'));
$crumbs->setBorder(true);
$title = pht('Login');
$view = array(
$header,
$invite_message,
+ $custom_message,
$out,
);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
private function processAjaxRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
// We end up here if the user clicks a workflow link that they need to
// login to use. We give them a dialog saying "You need to login...".
if ($request->isDialogFormPost()) {
return id(new AphrontRedirectResponse())->setURI(
$request->getRequestURI());
}
// Often, users end up here by clicking a disabled action link in the UI
// (for example, they might click "Edit Subtasks" on a Maniphest task
// page). After they log in we want to send them back to that main object
// page if we can, since it's confusing to end up on a standalone page with
// only a dialog (particularly if that dialog is another error,
// like a policy exception).
$via_header = AphrontRequest::getViaHeaderName();
$via_uri = AphrontRequest::getHTTPHeader($via_header);
if (strlen($via_uri)) {
PhabricatorCookies::setNextURICookie($request, $via_uri, $force = true);
}
return $this->newDialog()
->setTitle(pht('Login Required'))
->appendParagraph(pht('You must log in to take this action.'))
->addSubmitButton(pht('Log In'))
->addCancelButton('/');
}
private function processConduitRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
// A common source of errors in Conduit client configuration is getting
// the request path wrong. The client will end up here, so make some
// effort to give them a comprehensible error message.
$request_path = $this->getRequest()->getPath();
$conduit_path = '/api/<method>';
$example_path = '/api/conduit.ping';
$message = pht(
'ERROR: You are making a Conduit API request to "%s", but the correct '.
'HTTP request path to use in order to access a COnduit method is "%s" '.
'(for example, "%s"). Check your configuration.',
$request_path,
$conduit_path,
$example_path);
return id(new AphrontPlainTextResponse())->setContent($message);
}
protected function renderError($message) {
return $this->renderErrorPage(
pht('Authentication Failure'),
array($message));
}
private function tryAutoLogin(array $providers) {
$request = $this->getRequest();
// If the user just logged out, don't immediately log them in again.
if ($request->getURIData('loggedout')) {
return null;
}
// If we have more than one provider, we can't autologin because we
// don't know which one the user wants.
if (count($providers) != 1) {
return null;
}
$provider = head($providers);
if (!$provider->supportsAutoLogin()) {
return null;
}
$config = $provider->getProviderConfig();
if (!$config->getShouldAutoLogin()) {
return null;
}
$auto_uri = $provider->getAutoLoginURI($request);
return id(new AphrontRedirectResponse())
->setIsExternal(true)
->setURI($auto_uri);
}
+ private function newCustomStartMessage() {
+ $viewer = $this->getViewer();
+
+ $text = PhabricatorAuthMessage::loadMessageText(
+ $viewer,
+ PhabricatorAuthLoginMessageType::MESSAGEKEY);
+
+ if (!strlen($text)) {
+ return null;
+ }
+
+ $remarkup_view = new PHUIRemarkupView($viewer, $text);
+
+ return phutil_tag(
+ 'div',
+ array(
+ 'class' => 'auth-custom-message',
+ ),
+ $remarkup_view);
+ }
+
}
diff --git a/src/applications/auth/controller/config/PhabricatorAuthListController.php b/src/applications/auth/controller/config/PhabricatorAuthListController.php
index c5d4f7ad7..bb118d798 100644
--- a/src/applications/auth/controller/config/PhabricatorAuthListController.php
+++ b/src/applications/auth/controller/config/PhabricatorAuthListController.php
@@ -1,137 +1,142 @@
<?php
final class PhabricatorAuthListController
extends PhabricatorAuthProviderConfigController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$configs = id(new PhabricatorAuthProviderConfigQuery())
->setViewer($viewer)
->execute();
$list = new PHUIObjectItemListView();
$can_manage = $this->hasApplicationCapability(
AuthManageProvidersCapability::CAPABILITY);
foreach ($configs as $config) {
$item = new PHUIObjectItemView();
$id = $config->getID();
$edit_uri = $this->getApplicationURI('config/edit/'.$id.'/');
$enable_uri = $this->getApplicationURI('config/enable/'.$id.'/');
$disable_uri = $this->getApplicationURI('config/disable/'.$id.'/');
$provider = $config->getProvider();
if ($provider) {
$name = $provider->getProviderName();
} else {
$name = $config->getProviderType().' ('.$config->getProviderClass().')';
}
$item->setHeader($name);
if ($provider) {
$item->setHref($edit_uri);
} else {
$item->addAttribute(pht('Provider Implementation Missing!'));
}
$domain = null;
if ($provider) {
$domain = $provider->getProviderDomain();
if ($domain !== 'self') {
$item->addAttribute($domain);
}
}
if ($config->getShouldAllowRegistration()) {
$item->addAttribute(pht('Allows Registration'));
} else {
$item->addAttribute(pht('Does Not Allow Registration'));
}
if ($config->getIsEnabled()) {
$item->setStatusIcon('fa-check-circle green');
$item->addAction(
id(new PHUIListItemView())
->setIcon('fa-times')
->setHref($disable_uri)
->setDisabled(!$can_manage)
->addSigil('workflow'));
} else {
$item->setStatusIcon('fa-ban red');
$item->addIcon('fa-ban grey', pht('Disabled'));
$item->addAction(
id(new PHUIListItemView())
->setIcon('fa-plus')
->setHref($enable_uri)
->setDisabled(!$can_manage)
->addSigil('workflow'));
}
$list->addItem($item);
}
$list->setNoDataString(
pht(
'%s You have not added authentication providers yet. Use "%s" to add '.
'a provider, which will let users register new Phabricator accounts '.
'and log in.',
phutil_tag(
'strong',
array(),
pht('No Providers Configured:')),
phutil_tag(
'a',
array(
'href' => $this->getApplicationURI('config/new/'),
),
pht('Add Authentication Provider'))));
$crumbs = $this->buildApplicationCrumbs();
- $crumbs->addTextCrumb(pht('Auth Providers'));
+ $crumbs->addTextCrumb(pht('Login and Registration'));
$crumbs->setBorder(true);
$guidance_context = new PhabricatorAuthProvidersGuidanceContext();
$guidance = id(new PhabricatorGuidanceEngine())
->setViewer($viewer)
->setGuidanceContext($guidance_context)
->newInfoView();
$button = id(new PHUIButtonView())
- ->setTag('a')
- ->setButtonType(PHUIButtonView::BUTTONTYPE_SIMPLE)
- ->setHref($this->getApplicationURI('config/new/'))
- ->setIcon('fa-plus')
- ->setDisabled(!$can_manage)
- ->setText(pht('Add Provider'));
+ ->setTag('a')
+ ->setButtonType(PHUIButtonView::BUTTONTYPE_SIMPLE)
+ ->setHref($this->getApplicationURI('config/new/'))
+ ->setIcon('fa-plus')
+ ->setDisabled(!$can_manage)
+ ->setText(pht('Add Provider'));
$list->setFlush(true);
$list = id(new PHUIObjectBoxView())
->setHeaderText(pht('Providers'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($list);
- $title = pht('Auth Providers');
+ $title = pht('Login and Registration Providers');
$header = id(new PHUIHeaderView())
->setHeader($title)
->setHeaderIcon('fa-key')
->addActionLink($button);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter(array(
$guidance,
$list,
));
- return $this->newPage()
- ->setTitle($title)
+ $nav = $this->newNavigation()
->setCrumbs($crumbs)
->appendChild($view);
+
+ $nav->selectFilter('login');
+
+ return $this->newPage()
+ ->setTitle($title)
+ ->appendChild($nav);
}
}
diff --git a/src/applications/auth/controller/config/PhabricatorAuthProviderConfigController.php b/src/applications/auth/controller/config/PhabricatorAuthProviderConfigController.php
index db9ec0679..c2d20dd1b 100644
--- a/src/applications/auth/controller/config/PhabricatorAuthProviderConfigController.php
+++ b/src/applications/auth/controller/config/PhabricatorAuthProviderConfigController.php
@@ -1,32 +1,4 @@
<?php
abstract class PhabricatorAuthProviderConfigController
- extends PhabricatorAuthController {
-
- protected function buildSideNavView($for_app = false) {
- $nav = new AphrontSideNavFilterView();
- $nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
-
- if ($for_app) {
- $nav->addLabel(pht('Create'));
- $nav->addFilter('',
- pht('Add Authentication Provider'),
- $this->getApplicationURI('/config/new/'));
- }
- return $nav;
- }
-
- public function buildApplicationMenu() {
- return $this->buildSideNavView($for_app = true)->getMenu();
- }
-
- protected function buildApplicationCrumbs() {
- $crumbs = parent::buildApplicationCrumbs();
-
- $can_create = $this->hasApplicationCapability(
- AuthManageProvidersCapability::CAPABILITY);
-
- return $crumbs;
- }
-
-}
+ extends PhabricatorAuthProviderController {}
diff --git a/src/applications/auth/controller/config/PhabricatorAuthProviderController.php b/src/applications/auth/controller/config/PhabricatorAuthProviderController.php
new file mode 100644
index 000000000..2668da121
--- /dev/null
+++ b/src/applications/auth/controller/config/PhabricatorAuthProviderController.php
@@ -0,0 +1,57 @@
+<?php
+
+abstract class PhabricatorAuthProviderController
+ extends PhabricatorAuthController {
+
+ protected function newNavigation() {
+ $viewer = $this->getViewer();
+
+ $nav = id(new AphrontSideNavFilterView())
+ ->setBaseURI(new PhutilURI($this->getApplicationURI()))
+ ->setViewer($viewer);
+
+ $nav->addMenuItem(
+ id(new PHUIListItemView())
+ ->setName(pht('Authentication'))
+ ->setType(PHUIListItemView::TYPE_LABEL));
+
+ $nav->addMenuItem(
+ id(new PHUIListItemView())
+ ->setKey('login')
+ ->setName(pht('Login and Registration'))
+ ->setType(PHUIListItemView::TYPE_LINK)
+ ->setHref($this->getApplicationURI('/'))
+ ->setIcon('fa-key'));
+
+ $nav->addMenuItem(
+ id(new PHUIListItemView())
+ ->setKey('mfa')
+ ->setName(pht('Multi-Factor'))
+ ->setType(PHUIListItemView::TYPE_LINK)
+ ->setHref($this->getApplicationURI('mfa/'))
+ ->setIcon('fa-mobile'));
+
+ $nav->addMenuItem(
+ id(new PHUIListItemView())
+ ->setName(pht('Onboarding'))
+ ->setType(PHUIListItemView::TYPE_LABEL));
+
+ $nav->addMenuItem(
+ id(new PHUIListItemView())
+ ->setKey('message')
+ ->setName(pht('Customize Messages'))
+ ->setType(PHUIListItemView::TYPE_LINK)
+ ->setHref($this->getApplicationURI('message/'))
+ ->setIcon('fa-commenting-o'));
+
+
+ $nav->selectFilter(null);
+
+ return $nav;
+ }
+
+ public function buildApplicationMenu() {
+ return $this->newNavigation()->getMenu();
+ }
+
+}
diff --git a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberController.php b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberController.php
new file mode 100644
index 000000000..3ae923fbb
--- /dev/null
+++ b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberController.php
@@ -0,0 +1,31 @@
+<?php
+
+abstract class PhabricatorAuthContactNumberController
+ extends PhabricatorAuthController {
+
+ // Users may need to access these controllers to enroll in SMS MFA during
+ // account setup.
+
+ public function shouldRequireMultiFactorEnrollment() {
+ return false;
+ }
+
+ public function shouldRequireEnabledUser() {
+ return false;
+ }
+
+ public function shouldRequireEmailVerification() {
+ return false;
+ }
+
+ protected function buildApplicationCrumbs() {
+ $crumbs = parent::buildApplicationCrumbs();
+
+ $crumbs->addTextCrumb(
+ pht('Contact Numbers'),
+ pht('/settings/panel/contact/'));
+
+ return $crumbs;
+ }
+
+}
diff --git a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberDisableController.php b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberDisableController.php
new file mode 100644
index 000000000..a525e7b93
--- /dev/null
+++ b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberDisableController.php
@@ -0,0 +1,88 @@
+<?php
+
+final class PhabricatorAuthContactNumberDisableController
+ extends PhabricatorAuthContactNumberController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $request->getViewer();
+ $id = $request->getURIData('id');
+
+ $number = id(new PhabricatorAuthContactNumberQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($id))
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->executeOne();
+ if (!$number) {
+ return new Aphront404Response();
+ }
+
+ $is_disable = ($request->getURIData('action') == 'disable');
+ $id = $number->getID();
+ $cancel_uri = $number->getURI();
+
+ if ($request->isFormOrHisecPost()) {
+ $xactions = array();
+
+ if ($is_disable) {
+ $new_status = PhabricatorAuthContactNumber::STATUS_DISABLED;
+ } else {
+ $new_status = PhabricatorAuthContactNumber::STATUS_ACTIVE;
+ }
+
+ $xactions[] = id(new PhabricatorAuthContactNumberTransaction())
+ ->setTransactionType(
+ PhabricatorAuthContactNumberStatusTransaction::TRANSACTIONTYPE)
+ ->setNewValue($new_status);
+
+ $editor = id(new PhabricatorAuthContactNumberEditor())
+ ->setActor($viewer)
+ ->setContentSourceFromRequest($request)
+ ->setContinueOnNoEffect(true)
+ ->setContinueOnMissingFields(true)
+ ->setCancelURI($cancel_uri);
+
+ try {
+ $editor->applyTransactions($number, $xactions);
+ } catch (PhabricatorApplicationTransactionValidationException $ex) {
+ // This happens when you enable a number which collides with another
+ // number.
+ return $this->newDialog()
+ ->setTitle(pht('Changing Status Failed'))
+ ->setValidationException($ex)
+ ->addCancelButton($cancel_uri);
+ }
+
+ return id(new AphrontRedirectResponse())->setURI($cancel_uri);
+ }
+
+ $number_display = phutil_tag(
+ 'strong',
+ array(),
+ $number->getDisplayName());
+
+ if ($is_disable) {
+ $title = pht('Disable Contact Number');
+ $body = pht(
+ 'Disable the contact number %s?',
+ $number_display);
+ $button = pht('Disable Number');
+ } else {
+ $title = pht('Enable Contact Number');
+ $body = pht(
+ 'Enable the contact number %s?',
+ $number_display);
+ $button = pht('Enable Number');
+ }
+
+ return $this->newDialog()
+ ->setTitle($title)
+ ->appendParagraph($body)
+ ->addSubmitButton($button)
+ ->addCancelButton($cancel_uri);
+ }
+
+}
diff --git a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberEditController.php b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberEditController.php
new file mode 100644
index 000000000..95764496d
--- /dev/null
+++ b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberEditController.php
@@ -0,0 +1,12 @@
+<?php
+
+final class PhabricatorAuthContactNumberEditController
+ extends PhabricatorAuthContactNumberController {
+
+ public function handleRequest(AphrontRequest $request) {
+ return id(new PhabricatorAuthContactNumberEditEngine())
+ ->setController($this)
+ ->buildResponse();
+ }
+
+}
diff --git a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberPrimaryController.php b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberPrimaryController.php
new file mode 100644
index 000000000..cad1bbf3f
--- /dev/null
+++ b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberPrimaryController.php
@@ -0,0 +1,88 @@
+<?php
+
+final class PhabricatorAuthContactNumberPrimaryController
+ extends PhabricatorAuthContactNumberController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $request->getViewer();
+ $id = $request->getURIData('id');
+
+ $number = id(new PhabricatorAuthContactNumberQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($id))
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->executeOne();
+ if (!$number) {
+ return new Aphront404Response();
+ }
+
+ $id = $number->getID();
+ $cancel_uri = $number->getURI();
+
+ if ($number->isDisabled()) {
+ return $this->newDialog()
+ ->setTitle(pht('Number Disabled'))
+ ->appendParagraph(
+ pht(
+ 'You can not make a disabled number your primary contact number.'))
+ ->addCancelButton($cancel_uri);
+ }
+
+ if ($number->getIsPrimary()) {
+ return $this->newDialog()
+ ->setTitle(pht('Number Already Primary'))
+ ->appendParagraph(
+ pht(
+ 'This contact number is already your primary contact number.'))
+ ->addCancelButton($cancel_uri);
+ }
+
+ if ($request->isFormOrHisecPost()) {
+ $xactions = array();
+
+ $xactions[] = id(new PhabricatorAuthContactNumberTransaction())
+ ->setTransactionType(
+ PhabricatorAuthContactNumberPrimaryTransaction::TRANSACTIONTYPE)
+ ->setNewValue(true);
+
+ $editor = id(new PhabricatorAuthContactNumberEditor())
+ ->setActor($viewer)
+ ->setContentSourceFromRequest($request)
+ ->setContinueOnNoEffect(true)
+ ->setContinueOnMissingFields(true)
+ ->setCancelURI($cancel_uri);
+
+ try {
+ $editor->applyTransactions($number, $xactions);
+ } catch (PhabricatorApplicationTransactionValidationException $ex) {
+ // This happens when you try to make a number into your primary
+ // number, but you have contact number MFA on your account.
+ return $this->newDialog()
+ ->setTitle(pht('Unable to Make Primary'))
+ ->setValidationException($ex)
+ ->addCancelButton($cancel_uri);
+ }
+
+ return id(new AphrontRedirectResponse())->setURI($cancel_uri);
+ }
+
+ $number_display = phutil_tag(
+ 'strong',
+ array(),
+ $number->getDisplayName());
+
+ return $this->newDialog()
+ ->setTitle(pht('Set Primary Contact Number'))
+ ->appendParagraph(
+ pht(
+ 'Designate %s as your primary contact number?',
+ $number_display))
+ ->addSubmitButton(pht('Make Primary'))
+ ->addCancelButton($cancel_uri);
+ }
+
+}
diff --git a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberTestController.php b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberTestController.php
new file mode 100644
index 000000000..2c25fa3f4
--- /dev/null
+++ b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberTestController.php
@@ -0,0 +1,64 @@
+<?php
+
+final class PhabricatorAuthContactNumberTestController
+ extends PhabricatorAuthContactNumberController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $request->getViewer();
+ $id = $request->getURIData('id');
+
+ $number = id(new PhabricatorAuthContactNumberQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($id))
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->executeOne();
+ if (!$number) {
+ return new Aphront404Response();
+ }
+
+ $id = $number->getID();
+ $cancel_uri = $number->getURI();
+
+ // NOTE: This is a global limit shared by all users.
+ PhabricatorSystemActionEngine::willTakeAction(
+ array(id(new PhabricatorAuthApplication())->getPHID()),
+ new PhabricatorAuthTestSMSAction(),
+ 1);
+
+ if ($request->isFormPost()) {
+ $uri = PhabricatorEnv::getURI('/');
+ $uri = new PhutilURI($uri);
+
+ $mail = id(new PhabricatorMetaMTAMail())
+ ->setMessageType(PhabricatorMailSMSMessage::MESSAGETYPE)
+ ->addTos(array($viewer->getPHID()))
+ ->setSensitiveContent(false)
+ ->setBody(
+ pht(
+ 'This is a terse test text message from Phabricator (%s).',
+ $uri->getDomain()))
+ ->save();
+
+ return id(new AphrontRedirectResponse())->setURI($mail->getURI());
+ }
+
+ $number_display = phutil_tag(
+ 'strong',
+ array(),
+ $number->getDisplayName());
+
+ return $this->newDialog()
+ ->setTitle(pht('Set Test Message'))
+ ->appendParagraph(
+ pht(
+ 'Send a test message to %s?',
+ $number_display))
+ ->addSubmitButton(pht('Send SMS'))
+ ->addCancelButton($cancel_uri);
+ }
+
+}
diff --git a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php
new file mode 100644
index 000000000..027d288db
--- /dev/null
+++ b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php
@@ -0,0 +1,139 @@
+<?php
+
+final class PhabricatorAuthContactNumberViewController
+ extends PhabricatorAuthContactNumberController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $this->getViewer();
+
+ $number = id(new PhabricatorAuthContactNumberQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($request->getURIData('id')))
+ ->executeOne();
+ if (!$number) {
+ return new Aphront404Response();
+ }
+
+ $crumbs = $this->buildApplicationCrumbs()
+ ->addTextCrumb($number->getObjectName())
+ ->setBorder(true);
+
+ $header = $this->buildHeaderView($number);
+ $properties = $this->buildPropertiesView($number);
+ $curtain = $this->buildCurtain($number);
+
+ $timeline = $this->buildTransactionTimeline(
+ $number,
+ new PhabricatorAuthContactNumberTransactionQuery());
+ $timeline->setShouldTerminate(true);
+
+ $view = id(new PHUITwoColumnView())
+ ->setHeader($header)
+ ->setCurtain($curtain)
+ ->setMainColumn(
+ array(
+ $timeline,
+ ))
+ ->addPropertySection(pht('Details'), $properties);
+
+ return $this->newPage()
+ ->setTitle($number->getDisplayName())
+ ->setCrumbs($crumbs)
+ ->setPageObjectPHIDs(
+ array(
+ $number->getPHID(),
+ ))
+ ->appendChild($view);
+ }
+
+ private function buildHeaderView(PhabricatorAuthContactNumber $number) {
+ $viewer = $this->getViewer();
+
+ $view = id(new PHUIHeaderView())
+ ->setViewer($viewer)
+ ->setHeader($number->getObjectName())
+ ->setPolicyObject($number);
+
+ if ($number->isDisabled()) {
+ $view->setStatus('fa-ban', 'red', pht('Disabled'));
+ } else if ($number->getIsPrimary()) {
+ $view->setStatus('fa-certificate', 'blue', pht('Primary'));
+ }
+
+ return $view;
+ }
+
+ private function buildPropertiesView(
+ PhabricatorAuthContactNumber $number) {
+ $viewer = $this->getViewer();
+
+ $view = id(new PHUIPropertyListView())
+ ->setViewer($viewer);
+
+ $view->addProperty(
+ pht('Owner'),
+ $viewer->renderHandle($number->getObjectPHID()));
+
+ $view->addProperty(pht('Contact Number'), $number->getDisplayName());
+
+ return $view;
+ }
+
+ private function buildCurtain(PhabricatorAuthContactNumber $number) {
+ $viewer = $this->getViewer();
+ $id = $number->getID();
+
+ $can_edit = PhabricatorPolicyFilter::hasCapability(
+ $viewer,
+ $number,
+ PhabricatorPolicyCapability::CAN_EDIT);
+
+ $curtain = $this->newCurtainView($number);
+
+ $curtain->addAction(
+ id(new PhabricatorActionView())
+ ->setName(pht('Edit Contact Number'))
+ ->setIcon('fa-pencil')
+ ->setHref($this->getApplicationURI("contact/edit/{$id}/"))
+ ->setDisabled(!$can_edit)
+ ->setWorkflow(!$can_edit));
+
+ $curtain->addAction(
+ id(new PhabricatorActionView())
+ ->setName(pht('Send Test Message'))
+ ->setIcon('fa-envelope-o')
+ ->setHref($this->getApplicationURI("contact/test/{$id}/"))
+ ->setDisabled(!$can_edit)
+ ->setWorkflow(true));
+
+ if ($number->isDisabled()) {
+ $disable_uri = $this->getApplicationURI("contact/enable/{$id}/");
+ $disable_name = pht('Enable Contact Number');
+ $disable_icon = 'fa-check';
+ $can_primary = false;
+ } else {
+ $disable_uri = $this->getApplicationURI("contact/disable/{$id}/");
+ $disable_name = pht('Disable Contact Number');
+ $disable_icon = 'fa-ban';
+ $can_primary = !$number->getIsPrimary();
+ }
+
+ $curtain->addAction(
+ id(new PhabricatorActionView())
+ ->setName(pht('Make Primary Number'))
+ ->setIcon('fa-certificate')
+ ->setHref($this->getApplicationURI("contact/primary/{$id}/"))
+ ->setDisabled(!$can_primary)
+ ->setWorkflow(true));
+
+ $curtain->addAction(
+ id(new PhabricatorActionView())
+ ->setName($disable_name)
+ ->setIcon($disable_icon)
+ ->setHref($disable_uri)
+ ->setWorkflow(true));
+
+ return $curtain;
+ }
+
+}
diff --git a/src/applications/auth/controller/message/PhabricatorAuthMessageController.php b/src/applications/auth/controller/message/PhabricatorAuthMessageController.php
new file mode 100644
index 000000000..98bb908cf
--- /dev/null
+++ b/src/applications/auth/controller/message/PhabricatorAuthMessageController.php
@@ -0,0 +1,11 @@
+<?php
+
+abstract class PhabricatorAuthMessageController
+ extends PhabricatorAuthProviderController {
+
+ protected function buildApplicationCrumbs() {
+ return parent::buildApplicationCrumbs()
+ ->addTextCrumb(pht('Messages'), $this->getApplicationURI('message/'));
+ }
+
+}
diff --git a/src/applications/auth/controller/message/PhabricatorAuthMessageEditController.php b/src/applications/auth/controller/message/PhabricatorAuthMessageEditController.php
new file mode 100644
index 000000000..3cb4a4b0a
--- /dev/null
+++ b/src/applications/auth/controller/message/PhabricatorAuthMessageEditController.php
@@ -0,0 +1,31 @@
+<?php
+
+final class PhabricatorAuthMessageEditController
+ extends PhabricatorAuthMessageController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $this->requireApplicationCapability(
+ AuthManageProvidersCapability::CAPABILITY);
+
+ $engine = id(new PhabricatorAuthMessageEditEngine())
+ ->setController($this);
+
+ $id = $request->getURIData('id');
+ if (!$id) {
+ $message_key = $request->getStr('messageKey');
+
+ $message_types = PhabricatorAuthMessageType::getAllMessageTypes();
+ $message_type = idx($message_types, $message_key);
+ if (!$message_type) {
+ return new Aphront404Response();
+ }
+
+ $engine
+ ->addContextParameter('messageKey', $message_key)
+ ->setMessageType($message_type);
+ }
+
+ return $engine->buildResponse();
+ }
+
+}
diff --git a/src/applications/auth/controller/message/PhabricatorAuthMessageListController.php b/src/applications/auth/controller/message/PhabricatorAuthMessageListController.php
new file mode 100644
index 000000000..a3c518ab3
--- /dev/null
+++ b/src/applications/auth/controller/message/PhabricatorAuthMessageListController.php
@@ -0,0 +1,77 @@
+<?php
+
+final class PhabricatorAuthMessageListController
+ extends PhabricatorAuthProviderController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $this->getViewer();
+
+ $can_manage = $this->hasApplicationCapability(
+ AuthManageProvidersCapability::CAPABILITY);
+
+ $types = PhabricatorAuthMessageType::getAllMessageTypes();
+
+ $messages = id(new PhabricatorAuthMessageQuery())
+ ->setViewer($viewer)
+ ->execute();
+ $messages = mpull($messages, null, 'getMessageKey');
+
+ $list = new PHUIObjectItemListView();
+ foreach ($types as $type) {
+ $message = idx($messages, $type->getMessageTypeKey());
+ if ($message) {
+ $href = $message->getURI();
+ $name = $message->getMessageTypeDisplayName();
+ } else {
+ $href = '/auth/message/edit/?messageKey='.$type->getMessageTypeKey();
+ $name = $type->getDisplayName();
+ }
+
+ $item = id(new PHUIObjectItemView())
+ ->setHeader($name)
+ ->setHref($href)
+ ->addAttribute($type->getShortDescription());
+
+ if ($message) {
+ $item->addIcon('fa-circle', pht('Customized'));
+ } else {
+ $item->addIcon('fa-circle-o grey', pht('Default'));
+ }
+
+ $list->addItem($item);
+ }
+
+ $crumbs = $this->buildApplicationCrumbs()
+ ->addTextCrumb(pht('Messages'))
+ ->setBorder(true);
+
+ $list->setFlush(true);
+ $list = id(new PHUIObjectBoxView())
+ ->setHeaderText(pht('Auth Messages'))
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->appendChild($list);
+
+ $title = pht('Auth Messages');
+ $header = id(new PHUIHeaderView())
+ ->setHeader($title)
+ ->setHeaderIcon('fa-commenting-o');
+
+ $view = id(new PHUITwoColumnView())
+ ->setHeader($header)
+ ->setFooter(
+ array(
+ $list,
+ ));
+
+ $nav = $this->newNavigation()
+ ->setCrumbs($crumbs)
+ ->appendChild($view);
+
+ $nav->selectFilter('message');
+
+ return $this->newPage()
+ ->setTitle($title)
+ ->appendChild($nav);
+ }
+
+}
diff --git a/src/applications/auth/controller/message/PhabricatorAuthMessageViewController.php b/src/applications/auth/controller/message/PhabricatorAuthMessageViewController.php
new file mode 100644
index 000000000..db7e7e65e
--- /dev/null
+++ b/src/applications/auth/controller/message/PhabricatorAuthMessageViewController.php
@@ -0,0 +1,104 @@
+<?php
+
+final class PhabricatorAuthMessageViewController
+ extends PhabricatorAuthMessageController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $this->getViewer();
+
+ $this->requireApplicationCapability(
+ AuthManageProvidersCapability::CAPABILITY);
+
+ $message = id(new PhabricatorAuthMessageQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($request->getURIData('id')))
+ ->executeOne();
+ if (!$message) {
+ return new Aphront404Response();
+ }
+
+ $crumbs = $this->buildApplicationCrumbs()
+ ->addTextCrumb($message->getObjectName())
+ ->setBorder(true);
+
+ $header = $this->buildHeaderView($message);
+ $properties = $this->buildPropertiesView($message);
+ $curtain = $this->buildCurtain($message);
+
+ $timeline = $this->buildTransactionTimeline(
+ $message,
+ new PhabricatorAuthMessageTransactionQuery());
+ $timeline->setShouldTerminate(true);
+
+ $view = id(new PHUITwoColumnView())
+ ->setHeader($header)
+ ->setCurtain($curtain)
+ ->setMainColumn(
+ array(
+ $timeline,
+ ))
+ ->addPropertySection(pht('Details'), $properties);
+
+ return $this->newPage()
+ ->setTitle($message->getMessageTypeDisplayName())
+ ->setCrumbs($crumbs)
+ ->setPageObjectPHIDs(
+ array(
+ $message->getPHID(),
+ ))
+ ->appendChild($view);
+ }
+
+ private function buildHeaderView(PhabricatorAuthMessage $message) {
+ $viewer = $this->getViewer();
+
+ $view = id(new PHUIHeaderView())
+ ->setViewer($viewer)
+ ->setHeader($message->getMessageTypeDisplayName());
+
+ return $view;
+ }
+
+ private function buildPropertiesView(PhabricatorAuthMessage $message) {
+ $viewer = $this->getViewer();
+
+ $view = id(new PHUIPropertyListView())
+ ->setViewer($viewer);
+
+ $view->addProperty(
+ pht('Description'),
+ $message->getMessageType()->getShortDescription());
+
+ $view->addSectionHeader(
+ pht('Message Preview'),
+ PHUIPropertyListView::ICON_SUMMARY);
+
+ $view->addTextContent(
+ new PHUIRemarkupView($viewer, $message->getMessageText()));
+
+ return $view;
+ }
+
+ private function buildCurtain(PhabricatorAuthMessage $message) {
+ $viewer = $this->getViewer();
+ $id = $message->getID();
+
+ $can_edit = PhabricatorPolicyFilter::hasCapability(
+ $viewer,
+ $message,
+ PhabricatorPolicyCapability::CAN_EDIT);
+
+ $curtain = $this->newCurtainView($message);
+
+ $curtain->addAction(
+ id(new PhabricatorActionView())
+ ->setName(pht('Edit Message'))
+ ->setIcon('fa-pencil')
+ ->setHref($this->getApplicationURI("message/edit/{$id}/"))
+ ->setDisabled(!$can_edit)
+ ->setWorkflow(!$can_edit));
+
+ return $curtain;
+ }
+
+}
diff --git a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderController.php b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderController.php
new file mode 100644
index 000000000..53a8f10be
--- /dev/null
+++ b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderController.php
@@ -0,0 +1,11 @@
+<?php
+
+abstract class PhabricatorAuthFactorProviderController
+ extends PhabricatorAuthProviderController {
+
+ protected function buildApplicationCrumbs() {
+ return parent::buildApplicationCrumbs()
+ ->addTextCrumb(pht('Multi-Factor'), $this->getApplicationURI('mfa/'));
+ }
+
+}
diff --git a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php
new file mode 100644
index 000000000..a8d87e2ea
--- /dev/null
+++ b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php
@@ -0,0 +1,80 @@
+<?php
+
+final class PhabricatorAuthFactorProviderEditController
+ extends PhabricatorAuthFactorProviderController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $this->requireApplicationCapability(
+ AuthManageProvidersCapability::CAPABILITY);
+
+ $engine = id(new PhabricatorAuthFactorProviderEditEngine())
+ ->setController($this);
+
+ $id = $request->getURIData('id');
+ if (!$id) {
+ $factor_key = $request->getStr('providerFactorKey');
+
+ $map = PhabricatorAuthFactor::getAllFactors();
+ $factor = idx($map, $factor_key);
+ if (!$factor) {
+ return $this->buildFactorSelectionResponse();
+ }
+
+ $engine
+ ->addContextParameter('providerFactorKey', $factor_key)
+ ->setProviderFactor($factor);
+ }
+
+ return $engine->buildResponse();
+ }
+
+ private function buildFactorSelectionResponse() {
+ $request = $this->getRequest();
+ $viewer = $this->getViewer();
+
+ $cancel_uri = $this->getApplicationURI('mfa/');
+
+ $factors = PhabricatorAuthFactor::getAllFactors();
+
+ $menu = id(new PHUIObjectItemListView())
+ ->setUser($viewer)
+ ->setBig(true)
+ ->setFlush(true);
+
+ $factors = msortv($factors, 'newSortVector');
+
+ foreach ($factors as $factor_key => $factor) {
+ $factor_uri = id(new PhutilURI('/mfa/edit/'))
+ ->setQueryParam('providerFactorKey', $factor_key);
+ $factor_uri = $this->getApplicationURI($factor_uri);
+
+ $is_enabled = $factor->canCreateNewProvider();
+
+ $item = id(new PHUIObjectItemView())
+ ->setHeader($factor->getFactorName())
+ ->setImageIcon($factor->newIconView())
+ ->addAttribute($factor->getFactorCreateHelp());
+
+ if ($is_enabled) {
+ $item
+ ->setHref($factor_uri)
+ ->setClickable(true);
+ } else {
+ $item->setDisabled(true);
+ }
+
+ $create_description = $factor->getProviderCreateDescription();
+ if ($create_description) {
+ $item->appendChild($create_description);
+ }
+
+ $menu->addItem($item);
+ }
+
+ return $this->newDialog()
+ ->setTitle(pht('Choose Provider Type'))
+ ->appendChild($menu)
+ ->addCancelButton($cancel_uri);
+ }
+
+}
diff --git a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderListController.php b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderListController.php
new file mode 100644
index 000000000..d19671c3c
--- /dev/null
+++ b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderListController.php
@@ -0,0 +1,82 @@
+<?php
+
+final class PhabricatorAuthFactorProviderListController
+ extends PhabricatorAuthProviderController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $this->getViewer();
+
+ $can_manage = $this->hasApplicationCapability(
+ AuthManageProvidersCapability::CAPABILITY);
+
+ $providers = id(new PhabricatorAuthFactorProviderQuery())
+ ->setViewer($viewer)
+ ->execute();
+
+ $list = new PHUIObjectItemListView();
+ foreach ($providers as $provider) {
+ $item = id(new PHUIObjectItemView())
+ ->setObjectName($provider->getObjectName())
+ ->setHeader($provider->getDisplayName())
+ ->setHref($provider->getURI());
+
+ $status = $provider->newStatus();
+
+ $icon = $status->getListIcon();
+ $color = $status->getListColor();
+ if ($icon !== null) {
+ $item->setStatusIcon("{$icon} {$color}", $status->getName());
+ }
+
+ $item->setDisabled(!$status->isActive());
+
+ $list->addItem($item);
+ }
+
+ $list->setNoDataString(
+ pht('You have not configured any multi-factor providers yet.'));
+
+ $crumbs = $this->buildApplicationCrumbs()
+ ->addTextCrumb(pht('Multi-Factor'))
+ ->setBorder(true);
+
+ $button = id(new PHUIButtonView())
+ ->setTag('a')
+ ->setButtonType(PHUIButtonView::BUTTONTYPE_SIMPLE)
+ ->setHref($this->getApplicationURI('mfa/edit/'))
+ ->setIcon('fa-plus')
+ ->setDisabled(!$can_manage)
+ ->setWorkflow(true)
+ ->setText(pht('Add MFA Provider'));
+
+ $list->setFlush(true);
+ $list = id(new PHUIObjectBoxView())
+ ->setHeaderText(pht('MFA Providers'))
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->appendChild($list);
+
+ $title = pht('MFA Providers');
+ $header = id(new PHUIHeaderView())
+ ->setHeader($title)
+ ->setHeaderIcon('fa-mobile')
+ ->addActionLink($button);
+
+ $view = id(new PHUITwoColumnView())
+ ->setHeader($header)
+ ->setFooter(
+ array(
+ $list,
+ ));
+
+ $nav = $this->newNavigation()
+ ->setCrumbs($crumbs)
+ ->appendChild($view);
+
+ $nav->selectFilter('mfa');
+
+ return $this->newPage()
+ ->setTitle($title)
+ ->appendChild($nav);
+ }
+
+}
diff --git a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderMessageController.php b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderMessageController.php
new file mode 100644
index 000000000..563ee3993
--- /dev/null
+++ b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderMessageController.php
@@ -0,0 +1,84 @@
+<?php
+
+final class PhabricatorAuthFactorProviderMessageController
+ extends PhabricatorAuthFactorProviderController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $this->requireApplicationCapability(
+ AuthManageProvidersCapability::CAPABILITY);
+
+ $viewer = $request->getViewer();
+ $id = $request->getURIData('id');
+
+ $provider = id(new PhabricatorAuthFactorProviderQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($id))
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->executeOne();
+ if (!$provider) {
+ return new Aphront404Response();
+ }
+
+ $cancel_uri = $provider->getURI();
+ $enroll_key =
+ PhabricatorAuthFactorProviderEnrollMessageTransaction::TRANSACTIONTYPE;
+
+ $message = $provider->getEnrollMessage();
+
+ if ($request->isFormOrHisecPost()) {
+ $message = $request->getStr('message');
+
+ $xactions = array();
+
+ $xactions[] = id(new PhabricatorAuthFactorProviderTransaction())
+ ->setTransactionType($enroll_key)
+ ->setNewValue($message);
+
+ $editor = id(new PhabricatorAuthFactorProviderEditor())
+ ->setActor($viewer)
+ ->setContentSourceFromRequest($request)
+ ->setContinueOnNoEffect(true)
+ ->setContinueOnMissingFields(true)
+ ->setCancelURI($cancel_uri);
+
+ $editor->applyTransactions($provider, $xactions);
+
+ return id(new AphrontRedirectResponse())->setURI($cancel_uri);
+ }
+
+ $default_message = $provider->getEnrollDescription($viewer);
+ $default_message = new PHUIRemarkupView($viewer, $default_message);
+
+ $form = id(new AphrontFormView())
+ ->setViewer($viewer)
+ ->appendRemarkupInstructions(
+ pht(
+ 'When users add a factor for this provider, they are given this '.
+ 'enrollment guidance by default:'))
+ ->appendControl(
+ id(new AphrontFormMarkupControl())
+ ->setLabel(pht('Default Message'))
+ ->setValue($default_message))
+ ->appendRemarkupInstructions(
+ pht(
+ 'You may optionally customize the enrollment message users are '.
+ 'presented with by providing a replacement message below:'))
+ ->appendControl(
+ id(new PhabricatorRemarkupControl())
+ ->setLabel(pht('Custom Message'))
+ ->setName('message')
+ ->setValue($message));
+
+ return $this->newDialog()
+ ->setTitle(pht('Change Enroll Message'))
+ ->setWidth(AphrontDialogView::WIDTH_FORM)
+ ->appendForm($form)
+ ->addCancelButton($cancel_uri)
+ ->addSubmitButton(pht('Save'));
+ }
+
+}
diff --git a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php
new file mode 100644
index 000000000..1dac49bcf
--- /dev/null
+++ b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php
@@ -0,0 +1,127 @@
+<?php
+
+final class PhabricatorAuthFactorProviderViewController
+ extends PhabricatorAuthFactorProviderController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $this->getViewer();
+
+ $this->requireApplicationCapability(
+ AuthManageProvidersCapability::CAPABILITY);
+
+ $provider = id(new PhabricatorAuthFactorProviderQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($request->getURIData('id')))
+ ->executeOne();
+ if (!$provider) {
+ return new Aphront404Response();
+ }
+
+ $crumbs = $this->buildApplicationCrumbs()
+ ->addTextCrumb($provider->getObjectName())
+ ->setBorder(true);
+
+ $header = $this->buildHeaderView($provider);
+ $properties = $this->buildPropertiesView($provider);
+ $curtain = $this->buildCurtain($provider);
+
+
+ $timeline = $this->buildTransactionTimeline(
+ $provider,
+ new PhabricatorAuthFactorProviderTransactionQuery());
+ $timeline->setShouldTerminate(true);
+
+ $view = id(new PHUITwoColumnView())
+ ->setHeader($header)
+ ->setCurtain($curtain)
+ ->setMainColumn(
+ array(
+ $timeline,
+ ))
+ ->addPropertySection(pht('Details'), $properties);
+
+ return $this->newPage()
+ ->setTitle($provider->getDisplayName())
+ ->setCrumbs($crumbs)
+ ->setPageObjectPHIDs(
+ array(
+ $provider->getPHID(),
+ ))
+ ->appendChild($view);
+ }
+
+ private function buildHeaderView(PhabricatorAuthFactorProvider $provider) {
+ $viewer = $this->getViewer();
+
+ $view = id(new PHUIHeaderView())
+ ->setViewer($viewer)
+ ->setHeader($provider->getDisplayName())
+ ->setPolicyObject($provider);
+
+ $status = $provider->newStatus();
+
+ $header_icon = $status->getStatusHeaderIcon();
+ $header_color = $status->getStatusHeaderColor();
+ $header_name = $status->getName();
+ if ($header_icon !== null) {
+ $view->setStatus($header_icon, $header_color, $header_name);
+ }
+
+ return $view;
+ }
+
+ private function buildPropertiesView(
+ PhabricatorAuthFactorProvider $provider) {
+ $viewer = $this->getViewer();
+
+ $view = id(new PHUIPropertyListView())
+ ->setViewer($viewer);
+
+ $view->addProperty(
+ pht('Factor Type'),
+ $provider->getFactor()->getFactorName());
+
+
+ $custom_enroll = $provider->getEnrollMessage();
+ if (strlen($custom_enroll)) {
+ $view->addSectionHeader(
+ pht('Custom Enroll Message'),
+ PHUIPropertyListView::ICON_SUMMARY);
+ $view->addTextContent(
+ new PHUIRemarkupView($viewer, $custom_enroll));
+ }
+
+ return $view;
+ }
+
+ private function buildCurtain(PhabricatorAuthFactorProvider $provider) {
+ $viewer = $this->getViewer();
+ $id = $provider->getID();
+
+ $can_edit = PhabricatorPolicyFilter::hasCapability(
+ $viewer,
+ $provider,
+ PhabricatorPolicyCapability::CAN_EDIT);
+
+ $curtain = $this->newCurtainView($provider);
+
+ $curtain->addAction(
+ id(new PhabricatorActionView())
+ ->setName(pht('Edit MFA Provider'))
+ ->setIcon('fa-pencil')
+ ->setHref($this->getApplicationURI("mfa/edit/{$id}/"))
+ ->setDisabled(!$can_edit)
+ ->setWorkflow(!$can_edit));
+
+ $curtain->addAction(
+ id(new PhabricatorActionView())
+ ->setName(pht('Customize Enroll Message'))
+ ->setIcon('fa-commenting-o')
+ ->setHref($this->getApplicationURI("mfa/message/{$id}/"))
+ ->setDisabled(!$can_edit)
+ ->setWorkflow(true));
+
+ return $curtain;
+ }
+
+}
diff --git a/src/applications/auth/editor/PhabricatorAuthContactNumberEditEngine.php b/src/applications/auth/editor/PhabricatorAuthContactNumberEditEngine.php
new file mode 100644
index 000000000..5b1a059b2
--- /dev/null
+++ b/src/applications/auth/editor/PhabricatorAuthContactNumberEditEngine.php
@@ -0,0 +1,86 @@
+<?php
+
+final class PhabricatorAuthContactNumberEditEngine
+ extends PhabricatorEditEngine {
+
+ const ENGINECONST = 'auth.contact';
+
+ public function isEngineConfigurable() {
+ return false;
+ }
+
+ public function getEngineName() {
+ return pht('Contact Numbers');
+ }
+
+ public function getSummaryHeader() {
+ return pht('Edit Contact Numbers');
+ }
+
+ public function getSummaryText() {
+ return pht('This engine is used to edit contact numbers.');
+ }
+
+ public function getEngineApplicationClass() {
+ return 'PhabricatorAuthApplication';
+ }
+
+ protected function newEditableObject() {
+ $viewer = $this->getViewer();
+ return PhabricatorAuthContactNumber::initializeNewContactNumber($viewer);
+ }
+
+ protected function newObjectQuery() {
+ return new PhabricatorAuthContactNumberQuery();
+ }
+
+ protected function getObjectCreateTitleText($object) {
+ return pht('Create Contact Number');
+ }
+
+ protected function getObjectCreateButtonText($object) {
+ return pht('Create Contact Number');
+ }
+
+ protected function getObjectEditTitleText($object) {
+ return pht('Edit Contact Number');
+ }
+
+ protected function getObjectEditShortText($object) {
+ return $object->getObjectName();
+ }
+
+ protected function getObjectCreateShortText() {
+ return pht('Create Contact Number');
+ }
+
+ protected function getObjectName() {
+ return pht('Contact Number');
+ }
+
+ protected function getEditorURI() {
+ return '/auth/contact/edit/';
+ }
+
+ protected function getObjectCreateCancelURI($object) {
+ return '/settings/panel/contact/';
+ }
+
+ protected function getObjectViewURI($object) {
+ return $object->getURI();
+ }
+
+ protected function buildCustomEditFields($object) {
+ return array(
+ id(new PhabricatorTextEditField())
+ ->setKey('contactNumber')
+ ->setTransactionType(
+ PhabricatorAuthContactNumberNumberTransaction::TRANSACTIONTYPE)
+ ->setLabel(pht('Contact Number'))
+ ->setDescription(pht('The contact number.'))
+ ->setValue($object->getContactNumber())
+ ->setIsRequired(true),
+ );
+ }
+
+}
diff --git a/src/applications/auth/editor/PhabricatorAuthContactNumberEditor.php b/src/applications/auth/editor/PhabricatorAuthContactNumberEditor.php
new file mode 100644
index 000000000..9dfb569e8
--- /dev/null
+++ b/src/applications/auth/editor/PhabricatorAuthContactNumberEditor.php
@@ -0,0 +1,38 @@
+<?php
+
+final class PhabricatorAuthContactNumberEditor
+ extends PhabricatorApplicationTransactionEditor {
+
+ public function getEditorApplicationClass() {
+ return 'PhabricatorAuthApplication';
+ }
+
+ public function getEditorObjectsDescription() {
+ return pht('Contact Numbers');
+ }
+
+ public function getCreateObjectTitle($author, $object) {
+ return pht('%s created this contact number.', $author);
+ }
+
+ public function getCreateObjectTitleForFeed($author, $object) {
+ return pht('%s created %s.', $author, $object);
+ }
+
+ protected function didCatchDuplicateKeyException(
+ PhabricatorLiskDAO $object,
+ array $xactions,
+ Exception $ex) {
+
+ $errors = array();
+ $errors[] = new PhabricatorApplicationTransactionValidationError(
+ PhabricatorAuthContactNumberNumberTransaction::TRANSACTIONTYPE,
+ pht('Duplicate'),
+ pht('This contact number is already in use.'),
+ null);
+
+ throw new PhabricatorApplicationTransactionValidationException($errors);
+ }
+
+
+}
diff --git a/src/applications/auth/editor/PhabricatorAuthFactorProviderEditEngine.php b/src/applications/auth/editor/PhabricatorAuthFactorProviderEditEngine.php
new file mode 100644
index 000000000..ab74350cc
--- /dev/null
+++ b/src/applications/auth/editor/PhabricatorAuthFactorProviderEditEngine.php
@@ -0,0 +1,133 @@
+<?php
+
+final class PhabricatorAuthFactorProviderEditEngine
+ extends PhabricatorEditEngine {
+
+ private $providerFactor;
+
+ const ENGINECONST = 'auth.factor.provider';
+
+ public function isEngineConfigurable() {
+ return false;
+ }
+
+ public function getEngineName() {
+ return pht('MFA Providers');
+ }
+
+ public function getSummaryHeader() {
+ return pht('Edit MFA Providers');
+ }
+
+ public function getSummaryText() {
+ return pht('This engine is used to edit MFA providers.');
+ }
+
+ public function getEngineApplicationClass() {
+ return 'PhabricatorAuthApplication';
+ }
+
+ public function setProviderFactor(PhabricatorAuthFactor $factor) {
+ $this->providerFactor = $factor;
+ return $this;
+ }
+
+ public function getProviderFactor() {
+ return $this->providerFactor;
+ }
+
+ protected function newEditableObject() {
+ $factor = $this->getProviderFactor();
+ if ($factor) {
+ $provider = PhabricatorAuthFactorProvider::initializeNewProvider($factor);
+ } else {
+ $provider = new PhabricatorAuthFactorProvider();
+ }
+
+ return $provider;
+ }
+
+ protected function newObjectQuery() {
+ return new PhabricatorAuthFactorProviderQuery();
+ }
+
+ protected function getObjectCreateTitleText($object) {
+ return pht('Create MFA Provider');
+ }
+
+ protected function getObjectCreateButtonText($object) {
+ return pht('Create MFA Provider');
+ }
+
+ protected function getObjectEditTitleText($object) {
+ return pht('Edit MFA Provider');
+ }
+
+ protected function getObjectEditShortText($object) {
+ return $object->getObjectName();
+ }
+
+ protected function getObjectCreateShortText() {
+ return pht('Create MFA Provider');
+ }
+
+ protected function getObjectName() {
+ return pht('MFA Provider');
+ }
+
+ protected function getEditorURI() {
+ return '/auth/mfa/edit/';
+ }
+
+ protected function getObjectCreateCancelURI($object) {
+ return '/auth/mfa/';
+ }
+
+ protected function getObjectViewURI($object) {
+ return $object->getURI();
+ }
+
+ protected function getCreateNewObjectPolicy() {
+ return $this->getApplication()->getPolicy(
+ AuthManageProvidersCapability::CAPABILITY);
+ }
+
+ protected function buildCustomEditFields($object) {
+ $factor = $object->getFactor();
+ $factor_name = $factor->getFactorName();
+
+ $status_map = PhabricatorAuthFactorProviderStatus::getMap();
+
+ $fields = array(
+ id(new PhabricatorStaticEditField())
+ ->setKey('displayType')
+ ->setLabel(pht('Factor Type'))
+ ->setDescription(pht('Type of the MFA provider.'))
+ ->setValue($factor_name),
+ id(new PhabricatorTextEditField())
+ ->setKey('name')
+ ->setTransactionType(
+ PhabricatorAuthFactorProviderNameTransaction::TRANSACTIONTYPE)
+ ->setLabel(pht('Name'))
+ ->setDescription(pht('Display name for the MFA provider.'))
+ ->setValue($object->getName())
+ ->setPlaceholder($factor_name),
+ id(new PhabricatorSelectEditField())
+ ->setKey('status')
+ ->setTransactionType(
+ PhabricatorAuthFactorProviderStatusTransaction::TRANSACTIONTYPE)
+ ->setLabel(pht('Status'))
+ ->setDescription(pht('Status of the MFA provider.'))
+ ->setValue($object->getStatus())
+ ->setOptions($status_map),
+ );
+
+ $factor_fields = $factor->newEditEngineFields($this, $object);
+ foreach ($factor_fields as $field) {
+ $fields[] = $field;
+ }
+
+ return $fields;
+ }
+
+}
diff --git a/src/applications/auth/editor/PhabricatorAuthFactorProviderEditor.php b/src/applications/auth/editor/PhabricatorAuthFactorProviderEditor.php
new file mode 100644
index 000000000..144f27539
--- /dev/null
+++ b/src/applications/auth/editor/PhabricatorAuthFactorProviderEditor.php
@@ -0,0 +1,22 @@
+<?php
+
+final class PhabricatorAuthFactorProviderEditor
+ extends PhabricatorApplicationTransactionEditor {
+
+ public function getEditorApplicationClass() {
+ return 'PhabricatorAuthApplication';
+ }
+
+ public function getEditorObjectsDescription() {
+ return pht('MFA Providers');
+ }
+
+ public function getCreateObjectTitle($author, $object) {
+ return pht('%s created this MFA provider.', $author);
+ }
+
+ public function getCreateObjectTitleForFeed($author, $object) {
+ return pht('%s created %s.', $author, $object);
+ }
+
+}
diff --git a/src/applications/auth/editor/PhabricatorAuthMessageEditEngine.php b/src/applications/auth/editor/PhabricatorAuthMessageEditEngine.php
new file mode 100644
index 000000000..0a9aa32de
--- /dev/null
+++ b/src/applications/auth/editor/PhabricatorAuthMessageEditEngine.php
@@ -0,0 +1,108 @@
+<?php
+
+final class PhabricatorAuthMessageEditEngine
+ extends PhabricatorEditEngine {
+
+ private $messageType;
+
+ const ENGINECONST = 'auth.message';
+
+ public function isEngineConfigurable() {
+ return false;
+ }
+
+ public function getEngineName() {
+ return pht('Auth Messages');
+ }
+
+ public function getSummaryHeader() {
+ return pht('Edit Auth Messages');
+ }
+
+ public function getSummaryText() {
+ return pht('This engine is used to edit authentication messages.');
+ }
+
+ public function getEngineApplicationClass() {
+ return 'PhabricatorAuthApplication';
+ }
+
+ public function setMessageType(PhabricatorAuthMessageType $type) {
+ $this->messageType = $type;
+ return $this;
+ }
+
+ public function getMessageType() {
+ return $this->messageType;
+ }
+
+ protected function newEditableObject() {
+ $type = $this->getMessageType();
+
+ if ($type) {
+ $message = PhabricatorAuthMessage::initializeNewMessage($type);
+ } else {
+ $message = new PhabricatorAuthMessage();
+ }
+
+ return $message;
+ }
+
+ protected function newObjectQuery() {
+ return new PhabricatorAuthMessageQuery();
+ }
+
+ protected function getObjectCreateTitleText($object) {
+ return pht('Create Auth Message');
+ }
+
+ protected function getObjectCreateButtonText($object) {
+ return pht('Create Auth Message');
+ }
+
+ protected function getObjectEditTitleText($object) {
+ return pht('Edit Auth Message');
+ }
+
+ protected function getObjectEditShortText($object) {
+ return $object->getObjectName();
+ }
+
+ protected function getObjectCreateShortText() {
+ return pht('Create Auth Message');
+ }
+
+ protected function getObjectName() {
+ return pht('Auth Message');
+ }
+
+ protected function getEditorURI() {
+ return '/auth/message/edit/';
+ }
+
+ protected function getObjectCreateCancelURI($object) {
+ return '/auth/message/';
+ }
+
+ protected function getObjectViewURI($object) {
+ return $object->getURI();
+ }
+
+ protected function getCreateNewObjectPolicy() {
+ return $this->getApplication()->getPolicy(
+ AuthManageProvidersCapability::CAPABILITY);
+ }
+
+ protected function buildCustomEditFields($object) {
+ return array(
+ id(new PhabricatorRemarkupEditField())
+ ->setKey('messageText')
+ ->setTransactionType(
+ PhabricatorAuthMessageTextTransaction::TRANSACTIONTYPE)
+ ->setLabel(pht('Message Text'))
+ ->setDescription(pht('Custom text for the message.'))
+ ->setValue($object->getMessageText()),
+ );
+ }
+
+}
diff --git a/src/applications/auth/editor/PhabricatorAuthMessageEditor.php b/src/applications/auth/editor/PhabricatorAuthMessageEditor.php
new file mode 100644
index 000000000..56e8e716c
--- /dev/null
+++ b/src/applications/auth/editor/PhabricatorAuthMessageEditor.php
@@ -0,0 +1,22 @@
+<?php
+
+final class PhabricatorAuthMessageEditor
+ extends PhabricatorApplicationTransactionEditor {
+
+ public function getEditorApplicationClass() {
+ return 'PhabricatorAuthApplication';
+ }
+
+ public function getEditorObjectsDescription() {
+ return pht('Auth Messages');
+ }
+
+ public function getCreateObjectTitle($author, $object) {
+ return pht('%s created this message.', $author);
+ }
+
+ public function getCreateObjectTitleForFeed($author, $object) {
+ return pht('%s created %s.', $author, $object);
+ }
+
+}
diff --git a/src/applications/auth/engine/PhabricatorAuthCSRFEngine.php b/src/applications/auth/engine/PhabricatorAuthCSRFEngine.php
new file mode 100644
index 000000000..fcb8c13ab
--- /dev/null
+++ b/src/applications/auth/engine/PhabricatorAuthCSRFEngine.php
@@ -0,0 +1,119 @@
+<?php
+
+final class PhabricatorAuthCSRFEngine extends Phobject {
+
+ private $salt;
+ private $secret;
+
+ public function setSalt($salt) {
+ $this->salt = $salt;
+ return $this;
+ }
+
+ public function getSalt() {
+ return $this->salt;
+ }
+
+ public function setSecret(PhutilOpaqueEnvelope $secret) {
+ $this->secret = $secret;
+ return $this;
+ }
+
+ public function getSecret() {
+ return $this->secret;
+ }
+
+ public function newSalt() {
+ $salt_length = $this->getSaltLength();
+ return Filesystem::readRandomCharacters($salt_length);
+ }
+
+ public function newToken() {
+ $salt = $this->getSalt();
+
+ if (!$salt) {
+ throw new PhutilInvalidStateException('setSalt');
+ }
+
+ $token = $this->newRawToken($salt);
+ $prefix = $this->getBREACHPrefix();
+
+ return sprintf('%s%s%s', $prefix, $salt, $token);
+ }
+
+ public function isValidToken($token) {
+ $salt_length = $this->getSaltLength();
+
+ // We expect a BREACH-mitigating token. See T3684.
+ $breach_prefix = $this->getBREACHPrefix();
+ $breach_prelen = strlen($breach_prefix);
+ if (strncmp($token, $breach_prefix, $breach_prelen) !== 0) {
+ return false;
+ }
+
+ $salt = substr($token, $breach_prelen, $salt_length);
+ $token = substr($token, $breach_prelen + $salt_length);
+
+ foreach ($this->getWindowOffsets() as $offset) {
+ $expect_token = $this->newRawToken($salt, $offset);
+ if (phutil_hashes_are_identical($expect_token, $token)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private function newRawToken($salt, $offset = 0) {
+ $now = PhabricatorTime::getNow();
+ $cycle_frequency = $this->getCycleFrequency();
+
+ $time_block = (int)floor($now / $cycle_frequency);
+ $time_block = $time_block + $offset;
+
+ $secret = $this->getSecret();
+ if (!$secret) {
+ throw new PhutilInvalidStateException('setSecret');
+ }
+ $secret = $secret->openEnvelope();
+
+ $hash = PhabricatorHash::digestWithNamedKey(
+ $secret.$time_block.$salt,
+ 'csrf');
+
+ return substr($hash, 0, $this->getTokenLength());
+ }
+
+ private function getBREACHPrefix() {
+ return 'B@';
+ }
+
+ private function getSaltLength() {
+ return 8;
+ }
+
+ private function getTokenLength() {
+ return 16;
+ }
+
+ private function getCycleFrequency() {
+ return phutil_units('1 hour in seconds');
+ }
+
+ private function getWindowOffsets() {
+ // We accept some tokens from the recent past and near future. Users may
+ // have older tokens if they close their laptop and open it up again
+ // later. Users may have newer tokens if there are multiple web hosts with
+ // a bit of clock skew.
+
+ // Javascript on the client tries to keep CSRF tokens up to date, but
+ // it may fail, and it doesn't run if the user closes their laptop.
+
+ // The window during which our tokens remain valid is generally more
+ // conservative than other platforms. For example, Rails uses "session
+ // duration" and Django uses "forever".
+
+ return range(-6, 1);
+ }
+
+}
diff --git a/src/applications/auth/engine/PhabricatorAuthContactNumberMFAEngine.php b/src/applications/auth/engine/PhabricatorAuthContactNumberMFAEngine.php
new file mode 100644
index 000000000..969ca320a
--- /dev/null
+++ b/src/applications/auth/engine/PhabricatorAuthContactNumberMFAEngine.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhabricatorAuthContactNumberMFAEngine
+ extends PhabricatorEditEngineMFAEngine {
+
+ public function shouldTryMFA() {
+ return true;
+ }
+
+}
diff --git a/src/applications/auth/engine/PhabricatorAuthFactorProviderMFAEngine.php b/src/applications/auth/engine/PhabricatorAuthFactorProviderMFAEngine.php
new file mode 100644
index 000000000..39f80bb5b
--- /dev/null
+++ b/src/applications/auth/engine/PhabricatorAuthFactorProviderMFAEngine.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhabricatorAuthFactorProviderMFAEngine
+ extends PhabricatorEditEngineMFAEngine {
+
+ public function shouldTryMFA() {
+ return true;
+ }
+
+}
diff --git a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php
index 2020e4a54..c05280522 100644
--- a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php
+++ b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php
@@ -1,940 +1,1163 @@
<?php
/**
*
* @task use Using Sessions
* @task new Creating Sessions
* @task hisec High Security
* @task partial Partial Sessions
* @task onetime One Time Login URIs
* @task cache User Cache
*/
final class PhabricatorAuthSessionEngine extends Phobject {
/**
* Session issued to normal users after they login through a standard channel.
* Associates the client with a standard user identity.
*/
const KIND_USER = 'U';
/**
* Session issued to users who login with some sort of credentials but do not
* have full accounts. These are sometimes called "grey users".
*
* TODO: We do not currently issue these sessions, see T4310.
*/
const KIND_EXTERNAL = 'X';
/**
* Session issued to logged-out users which has no real identity information.
* Its purpose is to protect logged-out users from CSRF.
*/
const KIND_ANONYMOUS = 'A';
/**
* Session kind isn't known.
*/
const KIND_UNKNOWN = '?';
const ONETIME_RECOVER = 'recover';
const ONETIME_RESET = 'reset';
const ONETIME_WELCOME = 'welcome';
const ONETIME_USERNAME = 'rename';
+ private $workflowKey;
+ private $request;
+
+ public function setWorkflowKey($workflow_key) {
+ $this->workflowKey = $workflow_key;
+ return $this;
+ }
+
+ public function getWorkflowKey() {
+
+ // TODO: A workflow key should become required in order to issue an MFA
+ // challenge, but allow things to keep working for now until we can update
+ // callsites.
+ if ($this->workflowKey === null) {
+ return 'legacy';
+ }
+
+ return $this->workflowKey;
+ }
+
+ public function getRequest() {
+ return $this->request;
+ }
+
+
/**
* Get the session kind (e.g., anonymous, user, external account) from a
* session token. Returns a `KIND_` constant.
*
* @param string Session token.
* @return const Session kind constant.
*/
public static function getSessionKindFromToken($session_token) {
if (strpos($session_token, '/') === false) {
// Old-style session, these are all user sessions.
return self::KIND_USER;
}
list($kind, $key) = explode('/', $session_token, 2);
switch ($kind) {
case self::KIND_ANONYMOUS:
case self::KIND_USER:
case self::KIND_EXTERNAL:
return $kind;
default:
return self::KIND_UNKNOWN;
}
}
/**
* Load the user identity associated with a session of a given type,
* identified by token.
*
* When the user presents a session token to an API, this method verifies
* it is of the correct type and loads the corresponding identity if the
* session exists and is valid.
*
* NOTE: `$session_type` is the type of session that is required by the
* loading context. This prevents use of a Conduit sesssion as a Web
* session, for example.
*
* @param const The type of session to load.
* @param string The session token.
* @return PhabricatorUser|null
* @task use
*/
public function loadUserForSession($session_type, $session_token) {
$session_kind = self::getSessionKindFromToken($session_token);
switch ($session_kind) {
case self::KIND_ANONYMOUS:
// Don't bother trying to load a user for an anonymous session, since
// neither the session nor the user exist.
return null;
case self::KIND_UNKNOWN:
// If we don't know what kind of session this is, don't go looking for
// it.
return null;
case self::KIND_USER:
break;
case self::KIND_EXTERNAL:
// TODO: Implement these (T4310).
return null;
}
$session_table = new PhabricatorAuthSession();
$user_table = new PhabricatorUser();
$conn = $session_table->establishConnection('r');
// TODO: See T13225. We're moving sessions to a more modern digest
// algorithm, but still accept older cookies for compatibility.
$session_key = PhabricatorAuthSession::newSessionDigest(
new PhutilOpaqueEnvelope($session_token));
$weak_key = PhabricatorHash::weakDigest($session_token);
$cache_parts = $this->getUserCacheQueryParts($conn);
list($cache_selects, $cache_joins, $cache_map, $types_map) = $cache_parts;
$info = queryfx_one(
$conn,
'SELECT
s.id AS s_id,
s.phid AS s_phid,
s.sessionExpires AS s_sessionExpires,
s.sessionStart AS s_sessionStart,
s.highSecurityUntil AS s_highSecurityUntil,
s.isPartial AS s_isPartial,
s.signedLegalpadDocuments as s_signedLegalpadDocuments,
IF(s.sessionKey = %P, 1, 0) as s_weak,
u.*
%Q
FROM %R u JOIN %R s ON u.phid = s.userPHID
AND s.type = %s AND s.sessionKey IN (%P, %P) %Q',
new PhutilOpaqueEnvelope($weak_key),
$cache_selects,
$user_table,
$session_table,
$session_type,
new PhutilOpaqueEnvelope($session_key),
new PhutilOpaqueEnvelope($weak_key),
$cache_joins);
if (!$info) {
return null;
}
// TODO: Remove this, see T13225.
$is_weak = (bool)$info['s_weak'];
unset($info['s_weak']);
$session_dict = array(
'userPHID' => $info['phid'],
'sessionKey' => $session_key,
'type' => $session_type,
);
$cache_raw = array_fill_keys($cache_map, null);
foreach ($info as $key => $value) {
if (strncmp($key, 's_', 2) === 0) {
unset($info[$key]);
$session_dict[substr($key, 2)] = $value;
continue;
}
if (isset($cache_map[$key])) {
unset($info[$key]);
$cache_raw[$cache_map[$key]] = $value;
continue;
}
}
$user = $user_table->loadFromArray($info);
$cache_raw = $this->filterRawCacheData($user, $types_map, $cache_raw);
$user->attachRawCacheData($cache_raw);
switch ($session_type) {
case PhabricatorAuthSession::TYPE_WEB:
// Explicitly prevent bots and mailing lists from establishing web
// sessions. It's normally impossible to attach authentication to these
// accounts, and likewise impossible to generate sessions, but it's
// technically possible that a session could exist in the database. If
// one does somehow, refuse to load it.
if (!$user->canEstablishWebSessions()) {
return null;
}
break;
}
$session = id(new PhabricatorAuthSession())->loadFromArray($session_dict);
- $ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type);
-
- // If more than 20% of the time on this session has been used, refresh the
- // TTL back up to the full duration. The idea here is that sessions are
- // good forever if used regularly, but get GC'd when they fall out of use.
-
- // NOTE: If we begin rotating session keys when extending sessions, the
- // CSRF code needs to be updated so CSRF tokens survive session rotation.
-
- if (time() + (0.80 * $ttl) > $session->getSessionExpires()) {
- $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
- $conn_w = $session_table->establishConnection('w');
- queryfx(
- $conn_w,
- 'UPDATE %T SET sessionExpires = UNIX_TIMESTAMP() + %d WHERE id = %d',
- $session->getTableName(),
- $ttl,
- $session->getID());
- unset($unguarded);
- }
+ $this->extendSession($session);
// TODO: Remove this, see T13225.
if ($is_weak) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$conn_w = $session_table->establishConnection('w');
queryfx(
$conn_w,
'UPDATE %T SET sessionKey = %P WHERE id = %d',
$session->getTableName(),
new PhutilOpaqueEnvelope($session_key),
$session->getID());
unset($unguarded);
}
$user->attachSession($session);
return $user;
}
/**
* Issue a new session key for a given identity. Phabricator supports
* different types of sessions (like "web" and "conduit") and each session
* type may have multiple concurrent sessions (this allows a user to be
* logged in on multiple browsers at the same time, for instance).
*
* Note that this method is transport-agnostic and does not set cookies or
* issue other types of tokens, it ONLY generates a new session key.
*
* You can configure the maximum number of concurrent sessions for various
* session types in the Phabricator configuration.
*
* @param const Session type constant (see
* @{class:PhabricatorAuthSession}).
* @param phid|null Identity to establish a session for, usually a user
* PHID. With `null`, generates an anonymous session.
* @param bool True to issue a partial session.
* @return string Newly generated session key.
*/
public function establishSession($session_type, $identity_phid, $partial) {
// Consume entropy to generate a new session key, forestalling the eventual
// heat death of the universe.
$session_key = Filesystem::readRandomCharacters(40);
if ($identity_phid === null) {
return self::KIND_ANONYMOUS.'/'.$session_key;
}
$session_table = new PhabricatorAuthSession();
$conn_w = $session_table->establishConnection('w');
// This has a side effect of validating the session type.
- $session_ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type);
+ $session_ttl = PhabricatorAuthSession::getSessionTypeTTL(
+ $session_type,
+ $partial);
$digest_key = PhabricatorAuthSession::newSessionDigest(
new PhutilOpaqueEnvelope($session_key));
// Logging-in users don't have CSRF stuff yet, so we have to unguard this
// write.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
id(new PhabricatorAuthSession())
->setUserPHID($identity_phid)
->setType($session_type)
->setSessionKey($digest_key)
->setSessionStart(time())
->setSessionExpires(time() + $session_ttl)
->setIsPartial($partial ? 1 : 0)
->setSignedLegalpadDocuments(0)
->save();
$log = PhabricatorUserLog::initializeNewLog(
null,
$identity_phid,
($partial
? PhabricatorUserLog::ACTION_LOGIN_PARTIAL
: PhabricatorUserLog::ACTION_LOGIN));
$log->setDetails(
array(
'session_type' => $session_type,
));
$log->setSession($digest_key);
$log->save();
unset($unguarded);
$info = id(new PhabricatorAuthSessionInfo())
->setSessionType($session_type)
->setIdentityPHID($identity_phid)
->setIsPartial($partial);
$extensions = PhabricatorAuthSessionEngineExtension::getAllExtensions();
foreach ($extensions as $extension) {
$extension->didEstablishSession($info);
}
return $session_key;
}
/**
* Terminate all of a user's login sessions.
*
* This is used when users change passwords, linked accounts, or add
* multifactor authentication.
*
* @param PhabricatorUser User whose sessions should be terminated.
* @param string|null Optionally, one session to keep. Normally, the current
* login session.
*
* @return void
*/
public function terminateLoginSessions(
PhabricatorUser $user,
PhutilOpaqueEnvelope $except_session = null) {
$sessions = id(new PhabricatorAuthSessionQuery())
->setViewer($user)
->withIdentityPHIDs(array($user->getPHID()))
->execute();
if ($except_session !== null) {
$except_session = PhabricatorAuthSession::newSessionDigest(
$except_session);
}
foreach ($sessions as $key => $session) {
if ($except_session !== null) {
$is_except = phutil_hashes_are_identical(
$session->getSessionKey(),
$except_session);
if ($is_except) {
continue;
}
}
$session->delete();
}
}
public function logoutSession(
PhabricatorUser $user,
PhabricatorAuthSession $session) {
$log = PhabricatorUserLog::initializeNewLog(
$user,
$user->getPHID(),
PhabricatorUserLog::ACTION_LOGOUT);
$log->save();
$extensions = PhabricatorAuthSessionEngineExtension::getAllExtensions();
foreach ($extensions as $extension) {
$extension->didLogout($user, array($session));
}
$session->delete();
}
/* -( High Security )------------------------------------------------------ */
/**
* Require the user respond to a high security (MFA) check.
*
* This method differs from @{method:requireHighSecuritySession} in that it
* does not upgrade the user's session as a side effect. This method is
* appropriate for one-time checks.
*
* @param PhabricatorUser User whose session needs to be in high security.
* @param AphrontReqeust Current request.
* @param string URI to return the user to if they cancel.
* @return PhabricatorAuthHighSecurityToken Security token.
* @task hisec
*/
public function requireHighSecurityToken(
PhabricatorUser $viewer,
AphrontRequest $request,
$cancel_uri) {
return $this->newHighSecurityToken(
$viewer,
$request,
$cancel_uri,
false,
false);
}
/**
* Require high security, or prompt the user to enter high security.
*
* If the user's session is in high security, this method will return a
* token. Otherwise, it will throw an exception which will eventually
* be converted into a multi-factor authentication workflow.
*
* This method upgrades the user's session to high security for a short
* period of time, and is appropriate if you anticipate they may need to
* take multiple high security actions. To perform a one-time check instead,
* use @{method:requireHighSecurityToken}.
*
* @param PhabricatorUser User whose session needs to be in high security.
* @param AphrontReqeust Current request.
* @param string URI to return the user to if they cancel.
* @param bool True to jump partial sessions directly into high
* security instead of just upgrading them to full
* sessions.
* @return PhabricatorAuthHighSecurityToken Security token.
* @task hisec
*/
public function requireHighSecuritySession(
PhabricatorUser $viewer,
AphrontRequest $request,
$cancel_uri,
$jump_into_hisec = false) {
return $this->newHighSecurityToken(
$viewer,
$request,
$cancel_uri,
- false,
+ $jump_into_hisec,
true);
}
private function newHighSecurityToken(
PhabricatorUser $viewer,
AphrontRequest $request,
$cancel_uri,
$jump_into_hisec,
$upgrade_session) {
if (!$viewer->hasSession()) {
throw new Exception(
pht('Requiring a high-security session from a user with no session!'));
}
// TODO: If a user answers a "requireHighSecurityToken()" prompt and hits
// a "requireHighSecuritySession()" prompt a short time later, the one-shot
// token should be good enough to upgrade the session.
$session = $viewer->getSession();
// Check if the session is already in high security mode.
$token = $this->issueHighSecurityToken($session);
if ($token) {
return $token;
}
- // Load the multi-factor auth sources attached to this account.
- $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
- 'userPHID = %s',
- $viewer->getPHID());
+ // Load the multi-factor auth sources attached to this account. Note that
+ // we order factors from oldest to newest, which is not the default query
+ // ordering but makes the greatest sense in context.
+ $factors = id(new PhabricatorAuthFactorConfigQuery())
+ ->setViewer($viewer)
+ ->withUserPHIDs(array($viewer->getPHID()))
+ ->withFactorProviderStatuses(
+ array(
+ PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE,
+ PhabricatorAuthFactorProviderStatus::STATUS_DEPRECATED,
+ ))
+ ->execute();
+
+ // Sort factors in the same order that they appear in on the Settings
+ // panel. This means that administrators changing provider statuses may
+ // change the order of prompts for users, but the alternative is that the
+ // Settings panel order disagrees with the prompt order, which seems more
+ // disruptive.
+ $factors = msort($factors, 'newSortVector');
// If the account has no associated multi-factor auth, just issue a token
// without putting the session into high security mode. This is generally
// easier for users. A minor but desirable side effect is that when a user
// adds an auth factor, existing sessions won't get a free pass into hisec,
// since they never actually got marked as hisec.
if (!$factors) {
return $this->issueHighSecurityToken($session, true);
}
+ $this->request = $request;
+ foreach ($factors as $factor) {
+ $factor->setSessionEngine($this);
+ }
+
// Check for a rate limit without awarding points, so the user doesn't
// get partway through the workflow only to get blocked.
PhabricatorSystemActionEngine::willTakeAction(
array($viewer->getPHID()),
new PhabricatorAuthTryFactorAction(),
0);
+ $now = PhabricatorTime::getNow();
+
+ // We need to do challenge validation first, since this happens whether you
+ // submitted responses or not. You can't get a "bad response" error before
+ // you actually submit a response, but you can get a "wait, we can't
+ // issue a challenge yet" response. Load all issued challenges which are
+ // currently valid.
+ $challenges = id(new PhabricatorAuthChallengeQuery())
+ ->setViewer($viewer)
+ ->withFactorPHIDs(mpull($factors, 'getPHID'))
+ ->withUserPHIDs(array($viewer->getPHID()))
+ ->withChallengeTTLBetween($now, null)
+ ->execute();
+
+ PhabricatorAuthChallenge::newChallengeResponsesFromRequest(
+ $challenges,
+ $request);
+
+ $challenge_map = mgroup($challenges, 'getFactorPHID');
+
$validation_results = array();
+ $ok = true;
+
+ // Validate each factor against issued challenges. For example, this
+ // prevents you from receiving or responding to a TOTP challenge if another
+ // challenge was recently issued to a different session.
+ foreach ($factors as $factor) {
+ $factor_phid = $factor->getPHID();
+ $issued_challenges = idx($challenge_map, $factor_phid, array());
+ $provider = $factor->getFactorProvider();
+ $impl = $provider->getFactor();
+
+ $new_challenges = $impl->getNewIssuedChallenges(
+ $factor,
+ $viewer,
+ $issued_challenges);
+
+ // NOTE: We may get a list of challenges back, or may just get an early
+ // result. For example, this can happen on an SMS factor if all SMS
+ // mailers have been disabled.
+ if ($new_challenges instanceof PhabricatorAuthFactorResult) {
+ $result = $new_challenges;
+
+ if (!$result->getIsValid()) {
+ $ok = false;
+ }
+
+ $validation_results[$factor_phid] = $result;
+ $challenge_map[$factor_phid] = $issued_challenges;
+ continue;
+ }
+
+ foreach ($new_challenges as $new_challenge) {
+ $issued_challenges[] = $new_challenge;
+ }
+ $challenge_map[$factor_phid] = $issued_challenges;
+
+ if (!$issued_challenges) {
+ continue;
+ }
+
+ $result = $impl->getResultFromIssuedChallenges(
+ $factor,
+ $viewer,
+ $issued_challenges);
+
+ if (!$result) {
+ continue;
+ }
+
+ if (!$result->getIsValid()) {
+ $ok = false;
+ }
+
+ $validation_results[$factor_phid] = $result;
+ }
+
if ($request->isHTTPPost()) {
$request->validateCSRF();
if ($request->getExists(AphrontRequest::TYPE_HISEC)) {
// Limit factor verification rates to prevent brute force attacks.
- PhabricatorSystemActionEngine::willTakeAction(
- array($viewer->getPHID()),
- new PhabricatorAuthTryFactorAction(),
- 1);
+ $any_attempt = false;
+ foreach ($factors as $factor) {
+ $factor_phid = $factor->getPHID();
+
+ $provider = $factor->getFactorProvider();
+ $impl = $provider->getFactor();
+
+ // If we already have a result (normally "wait..."), we won't try
+ // to validate whatever the user submitted, so this doesn't count as
+ // an attempt for rate limiting purposes.
+ if (isset($validation_results[$factor_phid])) {
+ continue;
+ }
+
+ if ($impl->getRequestHasChallengeResponse($factor, $request)) {
+ $any_attempt = true;
+ break;
+ }
+ }
+
+ if ($any_attempt) {
+ PhabricatorSystemActionEngine::willTakeAction(
+ array($viewer->getPHID()),
+ new PhabricatorAuthTryFactorAction(),
+ 1);
+ }
- $ok = true;
foreach ($factors as $factor) {
- $id = $factor->getID();
- $impl = $factor->requireImplementation();
+ $factor_phid = $factor->getPHID();
- $validation_results[$id] = $impl->processValidateFactorForm(
+ // If we already have a validation result from previously issued
+ // challenges, skip validating this factor.
+ if (isset($validation_results[$factor_phid])) {
+ continue;
+ }
+
+ $issued_challenges = idx($challenge_map, $factor_phid, array());
+
+ $provider = $factor->getFactorProvider();
+ $impl = $provider->getFactor();
+
+ $validation_result = $impl->getResultFromChallengeResponse(
$factor,
$viewer,
- $request);
+ $request,
+ $issued_challenges);
- if (!$impl->isFactorValid($factor, $validation_results[$id])) {
+ if (!$validation_result->getIsValid()) {
$ok = false;
}
+
+ $validation_results[$factor_phid] = $validation_result;
}
if ($ok) {
+ // We're letting you through, so mark all the challenges you
+ // responded to as completed. These challenges can never be used
+ // again, even by the same session and workflow: you can't use the
+ // same response to take two different actions, even if those actions
+ // are of the same type.
+ foreach ($validation_results as $validation_result) {
+ $challenge = $validation_result->getAnsweredChallenge()
+ ->markChallengeAsCompleted();
+ }
+
// Give the user a credit back for a successful factor verification.
- PhabricatorSystemActionEngine::willTakeAction(
- array($viewer->getPHID()),
- new PhabricatorAuthTryFactorAction(),
- -1);
+ if ($any_attempt) {
+ PhabricatorSystemActionEngine::willTakeAction(
+ array($viewer->getPHID()),
+ new PhabricatorAuthTryFactorAction(),
+ -1);
+ }
if ($session->getIsPartial() && !$jump_into_hisec) {
// If we have a partial session and are not jumping directly into
// hisec, just issue a token without putting it in high security
// mode.
return $this->issueHighSecurityToken($session, true);
}
// If we aren't upgrading the session itself, just issue a token.
if (!$upgrade_session) {
return $this->issueHighSecurityToken($session, true);
}
$until = time() + phutil_units('15 minutes in seconds');
$session->setHighSecurityUntil($until);
queryfx(
$session->establishConnection('w'),
'UPDATE %T SET highSecurityUntil = %d WHERE id = %d',
$session->getTableName(),
$until,
$session->getID());
$log = PhabricatorUserLog::initializeNewLog(
$viewer,
$viewer->getPHID(),
PhabricatorUserLog::ACTION_ENTER_HISEC);
$log->save();
} else {
$log = PhabricatorUserLog::initializeNewLog(
$viewer,
$viewer->getPHID(),
PhabricatorUserLog::ACTION_FAIL_HISEC);
$log->save();
}
}
}
$token = $this->issueHighSecurityToken($session);
if ($token) {
return $token;
}
+ // If we don't have a validation result for some factors yet, fill them
+ // in with an empty result so form rendering doesn't have to care if the
+ // results exist or not. This happens when you first load the form and have
+ // not submitted any responses yet.
+ foreach ($factors as $factor) {
+ $factor_phid = $factor->getPHID();
+ if (isset($validation_results[$factor_phid])) {
+ continue;
+ }
+ $validation_results[$factor_phid] = new PhabricatorAuthFactorResult();
+ }
+
throw id(new PhabricatorAuthHighSecurityRequiredException())
->setCancelURI($cancel_uri)
+ ->setIsSessionUpgrade($upgrade_session)
->setFactors($factors)
->setFactorValidationResults($validation_results);
}
/**
* Issue a high security token for a session, if authorized.
*
* @param PhabricatorAuthSession Session to issue a token for.
* @param bool Force token issue.
* @return PhabricatorAuthHighSecurityToken|null Token, if authorized.
* @task hisec
*/
private function issueHighSecurityToken(
PhabricatorAuthSession $session,
$force = false) {
if ($session->isHighSecuritySession() || $force) {
return new PhabricatorAuthHighSecurityToken();
}
return null;
}
/**
* Render a form for providing relevant multi-factor credentials.
*
* @param PhabricatorUser Viewing user.
* @param AphrontRequest Current request.
* @return AphrontFormView Renderable form.
* @task hisec
*/
public function renderHighSecurityForm(
array $factors,
array $validation_results,
PhabricatorUser $viewer,
AphrontRequest $request) {
+ assert_instances_of($validation_results, 'PhabricatorAuthFactorResult');
$form = id(new AphrontFormView())
->setUser($viewer)
->appendRemarkupInstructions('');
+ $answered = array();
foreach ($factors as $factor) {
- $factor->requireImplementation()->renderValidateFactorForm(
+ $result = $validation_results[$factor->getPHID()];
+
+ $provider = $factor->getFactorProvider();
+ $impl = $provider->getFactor();
+
+ $impl->renderValidateFactorForm(
$factor,
$form,
$viewer,
- idx($validation_results, $factor->getID()));
+ $result);
+
+ $answered_challenge = $result->getAnsweredChallenge();
+ if ($answered_challenge) {
+ $answered[] = $answered_challenge;
+ }
}
$form->appendRemarkupInstructions('');
+ if ($answered) {
+ $http_params = PhabricatorAuthChallenge::newHTTPParametersFromChallenges(
+ $answered);
+ foreach ($http_params as $key => $value) {
+ $form->addHiddenInput($key, $value);
+ }
+ }
+
return $form;
}
/**
* Strip the high security flag from a session.
*
* Kicks a session out of high security and logs the exit.
*
* @param PhabricatorUser Acting user.
* @param PhabricatorAuthSession Session to return to normal security.
* @return void
* @task hisec
*/
public function exitHighSecurity(
PhabricatorUser $viewer,
PhabricatorAuthSession $session) {
if (!$session->getHighSecurityUntil()) {
return;
}
queryfx(
$session->establishConnection('w'),
'UPDATE %T SET highSecurityUntil = NULL WHERE id = %d',
$session->getTableName(),
$session->getID());
$log = PhabricatorUserLog::initializeNewLog(
$viewer,
$viewer->getPHID(),
PhabricatorUserLog::ACTION_EXIT_HISEC);
$log->save();
}
/* -( Partial Sessions )--------------------------------------------------- */
/**
* Upgrade a partial session to a full session.
*
* @param PhabricatorAuthSession Session to upgrade.
* @return void
* @task partial
*/
public function upgradePartialSession(PhabricatorUser $viewer) {
if (!$viewer->hasSession()) {
throw new Exception(
pht('Upgrading partial session of user with no session!'));
}
$session = $viewer->getSession();
if (!$session->getIsPartial()) {
throw new Exception(pht('Session is not partial!'));
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$session->setIsPartial(0);
queryfx(
$session->establishConnection('w'),
'UPDATE %T SET isPartial = %d WHERE id = %d',
$session->getTableName(),
0,
$session->getID());
$log = PhabricatorUserLog::initializeNewLog(
$viewer,
$viewer->getPHID(),
PhabricatorUserLog::ACTION_LOGIN_FULL);
$log->save();
unset($unguarded);
}
/* -( Legalpad Documents )-------------------------------------------------- */
/**
* Upgrade a session to have all legalpad documents signed.
*
* @param PhabricatorUser User whose session should upgrade.
* @param array LegalpadDocument objects
* @return void
* @task partial
*/
public function signLegalpadDocuments(PhabricatorUser $viewer, array $docs) {
if (!$viewer->hasSession()) {
throw new Exception(
pht('Signing session legalpad documents of user with no session!'));
}
$session = $viewer->getSession();
if ($session->getSignedLegalpadDocuments()) {
throw new Exception(pht(
'Session has already signed required legalpad documents!'));
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$session->setSignedLegalpadDocuments(1);
queryfx(
$session->establishConnection('w'),
'UPDATE %T SET signedLegalpadDocuments = %d WHERE id = %d',
$session->getTableName(),
1,
$session->getID());
if (!empty($docs)) {
$log = PhabricatorUserLog::initializeNewLog(
$viewer,
$viewer->getPHID(),
PhabricatorUserLog::ACTION_LOGIN_LEGALPAD);
$log->save();
}
unset($unguarded);
}
/* -( One Time Login URIs )------------------------------------------------ */
/**
* Retrieve a temporary, one-time URI which can log in to an account.
*
* These URIs are used for password recovery and to regain access to accounts
* which users have been locked out of.
*
* @param PhabricatorUser User to generate a URI for.
* @param PhabricatorUserEmail Optionally, email to verify when
* link is used.
* @param string Optional context string for the URI. This is purely cosmetic
* and used only to customize workflow and error messages.
+ * @param bool True to generate a URI which forces an immediate upgrade to
+ * a full session, bypassing MFA and other login checks.
* @return string Login URI.
* @task onetime
*/
public function getOneTimeLoginURI(
PhabricatorUser $user,
PhabricatorUserEmail $email = null,
- $type = self::ONETIME_RESET) {
+ $type = self::ONETIME_RESET,
+ $force_full_session = false) {
$key = Filesystem::readRandomCharacters(32);
$key_hash = $this->getOneTimeLoginKeyHash($user, $email, $key);
$onetime_type = PhabricatorAuthOneTimeLoginTemporaryTokenType::TOKENTYPE;
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
- id(new PhabricatorAuthTemporaryToken())
+ $token = id(new PhabricatorAuthTemporaryToken())
->setTokenResource($user->getPHID())
->setTokenType($onetime_type)
->setTokenExpires(time() + phutil_units('1 day in seconds'))
->setTokenCode($key_hash)
+ ->setShouldForceFullSession($force_full_session)
->save();
unset($unguarded);
$uri = '/login/once/'.$type.'/'.$user->getID().'/'.$key.'/';
if ($email) {
$uri = $uri.$email->getID().'/';
}
try {
$uri = PhabricatorEnv::getProductionURI($uri);
} catch (Exception $ex) {
// If a user runs `bin/auth recover` before configuring the base URI,
// just show the path. We don't have any way to figure out the domain.
// See T4132.
}
return $uri;
}
/**
* Load the temporary token associated with a given one-time login key.
*
* @param PhabricatorUser User to load the token for.
* @param PhabricatorUserEmail Optionally, email to verify when
* link is used.
* @param string Key user is presenting as a valid one-time login key.
* @return PhabricatorAuthTemporaryToken|null Token, if one exists.
* @task onetime
*/
public function loadOneTimeLoginKey(
PhabricatorUser $user,
PhabricatorUserEmail $email = null,
$key = null) {
$key_hash = $this->getOneTimeLoginKeyHash($user, $email, $key);
$onetime_type = PhabricatorAuthOneTimeLoginTemporaryTokenType::TOKENTYPE;
return id(new PhabricatorAuthTemporaryTokenQuery())
->setViewer($user)
->withTokenResources(array($user->getPHID()))
->withTokenTypes(array($onetime_type))
->withTokenCodes(array($key_hash))
->withExpired(false)
->executeOne();
}
/**
* Hash a one-time login key for storage as a temporary token.
*
* @param PhabricatorUser User this key is for.
* @param PhabricatorUserEmail Optionally, email to verify when
* link is used.
* @param string The one time login key.
* @return string Hash of the key.
* task onetime
*/
private function getOneTimeLoginKeyHash(
PhabricatorUser $user,
PhabricatorUserEmail $email = null,
$key = null) {
$parts = array(
$key,
$user->getAccountSecret(),
);
if ($email) {
$parts[] = $email->getVerificationCode();
}
return PhabricatorHash::weakDigest(implode(':', $parts));
}
/* -( User Cache )--------------------------------------------------------- */
/**
* @task cache
*/
private function getUserCacheQueryParts(AphrontDatabaseConnection $conn) {
$cache_selects = array();
$cache_joins = array();
$cache_map = array();
$keys = array();
$types_map = array();
$cache_types = PhabricatorUserCacheType::getAllCacheTypes();
foreach ($cache_types as $cache_type) {
foreach ($cache_type->getAutoloadKeys() as $autoload_key) {
$keys[] = $autoload_key;
$types_map[$autoload_key] = $cache_type;
}
}
$cache_table = id(new PhabricatorUserCache())->getTableName();
$cache_idx = 1;
foreach ($keys as $key) {
$join_as = 'ucache_'.$cache_idx;
$select_as = 'ucache_'.$cache_idx.'_v';
$cache_selects[] = qsprintf(
$conn,
'%T.cacheData %T',
$join_as,
$select_as);
$cache_joins[] = qsprintf(
$conn,
'LEFT JOIN %T AS %T ON u.phid = %T.userPHID
AND %T.cacheIndex = %s',
$cache_table,
$join_as,
$join_as,
$join_as,
PhabricatorHash::digestForIndex($key));
$cache_map[$select_as] = $key;
$cache_idx++;
}
if ($cache_selects) {
$cache_selects = qsprintf($conn, ', %LQ', $cache_selects);
} else {
$cache_selects = qsprintf($conn, '');
}
if ($cache_joins) {
$cache_joins = qsprintf($conn, '%LJ', $cache_joins);
} else {
$cache_joins = qsprintf($conn, '');
}
return array($cache_selects, $cache_joins, $cache_map, $types_map);
}
private function filterRawCacheData(
PhabricatorUser $user,
array $types_map,
array $cache_raw) {
foreach ($cache_raw as $cache_key => $cache_data) {
$type = $types_map[$cache_key];
if ($type->shouldValidateRawCacheData()) {
if (!$type->isRawCacheDataValid($user, $cache_key, $cache_data)) {
unset($cache_raw[$cache_key]);
}
}
}
return $cache_raw;
}
public function willServeRequestForUser(PhabricatorUser $user) {
// We allow the login user to generate any missing cache data inline.
$user->setAllowInlineCacheGeneration(true);
// Switch to the user's translation.
PhabricatorEnv::setLocaleCode($user->getTranslation());
$extensions = PhabricatorAuthSessionEngineExtension::getAllExtensions();
foreach ($extensions as $extension) {
$extension->willServeRequestForUser($user);
}
}
+ private function extendSession(PhabricatorAuthSession $session) {
+ $is_partial = $session->getIsPartial();
+
+ // Don't extend partial sessions. You have a relatively short window to
+ // upgrade into a full session, and your session expires otherwise.
+ if ($is_partial) {
+ return;
+ }
+
+ $session_type = $session->getType();
+
+ $ttl = PhabricatorAuthSession::getSessionTypeTTL(
+ $session_type,
+ $session->getIsPartial());
+
+ // If more than 20% of the time on this session has been used, refresh the
+ // TTL back up to the full duration. The idea here is that sessions are
+ // good forever if used regularly, but get GC'd when they fall out of use.
+
+ $now = PhabricatorTime::getNow();
+ if ($now + (0.80 * $ttl) <= $session->getSessionExpires()) {
+ return;
+ }
+
+ $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
+ queryfx(
+ $session->establishConnection('w'),
+ 'UPDATE %R SET sessionExpires = UNIX_TIMESTAMP() + %d
+ WHERE id = %d',
+ $session,
+ $ttl,
+ $session->getID());
+ unset($unguarded);
+ }
+
+
}
diff --git a/src/applications/auth/engineextension/PhabricatorAuthMFAEditEngineExtension.php b/src/applications/auth/engineextension/PhabricatorAuthMFAEditEngineExtension.php
new file mode 100644
index 000000000..adad1f927
--- /dev/null
+++ b/src/applications/auth/engineextension/PhabricatorAuthMFAEditEngineExtension.php
@@ -0,0 +1,52 @@
+<?php
+
+final class PhabricatorAuthMFAEditEngineExtension
+ extends PhabricatorEditEngineExtension {
+
+ const EXTENSIONKEY = 'auth.mfa';
+ const FIELDKEY = 'mfa';
+
+ public function getExtensionPriority() {
+ return 12000;
+ }
+
+ public function isExtensionEnabled() {
+ return true;
+ }
+
+ public function getExtensionName() {
+ return pht('MFA');
+ }
+
+ public function supportsObject(
+ PhabricatorEditEngine $engine,
+ PhabricatorApplicationTransactionInterface $object) {
+ return true;
+ }
+
+ public function buildCustomEditFields(
+ PhabricatorEditEngine $engine,
+ PhabricatorApplicationTransactionInterface $object) {
+
+ $mfa_type = PhabricatorTransactions::TYPE_MFA;
+
+ $viewer = $engine->getViewer();
+
+ $mfa_field = id(new PhabricatorApplyEditField())
+ ->setViewer($viewer)
+ ->setKey(self::FIELDKEY)
+ ->setLabel(pht('MFA'))
+ ->setIsFormField(false)
+ ->setCommentActionLabel(pht('Sign With MFA'))
+ ->setCommentActionOrder(12000)
+ ->setActionDescription(
+ pht('You will be prompted to provide MFA when you submit.'))
+ ->setDescription(pht('Sign this transaction group with MFA.'))
+ ->setTransactionType($mfa_type);
+
+ return array(
+ $mfa_field,
+ );
+ }
+
+}
diff --git a/src/applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php b/src/applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php
index 56a4f9fc8..dc197b3a4 100644
--- a/src/applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php
+++ b/src/applications/auth/exception/PhabricatorAuthHighSecurityRequiredException.php
@@ -1,37 +1,48 @@
<?php
final class PhabricatorAuthHighSecurityRequiredException extends Exception {
private $cancelURI;
private $factors;
private $factorValidationResults;
+ private $isSessionUpgrade;
public function setFactorValidationResults(array $results) {
+ assert_instances_of($results, 'PhabricatorAuthFactorResult');
$this->factorValidationResults = $results;
return $this;
}
public function getFactorValidationResults() {
return $this->factorValidationResults;
}
public function setFactors(array $factors) {
assert_instances_of($factors, 'PhabricatorAuthFactorConfig');
$this->factors = $factors;
return $this;
}
public function getFactors() {
return $this->factors;
}
public function setCancelURI($cancel_uri) {
$this->cancelURI = $cancel_uri;
return $this;
}
public function getCancelURI() {
return $this->cancelURI;
}
+ public function setIsSessionUpgrade($is_upgrade) {
+ $this->isSessionUpgrade = $is_upgrade;
+ return $this;
+ }
+
+ public function getIsSessionUpgrade() {
+ return $this->isSessionUpgrade;
+ }
+
}
diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php
index 7cddc2758..ec49f7f74 100644
--- a/src/applications/auth/factor/PhabricatorAuthFactor.php
+++ b/src/applications/auth/factor/PhabricatorAuthFactor.php
@@ -1,49 +1,566 @@
<?php
abstract class PhabricatorAuthFactor extends Phobject {
abstract public function getFactorName();
+ abstract public function getFactorShortName();
abstract public function getFactorKey();
+ abstract public function getFactorCreateHelp();
abstract public function getFactorDescription();
abstract public function processAddFactorForm(
+ PhabricatorAuthFactorProvider $provider,
AphrontFormView $form,
AphrontRequest $request,
PhabricatorUser $user);
abstract public function renderValidateFactorForm(
PhabricatorAuthFactorConfig $config,
AphrontFormView $form,
PhabricatorUser $viewer,
- $validation_result);
-
- abstract public function processValidateFactorForm(
- PhabricatorAuthFactorConfig $config,
- PhabricatorUser $viewer,
- AphrontRequest $request);
-
- public function isFactorValid(
- PhabricatorAuthFactorConfig $config,
- $validation_result) {
- return (idx($validation_result, 'valid') === true);
- }
+ PhabricatorAuthFactorResult $validation_result);
public function getParameterName(
PhabricatorAuthFactorConfig $config,
$name) {
return 'authfactor.'.$config->getID().'.'.$name;
}
public static function getAllFactors() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getFactorKey')
->execute();
}
protected function newConfigForUser(PhabricatorUser $user) {
return id(new PhabricatorAuthFactorConfig())
->setUserPHID($user->getPHID())
- ->setFactorKey($this->getFactorKey());
+ ->setFactorSecret('');
+ }
+
+ protected function newResult() {
+ return new PhabricatorAuthFactorResult();
+ }
+
+ public function newIconView() {
+ return id(new PHUIIconView())
+ ->setIcon('fa-mobile');
+ }
+
+ public function canCreateNewProvider() {
+ return true;
+ }
+
+ public function getProviderCreateDescription() {
+ return null;
+ }
+
+ public function canCreateNewConfiguration(
+ PhabricatorAuthFactorProvider $provider,
+ PhabricatorUser $user) {
+ return true;
+ }
+
+ public function getConfigurationCreateDescription(
+ PhabricatorAuthFactorProvider $provider,
+ PhabricatorUser $user) {
+ return null;
+ }
+
+ public function getConfigurationListDetails(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorAuthFactorProvider $provider,
+ PhabricatorUser $viewer) {
+ return null;
+ }
+
+ public function newEditEngineFields(
+ PhabricatorEditEngine $engine,
+ PhabricatorAuthFactorProvider $provider) {
+ return array();
+ }
+
+ /**
+ * Is this a factor which depends on the user's contact number?
+ *
+ * If a user has a "contact number" factor configured, they can not modify
+ * or switch their primary contact number.
+ *
+ * @return bool True if this factor should lock contact numbers.
+ */
+ public function isContactNumberFactor() {
+ return false;
+ }
+
+ abstract public function getEnrollDescription(
+ PhabricatorAuthFactorProvider $provider,
+ PhabricatorUser $user);
+
+ public function getEnrollButtonText(
+ PhabricatorAuthFactorProvider $provider,
+ PhabricatorUser $user) {
+ return pht('Continue');
+ }
+
+ public function getFactorOrder() {
+ return 1000;
+ }
+
+ final public function newSortVector() {
+ return id(new PhutilSortVector())
+ ->addInt($this->canCreateNewProvider() ? 0 : 1)
+ ->addInt($this->getFactorOrder())
+ ->addString($this->getFactorName());
+ }
+
+ protected function newChallenge(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorUser $viewer) {
+
+ $engine = $config->getSessionEngine();
+
+ return PhabricatorAuthChallenge::initializeNewChallenge()
+ ->setUserPHID($viewer->getPHID())
+ ->setSessionPHID($viewer->getSession()->getPHID())
+ ->setFactorPHID($config->getPHID())
+ ->setWorkflowKey($engine->getWorkflowKey());
+ }
+
+ abstract public function getRequestHasChallengeResponse(
+ PhabricatorAuthFactorConfig $config,
+ AphrontRequest $response);
+
+ final public function getNewIssuedChallenges(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorUser $viewer,
+ array $challenges) {
+ assert_instances_of($challenges, 'PhabricatorAuthChallenge');
+
+ $now = PhabricatorTime::getNow();
+
+ // Factor implementations may need to perform writes in order to issue
+ // challenges, particularly push factors like SMS.
+ $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
+
+ $new_challenges = $this->newIssuedChallenges(
+ $config,
+ $viewer,
+ $challenges);
+
+ if ($this->isAuthResult($new_challenges)) {
+ unset($unguarded);
+ return $new_challenges;
+ }
+
+ assert_instances_of($new_challenges, 'PhabricatorAuthChallenge');
+
+ foreach ($new_challenges as $new_challenge) {
+ $ttl = $new_challenge->getChallengeTTL();
+ if (!$ttl) {
+ throw new Exception(
+ pht('Newly issued MFA challenges must have a valid TTL!'));
+ }
+
+ if ($ttl < $now) {
+ throw new Exception(
+ pht(
+ 'Newly issued MFA challenges must have a future TTL. This '.
+ 'factor issued a bad TTL ("%s"). (Did you use a relative '.
+ 'time instead of an epoch?)',
+ $ttl));
+ }
+ }
+
+ foreach ($new_challenges as $challenge) {
+ $challenge->save();
+ }
+
+ unset($unguarded);
+
+ return $new_challenges;
+ }
+
+ abstract protected function newIssuedChallenges(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorUser $viewer,
+ array $challenges);
+
+ final public function getResultFromIssuedChallenges(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorUser $viewer,
+ array $challenges) {
+ assert_instances_of($challenges, 'PhabricatorAuthChallenge');
+
+ $result = $this->newResultFromIssuedChallenges(
+ $config,
+ $viewer,
+ $challenges);
+
+ if ($result === null) {
+ return $result;
+ }
+
+ if (!$this->isAuthResult($result)) {
+ throw new Exception(
+ pht(
+ 'Expected "newResultFromIssuedChallenges()" to return null or '.
+ 'an object of class "%s"; got something else (in "%s").',
+ 'PhabricatorAuthFactorResult',
+ get_class($this)));
+ }
+
+ $result->setIssuedChallenges($challenges);
+
+ return $result;
+ }
+
+ abstract protected function newResultFromIssuedChallenges(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorUser $viewer,
+ array $challenges);
+
+ final public function getResultFromChallengeResponse(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorUser $viewer,
+ AphrontRequest $request,
+ array $challenges) {
+ assert_instances_of($challenges, 'PhabricatorAuthChallenge');
+
+ $result = $this->newResultFromChallengeResponse(
+ $config,
+ $viewer,
+ $request,
+ $challenges);
+
+ if (!$this->isAuthResult($result)) {
+ throw new Exception(
+ pht(
+ 'Expected "newResultFromChallengeResponse()" to return an object '.
+ 'of class "%s"; got something else (in "%s").',
+ 'PhabricatorAuthFactorResult',
+ get_class($this)));
+ }
+
+ $result->setIssuedChallenges($challenges);
+
+ return $result;
+ }
+
+ abstract protected function newResultFromChallengeResponse(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorUser $viewer,
+ AphrontRequest $request,
+ array $challenges);
+
+ final protected function newAutomaticControl(
+ PhabricatorAuthFactorResult $result) {
+
+ $is_error = $result->getIsError();
+ if ($is_error) {
+ return $this->newErrorControl($result);
+ }
+
+ $is_continue = $result->getIsContinue();
+ if ($is_continue) {
+ return $this->newContinueControl($result);
+ }
+
+ $is_answered = (bool)$result->getAnsweredChallenge();
+ if ($is_answered) {
+ return $this->newAnsweredControl($result);
+ }
+
+ $is_wait = $result->getIsWait();
+ if ($is_wait) {
+ return $this->newWaitControl($result);
+ }
+
+ return null;
+ }
+
+ private function newWaitControl(
+ PhabricatorAuthFactorResult $result) {
+
+ $error = $result->getErrorMessage();
+
+ $icon = id(new PHUIIconView())
+ ->setIcon('fa-clock-o', 'red');
+
+ return id(new PHUIFormTimerControl())
+ ->setIcon($icon)
+ ->appendChild($error)
+ ->setError(pht('Wait'));
+ }
+
+ private function newAnsweredControl(
+ PhabricatorAuthFactorResult $result) {
+
+ $icon = id(new PHUIIconView())
+ ->setIcon('fa-check-circle-o', 'green');
+
+ return id(new PHUIFormTimerControl())
+ ->setIcon($icon)
+ ->appendChild(
+ pht('You responded to this challenge correctly.'));
+ }
+
+ private function newErrorControl(
+ PhabricatorAuthFactorResult $result) {
+
+ $error = $result->getErrorMessage();
+
+ $icon = id(new PHUIIconView())
+ ->setIcon('fa-times', 'red');
+
+ return id(new PHUIFormTimerControl())
+ ->setIcon($icon)
+ ->appendChild($error)
+ ->setError(pht('Error'));
+ }
+
+ private function newContinueControl(
+ PhabricatorAuthFactorResult $result) {
+
+ $error = $result->getErrorMessage();
+
+ $icon = id(new PHUIIconView())
+ ->setIcon('fa-commenting', 'green');
+
+ return id(new PHUIFormTimerControl())
+ ->setIcon($icon)
+ ->appendChild($error);
+ }
+
+
+
+/* -( Synchronizing New Factors )------------------------------------------ */
+
+
+ final protected function loadMFASyncToken(
+ PhabricatorAuthFactorProvider $provider,
+ AphrontRequest $request,
+ AphrontFormView $form,
+ PhabricatorUser $user) {
+
+ // If the form included a synchronization key, load the corresponding
+ // token. The user must synchronize to a key we generated because this
+ // raises the barrier to theoretical attacks where an attacker might
+ // provide a known key for factors like TOTP.
+
+ // (We store and verify the hash of the key, not the key itself, to limit
+ // how useful the data in the table is to an attacker.)
+
+ $sync_type = PhabricatorAuthMFASyncTemporaryTokenType::TOKENTYPE;
+ $sync_token = null;
+
+ $sync_key = $request->getStr($this->getMFASyncTokenFormKey());
+ if (strlen($sync_key)) {
+ $sync_key_digest = PhabricatorHash::digestWithNamedKey(
+ $sync_key,
+ PhabricatorAuthMFASyncTemporaryTokenType::DIGEST_KEY);
+
+ $sync_token = id(new PhabricatorAuthTemporaryTokenQuery())
+ ->setViewer($user)
+ ->withTokenResources(array($user->getPHID()))
+ ->withTokenTypes(array($sync_type))
+ ->withExpired(false)
+ ->withTokenCodes(array($sync_key_digest))
+ ->executeOne();
+ }
+
+ if (!$sync_token) {
+
+ // Don't generate a new sync token if there are too many outstanding
+ // tokens already. This is mostly relevant for push factors like SMS,
+ // where generating a token has the side effect of sending a user a
+ // message.
+
+ $outstanding_limit = 10;
+ $outstanding_tokens = id(new PhabricatorAuthTemporaryTokenQuery())
+ ->setViewer($user)
+ ->withTokenResources(array($user->getPHID()))
+ ->withTokenTypes(array($sync_type))
+ ->withExpired(false)
+ ->execute();
+ if (count($outstanding_tokens) > $outstanding_limit) {
+ throw new Exception(
+ pht(
+ 'Your account has too many outstanding, incomplete MFA '.
+ 'synchronization attempts. Wait an hour and try again.'));
+ }
+
+ $now = PhabricatorTime::getNow();
+
+ $sync_key = Filesystem::readRandomCharacters(32);
+ $sync_key_digest = PhabricatorHash::digestWithNamedKey(
+ $sync_key,
+ PhabricatorAuthMFASyncTemporaryTokenType::DIGEST_KEY);
+ $sync_ttl = $this->getMFASyncTokenTTL();
+
+ $sync_token = id(new PhabricatorAuthTemporaryToken())
+ ->setIsNewTemporaryToken(true)
+ ->setTokenResource($user->getPHID())
+ ->setTokenType($sync_type)
+ ->setTokenCode($sync_key_digest)
+ ->setTokenExpires($now + $sync_ttl);
+
+ $properties = $this->newMFASyncTokenProperties(
+ $provider,
+ $user);
+
+ if ($this->isAuthResult($properties)) {
+ return $properties;
+ }
+
+ foreach ($properties as $key => $value) {
+ $sync_token->setTemporaryTokenProperty($key, $value);
+ }
+
+ $sync_token->save();
+ }
+
+ $form->addHiddenInput($this->getMFASyncTokenFormKey(), $sync_key);
+
+ return $sync_token;
+ }
+
+ protected function newMFASyncTokenProperties(
+ PhabricatorAuthFactorProvider $provider,
+ PhabricatorUser $user) {
+ return array();
+ }
+
+ private function getMFASyncTokenFormKey() {
+ return 'sync.key';
+ }
+
+ private function getMFASyncTokenTTL() {
+ return phutil_units('1 hour in seconds');
+ }
+
+ final protected function getChallengeForCurrentContext(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorUser $viewer,
+ array $challenges) {
+
+ $session_phid = $viewer->getSession()->getPHID();
+ $engine = $config->getSessionEngine();
+ $workflow_key = $engine->getWorkflowKey();
+
+ foreach ($challenges as $challenge) {
+ if ($challenge->getSessionPHID() !== $session_phid) {
+ continue;
+ }
+
+ if ($challenge->getWorkflowKey() !== $workflow_key) {
+ continue;
+ }
+
+ if ($challenge->getIsCompleted()) {
+ continue;
+ }
+
+ if ($challenge->getIsReusedChallenge()) {
+ continue;
+ }
+
+ return $challenge;
+ }
+
+ return null;
+ }
+
+
+ /**
+ * @phutil-external-symbol class QRcode
+ */
+ final protected function newQRCode($uri) {
+ $root = dirname(phutil_get_library_root('phabricator'));
+ require_once $root.'/externals/phpqrcode/phpqrcode.php';
+
+ $lines = QRcode::text($uri);
+
+ $total_width = 240;
+ $cell_size = floor($total_width / count($lines));
+
+ $rows = array();
+ foreach ($lines as $line) {
+ $cells = array();
+ for ($ii = 0; $ii < strlen($line); $ii++) {
+ if ($line[$ii] == '1') {
+ $color = '#000';
+ } else {
+ $color = '#fff';
+ }
+
+ $cells[] = phutil_tag(
+ 'td',
+ array(
+ 'width' => $cell_size,
+ 'height' => $cell_size,
+ 'style' => 'background: '.$color,
+ ),
+ '');
+ }
+ $rows[] = phutil_tag('tr', array(), $cells);
+ }
+
+ return phutil_tag(
+ 'table',
+ array(
+ 'style' => 'margin: 24px auto;',
+ ),
+ $rows);
+ }
+
+ final protected function getInstallDisplayName() {
+ $uri = PhabricatorEnv::getURI('/');
+ $uri = new PhutilURI($uri);
+ return $uri->getDomain();
+ }
+
+ final protected function getChallengeResponseParameterName(
+ PhabricatorAuthFactorConfig $config) {
+ return $this->getParameterName($config, 'mfa.response');
+ }
+
+ final protected function getChallengeResponseFromRequest(
+ PhabricatorAuthFactorConfig $config,
+ AphrontRequest $request) {
+
+ $name = $this->getChallengeResponseParameterName($config);
+
+ $value = $request->getStr($name);
+ $value = (string)$value;
+ $value = trim($value);
+
+ return $value;
+ }
+
+ final protected function hasCSRF(PhabricatorAuthFactorConfig $config) {
+ $engine = $config->getSessionEngine();
+ $request = $engine->getRequest();
+
+ if (!$request->isHTTPPost()) {
+ return false;
+ }
+
+ return $request->validateCSRF();
+ }
+
+ final protected function loadConfigurationsForProvider(
+ PhabricatorAuthFactorProvider $provider,
+ PhabricatorUser $user) {
+
+ return id(new PhabricatorAuthFactorConfigQuery())
+ ->setViewer($user)
+ ->withUserPHIDs(array($user->getPHID()))
+ ->withFactorProviderPHIDs(array($provider->getPHID()))
+ ->execute();
+ }
+
+ final protected function isAuthResult($object) {
+ return ($object instanceof PhabricatorAuthFactorResult);
}
}
diff --git a/src/applications/auth/factor/PhabricatorAuthFactorResult.php b/src/applications/auth/factor/PhabricatorAuthFactorResult.php
new file mode 100644
index 000000000..2282f162a
--- /dev/null
+++ b/src/applications/auth/factor/PhabricatorAuthFactorResult.php
@@ -0,0 +1,95 @@
+<?php
+
+final class PhabricatorAuthFactorResult
+ extends Phobject {
+
+ private $answeredChallenge;
+ private $isWait = false;
+ private $isError = false;
+ private $isContinue = false;
+ private $errorMessage;
+ private $value;
+ private $issuedChallenges = array();
+
+ public function setAnsweredChallenge(PhabricatorAuthChallenge $challenge) {
+ if (!$challenge->getIsAnsweredChallenge()) {
+ throw new PhutilInvalidStateException('markChallengeAsAnswered');
+ }
+
+ if ($challenge->getIsCompleted()) {
+ throw new Exception(
+ pht(
+ 'A completed challenge was provided as an answered challenge. '.
+ 'The underlying factor is implemented improperly, challenges '.
+ 'may not be reused.'));
+ }
+
+ $this->answeredChallenge = $challenge;
+
+ return $this;
+ }
+
+ public function getAnsweredChallenge() {
+ return $this->answeredChallenge;
+ }
+
+ public function getIsValid() {
+ return (bool)$this->getAnsweredChallenge();
+ }
+
+ public function setIsWait($is_wait) {
+ $this->isWait = $is_wait;
+ return $this;
+ }
+
+ public function getIsWait() {
+ return $this->isWait;
+ }
+
+ public function setIsError($is_error) {
+ $this->isError = $is_error;
+ return $this;
+ }
+
+ public function getIsError() {
+ return $this->isError;
+ }
+
+ public function setIsContinue($is_continue) {
+ $this->isContinue = $is_continue;
+ return $this;
+ }
+
+ public function getIsContinue() {
+ return $this->isContinue;
+ }
+
+ public function setErrorMessage($error_message) {
+ $this->errorMessage = $error_message;
+ return $this;
+ }
+
+ public function getErrorMessage() {
+ return $this->errorMessage;
+ }
+
+ public function setValue($value) {
+ $this->value = $value;
+ return $this;
+ }
+
+ public function getValue() {
+ return $this->value;
+ }
+
+ public function setIssuedChallenges(array $issued_challenges) {
+ assert_instances_of($issued_challenges, 'PhabricatorAuthChallenge');
+ $this->issuedChallenges = $issued_challenges;
+ return $this;
+ }
+
+ public function getIssuedChallenges() {
+ return $this->issuedChallenges;
+ }
+
+}
diff --git a/src/applications/auth/factor/PhabricatorAuthTOTPKeyTemporaryTokenType.php b/src/applications/auth/factor/PhabricatorAuthMFASyncTemporaryTokenType.php
similarity index 52%
rename from src/applications/auth/factor/PhabricatorAuthTOTPKeyTemporaryTokenType.php
rename to src/applications/auth/factor/PhabricatorAuthMFASyncTemporaryTokenType.php
index 02f62e76b..e44da0b00 100644
--- a/src/applications/auth/factor/PhabricatorAuthTOTPKeyTemporaryTokenType.php
+++ b/src/applications/auth/factor/PhabricatorAuthMFASyncTemporaryTokenType.php
@@ -1,17 +1,18 @@
<?php
-final class PhabricatorAuthTOTPKeyTemporaryTokenType
+final class PhabricatorAuthMFASyncTemporaryTokenType
extends PhabricatorAuthTemporaryTokenType {
- const TOKENTYPE = 'mfa:totp:key';
+ const TOKENTYPE = 'mfa.sync';
+ const DIGEST_KEY = 'mfa.sync';
public function getTokenTypeDisplayName() {
- return pht('TOTP Synchronization');
+ return pht('MFA Sync');
}
public function getTokenReadableTypeName(
PhabricatorAuthTemporaryToken $token) {
- return pht('TOTP Sync Token');
+ return pht('MFA Sync Token');
}
}
diff --git a/src/applications/auth/factor/PhabricatorDuoAuthFactor.php b/src/applications/auth/factor/PhabricatorDuoAuthFactor.php
new file mode 100644
index 000000000..187e01195
--- /dev/null
+++ b/src/applications/auth/factor/PhabricatorDuoAuthFactor.php
@@ -0,0 +1,813 @@
+<?php
+
+final class PhabricatorDuoAuthFactor
+ extends PhabricatorAuthFactor {
+
+ const PROP_CREDENTIAL = 'duo.credentialPHID';
+ const PROP_ENROLL = 'duo.enroll';
+ const PROP_USERNAMES = 'duo.usernames';
+ const PROP_HOSTNAME = 'duo.hostname';
+
+ public function getFactorKey() {
+ return 'duo';
+ }
+
+ public function getFactorName() {
+ return pht('Duo Security');
+ }
+
+ public function getFactorShortName() {
+ return pht('Duo');
+ }
+
+ public function getFactorCreateHelp() {
+ return pht('Support for Duo push authentication.');
+ }
+
+ public function getFactorDescription() {
+ return pht(
+ 'When you need to authenticate, a request will be pushed to the '.
+ 'Duo application on your phone.');
+ }
+
+ public function getEnrollDescription(
+ PhabricatorAuthFactorProvider $provider,
+ PhabricatorUser $user) {
+ return pht(
+ 'To add a Duo factor, first download and install the Duo application '.
+ 'on your phone. Once you have launched the application and are ready '.
+ 'to perform setup, click continue.');
+ }
+
+ public function canCreateNewConfiguration(
+ PhabricatorAuthFactorProvider $provider,
+ PhabricatorUser $user) {
+
+ if ($this->loadConfigurationsForProvider($provider, $user)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function getConfigurationCreateDescription(
+ PhabricatorAuthFactorProvider $provider,
+ PhabricatorUser $user) {
+
+ $messages = array();
+
+ if ($this->loadConfigurationsForProvider($provider, $user)) {
+ $messages[] = id(new PHUIInfoView())
+ ->setSeverity(PHUIInfoView::SEVERITY_WARNING)
+ ->setErrors(
+ array(
+ pht(
+ 'You already have Duo authentication attached to your account '.
+ 'for this provider.'),
+ ));
+ }
+
+ return $messages;
+ }
+
+ public function getConfigurationListDetails(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorAuthFactorProvider $provider,
+ PhabricatorUser $viewer) {
+
+ $duo_user = $config->getAuthFactorConfigProperty('duo.username');
+
+ return pht('Duo Username: %s', $duo_user);
+ }
+
+
+ public function newEditEngineFields(
+ PhabricatorEditEngine $engine,
+ PhabricatorAuthFactorProvider $provider) {
+
+ $viewer = $engine->getViewer();
+
+ $credential_phid = $provider->getAuthFactorProviderProperty(
+ self::PROP_CREDENTIAL);
+
+ $hostname = $provider->getAuthFactorProviderProperty(self::PROP_HOSTNAME);
+ $usernames = $provider->getAuthFactorProviderProperty(self::PROP_USERNAMES);
+ $enroll = $provider->getAuthFactorProviderProperty(self::PROP_ENROLL);
+
+ $credential_type = PassphrasePasswordCredentialType::CREDENTIAL_TYPE;
+ $provides_type = PassphrasePasswordCredentialType::PROVIDES_TYPE;
+
+ $credentials = id(new PassphraseCredentialQuery())
+ ->setViewer($viewer)
+ ->withIsDestroyed(false)
+ ->withProvidesTypes(array($provides_type))
+ ->execute();
+
+ $xaction_hostname =
+ PhabricatorAuthFactorProviderDuoHostnameTransaction::TRANSACTIONTYPE;
+ $xaction_credential =
+ PhabricatorAuthFactorProviderDuoCredentialTransaction::TRANSACTIONTYPE;
+ $xaction_usernames =
+ PhabricatorAuthFactorProviderDuoUsernamesTransaction::TRANSACTIONTYPE;
+ $xaction_enroll =
+ PhabricatorAuthFactorProviderDuoEnrollTransaction::TRANSACTIONTYPE;
+
+ return array(
+ id(new PhabricatorTextEditField())
+ ->setLabel(pht('Duo API Hostname'))
+ ->setKey('duo.hostname')
+ ->setValue($hostname)
+ ->setTransactionType($xaction_hostname)
+ ->setIsRequired(true),
+ id(new PhabricatorCredentialEditField())
+ ->setLabel(pht('Duo API Credential'))
+ ->setKey('duo.credential')
+ ->setValue($credential_phid)
+ ->setTransactionType($xaction_credential)
+ ->setCredentialType($credential_type)
+ ->setCredentials($credentials),
+ id(new PhabricatorSelectEditField())
+ ->setLabel(pht('Duo Username'))
+ ->setKey('duo.usernames')
+ ->setValue($usernames)
+ ->setTransactionType($xaction_usernames)
+ ->setOptions(
+ array(
+ 'username' => pht('Use Phabricator Username'),
+ 'email' => pht('Use Primary Email Address'),
+ )),
+ id(new PhabricatorSelectEditField())
+ ->setLabel(pht('Create Accounts'))
+ ->setKey('duo.enroll')
+ ->setValue($enroll)
+ ->setTransactionType($xaction_enroll)
+ ->setOptions(
+ array(
+ 'deny' => pht('Require Existing Duo Account'),
+ 'allow' => pht('Create New Duo Account'),
+ )),
+ );
+ }
+
+
+ public function processAddFactorForm(
+ PhabricatorAuthFactorProvider $provider,
+ AphrontFormView $form,
+ AphrontRequest $request,
+ PhabricatorUser $user) {
+
+ $token = $this->loadMFASyncToken($provider, $request, $form, $user);
+ if ($this->isAuthResult($token)) {
+ $form->appendChild($this->newAutomaticControl($token));
+ return;
+ }
+
+ $enroll = $token->getTemporaryTokenProperty('duo.enroll');
+ $duo_id = $token->getTemporaryTokenProperty('duo.user-id');
+ $duo_uri = $token->getTemporaryTokenProperty('duo.uri');
+ $duo_user = $token->getTemporaryTokenProperty('duo.username');
+
+ $is_external = ($enroll === 'external');
+ $is_auto = ($enroll === 'auto');
+ $is_blocked = ($enroll === 'blocked');
+
+ if (!$token->getIsNewTemporaryToken()) {
+ if ($is_auto) {
+ return $this->newDuoConfig($user, $duo_user);
+ } else if ($is_external || $is_blocked) {
+ $parameters = array(
+ 'username' => $duo_user,
+ );
+
+ $result = $this->newDuoFuture($provider)
+ ->setMethod('preauth', $parameters)
+ ->resolve();
+
+ $result_code = $result['response']['result'];
+ switch ($result_code) {
+ case 'auth':
+ case 'allow':
+ return $this->newDuoConfig($user, $duo_user);
+ case 'enroll':
+ if ($is_blocked) {
+ // We'll render an equivalent static control below, so skip
+ // rendering here. We explicitly don't want to give the user
+ // an enroll workflow.
+ break;
+ }
+
+ $duo_uri = $result['response']['enroll_portal_url'];
+
+ $waiting_icon = id(new PHUIIconView())
+ ->setIcon('fa-mobile', 'red');
+
+ $waiting_control = id(new PHUIFormTimerControl())
+ ->setIcon($waiting_icon)
+ ->setError(pht('Not Complete'))
+ ->appendChild(
+ pht(
+ 'You have not completed Duo enrollment yet. '.
+ 'Complete enrollment, then click continue.'));
+
+ $form->appendControl($waiting_control);
+ break;
+ default:
+ case 'deny':
+ break;
+ }
+ } else {
+ $parameters = array(
+ 'user_id' => $duo_id,
+ 'activation_code' => $duo_uri,
+ );
+
+ $future = $this->newDuoFuture($provider)
+ ->setMethod('enroll_status', $parameters);
+
+ $result = $future->resolve();
+ $response = $result['response'];
+
+ switch ($response) {
+ case 'success':
+ return $this->newDuoConfig($user, $duo_user);
+ case 'waiting':
+ $waiting_icon = id(new PHUIIconView())
+ ->setIcon('fa-mobile', 'red');
+
+ $waiting_control = id(new PHUIFormTimerControl())
+ ->setIcon($waiting_icon)
+ ->setError(pht('Not Complete'))
+ ->appendChild(
+ pht(
+ 'You have not activated this enrollment in the Duo '.
+ 'application on your phone yet. Complete activation, then '.
+ 'click continue.'));
+
+ $form->appendControl($waiting_control);
+ break;
+ case 'invalid':
+ default:
+ throw new Exception(
+ pht(
+ 'This Duo enrollment attempt is invalid or has '.
+ 'expired ("%s"). Cancel the workflow and try again.',
+ $response));
+ }
+ }
+ }
+
+ if ($is_blocked) {
+ $blocked_icon = id(new PHUIIconView())
+ ->setIcon('fa-times', 'red');
+
+ $blocked_control = id(new PHUIFormTimerControl())
+ ->setIcon($blocked_icon)
+ ->appendChild(
+ pht(
+ 'Your Duo account ("%s") has not completed Duo enrollment. '.
+ 'Check your email and complete enrollment to continue.',
+ phutil_tag('strong', array(), $duo_user)));
+
+ $form->appendControl($blocked_control);
+ } else if ($is_auto) {
+ $auto_icon = id(new PHUIIconView())
+ ->setIcon('fa-check', 'green');
+
+ $auto_control = id(new PHUIFormTimerControl())
+ ->setIcon($auto_icon)
+ ->appendChild(
+ pht(
+ 'Duo account ("%s") is fully enrolled.',
+ phutil_tag('strong', array(), $duo_user)));
+
+ $form->appendControl($auto_control);
+ } else {
+ $duo_button = phutil_tag(
+ 'a',
+ array(
+ 'href' => $duo_uri,
+ 'class' => 'button button-grey',
+ 'target' => ($is_external ? '_blank' : null),
+ ),
+ pht('Enroll Duo Account: %s', $duo_user));
+
+ $duo_button = phutil_tag(
+ 'div',
+ array(
+ 'class' => 'mfa-form-enroll-button',
+ ),
+ $duo_button);
+
+ if ($is_external) {
+ $form->appendRemarkupInstructions(
+ pht(
+ 'Complete enrolling your phone with Duo:'));
+
+ $form->appendControl(
+ id(new AphrontFormMarkupControl())
+ ->setValue($duo_button));
+ } else {
+
+ $form->appendRemarkupInstructions(
+ pht(
+ 'Scan this QR code with the Duo application on your mobile '.
+ 'phone:'));
+
+
+ $qr_code = $this->newQRCode($duo_uri);
+ $form->appendChild($qr_code);
+
+ $form->appendRemarkupInstructions(
+ pht(
+ 'If you are currently using your phone to view this page, '.
+ 'click this button to open the Duo application:'));
+
+ $form->appendControl(
+ id(new AphrontFormMarkupControl())
+ ->setValue($duo_button));
+ }
+
+ $form->appendRemarkupInstructions(
+ pht(
+ 'Once you have completed setup on your phone, click continue.'));
+ }
+ }
+
+
+ protected function newMFASyncTokenProperties(
+ PhabricatorAuthFactorProvider $provider,
+ PhabricatorUser $user) {
+
+ $duo_user = $this->getDuoUsername($provider, $user);
+
+ // Duo automatically normalizes usernames to lowercase. Just do that here
+ // so that our value agrees more closely with Duo.
+ $duo_user = phutil_utf8_strtolower($duo_user);
+
+ $parameters = array(
+ 'username' => $duo_user,
+ );
+
+ $result = $this->newDuoFuture($provider)
+ ->setMethod('preauth', $parameters)
+ ->resolve();
+
+ $external_uri = null;
+ $result_code = $result['response']['result'];
+ $status_message = $result['response']['status_msg'];
+ switch ($result_code) {
+ case 'auth':
+ case 'allow':
+ // If the user already has a Duo account, they don't need to do
+ // anything.
+ return array(
+ 'duo.enroll' => 'auto',
+ 'duo.username' => $duo_user,
+ );
+ case 'enroll':
+ if (!$this->shouldAllowDuoEnrollment($provider)) {
+ return array(
+ 'duo.enroll' => 'blocked',
+ 'duo.username' => $duo_user,
+ );
+ }
+
+ $external_uri = $result['response']['enroll_portal_url'];
+
+ // Otherwise, enrollment is permitted so we're going to continue.
+ break;
+ default:
+ case 'deny':
+ return $this->newResult()
+ ->setIsError(true)
+ ->setErrorMessage(
+ pht(
+ 'Your Duo account ("%s") is not permitted to access this '.
+ 'system. Contact your Duo administrator for help. '.
+ 'The Duo preauth API responded with status message ("%s"): %s',
+ $duo_user,
+ $result_code,
+ $status_message));
+ }
+
+ // Duo's "/enroll" API isn't repeatable for the same username. If we're
+ // the first call, great: we can do inline enrollment, which is way more
+ // user friendly. Otherwise, we have to send the user on an adventure.
+
+ $parameters = array(
+ 'username' => $duo_user,
+ 'valid_secs' => phutil_units('1 hour in seconds'),
+ );
+
+ try {
+ $result = $this->newDuoFuture($provider)
+ ->setMethod('enroll', $parameters)
+ ->resolve();
+ } catch (HTTPFutureHTTPResponseStatus $ex) {
+ return array(
+ 'duo.enroll' => 'external',
+ 'duo.username' => $duo_user,
+ 'duo.uri' => $external_uri,
+ );
+ }
+
+ return array(
+ 'duo.enroll' => 'inline',
+ 'duo.uri' => $result['response']['activation_code'],
+ 'duo.username' => $duo_user,
+ 'duo.user-id' => $result['response']['user_id'],
+ );
+ }
+
+ protected function newIssuedChallenges(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorUser $viewer,
+ array $challenges) {
+
+ // If we already issued a valid challenge for this workflow and session,
+ // don't issue a new one.
+
+ $challenge = $this->getChallengeForCurrentContext(
+ $config,
+ $viewer,
+ $challenges);
+ if ($challenge) {
+ return array();
+ }
+
+ if (!$this->hasCSRF($config)) {
+ return $this->newResult()
+ ->setIsContinue(true)
+ ->setErrorMessage(
+ pht(
+ 'An authorization request will be pushed to the Duo '.
+ 'application on your phone.'));
+ }
+
+ $provider = $config->getFactorProvider();
+
+ // Otherwise, issue a new challenge.
+ $duo_user = (string)$config->getAuthFactorConfigProperty('duo.username');
+
+ $parameters = array(
+ 'username' => $duo_user,
+ );
+
+ $response = $this->newDuoFuture($provider)
+ ->setMethod('preauth', $parameters)
+ ->resolve();
+ $response = $response['response'];
+
+ $next_step = $response['result'];
+ $status_message = $response['status_msg'];
+ switch ($next_step) {
+ case 'auth':
+ // We're good to go.
+ break;
+ case 'allow':
+ // Duo is telling us to bypass MFA. For now, refuse.
+ return $this->newResult()
+ ->setIsError(true)
+ ->setErrorMessage(
+ pht(
+ 'Duo is not requiring a challenge, which defeats the '.
+ 'purpose of MFA. Duo must be configured to challenge you.'));
+ case 'enroll':
+ return $this->newResult()
+ ->setIsError(true)
+ ->setErrorMessage(
+ pht(
+ 'Your Duo account ("%s") requires enrollment. Contact your '.
+ 'Duo administrator for help. Duo status message: %s',
+ $duo_user,
+ $status_message));
+ case 'deny':
+ default:
+ return $this->newResult()
+ ->setIsError(true)
+ ->setErrorMessage(
+ pht(
+ 'Your Duo account ("%s") is not permitted to access this '.
+ 'system. Contact your Duo administrator for help. The Duo '.
+ 'preauth API responded with status message ("%s"): %s',
+ $duo_user,
+ $next_step,
+ $status_message));
+ }
+
+ $has_push = false;
+ $devices = $response['devices'];
+ foreach ($devices as $device) {
+ $capabilities = array_fuse($device['capabilities']);
+ if (isset($capabilities['push'])) {
+ $has_push = true;
+ break;
+ }
+ }
+
+ if (!$has_push) {
+ return $this->newResult()
+ ->setIsError(true)
+ ->setErrorMessage(
+ pht(
+ 'This factor has been removed from your device, so Phabricator '.
+ 'can not send you a challenge. To continue, an administrator '.
+ 'must strip this factor from your account.'));
+ }
+
+ $push_info = array(
+ pht('Domain') => $this->getInstallDisplayName(),
+ );
+ $push_info = phutil_build_http_querystring($push_info);
+
+ $parameters = array(
+ 'username' => $duo_user,
+ 'factor' => 'push',
+ 'async' => '1',
+
+ // Duo allows us to specify a device, or to pass "auto" to have it pick
+ // the first one. For now, just let it pick.
+ 'device' => 'auto',
+
+ // This is a hard-coded prefix for the word "... request" in the Duo UI,
+ // which defaults to "Login". We could pass richer information from
+ // workflows here, but it's not very flexible anyway.
+ 'type' => 'Authentication',
+
+ 'display_username' => $viewer->getUsername(),
+ 'pushinfo' => $push_info,
+ );
+
+ $result = $this->newDuoFuture($provider)
+ ->setMethod('auth', $parameters)
+ ->resolve();
+
+ $duo_xaction = $result['response']['txid'];
+
+ // The Duo push timeout is 60 seconds. Set our challenge to expire slightly
+ // more quickly so that we'll re-issue a new challenge before Duo times out.
+ // This should keep users away from a dead-end where they can't respond to
+ // Duo but Phabricator won't issue a new challenge yet.
+ $ttl_seconds = 55;
+
+ return array(
+ $this->newChallenge($config, $viewer)
+ ->setChallengeKey($duo_xaction)
+ ->setChallengeTTL(PhabricatorTime::getNow() + $ttl_seconds),
+ );
+ }
+
+ protected function newResultFromIssuedChallenges(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorUser $viewer,
+ array $challenges) {
+
+ $challenge = $this->getChallengeForCurrentContext(
+ $config,
+ $viewer,
+ $challenges);
+
+ if ($challenge->getIsAnsweredChallenge()) {
+ return $this->newResult()
+ ->setAnsweredChallenge($challenge);
+ }
+
+ $provider = $config->getFactorProvider();
+ $duo_xaction = $challenge->getChallengeKey();
+
+ $parameters = array(
+ 'txid' => $duo_xaction,
+ );
+
+ // This endpoint always long-polls, so use a timeout to force it to act
+ // more asynchronously.
+ try {
+ $result = $this->newDuoFuture($provider)
+ ->setHTTPMethod('GET')
+ ->setMethod('auth_status', $parameters)
+ ->setTimeout(5)
+ ->resolve();
+
+ $state = $result['response']['result'];
+ $status = $result['response']['status'];
+ } catch (HTTPFutureCURLResponseStatus $exception) {
+ if ($exception->isTimeout()) {
+ $state = 'waiting';
+ $status = 'poll';
+ } else {
+ throw $exception;
+ }
+ }
+
+ $now = PhabricatorTime::getNow();
+
+ switch ($state) {
+ case 'allow':
+ $ttl = PhabricatorTime::getNow()
+ + phutil_units('15 minutes in seconds');
+
+ $challenge
+ ->markChallengeAsAnswered($ttl);
+
+ return $this->newResult()
+ ->setAnsweredChallenge($challenge);
+ case 'waiting':
+ // No result yet, we'll render a default state later on.
+ break;
+ default:
+ case 'deny':
+ if ($status === 'timeout') {
+ return $this->newResult()
+ ->setIsError(true)
+ ->setErrorMessage(
+ pht(
+ 'This request has timed out because you took too long to '.
+ 'respond.'));
+ } else {
+ $wait_duration = ($challenge->getChallengeTTL() - $now) + 1;
+
+ return $this->newResult()
+ ->setIsWait(true)
+ ->setErrorMessage(
+ pht(
+ 'You denied this request. Wait %s second(s) to try again.',
+ new PhutilNumber($wait_duration)));
+ }
+ break;
+ }
+
+ return null;
+ }
+
+ public function renderValidateFactorForm(
+ PhabricatorAuthFactorConfig $config,
+ AphrontFormView $form,
+ PhabricatorUser $viewer,
+ PhabricatorAuthFactorResult $result) {
+
+ $control = $this->newAutomaticControl($result);
+ if (!$control) {
+ $result = $this->newResult()
+ ->setIsContinue(true)
+ ->setErrorMessage(
+ pht(
+ 'A challenge has been sent to your phone. Open the Duo '.
+ 'application and confirm the challenge, then continue.'));
+ $control = $this->newAutomaticControl($result);
+ }
+
+ $control
+ ->setLabel(pht('Duo'))
+ ->setCaption(pht('Factor Name: %s', $config->getFactorName()));
+
+ $form->appendChild($control);
+ }
+
+ public function getRequestHasChallengeResponse(
+ PhabricatorAuthFactorConfig $config,
+ AphrontRequest $request) {
+ $value = $this->getChallengeResponseFromRequest($config, $request);
+ return (bool)strlen($value);
+ }
+
+ protected function newResultFromChallengeResponse(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorUser $viewer,
+ AphrontRequest $request,
+ array $challenges) {
+
+ $challenge = $this->getChallengeForCurrentContext(
+ $config,
+ $viewer,
+ $challenges);
+
+ $code = $this->getChallengeResponseFromRequest(
+ $config,
+ $request);
+
+ $result = $this->newResult()
+ ->setValue($code);
+
+ if ($challenge->getIsAnsweredChallenge()) {
+ return $result->setAnsweredChallenge($challenge);
+ }
+
+ if (phutil_hashes_are_identical($code, $challenge->getChallengeKey())) {
+ $ttl = PhabricatorTime::getNow() + phutil_units('15 minutes in seconds');
+
+ $challenge
+ ->markChallengeAsAnswered($ttl);
+
+ return $result->setAnsweredChallenge($challenge);
+ }
+
+ if (strlen($code)) {
+ $error_message = pht('Invalid');
+ } else {
+ $error_message = pht('Required');
+ }
+
+ $result->setErrorMessage($error_message);
+
+ return $result;
+ }
+
+ private function newDuoFuture(PhabricatorAuthFactorProvider $provider) {
+ $credential_phid = $provider->getAuthFactorProviderProperty(
+ self::PROP_CREDENTIAL);
+
+ $omnipotent = PhabricatorUser::getOmnipotentUser();
+
+ $credential = id(new PassphraseCredentialQuery())
+ ->setViewer($omnipotent)
+ ->withPHIDs(array($credential_phid))
+ ->needSecrets(true)
+ ->executeOne();
+ if (!$credential) {
+ throw new Exception(
+ pht(
+ 'Unable to load Duo API credential ("%s").',
+ $credential_phid));
+ }
+
+ $duo_key = $credential->getUsername();
+ $duo_secret = $credential->getSecret();
+ if (!$duo_secret) {
+ throw new Exception(
+ pht(
+ 'Duo API credential ("%s") has no secret key.',
+ $credential_phid));
+ }
+
+ $duo_host = $provider->getAuthFactorProviderProperty(
+ self::PROP_HOSTNAME);
+ self::requireDuoAPIHostname($duo_host);
+
+ return id(new PhabricatorDuoFuture())
+ ->setIntegrationKey($duo_key)
+ ->setSecretKey($duo_secret)
+ ->setAPIHostname($duo_host)
+ ->setTimeout(10)
+ ->setHTTPMethod('POST');
+ }
+
+ private function getDuoUsername(
+ PhabricatorAuthFactorProvider $provider,
+ PhabricatorUser $user) {
+
+ $mode = $provider->getAuthFactorProviderProperty(self::PROP_USERNAMES);
+ switch ($mode) {
+ case 'username':
+ return $user->getUsername();
+ case 'email':
+ return $user->loadPrimaryEmailAddress();
+ default:
+ throw new Exception(
+ pht(
+ 'Duo username pairing mode ("%s") is not supported.',
+ $mode));
+ }
+ }
+
+ private function shouldAllowDuoEnrollment(
+ PhabricatorAuthFactorProvider $provider) {
+
+ $mode = $provider->getAuthFactorProviderProperty(self::PROP_ENROLL);
+ switch ($mode) {
+ case 'deny':
+ return false;
+ case 'allow':
+ return true;
+ default:
+ throw new Exception(
+ pht(
+ 'Duo enrollment mode ("%s") is not supported.',
+ $mode));
+ }
+ }
+
+ private function newDuoConfig(PhabricatorUser $user, $duo_user) {
+ $config_properties = array(
+ 'duo.username' => $duo_user,
+ );
+
+ $config = $this->newConfigForUser($user)
+ ->setFactorName(pht('Duo (%s)', $duo_user))
+ ->setProperties($config_properties);
+
+ return $config;
+ }
+
+ public static function requireDuoAPIHostname($hostname) {
+ if (preg_match('/\.duosecurity\.com\z/', $hostname)) {
+ return;
+ }
+
+ throw new Exception(
+ pht(
+ 'Duo API hostname ("%s") is invalid, hostname must be '.
+ '"*.duosecurity.com".',
+ $hostname));
+ }
+
+}
diff --git a/src/applications/auth/factor/PhabricatorSMSAuthFactor.php b/src/applications/auth/factor/PhabricatorSMSAuthFactor.php
new file mode 100644
index 000000000..ba46de980
--- /dev/null
+++ b/src/applications/auth/factor/PhabricatorSMSAuthFactor.php
@@ -0,0 +1,401 @@
+<?php
+
+final class PhabricatorSMSAuthFactor
+ extends PhabricatorAuthFactor {
+
+ public function getFactorKey() {
+ return 'sms';
+ }
+
+ public function getFactorName() {
+ return pht('Text Message (SMS)');
+ }
+
+ public function getFactorShortName() {
+ return pht('SMS');
+ }
+
+ public function getFactorCreateHelp() {
+ return pht(
+ 'Allow users to receive a code via SMS.');
+ }
+
+ public function getFactorDescription() {
+ return pht(
+ 'When you need to authenticate, a text message with a code will '.
+ 'be sent to your phone.');
+ }
+
+ public function getFactorOrder() {
+ // Sort this factor toward the end of the list because SMS is relatively
+ // weak.
+ return 2000;
+ }
+
+ public function isContactNumberFactor() {
+ return true;
+ }
+
+ public function canCreateNewProvider() {
+ return $this->isSMSMailerConfigured();
+ }
+
+ public function getProviderCreateDescription() {
+ $messages = array();
+
+ if (!$this->isSMSMailerConfigured()) {
+ $messages[] = id(new PHUIInfoView())
+ ->setErrors(
+ array(
+ pht(
+ 'You have not configured an outbound SMS mailer. You must '.
+ 'configure one before you can set up SMS. See: %s',
+ phutil_tag(
+ 'a',
+ array(
+ 'href' => '/config/edit/cluster.mailers/',
+ ),
+ 'cluster.mailers')),
+ ));
+ }
+
+ $messages[] = id(new PHUIInfoView())
+ ->setSeverity(PHUIInfoView::SEVERITY_WARNING)
+ ->setErrors(
+ array(
+ pht(
+ 'SMS is weak, and relatively easy for attackers to compromise. '.
+ 'Strongly consider using a different MFA provider.'),
+ ));
+
+ return $messages;
+ }
+
+ public function canCreateNewConfiguration(
+ PhabricatorAuthFactorProvider $provider,
+ PhabricatorUser $user) {
+
+ if (!$this->loadUserContactNumber($user)) {
+ return false;
+ }
+
+ if ($this->loadConfigurationsForProvider($provider, $user)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function getConfigurationCreateDescription(
+ PhabricatorAuthFactorProvider $provider,
+ PhabricatorUser $user) {
+
+ $messages = array();
+
+ if (!$this->loadUserContactNumber($user)) {
+ $messages[] = id(new PHUIInfoView())
+ ->setSeverity(PHUIInfoView::SEVERITY_WARNING)
+ ->setErrors(
+ array(
+ pht(
+ 'You have not configured a primary contact number. Configure '.
+ 'a contact number before adding SMS as an authentication '.
+ 'factor.'),
+ ));
+ }
+
+ if ($this->loadConfigurationsForProvider($provider, $user)) {
+ $messages[] = id(new PHUIInfoView())
+ ->setSeverity(PHUIInfoView::SEVERITY_WARNING)
+ ->setErrors(
+ array(
+ pht(
+ 'You already have SMS authentication attached to your account.'),
+ ));
+ }
+
+ return $messages;
+ }
+
+ public function getEnrollDescription(
+ PhabricatorAuthFactorProvider $provider,
+ PhabricatorUser $user) {
+ return pht(
+ 'To verify your phone as an authentication factor, a text message with '.
+ 'a secret code will be sent to the phone number you have listed as '.
+ 'your primary contact number.');
+ }
+
+ public function getEnrollButtonText(
+ PhabricatorAuthFactorProvider $provider,
+ PhabricatorUser $user) {
+ $contact_number = $this->loadUserContactNumber($user);
+
+ return pht('Send SMS: %s', $contact_number->getDisplayName());
+ }
+
+ public function processAddFactorForm(
+ PhabricatorAuthFactorProvider $provider,
+ AphrontFormView $form,
+ AphrontRequest $request,
+ PhabricatorUser $user) {
+
+ $token = $this->loadMFASyncToken($provider, $request, $form, $user);
+ $code = $request->getStr('sms.code');
+
+ $e_code = true;
+ if (!$token->getIsNewTemporaryToken()) {
+ $expect_code = $token->getTemporaryTokenProperty('code');
+
+ $okay = phutil_hashes_are_identical(
+ $this->normalizeSMSCode($code),
+ $this->normalizeSMSCode($expect_code));
+
+ if ($okay) {
+ $config = $this->newConfigForUser($user)
+ ->setFactorName(pht('SMS'));
+
+ return $config;
+ } else {
+ if (!strlen($code)) {
+ $e_code = pht('Required');
+ } else {
+ $e_code = pht('Invalid');
+ }
+ }
+ }
+
+ $form->appendRemarkupInstructions(
+ pht(
+ 'Enter the code from the text message which was sent to your '.
+ 'primary contact number.'));
+
+ $form->appendChild(
+ id(new PHUIFormNumberControl())
+ ->setLabel(pht('SMS Code'))
+ ->setName('sms.code')
+ ->setValue($code)
+ ->setError($e_code));
+ }
+
+ protected function newIssuedChallenges(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorUser $viewer,
+ array $challenges) {
+
+ // If we already issued a valid challenge for this workflow and session,
+ // don't issue a new one.
+
+ $challenge = $this->getChallengeForCurrentContext(
+ $config,
+ $viewer,
+ $challenges);
+ if ($challenge) {
+ return array();
+ }
+
+ if (!$this->loadUserContactNumber($viewer)) {
+ return $this->newResult()
+ ->setIsError(true)
+ ->setErrorMessage(
+ pht(
+ 'Your account has no primary contact number.'));
+ }
+
+ if (!$this->isSMSMailerConfigured()) {
+ return $this->newResult()
+ ->setIsError(true)
+ ->setErrorMessage(
+ pht(
+ 'No outbound mailer which can deliver SMS messages is '.
+ 'configured.'));
+ }
+
+ if (!$this->hasCSRF($config)) {
+ return $this->newResult()
+ ->setIsContinue(true)
+ ->setErrorMessage(
+ pht(
+ 'A text message with an authorization code will be sent to your '.
+ 'primary contact number.'));
+ }
+
+ // Otherwise, issue a new challenge.
+
+ $challenge_code = $this->newSMSChallengeCode();
+ $envelope = new PhutilOpaqueEnvelope($challenge_code);
+ $this->sendSMSCodeToUser($envelope, $viewer);
+
+ $ttl_seconds = phutil_units('15 minutes in seconds');
+
+ return array(
+ $this->newChallenge($config, $viewer)
+ ->setChallengeKey($challenge_code)
+ ->setChallengeTTL(PhabricatorTime::getNow() + $ttl_seconds),
+ );
+ }
+
+ protected function newResultFromIssuedChallenges(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorUser $viewer,
+ array $challenges) {
+
+ $challenge = $this->getChallengeForCurrentContext(
+ $config,
+ $viewer,
+ $challenges);
+
+ if ($challenge->getIsAnsweredChallenge()) {
+ return $this->newResult()
+ ->setAnsweredChallenge($challenge);
+ }
+
+ return null;
+ }
+
+ public function renderValidateFactorForm(
+ PhabricatorAuthFactorConfig $config,
+ AphrontFormView $form,
+ PhabricatorUser $viewer,
+ PhabricatorAuthFactorResult $result) {
+
+ $control = $this->newAutomaticControl($result);
+ if (!$control) {
+ $value = $result->getValue();
+ $error = $result->getErrorMessage();
+ $name = $this->getChallengeResponseParameterName($config);
+
+ $control = id(new PHUIFormNumberControl())
+ ->setName($name)
+ ->setDisableAutocomplete(true)
+ ->setValue($value)
+ ->setError($error);
+ }
+
+ $control
+ ->setLabel(pht('SMS Code'))
+ ->setCaption(pht('Factor Name: %s', $config->getFactorName()));
+
+ $form->appendChild($control);
+ }
+
+ public function getRequestHasChallengeResponse(
+ PhabricatorAuthFactorConfig $config,
+ AphrontRequest $request) {
+ $value = $this->getChallengeResponseFromRequest($config, $request);
+ return (bool)strlen($value);
+ }
+
+ protected function newResultFromChallengeResponse(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorUser $viewer,
+ AphrontRequest $request,
+ array $challenges) {
+
+ $challenge = $this->getChallengeForCurrentContext(
+ $config,
+ $viewer,
+ $challenges);
+
+ $code = $this->getChallengeResponseFromRequest(
+ $config,
+ $request);
+
+ $result = $this->newResult()
+ ->setValue($code);
+
+ if ($challenge->getIsAnsweredChallenge()) {
+ return $result->setAnsweredChallenge($challenge);
+ }
+
+ if (phutil_hashes_are_identical($code, $challenge->getChallengeKey())) {
+ $ttl = PhabricatorTime::getNow() + phutil_units('15 minutes in seconds');
+
+ $challenge
+ ->markChallengeAsAnswered($ttl);
+
+ return $result->setAnsweredChallenge($challenge);
+ }
+
+ if (strlen($code)) {
+ $error_message = pht('Invalid');
+ } else {
+ $error_message = pht('Required');
+ }
+
+ $result->setErrorMessage($error_message);
+
+ return $result;
+ }
+
+ private function newSMSChallengeCode() {
+ $value = Filesystem::readRandomInteger(0, 99999999);
+ $value = sprintf('%08d', $value);
+ return $value;
+ }
+
+ private function isSMSMailerConfigured() {
+ $mailers = PhabricatorMetaMTAMail::newMailers(
+ array(
+ 'outbound' => true,
+ 'media' => array(
+ PhabricatorMailSMSMessage::MESSAGETYPE,
+ ),
+ ));
+
+ return (bool)$mailers;
+ }
+
+ private function loadUserContactNumber(PhabricatorUser $user) {
+ $contact_numbers = id(new PhabricatorAuthContactNumberQuery())
+ ->setViewer($user)
+ ->withObjectPHIDs(array($user->getPHID()))
+ ->withStatuses(
+ array(
+ PhabricatorAuthContactNumber::STATUS_ACTIVE,
+ ))
+ ->withIsPrimary(true)
+ ->execute();
+
+ if (count($contact_numbers) !== 1) {
+ return null;
+ }
+
+ return head($contact_numbers);
+ }
+
+ protected function newMFASyncTokenProperties(
+ PhabricatorAuthFactorProvider $providerr,
+ PhabricatorUser $user) {
+
+ $sms_code = $this->newSMSChallengeCode();
+
+ $envelope = new PhutilOpaqueEnvelope($sms_code);
+ $this->sendSMSCodeToUser($envelope, $user);
+
+ return array(
+ 'code' => $sms_code,
+ );
+ }
+
+ private function sendSMSCodeToUser(
+ PhutilOpaqueEnvelope $envelope,
+ PhabricatorUser $user) {
+ return id(new PhabricatorMetaMTAMail())
+ ->setMessageType(PhabricatorMailSMSMessage::MESSAGETYPE)
+ ->addTos(array($user->getPHID()))
+ ->setForceDelivery(true)
+ ->setSensitiveContent(true)
+ ->setBody(
+ pht(
+ 'Phabricator (%s) MFA Code: %s',
+ $this->getInstallDisplayName(),
+ $envelope->openEnvelope()))
+ ->save();
+ }
+
+ private function normalizeSMSCode($code) {
+ return trim($code);
+ }
+
+}
diff --git a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php
index ae3608d52..ba6613c01 100644
--- a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php
+++ b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php
@@ -1,316 +1,452 @@
<?php
final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor {
- const DIGEST_TEMPORARY_KEY = 'mfa.totp.sync';
-
public function getFactorKey() {
return 'totp';
}
public function getFactorName() {
return pht('Mobile Phone App (TOTP)');
}
+ public function getFactorShortName() {
+ return pht('TOTP');
+ }
+
+ public function getFactorCreateHelp() {
+ return pht(
+ 'Allow users to attach a mobile authenticator application (like '.
+ 'Google Authenticator) to their account.');
+ }
+
public function getFactorDescription() {
return pht(
'Attach a mobile authenticator application (like Authy '.
'or Google Authenticator) to your account. When you need to '.
'authenticate, you will enter a code shown on your phone.');
}
- public function processAddFactorForm(
- AphrontFormView $form,
- AphrontRequest $request,
+ public function getEnrollDescription(
+ PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
- $totp_token_type = PhabricatorAuthTOTPKeyTemporaryTokenType::TOKENTYPE;
-
- $key = $request->getStr('totpkey');
- if (strlen($key)) {
- // If the user is providing a key, make sure it's a key we generated.
- // This raises the barrier to theoretical attacks where an attacker might
- // provide a known key (such attacks are already prevented by CSRF, but
- // this is a second barrier to overcome).
-
- // (We store and verify the hash of the key, not the key itself, to limit
- // how useful the data in the table is to an attacker.)
-
- $token_code = PhabricatorHash::digestWithNamedKey(
- $key,
- self::DIGEST_TEMPORARY_KEY);
-
- $temporary_token = id(new PhabricatorAuthTemporaryTokenQuery())
- ->setViewer($user)
- ->withTokenResources(array($user->getPHID()))
- ->withTokenTypes(array($totp_token_type))
- ->withExpired(false)
- ->withTokenCodes(array($token_code))
- ->executeOne();
- if (!$temporary_token) {
- // If we don't have a matching token, regenerate the key below.
- $key = null;
- }
- }
+ return pht(
+ 'To add a TOTP factor to your account, you will first need to install '.
+ 'a mobile authenticator application on your phone. Two applications '.
+ 'which work well are **Google Authenticator** and **Authy**, but any '.
+ 'other TOTP application should also work.'.
+ "\n\n".
+ 'If you haven\'t already, download and install a TOTP application on '.
+ 'your phone now. Once you\'ve launched the application and are ready '.
+ 'to add a new TOTP code, continue to the next step.');
+ }
- if (!strlen($key)) {
- $key = self::generateNewTOTPKey();
+ public function getConfigurationListDetails(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorAuthFactorProvider $provider,
+ PhabricatorUser $viewer) {
- // Mark this key as one we generated, so the user is allowed to submit
- // a response for it.
+ $bits = strlen($config->getFactorSecret()) * 8;
+ return pht('%d-Bit Secret', $bits);
+ }
- $token_code = PhabricatorHash::digestWithNamedKey(
- $key,
- self::DIGEST_TEMPORARY_KEY);
+ public function processAddFactorForm(
+ PhabricatorAuthFactorProvider $provider,
+ AphrontFormView $form,
+ AphrontRequest $request,
+ PhabricatorUser $user) {
- $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
- id(new PhabricatorAuthTemporaryToken())
- ->setTokenResource($user->getPHID())
- ->setTokenType($totp_token_type)
- ->setTokenExpires(time() + phutil_units('1 hour in seconds'))
- ->setTokenCode($token_code)
- ->save();
- unset($unguarded);
- }
+ $sync_token = $this->loadMFASyncToken(
+ $provider,
+ $request,
+ $form,
+ $user);
+ $secret = $sync_token->getTemporaryTokenProperty('secret');
$code = $request->getStr('totpcode');
$e_code = true;
- if ($request->getExists('totp')) {
- $okay = self::verifyTOTPCode(
- $user,
- new PhutilOpaqueEnvelope($key),
+ if (!$sync_token->getIsNewTemporaryToken()) {
+ $okay = (bool)$this->getTimestepAtWhichResponseIsValid(
+ $this->getAllowedTimesteps($this->getCurrentTimestep()),
+ new PhutilOpaqueEnvelope($secret),
$code);
if ($okay) {
$config = $this->newConfigForUser($user)
->setFactorName(pht('Mobile App (TOTP)'))
- ->setFactorSecret($key);
+ ->setFactorSecret($secret)
+ ->setMFASyncToken($sync_token);
return $config;
} else {
if (!strlen($code)) {
$e_code = pht('Required');
} else {
$e_code = pht('Invalid');
}
}
}
- $form->addHiddenInput('totp', true);
- $form->addHiddenInput('totpkey', $key);
-
- $form->appendRemarkupInstructions(
- pht(
- 'First, download an authenticator application on your phone. Two '.
- 'applications which work well are **Authy** and **Google '.
- 'Authenticator**, but any other TOTP application should also work.'));
-
$form->appendInstructions(
pht(
- 'Launch the application on your phone, and add a new entry for '.
- 'this Phabricator install. When prompted, scan the QR code or '.
- 'manually enter the key shown below into the application.'));
+ 'Scan the QR code or manually enter the key shown below into the '.
+ 'application.'));
$prod_uri = new PhutilURI(PhabricatorEnv::getProductionURI('/'));
$issuer = $prod_uri->getDomain();
$uri = urisprintf(
'otpauth://totp/%s:%s?secret=%s&issuer=%s',
$issuer,
$user->getUsername(),
- $key,
+ $secret,
$issuer);
- $qrcode = $this->renderQRCode($uri);
+ $qrcode = $this->newQRCode($uri);
$form->appendChild($qrcode);
$form->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('Key'))
- ->setValue(phutil_tag('strong', array(), $key)));
+ ->setValue(phutil_tag('strong', array(), $secret)));
$form->appendInstructions(
pht(
'(If given an option, select that this key is "Time Based", not '.
'"Counter Based".)'));
$form->appendInstructions(
pht(
'After entering the key, the application should display a numeric '.
'code. Enter that code below to confirm that you have configured '.
'the authenticator correctly:'));
$form->appendChild(
id(new PHUIFormNumberControl())
->setLabel(pht('TOTP Code'))
->setName('totpcode')
->setValue($code)
->setError($e_code));
}
- public function renderValidateFactorForm(
+ protected function newIssuedChallenges(
PhabricatorAuthFactorConfig $config,
- AphrontFormView $form,
PhabricatorUser $viewer,
- $validation_result) {
+ array $challenges) {
+
+ $current_step = $this->getCurrentTimestep();
- if (!$validation_result) {
- $validation_result = array();
+ // If we already issued a valid challenge, don't issue a new one.
+ if ($challenges) {
+ return array();
}
- $form->appendChild(
- id(new PHUIFormNumberControl())
- ->setName($this->getParameterName($config, 'totpcode'))
- ->setLabel(pht('App Code'))
- ->setDisableAutocomplete(true)
- ->setCaption(pht('Factor Name: %s', $config->getFactorName()))
- ->setValue(idx($validation_result, 'value'))
- ->setError(idx($validation_result, 'error', true)));
+ // Otherwise, generate a new challenge for the current timestep and compute
+ // the TTL.
+
+ // When computing the TTL, note that we accept codes within a certain
+ // window of the challenge timestep to account for clock skew and users
+ // needing time to enter codes.
+
+ // We don't want this challenge to expire until after all valid responses
+ // to it are no longer valid responses to any other challenge we might
+ // issue in the future. If the challenge expires too quickly, we may issue
+ // a new challenge which can accept the same TOTP code response.
+
+ // This means that we need to keep this challenge alive for double the
+ // window size: if we're currently at timestep 3, the user might respond
+ // with the code for timestep 5. This is valid, since timestep 5 is within
+ // the window for timestep 3.
+
+ // But the code for timestep 5 can be used to respond at timesteps 3, 4, 5,
+ // 6, and 7. To prevent any valid response to this challenge from being
+ // used again, we need to keep this challenge active until timestep 8.
+
+ $window_size = $this->getTimestepWindowSize();
+ $step_duration = $this->getTimestepDuration();
+
+ $ttl_steps = ($window_size * 2) + 1;
+ $ttl_seconds = ($ttl_steps * $step_duration);
+
+ return array(
+ $this->newChallenge($config, $viewer)
+ ->setChallengeKey($current_step)
+ ->setChallengeTTL(PhabricatorTime::getNow() + $ttl_seconds),
+ );
}
- public function processValidateFactorForm(
+ public function renderValidateFactorForm(
PhabricatorAuthFactorConfig $config,
+ AphrontFormView $form,
PhabricatorUser $viewer,
- AphrontRequest $request) {
+ PhabricatorAuthFactorResult $result) {
- $code = $request->getStr($this->getParameterName($config, 'totpcode'));
- $key = new PhutilOpaqueEnvelope($config->getFactorSecret());
+ $control = $this->newAutomaticControl($result);
+ if (!$control) {
+ $value = $result->getValue();
+ $error = $result->getErrorMessage();
+ $name = $this->getChallengeResponseParameterName($config);
- if (self::verifyTOTPCode($viewer, $key, $code)) {
- return array(
- 'error' => null,
- 'value' => $code,
- 'valid' => true,
- );
- } else {
- return array(
- 'error' => strlen($code) ? pht('Invalid') : pht('Required'),
- 'value' => $code,
- 'valid' => false,
- );
+ $control = id(new PHUIFormNumberControl())
+ ->setName($name)
+ ->setDisableAutocomplete(true)
+ ->setValue($value)
+ ->setError($error);
}
+
+ $control
+ ->setLabel(pht('App Code'))
+ ->setCaption(pht('Factor Name: %s', $config->getFactorName()));
+
+ $form->appendChild($control);
}
+ public function getRequestHasChallengeResponse(
+ PhabricatorAuthFactorConfig $config,
+ AphrontRequest $request) {
- public static function generateNewTOTPKey() {
- return strtoupper(Filesystem::readRandomCharacters(32));
+ $value = $this->getChallengeResponseFromRequest($config, $request);
+ return (bool)strlen($value);
}
- public static function verifyTOTPCode(
- PhabricatorUser $user,
- PhutilOpaqueEnvelope $key,
- $code) {
- $now = (int)(time() / 30);
+ protected function newResultFromIssuedChallenges(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorUser $viewer,
+ array $challenges) {
+
+ // If we've already issued a challenge at the current timestep or any
+ // nearby timestep, require that it was issued to the current session.
+ // This is defusing attacks where you (broadly) look at someone's phone
+ // and type the code in more quickly than they do.
+ $session_phid = $viewer->getSession()->getPHID();
+ $now = PhabricatorTime::getNow();
+
+ $engine = $config->getSessionEngine();
+ $workflow_key = $engine->getWorkflowKey();
+
+ $current_timestep = $this->getCurrentTimestep();
+
+ foreach ($challenges as $challenge) {
+ $challenge_timestep = (int)$challenge->getChallengeKey();
+ $wait_duration = ($challenge->getChallengeTTL() - $now) + 1;
+
+ if ($challenge->getSessionPHID() !== $session_phid) {
+ return $this->newResult()
+ ->setIsWait(true)
+ ->setErrorMessage(
+ pht(
+ 'This factor recently issued a challenge to a different login '.
+ 'session. Wait %s second(s) for the code to cycle, then try '.
+ 'again.',
+ new PhutilNumber($wait_duration)));
+ }
+
+ if ($challenge->getWorkflowKey() !== $workflow_key) {
+ return $this->newResult()
+ ->setIsWait(true)
+ ->setErrorMessage(
+ pht(
+ 'This factor recently issued a challenge for a different '.
+ 'workflow. Wait %s second(s) for the code to cycle, then try '.
+ 'again.',
+ new PhutilNumber($wait_duration)));
+ }
+
+ // If the current realtime timestep isn't a valid response to the current
+ // challenge but the challenge hasn't expired yet, we're locking out
+ // the factor to prevent challenge windows from overlapping. Let the user
+ // know that they should wait for a new challenge.
+ $challenge_timesteps = $this->getAllowedTimesteps($challenge_timestep);
+ if (!isset($challenge_timesteps[$current_timestep])) {
+ return $this->newResult()
+ ->setIsWait(true)
+ ->setErrorMessage(
+ pht(
+ 'This factor recently issued a challenge which has expired. '.
+ 'A new challenge can not be issued yet. Wait %s second(s) for '.
+ 'the code to cycle, then try again.',
+ new PhutilNumber($wait_duration)));
+ }
- // Allow the user to enter a code a few minutes away on either side, in
- // case the server or client has some clock skew.
- for ($offset = -2; $offset <= 2; $offset++) {
- $real = self::getTOTPCode($key, $now + $offset);
- if (phutil_hashes_are_identical($real, $code)) {
- return true;
+ if ($challenge->getIsReusedChallenge()) {
+ return $this->newResult()
+ ->setIsWait(true)
+ ->setErrorMessage(
+ pht(
+ 'You recently provided a response to this factor. Responses '.
+ 'may not be reused. Wait %s second(s) for the code to cycle, '.
+ 'then try again.',
+ new PhutilNumber($wait_duration)));
}
}
- // TODO: After validating a code, this should mark it as used and prevent
- // it from being reused.
+ return null;
+ }
+
+ protected function newResultFromChallengeResponse(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorUser $viewer,
+ AphrontRequest $request,
+ array $challenges) {
+
+ $code = $this->getChallengeResponseFromRequest(
+ $config,
+ $request);
+
+ $result = $this->newResult()
+ ->setValue($code);
+
+ // We expect to reach TOTP validation with exactly one valid challenge.
+ if (count($challenges) !== 1) {
+ throw new Exception(
+ pht(
+ 'Reached TOTP challenge validation with an unexpected number of '.
+ 'unexpired challenges (%d), expected exactly one.',
+ phutil_count($challenges)));
+ }
+
+ $challenge = head($challenges);
+
+ // If the client has already provided a valid answer to this challenge and
+ // submitted a token proving they answered it, we're all set.
+ if ($challenge->getIsAnsweredChallenge()) {
+ return $result->setAnsweredChallenge($challenge);
+ }
+
+ $challenge_timestep = (int)$challenge->getChallengeKey();
+ $current_timestep = $this->getCurrentTimestep();
+
+ $challenge_timesteps = $this->getAllowedTimesteps($challenge_timestep);
+ $current_timesteps = $this->getAllowedTimesteps($current_timestep);
- return false;
+ // We require responses be both valid for the challenge and valid for the
+ // current timestep. A longer challenge TTL doesn't let you use older
+ // codes for a longer period of time.
+ $valid_timestep = $this->getTimestepAtWhichResponseIsValid(
+ array_intersect_key($challenge_timesteps, $current_timesteps),
+ new PhutilOpaqueEnvelope($config->getFactorSecret()),
+ $code);
+
+ if ($valid_timestep) {
+ $ttl = PhabricatorTime::getNow() + 60;
+
+ $challenge
+ ->setProperty('totp.timestep', $valid_timestep)
+ ->markChallengeAsAnswered($ttl);
+
+ $result->setAnsweredChallenge($challenge);
+ } else {
+ if (strlen($code)) {
+ $error_message = pht('Invalid');
+ } else {
+ $error_message = pht('Required');
+ }
+ $result->setErrorMessage($error_message);
+ }
+
+ return $result;
}
+ public static function generateNewTOTPKey() {
+ return strtoupper(Filesystem::readRandomCharacters(32));
+ }
public static function base32Decode($buf) {
$buf = strtoupper($buf);
$map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
$map = str_split($map);
$map = array_flip($map);
$out = '';
$len = strlen($buf);
$acc = 0;
$bits = 0;
for ($ii = 0; $ii < $len; $ii++) {
$chr = $buf[$ii];
$val = $map[$chr];
$acc = $acc << 5;
$acc = $acc + $val;
$bits += 5;
if ($bits >= 8) {
$bits = $bits - 8;
$out .= chr(($acc & (0xFF << $bits)) >> $bits);
}
}
return $out;
}
public static function getTOTPCode(PhutilOpaqueEnvelope $key, $timestamp) {
$binary_timestamp = pack('N*', 0).pack('N*', $timestamp);
$binary_key = self::base32Decode($key->openEnvelope());
$hash = hash_hmac('sha1', $binary_timestamp, $binary_key, true);
// See RFC 4226.
$offset = ord($hash[19]) & 0x0F;
$code = ((ord($hash[$offset + 0]) & 0x7F) << 24) |
((ord($hash[$offset + 1]) & 0xFF) << 16) |
((ord($hash[$offset + 2]) & 0xFF) << 8) |
((ord($hash[$offset + 3]) ) );
$code = ($code % 1000000);
$code = str_pad($code, 6, '0', STR_PAD_LEFT);
return $code;
}
+ private function getTimestepDuration() {
+ return 30;
+ }
- /**
- * @phutil-external-symbol class QRcode
- */
- private function renderQRCode($uri) {
- $root = dirname(phutil_get_library_root('phabricator'));
- require_once $root.'/externals/phpqrcode/phpqrcode.php';
+ private function getCurrentTimestep() {
+ $duration = $this->getTimestepDuration();
+ return (int)(PhabricatorTime::getNow() / $duration);
+ }
- $lines = QRcode::text($uri);
+ private function getAllowedTimesteps($at_timestep) {
+ $window = $this->getTimestepWindowSize();
+ $range = range($at_timestep - $window, $at_timestep + $window);
+ return array_fuse($range);
+ }
- $total_width = 240;
- $cell_size = floor($total_width / count($lines));
+ private function getTimestepWindowSize() {
+ // The user is allowed to provide a code from the recent past or the
+ // near future to account for minor clock skew between the client
+ // and server, and the time it takes to actually enter a code.
+ return 1;
+ }
- $rows = array();
- foreach ($lines as $line) {
- $cells = array();
- for ($ii = 0; $ii < strlen($line); $ii++) {
- if ($line[$ii] == '1') {
- $color = '#000';
- } else {
- $color = '#fff';
- }
+ private function getTimestepAtWhichResponseIsValid(
+ array $timesteps,
+ PhutilOpaqueEnvelope $key,
+ $code) {
- $cells[] = phutil_tag(
- 'td',
- array(
- 'width' => $cell_size,
- 'height' => $cell_size,
- 'style' => 'background: '.$color,
- ),
- '');
+ foreach ($timesteps as $timestep) {
+ $expect_code = self::getTOTPCode($key, $timestep);
+ if (phutil_hashes_are_identical($code, $expect_code)) {
+ return $timestep;
}
- $rows[] = phutil_tag('tr', array(), $cells);
}
- return phutil_tag(
- 'table',
- array(
- 'style' => 'margin: 24px auto;',
- ),
- $rows);
+ return null;
+ }
+
+ protected function newMFASyncTokenProperties(
+ PhabricatorAuthFactorProvider $providerr,
+ PhabricatorUser $user) {
+ return array(
+ 'secret' => self::generateNewTOTPKey(),
+ );
}
}
diff --git a/src/applications/auth/future/PhabricatorDuoFuture.php b/src/applications/auth/future/PhabricatorDuoFuture.php
new file mode 100644
index 000000000..81a5a2a2b
--- /dev/null
+++ b/src/applications/auth/future/PhabricatorDuoFuture.php
@@ -0,0 +1,151 @@
+<?php
+
+final class PhabricatorDuoFuture
+ extends FutureProxy {
+
+ private $future;
+
+ private $integrationKey;
+ private $secretKey;
+ private $apiHostname;
+
+ private $httpMethod = 'POST';
+ private $method;
+ private $parameters;
+ private $timeout;
+
+ public function __construct() {
+ parent::__construct(null);
+ }
+
+ public function setIntegrationKey($integration_key) {
+ $this->integrationKey = $integration_key;
+ return $this;
+ }
+
+ public function setSecretKey(PhutilOpaqueEnvelope $key) {
+ $this->secretKey = $key;
+ return $this;
+ }
+
+ public function setAPIHostname($hostname) {
+ $this->apiHostname = $hostname;
+ return $this;
+ }
+
+ public function setMethod($method, array $parameters) {
+ $this->method = $method;
+ $this->parameters = $parameters;
+ return $this;
+ }
+
+ public function setTimeout($timeout) {
+ $this->timeout = $timeout;
+ return $this;
+ }
+
+ public function getTimeout() {
+ return $this->timeout;
+ }
+
+ public function setHTTPMethod($method) {
+ $this->httpMethod = $method;
+ return $this;
+ }
+
+ public function getHTTPMethod() {
+ return $this->httpMethod;
+ }
+
+ protected function getProxiedFuture() {
+ if (!$this->future) {
+ if ($this->integrationKey === null) {
+ throw new PhutilInvalidStateException('setIntegrationKey');
+ }
+
+ if ($this->secretKey === null) {
+ throw new PhutilInvalidStateException('setSecretKey');
+ }
+
+ if ($this->apiHostname === null) {
+ throw new PhutilInvalidStateException('setAPIHostname');
+ }
+
+ if ($this->method === null || $this->parameters === null) {
+ throw new PhutilInvalidStateException('setMethod');
+ }
+
+ $path = (string)urisprintf('/auth/v2/%s', $this->method);
+
+ $host = $this->apiHostname;
+ $host = phutil_utf8_strtolower($host);
+
+ $uri = id(new PhutilURI(''))
+ ->setProtocol('https')
+ ->setDomain($host)
+ ->setPath($path);
+
+ $data = $this->parameters;
+ $date = date('r');
+
+ $http_method = $this->getHTTPMethod();
+
+ ksort($data);
+ $data_parts = phutil_build_http_querystring($data);
+
+ $corpus = array(
+ $date,
+ $http_method,
+ $host,
+ $path,
+ $data_parts,
+ );
+ $corpus = implode("\n", $corpus);
+
+ $signature = hash_hmac(
+ 'sha1',
+ $corpus,
+ $this->secretKey->openEnvelope());
+ $signature = new PhutilOpaqueEnvelope($signature);
+
+ if ($http_method === 'GET') {
+ $uri->setQueryParams($data);
+ $data = array();
+ }
+
+ $future = id(new HTTPSFuture($uri, $data))
+ ->setHTTPBasicAuthCredentials($this->integrationKey, $signature)
+ ->setMethod($http_method)
+ ->addHeader('Accept', 'application/json')
+ ->addHeader('Date', $date);
+
+ $timeout = $this->getTimeout();
+ if ($timeout) {
+ $future->setTimeout($timeout);
+ }
+
+ $this->future = $future;
+ }
+
+ return $this->future;
+ }
+
+ protected function didReceiveResult($result) {
+ list($status, $body, $headers) = $result;
+
+ if ($status->isError()) {
+ throw $status;
+ }
+
+ try {
+ $data = phutil_json_decode($body);
+ } catch (PhutilJSONParserException $ex) {
+ throw new PhutilProxyException(
+ pht('Expected JSON response from Duo.'),
+ $ex);
+ }
+
+ return $data;
+ }
+
+}
diff --git a/src/applications/auth/garbagecollector/PhabricatorAuthChallengeGarbageCollector.php b/src/applications/auth/garbagecollector/PhabricatorAuthChallengeGarbageCollector.php
new file mode 100644
index 000000000..8a715cb17
--- /dev/null
+++ b/src/applications/auth/garbagecollector/PhabricatorAuthChallengeGarbageCollector.php
@@ -0,0 +1,28 @@
+<?php
+
+final class PhabricatorAuthChallengeGarbageCollector
+ extends PhabricatorGarbageCollector {
+
+ const COLLECTORCONST = 'auth.challenges';
+
+ public function getCollectorName() {
+ return pht('Authentication Challenges');
+ }
+
+ public function hasAutomaticPolicy() {
+ return true;
+ }
+
+ protected function collectGarbage() {
+ $challenge_table = new PhabricatorAuthChallenge();
+ $conn = $challenge_table->establishConnection('w');
+
+ queryfx(
+ $conn,
+ 'DELETE FROM %R WHERE challengeTTL < UNIX_TIMESTAMP() LIMIT 100',
+ $challenge_table);
+
+ return ($conn->getAffectedRows() == 100);
+ }
+
+}
diff --git a/src/applications/auth/management/PhabricatorAuthManagementListFactorsWorkflow.php b/src/applications/auth/management/PhabricatorAuthManagementListFactorsWorkflow.php
index 1367335cd..1e4ab7d8d 100644
--- a/src/applications/auth/management/PhabricatorAuthManagementListFactorsWorkflow.php
+++ b/src/applications/auth/management/PhabricatorAuthManagementListFactorsWorkflow.php
@@ -1,28 +1,27 @@
<?php
final class PhabricatorAuthManagementListFactorsWorkflow
extends PhabricatorAuthManagementWorkflow {
protected function didConstruct() {
$this
->setName('list-factors')
->setExamples('**list-factors**')
->setSynopsis(pht('List available multi-factor authentication factors.'))
->setArguments(array());
}
public function execute(PhutilArgumentParser $args) {
$factors = PhabricatorAuthFactor::getAllFactors();
- $console = PhutilConsole::getConsole();
foreach ($factors as $factor) {
- $console->writeOut(
+ echo tsprintf(
"%s\t%s\n",
$factor->getFactorKey(),
$factor->getFactorName());
}
return 0;
}
}
diff --git a/src/applications/auth/management/PhabricatorAuthManagementListMFAProvidersWorkflow.php b/src/applications/auth/management/PhabricatorAuthManagementListMFAProvidersWorkflow.php
new file mode 100644
index 000000000..8121bf955
--- /dev/null
+++ b/src/applications/auth/management/PhabricatorAuthManagementListMFAProvidersWorkflow.php
@@ -0,0 +1,33 @@
+<?php
+
+final class PhabricatorAuthManagementListMFAProvidersWorkflow
+ extends PhabricatorAuthManagementWorkflow {
+
+ protected function didConstruct() {
+ $this
+ ->setName('list-mfa-providers')
+ ->setExamples('**list-mfa-providerrs**')
+ ->setSynopsis(
+ pht(
+ 'List available multi-factor authentication providers.'))
+ ->setArguments(array());
+ }
+
+ public function execute(PhutilArgumentParser $args) {
+ $viewer = $this->getViewer();
+
+ $providers = id(new PhabricatorAuthFactorProviderQuery())
+ ->setViewer($viewer)
+ ->execute();
+
+ foreach ($providers as $provider) {
+ echo tsprintf(
+ "%s\t%s\n",
+ $provider->getPHID(),
+ $provider->getDisplayName());
+ }
+
+ return 0;
+ }
+
+}
diff --git a/src/applications/auth/management/PhabricatorAuthManagementRecoverWorkflow.php b/src/applications/auth/management/PhabricatorAuthManagementRecoverWorkflow.php
index 9efd07571..3190a842f 100644
--- a/src/applications/auth/management/PhabricatorAuthManagementRecoverWorkflow.php
+++ b/src/applications/auth/management/PhabricatorAuthManagementRecoverWorkflow.php
@@ -1,82 +1,91 @@
<?php
final class PhabricatorAuthManagementRecoverWorkflow
extends PhabricatorAuthManagementWorkflow {
protected function didConstruct() {
$this
->setName('recover')
->setExamples('**recover** __username__')
->setSynopsis(
pht(
'Recover access to an account if you have locked yourself out '.
'of Phabricator.'))
->setArguments(
array(
- 'username' => array(
+ array(
+ 'name' => 'force-full-session',
+ 'help' => pht(
+ 'Recover directly into a full session without requiring MFA '.
+ 'or other login checks.'),
+ ),
+ array(
'name' => 'username',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$usernames = $args->getArg('username');
if (!$usernames) {
throw new PhutilArgumentUsageException(
pht('You must specify the username of the account to recover.'));
} else if (count($usernames) > 1) {
throw new PhutilArgumentUsageException(
pht('You can only recover the username for one account.'));
}
$username = head($usernames);
$user = id(new PhabricatorPeopleQuery())
->setViewer($this->getViewer())
->withUsernames(array($username))
->executeOne();
if (!$user) {
throw new PhutilArgumentUsageException(
pht(
'No such user "%s" to recover.',
$username));
}
if (!$user->canEstablishWebSessions()) {
throw new PhutilArgumentUsageException(
pht(
'This account ("%s") can not establish web sessions, so it is '.
'not possible to generate a functional recovery link. Special '.
'accounts like daemons and mailing lists can not log in via the '.
'web UI.',
$username));
}
+ $force_full_session = $args->getArg('force-full-session');
+
$engine = new PhabricatorAuthSessionEngine();
$onetime_uri = $engine->getOneTimeLoginURI(
$user,
null,
- PhabricatorAuthSessionEngine::ONETIME_RECOVER);
+ PhabricatorAuthSessionEngine::ONETIME_RECOVER,
+ $force_full_session);
$console = PhutilConsole::getConsole();
$console->writeOut(
pht(
'Use this link to recover access to the "%s" account from the web '.
'interface:',
$username));
$console->writeOut("\n\n");
$console->writeOut(' %s', $onetime_uri);
$console->writeOut("\n\n");
$console->writeOut(
"%s\n",
pht(
'After logging in, you can use the "Auth" application to add or '.
'restore authentication providers and allow normal logins to '.
'succeed.'));
return 0;
}
}
diff --git a/src/applications/auth/management/PhabricatorAuthManagementStripWorkflow.php b/src/applications/auth/management/PhabricatorAuthManagementStripWorkflow.php
index f25d05301..22bfacb6f 100644
--- a/src/applications/auth/management/PhabricatorAuthManagementStripWorkflow.php
+++ b/src/applications/auth/management/PhabricatorAuthManagementStripWorkflow.php
@@ -1,175 +1,222 @@
<?php
final class PhabricatorAuthManagementStripWorkflow
extends PhabricatorAuthManagementWorkflow {
protected function didConstruct() {
$this
->setName('strip')
->setExamples('**strip** [--user username] [--type type]')
->setSynopsis(pht('Remove multi-factor authentication from an account.'))
->setArguments(
array(
array(
'name' => 'user',
'param' => 'username',
'repeat' => true,
'help' => pht('Strip factors from specified users.'),
),
array(
'name' => 'all-users',
'help' => pht('Strip factors from all users.'),
),
array(
'name' => 'type',
'param' => 'factortype',
'repeat' => true,
- 'help' => pht('Strip a specific factor type.'),
+ 'help' => pht(
+ 'Strip a specific factor type. Use `bin/auth list-factors` for '.
+ 'a list of factor types.'),
),
array(
'name' => 'all-types',
'help' => pht('Strip all factors, regardless of type.'),
),
+ array(
+ 'name' => 'provider',
+ 'param' => 'phid',
+ 'repeat' => true,
+ 'help' => pht(
+ 'Strip factors for a specific provider. Use '.
+ '`bin/auth list-mfa-providers` for a list of providers.'),
+ ),
array(
'name' => 'force',
'help' => pht('Strip factors without prompting.'),
),
array(
'name' => 'dry-run',
'help' => pht('Show factors, but do not strip them.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
+ $viewer = $this->getViewer();
+
$usernames = $args->getArg('user');
$all_users = $args->getArg('all-users');
if ($usernames && $all_users) {
throw new PhutilArgumentUsageException(
pht(
'Specify either specific users with %s, or all users with '.
'%s, but not both.',
'--user',
'--all-users'));
} else if (!$usernames && !$all_users) {
throw new PhutilArgumentUsageException(
pht(
- 'Use %s to specify which user to strip factors from, or '.
- '%s to strip factors from all users.',
- '--user',
- '--all-users'));
+ 'Use "--user <username>" to specify which user to strip factors '.
+ 'from, or "--all-users" to strip factors from all users.'));
} else if ($usernames) {
$users = id(new PhabricatorPeopleQuery())
->setViewer($this->getViewer())
->withUsernames($usernames)
->execute();
$users_by_username = mpull($users, null, 'getUsername');
foreach ($usernames as $username) {
if (empty($users_by_username[$username])) {
throw new PhutilArgumentUsageException(
pht(
'No user exists with username "%s".',
$username));
}
}
} else {
$users = null;
}
$types = $args->getArg('type');
+ $provider_phids = $args->getArg('provider');
$all_types = $args->getArg('all-types');
if ($types && $all_types) {
throw new PhutilArgumentUsageException(
pht(
- 'Specify either specific factors with --type, or all factors with '.
- '--all-types, but not both.'));
- } else if (!$types && !$all_types) {
+ 'Specify either specific factors with "--type", or all factors with '.
+ '"--all-types", but not both.'));
+ } else if ($provider_phids && $all_types) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Specify either specific factors with "--provider", or all factors '.
+ 'with "--all-types", but not both.'));
+ } else if (!$types && !$all_types && !$provider_phids) {
throw new PhutilArgumentUsageException(
pht(
- 'Use --type to specify which factor to strip, or --all-types to '.
- 'strip all factors. Use `auth list-factors` to show the available '.
- 'factor types.'));
+ 'Use "--type <type>" or "--provider <phid>" to specify which '.
+ 'factors to strip, or "--all-types" to strip all factors. '.
+ 'Use `bin/auth list-factors` to show the available factor types '.
+ 'or `bin/auth list-mfa-providers` to show available providers.'));
}
- if ($users && $types) {
- $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
- 'userPHID IN (%Ls) AND factorKey IN (%Ls)',
- mpull($users, 'getPHID'),
- $types);
- } else if ($users) {
- $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
- 'userPHID IN (%Ls)',
- mpull($users, 'getPHID'));
- } else if ($types) {
- $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
- 'factorKey IN (%Ls)',
- $types);
+ $type_map = PhabricatorAuthFactor::getAllFactors();
+
+ if ($types) {
+ foreach ($types as $type) {
+ if (!isset($type_map[$type])) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Factor type "%s" is unknown. Use `bin/auth list-factors` to '.
+ 'get a list of known factor types.',
+ $type));
+ }
+ }
+ }
+
+ $provider_query = id(new PhabricatorAuthFactorProviderQuery())
+ ->setViewer($viewer);
+
+ if ($provider_phids) {
+ $provider_query->withPHIDs($provider_phids);
+ }
+
+ if ($types) {
+ $provider_query->withProviderFactorKeys($types);
+ }
+
+ $providers = $provider_query->execute();
+ $providers = mpull($providers, null, 'getPHID');
+
+ if ($provider_phids) {
+ foreach ($provider_phids as $provider_phid) {
+ if (!isset($providers[$provider_phid])) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'No provider with PHID "%s" exists. '.
+ 'Use `bin/auth list-mfa-providers` to list providers.',
+ $provider_phid));
+ }
+ }
} else {
- $factors = id(new PhabricatorAuthFactorConfig())->loadAll();
+ if (!$providers) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'There are no configured multi-factor providers.'));
+ }
+ }
+
+ $factor_query = id(new PhabricatorAuthFactorConfigQuery())
+ ->setViewer($viewer)
+ ->withFactorProviderPHIDs(array_keys($providers));
+
+ if ($users) {
+ $factor_query->withUserPHIDs(mpull($users, 'getPHID'));
}
+ $factors = $factor_query->execute();
+
if (!$factors) {
throw new PhutilArgumentUsageException(
pht('There are no matching factors to strip.'));
}
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->getViewer())
->withPHIDs(mpull($factors, 'getUserPHID'))
->execute();
$console = PhutilConsole::getConsole();
$console->writeOut("%s\n\n", pht('These auth factors will be stripped:'));
foreach ($factors as $factor) {
- $impl = $factor->getImplementation();
- $console->writeOut(
+ $provider = $factor->getFactorProvider();
+
+ echo tsprintf(
" %s\t%s\t%s\n",
$handles[$factor->getUserPHID()]->getName(),
- $factor->getFactorKey(),
- ($impl
- ? $impl->getFactorName()
- : '?'));
+ $provider->getProviderFactorKey(),
+ $provider->getDisplayName());
}
$is_dry_run = $args->getArg('dry-run');
if ($is_dry_run) {
$console->writeOut(
"\n%s\n",
pht('End of dry run.'));
return 0;
}
$force = $args->getArg('force');
if (!$force) {
if (!$console->confirm(pht('Strip these authentication factors?'))) {
throw new PhutilArgumentUsageException(
pht('User aborted the workflow.'));
}
}
$console->writeOut("%s\n", pht('Stripping authentication factors...'));
+ $engine = new PhabricatorDestructionEngine();
foreach ($factors as $factor) {
- $user = id(new PhabricatorPeopleQuery())
- ->setViewer($this->getViewer())
- ->withPHIDs(array($factor->getUserPHID()))
- ->executeOne();
-
- $factor->delete();
-
- if ($user) {
- $user->updateMultiFactorEnrollment();
- }
+ $engine->destroyObject($factor);
}
$console->writeOut("%s\n", pht('Done.'));
return 0;
}
}
diff --git a/src/applications/auth/message/PhabricatorAuthLoginMessageType.php b/src/applications/auth/message/PhabricatorAuthLoginMessageType.php
new file mode 100644
index 000000000..666df7e98
--- /dev/null
+++ b/src/applications/auth/message/PhabricatorAuthLoginMessageType.php
@@ -0,0 +1,18 @@
+<?php
+
+final class PhabricatorAuthLoginMessageType
+ extends PhabricatorAuthMessageType {
+
+ const MESSAGEKEY = 'auth.login';
+
+ public function getDisplayName() {
+ return pht('Login Screen Instructions');
+ }
+
+ public function getShortDescription() {
+ return pht(
+ 'Guidance shown on the main login screen before users log in or '.
+ 'register.');
+ }
+
+}
diff --git a/src/applications/auth/message/PhabricatorAuthMessageType.php b/src/applications/auth/message/PhabricatorAuthMessageType.php
new file mode 100644
index 000000000..d5fd64c4a
--- /dev/null
+++ b/src/applications/auth/message/PhabricatorAuthMessageType.php
@@ -0,0 +1,32 @@
+<?php
+
+abstract class PhabricatorAuthMessageType
+ extends Phobject {
+
+ final public function getMessageTypeKey() {
+ return $this->getPhobjectClassConstant('MESSAGEKEY', 64);
+ }
+
+ final public static function getAllMessageTypes() {
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->setUniqueMethod('getMessageTypeKey')
+ ->execute();
+ }
+
+ final public static function newFromKey($key) {
+ $types = self::getAllMessageTypes();
+
+ if (empty($types[$key])) {
+ throw new Exception(
+ pht(
+ 'No message type exists with key "%s".',
+ $key));
+ }
+
+ return clone $types[$key];
+ }
+
+ abstract public function getDisplayName();
+
+}
diff --git a/src/applications/auth/message/PhabricatorAuthWelcomeMailMessageType.php b/src/applications/auth/message/PhabricatorAuthWelcomeMailMessageType.php
new file mode 100644
index 000000000..fe4b25cbb
--- /dev/null
+++ b/src/applications/auth/message/PhabricatorAuthWelcomeMailMessageType.php
@@ -0,0 +1,18 @@
+<?php
+
+final class PhabricatorAuthWelcomeMailMessageType
+ extends PhabricatorAuthMessageType {
+
+ const MESSAGEKEY = 'mail.welcome';
+
+ public function getDisplayName() {
+ return pht('Welcome Email Body');
+ }
+
+ public function getShortDescription() {
+ return pht(
+ 'Custom instructions included in "Welcome" mail when an '.
+ 'administrator creates a user account.');
+ }
+
+}
diff --git a/src/applications/auth/phid/PhabricatorAuthAuthFactorProviderPHIDType.php b/src/applications/auth/phid/PhabricatorAuthAuthFactorProviderPHIDType.php
new file mode 100644
index 000000000..f0f9f572e
--- /dev/null
+++ b/src/applications/auth/phid/PhabricatorAuthAuthFactorProviderPHIDType.php
@@ -0,0 +1,40 @@
+<?php
+
+final class PhabricatorAuthAuthFactorProviderPHIDType
+ extends PhabricatorPHIDType {
+
+ const TYPECONST = 'FPRV';
+
+ public function getTypeName() {
+ return pht('MFA Provider');
+ }
+
+ public function newObject() {
+ return new PhabricatorAuthFactorProvider();
+ }
+
+ public function getPHIDTypeApplicationClass() {
+ return 'PhabricatorAuthApplication';
+ }
+
+ protected function buildQueryForObjects(
+ PhabricatorObjectQuery $query,
+ array $phids) {
+
+ return id(new PhabricatorAuthFactorProviderQuery())
+ ->withPHIDs($phids);
+ }
+
+ public function loadHandles(
+ PhabricatorHandleQuery $query,
+ array $handles,
+ array $objects) {
+
+ foreach ($handles as $phid => $handle) {
+ $provider = $objects[$phid];
+
+ $handle->setURI($provider->getURI());
+ }
+ }
+
+}
diff --git a/src/applications/auth/phid/PhabricatorAuthChallengePHIDType.php b/src/applications/auth/phid/PhabricatorAuthChallengePHIDType.php
new file mode 100644
index 000000000..2d2fea26b
--- /dev/null
+++ b/src/applications/auth/phid/PhabricatorAuthChallengePHIDType.php
@@ -0,0 +1,32 @@
+<?php
+
+final class PhabricatorAuthChallengePHIDType extends PhabricatorPHIDType {
+
+ const TYPECONST = 'CHAL';
+
+ public function getTypeName() {
+ return pht('Auth Challenge');
+ }
+
+ public function newObject() {
+ return new PhabricatorAuthChallenge();
+ }
+
+ public function getPHIDTypeApplicationClass() {
+ return 'PhabricatorAuthApplication';
+ }
+
+ protected function buildQueryForObjects(
+ PhabricatorObjectQuery $query,
+ array $phids) {
+ return new PhabricatorAuthChallengeQuery();
+ }
+
+ public function loadHandles(
+ PhabricatorHandleQuery $query,
+ array $handles,
+ array $objects) {
+ return;
+ }
+
+}
diff --git a/src/applications/auth/phid/PhabricatorAuthContactNumberPHIDType.php b/src/applications/auth/phid/PhabricatorAuthContactNumberPHIDType.php
new file mode 100644
index 000000000..8a4953f4d
--- /dev/null
+++ b/src/applications/auth/phid/PhabricatorAuthContactNumberPHIDType.php
@@ -0,0 +1,38 @@
+<?php
+
+final class PhabricatorAuthContactNumberPHIDType
+ extends PhabricatorPHIDType {
+
+ const TYPECONST = 'CTNM';
+
+ public function getTypeName() {
+ return pht('Contact Number');
+ }
+
+ public function newObject() {
+ return new PhabricatorAuthContactNumber();
+ }
+
+ public function getPHIDTypeApplicationClass() {
+ return 'PhabricatorAuthApplication';
+ }
+
+ protected function buildQueryForObjects(
+ PhabricatorObjectQuery $query,
+ array $phids) {
+
+ return id(new PhabricatorAuthContactNumberQuery())
+ ->withPHIDs($phids);
+ }
+
+ public function loadHandles(
+ PhabricatorHandleQuery $query,
+ array $handles,
+ array $objects) {
+
+ foreach ($handles as $phid => $handle) {
+ $contact_number = $objects[$phid];
+ }
+ }
+
+}
diff --git a/src/applications/auth/phid/PhabricatorAuthMessagePHIDType.php b/src/applications/auth/phid/PhabricatorAuthMessagePHIDType.php
new file mode 100644
index 000000000..7a023c4a4
--- /dev/null
+++ b/src/applications/auth/phid/PhabricatorAuthMessagePHIDType.php
@@ -0,0 +1,34 @@
+<?php
+
+final class PhabricatorAuthMessagePHIDType extends PhabricatorPHIDType {
+
+ const TYPECONST = 'AMSG';
+
+ public function getTypeName() {
+ return pht('Auth Message');
+ }
+
+ public function newObject() {
+ return new PhabricatorAuthMessage();
+ }
+
+ public function getPHIDTypeApplicationClass() {
+ return 'PhabricatorAuthApplication';
+ }
+
+ protected function buildQueryForObjects(
+ PhabricatorObjectQuery $query,
+ array $phids) {
+
+ return id(new PhabricatorAuthMessageQuery())
+ ->withPHIDs($phids);
+ }
+
+ public function loadHandles(
+ PhabricatorHandleQuery $query,
+ array $handles,
+ array $objects) {
+ return;
+ }
+
+}
diff --git a/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php b/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php
index 9b72b9b62..febf16893 100644
--- a/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php
+++ b/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php
@@ -1,385 +1,415 @@
<?php
final class PhabricatorPasswordAuthProvider extends PhabricatorAuthProvider {
private $adapter;
public function getProviderName() {
return pht('Local Username/Password'); // c4science custo
}
public function getConfigurationHelp() {
return pht(
"(WARNING) Examine the table below for information on how password ".
"hashes will be stored in the database.\n\n".
"(NOTE) You can select a minimum password length by setting ".
"`%s` in configuration.",
'account.minimum-password-length');
}
public function renderConfigurationFooter() {
$hashers = PhabricatorPasswordHasher::getAllHashers();
$hashers = msort($hashers, 'getStrength');
$hashers = array_reverse($hashers);
$yes = phutil_tag(
'strong',
array(
'style' => 'color: #009900',
),
pht('Yes'));
$no = phutil_tag(
'strong',
array(
'style' => 'color: #990000',
),
pht('Not Installed'));
$best_hasher_name = null;
try {
$best_hasher = PhabricatorPasswordHasher::getBestHasher();
$best_hasher_name = $best_hasher->getHashName();
} catch (PhabricatorPasswordHasherUnavailableException $ex) {
// There are no suitable hashers. The user might be able to enable some,
// so we don't want to fatal here. We'll fatal when users try to actually
// use this stuff if it isn't fixed before then. Until then, we just
// don't highlight a row. In practice, at least one hasher should always
// be available.
}
$rows = array();
$rowc = array();
foreach ($hashers as $hasher) {
$is_installed = $hasher->canHashPasswords();
$rows[] = array(
$hasher->getHumanReadableName(),
$hasher->getHashName(),
$hasher->getHumanReadableStrength(),
($is_installed ? $yes : $no),
($is_installed ? null : $hasher->getInstallInstructions()),
);
$rowc[] = ($best_hasher_name == $hasher->getHashName())
? 'highlighted'
: null;
}
$table = new AphrontTableView($rows);
$table->setRowClasses($rowc);
$table->setHeaders(
array(
pht('Algorithm'),
pht('Name'),
pht('Strength'),
pht('Installed'),
pht('Install Instructions'),
));
$table->setColumnClasses(
array(
'',
'',
'',
'',
'wide',
));
$header = id(new PHUIHeaderView())
->setHeader(pht('Password Hash Algorithms'))
->setSubheader(
pht(
'Stronger algorithms are listed first. The highlighted algorithm '.
'will be used when storing new hashes. Older hashes will be '.
'upgraded to the best algorithm over time.'));
return id(new PHUIObjectBoxView())
->setHeader($header)
->setTable($table);
}
public function getDescriptionForCreate() {
return pht(
'Allow users to log in or register using a username and password.');
}
public function getAdapter() {
if (!$this->adapter) {
$adapter = new PhutilEmptyAuthAdapter();
$adapter->setAdapterType('password');
$adapter->setAdapterDomain('self');
$this->adapter = $adapter;
}
return $this->adapter;
}
public function getLoginOrder() {
// Make sure username/password appears first if it is enabled.
return '100-'.$this->getProviderName();
}
public function shouldAllowAccountLink() {
return false;
}
public function shouldAllowAccountUnlink() {
return false;
}
public function isDefaultRegistrationProvider() {
return true;
}
public function buildLoginForm(
PhabricatorAuthStartController $controller) {
$request = $controller->getRequest();
// c4science custo
$attributes = array(
'method' => 'GET',
'uri' => '/auth/login/password:self/',
);
return $this->renderStandardLoginButton($request, 'login', $attributes);
}
public function buildInviteForm(
PhabricatorAuthStartController $controller) {
$request = $controller->getRequest();
$viewer = $request->getViewer();
$form = id(new AphrontFormView())
->setUser($viewer)
->addHiddenInput('invite', true)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Username'))
->setName('username'));
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle(pht('Register an Account'))
->appendForm($form)
->setSubmitURI('/auth/register/')
->addSubmitButton(pht('Continue'));
return $dialog;
}
public function buildLinkForm(
PhabricatorAuthLinkController $controller) {
throw new Exception(pht("Password providers can't be linked."));
}
private function renderPasswordLoginForm(
AphrontRequest $request,
$require_captcha = false,
$captcha_valid = false) {
$viewer = $request->getUser();
$dialog = id(new AphrontDialogView())
->setSubmitURI($this->getLoginURI())
->setUser($viewer)
->setTitle(pht('Log In as external user')) // c4science custo
->addSubmitButton(pht('Log In'));
if ($this->shouldAllowRegistration()) {
$dialog->addCancelButton(
'/auth/register/',
pht('Register New Account'));
}
$dialog->addFooter(
phutil_tag(
'a',
array(
'href' => '/login/email/',
),
pht('Forgot your password?')));
$v_user = nonempty(
$request->getStr('username'),
$request->getCookie(PhabricatorCookies::COOKIE_USERNAME));
$e_user = null;
$e_pass = null;
$e_captcha = null;
$errors = array();
if ($require_captcha && !$captcha_valid) {
if (AphrontFormRecaptchaControl::hasCaptchaResponse($request)) {
$e_captcha = pht('Invalid');
$errors[] = pht('CAPTCHA was not entered correctly.');
} else {
$e_captcha = pht('Required');
$errors[] = pht(
'Too many login failures recently. You must '.
'submit a CAPTCHA with your login request.');
}
} else if ($request->isHTTPPost()) {
// NOTE: This is intentionally vague so as not to disclose whether a
// given username or email is registered.
$e_user = pht('Invalid');
$e_pass = pht('Invalid');
$errors[] = pht('Username or password are incorrect.');
}
if ($errors) {
$errors = id(new PHUIInfoView())->setErrors($errors);
}
$form = id(new PHUIFormLayoutView())
->setFullWidth(true)
->appendChild($errors)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Username or Email'))
->setName('username')
->setValue($v_user)
->setError($e_user))
->appendChild(
id(new AphrontFormPasswordControl())
->setLabel(pht('Password'))
->setName('password')
->setError($e_pass));
if ($require_captcha) {
$form->appendChild(
id(new AphrontFormRecaptchaControl())
->setError($e_captcha));
}
$dialog->appendChild($form);
return $dialog;
}
public function processLoginRequest(
PhabricatorAuthLoginController $controller) {
$request = $controller->getRequest();
$viewer = $request->getUser();
$content_source = PhabricatorContentSource::newFromRequest($request);
+ $captcha_limit = 5;
+ $hard_limit = 32;
+ $limit_window = phutil_units('15 minutes in seconds');
+
+ $failed_attempts = PhabricatorUserLog::loadRecentEventsFromThisIP(
+ PhabricatorUserLog::ACTION_LOGIN_FAILURE,
+ $limit_window);
+
+ // If the same remote address has submitted several failed login attempts
+ // recently, require they provide a CAPTCHA response for new attempts.
$require_captcha = false;
$captcha_valid = false;
if (AphrontFormRecaptchaControl::isRecaptchaEnabled()) {
- $failed_attempts = PhabricatorUserLog::loadRecentEventsFromThisIP(
- PhabricatorUserLog::ACTION_LOGIN_FAILURE,
- 60 * 15);
- if (count($failed_attempts) > 5) {
+ if (count($failed_attempts) > $captcha_limit) {
$require_captcha = true;
$captcha_valid = AphrontFormRecaptchaControl::processCaptcha($request);
}
}
+ // If the user has submitted quite a few failed login attempts recently,
+ // give them a hard limit.
+ if (count($failed_attempts) > $hard_limit) {
+ $guidance = array();
+
+ $guidance[] = pht(
+ 'Your remote address has failed too many login attempts recently. '.
+ 'Wait a few minutes before trying again.');
+
+ $guidance[] = pht(
+ 'If you are unable to log in to your account, you can '.
+ '[[ /login/email | send a reset link to your email address ]].');
+
+ $guidance = implode("\n\n", $guidance);
+
+ $dialog = $controller->newDialog()
+ ->setTitle(pht('Too Many Login Attempts'))
+ ->appendChild(new PHUIRemarkupView($viewer, $guidance))
+ ->addCancelButton('/auth/start/', pht('Wait Patiently'));
+
+ return array(null, $dialog);
+ }
+
$response = null;
$account = null;
$log_user = null;
if ($request->isFormPost()) {
if (!$require_captcha || $captcha_valid) {
$username_or_email = $request->getStr('username');
if (strlen($username_or_email)) {
$user = id(new PhabricatorUser())->loadOneWhere(
'username = %s',
$username_or_email);
if (!$user) {
$user = PhabricatorUser::loadOneWithEmailAddress(
$username_or_email);
}
if ($user) {
$envelope = new PhutilOpaqueEnvelope($request->getStr('password'));
$engine = id(new PhabricatorAuthPasswordEngine())
->setViewer($user)
->setContentSource($content_source)
->setPasswordType(PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT)
->setObject($user);
if ($engine->isValidPassword($envelope)) {
$account = $this->loadOrCreateAccount($user->getPHID());
$log_user = $user;
}
}
}
}
}
if (!$account) {
if ($request->isFormPost()) {
$log = PhabricatorUserLog::initializeNewLog(
null,
$log_user ? $log_user->getPHID() : null,
PhabricatorUserLog::ACTION_LOGIN_FAILURE);
$log->save();
}
$request->clearCookie(PhabricatorCookies::COOKIE_USERNAME);
$response = $controller->buildProviderPageResponse(
$this,
$this->renderPasswordLoginForm(
$request,
$require_captcha,
$captcha_valid));
}
return array($account, $response);
}
public function shouldRequireRegistrationPassword() {
return true;
}
public function getDefaultExternalAccount() {
$adapter = $this->getAdapter();
return id(new PhabricatorExternalAccount())
->setAccountType($adapter->getAdapterType())
->setAccountDomain($adapter->getAdapterDomain());
}
protected function willSaveAccount(PhabricatorExternalAccount $account) {
parent::willSaveAccount($account);
$account->setUserPHID($account->getAccountID());
}
public function willRegisterAccount(PhabricatorExternalAccount $account) {
parent::willRegisterAccount($account);
$account->setAccountID($account->getUserPHID());
}
public static function getPasswordProvider() {
$providers = self::getAllEnabledProviders();
foreach ($providers as $provider) {
if ($provider instanceof PhabricatorPasswordAuthProvider) {
return $provider;
}
}
return null;
}
public function willRenderLinkedAccount(
PhabricatorUser $viewer,
PHUIObjectItemView $item,
PhabricatorExternalAccount $account) {
return;
}
public function shouldAllowAccountRefresh() {
return false;
}
public function shouldAllowEmailTrustConfiguration() {
return false;
}
// c4science custo
public function isLoginFormAButton() {
return true;
}
}
diff --git a/src/applications/auth/query/PhabricatorAuthChallengeQuery.php b/src/applications/auth/query/PhabricatorAuthChallengeQuery.php
new file mode 100644
index 000000000..195abe088
--- /dev/null
+++ b/src/applications/auth/query/PhabricatorAuthChallengeQuery.php
@@ -0,0 +1,99 @@
+<?php
+
+final class PhabricatorAuthChallengeQuery
+ extends PhabricatorCursorPagedPolicyAwareQuery {
+
+ private $ids;
+ private $phids;
+ private $userPHIDs;
+ private $factorPHIDs;
+ private $challengeTTLMin;
+ private $challengeTTLMax;
+
+ public function withIDs(array $ids) {
+ $this->ids = $ids;
+ return $this;
+ }
+
+ public function withPHIDs(array $phids) {
+ $this->phids = $phids;
+ return $this;
+ }
+
+ public function withUserPHIDs(array $user_phids) {
+ $this->userPHIDs = $user_phids;
+ return $this;
+ }
+
+ public function withFactorPHIDs(array $factor_phids) {
+ $this->factorPHIDs = $factor_phids;
+ return $this;
+ }
+
+ public function withChallengeTTLBetween($challenge_min, $challenge_max) {
+ $this->challengeTTLMin = $challenge_min;
+ $this->challengeTTLMax = $challenge_max;
+ return $this;
+ }
+
+ public function newResultObject() {
+ return new PhabricatorAuthChallenge();
+ }
+
+ protected function loadPage() {
+ return $this->loadStandardPage($this->newResultObject());
+ }
+
+ protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
+ $where = parent::buildWhereClauseParts($conn);
+
+ if ($this->ids !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'id IN (%Ld)',
+ $this->ids);
+ }
+
+ if ($this->phids !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'phid IN (%Ls)',
+ $this->phids);
+ }
+
+ if ($this->userPHIDs !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'userPHID IN (%Ls)',
+ $this->userPHIDs);
+ }
+
+ if ($this->factorPHIDs !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'factorPHID IN (%Ls)',
+ $this->factorPHIDs);
+ }
+
+ if ($this->challengeTTLMin !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'challengeTTL >= %d',
+ $this->challengeTTLMin);
+ }
+
+ if ($this->challengeTTLMax !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'challengeTTL <= %d',
+ $this->challengeTTLMax);
+ }
+
+ return $where;
+ }
+
+ public function getQueryApplicationClass() {
+ return 'PhabricatorAuthApplication';
+ }
+
+}
diff --git a/src/applications/auth/query/PhabricatorAuthContactNumberQuery.php b/src/applications/auth/query/PhabricatorAuthContactNumberQuery.php
new file mode 100644
index 000000000..77b3b559d
--- /dev/null
+++ b/src/applications/auth/query/PhabricatorAuthContactNumberQuery.php
@@ -0,0 +1,103 @@
+<?php
+
+final class PhabricatorAuthContactNumberQuery
+ extends PhabricatorCursorPagedPolicyAwareQuery {
+
+ private $ids;
+ private $phids;
+ private $objectPHIDs;
+ private $statuses;
+ private $uniqueKeys;
+ private $isPrimary;
+
+ public function withIDs(array $ids) {
+ $this->ids = $ids;
+ return $this;
+ }
+
+ public function withPHIDs(array $phids) {
+ $this->phids = $phids;
+ return $this;
+ }
+
+ public function withObjectPHIDs(array $object_phids) {
+ $this->objectPHIDs = $object_phids;
+ return $this;
+ }
+
+ public function withStatuses(array $statuses) {
+ $this->statuses = $statuses;
+ return $this;
+ }
+
+ public function withUniqueKeys(array $unique_keys) {
+ $this->uniqueKeys = $unique_keys;
+ return $this;
+ }
+
+ public function withIsPrimary($is_primary) {
+ $this->isPrimary = $is_primary;
+ return $this;
+ }
+
+ public function newResultObject() {
+ return new PhabricatorAuthContactNumber();
+ }
+
+ protected function loadPage() {
+ return $this->loadStandardPage($this->newResultObject());
+ }
+
+ protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
+ $where = parent::buildWhereClauseParts($conn);
+
+ if ($this->ids !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'id IN (%Ld)',
+ $this->ids);
+ }
+
+ if ($this->phids !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'phid IN (%Ls)',
+ $this->phids);
+ }
+
+ if ($this->objectPHIDs !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'objectPHID IN (%Ls)',
+ $this->objectPHIDs);
+ }
+
+ if ($this->statuses !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'status IN (%Ls)',
+ $this->statuses);
+ }
+
+ if ($this->uniqueKeys !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'uniqueKey IN (%Ls)',
+ $this->uniqueKeys);
+ }
+
+ if ($this->isPrimary !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'isPrimary = %d',
+ (int)$this->isPrimary);
+ }
+
+ return $where;
+ }
+
+ public function getQueryApplicationClass() {
+ return 'PhabricatorAuthApplication';
+ }
+
+}
diff --git a/src/applications/auth/query/PhabricatorAuthContactNumberTransactionQuery.php b/src/applications/auth/query/PhabricatorAuthContactNumberTransactionQuery.php
new file mode 100644
index 000000000..a443cbab4
--- /dev/null
+++ b/src/applications/auth/query/PhabricatorAuthContactNumberTransactionQuery.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhabricatorAuthContactNumberTransactionQuery
+ extends PhabricatorApplicationTransactionQuery {
+
+ public function getTemplateApplicationTransaction() {
+ return new PhabricatorAuthContactNumberTransaction();
+ }
+
+}
diff --git a/src/applications/auth/query/PhabricatorAuthFactorConfigQuery.php b/src/applications/auth/query/PhabricatorAuthFactorConfigQuery.php
new file mode 100644
index 000000000..5f838f66b
--- /dev/null
+++ b/src/applications/auth/query/PhabricatorAuthFactorConfigQuery.php
@@ -0,0 +1,131 @@
+<?php
+
+final class PhabricatorAuthFactorConfigQuery
+ extends PhabricatorCursorPagedPolicyAwareQuery {
+
+ private $ids;
+ private $phids;
+ private $userPHIDs;
+ private $factorProviderPHIDs;
+ private $factorProviderStatuses;
+
+ public function withIDs(array $ids) {
+ $this->ids = $ids;
+ return $this;
+ }
+
+ public function withPHIDs(array $phids) {
+ $this->phids = $phids;
+ return $this;
+ }
+
+ public function withUserPHIDs(array $user_phids) {
+ $this->userPHIDs = $user_phids;
+ return $this;
+ }
+
+ public function withFactorProviderPHIDs(array $provider_phids) {
+ $this->factorProviderPHIDs = $provider_phids;
+ return $this;
+ }
+
+ public function withFactorProviderStatuses(array $statuses) {
+ $this->factorProviderStatuses = $statuses;
+ return $this;
+ }
+
+ public function newResultObject() {
+ return new PhabricatorAuthFactorConfig();
+ }
+
+ protected function loadPage() {
+ return $this->loadStandardPage($this->newResultObject());
+ }
+
+ protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
+ $where = parent::buildWhereClauseParts($conn);
+
+ if ($this->ids !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'config.id IN (%Ld)',
+ $this->ids);
+ }
+
+ if ($this->phids !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'config.phid IN (%Ls)',
+ $this->phids);
+ }
+
+ if ($this->userPHIDs !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'config.userPHID IN (%Ls)',
+ $this->userPHIDs);
+ }
+
+ if ($this->factorProviderPHIDs !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'config.factorProviderPHID IN (%Ls)',
+ $this->factorProviderPHIDs);
+ }
+
+ if ($this->factorProviderStatuses !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'provider.status IN (%Ls)',
+ $this->factorProviderStatuses);
+ }
+
+ return $where;
+ }
+
+ protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
+ $joins = parent::buildJoinClauseParts($conn);
+
+ if ($this->factorProviderStatuses !== null) {
+ $joins[] = qsprintf(
+ $conn,
+ 'JOIN %R provider ON config.factorProviderPHID = provider.phid',
+ new PhabricatorAuthFactorProvider());
+ }
+
+ return $joins;
+ }
+
+ protected function willFilterPage(array $configs) {
+ $provider_phids = mpull($configs, 'getFactorProviderPHID');
+
+ $providers = id(new PhabricatorAuthFactorProviderQuery())
+ ->setViewer($this->getViewer())
+ ->withPHIDs($provider_phids)
+ ->execute();
+ $providers = mpull($providers, null, 'getPHID');
+
+ foreach ($configs as $key => $config) {
+ $provider = idx($providers, $config->getFactorProviderPHID());
+
+ if (!$provider) {
+ unset($configs[$key]);
+ $this->didRejectResult($config);
+ continue;
+ }
+
+ $config->attachFactorProvider($provider);
+ }
+
+ return $configs;
+ }
+
+ protected function getPrimaryTableAlias() {
+ return 'config';
+ }
+
+ public function getQueryApplicationClass() {
+ return 'PhabricatorAuthApplication';
+ }
+
+}
diff --git a/src/applications/auth/query/PhabricatorAuthFactorProviderQuery.php b/src/applications/auth/query/PhabricatorAuthFactorProviderQuery.php
new file mode 100644
index 000000000..57b554885
--- /dev/null
+++ b/src/applications/auth/query/PhabricatorAuthFactorProviderQuery.php
@@ -0,0 +1,94 @@
+<?php
+
+final class PhabricatorAuthFactorProviderQuery
+ extends PhabricatorCursorPagedPolicyAwareQuery {
+
+ private $ids;
+ private $phids;
+ private $statuses;
+ private $providerFactorKeys;
+
+ public function withIDs(array $ids) {
+ $this->ids = $ids;
+ return $this;
+ }
+
+ public function withPHIDs(array $phids) {
+ $this->phids = $phids;
+ return $this;
+ }
+
+ public function withProviderFactorKeys(array $keys) {
+ $this->providerFactorKeys = $keys;
+ return $this;
+ }
+
+ public function withStatuses(array $statuses) {
+ $this->statuses = $statuses;
+ return $this;
+ }
+
+ public function newResultObject() {
+ return new PhabricatorAuthFactorProvider();
+ }
+
+ protected function loadPage() {
+ return $this->loadStandardPage($this->newResultObject());
+ }
+
+ protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
+ $where = parent::buildWhereClauseParts($conn);
+
+ if ($this->ids !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'id IN (%Ld)',
+ $this->ids);
+ }
+
+ if ($this->phids !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'phid IN (%Ls)',
+ $this->phids);
+ }
+
+ if ($this->statuses !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'status IN (%Ls)',
+ $this->statuses);
+ }
+
+ if ($this->providerFactorKeys !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'providerFactorKey IN (%Ls)',
+ $this->providerFactorKeys);
+ }
+
+ return $where;
+ }
+
+ protected function willFilterPage(array $providers) {
+ $map = PhabricatorAuthFactor::getAllFactors();
+ foreach ($providers as $key => $provider) {
+ $factor_key = $provider->getProviderFactorKey();
+ $factor = idx($map, $factor_key);
+
+ if (!$factor) {
+ unset($providers[$key]);
+ continue;
+ }
+
+ $provider->attachFactor($factor);
+ }
+
+ return $providers;
+ }
+
+ public function getQueryApplicationClass() {
+ return 'PhabricatorAuthApplication';
+ }
+
+}
diff --git a/src/applications/auth/query/PhabricatorAuthFactorProviderTransactionQuery.php b/src/applications/auth/query/PhabricatorAuthFactorProviderTransactionQuery.php
new file mode 100644
index 000000000..5add1345c
--- /dev/null
+++ b/src/applications/auth/query/PhabricatorAuthFactorProviderTransactionQuery.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhabricatorAuthFactorProviderTransactionQuery
+ extends PhabricatorApplicationTransactionQuery {
+
+ public function getTemplateApplicationTransaction() {
+ return new PhabricatorAuthFactorProviderTransaction();
+ }
+
+}
diff --git a/src/applications/auth/query/PhabricatorAuthMessageQuery.php b/src/applications/auth/query/PhabricatorAuthMessageQuery.php
new file mode 100644
index 000000000..384c8de23
--- /dev/null
+++ b/src/applications/auth/query/PhabricatorAuthMessageQuery.php
@@ -0,0 +1,83 @@
+<?php
+
+final class PhabricatorAuthMessageQuery
+ extends PhabricatorCursorPagedPolicyAwareQuery {
+
+ private $ids;
+ private $phids;
+ private $messageKeys;
+
+ public function withIDs(array $ids) {
+ $this->ids = $ids;
+ return $this;
+ }
+
+ public function withPHIDs(array $phids) {
+ $this->phids = $phids;
+ return $this;
+ }
+
+ public function withMessageKeys(array $keys) {
+ $this->messageKeys = $keys;
+ return $this;
+ }
+
+ public function newResultObject() {
+ return new PhabricatorAuthMessage();
+ }
+
+ protected function loadPage() {
+ return $this->loadStandardPage($this->newResultObject());
+ }
+
+ protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
+ $where = parent::buildWhereClauseParts($conn);
+
+ if ($this->ids !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'id IN (%Ld)',
+ $this->ids);
+ }
+
+ if ($this->phids !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'phid IN (%Ls)',
+ $this->phids);
+ }
+
+ if ($this->messageKeys !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'messageKey IN (%Ls)',
+ $this->messageKeys);
+ }
+
+ return $where;
+ }
+
+ protected function willFilterPage(array $messages) {
+ $message_types = PhabricatorAuthMessageType::getAllMessageTypes();
+
+ foreach ($messages as $key => $message) {
+ $message_key = $message->getMessageKey();
+
+ $message_type = idx($message_types, $message_key);
+ if (!$message_type) {
+ unset($messages[$key]);
+ $this->didRejectResult($message);
+ continue;
+ }
+
+ $message->attachMessageType($message_type);
+ }
+
+ return $messages;
+ }
+
+ public function getQueryApplicationClass() {
+ return 'PhabricatorAuthApplication';
+ }
+
+}
diff --git a/src/applications/auth/query/PhabricatorAuthMessageTransactionQuery.php b/src/applications/auth/query/PhabricatorAuthMessageTransactionQuery.php
new file mode 100644
index 000000000..0b2ce79db
--- /dev/null
+++ b/src/applications/auth/query/PhabricatorAuthMessageTransactionQuery.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhabricatorAuthMessageTransactionQuery
+ extends PhabricatorApplicationTransactionQuery {
+
+ public function getTemplateApplicationTransaction() {
+ return new PhabricatorAuthMessageTransaction();
+ }
+
+}
diff --git a/src/applications/auth/storage/PhabricatorAuthChallenge.php b/src/applications/auth/storage/PhabricatorAuthChallenge.php
new file mode 100644
index 000000000..8fa07d712
--- /dev/null
+++ b/src/applications/auth/storage/PhabricatorAuthChallenge.php
@@ -0,0 +1,262 @@
+<?php
+
+final class PhabricatorAuthChallenge
+ extends PhabricatorAuthDAO
+ implements PhabricatorPolicyInterface {
+
+ protected $userPHID;
+ protected $factorPHID;
+ protected $sessionPHID;
+ protected $workflowKey;
+ protected $challengeKey;
+ protected $challengeTTL;
+ protected $responseDigest;
+ protected $responseTTL;
+ protected $isCompleted;
+ protected $properties = array();
+
+ private $responseToken;
+
+ const HTTPKEY = '__hisec.challenges__';
+ const TOKEN_DIGEST_KEY = 'auth.challenge.token';
+
+ public static function initializeNewChallenge() {
+ return id(new self())
+ ->setIsCompleted(0);
+ }
+
+ public static function newHTTPParametersFromChallenges(array $challenges) {
+ assert_instances_of($challenges, __CLASS__);
+
+ $token_list = array();
+ foreach ($challenges as $challenge) {
+ $token = $challenge->getResponseToken();
+ if ($token) {
+ $token_list[] = sprintf(
+ '%s:%s',
+ $challenge->getPHID(),
+ $token->openEnvelope());
+ }
+ }
+
+ if (!$token_list) {
+ return array();
+ }
+
+ $token_list = implode(' ', $token_list);
+
+ return array(
+ self::HTTPKEY => $token_list,
+ );
+ }
+
+ public static function newChallengeResponsesFromRequest(
+ array $challenges,
+ AphrontRequest $request) {
+ assert_instances_of($challenges, __CLASS__);
+
+ $token_list = $request->getStr(self::HTTPKEY);
+ $token_list = explode(' ', $token_list);
+
+ $token_map = array();
+ foreach ($token_list as $token_element) {
+ $token_element = trim($token_element, ' ');
+
+ if (!strlen($token_element)) {
+ continue;
+ }
+
+ // NOTE: This error message is intentionally not printing the token to
+ // avoid disclosing it. As a result, it isn't terribly useful, but no
+ // normal user should ever end up here.
+ if (!preg_match('/^[^:]+:/', $token_element)) {
+ throw new Exception(
+ pht(
+ 'This request included an improperly formatted MFA challenge '.
+ 'token and can not be processed.'));
+ }
+
+ list($phid, $token) = explode(':', $token_element, 2);
+
+ if (isset($token_map[$phid])) {
+ throw new Exception(
+ pht(
+ 'This request improperly specifies an MFA challenge token ("%s") '.
+ 'multiple times and can not be processed.',
+ $phid));
+ }
+
+ $token_map[$phid] = new PhutilOpaqueEnvelope($token);
+ }
+
+ $challenges = mpull($challenges, null, 'getPHID');
+
+ $now = PhabricatorTime::getNow();
+ foreach ($challenges as $challenge_phid => $challenge) {
+ // If the response window has expired, don't attach the token.
+ if ($challenge->getResponseTTL() < $now) {
+ continue;
+ }
+
+ $token = idx($token_map, $challenge_phid);
+ if (!$token) {
+ continue;
+ }
+
+ $challenge->setResponseToken($token);
+ }
+ }
+
+
+ protected function getConfiguration() {
+ return array(
+ self::CONFIG_SERIALIZATION => array(
+ 'properties' => self::SERIALIZATION_JSON,
+ ),
+ self::CONFIG_AUX_PHID => true,
+ self::CONFIG_COLUMN_SCHEMA => array(
+ 'challengeKey' => 'text255',
+ 'challengeTTL' => 'epoch',
+ 'workflowKey' => 'text255',
+ 'responseDigest' => 'text255?',
+ 'responseTTL' => 'epoch?',
+ 'isCompleted' => 'bool',
+ ),
+ self::CONFIG_KEY_SCHEMA => array(
+ 'key_issued' => array(
+ 'columns' => array('userPHID', 'challengeTTL'),
+ ),
+ 'key_collection' => array(
+ 'columns' => array('challengeTTL'),
+ ),
+ ),
+ ) + parent::getConfiguration();
+ }
+
+ public function getPHIDType() {
+ return PhabricatorAuthChallengePHIDType::TYPECONST;
+ }
+
+ public function getIsReusedChallenge() {
+ if ($this->getIsCompleted()) {
+ return true;
+ }
+
+ if (!$this->getIsAnsweredChallenge()) {
+ return false;
+ }
+
+ // If the challenge has been answered but the client has provided a token
+ // proving that they answered it, this is still a valid response.
+ if ($this->getResponseToken()) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function getIsAnsweredChallenge() {
+ return (bool)$this->getResponseDigest();
+ }
+
+ public function markChallengeAsAnswered($ttl) {
+ $token = Filesystem::readRandomCharacters(32);
+ $token = new PhutilOpaqueEnvelope($token);
+
+ $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
+
+ $this
+ ->setResponseToken($token)
+ ->setResponseTTL($ttl)
+ ->save();
+
+ unset($unguarded);
+
+ return $this;
+ }
+
+ public function markChallengeAsCompleted() {
+ return $this
+ ->setIsCompleted(true)
+ ->save();
+ }
+
+ public function setResponseToken(PhutilOpaqueEnvelope $token) {
+ if (!$this->getUserPHID()) {
+ throw new PhutilInvalidStateException('setUserPHID');
+ }
+
+ if ($this->responseToken) {
+ throw new Exception(
+ pht(
+ 'This challenge already has a response token; you can not '.
+ 'set a new response token.'));
+ }
+
+ if (preg_match('/ /', $token->openEnvelope())) {
+ throw new Exception(
+ pht(
+ 'The response token for this challenge is invalid: response '.
+ 'tokens may not include spaces.'));
+ }
+
+ $digest = PhabricatorHash::digestWithNamedKey(
+ $token->openEnvelope(),
+ self::TOKEN_DIGEST_KEY);
+
+ if ($this->responseDigest !== null) {
+ if (!phutil_hashes_are_identical($digest, $this->responseDigest)) {
+ throw new Exception(
+ pht(
+ 'Invalid response token for this challenge: token digest does '.
+ 'not match stored digest.'));
+ }
+ } else {
+ $this->responseDigest = $digest;
+ }
+
+ $this->responseToken = $token;
+
+ return $this;
+ }
+
+ public function getResponseToken() {
+ return $this->responseToken;
+ }
+
+ public function setResponseDigest($value) {
+ throw new Exception(
+ pht(
+ 'You can not set the response digest for a challenge directly. '.
+ 'Instead, set a response token. A response digest will be computed '.
+ 'automatically.'));
+ }
+
+ public function setProperty($key, $value) {
+ $this->properties[$key] = $value;
+ return $this;
+ }
+
+ public function getProperty($key, $default = null) {
+ return $this->properties[$key];
+ }
+
+
+/* -( PhabricatorPolicyInterface )----------------------------------------- */
+
+
+ public function getCapabilities() {
+ return array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ );
+ }
+
+ public function getPolicy($capability) {
+ return PhabricatorPolicies::POLICY_NOONE;
+ }
+
+ public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
+ return ($viewer->getPHID() === $this->getUserPHID());
+ }
+
+}
diff --git a/src/applications/auth/storage/PhabricatorAuthContactNumber.php b/src/applications/auth/storage/PhabricatorAuthContactNumber.php
new file mode 100644
index 000000000..2a138e244
--- /dev/null
+++ b/src/applications/auth/storage/PhabricatorAuthContactNumber.php
@@ -0,0 +1,243 @@
+<?php
+
+
+final class PhabricatorAuthContactNumber
+ extends PhabricatorAuthDAO
+ implements
+ PhabricatorApplicationTransactionInterface,
+ PhabricatorPolicyInterface,
+ PhabricatorDestructibleInterface,
+ PhabricatorEditEngineMFAInterface {
+
+ protected $objectPHID;
+ protected $contactNumber;
+ protected $uniqueKey;
+ protected $status;
+ protected $isPrimary;
+ protected $properties = array();
+
+ const STATUS_ACTIVE = 'active';
+ const STATUS_DISABLED = 'disabled';
+
+ protected function getConfiguration() {
+ return array(
+ self::CONFIG_SERIALIZATION => array(
+ 'properties' => self::SERIALIZATION_JSON,
+ ),
+ self::CONFIG_AUX_PHID => true,
+ self::CONFIG_COLUMN_SCHEMA => array(
+ 'contactNumber' => 'text255',
+ 'status' => 'text32',
+ 'uniqueKey' => 'bytes12?',
+ 'isPrimary' => 'bool',
+ ),
+ self::CONFIG_KEY_SCHEMA => array(
+ 'key_object' => array(
+ 'columns' => array('objectPHID'),
+ ),
+ 'key_unique' => array(
+ 'columns' => array('uniqueKey'),
+ 'unique' => true,
+ ),
+ ),
+ ) + parent::getConfiguration();
+ }
+
+ public static function initializeNewContactNumber($object) {
+ return id(new self())
+ ->setStatus(self::STATUS_ACTIVE)
+ ->setObjectPHID($object->getPHID())
+ ->setIsPrimary(0);
+ }
+
+ public function getPHIDType() {
+ return PhabricatorAuthContactNumberPHIDType::TYPECONST;
+ }
+
+ public function getURI() {
+ return urisprintf('/auth/contact/%s/', $this->getID());
+ }
+
+ public function getObjectName() {
+ return pht('Contact Number %d', $this->getID());
+ }
+
+ public function getDisplayName() {
+ return $this->getContactNumber();
+ }
+
+ public function isDisabled() {
+ return ($this->getStatus() === self::STATUS_DISABLED);
+ }
+
+ public function newIconView() {
+ if ($this->isDisabled()) {
+ return id(new PHUIIconView())
+ ->setIcon('fa-ban', 'grey')
+ ->setTooltip(pht('Disabled'));
+ }
+
+ if ($this->getIsPrimary()) {
+ return id(new PHUIIconView())
+ ->setIcon('fa-certificate', 'blue')
+ ->setTooltip(pht('Primary Number'));
+ }
+
+ return id(new PHUIIconView())
+ ->setIcon('fa-hashtag', 'bluegrey')
+ ->setTooltip(pht('Active Phone Number'));
+ }
+
+ public function newUniqueKey() {
+ $parts = array(
+ // This is future-proofing for a world where we have multiple types
+ // of contact numbers, so we might be able to avoid re-hashing
+ // everything.
+ 'phone',
+ $this->getContactNumber(),
+ );
+
+ $parts = implode("\0", $parts);
+
+ return PhabricatorHash::digestForIndex($parts);
+ }
+
+ public function save() {
+ // We require that active contact numbers be unique, but it's okay to
+ // disable a number and then reuse it somewhere else.
+ if ($this->isDisabled()) {
+ $this->uniqueKey = null;
+ } else {
+ $this->uniqueKey = $this->newUniqueKey();
+ }
+
+ parent::save();
+
+ return $this->updatePrimaryContactNumber();
+ }
+
+ private function updatePrimaryContactNumber() {
+ // Update the "isPrimary" column so that at most one number is primary for
+ // each user, and no disabled number is primary.
+
+ $conn = $this->establishConnection('w');
+ $this_id = (int)$this->getID();
+
+ if ($this->getIsPrimary() && !$this->isDisabled()) {
+ // If we're trying to make this number primary and it's active, great:
+ // make this number the primary number.
+ $primary_id = $this_id;
+ } else {
+ // If we aren't trying to make this number primary or it is disabled,
+ // pick another number to make primary if we can. A number must be active
+ // to become primary.
+
+ // If there are multiple active numbers, pick the oldest one currently
+ // marked primary (usually, this should mean that we just keep the
+ // current primary number as primary).
+
+ // If none are marked primary, just pick the oldest one.
+ $primary_row = queryfx_one(
+ $conn,
+ 'SELECT id FROM %R
+ WHERE objectPHID = %s AND status = %s
+ ORDER BY isPrimary DESC, id ASC
+ LIMIT 1',
+ $this,
+ $this->getObjectPHID(),
+ self::STATUS_ACTIVE);
+ if ($primary_row) {
+ $primary_id = (int)$primary_row['id'];
+ } else {
+ $primary_id = -1;
+ }
+ }
+
+ // Set the chosen number to primary, and all other numbers to nonprimary.
+
+ queryfx(
+ $conn,
+ 'UPDATE %R SET isPrimary = IF(id = %d, 1, 0)
+ WHERE objectPHID = %s',
+ $this,
+ $primary_id,
+ $this->getObjectPHID());
+
+ $this->setIsPrimary((int)($primary_id === $this_id));
+
+ return $this;
+ }
+
+ public static function getStatusNameMap() {
+ return ipull(self::getStatusPropertyMap(), 'name');
+ }
+
+ private static function getStatusPropertyMap() {
+ return array(
+ self::STATUS_ACTIVE => array(
+ 'name' => pht('Active'),
+ ),
+ self::STATUS_DISABLED => array(
+ 'name' => pht('Disabled'),
+ ),
+ );
+ }
+
+ public function getSortVector() {
+ // Sort the primary number first, then active numbers, then disabled
+ // numbers. In each group, sort from oldest to newest.
+ return id(new PhutilSortVector())
+ ->addInt($this->getIsPrimary() ? 0 : 1)
+ ->addInt($this->isDisabled() ? 1 : 0)
+ ->addInt($this->getID());
+ }
+
+
+/* -( PhabricatorPolicyInterface )----------------------------------------- */
+
+
+ public function getCapabilities() {
+ return array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ );
+ }
+
+ public function getPolicy($capability) {
+ return $this->getObjectPHID();
+ }
+
+ public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
+ return false;
+ }
+
+
+/* -( PhabricatorDestructibleInterface )----------------------------------- */
+
+
+ public function destroyObjectPermanently(
+ PhabricatorDestructionEngine $engine) {
+ $this->delete();
+ }
+
+
+/* -( PhabricatorApplicationTransactionInterface )------------------------- */
+
+
+ public function getApplicationTransactionEditor() {
+ return new PhabricatorAuthContactNumberEditor();
+ }
+
+ public function getApplicationTransactionTemplate() {
+ return new PhabricatorAuthContactNumberTransaction();
+ }
+
+
+/* -( PhabricatorEditEngineMFAInterface )---------------------------------- */
+
+
+ public function newEditEngineMFAEngine() {
+ return new PhabricatorAuthContactNumberMFAEngine();
+ }
+
+}
diff --git a/src/applications/auth/storage/PhabricatorAuthContactNumberTransaction.php b/src/applications/auth/storage/PhabricatorAuthContactNumberTransaction.php
new file mode 100644
index 000000000..d6faccf49
--- /dev/null
+++ b/src/applications/auth/storage/PhabricatorAuthContactNumberTransaction.php
@@ -0,0 +1,18 @@
+<?php
+
+final class PhabricatorAuthContactNumberTransaction
+ extends PhabricatorModularTransaction {
+
+ public function getApplicationName() {
+ return 'auth';
+ }
+
+ public function getApplicationTransactionType() {
+ return PhabricatorAuthContactNumberPHIDType::TYPECONST;
+ }
+
+ public function getBaseTransactionClass() {
+ return 'PhabricatorAuthContactNumberTransactionType';
+ }
+
+}
diff --git a/src/applications/auth/storage/PhabricatorAuthFactorConfig.php b/src/applications/auth/storage/PhabricatorAuthFactorConfig.php
index 8420ea9ba..ed5a27f54 100644
--- a/src/applications/auth/storage/PhabricatorAuthFactorConfig.php
+++ b/src/applications/auth/storage/PhabricatorAuthFactorConfig.php
@@ -1,52 +1,127 @@
<?php
-final class PhabricatorAuthFactorConfig extends PhabricatorAuthDAO {
+
+final class PhabricatorAuthFactorConfig
+ extends PhabricatorAuthDAO
+ implements
+ PhabricatorPolicyInterface,
+ PhabricatorDestructibleInterface {
protected $userPHID;
- protected $factorKey;
+ protected $factorProviderPHID;
protected $factorName;
protected $factorSecret;
protected $properties = array();
+ private $sessionEngine;
+ private $factorProvider = self::ATTACHABLE;
+ private $mfaSyncToken;
+
protected function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
- 'factorKey' => 'text64',
'factorName' => 'text',
'factorSecret' => 'text',
),
self::CONFIG_KEY_SCHEMA => array(
'key_user' => array(
'columns' => array('userPHID'),
),
),
) + parent::getConfiguration();
}
- public function generatePHID() {
- return PhabricatorPHID::generateNewPHID(
- PhabricatorAuthAuthFactorPHIDType::TYPECONST);
+ public function getPHIDType() {
+ return PhabricatorAuthAuthFactorPHIDType::TYPECONST;
}
- public function getImplementation() {
- return idx(PhabricatorAuthFactor::getAllFactors(), $this->getFactorKey());
+ public function attachFactorProvider(
+ PhabricatorAuthFactorProvider $provider) {
+ $this->factorProvider = $provider;
+ return $this;
}
- public function requireImplementation() {
- $impl = $this->getImplementation();
- if (!$impl) {
- throw new Exception(
- pht(
- 'Attempting to operate on multi-factor auth which has no '.
- 'corresponding implementation (factor key is "%s").',
- $this->getFactorKey()));
+ public function getFactorProvider() {
+ return $this->assertAttached($this->factorProvider);
+ }
+
+ public function setSessionEngine(PhabricatorAuthSessionEngine $engine) {
+ $this->sessionEngine = $engine;
+ return $this;
+ }
+
+ public function getSessionEngine() {
+ if (!$this->sessionEngine) {
+ throw new PhutilInvalidStateException('setSessionEngine');
}
- return $impl;
+ return $this->sessionEngine;
+ }
+
+ public function setMFASyncToken(PhabricatorAuthTemporaryToken $token) {
+ $this->mfaSyncToken = $token;
+ return $this;
+ }
+
+ public function getMFASyncToken() {
+ return $this->mfaSyncToken;
+ }
+
+ public function getAuthFactorConfigProperty($key, $default = null) {
+ return idx($this->properties, $key, $default);
+ }
+
+ public function setAuthFactorConfigProperty($key, $value) {
+ $this->properties[$key] = $value;
+ return $this;
+ }
+
+ public function newSortVector() {
+ return id(new PhutilSortVector())
+ ->addInt($this->getFactorProvider()->newStatus()->getOrder())
+ ->addInt($this->getID());
+ }
+
+
+/* -( PhabricatorPolicyInterface )----------------------------------------- */
+
+
+ public function getCapabilities() {
+ return array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ );
+ }
+
+ public function getPolicy($capability) {
+ return $this->getUserPHID();
+ }
+
+ public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
+ return false;
+ }
+
+
+/* -( PhabricatorDestructibleInterface )----------------------------------- */
+
+
+ public function destroyObjectPermanently(
+ PhabricatorDestructionEngine $engine) {
+
+ $user = id(new PhabricatorPeopleQuery())
+ ->setViewer($engine->getViewer())
+ ->withPHIDs(array($this->getUserPHID()))
+ ->executeOne();
+
+ $this->delete();
+
+ if ($user) {
+ $user->updateMultiFactorEnrollment();
+ }
}
}
diff --git a/src/applications/auth/storage/PhabricatorAuthFactorProvider.php b/src/applications/auth/storage/PhabricatorAuthFactorProvider.php
new file mode 100644
index 000000000..2213535df
--- /dev/null
+++ b/src/applications/auth/storage/PhabricatorAuthFactorProvider.php
@@ -0,0 +1,207 @@
+<?php
+
+final class PhabricatorAuthFactorProvider
+ extends PhabricatorAuthDAO
+ implements
+ PhabricatorApplicationTransactionInterface,
+ PhabricatorPolicyInterface,
+ PhabricatorExtendedPolicyInterface,
+ PhabricatorEditEngineMFAInterface {
+
+ protected $providerFactorKey;
+ protected $name;
+ protected $status;
+ protected $properties = array();
+
+ private $factor = self::ATTACHABLE;
+
+ public static function initializeNewProvider(PhabricatorAuthFactor $factor) {
+ return id(new self())
+ ->setProviderFactorKey($factor->getFactorKey())
+ ->attachFactor($factor)
+ ->setStatus(PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE);
+ }
+
+ protected function getConfiguration() {
+ return array(
+ self::CONFIG_SERIALIZATION => array(
+ 'properties' => self::SERIALIZATION_JSON,
+ ),
+ self::CONFIG_AUX_PHID => true,
+ self::CONFIG_COLUMN_SCHEMA => array(
+ 'providerFactorKey' => 'text64',
+ 'name' => 'text255',
+ 'status' => 'text32',
+ ),
+ ) + parent::getConfiguration();
+ }
+
+ public function getPHIDType() {
+ return PhabricatorAuthAuthFactorProviderPHIDType::TYPECONST;
+ }
+
+ public function getURI() {
+ return '/auth/mfa/'.$this->getID().'/';
+ }
+
+ public function getObjectName() {
+ return pht('MFA Provider %d', $this->getID());
+ }
+
+ public function getAuthFactorProviderProperty($key, $default = null) {
+ return idx($this->properties, $key, $default);
+ }
+
+ public function setAuthFactorProviderProperty($key, $value) {
+ $this->properties[$key] = $value;
+ return $this;
+ }
+
+ public function getEnrollMessage() {
+ return $this->getAuthFactorProviderProperty('enroll-message');
+ }
+
+ public function setEnrollMessage($message) {
+ return $this->setAuthFactorProviderProperty('enroll-message', $message);
+ }
+
+ public function attachFactor(PhabricatorAuthFactor $factor) {
+ $this->factor = $factor;
+ return $this;
+ }
+
+ public function getFactor() {
+ return $this->assertAttached($this->factor);
+ }
+
+ public function getDisplayName() {
+ $name = $this->getName();
+ if (strlen($name)) {
+ return $name;
+ }
+
+ return $this->getFactor()->getFactorName();
+ }
+
+ public function newIconView() {
+ return $this->getFactor()->newIconView();
+ }
+
+ public function getDisplayDescription() {
+ return $this->getFactor()->getFactorDescription();
+ }
+
+ public function processAddFactorForm(
+ AphrontFormView $form,
+ AphrontRequest $request,
+ PhabricatorUser $user) {
+
+ $factor = $this->getFactor();
+
+ $config = $factor->processAddFactorForm($this, $form, $request, $user);
+ if ($config) {
+ $config->setFactorProviderPHID($this->getPHID());
+ }
+
+ return $config;
+ }
+
+ public function newSortVector() {
+ $factor = $this->getFactor();
+
+ return id(new PhutilSortVector())
+ ->addInt($factor->getFactorOrder())
+ ->addInt($this->getID());
+ }
+
+ public function getEnrollDescription(PhabricatorUser $user) {
+ return $this->getFactor()->getEnrollDescription($this, $user);
+ }
+
+ public function getEnrollButtonText(PhabricatorUser $user) {
+ return $this->getFactor()->getEnrollButtonText($this, $user);
+ }
+
+ public function newStatus() {
+ $status_key = $this->getStatus();
+ return PhabricatorAuthFactorProviderStatus::newForStatus($status_key);
+ }
+
+ public function canCreateNewConfiguration(PhabricatorUser $user) {
+ return $this->getFactor()->canCreateNewConfiguration($this, $user);
+ }
+
+ public function getConfigurationCreateDescription(PhabricatorUser $user) {
+ return $this->getFactor()->getConfigurationCreateDescription($this, $user);
+ }
+
+ public function getConfigurationListDetails(
+ PhabricatorAuthFactorConfig $config,
+ PhabricatorUser $viewer) {
+ return $this->getFactor()->getConfigurationListDetails(
+ $config,
+ $this,
+ $viewer);
+ }
+
+
+/* -( PhabricatorApplicationTransactionInterface )------------------------- */
+
+
+ public function getApplicationTransactionEditor() {
+ return new PhabricatorAuthFactorProviderEditor();
+ }
+
+ public function getApplicationTransactionTemplate() {
+ return new PhabricatorAuthFactorProviderTransaction();
+ }
+
+
+/* -( PhabricatorPolicyInterface )----------------------------------------- */
+
+
+ public function getCapabilities() {
+ return array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ );
+ }
+
+ public function getPolicy($capability) {
+ return PhabricatorPolicies::getMostOpenPolicy();
+ }
+
+ public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
+ return false;
+ }
+
+
+/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
+
+
+ public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
+ $extended = array();
+
+ switch ($capability) {
+ case PhabricatorPolicyCapability::CAN_VIEW:
+ break;
+ case PhabricatorPolicyCapability::CAN_EDIT:
+ $extended[] = array(
+ new PhabricatorAuthApplication(),
+ AuthManageProvidersCapability::CAPABILITY,
+ );
+ break;
+ }
+
+ return $extended;
+ }
+
+
+/* -( PhabricatorEditEngineMFAInterface )---------------------------------- */
+
+
+ public function newEditEngineMFAEngine() {
+ return new PhabricatorAuthFactorProviderMFAEngine();
+ }
+
+}
diff --git a/src/applications/auth/storage/PhabricatorAuthFactorProviderTransaction.php b/src/applications/auth/storage/PhabricatorAuthFactorProviderTransaction.php
new file mode 100644
index 000000000..0b7b7fc6a
--- /dev/null
+++ b/src/applications/auth/storage/PhabricatorAuthFactorProviderTransaction.php
@@ -0,0 +1,18 @@
+<?php
+
+final class PhabricatorAuthFactorProviderTransaction
+ extends PhabricatorModularTransaction {
+
+ public function getApplicationName() {
+ return 'auth';
+ }
+
+ public function getApplicationTransactionType() {
+ return PhabricatorAuthAuthFactorProviderPHIDType::TYPECONST;
+ }
+
+ public function getBaseTransactionClass() {
+ return 'PhabricatorAuthFactorProviderTransactionType';
+ }
+
+}
diff --git a/src/applications/auth/storage/PhabricatorAuthMessage.php b/src/applications/auth/storage/PhabricatorAuthMessage.php
new file mode 100644
index 000000000..00f5fbfba
--- /dev/null
+++ b/src/applications/auth/storage/PhabricatorAuthMessage.php
@@ -0,0 +1,138 @@
+<?php
+
+final class PhabricatorAuthMessage
+ extends PhabricatorAuthDAO
+ implements
+ PhabricatorApplicationTransactionInterface,
+ PhabricatorPolicyInterface,
+ PhabricatorDestructibleInterface {
+
+ protected $messageKey;
+ protected $messageText;
+
+ private $messageType = self::ATTACHABLE;
+
+ public static function initializeNewMessage(
+ PhabricatorAuthMessageType $type) {
+
+ return id(new self())
+ ->setMessageKey($type->getMessageTypeKey())
+ ->attachMessageType($type);
+ }
+
+ protected function getConfiguration() {
+ return array(
+ self::CONFIG_AUX_PHID => true,
+ self::CONFIG_COLUMN_SCHEMA => array(
+ 'messageKey' => 'text64',
+ 'messageText' => 'text',
+ ),
+ self::CONFIG_KEY_SCHEMA => array(
+ 'key_type' => array(
+ 'columns' => array('messageKey'),
+ 'unique' => true,
+ ),
+ ),
+ ) + parent::getConfiguration();
+ }
+
+ public function getPHIDType() {
+ return PhabricatorAuthMessagePHIDType::TYPECONST;
+ }
+
+ public function getObjectName() {
+ return pht('Auth Message %d', $this->getID());
+ }
+
+ public function getURI() {
+ return urisprintf('/auth/message/%s', $this->getID());
+ }
+
+ public function attachMessageType(PhabricatorAuthMessageType $type) {
+ $this->messageType = $type;
+ return $this;
+ }
+
+ public function getMessageType() {
+ return $this->assertAttached($this->messageType);
+ }
+
+ public function getMessageTypeDisplayName() {
+ return $this->getMessageType()->getDisplayName();
+ }
+
+ public static function loadMessage(
+ PhabricatorUser $viewer,
+ $message_key) {
+ return id(new PhabricatorAuthMessageQuery())
+ ->setViewer($viewer)
+ ->withMessageKeys(array($message_key))
+ ->executeOne();
+ }
+
+ public static function loadMessageText(
+ PhabricatorUser $viewer,
+ $message_key) {
+
+ $message = self::loadMessage($viewer, $message_key);
+
+ if (!$message) {
+ return null;
+ }
+
+ return $message->getMessageText();
+ }
+
+
+/* -( PhabricatorPolicyInterface )----------------------------------------- */
+
+
+ public function getCapabilities() {
+ return array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ );
+ }
+
+ public function getPolicy($capability) {
+ switch ($capability) {
+ case PhabricatorPolicyCapability::CAN_VIEW:
+ return PhabricatorPolicies::getMostOpenPolicy();
+ default:
+ return false;
+ }
+ }
+
+ public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
+ switch ($capability) {
+ case PhabricatorPolicyCapability::CAN_VIEW:
+ // Even if an install doesn't allow public users, you can still view
+ // auth messages: otherwise, we can't do things like show you
+ // guidance on the login screen.
+ return true;
+ default:
+ return false;
+ }
+ }
+
+/* -( PhabricatorApplicationTransactionInterface )------------------------- */
+
+
+ public function getApplicationTransactionEditor() {
+ return new PhabricatorAuthMessageEditor();
+ }
+
+ public function getApplicationTransactionTemplate() {
+ return new PhabricatorAuthMessageTransaction();
+ }
+
+
+/* -( PhabricatorDestructibleInterface )----------------------------------- */
+
+
+ public function destroyObjectPermanently(
+ PhabricatorDestructionEngine $engine) {
+ $this->delete();
+ }
+
+}
diff --git a/src/applications/auth/storage/PhabricatorAuthMessageTransaction.php b/src/applications/auth/storage/PhabricatorAuthMessageTransaction.php
new file mode 100644
index 000000000..407a9735c
--- /dev/null
+++ b/src/applications/auth/storage/PhabricatorAuthMessageTransaction.php
@@ -0,0 +1,18 @@
+<?php
+
+final class PhabricatorAuthMessageTransaction
+ extends PhabricatorModularTransaction {
+
+ public function getApplicationName() {
+ return 'auth';
+ }
+
+ public function getApplicationTransactionType() {
+ return PhabricatorAuthMessagePHIDType::TYPECONST;
+ }
+
+ public function getBaseTransactionClass() {
+ return 'PhabricatorAuthMessageTransactionType';
+ }
+
+}
diff --git a/src/applications/auth/storage/PhabricatorAuthPassword.php b/src/applications/auth/storage/PhabricatorAuthPassword.php
index 3bcb95693..3196b58e6 100644
--- a/src/applications/auth/storage/PhabricatorAuthPassword.php
+++ b/src/applications/auth/storage/PhabricatorAuthPassword.php
@@ -1,236 +1,224 @@
<?php
final class PhabricatorAuthPassword
extends PhabricatorAuthDAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface,
PhabricatorApplicationTransactionInterface {
protected $objectPHID;
protected $passwordType;
protected $passwordHash;
protected $passwordSalt;
protected $isRevoked;
protected $legacyDigestFormat;
private $object = self::ATTACHABLE;
const PASSWORD_TYPE_ACCOUNT = 'account';
const PASSWORD_TYPE_VCS = 'vcs';
const PASSWORD_TYPE_TEST = 'test';
public static function initializeNewPassword(
PhabricatorAuthPasswordHashInterface $object,
$type) {
return id(new self())
->setObjectPHID($object->getPHID())
->attachObject($object)
->setPasswordType($type)
->setIsRevoked(0);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'passwordType' => 'text64',
'passwordHash' => 'text128',
'passwordSalt' => 'text64',
'isRevoked' => 'bool',
'legacyDigestFormat' => 'text32?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_role' => array(
'columns' => array('objectPHID', 'passwordType'),
),
),
) + parent::getConfiguration();
}
public function getPHIDType() {
return PhabricatorAuthPasswordPHIDType::TYPECONST;
}
public function getObject() {
return $this->assertAttached($this->object);
}
public function attachObject($object) {
$this->object = $object;
return $this;
}
public function getHasher() {
$hash = $this->newPasswordEnvelope();
return PhabricatorPasswordHasher::getHasherForHash($hash);
}
public function canUpgrade() {
// If this password uses a legacy digest format, we can upgrade it to the
// new digest format even if a better hasher isn't available.
if ($this->getLegacyDigestFormat() !== null) {
return true;
}
$hash = $this->newPasswordEnvelope();
return PhabricatorPasswordHasher::canUpgradeHash($hash);
}
public function upgradePasswordHasher(
PhutilOpaqueEnvelope $envelope,
PhabricatorAuthPasswordHashInterface $object) {
// Before we make changes, double check that this is really the correct
// password. It could be really bad if we "upgraded" a password and changed
// the secret!
if (!$this->comparePassword($envelope, $object)) {
throw new Exception(
pht(
'Attempting to upgrade password hasher, but the password for the '.
'upgrade is not the stored credential!'));
}
return $this->setPassword($envelope, $object);
}
public function setPassword(
PhutilOpaqueEnvelope $password,
PhabricatorAuthPasswordHashInterface $object) {
$hasher = PhabricatorPasswordHasher::getBestHasher();
return $this->setPasswordWithHasher($password, $object, $hasher);
}
public function setPasswordWithHasher(
PhutilOpaqueEnvelope $password,
PhabricatorAuthPasswordHashInterface $object,
PhabricatorPasswordHasher $hasher) {
if (!strlen($password->openEnvelope())) {
throw new Exception(
pht('Attempting to set an empty password!'));
}
// Generate (or regenerate) the salt first.
$new_salt = Filesystem::readRandomCharacters(64);
$this->setPasswordSalt($new_salt);
// Clear any legacy digest format to force a modern digest.
$this->setLegacyDigestFormat(null);
$digest = $this->digestPassword($password, $object);
$hash = $hasher->getPasswordHashForStorage($digest);
$raw_hash = $hash->openEnvelope();
return $this->setPasswordHash($raw_hash);
}
public function comparePassword(
PhutilOpaqueEnvelope $password,
PhabricatorAuthPasswordHashInterface $object) {
$digest = $this->digestPassword($password, $object);
$hash = $this->newPasswordEnvelope();
return PhabricatorPasswordHasher::comparePassword($digest, $hash);
}
public function newPasswordEnvelope() {
return new PhutilOpaqueEnvelope($this->getPasswordHash());
}
private function digestPassword(
PhutilOpaqueEnvelope $password,
PhabricatorAuthPasswordHashInterface $object) {
$object_phid = $object->getPHID();
if ($this->getObjectPHID() !== $object->getPHID()) {
throw new Exception(
pht(
'This password is associated with an object PHID ("%s") for '.
'a different object than the provided one ("%s").',
$this->getObjectPHID(),
$object->getPHID()));
}
$digest = $object->newPasswordDigest($password, $this);
if (!($digest instanceof PhutilOpaqueEnvelope)) {
throw new Exception(
pht(
'Failed to digest password: object ("%s") did not return an '.
'opaque envelope with a password digest.',
$object->getPHID()));
}
return $digest;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return PhabricatorPolicies::getMostOpenPolicy();
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
return array(
array($this->getObject(), $capability),
);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->delete();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorAuthPasswordEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorAuthPasswordTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
-
}
diff --git a/src/applications/auth/storage/PhabricatorAuthProviderConfig.php b/src/applications/auth/storage/PhabricatorAuthProviderConfig.php
index ba9b43a96..1de34c407 100644
--- a/src/applications/auth/storage/PhabricatorAuthProviderConfig.php
+++ b/src/applications/auth/storage/PhabricatorAuthProviderConfig.php
@@ -1,133 +1,122 @@
<?php
final class PhabricatorAuthProviderConfig
extends PhabricatorAuthDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface {
protected $providerClass;
protected $providerType;
protected $providerDomain;
protected $isEnabled;
protected $shouldAllowLogin = 0;
protected $shouldAllowRegistration = 0;
protected $shouldAllowLink = 0;
protected $shouldAllowUnlink = 0;
protected $shouldTrustEmails = 0;
protected $shouldAutoLogin = 0;
protected $properties = array();
private $provider;
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorAuthAuthProviderPHIDType::TYPECONST);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'isEnabled' => 'bool',
'providerClass' => 'text128',
'providerType' => 'text32',
'providerDomain' => 'text128',
'shouldAllowLogin' => 'bool',
'shouldAllowRegistration' => 'bool',
'shouldAllowLink' => 'bool',
'shouldAllowUnlink' => 'bool',
'shouldTrustEmails' => 'bool',
'shouldAutoLogin' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_provider' => array(
'columns' => array('providerType', 'providerDomain'),
'unique' => true,
),
'key_class' => array(
'columns' => array('providerClass'),
),
),
) + parent::getConfiguration();
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getProvider() {
if (!$this->provider) {
$base = PhabricatorAuthProvider::getAllBaseProviders();
$found = null;
foreach ($base as $provider) {
if (get_class($provider) == $this->providerClass) {
$found = $provider;
break;
}
}
if ($found) {
$this->provider = id(clone $found)->attachProviderConfig($this);
}
}
return $this->provider;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorAuthProviderConfigEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorAuthProviderConfigTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::POLICY_USER;
case PhabricatorPolicyCapability::CAN_EDIT:
return PhabricatorPolicies::POLICY_ADMIN;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
}
diff --git a/src/applications/auth/storage/PhabricatorAuthSSHKey.php b/src/applications/auth/storage/PhabricatorAuthSSHKey.php
index 5bbb7de83..7350af8cf 100644
--- a/src/applications/auth/storage/PhabricatorAuthSSHKey.php
+++ b/src/applications/auth/storage/PhabricatorAuthSSHKey.php
@@ -1,176 +1,166 @@
<?php
final class PhabricatorAuthSSHKey
extends PhabricatorAuthDAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface,
PhabricatorApplicationTransactionInterface {
protected $objectPHID;
protected $name;
protected $keyType;
protected $keyIndex;
protected $keyBody;
protected $keyComment = '';
protected $isTrusted = 0;
protected $isActive;
private $object = self::ATTACHABLE;
public static function initializeNewSSHKey(
PhabricatorUser $viewer,
PhabricatorSSHPublicKeyInterface $object) {
// You must be able to edit an object to create a new key on it.
PhabricatorPolicyFilter::requireCapability(
$viewer,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
$object_phid = $object->getPHID();
return id(new self())
->setIsActive(1)
->setObjectPHID($object_phid)
->attachObject($object);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text255',
'keyType' => 'text255',
'keyIndex' => 'bytes12',
'keyBody' => 'text',
'keyComment' => 'text255',
'isTrusted' => 'bool',
'isActive' => 'bool?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_object' => array(
'columns' => array('objectPHID'),
),
'key_active' => array(
'columns' => array('isActive', 'objectPHID'),
),
// NOTE: This unique key includes a nullable column, effectively
// constraining uniqueness on active keys only.
'key_activeunique' => array(
'columns' => array('keyIndex', 'isActive'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function save() {
$this->setKeyIndex($this->toPublicKey()->getHash());
return parent::save();
}
public function toPublicKey() {
return PhabricatorAuthSSHPublicKey::newFromStoredKey($this);
}
public function getEntireKey() {
$parts = array(
$this->getKeyType(),
$this->getKeyBody(),
$this->getKeyComment(),
);
return trim(implode(' ', $parts));
}
public function getObject() {
return $this->assertAttached($this->object);
}
public function attachObject(PhabricatorSSHPublicKeyInterface $object) {
$this->object = $object;
return $this;
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorAuthSSHKeyPHIDType::TYPECONST);
}
public function getURI() {
$id = $this->getID();
return "/auth/sshkey/view/{$id}/";
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
if (!$this->getIsActive()) {
if ($capability == PhabricatorPolicyCapability::CAN_EDIT) {
return PhabricatorPolicies::POLICY_NOONE;
}
}
return $this->getObject()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if (!$this->getIsActive()) {
return false;
}
return $this->getObject()->hasAutomaticCapability($capability, $viewer);
}
public function describeAutomaticCapability($capability) {
if (!$this->getIsACtive()) {
return pht(
'Revoked SSH keys can not be edited or reinstated.');
}
return pht(
'SSH keys inherit the policies of the user or object they authenticate.');
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorAuthSSHKeyEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorAuthSSHKeyTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
- return $timeline;
- }
-
}
diff --git a/src/applications/auth/storage/PhabricatorAuthSession.php b/src/applications/auth/storage/PhabricatorAuthSession.php
index 6d54dda78..e007272f7 100644
--- a/src/applications/auth/storage/PhabricatorAuthSession.php
+++ b/src/applications/auth/storage/PhabricatorAuthSession.php
@@ -1,138 +1,142 @@
<?php
final class PhabricatorAuthSession extends PhabricatorAuthDAO
implements PhabricatorPolicyInterface {
const TYPE_WEB = 'web';
const TYPE_CONDUIT = 'conduit';
const SESSION_DIGEST_KEY = 'session.digest';
protected $userPHID;
protected $type;
protected $sessionKey;
protected $sessionStart;
protected $sessionExpires;
protected $highSecurityUntil;
protected $isPartial;
protected $signedLegalpadDocuments;
private $identityObject = self::ATTACHABLE;
public static function newSessionDigest(PhutilOpaqueEnvelope $session_token) {
return PhabricatorHash::digestWithNamedKey(
$session_token->openEnvelope(),
self::SESSION_DIGEST_KEY);
}
protected function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'type' => 'text32',
'sessionKey' => 'text64',
'sessionStart' => 'epoch',
'sessionExpires' => 'epoch',
'highSecurityUntil' => 'epoch?',
'isPartial' => 'bool',
'signedLegalpadDocuments' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'sessionKey' => array(
'columns' => array('sessionKey'),
'unique' => true,
),
'key_identity' => array(
'columns' => array('userPHID', 'type'),
),
'key_expires' => array(
'columns' => array('sessionExpires'),
),
),
) + parent::getConfiguration();
}
public function getApplicationName() {
// This table predates the "Auth" application, and really all applications.
return 'user';
}
public function getTableName() {
// This is a very old table with a nonstandard name.
return PhabricatorUser::SESSION_TABLE;
}
public function attachIdentityObject($identity_object) {
$this->identityObject = $identity_object;
return $this;
}
public function getIdentityObject() {
return $this->assertAttached($this->identityObject);
}
- public static function getSessionTypeTTL($session_type) {
+ public static function getSessionTypeTTL($session_type, $is_partial) {
switch ($session_type) {
case self::TYPE_WEB:
- return phutil_units('30 days in seconds');
+ if ($is_partial) {
+ return phutil_units('30 minutes in seconds');
+ } else {
+ return phutil_units('30 days in seconds');
+ }
case self::TYPE_CONDUIT:
return phutil_units('24 hours in seconds');
default:
throw new Exception(pht('Unknown session type "%s".', $session_type));
}
}
public function getPHIDType() {
return PhabricatorAuthSessionPHIDType::TYPECONST;
}
public function isHighSecuritySession() {
$until = $this->getHighSecurityUntil();
if (!$until) {
return false;
}
$now = PhabricatorTime::getNow();
if ($until < $now) {
return false;
}
return true;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
return PhabricatorPolicies::POLICY_NOONE;
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if (!$viewer->getPHID()) {
return false;
}
$object = $this->getIdentityObject();
if ($object instanceof PhabricatorUser) {
return ($object->getPHID() == $viewer->getPHID());
} else if ($object instanceof PhabricatorExternalAccount) {
return ($object->getUserPHID() == $viewer->getPHID());
}
return false;
}
public function describeAutomaticCapability($capability) {
return pht('A session is visible only to its owner.');
}
}
diff --git a/src/applications/auth/storage/PhabricatorAuthTemporaryToken.php b/src/applications/auth/storage/PhabricatorAuthTemporaryToken.php
index 76e935883..2b96c7815 100644
--- a/src/applications/auth/storage/PhabricatorAuthTemporaryToken.php
+++ b/src/applications/auth/storage/PhabricatorAuthTemporaryToken.php
@@ -1,128 +1,148 @@
<?php
final class PhabricatorAuthTemporaryToken extends PhabricatorAuthDAO
implements PhabricatorPolicyInterface {
// NOTE: This is usually a PHID, but may be some other kind of resource
// identifier for some token types.
protected $tokenResource;
protected $tokenType;
protected $tokenExpires;
protected $tokenCode;
protected $userPHID;
- protected $properties;
+ protected $properties = array();
+
+ private $isNew = false;
protected function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'tokenResource' => 'phid',
'tokenType' => 'text64',
'tokenExpires' => 'epoch',
'tokenCode' => 'text64',
'userPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_token' => array(
'columns' => array('tokenResource', 'tokenType', 'tokenCode'),
'unique' => true,
),
'key_expires' => array(
'columns' => array('tokenExpires'),
),
'key_user' => array(
'columns' => array('userPHID'),
),
),
) + parent::getConfiguration();
}
private function newTokenTypeImplementation() {
$types = PhabricatorAuthTemporaryTokenType::getAllTypes();
$type = idx($types, $this->tokenType);
if ($type) {
return clone $type;
}
return null;
}
public function getTokenReadableTypeName() {
$type = $this->newTokenTypeImplementation();
if ($type) {
return $type->getTokenReadableTypeName($this);
}
return $this->tokenType;
}
public function isRevocable() {
if ($this->tokenExpires < time()) {
return false;
}
$type = $this->newTokenTypeImplementation();
if ($type) {
return $type->isTokenRevocable($this);
}
return false;
}
public function revokeToken() {
if ($this->isRevocable()) {
$this->setTokenExpires(PhabricatorTime::getNow() - 1)->save();
}
return $this;
}
public static function revokeTokens(
PhabricatorUser $viewer,
array $token_resources,
array $token_types) {
$tokens = id(new PhabricatorAuthTemporaryTokenQuery())
->setViewer($viewer)
->withTokenResources($token_resources)
->withTokenTypes($token_types)
->withExpired(false)
->execute();
foreach ($tokens as $token) {
$token->revokeToken();
}
}
public function getTemporaryTokenProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setTemporaryTokenProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
+ public function setShouldForceFullSession($force_full) {
+ return $this->setTemporaryTokenProperty('force-full-session', $force_full);
+ }
+
+ public function getShouldForceFullSession() {
+ return $this->getTemporaryTokenProperty('force-full-session', false);
+ }
+
+ public function setIsNewTemporaryToken($is_new) {
+ $this->isNew = $is_new;
+ return $this;
+ }
+
+ public function getIsNewTemporaryToken() {
+ return $this->isNew;
+ }
+
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
// We're just implement this interface to get access to the standard
// query infrastructure.
return PhabricatorPolicies::getMostOpenPolicy();
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
}
diff --git a/src/applications/auth/xaction/PhabricatorAuthContactNumberNumberTransaction.php b/src/applications/auth/xaction/PhabricatorAuthContactNumberNumberTransaction.php
new file mode 100644
index 000000000..88d9d4bff
--- /dev/null
+++ b/src/applications/auth/xaction/PhabricatorAuthContactNumberNumberTransaction.php
@@ -0,0 +1,96 @@
+<?php
+
+final class PhabricatorAuthContactNumberNumberTransaction
+ extends PhabricatorAuthContactNumberTransactionType {
+
+ const TRANSACTIONTYPE = 'number';
+
+ public function generateOldValue($object) {
+ return $object->getContactNumber();
+ }
+
+ public function generateNewValue($object, $value) {
+ $number = new PhabricatorPhoneNumber($value);
+ return $number->toE164();
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setContactNumber($value);
+ }
+
+ public function getTitle() {
+ $old = $this->getOldValue();
+ $new = $this->getNewValue();
+
+ return pht(
+ '%s changed this contact number from %s to %s.',
+ $this->renderAuthor(),
+ $this->renderOldValue(),
+ $this->renderNewValue());
+ }
+
+ public function validateTransactions($object, array $xactions) {
+ $errors = array();
+
+ $current_value = $object->getContactNumber();
+ if ($this->isEmptyTextTransaction($current_value, $xactions)) {
+ $errors[] = $this->newRequiredError(
+ pht('Contact numbers must have a contact number.'));
+ return $errors;
+ }
+
+ $max_length = $object->getColumnMaximumByteLength('contactNumber');
+ foreach ($xactions as $xaction) {
+ $new_value = $xaction->getNewValue();
+ $new_length = strlen($new_value);
+ if ($new_length > $max_length) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Contact numbers can not be longer than %s characters.',
+ new PhutilNumber($max_length)),
+ $xaction);
+ continue;
+ }
+
+ try {
+ new PhabricatorPhoneNumber($new_value);
+ } catch (Exception $ex) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Contact number is invalid: %s',
+ $ex->getMessage()),
+ $xaction);
+ continue;
+ }
+
+ $new_value = $this->generateNewValue($object, $new_value);
+
+ $unique_key = id(clone $object)
+ ->setContactNumber($new_value)
+ ->newUniqueKey();
+
+ $other = id(new PhabricatorAuthContactNumberQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->withUniqueKeys(array($unique_key))
+ ->executeOne();
+
+ if ($other) {
+ if ($other->getID() !== $object->getID()) {
+ $errors[] = $this->newInvalidError(
+ pht('Contact number is already in use.'),
+ $xaction);
+ continue;
+ }
+ }
+
+ $mfa_error = $this->newContactNumberMFAError($object, $xaction);
+ if ($mfa_error) {
+ $errors[] = $mfa_error;
+ continue;
+ }
+ }
+
+ return $errors;
+ }
+
+}
diff --git a/src/applications/auth/xaction/PhabricatorAuthContactNumberPrimaryTransaction.php b/src/applications/auth/xaction/PhabricatorAuthContactNumberPrimaryTransaction.php
new file mode 100644
index 000000000..42788029b
--- /dev/null
+++ b/src/applications/auth/xaction/PhabricatorAuthContactNumberPrimaryTransaction.php
@@ -0,0 +1,55 @@
+<?php
+
+final class PhabricatorAuthContactNumberPrimaryTransaction
+ extends PhabricatorAuthContactNumberTransactionType {
+
+ const TRANSACTIONTYPE = 'primary';
+
+ public function generateOldValue($object) {
+ return (bool)$object->getIsPrimary();
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setIsPrimary((int)$value);
+ }
+
+ public function getTitle() {
+ return pht(
+ '%s made this the primary contact number.',
+ $this->renderAuthor());
+ }
+
+ public function validateTransactions($object, array $xactions) {
+ $errors = array();
+
+ foreach ($xactions as $xaction) {
+ $new_value = $xaction->getNewValue();
+
+ if (!$new_value) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'To choose a different primary contact number, make that '.
+ 'number primary (instead of trying to demote this one).'),
+ $xaction);
+ continue;
+ }
+
+ if ($object->isDisabled()) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'You can not make a disabled number a primary contact number.'),
+ $xaction);
+ continue;
+ }
+
+ $mfa_error = $this->newContactNumberMFAError($object, $xaction);
+ if ($mfa_error) {
+ $errors[] = $mfa_error;
+ continue;
+ }
+ }
+
+ return $errors;
+ }
+
+}
diff --git a/src/applications/auth/xaction/PhabricatorAuthContactNumberStatusTransaction.php b/src/applications/auth/xaction/PhabricatorAuthContactNumberStatusTransaction.php
new file mode 100644
index 000000000..5dab6fe8c
--- /dev/null
+++ b/src/applications/auth/xaction/PhabricatorAuthContactNumberStatusTransaction.php
@@ -0,0 +1,65 @@
+<?php
+
+final class PhabricatorAuthContactNumberStatusTransaction
+ extends PhabricatorAuthContactNumberTransactionType {
+
+ const TRANSACTIONTYPE = 'status';
+
+ public function generateOldValue($object) {
+ return $object->getStatus();
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setStatus($value);
+ }
+
+ public function getTitle() {
+ $new = $this->getNewValue();
+
+ if ($new === PhabricatorAuthContactNumber::STATUS_DISABLED) {
+ return pht(
+ '%s disabled this contact number.',
+ $this->renderAuthor());
+ } else {
+ return pht(
+ '%s enabled this contact number.',
+ $this->renderAuthor());
+ }
+ }
+
+ public function validateTransactions($object, array $xactions) {
+ $errors = array();
+
+ $map = PhabricatorAuthContactNumber::getStatusNameMap();
+
+ foreach ($xactions as $xaction) {
+ $new_value = $xaction->getNewValue();
+
+ if (!isset($map[$new_value])) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Status ("%s") is not a valid contact number status. Valid '.
+ 'status constants are: %s.',
+ $new_value,
+ implode(', ', array_keys($map))),
+ $xaction);
+ continue;
+ }
+
+ $mfa_error = $this->newContactNumberMFAError($object, $xaction);
+ if ($mfa_error) {
+ $errors[] = $mfa_error;
+ continue;
+ }
+
+ // NOTE: Enabling a contact number may cause us to collide with another
+ // active contact number. However, there might also be a transaction in
+ // this group that changes the number itself. Since we can't easily
+ // predict if we'll collide or not, just let the duplicate key logic
+ // handle it when we do.
+ }
+
+ return $errors;
+ }
+
+}
diff --git a/src/applications/auth/xaction/PhabricatorAuthContactNumberTransactionType.php b/src/applications/auth/xaction/PhabricatorAuthContactNumberTransactionType.php
new file mode 100644
index 000000000..a74c78d4c
--- /dev/null
+++ b/src/applications/auth/xaction/PhabricatorAuthContactNumberTransactionType.php
@@ -0,0 +1,72 @@
+<?php
+
+abstract class PhabricatorAuthContactNumberTransactionType
+ extends PhabricatorModularTransactionType {
+
+ protected function newContactNumberMFAError($object, $xaction) {
+ // If a contact number is attached to a user and that user has SMS MFA
+ // configured, don't let the user modify their primary contact number or
+ // make another contact number into their primary number.
+
+ $primary_type =
+ PhabricatorAuthContactNumberPrimaryTransaction::TRANSACTIONTYPE;
+
+ if ($xaction->getTransactionType() === $primary_type) {
+ // We're trying to make a non-primary number into the primary number,
+ // so do MFA checks.
+ $is_primary = false;
+ } else if ($object->getIsPrimary()) {
+ // We're editing the primary number, so do MFA checks.
+ $is_primary = true;
+ } else {
+ // Editing a non-primary number and not making it primary, so this is
+ // fine.
+ return null;
+ }
+
+ $target_phid = $object->getObjectPHID();
+ $omnipotent = PhabricatorUser::getOmnipotentUser();
+
+ $user_configs = id(new PhabricatorAuthFactorConfigQuery())
+ ->setViewer($omnipotent)
+ ->withUserPHIDs(array($target_phid))
+ ->execute();
+
+ $problem_configs = array();
+ foreach ($user_configs as $config) {
+ $provider = $config->getFactorProvider();
+ $factor = $provider->getFactor();
+
+ if ($factor->isContactNumberFactor()) {
+ $problem_configs[] = $config;
+ }
+ }
+
+ if (!$problem_configs) {
+ return null;
+ }
+
+ $problem_config = head($problem_configs);
+
+ if ($is_primary) {
+ return $this->newInvalidError(
+ pht(
+ 'You currently have multi-factor authentication ("%s") which '.
+ 'depends on your primary contact number. You must remove this '.
+ 'authentication factor before you can modify or disable your '.
+ 'primary contact number.',
+ $problem_config->getFactorName()),
+ $xaction);
+ } else {
+ return $this->newInvalidError(
+ pht(
+ 'You currently have multi-factor authentication ("%s") which '.
+ 'depends on your primary contact number. You must remove this '.
+ 'authentication factor before you can designate a new primary '.
+ 'contact number.',
+ $problem_config->getFactorName()),
+ $xaction);
+ }
+ }
+
+}
diff --git a/src/applications/auth/xaction/PhabricatorAuthFactorProviderDuoCredentialTransaction.php b/src/applications/auth/xaction/PhabricatorAuthFactorProviderDuoCredentialTransaction.php
new file mode 100644
index 000000000..f5a52cb90
--- /dev/null
+++ b/src/applications/auth/xaction/PhabricatorAuthFactorProviderDuoCredentialTransaction.php
@@ -0,0 +1,69 @@
+<?php
+
+final class PhabricatorAuthFactorProviderDuoCredentialTransaction
+ extends PhabricatorAuthFactorProviderTransactionType {
+
+ const TRANSACTIONTYPE = 'duo.credential';
+
+ public function generateOldValue($object) {
+ $key = PhabricatorDuoAuthFactor::PROP_CREDENTIAL;
+ return $object->getAuthFactorProviderProperty($key);
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $key = PhabricatorDuoAuthFactor::PROP_CREDENTIAL;
+ $object->setAuthFactorProviderProperty($key, $value);
+ }
+
+ public function getTitle() {
+ return pht(
+ '%s changed the credential for this provider from %s to %s.',
+ $this->renderAuthor(),
+ $this->renderOldHandle(),
+ $this->renderNewHandle());
+ }
+
+ public function validateTransactions($object, array $xactions) {
+ $actor = $this->getActor();
+ $errors = array();
+
+ if (!$this->isDuoProvider($object)) {
+ return $errors;
+ }
+
+ $old_value = $this->generateOldValue($object);
+ if ($this->isEmptyTextTransaction($old_value, $xactions)) {
+ $errors[] = $this->newRequiredError(
+ pht('Duo providers must have an API credential.'));
+ }
+
+ foreach ($xactions as $xaction) {
+ $new_value = $xaction->getNewValue();
+
+ if (!strlen($new_value)) {
+ continue;
+ }
+
+ if ($new_value === $old_value) {
+ continue;
+ }
+
+ $credential = id(new PassphraseCredentialQuery())
+ ->setViewer($actor)
+ ->withIsDestroyed(false)
+ ->withPHIDs(array($new_value))
+ ->executeOne();
+ if (!$credential) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Credential ("%s") is not valid.',
+ $new_value),
+ $xaction);
+ continue;
+ }
+ }
+
+ return $errors;
+ }
+
+}
diff --git a/src/applications/auth/xaction/PhabricatorAuthFactorProviderDuoEnrollTransaction.php b/src/applications/auth/xaction/PhabricatorAuthFactorProviderDuoEnrollTransaction.php
new file mode 100644
index 000000000..e1823274b
--- /dev/null
+++ b/src/applications/auth/xaction/PhabricatorAuthFactorProviderDuoEnrollTransaction.php
@@ -0,0 +1,26 @@
+<?php
+
+final class PhabricatorAuthFactorProviderDuoEnrollTransaction
+ extends PhabricatorAuthFactorProviderTransactionType {
+
+ const TRANSACTIONTYPE = 'duo.enroll';
+
+ public function generateOldValue($object) {
+ $key = PhabricatorDuoAuthFactor::PROP_ENROLL;
+ return $object->getAuthFactorProviderProperty($key);
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $key = PhabricatorDuoAuthFactor::PROP_ENROLL;
+ $object->setAuthFactorProviderProperty($key, $value);
+ }
+
+ public function getTitle() {
+ return pht(
+ '%s changed the enrollment policy for this provider from %s to %s.',
+ $this->renderAuthor(),
+ $this->renderOldValue(),
+ $this->renderNewValue());
+ }
+
+}
diff --git a/src/applications/auth/xaction/PhabricatorAuthFactorProviderDuoHostnameTransaction.php b/src/applications/auth/xaction/PhabricatorAuthFactorProviderDuoHostnameTransaction.php
new file mode 100644
index 000000000..27ae27113
--- /dev/null
+++ b/src/applications/auth/xaction/PhabricatorAuthFactorProviderDuoHostnameTransaction.php
@@ -0,0 +1,63 @@
+<?php
+
+final class PhabricatorAuthFactorProviderDuoHostnameTransaction
+ extends PhabricatorAuthFactorProviderTransactionType {
+
+ const TRANSACTIONTYPE = 'duo.hostname';
+
+ public function generateOldValue($object) {
+ $key = PhabricatorDuoAuthFactor::PROP_HOSTNAME;
+ return $object->getAuthFactorProviderProperty($key);
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $key = PhabricatorDuoAuthFactor::PROP_HOSTNAME;
+ $object->setAuthFactorProviderProperty($key, $value);
+ }
+
+ public function getTitle() {
+ return pht(
+ '%s changed the hostname for this provider from %s to %s.',
+ $this->renderAuthor(),
+ $this->renderOldValue(),
+ $this->renderNewValue());
+ }
+
+ public function validateTransactions($object, array $xactions) {
+ $errors = array();
+
+ if (!$this->isDuoProvider($object)) {
+ return $errors;
+ }
+
+ $old_value = $this->generateOldValue($object);
+ if ($this->isEmptyTextTransaction($old_value, $xactions)) {
+ $errors[] = $this->newRequiredError(
+ pht('Duo providers must have an API hostname.'));
+ }
+
+ foreach ($xactions as $xaction) {
+ $new_value = $xaction->getNewValue();
+
+ if (!strlen($new_value)) {
+ continue;
+ }
+
+ if ($new_value === $old_value) {
+ continue;
+ }
+
+ try {
+ PhabricatorDuoAuthFactor::requireDuoAPIHostname($new_value);
+ } catch (Exception $ex) {
+ $errors[] = $this->newInvalidError(
+ $ex->getMessage(),
+ $xaction);
+ continue;
+ }
+ }
+
+ return $errors;
+ }
+
+}
diff --git a/src/applications/auth/xaction/PhabricatorAuthFactorProviderDuoUsernamesTransaction.php b/src/applications/auth/xaction/PhabricatorAuthFactorProviderDuoUsernamesTransaction.php
new file mode 100644
index 000000000..8d9be1244
--- /dev/null
+++ b/src/applications/auth/xaction/PhabricatorAuthFactorProviderDuoUsernamesTransaction.php
@@ -0,0 +1,26 @@
+<?php
+
+final class PhabricatorAuthFactorProviderDuoUsernamesTransaction
+ extends PhabricatorAuthFactorProviderTransactionType {
+
+ const TRANSACTIONTYPE = 'duo.usernames';
+
+ public function generateOldValue($object) {
+ $key = PhabricatorDuoAuthFactor::PROP_USERNAMES;
+ return $object->getAuthFactorProviderProperty($key);
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $key = PhabricatorDuoAuthFactor::PROP_USERNAMES;
+ $object->setAuthFactorProviderProperty($key, $value);
+ }
+
+ public function getTitle() {
+ return pht(
+ '%s changed the username policy for this provider from %s to %s.',
+ $this->renderAuthor(),
+ $this->renderOldValue(),
+ $this->renderNewValue());
+ }
+
+}
diff --git a/src/applications/auth/xaction/PhabricatorAuthFactorProviderEnrollMessageTransaction.php b/src/applications/auth/xaction/PhabricatorAuthFactorProviderEnrollMessageTransaction.php
new file mode 100644
index 000000000..d6d26143c
--- /dev/null
+++ b/src/applications/auth/xaction/PhabricatorAuthFactorProviderEnrollMessageTransaction.php
@@ -0,0 +1,39 @@
+<?php
+
+final class PhabricatorAuthFactorProviderEnrollMessageTransaction
+ extends PhabricatorAuthFactorProviderTransactionType {
+
+ const TRANSACTIONTYPE = 'enroll-message';
+
+ public function generateOldValue($object) {
+ return $object->getEnrollMessage();
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setEnrollMessage($value);
+ }
+
+ public function getTitle() {
+ return pht(
+ '%s updated the enroll message.',
+ $this->renderAuthor());
+ }
+
+ public function hasChangeDetailView() {
+ return true;
+ }
+
+ public function getMailDiffSectionHeader() {
+ return pht('CHANGES TO ENROLL MESSAGE');
+ }
+
+ public function newChangeDetailView() {
+ $viewer = $this->getViewer();
+
+ return id(new PhabricatorApplicationTransactionTextDiffDetailView())
+ ->setViewer($viewer)
+ ->setOldText($this->getOldValue())
+ ->setNewText($this->getNewValue());
+ }
+
+}
diff --git a/src/applications/auth/xaction/PhabricatorAuthFactorProviderNameTransaction.php b/src/applications/auth/xaction/PhabricatorAuthFactorProviderNameTransaction.php
new file mode 100644
index 000000000..9a04d5610
--- /dev/null
+++ b/src/applications/auth/xaction/PhabricatorAuthFactorProviderNameTransaction.php
@@ -0,0 +1,69 @@
+<?php
+
+final class PhabricatorAuthFactorProviderNameTransaction
+ extends PhabricatorAuthFactorProviderTransactionType {
+
+ const TRANSACTIONTYPE = 'name';
+
+ public function generateOldValue($object) {
+ return $object->getName();
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setName($value);
+ }
+
+ public function getTitle() {
+ $old = $this->getOldValue();
+ $new = $this->getNewValue();
+
+ if (!strlen($old)) {
+ return pht(
+ '%s named this provider %s.',
+ $this->renderAuthor(),
+ $this->renderNewValue());
+ } else if (!strlen($new)) {
+ return pht(
+ '%s removed the name (%s) of this provider.',
+ $this->renderAuthor(),
+ $this->renderOldValue());
+ } else {
+ return pht(
+ '%s renamed this provider from %s to %s.',
+ $this->renderAuthor(),
+ $this->renderOldValue(),
+ $this->renderNewValue());
+ }
+ }
+
+ public function validateTransactions($object, array $xactions) {
+ $errors = array();
+
+ $max_length = $object->getColumnMaximumByteLength('name');
+ foreach ($xactions as $xaction) {
+ $new_value = $xaction->getNewValue();
+ $new_length = strlen($new_value);
+ if ($new_length > $max_length) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Provider names can not be longer than %s characters.',
+ new PhutilNumber($max_length)),
+ $xaction);
+ }
+ }
+
+ return $errors;
+ }
+
+ public function getTransactionTypeForConduit($xaction) {
+ return 'name';
+ }
+
+ public function getFieldValuesForConduit($xaction, $data) {
+ return array(
+ 'old' => $xaction->getOldValue(),
+ 'new' => $xaction->getNewValue(),
+ );
+ }
+
+}
diff --git a/src/applications/auth/xaction/PhabricatorAuthFactorProviderStatusTransaction.php b/src/applications/auth/xaction/PhabricatorAuthFactorProviderStatusTransaction.php
new file mode 100644
index 000000000..37674f7b3
--- /dev/null
+++ b/src/applications/auth/xaction/PhabricatorAuthFactorProviderStatusTransaction.php
@@ -0,0 +1,103 @@
+<?php
+
+final class PhabricatorAuthFactorProviderStatusTransaction
+ extends PhabricatorAuthFactorProviderTransactionType {
+
+ const TRANSACTIONTYPE = 'status';
+
+ public function generateOldValue($object) {
+ return $object->getStatus();
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setStatus($value);
+ }
+
+ public function getTitle() {
+ $old = $this->getOldValue();
+ $new = $this->getNewValue();
+
+ $old_display = PhabricatorAuthFactorProviderStatus::newForStatus($old)
+ ->getName();
+ $new_display = PhabricatorAuthFactorProviderStatus::newForStatus($new)
+ ->getName();
+
+ return pht(
+ '%s changed the status of this provider from %s to %s.',
+ $this->renderAuthor(),
+ $this->renderValue($old_display),
+ $this->renderValue($new_display));
+ }
+
+ public function validateTransactions($object, array $xactions) {
+ $errors = array();
+ $actor = $this->getActor();
+
+ $map = PhabricatorAuthFactorProviderStatus::getMap();
+ foreach ($xactions as $xaction) {
+ $new_value = $xaction->getNewValue();
+
+ if (!isset($map[$new_value])) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Status "%s" is invalid. Valid statuses are: %s.',
+ $new_value,
+ implode(', ', array_keys($map))),
+ $xaction);
+ continue;
+ }
+
+ $require_key = 'security.require-multi-factor-auth';
+ $require_mfa = PhabricatorEnv::getEnvConfig($require_key);
+
+ if ($require_mfa) {
+ $status_active = PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE;
+ if ($new_value !== $status_active) {
+ $active_providers = id(new PhabricatorAuthFactorProviderQuery())
+ ->setViewer($actor)
+ ->withStatuses(
+ array(
+ $status_active,
+ ))
+ ->execute();
+ $active_providers = mpull($active_providers, null, 'getID');
+ unset($active_providers[$object->getID()]);
+
+ if (!$active_providers) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'You can not deprecate or disable the last active MFA '.
+ 'provider while "%s" is enabled, because new users would '.
+ 'be unable to enroll in MFA. Disable the MFA requirement '.
+ 'in Config, or create or enable another MFA provider first.',
+ $require_key));
+ continue;
+ }
+ }
+ }
+ }
+
+ return $errors;
+ }
+
+ public function didCommitTransaction($object, $value) {
+ $status = PhabricatorAuthFactorProviderStatus::newForStatus($value);
+
+ // If a provider has undergone a status change, reset the MFA enrollment
+ // cache for all users. This may immediately force a lot of users to redo
+ // MFA enrollment.
+
+ // We could be more surgical about this: we only really need to affect
+ // users who had a factor under the provider, and only really need to
+ // do anything if a provider was disabled. This is just a little simpler.
+
+ $table = new PhabricatorUser();
+ $conn = $table->establishConnection('w');
+
+ queryfx(
+ $conn,
+ 'UPDATE %R SET isEnrolledInMultiFactor = 0',
+ $table);
+ }
+
+}
diff --git a/src/applications/auth/xaction/PhabricatorAuthFactorProviderTransactionType.php b/src/applications/auth/xaction/PhabricatorAuthFactorProviderTransactionType.php
new file mode 100644
index 000000000..3f16249e0
--- /dev/null
+++ b/src/applications/auth/xaction/PhabricatorAuthFactorProviderTransactionType.php
@@ -0,0 +1,12 @@
+<?php
+
+abstract class PhabricatorAuthFactorProviderTransactionType
+ extends PhabricatorModularTransactionType {
+
+ final protected function isDuoProvider(
+ PhabricatorAuthFactorProvider $provider) {
+ $duo_key = id(new PhabricatorDuoAuthFactor())->getFactorKey();
+ return ($provider->getProviderFactorKey() === $duo_key);
+ }
+
+}
diff --git a/src/applications/auth/xaction/PhabricatorAuthMessageTextTransaction.php b/src/applications/auth/xaction/PhabricatorAuthMessageTextTransaction.php
new file mode 100644
index 000000000..0a35c5f38
--- /dev/null
+++ b/src/applications/auth/xaction/PhabricatorAuthMessageTextTransaction.php
@@ -0,0 +1,39 @@
+<?php
+
+final class PhabricatorAuthMessageTextTransaction
+ extends PhabricatorAuthMessageTransactionType {
+
+ const TRANSACTIONTYPE = 'text';
+
+ public function generateOldValue($object) {
+ return $object->getMessageText();
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setMessageText($value);
+ }
+
+ public function getTitle() {
+ return pht(
+ '%s updated the message text.',
+ $this->renderAuthor());
+ }
+
+ public function hasChangeDetailView() {
+ return true;
+ }
+
+ public function getMailDiffSectionHeader() {
+ return pht('CHANGES TO MESSAGE');
+ }
+
+ public function newChangeDetailView() {
+ $viewer = $this->getViewer();
+
+ return id(new PhabricatorApplicationTransactionTextDiffDetailView())
+ ->setViewer($viewer)
+ ->setOldText($this->getOldValue())
+ ->setNewText($this->getNewValue());
+ }
+
+}
diff --git a/src/applications/auth/xaction/PhabricatorAuthMessageTransactionType.php b/src/applications/auth/xaction/PhabricatorAuthMessageTransactionType.php
new file mode 100644
index 000000000..eeb1b350f
--- /dev/null
+++ b/src/applications/auth/xaction/PhabricatorAuthMessageTransactionType.php
@@ -0,0 +1,4 @@
+<?php
+
+abstract class PhabricatorAuthMessageTransactionType
+ extends PhabricatorModularTransactionType {}
diff --git a/src/applications/badges/controller/PhabricatorBadgesCommentController.php b/src/applications/badges/controller/PhabricatorBadgesCommentController.php
index a9108fd8d..32e177514 100644
--- a/src/applications/badges/controller/PhabricatorBadgesCommentController.php
+++ b/src/applications/badges/controller/PhabricatorBadgesCommentController.php
@@ -1,63 +1,64 @@
<?php
final class PhabricatorBadgesCommentController
extends PhabricatorBadgesController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
if (!$request->isFormPost()) {
return new Aphront400Response();
}
$badge = id(new PhabricatorBadgesQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$badge) {
return new Aphront404Response();
}
$is_preview = $request->isPreviewRequest();
$draft = PhabricatorDraft::buildFromRequest($request);
$view_uri = $this->getApplicationURI('view/'.$badge->getID());
$xactions = array();
$xactions[] = id(new PhabricatorBadgesTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new PhabricatorBadgesTransactionComment())
->setContent($request->getStr('comment')));
$editor = id(new PhabricatorBadgesEditor())
->setActor($viewer)
->setContinueOnNoEffect($request->isContinueRequest())
->setContentSourceFromRequest($request)
->setIsPreview($is_preview);
try {
$xactions = $editor->applyTransactions($badge, $xactions);
} catch (PhabricatorApplicationTransactionNoEffectException $ex) {
return id(new PhabricatorApplicationTransactionNoEffectResponse())
->setCancelURI($view_uri)
->setException($ex);
}
if ($draft) {
$draft->replaceOrDelete();
}
if ($request->isAjax() && $is_preview) {
return id(new PhabricatorApplicationTransactionResponse())
+ ->setObject($badge)
->setViewer($viewer)
->setTransactions($xactions)
->setIsPreview($is_preview);
} else {
return id(new AphrontRedirectResponse())
->setURI($view_uri);
}
}
}
diff --git a/src/applications/badges/storage/PhabricatorBadgesBadge.php b/src/applications/badges/storage/PhabricatorBadgesBadge.php
index e2c63c1d0..2fc787d9e 100644
--- a/src/applications/badges/storage/PhabricatorBadgesBadge.php
+++ b/src/applications/badges/storage/PhabricatorBadgesBadge.php
@@ -1,215 +1,204 @@
<?php
final class PhabricatorBadgesBadge extends PhabricatorBadgesDAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorSubscribableInterface,
PhabricatorFlaggableInterface,
PhabricatorDestructibleInterface,
PhabricatorConduitResultInterface,
PhabricatorNgramsInterface {
protected $name;
protected $flavor;
protected $description;
protected $icon;
protected $quality;
protected $mailKey;
protected $editPolicy;
protected $status;
protected $creatorPHID;
const STATUS_ACTIVE = 'open';
const STATUS_ARCHIVED = 'closed';
const DEFAULT_ICON = 'fa-star';
public static function getStatusNameMap() {
return array(
self::STATUS_ACTIVE => pht('Active'),
self::STATUS_ARCHIVED => pht('Archived'),
);
}
public static function initializeNewBadge(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorBadgesApplication'))
->executeOne();
$view_policy = PhabricatorPolicies::getMostOpenPolicy();
$edit_policy =
$app->getPolicy(PhabricatorBadgesDefaultEditCapability::CAPABILITY);
return id(new PhabricatorBadgesBadge())
->setIcon(self::DEFAULT_ICON)
->setQuality(PhabricatorBadgesQuality::DEFAULT_QUALITY)
->setCreatorPHID($actor->getPHID())
->setEditPolicy($edit_policy)
->setFlavor('')
->setDescription('')
->setStatus(self::STATUS_ACTIVE);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort255',
'flavor' => 'text255',
'description' => 'text',
'icon' => 'text255',
'quality' => 'uint32',
'status' => 'text32',
'mailKey' => 'bytes20',
),
self::CONFIG_KEY_SCHEMA => array(
'key_creator' => array(
'columns' => array('creatorPHID', 'dateModified'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return
PhabricatorPHID::generateNewPHID(PhabricatorBadgesPHIDType::TYPECONST);
}
public function isArchived() {
return ($this->getStatus() == self::STATUS_ARCHIVED);
}
public function getViewURI() {
return '/badges/view/'.$this->getID().'/';
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::getMostOpenPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorBadgesEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorBadgesTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return false;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$awards = id(new PhabricatorBadgesAwardQuery())
->setViewer($engine->getViewer())
->withBadgePHIDs(array($this->getPHID()))
->execute();
foreach ($awards as $award) {
$engine->destroyObject($award);
}
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of the badge.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('creatorPHID')
->setType('phid')
->setDescription(pht('User PHID of the creator.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('status')
->setType('string')
->setDescription(pht('Active or archived status of the badge.')),
);
}
public function getFieldValuesForConduit() {
return array(
'name' => $this->getName(),
'creatorPHID' => $this->getCreatorPHID(),
'status' => $this->getStatus(),
);
}
public function getConduitSearchAttachments() {
return array();
}
/* -( PhabricatorNgramInterface )------------------------------------------ */
public function newNgrams() {
return array(
id(new PhabricatorBadgesBadgeNameNgrams())
->setValue($this->getName()),
);
}
}
diff --git a/src/applications/base/PhabricatorApplication.php b/src/applications/base/PhabricatorApplication.php
index 1cabeb070..9526fb144 100644
--- a/src/applications/base/PhabricatorApplication.php
+++ b/src/applications/base/PhabricatorApplication.php
@@ -1,666 +1,656 @@
<?php
/**
* @task info Application Information
* @task ui UI Integration
* @task uri URI Routing
* @task mail Email integration
* @task fact Fact Integration
* @task meta Application Management
*/
abstract class PhabricatorApplication
extends PhabricatorLiskDAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface {
const GROUP_CORE = 'core';
const GROUP_UTILITIES = 'util';
const GROUP_ADMIN = 'admin';
const GROUP_DEVELOPER = 'developer';
final public static function getApplicationGroups() {
return array(
self::GROUP_CORE => pht('Core Applications'),
self::GROUP_UTILITIES => pht('Utilities'),
self::GROUP_ADMIN => pht('Administration'),
self::GROUP_DEVELOPER => pht('Developer Tools'),
);
}
final public function getApplicationName() {
return 'application';
}
final public function getTableName() {
return 'application_application';
}
final protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
) + parent::getConfiguration();
}
final public function generatePHID() {
return $this->getPHID();
}
final public function save() {
// When "save()" is called on applications, we just return without
// actually writing anything to the database.
return $this;
}
/* -( Application Information )-------------------------------------------- */
abstract public function getName();
public function getShortDescription() {
return pht('%s Application', $this->getName());
}
final public function isInstalled() {
if (!$this->canUninstall()) {
return true;
}
$prototypes = PhabricatorEnv::getEnvConfig('phabricator.show-prototypes');
if (!$prototypes && $this->isPrototype()) {
return false;
}
$uninstalled = PhabricatorEnv::getEnvConfig(
'phabricator.uninstalled-applications');
return empty($uninstalled[get_class($this)]);
}
public function isPrototype() {
return false;
}
/**
* Return `true` if this application should never appear in application lists
* in the UI. Primarily intended for unit test applications or other
* pseudo-applications.
*
* Few applications should be unlisted. For most applications, use
* @{method:isLaunchable} to hide them from main launch views instead.
*
* @return bool True to remove application from UI lists.
*/
public function isUnlisted() {
return false;
}
/**
* Return `true` if this application is a normal application with a base
* URI and a web interface.
*
* Launchable applications can be pinned to the home page, and show up in the
* "Launcher" view of the Applications application. Making an application
* unlaunchable prevents pinning and hides it from this view.
*
* Usually, an application should be marked unlaunchable if:
*
* - it is available on every page anyway (like search); or
* - it does not have a web interface (like subscriptions); or
* - it is still pre-release and being intentionally buried.
*
* To hide applications more completely, use @{method:isUnlisted}.
*
* @return bool True if the application is launchable.
*/
public function isLaunchable() {
return true;
}
/**
* Return `true` if this application should be pinned by default.
*
* Users who have not yet set preferences see a default list of applications.
*
* @param PhabricatorUser User viewing the pinned application list.
* @return bool True if this application should be pinned by default.
*/
public function isPinnedByDefault(PhabricatorUser $viewer) {
return false;
}
/**
* Returns true if an application is first-party (developed by Phacility)
* and false otherwise.
*
* @return bool True if this application is developed by Phacility.
*/
final public function isFirstParty() {
$where = id(new ReflectionClass($this))->getFileName();
$root = phutil_get_library_root('phabricator');
if (!Filesystem::isDescendant($where, $root)) {
return false;
}
if (Filesystem::isDescendant($where, $root.'/extensions')) {
return false;
}
return true;
}
public function canUninstall() {
return true;
}
final public function getPHID() {
return 'PHID-APPS-'.get_class($this);
}
public function getTypeaheadURI() {
return $this->isLaunchable() ? $this->getBaseURI() : null;
}
public function getBaseURI() {
return null;
}
final public function getApplicationURI($path = '') {
return $this->getBaseURI().ltrim($path, '/');
}
public function getIcon() {
return 'fa-puzzle-piece';
}
public function getApplicationOrder() {
return PHP_INT_MAX;
}
public function getApplicationGroup() {
return self::GROUP_CORE;
}
public function getTitleGlyph() {
return null;
}
final public function getHelpMenuItems(PhabricatorUser $viewer) {
$items = array();
$articles = $this->getHelpDocumentationArticles($viewer);
if ($articles) {
foreach ($articles as $article) {
$item = id(new PhabricatorActionView())
->setName($article['name'])
->setHref($article['href'])
->addSigil('help-item')
->setOpenInNewWindow(true);
$items[] = $item;
}
}
$command_specs = $this->getMailCommandObjects();
if ($command_specs) {
foreach ($command_specs as $key => $spec) {
$object = $spec['object'];
$class = get_class($this);
$href = '/applications/mailcommands/'.$class.'/'.$key.'/';
$item = id(new PhabricatorActionView())
->setName($spec['name'])
->setHref($href)
->addSigil('help-item')
->setOpenInNewWindow(true);
$items[] = $item;
}
}
if ($items) {
$divider = id(new PhabricatorActionView())
->addSigil('help-item')
->setType(PhabricatorActionView::TYPE_DIVIDER);
array_unshift($items, $divider);
}
return array_values($items);
}
public function getHelpDocumentationArticles(PhabricatorUser $viewer) {
return array();
}
public function getOverview() {
return null;
}
public function getEventListeners() {
return array();
}
public function getRemarkupRules() {
return array();
}
public function getQuicksandURIPatternBlacklist() {
return array();
}
public function getMailCommandObjects() {
return array();
}
/* -( URI Routing )-------------------------------------------------------- */
public function getRoutes() {
return array();
}
public function getResourceRoutes() {
return array();
}
/* -( Email Integration )-------------------------------------------------- */
public function supportsEmailIntegration() {
return false;
}
final protected function getInboundEmailSupportLink() {
return PhabricatorEnv::getDoclink('Configuring Inbound Email');
}
public function getAppEmailBlurb() {
throw new PhutilMethodNotImplementedException();
}
/* -( Fact Integration )--------------------------------------------------- */
public function getFactObjectsForAnalysis() {
return array();
}
/* -( UI Integration )----------------------------------------------------- */
/**
* You can provide an optional piece of flavor text for the application. This
* is currently rendered in application launch views if the application has no
* status elements.
*
* @return string|null Flavor text.
* @task ui
*/
public function getFlavorText() {
return null;
}
/**
* Build items for the main menu.
*
* @param PhabricatorUser The viewing user.
* @param AphrontController The current controller. May be null for special
* pages like 404, exception handlers, etc.
* @return list<PHUIListItemView> List of menu items.
* @task ui
*/
public function buildMainMenuItems(
PhabricatorUser $user,
PhabricatorController $controller = null) {
return array();
}
/* -( Application Management )--------------------------------------------- */
final public static function getByClass($class_name) {
$selected = null;
$applications = self::getAllApplications();
foreach ($applications as $application) {
if (get_class($application) == $class_name) {
$selected = $application;
break;
}
}
if (!$selected) {
throw new Exception(pht("No application '%s'!", $class_name));
}
return $selected;
}
final public static function getAllApplications() {
static $applications;
if ($applications === null) {
$apps = id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setSortMethod('getApplicationOrder')
->execute();
// Reorder the applications into "application order". Notably, this
// ensures their event handlers register in application order.
$apps = mgroup($apps, 'getApplicationGroup');
$group_order = array_keys(self::getApplicationGroups());
$apps = array_select_keys($apps, $group_order) + $apps;
$apps = array_mergev($apps);
$applications = $apps;
}
return $applications;
}
final public static function getAllInstalledApplications() {
$all_applications = self::getAllApplications();
$apps = array();
foreach ($all_applications as $app) {
if (!$app->isInstalled()) {
continue;
}
$apps[] = $app;
}
return $apps;
}
/**
* Determine if an application is installed, by application class name.
*
* To check if an application is installed //and// available to a particular
* viewer, user @{method:isClassInstalledForViewer}.
*
* @param string Application class name.
* @return bool True if the class is installed.
* @task meta
*/
final public static function isClassInstalled($class) {
return self::getByClass($class)->isInstalled();
}
/**
* Determine if an application is installed and available to a viewer, by
* application class name.
*
* To check if an application is installed at all, use
* @{method:isClassInstalled}.
*
* @param string Application class name.
* @param PhabricatorUser Viewing user.
* @return bool True if the class is installed for the viewer.
* @task meta
*/
final public static function isClassInstalledForViewer(
$class,
PhabricatorUser $viewer) {
if ($viewer->isOmnipotent()) {
return true;
}
$cache = PhabricatorCaches::getRequestCache();
$viewer_fragment = $viewer->getCacheFragment();
$key = 'app.'.$class.'.installed.'.$viewer_fragment;
$result = $cache->getKey($key);
if ($result === null) {
if (!self::isClassInstalled($class)) {
$result = false;
} else {
$application = self::getByClass($class);
if (!$application->canUninstall()) {
// If the application can not be uninstalled, always allow viewers
// to see it. In particular, this allows logged-out viewers to see
// Settings and load global default settings even if the install
// does not allow public viewers.
$result = true;
} else {
$result = PhabricatorPolicyFilter::hasCapability(
$viewer,
self::getByClass($class),
PhabricatorPolicyCapability::CAN_VIEW);
}
}
$cache->setKey($key, $result);
}
return $result;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array_merge(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
),
array_keys($this->getCustomCapabilities()));
}
public function getPolicy($capability) {
$default = $this->getCustomPolicySetting($capability);
if ($default) {
return $default;
}
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::getMostOpenPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return PhabricatorPolicies::POLICY_ADMIN;
default:
$spec = $this->getCustomCapabilitySpecification($capability);
return idx($spec, 'default', PhabricatorPolicies::POLICY_USER);
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( Policies )----------------------------------------------------------- */
protected function getCustomCapabilities() {
return array();
}
final private function getCustomPolicySetting($capability) {
if (!$this->isCapabilityEditable($capability)) {
return null;
}
$policy_locked = PhabricatorEnv::getEnvConfig('policy.locked');
if (isset($policy_locked[$capability])) {
return $policy_locked[$capability];
}
$config = PhabricatorEnv::getEnvConfig('phabricator.application-settings');
$app = idx($config, $this->getPHID());
if (!$app) {
return null;
}
$policy = idx($app, 'policy');
if (!$policy) {
return null;
}
return idx($policy, $capability);
}
final private function getCustomCapabilitySpecification($capability) {
$custom = $this->getCustomCapabilities();
if (!isset($custom[$capability])) {
throw new Exception(pht("Unknown capability '%s'!", $capability));
}
return $custom[$capability];
}
final public function getCapabilityLabel($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return pht('Can Use Application');
case PhabricatorPolicyCapability::CAN_EDIT:
return pht('Can Configure Application');
}
$capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability);
if ($capobj) {
return $capobj->getCapabilityName();
}
return null;
}
final public function isCapabilityEditable($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->canUninstall();
case PhabricatorPolicyCapability::CAN_EDIT:
return true;
default:
$spec = $this->getCustomCapabilitySpecification($capability);
return idx($spec, 'edit', true);
}
}
final public function getCapabilityCaption($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
if (!$this->canUninstall()) {
return pht(
'This application is required for Phabricator to operate, so all '.
'users must have access to it.');
} else {
return null;
}
case PhabricatorPolicyCapability::CAN_EDIT:
return null;
default:
$spec = $this->getCustomCapabilitySpecification($capability);
return idx($spec, 'caption');
}
}
final public function getCapabilityTemplatePHIDType($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
case PhabricatorPolicyCapability::CAN_EDIT:
return null;
}
$spec = $this->getCustomCapabilitySpecification($capability);
return idx($spec, 'template');
}
final public function getDefaultObjectTypePolicyMap() {
$map = array();
foreach ($this->getCustomCapabilities() as $capability => $spec) {
if (empty($spec['template'])) {
continue;
}
if (empty($spec['capability'])) {
continue;
}
$default = $this->getPolicy($capability);
$map[$spec['template']][$spec['capability']] = $default;
}
return $map;
}
public function getApplicationSearchDocumentTypes() {
return array();
}
protected function getEditRoutePattern($base = null) {
return $base.'(?:'.
'(?P<id>[0-9]\d*)/)?'.
'(?:'.
'(?:'.
'(?P<editAction>parameters|nodefault|nocreate|nomanage|comment)/'.
'|'.
'(?:form/(?P<formKey>[^/]+)/)?(?:page/(?P<pageKey>[^/]+)/)?'.
')'.
')?';
}
protected function getBulkRoutePattern($base = null) {
return $base.'(?:query/(?P<queryKey>[^/]+)/)?';
}
protected function getQueryRoutePattern($base = null) {
return $base.'(?:query/(?P<queryKey>[^/]+)/(?:(?P<queryAction>[^/]+)/)?)?';
}
protected function getProfileMenuRouting($controller) {
$edit_route = $this->getEditRoutePattern();
$mode_route = '(?P<itemEditMode>global|custom)/';
return array(
'(?P<itemAction>view)/(?P<itemID>[^/]+)/' => $controller,
'(?P<itemAction>hide)/(?P<itemID>[^/]+)/' => $controller,
'(?P<itemAction>default)/(?P<itemID>[^/]+)/' => $controller,
'(?P<itemAction>configure)/' => $controller,
'(?P<itemAction>configure)/'.$mode_route => $controller,
'(?P<itemAction>reorder)/'.$mode_route => $controller,
'(?P<itemAction>edit)/'.$edit_route => $controller,
'(?P<itemAction>new)/'.$mode_route.'(?<itemKey>[^/]+)/'.$edit_route
=> $controller,
'(?P<itemAction>builtin)/(?<itemID>[^/]+)/'.$edit_route
=> $controller,
);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorApplicationEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorApplicationApplicationTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
}
diff --git a/src/applications/base/controller/PhabricatorController.php b/src/applications/base/controller/PhabricatorController.php
index df0c94c13..59df22a8a 100644
--- a/src/applications/base/controller/PhabricatorController.php
+++ b/src/applications/base/controller/PhabricatorController.php
@@ -1,632 +1,635 @@
<?php
abstract class PhabricatorController extends AphrontController {
private $handles;
public function shouldRequireLogin() {
return true;
}
public function shouldRequireAdmin() {
return false;
}
public function shouldRequireEnabledUser() {
return true;
}
public function shouldAllowPublic() {
return false;
}
public function shouldAllowPartialSessions() {
return false;
}
public function shouldRequireEmailVerification() {
return PhabricatorUserEmail::isEmailVerificationRequired();
}
public function shouldAllowRestrictedParameter($parameter_name) {
return false;
}
public function shouldRequireMultiFactorEnrollment() {
if (!$this->shouldRequireLogin()) {
return false;
}
if (!$this->shouldRequireEnabledUser()) {
return false;
}
if ($this->shouldAllowPartialSessions()) {
return false;
}
$user = $this->getRequest()->getUser();
if (!$user->getIsStandardUser()) {
return false;
}
return PhabricatorEnv::getEnvConfig('security.require-multi-factor-auth');
}
public function shouldAllowLegallyNonCompliantUsers() {
return false;
}
public function isGlobalDragAndDropUploadEnabled() {
return false;
}
public function willBeginExecution() {
$request = $this->getRequest();
if ($request->getUser()) {
// NOTE: Unit tests can set a user explicitly. Normal requests are not
// permitted to do this.
PhabricatorTestCase::assertExecutingUnitTests();
$user = $request->getUser();
} else {
$user = new PhabricatorUser();
$session_engine = new PhabricatorAuthSessionEngine();
$phsid = $request->getCookie(PhabricatorCookies::COOKIE_SESSION);
if (strlen($phsid)) {
$session_user = $session_engine->loadUserForSession(
PhabricatorAuthSession::TYPE_WEB,
$phsid);
if ($session_user) {
$user = $session_user;
}
} else {
// If the client doesn't have a session token, generate an anonymous
// session. This is used to provide CSRF protection to logged-out users.
$phsid = $session_engine->establishSession(
PhabricatorAuthSession::TYPE_WEB,
null,
$partial = false);
// This may be a resource request, in which case we just don't set
// the cookie.
if ($request->canSetCookies()) {
$request->setCookie(PhabricatorCookies::COOKIE_SESSION, $phsid);
}
}
if (!$user->isLoggedIn()) {
- $user->attachAlternateCSRFString(PhabricatorHash::weakDigest($phsid));
+ $csrf = PhabricatorHash::digestWithNamedKey($phsid, 'csrf.alternate');
+ $user->attachAlternateCSRFString($csrf);
}
$request->setUser($user);
}
id(new PhabricatorAuthSessionEngine())
->willServeRequestForUser($user);
if (PhabricatorEnv::getEnvConfig('darkconsole.enabled')) {
$dark_console = PhabricatorDarkConsoleSetting::SETTINGKEY;
if ($user->getUserSetting($dark_console) ||
PhabricatorEnv::getEnvConfig('darkconsole.always-on')) {
$console = new DarkConsoleCore();
$request->getApplicationConfiguration()->setConsole($console);
}
}
// NOTE: We want to set up the user first so we can render a real page
// here, but fire this before any real logic.
$restricted = array(
'code',
);
foreach ($restricted as $parameter) {
if ($request->getExists($parameter)) {
if (!$this->shouldAllowRestrictedParameter($parameter)) {
throw new Exception(
pht(
'Request includes restricted parameter "%s", but this '.
'controller ("%s") does not whitelist it. Refusing to '.
'serve this request because it might be part of a redirection '.
'attack.',
$parameter,
get_class($this)));
}
}
}
if ($this->shouldRequireEnabledUser()) {
if ($user->getIsDisabled()) {
$controller = new PhabricatorDisabledUserController();
return $this->delegateToController($controller);
}
}
$auth_class = 'PhabricatorAuthApplication';
$auth_application = PhabricatorApplication::getByClass($auth_class);
// Require partial sessions to finish login before doing anything.
if (!$this->shouldAllowPartialSessions()) {
if ($user->hasSession() &&
$user->getSession()->getIsPartial()) {
$login_controller = new PhabricatorAuthFinishController();
$this->setCurrentApplication($auth_application);
return $this->delegateToController($login_controller);
}
}
// Require users sign Legalpad documents before we check if they have
// MFA. If we don't do this, they can get stuck in a state where they
// can't add MFA until they sign, and can't sign until they add MFA.
// See T13024 and PHI223.
$result = $this->requireLegalpadSignatures();
if ($result !== null) {
return $result;
}
// Check if the user needs to configure MFA.
$need_mfa = $this->shouldRequireMultiFactorEnrollment();
$have_mfa = $user->getIsEnrolledInMultiFactor();
if ($need_mfa && !$have_mfa) {
// Check if the cache is just out of date. Otherwise, roadblock the user
// and require MFA enrollment.
$user->updateMultiFactorEnrollment();
if (!$user->getIsEnrolledInMultiFactor()) {
$mfa_controller = new PhabricatorAuthNeedsMultiFactorController();
$this->setCurrentApplication($auth_application);
return $this->delegateToController($mfa_controller);
}
}
if ($this->shouldRequireLogin()) {
// This actually means we need either:
// - a valid user, or a public controller; and
// - permission to see the application; and
// - permission to see at least one Space if spaces are configured.
$allow_public = $this->shouldAllowPublic() &&
PhabricatorEnv::getEnvConfig('policy.allow-public');
// If this controller isn't public, and the user isn't logged in, require
// login.
if (!$allow_public && !$user->isLoggedIn()) {
$login_controller = new PhabricatorAuthStartController();
$this->setCurrentApplication($auth_application);
return $this->delegateToController($login_controller);
}
if ($user->isLoggedIn()) {
if ($this->shouldRequireEmailVerification()) {
if (!$user->getIsEmailVerified()) {
$controller = new PhabricatorMustVerifyEmailController();
$this->setCurrentApplication($auth_application);
return $this->delegateToController($controller);
}
}
}
// If Spaces are configured, require that the user have access to at
// least one. If we don't do this, they'll get confusing error messages
// later on.
$spaces = PhabricatorSpacesNamespaceQuery::getSpacesExist();
if ($spaces) {
$viewer_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces(
$user);
if (!$viewer_spaces) {
$controller = new PhabricatorSpacesNoAccessController();
return $this->delegateToController($controller);
}
}
// If the user doesn't have access to the application, don't let them use
// any of its controllers. We query the application in order to generate
// a policy exception if the viewer doesn't have permission.
$application = $this->getCurrentApplication();
if ($application) {
id(new PhabricatorApplicationQuery())
->setViewer($user)
->withPHIDs(array($application->getPHID()))
->executeOne();
}
// If users need approval, require they wait here. We do this near the
// end so they can take other actions (like verifying email, signing
// documents, and enrolling in MFA) while waiting for an admin to take a
// look at things. See T13024 for more discussion.
if ($this->shouldRequireEnabledUser()) {
if ($user->isLoggedIn() && !$user->getIsApproved()) {
$controller = new PhabricatorAuthNeedsApprovalController();
return $this->delegateToController($controller);
}
}
}
// NOTE: We do this last so that users get a login page instead of a 403
// if they need to login.
if ($this->shouldRequireAdmin() && !$user->getIsAdmin()) {
return new Aphront403Response();
}
}
public function getApplicationURI($path = '') {
if (!$this->getCurrentApplication()) {
throw new Exception(pht('No application!'));
}
return $this->getCurrentApplication()->getApplicationURI($path);
}
public function willSendResponse(AphrontResponse $response) {
$request = $this->getRequest();
if ($response instanceof AphrontDialogResponse) {
if (!$request->isAjax() && !$request->isQuicksand()) {
$dialog = $response->getDialog();
$title = $dialog->getTitle();
$short = $dialog->getShortTitle();
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(coalesce($short, $title));
$page_content = array(
$crumbs,
$response->buildResponseString(),
);
$view = id(new PhabricatorStandardPageView())
->setRequest($request)
->setController($this)
->setDeviceReady(true)
->setTitle($title)
->appendChild($page_content);
$response = id(new AphrontWebpageResponse())
->setContent($view->render())
->setHTTPResponseCode($response->getHTTPResponseCode());
} else {
$response->getDialog()->setIsStandalone(true);
return id(new AphrontAjaxResponse())
->setContent(array(
'dialog' => $response->buildResponseString(),
));
}
} else if ($response instanceof AphrontRedirectResponse) {
if ($request->isAjax() || $request->isQuicksand()) {
return id(new AphrontAjaxResponse())
->setContent(
array(
'redirect' => $response->getURI(),
'close' => $response->getCloseDialogBeforeRedirect(),
));
}
}
return $response;
}
/**
* WARNING: Do not call this in new code.
*
* @deprecated See "Handles Technical Documentation".
*/
protected function loadViewerHandles(array $phids) {
return id(new PhabricatorHandleQuery())
->setViewer($this->getRequest()->getUser())
->withPHIDs($phids)
->execute();
}
public function buildApplicationMenu() {
return null;
}
protected function buildApplicationCrumbs() {
$crumbs = array();
$application = $this->getCurrentApplication();
if ($application) {
$icon = $application->getIcon();
if (!$icon) {
$icon = 'fa-puzzle';
}
$crumbs[] = id(new PHUICrumbView())
->setHref($this->getApplicationURI())
->setName($application->getName())
->setIcon($icon);
}
$view = new PHUICrumbsView();
foreach ($crumbs as $crumb) {
$view->addCrumb($crumb);
}
return $view;
}
protected function hasApplicationCapability($capability) {
return PhabricatorPolicyFilter::hasCapability(
$this->getRequest()->getUser(),
$this->getCurrentApplication(),
$capability);
}
protected function requireApplicationCapability($capability) {
PhabricatorPolicyFilter::requireCapability(
$this->getRequest()->getUser(),
$this->getCurrentApplication(),
$capability);
}
protected function explainApplicationCapability(
$capability,
$positive_message,
$negative_message) {
$can_act = $this->hasApplicationCapability($capability);
if ($can_act) {
$message = $positive_message;
$icon_name = 'fa-play-circle-o lightgreytext';
} else {
$message = $negative_message;
$icon_name = 'fa-lock';
}
$icon = id(new PHUIIconView())
->setIcon($icon_name);
require_celerity_resource('policy-css');
$phid = $this->getCurrentApplication()->getPHID();
$explain_uri = "/policy/explain/{$phid}/{$capability}/";
$message = phutil_tag(
'div',
array(
'class' => 'policy-capability-explanation',
),
array(
$icon,
javelin_tag(
'a',
array(
'href' => $explain_uri,
'sigil' => 'workflow',
),
$message),
));
return array($can_act, $message);
}
public function getDefaultResourceSource() {
return 'phabricator';
}
/**
* Create a new @{class:AphrontDialogView} with defaults filled in.
*
* @return AphrontDialogView New dialog.
*/
public function newDialog() {
$submit_uri = new PhutilURI($this->getRequest()->getRequestURI());
$submit_uri = $submit_uri->getPath();
return id(new AphrontDialogView())
->setUser($this->getRequest()->getUser())
->setSubmitURI($submit_uri);
}
public function newPage() {
$page = id(new PhabricatorStandardPageView())
->setRequest($this->getRequest())
->setController($this)
->setDeviceReady(true);
$application = $this->getCurrentApplication();
if ($application) {
$page->setApplicationName($application->getName());
if ($application->getTitleGlyph()) {
$page->setGlyph($application->getTitleGlyph());
}
}
$viewer = $this->getRequest()->getUser();
if ($viewer) {
$page->setUser($viewer);
}
return $page;
}
public function newApplicationMenu() {
return id(new PHUIApplicationMenuView())
->setViewer($this->getViewer());
}
public function newCurtainView($object = null) {
$viewer = $this->getViewer();
$action_id = celerity_generate_unique_node_id();
$action_list = id(new PhabricatorActionListView())
->setViewer($viewer)
->setID($action_id);
// NOTE: Applications (objects of class PhabricatorApplication) can't
// currently be set here, although they don't need any of the extensions
// anyway. This should probably work differently than it does, though.
if ($object) {
if ($object instanceof PhabricatorLiskDAO) {
$action_list->setObject($object);
}
}
$curtain = id(new PHUICurtainView())
->setViewer($viewer)
->setActionList($action_list);
if ($object) {
$panels = PHUICurtainExtension::buildExtensionPanels($viewer, $object);
foreach ($panels as $panel) {
$curtain->addPanel($panel);
}
}
return $curtain;
}
protected function buildTransactionTimeline(
PhabricatorApplicationTransactionInterface $object,
PhabricatorApplicationTransactionQuery $query,
PhabricatorMarkupEngine $engine = null,
- $render_data = array()) {
+ $view_data = array()) {
- $viewer = $this->getRequest()->getUser();
+ $request = $this->getRequest();
+ $viewer = $this->getViewer();
$xaction = $object->getApplicationTransactionTemplate();
- $view = $xaction->getApplicationTransactionViewObject();
$pager = id(new AphrontCursorPagerView())
- ->readFromRequest($this->getRequest())
+ ->readFromRequest($request)
->setURI(new PhutilURI(
'/transactions/showolder/'.$object->getPHID().'/'));
$xactions = $query
->setViewer($viewer)
->withObjectPHIDs(array($object->getPHID()))
->needComments(true)
->executeWithCursorPager($pager);
$xactions = array_reverse($xactions);
+ $timeline_engine = PhabricatorTimelineEngine::newForObject($object)
+ ->setViewer($viewer)
+ ->setTransactions($xactions)
+ ->setViewData($view_data);
+
+ $view = $timeline_engine->buildTimelineView();
+
if ($engine) {
foreach ($xactions as $xaction) {
if ($xaction->getComment()) {
$engine->addObject(
$xaction->getComment(),
PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT);
}
}
$engine->process();
$view->setMarkupEngine($engine);
}
$timeline = $view
- ->setUser($viewer)
- ->setObjectPHID($object->getPHID())
- ->setTransactions($xactions)
->setPager($pager)
- ->setRenderData($render_data)
->setQuoteTargetID($this->getRequest()->getStr('quoteTargetID'))
->setQuoteRef($this->getRequest()->getStr('quoteRef'));
- $object->willRenderTimeline($timeline, $this->getRequest());
return $timeline;
}
public function buildApplicationCrumbsForEditEngine() {
// TODO: This is kind of gross, I'm basically just making this public so
// I can use it in EditEngine. We could do this without making it public
// by using controller delegation, or make it properly public.
return $this->buildApplicationCrumbs();
}
private function requireLegalpadSignatures() {
if (!$this->shouldRequireLogin()) {
return null;
}
if ($this->shouldAllowLegallyNonCompliantUsers()) {
return null;
}
$viewer = $this->getViewer();
if (!$viewer->hasSession()) {
return null;
}
$session = $viewer->getSession();
if ($session->getIsPartial()) {
// If the user hasn't made it through MFA yet, require they survive
// MFA first.
return null;
}
if ($session->getSignedLegalpadDocuments()) {
return null;
}
if (!$viewer->isLoggedIn()) {
return null;
}
$must_sign_docs = array();
$sign_docs = array();
$legalpad_class = 'PhabricatorLegalpadApplication';
$legalpad_installed = PhabricatorApplication::isClassInstalledForViewer(
$legalpad_class,
$viewer);
if ($legalpad_installed) {
$sign_docs = id(new LegalpadDocumentQuery())
->setViewer($viewer)
->withSignatureRequired(1)
->needViewerSignatures(true)
->setOrder('oldest')
->execute();
foreach ($sign_docs as $sign_doc) {
if (!$sign_doc->getUserSignature($viewer->getPHID())) {
$must_sign_docs[] = $sign_doc;
}
}
}
if (!$must_sign_docs) {
// If nothing needs to be signed (either because there are no documents
// which require a signature, or because the user has already signed
// all of them) mark the session as good and continue.
$engine = id(new PhabricatorAuthSessionEngine())
->signLegalpadDocuments($viewer, $sign_docs);
return null;
}
$request = $this->getRequest();
$request->setURIMap(
array(
'id' => head($must_sign_docs)->getID(),
));
$application = PhabricatorApplication::getByClass($legalpad_class);
$this->setCurrentApplication($application);
$controller = new LegalpadDocumentSignController();
return $this->delegateToController($controller);
}
/* -( Deprecated )--------------------------------------------------------- */
/**
* DEPRECATED. Use @{method:newPage}.
*/
public function buildStandardPageView() {
return $this->newPage();
}
/**
* DEPRECATED. Use @{method:newPage}.
*/
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->appendChild($view);
return $page->produceAphrontResponse();
}
}
diff --git a/src/applications/base/controller/__tests__/PhabricatorAccessControlTestCase.php b/src/applications/base/controller/__tests__/PhabricatorAccessControlTestCase.php
index 98fa94872..7d763d6e6 100644
--- a/src/applications/base/controller/__tests__/PhabricatorAccessControlTestCase.php
+++ b/src/applications/base/controller/__tests__/PhabricatorAccessControlTestCase.php
@@ -1,280 +1,280 @@
<?php
final class PhabricatorAccessControlTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testControllerAccessControls() {
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/support/startup/PhabricatorStartup.php';
- $application_configuration = new AphrontDefaultApplicationConfiguration();
+ $application_configuration = new AphrontApplicationConfiguration();
$host = 'meow.example.com';
$_SERVER['REQUEST_METHOD'] = 'GET';
$request = id(new AphrontRequest($host, '/'))
->setApplicationConfiguration($application_configuration)
->setRequestData(array());
$controller = new PhabricatorTestController();
$controller->setRequest($request);
$u_public = id(new PhabricatorUser())
->setUsername('public');
$u_unverified = $this->generateNewTestUser()
->setUsername('unverified')
->save();
$u_unverified->setIsEmailVerified(0)->save();
$u_normal = $this->generateNewTestUser()
->setUsername('normal')
->save();
$u_disabled = $this->generateNewTestUser()
->setIsDisabled(true)
->setUsername('disabled')
->save();
$u_admin = $this->generateNewTestUser()
->setIsAdmin(true)
->setUsername('admin')
->save();
$u_notapproved = $this->generateNewTestUser()
->setIsApproved(0)
->setUsername('notapproved')
->save();
$env = PhabricatorEnv::beginScopedEnv();
$env->overrideEnvConfig('phabricator.base-uri', 'http://'.$host);
$env->overrideEnvConfig('policy.allow-public', false);
$env->overrideEnvConfig('auth.require-email-verification', false);
$env->overrideEnvConfig('security.require-multi-factor-auth', false);
// Test standard defaults.
$this->checkAccess(
pht('Default'),
id(clone $controller),
$request,
array(
$u_normal,
$u_admin,
$u_unverified,
),
array(
$u_public,
$u_disabled,
$u_notapproved,
));
// Test email verification.
$env->overrideEnvConfig('auth.require-email-verification', true);
$this->checkAccess(
pht('Email Verification Required'),
id(clone $controller),
$request,
array(
$u_normal,
$u_admin,
),
array(
$u_unverified,
$u_public,
$u_disabled,
$u_notapproved,
));
$this->checkAccess(
pht('Email Verification Required, With Exception'),
id(clone $controller)->setConfig('email', false),
$request,
array(
$u_normal,
$u_admin,
$u_unverified,
),
array(
$u_public,
$u_disabled,
$u_notapproved,
));
$env->overrideEnvConfig('auth.require-email-verification', false);
// Test admin access.
$this->checkAccess(
pht('Admin Required'),
id(clone $controller)->setConfig('admin', true),
$request,
array(
$u_admin,
),
array(
$u_normal,
$u_unverified,
$u_public,
$u_disabled,
$u_notapproved,
));
// Test disabled access.
$this->checkAccess(
pht('Allow Disabled'),
id(clone $controller)->setConfig('enabled', false),
$request,
array(
$u_normal,
$u_unverified,
$u_admin,
$u_disabled,
$u_notapproved,
),
array(
$u_public,
));
// Test no login required.
$this->checkAccess(
pht('No Login Required'),
id(clone $controller)->setConfig('login', false),
$request,
array(
$u_normal,
$u_unverified,
$u_admin,
$u_public,
$u_notapproved,
),
array(
$u_disabled,
));
// Test public access.
$this->checkAccess(
pht('Public Access'),
id(clone $controller)->setConfig('public', true),
$request,
array(
$u_normal,
$u_unverified,
$u_admin,
),
array(
$u_disabled,
$u_public,
));
$env->overrideEnvConfig('policy.allow-public', true);
$this->checkAccess(
pht('Public + configured'),
id(clone $controller)->setConfig('public', true),
$request,
array(
$u_normal,
$u_unverified,
$u_admin,
$u_public,
),
array(
$u_disabled,
$u_notapproved,
));
$env->overrideEnvConfig('policy.allow-public', false);
$app = PhabricatorApplication::getByClass('PhabricatorTestApplication');
$app->reset();
$app->setPolicy(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicies::POLICY_NOONE);
$app_controller = id(clone $controller)->setCurrentApplication($app);
$this->checkAccess(
pht('Application Controller'),
$app_controller,
$request,
array(
),
array(
$u_normal,
$u_unverified,
$u_admin,
$u_public,
$u_disabled,
$u_notapproved,
));
$this->checkAccess(
pht('Application Controller, No Login Required'),
id(clone $app_controller)->setConfig('login', false),
$request,
array(
$u_normal,
$u_unverified,
$u_admin,
$u_public,
$u_notapproved,
),
array(
$u_disabled,
));
}
private function checkAccess(
$label,
$controller,
$request,
array $yes,
array $no) {
foreach ($yes as $user) {
$request->setUser($user);
$uname = $user->getUsername();
try {
$result = id(clone $controller)->willBeginExecution();
} catch (Exception $ex) {
$result = $ex;
}
$this->assertTrue(
($result === null),
pht("Expect user '%s' to be allowed access to '%s'.", $uname, $label));
}
foreach ($no as $user) {
$request->setUser($user);
$uname = $user->getUsername();
try {
$result = id(clone $controller)->willBeginExecution();
} catch (Exception $ex) {
$result = $ex;
}
$this->assertFalse(
($result === null),
pht("Expect user '%s' to be denied access to '%s'.", $uname, $label));
}
}
}
diff --git a/src/applications/cache/PhabricatorKeyValueDatabaseCache.php b/src/applications/cache/PhabricatorKeyValueDatabaseCache.php
index 0b4609074..d6674c567 100644
--- a/src/applications/cache/PhabricatorKeyValueDatabaseCache.php
+++ b/src/applications/cache/PhabricatorKeyValueDatabaseCache.php
@@ -1,174 +1,173 @@
<?php
final class PhabricatorKeyValueDatabaseCache
extends PhutilKeyValueCache {
const CACHE_FORMAT_RAW = 'raw';
const CACHE_FORMAT_DEFLATE = 'deflate';
public function setKeys(array $keys, $ttl = null) {
if (PhabricatorEnv::isReadOnly()) {
return;
}
if ($keys) {
$map = $this->digestKeys(array_keys($keys));
$conn_w = $this->establishConnection('w');
$sql = array();
foreach ($map as $key => $hash) {
$value = $keys[$key];
list($format, $storage_value) = $this->willWriteValue($key, $value);
$sql[] = qsprintf(
$conn_w,
'(%s, %s, %s, %B, %d, %nd)',
$hash,
$key,
$format,
$storage_value,
time(),
$ttl ? (time() + $ttl) : null);
}
$guard = AphrontWriteGuard::beginScopedUnguardedWrites();
foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
queryfx(
$conn_w,
'INSERT INTO %T
(cacheKeyHash, cacheKey, cacheFormat, cacheData,
cacheCreated, cacheExpires) VALUES %LQ
ON DUPLICATE KEY UPDATE
cacheKey = VALUES(cacheKey),
cacheFormat = VALUES(cacheFormat),
cacheData = VALUES(cacheData),
cacheCreated = VALUES(cacheCreated),
cacheExpires = VALUES(cacheExpires)',
$this->getTableName(),
$chunk);
}
unset($guard);
}
return $this;
}
public function getKeys(array $keys) {
$results = array();
if ($keys) {
$map = $this->digestKeys($keys);
$rows = queryfx_all(
$this->establishConnection('r'),
'SELECT * FROM %T WHERE cacheKeyHash IN (%Ls)',
$this->getTableName(),
$map);
$rows = ipull($rows, null, 'cacheKey');
foreach ($keys as $key) {
if (empty($rows[$key])) {
continue;
}
$row = $rows[$key];
if ($row['cacheExpires'] && ($row['cacheExpires'] < time())) {
continue;
}
try {
$results[$key] = $this->didReadValue(
$row['cacheFormat'],
$row['cacheData']);
} catch (Exception $ex) {
// Treat this as a cache miss.
phlog($ex);
}
}
}
return $results;
}
public function deleteKeys(array $keys) {
if ($keys) {
$map = $this->digestKeys($keys);
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE cacheKeyHash IN (%Ls)',
$this->getTableName(),
$map);
}
return $this;
}
public function destroyCache() {
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T',
$this->getTableName());
return $this;
}
/* -( Raw Cache Access )--------------------------------------------------- */
public function establishConnection($mode) {
// TODO: This is the only concrete table we have on the database right
// now.
return id(new PhabricatorMarkupCache())->establishConnection($mode);
}
public function getTableName() {
return 'cache_general';
}
/* -( Implementation )----------------------------------------------------- */
private function digestKeys(array $keys) {
$map = array();
foreach ($keys as $key) {
$map[$key] = PhabricatorHash::digestForIndex($key);
}
return $map;
}
private function willWriteValue($key, $value) {
if (!is_string($value)) {
throw new Exception(pht('Only strings may be written to the DB cache!'));
}
static $can_deflate;
if ($can_deflate === null) {
- $can_deflate = function_exists('gzdeflate') &&
- PhabricatorEnv::getEnvConfig('cache.enable-deflate');
+ $can_deflate = function_exists('gzdeflate');
}
if ($can_deflate) {
$deflated = PhabricatorCaches::maybeDeflateData($value);
if ($deflated !== null) {
return array(self::CACHE_FORMAT_DEFLATE, $deflated);
}
}
return array(self::CACHE_FORMAT_RAW, $value);
}
private function didReadValue($format, $value) {
switch ($format) {
case self::CACHE_FORMAT_RAW:
return $value;
case self::CACHE_FORMAT_DEFLATE:
return PhabricatorCaches::inflateData($value);
default:
throw new Exception(pht('Unknown cache format.'));
}
}
}
diff --git a/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php b/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php
index f1b72dc0e..9e3b23a43 100644
--- a/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php
+++ b/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php
@@ -1,375 +1,375 @@
<?php
final class PhabricatorCalendarEventEditor
extends PhabricatorApplicationTransactionEditor {
private $oldIsAllDay;
private $newIsAllDay;
public function getEditorApplicationClass() {
return 'PhabricatorCalendarApplication';
}
public function getEditorObjectsDescription() {
return pht('Calendar');
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this event.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created %s.', $author, $object);
}
protected function shouldApplyInitialEffects(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
public function getOldIsAllDay() {
return $this->oldIsAllDay;
}
public function getNewIsAllDay() {
return $this->newIsAllDay;
}
protected function applyInitialEffects(
PhabricatorLiskDAO $object,
array $xactions) {
$actor = $this->requireActor();
if ($object->getIsStub()) {
$this->materializeStub($object);
}
// Before doing anything, figure out if the event will be an all day event
// or not after the edit. This affects how we store datetime values, and
// whether we render times or not.
$old_allday = $object->getIsAllDay();
$new_allday = $old_allday;
$type_allday = PhabricatorCalendarEventAllDayTransaction::TRANSACTIONTYPE;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() != $type_allday) {
continue;
}
$new_allday = (bool)$xaction->getNewValue();
}
$this->oldIsAllDay = $old_allday;
$this->newIsAllDay = $new_allday;
}
private function materializeStub(PhabricatorCalendarEvent $event) {
if (!$event->getIsStub()) {
throw new Exception(
pht('Can not materialize an event stub: this event is not a stub.'));
}
$actor = $this->getActor();
$invitees = $event->getInvitees();
$event->copyFromParent($actor);
$event->setIsStub(0);
$event->openTransaction();
$event->save();
foreach ($invitees as $invitee) {
$invitee
->setEventPHID($event->getPHID())
->save();
}
$event->saveTransaction();
$event->attachInvitees($invitees);
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
protected function adjustObjectForPolicyChecks(
PhabricatorLiskDAO $object,
array $xactions) {
$copy = parent::adjustObjectForPolicyChecks($object, $xactions);
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorCalendarEventHostTransaction::TRANSACTIONTYPE:
$copy->setHostPHID($xaction->getNewValue());
break;
case PhabricatorCalendarEventInviteTransaction::TRANSACTIONTYPE:
PhabricatorPolicyRule::passTransactionHintToRule(
$copy,
new PhabricatorCalendarEventInviteesPolicyRule(),
array_fuse($xaction->getNewValue()));
break;
}
}
return $copy;
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
// Clear the availability caches for users whose availability is affected
// by this edit.
$phids = mpull($object->getInvitees(), 'getInviteePHID');
$phids = array_fuse($phids);
$invalidate_all = false;
$invalidate_phids = array();
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorCalendarEventUntilDateTransaction::TRANSACTIONTYPE:
case PhabricatorCalendarEventStartDateTransaction::TRANSACTIONTYPE:
case PhabricatorCalendarEventEndDateTransaction::TRANSACTIONTYPE:
case PhabricatorCalendarEventCancelTransaction::TRANSACTIONTYPE:
case PhabricatorCalendarEventAllDayTransaction::TRANSACTIONTYPE:
// For these kinds of changes, we need to invalidate the availabilty
// caches for all attendees.
$invalidate_all = true;
break;
case PhabricatorCalendarEventAcceptTransaction::TRANSACTIONTYPE:
case PhabricatorCalendarEventDeclineTransaction::TRANSACTIONTYPE:
$acting_phid = $this->getActingAsPHID();
$invalidate_phids[$acting_phid] = $acting_phid;
break;
case PhabricatorCalendarEventInviteTransaction::TRANSACTIONTYPE:
foreach ($xaction->getOldValue() as $phid) {
// Add the possibly un-invited user to the list of potentially
// affected users if they are't already present.
$phids[$phid] = $phid;
$invalidate_phids[$phid] = $phid;
}
foreach ($xaction->getNewValue() as $phid) {
$invalidate_phids[$phid] = $phid;
}
break;
}
}
if (!$invalidate_all) {
$phids = array_select_keys($phids, $invalidate_phids);
}
if ($phids) {
$object->applyViewerTimezone($this->getActor());
$user = new PhabricatorUser();
$conn_w = $user->establishConnection('w');
queryfx(
$conn_w,
'UPDATE %T SET availabilityCacheTTL = NULL
WHERE phid IN (%Ls)',
$user->getTableName(),
$phids);
}
return $xactions;
}
protected function validateAllTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$start_date_xaction =
PhabricatorCalendarEventStartDateTransaction::TRANSACTIONTYPE;
$end_date_xaction =
PhabricatorCalendarEventEndDateTransaction::TRANSACTIONTYPE;
$is_recurrence_xaction =
PhabricatorCalendarEventRecurringTransaction::TRANSACTIONTYPE;
$recurrence_end_xaction =
PhabricatorCalendarEventUntilDateTransaction::TRANSACTIONTYPE;
$start_date = $object->getStartDateTimeEpoch();
$end_date = $object->getEndDateTimeEpoch();
$recurrence_end = $object->getUntilDateTimeEpoch();
$is_recurring = $object->getIsRecurring();
$errors = array();
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == $start_date_xaction) {
$start_date = $xaction->getNewValue()->getEpoch();
} else if ($xaction->getTransactionType() == $end_date_xaction) {
$end_date = $xaction->getNewValue()->getEpoch();
} else if ($xaction->getTransactionType() == $recurrence_end_xaction) {
$recurrence_end = $xaction->getNewValue()->getEpoch();
} else if ($xaction->getTransactionType() == $is_recurrence_xaction) {
$is_recurring = $xaction->getNewValue();
}
}
if ($start_date > $end_date) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$end_date_xaction,
pht('Invalid'),
pht('End date must be after start date.'),
null);
}
if ($recurrence_end && !$is_recurring) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$recurrence_end_xaction,
pht('Invalid'),
pht('Event must be recurring to have a recurrence end date.').
null);
}
return $errors;
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
if ($object->isImportedEvent()) {
return false;
}
return true;
}
protected function supportsSearch() {
return true;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
if ($object->isImportedEvent()) {
return false;
}
return true;
}
protected function getMailSubjectPrefix() {
return pht('[Calendar]');
}
protected function getMailTo(PhabricatorLiskDAO $object) {
$phids = array();
if ($object->getHostPHID()) {
$phids[] = $object->getHostPHID();
}
$phids[] = $this->getActingAsPHID();
$invitees = $object->getInvitees();
foreach ($invitees as $invitee) {
$status = $invitee->getStatus();
if ($status === PhabricatorCalendarEventInvitee::STATUS_ATTENDING
|| $status === PhabricatorCalendarEventInvitee::STATUS_INVITED) {
$phids[] = $invitee->getInviteePHID();
}
}
$phids = array_unique($phids);
return $phids;
}
public function getMailTagsMap() {
return array(
PhabricatorCalendarEventTransaction::MAILTAG_CONTENT =>
pht(
"An event's name, status, invite list, ".
"icon, and description changes."),
PhabricatorCalendarEventTransaction::MAILTAG_RESCHEDULE =>
pht(
"An event's start and end date ".
"and cancellation status changes."),
PhabricatorCalendarEventTransaction::MAILTAG_OTHER =>
pht('Other event activity not listed above occurs.'),
);
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new PhabricatorCalendarReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$name = $object->getName();
$monogram = $object->getMonogram();
return id(new PhabricatorMetaMTAMail())
->setSubject("{$monogram}: {$name}");
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$description = $object->getDescription();
if ($this->getIsNewObject()) {
if (strlen($description)) {
$body->addRemarkupSection(
pht('EVENT DESCRIPTION'),
$description);
}
}
$body->addLinkSection(
pht('EVENT DETAIL'),
PhabricatorEnv::getProductionURI($object->getURI()));
$ics_attachment = $this->newICSAttachment($object);
$body->addAttachment($ics_attachment);
return $body;
}
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
return id(new PhabricatorCalendarEventHeraldAdapter())
->setObject($object);
}
private function newICSAttachment(
PhabricatorCalendarEvent $event) {
$actor = $this->getActor();
$ics_data = id(new PhabricatorCalendarICSWriter())
->setViewer($actor)
->setEvents(array($event))
->writeICSDocument();
- $ics_attachment = new PhabricatorMetaMTAAttachment(
+ $ics_attachment = new PhabricatorMailAttachment(
$ics_data,
$event->getICSFilename(),
'text/calendar');
return $ics_attachment;
}
}
diff --git a/src/applications/calendar/mail/PhabricatorCalendarEventMailReceiver.php b/src/applications/calendar/mail/PhabricatorCalendarEventMailReceiver.php
index 853690748..01a036723 100644
--- a/src/applications/calendar/mail/PhabricatorCalendarEventMailReceiver.php
+++ b/src/applications/calendar/mail/PhabricatorCalendarEventMailReceiver.php
@@ -1,28 +1,28 @@
<?php
final class PhabricatorCalendarEventMailReceiver
extends PhabricatorObjectMailReceiver {
public function isEnabled() {
$app_class = 'PhabricatorCalendarApplication';
return PhabricatorApplication::isClassInstalled($app_class);
}
protected function getObjectPattern() {
return 'E[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
- $id = (int)trim($pattern, 'E');
+ $id = (int)substr($pattern, 1);
return id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
}
protected function getTransactionReplyHandler() {
return new PhabricatorCalendarReplyHandler();
}
}
diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php
index 501d02efa..a8092aaa8 100644
--- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php
+++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php
@@ -1,1459 +1,1449 @@
<?php
final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
implements
PhabricatorPolicyInterface,
PhabricatorExtendedPolicyInterface,
PhabricatorPolicyCodexInterface,
PhabricatorProjectInterface,
PhabricatorMarkupInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorSubscribableInterface,
PhabricatorTokenReceiverInterface,
PhabricatorDestructibleInterface,
PhabricatorMentionableInterface,
PhabricatorFlaggableInterface,
PhabricatorSpacesInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface,
PhabricatorConduitResultInterface {
protected $name;
protected $hostPHID;
protected $description;
protected $isCancelled;
protected $isAllDay;
protected $icon;
protected $mailKey;
protected $isStub;
protected $isRecurring = 0;
protected $seriesParentPHID;
protected $instanceOfEventPHID;
protected $sequenceIndex;
protected $viewPolicy;
protected $editPolicy;
protected $spacePHID;
protected $utcInitialEpoch;
protected $utcUntilEpoch;
protected $utcInstanceEpoch;
protected $parameters = array();
protected $importAuthorPHID;
protected $importSourcePHID;
protected $importUIDIndex;
protected $importUID;
private $parentEvent = self::ATTACHABLE;
private $invitees = self::ATTACHABLE;
private $importSource = self::ATTACHABLE;
private $rsvps = self::ATTACHABLE;
private $viewerTimezone;
private $isGhostEvent = false;
private $stubInvitees;
public static function initializeNewCalendarEvent(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorCalendarApplication'))
->executeOne();
$view_default = PhabricatorCalendarEventDefaultViewCapability::CAPABILITY;
$edit_default = PhabricatorCalendarEventDefaultEditCapability::CAPABILITY;
$view_policy = $app->getPolicy($view_default);
$edit_policy = $app->getPolicy($edit_default);
$now = PhabricatorTime::getNow();
$default_icon = 'fa-calendar';
$datetime_defaults = self::newDefaultEventDateTimes(
$actor,
$now);
list($datetime_start, $datetime_end) = $datetime_defaults;
// When importing events from a context like "bin/calendar reload", we may
// be acting as the omnipotent user.
$host_phid = $actor->getPHID();
if (!$host_phid) {
$host_phid = $app->getPHID();
}
return id(new PhabricatorCalendarEvent())
->setDescription('')
->setHostPHID($host_phid)
->setIsCancelled(0)
->setIsAllDay(0)
->setIsStub(0)
->setIsRecurring(0)
->setIcon($default_icon)
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setSpacePHID($actor->getDefaultSpacePHID())
->attachInvitees(array())
->setStartDateTime($datetime_start)
->setEndDateTime($datetime_end)
->attachImportSource(null)
->applyViewerTimezone($actor);
}
public static function newDefaultEventDateTimes(
PhabricatorUser $viewer,
$now) {
$datetime_start = PhutilCalendarAbsoluteDateTime::newFromEpoch(
$now,
$viewer->getTimezoneIdentifier());
// Advance the time by an hour, then round downwards to the nearest hour.
// For example, if it is currently 3:25 PM, we suggest a default start time
// of 4 PM.
$datetime_start = $datetime_start
->newRelativeDateTime('PT1H')
->newAbsoluteDateTime();
$datetime_start->setMinute(0);
$datetime_start->setSecond(0);
// Default the end time to an hour after the start time.
$datetime_end = $datetime_start
->newRelativeDateTime('PT1H')
->newAbsoluteDateTime();
return array($datetime_start, $datetime_end);
}
private function newChild(
PhabricatorUser $actor,
$sequence,
PhutilCalendarDateTime $start = null) {
if (!$this->isParentEvent()) {
throw new Exception(
pht(
'Unable to generate a new child event for an event which is not '.
'a recurring parent event!'));
}
$series_phid = $this->getSeriesParentPHID();
if (!$series_phid) {
$series_phid = $this->getPHID();
}
$child = id(new self())
->setIsCancelled(0)
->setIsStub(0)
->setInstanceOfEventPHID($this->getPHID())
->setSeriesParentPHID($series_phid)
->setSequenceIndex($sequence)
->setIsRecurring(true)
->attachParentEvent($this)
->attachImportSource(null);
return $child->copyFromParent($actor, $start);
}
protected function readField($field) {
static $inherit = array(
'hostPHID' => true,
'isAllDay' => true,
'icon' => true,
'spacePHID' => true,
'viewPolicy' => true,
'editPolicy' => true,
'name' => true,
'description' => true,
'isCancelled' => true,
);
// Read these fields from the parent event instead of this event. For
// example, we want any changes to the parent event's name to apply to
// the child.
if (isset($inherit[$field])) {
if ($this->getIsStub()) {
// TODO: This should be unconditional, but the execution order of
// CalendarEventQuery and applyViewerTimezone() are currently odd.
if ($this->parentEvent !== self::ATTACHABLE) {
return $this->getParentEvent()->readField($field);
}
}
}
return parent::readField($field);
}
public function copyFromParent(
PhabricatorUser $actor,
PhutilCalendarDateTime $start = null) {
if (!$this->isChildEvent()) {
throw new Exception(
pht(
'Unable to copy from parent event: this is not a child event.'));
}
$parent = $this->getParentEvent();
$this
->setHostPHID($parent->getHostPHID())
->setIsAllDay($parent->getIsAllDay())
->setIcon($parent->getIcon())
->setSpacePHID($parent->getSpacePHID())
->setViewPolicy($parent->getViewPolicy())
->setEditPolicy($parent->getEditPolicy())
->setName($parent->getName())
->setDescription($parent->getDescription())
->setIsCancelled($parent->getIsCancelled());
if ($start) {
$start_datetime = $start;
} else {
$sequence = $this->getSequenceIndex();
$start_datetime = $parent->newSequenceIndexDateTime($sequence);
if (!$start_datetime) {
throw new Exception(
pht(
'Sequence "%s" is not valid for event!',
$sequence));
}
}
$duration = $parent->newDuration();
$end_datetime = $start_datetime->newRelativeDateTime($duration);
$this
->setStartDateTime($start_datetime)
->setEndDateTime($end_datetime);
if ($parent->isImportedEvent()) {
$full_uid = $parent->getImportUID().'/'.$start_datetime->getEpoch();
// NOTE: We don't attach the import source because this gets called
// from CalendarEventQuery while building ghosts, before we've loaded
// and attached sources. Possibly this sequence should be flipped.
$this
->setImportAuthorPHID($parent->getImportAuthorPHID())
->setImportSourcePHID($parent->getImportSourcePHID())
->setImportUID($full_uid);
}
return $this;
}
public function isValidSequenceIndex(PhabricatorUser $viewer, $sequence) {
return (bool)$this->newSequenceIndexDateTime($sequence);
}
public function newSequenceIndexDateTime($sequence) {
$set = $this->newRecurrenceSet();
if (!$set) {
return null;
}
$limit = $sequence + 1;
$count = $this->getRecurrenceCount();
if ($count && ($count < $limit)) {
return null;
}
$instances = $set->getEventsBetween(
null,
$this->newUntilDateTime(),
$limit);
return idx($instances, $sequence, null);
}
public function newStub(PhabricatorUser $actor, $sequence) {
$stub = $this->newChild($actor, $sequence);
$stub->setIsStub(1);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$stub->save();
unset($unguarded);
$stub->applyViewerTimezone($actor);
return $stub;
}
public function newGhost(
PhabricatorUser $actor,
$sequence,
PhutilCalendarDateTime $start = null) {
$ghost = $this->newChild($actor, $sequence, $start);
$ghost
->setIsGhostEvent(true)
->makeEphemeral();
$ghost->applyViewerTimezone($actor);
return $ghost;
}
public function applyViewerTimezone(PhabricatorUser $viewer) {
$this->viewerTimezone = $viewer->getTimezoneIdentifier();
return $this;
}
public function getDuration() {
return ($this->getEndDateTimeEpoch() - $this->getStartDateTimeEpoch());
}
public function updateUTCEpochs() {
// The "intitial" epoch is the start time of the event, in UTC.
$start_date = $this->newStartDateTime()
->setViewerTimezone('UTC');
$start_epoch = $start_date->getEpoch();
$this->setUTCInitialEpoch($start_epoch);
// The "until" epoch is the last UTC epoch on which any instance of this
// event occurs. For infinitely recurring events, it is `null`.
if (!$this->getIsRecurring()) {
$end_date = $this->newEndDateTime()
->setViewerTimezone('UTC');
$until_epoch = $end_date->getEpoch();
} else {
$until_epoch = null;
$until_date = $this->newUntilDateTime();
if ($until_date) {
$until_date->setViewerTimezone('UTC');
$duration = $this->newDuration();
$until_epoch = id(new PhutilCalendarRelativeDateTime())
->setOrigin($until_date)
->setDuration($duration)
->getEpoch();
}
}
$this->setUTCUntilEpoch($until_epoch);
// The "instance" epoch is a property of instances of recurring events.
// It's the original UTC epoch on which the instance started. Usually that
// is the same as the start date, but they may be different if the instance
// has been edited.
// The ICS format uses this value (original start time) to identify event
// instances, and must do so because it allows additional arbitrary
// instances to be added (with "RDATE").
$instance_epoch = null;
$instance_date = $this->newInstanceDateTime();
if ($instance_date) {
$instance_epoch = $instance_date
->setViewerTimezone('UTC')
->getEpoch();
}
$this->setUTCInstanceEpoch($instance_epoch);
return $this;
}
public function save() {
if (!$this->mailKey) {
$this->mailKey = Filesystem::readRandomCharacters(20);
}
$import_uid = $this->getImportUID();
if ($import_uid !== null) {
$index = PhabricatorHash::digestForIndex($import_uid);
} else {
$index = null;
}
$this->setImportUIDIndex($index);
$this->updateUTCEpochs();
return parent::save();
}
/**
* Get the event start epoch for evaluating invitee availability.
*
* When assessing availability, we pretend events start earlier than they
* really do. This allows us to mark users away for the entire duration of a
* series of back-to-back meetings, even if they don't strictly overlap.
*
* @return int Event start date for availability caches.
*/
public function getStartDateTimeEpochForCache() {
$epoch = $this->getStartDateTimeEpoch();
$window = phutil_units('15 minutes in seconds');
return ($epoch - $window);
}
public function getEndDateTimeEpochForCache() {
return $this->getEndDateTimeEpoch();
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text',
'description' => 'text',
'isCancelled' => 'bool',
'isAllDay' => 'bool',
'icon' => 'text32',
'mailKey' => 'bytes20',
'isRecurring' => 'bool',
'seriesParentPHID' => 'phid?',
'instanceOfEventPHID' => 'phid?',
'sequenceIndex' => 'uint32?',
'isStub' => 'bool',
'utcInitialEpoch' => 'epoch',
'utcUntilEpoch' => 'epoch?',
'utcInstanceEpoch' => 'epoch?',
'importAuthorPHID' => 'phid?',
'importSourcePHID' => 'phid?',
'importUIDIndex' => 'bytes12?',
'importUID' => 'text?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_instance' => array(
'columns' => array('instanceOfEventPHID', 'sequenceIndex'),
'unique' => true,
),
'key_epoch' => array(
'columns' => array('utcInitialEpoch', 'utcUntilEpoch'),
),
'key_rdate' => array(
'columns' => array('instanceOfEventPHID', 'utcInstanceEpoch'),
'unique' => true,
),
'key_series' => array(
'columns' => array('seriesParentPHID', 'utcInitialEpoch'),
),
),
self::CONFIG_SERIALIZATION => array(
'parameters' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorCalendarEventPHIDType::TYPECONST);
}
public function getMonogram() {
return 'E'.$this->getID();
}
public function getInvitees() {
if ($this->getIsGhostEvent() || $this->getIsStub()) {
if ($this->stubInvitees === null) {
$this->stubInvitees = $this->newStubInvitees();
}
return $this->stubInvitees;
}
return $this->assertAttached($this->invitees);
}
public function getInviteeForPHID($phid) {
$invitees = $this->getInvitees();
$invitees = mpull($invitees, null, 'getInviteePHID');
return idx($invitees, $phid);
}
public static function getFrequencyMap() {
return array(
PhutilCalendarRecurrenceRule::FREQUENCY_DAILY => array(
'label' => pht('Daily'),
),
PhutilCalendarRecurrenceRule::FREQUENCY_WEEKLY => array(
'label' => pht('Weekly'),
),
PhutilCalendarRecurrenceRule::FREQUENCY_MONTHLY => array(
'label' => pht('Monthly'),
),
PhutilCalendarRecurrenceRule::FREQUENCY_YEARLY => array(
'label' => pht('Yearly'),
),
);
}
private function newStubInvitees() {
$parent = $this->getParentEvent();
$parent_invitees = $parent->getInvitees();
$stub_invitees = array();
foreach ($parent_invitees as $invitee) {
$stub_invitee = id(new PhabricatorCalendarEventInvitee())
->setInviteePHID($invitee->getInviteePHID())
->setInviterPHID($invitee->getInviterPHID())
->setStatus(PhabricatorCalendarEventInvitee::STATUS_INVITED);
$stub_invitees[] = $stub_invitee;
}
return $stub_invitees;
}
public function attachInvitees(array $invitees) {
$this->invitees = $invitees;
return $this;
}
public function getInviteePHIDsForEdit() {
$invitees = array();
foreach ($this->getInvitees() as $invitee) {
if ($invitee->isUninvited()) {
continue;
}
$invitees[] = $invitee->getInviteePHID();
}
return $invitees;
}
public function getUserInviteStatus($phid) {
$invitees = $this->getInvitees();
$invitees = mpull($invitees, null, 'getInviteePHID');
$invited = idx($invitees, $phid);
if (!$invited) {
return PhabricatorCalendarEventInvitee::STATUS_UNINVITED;
}
$invited = $invited->getStatus();
return $invited;
}
public function getIsUserAttending($phid) {
$attending_status = PhabricatorCalendarEventInvitee::STATUS_ATTENDING;
$old_status = $this->getUserInviteStatus($phid);
$is_attending = ($old_status == $attending_status);
return $is_attending;
}
public function getIsGhostEvent() {
return $this->isGhostEvent;
}
public function setIsGhostEvent($is_ghost_event) {
$this->isGhostEvent = $is_ghost_event;
return $this;
}
public function getURI() {
if ($this->getIsGhostEvent()) {
$base = $this->getParentEvent()->getURI();
$sequence = $this->getSequenceIndex();
return "{$base}/{$sequence}/";
}
return '/'.$this->getMonogram();
}
public function getParentEvent() {
return $this->assertAttached($this->parentEvent);
}
public function attachParentEvent(PhabricatorCalendarEvent $event = null) {
$this->parentEvent = $event;
return $this;
}
public function isParentEvent() {
return ($this->getIsRecurring() && !$this->getInstanceOfEventPHID());
}
public function isChildEvent() {
return ($this->instanceOfEventPHID !== null);
}
public function renderEventDate(
PhabricatorUser $viewer,
$show_end) {
$start = $this->newStartDateTime();
$end = $this->newEndDateTime();
$min_date = $start->newPHPDateTime();
$max_date = $end->newPHPDateTime();
if ($this->getIsAllDay()) {
// Subtract one second since the stored date is exclusive.
$max_date = $max_date->modify('-1 second');
}
if ($show_end) {
$min_day = $min_date->format('Y m d');
$max_day = $max_date->format('Y m d');
$show_end_date = ($min_day != $max_day);
} else {
$show_end_date = false;
}
$min_epoch = $min_date->format('U');
$max_epoch = $max_date->format('U');
if ($this->getIsAllDay()) {
if ($show_end_date) {
return pht(
'%s - %s, All Day',
phabricator_date($min_epoch, $viewer),
phabricator_date($max_epoch, $viewer));
} else {
return pht(
'%s, All Day',
phabricator_date($min_epoch, $viewer));
}
} else if ($show_end_date) {
return pht(
'%s - %s',
phabricator_datetime($min_epoch, $viewer),
phabricator_datetime($max_epoch, $viewer));
} else if ($show_end) {
return pht(
'%s - %s',
phabricator_datetime($min_epoch, $viewer),
phabricator_time($max_epoch, $viewer));
} else {
return pht(
'%s',
phabricator_datetime($min_epoch, $viewer));
}
}
public function getDisplayIcon(PhabricatorUser $viewer) {
if ($this->getIsCancelled()) {
return 'fa-times';
}
if ($viewer->isLoggedIn()) {
$viewer_phid = $viewer->getPHID();
if ($this->isRSVPInvited($viewer_phid)) {
return 'fa-users';
} else {
$status = $this->getUserInviteStatus($viewer_phid);
switch ($status) {
case PhabricatorCalendarEventInvitee::STATUS_ATTENDING:
return 'fa-check-circle';
case PhabricatorCalendarEventInvitee::STATUS_INVITED:
return 'fa-user-plus';
case PhabricatorCalendarEventInvitee::STATUS_DECLINED:
return 'fa-times-circle';
}
}
}
if ($this->isImportedEvent()) {
return 'fa-download';
}
return $this->getIcon();
}
public function getDisplayIconColor(PhabricatorUser $viewer) {
if ($this->getIsCancelled()) {
return 'red';
}
if ($this->isImportedEvent()) {
return 'orange';
}
if ($viewer->isLoggedIn()) {
$viewer_phid = $viewer->getPHID();
if ($this->isRSVPInvited($viewer_phid)) {
return 'green';
}
$status = $this->getUserInviteStatus($viewer_phid);
switch ($status) {
case PhabricatorCalendarEventInvitee::STATUS_ATTENDING:
return 'green';
case PhabricatorCalendarEventInvitee::STATUS_INVITED:
return 'green';
case PhabricatorCalendarEventInvitee::STATUS_DECLINED:
return 'grey';
}
}
return 'bluegrey';
}
public function getDisplayIconLabel(PhabricatorUser $viewer) {
if ($this->getIsCancelled()) {
return pht('Cancelled');
}
if ($viewer->isLoggedIn()) {
$status = $this->getUserInviteStatus($viewer->getPHID());
switch ($status) {
case PhabricatorCalendarEventInvitee::STATUS_ATTENDING:
return pht('Attending');
case PhabricatorCalendarEventInvitee::STATUS_INVITED:
return pht('Invited');
case PhabricatorCalendarEventInvitee::STATUS_DECLINED:
return pht('Declined');
}
}
return null;
}
public function getICSFilename() {
return $this->getMonogram().'.ics';
}
public function newIntermediateEventNode(
PhabricatorUser $viewer,
array $children) {
$base_uri = new PhutilURI(PhabricatorEnv::getProductionURI('/'));
$domain = $base_uri->getDomain();
// NOTE: For recurring events, all of the events in the series have the
// same UID (the UID of the parent). The child event instances are
// differentiated by the "RECURRENCE-ID" field.
if ($this->isChildEvent()) {
$parent = $this->getParentEvent();
$instance_datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch(
$this->getUTCInstanceEpoch());
$recurrence_id = $instance_datetime->getISO8601();
$rrule = null;
} else {
$parent = $this;
$recurrence_id = null;
$rrule = $this->newRecurrenceRule();
}
$uid = $parent->getPHID().'@'.$domain;
$created = $this->getDateCreated();
$created = PhutilCalendarAbsoluteDateTime::newFromEpoch($created);
$modified = $this->getDateModified();
$modified = PhutilCalendarAbsoluteDateTime::newFromEpoch($modified);
$date_start = $this->newStartDateTime();
$date_end = $this->newEndDateTime();
if ($this->getIsAllDay()) {
$date_start->setIsAllDay(true);
$date_end->setIsAllDay(true);
}
$host_phid = $this->getHostPHID();
$invitees = $this->getInvitees();
foreach ($invitees as $key => $invitee) {
if ($invitee->isUninvited()) {
unset($invitees[$key]);
}
}
$phids = array();
$phids[] = $host_phid;
foreach ($invitees as $invitee) {
$phids[] = $invitee->getInviteePHID();
}
$handles = $viewer->loadHandles($phids);
$host_handle = $handles[$host_phid];
$host_name = $host_handle->getFullName();
// NOTE: Gmail shows "Who: Unknown Organizer*" if the organizer URI does
// not look like an email address. Use a synthetic address so it shows
// the host name instead.
$install_uri = PhabricatorEnv::getProductionURI('/');
$install_uri = new PhutilURI($install_uri);
// This should possibly use "metamta.reply-handler-domain" instead, but
// we do not currently accept mail for users anyway, and that option may
// not be configured.
$mail_domain = $install_uri->getDomain();
$host_uri = "mailto:{$host_phid}@{$mail_domain}";
$organizer = id(new PhutilCalendarUserNode())
->setName($host_name)
->setURI($host_uri);
$attendees = array();
foreach ($invitees as $invitee) {
$invitee_phid = $invitee->getInviteePHID();
$invitee_handle = $handles[$invitee_phid];
$invitee_name = $invitee_handle->getFullName();
$invitee_uri = $invitee_handle->getURI();
$invitee_uri = PhabricatorEnv::getURI($invitee_uri);
switch ($invitee->getStatus()) {
case PhabricatorCalendarEventInvitee::STATUS_ATTENDING:
$status = PhutilCalendarUserNode::STATUS_ACCEPTED;
break;
case PhabricatorCalendarEventInvitee::STATUS_DECLINED:
$status = PhutilCalendarUserNode::STATUS_DECLINED;
break;
case PhabricatorCalendarEventInvitee::STATUS_INVITED:
default:
$status = PhutilCalendarUserNode::STATUS_INVITED;
break;
}
$attendees[] = id(new PhutilCalendarUserNode())
->setName($invitee_name)
->setURI($invitee_uri)
->setStatus($status);
}
// TODO: Use $children to generate EXDATE/RDATE information.
$node = id(new PhutilCalendarEventNode())
->setUID($uid)
->setName($this->getName())
->setDescription($this->getDescription())
->setCreatedDateTime($created)
->setModifiedDateTime($modified)
->setStartDateTime($date_start)
->setEndDateTime($date_end)
->setOrganizer($organizer)
->setAttendees($attendees);
if ($rrule) {
$node->setRecurrenceRule($rrule);
}
if ($recurrence_id) {
$node->setRecurrenceID($recurrence_id);
}
return $node;
}
public function newStartDateTime() {
$datetime = $this->getParameter('startDateTime');
return $this->newDateTimeFromDictionary($datetime);
}
public function getStartDateTimeEpoch() {
return $this->newStartDateTime()->getEpoch();
}
public function newEndDateTimeForEdit() {
$datetime = $this->getParameter('endDateTime');
return $this->newDateTimeFromDictionary($datetime);
}
public function newEndDateTime() {
$datetime = $this->newEndDateTimeForEdit();
// If this is an all day event, we move the end date time forward to the
// first second of the following day. This is consistent with what users
// expect: an all day event from "Nov 1" to "Nov 1" lasts the entire day.
// For imported events, the end date is already stored with this
// adjustment.
if ($this->getIsAllDay() && !$this->isImportedEvent()) {
$datetime = $datetime
->newAbsoluteDateTime()
->setHour(0)
->setMinute(0)
->setSecond(0)
->newRelativeDateTime('P1D')
->newAbsoluteDateTime();
}
return $datetime;
}
public function getEndDateTimeEpoch() {
return $this->newEndDateTime()->getEpoch();
}
public function newUntilDateTime() {
$datetime = $this->getParameter('untilDateTime');
if ($datetime) {
return $this->newDateTimeFromDictionary($datetime);
}
return null;
}
public function getUntilDateTimeEpoch() {
$datetime = $this->newUntilDateTime();
if (!$datetime) {
return null;
}
return $datetime->getEpoch();
}
public function newDuration() {
return id(new PhutilCalendarDuration())
->setSeconds($this->getDuration());
}
public function newInstanceDateTime() {
if (!$this->getIsRecurring()) {
return null;
}
$index = $this->getSequenceIndex();
if (!$index) {
return null;
}
return $this->newSequenceIndexDateTime($index);
}
private function newDateTimeFromEpoch($epoch) {
$datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch($epoch);
if ($this->getIsAllDay()) {
$datetime->setIsAllDay(true);
}
return $this->newDateTimeFromDateTime($datetime);
}
private function newDateTimeFromDictionary(array $dict) {
$datetime = PhutilCalendarAbsoluteDateTime::newFromDictionary($dict);
return $this->newDateTimeFromDateTime($datetime);
}
private function newDateTimeFromDateTime(PhutilCalendarDateTime $datetime) {
$viewer_timezone = $this->viewerTimezone;
if ($viewer_timezone) {
$datetime->setViewerTimezone($viewer_timezone);
}
return $datetime;
}
public function getParameter($key, $default = null) {
return idx($this->parameters, $key, $default);
}
public function setParameter($key, $value) {
$this->parameters[$key] = $value;
return $this;
}
public function setStartDateTime(PhutilCalendarDateTime $datetime) {
return $this->setParameter(
'startDateTime',
$datetime->newAbsoluteDateTime()->toDictionary());
}
public function setEndDateTime(PhutilCalendarDateTime $datetime) {
return $this->setParameter(
'endDateTime',
$datetime->newAbsoluteDateTime()->toDictionary());
}
public function setUntilDateTime(PhutilCalendarDateTime $datetime = null) {
if ($datetime) {
$value = $datetime->newAbsoluteDateTime()->toDictionary();
} else {
$value = null;
}
return $this->setParameter('untilDateTime', $value);
}
public function setRecurrenceRule(PhutilCalendarRecurrenceRule $rrule) {
return $this->setParameter(
'recurrenceRule',
$rrule->toDictionary());
}
public function newRecurrenceRule() {
if ($this->isChildEvent()) {
return $this->getParentEvent()->newRecurrenceRule();
}
if (!$this->getIsRecurring()) {
return null;
}
$dict = $this->getParameter('recurrenceRule');
if (!$dict) {
return null;
}
$rrule = PhutilCalendarRecurrenceRule::newFromDictionary($dict);
$start = $this->newStartDateTime();
$rrule->setStartDateTime($start);
$until = $this->newUntilDateTime();
if ($until) {
$rrule->setUntil($until);
}
$count = $this->getRecurrenceCount();
if ($count) {
$rrule->setCount($count);
}
return $rrule;
}
public function getRecurrenceCount() {
$count = (int)$this->getParameter('recurrenceCount');
if (!$count) {
return null;
}
return $count;
}
public function newRecurrenceSet() {
if ($this->isChildEvent()) {
return $this->getParentEvent()->newRecurrenceSet();
}
$set = new PhutilCalendarRecurrenceSet();
if ($this->viewerTimezone) {
$set->setViewerTimezone($this->viewerTimezone);
}
$rrule = $this->newRecurrenceRule();
if (!$rrule) {
return null;
}
$set->addSource($rrule);
return $set;
}
public function isImportedEvent() {
return (bool)$this->getImportSourcePHID();
}
public function getImportSource() {
return $this->assertAttached($this->importSource);
}
public function attachImportSource(
PhabricatorCalendarImport $import = null) {
$this->importSource = $import;
return $this;
}
public function loadForkTarget(PhabricatorUser $viewer) {
if (!$this->getIsRecurring()) {
// Can't fork an event which isn't recurring.
return null;
}
if ($this->isChildEvent()) {
// If this is a child event, this is the fork target.
return $this;
}
if (!$this->isValidSequenceIndex($viewer, 1)) {
// This appears to be a "recurring" event with no valid instances: for
// example, its "until" date is before the second instance would occur.
// This can happen if we already forked the event or if users entered
// silly stuff. Just edit the event directly without forking anything.
return null;
}
$next_event = id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->withInstanceSequencePairs(
array(
array($this->getPHID(), 1),
))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$next_event) {
$next_event = $this->newStub($viewer, 1);
}
return $next_event;
}
public function loadFutureEvents(PhabricatorUser $viewer) {
// NOTE: If you can't edit some of the future events, we just
// don't try to update them. This seems like it's probably what
// users are likely to expect.
// NOTE: This only affects events that are currently in the same
// series, not all events that were ever in the original series.
// We could use series PHIDs instead of parent PHIDs to affect more
// events if this turns out to be counterintuitive. Other
// applications differ in their behavior.
return id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->withParentEventPHIDs(array($this->getPHID()))
->withUTCInitialEpochBetween($this->getUTCInitialEpoch(), null)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->execute();
}
public function getNotificationPHIDs() {
$phids = array();
if ($this->getPHID()) {
$phids[] = $this->getPHID();
}
if ($this->getSeriesParentPHID()) {
$phids[] = $this->getSeriesParentPHID();
}
return $phids;
}
public function getRSVPs($phid) {
return $this->assertAttachedKey($this->rsvps, $phid);
}
public function attachRSVPs(array $rsvps) {
$this->rsvps = $rsvps;
return $this;
}
public function isRSVPInvited($phid) {
$status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED;
return ($this->getRSVPStatus($phid) == $status_invited);
}
public function hasRSVPAuthority($phid, $other_phid) {
foreach ($this->getRSVPs($phid) as $rsvp) {
if ($rsvp->getInviteePHID() == $other_phid) {
return true;
}
}
return false;
}
public function getRSVPStatus($phid) {
// Check for an individual invitee record first.
$invitees = $this->invitees;
$invitees = mpull($invitees, null, 'getInviteePHID');
$invitee = idx($invitees, $phid);
if ($invitee) {
return $invitee->getStatus();
}
// If we don't have one, try to find an invited status for the user's
// projects.
$status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED;
foreach ($this->getRSVPs($phid) as $rsvp) {
if ($rsvp->getStatus() == $status_invited) {
return $status_invited;
}
}
return PhabricatorCalendarEventInvitee::STATUS_UNINVITED;
}
/* -( Markup Interface )--------------------------------------------------- */
/**
* @task markup
*/
public function getMarkupFieldKey($field) {
$content = $this->getMarkupText($field);
return PhabricatorMarkupEngine::digestRemarkupContent($this, $content);
}
/**
* @task markup
*/
public function getMarkupText($field) {
return $this->getDescription();
}
/**
* @task markup
*/
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newCalendarMarkupEngine();
}
/**
* @task markup
*/
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
/**
* @task markup
*/
public function shouldUseMarkupCache($field) {
return (bool)$this->getID();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
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:
if ($this->isImportedEvent()) {
return PhabricatorPolicies::POLICY_NOONE;
} else {
return $this->getEditPolicy();
}
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($this->isImportedEvent()) {
return false;
}
// The host of an event can always view and edit it.
$user_phid = $this->getHostPHID();
if ($user_phid) {
$viewer_phid = $viewer->getPHID();
if ($viewer_phid == $user_phid) {
return true;
}
}
if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
$status = $this->getUserInviteStatus($viewer->getPHID());
if ($status == PhabricatorCalendarEventInvitee::STATUS_INVITED ||
$status == PhabricatorCalendarEventInvitee::STATUS_ATTENDING ||
$status == PhabricatorCalendarEventInvitee::STATUS_DECLINED) {
return true;
}
}
return false;
}
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
$extended = array();
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$import_source = $this->getImportSource();
if ($import_source) {
$extended[] = array(
$import_source,
PhabricatorPolicyCapability::CAN_VIEW,
);
}
break;
}
return $extended;
}
/* -( PhabricatorPolicyCodexInterface )------------------------------------ */
public function newPolicyCodex() {
return new PhabricatorCalendarEventPolicyCodex();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorCalendarEventEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorCalendarEventTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return ($phid == $this->getHostPHID());
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array($this->getHostPHID());
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$invitees = id(new PhabricatorCalendarEventInvitee())->loadAllWhere(
'eventPHID = %s',
$this->getPHID());
foreach ($invitees as $invitee) {
$invitee->delete();
}
$notifications = id(new PhabricatorCalendarNotification())->loadAllWhere(
'eventPHID = %s',
$this->getPHID());
foreach ($notifications as $notification) {
$notification->delete();
}
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorSpacesInterface )----------------------------------------- */
public function getSpacePHID() {
return $this->spacePHID;
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PhabricatorCalendarEventFulltextEngine();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new PhabricatorCalendarEventFerretEngine();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of the event.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('description')
->setType('string')
->setDescription(pht('The event description.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('isAllDay')
->setType('bool')
->setDescription(pht('True if the event is an all day event.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('startDateTime')
->setType('datetime')
->setDescription(pht('Start date and time of the event.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('endDateTime')
->setType('datetime')
->setDescription(pht('End date and time of the event.')),
);
}
public function getFieldValuesForConduit() {
$start_datetime = $this->newStartDateTime();
$end_datetime = $this->newEndDateTime();
return array(
'name' => $this->getName(),
'description' => $this->getDescription(),
'isAllDay' => (bool)$this->getIsAllDay(),
'startDateTime' => $this->getConduitDateTime($start_datetime),
'endDateTime' => $this->getConduitDateTime($end_datetime),
);
}
public function getConduitSearchAttachments() {
return array();
}
private function getConduitDateTime($datetime) {
if (!$datetime) {
return null;
}
$epoch = $datetime->getEpoch();
// TODO: Possibly pass the actual viewer in from the Conduit stuff, or
// retain it when setting the viewer timezone?
$viewer = id(new PhabricatorUser())
->overrideTimezoneIdentifier($this->viewerTimezone);
return array(
'epoch' => (int)$epoch,
'display' => array(
'default' => phabricator_datetime($epoch, $viewer),
),
'iso8601' => $datetime->getISO8601(),
'timezone' => $this->viewerTimezone,
);
}
}
diff --git a/src/applications/calendar/storage/PhabricatorCalendarExport.php b/src/applications/calendar/storage/PhabricatorCalendarExport.php
index 5a8ad84da..4ad2b457f 100644
--- a/src/applications/calendar/storage/PhabricatorCalendarExport.php
+++ b/src/applications/calendar/storage/PhabricatorCalendarExport.php
@@ -1,193 +1,182 @@
<?php
final class PhabricatorCalendarExport extends PhabricatorCalendarDAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorDestructibleInterface {
protected $name;
protected $authorPHID;
protected $policyMode;
protected $queryKey;
protected $secretKey;
protected $isDisabled = 0;
const MODE_PUBLIC = 'public';
const MODE_PRIVILEGED = 'privileged';
public static function initializeNewCalendarExport(PhabricatorUser $actor) {
return id(new self())
->setAuthorPHID($actor->getPHID())
->setPolicyMode(self::MODE_PRIVILEGED)
->setIsDisabled(0);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text',
'policyMode' => 'text64',
'queryKey' => 'text64',
'secretKey' => 'bytes20',
'isDisabled' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_author' => array(
'columns' => array('authorPHID'),
),
'key_secret' => array(
'columns' => array('secretKey'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function getPHIDType() {
return PhabricatorCalendarExportPHIDType::TYPECONST;
}
public function save() {
if (!$this->getSecretKey()) {
$this->setSecretKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public function getURI() {
$id = $this->getID();
return "/calendar/export/{$id}/";
}
private static function getPolicyModeMap() {
return array(
self::MODE_PUBLIC => array(
'icon' => 'fa-globe',
'name' => pht('Public'),
'color' => 'bluegrey',
'summary' => pht(
'Export only public data.'),
'description' => pht(
'Only publicly available data is exported.'),
),
self::MODE_PRIVILEGED => array(
'icon' => 'fa-unlock-alt',
'name' => pht('Privileged'),
'color' => 'red',
'summary' => pht(
'Export private data.'),
'description' => pht(
'Anyone who knows the URI for this export can view all event '.
'details as though they were logged in with your account.'),
),
);
}
private static function getPolicyModeSpec($const) {
return idx(self::getPolicyModeMap(), $const, array());
}
public static function getPolicyModeName($const) {
$spec = self::getPolicyModeSpec($const);
return idx($spec, 'name', $const);
}
public static function getPolicyModeIcon($const) {
$spec = self::getPolicyModeSpec($const);
return idx($spec, 'icon', $const);
}
public static function getPolicyModeColor($const) {
$spec = self::getPolicyModeSpec($const);
return idx($spec, 'color', $const);
}
public static function getPolicyModeSummary($const) {
$spec = self::getPolicyModeSpec($const);
return idx($spec, 'summary', $const);
}
public static function getPolicyModeDescription($const) {
$spec = self::getPolicyModeSpec($const);
return idx($spec, 'description', $const);
}
public static function getPolicyModes() {
return array_keys(self::getPolicyModeMap());
}
public static function getAvailablePolicyModes() {
$modes = array();
if (PhabricatorEnv::getEnvConfig('policy.allow-public')) {
$modes[] = self::MODE_PUBLIC;
}
$modes[] = self::MODE_PRIVILEGED;
return $modes;
}
public function getICSFilename() {
return PhabricatorSlug::normalizeProjectSlug($this->getName()).'.ics';
}
public function getICSURI() {
$secret_key = $this->getSecretKey();
$ics_name = $this->getICSFilename();
return "/calendar/export/ics/{$secret_key}/{$ics_name}";
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return $this->getAuthorPHID();
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorCalendarExportEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorCalendarExportTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->delete();
}
}
diff --git a/src/applications/calendar/storage/PhabricatorCalendarImport.php b/src/applications/calendar/storage/PhabricatorCalendarImport.php
index 1c7c1a543..37ddac857 100644
--- a/src/applications/calendar/storage/PhabricatorCalendarImport.php
+++ b/src/applications/calendar/storage/PhabricatorCalendarImport.php
@@ -1,209 +1,199 @@
<?php
final class PhabricatorCalendarImport
extends PhabricatorCalendarDAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorDestructibleInterface {
protected $name;
protected $authorPHID;
protected $viewPolicy;
protected $editPolicy;
protected $engineType;
protected $parameters = array();
protected $isDisabled = 0;
protected $triggerPHID;
protected $triggerFrequency;
const FREQUENCY_ONCE = 'once';
const FREQUENCY_HOURLY = 'hourly';
const FREQUENCY_DAILY = 'daily';
private $engine = self::ATTACHABLE;
public static function initializeNewCalendarImport(
PhabricatorUser $actor,
PhabricatorCalendarImportEngine $engine) {
return id(new self())
->setName('')
->setAuthorPHID($actor->getPHID())
->setViewPolicy($actor->getPHID())
->setEditPolicy($actor->getPHID())
->setIsDisabled(0)
->setEngineType($engine->getImportEngineType())
->attachEngine($engine)
->setTriggerFrequency(self::FREQUENCY_ONCE);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'parameters' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text',
'engineType' => 'text64',
'isDisabled' => 'bool',
'triggerPHID' => 'phid?',
'triggerFrequency' => 'text64',
),
self::CONFIG_KEY_SCHEMA => array(
'key_author' => array(
'columns' => array('authorPHID'),
),
),
) + parent::getConfiguration();
}
public function getPHIDType() {
return PhabricatorCalendarImportPHIDType::TYPECONST;
}
public function getURI() {
$id = $this->getID();
return "/calendar/import/{$id}/";
}
public function attachEngine(PhabricatorCalendarImportEngine $engine) {
$this->engine = $engine;
return $this;
}
public function getEngine() {
return $this->assertAttached($this->engine);
}
public function getParameter($key, $default = null) {
return idx($this->parameters, $key, $default);
}
public function setParameter($key, $value) {
$this->parameters[$key] = $value;
return $this;
}
public function getDisplayName() {
$name = $this->getName();
if (strlen($name)) {
return $name;
}
return $this->getEngine()->getDisplayName($this);
}
public static function getTriggerFrequencyMap() {
return array(
self::FREQUENCY_ONCE => array(
'name' => pht('No Automatic Updates'),
),
self::FREQUENCY_HOURLY => array(
'name' => pht('Update Hourly'),
),
self::FREQUENCY_DAILY => array(
'name' => pht('Update Daily'),
),
);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
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 $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorCalendarImportEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorCalendarImportTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
- return $timeline;
- }
-
public function newLogMessage($type, array $parameters) {
$parameters = array(
'type' => $type,
) + $parameters;
return id(new PhabricatorCalendarImportLog())
->setImportPHID($this->getPHID())
->setParameters($parameters)
->save();
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$viewer = $engine->getViewer();
$this->openTransaction();
$trigger_phid = $this->getTriggerPHID();
if ($trigger_phid) {
$trigger = id(new PhabricatorWorkerTriggerQuery())
->setViewer($viewer)
->withPHIDs(array($trigger_phid))
->executeOne();
if ($trigger) {
$engine->destroyObject($trigger);
}
}
$events = id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->withImportSourcePHIDs(array($this->getPHID()))
->execute();
foreach ($events as $event) {
$engine->destroyObject($event);
}
$logs = id(new PhabricatorCalendarImportLogQuery())
->setViewer($viewer)
->withImportPHIDs(array($this->getPHID()))
->execute();
foreach ($logs as $log) {
$engine->destroyObject($log);
}
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/celerity/CelerityResourceMapGenerator.php b/src/applications/celerity/CelerityResourceMapGenerator.php
index 91d0193ad..9280c9ecc 100644
--- a/src/applications/celerity/CelerityResourceMapGenerator.php
+++ b/src/applications/celerity/CelerityResourceMapGenerator.php
@@ -1,397 +1,407 @@
<?php
final class CelerityResourceMapGenerator extends Phobject {
private $debug = false;
private $resources;
private $nameMap = array();
private $symbolMap = array();
private $requiresMap = array();
private $packageMap = array();
public function __construct(CelerityPhysicalResources $resources) {
$this->resources = $resources;
}
public function getNameMap() {
return $this->nameMap;
}
public function getSymbolMap() {
return $this->symbolMap;
}
public function getRequiresMap() {
return $this->requiresMap;
}
public function getPackageMap() {
return $this->packageMap;
}
public function setDebug($debug) {
$this->debug = $debug;
return $this;
}
protected function log($message) {
if ($this->debug) {
$console = PhutilConsole::getConsole();
$console->writeErr("%s\n", $message);
}
}
public function generate() {
$binary_map = $this->rebuildBinaryResources($this->resources);
$this->log(pht('Found %d binary resources.', count($binary_map)));
$xformer = id(new CelerityResourceTransformer())
->setMinify(false)
->setRawURIMap(ipull($binary_map, 'uri'));
$text_map = $this->rebuildTextResources($this->resources, $xformer);
$this->log(pht('Found %d text resources.', count($text_map)));
$resource_graph = array();
$requires_map = array();
$symbol_map = array();
foreach ($text_map as $name => $info) {
if (isset($info['provides'])) {
$symbol_map[$info['provides']] = $info['hash'];
// We only need to check for cycles and add this to the requires map
// if it actually requires anything.
if (!empty($info['requires'])) {
$resource_graph[$info['provides']] = $info['requires'];
$requires_map[$info['hash']] = $info['requires'];
}
}
}
$this->detectGraphCycles($resource_graph);
$name_map = ipull($binary_map, 'hash') + ipull($text_map, 'hash');
$hash_map = array_flip($name_map);
$package_map = $this->rebuildPackages(
$this->resources,
$symbol_map,
$hash_map);
$this->log(pht('Found %d packages.', count($package_map)));
$component_map = array();
foreach ($package_map as $package_name => $package_info) {
foreach ($package_info['symbols'] as $symbol) {
$component_map[$symbol] = $package_name;
}
}
$name_map = $this->mergeNameMaps(
array(
array(pht('Binary'), ipull($binary_map, 'hash')),
array(pht('Text'), ipull($text_map, 'hash')),
array(pht('Package'), ipull($package_map, 'hash')),
));
$package_map = ipull($package_map, 'symbols');
ksort($name_map, SORT_STRING);
ksort($symbol_map, SORT_STRING);
ksort($requires_map, SORT_STRING);
ksort($package_map, SORT_STRING);
$this->nameMap = $name_map;
$this->symbolMap = $symbol_map;
$this->requiresMap = $requires_map;
$this->packageMap = $package_map;
return $this;
}
public function write() {
$map_content = $this->formatMapContent(array(
'names' => $this->getNameMap(),
'symbols' => $this->getSymbolMap(),
'requires' => $this->getRequiresMap(),
'packages' => $this->getPackageMap(),
));
$map_path = $this->resources->getPathToMap();
$this->log(pht('Writing map "%s".', Filesystem::readablePath($map_path)));
Filesystem::writeFile($map_path, $map_content);
return $this;
}
private function formatMapContent(array $data) {
$content = phutil_var_export($data);
$generated = '@'.'generated';
return <<<EOFILE
<?php
/**
* This file is automatically generated. Use 'bin/celerity map' to rebuild it.
*
* {$generated}
*/
return {$content};
EOFILE;
}
/**
* Find binary resources (like PNG and SWF) and return information about
* them.
*
* @param CelerityPhysicalResources Resource map to find binary resources for.
* @return map<string, map<string, string>> Resource information map.
*/
private function rebuildBinaryResources(
CelerityPhysicalResources $resources) {
$binary_map = $resources->findBinaryResources();
$result_map = array();
foreach ($binary_map as $name => $data_hash) {
- $hash = $resources->getCelerityHash($data_hash.$name);
+ $hash = $this->newResourceHash($data_hash.$name);
$result_map[$name] = array(
'hash' => $hash,
'uri' => $resources->getResourceURI($hash, $name),
);
}
return $result_map;
}
/**
* Find text resources (like JS and CSS) and return information about them.
*
* @param CelerityPhysicalResources Resource map to find text resources for.
* @param CelerityResourceTransformer Configured resource transformer.
* @return map<string, map<string, string>> Resource information map.
*/
private function rebuildTextResources(
CelerityPhysicalResources $resources,
CelerityResourceTransformer $xformer) {
$text_map = $resources->findTextResources();
$result_map = array();
foreach ($text_map as $name => $data_hash) {
$raw_data = $resources->getResourceData($name);
$xformed_data = $xformer->transformResource($name, $raw_data);
- $data_hash = $resources->getCelerityHash($xformed_data);
- $hash = $resources->getCelerityHash($data_hash.$name);
+ $data_hash = $this->newResourceHash($xformed_data);
+ $hash = $this->newResourceHash($data_hash.$name);
list($provides, $requires) = $this->getProvidesAndRequires(
$name,
$raw_data);
$result_map[$name] = array(
'hash' => $hash,
);
if ($provides !== null) {
$result_map[$name] += array(
'provides' => $provides,
'requires' => $requires,
);
}
}
return $result_map;
}
/**
* Parse the `@provides` and `@requires` symbols out of a text resource, like
* JS or CSS.
*
* @param string Resource name.
* @param string Resource data.
* @return pair<string|null, list<string>|null> The `@provides` symbol and
* the list of `@requires` symbols. If the resource is not part of the
* dependency graph, both are null.
*/
private function getProvidesAndRequires($name, $data) {
$parser = new PhutilDocblockParser();
$matches = array();
$ok = preg_match('@/[*][*].*?[*]/@s', $data, $matches);
if (!$ok) {
throw new Exception(
pht(
'Resource "%s" does not have a header doc comment. Encode '.
'dependency data in a header docblock.',
$name));
}
list($description, $metadata) = $parser->parse($matches[0]);
$provides = $this->parseResourceSymbolList(idx($metadata, 'provides'));
$requires = $this->parseResourceSymbolList(idx($metadata, 'requires'));
if (!$provides) {
// Tests and documentation-only JS is permitted to @provide no targets.
return array(null, null);
}
if (count($provides) > 1) {
throw new Exception(
pht(
'Resource "%s" must %s at most one Celerity target.',
$name,
'@provide'));
}
return array(head($provides), $requires);
}
/**
* Check for dependency cycles in the resource graph. Raises an exception if
* a cycle is detected.
*
* @param map<string, list<string>> Map of `@provides` symbols to their
* `@requires` symbols.
* @return void
*/
private function detectGraphCycles(array $nodes) {
$graph = id(new CelerityResourceGraph())
->addNodes($nodes)
->setResourceGraph($nodes)
->loadGraph();
foreach ($nodes as $provides => $requires) {
$cycle = $graph->detectCycles($provides);
if ($cycle) {
throw new Exception(
pht(
'Cycle detected in resource graph: %s',
implode(' > ', $cycle)));
}
}
}
/**
* Build package specifications for a given resource source.
*
* @param CelerityPhysicalResources Resource source to rebuild.
* @param map<string, string> Map of `@provides` to hashes.
* @param map<string, string> Map of hashes to resource names.
* @return map<string, map<string, string>> Package information maps.
*/
private function rebuildPackages(
CelerityPhysicalResources $resources,
array $symbol_map,
array $reverse_map) {
$package_map = array();
$package_spec = $resources->getResourcePackages();
foreach ($package_spec as $package_name => $package_symbols) {
$type = null;
$hashes = array();
foreach ($package_symbols as $symbol) {
$symbol_hash = idx($symbol_map, $symbol);
if ($symbol_hash === null) {
throw new Exception(
pht(
'Package specification for "%s" includes "%s", but that symbol '.
'is not %s by any resource.',
$package_name,
$symbol,
'@provided'));
}
$resource_name = $reverse_map[$symbol_hash];
$resource_type = $resources->getResourceType($resource_name);
if ($type === null) {
$type = $resource_type;
} else if ($type !== $resource_type) {
throw new Exception(
pht(
'Package specification for "%s" includes resources of multiple '.
'types (%s, %s). Each package may only contain one type of '.
'resource.',
$package_name,
$type,
$resource_type));
}
$hashes[] = $symbol.':'.$symbol_hash;
}
- $hash = $resources->getCelerityHash(implode("\n", $hashes));
+ $hash = $this->newResourceHash(implode("\n", $hashes));
$package_map[$package_name] = array(
'hash' => $hash,
'symbols' => $package_symbols,
);
}
return $package_map;
}
private function mergeNameMaps(array $maps) {
$result = array();
$origin = array();
foreach ($maps as $map) {
list($map_name, $data) = $map;
foreach ($data as $name => $hash) {
if (empty($result[$name])) {
$result[$name] = $hash;
$origin[$name] = $map_name;
} else {
$old = $origin[$name];
$new = $map_name;
throw new Exception(
pht(
'Resource source defines two resources with the same name, '.
'"%s". One is defined in the "%s" map; the other in the "%s" '.
'map. Each resource must have a unique name.',
$name,
$old,
$new));
}
}
}
return $result;
}
private function parseResourceSymbolList($list) {
if (!$list) {
return array();
}
// This is valid:
//
// @requires x y
//
// But so is this:
//
// @requires x
// @requires y
//
// Accept either form and produce a list of symbols.
$list = (array)$list;
// We can get `true` values if there was a bare `@requires` in the input.
foreach ($list as $key => $item) {
if ($item === true) {
unset($list[$key]);
}
}
$list = implode(' ', $list);
$list = trim($list);
$list = preg_split('/\s+/', $list);
$list = array_filter($list);
return $list;
}
+ private function newResourceHash($data) {
+ // This HMAC key is a static, hard-coded value because we don't want the
+ // hashes in the map to depend on database state: when two different
+ // developers regenerate the map, they should end up with the same output.
+
+ $hash = PhabricatorHash::digestHMACSHA256($data, 'celerity-resource-data');
+
+ return substr($hash, 0, 8);
+ }
+
}
diff --git a/src/applications/celerity/controller/CelerityPhabricatorResourceController.php b/src/applications/celerity/controller/CelerityPhabricatorResourceController.php
index 5751996db..375a19dd6 100644
--- a/src/applications/celerity/controller/CelerityPhabricatorResourceController.php
+++ b/src/applications/celerity/controller/CelerityPhabricatorResourceController.php
@@ -1,57 +1,55 @@
<?php
/**
* Delivers CSS and JS resources to the browser. This controller handles all
* `/res/` requests, and manages caching, package construction, and resource
* preprocessing.
*/
final class CelerityPhabricatorResourceController
extends CelerityResourceController {
private $path;
private $hash;
private $library;
private $postprocessorKey;
public function getCelerityResourceMap() {
return CelerityResourceMap::getNamedInstance($this->library);
}
public function handleRequest(AphrontRequest $request) {
$this->path = $request->getURIData('path');
$this->hash = $request->getURIData('hash');
$this->library = $request->getURIData('library');
$this->postprocessorKey = $request->getURIData('postprocessor');
// Check that the resource library exists before trying to serve resources
// from it.
try {
$this->getCelerityResourceMap();
} catch (Exception $ex) {
return new Aphront400Response();
}
return $this->serveResource(
array(
'path' => $this->path,
'hash' => $this->hash,
));
}
protected function buildResourceTransformer() {
- $minify_on = PhabricatorEnv::getEnvConfig('celerity.minify');
$developer_on = PhabricatorEnv::getEnvConfig('phabricator.developer-mode');
-
- $should_minify = ($minify_on && !$developer_on);
+ $should_minify = !$developer_on;
return id(new CelerityResourceTransformer())
->setMinify($should_minify)
->setPostprocessorKey($this->postprocessorKey)
->setCelerityMap($this->getCelerityResourceMap());
}
protected function getCacheKey($path) {
return parent::getCacheKey($path.';'.$this->postprocessorKey);
}
}
diff --git a/src/applications/celerity/resources/CelerityResources.php b/src/applications/celerity/resources/CelerityResources.php
index e0b919170..a5f3fd20e 100644
--- a/src/applications/celerity/resources/CelerityResources.php
+++ b/src/applications/celerity/resources/CelerityResources.php
@@ -1,38 +1,32 @@
<?php
/**
* Defines the location of static resources.
*/
abstract class CelerityResources extends Phobject {
abstract public function getName();
abstract public function getResourceData($name);
public function getResourceModifiedTime($name) {
return 0;
}
- public function getCelerityHash($data) {
- $tail = PhabricatorEnv::getEnvConfig('celerity.resource-hash');
- $hash = PhabricatorHash::weakDigest($data, $tail);
- return substr($hash, 0, 8);
- }
-
public function getResourceType($path) {
return CelerityResourceTransformer::getResourceType($path);
}
public function getResourceURI($hash, $name) {
$resources = $this->getName();
return "/res/{$resources}/{$hash}/{$name}";
}
public function getResourcePackages() {
return array();
}
public function loadMap() {
return array();
}
}
diff --git a/src/applications/conduit/application/PhabricatorConduitApplication.php b/src/applications/conduit/application/PhabricatorConduitApplication.php
index a036860aa..f9dae71a8 100644
--- a/src/applications/conduit/application/PhabricatorConduitApplication.php
+++ b/src/applications/conduit/application/PhabricatorConduitApplication.php
@@ -1,65 +1,69 @@
<?php
final class PhabricatorConduitApplication extends PhabricatorApplication {
public function getBaseURI() {
return '/conduit/';
}
public function getIcon() {
return 'fa-tty';
}
public function canUninstall() {
return false;
}
public function getHelpDocumentationArticles(PhabricatorUser $viewer) {
return array(
array(
'name' => pht('Conduit API Overview'),
'href' => PhabricatorEnv::getDoclink('Conduit API Overview'),
),
);
}
public function getName() {
return pht('Conduit');
}
public function getShortDescription() {
return pht('Developer API');
}
public function getTitleGlyph() {
return "\xE2\x87\xB5";
}
public function getApplicationGroup() {
return self::GROUP_DEVELOPER;
}
public function getApplicationOrder() {
return 0.100;
}
public function getRoutes() {
return array(
'/conduit/' => array(
- '(?:query/(?P<queryKey>[^/]+)/)?' => 'PhabricatorConduitListController',
+ $this->getQueryRoutePattern() => 'PhabricatorConduitListController',
'method/(?P<method>[^/]+)/' => 'PhabricatorConduitConsoleController',
- 'log/(?:query/(?P<queryKey>[^/]+)/)?' =>
- 'PhabricatorConduitLogController',
- 'log/view/(?P<view>[^/]+)/' => 'PhabricatorConduitLogController',
- 'token/' => 'PhabricatorConduitTokenController',
- 'token/edit/(?:(?P<id>\d+)/)?' =>
- 'PhabricatorConduitTokenEditController',
- 'token/terminate/(?:(?P<id>\d+)/)?' =>
- 'PhabricatorConduitTokenTerminateController',
+ 'log/' => array(
+ $this->getQueryRoutePattern() =>
+ 'PhabricatorConduitLogController',
+ 'view/(?P<view>[^/]+)/' => 'PhabricatorConduitLogController',
+ ),
+ 'token/' => array(
+ '' => 'PhabricatorConduitTokenController',
+ 'edit/(?:(?P<id>\d+)/)?' =>
+ 'PhabricatorConduitTokenEditController',
+ 'terminate/(?:(?P<id>\d+)/)?' =>
+ 'PhabricatorConduitTokenTerminateController',
+ ),
'login/' => 'PhabricatorConduitTokenHandshakeController',
),
'/api/(?P<method>[^/]+)' => 'PhabricatorConduitAPIController',
);
}
}
diff --git a/src/applications/conduit/controller/PhabricatorConduitAPIController.php b/src/applications/conduit/controller/PhabricatorConduitAPIController.php
index f217adf15..cab927667 100644
--- a/src/applications/conduit/controller/PhabricatorConduitAPIController.php
+++ b/src/applications/conduit/controller/PhabricatorConduitAPIController.php
@@ -1,714 +1,723 @@
<?php
final class PhabricatorConduitAPIController
extends PhabricatorConduitController {
public function shouldRequireLogin() {
return false;
}
public function handleRequest(AphrontRequest $request) {
$method = $request->getURIData('method');
$time_start = microtime(true);
$api_request = null;
$method_implementation = null;
$log = new PhabricatorConduitMethodCallLog();
$log->setMethod($method);
$metadata = array();
$multimeter = MultimeterControl::getInstance();
if ($multimeter) {
$multimeter->setEventContext('api.'.$method);
}
try {
list($metadata, $params, $strictly_typed) = $this->decodeConduitParams(
$request,
$method);
$call = new ConduitCall($method, $params, $strictly_typed);
$method_implementation = $call->getMethodImplementation();
$result = null;
// TODO: The relationship between ConduitAPIRequest and ConduitCall is a
// little odd here and could probably be improved. Specifically, the
// APIRequest is a sub-object of the Call, which does not parallel the
// role of AphrontRequest (which is an indepenent object).
// In particular, the setUser() and getUser() existing independently on
// the Call and APIRequest is very awkward.
$api_request = $call->getAPIRequest();
$allow_unguarded_writes = false;
$auth_error = null;
$conduit_username = '-';
if ($call->shouldRequireAuthentication()) {
$auth_error = $this->authenticateUser($api_request, $metadata, $method);
// If we've explicitly authenticated the user here and either done
// CSRF validation or are using a non-web authentication mechanism.
$allow_unguarded_writes = true;
if ($auth_error === null) {
$conduit_user = $api_request->getUser();
if ($conduit_user && $conduit_user->getPHID()) {
$conduit_username = $conduit_user->getUsername();
}
$call->setUser($api_request->getUser());
}
}
$access_log = PhabricatorAccessLog::getLog();
if ($access_log) {
$access_log->setData(
array(
'u' => $conduit_username,
'm' => $method,
));
}
if ($call->shouldAllowUnguardedWrites()) {
$allow_unguarded_writes = true;
}
if ($auth_error === null) {
if ($allow_unguarded_writes) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
}
try {
$result = $call->execute();
$error_code = null;
$error_info = null;
} catch (ConduitException $ex) {
$result = null;
$error_code = $ex->getMessage();
if ($ex->getErrorDescription()) {
$error_info = $ex->getErrorDescription();
} else {
$error_info = $call->getErrorDescription($error_code);
}
}
if ($allow_unguarded_writes) {
unset($unguarded);
}
} else {
list($error_code, $error_info) = $auth_error;
}
} catch (Exception $ex) {
if (!($ex instanceof ConduitMethodNotFoundException)) {
phlog($ex);
}
$result = null;
$error_code = ($ex instanceof ConduitException
? 'ERR-CONDUIT-CALL'
: 'ERR-CONDUIT-CORE');
$error_info = $ex->getMessage();
}
$log
->setCallerPHID(
isset($conduit_user)
? $conduit_user->getPHID()
: null)
->setError((string)$error_code)
->setDuration(phutil_microseconds_since($time_start));
if (!PhabricatorEnv::isReadOnly()) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$log->save();
unset($unguarded);
}
$response = id(new ConduitAPIResponse())
->setResult($result)
->setErrorCode($error_code)
->setErrorInfo($error_info);
switch ($request->getStr('output')) {
case 'human':
return $this->buildHumanReadableResponse(
$method,
$api_request,
$response->toDictionary(),
$method_implementation);
case 'json':
default:
return id(new AphrontJSONResponse())
->setAddJSONShield(false)
->setContent($response->toDictionary());
}
}
/**
* Authenticate the client making the request to a Phabricator user account.
*
* @param ConduitAPIRequest Request being executed.
* @param dict Request metadata.
* @return null|pair Null to indicate successful authentication, or
* an error code and error message pair.
*/
private function authenticateUser(
ConduitAPIRequest $api_request,
array $metadata,
$method) {
$request = $this->getRequest();
if ($request->getUser()->getPHID()) {
$request->validateCSRF();
return $this->validateAuthenticatedUser(
$api_request,
$request->getUser());
}
$auth_type = idx($metadata, 'auth.type');
if ($auth_type === ConduitClient::AUTH_ASYMMETRIC) {
$host = idx($metadata, 'auth.host');
if (!$host) {
return array(
'ERR-INVALID-AUTH',
pht(
'Request is missing required "%s" parameter.',
'auth.host'),
);
}
// TODO: Validate that we are the host!
$raw_key = idx($metadata, 'auth.key');
$public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($raw_key);
$ssl_public_key = $public_key->toPKCS8();
// First, verify the signature.
try {
$protocol_data = $metadata;
ConduitClient::verifySignature(
$method,
$api_request->getAllParameters(),
$protocol_data,
$ssl_public_key);
} catch (Exception $ex) {
return array(
'ERR-INVALID-AUTH',
pht(
'Signature verification failure. %s',
$ex->getMessage()),
);
}
// If the signature is valid, find the user or device which is
// associated with this public key.
$stored_key = id(new PhabricatorAuthSSHKeyQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withKeys(array($public_key))
->withIsActive(true)
->executeOne();
if (!$stored_key) {
$key_summary = id(new PhutilUTF8StringTruncator())
->setMaximumBytes(64)
->truncateString($raw_key);
return array(
'ERR-INVALID-AUTH',
pht(
'No user or device is associated with the public key "%s".',
$key_summary),
);
}
$object = $stored_key->getObject();
if ($object instanceof PhabricatorUser) {
$user = $object;
} else {
if (!$stored_key->getIsTrusted()) {
return array(
'ERR-INVALID-AUTH',
pht(
'The key which signed this request is not trusted. Only '.
'trusted keys can be used to sign API calls.'),
);
}
if (!PhabricatorEnv::isClusterRemoteAddress()) {
return array(
'ERR-INVALID-AUTH',
pht(
'This request originates from outside of the Phabricator '.
'cluster address range. Requests signed with trusted '.
'device keys must originate from within the cluster.'),
);
}
$user = PhabricatorUser::getOmnipotentUser();
// Flag this as an intracluster request.
$api_request->setIsClusterRequest(true);
}
return $this->validateAuthenticatedUser(
$api_request,
$user);
} else if ($auth_type === null) {
// No specified authentication type, continue with other authentication
// methods below.
} else {
return array(
'ERR-INVALID-AUTH',
pht(
'Provided "%s" ("%s") is not recognized.',
'auth.type',
$auth_type),
);
}
$token_string = idx($metadata, 'token');
if (strlen($token_string)) {
if (strlen($token_string) != 32) {
return array(
'ERR-INVALID-AUTH',
pht(
'API token "%s" has the wrong length. API tokens should be '.
'32 characters long.',
$token_string),
);
}
$type = head(explode('-', $token_string));
$valid_types = PhabricatorConduitToken::getAllTokenTypes();
$valid_types = array_fuse($valid_types);
if (empty($valid_types[$type])) {
return array(
'ERR-INVALID-AUTH',
pht(
'API token "%s" has the wrong format. API tokens should be '.
'32 characters long and begin with one of these prefixes: %s.',
$token_string,
implode(', ', $valid_types)),
);
}
$token = id(new PhabricatorConduitTokenQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withTokens(array($token_string))
->withExpired(false)
->executeOne();
if (!$token) {
$token = id(new PhabricatorConduitTokenQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withTokens(array($token_string))
->withExpired(true)
->executeOne();
if ($token) {
return array(
'ERR-INVALID-AUTH',
pht(
'API token "%s" was previously valid, but has expired.',
$token_string),
);
} else {
return array(
'ERR-INVALID-AUTH',
pht(
'API token "%s" is not valid.',
$token_string),
);
}
}
// If this is a "cli-" token, it expires shortly after it is generated
// by default. Once it is actually used, we extend its lifetime and make
// it permanent. This allows stray tokens to get cleaned up automatically
// if they aren't being used.
if ($token->getTokenType() == PhabricatorConduitToken::TYPE_COMMANDLINE) {
if ($token->getExpires()) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$token->setExpires(null);
$token->save();
unset($unguarded);
}
}
// If this is a "clr-" token, Phabricator must be configured in cluster
// mode and the remote address must be a cluster node.
if ($token->getTokenType() == PhabricatorConduitToken::TYPE_CLUSTER) {
if (!PhabricatorEnv::isClusterRemoteAddress()) {
return array(
'ERR-INVALID-AUTH',
pht(
'This request originates from outside of the Phabricator '.
'cluster address range. Requests signed with cluster API '.
'tokens must originate from within the cluster.'),
);
}
// Flag this as an intracluster request.
$api_request->setIsClusterRequest(true);
}
$user = $token->getObject();
if (!($user instanceof PhabricatorUser)) {
return array(
'ERR-INVALID-AUTH',
pht('API token is not associated with a valid user.'),
);
}
return $this->validateAuthenticatedUser(
$api_request,
$user);
}
$access_token = idx($metadata, 'access_token');
if ($access_token) {
$token = id(new PhabricatorOAuthServerAccessToken())
->loadOneWhere('token = %s', $access_token);
if (!$token) {
return array(
'ERR-INVALID-AUTH',
pht('Access token does not exist.'),
);
}
$oauth_server = new PhabricatorOAuthServer();
$authorization = $oauth_server->authorizeToken($token);
if (!$authorization) {
return array(
'ERR-INVALID-AUTH',
pht('Access token is invalid or expired.'),
);
}
$user = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($token->getUserPHID()))
->executeOne();
if (!$user) {
return array(
'ERR-INVALID-AUTH',
pht('Access token is for invalid user.'),
);
}
$ok = $this->authorizeOAuthMethodAccess($authorization, $method);
if (!$ok) {
return array(
'ERR-OAUTH-ACCESS',
pht('You do not have authorization to call this method.'),
);
}
$api_request->setOAuthToken($token);
return $this->validateAuthenticatedUser(
$api_request,
$user);
}
// For intracluster requests, use a public user if no authentication
// information is provided. We could do this safely for any request,
// but making the API fully public means there's no way to disable badly
// behaved clients.
if (PhabricatorEnv::isClusterRemoteAddress()) {
if (PhabricatorEnv::getEnvConfig('policy.allow-public')) {
$api_request->setIsClusterRequest(true);
$user = new PhabricatorUser();
return $this->validateAuthenticatedUser(
$api_request,
$user);
}
}
// Handle sessionless auth.
// TODO: This is super messy.
// TODO: Remove this in favor of token-based auth.
if (isset($metadata['authUser'])) {
$user = id(new PhabricatorUser())->loadOneWhere(
'userName = %s',
$metadata['authUser']);
if (!$user) {
return array(
'ERR-INVALID-AUTH',
pht('Authentication is invalid.'),
);
}
$token = idx($metadata, 'authToken');
$signature = idx($metadata, 'authSignature');
$certificate = $user->getConduitCertificate();
$hash = sha1($token.$certificate);
if (!phutil_hashes_are_identical($hash, $signature)) {
return array(
'ERR-INVALID-AUTH',
pht('Authentication is invalid.'),
);
}
return $this->validateAuthenticatedUser(
$api_request,
$user);
}
// Handle session-based auth.
// TODO: Remove this in favor of token-based auth.
$session_key = idx($metadata, 'sessionKey');
if (!$session_key) {
return array(
'ERR-INVALID-SESSION',
pht('Session key is not present.'),
);
}
$user = id(new PhabricatorAuthSessionEngine())
->loadUserForSession(PhabricatorAuthSession::TYPE_CONDUIT, $session_key);
if (!$user) {
return array(
'ERR-INVALID-SESSION',
pht('Session key is invalid.'),
);
}
return $this->validateAuthenticatedUser(
$api_request,
$user);
}
private function validateAuthenticatedUser(
ConduitAPIRequest $request,
PhabricatorUser $user) {
if (!$user->canEstablishAPISessions()) {
return array(
'ERR-INVALID-AUTH',
pht('User account is not permitted to use the API.'),
);
}
$request->setUser($user);
id(new PhabricatorAuthSessionEngine())
->willServeRequestForUser($user);
return null;
}
private function buildHumanReadableResponse(
$method,
ConduitAPIRequest $request = null,
$result = null,
ConduitAPIMethod $method_implementation = null) {
$param_rows = array();
$param_rows[] = array('Method', $this->renderAPIValue($method));
if ($request) {
foreach ($request->getAllParameters() as $key => $value) {
$param_rows[] = array(
$key,
$this->renderAPIValue($value),
);
}
}
$param_table = new AphrontTableView($param_rows);
$param_table->setColumnClasses(
array(
'header',
'wide',
));
$result_rows = array();
foreach ($result as $key => $value) {
$result_rows[] = array(
$key,
$this->renderAPIValue($value),
);
}
$result_table = new AphrontTableView($result_rows);
$result_table->setColumnClasses(
array(
'header',
'wide',
));
$param_panel = id(new PHUIObjectBoxView())
->setHeaderText(pht('Method Parameters'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($param_table);
$result_panel = id(new PHUIObjectBoxView())
->setHeaderText(pht('Method Result'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($result_table);
$method_uri = $this->getApplicationURI('method/'.$method.'/');
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb($method, $method_uri)
->addTextCrumb(pht('Call'))
->setBorder(true);
$example_panel = null;
if ($request && $method_implementation) {
$params = $request->getAllParameters();
$example_panel = $this->renderExampleBox(
$method_implementation,
$params);
}
$title = pht('Method Call Result');
$header = id(new PHUIHeaderView())
->setHeader($title)
->setHeaderIcon('fa-exchange');
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter(array(
$param_panel,
$result_panel,
$example_panel,
));
$title = pht('Method Call Result');
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
private function renderAPIValue($value) {
$json = new PhutilJSON();
if (is_array($value)) {
$value = $json->encodeFormatted($value);
}
$value = phutil_tag(
'pre',
array('style' => 'white-space: pre-wrap;'),
$value);
return $value;
}
private function decodeConduitParams(
AphrontRequest $request,
$method) {
+ $content_type = $request->getHTTPHeader('Content-Type');
+
+ if ($content_type == 'application/json') {
+ throw new Exception(
+ pht('Use form-encoded data to submit parameters to Conduit endpoints. '.
+ 'Sending a JSON-encoded body and setting \'Content-Type\': '.
+ '\'application/json\' is not currently supported.'));
+ }
+
// Look for parameters from the Conduit API Console, which are encoded
// as HTTP POST parameters in an array, e.g.:
//
// params[name]=value&params[name2]=value2
//
// The fields are individually JSON encoded, since we require users to
// enter JSON so that we avoid type ambiguity.
$params = $request->getArr('params', null);
if ($params !== null) {
foreach ($params as $key => $value) {
if ($value == '') {
// Interpret empty string null (e.g., the user didn't type anything
// into the box).
$value = 'null';
}
$decoded_value = json_decode($value, true);
if ($decoded_value === null && strtolower($value) != 'null') {
// When json_decode() fails, it returns null. This almost certainly
// indicates that a user was using the web UI and didn't put quotes
// around a string value. We can either do what we think they meant
// (treat it as a string) or fail. For now, err on the side of
// caution and fail. In the future, if we make the Conduit API
// actually do type checking, it might be reasonable to treat it as
// a string if the parameter type is string.
throw new Exception(
pht(
"The value for parameter '%s' is not valid JSON. All ".
"parameters must be encoded as JSON values, including strings ".
"(which means you need to surround them in double quotes). ".
"Check your syntax. Value was: %s.",
$key,
$value));
}
$params[$key] = $decoded_value;
}
$metadata = idx($params, '__conduit__', array());
unset($params['__conduit__']);
return array($metadata, $params, true);
}
// Otherwise, look for a single parameter called 'params' which has the
// entire param dictionary JSON encoded.
$params_json = $request->getStr('params');
if (strlen($params_json)) {
$params = null;
try {
$params = phutil_json_decode($params_json);
} catch (PhutilJSONParserException $ex) {
throw new PhutilProxyException(
pht(
"Invalid parameter information was passed to method '%s'.",
$method),
$ex);
}
$metadata = idx($params, '__conduit__', array());
unset($params['__conduit__']);
return array($metadata, $params, true);
}
// If we do not have `params`, assume this is a simple HTTP request with
// HTTP key-value pairs.
$params = array();
$metadata = array();
foreach ($request->getPassthroughRequestData() as $key => $value) {
$meta_key = ConduitAPIMethod::getParameterMetadataKey($key);
if ($meta_key !== null) {
$metadata[$meta_key] = $value;
} else {
$params[$key] = $value;
}
}
return array($metadata, $params, false);
}
private function authorizeOAuthMethodAccess(
PhabricatorOAuthClientAuthorization $authorization,
$method_name) {
$method = ConduitAPIMethod::getConduitMethod($method_name);
if (!$method) {
return false;
}
$required_scope = $method->getRequiredScope();
switch ($required_scope) {
case ConduitAPIMethod::SCOPE_ALWAYS:
return true;
case ConduitAPIMethod::SCOPE_NEVER:
return false;
}
$authorization_scope = $authorization->getScope();
if (!empty($authorization_scope[$required_scope])) {
return true;
}
return false;
}
}
diff --git a/src/applications/conduit/query/PhabricatorConduitLogQuery.php b/src/applications/conduit/query/PhabricatorConduitLogQuery.php
index 0dacd9406..6f0f6131d 100644
--- a/src/applications/conduit/query/PhabricatorConduitLogQuery.php
+++ b/src/applications/conduit/query/PhabricatorConduitLogQuery.php
@@ -1,82 +1,104 @@
<?php
final class PhabricatorConduitLogQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $callerPHIDs;
private $methods;
private $methodStatuses;
+ private $epochMin;
+ private $epochMax;
public function withCallerPHIDs(array $phids) {
$this->callerPHIDs = $phids;
return $this;
}
public function withMethods(array $methods) {
$this->methods = $methods;
return $this;
}
public function withMethodStatuses(array $statuses) {
$this->methodStatuses = $statuses;
return $this;
}
+ public function withEpochBetween($epoch_min, $epoch_max) {
+ $this->epochMin = $epoch_min;
+ $this->epochMax = $epoch_max;
+ return $this;
+ }
+
public function newResultObject() {
return new PhabricatorConduitMethodCallLog();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->callerPHIDs !== null) {
$where[] = qsprintf(
$conn,
'callerPHID IN (%Ls)',
$this->callerPHIDs);
}
if ($this->methods !== null) {
$where[] = qsprintf(
$conn,
'method IN (%Ls)',
$this->methods);
}
if ($this->methodStatuses !== null) {
$statuses = array_fuse($this->methodStatuses);
$methods = id(new PhabricatorConduitMethodQuery())
->setViewer($this->getViewer())
->execute();
$method_names = array();
foreach ($methods as $method) {
$status = $method->getMethodStatus();
if (isset($statuses[$status])) {
$method_names[] = $method->getAPIMethodName();
}
}
if (!$method_names) {
throw new PhabricatorEmptyQueryException();
}
$where[] = qsprintf(
$conn,
'method IN (%Ls)',
$method_names);
}
+ if ($this->epochMin !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'dateCreated >= %d',
+ $this->epochMin);
+ }
+
+ if ($this->epochMax !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'dateCreated <= %d',
+ $this->epochMax);
+ }
+
return $where;
}
public function getQueryApplicationClass() {
return 'PhabricatorConduitApplication';
}
}
diff --git a/src/applications/conduit/query/PhabricatorConduitLogSearchEngine.php b/src/applications/conduit/query/PhabricatorConduitLogSearchEngine.php
index 20d034168..ab86ae666 100644
--- a/src/applications/conduit/query/PhabricatorConduitLogSearchEngine.php
+++ b/src/applications/conduit/query/PhabricatorConduitLogSearchEngine.php
@@ -1,208 +1,276 @@
<?php
final class PhabricatorConduitLogSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Conduit Logs');
}
public function getApplicationClassName() {
return 'PhabricatorConduitApplication';
}
public function canUseInPanelContext() {
return false;
}
public function newQuery() {
return new PhabricatorConduitLogQuery();
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
if ($map['callerPHIDs']) {
$query->withCallerPHIDs($map['callerPHIDs']);
}
if ($map['methods']) {
$query->withMethods($map['methods']);
}
if ($map['statuses']) {
$query->withMethodStatuses($map['statuses']);
}
+ if ($map['epochMin'] || $map['epochMax']) {
+ $query->withEpochBetween(
+ $map['epochMin'],
+ $map['epochMax']);
+ }
+
return $query;
}
protected function buildCustomSearchFields() {
return array(
id(new PhabricatorUsersSearchField())
->setKey('callerPHIDs')
->setLabel(pht('Callers'))
->setAliases(array('caller', 'callers'))
->setDescription(pht('Find calls by specific users.')),
id(new PhabricatorSearchStringListField())
->setKey('methods')
->setLabel(pht('Methods'))
->setDescription(pht('Find calls to specific methods.')),
id(new PhabricatorSearchCheckboxesField())
->setKey('statuses')
->setLabel(pht('Method Status'))
->setAliases(array('status'))
->setDescription(
pht('Find calls to stable, unstable, or deprecated methods.'))
->setOptions(ConduitAPIMethod::getMethodStatusMap()),
+ id(new PhabricatorSearchDateField())
+ ->setLabel(pht('Called After'))
+ ->setKey('epochMin'),
+ id(new PhabricatorSearchDateField())
+ ->setLabel(pht('Called Before'))
+ ->setKey('epochMax'),
);
}
protected function getURI($path) {
return '/conduit/log/'.$path;
}
protected function getBuiltinQueryNames() {
$names = array();
$viewer = $this->requireViewer();
if ($viewer->isLoggedIn()) {
$names['viewer'] = pht('My Calls');
$names['viewerdeprecated'] = pht('My Deprecated Calls');
}
$names['all'] = pht('All Call Logs');
$names['deprecated'] = pht('Deprecated Call Logs');
return $names;
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
$viewer = $this->requireViewer();
$viewer_phid = $viewer->getPHID();
$deprecated = array(
ConduitAPIMethod::METHOD_STATUS_DEPRECATED,
);
switch ($query_key) {
case 'viewer':
return $query
->setParameter('callerPHIDs', array($viewer_phid));
case 'viewerdeprecated':
return $query
->setParameter('callerPHIDs', array($viewer_phid))
->setParameter('statuses', $deprecated);
case 'deprecated':
return $query
->setParameter('statuses', $deprecated);
case 'all':
return $query;
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
+ protected function newExportFields() {
+ $viewer = $this->requireViewer();
+
+ return array(
+ id(new PhabricatorPHIDExportField())
+ ->setKey('callerPHID')
+ ->setLabel(pht('Caller PHID')),
+ id(new PhabricatorStringExportField())
+ ->setKey('caller')
+ ->setLabel(pht('Caller')),
+ id(new PhabricatorStringExportField())
+ ->setKey('method')
+ ->setLabel(pht('Method')),
+ id(new PhabricatorIntExportField())
+ ->setKey('duration')
+ ->setLabel(pht('Call Duration (us)')),
+ id(new PhabricatorStringExportField())
+ ->setKey('error')
+ ->setLabel(pht('Error')),
+ );
+ }
+
+ protected function newExportData(array $logs) {
+ $viewer = $this->requireViewer();
+
+ $phids = array();
+ foreach ($logs as $log) {
+ if ($log->getCallerPHID()) {
+ $phids[] = $log->getCallerPHID();
+ }
+ }
+ $handles = $viewer->loadHandles($phids);
+
+ $export = array();
+ foreach ($logs as $log) {
+ $caller_phid = $log->getCallerPHID();
+ if ($caller_phid) {
+ $caller_name = $handles[$caller_phid]->getName();
+ } else {
+ $caller_name = null;
+ }
+
+ $map = array(
+ 'callerPHID' => $caller_phid,
+ 'caller' => $caller_name,
+ 'method' => $log->getMethod(),
+ 'duration' => (int)$log->getDuration(),
+ 'error' => $log->getError(),
+ );
+
+ $export[] = $map;
+ }
+
+ return $export;
+ }
+
protected function renderResultList(
array $logs,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($logs, 'PhabricatorConduitMethodCallLog');
$viewer = $this->requireViewer();
$methods = id(new PhabricatorConduitMethodQuery())
->setViewer($viewer)
->execute();
$methods = mpull($methods, null, 'getAPIMethodName');
Javelin::initBehavior('phabricator-tooltips');
$viewer = $this->requireViewer();
$rows = array();
foreach ($logs as $log) {
$caller_phid = $log->getCallerPHID();
if ($caller_phid) {
$caller = $viewer->renderHandle($caller_phid);
} else {
$caller = null;
}
$method = idx($methods, $log->getMethod());
if ($method) {
$method_status = $method->getMethodStatus();
} else {
$method_status = null;
}
switch ($method_status) {
case ConduitAPIMethod::METHOD_STATUS_STABLE:
$status = null;
break;
case ConduitAPIMethod::METHOD_STATUS_UNSTABLE:
$status = id(new PHUIIconView())
->setIcon('fa-exclamation-triangle yellow')
->addSigil('has-tooltip')
->setMetadata(
array(
'tip' => pht('Unstable'),
));
break;
case ConduitAPIMethod::METHOD_STATUS_DEPRECATED:
$status = id(new PHUIIconView())
->setIcon('fa-exclamation-triangle red')
->addSigil('has-tooltip')
->setMetadata(
array(
'tip' => pht('Deprecated'),
));
break;
default:
$status = id(new PHUIIconView())
->setIcon('fa-question-circle')
->addSigil('has-tooltip')
->setMetadata(
array(
'tip' => pht('Unknown ("%s")', $method_status),
));
break;
}
$rows[] = array(
$status,
$log->getMethod(),
$caller,
$log->getError(),
pht('%s us', new PhutilNumber($log->getDuration())),
phabricator_datetime($log->getDateCreated(), $viewer),
);
}
$table = id(new AphrontTableView($rows))
->setHeaders(
array(
null,
pht('Method'),
pht('Caller'),
pht('Error'),
pht('Duration'),
pht('Date'),
))
->setColumnClasses(
array(
null,
'pri',
null,
'wide right',
null,
null,
));
return id(new PhabricatorApplicationSearchResultView())
->setTable($table)
->setNoDataString(pht('No matching calls in log.'));
}
}
diff --git a/src/applications/conduit/settings/PhabricatorConduitTokensSettingsPanel.php b/src/applications/conduit/settings/PhabricatorConduitTokensSettingsPanel.php
index 207558238..cd97e2fd7 100644
--- a/src/applications/conduit/settings/PhabricatorConduitTokensSettingsPanel.php
+++ b/src/applications/conduit/settings/PhabricatorConduitTokensSettingsPanel.php
@@ -1,118 +1,122 @@
<?php
final class PhabricatorConduitTokensSettingsPanel
extends PhabricatorSettingsPanel {
public function isManagementPanel() {
if ($this->getUser()->getIsMailingList()) {
return false;
}
return true;
}
public function getPanelKey() {
return 'apitokens';
}
public function getPanelName() {
return pht('Conduit API Tokens');
}
+ public function getPanelMenuIcon() {
+ return id(new PhabricatorConduitApplication())->getIcon();
+ }
+
public function getPanelGroupKey() {
return PhabricatorSettingsLogsPanelGroup::PANELGROUPKEY;
}
public function isEnabled() {
return true;
}
public function processRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$user = $this->getUser();
$tokens = id(new PhabricatorConduitTokenQuery())
->setViewer($viewer)
->withObjectPHIDs(array($user->getPHID()))
->withExpired(false)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->execute();
$rows = array();
foreach ($tokens as $token) {
$rows[] = array(
javelin_tag(
'a',
array(
'href' => '/conduit/token/edit/'.$token->getID().'/',
'sigil' => 'workflow',
),
$token->getPublicTokenName()),
PhabricatorConduitToken::getTokenTypeName($token->getTokenType()),
phabricator_datetime($token->getDateCreated(), $viewer),
($token->getExpires()
? phabricator_datetime($token->getExpires(), $viewer)
: pht('Never')),
javelin_tag(
'a',
array(
'class' => 'button small button-grey',
'href' => '/conduit/token/terminate/'.$token->getID().'/',
'sigil' => 'workflow',
),
pht('Terminate')),
);
}
$table = new AphrontTableView($rows);
$table->setNoDataString(pht("You don't have any active API tokens."));
$table->setHeaders(
array(
pht('Token'),
pht('Type'),
pht('Created'),
pht('Expires'),
null,
));
$table->setColumnClasses(
array(
'wide pri',
'',
'right',
'right',
'action',
));
$generate_button = id(new PHUIButtonView())
->setText(pht('Generate Token'))
->setHref('/conduit/token/edit/?objectPHID='.$user->getPHID())
->setTag('a')
->setWorkflow(true)
->setIcon('fa-plus');
$terminate_button = id(new PHUIButtonView())
->setText(pht('Terminate Tokens'))
->setHref('/conduit/token/terminate/?objectPHID='.$user->getPHID())
->setTag('a')
->setWorkflow(true)
->setIcon('fa-exclamation-triangle')
->setColor(PHUIButtonView::RED);
$header = id(new PHUIHeaderView())
->setHeader(pht('Active API Tokens'))
->addActionLink($generate_button)
->addActionLink($terminate_button);
$panel = id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
->appendChild($table);
return $panel;
}
}
diff --git a/src/applications/config/check/PhabricatorExtensionsSetupCheck.php b/src/applications/config/check/PhabricatorExtensionsSetupCheck.php
index 973c80629..51105de1c 100644
--- a/src/applications/config/check/PhabricatorExtensionsSetupCheck.php
+++ b/src/applications/config/check/PhabricatorExtensionsSetupCheck.php
@@ -1,55 +1,54 @@
<?php
final class PhabricatorExtensionsSetupCheck extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_PHP;
}
public function isPreflightCheck() {
return true;
}
protected function executeChecks() {
- // TODO: Make 'mbstring' and 'iconv' soft requirements.
+ // TODO: Make 'mbstring' a soft requirement.
$required = array(
'hash',
'json',
'openssl',
'mbstring',
- 'iconv',
'ctype',
// There is a tiny chance we might not need this, but a significant
// number of applications require it and it's widely available.
'curl',
);
$need = array();
foreach ($required as $extension) {
if (!extension_loaded($extension)) {
$need[] = $extension;
}
}
if (!extension_loaded('mysqli') && !extension_loaded('mysql')) {
$need[] = 'mysqli or mysql';
}
if (!$need) {
return;
}
$message = pht('Required PHP extensions are not installed.');
$issue = $this->newIssue('php.extensions')
->setIsFatal(true)
->setName(pht('Missing Required Extensions'))
->setMessage($message);
foreach ($need as $extension) {
$issue->addPHPExtension($extension);
}
}
}
diff --git a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php
index 4bf51602b..1c8a593a7 100644
--- a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php
+++ b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php
@@ -1,362 +1,428 @@
<?php
final class PhabricatorExtraConfigSetupCheck extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_OTHER;
}
protected function executeChecks() {
$ancient_config = self::getAncientConfig();
$all_keys = PhabricatorEnv::getAllConfigKeys();
$all_keys = array_keys($all_keys);
sort($all_keys);
$defined_keys = PhabricatorApplicationConfigOptions::loadAllOptions();
foreach ($all_keys as $key) {
if (isset($defined_keys[$key])) {
continue;
}
if (isset($ancient_config[$key])) {
$summary = pht(
'This option has been removed. You may delete it at your '.
'convenience.');
$message = pht(
"The configuration option '%s' has been removed. You may delete ".
"it at your convenience.".
"\n\n%s",
$key,
$ancient_config[$key]);
$short = pht('Obsolete Config');
$name = pht('Obsolete Configuration Option "%s"', $key);
} else {
$summary = pht('This option is not recognized. It may be misspelled.');
$message = pht(
"The configuration option '%s' is not recognized. It may be ".
"misspelled, or it might have existed in an older version of ".
"Phabricator. It has no effect, and should be corrected or deleted.",
$key);
$short = pht('Unknown Config');
$name = pht('Unknown Configuration Option "%s"', $key);
}
$issue = $this->newIssue('config.unknown.'.$key)
->setShortName($short)
->setName($name)
->setSummary($summary);
$stack = PhabricatorEnv::getConfigSourceStack();
$stack = $stack->getStack();
$found = array();
$found_local = false;
$found_database = false;
foreach ($stack as $source_key => $source) {
$value = $source->getKeys(array($key));
if ($value) {
$found[] = $source->getName();
if ($source instanceof PhabricatorConfigDatabaseSource) {
$found_database = true;
}
if ($source instanceof PhabricatorConfigLocalSource) {
$found_local = true;
}
}
}
$message = $message."\n\n".pht(
'This configuration value is defined in these %d '.
'configuration source(s): %s.',
count($found),
implode(', ', $found));
$issue->setMessage($message);
if ($found_local) {
$command = csprintf('phabricator/ $ ./bin/config delete %s', $key);
$issue->addCommand($command);
}
if ($found_database) {
$issue->addPhabricatorConfig($key);
}
}
}
/**
* Return a map of deleted config options. Keys are option keys; values are
* explanations of what happened to the option.
*/
public static function getAncientConfig() {
$reason_auth = pht(
'This option has been migrated to the "Auth" application. Your old '.
'configuration is still in effect, but now stored in "Auth" instead of '.
'configuration. Going forward, you can manage authentication from '.
'the web UI.');
$auth_config = array(
'controller.oauth-registration',
'auth.password-auth-enabled',
'facebook.auth-enabled',
'facebook.registration-enabled',
'facebook.auth-permanent',
'facebook.application-id',
'facebook.application-secret',
'facebook.require-https-auth',
'github.auth-enabled',
'github.registration-enabled',
'github.auth-permanent',
'github.application-id',
'github.application-secret',
'google.auth-enabled',
'google.registration-enabled',
'google.auth-permanent',
'google.application-id',
'google.application-secret',
'ldap.auth-enabled',
'ldap.hostname',
'ldap.port',
'ldap.base_dn',
'ldap.search_attribute',
'ldap.search-first',
'ldap.username-attribute',
'ldap.real_name_attributes',
'ldap.activedirectory_domain',
'ldap.version',
'ldap.referrals',
'ldap.anonymous-user-name',
'ldap.anonymous-user-password',
'ldap.start-tls',
'disqus.auth-enabled',
'disqus.registration-enabled',
'disqus.auth-permanent',
'disqus.application-id',
'disqus.application-secret',
'phabricator.oauth-uri',
'phabricator.auth-enabled',
'phabricator.registration-enabled',
'phabricator.auth-permanent',
'phabricator.application-id',
'phabricator.application-secret',
);
$ancient_config = array_fill_keys($auth_config, $reason_auth);
$markup_reason = pht(
'Custom remarkup rules are now added by subclassing '.
'%s or %s.',
'PhabricatorRemarkupCustomInlineRule',
'PhabricatorRemarkupCustomBlockRule');
$session_reason = pht(
'Sessions now expire and are garbage collected rather than having an '.
'arbitrary concurrency limit.');
$differential_field_reason = pht(
'All Differential fields are now managed through the configuration '.
'option "%s". Use that option to configure which fields are shown.',
'differential.fields');
$reply_domain_reason = pht(
'Individual application reply handler domains have been removed. '.
'Configure a reply domain with "%s".',
'metamta.reply-handler-domain');
$reply_handler_reason = pht(
'Reply handlers can no longer be overridden with configuration.');
$monospace_reason = pht(
'Phabricator no longer supports global customization of monospaced '.
'fonts.');
$public_mail_reason = pht(
'Inbound mail addresses are now configured for each application '.
'in the Applications tool.');
$gc_reason = pht(
'Garbage collectors are now configured with "%s".',
'bin/garbage set-policy');
$aphlict_reason = pht(
'Configuration of the notification server has changed substantially. '.
'For discussion, see T10794.');
$stale_reason = pht(
'The Differential revision list view age UI elements have been removed '.
'to simplify the interface.');
$global_settings_reason = pht(
'The "Re: Prefix" and "Vary Subjects" settings are now configured '.
'in global settings.');
$dashboard_reason = pht(
- 'This option has been removed, you can use Dashboards to provide '.
- 'homepage customization. See T11533 for more details.');
+ 'This option has been removed, you can use Dashboards to provide '.
+ 'homepage customization. See T11533 for more details.');
$elastic_reason = pht(
- 'Elasticsearch is now configured with "%s".',
- 'cluster.search');
+ 'Elasticsearch is now configured with "%s".',
+ 'cluster.search');
+
+ $mailers_reason = pht(
+ 'Inbound and outbound mail is now configured with "cluster.mailers".');
+
+ $prefix_reason = pht(
+ 'Per-application mail subject prefix customization is no longer '.
+ 'directly supported. Prefixes and other strings may be customized with '.
+ '"translation.override".');
$ancient_config += array(
'phid.external-loaders' =>
pht(
'External loaders have been replaced. Extend `%s` '.
'to implement new PHID and handle types.',
'PhabricatorPHIDType'),
'maniphest.custom-task-extensions-class' =>
pht(
'Maniphest fields are now loaded automatically. '.
'You can configure them with `%s`.',
'maniphest.fields'),
'maniphest.custom-fields' =>
pht(
'Maniphest fields are now defined in `%s`. '.
'Existing definitions have been migrated.',
'maniphest.custom-field-definitions'),
'differential.custom-remarkup-rules' => $markup_reason,
'differential.custom-remarkup-block-rules' => $markup_reason,
'auth.sshkeys.enabled' => pht(
'SSH keys are now actually useful, so they are always enabled.'),
'differential.anonymous-access' => pht(
'Phabricator now has meaningful global access controls. See `%s`.',
'policy.allow-public'),
'celerity.resource-path' => pht(
'An alternate resource map is no longer supported. Instead, use '.
'multiple maps. See T4222.'),
'metamta.send-immediately' => pht(
'Mail is now always delivered by the daemons.'),
'auth.sessions.conduit' => $session_reason,
'auth.sessions.web' => $session_reason,
'tokenizer.ondemand' => pht(
'Phabricator now manages typeahead strategies automatically.'),
'differential.revision-custom-detail-renderer' => pht(
'Obsolete; use standard rendering events instead.'),
'differential.show-host-field' => $differential_field_reason,
'differential.show-test-plan-field' => $differential_field_reason,
'differential.field-selector' => $differential_field_reason,
'phabricator.show-beta-applications' => pht(
'This option has been renamed to `%s` to emphasize the '.
'unfinished nature of many prototype applications. '.
'Your existing setting has been migrated.',
'phabricator.show-prototypes'),
'notification.user' => pht(
'The notification server no longer requires root permissions. Start '.
'the server as the user you want it to run under.'),
'notification.debug' => pht(
'Notifications no longer have a dedicated debugging mode.'),
'translation.provider' => pht(
'The translation implementation has changed and providers are no '.
'longer used or supported.'),
'config.mask' => pht(
'Use `%s` instead of this option.',
'config.hide'),
'phd.start-taskmasters' => pht(
'Taskmasters now use an autoscaling pool. You can configure the '.
'pool size with `%s`.',
'phd.taskmasters'),
'storage.engine-selector' => pht(
'Phabricator now automatically discovers available storage engines '.
'at runtime.'),
'storage.upload-size-limit' => pht(
'Phabricator now supports arbitrarily large files. Consult the '.
'documentation for configuration details.'),
'security.allow-outbound-http' => pht(
'This option has been replaced with the more granular option `%s`.',
'security.outbound-blacklist'),
'metamta.reply.show-hints' => pht(
'Phabricator no longer shows reply hints in mail.'),
'metamta.differential.reply-handler-domain' => $reply_domain_reason,
'metamta.diffusion.reply-handler-domain' => $reply_domain_reason,
'metamta.macro.reply-handler-domain' => $reply_domain_reason,
'metamta.maniphest.reply-handler-domain' => $reply_domain_reason,
'metamta.pholio.reply-handler-domain' => $reply_domain_reason,
'metamta.diffusion.reply-handler' => $reply_handler_reason,
'metamta.differential.reply-handler' => $reply_handler_reason,
'metamta.maniphest.reply-handler' => $reply_handler_reason,
'metamta.package.reply-handler' => $reply_handler_reason,
'metamta.precedence-bulk' => pht(
'Phabricator now always sends transaction mail with '.
'"Precedence: bulk" to improve deliverability.'),
'style.monospace' => $monospace_reason,
'style.monospace.windows' => $monospace_reason,
'search.engine-selector' => pht(
'Phabricator now automatically discovers available search engines '.
'at runtime.'),
'metamta.files.public-create-email' => $public_mail_reason,
'metamta.maniphest.public-create-email' => $public_mail_reason,
'metamta.maniphest.default-public-author' => $public_mail_reason,
'metamta.paste.public-create-email' => $public_mail_reason,
'security.allow-conduit-act-as-user' => pht(
'Impersonating users over the API is no longer supported.'),
'feed.public' => pht('The framable public feed is no longer supported.'),
'auth.login-message' => pht(
'This configuration option has been replaced with a modular '.
'handler. See T9346.'),
'gcdaemon.ttl.herald-transcripts' => $gc_reason,
'gcdaemon.ttl.daemon-logs' => $gc_reason,
'gcdaemon.ttl.differential-parse-cache' => $gc_reason,
'gcdaemon.ttl.markup-cache' => $gc_reason,
'gcdaemon.ttl.task-archive' => $gc_reason,
'gcdaemon.ttl.general-cache' => $gc_reason,
'gcdaemon.ttl.conduit-logs' => $gc_reason,
'phd.variant-config' => pht(
'This configuration is no longer relevant because daemons '.
'restart automatically on configuration changes.'),
'notification.ssl-cert' => $aphlict_reason,
'notification.ssl-key' => $aphlict_reason,
'notification.pidfile' => $aphlict_reason,
'notification.log' => $aphlict_reason,
'notification.enabled' => $aphlict_reason,
'notification.client-uri' => $aphlict_reason,
'notification.server-uri' => $aphlict_reason,
'metamta.differential.unified-comment-context' => pht(
'Inline comments are now always rendered with a limited amount '.
'of context.'),
'differential.days-fresh' => $stale_reason,
'differential.days-stale' => $stale_reason,
'metamta.re-prefix' => $global_settings_reason,
'metamta.vary-subjects' => $global_settings_reason,
'ui.custom-header' => pht(
'This option has been replaced with `ui.logo`, which provides more '.
'flexible configuration options.'),
'welcome.html' => $dashboard_reason,
'maniphest.priorities.unbreak-now' => $dashboard_reason,
'maniphest.priorities.needs-triage' => $dashboard_reason,
'mysql.implementation' => pht(
'Phabricator now automatically selects the best available '.
'MySQL implementation.'),
'mysql.configuration-provider' => pht(
'Phabricator now has application-level management of partitioning '.
'and replicas.'),
'search.elastic.host' => $elastic_reason,
'search.elastic.namespace' => $elastic_reason,
+ 'metamta.mail-adapter' => $mailers_reason,
+ 'amazon-ses.access-key' => $mailers_reason,
+ 'amazon-ses.secret-key' => $mailers_reason,
+ 'amazon-ses.endpoint' => $mailers_reason,
+ 'mailgun.domain' => $mailers_reason,
+ 'mailgun.api-key' => $mailers_reason,
+ 'phpmailer.mailer' => $mailers_reason,
+ 'phpmailer.smtp-host' => $mailers_reason,
+ 'phpmailer.smtp-port' => $mailers_reason,
+ 'phpmailer.smtp-protocol' => $mailers_reason,
+ 'phpmailer.smtp-user' => $mailers_reason,
+ 'phpmailer.smtp-password' => $mailers_reason,
+ 'phpmailer.smtp-encoding' => $mailers_reason,
+ 'sendgrid.api-user' => $mailers_reason,
+ 'sendgrid.api-key' => $mailers_reason,
+
+ 'celerity.resource-hash' => pht(
+ 'This option generally did not prove useful. Resource hash keys '.
+ 'are now managed automatically.'),
+ 'celerity.enable-deflate' => pht(
+ 'Resource deflation is now managed automatically.'),
+ 'celerity.minify' => pht(
+ 'Resource minification is now managed automatically.'),
+
+ 'metamta.domain' => pht(
+ 'Mail thread IDs are now generated automatically.'),
+ 'metamta.placeholder-to-recipient' => pht(
+ 'Placeholder recipients are now generated automatically.'),
+
+ 'metamta.mail-key' => pht(
+ 'Mail object address hash keys are now generated automatically.'),
+
+ 'phabricator.csrf-key' => pht(
+ 'CSRF HMAC keys are now managed automatically.'),
+
+ 'metamta.insecure-auth-with-reply-to' => pht(
+ 'Authenticating users based on "Reply-To" is no longer supported.'),
+
+ 'phabricator.allow-email-users' => pht(
+ 'Public email is now accepted if the associated address has a '.
+ 'default author, and rejected otherwise.'),
+
+ 'metamta.conpherence.subject-prefix' => $prefix_reason,
+ 'metamta.differential.subject-prefix' => $prefix_reason,
+ 'metamta.diffusion.subject-prefix' => $prefix_reason,
+ 'metamta.files.subject-prefix' => $prefix_reason,
+ 'metamta.legalpad.subject-prefix' => $prefix_reason,
+ 'metamta.macro.subject-prefix' => $prefix_reason,
+ 'metamta.maniphest.subject-prefix' => $prefix_reason,
+ 'metamta.package.subject-prefix' => $prefix_reason,
+ 'metamta.paste.subject-prefix' => $prefix_reason,
+ 'metamta.pholio.subject-prefix' => $prefix_reason,
+ 'metamta.phriction.subject-prefix' => $prefix_reason,
+
+ 'aphront.default-application-configuration-class' => pht(
+ 'This ancient extension point has been replaced with other '.
+ 'mechanisms, including "AphrontSite".'),
+
);
return $ancient_config;
}
}
diff --git a/src/applications/config/check/PhabricatorMailSetupCheck.php b/src/applications/config/check/PhabricatorMailSetupCheck.php
index b3b6143ad..c89e0036a 100644
--- a/src/applications/config/check/PhabricatorMailSetupCheck.php
+++ b/src/applications/config/check/PhabricatorMailSetupCheck.php
@@ -1,104 +1,24 @@
<?php
final class PhabricatorMailSetupCheck extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_OTHER;
}
protected function executeChecks() {
if (PhabricatorEnv::getEnvConfig('cluster.mailers')) {
return;
}
- $adapter = PhabricatorEnv::getEnvConfig('metamta.mail-adapter');
-
- switch ($adapter) {
- case 'PhabricatorMailImplementationPHPMailerLiteAdapter':
- if (!Filesystem::pathExists('/usr/bin/sendmail') &&
- !Filesystem::pathExists('/usr/sbin/sendmail')) {
- $message = pht(
- 'Mail is configured to send via sendmail, but this system has '.
- 'no sendmail binary. Install sendmail or choose a different '.
- 'mail adapter.');
-
- $this->newIssue('config.metamta.mail-adapter')
- ->setShortName(pht('Missing Sendmail'))
- ->setName(pht('No Sendmail Binary Found'))
- ->setMessage($message)
- ->addRelatedPhabricatorConfig('metamta.mail-adapter');
- }
- break;
- case 'PhabricatorMailImplementationAmazonSESAdapter':
- if (PhabricatorEnv::getEnvConfig('metamta.can-send-as-user')) {
- $message = pht(
- 'Amazon SES does not support sending email as users. Disable '.
- 'send as user, or choose a different mail adapter.');
-
- $this->newIssue('config.can-send-as-user')
- ->setName(pht("SES Can't Send As User"))
- ->setMessage($message)
- ->addRelatedPhabricatorConfig('metamta.mail-adapter')
- ->addPhabricatorConfig('metamta.can-send-as-user');
- }
-
- if (!PhabricatorEnv::getEnvConfig('amazon-ses.access-key')) {
- $message = pht(
- 'Amazon SES is selected as the mail adapter, but no SES access '.
- 'key is configured. Provide an SES access key, or choose a '.
- 'different mail adapter.');
-
- $this->newIssue('config.amazon-ses.access-key')
- ->setName(pht('Amazon SES Access Key Not Set'))
- ->setMessage($message)
- ->addRelatedPhabricatorConfig('metamta.mail-adapter')
- ->addPhabricatorConfig('amazon-ses.access-key');
- }
-
- if (!PhabricatorEnv::getEnvConfig('amazon-ses.secret-key')) {
- $message = pht(
- 'Amazon SES is selected as the mail adapter, but no SES secret '.
- 'key is configured. Provide an SES secret key, or choose a '.
- 'different mail adapter.');
-
- $this->newIssue('config.amazon-ses.secret-key')
- ->setName(pht('Amazon SES Secret Key Not Set'))
- ->setMessage($message)
- ->addRelatedPhabricatorConfig('metamta.mail-adapter')
- ->addPhabricatorConfig('amazon-ses.secret-key');
- }
-
- if (!PhabricatorEnv::getEnvConfig('amazon-ses.endpoint')) {
- $message = pht(
- 'Amazon SES is selected as the mail adapter, but no SES endpoint '.
- 'is configured. Provide an SES endpoint or choose a different '.
- 'mail adapter.');
-
- $this->newIssue('config.amazon-ses.endpoint')
- ->setName(pht('Amazon SES Endpoint Not Set'))
- ->setMessage($message)
- ->addRelatedPhabricatorConfig('metamta.mail-adapter')
- ->addPhabricatorConfig('amazon-ses.endpoint');
- }
-
- $address_key = 'metamta.default-address';
- $options = PhabricatorApplicationConfigOptions::loadAllOptions();
- $default = $options[$address_key]->getDefault();
- $value = PhabricatorEnv::getEnvConfig($address_key);
- if ($default === $value) {
- $message = pht(
- 'Amazon SES requires verification of the "From" address, but '.
- 'you have not configured a "From" address. Configure and verify '.
- 'a "From" address, or choose a different mail adapter.');
-
- $this->newIssue('config.metamta.default-address')
- ->setName(pht('No SES From Address Configured'))
- ->setMessage($message)
- ->addRelatedPhabricatorConfig('metamta.mail-adapter')
- ->addPhabricatorConfig('metamta.default-address');
- }
- break;
- }
-
+ $message = pht(
+ 'You haven\'t configured mailers yet, so Phabricator won\'t be able '.
+ 'to send outbound mail or receive inbound mail. See the '.
+ 'configuration setting cluster.mailers for details.');
+
+ $this->newIssue('cluster.mailers')
+ ->setName(pht('Mailers Not Configured'))
+ ->setMessage($message)
+ ->addPhabricatorConfig('cluster.mailers');
}
}
diff --git a/src/applications/config/check/PhabricatorMySQLSetupCheck.php b/src/applications/config/check/PhabricatorMySQLSetupCheck.php
index dad9ba6d7..a4048cbc3 100644
--- a/src/applications/config/check/PhabricatorMySQLSetupCheck.php
+++ b/src/applications/config/check/PhabricatorMySQLSetupCheck.php
@@ -1,397 +1,425 @@
<?php
final class PhabricatorMySQLSetupCheck extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_MYSQL;
}
protected function executeChecks() {
$refs = PhabricatorDatabaseRef::getActiveDatabaseRefs();
foreach ($refs as $ref) {
try {
$this->executeRefChecks($ref);
} catch (AphrontConnectionQueryException $ex) {
// If we're unable to connect to a host, just skip the checks for it.
// This can happen if we're restarting during a cluster incident. See
// T12966 for discussion.
}
}
}
private function executeRefChecks(PhabricatorDatabaseRef $ref) {
$max_allowed_packet = $ref->loadRawMySQLConfigValue('max_allowed_packet');
$host_name = $ref->getRefKey();
// This primarily supports setting the filesize limit for MySQL to 8MB,
// which may produce a >16MB packet after escaping.
$recommended_minimum = (32 * 1024 * 1024);
if ($max_allowed_packet < $recommended_minimum) {
$message = pht(
'On host "%s", MySQL is configured with a small "%s" (%d), which '.
'may cause some large writes to fail. The recommended minimum value '.
'for this setting is "%d".',
$host_name,
'max_allowed_packet',
$max_allowed_packet,
$recommended_minimum);
$this->newIssue('mysql.max_allowed_packet')
->setName(pht('Small MySQL "%s"', 'max_allowed_packet'))
->setMessage($message)
->setDatabaseRef($ref)
->addMySQLConfig('max_allowed_packet');
}
$modes = $ref->loadRawMySQLConfigValue('sql_mode');
$modes = explode(',', $modes);
if (!in_array('STRICT_ALL_TABLES', $modes)) {
$summary = pht(
'MySQL is not in strict mode (on host "%s"), but using strict mode '.
'is strongly encouraged.',
$host_name);
$message = pht(
"On database host \"%s\", the global %s is not set to %s. ".
"It is strongly encouraged that you enable this mode when running ".
"Phabricator.\n\n".
"By default MySQL will silently ignore some types of errors, which ".
"can cause data loss and raise security concerns. Enabling strict ".
"mode makes MySQL raise an explicit error instead, and prevents this ".
"entire class of problems from doing any damage.\n\n".
"You can find more information about this mode (and how to configure ".
"it) in the MySQL manual. Usually, it is sufficient to add this to ".
"your %s file (in the %s section) and then restart %s:\n\n".
"%s\n".
"(Note that if you run other applications against the same database, ".
"they may not work in strict mode. Be careful about enabling it in ".
"these cases.)",
$host_name,
phutil_tag('tt', array(), 'sql_mode'),
phutil_tag('tt', array(), 'STRICT_ALL_TABLES'),
phutil_tag('tt', array(), 'my.cnf'),
phutil_tag('tt', array(), '[mysqld]'),
phutil_tag('tt', array(), 'mysqld'),
phutil_tag('pre', array(), 'sql_mode=STRICT_ALL_TABLES'));
$this->newIssue('mysql.mode')
->setName(pht('MySQL %s Mode Not Set', 'STRICT_ALL_TABLES'))
->setSummary($summary)
->setMessage($message)
->setDatabaseRef($ref)
->addMySQLConfig('sql_mode');
}
if (in_array('ONLY_FULL_GROUP_BY', $modes)) {
$summary = pht(
'MySQL is in ONLY_FULL_GROUP_BY mode (on host "%s"), but using this '.
'mode is strongly discouraged.',
$host_name);
$message = pht(
"On database host \"%s\", the global %s is set to %s. ".
"It is strongly encouraged that you disable this mode when running ".
"Phabricator.\n\n".
"With %s enabled, MySQL rejects queries for which the select list ".
"or (as of MySQL 5.0.23) %s list refer to nonaggregated columns ".
"that are not named in the %s clause. More importantly, Phabricator ".
"does not work properly with this mode enabled.\n\n".
"You can find more information about this mode (and how to configure ".
"it) in the MySQL manual. Usually, it is sufficient to change the %s ".
"in your %s file (in the %s section) and then restart %s:\n\n".
"%s\n".
"(Note that if you run other applications against the same database, ".
"they may not work with %s. Be careful about enabling ".
"it in these cases and consider migrating Phabricator to a different ".
"database.)",
$host_name,
phutil_tag('tt', array(), 'sql_mode'),
phutil_tag('tt', array(), 'ONLY_FULL_GROUP_BY'),
phutil_tag('tt', array(), 'ONLY_FULL_GROUP_BY'),
phutil_tag('tt', array(), 'HAVING'),
phutil_tag('tt', array(), 'GROUP BY'),
phutil_tag('tt', array(), 'sql_mode'),
phutil_tag('tt', array(), 'my.cnf'),
phutil_tag('tt', array(), '[mysqld]'),
phutil_tag('tt', array(), 'mysqld'),
phutil_tag('pre', array(), 'sql_mode=STRICT_ALL_TABLES'),
phutil_tag('tt', array(), 'ONLY_FULL_GROUP_BY'));
$this->newIssue('mysql.mode')
->setName(pht('MySQL %s Mode Set', 'ONLY_FULL_GROUP_BY'))
->setSummary($summary)
->setMessage($message)
->setDatabaseRef($ref)
->addMySQLConfig('sql_mode');
}
$is_innodb_fulltext = false;
$is_myisam_fulltext = false;
if ($this->shouldUseMySQLSearchEngine()) {
if (PhabricatorSearchDocument::isInnoDBFulltextEngineAvailable()) {
$is_innodb_fulltext = true;
} else {
$is_myisam_fulltext = true;
}
}
if ($is_myisam_fulltext) {
$stopword_file = $ref->loadRawMySQLConfigValue('ft_stopword_file');
if ($stopword_file === null) {
$summary = pht(
'Your version of MySQL (on database host "%s") does not support '.
'configuration of a stopword file. You will not be able to find '.
'search results for common words.',
$host_name);
$message = pht(
"Database host \"%s\" does not support the %s option. You will not ".
"be able to find search results for common words. You can gain ".
"access to this option by upgrading MySQL to a more recent ".
"version.\n\n".
"You can ignore this warning if you plan to configure Elasticsearch ".
"later, or aren't concerned about searching for common words.",
$host_name,
phutil_tag('tt', array(), 'ft_stopword_file'));
$this->newIssue('mysql.ft_stopword_file')
->setName(pht('MySQL %s Not Supported', 'ft_stopword_file'))
->setSummary($summary)
->setMessage($message)
->setDatabaseRef($ref)
->addMySQLConfig('ft_stopword_file');
} else if ($stopword_file == '(built-in)') {
$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 (on host "%s") is using a default stopword file, which '.
'will prevent searching for many common words.',
$host_name);
$message = pht(
"Database host \"%s\" 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",
$host_name,
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)
->setDatabaseRef($ref)
->addMySQLConfig('ft_stopword_file');
}
}
if ($is_myisam_fulltext) {
$min_len = $ref->loadRawMySQLConfigValue('ft_min_word_len');
if ($min_len >= 4) {
$namespace = PhabricatorEnv::getEnvConfig('storage.default-namespace');
$summary = pht(
'MySQL is configured (on host "%s") to only index words with at '.
'least %d characters.',
$host_name,
$min_len);
$message = pht(
"Database host \"%s\" 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",
$host_name,
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)
->setDatabaseRef($ref)
->addMySQLConfig('ft_min_word_len');
}
}
// NOTE: The default value of "innodb_ft_min_token_size" is 3, which is
// a reasonable value, so we do not warn about it: if it is set to
// something else, the user adjusted it on their own.
// NOTE: We populate a stopwords table at "phabricator_search.stopwords",
// but the default InnoDB stopword list is pretty reasonable (36 words,
// versus 500+ in MyISAM). Just use the builtin list until we run into
// concrete issues with it. Users can switch to our stopword table with:
//
// [mysqld]
// innodb_ft_server_stopword_table = phabricator_search/stopwords
$innodb_pool = $ref->loadRawMySQLConfigValue('innodb_buffer_pool_size');
$innodb_bytes = phutil_parse_bytes($innodb_pool);
$innodb_readable = phutil_format_bytes($innodb_bytes);
// This is arbitrary and just trying to detect values that the user
// probably didn't set themselves. The Mac OS X default is 128MB and
// 40% of an AWS EC2 Micro instance is 245MB, so keeping it somewhere
// between those two values seems like a reasonable approximation.
$minimum_readable = '225MB';
$minimum_bytes = phutil_parse_bytes($minimum_readable);
if ($innodb_bytes < $minimum_bytes) {
$summary = pht(
'MySQL (on host "%s") is configured with a very small '.
'innodb_buffer_pool_size, which may impact performance.',
$host_name);
$message = pht(
"Database host \"%s\" is configured with a very small %s (%s). ".
"This may cause poor database performance and lock exhaustion.\n\n".
"There are no hard-and-fast rules to setting an appropriate value, ".
"but a reasonable starting point for a standard install is something ".
"like 40%% of the total memory on the machine. For example, if you ".
"have 4GB of RAM on the machine you have installed Phabricator on, ".
"you might set this value to %s.\n\n".
"You can read more about this option in the MySQL documentation to ".
"help you make a decision about how to configure it for your use ".
"case. There are no concerns specific to Phabricator which make it ".
"different from normal workloads with respect to this setting.\n\n".
"To adjust the setting, add something like this to your %s file (in ".
"the %s section), replacing %s with an appropriate value for your ".
"host and use case. Then restart %s:\n\n".
"%s\n".
"If you're satisfied with the current setting, you can safely ".
"ignore this setup warning.",
$host_name,
phutil_tag('tt', array(), 'innodb_buffer_pool_size'),
phutil_tag('tt', array(), $innodb_readable),
phutil_tag('tt', array(), '1600M'),
phutil_tag('tt', array(), 'my.cnf'),
phutil_tag('tt', array(), '[mysqld]'),
phutil_tag('tt', array(), '1600M'),
phutil_tag('tt', array(), 'mysqld'),
phutil_tag('pre', array(), 'innodb_buffer_pool_size=1600M'));
$this->newIssue('mysql.innodb_buffer_pool_size')
->setName(pht('MySQL May Run Slowly'))
->setSummary($summary)
->setMessage($message)
->setDatabaseRef($ref)
->addMySQLConfig('innodb_buffer_pool_size');
}
$conn = $ref->newManagementConnection();
$ok = PhabricatorStorageManagementAPI::isCharacterSetAvailableOnConnection(
'utf8mb4',
$conn);
if (!$ok) {
$summary = pht(
'You are using an old version of MySQL (on host "%s"), and should '.
'upgrade.',
$host_name);
$message = pht(
'You are using an old version of MySQL (on host "%s") which has poor '.
'unicode support (it does not support the "utf8mb4" collation set). '.
'You will encounter limitations when working with some unicode data.'.
"\n\n".
'We strongly recommend you upgrade to MySQL 5.5 or newer.',
$host_name);
$this->newIssue('mysql.utf8mb4')
->setName(pht('Old MySQL Version'))
->setSummary($summary)
->setDatabaseRef($ref)
->setMessage($message);
}
$info = queryfx_one(
$conn,
'SELECT UNIX_TIMESTAMP() epoch');
$epoch = (int)$info['epoch'];
$local = PhabricatorTime::getNow();
$delta = (int)abs($local - $epoch);
if ($delta > 60) {
$this->newIssue('mysql.clock')
->setName(pht('Major Web/Database Clock Skew'))
->setSummary(
pht(
'This web host ("%s") is set to a very different time than a '.
'database host "%s".',
php_uname('n'),
$host_name))
->setMessage(
pht(
'A database host ("%s") and this web host ("%s") disagree on the '.
'current time by more than 60 seconds (absolute skew is %s '.
'seconds). Check that the current time is set correctly '.
'everywhere.',
$host_name,
php_uname('n'),
new PhutilNumber($delta)));
}
+ $local_infile = $ref->loadRawMySQLConfigValue('local_infile');
+ if ($local_infile) {
+ $summary = pht(
+ 'The MySQL "local_infile" option is enabled. This option is '.
+ 'unsafe.');
+
+ $message = pht(
+ 'Your MySQL server is configured with the "local_infile" option '.
+ 'enabled. This option allows an attacker who finds an SQL injection '.
+ 'hole to escalate their attack by copying files from the webserver '.
+ 'into the database with "LOAD DATA LOCAL INFILE" queries, then '.
+ 'reading the file content with "SELECT" queries.'.
+ "\n\n".
+ 'You should disable this option in your %s file, in the %s section:'.
+ "\n\n".
+ '%s',
+ phutil_tag('tt', array(), 'my.cnf'),
+ phutil_tag('tt', array(), '[mysqld]'),
+ phutil_tag('pre', array(), 'local_infile=0'));
+
+ $this->newIssue('mysql.local_infile')
+ ->setName(pht('Unsafe MySQL "local_infile" Setting Enabled'))
+ ->setSummary($summary)
+ ->setMessage($message)
+ ->setDatabaseRef($ref)
+ ->addMySQLConfig('local_infile');
+ }
+
}
protected function shouldUseMySQLSearchEngine() {
$services = PhabricatorSearchService::getAllServices();
foreach ($services as $service) {
if ($service instanceof PhabricatorMySQLSearchHost) {
return true;
}
}
return false;
}
}
diff --git a/src/applications/config/check/PhabricatorPHPConfigSetupCheck.php b/src/applications/config/check/PhabricatorPHPConfigSetupCheck.php
index cf3148b5b..bee2bc91b 100644
--- a/src/applications/config/check/PhabricatorPHPConfigSetupCheck.php
+++ b/src/applications/config/check/PhabricatorPHPConfigSetupCheck.php
@@ -1,117 +1,153 @@
<?php
/**
* Noncritical PHP configuration checks.
*
* For critical checks, see @{class:PhabricatorPHPPreflightSetupCheck}.
*/
final class PhabricatorPHPConfigSetupCheck extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_PHP;
}
protected function executeChecks() {
if (empty($_SERVER['REMOTE_ADDR'])) {
$doc_href = PhabricatorEnv::getDoclink('Configuring a Preamble Script');
$summary = pht(
'You likely need to fix your preamble script so '.
'REMOTE_ADDR is no longer empty.');
$message = pht(
'No REMOTE_ADDR is available, so Phabricator cannot determine the '.
'origin address for requests. This will prevent Phabricator from '.
'performing important security checks. This most often means you '.
'have a mistake in your preamble script. Consult the documentation '.
'(%s) and double-check that the script is written correctly.',
phutil_tag(
'a',
array(
'href' => $doc_href,
'target' => '_blank',
),
pht('Configuring a Preamble Script')));
$this->newIssue('php.remote_addr')
->setName(pht('No REMOTE_ADDR available'))
->setSummary($summary)
->setMessage($message);
}
if (version_compare(phpversion(), '7', '>=')) {
// This option was removed in PHP7.
$raw_post_data = -1;
} else {
$raw_post_data = (int)ini_get('always_populate_raw_post_data');
}
if ($raw_post_data != -1) {
$summary = pht(
'PHP setting "%s" should be set to "-1" to avoid deprecation '.
'warnings.',
'always_populate_raw_post_data');
$message = pht(
'The "%s" key is set to some value other than "-1" in your PHP '.
'configuration. This can cause PHP to raise deprecation warnings '.
'during process startup. Set this option to "-1" to prevent these '.
'warnings from appearing.',
'always_populate_raw_post_data');
$this->newIssue('php.always_populate_raw_post_data')
->setName(pht('Disable PHP %s', 'always_populate_raw_post_data'))
->setSummary($summary)
->setMessage($message)
->addPHPConfig('always_populate_raw_post_data');
}
if (!extension_loaded('mysqli')) {
$summary = pht(
'Install the MySQLi extension to improve database behavior.');
$message = pht(
'PHP is currently using the very old "mysql" extension to interact '.
'with the database. You should install the newer "mysqli" extension '.
'to improve behaviors (like error handling and query timeouts).'.
"\n\n".
'Phabricator will work with the older extension, but upgrading to the '.
'newer extension is recommended.'.
"\n\n".
'You may be able to install the extension with a command like: %s',
// NOTE: We're intentionally telling you to install "mysqlnd" here; on
// Ubuntu, there's no separate "mysqli" package.
phutil_tag('tt', array(), 'sudo apt-get install php5-mysqlnd'));
$this->newIssue('php.mysqli')
->setName(pht('MySQLi Extension Not Available'))
->setSummary($summary)
->setMessage($message);
} else if (!defined('MYSQLI_ASYNC')) {
$summary = pht(
'Configure the MySQL Native Driver to improve database behavior.');
$message = pht(
'PHP is currently using the older MySQL external driver instead of '.
'the newer MySQL native driver. The older driver lacks options and '.
'features (like support for query timeouts) which allow Phabricator '.
'to interact better with the database.'.
"\n\n".
'Phabricator will work with the older driver, but upgrading to the '.
'native driver is recommended.'.
"\n\n".
'You may be able to install the native driver with a command like: %s',
phutil_tag('tt', array(), 'sudo apt-get install php5-mysqlnd'));
$this->newIssue('php.myqlnd')
->setName(pht('MySQL Native Driver Not Available'))
->setSummary($summary)
->setMessage($message);
}
+
+ if (extension_loaded('mysqli')) {
+ $infile_key = 'mysqli.allow_local_infile';
+ } else {
+ $infile_key = 'mysql.allow_local_infile';
+ }
+
+ if (ini_get($infile_key)) {
+ $summary = pht(
+ 'Disable unsafe option "%s" in PHP configuration.',
+ $infile_key);
+
+ $message = pht(
+ 'PHP is currently configured to honor requests from any MySQL server '.
+ 'it connects to for the content of any local file.'.
+ "\n\n".
+ 'This capability supports MySQL "LOAD DATA LOCAL INFILE" queries, but '.
+ 'allows a malicious MySQL server read access to the local disk: the '.
+ 'server can ask the client to send the content of any local file, '.
+ 'and the client will comply.'.
+ "\n\n".
+ 'Although it is normally difficult for an attacker to convince '.
+ 'Phabricator to connect to a malicious MySQL server, you should '.
+ 'disable this option: this capability is unnecessary and inherently '.
+ 'dangerous.'.
+ "\n\n".
+ 'To disable this option, set: %s',
+ phutil_tag('tt', array(), pht('%s = 0', $infile_key)));
+
+ $this->newIssue('php.'.$infile_key)
+ ->setName(pht('Unsafe PHP "Local Infile" Configuration'))
+ ->setSummary($summary)
+ ->setMessage($message)
+ ->addPHPConfig($infile_key);
+ }
+
}
}
diff --git a/src/applications/config/check/PhabricatorPHPPreflightSetupCheck.php b/src/applications/config/check/PhabricatorPHPPreflightSetupCheck.php
index 7c9653f4f..30c6036c8 100644
--- a/src/applications/config/check/PhabricatorPHPPreflightSetupCheck.php
+++ b/src/applications/config/check/PhabricatorPHPPreflightSetupCheck.php
@@ -1,140 +1,147 @@
<?php
final class PhabricatorPHPPreflightSetupCheck extends PhabricatorSetupCheck {
public function getDefaultGroup() {
return self::GROUP_PHP;
}
public function isPreflightCheck() {
return true;
}
protected function executeChecks() {
- if (version_compare(phpversion(), 7, '>=') &&
- version_compare(phpversion(), 7.1, '<')) {
+ $version = phpversion();
+ if (version_compare($version, 7, '>=') &&
+ version_compare($version, 7.1, '<')) {
$message = pht(
- 'This version of Phabricator does not support PHP 7.0. You '.
- 'are running PHP %s. Upgrade to PHP 7.1 or newer.',
- phpversion());
+ 'You are running PHP version %s. Phabricator does not support PHP '.
+ 'versions between 7.0 and 7.1.'.
+ "\n\n".
+ 'PHP removed signal handling features that Phabricator requires in '.
+ 'PHP 7.0, and did not restore them until PHP 7.1.'.
+ "\n\n".
+ 'Upgrade to PHP 7.1 or newer (recommended) or downgrade to an older '.
+ 'version of PHP 5 (discouraged).',
+ $version);
$this->newIssue('php.version7')
->setIsFatal(true)
- ->setName(pht('PHP 7.0 Not Supported'))
+ ->setName(pht('PHP 7.0-7.1 Not Supported'))
->setMessage($message)
->addLink(
'https://phurl.io/u/php7',
pht('Phabricator PHP 7 Compatibility Information'));
return;
}
$safe_mode = ini_get('safe_mode');
if ($safe_mode) {
$message = pht(
"You have '%s' enabled in your PHP configuration, but Phabricator ".
"will not run in safe mode. Safe mode has been deprecated in PHP 5.3 ".
"and removed in PHP 5.4.\n\nDisable safe mode to continue.",
'safe_mode');
$this->newIssue('php.safe_mode')
->setIsFatal(true)
->setName(pht('Disable PHP %s', 'safe_mode'))
->setMessage($message)
->addPHPConfig('safe_mode');
return;
}
// Check for `disable_functions` or `disable_classes`. Although it's
// possible to disable a bunch of functions (say, `array_change_key_case()`)
// and classes and still have Phabricator work fine, it's unreasonably
// difficult for us to be sure we'll even survive setup if these options
// are enabled. Phabricator needs access to the most dangerous functions,
// so there is no reasonable configuration value here which actually
// provides a benefit while guaranteeing Phabricator will run properly.
$disable_options = array('disable_functions', 'disable_classes');
foreach ($disable_options as $disable_option) {
$disable_value = ini_get($disable_option);
if ($disable_value) {
// By default Debian installs the pcntl extension but disables all of
// its functions using configuration. Whitelist disabling these
// functions so that Debian PHP works out of the box (we do not need to
// call these functions from the web UI). This is pretty ridiculous but
// it's not the users' fault and they haven't done anything crazy to
// get here, so don't make them pay for Debian's unusual choices.
// See: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=605571
$fatal = true;
if ($disable_option == 'disable_functions') {
$functions = preg_split('/[, ]+/', $disable_value);
$functions = array_filter($functions);
foreach ($functions as $k => $function) {
if (preg_match('/^pcntl_/', $function)) {
unset($functions[$k]);
}
}
if (!$functions) {
$fatal = false;
}
}
if ($fatal) {
$message = pht(
"You have '%s' enabled in your PHP configuration.\n\n".
"This option is not compatible with Phabricator. Remove ".
"'%s' from your configuration to continue.",
$disable_option,
$disable_option);
$this->newIssue('php.'.$disable_option)
->setIsFatal(true)
->setName(pht('Remove PHP %s', $disable_option))
->setMessage($message)
->addPHPConfig($disable_option);
}
}
}
$overload_option = 'mbstring.func_overload';
$func_overload = ini_get($overload_option);
if ($func_overload) {
$message = pht(
"You have '%s' enabled in your PHP configuration.\n\n".
"This option is not compatible with Phabricator. Disable ".
"'%s' in your PHP configuration to continue.",
$overload_option,
$overload_option);
$this->newIssue('php'.$overload_option)
->setIsFatal(true)
->setName(pht('Disable PHP %s', $overload_option))
->setMessage($message)
->addPHPConfig($overload_option);
}
$open_basedir = ini_get('open_basedir');
if (strlen($open_basedir)) {
// If `open_basedir` is set, just fatal. It's technically possible for
// us to run with certain values of `open_basedir`, but: we can only
// raise fatal errors from preflight steps, so we'd have to do this check
// in two parts to support fatal and advisory versions; it's much simpler
// to just fatal instead of trying to test all the different things we
// may need to access in the filesystem; and use of this option seems
// rare (particularly in supported environments).
$message = pht(
"Your server is configured with '%s', which prevents Phabricator ".
"from opening files it requires access to.\n\n".
"Disable this setting to continue.",
'open_basedir');
$issue = $this->newIssue('php.open_basedir')
->setName(pht('Disable PHP %s', 'open_basedir'))
->addPHPConfig('open_basedir')
->setIsFatal(true)
->setMessage($message);
}
}
}
diff --git a/src/applications/config/controller/PhabricatorConfigHistoryController.php b/src/applications/config/controller/PhabricatorConfigHistoryController.php
index 238d09234..9157ecb8b 100644
--- a/src/applications/config/controller/PhabricatorConfigHistoryController.php
+++ b/src/applications/config/controller/PhabricatorConfigHistoryController.php
@@ -1,53 +1,49 @@
<?php
final class PhabricatorConfigHistoryController
extends PhabricatorConfigController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$xactions = id(new PhabricatorConfigTransactionQuery())
->setViewer($viewer)
->needComments(true)
->execute();
$object = new PhabricatorConfigEntry();
$xaction = $object->getApplicationTransactionTemplate();
- $view = $xaction->getApplicationTransactionViewObject();
-
- $timeline = $view
- ->setUser($viewer)
+ $timeline = id(new PhabricatorApplicationTransactionView())
+ ->setViewer($viewer)
->setTransactions($xactions)
->setRenderAsFeed(true)
->setObjectPHID(PhabricatorPHIDConstants::PHID_VOID);
$timeline->setShouldTerminate(true);
- $object->willRenderTimeline($timeline, $this->getRequest());
-
$title = pht('Settings History');
$header = $this->buildHeaderView($title);
$nav = $this->buildSideNavView();
$nav->selectFilter('history/');
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb($title)
->setBorder(true);
$content = id(new PHUITwoColumnView())
->setHeader($header)
->setNavigation($nav)
->setFixed(true)
->setMainColumn($timeline);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($content);
}
}
diff --git a/src/applications/config/controller/PhabricatorConfigVersionController.php b/src/applications/config/controller/PhabricatorConfigVersionController.php
index 8a87dec5c..a9571a1f8 100644
--- a/src/applications/config/controller/PhabricatorConfigVersionController.php
+++ b/src/applications/config/controller/PhabricatorConfigVersionController.php
@@ -1,242 +1,244 @@
<?php
final class PhabricatorConfigVersionController
extends PhabricatorConfigController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$title = pht('Version Information');
$versions = $this->renderModuleStatus($viewer);
$nav = $this->buildSideNavView();
$nav->selectFilter('version/');
$header = $this->buildHeaderView($title);
$view = $this->buildConfigBoxView(
pht('Installed Versions'),
$versions);
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb($title)
->setBorder(true);
$content = id(new PHUITwoColumnView())
->setHeader($header)
->setNavigation($nav)
->setFixed(true)
->setMainColumn($view);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($content);
}
public function renderModuleStatus($viewer) {
$versions = $this->loadVersions($viewer);
$version_property_list = id(new PHUIPropertyListView());
foreach ($versions as $name => $info) {
$version = $info['version'];
if ($info['branchpoint']) {
$display = pht(
'%s (branched from %s on %s)',
$version,
$info['branchpoint'],
$info['upstream']);
} else {
$display = $version;
}
$version_property_list->addProperty($name, $display);
}
$phabricator_root = dirname(phutil_get_library_root('phabricator'));
$version_path = $phabricator_root.'/conf/local/VERSION';
if (Filesystem::pathExists($version_path)) {
$version_from_file = Filesystem::readFile($version_path);
$version_property_list->addProperty(
pht('Local Version'),
$version_from_file);
}
+ $version_property_list->addProperty('php', phpversion());
+
$binaries = PhutilBinaryAnalyzer::getAllBinaries();
foreach ($binaries as $binary) {
if (!$binary->isBinaryAvailable()) {
$binary_info = pht('Not Available');
} else {
$version = $binary->getBinaryVersion();
$path = $binary->getBinaryPath();
if ($path === null && $version === null) {
$binary_info = pht('-');
} else if ($path === null) {
$binary_info = $version;
} else if ($version === null) {
$binary_info = pht('- at %s', $path);
} else {
$binary_info = pht('%s at %s', $version, $path);
}
}
$version_property_list->addProperty(
$binary->getBinaryName(),
$binary_info);
}
return $version_property_list;
}
private function loadVersions(PhabricatorUser $viewer) {
$specs = array(
'phabricator',
'arcanist',
'phutil',
);
$all_libraries = PhutilBootloader::getInstance()->getAllLibraries();
// This puts the core libraries at the top:
$other_libraries = array_diff($all_libraries, $specs);
$specs = array_merge($specs, $other_libraries);
$log_futures = array();
$remote_futures = array();
foreach ($specs as $lib) {
$root = dirname(phutil_get_library_root($lib));
$log_command = csprintf(
'git log --format=%s -n 1 --',
'%H %ct');
$remote_command = csprintf(
'git remote -v');
$log_futures[$lib] = id(new ExecFuture('%C', $log_command))
->setCWD($root);
$remote_futures[$lib] = id(new ExecFuture('%C', $remote_command))
->setCWD($root);
}
$all_futures = array_merge($log_futures, $remote_futures);
id(new FutureIterator($all_futures))
->resolveAll();
// A repository may have a bunch of remotes, but we're only going to look
// for remotes we host to try to figure out where this repository branched.
$upstream_pattern = '(github\.com/phacility/|secure\.phabricator\.com/)';
$upstream_futures = array();
$lib_upstreams = array();
foreach ($specs as $lib) {
$remote_future = $remote_futures[$lib];
list($err, $stdout) = $remote_future->resolve();
if ($err) {
// If this fails for whatever reason, just move on.
continue;
}
// These look like this, with a tab separating the first two fields:
// remote-name http://remote.uri/ (push)
$upstreams = array();
$remotes = phutil_split_lines($stdout, false);
foreach ($remotes as $remote) {
$remote_pattern = '/^([^\t]+)\t([^ ]+) \(([^)]+)\)\z/';
$matches = null;
if (!preg_match($remote_pattern, $remote, $matches)) {
continue;
}
// Remote URIs are either "push" or "fetch": we only care about "fetch"
// URIs.
$type = $matches[3];
if ($type != 'fetch') {
continue;
}
$uri = $matches[2];
$is_upstream = preg_match($upstream_pattern, $uri);
if (!$is_upstream) {
continue;
}
$name = $matches[1];
$upstreams[$name] = $name;
}
// If we have several suitable upstreams, try to pick the one named
// "origin", if it exists. Otherwise, just pick the first one.
if (isset($upstreams['origin'])) {
$upstream = $upstreams['origin'];
} else if ($upstreams) {
$upstream = head($upstreams);
} else {
$upstream = null;
}
if (!$upstream) {
continue;
}
$lib_upstreams[$lib] = $upstream;
$merge_base_command = csprintf(
'git merge-base HEAD %s/master --',
$upstream);
$root = dirname(phutil_get_library_root($lib));
$upstream_futures[$lib] = id(new ExecFuture('%C', $merge_base_command))
->setCWD($root);
}
if ($upstream_futures) {
id(new FutureIterator($upstream_futures))
->resolveAll();
}
$results = array();
foreach ($log_futures as $lib => $future) {
list($err, $stdout) = $future->resolve();
if (!$err) {
list($hash, $epoch) = explode(' ', $stdout);
$version = pht('%s (%s)', $hash, phabricator_date($epoch, $viewer));
} else {
$version = pht('Unknown');
}
$result = array(
'version' => $version,
'upstream' => null,
'branchpoint' => null,
);
$upstream_future = idx($upstream_futures, $lib);
if ($upstream_future) {
list($err, $stdout) = $upstream_future->resolve();
if (!$err) {
$branchpoint = trim($stdout);
if (strlen($branchpoint)) {
// We only list a branchpoint if it differs from HEAD.
if ($branchpoint != $hash) {
$result['upstream'] = $lib_upstreams[$lib];
$result['branchpoint'] = trim($stdout);
}
}
}
}
$results[$lib] = $result;
}
return $results;
}
}
diff --git a/src/applications/config/option/PhabricatorAWSConfigOptions.php b/src/applications/config/option/PhabricatorAWSConfigOptions.php
index 664793085..5f2246fda 100644
--- a/src/applications/config/option/PhabricatorAWSConfigOptions.php
+++ b/src/applications/config/option/PhabricatorAWSConfigOptions.php
@@ -1,78 +1,60 @@
<?php
final class PhabricatorAWSConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Amazon Web Services');
}
public function getDescription() {
return pht('Configure integration with AWS (EC2, SES, S3, etc).');
}
public function getIcon() {
return 'fa-server';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
return array(
- $this->newOption('amazon-ses.access-key', 'string', null)
- ->setLocked(true)
- ->setDescription(pht('Access key for Amazon SES.')),
- $this->newOption('amazon-ses.secret-key', 'string', null)
- ->setHidden(true)
- ->setDescription(pht('Secret key for Amazon SES.')),
- $this->newOption('amazon-ses.endpoint', 'string', null)
- ->setLocked(true)
- ->setDescription(
- pht(
- 'SES endpoint domain name. You can find a list of available '.
- 'regions and endpoints in the AWS documentation.'))
- ->addExample(
- 'email.us-east-1.amazonaws.com',
- pht('US East (N. Virginia, Older default endpoint)'))
- ->addExample(
- 'email.us-west-2.amazonaws.com',
- pht('US West (Oregon)')),
$this->newOption('amazon-s3.access-key', 'string', null)
->setLocked(true)
->setDescription(pht('Access key for Amazon S3.')),
$this->newOption('amazon-s3.secret-key', 'string', null)
->setHidden(true)
->setDescription(pht('Secret key for Amazon S3.')),
$this->newOption('amazon-s3.region', 'string', null)
->setLocked(true)
->setDescription(
pht(
'Amazon S3 region where your S3 bucket is located. When you '.
'specify a region, you should also specify a corresponding '.
'endpoint with `amazon-s3.endpoint`. You can find a list of '.
'available regions and endpoints in the AWS documentation.'))
->addExample('us-west-1', pht('USWest Region')),
$this->newOption('amazon-s3.endpoint', 'string', null)
->setLocked(true)
->setDescription(
pht(
'Explicit S3 endpoint to use. This should be the endpoint '.
'which corresponds to the region you have selected in '.
'`amazon-s3.region`. Phabricator can not determine the correct '.
'endpoint automatically because some endpoint locations are '.
'irregular.'))
->addExample(
's3-us-west-1.amazonaws.com',
pht('Use specific endpoint')),
$this->newOption('amazon-ec2.access-key', 'string', null)
->setLocked(true)
->setDescription(pht('Access key for Amazon EC2.')),
$this->newOption('amazon-ec2.secret-key', 'string', null)
->setHidden(true)
->setDescription(pht('Secret key for Amazon EC2.')),
);
}
}
diff --git a/src/applications/config/option/PhabricatorAuthenticationConfigOptions.php b/src/applications/config/option/PhabricatorAuthenticationConfigOptions.php
index 2239d9885..1440714bf 100644
--- a/src/applications/config/option/PhabricatorAuthenticationConfigOptions.php
+++ b/src/applications/config/option/PhabricatorAuthenticationConfigOptions.php
@@ -1,103 +1,103 @@
<?php
final class PhabricatorAuthenticationConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Authentication');
}
public function getDescription() {
return pht('Options relating to authentication.');
}
public function getIcon() {
return 'fa-key';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
return array(
$this->newOption('auth.require-email-verification', 'bool', false)
->setBoolOptions(
array(
pht('Require email verification'),
pht("Don't require email verification"),
))
->setSummary(
pht('Require email verification before a user can log in.'))
->setDescription(
pht(
'If true, email addresses must be verified (by clicking a link '.
'in an email) before a user can login. By default, verification '.
- 'is optional unless {{auth.email-domains}} is nonempty.')),
+ 'is optional unless @{config:auth.email-domains} is nonempty.')),
$this->newOption('auth.require-approval', 'bool', true)
->setBoolOptions(
array(
pht('Require Administrators to Approve Accounts'),
pht("Don't Require Manual Approval"),
))
->setSummary(
pht('Require administrators to approve new accounts.'))
->setDescription(
pht(
"Newly registered Phabricator accounts can either be placed ".
"into a manual approval queue for administrative review, or ".
"automatically activated immediately. The approval queue is ".
"enabled by default because it gives you greater control over ".
"who can register an account and access Phabricator.\n\n".
"If your install is completely public, or on a VPN, or users can ".
"only register with a trusted provider like LDAP, or you've ".
"otherwise configured Phabricator to prevent unauthorized ".
"registration, you can disable the queue to reduce administrative ".
"overhead.\n\n".
"NOTE: Before you disable the queue, make sure ".
- "{{auth.email-domains}} is configured correctly ".
+ "@{config:auth.email-domains} is configured correctly ".
"for your install!")),
$this->newOption('auth.email-domains', 'list<string>', array())
->setSummary(pht('Only allow registration from particular domains.'))
->setDescription(
pht(
"You can restrict allowed email addresses to certain domains ".
"(like `yourcompany.com`) by setting a list of allowed domains ".
"here.\n\nUsers will only be allowed to register using email ".
"addresses at one of the domains, and will only be able to add ".
"new email addresses for these domains. If you configure this, ".
- "it implies {{auth.require-email-verification}}.\n\n".
+ "it implies @{config:auth.require-email-verification}.\n\n".
"You should omit the `@` from domains. Note that the domain must ".
"match exactly. If you allow `yourcompany.com`, that permits ".
"`joe@yourcompany.com` but rejects `joe@mail.yourcompany.com`."))
->addExample(
"yourcompany.com\nmail.yourcompany.com",
pht('Valid Setting')),
$this->newOption('account.editable', 'bool', true)
->setBoolOptions(
array(
pht('Allow editing'),
pht('Prevent editing'),
))
->setSummary(
pht(
'Determines whether or not basic account information is editable.'))
->setDescription(
pht(
'This option controls whether users can edit account email '.
'addresses and profile real names.'.
"\n\n".
'If you set up Phabricator to automatically synchronize account '.
'information from some other authoritative system, you can '.
'prevent users from making these edits to ensure information '.
'remains consistent across both systems.')),
$this->newOption('account.minimum-password-length', 'int', 8)
->setSummary(pht('Minimum password length.'))
->setDescription(
pht(
'When users set or reset a password, it must have at least this '.
'many characters.')),
);
}
}
diff --git a/src/applications/config/option/PhabricatorConfigOption.php b/src/applications/config/option/PhabricatorConfigOption.php
index 385af002c..6d7f88bdf 100644
--- a/src/applications/config/option/PhabricatorConfigOption.php
+++ b/src/applications/config/option/PhabricatorConfigOption.php
@@ -1,221 +1,215 @@
<?php
final class PhabricatorConfigOption
extends Phobject {
private $key;
private $default;
private $summary;
private $description;
private $type;
private $boolOptions;
private $enumOptions;
private $group;
private $examples;
private $locked;
private $lockedMessage;
private $hidden;
private $baseClass;
private $customData;
private $customObject;
public function setBaseClass($base_class) {
$this->baseClass = $base_class;
return $this;
}
public function getBaseClass() {
return $this->baseClass;
}
public function setHidden($hidden) {
$this->hidden = $hidden;
return $this;
}
public function getHidden() {
if ($this->hidden) {
return true;
}
return idx(
PhabricatorEnv::getEnvConfig('config.hide'),
$this->getKey(),
false);
}
public function setLocked($locked) {
$this->locked = $locked;
return $this;
}
public function getLocked() {
if ($this->locked) {
return true;
}
if ($this->getHidden()) {
return true;
}
return idx(
PhabricatorEnv::getEnvConfig('config.lock'),
$this->getKey(),
false);
}
public function setLockedMessage($message) {
$this->lockedMessage = $message;
return $this;
}
public function getLockedMessage() {
if ($this->lockedMessage !== null) {
return $this->lockedMessage;
}
return pht(
'This configuration is locked and can not be edited from the web '.
'interface. Use %s in %s to edit it.',
phutil_tag('tt', array(), './bin/config'),
phutil_tag('tt', array(), 'phabricator/'));
}
public function addExample($value, $description) {
$this->examples[] = array($value, $description);
return $this;
}
public function getExamples() {
return $this->examples;
}
public function setGroup(PhabricatorApplicationConfigOptions $group) {
$this->group = $group;
return $this;
}
public function getGroup() {
return $this->group;
}
public function setBoolOptions(array $options) {
$this->boolOptions = $options;
return $this;
}
public function getBoolOptions() {
if ($this->boolOptions) {
return $this->boolOptions;
}
return array(
pht('True'),
pht('False'),
);
}
public function setEnumOptions(array $options) {
$this->enumOptions = $options;
return $this;
}
public function getEnumOptions() {
if ($this->enumOptions) {
return $this->enumOptions;
}
throw new PhutilInvalidStateException('setEnumOptions');
}
public function setKey($key) {
$this->key = $key;
return $this;
}
public function getKey() {
return $this->key;
}
public function setDefault($default) {
$this->default = $default;
return $this;
}
public function getDefault() {
return $this->default;
}
public function setSummary($summary) {
$this->summary = $summary;
return $this;
}
public function getSummary() {
if (empty($this->summary)) {
return $this->getDescription();
}
return $this->summary;
}
public function setDescription($description) {
$this->description = $description;
return $this;
}
public function getDescription() {
return $this->description;
}
public function setType($type) {
$this->type = $type;
return $this;
}
public function getType() {
return $this->type;
}
public function newOptionType() {
$type_key = $this->getType();
$type_map = PhabricatorConfigType::getAllTypes();
return idx($type_map, $type_key);
}
public function isCustomType() {
return !strncmp($this->getType(), 'custom:', 7);
}
public function getCustomObject() {
if (!$this->customObject) {
if (!$this->isCustomType()) {
throw new Exception(pht('This option does not have a custom type!'));
}
$this->customObject = newv(substr($this->getType(), 7), array());
}
return $this->customObject;
}
public function getCustomData() {
return $this->customData;
}
public function setCustomData($data) {
$this->customData = $data;
return $this;
}
public function newDescriptionRemarkupView(PhabricatorUser $viewer) {
$description = $this->getDescription();
if (!strlen($description)) {
return null;
}
- // TODO: Some day, we should probably implement this as a real rule.
- $description = preg_replace(
- '/{{([^}]+)}}/',
- '[[/config/edit/\\1/ | \\1]]',
- $description);
-
return new PHUIRemarkupView($viewer, $description);
}
}
diff --git a/src/applications/config/option/PhabricatorCoreConfigOptions.php b/src/applications/config/option/PhabricatorCoreConfigOptions.php
index 08266217e..48f6f2449 100644
--- a/src/applications/config/option/PhabricatorCoreConfigOptions.php
+++ b/src/applications/config/option/PhabricatorCoreConfigOptions.php
@@ -1,324 +1,316 @@
<?php
final class PhabricatorCoreConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Core');
}
public function getDescription() {
return pht('Configure core options, including URIs.');
}
public function getIcon() {
return 'fa-bullseye';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
if (phutil_is_windows()) {
$paths = array();
} else {
$paths = array(
'/bin',
'/usr/bin',
'/usr/local/bin',
);
}
$path = getenv('PATH');
$proto_doc_href = PhabricatorEnv::getDoclink(
'User Guide: Prototype Applications');
$proto_doc_name = pht('User Guide: Prototype Applications');
$applications_app_href = '/applications/';
$silent_description = $this->deformat(pht(<<<EOREMARKUP
This option allows you to stop Phabricator from sending data to most external
services: it will disable email, SMS, repository mirroring, remote builds,
Doorkeeper writes, and webhooks.
This option is intended to allow a Phabricator instance to be exported, copied,
imported, and run in a test environment without impacting users. For example,
if you are migrating to new hardware, you could perform a test migration first
with this flag set, make sure things work, and then do a production cutover
later with higher confidence and less disruption.
Without making use of this flag to silence the temporary test environment,
users would receive duplicate email during the time the test instance and old
production instance were both in operation.
EOREMARKUP
));
return array(
$this->newOption('phabricator.base-uri', 'string', null)
->setLocked(true)
->setSummary(pht('URI where Phabricator is installed.'))
->setDescription(
pht(
'Set the URI where Phabricator is installed. Setting this '.
'improves security by preventing cookies from being set on other '.
'domains, and allows daemons to send emails with links that have '.
'the correct domain.'))
->addExample('http://phabricator.example.com/', pht('Valid Setting')),
$this->newOption('phabricator.production-uri', 'string', null)
->setSummary(
pht('Primary install URI, for multi-environment installs.'))
->setDescription(
pht(
'If you have multiple Phabricator environments (like a '.
'development/staging environment for working on testing '.
'Phabricator, and a production environment for deploying it), '.
'set the production environment URI here so that emails and other '.
'durable URIs will always generate with links pointing at the '.
'production environment. If unset, defaults to `%s`. Most '.
'installs do not need to set this option.',
'phabricator.base-uri'))
->addExample('http://phabricator.example.com/', pht('Valid Setting')),
$this->newOption('phabricator.allowed-uris', 'list<string>', array())
->setLocked(true)
->setSummary(pht('Alternative URIs that can access Phabricator.'))
->setDescription(
pht(
"These alternative URIs will be able to access 'normal' pages ".
"on your Phabricator install. Other features such as OAuth ".
"won't work. The major use case for this is moving installs ".
"across domains."))
->addExample(
"http://phabricator2.example.com/\n".
"http://phabricator3.example.com/",
pht('Valid Setting')),
$this->newOption('phabricator.timezone', 'string', null)
->setSummary(
pht('The timezone Phabricator should use.'))
->setDescription(
pht(
"PHP requires that you set a timezone in your php.ini before ".
"using date functions, or it will emit a warning. If this isn't ".
"possible (for instance, because you are using HPHP) you can set ".
"some valid constant for %s here and Phabricator will set it on ".
"your behalf, silencing the warning.",
'date_default_timezone_set()'))
->addExample('America/New_York', pht('US East (EDT)'))
->addExample('America/Chicago', pht('US Central (CDT)'))
->addExample('America/Boise', pht('US Mountain (MDT)'))
->addExample('America/Los_Angeles', pht('US West (PDT)')),
$this->newOption('phabricator.cookie-prefix', 'string', null)
->setLocked(true)
->setSummary(
pht(
'Set a string Phabricator should use to prefix cookie names.'))
->setDescription(
pht(
'Cookies set for x.com are also sent for y.x.com. Assuming '.
'Phabricator instances are running on both domains, this will '.
'create a collision preventing you from logging in.'))
->addExample('dev', pht('Prefix cookie with "%s"', 'dev')),
$this->newOption('phabricator.show-prototypes', 'bool', false)
->setLocked(true)
->setBoolOptions(
array(
pht('Enable Prototypes'),
pht('Disable Prototypes'),
))
->setSummary(
pht(
'Install applications which are still under development.'))
->setDescription(
pht(
"IMPORTANT: The upstream does not provide support for prototype ".
"applications.".
"\n\n".
"Phabricator includes prototype applications which are in an ".
"**early stage of development**. By default, prototype ".
"applications are not installed, because they are often not yet ".
"developed enough to be generally usable. You can enable ".
"this option to install them if you're developing Phabricator ".
"or are interested in previewing upcoming features.".
"\n\n".
"To learn more about prototypes, see [[ %s | %s ]].".
"\n\n".
"After enabling prototypes, you can selectively uninstall them ".
"(like normal applications).",
$proto_doc_href,
$proto_doc_name)),
$this->newOption('phabricator.serious-business', 'bool', false)
->setBoolOptions(
array(
pht('Serious business'),
pht('Shenanigans'), // That should be interesting to translate. :P
))
->setSummary(
pht('Allows you to remove levity and jokes from the UI.'))
->setDescription(
pht(
'By default, Phabricator includes some flavor text in the UI, '.
'like a prompt to "Weigh In" rather than "Add Comment" in '.
'Maniphest. If you\'d prefer more traditional UI strings like '.
'"Add Comment", you can set this flag to disable most of the '.
'extra flavor.')),
$this->newOption('remarkup.ignored-object-names', 'string', '/^(Q|V)\d$/')
->setSummary(
pht('Text values that match this regex and are also object names '.
'will not be linked.'))
->setDescription(
pht(
'By default, Phabricator links object names in Remarkup fields '.
'to the corresponding object. This regex can be used to modify '.
'this behavior; object names that match this regex will not be '.
'linked.')),
$this->newOption('environment.append-paths', 'list<string>', $paths)
->setSummary(
pht(
'These paths get appended to your %s environment variable.',
'$PATH'))
->setDescription(
pht(
"Phabricator occasionally shells out to other binaries on the ".
"server. An example of this is the `%s` command, used to ".
"syntax-highlight code written in languages other than PHP. By ".
"default, it is assumed that these binaries are in the %s of the ".
"user running Phabricator (normally 'apache', 'httpd', or ".
"'nobody'). Here you can add extra directories to the %s ".
"environment variable, for when these binaries are in ".
"non-standard locations.\n\n".
"Note that you can also put binaries in `%s` (for example, by ".
"symlinking them).\n\n".
"The current value of PATH after configuration is applied is:\n\n".
" lang=text\n".
" %s",
'pygmentize',
'$PATH',
'$PATH',
'phabricator/support/bin/',
$path))
->setLocked(true)
->addExample('/usr/local/bin', pht('Add One Path'))
->addExample("/usr/bin\n/usr/local/bin", pht('Add Multiple Paths')),
$this->newOption('config.lock', 'set', array())
->setLocked(true)
->setDescription(pht('Additional configuration options to lock.')),
$this->newOption('config.hide', 'set', array())
->setLocked(true)
->setDescription(pht('Additional configuration options to hide.')),
$this->newOption('config.ignore-issues', 'set', array())
->setLocked(true)
->setDescription(pht('Setup issues to ignore.')),
$this->newOption('phabricator.env', 'string', null)
->setLocked(true)
->setDescription(pht('Internal.')),
$this->newOption('test.value', 'wild', null)
->setLocked(true)
->setDescription(pht('Unit test value.')),
$this->newOption('phabricator.uninstalled-applications', 'set', array())
->setLocked(true)
->setLockedMessage(pht(
'Use the %s to manage installed applications.',
phutil_tag(
'a',
array(
'href' => $applications_app_href,
),
pht('Applications application'))))
->setDescription(
pht('Array containing list of uninstalled applications.')),
$this->newOption('phabricator.application-settings', 'wild', array())
->setLocked(true)
->setDescription(
pht('Customized settings for Phabricator applications.')),
$this->newOption('phabricator.cache-namespace', 'string', 'phabricator')
->setLocked(true)
->setDescription(pht('Cache namespace.')),
- $this->newOption('phabricator.allow-email-users', 'bool', false)
- ->setBoolOptions(
- array(
- pht('Allow'),
- pht('Disallow'),
- ))
- ->setDescription(
- pht('Allow non-members to interact with tasks over email.')),
$this->newOption('phabricator.silent', 'bool', false)
->setLocked(true)
->setBoolOptions(
array(
pht('Run Silently'),
pht('Run Normally'),
))
->setSummary(pht('Stop Phabricator from sending any email, etc.'))
->setDescription($silent_description),
);
}
protected function didValidateOption(
PhabricatorConfigOption $option,
$value) {
$key = $option->getKey();
if ($key == 'phabricator.base-uri' ||
$key == 'phabricator.production-uri') {
$uri = new PhutilURI($value);
$protocol = $uri->getProtocol();
if ($protocol !== 'http' && $protocol !== 'https') {
throw new PhabricatorConfigValidationException(
pht(
'Config option "%s" is invalid. The URI must start with '.
'"%s" or "%s".',
$key,
'http://',
'https://'));
}
$domain = $uri->getDomain();
if (strpos($domain, '.') === false) {
throw new PhabricatorConfigValidationException(
pht(
'Config option "%s" is invalid. The URI must contain a dot '.
'("%s"), like "%s", not just a bare name like "%s". Some web '.
'browsers will not set cookies on domains with no TLD.',
$key,
'.',
'http://example.com/',
'http://example/'));
}
$path = $uri->getPath();
if ($path !== '' && $path !== '/') {
throw new PhabricatorConfigValidationException(
pht(
"Config option '%s' is invalid. The URI must NOT have a path, ".
"e.g. '%s' is OK, but '%s' is not. Phabricator must be installed ".
"on an entire domain; it can not be installed on a path.",
$key,
'http://phabricator.example.com/',
'http://example.com/phabricator/'));
}
}
if ($key === 'phabricator.timezone') {
$old = date_default_timezone_get();
$ok = @date_default_timezone_set($value);
@date_default_timezone_set($old);
if (!$ok) {
throw new PhabricatorConfigValidationException(
pht(
"Config option '%s' is invalid. The timezone identifier must ".
"be a valid timezone identifier recognized by PHP, like '%s'. "."
You can find a list of valid identifiers here: %s",
$key,
'America/Los_Angeles',
'http://php.net/manual/timezones.php'));
}
}
}
}
diff --git a/src/applications/config/option/PhabricatorDeveloperConfigOptions.php b/src/applications/config/option/PhabricatorDeveloperConfigOptions.php
index a84a7484a..f4b8e8b15 100644
--- a/src/applications/config/option/PhabricatorDeveloperConfigOptions.php
+++ b/src/applications/config/option/PhabricatorDeveloperConfigOptions.php
@@ -1,183 +1,157 @@
<?php
final class PhabricatorDeveloperConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Developer / Debugging');
}
public function getDescription() {
return pht('Options for Phabricator developers, including debugging.');
}
public function getIcon() {
return 'fa-bug';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
return array(
$this->newOption('darkconsole.enabled', 'bool', false)
->setBoolOptions(
array(
pht('Enable DarkConsole'),
pht('Disable DarkConsole'),
))
->setSummary(pht("Enable Phabricator's debugging console."))
->setDescription(
pht(
"DarkConsole is a development and profiling tool built into ".
"Phabricator's web interface. You should leave it disabled unless ".
"you are developing or debugging Phabricator.\n\n".
"Once you activate DarkConsole for the install, **you need to ".
"enable it for your account before it will actually appear on ".
"pages.** You can do this in Settings > Developer Settings.\n\n".
"DarkConsole exposes potentially sensitive data (like queries, ".
"stack traces, and configuration) so you generally should not ".
"turn it on in production.")),
$this->newOption('darkconsole.always-on', 'bool', false)
->setBoolOptions(
array(
pht('Always Activate DarkConsole'),
pht('Require DarkConsole Activation'),
))
->setSummary(pht('Activate DarkConsole on every page.'))
->setDescription(
pht(
"This option allows you to enable DarkConsole on every page, ".
"even for logged-out users. This is only really useful if you ".
"need to debug something on a logged-out page. You should not ".
"enable this option in production.\n\n".
"You must enable DarkConsole by setting '%s' ".
"before this option will have any effect.",
'darkconsole.enabled')),
$this->newOption('debug.time-limit', 'int', null)
->setSummary(
pht(
'Limit page execution time to debug hangs.'))
->setDescription(
pht(
"This option can help debug pages which are taking a very ".
"long time (more than 30 seconds) to render.\n\n".
"If a page is slow to render (but taking less than 30 seconds), ".
"the best tools to use to figure out why it is slow are usually ".
"the DarkConsole service call profiler and XHProf.\n\n".
"However, if a request takes a very long time to return, some ".
"components (like Apache, nginx, or PHP itself) may abort the ".
"request before it finishes. This can prevent you from using ".
"profiling tools to understand page performance in detail.\n\n".
"In these cases, you can use this option to force the page to ".
"abort after a smaller number of seconds (for example, 10), and ".
"dump a useful stack trace. This can provide useful information ".
"about why a page is hanging.\n\n".
"To use this option, set it to a small number (like 10), and ".
"reload a hanging page. The page should exit after 10 seconds ".
"and give you a stack trace.\n\n".
"You should turn this option off (set it to 0) when you are ".
"done with it. Leaving it on creates a small amount of overhead ".
"for all requests, even if they do not hit the time limit.")),
$this->newOption('debug.stop-on-redirect', 'bool', false)
->setBoolOptions(
array(
pht('Stop Before HTTP Redirect'),
pht('Use Normal HTTP Redirects'),
))
->setSummary(
pht(
'Confirm before redirecting so DarkConsole can be examined.'))
->setDescription(
pht(
'Normally, Phabricator issues HTTP redirects after a successful '.
'POST. This can make it difficult to debug things which happen '.
'while processing the POST, because service and profiling '.
'information are lost. By setting this configuration option, '.
'Phabricator will show a page instead of automatically '.
'redirecting, allowing you to examine service and profiling '.
'information. It also makes the UX awful, so you should only '.
'enable it when debugging.')),
$this->newOption('debug.profile-rate', 'int', 0)
->addExample(0, pht('No profiling'))
->addExample(1, pht('Profile every request (slow)'))
->addExample(1000, pht('Profile 0.1%% of all requests'))
->setSummary(pht('Automatically profile some percentage of pages.'))
->setDescription(
pht(
"Normally, Phabricator profiles pages only when explicitly ".
"requested via DarkConsole. However, it may be useful to profile ".
"some pages automatically.\n\n".
"Set this option to a positive integer N to profile 1 / N pages ".
"automatically. For example, setting it to 1 will profile every ".
"page, while setting it to 1000 will profile 1 page per 1000 ".
"requests (i.e., 0.1%% of requests).\n\n".
"Since profiling is slow and generates a lot of data, you should ".
"set this to 0 in production (to disable it) or to a large number ".
"(to collect a few samples, if you're interested in having some ".
"data to look at eventually). In development, it may be useful to ".
"set it to 1 in order to debug performance problems.\n\n".
"NOTE: You must install XHProf for profiling to work.")),
$this->newOption('debug.sample-rate', 'int', 1000)
->setLocked(true)
->addExample(0, pht('No performance sampling.'))
->addExample(1, pht('Sample every request (slow).'))
->addExample(1000, pht('Sample 0.1%% of requests.'))
->setSummary(pht('Automatically sample some fraction of requests.'))
->setDescription(
pht(
"The Multimeter application collects performance samples. You ".
"can use this data to help you understand what Phabricator is ".
"spending time and resources doing, and to identify problematic ".
"access patterns.".
"\n\n".
"This option controls how frequently sampling activates. Set it ".
"to some positive integer N to sample every 1 / N pages.".
"\n\n".
"For most installs, the default value (1 sample per 1000 pages) ".
"should collect enough data to be useful without requiring much ".
"storage or meaningfully impacting performance. If you're ".
"investigating performance issues, you can adjust the rate ".
"in order to collect more data.")),
$this->newOption('phabricator.developer-mode', 'bool', false)
->setBoolOptions(
array(
pht('Enable developer mode'),
pht('Disable developer mode'),
))
- ->setSummary(pht('Enable verbose error reporting and disk reads.'))
- ->setDescription(
- pht(
- 'This option enables verbose error reporting (stack traces, '.
- 'error callouts) and forces disk reads of static assets on '.
- 'every reload.')),
- $this->newOption('celerity.minify', 'bool', true)
- ->setBoolOptions(
- array(
- pht('Minify static resources.'),
- pht("Don't minify static resources."),
- ))
- ->setSummary(pht('Minify static Celerity resources.'))
- ->setDescription(
- pht(
- 'Minify static resources by removing whitespace and comments. You '.
- 'should enable this in production, but disable it in '.
- 'development.')),
- $this->newOption('cache.enable-deflate', 'bool', true)
- ->setBoolOptions(
- array(
- pht('Enable deflate compression'),
- pht('Disable deflate compression'),
- ))
- ->setSummary(
- pht('Toggle %s-based compression for some caches.', 'gzdeflate()'))
+ ->setSummary(pht('Enable verbose error reporting and disk reads.'))
->setDescription(
pht(
- 'Set this to false to disable the use of %s-based '.
- 'compression in some caches. This may give you less performant '.
- '(but more debuggable) caching.',
- 'gzdeflate()')),
+ 'This option enables verbose error reporting (stack traces, '.
+ 'error callouts) and forces disk reads of static assets on '.
+ 'every reload.')),
);
}
}
diff --git a/src/applications/config/option/PhabricatorExtendingPhabricatorConfigOptions.php b/src/applications/config/option/PhabricatorExtendingPhabricatorConfigOptions.php
index 28a0f619d..0a3ea32e4 100644
--- a/src/applications/config/option/PhabricatorExtendingPhabricatorConfigOptions.php
+++ b/src/applications/config/option/PhabricatorExtendingPhabricatorConfigOptions.php
@@ -1,50 +1,42 @@
<?php
final class PhabricatorExtendingPhabricatorConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Extending Phabricator');
}
public function getDescription() {
return pht('Make Phabricator even cooler!');
}
public function getIcon() {
return 'fa-rocket';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
return array(
$this->newOption('load-libraries', 'list<string>', array())
->setLocked(true)
->setSummary(pht('Paths to additional phutil libraries to load.'))
->addExample('/srv/our-libs/sekrit-phutil', pht('Valid Setting')),
$this->newOption('events.listeners', 'list<string>', array())
->setLocked(true)
->setSummary(
pht('Listeners receive callbacks when interesting things occur.'))
->setDescription(
pht(
'You can respond to various application events by installing '.
'listeners, which will receive callbacks when interesting things '.
'occur. Specify a list of classes which extend '.
'PhabricatorEventListener here.'))
->addExample('MyEventListener', pht('Valid Setting')),
- $this->newOption(
- 'aphront.default-application-configuration-class',
- 'class',
- 'AphrontDefaultApplicationConfiguration')
- ->setLocked(true)
- ->setBaseClass('AphrontApplicationConfiguration')
- // TODO: This could probably use some better documentation.
- ->setDescription(pht('Application configuration class.')),
);
}
}
diff --git a/src/applications/config/option/PhabricatorMailgunConfigOptions.php b/src/applications/config/option/PhabricatorMailgunConfigOptions.php
deleted file mode 100644
index cc4e71fb5..000000000
--- a/src/applications/config/option/PhabricatorMailgunConfigOptions.php
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php
-
-final class PhabricatorMailgunConfigOptions
- extends PhabricatorApplicationConfigOptions {
-
- public function getName() {
- return pht('Integration with Mailgun');
- }
-
- public function getDescription() {
- return pht('Configure Mailgun integration.');
- }
-
- public function getIcon() {
- return 'fa-send-o';
- }
-
- public function getGroup() {
- return 'core';
- }
-
- public function getOptions() {
- return array(
- $this->newOption('mailgun.domain', 'string', null)
- ->setLocked(true)
- ->setDescription(
- pht(
- 'Mailgun domain name. See %s.',
- 'https://mailgun.com/cp/domains'))
- ->addExample('mycompany.com', pht('Use specific domain')),
- $this->newOption('mailgun.api-key', 'string', null)
- ->setHidden(true)
- ->setDescription(pht('Mailgun API key.')),
- );
-
- }
-
-}
diff --git a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php
index ad8a62d18..ce24d48ea 100644
--- a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php
+++ b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php
@@ -1,321 +1,288 @@
<?php
final class PhabricatorMetaMTAConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Mail');
}
public function getDescription() {
return pht('Configure Mail.');
}
public function getIcon() {
return 'fa-send';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
$send_as_user_desc = $this->deformat(pht(<<<EODOC
When a user takes an action which generates an email notification (like
commenting on a Differential revision), Phabricator can either send that mail
"From" the user's email address (like "alincoln@logcabin.com") or "From" the
'%s' address.
The user experience is generally better if Phabricator uses the user's real
address as the "From" since the messages are easier to organize when they appear
in mail clients, but this will only work if the server is authorized to send
email on behalf of the "From" domain. Practically, this means:
- If you are doing an install for Example Corp and all the users will have
corporate @corp.example.com addresses and any hosts Phabricator is running
on are authorized to send email from corp.example.com, you can enable this
to make the user experience a little better.
- If you are doing an install for an open source project and your users will
be registering via Facebook and using personal email addresses, you probably
should not enable this or all of your outgoing email might vanish into SFP
blackholes.
- If your install is anything else, you're safer leaving this off, at least
initially, since the risk in turning it on is that your outgoing mail will
never arrive.
EODOC
,
'metamta.default-address'));
$one_mail_per_recipient_desc = $this->deformat(pht(<<<EODOC
When a message is sent to multiple recipients (for example, several reviewers on
a code review), Phabricator can either deliver one email to everyone (e.g., "To:
alincoln, usgrant, htaft") or separate emails to each user (e.g., "To:
alincoln", "To: usgrant", "To: htaft"). The major advantages and disadvantages
of each approach are:
- One mail to everyone:
- This violates policy controls. The body of the mail is generated without
respect for object policies.
- Recipients can see To/Cc at a glance.
- If you use mailing lists, you won't get duplicate mail if you're
a normal recipient and also Cc'd on a mailing list.
- Getting threading to work properly is harder, and probably requires
making mail less useful by turning off options.
- Sometimes people will "Reply All", which can send mail to too many
recipients. Phabricator will try not to send mail to users who already
received a similar message, but can not prevent all stray email arising
from "Reply All".
- Not supported with a private reply-to address.
- Mail messages are sent in the server default translation.
- Mail that must be delivered over secure channels will leak the recipient
list in the "To" and "Cc" headers.
- One mail to each user:
- Policy controls work correctly and are enforced per-user.
- Recipients need to look in the mail body to see To/Cc.
- If you use mailing lists, recipients may sometimes get duplicate
mail.
- Getting threading to work properly is easier, and threading settings
can be customzied by each user.
- "Reply All" will never send extra mail to other users involved in the
thread.
- Required if private reply-to addresses are configured.
- Mail messages are sent in the language of user preference.
EODOC
));
$reply_hints_description = $this->deformat(pht(<<<EODOC
You can disable the hints under "REPLY HANDLER ACTIONS" if users prefer
smaller messages. The actions themselves will still work properly.
EODOC
));
$recipient_hints_description = $this->deformat(pht(<<<EODOC
You can disable the "To:" and "Cc:" footers in mail if users prefer smaller
messages.
EODOC
));
$email_preferences_description = $this->deformat(pht(<<<EODOC
You can disable the email preference link in emails if users prefer smaller
emails.
EODOC
));
$re_prefix_description = $this->deformat(pht(<<<EODOC
Mail.app on OS X Lion won't respect threading headers unless the subject is
prefixed with "Re:". If you enable this option, Phabricator will add "Re:" to
the subject line of all mail which is expected to thread. If you've set
'metamta.one-mail-per-recipient', users can override this setting in their
preferences.
EODOC
));
$vary_subjects_description = $this->deformat(pht(<<<EODOC
If true, allow MetaMTA to change mail subjects to put text like '[Accepted]' and
'[Commented]' in them. This makes subjects more useful, but might break
threading on some clients. If you've set '%s', users can override this setting
in their preferences.
EODOC
,
'metamta.one-mail-per-recipient'));
$reply_to_description = $this->deformat(pht(<<<EODOC
If you enable `%s`, Phabricator uses "From" to authenticate users. You can
additionally enable this setting to try to authenticate with 'Reply-To'. Note
that this is completely spoofable and insecure (any user can set any 'Reply-To'
address) but depending on the nature of your install or other deliverability
conditions this might be okay. Generally, you can't do much more by spoofing
Reply-To than be annoying (you can write but not read content). But this is
still **COMPLETELY INSECURE**.
EODOC
,
'metamta.public-replies'));
$adapter_description = $this->deformat(pht(<<<EODOC
Adapter class to use to transmit mail to the MTA. The default uses
PHPMailerLite, which will invoke "sendmail". This is appropriate if sendmail
actually works on your host, but if you haven't configured mail it may not be so
great. A number of other mailers are available (e.g., SES, SendGrid, SMTP,
custom mailers). This option is deprecated in favor of 'cluster.mailers'.
EODOC
-));
-
- $placeholder_description = $this->deformat(pht(<<<EODOC
-When sending a message that has no To recipient (i.e. all recipients are CC'd),
-set the To field to the following value. If no value is set, messages with no
-To will have their CCs upgraded to To.
-EODOC
));
$public_replies_description = $this->deformat(pht(<<<EODOC
By default, Phabricator generates unique reply-to addresses and sends a separate
email to each recipient when you enable reply handling. This is more secure than
using "From" to establish user identity, but can mean users may receive multiple
emails when they are on mailing lists. Instead, you can use a single, non-unique
reply to address and authenticate users based on the "From" address by setting
this to 'true'. This trades away a little bit of security for convenience, but
it's reasonable in many installs. Object interactions are still protected using
hashes in the single public email address, so objects can not be replied to
blindly.
EODOC
));
$single_description = $this->deformat(pht(<<<EODOC
If you want to use a single mailbox for Phabricator reply mail, you can use this
and set a common prefix for reply addresses generated by Phabricator. It will
make use of the fact that a mail-address such as
`phabricator+D123+1hjk213h@example.com` will be delivered to the `phabricator`
user's mailbox. Set this to the left part of the email address and it will be
prepended to all generated reply addresses.
For example, if you want to use `phabricator@example.com`, this should be set
to `phabricator`.
EODOC
));
$address_description = $this->deformat(pht(<<<EODOC
When email is sent, what format should Phabricator use for user's email
addresses? Valid values are:
- `short`: 'gwashington <gwashington@example.com>'
- `real`: 'George Washington <gwashington@example.com>'
- `full`: 'gwashington (George Washington) <gwashington@example.com>'
The default is `full`.
EODOC
));
$mailers_description = $this->deformat(pht(<<<EODOC
Define one or more mail transmission services. For help with configuring
mailers, see **[[ %s | %s ]]** in the documentation.
EODOC
,
PhabricatorEnv::getDoclink('Configuring Outbound Email'),
pht('Configuring Outbound Email')));
return array(
- $this->newOption('cluster.mailers', 'cluster.mailers', null)
+ $this->newOption('cluster.mailers', 'cluster.mailers', array())
->setHidden(true)
->setDescription($mailers_description),
- $this->newOption(
- 'metamta.default-address',
- 'string',
- 'noreply@phabricator.example.com')
+ $this->newOption('metamta.default-address', 'string', null)
->setDescription(pht('Default "From" address.')),
- $this->newOption(
- 'metamta.domain',
- 'string',
- 'phabricator.example.com')
- ->setDescription(pht('Domain used to generate Message-IDs.')),
- $this->newOption(
- 'metamta.mail-adapter',
- 'class',
- 'PhabricatorMailImplementationPHPMailerLiteAdapter')
- ->setBaseClass('PhabricatorMailImplementationAdapter')
- ->setSummary(pht('Control how mail is sent.'))
- ->setDescription($adapter_description),
$this->newOption(
'metamta.one-mail-per-recipient',
'bool',
true)
->setLocked(true)
->setBoolOptions(
array(
pht('Send Mail To Each Recipient'),
pht('Send Mail To All Recipients'),
))
->setSummary(
pht(
'Controls whether Phabricator sends one email with multiple '.
'recipients in the "To:" line, or multiple emails, each with a '.
'single recipient in the "To:" line.'))
->setDescription($one_mail_per_recipient_desc),
$this->newOption('metamta.can-send-as-user', 'bool', false)
->setBoolOptions(
array(
pht('Send as User Taking Action'),
pht('Send as Phabricator'),
))
->setSummary(
pht(
'Controls whether Phabricator sends email "From" users.'))
->setDescription($send_as_user_desc),
$this->newOption(
'metamta.reply-handler-domain',
'string',
null)
->setLocked(true)
->setDescription(pht('Domain used for reply email addresses.'))
->addExample('phabricator.example.com', ''),
$this->newOption('metamta.recipients.show-hints', 'bool', true)
->setBoolOptions(
array(
pht('Show Recipient Hints'),
pht('No Recipient Hints'),
))
->setSummary(pht('Show "To:" and "Cc:" footer hints in email.'))
->setDescription($recipient_hints_description),
$this->newOption('metamta.email-preferences', 'bool', true)
->setBoolOptions(
array(
pht('Show Email Preferences Link'),
pht('No Email Preferences Link'),
))
->setSummary(pht('Show email preferences link in email.'))
->setDescription($email_preferences_description),
- $this->newOption('metamta.insecure-auth-with-reply-to', 'bool', false)
- ->setBoolOptions(
- array(
- pht('Allow Insecure Reply-To Auth'),
- pht('Disallow Reply-To Auth'),
- ))
- ->setSummary(pht('Trust "Reply-To" headers for authentication.'))
- ->setDescription($reply_to_description),
- $this->newOption('metamta.placeholder-to-recipient', 'string', null)
- ->setSummary(pht('Placeholder for mail with only CCs.'))
- ->setDescription($placeholder_description),
$this->newOption('metamta.public-replies', 'bool', false)
->setBoolOptions(
array(
pht('Use Public Replies (Less Secure)'),
pht('Use Private Replies (More Secure)'),
))
->setSummary(
pht(
'Phabricator can use less-secure but mailing list friendly public '.
'reply addresses.'))
->setDescription($public_replies_description),
$this->newOption('metamta.single-reply-handler-prefix', 'string', null)
->setSummary(
pht('Allow Phabricator to use a single mailbox for all replies.'))
->setDescription($single_description),
$this->newOption('metamta.user-address-format', 'enum', 'full')
->setEnumOptions(
array(
'short' => pht('Short'),
'real' => pht('Real'),
'full' => pht('Full'),
))
->setSummary(pht('Control how Phabricator renders user names in mail.'))
->setDescription($address_description)
->addExample('gwashington <gwashington@example.com>', 'short')
->addExample('George Washington <gwashington@example.com>', 'real')
->addExample(
'gwashington (George Washington) <gwashington@example.com>',
'full'),
$this->newOption('metamta.email-body-limit', 'int', 524288)
->setDescription(
pht(
'You can set a limit for the maximum byte size of outbound mail. '.
'Mail which is larger than this limit will be truncated before '.
'being sent. This can be useful if your MTA rejects mail which '.
'exceeds some limit (this is reasonably common). Specify a value '.
'in bytes.'))
->setSummary(pht('Global cap for size of generated emails (bytes).'))
->addExample(524288, pht('Truncate at 512KB'))
->addExample(1048576, pht('Truncate at 1MB')),
);
}
}
diff --git a/src/applications/config/option/PhabricatorPHPMailerConfigOptions.php b/src/applications/config/option/PhabricatorPHPMailerConfigOptions.php
deleted file mode 100644
index 2c0021341..000000000
--- a/src/applications/config/option/PhabricatorPHPMailerConfigOptions.php
+++ /dev/null
@@ -1,72 +0,0 @@
-<?php
-
-final class PhabricatorPHPMailerConfigOptions
- extends PhabricatorApplicationConfigOptions {
-
- public function getName() {
- return pht('PHPMailer');
- }
-
- public function getDescription() {
- return pht('Configure PHPMailer.');
- }
-
- public function getIcon() {
- return 'fa-send-o';
- }
-
- public function getGroup() {
- return 'core';
- }
-
- public function getOptions() {
- return array(
- $this->newOption('phpmailer.mailer', 'string', 'smtp')
- ->setLocked(true)
- ->setSummary(pht('Configure mailer used by PHPMailer.'))
- ->setDescription(
- pht(
- "If you're using PHPMailer to send email, provide the mailer and ".
- "options here. PHPMailer is much more enormous than ".
- "PHPMailerLite, and provides more mailers and greater enormity. ".
- "You need it when you want to use SMTP instead of sendmail as the ".
- "mailer.")),
- $this->newOption('phpmailer.smtp-host', 'string', null)
- ->setLocked(true)
- ->setDescription(pht('Host for SMTP.')),
- $this->newOption('phpmailer.smtp-port', 'int', 25)
- ->setLocked(true)
- ->setDescription(pht('Port for SMTP.')),
- // TODO: Implement "enum"? Valid values are empty, 'tls', or 'ssl'.
- $this->newOption('phpmailer.smtp-protocol', 'string', null)
- ->setLocked(true)
- ->setSummary(pht('Configure TLS or SSL for SMTP.'))
- ->setDescription(
- pht(
- "Using PHPMailer with SMTP, you can set this to one of '%s' or ".
- "'%s' to use TLS or SSL, respectively. Leave it blank for ".
- "vanilla SMTP. If you're sending via Gmail, set it to '%s'.",
- 'tls',
- 'ssl',
- 'ssl')),
- $this->newOption('phpmailer.smtp-user', 'string', null)
- ->setLocked(true)
- ->setDescription(pht('Username for SMTP.')),
- $this->newOption('phpmailer.smtp-password', 'string', null)
- ->setHidden(true)
- ->setDescription(pht('Password for SMTP.')),
- $this->newOption('phpmailer.smtp-encoding', 'string', 'base64')
- ->setSummary(pht('Configure how mail is encoded.'))
- ->setDescription(
- pht(
- "Mail is normally encoded in `8bit`, which works correctly with ".
- "most MTAs. However, some MTAs do not work well with this ".
- "encoding. If you're having trouble with mail being mangled or ".
- "arriving with too many or too few newlines, you may try ".
- "adjusting this setting.\n\n".
- "Supported values are `8bit`, `quoted-printable`, ".
- "`7bit`, `binary` and `base64`.")),
- );
- }
-
-}
diff --git a/src/applications/config/option/PhabricatorSMSConfigOptions.php b/src/applications/config/option/PhabricatorSMSConfigOptions.php
deleted file mode 100644
index 08f2e50ee..000000000
--- a/src/applications/config/option/PhabricatorSMSConfigOptions.php
+++ /dev/null
@@ -1,60 +0,0 @@
-<?php
-
-final class PhabricatorSMSConfigOptions
- extends PhabricatorApplicationConfigOptions {
-
- public function getName() {
- return pht('SMS');
- }
-
- public function getDescription() {
- return pht('Configure SMS.');
- }
-
- public function getIcon() {
- return 'fa-mobile';
- }
-
- public function getGroup() {
- return 'core';
- }
-
- public function getOptions() {
- $adapter_description = pht(
- 'Adapter class to use to transmit SMS to an external provider. A given '.
- 'external provider will most likely need more configuration which will '.
- 'most likely require registration and payment for the service.');
-
- return array(
- $this->newOption(
- 'sms.default-sender',
- 'string',
- null)
- ->setDescription(pht('Default "from" number.'))
- ->addExample('8675309', 'Jenny still has this number')
- ->addExample('18005555555', 'Maybe not a real number'),
- $this->newOption(
- 'sms.default-adapter',
- 'class',
- null)
- ->setBaseClass('PhabricatorSMSImplementationAdapter')
- ->setSummary(pht('Control how SMS is sent.'))
- ->setDescription($adapter_description),
- $this->newOption(
- 'twilio.account-sid',
- 'string',
- null)
- ->setDescription(pht('Account ID on Twilio service.'))
- ->setLocked(true)
- ->addExample('gf5kzccfn2sfknpnadvz7kokv6nz5v', pht('30 characters')),
- $this->newOption(
- 'twilio.auth-token',
- 'string',
- null)
- ->setDescription(pht('Authorization token from Twilio service.'))
- ->setHidden(true)
- ->addExample('f3jsi4i67wiwt6w54hf2zwvy3fjf5h', pht('30 characters')),
- );
- }
-
-}
diff --git a/src/applications/config/option/PhabricatorSecurityConfigOptions.php b/src/applications/config/option/PhabricatorSecurityConfigOptions.php
index 78f8e89e6..50fc24a85 100644
--- a/src/applications/config/option/PhabricatorSecurityConfigOptions.php
+++ b/src/applications/config/option/PhabricatorSecurityConfigOptions.php
@@ -1,365 +1,321 @@
<?php
final class PhabricatorSecurityConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Security');
}
public function getDescription() {
return pht('Security options.');
}
public function getIcon() {
return 'fa-lock';
}
public function getGroup() {
return 'core';
}
public function getOptions() {
$doc_href = PhabricatorEnv::getDoclink('Configuring a File Domain');
$doc_name = pht('Configuration Guide: Configuring a File Domain');
$default_address_blacklist = array(
// This is all of the IANA special/reserved blocks in IPv4 space.
'0.0.0.0/8',
'10.0.0.0/8',
'100.64.0.0/10',
'127.0.0.0/8',
'169.254.0.0/16',
'172.16.0.0/12',
'192.0.0.0/24',
'192.0.2.0/24',
'192.88.99.0/24',
'192.168.0.0/16',
'198.18.0.0/15',
'198.51.100.0/24',
'203.0.113.0/24',
'224.0.0.0/4',
'240.0.0.0/4',
'255.255.255.255/32',
// And these are the IANA special/reserved blocks in IPv6 space.
'::/128',
'::1/128',
'::ffff:0:0/96',
'100::/64',
'64:ff9b::/96',
'2001::/32',
'2001:10::/28',
'2001:20::/28',
'2001:db8::/32',
'2002::/16',
'fc00::/7',
'fe80::/10',
'ff00::/8',
);
$keyring_type = 'custom:PhabricatorKeyringConfigOptionType';
$keyring_description = $this->deformat(pht(<<<EOTEXT
The keyring stores master encryption keys. For help with configuring a keyring
and encryption, see **[[ %s | Configuring Encryption ]]**.
EOTEXT
,
PhabricatorEnv::getDoclink('Configuring Encryption')));
$require_mfa_description = $this->deformat(pht(<<<EOTEXT
By default, Phabricator allows users to add multi-factor authentication to
their accounts, but does not require it. By enabling this option, you can
force all users to add at least one authentication factor before they can use
their accounts.
Administrators can query a list of users who do not have MFA configured in
{nav People}:
- **[[ %s | %s ]]**
EOTEXT
,
'/people/?mfa=false',
pht('List of Users Without MFA')));
return array(
$this->newOption('security.alternate-file-domain', 'string', null)
->setLocked(true)
->setSummary(pht('Alternate domain to serve files from.'))
->setDescription(
pht(
'By default, Phabricator serves files from the same domain '.
'the application is served from. This is convenient, but '.
'presents a security risk.'.
"\n\n".
'You should configure a CDN or alternate file domain to mitigate '.
'this risk. Configuring a CDN will also improve performance. See '.
'[[ %s | %s ]] for instructions.',
$doc_href,
$doc_name))
->addExample('https://files.phabcdn.net/', pht('Valid Setting')),
$this->newOption(
'security.hmac-key',
'string',
'[D\t~Y7eNmnQGJ;rnH6aF;m2!vJ8@v8C=Cs:aQS\.Qw')
->setHidden(true)
->setSummary(
pht('Key for HMAC digests.'))
->setDescription(
pht(
'Default key for HMAC digests where the key is not important '.
'(i.e., the hash itself is secret). You can change this if you '.
'want (to any other string), but doing so will break existing '.
'sessions and CSRF tokens. This option is deprecated. Newer '.
'code automatically manages HMAC keys.')),
$this->newOption('security.require-https', 'bool', false)
->setLocked(true)
->setSummary(
pht('Force users to connect via HTTPS instead of HTTP.'))
->setDescription(
pht(
"If the web server responds to both HTTP and HTTPS requests but ".
"you want users to connect with only HTTPS, you can set this ".
"to `true` to make Phabricator redirect HTTP requests to HTTPS.".
"\n\n".
"Normally, you should just configure your server not to accept ".
"HTTP traffic, but this setting may be useful if you originally ".
"used HTTP and have now switched to HTTPS but don't want to ".
"break old links, or if your webserver sits behind a load ".
"balancer which terminates HTTPS connections and you can not ".
"reasonably configure more granular behavior there.".
"\n\n".
"IMPORTANT: Phabricator determines if a request is HTTPS or not ".
"by examining the PHP `%s` variable. If you run ".
"Apache/mod_php this will probably be set correctly for you ".
"automatically, but if you run Phabricator as CGI/FCGI (e.g., ".
"through nginx or lighttpd), you need to configure your web ".
"server so that it passes the value correctly based on the ".
"connection type.".
"\n\n".
"If you configure Phabricator in cluster mode, note that this ".
"setting is ignored by intracluster requests.",
"\$_SERVER['HTTPS']"))
->setBoolOptions(
array(
pht('Force HTTPS'),
pht('Allow HTTP'),
)),
$this->newOption('security.require-multi-factor-auth', 'bool', false)
->setLocked(true)
->setSummary(
pht('Require all users to configure multi-factor authentication.'))
->setDescription($require_mfa_description)
->setBoolOptions(
array(
pht('Multi-Factor Required'),
pht('Multi-Factor Optional'),
)),
- $this->newOption(
- 'phabricator.csrf-key',
- 'string',
- '0b7ec0592e0a2829d8b71df2fa269b2c6172eca3')
- ->setHidden(true)
- ->setSummary(
- pht('Hashed with other inputs to generate CSRF tokens.'))
- ->setDescription(
- pht(
- 'This is hashed with other inputs to generate CSRF tokens. If '.
- 'you want, you can change it to some other string which is '.
- 'unique to your install. This will make your install more secure '.
- 'in a vague, mostly theoretical way. But it will take you like 3 '.
- 'seconds of mashing on your keyboard to set it up so you might '.
- 'as well.')),
- $this->newOption(
- 'phabricator.mail-key',
- 'string',
- '5ce3e7e8787f6e40dfae861da315a5cdf1018f12')
- ->setHidden(true)
- ->setSummary(
- pht('Hashed with other inputs to generate mail tokens.'))
- ->setDescription(
- pht(
- "This is hashed with other inputs to generate mail tokens. If ".
- "you want, you can change it to some other string which is ".
- "unique to your install. In particular, you will want to do ".
- "this if you accidentally send a bunch of mail somewhere you ".
- "shouldn't have, to invalidate all old reply-to addresses.")),
$this->newOption(
'uri.allowed-protocols',
'set',
array(
'http' => true,
'https' => true,
'mailto' => true,
))
->setSummary(
pht('Determines which URI protocols are auto-linked.'))
->setDescription(
pht(
"When users write comments which have URIs, they'll be ".
"automatically linked if the protocol appears in this set. This ".
"whitelist is primarily to prevent security issues like ".
"%s URIs.",
'javascript://'))
->addExample("http\nhttps", pht('Valid Setting'))
->setLocked(true),
$this->newOption(
'uri.allowed-editor-protocols',
'set',
array(
'http' => true,
'https' => true,
// This handler is installed by Textmate.
'txmt' => true,
// This handler is for MacVim.
'mvim' => true,
// Unofficial handler for Vim.
'vim' => true,
// Unofficial handler for Sublime.
'subl' => true,
// Unofficial handler for Emacs.
'emacs' => true,
// This isn't a standard handler installed by an application, but
// is a reasonable name for a user-installed handler.
'editor' => true,
))
->setSummary(pht('Whitelists editor protocols for "Open in Editor".'))
->setDescription(
pht(
'Users can configure a URI pattern to open files in a text '.
'editor. The URI must use a protocol on this whitelist.'))
->setLocked(true),
- $this->newOption(
- 'celerity.resource-hash',
- 'string',
- 'd9455ea150622ee044f7931dabfa52aa')
- ->setSummary(
- pht('An input to the hash function when building resource hashes.'))
- ->setDescription(
- pht(
- 'This value is an input to the hash function when building '.
- 'resource hashes. It has no security value, but if you '.
- 'accidentally poison user caches (by pushing a bad patch or '.
- 'having something go wrong with a CDN, e.g.) you can change this '.
- 'to something else and rebuild the Celerity map to break user '.
- 'caches. Unless you are doing Celerity development, it is '.
- 'exceptionally unlikely that you need to modify this.')),
$this->newOption('remarkup.enable-embedded-youtube', 'bool', false)
->setBoolOptions(
array(
pht('Embed YouTube videos'),
pht("Don't embed YouTube videos"),
))
->setSummary(
pht('Determines whether or not YouTube videos get embedded.'))
->setDescription(
pht(
"If you enable this, linked YouTube videos will be embedded ".
"inline. This has mild security implications (you'll leak ".
"referrers to YouTube) and is pretty silly (but sort of ".
"awesome).")),
$this->newOption(
'security.outbound-blacklist',
'list<string>',
$default_address_blacklist)
->setLocked(true)
->setSummary(
pht(
'Blacklist subnets to prevent user-initiated outbound '.
'requests.'))
->setDescription(
pht(
'Phabricator users can make requests to other services from '.
'the Phabricator host in some circumstances (for example, by '.
'creating a repository with a remote URL or having Phabricator '.
'fetch an image from a remote server).'.
"\n\n".
'This may represent a security vulnerability if services on '.
'the same subnet will accept commands or reveal private '.
'information over unauthenticated HTTP GET, based on the source '.
'IP address. In particular, all hosts in EC2 have access to '.
'such a service.'.
"\n\n".
'This option defines a list of netblocks which Phabricator '.
'will decline to connect to. Generally, you should list all '.
'private IP space here.'))
->addExample(array('0.0.0.0/0'), pht('No Outbound Requests')),
$this->newOption('security.strict-transport-security', 'bool', false)
->setLocked(true)
->setBoolOptions(
array(
pht('Use HSTS'),
pht('Do Not Use HSTS'),
))
->setSummary(pht('Enable HTTP Strict Transport Security (HSTS).'))
->setDescription(
pht(
'HTTP Strict Transport Security (HSTS) sends a header which '.
'instructs browsers that the site should only be accessed '.
'over HTTPS, never HTTP. This defuses an attack where an '.
'adversary gains access to your network, then proxies requests '.
'through an unsecured link.'.
"\n\n".
'Do not enable this option if you serve (or plan to ever serve) '.
'unsecured content over plain HTTP. It is very difficult to '.
'undo this change once users\' browsers have accepted the '.
'setting.')),
$this->newOption('keyring', $keyring_type, array())
->setHidden(true)
->setSummary(pht('Configure master encryption keys.'))
->setDescription($keyring_description),
);
}
protected function didValidateOption(
PhabricatorConfigOption $option,
$value) {
$key = $option->getKey();
if ($key == 'security.alternate-file-domain') {
$uri = new PhutilURI($value);
$protocol = $uri->getProtocol();
if ($protocol !== 'http' && $protocol !== 'https') {
throw new PhabricatorConfigValidationException(
pht(
"Config option '%s' is invalid. The URI must start with ".
"'%s' or '%s'.",
$key,
'http://',
'https://'));
}
$domain = $uri->getDomain();
if (strpos($domain, '.') === false) {
throw new PhabricatorConfigValidationException(
pht(
"Config option '%s' is invalid. The URI must contain a dot ('.'), ".
"like '%s', not just a bare name like '%s'. ".
"Some web browsers will not set cookies on domains with no TLD.",
$key,
'http://example.com/',
'http://example/'));
}
$path = $uri->getPath();
if ($path !== '' && $path !== '/') {
throw new PhabricatorConfigValidationException(
pht(
"Config option '%s' is invalid. The URI must NOT have a path, ".
"e.g. '%s' is OK, but '%s' is not. Phabricator must be installed ".
"on an entire domain; it can not be installed on a path.",
$key,
'http://phabricator.example.com/',
'http://example.com/phabricator/'));
}
}
}
}
diff --git a/src/applications/config/option/PhabricatorSendGridConfigOptions.php b/src/applications/config/option/PhabricatorSendGridConfigOptions.php
deleted file mode 100644
index 0baa7390a..000000000
--- a/src/applications/config/option/PhabricatorSendGridConfigOptions.php
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php
-
-final class PhabricatorSendGridConfigOptions
- extends PhabricatorApplicationConfigOptions {
-
- public function getName() {
- return pht('Integration with SendGrid');
- }
-
- public function getDescription() {
- return pht('Configure SendGrid integration.');
- }
-
- public function getIcon() {
- return 'fa-send-o';
- }
-
- public function getGroup() {
- return 'core';
- }
-
- public function getOptions() {
- return array(
- $this->newOption('sendgrid.api-user', 'string', null)
- ->setLocked(true)
- ->setDescription(pht('SendGrid API username.')),
- $this->newOption('sendgrid.api-key', 'string', null)
- ->setHidden(true)
- ->setDescription(pht('SendGrid API key.')),
- );
- }
-
-}
diff --git a/src/applications/config/storage/PhabricatorConfigEntry.php b/src/applications/config/storage/PhabricatorConfigEntry.php
index a8f00133a..3462c4e7b 100644
--- a/src/applications/config/storage/PhabricatorConfigEntry.php
+++ b/src/applications/config/storage/PhabricatorConfigEntry.php
@@ -1,98 +1,87 @@
<?php
final class PhabricatorConfigEntry
extends PhabricatorConfigEntryDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface {
protected $namespace;
protected $configKey;
protected $value;
protected $isDeleted;
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'value' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'namespace' => 'text64',
'configKey' => 'text64',
'isDeleted' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_name' => array(
'columns' => array('namespace', 'configKey'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorConfigConfigPHIDType::TYPECONST);
}
public static function loadConfigEntry($key) {
$config_entry = id(new PhabricatorConfigEntry())
->loadOneWhere(
'configKey = %s AND namespace = %s',
$key,
'default');
if (!$config_entry) {
$config_entry = id(new PhabricatorConfigEntry())
->setConfigKey($key)
->setNamespace('default')
->setIsDeleted(0);
}
return $config_entry;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorConfigEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorConfigTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return PhabricatorPolicies::POLICY_ADMIN;
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
}
diff --git a/src/applications/conpherence/config/ConpherenceConfigOptions.php b/src/applications/conpherence/config/ConpherenceConfigOptions.php
deleted file mode 100644
index 24e1cb126..000000000
--- a/src/applications/conpherence/config/ConpherenceConfigOptions.php
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-
-final class ConpherenceConfigOptions
- extends PhabricatorApplicationConfigOptions {
-
- public function getName() {
- return pht('Conpherence');
- }
-
- public function getDescription() {
- return pht('Configure Conpherence messaging.');
- }
-
- public function getIcon() {
- return 'fa-comments';
- }
-
- public function getGroup() {
- return 'apps';
- }
-
- public function getOptions() {
- return array(
- $this->newOption(
- 'metamta.conpherence.subject-prefix',
- 'string',
- '[Conpherence]')
- ->setDescription(pht('Subject prefix for Conpherence mail.')),
- );
- }
-
-}
diff --git a/src/applications/conpherence/editor/ConpherenceEditor.php b/src/applications/conpherence/editor/ConpherenceEditor.php
index 3b4ad09e1..a1d6431d8 100644
--- a/src/applications/conpherence/editor/ConpherenceEditor.php
+++ b/src/applications/conpherence/editor/ConpherenceEditor.php
@@ -1,249 +1,249 @@
<?php
final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor {
const ERROR_EMPTY_PARTICIPANTS = 'error-empty-participants';
const ERROR_EMPTY_MESSAGE = 'error-empty-message';
public function getEditorApplicationClass() {
return 'PhabricatorConpherenceApplication';
}
public function getEditorObjectsDescription() {
return pht('Conpherence Rooms');
}
public static function createThread(
PhabricatorUser $creator,
array $participant_phids,
$title,
$message,
PhabricatorContentSource $source,
$topic) {
$conpherence = ConpherenceThread::initializeNewRoom($creator);
$errors = array();
if (empty($participant_phids)) {
$errors[] = self::ERROR_EMPTY_PARTICIPANTS;
} else {
$participant_phids[] = $creator->getPHID();
$participant_phids = array_unique($participant_phids);
}
if (empty($message)) {
$errors[] = self::ERROR_EMPTY_MESSAGE;
}
if (!$errors) {
$xactions = array();
$xactions[] = id(new ConpherenceTransaction())
->setTransactionType(
ConpherenceThreadParticipantsTransaction::TRANSACTIONTYPE)
->setNewValue(array('+' => $participant_phids));
if ($title) {
$xactions[] = id(new ConpherenceTransaction())
->setTransactionType(
ConpherenceThreadTitleTransaction::TRANSACTIONTYPE)
->setNewValue($title);
}
if (strlen($topic)) {
$xactions[] = id(new ConpherenceTransaction())
->setTransactionType(
ConpherenceThreadTopicTransaction::TRANSACTIONTYPE)
->setNewValue($topic);
}
$xactions[] = id(new ConpherenceTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new ConpherenceTransactionComment())
->setContent($message)
->setConpherencePHID($conpherence->getPHID()));
id(new ConpherenceEditor())
->setActor($creator)
->setContentSource($source)
->setContinueOnNoEffect(true)
->applyTransactions($conpherence, $xactions);
}
return array($errors, $conpherence);
}
public function generateTransactionsFromText(
PhabricatorUser $viewer,
ConpherenceThread $conpherence,
$text) {
$xactions = array();
$xactions[] = id(new ConpherenceTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new ConpherenceTransactionComment())
->setContent($text)
->setConpherencePHID($conpherence->getPHID()));
return $xactions;
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this room.', $author);
}
protected function applyBuiltinInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
$object->setMessageCount((int)$object->getMessageCount() + 1);
break;
}
return parent::applyBuiltinInternalTransaction($object, $xaction);
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
$acting_phid = $this->getActingAsPHID();
$participants = $object->getParticipants();
foreach ($participants as $participant) {
if ($participant->getParticipantPHID() == $acting_phid) {
$participant->markUpToDate($object);
}
}
if ($participants) {
PhabricatorUserCache::clearCaches(
PhabricatorUserMessageCountCacheType::KEY_COUNT,
array_keys($participants));
}
if ($xactions) {
$data = array(
'type' => 'message',
'threadPHID' => $object->getPHID(),
'messageID' => last($xactions)->getID(),
'subscribers' => array($object->getPHID()),
);
PhabricatorNotificationClient::tryToPostMessage($data);
}
return $xactions;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new ConpherenceReplyHandler())
->setActor($this->getActor())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$title = $object->getTitle();
if (!$title) {
$title = pht(
'%s sent you a message.',
$this->getActor()->getUserName());
}
return id(new PhabricatorMetaMTAMail())
->setSubject("Z{$id}: {$title}");
}
protected function getMailTo(PhabricatorLiskDAO $object) {
$to_phids = array();
$participants = $object->getParticipants();
if (!$participants) {
return $to_phids;
}
$participant_phids = mpull($participants, 'getParticipantPHID');
$users = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($participant_phids)
->needUserSettings(true)
->execute();
$users = mpull($users, null, 'getPHID');
$notification_key = PhabricatorConpherenceNotificationsSetting::SETTINGKEY;
$notification_email =
PhabricatorConpherenceNotificationsSetting::VALUE_CONPHERENCE_EMAIL;
foreach ($participants as $phid => $participant) {
$user = idx($users, $phid);
if ($user) {
$default = $user->getUserSetting($notification_key);
} else {
$default = $notification_email;
}
$settings = $participant->getSettings();
$notifications = idx($settings, 'notifications', $default);
if ($notifications == $notification_email) {
$to_phids[] = $phid;
}
}
return $to_phids;
}
protected function getMailCC(PhabricatorLiskDAO $object) {
return array();
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$body->addLinkSection(
pht('CONPHERENCE DETAIL'),
PhabricatorEnv::getProductionURI('/'.$object->getMonogram()));
return $body;
}
protected function addEmailPreferenceSectionToMailBody(
PhabricatorMetaMTAMailBody $body,
PhabricatorLiskDAO $object,
array $xactions) {
$href = PhabricatorEnv::getProductionURI(
'/'.$object->getMonogram().'?settings');
$label = pht('EMAIL PREFERENCES FOR THIS ROOM');
$body->addLinkSection($label, $href);
}
protected function getMailSubjectPrefix() {
- return PhabricatorEnv::getEnvConfig('metamta.conpherence.subject-prefix');
+ return pht('[Conpherence]');
}
protected function supportsSearch() {
return true;
}
}
diff --git a/src/applications/conpherence/mail/ConpherenceThreadMailReceiver.php b/src/applications/conpherence/mail/ConpherenceThreadMailReceiver.php
index e926d3cfc..aeb4e2899 100644
--- a/src/applications/conpherence/mail/ConpherenceThreadMailReceiver.php
+++ b/src/applications/conpherence/mail/ConpherenceThreadMailReceiver.php
@@ -1,28 +1,28 @@
<?php
final class ConpherenceThreadMailReceiver
extends PhabricatorObjectMailReceiver {
public function isEnabled() {
$app_class = 'PhabricatorConpherenceApplication';
return PhabricatorApplication::isClassInstalled($app_class);
}
protected function getObjectPattern() {
return 'Z[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
- $id = (int)trim($pattern, 'Z');
+ $id = (int)substr($pattern, 1);
return id(new ConpherenceThreadQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
}
protected function getTransactionReplyHandler() {
return new ConpherenceReplyHandler();
}
}
diff --git a/src/applications/conpherence/storage/ConpherenceThread.php b/src/applications/conpherence/storage/ConpherenceThread.php
index bb355154a..7a5f97ed4 100644
--- a/src/applications/conpherence/storage/ConpherenceThread.php
+++ b/src/applications/conpherence/storage/ConpherenceThread.php
@@ -1,357 +1,348 @@
<?php
final class ConpherenceThread extends ConpherenceDAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorMentionableInterface,
PhabricatorDestructibleInterface,
PhabricatorNgramsInterface {
protected $title;
protected $topic;
protected $profileImagePHID;
protected $messageCount;
protected $mailKey;
protected $viewPolicy;
protected $editPolicy;
protected $joinPolicy;
private $participants = self::ATTACHABLE;
private $transactions = self::ATTACHABLE;
private $profileImageFile = self::ATTACHABLE;
private $handles = self::ATTACHABLE;
public static function initializeNewRoom(PhabricatorUser $sender) {
$default_policy = id(new ConpherenceThreadMembersPolicyRule())
->getObjectPolicyFullKey();
return id(new ConpherenceThread())
->setMessageCount(0)
->setTitle('')
->setTopic('')
->attachParticipants(array())
->setViewPolicy($default_policy)
->setEditPolicy($default_policy)
->setJoinPolicy('');
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'title' => 'text255?',
'topic' => 'text255',
'messageCount' => 'uint64',
'mailKey' => 'text20',
'joinPolicy' => 'policy',
'profileImagePHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorConpherenceThreadPHIDType::TYPECONST);
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public function getMonogram() {
return 'Z'.$this->getID();
}
public function getURI() {
return '/'.$this->getMonogram();
}
public function attachParticipants(array $participants) {
assert_instances_of($participants, 'ConpherenceParticipant');
$this->participants = $participants;
return $this;
}
public function getParticipants() {
return $this->assertAttached($this->participants);
}
public function getParticipant($phid) {
$participants = $this->getParticipants();
return $participants[$phid];
}
public function getParticipantIfExists($phid, $default = null) {
$participants = $this->getParticipants();
return idx($participants, $phid, $default);
}
public function getParticipantPHIDs() {
$participants = $this->getParticipants();
return array_keys($participants);
}
public function attachHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function getHandles() {
return $this->assertAttached($this->handles);
}
public function attachTransactions(array $transactions) {
assert_instances_of($transactions, 'ConpherenceTransaction');
$this->transactions = $transactions;
return $this;
}
public function getTransactions($assert_attached = true) {
return $this->assertAttached($this->transactions);
}
public function hasAttachedTransactions() {
return $this->transactions !== self::ATTACHABLE;
}
public function getTransactionsFrom($begin = 0, $amount = null) {
$length = count($this->transactions);
return array_slice(
$this->getTransactions(),
$length - $begin - $amount,
$amount);
}
public function getProfileImageURI() {
return $this->getProfileImageFile()->getBestURI();
}
public function attachProfileImageFile(PhabricatorFile $file) {
$this->profileImageFile = $file;
return $this;
}
public function getProfileImageFile() {
return $this->assertAttached($this->profileImageFile);
}
/**
* Get a thread title which doesn't require handles to be attached.
*
* This is a less rich title than @{method:getDisplayTitle}, but does not
* require handles to be attached. We use it to build thread handles without
* risking cycles or recursion while querying.
*
* @return string Lower quality human-readable title.
*/
public function getStaticTitle() {
$title = $this->getTitle();
if (strlen($title)) {
return $title;
}
return pht('Private Room');
}
public function getDisplayData(PhabricatorUser $viewer) {
$handles = $this->getHandles();
if ($this->hasAttachedTransactions()) {
$transactions = $this->getTransactions();
} else {
$transactions = array();
}
$img_src = $this->getProfileImageURI();
$message_transaction = null;
foreach ($transactions as $transaction) {
if ($message_transaction) {
break;
}
switch ($transaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
$message_transaction = $transaction;
break;
default:
break;
}
}
if ($message_transaction) {
$message_handle = $handles[$message_transaction->getAuthorPHID()];
$subtitle = sprintf(
'%s: %s',
$message_handle->getName(),
id(new PhutilUTF8StringTruncator())
->setMaximumGlyphs(60)
->truncateString(
$message_transaction->getComment()->getContent()));
} else {
// Kinda lame, but maybe add last message to cache?
$subtitle = pht('No recent messages');
}
$user_participation = $this->getParticipantIfExists($viewer->getPHID());
$theme = ConpherenceRoomSettings::COLOR_LIGHT;
if ($user_participation) {
$user_seen_count = $user_participation->getSeenMessageCount();
$participant = $this->getParticipant($viewer->getPHID());
$settings = $participant->getSettings();
$theme = idx($settings, 'theme', $theme);
} else {
$user_seen_count = 0;
}
$unread_count = $this->getMessageCount() - $user_seen_count;
$theme_class = ConpherenceRoomSettings::getThemeClass($theme);
$title = $this->getTitle();
$topic = $this->getTopic();
return array(
'title' => $title,
'topic' => $topic,
'subtitle' => $subtitle,
'unread_count' => $unread_count,
'epoch' => $this->getDateModified(),
'image' => $img_src,
'theme' => $theme_class,
);
}
/* -( 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 $this->getEditPolicy();
}
return PhabricatorPolicies::POLICY_NOONE;
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
// this bad boy isn't even created yet so go nuts $user
if (!$this->getID()) {
return true;
}
switch ($capability) {
case PhabricatorPolicyCapability::CAN_EDIT:
return false;
}
$participants = $this->getParticipants();
return isset($participants[$user->getPHID()]);
}
public function describeAutomaticCapability($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return pht('Participants in a room can always view it.');
break;
}
}
public static function loadViewPolicyObjects(
PhabricatorUser $viewer,
array $conpherences) {
assert_instances_of($conpherences, __CLASS__);
$policies = array();
foreach ($conpherences as $room) {
$policies[$room->getViewPolicy()] = 1;
}
$policy_objects = array();
if ($policies) {
$policy_objects = id(new PhabricatorPolicyQuery())
->setViewer($viewer)
->withPHIDs(array_keys($policies))
->execute();
}
return $policy_objects;
}
public function getPolicyIconName(array $policy_objects) {
assert_instances_of($policy_objects, 'PhabricatorPolicy');
$icon = $policy_objects[$this->getViewPolicy()]->getIcon();
return $icon;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new ConpherenceEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new ConpherenceTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
- return $timeline;
- }
/* -( PhabricatorNgramInterface )------------------------------------------ */
public function newNgrams() {
return array(
id(new ConpherenceThreadTitleNgrams())
->setValue($this->getTitle()),
);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$participants = id(new ConpherenceParticipant())
->loadAllWhere('conpherencePHID = %s', $this->getPHID());
foreach ($participants as $participant) {
$participant->delete();
}
$this->saveTransaction();
}
}
diff --git a/src/applications/countdown/mail/PhabricatorCountdownMailReceiver.php b/src/applications/countdown/mail/PhabricatorCountdownMailReceiver.php
index d0218de59..9b448ef10 100644
--- a/src/applications/countdown/mail/PhabricatorCountdownMailReceiver.php
+++ b/src/applications/countdown/mail/PhabricatorCountdownMailReceiver.php
@@ -1,28 +1,28 @@
<?php
final class PhabricatorCountdownMailReceiver
extends PhabricatorObjectMailReceiver {
public function isEnabled() {
return PhabricatorApplication::isClassInstalled(
'PhabricatorCountdownApplication');
}
protected function getObjectPattern() {
return 'C[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
- $id = (int)substr($pattern, 4);
+ $id = (int)substr($pattern, 1);
return id(new PhabricatorCountdownQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
}
protected function getTransactionReplyHandler() {
return new PhabricatorCountdownReplyHandler();
}
}
diff --git a/src/applications/countdown/storage/PhabricatorCountdown.php b/src/applications/countdown/storage/PhabricatorCountdown.php
index cfc669008..1c61ae7ff 100644
--- a/src/applications/countdown/storage/PhabricatorCountdown.php
+++ b/src/applications/countdown/storage/PhabricatorCountdown.php
@@ -1,196 +1,186 @@
<?php
final class PhabricatorCountdown extends PhabricatorCountdownDAO
implements
PhabricatorPolicyInterface,
PhabricatorFlaggableInterface,
PhabricatorSubscribableInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorTokenReceiverInterface,
PhabricatorSpacesInterface,
PhabricatorProjectInterface,
PhabricatorDestructibleInterface,
PhabricatorConduitResultInterface {
protected $title;
protected $authorPHID;
protected $epoch;
protected $description;
protected $viewPolicy;
protected $editPolicy;
protected $mailKey;
protected $spacePHID;
public static function initializeNewCountdown(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorCountdownApplication'))
->executeOne();
$view_policy = $app->getPolicy(
PhabricatorCountdownDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(
PhabricatorCountdownDefaultEditCapability::CAPABILITY);
return id(new PhabricatorCountdown())
->setAuthorPHID($actor->getPHID())
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setSpacePHID($actor->getDefaultSpacePHID());
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'title' => 'text255',
'description' => 'text',
'mailKey' => 'bytes20',
),
self::CONFIG_KEY_SCHEMA => array(
'key_epoch' => array(
'columns' => array('epoch'),
),
'key_author' => array(
'columns' => array('authorPHID', 'epoch'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorCountdownCountdownPHIDType::TYPECONST);
}
public function getMonogram() {
return 'C'.$this->getID();
}
public function getURI() {
return '/'.$this->getMonogram();
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return ($phid == $this->getAuthorPHID());
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorCountdownEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorCountdownTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array($this->getAuthorPHID());
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
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 $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorSpacesInterface )------------------------------------------- */
public function getSpacePHID() {
return $this->spacePHID;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('title')
->setType('string')
->setDescription(pht('The title of the countdown.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('description')
->setType('remarkup')
->setDescription(pht('The description of the countdown.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('epoch')
->setType('epoch')
->setDescription(pht('The end date of the countdown.')),
);
}
public function getFieldValuesForConduit() {
return array(
'title' => $this->getTitle(),
'description' => array(
'raw' => $this->getDescription(),
),
'epoch' => (int)$this->getEpoch(),
);
}
public function getConduitSearchAttachments() {
return array();
}
}
diff --git a/src/applications/dashboard/storage/PhabricatorDashboard.php b/src/applications/dashboard/storage/PhabricatorDashboard.php
index 2e673de19..53bc2f857 100644
--- a/src/applications/dashboard/storage/PhabricatorDashboard.php
+++ b/src/applications/dashboard/storage/PhabricatorDashboard.php
@@ -1,202 +1,191 @@
<?php
/**
* A collection of dashboard panels with a specific layout.
*/
final class PhabricatorDashboard extends PhabricatorDashboardDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorFlaggableInterface,
PhabricatorDestructibleInterface,
PhabricatorProjectInterface,
PhabricatorNgramsInterface {
protected $name;
protected $authorPHID;
protected $viewPolicy;
protected $editPolicy;
protected $status;
protected $icon;
protected $layoutConfig = array();
const STATUS_ACTIVE = 'active';
const STATUS_ARCHIVED = 'archived';
private $panelPHIDs = self::ATTACHABLE;
private $panels = self::ATTACHABLE;
private $edgeProjectPHIDs = self::ATTACHABLE;
public static function initializeNewDashboard(PhabricatorUser $actor) {
return id(new PhabricatorDashboard())
->setName('')
->setIcon('fa-dashboard')
->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy())
->setEditPolicy($actor->getPHID())
->setStatus(self::STATUS_ACTIVE)
->setAuthorPHID($actor->getPHID())
->attachPanels(array())
->attachPanelPHIDs(array());
}
public static function getStatusNameMap() {
return array(
self::STATUS_ACTIVE => pht('Active'),
self::STATUS_ARCHIVED => pht('Archived'),
);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'layoutConfig' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort255',
'status' => 'text32',
'icon' => 'text32',
'authorPHID' => 'phid',
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorDashboardDashboardPHIDType::TYPECONST);
}
public function getLayoutConfigObject() {
return PhabricatorDashboardLayoutConfig::newFromDictionary(
$this->getLayoutConfig());
}
public function setLayoutConfigFromObject(
PhabricatorDashboardLayoutConfig $object) {
$this->setLayoutConfig($object->toDictionary());
// See PHI385. Dashboard panel mutations rely on changes to the Dashboard
// object persisting when transactions are applied, but this assumption is
// no longer valid after T13054. For now, just save the dashboard
// explicitly.
$this->save();
return $this;
}
public function getProjectPHIDs() {
return $this->assertAttached($this->edgeProjectPHIDs);
}
public function attachProjectPHIDs(array $phids) {
$this->edgeProjectPHIDs = $phids;
return $this;
}
public function attachPanelPHIDs(array $phids) {
$this->panelPHIDs = $phids;
return $this;
}
public function getPanelPHIDs() {
return $this->assertAttached($this->panelPHIDs);
}
public function attachPanels(array $panels) {
assert_instances_of($panels, 'PhabricatorDashboardPanel');
$this->panels = $panels;
return $this;
}
public function getPanels() {
return $this->assertAttached($this->panels);
}
public function isArchived() {
return ($this->getStatus() == self::STATUS_ARCHIVED);
}
public function getViewURI() {
return '/dashboard/view/'.$this->getID().'/';
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorDashboardTransactionEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorDashboardTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorPolicyInterface )----------------------------------------- */
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 $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$installs = id(new PhabricatorDashboardInstall())->loadAllWhere(
'dashboardPHID = %s',
$this->getPHID());
foreach ($installs as $install) {
$install->delete();
}
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorNgramInterface )------------------------------------------ */
public function newNgrams() {
return array(
id(new PhabricatorDashboardNgrams())
->setValue($this->getName()),
);
}
}
diff --git a/src/applications/dashboard/storage/PhabricatorDashboardPanel.php b/src/applications/dashboard/storage/PhabricatorDashboardPanel.php
index 9f8875dc1..89b577ab8 100644
--- a/src/applications/dashboard/storage/PhabricatorDashboardPanel.php
+++ b/src/applications/dashboard/storage/PhabricatorDashboardPanel.php
@@ -1,199 +1,188 @@
<?php
/**
* An individual dashboard panel.
*/
final class PhabricatorDashboardPanel
extends PhabricatorDashboardDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorCustomFieldInterface,
PhabricatorFlaggableInterface,
PhabricatorDestructibleInterface,
PhabricatorNgramsInterface {
protected $name;
protected $panelType;
protected $viewPolicy;
protected $editPolicy;
protected $authorPHID;
protected $isArchived = 0;
protected $properties = array();
private $customFields = self::ATTACHABLE;
public static function initializeNewPanel(PhabricatorUser $actor) {
return id(new PhabricatorDashboardPanel())
->setName('')
->setAuthorPHID($actor->getPHID())
->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy())
->setEditPolicy($actor->getPHID());
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort255',
'panelType' => 'text64',
'authorPHID' => 'phid',
'isArchived' => 'bool',
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorDashboardPanelPHIDType::TYPECONST);
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getMonogram() {
return 'W'.$this->getID();
}
public function getURI() {
return '/'.$this->getMonogram();
}
public function getPanelTypes() {
$panel_types = PhabricatorDashboardPanelType::getAllPanelTypes();
$panel_types = mpull($panel_types, 'getPanelTypeName', 'getPanelTypeKey');
asort($panel_types);
$panel_types = (array('' => pht('(All Types)')) + $panel_types);
return $panel_types;
}
public function getStatuses() {
$statuses =
array(
'' => pht('(All Panels)'),
'active' => pht('Active Panels'),
'archived' => pht('Archived Panels'),
);
return $statuses;
}
public function getImplementation() {
return idx(
PhabricatorDashboardPanelType::getAllPanelTypes(),
$this->getPanelType());
}
public function requireImplementation() {
$impl = $this->getImplementation();
if (!$impl) {
throw new Exception(
pht(
'Attempting to use a panel in a way that requires an '.
'implementation, but the panel implementation ("%s") is unknown to '.
'Phabricator.',
$this->getPanelType()));
}
return $impl;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorDashboardPanelTransactionEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorDashboardPanelTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorPolicyInterface )----------------------------------------- */
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 $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return array();
}
public function getCustomFieldBaseClass() {
return 'PhabricatorDashboardPanelCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorNgramInterface )------------------------------------------ */
public function newNgrams() {
return array(
id(new PhabricatorDashboardPanelNgrams())
->setValue($this->getName()),
);
}
}
diff --git a/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php b/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php
index 0d00207b4..ec2099f8d 100644
--- a/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php
+++ b/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php
@@ -1,271 +1,266 @@
<?php
final class PhabricatorDifferentialConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Differential');
}
public function getDescription() {
return pht('Configure Differential code review.');
}
public function getIcon() {
return 'fa-cog';
}
public function getGroup() {
return 'apps';
}
public function getOptions() {
$caches_href = PhabricatorEnv::getDoclink('Managing Caches');
$custom_field_type = 'custom:PhabricatorCustomFieldConfigOptionType';
$fields = array(
new DifferentialSummaryField(),
new DifferentialTestPlanField(),
new DifferentialReviewersField(),
new DifferentialProjectReviewersField(),
new DifferentialRepositoryField(),
new DifferentialManiphestTasksField(),
new DifferentialCommitsField(),
new DifferentialJIRAIssuesField(),
new DifferentialAsanaRepresentationField(),
new DifferentialChangesSinceLastUpdateField(),
new DifferentialBranchField(),
new DifferentialBlameRevisionField(),
new DifferentialPathField(),
new DifferentialHostField(),
new DifferentialLintField(),
new DifferentialUnitField(),
new DifferentialRevertPlanField(),
);
$default_fields = array();
foreach ($fields as $field) {
$default_fields[$field->getFieldKey()] = array(
'disabled' => $field->shouldDisableByDefault(),
);
}
$inline_description = $this->deformat(
pht(<<<EOHELP
To include patches inline in email bodies, set this option to a positive
integer. Patches will be inlined if they are at most that many lines and at
most 256 times that many bytes.
For example, a value of 100 means "inline patches if they are at not more than
100 lines long and not more than 25,600 bytes large".
By default, patches are not inlined.
EOHELP
));
return array(
$this->newOption(
'differential.fields',
$custom_field_type,
$default_fields)
->setCustomData(
id(new DifferentialRevision())->getCustomFieldBaseClass())
->setDescription(
pht(
"Select and reorder revision fields.\n\n".
"NOTE: This feature is under active development and subject ".
"to change.")),
$this->newOption(
'differential.whitespace-matters',
'list<regex>',
array(
'/\.py$/',
'/\.l?hs$/',
'/\.ya?ml$/',
))
->setDescription(
pht(
"List of file regexps where whitespace is meaningful and should ".
"not use 'ignore-all' by default")),
$this->newOption('differential.require-test-plan-field', 'bool', true)
->setBoolOptions(
array(
pht("Require 'Test Plan' field"),
pht("Make 'Test Plan' field optional"),
))
->setSummary(pht('Require "Test Plan" field?'))
->setDescription(
pht(
"Differential has a required 'Test Plan' field by default. You ".
"can make it optional by setting this to false. You can also ".
"completely remove it above, if you prefer.")),
$this->newOption('differential.enable-email-accept', 'bool', false)
->setBoolOptions(
array(
pht('Enable Email "!accept" Action'),
pht('Disable Email "!accept" Action'),
))
->setSummary(pht('Enable or disable "!accept" action via email.'))
->setDescription(
pht(
'If inbound email is configured, users can interact with '.
'revisions by using "!actions" in email replies (for example, '.
'"!resign" or "!rethink"). However, by default, users may not '.
'"!accept" revisions via email: email authentication can be '.
'configured to be very weak, and email "!accept" is kind of '.
'sketchy and implies the revision may not actually be receiving '.
'thorough review. You can enable "!accept" by setting this '.
'option to true.')),
$this->newOption('differential.generated-paths', 'list<regex>', array())
->setSummary(pht('File regexps to treat as automatically generated.'))
->setDescription(
pht(
'List of file regexps that should be treated as if they are '.
'generated by an automatic process, and thus be hidden by '.
'default in Differential.'.
"\n\n".
'NOTE: This property is cached, so you will need to purge the '.
'cache after making changes if you want the new configuration '.
'to affect existing revisions. For instructions, see '.
'**[[ %s | Managing Caches ]]** in the documentation.',
$caches_href))
->addExample("/config\.h$/\n#(^|/)autobuilt/#", pht('Valid Setting')),
$this->newOption('differential.sticky-accept', 'bool', true)
->setBoolOptions(
array(
pht('Accepts persist across updates'),
pht('Accepts are reset by updates'),
))
->setSummary(
pht('Should "Accepted" revisions remain "Accepted" after updates?'))
->setDescription(
pht(
'Normally, when revisions that have been "Accepted" are updated, '.
'they remain "Accepted". This allows reviewers to suggest minor '.
'alterations when accepting, and encourages authors to update '.
'if they make minor changes in response to this feedback.'.
"\n\n".
'If you want updates to always require re-review, you can disable '.
'the "stickiness" of the "Accepted" status with this option. '.
'This may make the process for minor changes much more burdensome '.
'to both authors and reviewers.')),
$this->newOption('differential.allow-self-accept', 'bool', false)
->setBoolOptions(
array(
pht('Allow self-accept'),
pht('Disallow self-accept'),
))
->setSummary(pht('Allows users to accept their own revisions.'))
->setDescription(
pht(
"If you set this to true, users can accept their own revisions. ".
"This action is disabled by default because it's most likely not ".
"a behavior you want, but it proves useful if you are working ".
"alone on a project and want to make use of all of ".
"differential's features.")),
$this->newOption('differential.always-allow-close', 'bool', false)
->setBoolOptions(
array(
pht('Allow any user'),
pht('Restrict to submitter'),
))
->setSummary(pht('Allows any user to close accepted revisions.'))
->setDescription(
pht(
'If you set this to true, any user can close any revision so '.
'long as it has been accepted. This can be useful depending on '.
'your development model. For example, github-style pull requests '.
'where the reviewer is often the actual committer can benefit '.
'from turning this option to true. If false, only the submitter '.
'can close a revision.')),
$this->newOption('differential.always-allow-abandon', 'bool', false)
->setBoolOptions(
array(
pht('Allow any user'),
pht('Restrict to submitter'),
))
->setSummary(pht('Allows any user to abandon revisions.'))
->setDescription(
pht(
'If you set this to true, any user can abandon any revision. If '.
'false, only the submitter can abandon a revision.')),
$this->newOption('differential.allow-reopen', 'bool', false)
->setBoolOptions(
array(
pht('Enable reopen'),
pht('Disable reopen'),
))
->setSummary(pht('Allows any user to reopen a closed revision.'))
->setDescription(
pht(
'If you set this to true, any user can reopen a revision so '.
'long as it has been closed. This can be useful if a revision '.
'is accidentally closed or if a developer changes his or her '.
'mind after closing a revision. If it is false, reopening '.
'is not allowed.')),
$this->newOption('differential.close-on-accept', 'bool', false)
->setBoolOptions(
array(
pht('Treat Accepted Revisions as "Closed"'),
pht('Treat Accepted Revisions as "Open"'),
))
->setSummary(pht('Allows "Accepted" to act as a closed status.'))
->setDescription(
pht(
'Normally, Differential revisions remain on the dashboard when '.
'they are "Accepted", and the author then commits the changes '.
'to "Close" the revision and move it off the dashboard.'.
"\n\n".
'If you have an unusual workflow where Differential is used for '.
'post-commit review (normally called "Audit", elsewhere in '.
'Phabricator), you can set this flag to treat the "Accepted" '.
'state as a "Closed" state and end the review workflow early.'.
"\n\n".
'This sort of workflow is very unusual. Very few installs should '.
'need to change this option.')),
- $this->newOption(
- 'metamta.differential.subject-prefix',
- 'string',
- '[Differential]')
- ->setDescription(pht('Subject prefix for Differential mail.')),
$this->newOption(
'metamta.differential.attach-patches',
'bool',
false)
->setBoolOptions(
array(
pht('Attach Patches'),
pht('Do Not Attach Patches'),
))
->setSummary(pht('Attach patches to email, as text attachments.'))
->setDescription(
pht(
'If you set this to true, Phabricator will attach patches to '.
'Differential mail (as text attachments). This will not work if '.
'you are using SendGrid as your mail adapter.')),
$this->newOption(
'metamta.differential.inline-patches',
'int',
0)
->setSummary(pht('Inline patches in email, as body text.'))
->setDescription($inline_description),
$this->newOption(
'metamta.differential.patch-format',
'enum',
'unified')
->setDescription(
pht('Format for inlined or attached patches.'))
->setEnumOptions(
array(
'unified' => pht('Unified'),
'git' => pht('Git'),
)),
);
}
}
diff --git a/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php b/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php
index 59223f20d..3cfe7a714 100644
--- a/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php
+++ b/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php
@@ -1,88 +1,87 @@
<?php
final class DifferentialDoorkeeperRevisionFeedStoryPublisher
extends DoorkeeperFeedStoryPublisher {
public function canPublishStory(PhabricatorFeedStory $story, $object) {
return ($object instanceof DifferentialRevision);
}
public function isStoryAboutObjectCreation($object) {
$story = $this->getFeedStory();
$action = $story->getStoryData()->getValue('action');
return ($action == DifferentialAction::ACTION_CREATE);
}
public function isStoryAboutObjectClosure($object) {
$story = $this->getFeedStory();
$action = $story->getStoryData()->getValue('action');
return ($action == DifferentialAction::ACTION_CLOSE) ||
($action == DifferentialAction::ACTION_ABANDON);
}
public function willPublishStory($object) {
return id(new DifferentialRevisionQuery())
->setViewer($this->getViewer())
->withIDs(array($object->getID()))
->needReviewers(true)
->executeOne();
}
public function getOwnerPHID($object) {
return $object->getAuthorPHID();
}
public function getActiveUserPHIDs($object) {
if ($object->isNeedsReview()) {
return $object->getReviewerPHIDs();
} else {
return array();
}
}
public function getPassiveUserPHIDs($object) {
if ($object->isNeedsReview()) {
return array();
} else {
return $object->getReviewerPHIDs();
}
}
public function getCCUserPHIDs($object) {
return PhabricatorSubscribersQuery::loadSubscribersForPHID(
$object->getPHID());
}
public function getObjectTitle($object) {
$id = $object->getID();
$title = $object->getTitle();
return "D{$id}: {$title}";
}
public function getObjectURI($object) {
return PhabricatorEnv::getProductionURI('/D'.$object->getID());
}
public function getObjectDescription($object) {
return $object->getSummary();
}
public function isObjectClosed($object) {
return $object->isClosed();
}
public function getResponsibilityTitle($object) {
$prefix = $this->getTitlePrefix($object);
return pht('%s Review Request', $prefix);
}
private function getTitlePrefix(DifferentialRevision $revision) {
- $prefix_key = 'metamta.differential.subject-prefix';
- return PhabricatorEnv::getEnvConfig($prefix_key);
+ return pht('[Differential]');
}
}
diff --git a/src/applications/differential/editor/DifferentialRevisionEditEngine.php b/src/applications/differential/editor/DifferentialRevisionEditEngine.php
index 07b693044..9c399036d 100644
--- a/src/applications/differential/editor/DifferentialRevisionEditEngine.php
+++ b/src/applications/differential/editor/DifferentialRevisionEditEngine.php
@@ -1,327 +1,353 @@
<?php
final class DifferentialRevisionEditEngine
extends PhabricatorEditEngine {
private $diff;
const ENGINECONST = 'differential.revision';
const ACTIONGROUP_REVIEW = 'review';
const ACTIONGROUP_REVISION = 'revision';
public function getEngineName() {
return pht('Revisions');
}
public function getSummaryHeader() {
return pht('Configure Revision Forms');
}
public function getSummaryText() {
return pht(
'Configure creation and editing revision forms in Differential.');
}
public function getEngineApplicationClass() {
return 'PhabricatorDifferentialApplication';
}
public function isEngineConfigurable() {
return false;
}
protected function newEditableObject() {
$viewer = $this->getViewer();
return DifferentialRevision::initializeNewRevision($viewer);
}
protected function newObjectQuery() {
return id(new DifferentialRevisionQuery())
->needActiveDiffs(true)
->needReviewers(true)
->needReviewerAuthority(true);
}
protected function getObjectCreateTitleText($object) {
return pht('Create New Revision');
}
protected function getObjectEditTitleText($object) {
$monogram = $object->getMonogram();
$title = $object->getTitle();
$diff = $this->getDiff();
if ($diff) {
return pht('Update Revision %s: %s', $monogram, $title);
} else {
return pht('Edit Revision %s: %s', $monogram, $title);
}
}
protected function getObjectEditShortText($object) {
return $object->getMonogram();
}
protected function getObjectCreateShortText() {
return pht('Create Revision');
}
protected function getObjectName() {
return pht('Revision');
}
protected function getCommentViewButtonText($object) {
if ($object->isDraft()) {
return pht('Submit Quietly');
}
return parent::getCommentViewButtonText($object);
}
protected function getObjectViewURI($object) {
return $object->getURI();
}
protected function getEditorURI() {
return $this->getApplication()->getApplicationURI('revision/edit/');
}
public function setDiff(DifferentialDiff $diff) {
$this->diff = $diff;
return $this;
}
public function getDiff() {
return $this->diff;
}
protected function newCommentActionGroups() {
return array(
id(new PhabricatorEditEngineCommentActionGroup())
->setKey(self::ACTIONGROUP_REVIEW)
->setLabel(pht('Review Actions')),
id(new PhabricatorEditEngineCommentActionGroup())
->setKey(self::ACTIONGROUP_REVISION)
->setLabel(pht('Revision Actions')),
);
}
protected function buildCustomEditFields($object) {
$viewer = $this->getViewer();
$plan_required = PhabricatorEnv::getEnvConfig(
'differential.require-test-plan-field');
$plan_enabled = $this->isCustomFieldEnabled(
$object,
'differential:test-plan');
$diff = $this->getDiff();
if ($diff) {
$diff_phid = $diff->getPHID();
} else {
$diff_phid = null;
}
$is_create = $this->getIsCreate();
$is_update = ($diff && !$is_create);
$fields = array();
$fields[] = id(new PhabricatorHandlesEditField())
->setKey(DifferentialRevisionUpdateTransaction::EDITKEY)
->setLabel(pht('Update Diff'))
->setDescription(pht('New diff to create or update the revision with.'))
->setConduitDescription(pht('Create or update a revision with a diff.'))
->setConduitTypeDescription(pht('PHID of the diff.'))
->setTransactionType(
DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE)
->setHandleParameterType(new AphrontPHIDListHTTPParameterType())
->setSingleValue($diff_phid)
->setIsFormField((bool)$diff)
->setIsReorderable(false)
->setIsDefaultable(false)
->setIsInvisible(true)
->setIsLockable(false);
if ($is_update) {
$fields[] = id(new PhabricatorInstructionsEditField())
->setKey('update.help')
->setValue(pht('Describe the updates you have made to the diff.'));
$fields[] = id(new PhabricatorCommentEditField())
->setKey('update.comment')
->setLabel(pht('Comment'))
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->setIsWebOnly(true)
->setDescription(pht('Comments providing context for the update.'));
$fields[] = id(new PhabricatorSubmitEditField())
->setKey('update.submit')
->setValue($this->getObjectEditButtonText($object));
$fields[] = id(new PhabricatorDividerEditField())
->setKey('update.note');
}
$fields[] = id(new PhabricatorTextEditField())
->setKey(DifferentialRevisionTitleTransaction::EDITKEY)
->setLabel(pht('Title'))
->setIsRequired(true)
->setTransactionType(
DifferentialRevisionTitleTransaction::TRANSACTIONTYPE)
->setDescription(pht('The title of the revision.'))
->setConduitDescription(pht('Retitle the revision.'))
->setConduitTypeDescription(pht('New revision title.'))
->setValue($object->getTitle());
$fields[] = id(new PhabricatorRemarkupEditField())
->setKey(DifferentialRevisionSummaryTransaction::EDITKEY)
->setLabel(pht('Summary'))
->setTransactionType(
DifferentialRevisionSummaryTransaction::TRANSACTIONTYPE)
->setDescription(pht('The summary of the revision.'))
->setConduitDescription(pht('Change the revision summary.'))
->setConduitTypeDescription(pht('New revision summary.'))
->setValue($object->getSummary());
if ($plan_enabled) {
$fields[] = id(new PhabricatorRemarkupEditField())
->setKey(DifferentialRevisionTestPlanTransaction::EDITKEY)
->setLabel(pht('Test Plan'))
->setIsRequired($plan_required)
->setTransactionType(
DifferentialRevisionTestPlanTransaction::TRANSACTIONTYPE)
->setDescription(
pht('Actions performed to verify the behavior of the change.'))
->setConduitDescription(pht('Update the revision test plan.'))
->setConduitTypeDescription(pht('New test plan.'))
->setValue($object->getTestPlan());
}
$fields[] = id(new PhabricatorDatasourceEditField())
->setKey(DifferentialRevisionReviewersTransaction::EDITKEY)
->setLabel(pht('Reviewers'))
->setDatasource(new DifferentialReviewerDatasource())
->setUseEdgeTransactions(true)
->setTransactionType(
DifferentialRevisionReviewersTransaction::TRANSACTIONTYPE)
->setCommentActionLabel(pht('Change Reviewers'))
->setDescription(pht('Reviewers for this revision.'))
->setConduitDescription(pht('Change the reviewers for this revision.'))
->setConduitTypeDescription(pht('New reviewers.'))
->setValue($object->getReviewerPHIDsForEdit());
$fields[] = id(new PhabricatorDatasourceEditField())
->setKey('repositoryPHID')
->setLabel(pht('Repository'))
->setDatasource(new DiffusionRepositoryDatasource())
->setTransactionType(
DifferentialRevisionRepositoryTransaction::TRANSACTIONTYPE)
->setDescription(pht('The repository the revision belongs to.'))
->setConduitDescription(pht('Change the repository for this revision.'))
->setConduitTypeDescription(pht('New repository.'))
->setSingleValue($object->getRepositoryPHID());
// This is a little flimsy, but allows "Maniphest Tasks: ..." to continue
// working properly in commit messages until we fully sort out T5873.
$fields[] = id(new PhabricatorHandlesEditField())
->setKey('tasks')
->setUseEdgeTransactions(true)
->setIsFormField(false)
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
DifferentialRevisionHasTaskEdgeType::EDGECONST)
->setDescription(pht('Tasks associated with this revision.'))
->setConduitDescription(pht('Change associated tasks.'))
->setConduitTypeDescription(pht('List of tasks.'))
->setValue(array());
+ $fields[] = id(new PhabricatorHandlesEditField())
+ ->setKey('parents')
+ ->setUseEdgeTransactions(true)
+ ->setIsFormField(false)
+ ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
+ ->setMetadataValue(
+ 'edge:type',
+ DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST)
+ ->setDescription(pht('Parent revisions of this revision.'))
+ ->setConduitDescription(pht('Change associated parent revisions.'))
+ ->setConduitTypeDescription(pht('List of revisions.'))
+ ->setValue(array());
+
+ $fields[] = id(new PhabricatorHandlesEditField())
+ ->setKey('children')
+ ->setUseEdgeTransactions(true)
+ ->setIsFormField(false)
+ ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
+ ->setMetadataValue(
+ 'edge:type',
+ DifferentialRevisionDependedOnByRevisionEdgeType::EDGECONST)
+ ->setDescription(pht('Child revisions of this revision.'))
+ ->setConduitDescription(pht('Change associated child revisions.'))
+ ->setConduitTypeDescription(pht('List of revisions.'))
+ ->setValue(array());
+
$actions = DifferentialRevisionActionTransaction::loadAllActions();
$actions = msortv($actions, 'getRevisionActionOrderVector');
foreach ($actions as $key => $action) {
$fields[] = $action->newEditField($object, $viewer);
}
$fields[] = id(new PhabricatorBoolEditField())
->setKey('draft')
->setLabel(pht('Hold as Draft'))
->setIsFormField(false)
->setOptions(
pht('Autosubmit Once Builds Finish'),
pht('Hold as Draft'))
->setTransactionType(
DifferentialRevisionHoldDraftTransaction::TRANSACTIONTYPE)
->setDescription(pht('Hold revision as as draft.'))
->setConduitDescription(
pht(
'Change autosubmission from draft state after builds finish.'))
->setConduitTypeDescription(pht('New "Hold as Draft" setting.'))
->setValue($object->getHoldAsDraft());
return $fields;
}
private function isCustomFieldEnabled(DifferentialRevision $revision, $key) {
$field_list = PhabricatorCustomField::getObjectFields(
$revision,
PhabricatorCustomField::ROLE_VIEW);
$fields = $field_list->getFields();
return isset($fields[$key]);
}
protected function newAutomaticCommentTransactions($object) {
$viewer = $this->getViewer();
$xactions = array();
$inlines = DifferentialTransactionQuery::loadUnsubmittedInlineComments(
$viewer,
$object);
$inlines = msort($inlines, 'getID');
$editor = $object->getApplicationTransactionEditor()
->setActor($viewer);
$query_template = id(new DifferentialDiffInlineCommentQuery())
->withRevisionPHIDs(array($object->getPHID()));
$xactions = $editor->newAutomaticInlineTransactions(
$object,
$inlines,
DifferentialTransaction::TYPE_INLINE,
$query_template);
return $xactions;
}
protected function newCommentPreviewContent($object, array $xactions) {
$viewer = $this->getViewer();
$type_inline = DifferentialTransaction::TYPE_INLINE;
$inlines = array();
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() === $type_inline) {
$inlines[] = $xaction->getComment();
}
}
$content = array();
if ($inlines) {
$inline_preview = id(new PHUIDiffInlineCommentPreviewListView())
->setViewer($viewer)
->setInlineComments($inlines);
$content[] = phutil_tag(
'div',
array(
'id' => 'inline-comment-preview',
),
$inline_preview);
}
return $content;
}
}
diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php
index 8072c19e9..a250d5a2a 100644
--- a/src/applications/differential/editor/DifferentialTransactionEditor.php
+++ b/src/applications/differential/editor/DifferentialTransactionEditor.php
@@ -1,1733 +1,1733 @@
<?php
final class DifferentialTransactionEditor
extends PhabricatorApplicationTransactionEditor {
private $changedPriorToCommitURI;
private $isCloseByCommit;
private $repositoryPHIDOverride = false;
private $didExpandInlineState = false;
private $firstBroadcast = false;
private $wasBroadcasting;
private $isDraftDemotion;
private $ownersDiff;
private $ownersChangesets;
public function getEditorApplicationClass() {
return 'PhabricatorDifferentialApplication';
}
public function getEditorObjectsDescription() {
return pht('Differential Revisions');
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this revision.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created %s.', $author, $object);
}
public function isFirstBroadcast() {
return $this->firstBroadcast;
}
public function getDiffUpdateTransaction(array $xactions) {
$type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == $type_update) {
return $xaction;
}
}
return null;
}
public function setIsCloseByCommit($is_close_by_commit) {
$this->isCloseByCommit = $is_close_by_commit;
return $this;
}
public function getIsCloseByCommit() {
return $this->isCloseByCommit;
}
public function setChangedPriorToCommitURI($uri) {
$this->changedPriorToCommitURI = $uri;
return $this;
}
public function getChangedPriorToCommitURI() {
return $this->changedPriorToCommitURI;
}
public function setRepositoryPHIDOverride($phid_or_null) {
$this->repositoryPHIDOverride = $phid_or_null;
return $this;
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
$types[] = PhabricatorTransactions::TYPE_INLINESTATE;
$types[] = DifferentialTransaction::TYPE_INLINE;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_INLINE:
return null;
}
return parent::getCustomTransactionOldValue($object, $xaction);
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_INLINE:
return null;
}
return parent::getCustomTransactionNewValue($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_INLINE:
return;
}
return parent::applyCustomInternalTransaction($object, $xaction);
}
protected function expandTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_INLINESTATE:
// If we have an "Inline State" transaction already, the caller
// built it for us so we don't need to expand it again.
$this->didExpandInlineState = true;
break;
case DifferentialRevisionPlanChangesTransaction::TRANSACTIONTYPE:
if ($xaction->getMetadataValue('draft.demote')) {
$this->isDraftDemotion = true;
}
break;
}
}
$this->wasBroadcasting = $object->getShouldBroadcast();
return parent::expandTransactions($object, $xactions);
}
protected function expandTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$results = parent::expandTransaction($object, $xaction);
$actor = $this->getActor();
$actor_phid = $this->getActingAsPHID();
$type_edge = PhabricatorTransactions::TYPE_EDGE;
$edge_ref_task = DifferentialRevisionHasTaskEdgeType::EDGECONST;
$want_downgrade = array();
$must_downgrade = array();
if ($this->getIsCloseByCommit()) {
// Never downgrade reviewers when we're closing a revision after a
// commit.
} else {
switch ($xaction->getTransactionType()) {
case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:
$want_downgrade[] = DifferentialReviewerStatus::STATUS_ACCEPTED;
$want_downgrade[] = DifferentialReviewerStatus::STATUS_REJECTED;
break;
case DifferentialRevisionRequestReviewTransaction::TRANSACTIONTYPE:
if (!$object->isChangePlanned()) {
// If the old state isn't "Changes Planned", downgrade the accepts
// even if they're sticky.
// We don't downgrade for "Changes Planned" to allow an author to
// undo a "Plan Changes" by immediately following it up with a
// "Request Review".
$want_downgrade[] = DifferentialReviewerStatus::STATUS_ACCEPTED;
$must_downgrade[] = DifferentialReviewerStatus::STATUS_ACCEPTED;
}
$want_downgrade[] = DifferentialReviewerStatus::STATUS_REJECTED;
break;
}
}
if ($want_downgrade) {
$void_type = DifferentialRevisionVoidTransaction::TRANSACTIONTYPE;
$results[] = id(new DifferentialTransaction())
->setTransactionType($void_type)
->setIgnoreOnNoEffect(true)
->setMetadataValue('void.force', $must_downgrade)
->setNewValue($want_downgrade);
}
$is_commandeer = false;
switch ($xaction->getTransactionType()) {
case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:
if ($this->getIsCloseByCommit()) {
// Don't bother with any of this if this update is a side effect of
// commit detection.
break;
}
// When a revision is updated and the diff comes from a branch named
// "T123" or similar, automatically associate the commit with the
// task that the branch names.
$maniphest = 'PhabricatorManiphestApplication';
if (PhabricatorApplication::isClassInstalled($maniphest)) {
$diff = $this->requireDiff($xaction->getNewValue());
$branch = $diff->getBranch();
// No "$", to allow for branches like T123_demo.
$match = null;
if (preg_match('/^T(\d+)/i', $branch, $match)) {
$task_id = $match[1];
$tasks = id(new ManiphestTaskQuery())
->setViewer($this->getActor())
->withIDs(array($task_id))
->execute();
if ($tasks) {
$task = head($tasks);
$task_phid = $task->getPHID();
$results[] = id(new DifferentialTransaction())
->setTransactionType($type_edge)
->setMetadataValue('edge:type', $edge_ref_task)
->setIgnoreOnNoEffect(true)
->setNewValue(array('+' => array($task_phid => $task_phid)));
}
}
}
break;
case DifferentialRevisionCommandeerTransaction::TRANSACTIONTYPE:
$is_commandeer = true;
break;
}
if ($is_commandeer) {
$results[] = $this->newCommandeerReviewerTransaction($object);
}
if (!$this->didExpandInlineState) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:
case DifferentialTransaction::TYPE_INLINE:
$this->didExpandInlineState = true;
$query_template = id(new DifferentialDiffInlineCommentQuery())
->withRevisionPHIDs(array($object->getPHID()));
$state_xaction = $this->newInlineStateTransaction(
$object,
$query_template);
if ($state_xaction) {
$results[] = $state_xaction;
}
break;
}
}
return $results;
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_INLINE:
$reply = $xaction->getComment()->getReplyToComment();
if ($reply && !$reply->getHasReplies()) {
$reply->setHasReplies(1)->save();
}
return;
}
return parent::applyCustomExternalTransaction($object, $xaction);
}
protected function applyBuiltinExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_INLINESTATE:
$table = new DifferentialTransactionComment();
$conn_w = $table->establishConnection('w');
foreach ($xaction->getNewValue() as $phid => $state) {
queryfx(
$conn_w,
'UPDATE %T SET fixedState = %s WHERE phid = %s',
$table->getTableName(),
$state,
$phid);
}
break;
}
return parent::applyBuiltinExternalTransaction($object, $xaction);
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
// Load the most up-to-date version of the revision and its reviewers,
// so we don't need to try to deduce the state of reviewers by examining
// all the changes made by the transactions. Then, update the reviewers
// on the object to make sure we're acting on the current reviewer set
// (and, for example, sending mail to the right people).
$new_revision = id(new DifferentialRevisionQuery())
->setViewer($this->getActor())
->needReviewers(true)
->needActiveDiffs(true)
->withIDs(array($object->getID()))
->executeOne();
if (!$new_revision) {
throw new Exception(
pht('Failed to load revision from transaction finalization.'));
}
$object->attachReviewers($new_revision->getReviewers());
$object->attachActiveDiff($new_revision->getActiveDiff());
$object->attachRepository($new_revision->getRepository());
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:
$diff = $this->requireDiff($xaction->getNewValue(), true);
// Update these denormalized index tables when we attach a new
// diff to a revision.
$this->updateRevisionHashTable($object, $diff);
$this->updateAffectedPathTable($object, $diff);
break;
}
}
$xactions = $this->updateReviewStatus($object, $xactions);
$this->markReviewerComments($object, $xactions);
return $xactions;
}
private function updateReviewStatus(
DifferentialRevision $revision,
array $xactions) {
$was_accepted = $revision->isAccepted();
$was_revision = $revision->isNeedsRevision();
$was_review = $revision->isNeedsReview();
if (!$was_accepted && !$was_revision && !$was_review) {
// Revisions can't transition out of other statuses (like closed or
// abandoned) as a side effect of reviewer status changes.
return $xactions;
}
// Try to move a revision to "accepted". We look for:
//
// - at least one accepting reviewer who is a user; and
// - no rejects; and
// - no rejects of older diffs; and
// - no blocking reviewers.
$has_accepting_user = false;
$has_rejecting_reviewer = false;
$has_rejecting_older_reviewer = false;
$has_blocking_reviewer = false;
$active_diff = $revision->getActiveDiff();
foreach ($revision->getReviewers() as $reviewer) {
$reviewer_status = $reviewer->getReviewerStatus();
switch ($reviewer_status) {
case DifferentialReviewerStatus::STATUS_REJECTED:
$active_phid = $active_diff->getPHID();
if ($reviewer->isRejected($active_phid)) {
$has_rejecting_reviewer = true;
} else {
$has_rejecting_older_reviewer = true;
}
break;
case DifferentialReviewerStatus::STATUS_REJECTED_OLDER:
$has_rejecting_older_reviewer = true;
break;
case DifferentialReviewerStatus::STATUS_BLOCKING:
$has_blocking_reviewer = true;
break;
case DifferentialReviewerStatus::STATUS_ACCEPTED:
if ($reviewer->isUser()) {
$active_phid = $active_diff->getPHID();
if ($reviewer->isAccepted($active_phid)) {
$has_accepting_user = true;
}
}
break;
}
}
$new_status = null;
if ($has_accepting_user &&
!$has_rejecting_reviewer &&
!$has_rejecting_older_reviewer &&
!$has_blocking_reviewer) {
$new_status = DifferentialRevisionStatus::ACCEPTED;
} else if ($has_rejecting_reviewer) {
// This isn't accepted, and there's at least one rejecting reviewer,
// so the revision needs changes. This usually happens after a
// "reject".
$new_status = DifferentialRevisionStatus::NEEDS_REVISION;
} else if ($was_accepted) {
// This revision was accepted, but it no longer satisfies the
// conditions for acceptance. This usually happens after an accepting
// reviewer resigns or is removed.
$new_status = DifferentialRevisionStatus::NEEDS_REVIEW;
} else if ($was_revision) {
// This revision was "Needs Revision", but no longer has any rejecting
// reviewers. This usually happens after the last rejecting reviewer
// resigns or is removed. Put the revision back in "Needs Review".
$new_status = DifferentialRevisionStatus::NEEDS_REVIEW;
}
if ($new_status === null) {
return $xactions;
}
$old_status = $revision->getModernRevisionStatus();
if ($new_status == $old_status) {
return $xactions;
}
$xaction = id(new DifferentialTransaction())
->setTransactionType(
DifferentialRevisionStatusTransaction::TRANSACTIONTYPE)
->setOldValue($old_status)
->setNewValue($new_status);
$xaction = $this->populateTransaction($revision, $xaction)
->save();
$xactions[] = $xaction;
// Save the status adjustment we made earlier.
$revision
->setModernRevisionStatus($new_status)
->save();
return $xactions;
}
protected function sortTransactions(array $xactions) {
$xactions = parent::sortTransactions($xactions);
$head = array();
$tail = array();
foreach ($xactions as $xaction) {
$type = $xaction->getTransactionType();
if ($type == DifferentialTransaction::TYPE_INLINE) {
$tail[] = $xaction;
} else {
$head[] = $xaction;
}
}
return array_values(array_merge($head, $tail));
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
if (!$object->getShouldBroadcast()) {
return false;
}
return true;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailTo(PhabricatorLiskDAO $object) {
if ($object->getShouldBroadcast()) {
$this->requireReviewers($object);
$phids = array();
$phids[] = $object->getAuthorPHID();
foreach ($object->getReviewers() as $reviewer) {
if ($reviewer->isResigned()) {
continue;
}
$phids[] = $reviewer->getReviewerPHID();
}
return $phids;
}
// If we're demoting a draft after a build failure, just notify the author.
if ($this->isDraftDemotion) {
$author_phid = $object->getAuthorPHID();
return array(
$author_phid,
);
}
return array();
}
protected function getMailCC(PhabricatorLiskDAO $object) {
if (!$object->getShouldBroadcast()) {
return array();
}
return parent::getMailCC($object);
}
protected function newMailUnexpandablePHIDs(PhabricatorLiskDAO $object) {
$this->requireReviewers($object);
$phids = array();
foreach ($object->getReviewers() as $reviewer) {
if ($reviewer->isResigned()) {
$phids[] = $reviewer->getReviewerPHID();
}
}
return $phids;
}
protected function getMailAction(
PhabricatorLiskDAO $object,
array $xactions) {
$show_lines = false;
if ($this->isFirstBroadcast()) {
$action = pht('Request');
$show_lines = true;
} else {
$action = parent::getMailAction($object, $xactions);
$strongest = $this->getStrongestAction($object, $xactions);
$type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE;
if ($strongest->getTransactionType() == $type_update) {
$show_lines = true;
}
}
if ($show_lines) {
$count = new PhutilNumber($object->getLineCount());
$action = pht('%s] [%s', $action, $object->getRevisionScaleGlyphs());
}
return $action;
}
protected function getMailSubjectPrefix() {
- return PhabricatorEnv::getEnvConfig('metamta.differential.subject-prefix');
+ return pht('[Differential]');
}
protected function getMailThreadID(PhabricatorLiskDAO $object) {
// This is nonstandard, but retains threading with older messages.
$phid = $object->getPHID();
return "differential-rev-{$phid}-req";
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new DifferentialReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$monogram = $object->getMonogram();
$title = $object->getTitle();
return id(new PhabricatorMetaMTAMail())
->setSubject(pht('%s: %s', $monogram, $title))
->setMustEncryptSubject(pht('%s: Revision Updated', $monogram))
->setMustEncryptURI($object->getURI());
}
protected function getTransactionsForMail(
PhabricatorLiskDAO $object,
array $xactions) {
// If this is the first time we're sending mail about this revision, we
// generate mail for all prior transactions, not just whatever is being
// applied now. This gets the "added reviewers" lines and other relevant
// information into the mail.
if ($this->isFirstBroadcast()) {
return $this->loadUnbroadcastTransactions($object);
}
return $xactions;
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$viewer = $this->requireActor();
$body = id(new PhabricatorMetaMTAMailBody())
->setViewer($viewer);
$revision_uri = $object->getURI();
$revision_uri = PhabricatorEnv::getProductionURI($revision_uri);
$new_uri = $revision_uri.'/new/';
$this->addHeadersAndCommentsToMailBody(
$body,
$xactions,
pht('View Revision'),
$revision_uri);
$type_inline = DifferentialTransaction::TYPE_INLINE;
$inlines = array();
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == $type_inline) {
$inlines[] = $xaction;
}
}
if ($inlines) {
$this->appendInlineCommentsForMail($object, $inlines, $body);
}
$update_xaction = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:
$update_xaction = $xaction;
break;
}
}
if ($update_xaction) {
$diff = $this->requireDiff($update_xaction->getNewValue(), true);
} else {
$diff = null;
}
$changed_uri = $this->getChangedPriorToCommitURI();
if ($changed_uri) {
$body->addLinkSection(
pht('CHANGED PRIOR TO COMMIT'),
$changed_uri);
}
$this->addCustomFieldsToMailBody($body, $object, $xactions);
if (!$this->isFirstBroadcast()) {
$body->addLinkSection(pht('CHANGES SINCE LAST ACTION'), $new_uri);
}
$body->addLinkSection(
pht('REVISION DETAIL'),
$revision_uri);
if ($update_xaction) {
$body->addTextSection(
pht('AFFECTED FILES'),
$this->renderAffectedFilesForMail($diff));
$config_key_inline = 'metamta.differential.inline-patches';
$config_inline = PhabricatorEnv::getEnvConfig($config_key_inline);
$config_key_attach = 'metamta.differential.attach-patches';
$config_attach = PhabricatorEnv::getEnvConfig($config_key_attach);
if ($config_inline || $config_attach) {
$body_limit = PhabricatorEnv::getEnvConfig('metamta.email-body-limit');
try {
$patch = $this->buildPatchForMail($diff, $body_limit);
} catch (ArcanistDiffByteSizeException $ex) {
$patch = null;
}
if (($patch !== null) && $config_inline) {
$lines = substr_count($patch, "\n");
$bytes = strlen($patch);
// Limit the patch size to the smaller of 256 bytes per line or
// the mail body limit. This prevents degenerate behavior for patches
// with one line that is 10MB long. See T11748.
$byte_limits = array();
$byte_limits[] = (256 * $config_inline);
$byte_limits[] = $body_limit;
$byte_limit = min($byte_limits);
$lines_ok = ($lines <= $config_inline);
$bytes_ok = ($bytes <= $byte_limit);
if ($lines_ok && $bytes_ok) {
$this->appendChangeDetailsForMail($object, $diff, $patch, $body);
} else {
// TODO: Provide a helpful message about the patch being too
// large or lengthy here.
}
}
if (($patch !== null) && $config_attach) {
// See T12033, T11767, and PHI55. This is a crude fix to stop the
// major concrete problems that lackluster email size limits cause.
if (strlen($patch) < $body_limit) {
$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 PhabricatorMailAttachment($patch, $name, $mime_type));
}
}
}
}
return $body;
}
public function getMailTagsMap() {
return array(
DifferentialTransaction::MAILTAG_REVIEW_REQUEST =>
pht('A revision is created.'),
DifferentialTransaction::MAILTAG_UPDATED =>
pht('A revision is updated.'),
DifferentialTransaction::MAILTAG_COMMENT =>
pht('Someone comments on a revision.'),
DifferentialTransaction::MAILTAG_CLOSED =>
pht('A revision is closed.'),
DifferentialTransaction::MAILTAG_REVIEWERS =>
pht("A revision's reviewers change."),
DifferentialTransaction::MAILTAG_CC =>
pht("A revision's CCs change."),
DifferentialTransaction::MAILTAG_OTHER =>
pht('Other revision activity not listed above occurs.'),
);
}
protected function supportsSearch() {
return true;
}
protected function expandCustomRemarkupBlockTransactions(
PhabricatorLiskDAO $object,
array $xactions,
array $changes,
PhutilMarkupEngine $engine) {
// For "Fixes ..." and "Depends on ...", we're only going to look at
// content blocks which are part of the revision itself (like "Summary"
// and "Test Plan"), not comments.
$content_parts = array();
foreach ($changes as $change) {
if ($change->getTransaction()->isCommentTransaction()) {
continue;
}
$content_parts[] = $change->getNewValue();
}
if (!$content_parts) {
return array();
}
$content_block = implode("\n\n", $content_parts);
$task_map = array();
$task_refs = id(new ManiphestCustomFieldStatusParser())
->parseCorpus($content_block);
foreach ($task_refs as $match) {
foreach ($match['monograms'] as $monogram) {
$task_id = (int)trim($monogram, 'tT');
$task_map[$task_id] = true;
}
}
$rev_map = array();
$rev_refs = id(new DifferentialCustomFieldDependsOnParser())
->parseCorpus($content_block);
foreach ($rev_refs as $match) {
foreach ($match['monograms'] as $monogram) {
$rev_id = (int)trim($monogram, 'dD');
$rev_map[$rev_id] = true;
}
}
$edges = array();
$task_phids = array();
$rev_phids = array();
if ($task_map) {
$tasks = id(new ManiphestTaskQuery())
->setViewer($this->getActor())
->withIDs(array_keys($task_map))
->execute();
if ($tasks) {
$task_phids = mpull($tasks, 'getPHID', 'getPHID');
$edge_related = DifferentialRevisionHasTaskEdgeType::EDGECONST;
$edges[$edge_related] = $task_phids;
}
}
if ($rev_map) {
$revs = id(new DifferentialRevisionQuery())
->setViewer($this->getActor())
->withIDs(array_keys($rev_map))
->execute();
$rev_phids = mpull($revs, 'getPHID', 'getPHID');
// NOTE: Skip any write attempts if a user cleverly implies a revision
// depends upon itself.
unset($rev_phids[$object->getPHID()]);
if ($revs) {
$depends = DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST;
$edges[$depends] = $rev_phids;
}
}
$revert_refs = id(new DifferentialCustomFieldRevertsParser())
->parseCorpus($content_block);
$revert_monograms = array();
foreach ($revert_refs as $match) {
foreach ($match['monograms'] as $monogram) {
$revert_monograms[] = $monogram;
}
}
if ($revert_monograms) {
$revert_objects = id(new PhabricatorObjectQuery())
->setViewer($this->getActor())
->withNames($revert_monograms)
->withTypes(
array(
DifferentialRevisionPHIDType::TYPECONST,
PhabricatorRepositoryCommitPHIDType::TYPECONST,
))
->execute();
$revert_phids = mpull($revert_objects, 'getPHID', 'getPHID');
// Don't let an object revert itself, although other silly stuff like
// cycles of objects reverting each other is not prevented.
unset($revert_phids[$object->getPHID()]);
$revert_type = DiffusionCommitRevertsCommitEdgeType::EDGECONST;
$edges[$revert_type] = $revert_phids;
} else {
$revert_phids = array();
}
// See PHI574. Respect any unmentionable PHIDs which were set on the
// Editor by the caller.
$unmentionable_map = $this->getUnmentionablePHIDMap();
$unmentionable_map += $task_phids;
$unmentionable_map += $rev_phids;
$unmentionable_map += $revert_phids;
$this->setUnmentionablePHIDMap($unmentionable_map);
$result = array();
foreach ($edges as $type => $specs) {
$result[] = id(new DifferentialTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $type)
->setNewValue(array('+' => $specs));
}
return $result;
}
private function appendInlineCommentsForMail(
PhabricatorLiskDAO $object,
array $inlines,
PhabricatorMetaMTAMailBody $body) {
$limit = 100;
$limit_note = null;
if (count($inlines) > $limit) {
$limit_note = pht(
'(Showing first %s of %s inline comments.)',
new PhutilNumber($limit),
phutil_count($inlines));
$inlines = array_slice($inlines, 0, $limit, true);
}
$section = id(new DifferentialInlineCommentMailView())
->setViewer($this->getActor())
->setInlines($inlines)
->buildMailSection();
$header = pht('INLINE COMMENTS');
$section_text = "\n".$section->getPlaintext();
if ($limit_note) {
$section_text = $limit_note."\n".$section_text;
}
$style = array(
'margin: 6px 0 12px 0;',
);
$section_html = phutil_tag(
'div',
array(
'style' => implode(' ', $style),
),
$section->getHTML());
if ($limit_note) {
$section_html = array(
phutil_tag(
'em',
array(),
$limit_note),
$section_html,
);
}
$body->addPlaintextSection($header, $section_text, false);
$body->addHTMLSection($header, $section_html);
}
private function appendChangeDetailsForMail(
PhabricatorLiskDAO $object,
DifferentialDiff $diff,
$patch,
PhabricatorMetaMTAMailBody $body) {
$section = id(new DifferentialChangeDetailMailView())
->setViewer($this->getActor())
->setDiff($diff)
->setPatch($patch)
->buildMailSection();
$header = pht('CHANGE DETAILS');
$section_text = "\n".$section->getPlaintext();
$style = array(
'margin: 6px 0 12px 0;',
);
$section_html = phutil_tag(
'div',
array(
'style' => implode(' ', $style),
),
$section->getHTML());
$body->addPlaintextSection($header, $section_text, false);
$body->addHTMLSection($header, $section_html);
}
private function loadDiff($phid, $need_changesets = false) {
$query = id(new DifferentialDiffQuery())
->withPHIDs(array($phid))
->setViewer($this->getActor());
if ($need_changesets) {
$query->needChangesets(true);
}
return $query->executeOne();
}
public function requireDiff($phid, $need_changesets = false) {
$diff = $this->loadDiff($phid, $need_changesets);
if (!$diff) {
throw new Exception(pht('Diff "%s" does not exist!', $phid));
}
return $diff;
}
/* -( Herald Integration )------------------------------------------------- */
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function didApplyHeraldRules(
PhabricatorLiskDAO $object,
HeraldAdapter $adapter,
HeraldTranscript $transcript) {
$repository = $object->getRepository();
if (!$repository) {
return array();
}
$diff = $this->ownersDiff;
$changesets = $this->ownersChangesets;
$this->ownersDiff = null;
$this->ownersChangesets = null;
if (!$changesets) {
return array();
}
$packages = PhabricatorOwnersPackage::loadAffectedPackagesForChangesets(
$repository,
$diff,
$changesets);
if (!$packages) {
return array();
}
// Identify the packages with "Non-Owner Author" review rules and remove
// them if the author has authority over the package.
$autoreview_map = PhabricatorOwnersPackage::getAutoreviewOptionsMap();
$need_authority = array();
foreach ($packages as $package) {
$autoreview_setting = $package->getAutoReview();
$spec = idx($autoreview_map, $autoreview_setting);
if (!$spec) {
continue;
}
if (idx($spec, 'authority')) {
$need_authority[$package->getPHID()] = $package->getPHID();
}
}
if ($need_authority) {
$authority = id(new PhabricatorOwnersPackageQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($need_authority)
->withAuthorityPHIDs(array($object->getAuthorPHID()))
->execute();
$authority = mpull($authority, null, 'getPHID');
foreach ($packages as $key => $package) {
$package_phid = $package->getPHID();
if (isset($authority[$package_phid])) {
unset($packages[$key]);
continue;
}
}
if (!$packages) {
return array();
}
}
$auto_subscribe = array();
$auto_review = array();
$auto_block = array();
foreach ($packages as $package) {
switch ($package->getAutoReview()) {
case PhabricatorOwnersPackage::AUTOREVIEW_REVIEW:
case PhabricatorOwnersPackage::AUTOREVIEW_REVIEW_ALWAYS:
$auto_review[] = $package;
break;
case PhabricatorOwnersPackage::AUTOREVIEW_BLOCK:
case PhabricatorOwnersPackage::AUTOREVIEW_BLOCK_ALWAYS:
$auto_block[] = $package;
break;
case PhabricatorOwnersPackage::AUTOREVIEW_SUBSCRIBE:
case PhabricatorOwnersPackage::AUTOREVIEW_SUBSCRIBE_ALWAYS:
$auto_subscribe[] = $package;
break;
case PhabricatorOwnersPackage::AUTOREVIEW_NONE:
default:
break;
}
}
$owners_phid = id(new PhabricatorOwnersApplication())
->getPHID();
$xactions = array();
if ($auto_subscribe) {
$xactions[] = $object->getApplicationTransactionTemplate()
->setAuthorPHID($owners_phid)
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setNewValue(
array(
'+' => mpull($auto_subscribe, 'getPHID'),
));
}
$specs = array(
array($auto_review, false),
array($auto_block, true),
);
foreach ($specs as $spec) {
list($reviewers, $blocking) = $spec;
if (!$reviewers) {
continue;
}
$phids = mpull($reviewers, 'getPHID');
$xaction = $this->newAutoReviewTransaction($object, $phids, $blocking);
if ($xaction) {
$xactions[] = $xaction;
}
}
return $xactions;
}
private function newAutoReviewTransaction(
PhabricatorLiskDAO $object,
array $phids,
$is_blocking) {
// TODO: This is substantially similar to DifferentialReviewersHeraldAction
// and both are needlessly complex. This logic should live in the normal
// transaction application pipeline. See T10967.
$reviewers = $object->getReviewers();
$reviewers = mpull($reviewers, null, 'getReviewerPHID');
if ($is_blocking) {
$new_status = DifferentialReviewerStatus::STATUS_BLOCKING;
} else {
$new_status = DifferentialReviewerStatus::STATUS_ADDED;
}
$new_strength = DifferentialReviewerStatus::getStatusStrength(
$new_status);
$current = array();
foreach ($phids as $phid) {
if (!isset($reviewers[$phid])) {
continue;
}
// If we're applying a stronger status (usually, upgrading a reviewer
// into a blocking reviewer), skip this check so we apply the change.
$old_strength = DifferentialReviewerStatus::getStatusStrength(
$reviewers[$phid]->getReviewerStatus());
if ($old_strength <= $new_strength) {
continue;
}
$current[] = $phid;
}
$phids = array_diff($phids, $current);
if (!$phids) {
return null;
}
$phids = array_fuse($phids);
$value = array();
foreach ($phids as $phid) {
if ($is_blocking) {
$value[] = 'blocking('.$phid.')';
} else {
$value[] = $phid;
}
}
$owners_phid = id(new PhabricatorOwnersApplication())
->getPHID();
$reviewers_type = DifferentialRevisionReviewersTransaction::TRANSACTIONTYPE;
return $object->getApplicationTransactionTemplate()
->setAuthorPHID($owners_phid)
->setTransactionType($reviewers_type)
->setNewValue(
array(
'+' => $value,
));
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
$revision = id(new DifferentialRevisionQuery())
->setViewer($this->getActor())
->withPHIDs(array($object->getPHID()))
->needActiveDiffs(true)
->needReviewers(true)
->executeOne();
if (!$revision) {
throw new Exception(
pht('Failed to load revision for Herald adapter construction!'));
}
$adapter = HeraldDifferentialRevisionAdapter::newLegacyAdapter(
$revision,
$revision->getActiveDiff());
// If the object is still a draft, prevent "Send me an email" and other
// similar rules from acting yet.
if (!$object->getShouldBroadcast()) {
$adapter->setForbiddenAction(
HeraldMailableState::STATECONST,
DifferentialHeraldStateReasons::REASON_DRAFT);
}
// If this edit didn't actually change the diff (for example, a user
// edited the title or changed subscribers), prevent "Run build plan"
// and other similar rules from acting yet, since the build results will
// not (or, at least, should not) change unless the actual source changes.
// We also don't run Differential builds if the update was caused by
// discovering a commit, as the expectation is that Diffusion builds take
// over once things land.
$has_update = false;
$has_commit = false;
$type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() != $type_update) {
continue;
}
if ($xaction->getMetadataValue('isCommitUpdate')) {
$has_commit = true;
} else {
$has_update = true;
}
break;
}
if ($has_commit) {
$adapter->setForbiddenAction(
HeraldBuildableState::STATECONST,
DifferentialHeraldStateReasons::REASON_LANDED);
} else if (!$has_update) {
$adapter->setForbiddenAction(
HeraldBuildableState::STATECONST,
DifferentialHeraldStateReasons::REASON_UNCHANGED);
}
return $adapter;
}
/**
* Update the table which links Differential revisions to paths they affect,
* so Diffusion can efficiently find pending revisions for a given file.
*/
private function updateAffectedPathTable(
DifferentialRevision $revision,
DifferentialDiff $diff) {
$repository = $revision->getRepository();
if (!$repository) {
// The repository where the code lives is untracked.
return;
}
$path_prefix = null;
$local_root = $diff->getSourceControlPath();
if ($local_root) {
// We're in a working copy which supports subdirectory checkouts (e.g.,
// SVN) so we need to figure out what prefix we should add to each path
// (e.g., trunk/projects/example/) to get the absolute path from the
// root of the repository. DVCS systems like Git and Mercurial are not
// affected.
// Normalize both paths and check if the repository root is a prefix of
// the local root. If so, throw it away. Note that this correctly handles
// the case where the remote path is "/".
$local_root = id(new PhutilURI($local_root))->getPath();
$local_root = rtrim($local_root, '/');
$repo_root = id(new PhutilURI($repository->getRemoteURI()))->getPath();
$repo_root = rtrim($repo_root, '/');
if (!strncmp($repo_root, $local_root, strlen($repo_root))) {
$path_prefix = substr($local_root, strlen($repo_root));
}
}
$changesets = $diff->getChangesets();
$paths = array();
foreach ($changesets as $changeset) {
$paths[] = $path_prefix.'/'.$changeset->getFilename();
}
// If this change affected paths, save the changesets so we can apply
// Owners rules to them later.
if ($paths) {
$this->ownersDiff = $diff;
$this->ownersChangesets = $changesets;
}
// Mark this as also touching all parent paths, so you can see all pending
// changes to any file within a directory.
$all_paths = array();
foreach ($paths as $local) {
foreach (DiffusionPathIDQuery::expandPathToRoot($local) as $path) {
$all_paths[$path] = true;
}
}
$all_paths = array_keys($all_paths);
$path_ids =
PhabricatorRepositoryCommitChangeParserWorker::lookupOrCreatePaths(
$all_paths);
$table = new DifferentialAffectedPath();
$conn_w = $table->establishConnection('w');
$sql = array();
foreach ($path_ids as $path_id) {
$sql[] = qsprintf(
$conn_w,
'(%d, %d, %d, %d)',
$repository->getID(),
$path_id,
time(),
$revision->getID());
}
queryfx(
$conn_w,
'DELETE FROM %T WHERE revisionID = %d',
$table->getTableName(),
$revision->getID());
foreach (array_chunk($sql, 256) as $chunk) {
queryfx(
$conn_w,
'INSERT INTO %T (repositoryID, pathID, epoch, revisionID) VALUES %LQ',
$table->getTableName(),
$chunk);
}
}
/**
* Update the table connecting revisions to DVCS local hashes, so we can
* identify revisions by commit/tree hashes.
*/
private function updateRevisionHashTable(
DifferentialRevision $revision,
DifferentialDiff $diff) {
$vcs = $diff->getSourceControlSystem();
if ($vcs == DifferentialRevisionControlSystem::SVN) {
// Subversion has no local commit or tree hash information, so we don't
// have to do anything.
return;
}
$property = id(new DifferentialDiffProperty())->loadOneWhere(
'diffID = %d AND name = %s',
$diff->getID(),
'local:commits');
if (!$property) {
return;
}
$hashes = array();
$data = $property->getData();
switch ($vcs) {
case DifferentialRevisionControlSystem::GIT:
foreach ($data as $commit) {
$hashes[] = array(
ArcanistDifferentialRevisionHash::HASH_GIT_COMMIT,
$commit['commit'],
);
$hashes[] = array(
ArcanistDifferentialRevisionHash::HASH_GIT_TREE,
$commit['tree'],
);
}
break;
case DifferentialRevisionControlSystem::MERCURIAL:
foreach ($data as $commit) {
$hashes[] = array(
ArcanistDifferentialRevisionHash::HASH_MERCURIAL_COMMIT,
$commit['rev'],
);
}
break;
}
$conn_w = $revision->establishConnection('w');
$sql = array();
foreach ($hashes as $info) {
list($type, $hash) = $info;
$sql[] = qsprintf(
$conn_w,
'(%d, %s, %s)',
$revision->getID(),
$type,
$hash);
}
queryfx(
$conn_w,
'DELETE FROM %T WHERE revisionID = %d',
ArcanistDifferentialRevisionHash::TABLE_NAME,
$revision->getID());
if ($sql) {
queryfx(
$conn_w,
'INSERT INTO %T (revisionID, type, hash) VALUES %LQ',
ArcanistDifferentialRevisionHash::TABLE_NAME,
$sql);
}
}
private function renderAffectedFilesForMail(DifferentialDiff $diff) {
$changesets = $diff->getChangesets();
$filenames = mpull($changesets, 'getDisplayFilename');
sort($filenames);
$count = count($filenames);
$max = 250;
if ($count > $max) {
$filenames = array_slice($filenames, 0, $max);
$filenames[] = pht('(%d more files...)', ($count - $max));
}
return implode("\n", $filenames);
}
private function renderPatchHTMLForMail($patch) {
return phutil_tag('pre',
array('style' => 'font-family: monospace;'), $patch);
}
private function buildPatchForMail(DifferentialDiff $diff, $byte_limit) {
$format = PhabricatorEnv::getEnvConfig('metamta.differential.patch-format');
return id(new DifferentialRawDiffRenderer())
->setViewer($this->getActor())
->setFormat($format)
->setChangesets($diff->getChangesets())
->setByteLimit($byte_limit)
->buildPatch();
}
protected function willPublish(PhabricatorLiskDAO $object, array $xactions) {
// Reload to pick up the active diff and reviewer status.
return id(new DifferentialRevisionQuery())
->setViewer($this->getActor())
->needReviewers(true)
->needActiveDiffs(true)
->withIDs(array($object->getID()))
->executeOne();
}
protected function getCustomWorkerState() {
return array(
'changedPriorToCommitURI' => $this->changedPriorToCommitURI,
'firstBroadcast' => $this->firstBroadcast,
'isDraftDemotion' => $this->isDraftDemotion,
);
}
protected function loadCustomWorkerState(array $state) {
$this->changedPriorToCommitURI = idx($state, 'changedPriorToCommitURI');
$this->firstBroadcast = idx($state, 'firstBroadcast');
$this->isDraftDemotion = idx($state, 'isDraftDemotion');
return $this;
}
private function newCommandeerReviewerTransaction(
DifferentialRevision $revision) {
$actor_phid = $this->getActingAsPHID();
$owner_phid = $revision->getAuthorPHID();
// If the user is commandeering, add the previous owner as a
// reviewer and remove the actor.
$edits = array(
'-' => array(
$actor_phid,
),
'+' => array(
$owner_phid,
),
);
// NOTE: We're setting setIsCommandeerSideEffect() on this because normally
// you can't add a revision's author as a reviewer, but this action swaps
// them after validation executes.
$xaction_type = DifferentialRevisionReviewersTransaction::TRANSACTIONTYPE;
return id(new DifferentialTransaction())
->setTransactionType($xaction_type)
->setIgnoreOnNoEffect(true)
->setIsCommandeerSideEffect(true)
->setNewValue($edits);
}
public function getActiveDiff($object) {
if ($this->getIsNewObject()) {
return null;
} else {
return $object->getActiveDiff();
}
}
/**
* When a reviewer makes a comment, mark the last revision they commented
* on.
*
* This allows us to show a hint to help authors and other reviewers quickly
* distinguish between reviewers who have participated in the discussion and
* reviewers who haven't been part of it.
*/
private function markReviewerComments($object, array $xactions) {
$acting_phid = $this->getActingAsPHID();
if (!$acting_phid) {
return;
}
$diff = $this->getActiveDiff($object);
if (!$diff) {
return;
}
$has_comment = false;
foreach ($xactions as $xaction) {
if ($xaction->hasComment()) {
$has_comment = true;
break;
}
}
if (!$has_comment) {
return;
}
$reviewer_table = new DifferentialReviewer();
$conn = $reviewer_table->establishConnection('w');
queryfx(
$conn,
'UPDATE %T SET lastCommentDiffPHID = %s
WHERE revisionPHID = %s
AND reviewerPHID = %s',
$reviewer_table->getTableName(),
$diff->getPHID(),
$object->getPHID(),
$acting_phid);
}
private function loadUnbroadcastTransactions($object) {
$viewer = $this->requireActor();
$xactions = id(new DifferentialTransactionQuery())
->setViewer($viewer)
->withObjectPHIDs(array($object->getPHID()))
->execute();
return array_reverse($xactions);
}
protected function didApplyTransactions($object, array $xactions) {
// In a moment, we're going to try to publish draft revisions which have
// completed all their builds. However, we only want to do that if the
// actor is either the revision author or an omnipotent user (generally,
// the Harbormaster application).
// If we let any actor publish the revision as a side effect of other
// changes then an unlucky third party who innocently comments on the draft
// can end up racing Harbormaster and promoting the revision. At best, this
// is confusing. It can also run into validation problems with the "Request
// Review" transaction. See PHI309 for some discussion.
$author_phid = $object->getAuthorPHID();
$viewer = $this->requireActor();
$can_undraft =
($this->getActingAsPHID() === $author_phid) ||
($viewer->isOmnipotent());
// If a draft revision has no outstanding builds and we're automatically
// making drafts public after builds finish, make the revision public.
if ($can_undraft) {
$auto_undraft = !$object->getHoldAsDraft();
} else {
$auto_undraft = false;
}
$can_promote = false;
$can_demote = false;
// "Draft" revisions can promote to "Review Requested" after builds pass,
// or demote to "Changes Planned" after builds fail.
if ($object->isDraft()) {
$can_promote = true;
$can_demote = true;
}
// See PHI584. "Changes Planned" revisions which are not yet broadcasting
// can promote to "Review Requested" if builds pass.
// This pass is presumably the result of someone restarting the builds and
// having them work this time, perhaps because the builds are not perfectly
// reliable or perhaps because someone fixed some issue with build hardware
// or some other dependency.
// Currently, there's no legitimate way to end up in this state except
// through automatic demotion, so this behavior should not generate an
// undue level of confusion or ambiguity. Also note that these changes can
// not demote again since they've already been demoted once.
if ($object->isChangePlanned()) {
if (!$object->getShouldBroadcast()) {
$can_promote = true;
}
}
if (($can_promote || $can_demote) && $auto_undraft) {
$status = $this->loadCompletedBuildableStatus($object);
$is_passed = ($status === HarbormasterBuildableStatus::STATUS_PASSED);
$is_failed = ($status === HarbormasterBuildableStatus::STATUS_FAILED);
if ($is_passed && $can_promote) {
// When Harbormaster moves a revision out of the draft state, we
// attribute the action to the revision author since this is more
// natural and more useful.
// Additionally, we change the acting PHID for the transaction set
// to the author if it isn't already a user so that mail comes from
// the natural author.
$acting_phid = $this->getActingAsPHID();
$user_type = PhabricatorPeopleUserPHIDType::TYPECONST;
if (phid_get_type($acting_phid) != $user_type) {
$this->setActingAsPHID($author_phid);
}
$xaction = $object->getApplicationTransactionTemplate()
->setAuthorPHID($author_phid)
->setTransactionType(
DifferentialRevisionRequestReviewTransaction::TRANSACTIONTYPE)
->setNewValue(true);
// If we're creating this revision and immediately moving it out of
// the draft state, mark this as a create transaction so it gets
// hidden in the timeline and mail, since it isn't interesting: it
// is as though the draft phase never happened.
if ($this->getIsNewObject()) {
$xaction->setIsCreateTransaction(true);
}
// Queue this transaction and apply it separately after the current
// batch of transactions finishes so that Herald can fire on the new
// revision state. See T13027 for discussion.
$this->queueTransaction($xaction);
} else if ($is_failed && $can_demote) {
// When demoting a revision, we act as "Harbormaster" instead of
// the author since this feels a little more natural.
$harbormaster_phid = id(new PhabricatorHarbormasterApplication())
->getPHID();
$xaction = $object->getApplicationTransactionTemplate()
->setAuthorPHID($harbormaster_phid)
->setMetadataValue('draft.demote', true)
->setTransactionType(
DifferentialRevisionPlanChangesTransaction::TRANSACTIONTYPE)
->setNewValue(true);
$this->queueTransaction($xaction);
}
}
// If the revision is new or was a draft, and is no longer a draft, we
// might be sending the first email about it.
// This might mean it was created directly into a non-draft state, or
// it just automatically undrafted after builds finished, or a user
// explicitly promoted it out of the draft state with an action like
// "Request Review".
// If we haven't sent any email about it yet, mark this email as the first
// email so the mail gets enriched with "SUMMARY" and "TEST PLAN".
$is_new = $this->getIsNewObject();
$was_broadcasting = $this->wasBroadcasting;
if ($object->getShouldBroadcast()) {
if (!$was_broadcasting || $is_new) {
// Mark this as the first broadcast we're sending about the revision
// so mail can generate specially.
$this->firstBroadcast = true;
}
}
return $xactions;
}
private function loadCompletedBuildableStatus(
DifferentialRevision $revision) {
$viewer = $this->requireActor();
$builds = $revision->loadImpactfulBuilds($viewer);
return $revision->newBuildableStatusForBuilds($builds);
}
private function requireReviewers(DifferentialRevision $revision) {
if ($revision->hasAttachedReviewers()) {
return;
}
$with_reviewers = id(new DifferentialRevisionQuery())
->setViewer($this->getActor())
->needReviewers(true)
->withPHIDs(array($revision->getPHID()))
->executeOne();
if (!$with_reviewers) {
throw new Exception(
pht(
'Failed to reload revision ("%s").',
$revision->getPHID()));
}
$revision->attachReviewers($with_reviewers->getReviewers());
}
}
diff --git a/src/applications/differential/engine/DifferentialDiffExtractionEngine.php b/src/applications/differential/engine/DifferentialDiffExtractionEngine.php
index d7d7c767e..861d2ad22 100644
--- a/src/applications/differential/engine/DifferentialDiffExtractionEngine.php
+++ b/src/applications/differential/engine/DifferentialDiffExtractionEngine.php
@@ -1,318 +1,325 @@
<?php
final class DifferentialDiffExtractionEngine extends Phobject {
private $viewer;
private $authorPHID;
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setAuthorPHID($author_phid) {
$this->authorPHID = $author_phid;
return $this;
}
public function getAuthorPHID() {
return $this->authorPHID;
}
public function newDiffFromCommit(PhabricatorRepositoryCommit $commit) {
$viewer = $this->getViewer();
// If we already have an unattached diff for this commit, just reuse it.
// This stops us from repeatedly generating diffs if something goes wrong
// later in the process. See T10968 for context.
$existing_diffs = id(new DifferentialDiffQuery())
->setViewer($viewer)
->withCommitPHIDs(array($commit->getPHID()))
->withHasRevision(false)
->needChangesets(true)
->execute();
if ($existing_diffs) {
return head($existing_diffs);
}
$repository = $commit->getRepository();
$identifier = $commit->getCommitIdentifier();
$monogram = $commit->getMonogram();
$drequest = DiffusionRequest::newFromDictionary(
array(
'user' => $viewer,
'repository' => $repository,
));
$diff_info = DiffusionQuery::callConduitWithDiffusionRequest(
$viewer,
$drequest,
'diffusion.rawdiffquery',
array(
'commit' => $identifier,
));
$file_phid = $diff_info['filePHID'];
$diff_file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($file_phid))
->executeOne();
if (!$diff_file) {
throw new Exception(
pht(
'Failed to load file ("%s") returned by "%s".',
$file_phid,
'diffusion.rawdiffquery'));
}
$raw_diff = $diff_file->loadFileData();
// TODO: Support adds, deletes and moves under SVN.
if (strlen($raw_diff)) {
$changes = id(new ArcanistDiffParser())->parseDiff($raw_diff);
} else {
// This is an empty diff, maybe made with `git commit --allow-empty`.
// NOTE: These diffs have the same tree hash as their ancestors, so
// they may attach to revisions in an unexpected way. Just let this
// happen for now, although it might make sense to special case it
// eventually.
$changes = array();
}
$diff = DifferentialDiff::newFromRawChanges($viewer, $changes)
->setRepositoryPHID($repository->getPHID())
->setCommitPHID($commit->getPHID())
->setCreationMethod('commit')
->setSourceControlSystem($repository->getVersionControlSystem())
->setLintStatus(DifferentialLintStatus::LINT_AUTO_SKIP)
->setUnitStatus(DifferentialUnitStatus::UNIT_AUTO_SKIP)
->setDateCreated($commit->getEpoch())
->setDescription($monogram);
$author_phid = $this->getAuthorPHID();
if ($author_phid !== null) {
$diff->setAuthorPHID($author_phid);
}
$parents = DiffusionQuery::callConduitWithDiffusionRequest(
$viewer,
$drequest,
'diffusion.commitparentsquery',
array(
'commit' => $identifier,
));
if ($parents) {
$diff->setSourceControlBaseRevision(head($parents));
}
// TODO: Attach binary files.
return $diff->save();
}
public function isDiffChangedBeforeCommit(
PhabricatorRepositoryCommit $commit,
DifferentialDiff $old,
DifferentialDiff $new) {
$viewer = $this->getViewer();
$repository = $commit->getRepository();
$identifier = $commit->getCommitIdentifier();
$vs_changesets = array();
foreach ($old->getChangesets() as $changeset) {
$path = $changeset->getAbsoluteRepositoryPath($repository, $old);
$path = ltrim($path, '/');
$vs_changesets[$path] = $changeset;
}
$changesets = array();
foreach ($new->getChangesets() as $changeset) {
$path = $changeset->getAbsoluteRepositoryPath($repository, $new);
$path = ltrim($path, '/');
$changesets[$path] = $changeset;
}
if (array_fill_keys(array_keys($changesets), true) !=
array_fill_keys(array_keys($vs_changesets), true)) {
return true;
}
$file_phids = array();
foreach ($vs_changesets as $changeset) {
$metadata = $changeset->getMetadata();
$file_phid = idx($metadata, 'new:binary-phid');
if ($file_phid) {
$file_phids[$file_phid] = $file_phid;
}
}
$files = array();
if ($file_phids) {
$files = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
}
foreach ($changesets as $path => $changeset) {
$vs_changeset = $vs_changesets[$path];
$file_phid = idx($vs_changeset->getMetadata(), 'new:binary-phid');
if ($file_phid) {
if (!isset($files[$file_phid])) {
return true;
}
$drequest = DiffusionRequest::newFromDictionary(
array(
'user' => $viewer,
'repository' => $repository,
));
- $response = DiffusionQuery::callConduitWithDiffusionRequest(
- $viewer,
- $drequest,
- 'diffusion.filecontentquery',
- array(
- 'commit' => $identifier,
- 'path' => $path,
- ));
+ try {
+ $response = DiffusionQuery::callConduitWithDiffusionRequest(
+ $viewer,
+ $drequest,
+ 'diffusion.filecontentquery',
+ array(
+ 'commit' => $identifier,
+ 'path' => $path,
+ ));
+ } catch (Exception $ex) {
+ // TODO: See PHI1044. This call may fail if the diff deleted the
+ // file. If the call fails, just detect a change for now. This should
+ // generally be made cleaner in the future.
+ return true;
+ }
$new_file_phid = $response['filePHID'];
if (!$new_file_phid) {
return true;
}
$new_file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($new_file_phid))
->executeOne();
if (!$new_file) {
return true;
}
if ($files[$file_phid]->loadFileData() != $new_file->loadFileData()) {
return true;
}
} else {
$context = implode("\n", $changeset->makeChangesWithContext());
$vs_context = implode("\n", $vs_changeset->makeChangesWithContext());
// We couldn't just compare $context and $vs_context because following
// diffs will be considered different:
//
// -(empty line)
// -echo 'test';
// (empty line)
//
// (empty line)
// -echo "test";
// -(empty line)
$hunk = id(new DifferentialHunk())->setChanges($context);
$vs_hunk = id(new DifferentialHunk())->setChanges($vs_context);
if ($hunk->makeOldFile() != $vs_hunk->makeOldFile() ||
$hunk->makeNewFile() != $vs_hunk->makeNewFile()) {
return true;
}
}
}
return false;
}
public function updateRevisionWithCommit(
DifferentialRevision $revision,
PhabricatorRepositoryCommit $commit,
array $more_xactions,
PhabricatorContentSource $content_source) {
$viewer = $this->getViewer();
$result_data = array();
$new_diff = $this->newDiffFromCommit($commit);
$old_diff = $revision->getActiveDiff();
$changed_uri = null;
if ($old_diff) {
$old_diff = id(new DifferentialDiffQuery())
->setViewer($viewer)
->withIDs(array($old_diff->getID()))
->needChangesets(true)
->executeOne();
if ($old_diff) {
$has_changed = $this->isDiffChangedBeforeCommit(
$commit,
$old_diff,
$new_diff);
if ($has_changed) {
$result_data['vsDiff'] = $old_diff->getID();
$revision_monogram = $revision->getMonogram();
$old_id = $old_diff->getID();
$new_id = $new_diff->getID();
$changed_uri = "/{$revision_monogram}?vs={$old_id}&id={$new_id}#toc";
$changed_uri = PhabricatorEnv::getProductionURI($changed_uri);
}
}
}
$xactions = array();
// If the revision isn't closed or "Accepted", write a warning into the
// transaction log. This makes it more clear when users bend the rules.
if (!$revision->isClosed() && !$revision->isAccepted()) {
$wrong_type = DifferentialRevisionWrongStateTransaction::TRANSACTIONTYPE;
$xactions[] = id(new DifferentialTransaction())
->setTransactionType($wrong_type)
->setNewValue($revision->getModernRevisionStatus());
}
$type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE;
$xactions[] = id(new DifferentialTransaction())
->setTransactionType($type_update)
->setIgnoreOnNoEffect(true)
->setNewValue($new_diff->getPHID())
->setMetadataValue('isCommitUpdate', true)
->setMetadataValue('commitPHIDs', array($commit->getPHID()));
foreach ($more_xactions as $more_xaction) {
$xactions[] = $more_xaction;
}
$editor = id(new DifferentialTransactionEditor())
->setActor($viewer)
->setContinueOnMissingFields(true)
->setContentSource($content_source)
->setChangedPriorToCommitURI($changed_uri)
->setIsCloseByCommit(true);
$author_phid = $this->getAuthorPHID();
if ($author_phid !== null) {
$editor->setActingAsPHID($author_phid);
}
try {
$editor->applyTransactions($revision, $xactions);
} catch (PhabricatorApplicationTransactionNoEffectException $ex) {
// NOTE: We've marked transactions other than the CLOSE transaction
// as ignored when they don't have an effect, so this means that we
// lost a race to close the revision. That's perfectly fine, we can
// just continue normally.
}
return $result_data;
}
}
diff --git a/src/applications/differential/engine/DifferentialRevisionTimelineEngine.php b/src/applications/differential/engine/DifferentialRevisionTimelineEngine.php
new file mode 100644
index 000000000..51d2c28fe
--- /dev/null
+++ b/src/applications/differential/engine/DifferentialRevisionTimelineEngine.php
@@ -0,0 +1,78 @@
+<?php
+
+final class DifferentialRevisionTimelineEngine
+ extends PhabricatorTimelineEngine {
+
+ protected function newTimelineView() {
+ $viewer = $this->getViewer();
+ $xactions = $this->getTransactions();
+ $revision = $this->getObject();
+
+ $view_data = $this->getViewData();
+ if (!$view_data) {
+ $view_data = array();
+ }
+
+ $left = idx($view_data, 'left');
+ $right = idx($view_data, 'right');
+
+ $diffs = id(new DifferentialDiffQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($left, $right))
+ ->execute();
+ $diffs = mpull($diffs, null, 'getID');
+ $left_diff = $diffs[$left];
+ $right_diff = $diffs[$right];
+
+ $old_ids = idx($view_data, 'old');
+ $new_ids = idx($view_data, 'new');
+ $old_ids = array_filter(explode(',', $old_ids));
+ $new_ids = array_filter(explode(',', $new_ids));
+
+ $type_inline = DifferentialTransaction::TYPE_INLINE;
+ $changeset_ids = array_merge($old_ids, $new_ids);
+ $inlines = array();
+ foreach ($xactions as $xaction) {
+ if ($xaction->getTransactionType() == $type_inline) {
+ $inlines[] = $xaction->getComment();
+ $changeset_ids[] = $xaction->getComment()->getChangesetID();
+ }
+ }
+
+ if ($changeset_ids) {
+ $changesets = id(new DifferentialChangesetQuery())
+ ->setViewer($viewer)
+ ->withIDs($changeset_ids)
+ ->execute();
+ $changesets = mpull($changesets, null, 'getID');
+ } else {
+ $changesets = array();
+ }
+
+ foreach ($inlines as $key => $inline) {
+ $inlines[$key] = DifferentialInlineComment::newFromModernComment(
+ $inline);
+ }
+
+ $query = id(new DifferentialInlineCommentQuery())
+ ->needHidden(true)
+ ->setViewer($viewer);
+
+ // NOTE: This is a bit sketchy: this method adjusts the inlines as a
+ // side effect, which means it will ultimately adjust the transaction
+ // comments and affect timeline rendering.
+ $query->adjustInlinesForChangesets(
+ $inlines,
+ array_select_keys($changesets, $old_ids),
+ array_select_keys($changesets, $new_ids),
+ $revision);
+
+ return id(new DifferentialTransactionView())
+ ->setViewData($view_data)
+ ->setChangesets($changesets)
+ ->setRevision($revision)
+ ->setLeftDiff($left_diff)
+ ->setRightDiff($right_diff);
+ }
+
+}
diff --git a/src/applications/differential/mail/DifferentialCreateMailReceiver.php b/src/applications/differential/mail/DifferentialCreateMailReceiver.php
index f5d9dc59f..a77277e98 100644
--- a/src/applications/differential/mail/DifferentialCreateMailReceiver.php
+++ b/src/applications/differential/mail/DifferentialCreateMailReceiver.php
@@ -1,124 +1,120 @@
<?php
-final class DifferentialCreateMailReceiver extends PhabricatorMailReceiver {
+final class DifferentialCreateMailReceiver
+ extends PhabricatorApplicationMailReceiver {
- public function isEnabled() {
- return PhabricatorApplication::isClassInstalled(
- 'PhabricatorDifferentialApplication');
- }
-
- public function canAcceptMail(PhabricatorMetaMTAReceivedMail $mail) {
- $differential_app = new PhabricatorDifferentialApplication();
- return $this->canAcceptApplicationMail($differential_app, $mail);
+ protected function newApplication() {
+ return new PhabricatorDifferentialApplication();
}
protected function processReceivedMail(
PhabricatorMetaMTAReceivedMail $mail,
- PhabricatorUser $sender) {
+ PhutilEmailAddress $target) {
+
+ $author = $this->getAuthor();
$attachments = $mail->getAttachments();
$files = array();
$errors = array();
if ($attachments) {
$files = id(new PhabricatorFileQuery())
- ->setViewer($sender)
+ ->setViewer($author)
->withPHIDs($attachments)
->execute();
foreach ($files as $index => $file) {
if ($file->getMimeType() != 'text/plain') {
$errors[] = pht(
'Could not parse file %s; only files with mimetype text/plain '.
'can be parsed via email.',
$file->getName());
unset($files[$index]);
}
}
}
$diffs = array();
foreach ($files as $file) {
$call = new ConduitCall(
'differential.createrawdiff',
array(
'diff' => $file->loadFileData(),
));
- $call->setUser($sender);
+ $call->setUser($author);
try {
$result = $call->execute();
$diffs[$file->getName()] = $result['uri'];
} catch (Exception $e) {
$errors[] = pht(
'Could not parse attachment %s; only attachments (and mail bodies) '.
'generated via "diff" commands can be parsed.',
$file->getName());
}
}
$body = $mail->getCleanTextBody();
if ($body) {
$call = new ConduitCall(
'differential.createrawdiff',
array(
'diff' => $body,
));
- $call->setUser($sender);
+ $call->setUser($author);
try {
$result = $call->execute();
$diffs[pht('Mail Body')] = $result['uri'];
} catch (Exception $e) {
$errors[] = pht(
'Could not parse mail body; only mail bodies (and attachments) '.
'generated via "diff" commands can be parsed.');
}
}
- $subject_prefix =
- PhabricatorEnv::getEnvConfig('metamta.differential.subject-prefix');
+ $subject_prefix = pht('[Differential]');
if (count($diffs)) {
$subject = pht(
'You successfully created %d diff(s).',
count($diffs));
} else {
$subject = pht(
'Diff creation failed; see body for %s error(s).',
phutil_count($errors));
}
$body = new PhabricatorMetaMTAMailBody();
$body->addRawSection($subject);
if (count($diffs)) {
$text_body = '';
$html_body = array();
$body_label = pht('%s DIFF LINK(S)', phutil_count($diffs));
foreach ($diffs as $filename => $diff_uri) {
$text_body .= $filename.': '.$diff_uri."\n";
$html_body[] = phutil_tag(
'a',
array(
'href' => $diff_uri,
),
$filename);
$html_body[] = phutil_tag('br');
}
$body->addTextSection($body_label, $text_body);
$body->addHTMLSection($body_label, $html_body);
}
if (count($errors)) {
$body_section = new PhabricatorMetaMTAMailSection();
$body_label = pht('%s ERROR(S)', phutil_count($errors));
foreach ($errors as $error) {
$body_section->addFragment($error);
}
$body->addTextSection($body_label, $body_section);
}
id(new PhabricatorMetaMTAMail())
- ->addTos(array($sender->getPHID()))
+ ->addTos(array($author->getPHID()))
->setSubject($subject)
->setSubjectPrefix($subject_prefix)
- ->setFrom($sender->getPHID())
+ ->setFrom($author->getPHID())
->setBody($body->render())
->saveAndSend();
}
}
diff --git a/src/applications/differential/mail/DifferentialRevisionMailReceiver.php b/src/applications/differential/mail/DifferentialRevisionMailReceiver.php
index 929ee7264..0fe8583a1 100644
--- a/src/applications/differential/mail/DifferentialRevisionMailReceiver.php
+++ b/src/applications/differential/mail/DifferentialRevisionMailReceiver.php
@@ -1,31 +1,31 @@
<?php
final class DifferentialRevisionMailReceiver
extends PhabricatorObjectMailReceiver {
public function isEnabled() {
return PhabricatorApplication::isClassInstalled(
'PhabricatorDifferentialApplication');
}
protected function getObjectPattern() {
return 'D[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
- $id = (int)trim($pattern, 'D');
+ $id = (int)substr($pattern, 1);
return id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withIDs(array($id))
->needReviewers(true)
->needReviewerAuthority(true)
->needActiveDiffs(true)
->executeOne();
}
protected function getTransactionReplyHandler() {
return new DifferentialReplyHandler();
}
}
diff --git a/src/applications/differential/storage/DifferentialDiff.php b/src/applications/differential/storage/DifferentialDiff.php
index 5f229f39b..e4c33dc76 100644
--- a/src/applications/differential/storage/DifferentialDiff.php
+++ b/src/applications/differential/storage/DifferentialDiff.php
@@ -1,821 +1,810 @@
<?php
final class DifferentialDiff
extends DifferentialDAO
implements
PhabricatorPolicyInterface,
PhabricatorExtendedPolicyInterface,
HarbormasterBuildableInterface,
HarbormasterCircleCIBuildableInterface,
HarbormasterBuildkiteBuildableInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorDestructibleInterface,
PhabricatorConduitResultInterface {
protected $revisionID;
protected $authorPHID;
protected $repositoryPHID;
protected $commitPHID;
protected $sourceMachine;
protected $sourcePath;
protected $sourceControlSystem;
protected $sourceControlBaseRevision;
protected $sourceControlPath;
protected $lintStatus;
protected $unitStatus;
protected $lineCount;
protected $branch;
protected $bookmark;
protected $creationMethod;
protected $repositoryUUID;
protected $description;
protected $viewPolicy;
private $unsavedChangesets = array();
private $changesets = self::ATTACHABLE;
private $revision = self::ATTACHABLE;
private $properties = array();
private $buildable = self::ATTACHABLE;
private $unitMessages = self::ATTACHABLE;
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'revisionID' => 'id?',
'authorPHID' => 'phid?',
'repositoryPHID' => 'phid?',
'sourceMachine' => 'text255?',
'sourcePath' => 'text255?',
'sourceControlSystem' => 'text64?',
'sourceControlBaseRevision' => 'text255?',
'sourceControlPath' => 'text255?',
'lintStatus' => 'uint32',
'unitStatus' => 'uint32',
'lineCount' => 'uint32',
'branch' => 'text255?',
'bookmark' => 'text255?',
'repositoryUUID' => 'text64?',
'commitPHID' => 'phid?',
// T6203/NULLABILITY
// These should be non-null; all diffs should have a creation method
// and the description should just be empty.
'creationMethod' => 'text255?',
'description' => 'text255?',
),
self::CONFIG_KEY_SCHEMA => array(
'revisionID' => array(
'columns' => array('revisionID'),
),
'key_commit' => array(
'columns' => array('commitPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
DifferentialDiffPHIDType::TYPECONST);
}
public function addUnsavedChangeset(DifferentialChangeset $changeset) {
if ($this->changesets === null) {
$this->changesets = array();
}
$this->unsavedChangesets[] = $changeset;
$this->changesets[] = $changeset;
return $this;
}
public function attachChangesets(array $changesets) {
assert_instances_of($changesets, 'DifferentialChangeset');
$this->changesets = $changesets;
return $this;
}
public function getChangesets() {
return $this->assertAttached($this->changesets);
}
public function loadChangesets() {
if (!$this->getID()) {
return array();
}
$changesets = id(new DifferentialChangeset())->loadAllWhere(
'diffID = %d',
$this->getID());
foreach ($changesets as $changeset) {
$changeset->attachDiff($this);
}
return $changesets;
}
public function save() {
$this->openTransaction();
$ret = parent::save();
foreach ($this->unsavedChangesets as $changeset) {
$changeset->setDiffID($this->getID());
$changeset->save();
}
$this->saveTransaction();
return $ret;
}
public static function initializeNewDiff(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorDifferentialApplication'))
->executeOne();
$view_policy = $app->getPolicy(
DifferentialDefaultViewCapability::CAPABILITY);
$diff = id(new DifferentialDiff())
->setViewPolicy($view_policy);
return $diff;
}
public static function newFromRawChanges(
PhabricatorUser $actor,
array $changes) {
assert_instances_of($changes, 'ArcanistDiffChange');
$diff = self::initializeNewDiff($actor);
return self::buildChangesetsFromRawChanges($diff, $changes);
}
public static function newEphemeralFromRawChanges(array $changes) {
assert_instances_of($changes, 'ArcanistDiffChange');
$diff = id(new DifferentialDiff())->makeEphemeral();
return self::buildChangesetsFromRawChanges($diff, $changes);
}
private static function buildChangesetsFromRawChanges(
DifferentialDiff $diff,
array $changes) {
// There may not be any changes; initialize the changesets list so that
// we don't throw later when accessing it.
$diff->attachChangesets(array());
$lines = 0;
foreach ($changes as $change) {
if ($change->getType() == ArcanistDiffChangeType::TYPE_MESSAGE) {
// If a user pastes a diff into Differential which includes a commit
// message (e.g., they ran `git show` to generate it), discard that
// change when constructing a DifferentialDiff.
continue;
}
$changeset = new DifferentialChangeset();
$add_lines = 0;
$del_lines = 0;
$first_line = PHP_INT_MAX;
$hunks = $change->getHunks();
if ($hunks) {
foreach ($hunks as $hunk) {
$dhunk = new DifferentialHunk();
$dhunk->setOldOffset($hunk->getOldOffset());
$dhunk->setOldLen($hunk->getOldLength());
$dhunk->setNewOffset($hunk->getNewOffset());
$dhunk->setNewLen($hunk->getNewLength());
$dhunk->setChanges($hunk->getCorpus());
$changeset->addUnsavedHunk($dhunk);
$add_lines += $hunk->getAddLines();
$del_lines += $hunk->getDelLines();
$added_lines = $hunk->getChangedLines('new');
if ($added_lines) {
$first_line = min($first_line, head_key($added_lines));
}
}
$lines += $add_lines + $del_lines;
} else {
// This happens when you add empty files.
$changeset->attachHunks(array());
}
$metadata = $change->getAllMetadata();
if ($first_line != PHP_INT_MAX) {
$metadata['line:first'] = $first_line;
}
$changeset->setOldFile($change->getOldPath());
$changeset->setFilename($change->getCurrentPath());
$changeset->setChangeType($change->getType());
$changeset->setFileType($change->getFileType());
$changeset->setMetadata($metadata);
$changeset->setOldProperties($change->getOldProperties());
$changeset->setNewProperties($change->getNewProperties());
$changeset->setAwayPaths($change->getAwayPaths());
$changeset->setAddLines($add_lines);
$changeset->setDelLines($del_lines);
$diff->addUnsavedChangeset($changeset);
}
$diff->setLineCount($lines);
$changesets = $diff->getChangesets();
id(new DifferentialChangesetEngine())
->rebuildChangesets($changesets);
return $diff;
}
public function getDiffDict() {
$dict = array(
'id' => $this->getID(),
'revisionID' => $this->getRevisionID(),
'dateCreated' => $this->getDateCreated(),
'dateModified' => $this->getDateModified(),
'sourceControlBaseRevision' => $this->getSourceControlBaseRevision(),
'sourceControlPath' => $this->getSourceControlPath(),
'sourceControlSystem' => $this->getSourceControlSystem(),
'branch' => $this->getBranch(),
'bookmark' => $this->getBookmark(),
'creationMethod' => $this->getCreationMethod(),
'description' => $this->getDescription(),
'unitStatus' => $this->getUnitStatus(),
'lintStatus' => $this->getLintStatus(),
'changes' => array(),
);
$dict['changes'] = $this->buildChangesList();
return $dict + $this->getDiffAuthorshipDict();
}
public function getDiffAuthorshipDict() {
$dict = array('properties' => array());
$properties = id(new DifferentialDiffProperty())->loadAllWhere(
'diffID = %d',
$this->getID());
foreach ($properties as $property) {
$dict['properties'][$property->getName()] = $property->getData();
if ($property->getName() == 'local:commits') {
foreach ($property->getData() as $commit) {
$dict['authorName'] = $commit['author'];
$dict['authorEmail'] = idx($commit, 'authorEmail');
break;
}
}
}
return $dict;
}
public function buildChangesList() {
$changes = array();
foreach ($this->getChangesets() as $changeset) {
$hunks = array();
foreach ($changeset->getHunks() as $hunk) {
$hunks[] = array(
'oldOffset' => $hunk->getOldOffset(),
'newOffset' => $hunk->getNewOffset(),
'oldLength' => $hunk->getOldLen(),
'newLength' => $hunk->getNewLen(),
'addLines' => null,
'delLines' => null,
'isMissingOldNewline' => null,
'isMissingNewNewline' => null,
'corpus' => $hunk->getChanges(),
);
}
$change = array(
'id' => $changeset->getID(),
'metadata' => $changeset->getMetadata(),
'oldPath' => $changeset->getOldFile(),
'currentPath' => $changeset->getFilename(),
'awayPaths' => $changeset->getAwayPaths(),
'oldProperties' => $changeset->getOldProperties(),
'newProperties' => $changeset->getNewProperties(),
'type' => $changeset->getChangeType(),
'fileType' => $changeset->getFileType(),
'commitHash' => null,
'addLines' => $changeset->getAddLines(),
'delLines' => $changeset->getDelLines(),
'hunks' => $hunks,
);
$changes[] = $change;
}
return $changes;
}
public function hasRevision() {
return $this->revision !== self::ATTACHABLE;
}
public function getRevision() {
return $this->assertAttached($this->revision);
}
public function attachRevision(DifferentialRevision $revision = null) {
$this->revision = $revision;
return $this;
}
public function attachProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getProperty($key) {
return $this->assertAttachedKey($this->properties, $key);
}
public function hasDiffProperty($key) {
$properties = $this->getDiffProperties();
return array_key_exists($key, $properties);
}
public function attachDiffProperties(array $properties) {
$this->properties = $properties;
return $this;
}
public function getDiffProperties() {
return $this->assertAttached($this->properties);
}
public function attachBuildable(HarbormasterBuildable $buildable = null) {
$this->buildable = $buildable;
return $this;
}
public function getBuildable() {
return $this->assertAttached($this->buildable);
}
public function getBuildTargetPHIDs() {
$buildable = $this->getBuildable();
if (!$buildable) {
return array();
}
$target_phids = array();
foreach ($buildable->getBuilds() as $build) {
foreach ($build->getBuildTargets() as $target) {
$target_phids[] = $target->getPHID();
}
}
return $target_phids;
}
public function loadCoverageMap(PhabricatorUser $viewer) {
$target_phids = $this->getBuildTargetPHIDs();
if (!$target_phids) {
return array();
}
$unit = id(new HarbormasterBuildUnitMessage())->loadAllWhere(
'buildTargetPHID IN (%Ls)',
$target_phids);
$map = array();
foreach ($unit as $message) {
$coverage = $message->getProperty('coverage', array());
foreach ($coverage as $path => $coverage_data) {
$map[$path][] = $coverage_data;
}
}
foreach ($map as $path => $coverage_items) {
$map[$path] = ArcanistUnitTestResult::mergeCoverage($coverage_items);
}
return $map;
}
public function getURI() {
$id = $this->getID();
return "/differential/diff/{$id}/";
}
public function attachUnitMessages(array $unit_messages) {
$this->unitMessages = $unit_messages;
return $this;
}
public function getUnitMessages() {
return $this->assertAttached($this->unitMessages);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
if ($this->hasRevision()) {
return PhabricatorPolicies::getMostOpenPolicy();
}
return $this->viewPolicy;
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($this->hasRevision()) {
return $this->getRevision()->hasAutomaticCapability($capability, $viewer);
}
return ($this->getAuthorPHID() == $viewer->getPHID());
}
public function describeAutomaticCapability($capability) {
if ($this->hasRevision()) {
return pht(
'This diff is attached to a revision, and inherits its policies.');
}
return pht('The author of a diff can see it.');
}
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
$extended = array();
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
if ($this->hasRevision()) {
$extended[] = array(
$this->getRevision(),
PhabricatorPolicyCapability::CAN_VIEW,
);
}
break;
}
return $extended;
}
/* -( HarbormasterBuildableInterface )------------------------------------- */
public function getHarbormasterBuildableDisplayPHID() {
$container_phid = $this->getHarbormasterContainerPHID();
if ($container_phid) {
return $container_phid;
}
return $this->getHarbormasterBuildablePHID();
}
public function getHarbormasterBuildablePHID() {
return $this->getPHID();
}
public function getHarbormasterContainerPHID() {
if ($this->getRevisionID()) {
$revision = id(new DifferentialRevision())->load($this->getRevisionID());
if ($revision) {
return $revision->getPHID();
}
}
return null;
}
public function getBuildVariables() {
$results = array();
$results['buildable.diff'] = $this->getID();
if ($this->revisionID) {
$revision = $this->getRevision();
$results['buildable.revision'] = $revision->getID();
$repo = $revision->getRepository();
if ($repo) {
$results['repository.callsign'] = $repo->getCallsign();
$results['repository.phid'] = $repo->getPHID();
$results['repository.vcs'] = $repo->getVersionControlSystem();
$results['repository.uri'] = $repo->getPublicCloneURI();
$results['repository.staging.uri'] = $repo->getStagingURI();
$results['repository.staging.ref'] = $this->getStagingRef();
}
}
return $results;
}
public function getAvailableBuildVariables() {
return array(
'buildable.diff' =>
pht('The differential diff ID, if applicable.'),
'buildable.revision' =>
pht('The differential revision ID, if applicable.'),
'repository.callsign' =>
pht('The callsign of the repository in Phabricator.'),
'repository.phid' =>
pht('The PHID of the repository in Phabricator.'),
'repository.vcs' =>
pht('The version control system, either "svn", "hg" or "git".'),
'repository.uri' =>
pht('The URI to clone or checkout the repository from.'),
'repository.staging.uri' =>
pht('The URI of the staging repository.'),
'repository.staging.ref' =>
pht('The ref name for this change in the staging repository.'),
);
}
public function newBuildableEngine() {
return new DifferentialBuildableEngine();
}
/* -( HarbormasterCircleCIBuildableInterface )----------------------------- */
public function getCircleCIGitHubRepositoryURI() {
$diff_phid = $this->getPHID();
$repository_phid = $this->getRepositoryPHID();
if (!$repository_phid) {
throw new Exception(
pht(
'This diff ("%s") is not associated with a repository. A diff '.
'must belong to a tracked repository to be built by CircleCI.',
$diff_phid));
}
$repository = id(new PhabricatorRepositoryQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($repository_phid))
->executeOne();
if (!$repository) {
throw new Exception(
pht(
'This diff ("%s") is associated with a repository ("%s") which '.
'could not be loaded.',
$diff_phid,
$repository_phid));
}
$staging_uri = $repository->getStagingURI();
if (!$staging_uri) {
throw new Exception(
pht(
'This diff ("%s") is associated with a repository ("%s") that '.
'does not have a Staging Area configured. You must configure a '.
'Staging Area to use CircleCI integration.',
$diff_phid,
$repository_phid));
}
$path = HarbormasterCircleCIBuildStepImplementation::getGitHubPath(
$staging_uri);
if (!$path) {
throw new Exception(
pht(
'This diff ("%s") is associated with a repository ("%s") that '.
'does not have a Staging Area ("%s") that is hosted on GitHub. '.
'CircleCI can only build from GitHub, so the Staging Area for '.
'the repository must be hosted there.',
$diff_phid,
$repository_phid,
$staging_uri));
}
return $staging_uri;
}
public function getCircleCIBuildIdentifierType() {
return 'tag';
}
public function getCircleCIBuildIdentifier() {
$ref = $this->getStagingRef();
$ref = preg_replace('(^refs/tags/)', '', $ref);
return $ref;
}
/* -( HarbormasterBuildkiteBuildableInterface )---------------------------- */
public function getBuildkiteBranch() {
$ref = $this->getStagingRef();
// NOTE: Circa late January 2017, Buildkite fails with the error message
// "Tags have been disabled for this project" if we pass the "refs/tags/"
// prefix via the API and the project doesn't have GitHub tag builds
// enabled, even if GitHub builds are disabled. The tag builds fine
// without this prefix.
$ref = preg_replace('(^refs/tags/)', '', $ref);
return $ref;
}
public function getBuildkiteCommit() {
return 'HEAD';
}
public function getStagingRef() {
// TODO: We're just hoping to get lucky. Instead, `arc` should store
// where it sent changes and we should only provide staging details
// if we reasonably believe they are accurate.
return 'refs/tags/phabricator/diff/'.$this->getID();
}
public function loadTargetBranch() {
// TODO: This is sketchy, but just eat the query cost until this can get
// cleaned up.
// For now, we're only returning a target if there's exactly one and it's
// a branch, since we don't support landing to more esoteric targets like
// tags yet.
$property = id(new DifferentialDiffProperty())->loadOneWhere(
'diffID = %d AND name = %s',
$this->getID(),
'arc:onto');
if (!$property) {
return null;
}
$data = $property->getData();
if (!$data) {
return null;
}
if (!is_array($data)) {
return null;
}
if (count($data) != 1) {
return null;
}
$onto = head($data);
if (!is_array($onto)) {
return null;
}
$type = idx($onto, 'type');
if ($type != 'branch') {
return null;
}
return idx($onto, 'name');
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new DifferentialDiffEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new DifferentialDiffTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
foreach ($this->loadChangesets() as $changeset) {
$engine->destroyObject($changeset);
}
$properties = id(new DifferentialDiffProperty())->loadAllWhere(
'diffID = %d',
$this->getID());
foreach ($properties as $prop) {
$prop->delete();
}
$this->saveTransaction();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('revisionPHID')
->setType('phid')
->setDescription(pht('Associated revision PHID.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('authorPHID')
->setType('phid')
->setDescription(pht('Revision author PHID.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('repositoryPHID')
->setType('phid')
->setDescription(pht('Associated repository PHID.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('refs')
->setType('map<string, wild>')
->setDescription(pht('List of related VCS references.')),
);
}
public function getFieldValuesForConduit() {
$refs = array();
$branch = $this->getBranch();
if (strlen($branch)) {
$refs[] = array(
'type' => 'branch',
'name' => $branch,
);
}
$onto = $this->loadTargetBranch();
if (strlen($onto)) {
$refs[] = array(
'type' => 'onto',
'name' => $onto,
);
}
$base = $this->getSourceControlBaseRevision();
if (strlen($base)) {
$refs[] = array(
'type' => 'base',
'identifier' => $base,
);
}
$bookmark = $this->getBookmark();
if (strlen($bookmark)) {
$refs[] = array(
'type' => 'bookmark',
'name' => $bookmark,
);
}
$revision_phid = null;
if ($this->getRevisionID()) {
$revision_phid = $this->getRevision()->getPHID();
}
return array(
'revisionPHID' => $revision_phid,
'authorPHID' => $this->getAuthorPHID(),
'repositoryPHID' => $this->getRepositoryPHID(),
'refs' => $refs,
);
}
public function getConduitSearchAttachments() {
return array(
id(new DifferentialCommitsSearchEngineAttachment())
->setAttachmentKey('commits'),
);
}
}
diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php
index 9b169f3c6..3397f9cb0 100644
--- a/src/applications/differential/storage/DifferentialRevision.php
+++ b/src/applications/differential/storage/DifferentialRevision.php
@@ -1,1209 +1,1148 @@
<?php
final class DifferentialRevision extends DifferentialDAO
implements
PhabricatorTokenReceiverInterface,
PhabricatorPolicyInterface,
PhabricatorExtendedPolicyInterface,
PhabricatorFlaggableInterface,
PhrequentTrackableInterface,
HarbormasterBuildableInterface,
PhabricatorSubscribableInterface,
PhabricatorCustomFieldInterface,
PhabricatorApplicationTransactionInterface,
+ PhabricatorTimelineInterface,
PhabricatorMentionableInterface,
PhabricatorDestructibleInterface,
PhabricatorProjectInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface,
PhabricatorConduitResultInterface,
PhabricatorDraftInterface {
protected $title = '';
protected $status;
protected $summary = '';
protected $testPlan = '';
protected $authorPHID;
protected $lastReviewerPHID;
protected $lineCount = 0;
protected $attached = array();
protected $mailKey;
protected $branchName;
protected $repositoryPHID;
protected $activeDiffPHID;
protected $viewPolicy = PhabricatorPolicies::POLICY_USER;
protected $editPolicy = PhabricatorPolicies::POLICY_USER;
protected $properties = array();
private $commits = self::ATTACHABLE;
private $activeDiff = self::ATTACHABLE;
private $diffIDs = self::ATTACHABLE;
private $hashes = self::ATTACHABLE;
private $repository = self::ATTACHABLE;
private $reviewerStatus = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
private $drafts = array();
private $flags = array();
private $forceMap = array();
const TABLE_COMMIT = 'differential_commit';
const RELATION_REVIEWER = 'revw';
const RELATION_SUBSCRIBED = 'subd';
const PROPERTY_CLOSED_FROM_ACCEPTED = 'wasAcceptedBeforeClose';
const PROPERTY_DRAFT_HOLD = 'draft.hold';
const PROPERTY_SHOULD_BROADCAST = 'draft.broadcast';
const PROPERTY_LINES_ADDED = 'lines.added';
const PROPERTY_LINES_REMOVED = 'lines.removed';
const PROPERTY_BUILDABLES = 'buildables';
public static function initializeNewRevision(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorDifferentialApplication'))
->executeOne();
$view_policy = $app->getPolicy(
DifferentialDefaultViewCapability::CAPABILITY);
if (PhabricatorEnv::getEnvConfig('phabricator.show-prototypes')) {
$initial_state = DifferentialRevisionStatus::DRAFT;
$should_broadcast = false;
} else {
$initial_state = DifferentialRevisionStatus::NEEDS_REVIEW;
$should_broadcast = true;
}
return id(new DifferentialRevision())
->setViewPolicy($view_policy)
->setAuthorPHID($actor->getPHID())
->attachRepository(null)
->attachActiveDiff(null)
->attachReviewers(array())
->setModernRevisionStatus($initial_state)
->setShouldBroadcast($should_broadcast);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'attached' => self::SERIALIZATION_JSON,
'unsubscribed' => self::SERIALIZATION_JSON,
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'title' => 'text255',
'status' => 'text32',
'summary' => 'text',
'testPlan' => 'text',
'authorPHID' => 'phid?',
'lastReviewerPHID' => 'phid?',
'lineCount' => 'uint32?',
'mailKey' => 'bytes40',
'branchName' => 'text255?',
'repositoryPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'authorPHID' => array(
'columns' => array('authorPHID', 'status'),
),
'repositoryPHID' => array(
'columns' => array('repositoryPHID'),
),
// If you (or a project you are a member of) is reviewing a significant
// fraction of the revisions on an install, the result set of open
// revisions may be smaller than the result set of revisions where you
// are a reviewer. In these cases, this key is better than keys on the
// edge table.
'key_status' => array(
'columns' => array('status', 'phid'),
),
),
) + parent::getConfiguration();
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function hasRevisionProperty($key) {
return array_key_exists($key, $this->properties);
}
public function getMonogram() {
$id = $this->getID();
return "D{$id}";
}
public function getURI() {
return '/'.$this->getMonogram();
}
public function loadIDsByCommitPHIDs($phids) {
if (!$phids) {
return array();
}
$revision_ids = queryfx_all(
$this->establishConnection('r'),
'SELECT * FROM %T WHERE commitPHID IN (%Ls)',
self::TABLE_COMMIT,
$phids);
return ipull($revision_ids, 'revisionID', 'commitPHID');
}
public function loadCommitPHIDs() {
if (!$this->getID()) {
return ($this->commits = array());
}
$commits = queryfx_all(
$this->establishConnection('r'),
'SELECT commitPHID FROM %T WHERE revisionID = %d',
self::TABLE_COMMIT,
$this->getID());
$commits = ipull($commits, 'commitPHID');
return ($this->commits = $commits);
}
public function getCommitPHIDs() {
return $this->assertAttached($this->commits);
}
public function getActiveDiff() {
// TODO: Because it's currently technically possible to create a revision
// without an associated diff, we allow an attached-but-null active diff.
// It would be good to get rid of this once we make diff-attaching
// transactional.
return $this->assertAttached($this->activeDiff);
}
public function attachActiveDiff($diff) {
$this->activeDiff = $diff;
return $this;
}
public function getDiffIDs() {
return $this->assertAttached($this->diffIDs);
}
public function attachDiffIDs(array $ids) {
rsort($ids);
$this->diffIDs = array_values($ids);
return $this;
}
public function attachCommitPHIDs(array $phids) {
$this->commits = array_values($phids);
return $this;
}
public function getAttachedPHIDs($type) {
return array_keys(idx($this->attached, $type, array()));
}
public function setAttachedPHIDs($type, array $phids) {
$this->attached[$type] = array_fill_keys($phids, array());
return $this;
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
DifferentialRevisionPHIDType::TYPECONST);
}
public function loadActiveDiff() {
return id(new DifferentialDiff())->loadOneWhere(
'revisionID = %d ORDER BY id DESC LIMIT 1',
$this->getID());
}
public function save() {
if (!$this->getMailKey()) {
$this->mailKey = Filesystem::readRandomCharacters(40);
}
return parent::save();
}
public function getHashes() {
return $this->assertAttached($this->hashes);
}
public function attachHashes(array $hashes) {
$this->hashes = $hashes;
return $this;
}
public function canReviewerForceAccept(
PhabricatorUser $viewer,
DifferentialReviewer $reviewer) {
if (!$reviewer->isPackage()) {
return false;
}
$map = $this->getReviewerForceAcceptMap($viewer);
if (!$map) {
return false;
}
if (isset($map[$reviewer->getReviewerPHID()])) {
return true;
}
return false;
}
private function getReviewerForceAcceptMap(PhabricatorUser $viewer) {
$fragment = $viewer->getCacheFragment();
if (!array_key_exists($fragment, $this->forceMap)) {
$map = $this->newReviewerForceAcceptMap($viewer);
$this->forceMap[$fragment] = $map;
}
return $this->forceMap[$fragment];
}
private function newReviewerForceAcceptMap(PhabricatorUser $viewer) {
$diff = $this->getActiveDiff();
if (!$diff) {
return null;
}
$repository_phid = $diff->getRepositoryPHID();
if (!$repository_phid) {
return null;
}
$paths = array();
try {
$changesets = $diff->getChangesets();
} catch (Exception $ex) {
$changesets = id(new DifferentialChangesetQuery())
->setViewer($viewer)
->withDiffs(array($diff))
->execute();
}
foreach ($changesets as $changeset) {
$paths[] = $changeset->getOwnersFilename();
}
if (!$paths) {
return null;
}
$reviewer_phids = array();
foreach ($this->getReviewers() as $reviewer) {
if (!$reviewer->isPackage()) {
continue;
}
$reviewer_phids[] = $reviewer->getReviewerPHID();
}
if (!$reviewer_phids) {
return null;
}
// Load all the reviewing packages which have control over some of the
// paths in the change. These are packages which the actor may be able
// to force-accept on behalf of.
$control_query = id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE))
->withPHIDs($reviewer_phids)
->withControl($repository_phid, $paths);
$control_packages = $control_query->execute();
if (!$control_packages) {
return null;
}
// Load all the packages which have potential control over some of the
// paths in the change and are owned by the actor. These are packages
// which the actor may be able to use their authority over to gain the
// ability to force-accept for other packages. This query doesn't apply
// dominion rules yet, and we'll bypass those rules later on.
$authority_query = id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE))
->withAuthorityPHIDs(array($viewer->getPHID()))
->withControl($repository_phid, $paths);
$authority_packages = $authority_query->execute();
if (!$authority_packages) {
return null;
}
$authority_packages = mpull($authority_packages, null, 'getPHID');
// Build a map from each path in the revision to the reviewer packages
// which control it.
$control_map = array();
foreach ($paths as $path) {
$control_packages = $control_query->getControllingPackagesForPath(
$repository_phid,
$path);
// Remove packages which the viewer has authority over. We don't need
// to check these for force-accept because they can just accept them
// normally.
$control_packages = mpull($control_packages, null, 'getPHID');
foreach ($control_packages as $phid => $control_package) {
if (isset($authority_packages[$phid])) {
unset($control_packages[$phid]);
}
}
if (!$control_packages) {
continue;
}
$control_map[$path] = $control_packages;
}
if (!$control_map) {
return null;
}
// From here on out, we only care about paths which we have at least one
// controlling package for.
$paths = array_keys($control_map);
// Now, build a map from each path to the packages which would control it
// if there were no dominion rules.
$authority_map = array();
foreach ($paths as $path) {
$authority_packages = $authority_query->getControllingPackagesForPath(
$repository_phid,
$path,
$ignore_dominion = true);
$authority_map[$path] = mpull($authority_packages, null, 'getPHID');
}
// For each path, find the most general package that the viewer has
// authority over. For example, we'll prefer a package that owns "/" to a
// package that owns "/src/".
$force_map = array();
foreach ($authority_map as $path => $package_map) {
$path_fragments = PhabricatorOwnersPackage::splitPath($path);
$fragment_count = count($path_fragments);
// Find the package that we have authority over which has the most
// general match for this path.
$best_match = null;
$best_package = null;
foreach ($package_map as $package_phid => $package) {
$package_paths = $package->getPathsForRepository($repository_phid);
foreach ($package_paths as $package_path) {
// NOTE: A strength of 0 means "no match". A strength of 1 means
// that we matched "/", so we can not possibly find another stronger
// match.
$strength = $package_path->getPathMatchStrength(
$path_fragments,
$fragment_count);
if (!$strength) {
continue;
}
if ($strength < $best_match || !$best_package) {
$best_match = $strength;
$best_package = $package;
if ($strength == 1) {
break 2;
}
}
}
}
if ($best_package) {
$force_map[$path] = array(
'strength' => $best_match,
'package' => $best_package,
);
}
}
// For each path which the viewer owns a package for, find other packages
// which that authority can be used to force-accept. Once we find a way to
// force-accept a package, we don't need to keep looking.
$has_control = array();
foreach ($force_map as $path => $spec) {
$path_fragments = PhabricatorOwnersPackage::splitPath($path);
$fragment_count = count($path_fragments);
$authority_strength = $spec['strength'];
$control_packages = $control_map[$path];
foreach ($control_packages as $control_phid => $control_package) {
if (isset($has_control[$control_phid])) {
continue;
}
$control_paths = $control_package->getPathsForRepository(
$repository_phid);
foreach ($control_paths as $control_path) {
$strength = $control_path->getPathMatchStrength(
$path_fragments,
$fragment_count);
if (!$strength) {
continue;
}
if ($strength > $authority_strength) {
$authority = $spec['package'];
$has_control[$control_phid] = array(
'authority' => $authority,
'phid' => $authority->getPHID(),
);
break;
}
}
}
}
// Return a map from packages which may be force accepted to the packages
// which permit that forced acceptance.
return ipull($has_control, 'phid');
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
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 $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
// A revision's author (which effectively means "owner" after we added
// commandeering) can always view and edit it.
$author_phid = $this->getAuthorPHID();
if ($author_phid) {
if ($user->getPHID() == $author_phid) {
return true;
}
}
return false;
}
public function describeAutomaticCapability($capability) {
$description = array(
pht('The owner of a revision can always view and edit it.'),
);
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$description[] = pht(
'If a revision belongs to a repository, other users must be able '.
'to view the repository in order to view the revision.');
break;
}
return $description;
}
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
$extended = array();
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$repository_phid = $this->getRepositoryPHID();
$repository = $this->getRepository();
// Try to use the object if we have it, since it will save us some
// data fetching later on. In some cases, we might not have it.
$repository_ref = nonempty($repository, $repository_phid);
if ($repository_ref) {
$extended[] = array(
$repository_ref,
PhabricatorPolicyCapability::CAN_VIEW,
);
}
break;
}
return $extended;
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getAuthorPHID(),
);
}
public function getReviewers() {
return $this->assertAttached($this->reviewerStatus);
}
public function attachReviewers(array $reviewers) {
assert_instances_of($reviewers, 'DifferentialReviewer');
$reviewers = mpull($reviewers, null, 'getReviewerPHID');
$this->reviewerStatus = $reviewers;
return $this;
}
public function hasAttachedReviewers() {
return ($this->reviewerStatus !== self::ATTACHABLE);
}
public function getReviewerPHIDs() {
$reviewers = $this->getReviewers();
return mpull($reviewers, 'getReviewerPHID');
}
public function getReviewerPHIDsForEdit() {
$reviewers = $this->getReviewers();
$status_blocking = DifferentialReviewerStatus::STATUS_BLOCKING;
$value = array();
foreach ($reviewers as $reviewer) {
$phid = $reviewer->getReviewerPHID();
if ($reviewer->getReviewerStatus() == $status_blocking) {
$value[] = 'blocking('.$phid.')';
} else {
$value[] = $phid;
}
}
return $value;
}
public function getRepository() {
return $this->assertAttached($this->repository);
}
public function attachRepository(PhabricatorRepository $repository = null) {
$this->repository = $repository;
return $this;
}
public function setModernRevisionStatus($status) {
return $this->setStatus($status);
}
public function getModernRevisionStatus() {
return $this->getStatus();
}
public function getLegacyRevisionStatus() {
return $this->getStatusObject()->getLegacyKey();
}
public function isClosed() {
return $this->getStatusObject()->isClosedStatus();
}
public function isAbandoned() {
return $this->getStatusObject()->isAbandoned();
}
public function isAccepted() {
return $this->getStatusObject()->isAccepted();
}
public function isNeedsReview() {
return $this->getStatusObject()->isNeedsReview();
}
public function isNeedsRevision() {
return $this->getStatusObject()->isNeedsRevision();
}
public function isChangePlanned() {
return $this->getStatusObject()->isChangePlanned();
}
public function isPublished() {
return $this->getStatusObject()->isPublished();
}
public function isDraft() {
return $this->getStatusObject()->isDraft();
}
public function getStatusIcon() {
return $this->getStatusObject()->getIcon();
}
public function getStatusDisplayName() {
return $this->getStatusObject()->getDisplayName();
}
public function getStatusIconColor() {
return $this->getStatusObject()->getIconColor();
}
public function getStatusTagColor() {
return $this->getStatusObject()->getTagColor();
}
public function getStatusObject() {
$status = $this->getStatus();
return DifferentialRevisionStatus::newForStatus($status);
}
public function getFlag(PhabricatorUser $viewer) {
return $this->assertAttachedKey($this->flags, $viewer->getPHID());
}
public function attachFlag(
PhabricatorUser $viewer,
PhabricatorFlag $flag = null) {
$this->flags[$viewer->getPHID()] = $flag;
return $this;
}
public function getHasDraft(PhabricatorUser $viewer) {
return $this->assertAttachedKey($this->drafts, $viewer->getCacheFragment());
}
public function attachHasDraft(PhabricatorUser $viewer, $has_draft) {
$this->drafts[$viewer->getCacheFragment()] = $has_draft;
return $this;
}
public function getHoldAsDraft() {
return $this->getProperty(self::PROPERTY_DRAFT_HOLD, false);
}
public function setHoldAsDraft($hold) {
return $this->setProperty(self::PROPERTY_DRAFT_HOLD, $hold);
}
public function getShouldBroadcast() {
return $this->getProperty(self::PROPERTY_SHOULD_BROADCAST, true);
}
public function setShouldBroadcast($should_broadcast) {
return $this->setProperty(
self::PROPERTY_SHOULD_BROADCAST,
$should_broadcast);
}
public function setAddedLineCount($count) {
return $this->setProperty(self::PROPERTY_LINES_ADDED, $count);
}
public function getAddedLineCount() {
return $this->getProperty(self::PROPERTY_LINES_ADDED);
}
public function setRemovedLineCount($count) {
return $this->setProperty(self::PROPERTY_LINES_REMOVED, $count);
}
public function getRemovedLineCount() {
return $this->getProperty(self::PROPERTY_LINES_REMOVED);
}
public function hasLineCounts() {
// This data was not populated on older revisions, so it may not be
// present on all revisions.
return isset($this->properties[self::PROPERTY_LINES_ADDED]);
}
public function getRevisionScaleGlyphs() {
$add = $this->getAddedLineCount();
$rem = $this->getRemovedLineCount();
$all = ($add + $rem);
if (!$all) {
return ' ';
}
$map = array(
20 => 2,
50 => 3,
150 => 4,
375 => 5,
1000 => 6,
2500 => 7,
);
$n = 1;
foreach ($map as $size => $count) {
if ($size <= $all) {
$n = $count;
} else {
break;
}
}
$add_n = (int)ceil(($add / $all) * $n);
$rem_n = (int)ceil(($rem / $all) * $n);
while ($add_n + $rem_n > $n) {
if ($add_n > 1) {
$add_n--;
} else {
$rem_n--;
}
}
return
str_repeat('+', $add_n).
str_repeat('-', $rem_n).
str_repeat(' ', (7 - $n));
}
public function getBuildableStatus($phid) {
$buildables = $this->getProperty(self::PROPERTY_BUILDABLES);
if (!is_array($buildables)) {
$buildables = array();
}
$buildable = idx($buildables, $phid);
if (!is_array($buildable)) {
$buildable = array();
}
return idx($buildable, 'status');
}
public function setBuildableStatus($phid, $status) {
$buildables = $this->getProperty(self::PROPERTY_BUILDABLES);
if (!is_array($buildables)) {
$buildables = array();
}
$buildable = idx($buildables, $phid);
if (!is_array($buildable)) {
$buildable = array();
}
$buildable['status'] = $status;
$buildables[$phid] = $buildable;
return $this->setProperty(self::PROPERTY_BUILDABLES, $buildables);
}
public function newBuildableStatus(PhabricatorUser $viewer, $phid) {
// For Differential, we're ignoring autobuilds (local lint and unit)
// when computing build status. Differential only cares about remote
// builds when making publishing and undrafting decisions.
$builds = $this->loadImpactfulBuildsForBuildablePHIDs(
$viewer,
array($phid));
return $this->newBuildableStatusForBuilds($builds);
}
public function newBuildableStatusForBuilds(array $builds) {
// If we have nothing but passing builds, the buildable passes.
if (!$builds) {
return HarbormasterBuildableStatus::STATUS_PASSED;
}
// If we have any completed, non-passing builds, the buildable fails.
foreach ($builds as $build) {
if ($build->isComplete()) {
return HarbormasterBuildableStatus::STATUS_FAILED;
}
}
// Otherwise, we're still waiting for the build to pass or fail.
return null;
}
public function loadImpactfulBuilds(PhabricatorUser $viewer) {
$diff = $this->getActiveDiff();
// NOTE: We can't use `withContainerPHIDs()` here because the container
// update in Harbormaster is not synchronous.
$buildables = id(new HarbormasterBuildableQuery())
->setViewer($viewer)
->withBuildablePHIDs(array($diff->getPHID()))
->withManualBuildables(false)
->execute();
if (!$buildables) {
return array();
}
return $this->loadImpactfulBuildsForBuildablePHIDs(
$viewer,
mpull($buildables, 'getPHID'));
}
private function loadImpactfulBuildsForBuildablePHIDs(
PhabricatorUser $viewer,
array $phids) {
return id(new HarbormasterBuildQuery())
->setViewer($viewer)
->withBuildablePHIDs($phids)
->withAutobuilds(false)
->withBuildStatuses(
array(
HarbormasterBuildStatus::STATUS_INACTIVE,
HarbormasterBuildStatus::STATUS_PENDING,
HarbormasterBuildStatus::STATUS_BUILDING,
HarbormasterBuildStatus::STATUS_FAILED,
HarbormasterBuildStatus::STATUS_ABORTED,
HarbormasterBuildStatus::STATUS_ERROR,
HarbormasterBuildStatus::STATUS_PAUSED,
HarbormasterBuildStatus::STATUS_DEADLOCKED,
))
->execute();
}
/* -( HarbormasterBuildableInterface )------------------------------------- */
public function getHarbormasterBuildableDisplayPHID() {
return $this->getHarbormasterContainerPHID();
}
public function getHarbormasterBuildablePHID() {
return $this->loadActiveDiff()->getPHID();
}
public function getHarbormasterContainerPHID() {
return $this->getPHID();
}
public function getBuildVariables() {
return array();
}
public function getAvailableBuildVariables() {
return array();
}
public function newBuildableEngine() {
return new DifferentialBuildableEngine();
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
if ($phid == $this->getAuthorPHID()) {
return true;
}
// TODO: This only happens when adding or removing CCs, and is safe from a
// policy perspective, but the subscription pathway should have some
// opportunity to load this data properly. For now, this is the only case
// where implicit subscription is not an intrinsic property of the object.
if ($this->reviewerStatus == self::ATTACHABLE) {
$reviewers = id(new DifferentialRevisionQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($this->getPHID()))
->needReviewers(true)
->executeOne()
->getReviewers();
} else {
$reviewers = $this->getReviewers();
}
foreach ($reviewers as $reviewer) {
if ($reviewer->getReviewerPHID() !== $phid) {
continue;
}
if ($reviewer->isResigned()) {
continue;
}
return true;
}
return false;
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('differential.fields');
}
public function getCustomFieldBaseClass() {
return 'DifferentialCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new DifferentialTransactionEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new DifferentialTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
- $viewer = $request->getViewer();
-
- $render_data = $timeline->getRenderData();
- $left = $request->getInt('left', idx($render_data, 'left'));
- $right = $request->getInt('right', idx($render_data, 'right'));
-
- $diffs = id(new DifferentialDiffQuery())
- ->setViewer($request->getUser())
- ->withIDs(array($left, $right))
- ->execute();
- $diffs = mpull($diffs, null, 'getID');
- $left_diff = $diffs[$left];
- $right_diff = $diffs[$right];
-
- $old_ids = $request->getStr('old', idx($render_data, 'old'));
- $new_ids = $request->getStr('new', idx($render_data, 'new'));
- $old_ids = array_filter(explode(',', $old_ids));
- $new_ids = array_filter(explode(',', $new_ids));
-
- $type_inline = DifferentialTransaction::TYPE_INLINE;
- $changeset_ids = array_merge($old_ids, $new_ids);
- $inlines = array();
- foreach ($timeline->getTransactions() as $xaction) {
- if ($xaction->getTransactionType() == $type_inline) {
- $inlines[] = $xaction->getComment();
- $changeset_ids[] = $xaction->getComment()->getChangesetID();
- }
- }
-
- if ($changeset_ids) {
- $changesets = id(new DifferentialChangesetQuery())
- ->setViewer($request->getUser())
- ->withIDs($changeset_ids)
- ->execute();
- $changesets = mpull($changesets, null, 'getID');
- } else {
- $changesets = array();
- }
-
- foreach ($inlines as $key => $inline) {
- $inlines[$key] = DifferentialInlineComment::newFromModernComment(
- $inline);
- }
-
- $query = id(new DifferentialInlineCommentQuery())
- ->needHidden(true)
- ->setViewer($viewer);
-
- // NOTE: This is a bit sketchy: this method adjusts the inlines as a
- // side effect, which means it will ultimately adjust the transaction
- // comments and affect timeline rendering.
- $query->adjustInlinesForChangesets(
- $inlines,
- array_select_keys($changesets, $old_ids),
- array_select_keys($changesets, $new_ids),
- $this);
-
- return $timeline
- ->setChangesets($changesets)
- ->setRevision($this)
- ->setLeftDiff($left_diff)
- ->setRightDiff($right_diff);
- }
-
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$diffs = id(new DifferentialDiffQuery())
->setViewer($engine->getViewer())
->withRevisionIDs(array($this->getID()))
->execute();
foreach ($diffs as $diff) {
$engine->destroyObject($diff);
}
$conn_w = $this->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE revisionID = %d',
self::TABLE_COMMIT,
$this->getID());
// we have to do paths a little differently as they do not have
// an id or phid column for delete() to act on
$dummy_path = new DifferentialAffectedPath();
queryfx(
$conn_w,
'DELETE FROM %T WHERE revisionID = %d',
$dummy_path->getTableName(),
$this->getID());
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new DifferentialRevisionFulltextEngine();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new DifferentialRevisionFerretEngine();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('title')
->setType('string')
->setDescription(pht('The revision title.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('authorPHID')
->setType('phid')
->setDescription(pht('Revision author PHID.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('status')
->setType('map<string, wild>')
->setDescription(pht('Information about revision status.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('repositoryPHID')
->setType('phid?')
->setDescription(pht('Revision repository PHID.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('diffPHID')
->setType('phid')
->setDescription(pht('Active diff PHID.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('summary')
->setType('string')
->setDescription(pht('Revision summary.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('testPlan')
->setType('string')
->setDescription(pht('Revision test plan.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('isDraft')
->setType('bool')
->setDescription(
pht(
'True if this revision is in any draft state, and thus not '.
'notifying reviewers and subscribers about changes.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('holdAsDraft')
->setType('bool')
->setDescription(
pht(
'True if this revision is being held as a draft. It will not be '.
'automatically submitted for review even if tests pass.')),
);
}
public function getFieldValuesForConduit() {
$status = $this->getStatusObject();
$status_info = array(
'value' => $status->getKey(),
'name' => $status->getDisplayName(),
'closed' => $status->isClosedStatus(),
'color.ansi' => $status->getANSIColor(),
);
return array(
'title' => $this->getTitle(),
'authorPHID' => $this->getAuthorPHID(),
'status' => $status_info,
'repositoryPHID' => $this->getRepositoryPHID(),
'diffPHID' => $this->getActiveDiffPHID(),
'summary' => $this->getSummary(),
'testPlan' => $this->getTestPlan(),
'isDraft' => !$this->getShouldBroadcast(),
'holdAsDraft' => (bool)$this->getHoldAsDraft(),
);
}
public function getConduitSearchAttachments() {
return array(
id(new DifferentialReviewersSearchEngineAttachment())
->setAttachmentKey('reviewers'),
);
}
/* -( PhabricatorDraftInterface )------------------------------------------ */
public function newDraftEngine() {
return new DifferentialRevisionDraftEngine();
}
+
+/* -( PhabricatorTimelineInterface )--------------------------------------- */
+
+
+ public function newTimelineEngine() {
+ return new DifferentialRevisionTimelineEngine();
+ }
+
+
}
diff --git a/src/applications/differential/storage/DifferentialTransaction.php b/src/applications/differential/storage/DifferentialTransaction.php
index 53fdc71a1..c49e40f98 100644
--- a/src/applications/differential/storage/DifferentialTransaction.php
+++ b/src/applications/differential/storage/DifferentialTransaction.php
@@ -1,586 +1,582 @@
<?php
final class DifferentialTransaction
extends PhabricatorModularTransaction {
private $isCommandeerSideEffect;
const TYPE_INLINE = 'differential:inline';
const TYPE_ACTION = 'differential:action';
const MAILTAG_REVIEWERS = 'differential-reviewers';
const MAILTAG_CLOSED = 'differential-committed';
const MAILTAG_CC = 'differential-cc';
const MAILTAG_COMMENT = 'differential-comment';
const MAILTAG_UPDATED = 'differential-updated';
const MAILTAG_REVIEW_REQUEST = 'differential-review-request';
const MAILTAG_OTHER = 'differential-other';
public function getBaseTransactionClass() {
return 'DifferentialRevisionTransactionType';
}
protected function newFallbackModularTransactionType() {
// TODO: This allows us to render modern strings for older transactions
// without doing a migration. At some point, we should do a migration and
// throw this away.
// NOTE: Old reviewer edits are raw edge transactions. They could be
// migrated to modular transactions when the rest of this migrates.
$xaction_type = $this->getTransactionType();
if ($xaction_type == PhabricatorTransactions::TYPE_CUSTOMFIELD) {
switch ($this->getMetadataValue('customfield:key')) {
case 'differential:title':
return new DifferentialRevisionTitleTransaction();
case 'differential:test-plan':
return new DifferentialRevisionTestPlanTransaction();
case 'differential:repository':
return new DifferentialRevisionRepositoryTransaction();
}
}
return parent::newFallbackModularTransactionType();
}
public function setIsCommandeerSideEffect($is_side_effect) {
$this->isCommandeerSideEffect = $is_side_effect;
return $this;
}
public function getIsCommandeerSideEffect() {
return $this->isCommandeerSideEffect;
}
public function getApplicationName() {
return 'differential';
}
public function getApplicationTransactionType() {
return DifferentialRevisionPHIDType::TYPECONST;
}
public function getApplicationTransactionCommentObject() {
return new DifferentialTransactionComment();
}
- public function getApplicationTransactionViewObject() {
- return new DifferentialTransactionView();
- }
-
public function shouldHide() {
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case DifferentialRevisionRequestReviewTransaction::TRANSACTIONTYPE:
// Don't hide the initial "X requested review: ..." transaction from
// mail or feed even when it occurs during creation. We need this
// transaction to survive so we'll generate mail and feed stories when
// revisions immediately leave the draft state. See T13035 for
// discussion.
return false;
}
return parent::shouldHide();
}
public function shouldHideForMail(array $xactions) {
switch ($this->getTransactionType()) {
case DifferentialRevisionReviewersTransaction::TRANSACTIONTYPE:
// Don't hide the initial "X added reviewers: ..." transaction during
// object creation from mail. See T12118 and PHI54.
return false;
}
return parent::shouldHideForMail($xactions);
}
public function isInlineCommentTransaction() {
switch ($this->getTransactionType()) {
case self::TYPE_INLINE:
return true;
}
return parent::isInlineCommentTransaction();
}
public function getRequiredHandlePHIDs() {
$phids = parent::getRequiredHandlePHIDs();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_ACTION:
if ($new == DifferentialAction::ACTION_CLOSE &&
$this->getMetadataValue('isCommitClose')) {
$phids[] = $this->getMetadataValue('commitPHID');
if ($this->getMetadataValue('committerPHID')) {
$phids[] = $this->getMetadataValue('committerPHID');
}
if ($this->getMetadataValue('authorPHID')) {
$phids[] = $this->getMetadataValue('authorPHID');
}
}
break;
}
return $phids;
}
public function getActionStrength() {
switch ($this->getTransactionType()) {
case self::TYPE_ACTION:
return 3;
}
return parent::getActionStrength();
}
public function getActionName() {
switch ($this->getTransactionType()) {
case self::TYPE_INLINE:
return pht('Commented On');
case self::TYPE_ACTION:
$map = array(
DifferentialAction::ACTION_ACCEPT => pht('Accepted'),
DifferentialAction::ACTION_REJECT => pht('Requested Changes To'),
DifferentialAction::ACTION_RETHINK => pht('Planned Changes To'),
DifferentialAction::ACTION_ABANDON => pht('Abandoned'),
DifferentialAction::ACTION_CLOSE => pht('Closed'),
DifferentialAction::ACTION_REQUEST => pht('Requested A Review Of'),
DifferentialAction::ACTION_RESIGN => pht('Resigned From'),
DifferentialAction::ACTION_ADDREVIEWERS => pht('Added Reviewers'),
DifferentialAction::ACTION_CLAIM => pht('Commandeered'),
DifferentialAction::ACTION_REOPEN => pht('Reopened'),
);
$name = idx($map, $this->getNewValue());
if ($name !== null) {
return $name;
}
break;
}
return parent::getActionName();
}
public function getMailTags() {
$tags = array();
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_SUBSCRIBERS;
$tags[] = self::MAILTAG_CC;
break;
case self::TYPE_ACTION:
switch ($this->getNewValue()) {
case DifferentialAction::ACTION_CLOSE:
$tags[] = self::MAILTAG_CLOSED;
break;
}
break;
case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE:
$old = $this->getOldValue();
if ($old === null) {
$tags[] = self::MAILTAG_REVIEW_REQUEST;
} else {
$tags[] = self::MAILTAG_UPDATED;
}
break;
case PhabricatorTransactions::TYPE_COMMENT:
case self::TYPE_INLINE:
$tags[] = self::MAILTAG_COMMENT;
break;
case DifferentialRevisionReviewersTransaction::TRANSACTIONTYPE:
$tags[] = self::MAILTAG_REVIEWERS;
break;
case DifferentialRevisionCloseTransaction::TRANSACTIONTYPE:
$tags[] = self::MAILTAG_CLOSED;
break;
}
if (!$tags) {
$tags[] = self::MAILTAG_OTHER;
}
return $tags;
}
public function getTitle() {
$author_phid = $this->getAuthorPHID();
$author_handle = $this->renderHandleLink($author_phid);
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_INLINE:
return pht(
'%s added inline comments.',
$author_handle);
case self::TYPE_ACTION:
switch ($new) {
case DifferentialAction::ACTION_CLOSE:
if (!$this->getMetadataValue('isCommitClose')) {
return DifferentialAction::getBasicStoryText(
$new,
$author_handle);
}
$commit_name = $this->renderHandleLink(
$this->getMetadataValue('commitPHID'));
$committer_phid = $this->getMetadataValue('committerPHID');
$author_phid = $this->getMetadataValue('authorPHID');
if ($this->getHandleIfExists($committer_phid)) {
$committer_name = $this->renderHandleLink($committer_phid);
} else {
$committer_name = $this->getMetadataValue('committerName');
}
if ($this->getHandleIfExists($author_phid)) {
$author_name = $this->renderHandleLink($author_phid);
} else {
$author_name = $this->getMetadataValue('authorName');
}
if ($committer_name && ($committer_name != $author_name)) {
return pht(
'Closed by commit %s (authored by %s, committed by %s).',
$commit_name,
$author_name,
$committer_name);
} else {
return pht(
'Closed by commit %s (authored by %s).',
$commit_name,
$author_name);
}
break;
default:
return DifferentialAction::getBasicStoryText($new, $author_handle);
}
break;
}
return parent::getTitle();
}
public function renderExtraInformationLink() {
if ($this->getMetadataValue('revisionMatchData')) {
$details_href =
'/differential/revision/closedetails/'.$this->getPHID().'/';
$details_link = javelin_tag(
'a',
array(
'href' => $details_href,
'sigil' => 'workflow',
),
pht('Explain Why'));
return $details_link;
}
return parent::renderExtraInformationLink();
}
public function getTitleForFeed() {
$author_phid = $this->getAuthorPHID();
$object_phid = $this->getObjectPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
$author_link = $this->renderHandleLink($author_phid);
$object_link = $this->renderHandleLink($object_phid);
switch ($this->getTransactionType()) {
case self::TYPE_INLINE:
return pht(
'%s added inline comments to %s.',
$author_link,
$object_link);
case self::TYPE_ACTION:
switch ($new) {
case DifferentialAction::ACTION_ACCEPT:
return pht(
'%s accepted %s.',
$author_link,
$object_link);
case DifferentialAction::ACTION_REJECT:
return pht(
'%s requested changes to %s.',
$author_link,
$object_link);
case DifferentialAction::ACTION_RETHINK:
return pht(
'%s planned changes to %s.',
$author_link,
$object_link);
case DifferentialAction::ACTION_ABANDON:
return pht(
'%s abandoned %s.',
$author_link,
$object_link);
case DifferentialAction::ACTION_CLOSE:
if (!$this->getMetadataValue('isCommitClose')) {
return pht(
'%s closed %s.',
$author_link,
$object_link);
} else {
$commit_name = $this->renderHandleLink(
$this->getMetadataValue('commitPHID'));
$committer_phid = $this->getMetadataValue('committerPHID');
$author_phid = $this->getMetadataValue('authorPHID');
if ($this->getHandleIfExists($committer_phid)) {
$committer_name = $this->renderHandleLink($committer_phid);
} else {
$committer_name = $this->getMetadataValue('committerName');
}
if ($this->getHandleIfExists($author_phid)) {
$author_name = $this->renderHandleLink($author_phid);
} else {
$author_name = $this->getMetadataValue('authorName');
}
// Check if the committer and author are the same. They're the
// same if both resolved and are the same user, or if neither
// resolved and the text is identical.
if ($committer_phid && $author_phid) {
$same_author = ($committer_phid == $author_phid);
} else if (!$committer_phid && !$author_phid) {
$same_author = ($committer_name == $author_name);
} else {
$same_author = false;
}
if ($committer_name && !$same_author) {
return pht(
'%s closed %s by committing %s (authored by %s).',
$author_link,
$object_link,
$commit_name,
$author_name);
} else {
return pht(
'%s closed %s by committing %s.',
$author_link,
$object_link,
$commit_name);
}
}
break;
case DifferentialAction::ACTION_REQUEST:
return pht(
'%s requested review of %s.',
$author_link,
$object_link);
case DifferentialAction::ACTION_RECLAIM:
return pht(
'%s reclaimed %s.',
$author_link,
$object_link);
case DifferentialAction::ACTION_RESIGN:
return pht(
'%s resigned from %s.',
$author_link,
$object_link);
case DifferentialAction::ACTION_CLAIM:
return pht(
'%s commandeered %s.',
$author_link,
$object_link);
case DifferentialAction::ACTION_REOPEN:
return pht(
'%s reopened %s.',
$author_link,
$object_link);
}
break;
}
return parent::getTitleForFeed();
}
public function getIcon() {
switch ($this->getTransactionType()) {
case self::TYPE_INLINE:
return 'fa-comment';
case self::TYPE_ACTION:
switch ($this->getNewValue()) {
case DifferentialAction::ACTION_CLOSE:
return 'fa-check';
case DifferentialAction::ACTION_ACCEPT:
return 'fa-check-circle-o';
case DifferentialAction::ACTION_REJECT:
return 'fa-times-circle-o';
case DifferentialAction::ACTION_ABANDON:
return 'fa-plane';
case DifferentialAction::ACTION_RETHINK:
return 'fa-headphones';
case DifferentialAction::ACTION_REQUEST:
return 'fa-refresh';
case DifferentialAction::ACTION_RECLAIM:
case DifferentialAction::ACTION_REOPEN:
return 'fa-bullhorn';
case DifferentialAction::ACTION_RESIGN:
return 'fa-flag';
case DifferentialAction::ACTION_CLAIM:
return 'fa-flag';
}
case PhabricatorTransactions::TYPE_EDGE:
switch ($this->getMetadataValue('edge:type')) {
case DifferentialRevisionHasReviewerEdgeType::EDGECONST:
return 'fa-user';
}
}
return parent::getIcon();
}
public function shouldDisplayGroupWith(array $group) {
// Never group status changes with other types of actions, they're indirect
// and don't make sense when combined with direct actions.
if ($this->isStatusTransaction($this)) {
return false;
}
foreach ($group as $xaction) {
if ($this->isStatusTransaction($xaction)) {
return false;
}
}
return parent::shouldDisplayGroupWith($group);
}
private function isStatusTransaction($xaction) {
$status_type = DifferentialRevisionStatusTransaction::TRANSACTIONTYPE;
if ($xaction->getTransactionType() == $status_type) {
return true;
}
return false;
}
public function getColor() {
switch ($this->getTransactionType()) {
case self::TYPE_ACTION:
switch ($this->getNewValue()) {
case DifferentialAction::ACTION_CLOSE:
return PhabricatorTransactions::COLOR_INDIGO;
case DifferentialAction::ACTION_ACCEPT:
return PhabricatorTransactions::COLOR_GREEN;
case DifferentialAction::ACTION_REJECT:
return PhabricatorTransactions::COLOR_RED;
case DifferentialAction::ACTION_ABANDON:
return PhabricatorTransactions::COLOR_INDIGO;
case DifferentialAction::ACTION_RETHINK:
return PhabricatorTransactions::COLOR_RED;
case DifferentialAction::ACTION_REQUEST:
return PhabricatorTransactions::COLOR_SKY;
case DifferentialAction::ACTION_RECLAIM:
return PhabricatorTransactions::COLOR_SKY;
case DifferentialAction::ACTION_REOPEN:
return PhabricatorTransactions::COLOR_SKY;
case DifferentialAction::ACTION_RESIGN:
return PhabricatorTransactions::COLOR_ORANGE;
case DifferentialAction::ACTION_CLAIM:
return PhabricatorTransactions::COLOR_YELLOW;
}
}
return parent::getColor();
}
public function getNoEffectDescription() {
switch ($this->getTransactionType()) {
case self::TYPE_ACTION:
switch ($this->getNewValue()) {
case DifferentialAction::ACTION_CLOSE:
return pht('This revision is already closed.');
case DifferentialAction::ACTION_ABANDON:
return pht('This revision has already been abandoned.');
case DifferentialAction::ACTION_RECLAIM:
return pht(
'You can not reclaim this revision because his revision is '.
'not abandoned.');
case DifferentialAction::ACTION_REOPEN:
return pht(
'You can not reopen this revision because this revision is '.
'not closed.');
case DifferentialAction::ACTION_RETHINK:
return pht('This revision already requires changes.');
case DifferentialAction::ACTION_CLAIM:
return pht(
'You can not commandeer this revision because you already own '.
'it.');
}
break;
}
return parent::getNoEffectDescription();
}
public function renderAsTextForDoorkeeper(
DoorkeeperFeedStoryPublisher $publisher,
PhabricatorFeedStory $story,
array $xactions) {
$body = parent::renderAsTextForDoorkeeper($publisher, $story, $xactions);
$inlines = array();
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == self::TYPE_INLINE) {
$inlines[] = $xaction;
}
}
// TODO: This is a bit gross, but far less bad than it used to be. It
// could be further cleaned up at some point.
if ($inlines) {
$engine = PhabricatorMarkupEngine::newMarkupEngine(array())
->setConfig('viewer', new PhabricatorUser())
->setMode(PhutilRemarkupEngine::MODE_TEXT);
$body .= "\n\n";
$body .= pht('Inline Comments');
$body .= "\n";
$changeset_ids = array();
foreach ($inlines as $inline) {
$changeset_ids[] = $inline->getComment()->getChangesetID();
}
$changesets = id(new DifferentialChangeset())->loadAllWhere(
'id IN (%Ld)',
$changeset_ids);
foreach ($inlines as $inline) {
$comment = $inline->getComment();
$changeset = idx($changesets, $comment->getChangesetID());
if (!$changeset) {
continue;
}
$filename = $changeset->getDisplayFilename();
$linenumber = $comment->getLineNumber();
$inline_text = $engine->markupText($comment->getContent());
$inline_text = rtrim($inline_text);
$body .= "{$filename}:{$linenumber} {$inline_text}\n";
}
}
return $body;
}
}
diff --git a/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php b/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php
index d15fb678d..a0c2277f6 100644
--- a/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php
+++ b/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php
@@ -1,165 +1,160 @@
<?php
final class PhabricatorDiffusionConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Diffusion');
}
public function getDescription() {
return pht('Configure Diffusion repository browsing.');
}
public function getIcon() {
return 'fa-code';
}
public function getGroup() {
return 'apps';
}
public function getOptions() {
$custom_field_type = 'custom:PhabricatorCustomFieldConfigOptionType';
$fields = array(
new PhabricatorCommitRepositoryField(),
new PhabricatorCommitBranchesField(),
new PhabricatorCommitTagsField(),
new PhabricatorCommitMergedCommitsField(),
);
$default_fields = array();
foreach ($fields as $field) {
$default_fields[$field->getFieldKey()] = array(
'disabled' => $field->shouldDisableByDefault(),
);
}
return array(
- $this->newOption(
- 'metamta.diffusion.subject-prefix',
- 'string',
- '[Diffusion]')
- ->setDescription(pht('Subject prefix for Diffusion mail.')),
$this->newOption(
'metamta.diffusion.attach-patches',
'bool',
false)
->setBoolOptions(
array(
pht('Attach Patches'),
pht('Do Not Attach Patches'),
))
->setDescription(
pht(
'Set this to true if you want patches to be attached to commit '.
'notifications from Diffusion.')),
$this->newOption('metamta.diffusion.inline-patches', 'int', 0)
->setSummary(pht('Include patches in Diffusion mail as body text.'))
->setDescription(
pht(
'To include patches in Diffusion email bodies, set this to a '.
'positive integer. Patches will be inlined if they are at most '.
'that many lines. By default, patches are not inlined.')),
$this->newOption('metamta.diffusion.byte-limit', 'int', 1024 * 1024)
->setDescription(pht('Hard byte limit on including patches in email.')),
$this->newOption('metamta.diffusion.time-limit', 'int', 60)
->setDescription(pht('Hard time limit on generating patches.')),
$this->newOption(
'audit.can-author-close-audit',
'bool',
false)
->setBoolOptions(
array(
pht('Enable Closing Audits'),
pht('Disable Closing Audits'),
))
->setDescription(pht('Controls whether Author can Close Audits.')),
$this->newOption('bugtraq.url', 'string', null)
->addExample('https://bugs.php.net/%BUGID%', pht('PHP bugs'))
->addExample('/%BUGID%', pht('Local Maniphest URL'))
->setDescription(
pht(
'URL of external bug tracker used by Diffusion. %s will be '.
'substituted by the bug ID.',
'%BUGID%')),
$this->newOption('bugtraq.logregex', 'list<regex>', array())
->addExample(array('/\B#([1-9]\d*)\b/'), pht('Issue #123'))
->addExample(
array('/[Ii]ssues?:?(\s*,?\s*#\d+)+/', '/(\d+)/'),
pht('Issue #123, #456'))
->addExample(array('/(?<!#)\b(T[1-9]\d*)\b/'), pht('Task T123'))
->addExample('/[A-Z]{2,}-\d+/', pht('JIRA-1234'))
->setDescription(
pht(
'Regular expression to link external bug tracker. See '.
'http://tortoisesvn.net/docs/release/TortoiseSVN_en/'.
'tsvn-dug-bugtracker.html for further explanation.')),
$this->newOption('diffusion.allow-http-auth', 'bool', false)
->setBoolOptions(
array(
pht('Allow HTTP Basic Auth'),
pht('Disallow HTTP Basic Auth'),
))
->setSummary(pht('Enable HTTP Basic Auth for repositories.'))
->setDescription(
pht(
"Phabricator can serve repositories over HTTP, using HTTP basic ".
"auth.\n\n".
"Because HTTP basic auth is less secure than SSH auth, it is ".
"disabled by default. You can enable it here if you'd like to use ".
"it anyway. There's nothing fundamentally insecure about it as ".
"long as Phabricator uses HTTPS, but it presents a much lower ".
"barrier to attackers than SSH does.\n\n".
"Consider using SSH for authenticated access to repositories ".
"instead of HTTP.")),
$this->newOption('diffusion.allow-git-lfs', 'bool', false)
->setBoolOptions(
array(
pht('Allow Git LFS'),
pht('Disallow Git LFS'),
))
->setLocked(true)
->setSummary(pht('Allow Git Large File Storage (LFS).'))
->setDescription(
pht(
'Phabricator supports Git LFS, a Git extension for storing large '.
'files alongside a repository. Activate this setting to allow '.
'the extension to store file data in Phabricator.')),
$this->newOption('diffusion.ssh-user', 'string', null)
->setLocked(true)
->setSummary(pht('Login username for SSH connections to repositories.'))
->setDescription(
pht(
'When constructing clone URIs to show to users, Diffusion will '.
'fill in this login username. If you have configured a VCS user '.
'like `git`, you should provide it here.')),
$this->newOption('diffusion.ssh-port', 'int', null)
->setLocked(true)
->setSummary(pht('Port for SSH connections to repositories.'))
->setDescription(
pht(
'When constructing clone URIs to show to users, Diffusion by '.
'default will not display a port assuming the default for your '.
'VCS. Explicitly declare when running on a non-standard port.')),
$this->newOption('diffusion.ssh-host', 'string', null)
->setLocked(true)
->setSummary(pht('Host for SSH connections to repositories.'))
->setDescription(
pht(
'If you accept Phabricator SSH traffic on a different host '.
'from web traffic (for example, if you use different SSH and '.
'web load balancers), you can set the SSH hostname here. This '.
'is an advanced option.')),
$this->newOption('diffusion.fields', $custom_field_type, $default_fields)
->setCustomData(
id(new PhabricatorRepositoryCommit())
->getCustomFieldBaseClass())
->setDescription(
pht('Select and reorder Diffusion fields.')),
);
}
}
diff --git a/src/applications/diffusion/controller/DiffusionBlameController.php b/src/applications/diffusion/controller/DiffusionBlameController.php
index 95cc03921..591462590 100644
--- a/src/applications/diffusion/controller/DiffusionBlameController.php
+++ b/src/applications/diffusion/controller/DiffusionBlameController.php
@@ -1,281 +1,284 @@
<?php
final class DiffusionBlameController extends DiffusionController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$response = $this->loadDiffusionContext();
if ($response) {
return $response;
}
$viewer = $this->getViewer();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$blame = $this->loadBlame();
$identifiers = array_fuse($blame);
if ($identifiers) {
$commits = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withRepository($repository)
->withIdentifiers($identifiers)
->needIdentities(true)
+ // See PHI1014. If identities haven't been built yet, we may need to
+ // fall back to raw commit data.
+ ->needCommitData(true)
->execute();
$commits = mpull($commits, null, 'getCommitIdentifier');
} else {
$commits = array();
}
$commit_map = mpull($commits, 'getCommitIdentifier', 'getPHID');
$revisions = array();
$revision_map = array();
if ($commits) {
$revision_ids = id(new DifferentialRevision())
->loadIDsByCommitPHIDs(array_keys($commit_map));
if ($revision_ids) {
$revisions = id(new DifferentialRevisionQuery())
->setViewer($viewer)
->withIDs($revision_ids)
->execute();
$revisions = mpull($revisions, null, 'getID');
}
foreach ($revision_ids as $commit_phid => $revision_id) {
// If the viewer can't actually see this revision, skip it.
if (!isset($revisions[$revision_id])) {
continue;
}
$revision_map[$commit_map[$commit_phid]] = $revision_id;
}
}
$base_href = (string)$drequest->generateURI(
array(
'action' => 'browse',
'stable' => true,
));
$skip_text = pht('Skip Past This Commit');
$skip_icon = id(new PHUIIconView())
->setIcon('fa-backward');
Javelin::initBehavior('phabricator-tooltips');
$handle_phids = array();
foreach ($commits as $commit) {
$handle_phids[] = $commit->getAuthorDisplayPHID();
}
foreach ($revisions as $revision) {
$handle_phids[] = $revision->getAuthorPHID();
}
$handles = $viewer->loadHandles($handle_phids);
$map = array();
$epochs = array();
foreach ($identifiers as $identifier) {
$revision_id = idx($revision_map, $identifier);
if ($revision_id) {
$revision = idx($revisions, $revision_id);
} else {
$revision = null;
}
$skip_href = $base_href.'?before='.$identifier;
$skip_link = javelin_tag(
'a',
array(
'href' => $skip_href,
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $skip_text,
'align' => 'E',
'size' => 300,
),
),
$skip_icon);
// We may not have a commit object for a given identifier if the commit
// has not imported yet.
// At time of writing, this can also happen if a line was part of the
// initial import: blame produces a "^abc123" identifier in Git, which
// doesn't correspond to a real commit.
$commit = idx($commits, $identifier);
$author_phid = null;
if ($commit) {
$author_phid = $commit->getAuthorDisplayPHID();
}
if (!$author_phid) {
// This means we couldn't identify an author for the commit or the
// revision. We just render a blank for alignment.
$author_style = null;
$author_href = null;
$author_sigil = null;
$author_meta = null;
} else {
$author_src = $handles[$author_phid]->getImageURI();
$author_style = 'background-image: url('.$author_src.');';
$author_href = $handles[$author_phid]->getURI();
$author_sigil = 'has-tooltip';
$author_meta = array(
'tip' => $handles[$author_phid]->getName(),
'align' => 'E',
'size' => 'auto',
);
}
$author_link = javelin_tag(
$author_href ? 'a' : 'span',
array(
'class' => 'phabricator-source-blame-author',
'style' => $author_style,
'href' => $author_href,
'sigil' => $author_sigil,
'meta' => $author_meta,
));
if ($commit) {
$commit_link = javelin_tag(
'a',
array(
'href' => $commit->getURI(),
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $this->renderCommitTooltip($commit, $handles),
'align' => 'E',
'size' => 600,
),
),
$commit->getLocalName());
} else {
$commit_link = null;
}
$info = array(
$author_link,
$commit_link,
);
if ($revision) {
$revision_link = javelin_tag(
'a',
array(
'href' => $revision->getURI(),
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $this->renderRevisionTooltip($revision, $handles),
'align' => 'E',
'size' => 600,
),
),
$revision->getMonogram());
$info = array(
$info,
" \xC2\xB7 ",
$revision_link,
);
}
if ($commit) {
$epoch = $commit->getEpoch();
} else {
$epoch = 0;
}
$epochs[] = $epoch;
$data = array(
'skip' => $skip_link,
'info' => hsprintf('%s', $info),
'epoch' => $epoch,
);
$map[$identifier] = $data;
}
$epoch_min = min($epochs);
$epoch_max = max($epochs);
return id(new AphrontAjaxResponse())->setContent(
array(
'blame' => $blame,
'map' => $map,
'epoch' => array(
'min' => $epoch_min,
'max' => $epoch_max,
),
));
}
private function loadBlame() {
$drequest = $this->getDiffusionRequest();
$commit = $drequest->getCommit();
$path = $drequest->getPath();
$blame_timeout = 15;
$blame = $this->callConduitWithDiffusionRequest(
'diffusion.blame',
array(
'commit' => $commit,
'paths' => array($path),
'timeout' => $blame_timeout,
));
return idx($blame, $path, array());
}
private function renderRevisionTooltip(
DifferentialRevision $revision,
$handles) {
$viewer = $this->getViewer();
$date = phabricator_date($revision->getDateModified(), $viewer);
$monogram = $revision->getMonogram();
$title = $revision->getTitle();
$header = "{$monogram} {$title}";
$author = $handles[$revision->getAuthorPHID()]->getName();
return "{$header}\n{$date} \xC2\xB7 {$author}";
}
private function renderCommitTooltip(
PhabricatorRepositoryCommit $commit,
$handles) {
$viewer = $this->getViewer();
$date = phabricator_date($commit->getEpoch(), $viewer);
$summary = trim($commit->getSummary());
$author_phid = $commit->getAuthorPHID();
if ($author_phid && isset($handles[$author_phid])) {
$author_name = $handles[$author_phid]->getName();
} else {
$author_name = null;
}
if ($author_name) {
return "{$summary}\n{$date} \xC2\xB7 {$author_name}";
} else {
return "{$summary}\n{$date}";
}
}
}
diff --git a/src/applications/diffusion/controller/DiffusionCommitController.php b/src/applications/diffusion/controller/DiffusionCommitController.php
index 5621b1fa1..fae215f38 100644
--- a/src/applications/diffusion/controller/DiffusionCommitController.php
+++ b/src/applications/diffusion/controller/DiffusionCommitController.php
@@ -1,1154 +1,1152 @@
<?php
final class DiffusionCommitController extends DiffusionController {
const CHANGES_LIMIT = 100;
private $commitParents;
private $commitRefs;
private $commitMerges;
private $commitErrors;
private $commitExists;
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$response = $this->loadDiffusionContext();
if ($response) {
return $response;
}
$drequest = $this->getDiffusionRequest();
$viewer = $request->getUser();
$repository = $drequest->getRepository();
$commit_identifier = $drequest->getCommit();
// If this page is being accessed via "/source/xyz/commit/...", redirect
// to the canonical URI.
$has_callsign = strlen($request->getURIData('repositoryCallsign'));
$has_id = strlen($request->getURIData('repositoryID'));
if (!$has_callsign && !$has_id) {
$canonical_uri = $repository->getCommitURI($commit_identifier);
return id(new AphrontRedirectResponse())
->setURI($canonical_uri);
}
if ($request->getStr('diff')) {
return $this->buildRawDiffResponse($drequest);
}
$commits = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withRepository($repository)
->withIdentifiers(array($commit_identifier))
->needCommitData(true)
->needAuditRequests(true)
->needAuditAuthority(array($viewer))
->setLimit(100)
->needIdentities(true)
->execute();
$multiple_results = count($commits) > 1;
$crumbs = $this->buildCrumbs(array(
'commit' => !$multiple_results,
));
$crumbs->setBorder(true);
if (!$commits) {
if (!$this->getCommitExists()) {
return new Aphront404Response();
}
$error = id(new PHUIInfoView())
->setTitle(pht('Commit Still Parsing'))
->appendChild(
pht(
'Failed to load the commit because the commit has not been '.
'parsed yet.'));
$title = pht('Commit Still Parsing');
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($error);
} else if ($multiple_results) {
$warning_message =
pht(
'The identifier %s is ambiguous and matches more than one commit.',
phutil_tag(
'strong',
array(),
$commit_identifier));
$error = id(new PHUIInfoView())
->setTitle(pht('Ambiguous Commit'))
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->appendChild($warning_message);
$list = id(new DiffusionCommitListView())
->setViewer($viewer)
->setCommits($commits)
->setNoDataString(pht('No recent commits.'));
$crumbs->addTextCrumb(pht('Ambiguous Commit'));
$matched_commits = id(new PHUITwoColumnView())
->setFooter(array(
$error,
$list,
));
return $this->newPage()
->setTitle(pht('Ambiguous Commit'))
->setCrumbs($crumbs)
->appendChild($matched_commits);
} else {
$commit = head($commits);
}
$audit_requests = $commit->getAudits();
$commit_data = $commit->getCommitData();
$is_foreign = $commit_data->getCommitDetail('foreign-svn-stub');
$error_panel = null;
$hard_limit = 1000;
if ($commit->isImported()) {
$change_query = DiffusionPathChangeQuery::newFromDiffusionRequest(
$drequest);
$change_query->setLimit($hard_limit + 1);
$changes = $change_query->loadChanges();
} else {
$changes = array();
}
$was_limited = (count($changes) > $hard_limit);
if ($was_limited) {
$changes = array_slice($changes, 0, $hard_limit);
}
$count = count($changes);
$is_unreadable = false;
$hint = null;
if (!$count || $commit->isUnreachable()) {
$hint = id(new DiffusionCommitHintQuery())
->setViewer($viewer)
->withRepositoryPHIDs(array($repository->getPHID()))
->withOldCommitIdentifiers(array($commit->getCommitIdentifier()))
->executeOne();
if ($hint) {
$is_unreadable = $hint->isUnreadable();
}
}
if ($is_foreign) {
$subpath = $commit_data->getCommitDetail('svn-subpath');
$error_panel = new PHUIInfoView();
$error_panel->setTitle(pht('Commit Not Tracked'));
$error_panel->setSeverity(PHUIInfoView::SEVERITY_WARNING);
$error_panel->appendChild(
pht(
"This Diffusion repository is configured to track only one ".
"subdirectory of the entire Subversion repository, and this commit ".
"didn't affect the tracked subdirectory ('%s'), so no ".
"information is available.",
$subpath));
} else {
$engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine();
$engine->setConfig('viewer', $viewer);
$commit_tag = $this->renderCommitHashTag($drequest);
$header = id(new PHUIHeaderView())
->setHeader(nonempty($commit->getSummary(), pht('Commit Detail')))
->setHeaderIcon('fa-code-fork')
->addTag($commit_tag);
if (!$commit->isAuditStatusNoAudit()) {
$status = $commit->getAuditStatusObject();
$icon = $status->getIcon();
$color = $status->getColor();
$status = $status->getName();
$header->setStatus($icon, $color, $status);
}
$curtain = $this->buildCurtain($commit, $repository);
$subheader = $this->buildSubheaderView($commit, $commit_data);
$details = $this->buildPropertyListView(
$commit,
$commit_data,
$audit_requests);
$message = $commit_data->getCommitMessage();
$revision = $commit->getCommitIdentifier();
$message = $this->linkBugtraq($message);
$message = $engine->markupText($message);
$detail_list = new PHUIPropertyListView();
$detail_list->addTextContent(
phutil_tag(
'div',
array(
'class' => 'diffusion-commit-message phabricator-remarkup',
),
$message));
if ($commit->isUnreachable()) {
$did_rewrite = false;
if ($hint) {
if ($hint->isRewritten()) {
$rewritten = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withRepository($repository)
->withIdentifiers(array($hint->getNewCommitIdentifier()))
->executeOne();
if ($rewritten) {
$did_rewrite = true;
$rewritten_uri = $rewritten->getURI();
$rewritten_name = $rewritten->getLocalName();
$rewritten_link = phutil_tag(
'a',
array(
'href' => $rewritten_uri,
),
$rewritten_name);
$this->commitErrors[] = pht(
'This commit was rewritten after it was published, which '.
'changed the commit hash. This old version of the commit is '.
'no longer reachable from any branch, tag or ref. The new '.
'version of this commit is %s.',
$rewritten_link);
}
}
}
if (!$did_rewrite) {
$this->commitErrors[] = pht(
'This commit has been deleted in the repository: it is no longer '.
'reachable from any branch, tag, or ref.');
}
}
if ($this->getCommitErrors()) {
$error_panel = id(new PHUIInfoView())
->appendChild($this->getCommitErrors())
->setSeverity(PHUIInfoView::SEVERITY_WARNING);
}
}
$timeline = $this->buildComments($commit);
$merge_table = $this->buildMergesTable($commit);
$show_changesets = false;
$info_panel = null;
$change_list = null;
$change_table = null;
if ($is_unreadable) {
$info_panel = $this->renderStatusMessage(
pht('Unreadable Commit'),
pht(
'This commit has been marked as unreadable by an administrator. '.
'It may have been corrupted or created improperly by an external '.
'tool.'));
} else if ($is_foreign) {
// Don't render anything else.
} else if (!$commit->isImported()) {
$info_panel = $this->renderStatusMessage(
pht('Still Importing...'),
pht(
'This commit is still importing. Changes will be visible once '.
'the import finishes.'));
} else if (!count($changes)) {
$info_panel = $this->renderStatusMessage(
pht('Empty Commit'),
pht(
'This commit is empty and does not affect any paths.'));
} else if ($was_limited) {
$info_panel = $this->renderStatusMessage(
pht('Very Large Commit'),
pht(
'This commit is very large, and affects more than %d files. '.
'Changes are not shown.',
$hard_limit));
} else if (!$this->getCommitExists()) {
$info_panel = $this->renderStatusMessage(
pht('Commit No Longer Exists'),
pht('This commit no longer exists in the repository.'));
} else {
$show_changesets = true;
// The user has clicked "Show All Changes", and we should show all the
// changes inline even if there are more than the soft limit.
$show_all_details = $request->getBool('show_all');
$change_header = id(new PHUIHeaderView())
->setHeader(pht('Changes (%s)', new PhutilNumber($count)));
$warning_view = null;
if ($count > self::CHANGES_LIMIT && !$show_all_details) {
$button = id(new PHUIButtonView())
->setText(pht('Show All Changes'))
->setHref('?show_all=true')
->setTag('a')
->setIcon('fa-files-o');
$warning_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setTitle(pht('Very Large Commit'))
->appendChild(
pht('This commit is very large. Load each file individually.'));
$change_header->addActionLink($button);
}
$changesets = DiffusionPathChange::convertToDifferentialChangesets(
$viewer,
$changes);
// TODO: This table and panel shouldn't really be separate, but we need
// to clean up the "Load All Files" interaction first.
$change_table = $this->buildTableOfContents(
$changesets,
$change_header,
$warning_view);
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$vcs_supports_directory_changes = true;
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$vcs_supports_directory_changes = false;
break;
default:
throw new Exception(pht('Unknown VCS.'));
}
$references = array();
foreach ($changesets as $key => $changeset) {
$file_type = $changeset->getFileType();
if ($file_type == DifferentialChangeType::FILE_DIRECTORY) {
if (!$vcs_supports_directory_changes) {
unset($changesets[$key]);
continue;
}
}
$references[$key] = $drequest->generateURI(
array(
'action' => 'rendering-ref',
'path' => $changeset->getFilename(),
));
}
// TODO: Some parts of the views still rely on properties of the
// DifferentialChangeset. Make the objects ephemeral to make sure we don't
// accidentally save them, and then set their ID to the appropriate ID for
// this application (the path IDs).
$path_ids = array_flip(mpull($changes, 'getPath'));
foreach ($changesets as $changeset) {
$changeset->makeEphemeral();
$changeset->setID($path_ids[$changeset->getFilename()]);
}
if ($count <= self::CHANGES_LIMIT || $show_all_details) {
$visible_changesets = $changesets;
} else {
$visible_changesets = array();
$inlines = PhabricatorAuditInlineComment::loadDraftAndPublishedComments(
$viewer,
$commit->getPHID());
$path_ids = mpull($inlines, null, 'getPathID');
foreach ($changesets as $key => $changeset) {
if (array_key_exists($changeset->getID(), $path_ids)) {
$visible_changesets[$key] = $changeset;
}
}
}
$change_list_title = $commit->getDisplayName();
$change_list = new DifferentialChangesetListView();
$change_list->setTitle($change_list_title);
$change_list->setChangesets($changesets);
$change_list->setVisibleChangesets($visible_changesets);
$change_list->setRenderingReferences($references);
$change_list->setRenderURI($repository->getPathURI('diff/'));
$change_list->setRepository($repository);
$change_list->setUser($viewer);
$change_list->setBackground(PHUIObjectBoxView::BLUE_PROPERTY);
// TODO: Try to setBranch() to something reasonable here?
$change_list->setStandaloneURI(
$repository->getPathURI('diff/'));
$change_list->setRawFileURIs(
// TODO: Implement this, somewhat tricky if there's an octopus merge
// or whatever?
null,
$repository->getPathURI('diff/?view=r'));
$change_list->setInlineCommentControllerURI(
'/diffusion/inline/edit/'.phutil_escape_uri($commit->getPHID()).'/');
}
$add_comment = $this->renderAddCommentPanel(
$commit,
$timeline);
$filetree_on = $viewer->compareUserSetting(
PhabricatorShowFiletreeSetting::SETTINGKEY,
PhabricatorShowFiletreeSetting::VALUE_ENABLE_FILETREE);
$nav = null;
if ($show_changesets && $filetree_on) {
$pref_collapse = PhabricatorFiletreeVisibleSetting::SETTINGKEY;
$collapsed = $viewer->getUserSetting($pref_collapse);
$pref_width = PhabricatorFiletreeWidthSetting::SETTINGKEY;
$width = $viewer->getUserSetting($pref_width);
$nav = id(new DifferentialChangesetFileTreeSideNavBuilder())
->setTitle($commit->getDisplayName())
->setBaseURI(new PhutilURI($commit->getURI()))
->build($changesets)
->setCrumbs($crumbs)
->setCollapsed((bool)$collapsed)
->setWidth((int)$width);
}
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setSubheader($subheader)
->setMainColumn(array(
$error_panel,
$timeline,
$merge_table,
$info_panel,
))
->setFooter(array(
$change_table,
$change_list,
$add_comment,
))
->addPropertySection(pht('Description'), $detail_list)
->addPropertySection(pht('Details'), $details)
->setCurtain($curtain);
$page = $this->newPage()
->setTitle($commit->getDisplayName())
->setCrumbs($crumbs)
->setPageObjectPHIDS(array($commit->getPHID()))
->appendChild(
array(
$view,
));
if ($nav) {
$page->setNavigation($nav);
}
return $page;
}
private function buildPropertyListView(
PhabricatorRepositoryCommit $commit,
PhabricatorRepositoryCommitData $data,
array $audit_requests) {
$viewer = $this->getViewer();
$commit_phid = $commit->getPHID();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$view = id(new PHUIPropertyListView())
->setUser($this->getRequest()->getUser())
->setObject($commit);
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($commit_phid))
->withEdgeTypes(array(
DiffusionCommitHasTaskEdgeType::EDGECONST,
DiffusionCommitHasRevisionEdgeType::EDGECONST,
DiffusionCommitRevertsCommitEdgeType::EDGECONST,
DiffusionCommitRevertedByCommitEdgeType::EDGECONST,
));
$edges = $edge_query->execute();
$task_phids = array_keys(
$edges[$commit_phid][DiffusionCommitHasTaskEdgeType::EDGECONST]);
$revision_phid = key(
$edges[$commit_phid][DiffusionCommitHasRevisionEdgeType::EDGECONST]);
$reverts_phids = array_keys(
$edges[$commit_phid][DiffusionCommitRevertsCommitEdgeType::EDGECONST]);
$reverted_by_phids = array_keys(
$edges[$commit_phid][DiffusionCommitRevertedByCommitEdgeType::EDGECONST]);
$phids = $edge_query->getDestinationPHIDs(array($commit_phid));
if ($data->getCommitDetail('reviewerPHID')) {
$phids[] = $data->getCommitDetail('reviewerPHID');
}
$phids[] = $commit->getCommitterDisplayPHID();
$phids[] = $commit->getAuthorDisplayPHID();
// NOTE: We should never normally have more than a single push log, but
// it can occur naturally if a commit is pushed, then the branch it was
// on is deleted, then the commit is pushed again (or through other similar
// chains of events). This should be rare, but does not indicate a bug
// or data issue.
// NOTE: We never query push logs in SVN because the committer is always
// the pusher and the commit time is always the push time; the push log
// is redundant and we save a query by skipping it.
$push_logs = array();
if ($repository->isHosted() && !$repository->isSVN()) {
$push_logs = id(new PhabricatorRepositoryPushLogQuery())
->setViewer($viewer)
->withRepositoryPHIDs(array($repository->getPHID()))
->withNewRefs(array($commit->getCommitIdentifier()))
->withRefTypes(array(PhabricatorRepositoryPushLog::REFTYPE_COMMIT))
->execute();
foreach ($push_logs as $log) {
$phids[] = $log->getPusherPHID();
}
}
$handles = array();
if ($phids) {
$handles = $this->loadViewerHandles($phids);
}
$props = array();
if ($audit_requests) {
$user_requests = array();
$other_requests = array();
foreach ($audit_requests as $audit_request) {
if (!$audit_request->isInteresting()) {
continue;
}
if ($audit_request->isUser()) {
$user_requests[] = $audit_request;
} else {
$other_requests[] = $audit_request;
}
}
if ($user_requests) {
$view->addProperty(
pht('Auditors'),
$this->renderAuditStatusView($commit, $user_requests));
}
if ($other_requests) {
$view->addProperty(
pht('Group Auditors'),
$this->renderAuditStatusView($commit, $other_requests));
}
}
$author_epoch = $data->getCommitDetail('authorEpoch');
$committed_info = id(new PHUIStatusItemView())
->setNote(phabricator_datetime($commit->getEpoch(), $viewer))
->setTarget($commit->renderAnyCommitter($viewer, $handles));
$committed_list = new PHUIStatusListView();
$committed_list->addItem($committed_info);
$view->addProperty(
pht('Committed'),
$committed_list);
if ($push_logs) {
$pushed_list = new PHUIStatusListView();
foreach ($push_logs as $push_log) {
$pushed_item = id(new PHUIStatusItemView())
->setTarget($handles[$push_log->getPusherPHID()]->renderLink())
->setNote(phabricator_datetime($push_log->getEpoch(), $viewer));
$pushed_list->addItem($pushed_item);
}
$view->addProperty(
pht('Pushed'),
$pushed_list);
}
$reviewer_phid = $data->getCommitDetail('reviewerPHID');
if ($reviewer_phid) {
$view->addProperty(
pht('Reviewer'),
$handles[$reviewer_phid]->renderLink());
}
if ($revision_phid) {
$view->addProperty(
pht('Differential Revision'),
$handles[$revision_phid]->renderLink());
}
$parents = $this->getCommitParents();
if ($parents) {
$view->addProperty(
pht('Parents'),
$viewer->renderHandleList(mpull($parents, 'getPHID')));
}
if ($this->getCommitExists()) {
$view->addProperty(
pht('Branches'),
phutil_tag(
'span',
array(
'id' => 'commit-branches',
),
pht('Unknown')));
$view->addProperty(
pht('Tags'),
phutil_tag(
'span',
array(
'id' => 'commit-tags',
),
pht('Unknown')));
$identifier = $commit->getCommitIdentifier();
$root = $repository->getPathURI("commit/{$identifier}");
Javelin::initBehavior(
'diffusion-commit-branches',
array(
$root.'/branches/' => 'commit-branches',
$root.'/tags/' => 'commit-tags',
));
}
$refs = $this->getCommitRefs();
if ($refs) {
$ref_links = array();
foreach ($refs as $ref_data) {
$ref_links[] = phutil_tag(
'a',
array(
'href' => $ref_data['href'],
),
$ref_data['ref']);
}
$view->addProperty(
pht('References'),
phutil_implode_html(', ', $ref_links));
}
if ($reverts_phids) {
$view->addProperty(
pht('Reverts'),
$viewer->renderHandleList($reverts_phids));
}
if ($reverted_by_phids) {
$view->addProperty(
pht('Reverted By'),
$viewer->renderHandleList($reverted_by_phids));
}
if ($task_phids) {
$task_list = array();
foreach ($task_phids as $phid) {
$task_list[] = $handles[$phid]->renderLink();
}
$task_list = phutil_implode_html(phutil_tag('br'), $task_list);
$view->addProperty(
pht('Tasks'),
$task_list);
}
return $view;
}
private function buildSubheaderView(
PhabricatorRepositoryCommit $commit,
PhabricatorRepositoryCommitData $data) {
$viewer = $this->getViewer();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
if ($repository->isSVN()) {
return null;
}
$author_phid = $commit->getAuthorDisplayPHID();
$author_name = $data->getAuthorName();
$author_epoch = $data->getCommitDetail('authorEpoch');
$date = null;
if ($author_epoch !== null) {
$date = phabricator_datetime($author_epoch, $viewer);
}
if ($author_phid) {
$handles = $viewer->loadHandles(array($author_phid));
$image_uri = $handles[$author_phid]->getImageURI();
$image_href = $handles[$author_phid]->getURI();
$author = $handles[$author_phid]->renderLink();
} else if (strlen($author_name)) {
$author = $author_name;
$image_uri = null;
$image_href = null;
} else {
return null;
}
$author = phutil_tag('strong', array(), $author);
if ($date) {
$content = pht('Authored by %s on %s.', $author, $date);
} else {
$content = pht('Authored by %s.', $author);
}
return id(new PHUIHeadThingView())
->setImage($image_uri)
->setImageHref($image_href)
->setContent($content);
}
private function buildComments(PhabricatorRepositoryCommit $commit) {
$timeline = $this->buildTransactionTimeline(
$commit,
new PhabricatorAuditTransactionQuery());
- $commit->willRenderTimeline($timeline, $this->getRequest());
-
$timeline->setQuoteRef($commit->getMonogram());
return $timeline;
}
private function renderAddCommentPanel(
PhabricatorRepositoryCommit $commit,
$timeline) {
$request = $this->getRequest();
$viewer = $request->getUser();
// TODO: This is pretty awkward, unify the CSS between Diffusion and
// Differential better.
require_celerity_resource('differential-core-view-css');
$comment_view = id(new DiffusionCommitEditEngine())
->setViewer($viewer)
->buildEditEngineCommentView($commit);
$comment_view->setTransactionTimeline($timeline);
return $comment_view;
}
private function buildMergesTable(PhabricatorRepositoryCommit $commit) {
$viewer = $this->getViewer();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$merges = $this->getCommitMerges();
if (!$merges) {
return null;
}
$limit = $this->getMergeDisplayLimit();
$caption = null;
if (count($merges) > $limit) {
$merges = array_slice($merges, 0, $limit);
$caption = new PHUIInfoView();
$caption->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
$caption->appendChild(
pht(
'This commit merges a very large number of changes. '.
'Only the first %s are shown.',
new PhutilNumber($limit)));
}
$history_table = id(new DiffusionHistoryTableView())
->setUser($viewer)
->setDiffusionRequest($drequest)
->setHistory($merges);
$history_table->loadRevisions();
$panel = id(new PHUIObjectBoxView())
->setHeaderText(pht('Merged Changes'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($history_table);
if ($caption) {
$panel->setInfoView($caption);
}
return $panel;
}
private function buildCurtain(
PhabricatorRepositoryCommit $commit,
PhabricatorRepository $repository) {
$request = $this->getRequest();
$viewer = $this->getViewer();
$curtain = $this->newCurtainView($commit);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$commit,
PhabricatorPolicyCapability::CAN_EDIT);
$id = $commit->getID();
$edit_uri = $this->getApplicationURI("/commit/edit/{$id}/");
$action = id(new PhabricatorActionView())
->setName(pht('Edit Commit'))
->setHref($edit_uri)
->setIcon('fa-pencil')
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit);
$curtain->addAction($action);
$action = id(new PhabricatorActionView())
->setName(pht('Download Raw Diff'))
->setHref($request->getRequestURI()->alter('diff', true))
->setIcon('fa-download');
$curtain->addAction($action);
$relationship_list = PhabricatorObjectRelationshipList::newForObject(
$viewer,
$commit);
$relationship_submenu = $relationship_list->newActionMenu();
if ($relationship_submenu) {
$curtain->addAction($relationship_submenu);
}
return $curtain;
}
private function buildRawDiffResponse(DiffusionRequest $drequest) {
$diff_info = $this->callConduitWithDiffusionRequest(
'diffusion.rawdiffquery',
array(
'commit' => $drequest->getCommit(),
'path' => $drequest->getPath(),
));
$file_phid = $diff_info['filePHID'];
$file = id(new PhabricatorFileQuery())
->setViewer($this->getViewer())
->withPHIDs(array($file_phid))
->executeOne();
if (!$file) {
throw new Exception(
pht(
'Failed to load file ("%s") returned by "%s".',
$file_phid,
'diffusion.rawdiffquery'));
}
return $file->getRedirectResponse();
}
private function renderAuditStatusView(
PhabricatorRepositoryCommit $commit,
array $audit_requests) {
assert_instances_of($audit_requests, 'PhabricatorRepositoryAuditRequest');
$viewer = $this->getViewer();
$view = new PHUIStatusListView();
foreach ($audit_requests as $request) {
$code = $request->getAuditStatus();
$item = new PHUIStatusItemView();
$item->setIcon(
PhabricatorAuditStatusConstants::getStatusIcon($code),
PhabricatorAuditStatusConstants::getStatusColor($code),
PhabricatorAuditStatusConstants::getStatusName($code));
$auditor_phid = $request->getAuditorPHID();
$target = $viewer->renderHandle($auditor_phid);
$item->setTarget($target);
if ($commit->hasAuditAuthority($viewer, $request)) {
$item->setHighlighted(true);
}
$view->addItem($item);
}
return $view;
}
private function linkBugtraq($corpus) {
$url = PhabricatorEnv::getEnvConfig('bugtraq.url');
if (!strlen($url)) {
return $corpus;
}
$regexes = PhabricatorEnv::getEnvConfig('bugtraq.logregex');
if (!$regexes) {
return $corpus;
}
$parser = id(new PhutilBugtraqParser())
->setBugtraqPattern("[[ {$url} | %BUGID% ]]")
->setBugtraqCaptureExpression(array_shift($regexes));
$select = array_shift($regexes);
if ($select) {
$parser->setBugtraqSelectExpression($select);
}
return $parser->processCorpus($corpus);
}
private function buildTableOfContents(
array $changesets,
$header,
$info_view) {
$drequest = $this->getDiffusionRequest();
$viewer = $this->getViewer();
$toc_view = id(new PHUIDiffTableOfContentsListView())
->setUser($viewer)
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY);
if ($info_view) {
$toc_view->setInfoView($info_view);
}
// TODO: This is hacky, we just want access to the linkX() methods on
// DiffusionView.
$diffusion_view = id(new DiffusionEmptyResultView())
->setDiffusionRequest($drequest);
$have_owners = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorOwnersApplication',
$viewer);
if (!$changesets) {
$have_owners = false;
}
if ($have_owners) {
if ($viewer->getPHID()) {
$packages = id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE))
->withAuthorityPHIDs(array($viewer->getPHID()))
->execute();
$toc_view->setAuthorityPackages($packages);
}
$repository = $drequest->getRepository();
$repository_phid = $repository->getPHID();
$control_query = id(new PhabricatorOwnersPackageQuery())
->setViewer($viewer)
->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE))
->withControl($repository_phid, mpull($changesets, 'getFilename'));
$control_query->execute();
}
foreach ($changesets as $changeset_id => $changeset) {
$path = $changeset->getFilename();
$anchor = $changeset->getAnchorName();
$history_link = $diffusion_view->linkHistory($path);
$browse_link = $diffusion_view->linkBrowse(
$path,
array(
'type' => $changeset->getFileType(),
));
$item = id(new PHUIDiffTableOfContentsItemView())
->setChangeset($changeset)
->setAnchor($anchor)
->setContext(
array(
$history_link,
' ',
$browse_link,
));
if ($have_owners) {
$packages = $control_query->getControllingPackagesForPath(
$repository_phid,
$changeset->getFilename());
$item->setPackages($packages);
}
$toc_view->addItem($item);
}
return $toc_view;
}
private function loadCommitState() {
$viewer = $this->getViewer();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$commit = $drequest->getCommit();
// TODO: We could use futures here and resolve these calls in parallel.
$exceptions = array();
try {
$parent_refs = $this->callConduitWithDiffusionRequest(
'diffusion.commitparentsquery',
array(
'commit' => $commit,
));
if ($parent_refs) {
$parents = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withRepository($repository)
->withIdentifiers($parent_refs)
->execute();
} else {
$parents = array();
}
$this->commitParents = $parents;
} catch (Exception $ex) {
$this->commitParents = false;
$exceptions[] = $ex;
}
$merge_limit = $this->getMergeDisplayLimit();
try {
if ($repository->isSVN()) {
$this->commitMerges = array();
} else {
$merges = $this->callConduitWithDiffusionRequest(
'diffusion.mergedcommitsquery',
array(
'commit' => $commit,
'limit' => $merge_limit + 1,
));
$this->commitMerges = DiffusionPathChange::newFromConduit($merges);
}
} catch (Exception $ex) {
$this->commitMerges = false;
$exceptions[] = $ex;
}
try {
if ($repository->isGit()) {
$refs = $this->callConduitWithDiffusionRequest(
'diffusion.refsquery',
array(
'commit' => $commit,
));
} else {
$refs = array();
}
$this->commitRefs = $refs;
} catch (Exception $ex) {
$this->commitRefs = false;
$exceptions[] = $ex;
}
if ($exceptions) {
$exists = $this->callConduitWithDiffusionRequest(
'diffusion.existsquery',
array(
'commit' => $commit,
));
if ($exists) {
$this->commitExists = true;
foreach ($exceptions as $exception) {
$this->commitErrors[] = $exception->getMessage();
}
} else {
$this->commitExists = false;
$this->commitErrors[] = pht(
'This commit no longer exists in the repository. It may have '.
'been part of a branch which was deleted.');
}
} else {
$this->commitExists = true;
$this->commitErrors = array();
}
}
private function getMergeDisplayLimit() {
return 50;
}
private function getCommitExists() {
if ($this->commitExists === null) {
$this->loadCommitState();
}
return $this->commitExists;
}
private function getCommitParents() {
if ($this->commitParents === null) {
$this->loadCommitState();
}
return $this->commitParents;
}
private function getCommitRefs() {
if ($this->commitRefs === null) {
$this->loadCommitState();
}
return $this->commitRefs;
}
private function getCommitMerges() {
if ($this->commitMerges === null) {
$this->loadCommitState();
}
return $this->commitMerges;
}
private function getCommitErrors() {
if ($this->commitErrors === null) {
$this->loadCommitState();
}
return $this->commitErrors;
}
}
diff --git a/src/applications/diffusion/controller/DiffusionServeController.php b/src/applications/diffusion/controller/DiffusionServeController.php
index 5a5c446a2..cb4ad0ba9 100644
--- a/src/applications/diffusion/controller/DiffusionServeController.php
+++ b/src/applications/diffusion/controller/DiffusionServeController.php
@@ -1,1242 +1,1242 @@
<?php
final class DiffusionServeController extends DiffusionController {
private $serviceViewer;
private $serviceRepository;
private $isGitLFSRequest;
private $gitLFSToken;
private $gitLFSInput;
public function setServiceViewer(PhabricatorUser $viewer) {
$this->getRequest()->setUser($viewer);
$this->serviceViewer = $viewer;
return $this;
}
public function getServiceViewer() {
return $this->serviceViewer;
}
public function setServiceRepository(PhabricatorRepository $repository) {
$this->serviceRepository = $repository;
return $this;
}
public function getServiceRepository() {
return $this->serviceRepository;
}
public function getIsGitLFSRequest() {
return $this->isGitLFSRequest;
}
public function getGitLFSToken() {
return $this->gitLFSToken;
}
public function isVCSRequest(AphrontRequest $request) {
$identifier = $this->getRepositoryIdentifierFromRequest($request);
if ($identifier === null) {
return null;
}
$content_type = $request->getHTTPHeader('Content-Type');
$user_agent = idx($_SERVER, 'HTTP_USER_AGENT');
$request_type = $request->getHTTPHeader('X-Phabricator-Request-Type');
// This may have a "charset" suffix, so only match the prefix.
$lfs_pattern = '(^application/vnd\\.git-lfs\\+json(;|\z))';
$vcs = null;
if ($request->getExists('service')) {
$service = $request->getStr('service');
// We get this initially for `info/refs`.
// Git also gives us a User-Agent like "git/1.8.2.3".
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
} else if (strncmp($user_agent, 'git/', 4) === 0) {
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
} else if ($content_type == 'application/x-git-upload-pack-request') {
// We get this for `git-upload-pack`.
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
} else if ($content_type == 'application/x-git-receive-pack-request') {
// We get this for `git-receive-pack`.
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
} else if (preg_match($lfs_pattern, $content_type)) {
// This is a Git LFS HTTP API request.
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
$this->isGitLFSRequest = true;
} else if ($request_type == 'git-lfs') {
// This is a Git LFS object content request.
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
$this->isGitLFSRequest = true;
} else if ($request->getExists('cmd')) {
// Mercurial also sends an Accept header like
// "application/mercurial-0.1", and a User-Agent like
// "mercurial/proto-1.0".
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL;
} else {
// Subversion also sends an initial OPTIONS request (vs GET/POST), and
// has a User-Agent like "SVN/1.8.3 (x86_64-apple-darwin11.4.2)
// serf/1.3.2".
$dav = $request->getHTTPHeader('DAV');
$dav = new PhutilURI($dav);
if ($dav->getDomain() === 'subversion.tigris.org') {
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_SVN;
}
}
return $vcs;
}
public function handleRequest(AphrontRequest $request) {
$service_exception = null;
$response = null;
try {
$response = $this->serveRequest($request);
} catch (Exception $ex) {
$service_exception = $ex;
}
try {
$remote_addr = $request->getRemoteAddress();
if ($request->isHTTPS()) {
$remote_protocol = PhabricatorRepositoryPullEvent::PROTOCOL_HTTPS;
} else {
$remote_protocol = PhabricatorRepositoryPullEvent::PROTOCOL_HTTP;
}
$pull_event = id(new PhabricatorRepositoryPullEvent())
->setEpoch(PhabricatorTime::getNow())
->setRemoteAddress($remote_addr)
->setRemoteProtocol($remote_protocol);
if ($response) {
$response_code = $response->getHTTPResponseCode();
if ($response_code == 200) {
$pull_event
->setResultType(PhabricatorRepositoryPullEvent::RESULT_PULL)
->setResultCode($response_code);
} else {
$pull_event
->setResultType(PhabricatorRepositoryPullEvent::RESULT_ERROR)
->setResultCode($response_code);
}
if ($response instanceof PhabricatorVCSResponse) {
$pull_event->setProperties(
array(
'response.message' => $response->getMessage(),
));
}
} else {
$pull_event
->setResultType(PhabricatorRepositoryPullEvent::RESULT_EXCEPTION)
->setResultCode(500)
->setProperties(
array(
'exception.class' => get_class($ex),
'exception.message' => $ex->getMessage(),
));
}
$viewer = $this->getServiceViewer();
if ($viewer) {
$pull_event->setPullerPHID($viewer->getPHID());
}
$repository = $this->getServiceRepository();
if ($repository) {
$pull_event->setRepositoryPHID($repository->getPHID());
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$pull_event->save();
unset($unguarded);
} catch (Exception $ex) {
if ($service_exception) {
throw $service_exception;
}
throw $ex;
}
if ($service_exception) {
throw $service_exception;
}
return $response;
}
private function serveRequest(AphrontRequest $request) {
$identifier = $this->getRepositoryIdentifierFromRequest($request);
// If authentication credentials have been provided, try to find a user
// that actually matches those credentials.
// We require both the username and password to be nonempty, because Git
// won't prompt users who provide a username but no password otherwise.
// See T10797 for discussion.
$have_user = strlen(idx($_SERVER, 'PHP_AUTH_USER'));
$have_pass = strlen(idx($_SERVER, 'PHP_AUTH_PW'));
if ($have_user && $have_pass) {
$username = $_SERVER['PHP_AUTH_USER'];
$password = new PhutilOpaqueEnvelope($_SERVER['PHP_AUTH_PW']);
// Try Git LFS auth first since we can usually reject it without doing
// any queries, since the username won't match the one we expect or the
// request won't be LFS.
$viewer = $this->authenticateGitLFSUser($username, $password);
// If that failed, try normal auth. Note that we can use normal auth on
// LFS requests, so this isn't strictly an alternative to LFS auth.
if (!$viewer) {
$viewer = $this->authenticateHTTPRepositoryUser($username, $password);
}
if (!$viewer) {
return new PhabricatorVCSResponse(
403,
pht('Invalid credentials.'));
}
} else {
// User hasn't provided credentials, which means we count them as
// being "not logged in".
$viewer = new PhabricatorUser();
}
$this->setServiceViewer($viewer);
$allow_public = PhabricatorEnv::getEnvConfig('policy.allow-public');
$allow_auth = PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth');
if (!$allow_public) {
if (!$viewer->isLoggedIn()) {
if ($allow_auth) {
return new PhabricatorVCSResponse(
401,
pht('You must log in to access repositories.'));
} else {
return new PhabricatorVCSResponse(
403,
pht('Public and authenticated HTTP access are both forbidden.'));
}
}
}
try {
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withIdentifiers(array($identifier))
->needURIs(true)
->executeOne();
if (!$repository) {
return new PhabricatorVCSResponse(
404,
pht('No such repository exists.'));
}
} catch (PhabricatorPolicyException $ex) {
if ($viewer->isLoggedIn()) {
return new PhabricatorVCSResponse(
403,
pht('You do not have permission to access this repository.'));
} else {
if ($allow_auth) {
return new PhabricatorVCSResponse(
401,
pht('You must log in to access this repository.'));
} else {
return new PhabricatorVCSResponse(
403,
pht(
'This repository requires authentication, which is forbidden '.
'over HTTP.'));
}
}
}
$response = $this->validateGitLFSRequest($repository, $viewer);
if ($response) {
return $response;
}
$this->setServiceRepository($repository);
if (!$repository->isTracked()) {
return new PhabricatorVCSResponse(
403,
pht('This repository is inactive.'));
}
$is_push = !$this->isReadOnlyRequest($repository);
if ($this->getIsGitLFSRequest() && $this->getGitLFSToken()) {
// We allow git LFS requests over HTTP even if the repository does not
// otherwise support HTTP reads or writes, as long as the user is using a
// token from SSH. If they're using HTTP username + password auth, they
// have to obey the normal HTTP rules.
} else {
// For now, we don't distinguish between HTTP and HTTPS-originated
// requests that are proxied within the cluster, so the user can connect
// with HTTPS but we may be on HTTP by the time we reach this part of
// the code. Allow things to move forward as long as either protocol
// can be served.
$proto_https = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTPS;
$proto_http = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTP;
$can_read =
$repository->canServeProtocol($proto_https, false) ||
$repository->canServeProtocol($proto_http, false);
if (!$can_read) {
return new PhabricatorVCSResponse(
403,
pht('This repository is not available over HTTP.'));
}
if ($is_push) {
$can_write =
$repository->canServeProtocol($proto_https, true) ||
$repository->canServeProtocol($proto_http, true);
if (!$can_write) {
return new PhabricatorVCSResponse(
403,
pht('This repository is read-only over HTTP.'));
}
}
}
if ($is_push) {
$can_push = PhabricatorPolicyFilter::hasCapability(
$viewer,
$repository,
DiffusionPushCapability::CAPABILITY);
if (!$can_push) {
if ($viewer->isLoggedIn()) {
$error_code = 403;
$error_message = pht(
'You do not have permission to push to this repository ("%s").',
$repository->getDisplayName());
if ($this->getIsGitLFSRequest()) {
return DiffusionGitLFSResponse::newErrorResponse(
$error_code,
$error_message);
} else {
return new PhabricatorVCSResponse(
$error_code,
$error_message);
}
} else {
if ($allow_auth) {
return new PhabricatorVCSResponse(
401,
pht('You must log in to push to this repository.'));
} else {
return new PhabricatorVCSResponse(
403,
pht(
'Pushing to this repository requires authentication, '.
'which is forbidden over HTTP.'));
}
}
}
}
$vcs_type = $repository->getVersionControlSystem();
$req_type = $this->isVCSRequest($request);
if ($vcs_type != $req_type) {
switch ($req_type) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$result = new PhabricatorVCSResponse(
500,
pht(
'This repository ("%s") is not a Git repository.',
$repository->getDisplayName()));
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$result = new PhabricatorVCSResponse(
500,
pht(
'This repository ("%s") is not a Mercurial repository.',
$repository->getDisplayName()));
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$result = new PhabricatorVCSResponse(
500,
pht(
'This repository ("%s") is not a Subversion repository.',
$repository->getDisplayName()));
break;
default:
$result = new PhabricatorVCSResponse(
500,
pht('Unknown request type.'));
break;
}
} else {
switch ($vcs_type) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$result = $this->serveVCSRequest($repository, $viewer);
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$result = new PhabricatorVCSResponse(
500,
pht(
'Phabricator does not support HTTP access to Subversion '.
'repositories.'));
break;
default:
$result = new PhabricatorVCSResponse(
500,
pht('Unknown version control system.'));
break;
}
}
$code = $result->getHTTPResponseCode();
if ($is_push && ($code == 200)) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$repository->writeStatusMessage(
PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE,
PhabricatorRepositoryStatusMessage::CODE_OKAY);
unset($unguarded);
}
return $result;
}
private function serveVCSRequest(
PhabricatorRepository $repository,
PhabricatorUser $viewer) {
// We can serve Git LFS requests first, since we don't need to proxy them.
// It's also important that LFS requests never fall through to standard
// service pathways, because that would let you use LFS tokens to read
// normal repository data.
if ($this->getIsGitLFSRequest()) {
return $this->serveGitLFSRequest($repository, $viewer);
}
// If this repository is hosted on a service, we need to proxy the request
// to a host which can serve it.
$is_cluster_request = $this->getRequest()->isProxiedClusterRequest();
$uri = $repository->getAlmanacServiceURI(
$viewer,
array(
'neverProxy' => $is_cluster_request,
'protocols' => array(
'http',
'https',
),
'writable' => !$this->isReadOnlyRequest($repository),
));
if ($uri) {
$future = $this->getRequest()->newClusterProxyFuture($uri);
return id(new AphrontHTTPProxyResponse())
->setHTTPFuture($future);
}
// Otherwise, we're going to handle the request locally.
$vcs_type = $repository->getVersionControlSystem();
switch ($vcs_type) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$result = $this->serveGitRequest($repository, $viewer);
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$result = $this->serveMercurialRequest($repository, $viewer);
break;
}
return $result;
}
private function isReadOnlyRequest(
PhabricatorRepository $repository) {
$request = $this->getRequest();
$method = $_SERVER['REQUEST_METHOD'];
// TODO: This implementation is safe by default, but very incomplete.
if ($this->getIsGitLFSRequest()) {
return $this->isGitLFSReadOnlyRequest($repository);
}
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$service = $request->getStr('service');
$path = $this->getRequestDirectoryPath($repository);
// NOTE: Service names are the reverse of what you might expect, as they
// are from the point of view of the server. The main read service is
// "git-upload-pack", and the main write service is "git-receive-pack".
if ($method == 'GET' &&
$path == '/info/refs' &&
$service == 'git-upload-pack') {
return true;
}
if ($path == '/git-upload-pack') {
return true;
}
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$cmd = $request->getStr('cmd');
if ($cmd == 'batch') {
$cmds = idx($this->getMercurialArguments(), 'cmds');
return DiffusionMercurialWireProtocol::isReadOnlyBatchCommand($cmds);
}
return DiffusionMercurialWireProtocol::isReadOnlyCommand($cmd);
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
break;
}
return false;
}
/**
* @phutil-external-symbol class PhabricatorStartup
*/
private function serveGitRequest(
PhabricatorRepository $repository,
PhabricatorUser $viewer) {
$request = $this->getRequest();
$request_path = $this->getRequestDirectoryPath($repository);
$repository_root = $repository->getLocalPath();
// Rebuild the query string to strip `__magic__` parameters and prevent
// issues where we might interpret inputs like "service=read&service=write"
// differently than the server does and pass it an unsafe command.
// NOTE: This does not use getPassthroughRequestParameters() because
// that code is HTTP-method agnostic and will encode POST data.
$query_data = $_GET;
foreach ($query_data as $key => $value) {
if (!strncmp($key, '__', 2)) {
unset($query_data[$key]);
}
}
- $query_string = http_build_query($query_data, '', '&');
+ $query_string = phutil_build_http_querystring($query_data);
// We're about to wipe out PATH with the rest of the environment, so
// resolve the binary first.
$bin = Filesystem::resolveBinary('git-http-backend');
if (!$bin) {
throw new Exception(
pht(
'Unable to find `%s` in %s!',
'git-http-backend',
'$PATH'));
}
// NOTE: We do not set HTTP_CONTENT_ENCODING here, because we already
// decompressed the request when we read the request body, so the body is
// just plain data with no encoding.
$env = array(
'REQUEST_METHOD' => $_SERVER['REQUEST_METHOD'],
'QUERY_STRING' => $query_string,
'CONTENT_TYPE' => $request->getHTTPHeader('Content-Type'),
'REMOTE_ADDR' => $_SERVER['REMOTE_ADDR'],
'GIT_PROJECT_ROOT' => $repository_root,
'GIT_HTTP_EXPORT_ALL' => '1',
'PATH_INFO' => $request_path,
'REMOTE_USER' => $viewer->getUsername(),
// TODO: Set these correctly.
// GIT_COMMITTER_NAME
// GIT_COMMITTER_EMAIL
) + $this->getCommonEnvironment($viewer);
$input = PhabricatorStartup::getRawInput();
$command = csprintf('%s', $bin);
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$cluster_engine = id(new DiffusionRepositoryClusterEngine())
->setViewer($viewer)
->setRepository($repository);
$did_write_lock = false;
if ($this->isReadOnlyRequest($repository)) {
$cluster_engine->synchronizeWorkingCopyBeforeRead();
} else {
$did_write_lock = true;
$cluster_engine->synchronizeWorkingCopyBeforeWrite();
}
$caught = null;
try {
list($err, $stdout, $stderr) = id(new ExecFuture('%C', $command))
->setEnv($env, true)
->write($input)
->resolve();
} catch (Exception $ex) {
$caught = $ex;
}
if ($did_write_lock) {
$cluster_engine->synchronizeWorkingCopyAfterWrite();
}
unset($unguarded);
if ($caught) {
throw $caught;
}
if ($err) {
if ($this->isValidGitShallowCloneResponse($stdout, $stderr)) {
// Ignore the error if the response passes this special check for
// validity.
$err = 0;
}
}
if ($err) {
return new PhabricatorVCSResponse(
500,
pht(
'Error %d: %s',
$err,
phutil_utf8ize($stderr)));
}
return id(new DiffusionGitResponse())->setGitData($stdout);
}
private function getRequestDirectoryPath(PhabricatorRepository $repository) {
$request = $this->getRequest();
$request_path = $request->getRequestURI()->getPath();
$info = PhabricatorRepository::parseRepositoryServicePath(
$request_path,
$repository->getVersionControlSystem());
$base_path = $info['path'];
// For Git repositories, strip an optional directory component if it
// isn't the name of a known Git resource. This allows users to clone
// repositories as "/diffusion/X/anything.git", for example.
if ($repository->isGit()) {
$known = array(
'info',
'git-upload-pack',
'git-receive-pack',
);
foreach ($known as $key => $path) {
$known[$key] = preg_quote($path, '@');
}
$known = implode('|', $known);
if (preg_match('@^/([^/]+)/('.$known.')(/|$)@', $base_path)) {
$base_path = preg_replace('@^/([^/]+)@', '', $base_path);
}
}
return $base_path;
}
private function authenticateGitLFSUser(
$username,
PhutilOpaqueEnvelope $password) {
// Never accept these credentials for requests which aren't LFS requests.
if (!$this->getIsGitLFSRequest()) {
return null;
}
// If we have the wrong username, don't bother checking if the token
// is right.
if ($username !== DiffusionGitLFSTemporaryTokenType::HTTP_USERNAME) {
return null;
}
$lfs_pass = $password->openEnvelope();
$lfs_hash = PhabricatorHash::weakDigest($lfs_pass);
$token = id(new PhabricatorAuthTemporaryTokenQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withTokenTypes(array(DiffusionGitLFSTemporaryTokenType::TOKENTYPE))
->withTokenCodes(array($lfs_hash))
->withExpired(false)
->executeOne();
if (!$token) {
return null;
}
$user = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($token->getUserPHID()))
->executeOne();
if (!$user) {
return null;
}
if (!$user->isUserActivated()) {
return null;
}
$this->gitLFSToken = $token;
return $user;
}
private function authenticateHTTPRepositoryUser(
$username,
PhutilOpaqueEnvelope $password) {
if (!PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth')) {
// No HTTP auth permitted.
return null;
}
if (!strlen($username)) {
// No username.
return null;
}
if (!strlen($password->openEnvelope())) {
// No password.
return null;
}
$user = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withUsernames(array($username))
->executeOne();
if (!$user) {
// Username doesn't match anything.
return null;
}
if (!$user->isUserActivated()) {
// User is not activated.
return null;
}
$request = $this->getRequest();
$content_source = PhabricatorContentSource::newFromRequest($request);
$engine = id(new PhabricatorAuthPasswordEngine())
->setViewer($user)
->setContentSource($content_source)
->setPasswordType(PhabricatorAuthPassword::PASSWORD_TYPE_VCS)
->setObject($user);
if (!$engine->isValidPassword($password)) {
return null;
}
return $user;
}
private function serveMercurialRequest(
PhabricatorRepository $repository,
PhabricatorUser $viewer) {
$request = $this->getRequest();
$bin = Filesystem::resolveBinary('hg');
if (!$bin) {
throw new Exception(
pht(
'Unable to find `%s` in %s!',
'hg',
'$PATH'));
}
$env = $this->getCommonEnvironment($viewer);
$input = PhabricatorStartup::getRawInput();
$cmd = $request->getStr('cmd');
$args = $this->getMercurialArguments();
$args = $this->formatMercurialArguments($cmd, $args);
if (strlen($input)) {
$input = strlen($input)."\n".$input."0\n";
}
$command = csprintf(
'%s -R %s serve --stdio',
$bin,
$repository->getLocalPath());
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
list($err, $stdout, $stderr) = id(new ExecFuture('%C', $command))
->setEnv($env, true)
->setCWD($repository->getLocalPath())
->write("{$cmd}\n{$args}{$input}")
->resolve();
if ($err) {
return new PhabricatorVCSResponse(
500,
pht('Error %d: %s', $err, $stderr));
}
if ($cmd == 'getbundle' ||
$cmd == 'changegroup' ||
$cmd == 'changegroupsubset') {
// We're not completely sure that "changegroup" and "changegroupsubset"
// actually work, they're for very old Mercurial.
$body = gzcompress($stdout);
} else if ($cmd == 'unbundle') {
// This includes diagnostic information and anything echoed by commit
// hooks. We ignore `stdout` since it just has protocol garbage, and
// substitute `stderr`.
$body = strlen($stderr)."\n".$stderr;
} else {
list($length, $body) = explode("\n", $stdout, 2);
if ($cmd == 'capabilities') {
$body = DiffusionMercurialWireProtocol::filterBundle2Capability($body);
}
}
return id(new DiffusionMercurialResponse())->setContent($body);
}
private function getMercurialArguments() {
// Mercurial sends arguments in HTTP headers. "Why?", you might wonder,
// "Why would you do this?".
$args_raw = array();
for ($ii = 1;; $ii++) {
$header = 'HTTP_X_HGARG_'.$ii;
if (!array_key_exists($header, $_SERVER)) {
break;
}
$args_raw[] = $_SERVER[$header];
}
$args_raw = implode('', $args_raw);
return id(new PhutilQueryStringParser())
->parseQueryString($args_raw);
}
private function formatMercurialArguments($command, array $arguments) {
$spec = DiffusionMercurialWireProtocol::getCommandArgs($command);
$out = array();
// Mercurial takes normal arguments like this:
//
// name <length(value)>
// value
$has_star = false;
foreach ($spec as $arg_key) {
if ($arg_key == '*') {
$has_star = true;
continue;
}
if (isset($arguments[$arg_key])) {
$value = $arguments[$arg_key];
$size = strlen($value);
$out[] = "{$arg_key} {$size}\n{$value}";
unset($arguments[$arg_key]);
}
}
if ($has_star) {
// Mercurial takes arguments for variable argument lists roughly like
// this:
//
// * <count(args)>
// argname1 <length(argvalue1)>
// argvalue1
// argname2 <length(argvalue2)>
// argvalue2
$count = count($arguments);
$out[] = "* {$count}\n";
foreach ($arguments as $key => $value) {
if (in_array($key, $spec)) {
// We already added this argument above, so skip it.
continue;
}
$size = strlen($value);
$out[] = "{$key} {$size}\n{$value}";
}
}
return implode('', $out);
}
private function isValidGitShallowCloneResponse($stdout, $stderr) {
// If you execute `git clone --depth N ...`, git sends a request which
// `git-http-backend` responds to by emitting valid output and then exiting
// with a failure code and an error message. If we ignore this error,
// everything works.
// This is a pretty funky fix: it would be nice to more precisely detect
// that a request is a `--depth N` clone request, but we don't have any code
// to decode protocol frames yet. Instead, look for reasonable evidence
// in the error and output that we're looking at a `--depth` clone.
// For evidence this isn't completely crazy, see:
// https://github.com/schacon/grack/pull/7
$stdout_regexp = '(^Content-Type: application/x-git-upload-pack-result)m';
$stderr_regexp = '(The remote end hung up unexpectedly)';
$has_pack = preg_match($stdout_regexp, $stdout);
$is_hangup = preg_match($stderr_regexp, $stderr);
return $has_pack && $is_hangup;
}
private function getCommonEnvironment(PhabricatorUser $viewer) {
$remote_address = $this->getRequest()->getRemoteAddress();
return array(
DiffusionCommitHookEngine::ENV_USER => $viewer->getUsername(),
DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS => $remote_address,
DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL => 'http',
);
}
private function validateGitLFSRequest(
PhabricatorRepository $repository,
PhabricatorUser $viewer) {
if (!$this->getIsGitLFSRequest()) {
return null;
}
if (!$repository->canUseGitLFS()) {
return new PhabricatorVCSResponse(
403,
pht(
'The requested repository ("%s") does not support Git LFS.',
$repository->getDisplayName()));
}
// If this is using an LFS token, sanity check that we're using it on the
// correct repository. This shouldn't really matter since the user could
// just request a proper token anyway, but it suspicious and should not
// be permitted.
$token = $this->getGitLFSToken();
if ($token) {
$resource = $token->getTokenResource();
if ($resource !== $repository->getPHID()) {
return new PhabricatorVCSResponse(
403,
pht(
'The authentication token provided in the request is bound to '.
'a different repository than the requested repository ("%s").',
$repository->getDisplayName()));
}
}
return null;
}
private function serveGitLFSRequest(
PhabricatorRepository $repository,
PhabricatorUser $viewer) {
if (!$this->getIsGitLFSRequest()) {
throw new Exception(pht('This is not a Git LFS request!'));
}
$path = $this->getGitLFSRequestPath($repository);
$matches = null;
if (preg_match('(^upload/(.*)\z)', $path, $matches)) {
$oid = $matches[1];
return $this->serveGitLFSUploadRequest($repository, $viewer, $oid);
} else if ($path == 'objects/batch') {
return $this->serveGitLFSBatchRequest($repository, $viewer);
} else {
return DiffusionGitLFSResponse::newErrorResponse(
404,
pht(
'Git LFS operation "%s" is not supported by this server.',
$path));
}
}
private function serveGitLFSBatchRequest(
PhabricatorRepository $repository,
PhabricatorUser $viewer) {
$input = $this->getGitLFSInput();
$operation = idx($input, 'operation');
switch ($operation) {
case 'upload':
$want_upload = true;
break;
case 'download':
$want_upload = false;
break;
default:
return DiffusionGitLFSResponse::newErrorResponse(
404,
pht(
'Git LFS batch operation "%s" is not supported by this server.',
$operation));
}
$objects = idx($input, 'objects', array());
$hashes = array();
foreach ($objects as $object) {
$hashes[] = idx($object, 'oid');
}
if ($hashes) {
$refs = id(new PhabricatorRepositoryGitLFSRefQuery())
->setViewer($viewer)
->withRepositoryPHIDs(array($repository->getPHID()))
->withObjectHashes($hashes)
->execute();
$refs = mpull($refs, null, 'getObjectHash');
} else {
$refs = array();
}
$file_phids = mpull($refs, 'getFilePHID');
if ($file_phids) {
$files = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
} else {
$files = array();
}
$authorization = null;
$output = array();
foreach ($objects as $object) {
$oid = idx($object, 'oid');
$size = idx($object, 'size');
$ref = idx($refs, $oid);
$error = null;
// NOTE: If we already have a ref for this object, we only emit a
// "download" action. The client should not upload the file again.
$actions = array();
if ($ref) {
$file = idx($files, $ref->getFilePHID());
if ($file) {
// Git LFS may prompt users for authentication if the action does
// not provide an "Authorization" header and does not have a query
// parameter named "token". See here for discussion:
// <https://github.com/github/git-lfs/issues/1088>
$no_authorization = 'Basic '.base64_encode('none');
$get_uri = $file->getCDNURI('data');
$actions['download'] = array(
'href' => $get_uri,
'header' => array(
'Authorization' => $no_authorization,
'X-Phabricator-Request-Type' => 'git-lfs',
),
);
} else {
$error = array(
'code' => 404,
'message' => pht(
'Object "%s" was previously uploaded, but no longer exists '.
'on this server.',
$oid),
);
}
} else if ($want_upload) {
if (!$authorization) {
// Here, we could reuse the existing authorization if we have one,
// but it's a little simpler to just generate a new one
// unconditionally.
$authorization = $this->newGitLFSHTTPAuthorization(
$repository,
$viewer,
$operation);
}
$put_uri = $repository->getGitLFSURI("info/lfs/upload/{$oid}");
$actions['upload'] = array(
'href' => $put_uri,
'header' => array(
'Authorization' => $authorization,
'X-Phabricator-Request-Type' => 'git-lfs',
),
);
}
$object = array(
'oid' => $oid,
'size' => $size,
);
if ($actions) {
$object['actions'] = $actions;
}
if ($error) {
$object['error'] = $error;
}
$output[] = $object;
}
$output = array(
'objects' => $output,
);
return id(new DiffusionGitLFSResponse())
->setContent($output);
}
private function serveGitLFSUploadRequest(
PhabricatorRepository $repository,
PhabricatorUser $viewer,
$oid) {
$ref = id(new PhabricatorRepositoryGitLFSRefQuery())
->setViewer($viewer)
->withRepositoryPHIDs(array($repository->getPHID()))
->withObjectHashes(array($oid))
->executeOne();
if ($ref) {
return DiffusionGitLFSResponse::newErrorResponse(
405,
pht(
'Content for object "%s" is already known to this server. It can '.
'not be uploaded again.',
$oid));
}
// Remove the execution time limit because uploading large files may take
// a while.
set_time_limit(0);
$request_stream = new AphrontRequestStream();
$request_iterator = $request_stream->getIterator();
$hashing_iterator = id(new PhutilHashingIterator($request_iterator))
->setAlgorithm('sha256');
$source = id(new PhabricatorIteratorFileUploadSource())
->setName('lfs-'.$oid)
->setViewPolicy(PhabricatorPolicies::POLICY_NOONE)
->setIterator($hashing_iterator);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$file = $source->uploadFile();
unset($unguarded);
$hash = $hashing_iterator->getHash();
if ($hash !== $oid) {
return DiffusionGitLFSResponse::newErrorResponse(
400,
pht(
'Uploaded data is corrupt or invalid. Expected hash "%s", actual '.
'hash "%s".',
$oid,
$hash));
}
$ref = id(new PhabricatorRepositoryGitLFSRef())
->setRepositoryPHID($repository->getPHID())
->setObjectHash($hash)
->setByteSize($file->getByteSize())
->setAuthorPHID($viewer->getPHID())
->setFilePHID($file->getPHID());
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
// Attach the file to the repository to give users permission
// to access it.
$file->attachToObject($repository->getPHID());
$ref->save();
unset($unguarded);
// This is just a plain HTTP 200 with no content, which is what `git lfs`
// expects.
return new DiffusionGitLFSResponse();
}
private function newGitLFSHTTPAuthorization(
PhabricatorRepository $repository,
PhabricatorUser $viewer,
$operation) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$authorization = DiffusionGitLFSTemporaryTokenType::newHTTPAuthorization(
$repository,
$viewer,
$operation);
unset($unguarded);
return $authorization;
}
private function getGitLFSRequestPath(PhabricatorRepository $repository) {
$request_path = $this->getRequestDirectoryPath($repository);
$matches = null;
if (preg_match('(^/info/lfs(?:\z|/)(.*))', $request_path, $matches)) {
return $matches[1];
}
return null;
}
private function getGitLFSInput() {
if (!$this->gitLFSInput) {
$input = PhabricatorStartup::getRawInput();
$input = phutil_json_decode($input);
$this->gitLFSInput = $input;
}
return $this->gitLFSInput;
}
private function isGitLFSReadOnlyRequest(PhabricatorRepository $repository) {
if (!$this->getIsGitLFSRequest()) {
return false;
}
$path = $this->getGitLFSRequestPath($repository);
if ($path === 'objects/batch') {
$input = $this->getGitLFSInput();
$operation = idx($input, 'operation');
switch ($operation) {
case 'download':
return true;
default:
return false;
}
}
return false;
}
}
diff --git a/src/applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php b/src/applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php
index 712b7ae15..088e5dc71 100644
--- a/src/applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php
+++ b/src/applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php
@@ -1,179 +1,178 @@
<?php
final class DiffusionDoorkeeperCommitFeedStoryPublisher
extends DoorkeeperFeedStoryPublisher {
private $auditRequests;
private $activePHIDs;
private $passivePHIDs;
private function getAuditRequests() {
return $this->auditRequests;
}
public function canPublishStory(PhabricatorFeedStory $story, $object) {
return
($story instanceof PhabricatorApplicationTransactionFeedStory) &&
($object instanceof PhabricatorRepositoryCommit);
}
public function isStoryAboutObjectCreation($object) {
// TODO: Although creation stories exist, they currently don't have a
// primary object PHID set, so they'll never make it here because they
// won't pass `canPublishStory()`.
return false;
}
public function isStoryAboutObjectClosure($object) {
// TODO: This isn't quite accurate, but pretty close: check if this story
// is a close (which clearly is about object closure) or is an "Accept" and
// the commit is fully audited (which is almost certainly a closure).
// After ApplicationTransactions, we could annotate feed stories more
// explicitly.
$story = $this->getFeedStory();
$xaction = $story->getPrimaryTransaction();
switch ($xaction->getTransactionType()) {
case PhabricatorAuditActionConstants::ACTION:
switch ($xaction->getNewValue()) {
case PhabricatorAuditActionConstants::CLOSE:
return true;
case PhabricatorAuditActionConstants::ACCEPT:
if ($object->isAuditStatusAudited()) {
return true;
}
break;
}
}
return false;
}
public function willPublishStory($commit) {
$requests = id(new DiffusionCommitQuery())
->setViewer($this->getViewer())
->withPHIDs(array($commit->getPHID()))
->needAuditRequests(true)
->executeOne()
->getAudits();
// TODO: This is messy and should be generalized, but we don't have a good
// query for it yet. Since we run in the daemons, just do the easiest thing
// we can for the moment. Figure out who all of the "active" (need to
// audit) and "passive" (no action necessary) users are.
$auditor_phids = mpull($requests, 'getAuditorPHID');
$objects = id(new PhabricatorObjectQuery())
->setViewer($this->getViewer())
->withPHIDs($auditor_phids)
->execute();
$active = array();
$passive = array();
foreach ($requests as $request) {
$status = $request->getAuditStatus();
$object = idx($objects, $request->getAuditorPHID());
if (!$object) {
continue;
}
$request_phids = array();
if ($object instanceof PhabricatorUser) {
$request_phids = array($object->getPHID());
} else if ($object instanceof PhabricatorOwnersPackage) {
$request_phids = PhabricatorOwnersOwner::loadAffiliatedUserPHIDs(
array($object->getID()));
} else if ($object instanceof PhabricatorProject) {
$project = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->withIDs(array($object->getID()))
->needMembers(true)
->executeOne();
$request_phids = $project->getMemberPHIDs();
} else {
// Dunno what this is.
$request_phids = array();
}
switch ($status) {
case PhabricatorAuditStatusConstants::AUDIT_REQUIRED:
case PhabricatorAuditStatusConstants::AUDIT_REQUESTED:
case PhabricatorAuditStatusConstants::CONCERNED:
$active += array_fuse($request_phids);
break;
default:
$passive += array_fuse($request_phids);
break;
}
}
// Remove "Active" users from the "Passive" list.
$passive = array_diff_key($passive, $active);
$this->activePHIDs = $active;
$this->passivePHIDs = $passive;
$this->auditRequests = $requests;
return $commit;
}
public function getOwnerPHID($object) {
return $object->getAuthorPHID();
}
public function getActiveUserPHIDs($object) {
return $this->activePHIDs;
}
public function getPassiveUserPHIDs($object) {
return $this->passivePHIDs;
}
public function getCCUserPHIDs($object) {
return PhabricatorSubscribersQuery::loadSubscribersForPHID(
$object->getPHID());
}
public function getObjectTitle($object) {
$prefix = $this->getTitlePrefix($object);
$repository = $object->getRepository();
$name = $repository->formatCommitName($object->getCommitIdentifier());
$title = $object->getSummary();
return ltrim("{$prefix} {$name}: {$title}");
}
public function getObjectURI($object) {
$repository = $object->getRepository();
$name = $repository->formatCommitName($object->getCommitIdentifier());
return PhabricatorEnv::getProductionURI('/'.$name);
}
public function getObjectDescription($object) {
$data = $object->loadCommitData();
if ($data) {
return $data->getCommitMessage();
}
return null;
}
public function isObjectClosed($object) {
return $object->getAuditStatusObject()->getIsClosed();
}
public function getResponsibilityTitle($object) {
$prefix = $this->getTitlePrefix($object);
return pht('%s Audit', $prefix);
}
private function getTitlePrefix(PhabricatorRepositoryCommit $commit) {
- $prefix_key = 'metamta.diffusion.subject-prefix';
- return PhabricatorEnv::getEnvConfig($prefix_key);
+ return pht('[Diffusion]');
}
}
diff --git a/src/applications/diffusion/engine/DiffusionCommitTimelineEngine.php b/src/applications/diffusion/engine/DiffusionCommitTimelineEngine.php
new file mode 100644
index 000000000..49914c4b4
--- /dev/null
+++ b/src/applications/diffusion/engine/DiffusionCommitTimelineEngine.php
@@ -0,0 +1,30 @@
+<?php
+
+final class DiffusionCommitTimelineEngine
+ extends PhabricatorTimelineEngine {
+
+ protected function newTimelineView() {
+ $xactions = $this->getTransactions();
+
+ $path_ids = array();
+ foreach ($xactions as $xaction) {
+ if ($xaction->hasComment()) {
+ $path_id = $xaction->getComment()->getPathID();
+ if ($path_id) {
+ $path_ids[] = $path_id;
+ }
+ }
+ }
+
+ $path_map = array();
+ if ($path_ids) {
+ $path_map = id(new DiffusionPathQuery())
+ ->withPathIDs($path_ids)
+ ->execute();
+ $path_map = ipull($path_map, 'path', 'id');
+ }
+
+ return id(new PhabricatorAuditTransactionView())
+ ->setPathMap($path_map);
+ }
+}
diff --git a/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php
index f2a57f7dd..1d22d99a7 100644
--- a/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php
+++ b/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php
@@ -1,728 +1,747 @@
<?php
final class DiffusionRepositoryBasicsManagementPanel
extends DiffusionRepositoryManagementPanel {
const PANELKEY = 'basics';
// c4science customization
public function allowAccess() {
return true;
}
public function getManagementPanelLabel() {
return pht('Basics');
}
public function getManagementPanelOrder() {
return 100;
}
public function getManagementPanelIcon() {
return 'fa-code';
}
protected function getEditEngineFieldKeys() {
return array(
'name',
'callsign',
'shortName',
'description',
'projectPHIDs',
);
}
public function buildManagementPanelCurtain() {
$repository = $this->getRepository();
$viewer = $this->getViewer();
$action_list = id(new PhabricatorActionListView())
->setViewer($viewer);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$repository,
PhabricatorPolicyCapability::CAN_EDIT);
$edit_uri = $this->getEditPageURI();
$activate_uri = $repository->getPathURI('edit/activate/');
$delete_uri = $repository->getPathURI('edit/delete/');
$encoding_uri = $this->getEditPageURI('encoding');
$dangerous_uri = $repository->getPathURI('edit/dangerous/');
$enormous_uri = $repository->getPathURI('edit/enormous/');
$update_uri = $repository->getPathURI('edit/update/');
if ($repository->isTracked()) {
$activate_icon = 'fa-ban';
$activate_label = pht('Deactivate Repository');
} else {
$activate_icon = 'fa-check';
$activate_label = pht('Activate Repository');
}
$should_dangerous = $repository->shouldAllowDangerousChanges();
if ($should_dangerous) {
$dangerous_icon = 'fa-shield';
$dangerous_name = pht('Prevent Dangerous Changes');
$can_dangerous = $can_edit;
} else {
$dangerous_icon = 'fa-exclamation-triangle';
$dangerous_name = pht('Allow Dangerous Changes');
$can_dangerous = ($can_edit && $repository->canAllowDangerousChanges());
}
$should_enormous = $repository->shouldAllowEnormousChanges();
if ($should_enormous) {
$enormous_icon = 'fa-shield';
$enormous_name = pht('Prevent Enormous Changes');
$can_enormous = $can_edit;
} else {
$enormous_icon = 'fa-exclamation-triangle';
$enormous_name = pht('Allow Enormous Changes');
$can_enormous = ($can_edit && $repository->canAllowEnormousChanges());
}
$action_list->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Basic Information'))
->setHref($edit_uri)
->setIcon('fa-pencil')
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$action_list->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Text Encoding'))
->setIcon('fa-text-width')
->setHref($encoding_uri)
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$action_list->addAction(
id(new PhabricatorActionView())
->setName($dangerous_name)
->setHref($dangerous_uri)
->setIcon($dangerous_icon)
->setDisabled(!$can_dangerous)
->setWorkflow(true));
$action_list->addAction(
id(new PhabricatorActionView())
->setName($enormous_name)
->setHref($enormous_uri)
->setIcon($enormous_icon)
->setDisabled(!$can_enormous)
->setWorkflow(true));
$action_list->addAction(
id(new PhabricatorActionView())
->setName($activate_label)
->setHref($activate_uri)
->setIcon($activate_icon)
->setDisabled(!$can_edit)
->setWorkflow(true));
$action_list->addAction(
id(new PhabricatorActionView())
->setName(pht('Update Now'))
->setHref($update_uri)
->setIcon('fa-refresh')
->setWorkflow(true)
->setDisabled(!$can_edit));
// c4science customization
// $action_list->addAction(
// id(new PhabricatorActionView())
// ->setType(PhabricatorActionView::TYPE_DIVIDER));
// c4science customization
// $action_list->addAction(
// id(new PhabricatorActionView())
// ->setName(pht('Delete Repository'))
// ->setHref($delete_uri)
// ->setIcon('fa-times')
// ->setColor(PhabricatorActionView::RED)
// ->setDisabled(true)
// ->setWorkflow(true));
+ id(new PhabricatorActionView())
+ ->setName(pht('Update Now'))
+ ->setHref($update_uri)
+ ->setIcon('fa-refresh')
+ ->setWorkflow(true)
+ ->setDisabled(!$can_edit));
+
+ $action_list->addAction(
+ id(new PhabricatorActionView())
+ ->setType(PhabricatorActionView::TYPE_DIVIDER));
+
+ $action_list->addAction(
+ id(new PhabricatorActionView())
+ ->setName(pht('Delete Repository'))
+ ->setHref($delete_uri)
+ ->setIcon('fa-times')
+ ->setColor(PhabricatorActionView::RED)
+ ->setDisabled(true)
+ ->setWorkflow(true));
return $this->newCurtainView()
->setActionList($action_list);
}
public function buildManagementPanelContent() {
$basics = $this->buildBasics();
$basics = $this->newBox(pht('Properties'), $basics);
$repository = $this->getRepository();
$is_new = $repository->isNewlyInitialized();
$info_view = null;
if ($is_new) {
$messages = array();
$messages[] = pht(
'This newly created repository is not active yet. Configure policies, '.
'options, and URIs. When ready, %s the repository.',
phutil_tag('strong', array(), pht('Activate')));
if ($repository->isHosted()) {
$messages[] = pht(
'If activated now, this repository will become a new hosted '.
'repository. To observe an existing repository instead, configure '.
'it in the %s panel.',
phutil_tag('strong', array(), pht('URIs')));
} else {
$messages[] = pht(
'If activated now, this repository will observe an existing remote '.
'repository and begin importing changes.');
}
$info_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->setErrors($messages);
}
$description = $this->buildDescription();
if ($description) {
$description = $this->newBox(pht('Description'), $description);
}
$status = $this->buildStatus();
return array($info_view, $basics, $description, $status);
}
private function buildBasics() {
$repository = $this->getRepository();
$viewer = $this->getViewer();
$view = id(new PHUIPropertyListView())
->setViewer($viewer);
$name = $repository->getName();
$view->addProperty(pht('Name'), $name);
$type = PhabricatorRepositoryType::getNameForRepositoryType(
$repository->getVersionControlSystem());
$view->addProperty(pht('Type'), $type);
$callsign = $repository->getCallsign();
if (!strlen($callsign)) {
$callsign = phutil_tag('em', array(), pht('No Callsign'));
}
$view->addProperty(pht('Callsign'), $callsign);
$short_name = $repository->getRepositorySlug();
if ($short_name === null) {
$short_name = phutil_tag('em', array(), pht('No Short Name'));
}
$view->addProperty(pht('Short Name'), $short_name);
$encoding = $repository->getDetail('encoding');
if (!$encoding) {
$encoding = phutil_tag('em', array(), pht('Use Default (UTF-8)'));
}
$view->addProperty(pht('Encoding'), $encoding);
$can_dangerous = $repository->canAllowDangerousChanges();
if (!$can_dangerous) {
$dangerous = phutil_tag('em', array(), pht('Not Preventable'));
} else {
$should_dangerous = $repository->shouldAllowDangerousChanges();
if ($should_dangerous) {
$dangerous = pht('Allowed');
} else {
$dangerous = pht('Not Allowed');
}
}
$view->addProperty(pht('Dangerous Changes'), $dangerous);
$can_enormous = $repository->canAllowEnormousChanges();
if (!$can_enormous) {
$enormous = phutil_tag('em', array(), pht('Not Preventable'));
} else {
$should_enormous = $repository->shouldAllowEnormousChanges();
if ($should_enormous) {
$enormous = pht('Allowed');
} else {
$enormous = pht('Not Allowed');
}
}
$view->addProperty(pht('Enormous Changes'), $enormous);
return $view;
}
private function buildDescription() {
$repository = $this->getRepository();
$viewer = $this->getViewer();
$description = $repository->getDetail('description');
$view = id(new PHUIPropertyListView())
->setViewer($viewer);
if (!strlen($description)) {
return null;
} else {
$description = new PHUIRemarkupView($viewer, $description);
}
$view->addTextContent($description);
return $view;
}
private function buildStatus() {
$repository = $this->getRepository();
$viewer = $this->getViewer();
$view = id(new PHUIPropertyListView())
->setViewer($viewer);
$view->addProperty(
pht('Update Frequency'),
$this->buildRepositoryUpdateInterval($repository));
$messages = $this->loadStatusMessages($repository);
$status = $this->buildRepositoryStatus($repository, $messages);
$raw_error = $this->buildRepositoryRawError($repository, $messages);
$view->addProperty(pht('Status'), $status);
if ($raw_error) {
$view->addSectionHeader(pht('Raw Error'));
$view->addTextContent($raw_error);
}
return $this->newBox(pht('Status'), $view);
}
private function buildRepositoryUpdateInterval(
PhabricatorRepository $repository) {
$smart_wait = $repository->loadUpdateInterval();
$doc_href = PhabricatorEnv::getDoclink(
'Diffusion User Guide: Repository Updates');
return array(
phutil_format_relative_time_detailed($smart_wait),
" \xC2\xB7 ",
phutil_tag(
'a',
array(
'href' => $doc_href,
'target' => '_blank',
),
pht('Learn More')),
);
}
private function buildRepositoryStatus(
PhabricatorRepository $repository,
array $messages) {
$viewer = $this->getViewer();
$is_cluster = $repository->getAlmanacServicePHID();
$view = new PHUIStatusListView();
if ($repository->isTracked()) {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Repository Active')));
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'bluegrey')
->setTarget(pht('Repository Inactive'))
->setNote(
pht('Activate this repository to begin or resume import.')));
return $view;
}
$binaries = array();
$svnlook_check = false;
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$binaries[] = 'git';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$binaries[] = 'svn';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$binaries[] = 'hg';
break;
}
if ($repository->isHosted()) {
$proto_https = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTPS;
$proto_http = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTP;
$can_http = $repository->canServeProtocol($proto_http, false) ||
$repository->canServeProtocol($proto_https, false);
if ($can_http) {
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$binaries[] = 'git-http-backend';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$binaries[] = 'svnserve';
$binaries[] = 'svnadmin';
$binaries[] = 'svnlook';
$svnlook_check = true;
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$binaries[] = 'hg';
break;
}
}
$proto_ssh = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH;
$can_ssh = $repository->canServeProtocol($proto_ssh, false);
if ($can_ssh) {
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$binaries[] = 'git-receive-pack';
$binaries[] = 'git-upload-pack';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$binaries[] = 'svnserve';
$binaries[] = 'svnadmin';
$binaries[] = 'svnlook';
$svnlook_check = true;
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$binaries[] = 'hg';
break;
}
}
}
$binaries = array_unique($binaries);
if (!$is_cluster) {
// We're only checking for binaries if we aren't running with a cluster
// configuration. In theory, we could check for binaries on the
// repository host machine, but we'd need to make this more complicated
// to do that.
foreach ($binaries as $binary) {
$where = Filesystem::resolveBinary($binary);
if (!$where) {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(
pht('Missing Binary %s', phutil_tag('tt', array(), $binary)))
->setNote(pht(
"Unable to find this binary in the webserver's PATH. You may ".
"need to configure %s.",
$this->getEnvConfigLink())));
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(
pht('Found Binary %s', phutil_tag('tt', array(), $binary)))
->setNote(phutil_tag('tt', array(), $where)));
}
}
// This gets checked generically above. However, for svn commit hooks, we
// need this to be in environment.append-paths because subversion strips
// PATH.
if ($svnlook_check) {
$where = Filesystem::resolveBinary('svnlook');
if ($where) {
$path = substr($where, 0, strlen($where) - strlen('svnlook'));
$dirs = PhabricatorEnv::getEnvConfig('environment.append-paths');
$in_path = false;
foreach ($dirs as $dir) {
if (Filesystem::isDescendant($path, $dir)) {
$in_path = true;
break;
}
}
if (!$in_path) {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(
pht('Missing Binary %s', phutil_tag('tt', array(), $binary)))
->setNote(pht(
'Unable to find this binary in `%s`. '.
'You need to configure %s and include %s.',
'environment.append-paths',
$this->getEnvConfigLink(),
$path)));
}
}
}
}
$doc_href = PhabricatorEnv::getDoclink('Managing Daemons with phd');
$daemon_instructions = pht(
'Use %s to start daemons. See %s.',
phutil_tag('tt', array(), 'bin/phd start'),
phutil_tag(
'a',
array(
'href' => $doc_href,
),
pht('Managing Daemons with phd')));
$pull_daemon = id(new PhabricatorDaemonLogQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE)
->withDaemonClasses(array('PhabricatorRepositoryPullLocalDaemon'))
->setLimit(1)
->execute();
if ($pull_daemon) {
// TODO: In a cluster environment, we need a daemon on this repository's
// host, specifically, and we aren't checking for that right now. This
// is a reasonable proxy for things being more-or-less correctly set up,
// though.
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Pull Daemon Running')));
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(pht('Pull Daemon Not Running'))
->setNote($daemon_instructions));
}
$task_daemon = id(new PhabricatorDaemonLogQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE)
->withDaemonClasses(array('PhabricatorTaskmasterDaemon'))
->setLimit(1)
->execute();
if ($task_daemon) {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Task Daemon Running')));
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(pht('Task Daemon Not Running'))
->setNote($daemon_instructions));
}
if ($is_cluster) {
// Just omit this status check for now in cluster environments. We
// could make a service call and pull it from the repository host
// eventually.
} else if ($repository->usesLocalWorkingCopy()) {
$local_parent = dirname($repository->getLocalPath());
if (Filesystem::pathExists($local_parent)) {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Storage Directory OK'))
->setNote(phutil_tag('tt', array(), $local_parent)));
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(pht('No Storage Directory'))
->setNote(
pht(
'Storage directory %s does not exist, or is not readable by '.
'the webserver. Create this directory or make it readable.',
phutil_tag('tt', array(), $local_parent))));
return $view;
}
$local_path = $repository->getLocalPath();
$message = idx($messages, PhabricatorRepositoryStatusMessage::TYPE_INIT);
if ($message) {
switch ($message->getStatusCode()) {
case PhabricatorRepositoryStatusMessage::CODE_ERROR:
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(pht('Initialization Error'))
->setNote($message->getParameter('message')));
return $view;
case PhabricatorRepositoryStatusMessage::CODE_OKAY:
if (Filesystem::pathExists($local_path)) {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Working Copy OK'))
->setNote(phutil_tag('tt', array(), $local_path)));
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(pht('Working Copy Error'))
->setNote(
pht(
'Working copy %s has been deleted, or is not '.
'readable by the webserver. Make this directory '.
'readable. If it has been deleted, the daemons should '.
'restore it automatically.',
phutil_tag('tt', array(), $local_path))));
return $view;
}
break;
default:
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_CLOCK, 'green')
->setTarget(pht('Initializing Working Copy'))
->setNote(pht('Daemons are initializing the working copy.')));
return $view;
}
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_CLOCK, 'orange')
->setTarget(pht('No Working Copy Yet'))
->setNote(
pht('Waiting for daemons to build a working copy.')));
return $view;
}
}
$message = idx($messages, PhabricatorRepositoryStatusMessage::TYPE_FETCH);
if ($message) {
switch ($message->getStatusCode()) {
case PhabricatorRepositoryStatusMessage::CODE_ERROR:
$message = $message->getParameter('message');
$suggestion = null;
if (preg_match('/Permission denied \(publickey\)./', $message)) {
$suggestion = pht(
'Public Key Error: This error usually indicates that the '.
'keypair you have configured does not have permission to '.
'access the repository.');
}
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_WARNING, 'red')
->setTarget(pht('Update Error'))
->setNote($suggestion));
return $view;
case PhabricatorRepositoryStatusMessage::CODE_OKAY:
$ago = (PhabricatorTime::getNow() - $message->getEpoch());
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Updates OK'))
->setNote(
pht(
'Last updated %s (%s ago).',
phabricator_datetime($message->getEpoch(), $viewer),
phutil_format_relative_time_detailed($ago))));
break;
}
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_CLOCK, 'orange')
->setTarget(pht('Waiting For Update'))
->setNote(
pht('Waiting for daemons to read updates.')));
}
if ($repository->isImporting()) {
$ratio = $repository->loadImportProgress();
$percentage = sprintf('%.2f%%', 100 * $ratio);
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_CLOCK, 'green')
->setTarget(pht('Importing'))
->setNote(
pht('%s Complete', $percentage)));
} else {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green')
->setTarget(pht('Fully Imported')));
}
if (idx($messages, PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE)) {
$view->addItem(
id(new PHUIStatusItemView())
->setIcon(PHUIStatusItemView::ICON_UP, 'indigo')
->setTarget(pht('Prioritized'))
->setNote(pht('This repository will be updated soon!')));
}
return $view;
}
private function buildRepositoryRawError(
PhabricatorRepository $repository,
array $messages) {
$viewer = $this->getViewer();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$repository,
PhabricatorPolicyCapability::CAN_EDIT);
$raw_error = null;
$message = idx($messages, PhabricatorRepositoryStatusMessage::TYPE_FETCH);
if ($message) {
switch ($message->getStatusCode()) {
case PhabricatorRepositoryStatusMessage::CODE_ERROR:
$raw_error = $message->getParameter('message');
break;
}
}
if ($raw_error !== null) {
if (!$can_edit) {
$raw_message = pht(
'You must be able to edit a repository to see raw error messages '.
'because they sometimes disclose sensitive information.');
$raw_message = phutil_tag('em', array(), $raw_message);
} else {
$raw_message = phutil_escape_html_newlines($raw_error);
}
} else {
$raw_message = null;
}
return $raw_message;
}
private function loadStatusMessages(PhabricatorRepository $repository) {
$messages = id(new PhabricatorRepositoryStatusMessage())
->loadAllWhere('repositoryID = %d', $repository->getID());
$messages = mpull($messages, null, 'getStatusType');
return $messages;
}
private function getEnvConfigLink() {
$config_href = '/config/edit/environment.append-paths/';
return phutil_tag(
'a',
array(
'href' => $config_href,
),
'environment.append-paths');
}
}
diff --git a/src/applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php b/src/applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php
index 189930222..789adfbf5 100644
--- a/src/applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php
+++ b/src/applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php
@@ -1,264 +1,268 @@
<?php
final class DiffusionSetPasswordSettingsPanel extends PhabricatorSettingsPanel {
public function isManagementPanel() {
if ($this->getUser()->getIsMailingList()) {
return false;
}
return true;
}
public function getPanelKey() {
return 'vcspassword';
}
public function getPanelName() {
return pht('VCS Password');
}
+ public function getPanelMenuIcon() {
+ return 'fa-code';
+ }
+
public function getPanelGroupKey() {
return PhabricatorSettingsAuthenticationPanelGroup::PANELGROUPKEY;
}
public function isEnabled() {
return PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth');
}
public function processRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$user = $this->getUser();
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
'/settings/');
$vcs_type = PhabricatorAuthPassword::PASSWORD_TYPE_VCS;
$vcspasswords = id(new PhabricatorAuthPasswordQuery())
->setViewer($viewer)
->withObjectPHIDs(array($user->getPHID()))
->withPasswordTypes(array($vcs_type))
->withIsRevoked(false)
->execute();
if ($vcspasswords) {
$vcspassword = head($vcspasswords);
} else {
$vcspassword = PhabricatorAuthPassword::initializeNewPassword(
$user,
$vcs_type);
}
$panel_uri = $this->getPanelURI('?saved=true');
$errors = array();
$e_password = true;
$e_confirm = true;
$content_source = PhabricatorContentSource::newFromRequest($request);
// NOTE: This test is against $viewer (not $user), so that the error
// message below makes sense in the case that the two are different,
// and because an admin reusing their own password is bad, while
// system agents generally do not have passwords anyway.
$engine = id(new PhabricatorAuthPasswordEngine())
->setViewer($viewer)
->setContentSource($content_source)
->setObject($viewer)
->setPasswordType($vcs_type);
if ($request->isFormPost()) {
if ($request->getBool('remove')) {
if ($vcspassword->getID()) {
$vcspassword->delete();
return id(new AphrontRedirectResponse())->setURI($panel_uri);
}
}
$new_password = $request->getStr('password');
$confirm = $request->getStr('confirm');
$envelope = new PhutilOpaqueEnvelope($new_password);
$confirm_envelope = new PhutilOpaqueEnvelope($confirm);
try {
$engine->checkNewPassword($envelope, $confirm_envelope);
$e_password = null;
$e_confirm = null;
} catch (PhabricatorAuthPasswordException $ex) {
$errors[] = $ex->getMessage();
$e_password = $ex->getPasswordError();
$e_confirm = $ex->getConfirmError();
}
if (!$errors) {
$vcspassword
->setPassword($envelope, $user)
->save();
return id(new AphrontRedirectResponse())->setURI($panel_uri);
}
}
$title = pht('Set VCS Password');
$form = id(new AphrontFormView())
->setUser($viewer)
->appendRemarkupInstructions(
pht(
'To access repositories hosted by Phabricator over HTTP, you must '.
'set a version control password. This password should be unique.'.
"\n\n".
"This password applies to all repositories available over ".
"HTTP."));
if ($vcspassword->getID()) {
$form
->appendChild(
id(new AphrontFormPasswordControl())
->setDisableAutocomplete(true)
->setLabel(pht('Current Password'))
->setDisabled(true)
->setValue('********************'));
} else {
$form
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Current Password'))
->setValue(phutil_tag('em', array(), pht('No Password Set'))));
}
$form
->appendChild(
id(new AphrontFormPasswordControl())
->setDisableAutocomplete(true)
->setName('password')
->setLabel(pht('New VCS Password'))
->setError($e_password))
->appendChild(
id(new AphrontFormPasswordControl())
->setDisableAutocomplete(true)
->setName('confirm')
->setLabel(pht('Confirm VCS Password'))
->setError($e_confirm))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Change Password')));
if (!$vcspassword->getID()) {
$is_serious = PhabricatorEnv::getEnvConfig(
'phabricator.serious-business');
$suggest = Filesystem::readRandomBytes(128);
$suggest = preg_replace('([^A-Za-z0-9/!().,;{}^&*%~])', '', $suggest);
$suggest = substr($suggest, 0, 20);
if ($is_serious) {
$form->appendRemarkupInstructions(
pht(
'Having trouble coming up with a good password? Try this randomly '.
'generated one, made by a computer:'.
"\n\n".
"`%s`",
$suggest));
} else {
$form->appendRemarkupInstructions(
pht(
'Having trouble coming up with a good password? Try this '.
'artisanal password, hand made in small batches by our expert '.
'craftspeople: '.
"\n\n".
"`%s`",
$suggest));
}
}
$hash_envelope = new PhutilOpaqueEnvelope($vcspassword->getPasswordHash());
$form->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('Current Algorithm'))
->setValue(
PhabricatorPasswordHasher::getCurrentAlgorithmName($hash_envelope)));
$form->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('Best Available Algorithm'))
->setValue(PhabricatorPasswordHasher::getBestAlgorithmName()));
if (strlen($hash_envelope->openEnvelope())) {
try {
$can_upgrade = PhabricatorPasswordHasher::canUpgradeHash(
$hash_envelope);
} catch (PhabricatorPasswordHasherUnavailableException $ex) {
$can_upgrade = false;
$errors[] = pht(
'Your VCS password is currently hashed using an algorithm which is '.
'no longer available on this install.');
$errors[] = pht(
'Because the algorithm implementation is missing, your password '.
'can not be used.');
$errors[] = pht(
'You can set a new password to replace the old password.');
}
if ($can_upgrade) {
$errors[] = pht(
'The strength of your stored VCS password hash can be upgraded. '.
'To upgrade, either: use the password to authenticate with a '.
'repository; or change your password.');
}
}
$object_box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
->setForm($form)
->setFormErrors($errors);
$remove_form = id(new AphrontFormView())
->setUser($viewer);
if ($vcspassword->getID()) {
$remove_form
->addHiddenInput('remove', true)
->appendRemarkupInstructions(
pht(
'You can remove your VCS password, which will prevent your '.
'account from accessing repositories.'))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Remove Password')));
} else {
$remove_form->appendRemarkupInstructions(
pht(
'You do not currently have a VCS password set. If you set one, you '.
'can remove it here later.'));
}
$remove_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Remove VCS Password'))
->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
->setForm($remove_form);
$saved = null;
if ($request->getBool('saved')) {
$saved = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->setTitle(pht('Password Updated'))
->appendChild(pht('Your VCS password has been updated.'));
}
return array(
$saved,
$object_box,
$remove_box,
);
}
}
diff --git a/src/applications/diffusion/protocol/DiffusionRepositoryClusterEngine.php b/src/applications/diffusion/protocol/DiffusionRepositoryClusterEngine.php
index 57ee5476b..136282b31 100644
--- a/src/applications/diffusion/protocol/DiffusionRepositoryClusterEngine.php
+++ b/src/applications/diffusion/protocol/DiffusionRepositoryClusterEngine.php
@@ -1,904 +1,905 @@
<?php
/**
* Manages repository synchronization for cluster repositories.
*
* @task config Configuring Synchronization
* @task sync Cluster Synchronization
* @task internal Internals
*/
final class DiffusionRepositoryClusterEngine extends Phobject {
private $repository;
private $viewer;
private $logger;
private $clusterWriteLock;
private $clusterWriteVersion;
private $clusterWriteOwner;
/* -( Configuring Synchronization )---------------------------------------- */
public function setRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
public function getRepository() {
return $this->repository;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setLog(DiffusionRepositoryClusterEngineLogInterface $log) {
$this->logger = $log;
return $this;
}
/* -( Cluster Synchronization )-------------------------------------------- */
/**
* Synchronize repository version information after creating a repository.
*
* This initializes working copy versions for all currently bound devices to
* 0, so that we don't get stuck making an ambiguous choice about which
* devices are leaders when we later synchronize before a read.
*
* @task sync
*/
public function synchronizeWorkingCopyAfterCreation() {
if (!$this->shouldEnableSynchronization(false)) {
return;
}
$repository = $this->getRepository();
$repository_phid = $repository->getPHID();
$service = $repository->loadAlmanacService();
if (!$service) {
throw new Exception(pht('Failed to load repository cluster service.'));
}
$bindings = $service->getActiveBindings();
foreach ($bindings as $binding) {
PhabricatorRepositoryWorkingCopyVersion::updateVersion(
$repository_phid,
$binding->getDevicePHID(),
0);
}
return $this;
}
/**
* @task sync
*/
public function synchronizeWorkingCopyAfterHostingChange() {
if (!$this->shouldEnableSynchronization(false)) {
return;
}
$repository = $this->getRepository();
$repository_phid = $repository->getPHID();
$versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions(
$repository_phid);
$versions = mpull($versions, null, 'getDevicePHID');
// After converting a hosted repository to observed, or vice versa, we
// need to reset version numbers because the clocks for observed and hosted
// repositories run on different units.
// We identify all the cluster leaders and reset their version to 0.
// We identify all the cluster followers and demote them.
// This allows the cluster to start over again at version 0 but keep the
// same leaders.
if ($versions) {
$max_version = (int)max(mpull($versions, 'getRepositoryVersion'));
foreach ($versions as $version) {
$device_phid = $version->getDevicePHID();
if ($version->getRepositoryVersion() == $max_version) {
PhabricatorRepositoryWorkingCopyVersion::updateVersion(
$repository_phid,
$device_phid,
0);
} else {
PhabricatorRepositoryWorkingCopyVersion::demoteDevice(
$repository_phid,
$device_phid);
}
}
}
return $this;
}
/**
* @task sync
*/
public function synchronizeWorkingCopyBeforeRead() {
if (!$this->shouldEnableSynchronization(true)) {
return;
}
$repository = $this->getRepository();
$repository_phid = $repository->getPHID();
$device = AlmanacKeys::getLiveDevice();
$device_phid = $device->getPHID();
$viewer = $this->getViewer(); // c4s custo
$read_lock = PhabricatorRepositoryWorkingCopyVersion::getReadLock(
$repository_phid,
$device_phid);
$lock_wait = phutil_units('2 minutes in seconds');
if($viewer->getIsAdmin()) { // c4s custo
$this->logLine(
pht(
'Acquiring read lock for repository "%s" on device "%s"...',
$repository->getDisplayName(),
$device->getName()));
}
try {
$start = PhabricatorTime::getNow();
$read_lock->lock($lock_wait);
$waited = (PhabricatorTime::getNow() - $start);
if ($waited) {
if($viewer->getIsAdmin()) { // c4s custo
$this->logLine(
pht(
'Acquired read lock after %s second(s).',
new PhutilNumber($waited)));
}
} else {
if($viewer->getIsAdmin()) { // c4s custo
$this->logLine(
pht(
'Acquired read lock immediately.'));
}
}
} catch (PhutilLockException $ex) {
throw new PhutilProxyException(
pht(
'Failed to acquire read lock after waiting %s second(s). You '.
'may be able to retry later. (%s)',
new PhutilNumber($lock_wait),
$ex->getHint()),
$ex);
}
$versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions(
$repository_phid);
$versions = mpull($versions, null, 'getDevicePHID');
$this_version = idx($versions, $device_phid);
if ($this_version) {
$this_version = (int)$this_version->getRepositoryVersion();
} else {
- $this_version = 0; // c4s custo (bugfix T2513)
+ $this_version = null;
}
if ($versions) {
// This is the normal case, where we have some version information and
// can identify which nodes are leaders. If the current node is not a
// leader, we want to fetch from a leader and then update our version.
$max_version = (int)max(mpull($versions, 'getRepositoryVersion'));
- if ($max_version > $this_version) {
+ if (($this_version === null) || ($max_version > $this_version)) {
if ($repository->isHosted()) {
$fetchable = array();
foreach ($versions as $version) {
if ($version->getRepositoryVersion() == $max_version) {
$fetchable[] = $version->getDevicePHID();
}
}
+
$this->synchronizeWorkingCopyFromDevices(
$fetchable,
$this_version,
$max_version);
} else {
$this->synchronizeWorkingCopyFromRemote();
}
PhabricatorRepositoryWorkingCopyVersion::updateVersion(
$repository_phid,
$device_phid,
$max_version);
} else {
if($viewer->getIsAdmin()) { // c4s custo
$this->logLine(
pht(
'Device "%s" is already a cluster leader and does not need '.
'to be synchronized.',
$device->getName()));
}
}
$result_version = $max_version;
} else {
// If no version records exist yet, we need to be careful, because we
// can not tell which nodes are leaders.
// There might be several nodes with arbitrary existing data, and we have
// no way to tell which one has the "right" data. If we pick wrong, we
// might erase some or all of the data in the repository.
// Since this is dangerous, we refuse to guess unless there is only one
// device. If we're the only device in the group, we obviously must be
// a leader.
$service = $repository->loadAlmanacService();
if (!$service) {
throw new Exception(pht('Failed to load repository cluster service.'));
}
$bindings = $service->getActiveBindings();
$device_map = array();
foreach ($bindings as $binding) {
$device_map[$binding->getDevicePHID()] = true;
}
if (count($device_map) > 1) {
throw new Exception(
pht(
'Repository "%s" exists on more than one device, but no device '.
'has any repository version information. Phabricator can not '.
'guess which copy of the existing data is authoritative. Promote '.
'a device or see "Ambiguous Leaders" in the documentation.',
$repository->getDisplayName()));
}
if (empty($device_map[$device->getPHID()])) {
throw new Exception(
pht(
'Repository "%s" is being synchronized on device "%s", but '.
'this device is not bound to the corresponding cluster '.
'service ("%s").',
$repository->getDisplayName(),
$device->getName(),
$service->getName()));
}
// The current device is the only device in service, so it must be a
// leader. We can safely have any future nodes which come online read
// from it.
PhabricatorRepositoryWorkingCopyVersion::updateVersion(
$repository_phid,
$device_phid,
0);
$result_version = 0;
}
$read_lock->unlock();
return $result_version;
}
/**
* @task sync
*/
public function synchronizeWorkingCopyBeforeWrite() {
if (!$this->shouldEnableSynchronization(true)) {
return;
}
$repository = $this->getRepository();
$viewer = $this->getViewer();
$repository_phid = $repository->getPHID();
$device = AlmanacKeys::getLiveDevice();
$device_phid = $device->getPHID();
$table = new PhabricatorRepositoryWorkingCopyVersion();
$locked_connection = $table->establishConnection('w');
$write_lock = PhabricatorRepositoryWorkingCopyVersion::getWriteLock(
$repository_phid);
$write_lock->useSpecificConnection($locked_connection);
if($viewer->getIsAdmin()) { // c4s custo
$this->logLine(
pht(
'Acquiring write lock for repository "%s"...',
$repository->getDisplayName()));
}
$lock_wait = phutil_units('2 minutes in seconds');
try {
$write_wait_start = microtime(true);
$start = PhabricatorTime::getNow();
$step_wait = 1;
while (true) {
try {
$write_lock->lock((int)floor($step_wait));
$write_wait_end = microtime(true);
break;
} catch (PhutilLockException $ex) {
$waited = (PhabricatorTime::getNow() - $start);
if ($waited > $lock_wait) {
throw $ex;
}
$this->logActiveWriter($viewer, $repository);
}
// Wait a little longer before the next message we print.
$step_wait = $step_wait + 0.5;
$step_wait = min($step_wait, 3);
}
$waited = (PhabricatorTime::getNow() - $start);
if ($waited) {
if($viewer->getIsAdmin()) { // c4s custo
$this->logLine(
pht(
'Acquired write lock after %s second(s).',
new PhutilNumber($waited)));
}
} else {
if($viewer->getIsAdmin()) { // c4s custo
$this->logLine(
pht(
'Acquired write lock immediately.'));
}
}
} catch (PhutilLockException $ex) {
throw new PhutilProxyException(
pht(
'Failed to acquire write lock after waiting %s second(s). You '.
'may be able to retry later. (%s)',
new PhutilNumber($lock_wait),
$ex->getHint()),
$ex);
}
$versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions(
$repository_phid);
foreach ($versions as $version) {
if (!$version->getIsWriting()) {
continue;
}
throw new Exception(
pht(
'An previous write to this repository was interrupted; refusing '.
'new writes. This issue requires operator intervention to resolve, '.
'see "Write Interruptions" in the "Cluster: Repositories" in the '.
'documentation for instructions.'));
}
$read_wait_start = microtime(true);
try {
$max_version = $this->synchronizeWorkingCopyBeforeRead();
} catch (Exception $ex) {
$write_lock->unlock();
throw $ex;
}
$read_wait_end = microtime(true);
$pid = getmypid();
$hash = Filesystem::readRandomCharacters(12);
$this->clusterWriteOwner = "{$pid}.{$hash}";
PhabricatorRepositoryWorkingCopyVersion::willWrite(
$locked_connection,
$repository_phid,
$device_phid,
array(
'userPHID' => $viewer->getPHID(),
'epoch' => PhabricatorTime::getNow(),
'devicePHID' => $device_phid,
),
$this->clusterWriteOwner);
$this->clusterWriteVersion = $max_version;
$this->clusterWriteLock = $write_lock;
$write_wait = ($write_wait_end - $write_wait_start);
$read_wait = ($read_wait_end - $read_wait_start);
$log = $this->logger;
if ($log) {
$log->writeClusterEngineLogProperty('writeWait', $write_wait);
$log->writeClusterEngineLogProperty('readWait', $read_wait);
}
}
public function synchronizeWorkingCopyAfterDiscovery($new_version) {
if (!$this->shouldEnableSynchronization(true)) {
return;
}
$repository = $this->getRepository();
$repository_phid = $repository->getPHID();
if ($repository->isHosted()) {
return;
}
$viewer = $this->getViewer();
$device = AlmanacKeys::getLiveDevice();
$device_phid = $device->getPHID();
// NOTE: We are not holding a lock here because this method is only called
// from PhabricatorRepositoryDiscoveryEngine, which already holds a device
// lock. Even if we do race here and record an older version, the
// consequences are mild: we only do extra work to correct it later.
$versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions(
$repository_phid);
$versions = mpull($versions, null, 'getDevicePHID');
$this_version = idx($versions, $device_phid);
if ($this_version) {
$this_version = (int)$this_version->getRepositoryVersion();
} else {
- $this_version = -1;
+ $this_version = null;
}
- if ($new_version > $this_version) {
+ if (($this_version === null) || ($new_version > $this_version)) {
PhabricatorRepositoryWorkingCopyVersion::updateVersion(
$repository_phid,
$device_phid,
$new_version);
}
}
/**
* @task sync
*/
public function synchronizeWorkingCopyAfterWrite() {
if (!$this->shouldEnableSynchronization(true)) {
return;
}
if (!$this->clusterWriteLock) {
throw new Exception(
pht(
'Trying to synchronize after write, but not holding a write '.
'lock!'));
}
$repository = $this->getRepository();
$repository_phid = $repository->getPHID();
$device = AlmanacKeys::getLiveDevice();
$device_phid = $device->getPHID();
// It is possible that we've lost the global lock while receiving the push.
// For example, the master database may have been restarted between the
// time we acquired the global lock and now, when the push has finished.
// We wrote a durable lock while we were holding the the global lock,
// essentially upgrading our lock. We can still safely release this upgraded
// lock even if we're no longer holding the global lock.
// If we fail to release the lock, the repository will be frozen until
// an operator can figure out what happened, so we try pretty hard to
// reconnect to the database and release the lock.
$now = PhabricatorTime::getNow();
$duration = phutil_units('5 minutes in seconds');
$try_until = $now + $duration;
$did_release = false;
$already_failed = false;
while (PhabricatorTime::getNow() <= $try_until) {
try {
// NOTE: This means we're still bumping the version when pushes fail. We
// could select only un-rejected events instead to bump a little less
// often.
$new_log = id(new PhabricatorRepositoryPushEventQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withRepositoryPHIDs(array($repository_phid))
->setLimit(1)
->executeOne();
$old_version = $this->clusterWriteVersion;
if ($new_log) {
$new_version = $new_log->getID();
} else {
$new_version = $old_version;
}
PhabricatorRepositoryWorkingCopyVersion::didWrite(
$repository_phid,
$device_phid,
$this->clusterWriteVersion,
$new_version,
$this->clusterWriteOwner);
$did_release = true;
break;
} catch (AphrontConnectionQueryException $ex) {
$connection_exception = $ex;
} catch (AphrontConnectionLostQueryException $ex) {
$connection_exception = $ex;
}
if (!$already_failed) {
$already_failed = true;
$this->logLine(
pht('CRITICAL. Failed to release cluster write lock!'));
$this->logLine(
pht(
'The connection to the master database was lost while receiving '.
'the write.'));
$this->logLine(
pht(
'This process will spend %s more second(s) attempting to '.
'recover, then give up.',
new PhutilNumber($duration)));
}
sleep(1);
}
if ($did_release) {
if ($already_failed) {
$this->logLine(
pht('RECOVERED. Link to master database was restored.'));
}
$viewer = $this->getViewer(); // c4s custo
if($viewer->getIsAdmin()) { // c4s custo
$this->logLine(pht('Released cluster write lock.'));
}
} else {
throw new Exception(
pht(
'Failed to reconnect to master database and release held write '.
'lock ("%s") on device "%s" for repository "%s" after trying '.
'for %s seconds(s). This repository will be frozen.',
$this->clusterWriteOwner,
$device->getName(),
$this->getDisplayName(),
new PhutilNumber($duration)));
}
// We can continue even if we've lost this lock, everything is still
// consistent.
try {
$this->clusterWriteLock->unlock();
} catch (Exception $ex) {
// Ignore.
}
$this->clusterWriteLock = null;
$this->clusterWriteOwner = null;
}
/* -( Internals )---------------------------------------------------------- */
/**
* @task internal
*/
private function shouldEnableSynchronization($require_device) {
$repository = $this->getRepository();
$service_phid = $repository->getAlmanacServicePHID();
if (!$service_phid) {
return false;
}
if (!$repository->supportsSynchronization()) {
return false;
}
if ($require_device) {
$device = AlmanacKeys::getLiveDevice();
if (!$device) {
return false;
}
}
return true;
}
/**
* @task internal
*/
private function synchronizeWorkingCopyFromRemote() {
$repository = $this->getRepository();
$device = AlmanacKeys::getLiveDevice();
$local_path = $repository->getLocalPath();
$fetch_uri = $repository->getRemoteURIEnvelope();
if ($repository->isGit()) {
$this->requireWorkingCopy();
$argv = array(
'fetch --prune -- %P %s',
$fetch_uri,
'+refs/*:refs/*',
);
} else {
throw new Exception(pht('Remote sync only supported for git!'));
}
$future = DiffusionCommandEngine::newCommandEngine($repository)
->setArgv($argv)
->setSudoAsDaemon(true)
->setCredentialPHID($repository->getCredentialPHID())
->setURI($repository->getRemoteURIObject())
->newFuture();
$future->setCWD($local_path);
try {
$future->resolvex();
} catch (Exception $ex) {
$this->logLine(
pht(
'Synchronization of "%s" from remote failed: %s',
$device->getName(),
$ex->getMessage()));
throw $ex;
}
}
/**
* @task internal
*/
private function synchronizeWorkingCopyFromDevices(
array $device_phids,
$local_version,
$remote_version) {
$repository = $this->getRepository();
$service = $repository->loadAlmanacService();
if (!$service) {
throw new Exception(pht('Failed to load repository cluster service.'));
}
$device_map = array_fuse($device_phids);
$bindings = $service->getActiveBindings();
$fetchable = array();
foreach ($bindings as $binding) {
// We can't fetch from nodes which don't have the newest version.
$device_phid = $binding->getDevicePHID();
if (empty($device_map[$device_phid])) {
continue;
}
// TODO: For now, only fetch over SSH. We could support fetching over
// HTTP eventually.
if ($binding->getAlmanacPropertyValue('protocol') != 'ssh') {
continue;
}
$fetchable[] = $binding;
}
if (!$fetchable) {
throw new Exception(
pht(
'Leader lost: no up-to-date nodes in repository cluster are '.
'fetchable.'));
}
// If we can synchronize from multiple sources, choose one at random.
shuffle($fetchable);
$caught = null;
foreach ($fetchable as $binding) {
try {
$this->synchronizeWorkingCopyFromBinding(
$binding,
$local_version,
$remote_version);
$caught = null;
break;
} catch (Exception $ex) {
$caught = $ex;
}
}
if ($caught) {
throw $caught;
}
}
/**
* @task internal
*/
private function synchronizeWorkingCopyFromBinding(
AlmanacBinding $binding,
$local_version,
$remote_version) {
$repository = $this->getRepository();
$device = AlmanacKeys::getLiveDevice();
$viewer = $this->getViewer(); // c4s custo
if($viewer->getIsAdmin()) { // c4s custo
$this->logLine(
pht(
'Synchronizing this device ("%s") from cluster leader ("%s").',
$device->getName(),
$binding->getDevice()->getName()));
}
$fetch_uri = $repository->getClusterRepositoryURIFromBinding($binding);
$local_path = $repository->getLocalPath();
if ($repository->isGit()) {
$this->requireWorkingCopy();
$argv = array(
'fetch --prune -- %s %s',
$fetch_uri,
'+refs/*:refs/*',
);
} else {
throw new Exception(pht('Binding sync only supported for git!'));
}
$future = DiffusionCommandEngine::newCommandEngine($repository)
->setArgv($argv)
->setConnectAsDevice(true)
->setSudoAsDaemon(true)
->setURI($fetch_uri)
->newFuture();
$future->setCWD($local_path);
$log = PhabricatorRepositorySyncEvent::initializeNewEvent()
->setRepositoryPHID($repository->getPHID())
->setEpoch(PhabricatorTime::getNow())
->setDevicePHID($device->getPHID())
->setFromDevicePHID($binding->getDevice()->getPHID())
->setDeviceVersion($local_version)
->setFromDeviceVersion($remote_version);
$sync_start = microtime(true);
try {
$future->resolvex();
} catch (Exception $ex) {
$log->setSyncWait(phutil_microseconds_since($sync_start));
if ($ex instanceof CommandException) {
if ($future->getWasKilledByTimeout()) {
$result_type = PhabricatorRepositorySyncEvent::RESULT_TIMEOUT;
} else {
$result_type = PhabricatorRepositorySyncEvent::RESULT_ERROR;
}
$log
->setResultCode($ex->getError())
->setResultType($result_type)
->setProperty('stdout', $ex->getStdout())
->setProperty('stderr', $ex->getStderr());
} else {
$log
->setResultCode(1)
->setResultType(PhabricatorRepositorySyncEvent::RESULT_EXCEPTION)
->setProperty('message', $ex->getMessage());
}
$log->save();
$this->logLine(
pht(
'Synchronization of "%s" from leader "%s" failed: %s',
$device->getName(),
$binding->getDevice()->getName(),
$ex->getMessage()));
throw $ex;
}
$log
->setSyncWait(phutil_microseconds_since($sync_start))
->setResultCode(0)
->setResultType(PhabricatorRepositorySyncEvent::RESULT_SYNC)
->save();
}
/**
* @task internal
*/
private function logLine($message) {
return $this->logText("# {$message}\n");
}
/**
* @task internal
*/
private function logText($message) {
$log = $this->logger;
if ($log) {
$log->writeClusterEngineLogMessage($message);
}
return $this;
}
private function requireWorkingCopy() {
$repository = $this->getRepository();
$local_path = $repository->getLocalPath();
if (!Filesystem::pathExists($local_path)) {
$device = AlmanacKeys::getLiveDevice();
throw new Exception(
pht(
'Repository "%s" does not have a working copy on this device '.
'yet, so it can not be synchronized. Wait for the daemons to '.
'construct one or run `bin/repository update %s` on this host '.
'("%s") to build it explicitly.',
$repository->getDisplayName(),
$repository->getMonogram(),
$device->getName()));
}
}
private function logActiveWriter(
PhabricatorUser $viewer,
PhabricatorRepository $repository) {
$writer = PhabricatorRepositoryWorkingCopyVersion::loadWriter(
$repository->getPHID());
if (!$writer) {
$this->logLine(pht('Waiting on another user to finish writing...'));
return;
}
$user_phid = $writer->getWriteProperty('userPHID');
$device_phid = $writer->getWriteProperty('devicePHID');
$epoch = $writer->getWriteProperty('epoch');
$phids = array($user_phid, $device_phid);
$handles = $viewer->loadHandles($phids);
$duration = (PhabricatorTime::getNow() - $epoch) + 1;
$this->logLine(
pht(
'Waiting for %s to finish writing (on device "%s" for %ss)...',
$handles[$user_phid]->getName(),
$handles[$device_phid]->getName(),
new PhutilNumber($duration)));
}
}
diff --git a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php
index 544ba9cb0..a31761461 100644
--- a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php
+++ b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php
@@ -1,310 +1,312 @@
<?php
abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow {
private $args;
private $repository;
private $hasWriteAccess;
private $shouldProxy;
private $baseRequestPath;
public function getRepository() {
if (!$this->repository) {
throw new Exception(pht('Repository is not available yet!'));
}
return $this->repository;
}
private function setRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
public function getArgs() {
return $this->args;
}
public function getEnvironment() {
$env = array(
DiffusionCommitHookEngine::ENV_USER => $this->getSSHUser()->getUsername(),
DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL => 'ssh',
);
$identifier = $this->getRequestIdentifier();
if ($identifier !== null) {
$env[DiffusionCommitHookEngine::ENV_REQUEST] = $identifier;
}
$remote_address = $this->getSSHRemoteAddress();
if ($remote_address !== null) {
$env[DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS] = $remote_address;
}
return $env;
}
/**
* Identify and load the affected repository.
*/
abstract protected function identifyRepository();
abstract protected function executeRepositoryOperations();
abstract protected function raiseWrongVCSException(
PhabricatorRepository $repository);
protected function getBaseRequestPath() {
return $this->baseRequestPath;
}
protected function writeError($message) {
$this->getErrorChannel()->write($message);
return $this;
}
protected function getCurrentDeviceName() {
$device = AlmanacKeys::getLiveDevice();
if ($device) {
return $device->getName();
}
return php_uname('n');
}
protected function shouldProxy() {
return $this->shouldProxy;
}
protected function getProxyCommand($for_write) {
$viewer = $this->getSSHUser();
$repository = $this->getRepository();
$is_cluster_request = $this->getIsClusterRequest();
$uri = $repository->getAlmanacServiceURI(
$viewer,
array(
'neverProxy' => $is_cluster_request,
'protocols' => array(
'ssh',
),
'writable' => $for_write,
));
if (!$uri) {
throw new Exception(
pht(
'Failed to generate an intracluster proxy URI even though this '.
'request was routed as a proxy request.'));
}
$uri = new PhutilURI($uri);
$username = AlmanacKeys::getClusterSSHUser();
if ($username === null) {
throw new Exception(
pht(
'Unable to determine the username to connect with when trying '.
'to proxy an SSH request within the Phabricator cluster.'));
}
$port = $uri->getPort();
$host = $uri->getDomain();
$key_path = AlmanacKeys::getKeyPath('device.key');
if (!Filesystem::pathExists($key_path)) {
throw new Exception(
pht(
'Unable to proxy this SSH request within the cluster: this device '.
'is not registered and has a missing device key (expected to '.
'find key at "%s").',
$key_path));
}
$options = array();
$options[] = '-o';
$options[] = 'StrictHostKeyChecking=no';
$options[] = '-o';
$options[] = 'UserKnownHostsFile=/dev/null';
// This is suppressing "added <address> to the list of known hosts"
// messages, which are confusing and irrelevant when they arise from
// proxied requests. It might also be suppressing lots of useful errors,
// of course. Ideally, we would enforce host keys eventually.
$options[] = '-o';
$options[] = 'LogLevel=quiet';
// NOTE: We prefix the command with "@username", which the far end of the
// connection will parse in order to act as the specified user. This
// behavior is only available to cluster requests signed by a trusted
// device key.
return csprintf(
'ssh %Ls -l %s -i %s -p %s %s -- %s %Ls',
$options,
$username,
$key_path,
$port,
$host,
'@'.$this->getSSHUser()->getUsername(),
$this->getOriginalArguments());
}
final public function execute(PhutilArgumentParser $args) {
$this->args = $args;
$viewer = $this->getSSHUser();
$have_diffusion = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorDiffusionApplication',
$viewer);
if (!$have_diffusion) {
throw new Exception(
pht(
'You do not have permission to access the Diffusion application, '.
'so you can not interact with repositories over SSH.'));
}
$repository = $this->identifyRepository();
$this->setRepository($repository);
// NOTE: Here, we're just figuring out if this is a proxyable request to
// a clusterized repository or not. We don't (and can't) use the URI we get
// back directly.
// For example, we may get a read-only URI here but be handling a write
// request. We only care if we get back `null` (which means we should
// handle the request locally) or anything else (which means we should
// proxy it to an appropriate device).
$is_cluster_request = $this->getIsClusterRequest();
$uri = $repository->getAlmanacServiceURI(
$viewer,
array(
'neverProxy' => $is_cluster_request,
'protocols' => array(
'ssh',
),
));
$this->shouldProxy = (bool)$uri;
try {
return $this->executeRepositoryOperations();
} catch (Exception $ex) {
$this->writeError(get_class($ex).': '.$ex->getMessage());
return 1;
}
}
protected function loadRepositoryWithPath($path, $vcs) {
$viewer = $this->getSSHUser();
$info = PhabricatorRepository::parseRepositoryServicePath($path, $vcs);
if ($info === null) {
throw new Exception(
pht(
'Unrecognized repository path "%s". Expected a path like "%s", '.
'"%s", or "%s".',
$path,
'/diffusion/X/',
'/diffusion/123/',
'/source/thaumaturgy.git'));
}
$identifier = $info['identifier'];
$base = $info['base'];
$this->baseRequestPath = $base;
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withIdentifiers(array($identifier))
->needURIs(true)
->executeOne();
if (!$repository) {
throw new Exception(
pht('No repository "%s" exists!', $identifier));
}
+ $is_cluster = $this->getIsClusterRequest();
+
$protocol = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH;
- if (!$repository->canServeProtocol($protocol, false)) {
+ if (!$repository->canServeProtocol($protocol, false, $is_cluster)) {
throw new Exception(
pht(
'This repository ("%s") is not available over SSH.',
$repository->getDisplayName()));
}
if ($repository->getVersionControlSystem() != $vcs) {
$this->raiseWrongVCSException($repository);
}
return $repository;
}
protected function requireWriteAccess($protocol_command = null) {
if ($this->hasWriteAccess === true) {
return;
}
$repository = $this->getRepository();
$viewer = $this->getSSHUser();
// c4science custo
//if ($viewer->isOmnipotent()) {
// throw new Exception(
// pht(
// 'This request is authenticated as a cluster device, but is '.
// 'performing a write. Writes must be performed with a real '.
// 'user account.'));
//}
$protocol = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH;
if ($repository->canServeProtocol($protocol, true)) {
$can_push = PhabricatorPolicyFilter::hasCapability(
$viewer,
$repository,
DiffusionPushCapability::CAPABILITY);
if (!$can_push) {
throw new Exception(
pht('You do not have permission to push to this repository.'));
}
} else {
if ($protocol_command !== null) {
throw new Exception(
pht(
'This repository is read-only over SSH (tried to execute '.
'protocol command "%s").',
$protocol_command));
} else {
throw new Exception(
pht('This repository is read-only over SSH.'));
}
}
$this->hasWriteAccess = true;
return $this->hasWriteAccess;
}
protected function shouldSkipReadSynchronization() {
$viewer = $this->getSSHUser();
// Currently, the only case where devices interact over SSH without
// assuming user credentials is when synchronizing before a read. These
// synchronizing reads do not themselves need to be synchronized.
if ($viewer->isOmnipotent()) {
return true;
}
return false;
}
protected function newPullEvent() {
$viewer = $this->getSSHUser();
$repository = $this->getRepository();
$remote_address = $this->getSSHRemoteAddress();
return id(new PhabricatorRepositoryPullEvent())
->setEpoch(PhabricatorTime::getNow())
->setRemoteAddress($remote_address)
->setRemoteProtocol(PhabricatorRepositoryPullEvent::PROTOCOL_SSH)
->setPullerPHID($viewer->getPHID())
->setRepositoryPHID($repository->getPHID());
}
}
diff --git a/src/applications/diffusion/view/DiffusionHistoryListView.php b/src/applications/diffusion/view/DiffusionHistoryListView.php
index d4ed8cd31..473daf38a 100644
--- a/src/applications/diffusion/view/DiffusionHistoryListView.php
+++ b/src/applications/diffusion/view/DiffusionHistoryListView.php
@@ -1,236 +1,238 @@
<?php
final class DiffusionHistoryListView extends DiffusionHistoryView {
public function render() {
$drequest = $this->getDiffusionRequest();
$viewer = $this->getUser();
$repository = $drequest->getRepository();
require_celerity_resource('diffusion-css');
Javelin::initBehavior('phabricator-tooltips');
$buildables = $this->loadBuildables(
mpull($this->getHistory(), 'getCommit'));
$show_revisions = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorDifferentialApplication',
$viewer);
$handles = $viewer->loadHandles($this->getRequiredHandlePHIDs());
$show_builds = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorHarbormasterApplication',
$this->getUser());
// c4science customization
if($repository->getVersionControlSystem() == 'git') {
// Get all TAGS
$conduit_result = DiffusionQuery::callConduitWithDiffusionRequest(
$viewer,
$drequest,
'diffusion.tagsquery',
array(
'repository' => $repository->getPHID(),
));
$tags = DiffusionRepositoryTag::newFromConduit($conduit_result);
$tags_commit = mpull($tags, 'getCommitIdentifier');
// Get all BRANCHES
$conduit_result = DiffusionQuery::callConduitWithDiffusionRequest(
$viewer,
$drequest,
'diffusion.branchquery',
array(
'repository' => $repository->getPHID(),
));
$branches = DiffusionRepositoryRef::loadAllFromDictionaries($conduit_result);
$branches_commit = mpull($branches, 'getCommitIdentifier');
}
// end of c4s custo
$cur_date = null;
$view = array();
foreach ($this->getHistory() as $history) {
$epoch = $history->getEpoch();
$new_date = phabricator_date($history->getEpoch(), $viewer);
if ($cur_date !== $new_date) {
$date = ucfirst(
phabricator_relative_date($history->getEpoch(), $viewer));
$header = id(new PHUIHeaderView())
->setHeader($date);
$list = id(new PHUIObjectItemListView())
->setFlush(true)
->addClass('diffusion-history-list');
$view[] = id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addClass('diffusion-mobile-view')
->setObjectList($list);
}
if ($epoch) {
$committed = $viewer->formatShortDateTime($epoch);
} else {
$committed = null;
}
$data = $history->getCommitData();
$author_phid = $committer = $committer_phid = null;
if ($data) {
$author_phid = $data->getCommitDetail('authorPHID');
$committer_phid = $data->getCommitDetail('committerPHID');
$committer = $data->getCommitDetail('committer');
}
if ($author_phid && isset($handles[$author_phid])) {
$author_name = $handles[$author_phid]->renderLink();
$author_image = $handles[$author_phid]->getImageURI();
} else {
$author_name = self::renderName($history->getAuthorName());
$author_image =
celerity_get_resource_uri('/rsrc/image/people/user0.png');
}
$different_committer = false;
if ($committer_phid) {
$different_committer = ($committer_phid != $author_phid);
} else if ($committer != '') {
$different_committer = ($committer != $history->getAuthorName());
}
if ($different_committer) {
if ($committer_phid && isset($handles[$committer_phid])) {
$committer = $handles[$committer_phid]->renderLink();
} else {
$committer = self::renderName($committer);
}
$author_name = hsprintf('%s / %s', $author_name, $committer);
}
// We can show details once the message and change have been imported.
$partial_import = PhabricatorRepositoryCommit::IMPORTED_MESSAGE |
PhabricatorRepositoryCommit::IMPORTED_CHANGE;
$commit = $history->getCommit();
if ($commit && $commit->isPartiallyImported($partial_import) && $data) {
$commit_desc = $history->getSummary();
} else {
$commit_desc = phutil_tag('em', array(), pht("Importing\xE2\x80\xA6"));
}
$browse_button = $this->linkBrowse(
$history->getPath(),
array(
'commit' => $history->getCommitIdentifier(),
'branch' => $drequest->getBranch(),
'type' => $history->getFileType(),
),
true);
$diff_tag = null;
if ($show_revisions && $commit) {
$d_id = idx($this->getRevisions(), $commit->getPHID());
if ($d_id) {
$diff_tag = id(new PHUITagView())
->setName('D'.$d_id)
->setType(PHUITagView::TYPE_SHADE)
->setColor(PHUITagView::COLOR_BLUE)
->setHref('/D'.$d_id)
->setBorder(PHUITagView::BORDER_NONE)
->setSlimShady(true);
}
}
$build_view = null;
if ($show_builds) {
$buildable = idx($buildables, $commit->getPHID());
if ($buildable !== null) {
$build_view = $this->renderBuildable($buildable, 'button');
}
}
$message = null;
$commit_link = $repository->getCommitURI(
$history->getCommitIdentifier());
$commit_name = $repository->formatCommitName(
$history->getCommitIdentifier(), $local = true);
$committed = phabricator_datetime($commit->getEpoch(), $viewer);
$author_name = phutil_tag(
'strong',
array(
'class' => 'diffusion-history-author-name',
),
$author_name);
$authored = pht('%s on %s.', $author_name, $committed);
$commit_tag = id(new PHUITagView())
->setName($commit_name)
->setType(PHUITagView::TYPE_SHADE)
->setColor(PHUITagView::COLOR_INDIGO)
->setBorder(PHUITagView::BORDER_NONE)
->setSlimShady(true);
$item = id(new PHUIObjectItemView())
->setHeader($commit_desc)
->setHref($commit_link)
->setDisabled($commit->isUnreachable())
->setDescription($message)
->setImageURI($author_image)
+ ->addAttribute(array($commit_tag, ' ', $diff_tag)) // For Copy Pasta
+ ->addAttribute($authored)
->setSideColumn(array(
$build_view,
$browse_button,
));
// c4science customization
$item->addAttribute($commit_tag);
if($repository->getVersionControlSystem() == 'git') {
$commit_id = $commit->getCommitIdentifier();
// Show tags
if(in_array($commit_id, $tags_commit)) {
foreach($tags as $tag){
if($commit_id == $tag->getCommitIdentifier()){
$tag_tag = id(new PHUITagView())
->setName('tag: ' . $tag->getName())
->setType(PHUITagView::TYPE_SHADE)
->setColor(PHUITagView::COLOR_RED)
->setBorder(PHUITagView::BORDER_NONE)
->setSlimShady(true);
$item->addAttribute($tag_tag);
}
}
}
// Show branches
if(in_array($commit_id, $branches_commit)) {
foreach($branches as $branch){
if($commit_id == $branch->getCommitIdentifier()){
$branch_tag = id(new PHUITagView())
->setName('branch: ' . $branch->getShortName())
->setType(PHUITagView::TYPE_SHADE)
->setColor(PHUITagView::COLOR_YELLOW)
->setBorder(PHUITagView::BORDER_NONE)
->setSlimShady(true);
$item->addAttribute($branch_tag);
}
}
}
}
$item->addAttribute($diff_tag);
$item->addAttribute($authored);
// end of c4s custo
$list->addItem($item);
$cur_date = $new_date;
}
return $view;
}
}
diff --git a/src/applications/diviner/storage/DivinerLiveBook.php b/src/applications/diviner/storage/DivinerLiveBook.php
index 07089264a..480bc50d0 100644
--- a/src/applications/diviner/storage/DivinerLiveBook.php
+++ b/src/applications/diviner/storage/DivinerLiveBook.php
@@ -1,169 +1,158 @@
<?php
final class DivinerLiveBook extends DivinerDAO
implements
PhabricatorPolicyInterface,
PhabricatorProjectInterface,
PhabricatorDestructibleInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorFulltextInterface {
protected $name;
protected $repositoryPHID;
protected $viewPolicy;
protected $editPolicy;
protected $configurationData = array();
private $projectPHIDs = self::ATTACHABLE;
private $repository = self::ATTACHABLE;
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'configurationData' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text64',
'repositoryPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'name' => array(
'columns' => array('name'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function getConfig($key, $default = null) {
return idx($this->configurationData, $key, $default);
}
public function setConfig($key, $value) {
$this->configurationData[$key] = $value;
return $this;
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(DivinerBookPHIDType::TYPECONST);
}
public function getTitle() {
return $this->getConfig('title', $this->getName());
}
public function getShortTitle() {
return $this->getConfig('short', $this->getTitle());
}
public function getPreface() {
return $this->getConfig('preface');
}
public function getGroupName($group) {
$groups = $this->getConfig('groups', array());
$spec = idx($groups, $group, array());
return idx($spec, 'name', $group);
}
public function attachRepository(PhabricatorRepository $repository = null) {
$this->repository = $repository;
return $this;
}
public function getRepository() {
return $this->assertAttached($this->repository);
}
public function attachProjectPHIDs(array $project_phids) {
$this->projectPHIDs = $project_phids;
return $this;
}
public function getProjectPHIDs() {
return $this->assertAttached($this->projectPHIDs);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
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 $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$atoms = id(new DivinerAtomQuery())
->setViewer($engine->getViewer())
->withBookPHIDs(array($this->getPHID()))
->execute();
foreach ($atoms as $atom) {
$engine->destroyObject($atom);
}
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new DivinerLiveBookEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new DivinerLiveBookTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new DivinerLiveBookFulltextEngine();
}
}
diff --git a/src/applications/drydock/storage/DrydockBlueprint.php b/src/applications/drydock/storage/DrydockBlueprint.php
index 61e2dbfcf..ebe2f9f60 100644
--- a/src/applications/drydock/storage/DrydockBlueprint.php
+++ b/src/applications/drydock/storage/DrydockBlueprint.php
@@ -1,398 +1,387 @@
<?php
/**
* @task resource Allocating Resources
* @task lease Acquiring Leases
*/
final class DrydockBlueprint extends DrydockDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorCustomFieldInterface,
PhabricatorNgramsInterface,
PhabricatorProjectInterface,
PhabricatorConduitResultInterface {
protected $className;
protected $blueprintName;
protected $viewPolicy;
protected $editPolicy;
protected $details = array();
protected $isDisabled;
private $implementation = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
private $fields = null;
public static function initializeNewBlueprint(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorDrydockApplication'))
->executeOne();
$view_policy = $app->getPolicy(
DrydockDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(
DrydockDefaultEditCapability::CAPABILITY);
return id(new DrydockBlueprint())
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setBlueprintName('')
->setIsDisabled(0);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'details' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'className' => 'text255',
'blueprintName' => 'sort255',
'isDisabled' => 'bool',
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
DrydockBlueprintPHIDType::TYPECONST);
}
public function getImplementation() {
return $this->assertAttached($this->implementation);
}
public function attachImplementation(DrydockBlueprintImplementation $impl) {
$this->implementation = $impl;
return $this;
}
public function hasImplementation() {
return ($this->implementation !== self::ATTACHABLE);
}
public function getDetail($key, $default = null) {
return idx($this->details, $key, $default);
}
public function setDetail($key, $value) {
$this->details[$key] = $value;
return $this;
}
public function getFieldValue($key) {
$key = "std:drydock:core:{$key}";
$fields = $this->loadCustomFields();
$field = idx($fields, $key);
if (!$field) {
throw new Exception(
pht(
'Unknown blueprint field "%s"!',
$key));
}
return $field->getBlueprintFieldValue();
}
private function loadCustomFields() {
if ($this->fields === null) {
$field_list = PhabricatorCustomField::getObjectFields(
$this,
PhabricatorCustomField::ROLE_VIEW);
$field_list->readFieldsFromStorage($this);
$this->fields = $field_list->getFields();
}
return $this->fields;
}
public function logEvent($type, array $data = array()) {
$log = id(new DrydockLog())
->setEpoch(PhabricatorTime::getNow())
->setType($type)
->setData($data);
$log->setBlueprintPHID($this->getPHID());
return $log->save();
}
public function getURI() {
$id = $this->getID();
return "/drydock/blueprint/{$id}/";
}
/* -( Allocating Resources )----------------------------------------------- */
/**
* @task resource
*/
public function canEverAllocateResourceForLease(DrydockLease $lease) {
return $this->getImplementation()->canEverAllocateResourceForLease(
$this,
$lease);
}
/**
* @task resource
*/
public function canAllocateResourceForLease(DrydockLease $lease) {
return $this->getImplementation()->canAllocateResourceForLease(
$this,
$lease);
}
/**
* @task resource
*/
public function allocateResource(DrydockLease $lease) {
return $this->getImplementation()->allocateResource(
$this,
$lease);
}
/**
* @task resource
*/
public function activateResource(DrydockResource $resource) {
return $this->getImplementation()->activateResource(
$this,
$resource);
}
/**
* @task resource
*/
public function destroyResource(DrydockResource $resource) {
$this->getImplementation()->destroyResource(
$this,
$resource);
return $this;
}
/**
* @task resource
*/
public function getResourceName(DrydockResource $resource) {
return $this->getImplementation()->getResourceName(
$this,
$resource);
}
/* -( Acquiring Leases )--------------------------------------------------- */
/**
* @task lease
*/
public function canAcquireLeaseOnResource(
DrydockResource $resource,
DrydockLease $lease) {
return $this->getImplementation()->canAcquireLeaseOnResource(
$this,
$resource,
$lease);
}
/**
* @task lease
*/
public function acquireLease(
DrydockResource $resource,
DrydockLease $lease) {
return $this->getImplementation()->acquireLease(
$this,
$resource,
$lease);
}
/**
* @task lease
*/
public function activateLease(
DrydockResource $resource,
DrydockLease $lease) {
return $this->getImplementation()->activateLease(
$this,
$resource,
$lease);
}
/**
* @task lease
*/
public function didReleaseLease(
DrydockResource $resource,
DrydockLease $lease) {
$this->getImplementation()->didReleaseLease(
$this,
$resource,
$lease);
return $this;
}
/**
* @task lease
*/
public function destroyLease(
DrydockResource $resource,
DrydockLease $lease) {
$this->getImplementation()->destroyLease(
$this,
$resource,
$lease);
return $this;
}
public function getInterface(
DrydockResource $resource,
DrydockLease $lease,
$type) {
$interface = $this->getImplementation()
->getInterface($this, $resource, $lease, $type);
if (!$interface) {
throw new Exception(
pht(
'Unable to build resource interface of type "%s".',
$type));
}
return $interface;
}
public function shouldAllocateSupplementalResource(
DrydockResource $resource,
DrydockLease $lease) {
return $this->getImplementation()->shouldAllocateSupplementalResource(
$this,
$resource,
$lease);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new DrydockBlueprintEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new DrydockBlueprintTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorPolicyInterface )----------------------------------------- */
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 $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return array();
}
public function getCustomFieldBaseClass() {
return 'DrydockBlueprintCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorNgramsInterface )----------------------------------------- */
public function newNgrams() {
return array(
id(new DrydockBlueprintNameNgrams())
->setValue($this->getBlueprintName()),
);
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of this blueprint.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('type')
->setType('string')
->setDescription(pht('The type of resource this blueprint provides.')),
);
}
public function getFieldValuesForConduit() {
return array(
'name' => $this->getBlueprintName(),
'type' => $this->getImplementation()->getType(),
);
}
public function getConduitSearchAttachments() {
return array(
);
}
}
diff --git a/src/applications/feed/query/PhabricatorFeedQuery.php b/src/applications/feed/query/PhabricatorFeedQuery.php
index c8289d790..9818d2e35 100644
--- a/src/applications/feed/query/PhabricatorFeedQuery.php
+++ b/src/applications/feed/query/PhabricatorFeedQuery.php
@@ -1,180 +1,191 @@
<?php
final class PhabricatorFeedQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $filterPHIDs;
private $filterOutPHIDs; // c4science custo
private $chronologicalKeys;
private $rangeMin;
private $rangeMax;
public function withFilterPHIDs(array $phids) {
$this->filterPHIDs = $phids;
return $this;
}
public function withChronologicalKeys(array $keys) {
$this->chronologicalKeys = $keys;
return $this;
}
public function withEpochInRange($range_min, $range_max) {
$this->rangeMin = $range_min;
$this->rangeMax = $range_max;
return $this;
}
public function newResultObject() {
return new PhabricatorFeedStoryData();
}
protected function loadPage() {
// NOTE: We return raw rows from this method, which is a little unusual.
return $this->loadStandardPageRows($this->newResultObject());
}
protected function willFilterPage(array $data) {
$stories = PhabricatorFeedStory::loadAllFromRows($data, $this->getViewer());
foreach ($stories as $key => $story) {
if (!$story->isVisibleInFeed()) {
unset($stories[$key]);
}
}
return $stories;
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
// NOTE: We perform this join unconditionally (even if we have no filter
// PHIDs) to omit rows which have no story references. These story data
// rows are notifications or realtime alerts.
$ref_table = new PhabricatorFeedStoryReference();
$joins[] = qsprintf(
$conn,
'JOIN %T ref ON ref.chronologicalKey = story.chronologicalKey',
$ref_table->getTableName());
return $joins;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->filterPHIDs) { // c4science custo
$where[] = qsprintf(
$conn,
'ref.objectPHID IN (%Ls)',
$this->filterPHIDs);
}
// C4science customization
if ($this->filterOutPHIDs !== null) {
$where[] = qsprintf(
$conn,
'ref.objectPHID NOT IN (%Ls)',
$this->filterOutPHIDs);
}
if ($this->chronologicalKeys !== null) {
// NOTE: We can't use "%d" to format these large integers on 32-bit
// systems. Historically, we formatted these into integers in an
// awkward way because MySQL could sometimes (?) fail to use the proper
// keys if the values were formatted as strings instead of integers.
// After the "qsprintf()" update to use PhutilQueryString, we can no
// longer do this in a sneaky way. However, the MySQL key issue also
// no longer appears to reproduce across several systems. So: just use
// strings until problems turn up?
$where[] = qsprintf(
$conn,
'ref.chronologicalKey IN (%Ls)',
$this->chronologicalKeys);
}
// NOTE: We may not have 64-bit PHP, so do the shifts in MySQL instead.
// From EXPLAIN, it appears like MySQL is smart enough to compute the
// result and make use of keys to execute the query.
+ if ($this->rangeMin !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'ref.chronologicalKey IN (%Ls)',
+ $this->chronologicalKeys);
+ }
+
+ // NOTE: We may not have 64-bit PHP, so do the shifts in MySQL instead.
+ // From EXPLAIN, it appears like MySQL is smart enough to compute the
+ // result and make use of keys to execute the query.
+
if ($this->rangeMin !== null) {
$where[] = qsprintf(
$conn,
'ref.chronologicalKey >= (%d << 32)',
$this->rangeMin);
}
if ($this->rangeMax !== null) {
$where[] = qsprintf(
$conn,
'ref.chronologicalKey < (%d << 32)',
$this->rangeMax);
}
return $where;
}
protected function buildGroupClause(AphrontDatabaseConnection $conn) {
if ($this->filterPHIDs !== null) {
return qsprintf($conn, 'GROUP BY ref.chronologicalKey');
} else {
return qsprintf($conn, 'GROUP BY story.chronologicalKey');
}
}
protected function getDefaultOrderVector() {
return array('key');
}
public function getBuiltinOrders() {
return array(
'newest' => array(
'vector' => array('key'),
'name' => pht('Creation (Newest First)'),
'aliases' => array('created'),
),
'oldest' => array(
'vector' => array('-key'),
'name' => pht('Creation (Oldest First)'),
),
);
}
public function getOrderableColumns() {
$table = ($this->filterPHIDs ? 'ref' : 'story');
return array(
'key' => array(
'table' => $table,
'column' => 'chronologicalKey',
'type' => 'string',
'unique' => true,
),
);
}
protected function getPagingValueMap($cursor, array $keys) {
return array(
'key' => $cursor,
);
}
protected function getResultCursor($item) {
if ($item instanceof PhabricatorFeedStory) {
return $item->getChronologicalKey();
}
return $item['chronologicalKey'];
}
protected function getPrimaryTableAlias() {
return 'story';
}
public function getQueryApplicationClass() {
return 'PhabricatorFeedApplication';
}
}
diff --git a/src/applications/files/config/PhabricatorFilesConfigOptions.php b/src/applications/files/config/PhabricatorFilesConfigOptions.php
index 751a06ffd..735ddfcb0 100644
--- a/src/applications/files/config/PhabricatorFilesConfigOptions.php
+++ b/src/applications/files/config/PhabricatorFilesConfigOptions.php
@@ -1,222 +1,217 @@
<?php
final class PhabricatorFilesConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Files');
}
public function getDescription() {
return pht('Configure files and file storage.');
}
public function getIcon() {
return 'fa-file';
}
public function getGroup() {
return 'apps';
}
public function getOptions() {
$viewable_default = array(
'image/jpeg' => 'image/jpeg',
'image/jpg' => 'image/jpg',
'image/png' => 'image/png',
'image/gif' => 'image/gif',
'text/plain' => 'text/plain; charset=utf-8',
'text/x-diff' => 'text/plain; charset=utf-8',
// ".ico" favicon files, which have mime type diversity. See:
// http://en.wikipedia.org/wiki/ICO_(file_format)#MIME_type
'image/x-ico' => 'image/x-icon',
'image/x-icon' => 'image/x-icon',
'image/vnd.microsoft.icon' => 'image/x-icon',
// This is a generic type for both OGG video and OGG audio.
'application/ogg' => 'application/ogg',
'audio/x-wav' => 'audio/x-wav',
'audio/mpeg' => 'audio/mpeg',
'audio/ogg' => 'audio/ogg',
'video/mp4' => 'video/mp4',
'video/ogg' => 'video/ogg',
'video/webm' => 'video/webm',
'video/quicktime' => 'video/quicktime',
'application/pdf' => 'application/pdf',
);
$image_default = array(
'image/jpeg' => true,
'image/jpg' => true,
'image/png' => true,
'image/gif' => true,
'image/x-ico' => true,
'image/x-icon' => true,
'image/vnd.microsoft.icon' => true,
);
// The "application/ogg" type is listed as both an audio and video type,
// because it may contain either type of content.
$audio_default = array(
'audio/x-wav' => true,
'audio/mpeg' => true,
'audio/ogg' => true,
// These are video or ambiguous types, but can be forced to render as
// audio with `media=audio`, which seems to work properly in browsers.
// (For example, you can embed a music video as audio if you just want
// to set the mood for your task without distracting viewers.)
'video/mp4' => true,
'video/ogg' => true,
'video/quicktime' => true,
'application/ogg' => true,
);
$video_default = array(
'video/mp4' => true,
'video/ogg' => true,
'video/webm' => true,
'video/quicktime' => true,
'application/ogg' => true,
);
// largely lifted from http://en.wikipedia.org/wiki/Internet_media_type
$icon_default = array(
// audio file icon
'audio/basic' => 'fa-file-audio-o',
'audio/L24' => 'fa-file-audio-o',
'audio/mp4' => 'fa-file-audio-o',
'audio/mpeg' => 'fa-file-audio-o',
'audio/ogg' => 'fa-file-audio-o',
'audio/vorbis' => 'fa-file-audio-o',
'audio/vnd.rn-realaudio' => 'fa-file-audio-o',
'audio/vnd.wave' => 'fa-file-audio-o',
'audio/webm' => 'fa-file-audio-o',
// movie file icon
'video/mpeg' => 'fa-file-movie-o',
'video/mp4' => 'fa-file-movie-o',
'application/ogg' => 'fa-file-movie-o',
'video/ogg' => 'fa-file-movie-o',
'video/quicktime' => 'fa-file-movie-o',
'video/webm' => 'fa-file-movie-o',
'video/x-matroska' => 'fa-file-movie-o',
'video/x-ms-wmv' => 'fa-file-movie-o',
'video/x-flv' => 'fa-file-movie-o',
// pdf file icon
'application/pdf' => 'fa-file-pdf-o',
// zip file icon
'application/zip' => 'fa-file-zip-o',
// msword icon
'application/msword' => 'fa-file-word-o',
// msexcel
'application/vnd.ms-excel' => 'fa-file-excel-o',
// mspowerpoint
'application/vnd.ms-powerpoint' => 'fa-file-powerpoint-o',
) + array_fill_keys(array_keys($image_default), 'fa-file-image-o');
// NOTE: These options are locked primarily because adding "text/plain"
// as an image MIME type increases SSRF vulnerability by allowing users
// to load text files from remote servers as "images" (see T6755 for
// discussion).
return array(
$this->newOption('files.viewable-mime-types', 'wild', $viewable_default)
->setLocked(true)
->setSummary(
pht('Configure which MIME types are viewable in the browser.'))
->setDescription(
pht(
"Configure which uploaded file types may be viewed directly ".
"in the browser. Other file types will be downloaded instead ".
"of displayed. This is mainly a usability consideration, since ".
"browsers tend to freak out when viewing very large binary files.".
"\n\n".
"The keys in this map are viewable MIME types; the values are ".
"the MIME types they are delivered as when they are viewed in ".
"the browser.")),
$this->newOption('files.image-mime-types', 'set', $image_default)
->setLocked(true)
->setSummary(pht('Configure which MIME types are images.'))
->setDescription(
pht(
'List of MIME types which can be used as the `%s` for an `%s` tag.',
'src',
'<img />')),
$this->newOption('files.audio-mime-types', 'set', $audio_default)
->setLocked(true)
->setSummary(pht('Configure which MIME types are audio.'))
->setDescription(
pht(
'List of MIME types which can be rendered with an `%s` tag.',
'<audio />')),
$this->newOption('files.video-mime-types', 'set', $video_default)
->setLocked(true)
->setSummary(pht('Configure which MIME types are video.'))
->setDescription(
pht(
'List of MIME types which can be rendered with a `%s` tag.',
'<video />')),
$this->newOption('files.icon-mime-types', 'wild', $icon_default)
->setLocked(true)
->setSummary(pht('Configure which MIME types map to which icons.'))
->setDescription(
pht(
'Map of MIME type to icon name. MIME types which can not be '.
'found default to icon `%s`.',
'doc_files')),
$this->newOption('storage.mysql-engine.max-size', 'int', 1000000)
->setSummary(
pht(
'Configure the largest file which will be put into the MySQL '.
'storage engine.')),
$this->newOption('storage.local-disk.path', 'string', null)
->setLocked(true)
->setSummary(pht('Local storage disk path.'))
->setDescription(
pht(
"Phabricator provides a local disk storage engine, which just ".
"writes files to some directory on local disk. The webserver ".
"must have read/write permissions on this directory. This is ".
"straightforward and suitable for most installs, but will not ".
"scale past one web frontend unless the path is actually an NFS ".
"mount, since you'll end up with some of the files written to ".
"each web frontend and no way for them to share. To use the ".
"local disk storage engine, specify the path to a directory ".
"here. To disable it, specify null.")),
$this->newOption('storage.s3.bucket', 'string', null)
->setSummary(pht('Amazon S3 bucket.'))
->setDescription(
pht(
"Set this to a valid Amazon S3 bucket to store files there. You ".
"must also configure S3 access keys in the 'Amazon Web Services' ".
"group.")),
- $this->newOption(
- 'metamta.files.subject-prefix',
- 'string',
- '[File]')
- ->setDescription(pht('Subject prefix for Files email.')),
$this->newOption('files.enable-imagemagick', 'bool', false)
->setBoolOptions(
array(
pht('Enable'),
pht('Disable'),
))
->setDescription(
pht(
'This option will use Imagemagick to rescale images, so animated '.
'GIFs can be thumbnailed and set as profile pictures. Imagemagick '.
'must be installed and the "%s" binary must be available to '.
'the webserver for this to work.',
'convert')),
);
}
}
diff --git a/src/applications/files/editor/PhabricatorFileEditor.php b/src/applications/files/editor/PhabricatorFileEditor.php
index db974cec6..91d168e68 100644
--- a/src/applications/files/editor/PhabricatorFileEditor.php
+++ b/src/applications/files/editor/PhabricatorFileEditor.php
@@ -1,76 +1,76 @@
<?php
final class PhabricatorFileEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorFilesApplication';
}
public function getEditorObjectsDescription() {
return pht('Files');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
return $types;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailSubjectPrefix() {
- return PhabricatorEnv::getEnvConfig('metamta.files.subject-prefix');
+ return pht('[File]');
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$object->getAuthorPHID(),
$this->requireActor()->getPHID(),
);
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new FileReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$name = $object->getName();
return id(new PhabricatorMetaMTAMail())
->setSubject("F{$id}: {$name}");
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$body->addTextSection(
pht('FILE DETAIL'),
PhabricatorEnv::getProductionURI($object->getInfoURI()));
return $body;
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function supportsSearch() {
return true;
}
}
diff --git a/src/applications/files/mail/FileCreateMailReceiver.php b/src/applications/files/mail/FileCreateMailReceiver.php
index fa9c6691e..f3f31d913 100644
--- a/src/applications/files/mail/FileCreateMailReceiver.php
+++ b/src/applications/files/mail/FileCreateMailReceiver.php
@@ -1,58 +1,58 @@
<?php
-final class FileCreateMailReceiver extends PhabricatorMailReceiver {
+final class FileCreateMailReceiver
+ extends PhabricatorApplicationMailReceiver {
- public function isEnabled() {
- $app_class = 'PhabricatorFilesApplication';
- return PhabricatorApplication::isClassInstalled($app_class);
- }
-
- public function canAcceptMail(PhabricatorMetaMTAReceivedMail $mail) {
- $files_app = new PhabricatorFilesApplication();
- return $this->canAcceptApplicationMail($files_app, $mail);
+ protected function newApplication() {
+ return new PhabricatorFilesApplication();
}
protected function processReceivedMail(
PhabricatorMetaMTAReceivedMail $mail,
- PhabricatorUser $sender) {
+ PhutilEmailAddress $target) {
+ $author = $this->getAuthor();
$attachment_phids = $mail->getAttachments();
if (empty($attachment_phids)) {
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_UNHANDLED_EXCEPTION,
pht(
'Ignoring email to create files that did not include attachments.'));
}
$first_phid = head($attachment_phids);
$mail->setRelatedPHID($first_phid);
+ $sender = $this->getSender();
+ if (!$sender) {
+ return;
+ }
+
$attachment_count = count($attachment_phids);
if ($attachment_count > 1) {
$subject = pht('You successfully uploaded %d files.', $attachment_count);
} else {
$subject = pht('You successfully uploaded a file.');
}
- $subject_prefix =
- PhabricatorEnv::getEnvConfig('metamta.files.subject-prefix');
+ $subject_prefix = pht('[File]');
$file_uris = array();
foreach ($attachment_phids as $phid) {
$file_uris[] =
PhabricatorEnv::getProductionURI('/file/info/'.$phid.'/');
}
$body = new PhabricatorMetaMTAMailBody();
$body->addRawSection($subject);
$body->addTextSection(pht('FILE LINKS'), implode("\n", $file_uris));
id(new PhabricatorMetaMTAMail())
->addTos(array($sender->getPHID()))
->setSubject($subject)
->setSubjectPrefix($subject_prefix)
->setFrom($sender->getPHID())
->setRelatedPHID($first_phid)
->setBody($body->render())
->saveAndSend();
}
}
diff --git a/src/applications/files/mail/FileMailReceiver.php b/src/applications/files/mail/FileMailReceiver.php
index cdad22c5c..f4074d9ba 100644
--- a/src/applications/files/mail/FileMailReceiver.php
+++ b/src/applications/files/mail/FileMailReceiver.php
@@ -1,27 +1,27 @@
<?php
final class FileMailReceiver extends PhabricatorObjectMailReceiver {
public function isEnabled() {
return PhabricatorApplication::isClassInstalled(
'PhabricatorFilesApplication');
}
protected function getObjectPattern() {
return 'F[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
- $id = (int)trim($pattern, 'F');
+ $id = (int)substr($pattern, 1);
return id(new PhabricatorFileQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
}
protected function getTransactionReplyHandler() {
return new FileReplyHandler();
}
}
diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php
index 5109202a3..cc80d272b 100644
--- a/src/applications/files/storage/PhabricatorFile.php
+++ b/src/applications/files/storage/PhabricatorFile.php
@@ -1,1702 +1,1691 @@
<?php
/**
* Parameters
* ==========
*
* When creating a new file using a method like @{method:newFromFileData}, these
* parameters are supported:
*
* | name | Human readable filename.
* | authorPHID | User PHID of uploader.
* | ttl.absolute | Temporary file lifetime as an epoch timestamp.
* | ttl.relative | Temporary file lifetime, relative to now, in seconds.
* | viewPolicy | File visibility policy.
* | isExplicitUpload | Used to show users files they explicitly uploaded.
* | canCDN | Allows the file to be cached and delivered over a CDN.
* | profile | Marks the file as a profile image.
* | format | Internal encoding format.
* | mime-type | Optional, explicit file MIME type.
* | builtin | Optional filename, identifies this as a builtin.
*
*/
final class PhabricatorFile extends PhabricatorFileDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorTokenReceiverInterface,
PhabricatorSubscribableInterface,
PhabricatorFlaggableInterface,
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface,
PhabricatorConduitResultInterface,
PhabricatorIndexableInterface,
PhabricatorNgramsInterface {
const METADATA_IMAGE_WIDTH = 'width';
const METADATA_IMAGE_HEIGHT = 'height';
const METADATA_CAN_CDN = 'canCDN';
const METADATA_BUILTIN = 'builtin';
const METADATA_PARTIAL = 'partial';
const METADATA_PROFILE = 'profile';
const METADATA_STORAGE = 'storage';
const METADATA_INTEGRITY = 'integrity';
const METADATA_CHUNK = 'chunk';
const STATUS_ACTIVE = 'active';
const STATUS_DELETED = 'deleted';
protected $name;
protected $mimeType;
protected $byteSize;
protected $authorPHID;
protected $secretKey;
protected $contentHash;
protected $metadata = array();
protected $mailKey;
protected $builtinKey;
protected $storageEngine;
protected $storageFormat;
protected $storageHandle;
protected $ttl;
protected $isExplicitUpload = 1;
protected $viewPolicy = PhabricatorPolicies::POLICY_USER;
protected $isPartial = 0;
protected $isDeleted = 0;
private $objects = self::ATTACHABLE;
private $objectPHIDs = self::ATTACHABLE;
private $originalFile = self::ATTACHABLE;
private $transforms = self::ATTACHABLE;
public static function initializeNewFile() {
$app = id(new PhabricatorApplicationQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withClasses(array('PhabricatorFilesApplication'))
->executeOne();
$view_policy = $app->getPolicy(
FilesDefaultViewCapability::CAPABILITY);
return id(new PhabricatorFile())
->setViewPolicy($view_policy)
->setIsPartial(0)
->attachOriginalFile(null)
->attachObjects(array())
->attachObjectPHIDs(array());
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'metadata' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort255?',
'mimeType' => 'text255?',
'byteSize' => 'uint64',
'storageEngine' => 'text32',
'storageFormat' => 'text32',
'storageHandle' => 'text255',
'authorPHID' => 'phid?',
'secretKey' => 'bytes20?',
'contentHash' => 'bytes64?',
'ttl' => 'epoch?',
'isExplicitUpload' => 'bool?',
'mailKey' => 'bytes20',
'isPartial' => 'bool',
'builtinKey' => 'text64?',
'isDeleted' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'authorPHID' => array(
'columns' => array('authorPHID'),
),
'contentHash' => array(
'columns' => array('contentHash'),
),
'key_ttl' => array(
'columns' => array('ttl'),
),
'key_dateCreated' => array(
'columns' => array('dateCreated'),
),
'key_partial' => array(
'columns' => array('authorPHID', 'isPartial'),
),
'key_builtin' => array(
'columns' => array('builtinKey'),
'unique' => true,
),
'key_engine' => array(
'columns' => array('storageEngine', 'storageHandle(64)'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorFileFilePHIDType::TYPECONST);
}
public function save() {
if (!$this->getSecretKey()) {
$this->setSecretKey($this->generateSecretKey());
}
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public function saveAndIndex() {
$this->save();
if ($this->isIndexableFile()) {
PhabricatorSearchWorker::queueDocumentForIndexing($this->getPHID());
}
return $this;
}
private function isIndexableFile() {
if ($this->getIsChunk()) {
return false;
}
return true;
}
public function getMonogram() {
return 'F'.$this->getID();
}
public function scrambleSecret() {
return $this->setSecretKey($this->generateSecretKey());
}
public static function readUploadedFileData($spec) {
if (!$spec) {
throw new Exception(pht('No file was uploaded!'));
}
$err = idx($spec, 'error');
if ($err) {
throw new PhabricatorFileUploadException($err);
}
$tmp_name = idx($spec, 'tmp_name');
// NOTE: If we parsed the request body ourselves, the files we wrote will
// not be registered in the `is_uploaded_file()` list. It's fine to skip
// this check: it just protects against sloppy code from the long ago era
// of "register_globals".
if (ini_get('enable_post_data_reading')) {
$is_valid = @is_uploaded_file($tmp_name);
if (!$is_valid) {
throw new Exception(pht('File is not an uploaded file.'));
}
}
$file_data = Filesystem::readFile($tmp_name);
$file_size = idx($spec, 'size');
if (strlen($file_data) != $file_size) {
throw new Exception(pht('File size disagrees with uploaded size.'));
}
return $file_data;
}
public static function newFromPHPUpload($spec, array $params = array()) {
$file_data = self::readUploadedFileData($spec);
$file_name = nonempty(
idx($params, 'name'),
idx($spec, 'name'));
$params = array(
'name' => $file_name,
) + $params;
return self::newFromFileData($file_data, $params);
}
public static function newFromXHRUpload($data, array $params = array()) {
return self::newFromFileData($data, $params);
}
public static function newFileFromContentHash($hash, array $params) {
if ($hash === null) {
return null;
}
// Check to see if a file with same hash already exists.
$file = id(new PhabricatorFile())->loadOneWhere(
'contentHash = %s LIMIT 1',
$hash);
if (!$file) {
return null;
}
$copy_of_storage_engine = $file->getStorageEngine();
$copy_of_storage_handle = $file->getStorageHandle();
$copy_of_storage_format = $file->getStorageFormat();
$copy_of_storage_properties = $file->getStorageProperties();
$copy_of_byte_size = $file->getByteSize();
$copy_of_mime_type = $file->getMimeType();
$new_file = self::initializeNewFile();
$new_file->setByteSize($copy_of_byte_size);
$new_file->setContentHash($hash);
$new_file->setStorageEngine($copy_of_storage_engine);
$new_file->setStorageHandle($copy_of_storage_handle);
$new_file->setStorageFormat($copy_of_storage_format);
$new_file->setStorageProperties($copy_of_storage_properties);
$new_file->setMimeType($copy_of_mime_type);
$new_file->copyDimensions($file);
$new_file->readPropertiesFromParameters($params);
$new_file->saveAndIndex();
return $new_file;
}
public static function newChunkedFile(
PhabricatorFileStorageEngine $engine,
$length,
array $params) {
$file = self::initializeNewFile();
$file->setByteSize($length);
// NOTE: Once we receive the first chunk, we'll detect its MIME type and
// update the parent file if a MIME type hasn't been provided. This matters
// for large media files like video.
$mime_type = idx($params, 'mime-type');
if (!strlen($mime_type)) {
$file->setMimeType('application/octet-stream');
}
$chunked_hash = idx($params, 'chunkedHash');
// Get rid of this parameter now; we aren't passing it any further down
// the stack.
unset($params['chunkedHash']);
if ($chunked_hash) {
$file->setContentHash($chunked_hash);
} else {
// See PhabricatorChunkedFileStorageEngine::getChunkedHash() for some
// discussion of this.
$seed = Filesystem::readRandomBytes(64);
$hash = PhabricatorChunkedFileStorageEngine::getChunkedHashForInput(
$seed);
$file->setContentHash($hash);
}
$file->setStorageEngine($engine->getEngineIdentifier());
$file->setStorageHandle(PhabricatorFileChunk::newChunkHandle());
// Chunked files are always stored raw because they do not actually store
// data. The chunks do, and can be individually formatted.
$file->setStorageFormat(PhabricatorFileRawStorageFormat::FORMATKEY);
$file->setIsPartial(1);
$file->readPropertiesFromParameters($params);
return $file;
}
private static function buildFromFileData($data, array $params = array()) {
if (isset($params['storageEngines'])) {
$engines = $params['storageEngines'];
} else {
$size = strlen($data);
$engines = PhabricatorFileStorageEngine::loadStorageEngines($size);
if (!$engines) {
throw new Exception(
pht(
'No configured storage engine can store this file. See '.
'"Configuring File Storage" in the documentation for '.
'information on configuring storage engines.'));
}
}
assert_instances_of($engines, 'PhabricatorFileStorageEngine');
if (!$engines) {
throw new Exception(pht('No valid storage engines are available!'));
}
$file = self::initializeNewFile();
$aes_type = PhabricatorFileAES256StorageFormat::FORMATKEY;
$has_aes = PhabricatorKeyring::getDefaultKeyName($aes_type);
if ($has_aes !== null) {
$default_key = PhabricatorFileAES256StorageFormat::FORMATKEY;
} else {
$default_key = PhabricatorFileRawStorageFormat::FORMATKEY;
}
$key = idx($params, 'format', $default_key);
// Callers can pass in an object explicitly instead of a key. This is
// primarily useful for unit tests.
if ($key instanceof PhabricatorFileStorageFormat) {
$format = clone $key;
} else {
$format = clone PhabricatorFileStorageFormat::requireFormat($key);
}
$format->setFile($file);
$properties = $format->newStorageProperties();
$file->setStorageFormat($format->getStorageFormatKey());
$file->setStorageProperties($properties);
$data_handle = null;
$engine_identifier = null;
$integrity_hash = null;
$exceptions = array();
foreach ($engines as $engine) {
$engine_class = get_class($engine);
try {
$result = $file->writeToEngine(
$engine,
$data,
$params);
list($engine_identifier, $data_handle, $integrity_hash) = $result;
// We stored the file somewhere so stop trying to write it to other
// places.
break;
} catch (PhabricatorFileStorageConfigurationException $ex) {
// If an engine is outright misconfigured (or misimplemented), raise
// that immediately since it probably needs attention.
throw $ex;
} catch (Exception $ex) {
phlog($ex);
// If an engine doesn't work, keep trying all the other valid engines
// in case something else works.
$exceptions[$engine_class] = $ex;
}
}
if (!$data_handle) {
throw new PhutilAggregateException(
pht('All storage engines failed to write file:'),
$exceptions);
}
$file->setByteSize(strlen($data));
$hash = self::hashFileContent($data);
$file->setContentHash($hash);
$file->setStorageEngine($engine_identifier);
$file->setStorageHandle($data_handle);
$file->setIntegrityHash($integrity_hash);
$file->readPropertiesFromParameters($params);
if (!$file->getMimeType()) {
$tmp = new TempFile();
Filesystem::writeFile($tmp, $data);
$file->setMimeType(Filesystem::getMimeType($tmp));
unset($tmp);
}
try {
$file->updateDimensions(false);
} catch (Exception $ex) {
// Do nothing.
}
$file->saveAndIndex();
return $file;
}
public static function newFromFileData($data, array $params = array()) {
$hash = self::hashFileContent($data);
if ($hash !== null) {
$file = self::newFileFromContentHash($hash, $params);
if ($file) {
return $file;
}
}
return self::buildFromFileData($data, $params);
}
public function migrateToEngine(
PhabricatorFileStorageEngine $engine,
$make_copy) {
if (!$this->getID() || !$this->getStorageHandle()) {
throw new Exception(
pht("You can not migrate a file which hasn't yet been saved."));
}
$data = $this->loadFileData();
$params = array(
'name' => $this->getName(),
);
list($new_identifier, $new_handle, $integrity_hash) = $this->writeToEngine(
$engine,
$data,
$params);
$old_engine = $this->instantiateStorageEngine();
$old_identifier = $this->getStorageEngine();
$old_handle = $this->getStorageHandle();
$this->setStorageEngine($new_identifier);
$this->setStorageHandle($new_handle);
$this->setIntegrityHash($integrity_hash);
$this->save();
if (!$make_copy) {
$this->deleteFileDataIfUnused(
$old_engine,
$old_identifier,
$old_handle);
}
return $this;
}
public function migrateToStorageFormat(PhabricatorFileStorageFormat $format) {
if (!$this->getID() || !$this->getStorageHandle()) {
throw new Exception(
pht("You can not migrate a file which hasn't yet been saved."));
}
$data = $this->loadFileData();
$params = array(
'name' => $this->getName(),
);
$engine = $this->instantiateStorageEngine();
$old_handle = $this->getStorageHandle();
$properties = $format->newStorageProperties();
$this->setStorageFormat($format->getStorageFormatKey());
$this->setStorageProperties($properties);
list($identifier, $new_handle, $integrity_hash) = $this->writeToEngine(
$engine,
$data,
$params);
$this->setStorageHandle($new_handle);
$this->setIntegrityHash($integrity_hash);
$this->save();
$this->deleteFileDataIfUnused(
$engine,
$identifier,
$old_handle);
return $this;
}
public function cycleMasterStorageKey(PhabricatorFileStorageFormat $format) {
if (!$this->getID() || !$this->getStorageHandle()) {
throw new Exception(
pht("You can not cycle keys for a file which hasn't yet been saved."));
}
$properties = $format->cycleStorageProperties();
$this->setStorageProperties($properties);
$this->save();
return $this;
}
private function writeToEngine(
PhabricatorFileStorageEngine $engine,
$data,
array $params) {
$engine_class = get_class($engine);
$format = $this->newStorageFormat();
$data_iterator = array($data);
$formatted_iterator = $format->newWriteIterator($data_iterator);
$formatted_data = $this->loadDataFromIterator($formatted_iterator);
$integrity_hash = $engine->newIntegrityHash($formatted_data, $format);
$data_handle = $engine->writeFile($formatted_data, $params);
if (!$data_handle || strlen($data_handle) > 255) {
// This indicates an improperly implemented storage engine.
throw new PhabricatorFileStorageConfigurationException(
pht(
"Storage engine '%s' executed %s but did not return a valid ".
"handle ('%s') to the data: it must be nonempty and no longer ".
"than 255 characters.",
$engine_class,
'writeFile()',
$data_handle));
}
$engine_identifier = $engine->getEngineIdentifier();
if (!$engine_identifier || strlen($engine_identifier) > 32) {
throw new PhabricatorFileStorageConfigurationException(
pht(
"Storage engine '%s' returned an improper engine identifier '{%s}': ".
"it must be nonempty and no longer than 32 characters.",
$engine_class,
$engine_identifier));
}
return array($engine_identifier, $data_handle, $integrity_hash);
}
/**
* Download a remote resource over HTTP and save the response body as a file.
*
* This method respects `security.outbound-blacklist`, and protects against
* HTTP redirection (by manually following "Location" headers and verifying
* each destination). It does not protect against DNS rebinding. See
* discussion in T6755.
*/
public static function newFromFileDownload($uri, array $params = array()) {
$timeout = 5;
$redirects = array();
$current = $uri;
while (true) {
try {
if (count($redirects) > 10) {
throw new Exception(
pht('Too many redirects trying to fetch remote URI.'));
}
$resolved = PhabricatorEnv::requireValidRemoteURIForFetch(
$current,
array(
'http',
'https',
));
list($resolved_uri, $resolved_domain) = $resolved;
$current = new PhutilURI($current);
if ($current->getProtocol() == 'http') {
// For HTTP, we can use a pre-resolved URI to defuse DNS rebinding.
$fetch_uri = $resolved_uri;
$fetch_host = $resolved_domain;
} else {
// For HTTPS, we can't: cURL won't verify the SSL certificate if
// the domain has been replaced with an IP. But internal services
// presumably will not have valid certificates for rebindable
// domain names on attacker-controlled domains, so the DNS rebinding
// attack should generally not be possible anyway.
$fetch_uri = $current;
$fetch_host = null;
}
$future = id(new HTTPSFuture($fetch_uri))
->setFollowLocation(false)
->setTimeout($timeout);
if ($fetch_host !== null) {
$future->addHeader('Host', $fetch_host);
}
list($status, $body, $headers) = $future->resolve();
if ($status->isRedirect()) {
// This is an HTTP 3XX status, so look for a "Location" header.
$location = null;
foreach ($headers as $header) {
list($name, $value) = $header;
if (phutil_utf8_strtolower($name) == 'location') {
$location = $value;
break;
}
}
// HTTP 3XX status with no "Location" header, just treat this like
// a normal HTTP error.
if ($location === null) {
throw $status;
}
if (isset($redirects[$location])) {
throw new Exception(
pht('Encountered loop while following redirects.'));
}
$redirects[$location] = $location;
$current = $location;
// We'll fall off the bottom and go try this URI now.
} else if ($status->isError()) {
// This is something other than an HTTP 2XX or HTTP 3XX status, so
// just bail out.
throw $status;
} else {
// This is HTTP 2XX, so use the response body to save the file data.
// Provide a default name based on the URI, truncating it if the URI
// is exceptionally long.
$default_name = basename($uri);
$default_name = id(new PhutilUTF8StringTruncator())
->setMaximumBytes(64)
->truncateString($default_name);
$params = $params + array(
'name' => $default_name,
);
return self::newFromFileData($body, $params);
}
} catch (Exception $ex) {
if ($redirects) {
throw new PhutilProxyException(
pht(
'Failed to fetch remote URI "%s" after following %s redirect(s) '.
'(%s): %s',
$uri,
phutil_count($redirects),
implode(' > ', array_keys($redirects)),
$ex->getMessage()),
$ex);
} else {
throw $ex;
}
}
}
}
public static function normalizeFileName($file_name) {
$pattern = "@[\\x00-\\x19#%&+!~'\$\"\/=\\\\?<> ]+@";
$file_name = preg_replace($pattern, '_', $file_name);
$file_name = preg_replace('@_+@', '_', $file_name);
$file_name = trim($file_name, '_');
$disallowed_filenames = array(
'.' => 'dot',
'..' => 'dotdot',
'' => 'file',
);
$file_name = idx($disallowed_filenames, $file_name, $file_name);
return $file_name;
}
public function delete() {
// We want to delete all the rows which mark this file as the transformation
// of some other file (since we're getting rid of it). We also delete all
// the transformations of this file, so that a user who deletes an image
// doesn't need to separately hunt down and delete a bunch of thumbnails and
// resizes of it.
$outbound_xforms = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withTransforms(
array(
array(
'originalPHID' => $this->getPHID(),
'transform' => true,
),
))
->execute();
foreach ($outbound_xforms as $outbound_xform) {
$outbound_xform->delete();
}
$inbound_xforms = id(new PhabricatorTransformedFile())->loadAllWhere(
'transformedPHID = %s',
$this->getPHID());
$this->openTransaction();
foreach ($inbound_xforms as $inbound_xform) {
$inbound_xform->delete();
}
$ret = parent::delete();
$this->saveTransaction();
$this->deleteFileDataIfUnused(
$this->instantiateStorageEngine(),
$this->getStorageEngine(),
$this->getStorageHandle());
return $ret;
}
/**
* Destroy stored file data if there are no remaining files which reference
* it.
*/
public function deleteFileDataIfUnused(
PhabricatorFileStorageEngine $engine,
$engine_identifier,
$handle) {
// Check to see if any files are using storage.
$usage = id(new PhabricatorFile())->loadAllWhere(
'storageEngine = %s AND storageHandle = %s LIMIT 1',
$engine_identifier,
$handle);
// If there are no files using the storage, destroy the actual storage.
if (!$usage) {
try {
$engine->deleteFile($handle);
} catch (Exception $ex) {
// In the worst case, we're leaving some data stranded in a storage
// engine, which is not a big deal.
phlog($ex);
}
}
}
public static function hashFileContent($data) {
// NOTE: Hashing can fail if the algorithm isn't available in the current
// build of PHP. It's fine if we're unable to generate a content hash:
// it just means we'll store extra data when users upload duplicate files
// instead of being able to deduplicate it.
$hash = hash('sha256', $data, $raw_output = false);
if ($hash === false) {
return null;
}
return $hash;
}
public function loadFileData() {
$iterator = $this->getFileDataIterator();
return $this->loadDataFromIterator($iterator);
}
/**
* Return an iterable which emits file content bytes.
*
* @param int Offset for the start of data.
* @param int Offset for the end of data.
* @return Iterable Iterable object which emits requested data.
*/
public function getFileDataIterator($begin = null, $end = null) {
$engine = $this->instantiateStorageEngine();
$format = $this->newStorageFormat();
$iterator = $engine->getRawFileDataIterator(
$this,
$begin,
$end,
$format);
return $iterator;
}
public function getURI() {
return $this->getInfoURI();
}
public function getViewURI() {
if (!$this->getPHID()) {
throw new Exception(
pht('You must save a file before you can generate a view URI.'));
}
return $this->getCDNURI('data');
}
public function getCDNURI($request_kind) {
if (($request_kind !== 'data') &&
($request_kind !== 'download')) {
throw new Exception(
pht(
'Unknown file content request kind "%s".',
$request_kind));
}
$name = self::normalizeFileName($this->getName());
$name = phutil_escape_uri($name);
$parts = array();
$parts[] = 'file';
$parts[] = $request_kind;
// If this is an instanced install, add the instance identifier to the URI.
// Instanced configurations behind a CDN may not be able to control the
// request domain used by the CDN (as with AWS CloudFront). Embedding the
// instance identity in the path allows us to distinguish between requests
// originating from different instances but served through the same CDN.
$instance = PhabricatorEnv::getEnvConfig('cluster.instance');
if (strlen($instance)) {
$parts[] = '@'.$instance;
}
$parts[] = $this->getSecretKey();
$parts[] = $this->getPHID();
$parts[] = $name;
$path = '/'.implode('/', $parts);
// If this file is only partially uploaded, we're just going to return a
// local URI to make sure that Ajax works, since the page is inevitably
// going to give us an error back.
if ($this->getIsPartial()) {
return PhabricatorEnv::getURI($path);
} else {
return PhabricatorEnv::getCDNURI($path);
}
}
public function getInfoURI() {
return '/'.$this->getMonogram();
}
public function getBestURI() {
if ($this->isViewableInBrowser()) {
return $this->getViewURI();
} else {
return $this->getInfoURI();
}
}
public function getDownloadURI() {
return $this->getCDNURI('download');
}
public function getURIForTransform(PhabricatorFileTransform $transform) {
return $this->getTransformedURI($transform->getTransformKey());
}
private function getTransformedURI($transform) {
$parts = array();
$parts[] = 'file';
$parts[] = 'xform';
$instance = PhabricatorEnv::getEnvConfig('cluster.instance');
if (strlen($instance)) {
$parts[] = '@'.$instance;
}
$parts[] = $transform;
$parts[] = $this->getPHID();
$parts[] = $this->getSecretKey();
$path = implode('/', $parts);
$path = $path.'/';
return PhabricatorEnv::getCDNURI($path);
}
public function isViewableInBrowser() {
return ($this->getViewableMimeType() !== null);
}
public function isViewableImage() {
if (!$this->isViewableInBrowser()) {
return false;
}
$mime_map = PhabricatorEnv::getEnvConfig('files.image-mime-types');
$mime_type = $this->getMimeType();
return idx($mime_map, $mime_type);
}
public function isAudio() {
if (!$this->isViewableInBrowser()) {
return false;
}
$mime_map = PhabricatorEnv::getEnvConfig('files.audio-mime-types');
$mime_type = $this->getMimeType();
return idx($mime_map, $mime_type);
}
public function isVideo() {
if (!$this->isViewableInBrowser()) {
return false;
}
$mime_map = PhabricatorEnv::getEnvConfig('files.video-mime-types');
$mime_type = $this->getMimeType();
return idx($mime_map, $mime_type);
}
public function isPDF() {
if (!$this->isViewableInBrowser()) {
return false;
}
$mime_map = array(
'application/pdf' => 'application/pdf',
);
$mime_type = $this->getMimeType();
return idx($mime_map, $mime_type);
}
public function isTransformableImage() {
// NOTE: The way the 'gd' extension works in PHP is that you can install it
// with support for only some file types, so it might be able to handle
// PNG but not JPEG. Try to generate thumbnails for whatever we can. Setup
// warns you if you don't have complete support.
$matches = null;
$ok = preg_match(
'@^image/(gif|png|jpe?g)@',
$this->getViewableMimeType(),
$matches);
if (!$ok) {
return false;
}
switch ($matches[1]) {
case 'jpg';
case 'jpeg':
return function_exists('imagejpeg');
break;
case 'png':
return function_exists('imagepng');
break;
case 'gif':
return function_exists('imagegif');
break;
default:
throw new Exception(pht('Unknown type matched as image MIME type.'));
}
}
public static function getTransformableImageFormats() {
$supported = array();
if (function_exists('imagejpeg')) {
$supported[] = 'jpg';
}
if (function_exists('imagepng')) {
$supported[] = 'png';
}
if (function_exists('imagegif')) {
$supported[] = 'gif';
}
return $supported;
}
public function getDragAndDropDictionary() {
return array(
'id' => $this->getID(),
'phid' => $this->getPHID(),
'uri' => $this->getBestURI(),
);
}
public function instantiateStorageEngine() {
return self::buildEngine($this->getStorageEngine());
}
public static function buildEngine($engine_identifier) {
$engines = self::buildAllEngines();
foreach ($engines as $engine) {
if ($engine->getEngineIdentifier() == $engine_identifier) {
return $engine;
}
}
throw new Exception(
pht(
"Storage engine '%s' could not be located!",
$engine_identifier));
}
public static function buildAllEngines() {
return id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorFileStorageEngine')
->execute();
}
public function getViewableMimeType() {
$mime_map = PhabricatorEnv::getEnvConfig('files.viewable-mime-types');
$mime_type = $this->getMimeType();
$mime_parts = explode(';', $mime_type);
$mime_type = trim(reset($mime_parts));
return idx($mime_map, $mime_type);
}
public function getDisplayIconForMimeType() {
$mime_map = PhabricatorEnv::getEnvConfig('files.icon-mime-types');
$mime_type = $this->getMimeType();
return idx($mime_map, $mime_type, 'fa-file-o');
}
public function validateSecretKey($key) {
return ($key == $this->getSecretKey());
}
public function generateSecretKey() {
return Filesystem::readRandomCharacters(20);
}
public function setStorageProperties(array $properties) {
$this->metadata[self::METADATA_STORAGE] = $properties;
return $this;
}
public function getStorageProperties() {
return idx($this->metadata, self::METADATA_STORAGE, array());
}
public function getStorageProperty($key, $default = null) {
$properties = $this->getStorageProperties();
return idx($properties, $key, $default);
}
public function loadDataFromIterator($iterator) {
$result = '';
foreach ($iterator as $chunk) {
$result .= $chunk;
}
return $result;
}
public function updateDimensions($save = true) {
if (!$this->isViewableImage()) {
throw new Exception(pht('This file is not a viewable image.'));
}
if (!function_exists('imagecreatefromstring')) {
throw new Exception(pht('Cannot retrieve image information.'));
}
if ($this->getIsChunk()) {
throw new Exception(
pht('Refusing to assess image dimensions of file chunk.'));
}
$engine = $this->instantiateStorageEngine();
if ($engine->isChunkEngine()) {
throw new Exception(
pht('Refusing to assess image dimensions of chunked file.'));
}
$data = $this->loadFileData();
$img = @imagecreatefromstring($data);
if ($img === false) {
throw new Exception(pht('Error when decoding image.'));
}
$this->metadata[self::METADATA_IMAGE_WIDTH] = imagesx($img);
$this->metadata[self::METADATA_IMAGE_HEIGHT] = imagesy($img);
if ($save) {
$this->save();
}
return $this;
}
public function copyDimensions(PhabricatorFile $file) {
$metadata = $file->getMetadata();
$width = idx($metadata, self::METADATA_IMAGE_WIDTH);
if ($width) {
$this->metadata[self::METADATA_IMAGE_WIDTH] = $width;
}
$height = idx($metadata, self::METADATA_IMAGE_HEIGHT);
if ($height) {
$this->metadata[self::METADATA_IMAGE_HEIGHT] = $height;
}
return $this;
}
/**
* Load (or build) the {@class:PhabricatorFile} objects for builtin file
* resources. The builtin mechanism allows files shipped with Phabricator
* to be treated like normal files so that APIs do not need to special case
* things like default images or deleted files.
*
* Builtins are located in `resources/builtin/` and identified by their
* name.
*
* @param PhabricatorUser Viewing user.
* @param list<PhabricatorFilesBuiltinFile> List of builtin file specs.
* @return dict<string, PhabricatorFile> Dictionary of named builtins.
*/
public static function loadBuiltins(PhabricatorUser $user, array $builtins) {
$builtins = mpull($builtins, null, 'getBuiltinFileKey');
// NOTE: Anyone is allowed to access builtin files.
$files = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withBuiltinKeys(array_keys($builtins))
->execute();
$results = array();
foreach ($files as $file) {
$builtin_key = $file->getBuiltinName();
if ($builtin_key !== null) {
$results[$builtin_key] = $file;
}
}
$build = array();
foreach ($builtins as $key => $builtin) {
if (isset($results[$key])) {
continue;
}
$data = $builtin->loadBuiltinFileData();
$params = array(
'name' => $builtin->getBuiltinDisplayName(),
'canCDN' => true,
'builtin' => $key,
);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
try {
$file = self::newFromFileData($data, $params);
} catch (AphrontDuplicateKeyQueryException $ex) {
$file = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withBuiltinKeys(array($key))
->executeOne();
if (!$file) {
throw new Exception(
pht(
'Collided mid-air when generating builtin file "%s", but '.
'then failed to load the object we collided with.',
$key));
}
}
unset($unguarded);
$file->attachObjectPHIDs(array());
$file->attachObjects(array());
$results[$key] = $file;
}
return $results;
}
/**
* Convenience wrapper for @{method:loadBuiltins}.
*
* @param PhabricatorUser Viewing user.
* @param string Single builtin name to load.
* @return PhabricatorFile Corresponding builtin file.
*/
public static function loadBuiltin(PhabricatorUser $user, $name) {
$builtin = id(new PhabricatorFilesOnDiskBuiltinFile())
->setName($name);
$key = $builtin->getBuiltinFileKey();
return idx(self::loadBuiltins($user, array($builtin)), $key);
}
public function getObjects() {
return $this->assertAttached($this->objects);
}
public function attachObjects(array $objects) {
$this->objects = $objects;
return $this;
}
public function getObjectPHIDs() {
return $this->assertAttached($this->objectPHIDs);
}
public function attachObjectPHIDs(array $object_phids) {
$this->objectPHIDs = $object_phids;
return $this;
}
public function getOriginalFile() {
return $this->assertAttached($this->originalFile);
}
public function attachOriginalFile(PhabricatorFile $file = null) {
$this->originalFile = $file;
return $this;
}
public function getImageHeight() {
if (!$this->isViewableImage()) {
return null;
}
return idx($this->metadata, self::METADATA_IMAGE_HEIGHT);
}
public function getImageWidth() {
if (!$this->isViewableImage()) {
return null;
}
return idx($this->metadata, self::METADATA_IMAGE_WIDTH);
}
public function getCanCDN() {
if (!$this->isViewableImage()) {
return false;
}
return idx($this->metadata, self::METADATA_CAN_CDN);
}
public function setCanCDN($can_cdn) {
$this->metadata[self::METADATA_CAN_CDN] = $can_cdn ? 1 : 0;
return $this;
}
public function isBuiltin() {
return ($this->getBuiltinName() !== null);
}
public function getBuiltinName() {
return idx($this->metadata, self::METADATA_BUILTIN);
}
public function setBuiltinName($name) {
$this->metadata[self::METADATA_BUILTIN] = $name;
return $this;
}
public function getIsProfileImage() {
return idx($this->metadata, self::METADATA_PROFILE);
}
public function setIsProfileImage($value) {
$this->metadata[self::METADATA_PROFILE] = $value;
return $this;
}
public function getIsChunk() {
return idx($this->metadata, self::METADATA_CHUNK);
}
public function setIsChunk($value) {
$this->metadata[self::METADATA_CHUNK] = $value;
return $this;
}
public function setIntegrityHash($integrity_hash) {
$this->metadata[self::METADATA_INTEGRITY] = $integrity_hash;
return $this;
}
public function getIntegrityHash() {
return idx($this->metadata, self::METADATA_INTEGRITY);
}
public function newIntegrityHash() {
$engine = $this->instantiateStorageEngine();
if ($engine->isChunkEngine()) {
return null;
}
$format = $this->newStorageFormat();
$storage_handle = $this->getStorageHandle();
$data = $engine->readFile($storage_handle);
return $engine->newIntegrityHash($data, $format);
}
/**
* Write the policy edge between this file and some object.
*
* @param phid Object PHID to attach to.
* @return this
*/
public function attachToObject($phid) {
$edge_type = PhabricatorObjectHasFileEdgeType::EDGECONST;
id(new PhabricatorEdgeEditor())
->addEdge($phid, $edge_type, $this->getPHID())
->save();
return $this;
}
/**
* Remove the policy edge between this file and some object.
*
* @param phid Object PHID to detach from.
* @return this
*/
public function detachFromObject($phid) {
$edge_type = PhabricatorObjectHasFileEdgeType::EDGECONST;
id(new PhabricatorEdgeEditor())
->removeEdge($phid, $edge_type, $this->getPHID())
->save();
return $this;
}
/**
* Configure a newly created file object according to specified parameters.
*
* This method is called both when creating a file from fresh data, and
* when creating a new file which reuses existing storage.
*
* @param map<string, wild> Bag of parameters, see @{class:PhabricatorFile}
* for documentation.
* @return this
*/
private function readPropertiesFromParameters(array $params) {
PhutilTypeSpec::checkMap(
$params,
array(
'name' => 'optional string',
'authorPHID' => 'optional string',
'ttl.relative' => 'optional int',
'ttl.absolute' => 'optional int',
'viewPolicy' => 'optional string',
'isExplicitUpload' => 'optional bool',
'canCDN' => 'optional bool',
'profile' => 'optional bool',
'format' => 'optional string|PhabricatorFileStorageFormat',
'mime-type' => 'optional string',
'builtin' => 'optional string',
'storageEngines' => 'optional list<PhabricatorFileStorageEngine>',
'chunk' => 'optional bool',
));
$file_name = idx($params, 'name');
$this->setName($file_name);
$author_phid = idx($params, 'authorPHID');
$this->setAuthorPHID($author_phid);
$absolute_ttl = idx($params, 'ttl.absolute');
$relative_ttl = idx($params, 'ttl.relative');
if ($absolute_ttl !== null && $relative_ttl !== null) {
throw new Exception(
pht(
'Specify an absolute TTL or a relative TTL, but not both.'));
} else if ($absolute_ttl !== null) {
if ($absolute_ttl < PhabricatorTime::getNow()) {
throw new Exception(
pht(
'Absolute TTL must be in the present or future, but TTL "%s" '.
'is in the past.',
$absolute_ttl));
}
$this->setTtl($absolute_ttl);
} else if ($relative_ttl !== null) {
if ($relative_ttl < 0) {
throw new Exception(
pht(
'Relative TTL must be zero or more seconds, but "%s" is '.
'negative.',
$relative_ttl));
}
$max_relative = phutil_units('365 days in seconds');
if ($relative_ttl > $max_relative) {
throw new Exception(
pht(
'Relative TTL must not be more than "%s" seconds, but TTL '.
'"%s" was specified.',
$max_relative,
$relative_ttl));
}
$absolute_ttl = PhabricatorTime::getNow() + $relative_ttl;
$this->setTtl($absolute_ttl);
}
$view_policy = idx($params, 'viewPolicy');
if ($view_policy) {
$this->setViewPolicy($params['viewPolicy']);
}
$is_explicit = (idx($params, 'isExplicitUpload') ? 1 : 0);
$this->setIsExplicitUpload($is_explicit);
$can_cdn = idx($params, 'canCDN');
if ($can_cdn) {
$this->setCanCDN(true);
}
$builtin = idx($params, 'builtin');
if ($builtin) {
$this->setBuiltinName($builtin);
$this->setBuiltinKey($builtin);
}
$profile = idx($params, 'profile');
if ($profile) {
$this->setIsProfileImage(true);
}
$mime_type = idx($params, 'mime-type');
if ($mime_type) {
$this->setMimeType($mime_type);
}
$is_chunk = idx($params, 'chunk');
if ($is_chunk) {
$this->setIsChunk(true);
}
return $this;
}
public function getRedirectResponse() {
$uri = $this->getBestURI();
// TODO: This is a bit iffy. Sometimes, getBestURI() returns a CDN URI
// (if the file is a viewable image) and sometimes a local URI (if not).
// For now, just detect which one we got and configure the response
// appropriately. In the long run, if this endpoint is served from a CDN
// domain, we can't issue a local redirect to an info URI (which is not
// present on the CDN domain). We probably never actually issue local
// redirects here anyway, since we only ever transform viewable images
// right now.
$is_external = strlen(id(new PhutilURI($uri))->getDomain());
return id(new AphrontRedirectResponse())
->setIsExternal($is_external)
->setURI($uri);
}
public function newDownloadResponse() {
// We're cheating a little bit here and relying on the fact that
// getDownloadURI() always returns a fully qualified URI with a complete
// domain.
return id(new AphrontRedirectResponse())
->setIsExternal(true)
->setCloseDialogBeforeRedirect(true)
->setURI($this->getDownloadURI());
}
public function attachTransforms(array $map) {
$this->transforms = $map;
return $this;
}
public function getTransform($key) {
return $this->assertAttachedKey($this->transforms, $key);
}
public function newStorageFormat() {
$key = $this->getStorageFormat();
$template = PhabricatorFileStorageFormat::requireFormat($key);
$format = id(clone $template)
->setFile($this);
return $format;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorFileEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorFileTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
if ($this->isBuiltin()) {
return PhabricatorPolicies::getMostOpenPolicy();
}
if ($this->getIsProfileImage()) {
return PhabricatorPolicies::getMostOpenPolicy();
}
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return PhabricatorPolicies::POLICY_NOONE;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
$viewer_phid = $viewer->getPHID();
if ($viewer_phid) {
if ($this->getAuthorPHID() == $viewer_phid) {
return true;
}
}
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
// If you can see the file this file is a transform of, you can see
// this file.
if ($this->getOriginalFile()) {
return true;
}
// If you can see any object this file is attached to, you can see
// the file.
return (count($this->getObjects()) > 0);
}
return false;
}
public function describeAutomaticCapability($capability) {
$out = array();
$out[] = pht('The user who uploaded a file can always view and edit it.');
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$out[] = pht(
'Files attached to objects are visible to users who can view '.
'those objects.');
$out[] = pht(
'Thumbnails are visible only to users who can view the original '.
'file.');
break;
}
return $out;
}
/* -( PhabricatorSubscribableInterface Implementation )-------------------- */
public function isAutomaticallySubscribed($phid) {
return ($this->authorPHID == $phid);
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getAuthorPHID(),
);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of the file.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('dataURI')
->setType('string')
->setDescription(pht('Download URI for the file data.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('size')
->setType('int')
->setDescription(pht('File size, in bytes.')),
);
}
public function getFieldValuesForConduit() {
return array(
'name' => $this->getName(),
'dataURI' => $this->getCDNURI('data'),
'size' => (int)$this->getByteSize(),
);
}
public function getConduitSearchAttachments() {
return array();
}
/* -( PhabricatorNgramInterface )------------------------------------------ */
public function newNgrams() {
return array(
id(new PhabricatorFileNameNgrams())
->setValue($this->getName()),
);
}
}
diff --git a/src/applications/fund/storage/FundBacker.php b/src/applications/fund/storage/FundBacker.php
index d2a97cd32..87ab342e2 100644
--- a/src/applications/fund/storage/FundBacker.php
+++ b/src/applications/fund/storage/FundBacker.php
@@ -1,128 +1,117 @@
<?php
final class FundBacker extends FundDAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface {
protected $initiativePHID;
protected $backerPHID;
protected $amountAsCurrency;
protected $status;
protected $properties = array();
private $initiative = self::ATTACHABLE;
const STATUS_NEW = 'new';
const STATUS_IN_CART = 'in-cart';
const STATUS_PURCHASED = 'purchased';
public static function initializeNewBacker(PhabricatorUser $actor) {
return id(new FundBacker())
->setBackerPHID($actor->getPHID())
->setStatus(self::STATUS_NEW);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_APPLICATION_SERIALIZERS => array(
'amountAsCurrency' => new PhortuneCurrencySerializer(),
),
self::CONFIG_COLUMN_SCHEMA => array(
'status' => 'text32',
'amountAsCurrency' => 'text64',
),
self::CONFIG_KEY_SCHEMA => array(
'key_initiative' => array(
'columns' => array('initiativePHID'),
),
'key_backer' => array(
'columns' => array('backerPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(FundBackerPHIDType::TYPECONST);
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getInitiative() {
return $this->assertAttached($this->initiative);
}
public function attachInitiative(FundInitiative $initiative = null) {
$this->initiative = $initiative;
return $this;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
// If we have the initiative, use the initiative's policy.
// Otherwise, return NOONE. This allows the backer to continue seeing
// a backer even if they're no longer allowed to see the initiative.
$initiative = $this->getInitiative();
if ($initiative) {
return $initiative->getPolicy($capability);
}
return PhabricatorPolicies::POLICY_NOONE;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return ($viewer->getPHID() == $this->getBackerPHID());
}
public function describeAutomaticCapability($capability) {
return pht('A backer can always see what they have backed.');
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new FundBackerEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new FundBackerTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
}
diff --git a/src/applications/fund/storage/FundInitiative.php b/src/applications/fund/storage/FundInitiative.php
index 2b9fbf1f7..5e4dd4802 100644
--- a/src/applications/fund/storage/FundInitiative.php
+++ b/src/applications/fund/storage/FundInitiative.php
@@ -1,224 +1,213 @@
<?php
final class FundInitiative extends FundDAO
implements
PhabricatorPolicyInterface,
PhabricatorProjectInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorSubscribableInterface,
PhabricatorMentionableInterface,
PhabricatorFlaggableInterface,
PhabricatorTokenReceiverInterface,
PhabricatorDestructibleInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface {
protected $name;
protected $ownerPHID;
protected $merchantPHID;
protected $description;
protected $risks;
protected $viewPolicy;
protected $editPolicy;
protected $status;
protected $totalAsCurrency;
protected $mailKey;
private $projectPHIDs = self::ATTACHABLE;
const STATUS_OPEN = 'open';
const STATUS_CLOSED = 'closed';
public static function getStatusNameMap() {
return array(
self::STATUS_OPEN => pht('Open'),
self::STATUS_CLOSED => pht('Closed'),
);
}
public static function initializeNewInitiative(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorFundApplication'))
->executeOne();
$view_policy = $app->getPolicy(FundDefaultViewCapability::CAPABILITY);
return id(new FundInitiative())
->setOwnerPHID($actor->getPHID())
->setViewPolicy($view_policy)
->setEditPolicy($actor->getPHID())
->setStatus(self::STATUS_OPEN)
->setTotalAsCurrency(PhortuneCurrency::newEmptyCurrency());
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text255',
'description' => 'text',
'risks' => 'text',
'status' => 'text32',
'merchantPHID' => 'phid?',
'totalAsCurrency' => 'text64',
'mailKey' => 'bytes20',
),
self::CONFIG_APPLICATION_SERIALIZERS => array(
'totalAsCurrency' => new PhortuneCurrencySerializer(),
),
self::CONFIG_KEY_SCHEMA => array(
'key_status' => array(
'columns' => array('status'),
),
'key_owner' => array(
'columns' => array('ownerPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(FundInitiativePHIDType::TYPECONST);
}
public function getMonogram() {
return 'I'.$this->getID();
}
public function getViewURI() {
return '/'.$this->getMonogram();
}
public function getProjectPHIDs() {
return $this->assertAttached($this->projectPHIDs);
}
public function attachProjectPHIDs(array $phids) {
$this->projectPHIDs = $phids;
return $this;
}
public function isClosed() {
return ($this->getStatus() == self::STATUS_CLOSED);
}
public function save() {
if (!$this->mailKey) {
$this->mailKey = Filesystem::readRandomCharacters(20);
}
return parent::save();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
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 $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($viewer->getPHID() == $this->getOwnerPHID()) {
return true;
}
if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
foreach ($viewer->getAuthorities() as $authority) {
if ($authority instanceof PhortuneMerchant) {
if ($authority->getPHID() == $this->getMerchantPHID()) {
return true;
}
}
}
}
return false;
}
public function describeAutomaticCapability($capability) {
return pht('The owner of an initiative can always view and edit it.');
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new FundInitiativeEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new FundInitiativeTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return ($phid == $this->getOwnerPHID());
}
/* -( PhabricatorTokenRecevierInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getOwnerPHID(),
);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new FundInitiativeFulltextEngine();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new FundInitiativeFerretEngine();
}
}
diff --git a/src/applications/harbormaster/engine/HarbormasterBuildableEngine.php b/src/applications/harbormaster/engine/HarbormasterBuildableEngine.php
index 25222975b..7f743b3ad 100644
--- a/src/applications/harbormaster/engine/HarbormasterBuildableEngine.php
+++ b/src/applications/harbormaster/engine/HarbormasterBuildableEngine.php
@@ -1,108 +1,106 @@
<?php
abstract class HarbormasterBuildableEngine
extends Phobject {
private $viewer;
private $actingAsPHID;
private $contentSource;
private $object;
final public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
final public function getViewer() {
return $this->viewer;
}
final public function setActingAsPHID($acting_as_phid) {
$this->actingAsPHID = $acting_as_phid;
return $this;
}
final public function getActingAsPHID() {
return $this->actingAsPHID;
}
final public function setContentSource(
PhabricatorContentSource $content_source) {
$this->contentSource = $content_source;
return $this;
}
final public function getContentSource() {
return $this->contentSource;
}
final public function setObject(HarbormasterBuildableInterface $object) {
$this->object = $object;
return $this;
}
final public function getObject() {
return $this->object;
}
protected function getPublishableObject() {
return $this->getObject();
}
public function publishBuildable(
HarbormasterBuildable $old,
HarbormasterBuildable $new) {
return;
}
final public static function newForObject(
HarbormasterBuildableInterface $object,
PhabricatorUser $viewer) {
return $object->newBuildableEngine()
->setViewer($viewer)
->setObject($object);
}
final protected function newEditor() {
$publishable = $this->getPublishableObject();
$viewer = $this->getViewer();
$editor = $publishable->getApplicationTransactionEditor()
->setActor($viewer)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
$acting_as_phid = $this->getActingAsPHID();
if ($acting_as_phid !== null) {
$editor->setActingAsPHID($acting_as_phid);
}
$content_source = $this->getContentSource();
if ($content_source !== null) {
$editor->setContentSource($content_source);
}
return $editor;
}
final protected function newTransaction() {
$publishable = $this->getPublishableObject();
return $publishable->getApplicationTransactionTemplate();
}
final protected function applyTransactions(array $xactions) {
$publishable = $this->getPublishableObject();
$editor = $this->newEditor();
- $editor->applyTransactions(
- $publishable->getApplicationTransactionObject(),
- $xactions);
+ $editor->applyTransactions($publishable, $xactions);
}
public function getAuthorIdentity() {
return null;
}
}
diff --git a/src/applications/harbormaster/storage/HarbormasterBuildable.php b/src/applications/harbormaster/storage/HarbormasterBuildable.php
index 98f374579..aabfd49eb 100644
--- a/src/applications/harbormaster/storage/HarbormasterBuildable.php
+++ b/src/applications/harbormaster/storage/HarbormasterBuildable.php
@@ -1,427 +1,416 @@
<?php
final class HarbormasterBuildable
extends HarbormasterDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
HarbormasterBuildableInterface,
PhabricatorConduitResultInterface,
PhabricatorDestructibleInterface {
protected $buildablePHID;
protected $containerPHID;
protected $buildableStatus;
protected $isManualBuildable;
private $buildableObject = self::ATTACHABLE;
private $containerObject = self::ATTACHABLE;
private $builds = self::ATTACHABLE;
public static function initializeNewBuildable(PhabricatorUser $actor) {
return id(new HarbormasterBuildable())
->setIsManualBuildable(0)
->setBuildableStatus(HarbormasterBuildableStatus::STATUS_PREPARING);
}
public function getMonogram() {
return 'B'.$this->getID();
}
public function getURI() {
return '/'.$this->getMonogram();
}
/**
* Returns an existing buildable for the object's PHID or creates a
* new buildable implicitly if needed.
*/
public static function createOrLoadExisting(
PhabricatorUser $actor,
$buildable_object_phid,
$container_object_phid) {
$buildable = id(new HarbormasterBuildableQuery())
->setViewer($actor)
->withBuildablePHIDs(array($buildable_object_phid))
->withManualBuildables(false)
->setLimit(1)
->executeOne();
if ($buildable) {
return $buildable;
}
$buildable = self::initializeNewBuildable($actor)
->setBuildablePHID($buildable_object_phid)
->setContainerPHID($container_object_phid);
$buildable->save();
return $buildable;
}
/**
* Start builds for a given buildable.
*
* @param phid PHID of the object to build.
* @param phid Container PHID for the buildable.
* @param list<HarbormasterBuildRequest> List of builds to perform.
* @return void
*/
public static function applyBuildPlans(
$phid,
$container_phid,
array $requests) {
assert_instances_of($requests, 'HarbormasterBuildRequest');
if (!$requests) {
return;
}
// Skip all of this logic if the Harbormaster application
// isn't currently installed.
$harbormaster_app = 'PhabricatorHarbormasterApplication';
if (!PhabricatorApplication::isClassInstalled($harbormaster_app)) {
return;
}
$viewer = PhabricatorUser::getOmnipotentUser();
$buildable = self::createOrLoadExisting(
$viewer,
$phid,
$container_phid);
$plan_phids = mpull($requests, 'getBuildPlanPHID');
$plans = id(new HarbormasterBuildPlanQuery())
->setViewer($viewer)
->withPHIDs($plan_phids)
->execute();
$plans = mpull($plans, null, 'getPHID');
foreach ($requests as $request) {
$plan_phid = $request->getBuildPlanPHID();
$plan = idx($plans, $plan_phid);
if (!$plan) {
throw new Exception(
pht(
'Failed to load build plan ("%s").',
$plan_phid));
}
if ($plan->isDisabled()) {
// TODO: This should be communicated more clearly -- maybe we should
// create the build but set the status to "disabled" or "derelict".
continue;
}
$parameters = $request->getBuildParameters();
$buildable->applyPlan($plan, $parameters, $request->getInitiatorPHID());
}
}
public function applyPlan(
HarbormasterBuildPlan $plan,
array $parameters,
$initiator_phid) {
$viewer = PhabricatorUser::getOmnipotentUser();
$build = HarbormasterBuild::initializeNewBuild($viewer)
->setBuildablePHID($this->getPHID())
->setBuildPlanPHID($plan->getPHID())
->setBuildParameters($parameters)
->setBuildStatus(HarbormasterBuildStatus::STATUS_PENDING);
if ($initiator_phid) {
$build->setInitiatorPHID($initiator_phid);
}
$auto_key = $plan->getPlanAutoKey();
if ($auto_key) {
$build->setPlanAutoKey($auto_key);
}
$build->save();
$steps = id(new HarbormasterBuildStepQuery())
->setViewer($viewer)
->withBuildPlanPHIDs(array($plan->getPHID()))
->execute();
foreach ($steps as $step) {
$step->willStartBuild($viewer, $this, $build, $plan);
}
PhabricatorWorker::scheduleTask(
'HarbormasterBuildWorker',
array(
'buildID' => $build->getID(),
),
array(
'objectPHID' => $build->getPHID(),
));
return $build;
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'containerPHID' => 'phid?',
'buildableStatus' => 'text32',
'isManualBuildable' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_buildable' => array(
'columns' => array('buildablePHID'),
),
'key_container' => array(
'columns' => array('containerPHID'),
),
'key_manual' => array(
'columns' => array('isManualBuildable'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
HarbormasterBuildablePHIDType::TYPECONST);
}
public function attachBuildableObject($buildable_object) {
$this->buildableObject = $buildable_object;
return $this;
}
public function getBuildableObject() {
return $this->assertAttached($this->buildableObject);
}
public function attachContainerObject($container_object) {
$this->containerObject = $container_object;
return $this;
}
public function getContainerObject() {
return $this->assertAttached($this->containerObject);
}
public function attachBuilds(array $builds) {
assert_instances_of($builds, 'HarbormasterBuild');
$this->builds = $builds;
return $this;
}
public function getBuilds() {
return $this->assertAttached($this->builds);
}
/* -( Status )------------------------------------------------------------- */
public function getBuildableStatusObject() {
$status = $this->getBuildableStatus();
return HarbormasterBuildableStatus::newBuildableStatusObject($status);
}
public function getStatusIcon() {
return $this->getBuildableStatusObject()->getIcon();
}
public function getStatusDisplayName() {
return $this->getBuildableStatusObject()->getDisplayName();
}
public function getStatusColor() {
return $this->getBuildableStatusObject()->getColor();
}
public function isPreparing() {
return $this->getBuildableStatusObject()->isPreparing();
}
public function isBuilding() {
return $this->getBuildableStatusObject()->isBuilding();
}
/* -( Messages )----------------------------------------------------------- */
public function sendMessage(
PhabricatorUser $viewer,
$message_type,
$queue_update) {
$message = HarbormasterBuildMessage::initializeNewMessage($viewer)
->setReceiverPHID($this->getPHID())
->setType($message_type)
->save();
if ($queue_update) {
PhabricatorWorker::scheduleTask(
'HarbormasterBuildWorker',
array(
'buildablePHID' => $this->getPHID(),
),
array(
'objectPHID' => $this->getPHID(),
));
}
return $message;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new HarbormasterBuildableTransactionEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new HarbormasterBuildableTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return $this->getBuildableObject()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getBuildableObject()->hasAutomaticCapability(
$capability,
$viewer);
}
public function describeAutomaticCapability($capability) {
return pht('A buildable inherits policies from the underlying object.');
}
/* -( HarbormasterBuildableInterface )------------------------------------- */
public function getHarbormasterBuildableDisplayPHID() {
return $this->getBuildableObject()->getHarbormasterBuildableDisplayPHID();
}
public function getHarbormasterBuildablePHID() {
// NOTE: This is essentially just for convenience, as it allows you create
// a copy of a buildable by specifying `B123` without bothering to go
// look up the underlying object.
return $this->getBuildablePHID();
}
public function getHarbormasterContainerPHID() {
return $this->getContainerPHID();
}
public function getBuildVariables() {
return array();
}
public function getAvailableBuildVariables() {
return array();
}
public function newBuildableEngine() {
return $this->getBuildableObject()->newBuildableEngine();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('objectPHID')
->setType('phid')
->setDescription(pht('PHID of the object that is built.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('containerPHID')
->setType('phid')
->setDescription(pht('PHID of the object containing this buildable.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('buildableStatus')
->setType('map<string, wild>')
->setDescription(pht('The current status of this buildable.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('isManual')
->setType('bool')
->setDescription(pht('True if this is a manual buildable.')),
);
}
public function getFieldValuesForConduit() {
return array(
'objectPHID' => $this->getBuildablePHID(),
'containerPHID' => $this->getContainerPHID(),
'buildableStatus' => array(
'value' => $this->getBuildableStatus(),
),
'isManual' => (bool)$this->getIsManualBuildable(),
);
}
public function getConduitSearchAttachments() {
return array();
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$viewer = $engine->getViewer();
$this->openTransaction();
$builds = id(new HarbormasterBuildQuery())
->setViewer($viewer)
->withBuildablePHIDs(array($this->getPHID()))
->execute();
foreach ($builds as $build) {
$engine->destroyObject($build);
}
$messages = id(new HarbormasterBuildMessageQuery())
->setViewer($viewer)
->withReceiverPHIDs(array($this->getPHID()))
->execute();
foreach ($messages as $message) {
$engine->destroyObject($message);
}
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php
index 6acbac646..602e38847 100644
--- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php
+++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php
@@ -1,517 +1,506 @@
<?php
final class HarbormasterBuild extends HarbormasterDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorConduitResultInterface,
PhabricatorDestructibleInterface {
protected $buildablePHID;
protected $buildPlanPHID;
protected $buildStatus;
protected $buildGeneration;
protected $buildParameters = array();
protected $initiatorPHID;
protected $planAutoKey;
private $buildable = self::ATTACHABLE;
private $buildPlan = self::ATTACHABLE;
private $buildTargets = self::ATTACHABLE;
private $unprocessedCommands = self::ATTACHABLE;
public static function initializeNewBuild(PhabricatorUser $actor) {
return id(new HarbormasterBuild())
->setBuildStatus(HarbormasterBuildStatus::STATUS_INACTIVE)
->setBuildGeneration(0);
}
public function delete() {
$this->openTransaction();
$this->deleteUnprocessedCommands();
$result = parent::delete();
$this->saveTransaction();
return $result;
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'buildParameters' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'buildStatus' => 'text32',
'buildGeneration' => 'uint32',
'planAutoKey' => 'text32?',
'initiatorPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_buildable' => array(
'columns' => array('buildablePHID'),
),
'key_plan' => array(
'columns' => array('buildPlanPHID'),
),
'key_status' => array(
'columns' => array('buildStatus'),
),
'key_planautokey' => array(
'columns' => array('buildablePHID', 'planAutoKey'),
'unique' => true,
),
'key_initiator' => array(
'columns' => array('initiatorPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
HarbormasterBuildPHIDType::TYPECONST);
}
public function attachBuildable(HarbormasterBuildable $buildable) {
$this->buildable = $buildable;
return $this;
}
public function getBuildable() {
return $this->assertAttached($this->buildable);
}
public function getName() {
if ($this->getBuildPlan()) {
return $this->getBuildPlan()->getName();
}
return pht('Build');
}
public function attachBuildPlan(
HarbormasterBuildPlan $build_plan = null) {
$this->buildPlan = $build_plan;
return $this;
}
public function getBuildPlan() {
return $this->assertAttached($this->buildPlan);
}
public function getBuildTargets() {
return $this->assertAttached($this->buildTargets);
}
public function attachBuildTargets(array $targets) {
$this->buildTargets = $targets;
return $this;
}
public function isBuilding() {
return $this->getBuildStatusObject()->isBuilding();
}
public function isAutobuild() {
return ($this->getPlanAutoKey() !== null);
}
public function retrieveVariablesFromBuild() {
$results = array(
'buildable.diff' => null,
'buildable.revision' => null,
'buildable.commit' => null,
'repository.callsign' => null,
'repository.phid' => null,
'repository.vcs' => null,
'repository.uri' => null,
'step.timestamp' => null,
'build.id' => null,
'initiator.phid' => null,
);
foreach ($this->getBuildParameters() as $key => $value) {
$results['build/'.$key] = $value;
}
$buildable = $this->getBuildable();
$object = $buildable->getBuildableObject();
$object_variables = $object->getBuildVariables();
$results = $object_variables + $results;
$results['step.timestamp'] = time();
$results['build.id'] = $this->getID();
$results['initiator.phid'] = $this->getInitiatorPHID();
return $results;
}
public static function getAvailableBuildVariables() {
$objects = id(new PhutilClassMapQuery())
->setAncestorClass('HarbormasterBuildableInterface')
->execute();
$variables = array();
$variables[] = array(
'step.timestamp' => pht('The current UNIX timestamp.'),
'build.id' => pht('The ID of the current build.'),
'target.phid' => pht('The PHID of the current build target.'),
'initiator.phid' => pht(
'The PHID of the user or Object that initiated the build, '.
'if applicable.'),
);
foreach ($objects as $object) {
$variables[] = $object->getAvailableBuildVariables();
}
$variables = array_mergev($variables);
return $variables;
}
public function isComplete() {
return $this->getBuildStatusObject()->isComplete();
}
public function isPaused() {
return $this->getBuildStatusObject()->isPaused();
}
public function isPassed() {
return $this->getBuildStatusObject()->isPassed();
}
public function getURI() {
$id = $this->getID();
return "/harbormaster/build/{$id}/";
}
protected function getBuildStatusObject() {
$status_key = $this->getBuildStatus();
return HarbormasterBuildStatus::newBuildStatusObject($status_key);
}
/* -( Build Commands )----------------------------------------------------- */
private function getUnprocessedCommands() {
return $this->assertAttached($this->unprocessedCommands);
}
public function attachUnprocessedCommands(array $commands) {
$this->unprocessedCommands = $commands;
return $this;
}
public function canRestartBuild() {
if ($this->isAutobuild()) {
return false;
}
return !$this->isRestarting();
}
public function canPauseBuild() {
if ($this->isAutobuild()) {
return false;
}
return !$this->isComplete() &&
!$this->isPaused() &&
!$this->isPausing();
}
public function canAbortBuild() {
if ($this->isAutobuild()) {
return false;
}
return !$this->isComplete();
}
public function canResumeBuild() {
if ($this->isAutobuild()) {
return false;
}
return $this->isPaused() &&
!$this->isResuming();
}
public function isPausing() {
$is_pausing = false;
foreach ($this->getUnprocessedCommands() as $command_object) {
$command = $command_object->getCommand();
switch ($command) {
case HarbormasterBuildCommand::COMMAND_PAUSE:
$is_pausing = true;
break;
case HarbormasterBuildCommand::COMMAND_RESUME:
case HarbormasterBuildCommand::COMMAND_RESTART:
$is_pausing = false;
break;
case HarbormasterBuildCommand::COMMAND_ABORT:
$is_pausing = true;
break;
}
}
return $is_pausing;
}
public function isResuming() {
$is_resuming = false;
foreach ($this->getUnprocessedCommands() as $command_object) {
$command = $command_object->getCommand();
switch ($command) {
case HarbormasterBuildCommand::COMMAND_RESTART:
case HarbormasterBuildCommand::COMMAND_RESUME:
$is_resuming = true;
break;
case HarbormasterBuildCommand::COMMAND_PAUSE:
$is_resuming = false;
break;
case HarbormasterBuildCommand::COMMAND_ABORT:
$is_resuming = false;
break;
}
}
return $is_resuming;
}
public function isRestarting() {
$is_restarting = false;
foreach ($this->getUnprocessedCommands() as $command_object) {
$command = $command_object->getCommand();
switch ($command) {
case HarbormasterBuildCommand::COMMAND_RESTART:
$is_restarting = true;
break;
}
}
return $is_restarting;
}
public function isAborting() {
$is_aborting = false;
foreach ($this->getUnprocessedCommands() as $command_object) {
$command = $command_object->getCommand();
switch ($command) {
case HarbormasterBuildCommand::COMMAND_ABORT:
$is_aborting = true;
break;
}
}
return $is_aborting;
}
public function deleteUnprocessedCommands() {
foreach ($this->getUnprocessedCommands() as $key => $command_object) {
$command_object->delete();
unset($this->unprocessedCommands[$key]);
}
return $this;
}
public function canIssueCommand(PhabricatorUser $viewer, $command) {
try {
$this->assertCanIssueCommand($viewer, $command);
return true;
} catch (Exception $ex) {
return false;
}
}
public function assertCanIssueCommand(PhabricatorUser $viewer, $command) {
$need_edit = false;
switch ($command) {
case HarbormasterBuildCommand::COMMAND_RESTART:
break;
case HarbormasterBuildCommand::COMMAND_PAUSE:
case HarbormasterBuildCommand::COMMAND_RESUME:
case HarbormasterBuildCommand::COMMAND_ABORT:
$need_edit = true;
break;
default:
throw new Exception(
pht(
'Invalid Harbormaster build command "%s".',
$command));
}
// Issuing these commands requires that you be able to edit the build, to
// prevent enemy engineers from sabotaging your builds. See T9614.
if ($need_edit) {
PhabricatorPolicyFilter::requireCapability(
$viewer,
$this->getBuildPlan(),
PhabricatorPolicyCapability::CAN_EDIT);
}
}
public function sendMessage(PhabricatorUser $viewer, $command) {
// TODO: This should not be an editor transaction, but there are plans to
// merge BuildCommand into BuildMessage which should moot this. As this
// exists today, it can race against BuildEngine.
// This is a bogus content source, but this whole flow should be obsolete
// soon.
$content_source = PhabricatorContentSource::newForSource(
PhabricatorConsoleContentSource::SOURCECONST);
$editor = id(new HarbormasterBuildTransactionEditor())
->setActor($viewer)
->setContentSource($content_source)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
$viewer_phid = $viewer->getPHID();
if (!$viewer_phid) {
$acting_phid = id(new PhabricatorHarbormasterApplication())->getPHID();
$editor->setActingAsPHID($acting_phid);
}
$xaction = id(new HarbormasterBuildTransaction())
->setTransactionType(HarbormasterBuildTransaction::TYPE_COMMAND)
->setNewValue($command);
$editor->applyTransactions($this, array($xaction));
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new HarbormasterBuildTransactionEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new HarbormasterBuildTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return $this->getBuildable()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getBuildable()->hasAutomaticCapability(
$capability,
$viewer);
}
public function describeAutomaticCapability($capability) {
return pht('A build inherits policies from its buildable.');
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('buildablePHID')
->setType('phid')
->setDescription(pht('PHID of the object this build is building.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('buildPlanPHID')
->setType('phid')
->setDescription(pht('PHID of the build plan being run.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('buildStatus')
->setType('map<string, wild>')
->setDescription(pht('The current status of this build.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('initiatorPHID')
->setType('phid')
->setDescription(pht('The person (or thing) that started this build.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of this build.')),
);
}
public function getFieldValuesForConduit() {
$status = $this->getBuildStatus();
return array(
'buildablePHID' => $this->getBuildablePHID(),
'buildPlanPHID' => $this->getBuildPlanPHID(),
'buildStatus' => array(
'value' => $status,
'name' => HarbormasterBuildStatus::getBuildStatusName($status),
'color.ansi' =>
HarbormasterBuildStatus::getBuildStatusANSIColor($status),
),
'initiatorPHID' => nonempty($this->getInitiatorPHID(), null),
'name' => $this->getName(),
);
}
public function getConduitSearchAttachments() {
return array(
id(new HarbormasterQueryBuildsSearchEngineAttachment())
->setAttachmentKey('querybuilds'),
);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$viewer = $engine->getViewer();
$this->openTransaction();
$targets = id(new HarbormasterBuildTargetQuery())
->setViewer($viewer)
->withBuildPHIDs(array($this->getPHID()))
->execute();
foreach ($targets as $target) {
$engine->destroyObject($target);
}
$messages = id(new HarbormasterBuildMessageQuery())
->setViewer($viewer)
->withReceiverPHIDs(array($this->getPHID()))
->execute();
foreach ($messages as $message) {
$engine->destroyObject($message);
}
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php
index fc9ab5f57..2e379aab2 100644
--- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php
+++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php
@@ -1,241 +1,229 @@
<?php
/**
* @task autoplan Autoplans
*/
final class HarbormasterBuildPlan extends HarbormasterDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorSubscribableInterface,
PhabricatorNgramsInterface,
PhabricatorConduitResultInterface,
PhabricatorProjectInterface {
protected $name;
protected $planStatus;
protected $planAutoKey;
protected $viewPolicy;
protected $editPolicy;
const STATUS_ACTIVE = 'active';
const STATUS_DISABLED = 'disabled';
private $buildSteps = self::ATTACHABLE;
public static function initializeNewBuildPlan(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorHarbormasterApplication'))
->executeOne();
$view_policy = $app->getPolicy(
HarbormasterBuildPlanDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(
HarbormasterBuildPlanDefaultEditCapability::CAPABILITY);
return id(new HarbormasterBuildPlan())
->setName('')
->setPlanStatus(self::STATUS_ACTIVE)
->attachBuildSteps(array())
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort128',
'planStatus' => 'text32',
'planAutoKey' => 'text32?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_status' => array(
'columns' => array('planStatus'),
),
'key_name' => array(
'columns' => array('name'),
),
'key_planautokey' => array(
'columns' => array('planAutoKey'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
HarbormasterBuildPlanPHIDType::TYPECONST);
}
public function attachBuildSteps(array $steps) {
assert_instances_of($steps, 'HarbormasterBuildStep');
$this->buildSteps = $steps;
return $this;
}
public function getBuildSteps() {
return $this->assertAttached($this->buildSteps);
}
public function isDisabled() {
return ($this->getPlanStatus() == self::STATUS_DISABLED);
}
/* -( Autoplans )---------------------------------------------------------- */
public function isAutoplan() {
return ($this->getPlanAutoKey() !== null);
}
public function getAutoplan() {
if (!$this->isAutoplan()) {
return null;
}
return HarbormasterBuildAutoplan::getAutoplan($this->getPlanAutoKey());
}
public function canRunManually() {
if ($this->isAutoplan()) {
return false;
}
return true;
}
public function getName() {
$autoplan = $this->getAutoplan();
if ($autoplan) {
return $autoplan->getAutoplanName();
}
return parent::getName();
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return false;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new HarbormasterBuildPlanEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new HarbormasterBuildPlanTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
-
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
if ($this->isAutoplan()) {
return PhabricatorPolicies::getMostOpenPolicy();
}
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
if ($this->isAutoplan()) {
return PhabricatorPolicies::POLICY_NOONE;
}
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
$messages = array();
switch ($capability) {
case PhabricatorPolicyCapability::CAN_EDIT:
if ($this->isAutoplan()) {
$messages[] = pht(
'This is an autoplan (a builtin plan provided by an application) '.
'so it can not be edited.');
}
break;
}
return $messages;
}
/* -( PhabricatorNgramsInterface )----------------------------------------- */
public function newNgrams() {
return array(
id(new HarbormasterBuildPlanNameNgrams())
->setValue($this->getName()),
);
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of this build plan.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('status')
->setType('map<string, wild>')
->setDescription(pht('The current status of this build plan.')),
);
}
public function getFieldValuesForConduit() {
return array(
'name' => $this->getName(),
'status' => array(
'value' => $this->getPlanStatus(),
),
);
}
public function getConduitSearchAttachments() {
return array();
}
}
diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php
index 54c069e97..dd0ebdc50 100644
--- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php
+++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php
@@ -1,184 +1,173 @@
<?php
final class HarbormasterBuildStep extends HarbormasterDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorCustomFieldInterface {
protected $name;
protected $description;
protected $buildPlanPHID;
protected $className;
protected $details = array();
protected $sequence = 0;
protected $stepAutoKey;
private $buildPlan = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
private $implementation;
public static function initializeNewStep(PhabricatorUser $actor) {
return id(new HarbormasterBuildStep())
->setName('')
->setDescription('');
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'details' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'className' => 'text255',
'sequence' => 'uint32',
'description' => 'text',
// T6203/NULLABILITY
// This should not be nullable. Current `null` values indicate steps
// which predated editable names. These should be backfilled with
// default names, then the code for handling `null` should be removed.
'name' => 'text255?',
'stepAutoKey' => 'text32?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_plan' => array(
'columns' => array('buildPlanPHID'),
),
'key_stepautokey' => array(
'columns' => array('buildPlanPHID', 'stepAutoKey'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
HarbormasterBuildStepPHIDType::TYPECONST);
}
public function attachBuildPlan(HarbormasterBuildPlan $plan) {
$this->buildPlan = $plan;
return $this;
}
public function getBuildPlan() {
return $this->assertAttached($this->buildPlan);
}
public function getDetail($key, $default = null) {
return idx($this->details, $key, $default);
}
public function setDetail($key, $value) {
$this->details[$key] = $value;
return $this;
}
public function getName() {
if (strlen($this->name)) {
return $this->name;
}
return $this->getStepImplementation()->getName();
}
public function getStepImplementation() {
if ($this->implementation === null) {
$obj = HarbormasterBuildStepImplementation::requireImplementation(
$this->className);
$obj->loadSettings($this);
$this->implementation = $obj;
}
return $this->implementation;
}
public function isAutostep() {
return ($this->getStepAutoKey() !== null);
}
public function willStartBuild(
PhabricatorUser $viewer,
HarbormasterBuildable $buildable,
HarbormasterBuild $build,
HarbormasterBuildPlan $plan) {
return $this->getStepImplementation()->willStartBuild(
$viewer,
$buildable,
$build,
$plan,
$this);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new HarbormasterBuildStepEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new HarbormasterBuildStepTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return $this->getBuildPlan()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getBuildPlan()->hasAutomaticCapability($capability, $viewer);
}
public function describeAutomaticCapability($capability) {
return pht('A build step has the same policies as its build plan.');
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return array();
}
public function getCustomFieldBaseClass() {
return 'HarbormasterBuildStepCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
}
diff --git a/src/applications/herald/controller/HeraldWebhookViewController.php b/src/applications/herald/controller/HeraldWebhookViewController.php
index 9b11f5d43..d8e5eb3c5 100644
--- a/src/applications/herald/controller/HeraldWebhookViewController.php
+++ b/src/applications/herald/controller/HeraldWebhookViewController.php
@@ -1,184 +1,197 @@
<?php
final class HeraldWebhookViewController
extends HeraldWebhookController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$hook = id(new HeraldWebhookQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->executeOne();
if (!$hook) {
return new Aphront404Response();
}
$header = $this->buildHeaderView($hook);
$warnings = null;
if ($hook->isInErrorBackoff($viewer)) {
$message = pht(
'Many requests to this webhook have failed recently (at least %s '.
'errors in the last %s seconds). New requests are temporarily paused.',
$hook->getErrorBackoffThreshold(),
$hook->getErrorBackoffWindow());
$warnings = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors(
array(
$message,
));
}
$curtain = $this->buildCurtain($hook);
$properties_view = $this->buildPropertiesView($hook);
$timeline = $this->buildTransactionTimeline(
$hook,
new HeraldWebhookTransactionQuery());
$timeline->setShouldTerminate(true);
$requests = id(new HeraldWebhookRequestQuery())
->setViewer($viewer)
->withWebhookPHIDs(array($hook->getPHID()))
->setLimit(20)
->execute();
+ $warnings = array();
+ if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
+ $message = pht(
+ 'Phabricator is currently configured in silent mode, so it will not '.
+ 'publish webhooks. To adjust this setting, see '.
+ '@{config:phabricator.silent} in Config.');
+
+ $warnings[] = id(new PHUIInfoView())
+ ->setTitle(pht('Silent Mode'))
+ ->setSeverity(PHUIInfoView::SEVERITY_WARNING)
+ ->appendChild(new PHUIRemarkupView($viewer, $message));
+ }
+
$requests_table = id(new HeraldWebhookRequestListView())
->setViewer($viewer)
->setRequests($requests)
->setHighlightID($request->getURIData('requestID'));
$requests_view = id(new PHUIObjectBoxView())
->setHeaderText(pht('Recent Requests'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($requests_table);
$hook_view = id(new PHUITwoColumnView())
->setHeader($header)
->setMainColumn(
array(
$warnings,
$properties_view,
$requests_view,
$timeline,
))
->setCurtain($curtain);
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb(pht('Webhook %d', $hook->getID()))
->setBorder(true);
return $this->newPage()
->setTitle(
array(
pht('Webhook %d', $hook->getID()),
$hook->getName(),
))
->setCrumbs($crumbs)
->setPageObjectPHIDs(
array(
$hook->getPHID(),
))
->appendChild($hook_view);
}
private function buildHeaderView(HeraldWebhook $hook) {
$viewer = $this->getViewer();
$title = $hook->getName();
$status_icon = $hook->getStatusIcon();
$status_color = $hook->getStatusColor();
$status_name = $hook->getStatusDisplayName();
$header = id(new PHUIHeaderView())
->setHeader($title)
->setViewer($viewer)
->setPolicyObject($hook)
->setStatus($status_icon, $status_color, $status_name)
->setHeaderIcon('fa-cloud-upload');
return $header;
}
private function buildCurtain(HeraldWebhook $hook) {
$viewer = $this->getViewer();
$curtain = $this->newCurtainView($hook);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$hook,
PhabricatorPolicyCapability::CAN_EDIT);
$id = $hook->getID();
$edit_uri = $this->getApplicationURI("webhook/edit/{$id}/");
$test_uri = $this->getApplicationURI("webhook/test/{$id}/");
$key_view_uri = $this->getApplicationURI("webhook/key/view/{$id}/");
$key_cycle_uri = $this->getApplicationURI("webhook/key/cycle/{$id}/");
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Webhook'))
->setIcon('fa-pencil')
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit)
->setHref($edit_uri));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('New Test Request'))
->setIcon('fa-cloud-upload')
->setDisabled(!$can_edit)
->setWorkflow(true)
->setHref($test_uri));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('View HMAC Key'))
->setIcon('fa-key')
->setDisabled(!$can_edit)
->setWorkflow(true)
->setHref($key_view_uri));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Regenerate HMAC Key'))
->setIcon('fa-refresh')
->setDisabled(!$can_edit)
->setWorkflow(true)
->setHref($key_cycle_uri));
return $curtain;
}
private function buildPropertiesView(HeraldWebhook $hook) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setViewer($viewer);
$properties->addProperty(
pht('URI'),
$hook->getWebhookURI());
$properties->addProperty(
pht('Status'),
$hook->getStatusDisplayName());
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Details'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($properties);
}
}
diff --git a/src/applications/herald/storage/HeraldRule.php b/src/applications/herald/storage/HeraldRule.php
index d2b8d36f0..1b005898c 100644
--- a/src/applications/herald/storage/HeraldRule.php
+++ b/src/applications/herald/storage/HeraldRule.php
@@ -1,404 +1,393 @@
<?php
final class HeraldRule extends HeraldDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorFlaggableInterface,
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface,
PhabricatorSubscribableInterface {
const TABLE_RULE_APPLIED = 'herald_ruleapplied';
protected $name;
protected $authorPHID;
protected $contentType;
protected $mustMatchAll;
protected $repetitionPolicy;
protected $ruleType;
protected $isDisabled = 0;
protected $triggerObjectPHID;
protected $configVersion = 38;
// PHIDs for which this rule has been applied
private $ruleApplied = self::ATTACHABLE;
private $validAuthor = self::ATTACHABLE;
private $author = self::ATTACHABLE;
private $conditions;
private $actions;
private $triggerObject = self::ATTACHABLE;
const REPEAT_EVERY = 'every';
const REPEAT_FIRST = 'first';
const REPEAT_CHANGE = 'change';
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort255',
'contentType' => 'text255',
'mustMatchAll' => 'bool',
'configVersion' => 'uint32',
'repetitionPolicy' => 'text32',
'ruleType' => 'text32',
'isDisabled' => 'uint32',
'triggerObjectPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_name' => array(
'columns' => array('name(128)'),
),
'key_author' => array(
'columns' => array('authorPHID'),
),
'key_ruletype' => array(
'columns' => array('ruleType'),
),
'key_trigger' => array(
'columns' => array('triggerObjectPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(HeraldRulePHIDType::TYPECONST);
}
public function getRuleApplied($phid) {
return $this->assertAttachedKey($this->ruleApplied, $phid);
}
public function setRuleApplied($phid, $applied) {
if ($this->ruleApplied === self::ATTACHABLE) {
$this->ruleApplied = array();
}
$this->ruleApplied[$phid] = $applied;
return $this;
}
public function loadConditions() {
if (!$this->getID()) {
return array();
}
return id(new HeraldCondition())->loadAllWhere(
'ruleID = %d',
$this->getID());
}
public function attachConditions(array $conditions) {
assert_instances_of($conditions, 'HeraldCondition');
$this->conditions = $conditions;
return $this;
}
public function getConditions() {
// TODO: validate conditions have been attached.
return $this->conditions;
}
public function loadActions() {
if (!$this->getID()) {
return array();
}
return id(new HeraldActionRecord())->loadAllWhere(
'ruleID = %d',
$this->getID());
}
public function attachActions(array $actions) {
// TODO: validate actions have been attached.
assert_instances_of($actions, 'HeraldActionRecord');
$this->actions = $actions;
return $this;
}
public function getActions() {
return $this->actions;
}
public function saveConditions(array $conditions) {
assert_instances_of($conditions, 'HeraldCondition');
return $this->saveChildren(
id(new HeraldCondition())->getTableName(),
$conditions);
}
public function saveActions(array $actions) {
assert_instances_of($actions, 'HeraldActionRecord');
return $this->saveChildren(
id(new HeraldActionRecord())->getTableName(),
$actions);
}
protected function saveChildren($table_name, array $children) {
assert_instances_of($children, 'HeraldDAO');
if (!$this->getID()) {
throw new PhutilInvalidStateException('save');
}
foreach ($children as $child) {
$child->setRuleID($this->getID());
}
$this->openTransaction();
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE ruleID = %d',
$table_name,
$this->getID());
foreach ($children as $child) {
$child->save();
}
$this->saveTransaction();
}
public function delete() {
$this->openTransaction();
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE ruleID = %d',
id(new HeraldCondition())->getTableName(),
$this->getID());
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE ruleID = %d',
id(new HeraldActionRecord())->getTableName(),
$this->getID());
$result = parent::delete();
$this->saveTransaction();
return $result;
}
public function hasValidAuthor() {
return $this->assertAttached($this->validAuthor);
}
public function attachValidAuthor($valid) {
$this->validAuthor = $valid;
return $this;
}
public function getAuthor() {
return $this->assertAttached($this->author);
}
public function attachAuthor(PhabricatorUser $user) {
$this->author = $user;
return $this;
}
public function isGlobalRule() {
return ($this->getRuleType() === HeraldRuleTypeConfig::RULE_TYPE_GLOBAL);
}
public function isPersonalRule() {
return ($this->getRuleType() === HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);
}
public function isObjectRule() {
return ($this->getRuleType() == HeraldRuleTypeConfig::RULE_TYPE_OBJECT);
}
public function attachTriggerObject($trigger_object) {
$this->triggerObject = $trigger_object;
return $this;
}
public function getTriggerObject() {
return $this->assertAttached($this->triggerObject);
}
/**
* Get a sortable key for rule execution order.
*
* Rules execute in a well-defined order: personal rules first, then object
* rules, then global rules. Within each rule type, rules execute from lowest
* ID to highest ID.
*
* This ordering allows more powerful rules (like global rules) to override
* weaker rules (like personal rules) when multiple rules exist which try to
* affect the same field. Executing from low IDs to high IDs makes
* interactions easier to understand when adding new rules, because the newest
* rules always happen last.
*
* @return string A sortable key for this rule.
*/
public function getRuleExecutionOrderSortKey() {
$rule_type = $this->getRuleType();
switch ($rule_type) {
case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
$type_order = 1;
break;
case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
$type_order = 2;
break;
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
$type_order = 3;
break;
default:
throw new Exception(pht('Unknown rule type "%s"!', $rule_type));
}
return sprintf('~%d%010d', $type_order, $this->getID());
}
public function getMonogram() {
return 'H'.$this->getID();
}
public function getURI() {
return '/'.$this->getMonogram();
}
/* -( Repetition Policies )------------------------------------------------ */
public function getRepetitionPolicyStringConstant() {
return $this->getRepetitionPolicy();
}
public function setRepetitionPolicyStringConstant($value) {
$map = self::getRepetitionPolicyMap();
if (!isset($map[$value])) {
throw new Exception(
pht(
'Rule repetition string constant "%s" is unknown.',
$value));
}
return $this->setRepetitionPolicy($value);
}
public function isRepeatEvery() {
return ($this->getRepetitionPolicyStringConstant() === self::REPEAT_EVERY);
}
public function isRepeatFirst() {
return ($this->getRepetitionPolicyStringConstant() === self::REPEAT_FIRST);
}
public function isRepeatOnChange() {
return ($this->getRepetitionPolicyStringConstant() === self::REPEAT_CHANGE);
}
public static function getRepetitionPolicySelectOptionMap() {
$map = self::getRepetitionPolicyMap();
return ipull($map, 'select');
}
private static function getRepetitionPolicyMap() {
return array(
self::REPEAT_EVERY => array(
'select' => pht('every time this rule matches:'),
),
self::REPEAT_FIRST => array(
'select' => pht('only the first time this rule matches:'),
),
self::REPEAT_CHANGE => array(
'select' => pht('if this rule did not match the last time:'),
),
);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new HeraldRuleEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new HeraldRuleTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
return PhabricatorPolicies::getMostOpenPolicy();
}
if ($this->isGlobalRule()) {
$app = 'PhabricatorHeraldApplication';
$herald = PhabricatorApplication::getByClass($app);
$global = HeraldManageGlobalRulesCapability::CAPABILITY;
return $herald->getPolicy($global);
} else if ($this->isObjectRule()) {
return $this->getTriggerObject()->getPolicy($capability);
} else {
return $this->getAuthorPHID();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
return null;
}
if ($this->isGlobalRule()) {
return pht(
'Global Herald rules can be edited by users with the "Can Manage '.
'Global Rules" Herald application permission.');
} else if ($this->isObjectRule()) {
return pht('Object rules inherit the edit policies of their objects.');
} else {
return pht('A personal rule can only be edited by its owner.');
}
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return $this->isPersonalRule() && $phid == $this->getAuthorPHID();
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/herald/storage/HeraldWebhook.php b/src/applications/herald/storage/HeraldWebhook.php
index 05ec69e19..0101dbef5 100644
--- a/src/applications/herald/storage/HeraldWebhook.php
+++ b/src/applications/herald/storage/HeraldWebhook.php
@@ -1,244 +1,234 @@
<?php
final class HeraldWebhook
extends HeraldDAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorDestructibleInterface,
PhabricatorProjectInterface {
protected $name;
protected $webhookURI;
protected $viewPolicy;
protected $editPolicy;
protected $status;
protected $hmacKey;
const HOOKSTATUS_FIREHOSE = 'firehose';
const HOOKSTATUS_ENABLED = 'enabled';
const HOOKSTATUS_DISABLED = 'disabled';
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text128',
'webhookURI' => 'text255',
'status' => 'text32',
'hmacKey' => 'text32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_status' => array(
'columns' => array('status'),
),
),
) + parent::getConfiguration();
}
public function getPHIDType() {
return HeraldWebhookPHIDType::TYPECONST;
}
public static function initializeNewWebhook(PhabricatorUser $viewer) {
return id(new self())
->setStatus(self::HOOKSTATUS_ENABLED)
->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy())
->setEditPolicy($viewer->getPHID())
->regenerateHMACKey();
}
public function getURI() {
return '/herald/webhook/view/'.$this->getID().'/';
}
public function isDisabled() {
return ($this->getStatus() === self::HOOKSTATUS_DISABLED);
}
public static function getStatusDisplayNameMap() {
$specs = self::getStatusSpecifications();
return ipull($specs, 'name', 'key');
}
private static function getStatusSpecifications() {
$specs = array(
array(
'key' => self::HOOKSTATUS_FIREHOSE,
'name' => pht('Firehose'),
'color' => 'orange',
'icon' => 'fa-star-o',
),
array(
'key' => self::HOOKSTATUS_ENABLED,
'name' => pht('Enabled'),
'color' => 'bluegrey',
'icon' => 'fa-check',
),
array(
'key' => self::HOOKSTATUS_DISABLED,
'name' => pht('Disabled'),
'color' => 'dark',
'icon' => 'fa-ban',
),
);
return ipull($specs, null, 'key');
}
private static function getSpecificationForStatus($status) {
$specs = self::getStatusSpecifications();
if (isset($specs[$status])) {
return $specs[$status];
}
return array(
'key' => $status,
'name' => pht('Unknown ("%s")', $status),
'icon' => 'fa-question',
'color' => 'indigo',
);
}
public static function getDisplayNameForStatus($status) {
$spec = self::getSpecificationForStatus($status);
return $spec['name'];
}
public static function getIconForStatus($status) {
$spec = self::getSpecificationForStatus($status);
return $spec['icon'];
}
public static function getColorForStatus($status) {
$spec = self::getSpecificationForStatus($status);
return $spec['color'];
}
public function getStatusDisplayName() {
$status = $this->getStatus();
return self::getDisplayNameForStatus($status);
}
public function getStatusIcon() {
$status = $this->getStatus();
return self::getIconForStatus($status);
}
public function getStatusColor() {
$status = $this->getStatus();
return self::getColorForStatus($status);
}
public function getErrorBackoffWindow() {
return phutil_units('5 minutes in seconds');
}
public function getErrorBackoffThreshold() {
return 10;
}
public function isInErrorBackoff(PhabricatorUser $viewer) {
$backoff_window = $this->getErrorBackoffWindow();
$backoff_threshold = $this->getErrorBackoffThreshold();
$now = PhabricatorTime::getNow();
$window_start = ($now - $backoff_window);
$requests = id(new HeraldWebhookRequestQuery())
->setViewer($viewer)
->withWebhookPHIDs(array($this->getPHID()))
->withLastRequestEpochBetween($window_start, null)
->withLastRequestResults(
array(
HeraldWebhookRequest::RESULT_FAIL,
))
->execute();
if (count($requests) >= $backoff_threshold) {
return true;
}
return false;
}
public function regenerateHMACKey() {
return $this->setHMACKey(Filesystem::readRandomCharacters(32));
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
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 $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new HeraldWebhookEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new HeraldWebhookTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
- return $timeline;
- }
-
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
while (true) {
$requests = id(new HeraldWebhookRequestQuery())
->setViewer($engine->getViewer())
->withWebhookPHIDs(array($this->getPHID()))
->setLimit(100)
->execute();
if (!$requests) {
break;
}
foreach ($requests as $request) {
$request->delete();
}
}
$this->delete();
}
}
diff --git a/src/applications/herald/storage/HeraldWebhookRequest.php b/src/applications/herald/storage/HeraldWebhookRequest.php
index 5db5b2916..3381f6a99 100644
--- a/src/applications/herald/storage/HeraldWebhookRequest.php
+++ b/src/applications/herald/storage/HeraldWebhookRequest.php
@@ -1,223 +1,276 @@
<?php
final class HeraldWebhookRequest
extends HeraldDAO
implements
PhabricatorPolicyInterface,
PhabricatorExtendedPolicyInterface {
protected $webhookPHID;
protected $objectPHID;
protected $status;
protected $properties = array();
protected $lastRequestResult;
protected $lastRequestEpoch;
private $webhook = self::ATTACHABLE;
const RETRY_NEVER = 'never';
const RETRY_FOREVER = 'forever';
const STATUS_QUEUED = 'queued';
const STATUS_FAILED = 'failed';
const STATUS_SENT = 'sent';
const RESULT_NONE = 'none';
const RESULT_OKAY = 'okay';
const RESULT_FAIL = 'fail';
+ const ERRORTYPE_HOOK = 'hook';
+ const ERRORTYPE_HTTP = 'http';
+ const ERRORTYPE_TIMEOUT = 'timeout';
+
+ const ERROR_SILENT = 'silent';
+ const ERROR_DISABLED = 'disabled';
+ const ERROR_URI = 'uri';
+ const ERROR_OBJECT = 'object';
+
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'status' => 'text32',
'lastRequestResult' => 'text32',
'lastRequestEpoch' => 'epoch',
),
self::CONFIG_KEY_SCHEMA => array(
'key_ratelimit' => array(
'columns' => array(
'webhookPHID',
'lastRequestResult',
'lastRequestEpoch',
),
),
'key_collect' => array(
'columns' => array('dateCreated'),
),
),
) + parent::getConfiguration();
}
public function getPHIDType() {
return HeraldWebhookRequestPHIDType::TYPECONST;
}
public static function initializeNewWebhookRequest(HeraldWebhook $hook) {
return id(new self())
->setWebhookPHID($hook->getPHID())
->attachWebhook($hook)
->setStatus(self::STATUS_QUEUED)
->setRetryMode(self::RETRY_NEVER)
->setLastRequestResult(self::RESULT_NONE)
->setLastRequestEpoch(0);
}
public function getWebhook() {
return $this->assertAttached($this->webhook);
}
public function attachWebhook(HeraldWebhook $hook) {
$this->webhook = $hook;
return $this;
}
protected function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
protected function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setRetryMode($mode) {
return $this->setProperty('retry', $mode);
}
public function getRetryMode() {
return $this->getProperty('retry');
}
public function setErrorType($error_type) {
return $this->setProperty('errorType', $error_type);
}
public function getErrorType() {
return $this->getProperty('errorType');
}
public function setErrorCode($error_code) {
return $this->setProperty('errorCode', $error_code);
}
public function getErrorCode() {
return $this->getProperty('errorCode');
}
+ public function getErrorTypeForDisplay() {
+ $map = array(
+ self::ERRORTYPE_HOOK => pht('Hook Error'),
+ self::ERRORTYPE_HTTP => pht('HTTP Error'),
+ self::ERRORTYPE_TIMEOUT => pht('Request Timeout'),
+ );
+
+ $type = $this->getErrorType();
+ return idx($map, $type, $type);
+ }
+
+ public function getErrorCodeForDisplay() {
+ $code = $this->getErrorCode();
+
+ if ($this->getErrorType() !== self::ERRORTYPE_HOOK) {
+ return $code;
+ }
+
+ $spec = $this->getHookErrorSpec($code);
+ return idx($spec, 'display', $code);
+ }
+
public function setTransactionPHIDs(array $phids) {
return $this->setProperty('transactionPHIDs', $phids);
}
public function getTransactionPHIDs() {
return $this->getProperty('transactionPHIDs', array());
}
public function setTriggerPHIDs(array $phids) {
return $this->setProperty('triggerPHIDs', $phids);
}
public function getTriggerPHIDs() {
return $this->getProperty('triggerPHIDs', array());
}
public function setIsSilentAction($bool) {
return $this->setProperty('silent', $bool);
}
public function getIsSilentAction() {
return $this->getProperty('silent', false);
}
public function setIsTestAction($bool) {
return $this->setProperty('test', $bool);
}
public function getIsTestAction() {
return $this->getProperty('test', false);
}
public function setIsSecureAction($bool) {
return $this->setProperty('secure', $bool);
}
public function getIsSecureAction() {
return $this->getProperty('secure', false);
}
public function queueCall() {
PhabricatorWorker::scheduleTask(
'HeraldWebhookWorker',
array(
'webhookRequestPHID' => $this->getPHID(),
),
array(
'objectPHID' => $this->getPHID(),
));
return $this;
}
public function newStatusIcon() {
switch ($this->getStatus()) {
case self::STATUS_QUEUED:
$icon = 'fa-refresh';
$color = 'blue';
$tooltip = pht('Queued');
break;
case self::STATUS_SENT:
$icon = 'fa-check';
$color = 'green';
$tooltip = pht('Sent');
break;
case self::STATUS_FAILED:
default:
$icon = 'fa-times';
$color = 'red';
$tooltip = pht('Failed');
break;
}
return id(new PHUIIconView())
->setIcon($icon, $color)
->setTooltip($tooltip);
}
+ private function getHookErrorSpec($code) {
+ $map = $this->getHookErrorMap();
+ return idx($map, $code, array());
+ }
+
+ private function getHookErrorMap() {
+ return array(
+ self::ERROR_SILENT => array(
+ 'display' => pht('In Silent Mode'),
+ ),
+ self::ERROR_DISABLED => array(
+ 'display' => pht('Hook Disabled'),
+ ),
+ self::ERROR_URI => array(
+ 'display' => pht('Invalid URI'),
+ ),
+ self::ERROR_OBJECT => array(
+ 'display' => pht('Invalid Object'),
+ ),
+ );
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::getMostOpenPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
return array(
array($this->getWebhook(), PhabricatorPolicyCapability::CAN_VIEW),
);
}
}
diff --git a/src/applications/herald/view/HeraldWebhookRequestListView.php b/src/applications/herald/view/HeraldWebhookRequestListView.php
index 4e0f6510b..082d320bb 100644
--- a/src/applications/herald/view/HeraldWebhookRequestListView.php
+++ b/src/applications/herald/view/HeraldWebhookRequestListView.php
@@ -1,88 +1,88 @@
<?php
final class HeraldWebhookRequestListView
extends AphrontView {
private $requests;
private $highlightID;
public function setRequests(array $requests) {
assert_instances_of($requests, 'HeraldWebhookRequest');
$this->requests = $requests;
return $this;
}
public function setHighlightID($highlight_id) {
$this->highlightID = $highlight_id;
return $this;
}
public function getHighlightID() {
return $this->highlightID;
}
public function render() {
$viewer = $this->getViewer();
$requests = $this->requests;
$handle_phids = array();
foreach ($requests as $request) {
$handle_phids[] = $request->getObjectPHID();
}
$handles = $viewer->loadHandles($handle_phids);
$highlight_id = $this->getHighlightID();
$rows = array();
$rowc = array();
foreach ($requests as $request) {
$icon = $request->newStatusIcon();
if ($highlight_id == $request->getID()) {
$rowc[] = 'highlighted';
} else {
$rowc[] = null;
}
$last_epoch = $request->getLastRequestEpoch();
if ($request->getLastRequestEpoch()) {
$last_request = phabricator_datetime($last_epoch, $viewer);
} else {
$last_request = null;
}
$rows[] = array(
$request->getID(),
$icon,
$handles[$request->getObjectPHID()]->renderLink(),
- $request->getErrorType(),
- $request->getErrorCode(),
+ $request->getErrorTypeForDisplay(),
+ $request->getErrorCodeForDisplay(),
$last_request,
);
}
$table = id(new AphrontTableView($rows))
->setRowClasses($rowc)
->setHeaders(
array(
pht('ID'),
- '',
+ null,
pht('Object'),
pht('Type'),
pht('Code'),
pht('Requested At'),
))
->setColumnClasses(
array(
'n',
'',
'wide',
'',
'',
'',
));
return $table;
}
}
diff --git a/src/applications/herald/worker/HeraldWebhookWorker.php b/src/applications/herald/worker/HeraldWebhookWorker.php
index 150f98fd5..bc93f092d 100644
--- a/src/applications/herald/worker/HeraldWebhookWorker.php
+++ b/src/applications/herald/worker/HeraldWebhookWorker.php
@@ -1,250 +1,263 @@
<?php
final class HeraldWebhookWorker
extends PhabricatorWorker {
protected function doWork() {
$viewer = PhabricatorUser::getOmnipotentUser();
$data = $this->getTaskData();
$request_phid = idx($data, 'webhookRequestPHID');
$request = id(new HeraldWebhookRequestQuery())
->setViewer($viewer)
->withPHIDs(array($request_phid))
->executeOne();
if (!$request) {
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Unable to load webhook request ("%s"). It may have been '.
'garbage collected.',
$request_phid));
}
$status = $request->getStatus();
if ($status !== HeraldWebhookRequest::STATUS_QUEUED) {
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Webhook request ("%s") is not in "%s" status (actual '.
'status is "%s"). Declining call to hook.',
$request_phid,
HeraldWebhookRequest::STATUS_QUEUED,
$status));
}
// If we're in silent mode, permanently fail the webhook request and then
// return to complete this task.
if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
- $this->failRequest($request, 'hook', 'silent');
+ $this->failRequest(
+ $request,
+ HeraldWebhookRequest::ERRORTYPE_HOOK,
+ HeraldWebhookRequest::ERROR_SILENT);
return;
}
$hook = $request->getWebhook();
if ($hook->isDisabled()) {
- $this->failRequest($request, 'hook', 'disabled');
+ $this->failRequest(
+ $request,
+ HeraldWebhookRequest::ERRORTYPE_HOOK,
+ HeraldWebhookRequest::ERROR_DISABLED);
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Associated hook ("%s") for webhook request ("%s") is disabled.',
$hook->getPHID(),
$request_phid));
}
$uri = $hook->getWebhookURI();
try {
PhabricatorEnv::requireValidRemoteURIForFetch(
$uri,
array(
'http',
'https',
));
} catch (Exception $ex) {
- $this->failRequest($request, 'hook', 'uri');
+ $this->failRequest(
+ $request,
+ HeraldWebhookRequest::ERRORTYPE_HOOK,
+ HeraldWebhookRequest::ERROR_URI);
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Associated hook ("%s") for webhook request ("%s") has invalid '.
'fetch URI: %s',
$hook->getPHID(),
$request_phid,
$ex->getMessage()));
}
$object_phid = $request->getObjectPHID();
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($object_phid))
->executeOne();
if (!$object) {
- $this->failRequest($request, 'hook', 'object');
+ $this->failRequest(
+ $request,
+ HeraldWebhookRequest::ERRORTYPE_HOOK,
+ HeraldWebhookRequest::ERROR_OBJECT);
+
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Unable to load object ("%s") for webhook request ("%s").',
$object_phid,
$request_phid));
}
$xaction_query = PhabricatorApplicationTransactionQuery::newQueryForObject(
$object);
$xaction_phids = $request->getTransactionPHIDs();
if ($xaction_phids) {
$xactions = $xaction_query
->setViewer($viewer)
->withObjectPHIDs(array($object_phid))
->withPHIDs($xaction_phids)
->execute();
$xactions = mpull($xactions, null, 'getPHID');
} else {
$xactions = array();
}
// To prevent thundering herd issues for high volume webhooks (where
// a large number of workers might try to work through a request backlog
// simultaneously, before the error backoff can catch up), we never
// parallelize requests to a particular webhook.
$lock_key = 'webhook('.$hook->getPHID().')';
$lock = PhabricatorGlobalLock::newLock($lock_key);
try {
$lock->lock();
} catch (Exception $ex) {
phlog($ex);
throw new PhabricatorWorkerYieldException(15);
}
$caught = null;
try {
$this->callWebhookWithLock($hook, $request, $object, $xactions);
} catch (Exception $ex) {
$caught = $ex;
}
$lock->unlock();
if ($caught) {
throw $caught;
}
}
private function callWebhookWithLock(
HeraldWebhook $hook,
HeraldWebhookRequest $request,
$object,
array $xactions) {
$viewer = PhabricatorUser::getOmnipotentUser();
if ($hook->isInErrorBackoff($viewer)) {
throw new PhabricatorWorkerYieldException($hook->getErrorBackoffWindow());
}
$xaction_data = array();
foreach ($xactions as $xaction) {
$xaction_data[] = array(
'phid' => $xaction->getPHID(),
);
}
$trigger_data = array();
foreach ($request->getTriggerPHIDs() as $trigger_phid) {
$trigger_data[] = array(
'phid' => $trigger_phid,
);
}
$payload = array(
'object' => array(
'type' => phid_get_type($object->getPHID()),
'phid' => $object->getPHID(),
),
'triggers' => $trigger_data,
'action' => array(
'test' => $request->getIsTestAction(),
'silent' => $request->getIsSilentAction(),
'secure' => $request->getIsSecureAction(),
'epoch' => (int)$request->getDateCreated(),
),
'transactions' => $xaction_data,
);
$payload = id(new PhutilJSON())->encodeFormatted($payload);
$key = $hook->getHmacKey();
$signature = PhabricatorHash::digestHMACSHA256($payload, $key);
$uri = $hook->getWebhookURI();
$future = id(new HTTPSFuture($uri))
->setMethod('POST')
->addHeader('Content-Type', 'application/json')
->addHeader('X-Phabricator-Webhook-Signature', $signature)
->setTimeout(15)
->setData($payload);
list($status) = $future->resolve();
if ($status->isTimeout()) {
- $error_type = 'timeout';
+ $error_type = HeraldWebhookRequest::ERRORTYPE_TIMEOUT;
} else {
- $error_type = 'http';
+ $error_type = HeraldWebhookRequest::ERRORTYPE_HTTP;
}
$error_code = $status->getStatusCode();
$request
->setErrorType($error_type)
->setErrorCode($error_code)
->setLastRequestEpoch(PhabricatorTime::getNow());
$retry_forever = HeraldWebhookRequest::RETRY_FOREVER;
if ($status->isTimeout() || $status->isError()) {
$should_retry = ($request->getRetryMode() === $retry_forever);
$request
->setLastRequestResult(HeraldWebhookRequest::RESULT_FAIL);
if ($should_retry) {
$request->save();
throw new Exception(
pht(
'Webhook request ("%s", to "%s") failed (%s / %s). The request '.
'will be retried.',
$request->getPHID(),
$uri,
$error_type,
$error_code));
} else {
$request
->setStatus(HeraldWebhookRequest::STATUS_FAILED)
->save();
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Webhook request ("%s", to "%s") failed (%s / %s). The request '.
'will not be retried.',
$request->getPHID(),
$uri,
$error_type,
$error_code));
}
} else {
$request
->setLastRequestResult(HeraldWebhookRequest::RESULT_OKAY)
->setStatus(HeraldWebhookRequest::STATUS_SENT)
->save();
}
}
private function failRequest(
HeraldWebhookRequest $request,
$error_type,
$error_code) {
$request
->setStatus(HeraldWebhookRequest::STATUS_FAILED)
->setErrorType($error_type)
->setErrorCode($error_code)
->setLastRequestResult(HeraldWebhookRequest::RESULT_NONE)
->setLastRequestEpoch(0)
->save();
}
}
diff --git a/src/applications/legalpad/config/PhabricatorLegalpadConfigOptions.php b/src/applications/legalpad/config/PhabricatorLegalpadConfigOptions.php
deleted file mode 100644
index a9584fc94..000000000
--- a/src/applications/legalpad/config/PhabricatorLegalpadConfigOptions.php
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-
-final class PhabricatorLegalpadConfigOptions
- extends PhabricatorApplicationConfigOptions {
-
- public function getName() {
- return pht('Legalpad');
- }
-
- public function getDescription() {
- return pht('Configure Legalpad.');
- }
-
- public function getIcon() {
- return 'fa-gavel';
- }
-
- public function getGroup() {
- return 'apps';
- }
-
- public function getOptions() {
- return array(
- $this->newOption(
- 'metamta.legalpad.subject-prefix',
- 'string',
- '[Legalpad]')
- ->setDescription(pht('Subject prefix for Legalpad email.')),
- );
- }
-
-}
diff --git a/src/applications/legalpad/controller/LegalpadDocumentSignController.php b/src/applications/legalpad/controller/LegalpadDocumentSignController.php
index 8ef35e349..ab98c0bb7 100644
--- a/src/applications/legalpad/controller/LegalpadDocumentSignController.php
+++ b/src/applications/legalpad/controller/LegalpadDocumentSignController.php
@@ -1,702 +1,706 @@
<?php
final class LegalpadDocumentSignController extends LegalpadController {
public function shouldAllowPublic() {
return true;
}
public function shouldAllowLegallyNonCompliantUsers() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$document = id(new LegalpadDocumentQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->needDocumentBodies(true)
->executeOne();
if (!$document) {
return new Aphront404Response();
}
$information = $this->readSignerInformation(
$document,
$request);
if ($information instanceof AphrontResponse) {
return $information;
}
list($signer_phid, $signature_data) = $information;
$signature = null;
$type_individual = LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL;
$is_individual = ($document->getSignatureType() == $type_individual);
switch ($document->getSignatureType()) {
case LegalpadDocument::SIGNATURE_TYPE_NONE:
// nothing to sign means this should be true
$has_signed = true;
// this is a status UI element
$signed_status = null;
break;
case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:
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))
->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());
// 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 PHUIInfoView())
->setSeverity(PHUIInfoView::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();
if ($signature->getIsExemption()) {
$exemption_phid = $signature->getExemptionPHID();
$handles = $this->loadViewerHandles(array($exemption_phid));
$exemption_handle = $handles[$exemption_phid];
$signed_text = pht(
'You do not need to sign this document. '.
'%s added a signature exemption for you on %s.',
$exemption_handle->renderLink(),
phabricator_datetime($signed_at, $viewer));
} else {
$signed_text = pht(
'You signed this document on %s.',
phabricator_datetime($signed_at, $viewer));
}
$signed_status = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->setErrors(array($signed_text));
}
$field_errors = array(
'name' => true,
'email' => true,
'agree' => true,
);
$signature->setSignatureData($signature_data);
break;
case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:
$signature = id(new LegalpadDocumentSignature())
->setDocumentPHID($document->getPHID())
->setDocumentVersion($document->getVersions());
if ($viewer->isLoggedIn()) {
$has_signed = false;
$signed_status = null;
} else {
// This just hides the form.
$has_signed = true;
$login_text = pht(
'This document requires a corporate signatory. You must log in to '.
'accept this document on behalf of a company you represent.');
$signed_status = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors(array($login_text));
}
$field_errors = array(
'name' => true,
'address' => true,
'contact.name' => true,
'email' => true,
);
$signature->setSignatureData($signature_data);
break;
}
$errors = array();
$hisec_token = null;
if ($request->isFormOrHisecPost() && !$has_signed) {
-
- // Require two-factor auth to sign legal documents.
- if ($viewer->isLoggedIn()) {
- $hisec_token = id(new PhabricatorAuthSessionEngine())
- ->requireHighSecurityToken(
- $viewer,
- $request,
- $document->getURI());
- }
-
list($form_data, $errors, $field_errors) = $this->readSignatureForm(
$document,
$request);
$signature_data = $form_data + $signature_data;
$signature->setSignatureData($signature_data);
$signature->setSignatureType($document->getSignatureType());
$signature->setSignerName((string)idx($signature_data, 'name'));
$signature->setSignerEmail((string)idx($signature_data, 'email'));
$agree = $request->getExists('agree');
if (!$agree) {
$errors[] = pht(
'You must check "I agree to the terms laid forth above."');
$field_errors['agree'] = pht('Required');
}
if ($viewer->isLoggedIn() && $is_individual) {
$verified = LegalpadDocumentSignature::VERIFIED;
} else {
$verified = LegalpadDocumentSignature::UNVERIFIED;
}
$signature->setVerified($verified);
if (!$errors) {
+ // Require MFA to sign legal documents.
+ if ($viewer->isLoggedIn()) {
+ $workflow_key = sprintf(
+ 'legalpad.sign(%s)',
+ $document->getPHID());
+
+ $hisec_token = id(new PhabricatorAuthSessionEngine())
+ ->setWorkflowKey($workflow_key)
+ ->requireHighSecurityToken(
+ $viewer,
+ $request,
+ $document->getURI());
+ }
+
$signature->save();
// If the viewer is logged in, signing for themselves, send them to
// the document page, which will show that they have signed the
// document. Unless of course they were required to sign the
// document to use Phabricator; in that case try really hard to
// re-direct them to where they wanted to go.
//
// Otherwise, send them to a completion page.
if ($viewer->isLoggedIn() && $is_individual) {
$next_uri = '/'.$document->getMonogram();
if ($document->getRequireSignature()) {
$request_uri = $request->getRequestURI();
$next_uri = (string)$request_uri;
}
} else {
$this->sendVerifySignatureEmail(
$document,
$signature);
$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);
// Use the last content update as the modified date. We don't want to
// show that a document like a TOS was "updated" by an incidental change
// to a field like the preamble or privacy settings which does not actually
// affect the content of the agreement.
$content_updated = $document_body->getDateCreated();
// NOTE: We're avoiding `setPolicyObject()` here so we don't pick up
// extra UI elements that are unnecessary and clutter the signature page.
// These details are available on the "Manage" page.
$header = id(new PHUIHeaderView())
->setHeader($title)
->setUser($viewer)
->setEpoch($content_updated)
->addActionLink(
id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-pencil')
->setText(pht('Manage'))
->setHref($manage_uri)
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$preamble_box = null;
if (strlen($document->getPreamble())) {
$preamble_text = new PHUIRemarkupView($viewer, $document->getPreamble());
// NOTE: We're avoiding `setObject()` here so we don't pick up extra UI
// elements like "Subscribers". This information is available on the
// "Manage" page, but just clutters up the "Signature" page.
$preamble = id(new PHUIPropertyListView())
->setUser($viewer)
->addSectionHeader(pht('Preamble'))
->addTextContent($preamble_text);
$preamble_box = new PHUIPropertyGroupView();
$preamble_box->addPropertyList($preamble);
}
$content = id(new PHUIDocumentView())
->addClass('legalpad')
->setHeader($header)
->appendChild(
array(
$signed_status,
$preamble_box,
$document_markup,
));
$signature_box = null;
if (!$has_signed) {
$error_view = null;
if ($errors) {
$error_view = id(new PHUIInfoView())
->setErrors($errors);
}
$signature_form = $this->buildSignatureForm(
$document,
$signature,
$field_errors);
switch ($document->getSignatureType()) {
default:
break;
case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:
case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:
$box = id(new PHUIObjectBoxView())
->addClass('document-sign-box')
->setHeaderText(pht('Agree and Sign Document'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setForm($signature_form);
if ($error_view) {
$box->setInfoView($error_view);
}
$signature_box = phutil_tag_div(
'phui-document-view-pro-box plt', $box);
break;
}
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->setBorder(true);
$crumbs->addTextCrumb($document->getMonogram());
$box = id(new PHUITwoColumnView())
->setFooter($signature_box);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->setPageObjectPHIDs(array($document->getPHID()))
->appendChild(array(
$content,
$box,
));
}
private function readSignerInformation(
LegalpadDocument $document,
AphrontRequest $request) {
$viewer = $request->getUser();
$signer_phid = null;
$signature_data = array();
switch ($document->getSignatureType()) {
case LegalpadDocument::SIGNATURE_TYPE_NONE:
break;
case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:
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();
}
}
break;
case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:
$signer_phid = $viewer->getPHID();
if ($signer_phid) {
$signature_data = array(
'contact.name' => $viewer->getRealName(),
'email' => $viewer->loadPrimaryEmailAddress(),
'actorPHID' => $viewer->getPHID(),
);
}
break;
}
return array($signer_phid, $signature_data);
}
private function buildSignatureForm(
LegalpadDocument $document,
LegalpadDocumentSignature $signature,
array $errors) {
$viewer = $this->getRequest()->getUser();
$data = $signature->getSignatureData();
$form = id(new AphrontFormView())
->setUser($viewer);
$signature_type = $document->getSignatureType();
switch ($signature_type) {
case LegalpadDocument::SIGNATURE_TYPE_NONE:
// bail out of here quick
return;
case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:
$this->buildIndividualSignatureForm(
$form,
$document,
$signature,
$errors);
break;
case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:
$this->buildCorporateSignatureForm(
$form,
$document,
$signature,
$errors);
break;
default:
throw new Exception(
pht(
'This document has an unknown signature type ("%s").',
$signature_type));
}
$form
->appendChild(
id(new AphrontFormCheckboxControl())
->setError(idx($errors, 'agree', null))
->addCheckbox(
'agree',
'agree',
pht('I agree to the terms laid forth above.'),
false));
if ($document->getRequireSignature()) {
$cancel_uri = '/logout/';
$cancel_text = pht('Log Out');
} else {
$cancel_uri = $this->getApplicationURI();
$cancel_text = pht('Cancel');
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Sign Document'))
->addCancelButton($cancel_uri, $cancel_text));
return $form;
}
private function buildIndividualSignatureForm(
AphrontFormView $form,
LegalpadDocument $document,
LegalpadDocumentSignature $signature,
array $errors) {
$data = $signature->getSignatureData();
$form
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Name'))
->setValue(idx($data, 'name', ''))
->setName('name')
->setError(idx($errors, 'name', null)));
$viewer = $this->getRequest()->getUser();
if (!$viewer->isLoggedIn()) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Email'))
->setValue(idx($data, 'email', ''))
->setName('email')
->setError(idx($errors, 'email', null)));
}
return $form;
}
private function buildCorporateSignatureForm(
AphrontFormView $form,
LegalpadDocument $document,
LegalpadDocumentSignature $signature,
array $errors) {
$data = $signature->getSignatureData();
$form
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Company Name'))
->setValue(idx($data, 'name', ''))
->setName('name')
->setError(idx($errors, 'name', null)))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel(pht('Company Address'))
->setValue(idx($data, 'address', ''))
->setName('address')
->setError(idx($errors, 'address', null)))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Contact Name'))
->setValue(idx($data, 'contact.name', ''))
->setName('contact.name')
->setError(idx($errors, 'contact.name', null)))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Contact Email'))
->setValue(idx($data, 'email', ''))
->setName('email')
->setError(idx($errors, 'email', null)));
return $form;
}
private function readSignatureForm(
LegalpadDocument $document,
AphrontRequest $request) {
$signature_type = $document->getSignatureType();
switch ($signature_type) {
case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:
$result = $this->readIndividualSignatureForm(
$document,
$request);
break;
case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:
$result = $this->readCorporateSignatureForm(
$document,
$request);
break;
default:
throw new Exception(
pht(
'This document has an unknown signature type ("%s").',
$signature_type));
}
return $result;
}
private function readIndividualSignatureForm(
LegalpadDocument $document,
AphrontRequest $request) {
$signature_data = array();
$errors = array();
$field_errors = array();
$name = $request->getStr('name');
if (!strlen($name)) {
$field_errors['name'] = pht('Required');
$errors[] = pht('Name field is required.');
} else {
$field_errors['name'] = null;
}
$signature_data['name'] = $name;
$viewer = $request->getUser();
if ($viewer->isLoggedIn()) {
$email = $viewer->loadPrimaryEmailAddress();
} else {
$email = $request->getStr('email');
$addr_obj = null;
if (!strlen($email)) {
$field_errors['email'] = pht('Required');
$errors[] = pht('Email field is required.');
} else {
$addr_obj = new PhutilEmailAddress($email);
$domain = $addr_obj->getDomainName();
if (!$domain) {
$field_errors['email'] = pht('Invalid');
$errors[] = pht('A valid email is required.');
} else {
$field_errors['email'] = null;
}
}
}
$signature_data['email'] = $email;
return array($signature_data, $errors, $field_errors);
}
private function readCorporateSignatureForm(
LegalpadDocument $document,
AphrontRequest $request) {
$viewer = $request->getUser();
if (!$viewer->isLoggedIn()) {
throw new Exception(
pht(
'You can not sign a document on behalf of a corporation unless '.
'you are logged in.'));
}
$signature_data = array();
$errors = array();
$field_errors = array();
$name = $request->getStr('name');
if (!strlen($name)) {
$field_errors['name'] = pht('Required');
$errors[] = pht('Company name is required.');
} else {
$field_errors['name'] = null;
}
$signature_data['name'] = $name;
$address = $request->getStr('address');
if (!strlen($address)) {
$field_errors['address'] = pht('Required');
$errors[] = pht('Company address is required.');
} else {
$field_errors['address'] = null;
}
$signature_data['address'] = $address;
$contact_name = $request->getStr('contact.name');
if (!strlen($contact_name)) {
$field_errors['contact.name'] = pht('Required');
$errors[] = pht('Contact name is required.');
} else {
$field_errors['contact.name'] = null;
}
$signature_data['contact.name'] = $contact_name;
$email = $request->getStr('email');
$addr_obj = null;
if (!strlen($email)) {
$field_errors['email'] = pht('Required');
$errors[] = pht('Contact email is required.');
} else {
$addr_obj = new PhutilEmailAddress($email);
$domain = $addr_obj->getDomainName();
if (!$domain) {
$field_errors['email'] = pht('Invalid');
$errors[] = pht('A valid email is required.');
} else {
$field_errors['email'] = null;
}
}
$signature_data['email'] = $email;
return array($signature_data, $errors, $field_errors);
}
private function sendVerifySignatureEmail(
LegalpadDocument $doc,
LegalpadDocumentSignature $signature) {
$signature_data = $signature->getSignatureData();
$email = new PhutilEmailAddress($signature_data['email']);
$doc_name = $doc->getTitle();
$doc_link = PhabricatorEnv::getProductionURI('/'.$doc->getMonogram());
$path = $this->getApplicationURI(sprintf(
'/verify/%s/',
$signature->getSecretKey()));
$link = PhabricatorEnv::getProductionURI($path);
$name = idx($signature_data, 'name');
$body = pht(
"%s:\n\n".
"This email address was used to sign a Legalpad document ".
"in Phabricator:\n\n".
" %s\n\n".
"Please verify you own this email address and accept the ".
"agreement by clicking this link:\n\n".
" %s\n\n".
"Your signature is not valid until you complete this ".
"verification step.\n\nYou can review the document here:\n\n".
" %s\n",
$name,
$doc_name,
$link,
$doc_link);
id(new PhabricatorMetaMTAMail())
->addRawTos(array($email->getAddress()))
->setSubject(pht('[Legalpad] Signature Verification'))
->setForceDelivery(true)
->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/editor/LegalpadDocumentEditor.php b/src/applications/legalpad/editor/LegalpadDocumentEditor.php
index e4b43186e..4ede196e3 100644
--- a/src/applications/legalpad/editor/LegalpadDocumentEditor.php
+++ b/src/applications/legalpad/editor/LegalpadDocumentEditor.php
@@ -1,183 +1,183 @@
<?php
final class LegalpadDocumentEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorLegalpadApplication';
}
public function getEditorObjectsDescription() {
return pht('Legalpad Documents');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this document.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created %s.', $author, $object);
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
$is_contribution = false;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case LegalpadDocumentTitleTransaction::TRANSACTIONTYPE:
case LegalpadDocumentTextTransaction::TRANSACTIONTYPE:
$is_contribution = true;
break;
}
}
if ($is_contribution) {
$text = $object->getDocumentBody()->getText();
$title = $object->getDocumentBody()->getTitle();
$object->setVersions($object->getVersions() + 1);
$body = new LegalpadDocumentBody();
$body->setCreatorPHID($this->getActingAsPHID());
$body->setText($text);
$body->setTitle($title);
$body->setVersion($object->getVersions());
$body->setDocumentPHID($object->getPHID());
$body->save();
$object->setDocumentBodyPHID($body->getPHID());
$type = PhabricatorContributedToObjectEdgeType::EDGECONST;
id(new PhabricatorEdgeEditor())
->addEdge($this->getActingAsPHID(), $type, $object->getPHID())
->save();
$type = PhabricatorObjectHasContributorEdgeType::EDGECONST;
$contributors = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
$type);
$object->setRecentContributorPHIDs(array_slice($contributors, 0, 3));
$object->setContributorCount(count($contributors));
$object->save();
}
return $xactions;
}
protected function validateAllTransactions(PhabricatorLiskDAO $object,
array $xactions) {
$errors = array();
$is_required = (bool)$object->getRequireSignature();
$document_type = $object->getSignatureType();
$individual = LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case LegalpadDocumentRequireSignatureTransaction::TRANSACTIONTYPE:
$is_required = (bool)$xaction->getNewValue();
break;
case LegalpadDocumentSignatureTypeTransaction::TRANSACTIONTYPE:
$document_type = $xaction->getNewValue();
break;
}
}
if ($is_required && ($document_type != $individual)) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
LegalpadDocumentRequireSignatureTransaction::TRANSACTIONTYPE,
pht('Invalid'),
pht('Only documents with signature type "individual" may '.
'require signing to use Phabricator.'),
null);
}
return $errors;
}
/* -( Sending Mail )------------------------------------------------------- */
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new LegalpadReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$title = $object->getDocumentBody()->getTitle();
return id(new PhabricatorMetaMTAMail())
->setSubject("L{$id}: {$title}");
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$object->getCreatorPHID(),
$this->requireActor()->getPHID(),
);
}
protected function shouldImplyCC(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case LegalpadDocumentTextTransaction::TRANSACTIONTYPE:
case LegalpadDocumentTitleTransaction::TRANSACTIONTYPE:
case LegalpadDocumentPreambleTransaction::TRANSACTIONTYPE:
case LegalpadDocumentRequireSignatureTransaction::TRANSACTIONTYPE:
return true;
}
return parent::shouldImplyCC($object, $xaction);
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$body->addLinkSection(
pht('DOCUMENT DETAIL'),
PhabricatorEnv::getProductionURI('/legalpad/view/'.$object->getID().'/'));
return $body;
}
protected function getMailSubjectPrefix() {
- return PhabricatorEnv::getEnvConfig('metamta.legalpad.subject-prefix');
+ return pht('[Legalpad]');
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return false;
}
protected function supportsSearch() {
return false;
}
}
diff --git a/src/applications/legalpad/mail/LegalpadMailReceiver.php b/src/applications/legalpad/mail/LegalpadMailReceiver.php
index 5fe0dfd4e..679812e4a 100644
--- a/src/applications/legalpad/mail/LegalpadMailReceiver.php
+++ b/src/applications/legalpad/mail/LegalpadMailReceiver.php
@@ -1,28 +1,28 @@
<?php
final class LegalpadMailReceiver extends PhabricatorObjectMailReceiver {
public function isEnabled() {
return PhabricatorApplication::isClassInstalled(
'PhabricatorLegalpadApplication');
}
protected function getObjectPattern() {
return 'L[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
- $id = (int)trim($pattern, 'L');
+ $id = (int)substr($pattern, 1);
return id(new LegalpadDocumentQuery())
->setViewer($viewer)
->withIDs(array($id))
->needDocumentBodies(true)
->executeOne();
}
protected function getTransactionReplyHandler() {
return new LegalpadReplyHandler();
}
}
diff --git a/src/applications/legalpad/storage/LegalpadDocument.php b/src/applications/legalpad/storage/LegalpadDocument.php
index 004802de3..efcd6b5f1 100644
--- a/src/applications/legalpad/storage/LegalpadDocument.php
+++ b/src/applications/legalpad/storage/LegalpadDocument.php
@@ -1,254 +1,243 @@
<?php
final class LegalpadDocument extends LegalpadDAO
implements
PhabricatorPolicyInterface,
PhabricatorSubscribableInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorDestructibleInterface {
protected $title;
protected $contributorCount;
protected $recentContributorPHIDs = array();
protected $creatorPHID;
protected $versions;
protected $documentBodyPHID;
protected $viewPolicy;
protected $editPolicy;
protected $mailKey;
protected $signatureType;
protected $preamble;
protected $requireSignature;
const SIGNATURE_TYPE_NONE = 'none';
const SIGNATURE_TYPE_INDIVIDUAL = 'user';
const SIGNATURE_TYPE_CORPORATION = 'corp';
private $documentBody = self::ATTACHABLE;
private $contributors = self::ATTACHABLE;
private $signatures = self::ATTACHABLE;
private $userSignatures = array();
public static function initializeNewDocument(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorLegalpadApplication'))
->executeOne();
$view_policy = $app->getPolicy(LegalpadDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(LegalpadDefaultEditCapability::CAPABILITY);
return id(new LegalpadDocument())
->setVersions(0)
->setCreatorPHID($actor->getPHID())
->setContributorCount(0)
->setRecentContributorPHIDs(array())
->attachSignatures(array())
->setSignatureType(self::SIGNATURE_TYPE_INDIVIDUAL)
->setPreamble('')
->setRequireSignature(0)
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'recentContributorPHIDs' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'title' => 'text255',
'contributorCount' => 'uint32',
'versions' => 'uint32',
'mailKey' => 'bytes20',
'signatureType' => 'text4',
'preamble' => 'text',
'requireSignature' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_creator' => array(
'columns' => array('creatorPHID', 'dateModified'),
),
'key_required' => array(
'columns' => array('requireSignature', 'dateModified'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorLegalpadDocumentPHIDType::TYPECONST);
}
public function getDocumentBody() {
return $this->assertAttached($this->documentBody);
}
public function attachDocumentBody(LegalpadDocumentBody $body) {
$this->documentBody = $body;
return $this;
}
public function getContributors() {
return $this->assertAttached($this->contributors);
}
public function attachContributors(array $contributors) {
$this->contributors = $contributors;
return $this;
}
public function getSignatures() {
return $this->assertAttached($this->signatures);
}
public function attachSignatures(array $signatures) {
$this->signatures = $signatures;
return $this;
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public function getMonogram() {
return 'L'.$this->getID();
}
public function getURI() {
return '/'.$this->getMonogram();
}
public function getUserSignature($phid) {
return $this->assertAttachedKey($this->userSignatures, $phid);
}
public function attachUserSignature(
$user_phid,
LegalpadDocumentSignature $signature = null) {
$this->userSignatures[$user_phid] = $signature;
return $this;
}
public static function getSignatureTypeMap() {
return array(
self::SIGNATURE_TYPE_INDIVIDUAL => pht('Individuals'),
self::SIGNATURE_TYPE_CORPORATION => pht('Corporations'),
self::SIGNATURE_TYPE_NONE => pht('No One'),
);
}
public function getSignatureTypeName() {
$type = $this->getSignatureType();
return idx(self::getSignatureTypeMap(), $type, $type);
}
public function getSignatureTypeIcon() {
$type = $this->getSignatureType();
$map = array(
self::SIGNATURE_TYPE_NONE => '',
self::SIGNATURE_TYPE_INDIVIDUAL => 'fa-user grey',
self::SIGNATURE_TYPE_CORPORATION => 'fa-building-o grey',
);
return idx($map, $type, 'fa-user grey');
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return ($this->creatorPHID == $phid);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$policy = $this->viewPolicy;
break;
case PhabricatorPolicyCapability::CAN_EDIT:
$policy = $this->editPolicy;
break;
default:
$policy = PhabricatorPolicies::POLICY_NOONE;
break;
}
return $policy;
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
return ($user->getPHID() == $this->getCreatorPHID());
}
public function describeAutomaticCapability($capability) {
return pht('The author of a document can always view and edit it.');
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new LegalpadDocumentEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new LegalpadTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$bodies = id(new LegalpadDocumentBody())->loadAllWhere(
'documentPHID = %s',
$this->getPHID());
foreach ($bodies as $body) {
$body->delete();
}
$signatures = id(new LegalpadDocumentSignature())->loadAllWhere(
'documentPHID = %s',
$this->getPHID());
foreach ($signatures as $signature) {
$signature->delete();
}
$this->saveTransaction();
}
}
diff --git a/src/applications/legalpad/storage/LegalpadTransaction.php b/src/applications/legalpad/storage/LegalpadTransaction.php
index c43c86c5f..dc4bbfe94 100644
--- a/src/applications/legalpad/storage/LegalpadTransaction.php
+++ b/src/applications/legalpad/storage/LegalpadTransaction.php
@@ -1,25 +1,21 @@
<?php
final class LegalpadTransaction extends PhabricatorModularTransaction {
public function getApplicationName() {
return 'legalpad';
}
public function getApplicationTransactionType() {
return PhabricatorLegalpadDocumentPHIDType::TYPECONST;
}
public function getApplicationTransactionCommentObject() {
return new LegalpadTransactionComment();
}
- public function getApplicationTransactionViewObject() {
- return new LegalpadTransactionView();
- }
-
public function getBaseTransactionClass() {
return 'LegalpadDocumentTransactionType';
}
}
diff --git a/src/applications/legalpad/view/LegalpadTransactionView.php b/src/applications/legalpad/view/LegalpadTransactionView.php
deleted file mode 100644
index a68619c00..000000000
--- a/src/applications/legalpad/view/LegalpadTransactionView.php
+++ /dev/null
@@ -1,4 +0,0 @@
-<?php
-
-final class LegalpadTransactionView
- extends PhabricatorApplicationTransactionView {}
diff --git a/src/applications/macro/config/PhabricatorMacroConfigOptions.php b/src/applications/macro/config/PhabricatorMacroConfigOptions.php
deleted file mode 100644
index 2cb01ff29..000000000
--- a/src/applications/macro/config/PhabricatorMacroConfigOptions.php
+++ /dev/null
@@ -1,29 +0,0 @@
-<?php
-
-final class PhabricatorMacroConfigOptions
- extends PhabricatorApplicationConfigOptions {
-
- public function getName() {
- return pht('Macro');
- }
-
- public function getDescription() {
- return pht('Configure Macro.');
- }
-
- public function getIcon() {
- return 'fa-file-image-o';
- }
-
- public function getGroup() {
- return 'apps';
- }
-
- public function getOptions() {
- return array(
- $this->newOption('metamta.macro.subject-prefix', 'string', '[Macro]')
- ->setDescription(pht('Subject prefix for Macro email.')),
- );
- }
-
-}
diff --git a/src/applications/macro/editor/PhabricatorMacroEditor.php b/src/applications/macro/editor/PhabricatorMacroEditor.php
index f59c29b42..91ed23a25 100644
--- a/src/applications/macro/editor/PhabricatorMacroEditor.php
+++ b/src/applications/macro/editor/PhabricatorMacroEditor.php
@@ -1,68 +1,68 @@
<?php
final class PhabricatorMacroEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorMacroApplication';
}
public function getEditorObjectsDescription() {
return pht('Macros');
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this macro.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created %s.', $author, $object);
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new PhabricatorMacroReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$name = $object->getName();
$name = 'Image Macro "'.$name.'"';
return id(new PhabricatorMetaMTAMail())
->setSubject($name);
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$this->requireActor()->getPHID(),
);
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$body->addLinkSection(
pht('MACRO DETAIL'),
PhabricatorEnv::getProductionURI('/macro/view/'.$object->getID().'/'));
return $body;
}
protected function getMailSubjectPrefix() {
- return PhabricatorEnv::getEnvConfig('metamta.macro.subject-prefix');
+ return pht('[Macro]');
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
}
diff --git a/src/applications/macro/storage/PhabricatorFileImageMacro.php b/src/applications/macro/storage/PhabricatorFileImageMacro.php
index 0cd6726f5..656a4c9c5 100644
--- a/src/applications/macro/storage/PhabricatorFileImageMacro.php
+++ b/src/applications/macro/storage/PhabricatorFileImageMacro.php
@@ -1,160 +1,149 @@
<?php
final class PhabricatorFileImageMacro extends PhabricatorFileDAO
implements
PhabricatorSubscribableInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorFlaggableInterface,
PhabricatorTokenReceiverInterface,
PhabricatorPolicyInterface {
protected $authorPHID;
protected $filePHID;
protected $name;
protected $isDisabled = 0;
protected $audioPHID;
protected $audioBehavior = self::AUDIO_BEHAVIOR_NONE;
protected $mailKey;
private $file = self::ATTACHABLE;
private $audio = self::ATTACHABLE;
const AUDIO_BEHAVIOR_NONE = 'audio:none';
const AUDIO_BEHAVIOR_ONCE = 'audio:once';
const AUDIO_BEHAVIOR_LOOP = 'audio:loop';
public function attachFile(PhabricatorFile $file) {
$this->file = $file;
return $this;
}
public function getFile() {
return $this->assertAttached($this->file);
}
public function attachAudio(PhabricatorFile $audio = null) {
$this->audio = $audio;
return $this;
}
public function getAudio() {
return $this->assertAttached($this->audio);
}
public static function initializeNewFileImageMacro(PhabricatorUser $actor) {
$macro = id(new self())
->setAuthorPHID($actor->getPHID());
return $macro;
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text128',
'authorPHID' => 'phid?',
'isDisabled' => 'bool',
'audioPHID' => 'phid?',
'audioBehavior' => 'text64',
'mailKey' => 'bytes20',
),
self::CONFIG_KEY_SCHEMA => array(
'name' => array(
'columns' => array('name'),
'unique' => true,
),
'key_disabled' => array(
'columns' => array('isDisabled'),
),
'key_dateCreated' => array(
'columns' => array('dateCreated'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorMacroMacroPHIDType::TYPECONST);
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public function getViewURI() {
return '/macro/view/'.$this->getID().'/';
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorMacroEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorMacroTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return false;
}
/* -( PhabricatorTokenRecevierInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getAuthorPHID(),
);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::getMostOpenPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
$app = PhabricatorApplication::getByClass(
'PhabricatorMacroApplication');
return $app->getPolicy(PhabricatorMacroManageCapability::CAPABILITY);
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
}
diff --git a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php
index 609db0d1b..8f2830908 100644
--- a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php
+++ b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php
@@ -1,477 +1,474 @@
<?php
final class PhabricatorManiphestConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Maniphest');
}
public function getDescription() {
return pht('Configure Maniphest.');
}
public function getIcon() {
return 'fa-anchor';
}
public function getGroup() {
return 'apps';
}
public function getOptions() {
$priority_type = 'maniphest.priorities';
$priority_defaults = array(
100 => array(
'name' => pht('Unbreak Now!'),
'keywords' => array('unbreak'),
'short' => pht('Unbreak!'),
'color' => 'pink',
),
90 => array(
'name' => pht('Needs Triage'),
'keywords' => array('triage'),
'short' => pht('Triage'),
'color' => 'violet',
),
80 => array(
'name' => pht('High'),
'keywords' => array('high'),
'short' => pht('High'),
'color' => 'red',
),
50 => array(
'name' => pht('Normal'),
'keywords' => array('normal'),
'short' => pht('Normal'),
'color' => 'orange',
),
25 => array(
'name' => pht('Low'),
'keywords' => array('low'),
'short' => pht('Low'),
'color' => 'yellow',
),
0 => array(
'name' => pht('Wishlist'),
'keywords' => array('wish', 'wishlist'),
'short' => pht('Wish'),
'color' => 'sky',
),
);
$status_type = 'maniphest.statuses';
$status_defaults = array(
'open' => array(
'name' => pht('Open'),
'special' => ManiphestTaskStatus::SPECIAL_DEFAULT,
'prefixes' => array(
'open',
'opens',
'reopen',
'reopens',
),
),
'resolved' => array(
'name' => pht('Resolved'),
'name.full' => pht('Closed, Resolved'),
'closed' => true,
'special' => ManiphestTaskStatus::SPECIAL_CLOSED,
'transaction.icon' => 'fa-check-circle',
'prefixes' => array(
'closed',
'closes',
'close',
'fix',
'fixes',
'fixed',
'resolve',
'resolves',
'resolved',
),
'suffixes' => array(
'as resolved',
'as fixed',
),
'keywords' => array('closed', 'fixed', 'resolved'),
),
'wontfix' => array(
'name' => pht('Wontfix'),
'name.full' => pht('Closed, Wontfix'),
'transaction.icon' => 'fa-ban',
'closed' => true,
'prefixes' => array(
'wontfix',
'wontfixes',
'wontfixed',
),
'suffixes' => array(
'as wontfix',
),
),
'invalid' => array(
'name' => pht('Invalid'),
'name.full' => pht('Closed, Invalid'),
'transaction.icon' => 'fa-minus-circle',
'closed' => true,
'claim' => false,
'prefixes' => array(
'invalidate',
'invalidates',
'invalidated',
),
'suffixes' => array(
'as invalid',
),
),
'duplicate' => array(
'name' => pht('Duplicate'),
'name.full' => pht('Closed, Duplicate'),
'transaction.icon' => 'fa-files-o',
'special' => ManiphestTaskStatus::SPECIAL_DUPLICATE,
'closed' => true,
'claim' => false,
),
'spite' => array(
'name' => pht('Spite'),
'name.full' => pht('Closed, Spite'),
'name.action' => pht('Spited'),
'transaction.icon' => 'fa-thumbs-o-down',
'silly' => true,
'closed' => true,
'prefixes' => array(
'spite',
'spites',
'spited',
),
'suffixes' => array(
'out of spite',
'as spite',
),
),
);
$status_description = $this->deformat(pht(<<<EOTEXT
Allows you to edit, add, or remove the task statuses available in Maniphest,
like "Open", "Resolved" and "Invalid". The configuration should contain a map
of status constants to status specifications (see defaults below for examples).
The constant for each status should be 1-12 characters long and contain only
lowercase letters and digits. Valid examples are "open", "closed", and
"invalid". Users will not normally see these values.
The keys you can provide in a specification are:
- `name` //Required string.// Name of the status, like "Invalid".
- `name.full` //Optional string.// Longer name, like "Closed, Invalid". This
appears on the task detail view in the header.
- `name.action` //Optional string.// Action name for email subjects, like
"Marked Invalid".
- `closed` //Optional bool.// Statuses are either "open" or "closed".
Specifying `true` here will mark the status as closed (like "Resolved" or
"Invalid"). By default, statuses are open.
- `special` //Optional string.// Mark this status as special. The special
statuses are:
- `default` This is the default status for newly created tasks. You must
designate one status as default, and it must be an open status.
- `closed` This is the default status for closed tasks (for example, tasks
closed via the "!close" action in email or via the quick close button in
Maniphest). You must designate one status as the default closed status,
and it must be a closed status.
- `duplicate` This is the status used when tasks are merged into one
another as duplicates. You must designate one status for duplicates,
and it must be a closed status.
- `transaction.icon` //Optional string.// Allows you to choose a different
icon to use for this status when showing status changes in the transaction
log. Please see UIExamples, Icons and Images for a list.
- `transaction.color` //Optional string.// Allows you to choose a different
color to use for this status when showing status changes in the transaction
log.
- `silly` //Optional bool.// Marks this status as silly, and thus wholly
inappropriate for use by serious businesses.
- `prefixes` //Optional list<string>.// Allows you to specify a list of
text prefixes which will trigger a task transition into this status
when mentioned in a commit message. For example, providing "closes" here
will allow users to move tasks to this status by writing `Closes T123` in
commit messages.
- `suffixes` //Optional list<string>.// Allows you to specify a list of
text suffixes which will trigger a task transition into this status
when mentioned in a commit message, after a valid prefix. For example,
providing "as invalid" here will allow users to move tasks
to this status by writing `Closes T123 as invalid`, even if another status
is selected by the "Closes" prefix.
- `keywords` //Optional list<string>.// Allows you to specify a list
of keywords which can be used with `!status` commands in email to select
this status.
- `disabled` //Optional bool.// Marks this status as no longer in use so
tasks can not be created or edited to have this status. Existing tasks with
this status will not be affected, but you can batch edit them or let them
die out on their own.
- `claim` //Optional bool.// By default, closing an unassigned task claims
it. You can set this to `false` to disable this behavior for a particular
status.
- `locked` //Optional bool.// Lock tasks in this status, preventing users
from commenting.
+ - `mfa` //Optional bool.// Require all edits to this task to be signed with
+ multi-factor authentication.
Statuses will appear in the UI in the order specified. Note the status marked
`special` as `duplicate` is not settable directly and will not appear in UI
elements, and that any status marked `silly` does not appear if Phabricator
is configured with `phabricator.serious-business` set to true.
Examining the default configuration and examples below will probably be helpful
in understanding these options.
EOTEXT
));
$status_example = array(
'open' => array(
'name' => pht('Open'),
'special' => 'default',
),
'closed' => array(
'name' => pht('Closed'),
'special' => 'closed',
'closed' => true,
),
'duplicate' => array(
'name' => pht('Duplicate'),
'special' => 'duplicate',
'closed' => true,
),
);
$json = new PhutilJSON();
$status_example = $json->encodeFormatted($status_example);
// This is intentionally blank for now, until we can move more Maniphest
// logic to custom fields.
$default_fields = array();
foreach ($default_fields as $key => $enabled) {
$default_fields[$key] = array(
'disabled' => !$enabled,
);
}
$custom_field_type = 'custom:PhabricatorCustomFieldConfigOptionType';
$fields_example = array(
'mycompany.estimated-hours' => array(
'name' => pht('Estimated Hours'),
'type' => 'int',
'caption' => pht('Estimated number of hours this will take.'),
),
);
$fields_json = id(new PhutilJSON())->encodeFormatted($fields_example);
$points_type = 'maniphest.points';
$points_example_1 = array(
'enabled' => true,
'label' => pht('Story Points'),
'action' => pht('Change Story Points'),
);
$points_json_1 = id(new PhutilJSON())->encodeFormatted($points_example_1);
$points_example_2 = array(
'enabled' => true,
'label' => pht('Estimated Hours'),
'action' => pht('Change Estimate'),
);
$points_json_2 = id(new PhutilJSON())->encodeFormatted($points_example_2);
$points_description = $this->deformat(pht(<<<EOTEXT
Activates a points field on tasks. You can use points for estimation or
planning. If configured, points will appear on workboards.
To activate points, set this value to a map with these keys:
- `enabled` //Optional bool.// Use `true` to enable points, or
`false` to disable them.
- `label` //Optional string.// Label for points, like "Story Points" or
"Estimated Hours". If omitted, points will be called "Points".
- `action` //Optional string.// Label for the action which changes points
in Maniphest, like "Change Estimate". If omitted, the action will
be called "Change Points".
See the example below for a starting point.
EOTEXT
));
$subtype_type = 'maniphest.subtypes';
$subtype_default_key = PhabricatorEditEngineSubtype::SUBTYPE_DEFAULT;
$subtype_example = array(
array(
'key' => $subtype_default_key,
'name' => pht('Task'),
),
array(
'key' => 'bug',
'name' => pht('Bug'),
),
array(
'key' => 'feature',
'name' => pht('Feature Request'),
),
);
$subtype_example = id(new PhutilJSON())->encodeAsList($subtype_example);
$subtype_default = array(
array(
'key' => $subtype_default_key,
'name' => pht('Task'),
),
);
$subtype_description = $this->deformat(pht(<<<EOTEXT
Allows you to define task subtypes. Subtypes let you hide fields you don't
need to simplify the workflows for editing tasks.
To define subtypes, provide a list of subtypes. Each subtype should be a
dictionary with these keys:
- `key` //Required string.// Internal identifier for the subtype, like
"task", "feature", or "bug".
- `name` //Required string.// Human-readable name for this subtype, like
"Task", "Feature Request" or "Bug Report".
- `tag` //Optional string.// Tag text for this subtype.
- `color` //Optional string.// Display color for this subtype.
- `icon` //Optional string.// Icon for the subtype.
- `children` //Optional map.// Configure options shown to the user when
they "Create Subtask". See below.
Each subtype must have a unique key, and you must define a subtype with
the key "%s", which is used as a default subtype.
The tag text (`tag`) is used to set the text shown in the subtype tag on list
views and workboards. If you do not configure it, the default subtype will have
no subtype tag and other subtypes will use their name as tag text.
The `children` key allows you to configure which options are presented to the
user when they "Create Subtask" from a task of this subtype. You can specify
these keys:
- `subtypes`: //Optional list<string>.// Show users creation forms for these
task subtypes.
- `forms`: //Optional list<string|int>.// Show users these specific forms,
in order.
If you don't specify either constraint, users will be shown creation forms
for the same subtype.
For example, if you have a "quest" subtype and do not configure `children`,
users who click "Create Subtask" will be presented with all create forms for
"quest" tasks.
If you want to present them with forms for a different task subtype or set of
subtypes instead, use `subtypes`:
```
{
...
"children": {
"subtypes": ["objective", "boss", "reward"]
}
...
}
```
If you want to present them with specific forms, use `forms` and specify form
IDs:
```
{
...
"children": {
"forms": [12, 16]
}
...
}
```
When specifying forms by ID explicitly, the order you specify the forms in will
be used when presenting options to the user.
If only one option would be presented, the user will be taken directly to the
appropriate form instead of being prompted to choose a form.
EOTEXT
,
$subtype_default_key));
$priorities_description = $this->deformat(pht(<<<EOTEXT
Allows you to edit or override the default priorities available in Maniphest,
like "High", "Normal" and "Low". The configuration should contain a map of
numeric priority values (where larger numbers correspond to higher priorities)
to priority specifications (see defaults below for examples).
The keys you can define for a priority are:
- `name` //Required string.// Name of the priority.
- `keywords` //Required list<string>.// List of unique keywords which identify
this priority, like "high" or "low". Each priority must have at least one
keyword and two priorities may not share the same keyword.
- `short` //Optional string.// Alternate shorter name, used in UIs where
there is less space available.
- `color` //Optional string.// Color for this priority, like "red" or
"blue".
- `disabled` //Optional bool.// Set to true to prevent users from choosing
this priority when creating or editing tasks. Existing tasks will not be
affected, and can be batch edited to a different priority or left to
eventually die out.
You can choose the default priority for newly created tasks with
"maniphest.default-priority".
EOTEXT
));
return array(
$this->newOption('maniphest.custom-field-definitions', 'wild', array())
->setSummary(pht('Custom Maniphest fields.'))
->setDescription(
pht(
'Array of custom fields for Maniphest tasks. For details on '.
'adding custom fields to Maniphest, see "Configuring Custom '.
'Fields" in the documentation.'))
->addExample($fields_json, pht('Valid setting')),
$this->newOption('maniphest.fields', $custom_field_type, $default_fields)
->setCustomData(id(new ManiphestTask())->getCustomFieldBaseClass())
->setDescription(pht('Select and reorder task fields.')),
$this->newOption(
'maniphest.priorities',
$priority_type,
$priority_defaults)
->setSummary(pht('Configure Maniphest priority names.'))
->setDescription($priorities_description),
$this->newOption('maniphest.statuses', $status_type, $status_defaults)
->setSummary(pht('Configure Maniphest task statuses.'))
->setDescription($status_description)
->addExample($status_example, pht('Minimal Valid Config')),
$this->newOption('maniphest.default-priority', 'int', 90)
->setSummary(pht('Default task priority for create flows.'))
->setDescription(
pht(
'Choose a default priority for newly created tasks. You can '.
'review and adjust available priorities by using the '.
'%s configuration option. The default value (`90`) '.
'corresponds to the default "Needs Triage" priority.',
'maniphest.priorities')),
- $this->newOption(
- 'metamta.maniphest.subject-prefix',
- 'string',
- '[Maniphest]')
- ->setDescription(pht('Subject prefix for Maniphest mail.')),
$this->newOption('maniphest.points', $points_type, array())
->setSummary(pht('Configure point values for tasks.'))
->setDescription($points_description)
->addExample($points_json_1, pht('Points Config'))
->addExample($points_json_2, pht('Hours Config')),
$this->newOption('maniphest.subtypes', $subtype_type, $subtype_default)
->setSummary(pht('Define task subtypes.'))
->setDescription($subtype_description)
->addExample($subtype_example, pht('Simple Subtypes')),
);
}
}
diff --git a/src/applications/maniphest/constants/ManiphestTaskStatus.php b/src/applications/maniphest/constants/ManiphestTaskStatus.php
index 53d2e1afe..4d58816e2 100644
--- a/src/applications/maniphest/constants/ManiphestTaskStatus.php
+++ b/src/applications/maniphest/constants/ManiphestTaskStatus.php
@@ -1,363 +1,368 @@
<?php
/**
* @task validate Configuration Validation
*/
final class ManiphestTaskStatus extends ManiphestConstants {
const STATUS_OPEN = 'open';
const STATUS_CLOSED_RESOLVED = 'resolved';
const STATUS_CLOSED_WONTFIX = 'wontfix';
const STATUS_CLOSED_INVALID = 'invalid';
const STATUS_CLOSED_DUPLICATE = 'duplicate';
const STATUS_CLOSED_SPITE = 'spite';
const SPECIAL_DEFAULT = 'default';
const SPECIAL_CLOSED = 'closed';
const SPECIAL_DUPLICATE = 'duplicate';
private static function getStatusConfig() {
return PhabricatorEnv::getEnvConfig('maniphest.statuses');
}
private static function getEnabledStatusMap() {
$spec = self::getStatusConfig();
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
foreach ($spec as $const => $status) {
if ($is_serious && !empty($status['silly'])) {
unset($spec[$const]);
continue;
}
}
return $spec;
}
public static function getTaskStatusMap() {
return ipull(self::getEnabledStatusMap(), 'name');
}
/**
* Get the statuses and their command keywords.
*
* @return map Statuses to lists of command keywords.
*/
public static function getTaskStatusKeywordsMap() {
$map = self::getEnabledStatusMap();
foreach ($map as $key => $spec) {
$words = idx($spec, 'keywords', array());
if (!is_array($words)) {
$words = array($words);
}
// For statuses, we include the status name because it's usually
// at least somewhat meaningful.
$words[] = $key;
foreach ($words as $word_key => $word) {
$words[$word_key] = phutil_utf8_strtolower($word);
}
$words = array_unique($words);
$map[$key] = $words;
}
return $map;
}
public static function getTaskStatusName($status) {
return self::getStatusAttribute($status, 'name', pht('Unknown Status'));
}
public static function getTaskStatusFullName($status) {
$name = self::getStatusAttribute($status, 'name.full');
if ($name !== null) {
return $name;
}
return self::getStatusAttribute($status, 'name', pht('Unknown Status'));
}
public static function renderFullDescription($status, $priority) {
if (self::isOpenStatus($status)) {
$name = pht('%s, %s', self::getTaskStatusFullName($status), $priority);
$color = 'grey';
$icon = 'fa-square-o';
} else {
$name = self::getTaskStatusFullName($status);
$color = 'indigo';
$icon = 'fa-check-square-o';
}
$tag = id(new PHUITagView())
->setName($name)
->setIcon($icon)
->setType(PHUITagView::TYPE_SHADE)
->setColor($color);
return $tag;
}
private static function getSpecialStatus($special) {
foreach (self::getStatusConfig() as $const => $status) {
if (idx($status, 'special') == $special) {
return $const;
}
}
return null;
}
public static function getDefaultStatus() {
return self::getSpecialStatus(self::SPECIAL_DEFAULT);
}
public static function getDefaultClosedStatus() {
return self::getSpecialStatus(self::SPECIAL_CLOSED);
}
public static function getDuplicateStatus() {
return self::getSpecialStatus(self::SPECIAL_DUPLICATE);
}
public static function getOpenStatusConstants() {
$result = array();
foreach (self::getEnabledStatusMap() as $const => $status) {
if (empty($status['closed'])) {
$result[] = $const;
}
}
return $result;
}
public static function getClosedStatusConstants() {
$all = array_keys(self::getTaskStatusMap());
$open = self::getOpenStatusConstants();
return array_diff($all, $open);
}
public static function isOpenStatus($status) {
foreach (self::getOpenStatusConstants() as $constant) {
if ($status == $constant) {
return true;
}
}
return false;
}
public static function isClaimStatus($status) {
return self::getStatusAttribute($status, 'claim', true);
}
public static function isClosedStatus($status) {
return !self::isOpenStatus($status);
}
public static function isLockedStatus($status) {
return self::getStatusAttribute($status, 'locked', false);
}
+ public static function isMFAStatus($status) {
+ return self::getStatusAttribute($status, 'mfa', false);
+ }
+
public static function getStatusActionName($status) {
return self::getStatusAttribute($status, 'name.action');
}
public static function getStatusColor($status) {
return self::getStatusAttribute($status, 'transaction.color');
}
public static function isDisabledStatus($status) {
return self::getStatusAttribute($status, 'disabled');
}
public static function getStatusIcon($status) {
$icon = self::getStatusAttribute($status, 'transaction.icon');
if ($icon) {
return $icon;
}
if (self::isOpenStatus($status)) {
return 'fa-exclamation-circle';
} else {
return 'fa-check-square-o';
}
}
public static function getStatusPrefixMap() {
$map = array();
foreach (self::getEnabledStatusMap() as $const => $status) {
foreach (idx($status, 'prefixes', array()) as $prefix) {
$map[$prefix] = $const;
}
}
$map += array(
'ref' => null,
'refs' => null,
'references' => null,
'cf.' => null,
);
return $map;
}
public static function getStatusSuffixMap() {
$map = array();
foreach (self::getEnabledStatusMap() as $const => $status) {
foreach (idx($status, 'suffixes', array()) as $prefix) {
$map[$prefix] = $const;
}
}
return $map;
}
private static function getStatusAttribute($status, $key, $default = null) {
$config = self::getStatusConfig();
$spec = idx($config, $status);
if ($spec) {
return idx($spec, $key, $default);
}
return $default;
}
/* -( Configuration Validation )------------------------------------------- */
/**
* @task validate
*/
public static function isValidStatusConstant($constant) {
if (!strlen($constant) || strlen($constant) > 64) {
return false;
}
// Alphanumeric, but not exclusively numeric
if (!preg_match('/^(?![0-9]*$)[a-zA-Z0-9]+$/', $constant)) {
return false;
}
return true;
}
/**
* @task validate
*/
public static function validateConfiguration(array $config) {
foreach ($config as $key => $value) {
if (!self::isValidStatusConstant($key)) {
throw new Exception(
pht(
'Key "%s" is not a valid status constant. Status constants '.
'must be 1-64 alphanumeric characters and cannot be exclusively '.
'digits. For example, "%s" or "%s" are reasonable choices.',
$key,
'open',
'closed'));
}
if (!is_array($value)) {
throw new Exception(
pht(
'Value for key "%s" should be a dictionary.',
$key));
}
PhutilTypeSpec::checkMap(
$value,
array(
'name' => 'string',
'name.full' => 'optional string',
'name.action' => 'optional string',
'closed' => 'optional bool',
'special' => 'optional string',
'transaction.icon' => 'optional string',
'transaction.color' => 'optional string',
'silly' => 'optional bool',
'prefixes' => 'optional list<string>',
'suffixes' => 'optional list<string>',
'keywords' => 'optional list<string>',
'disabled' => 'optional bool',
'claim' => 'optional bool',
'locked' => 'optional bool',
+ 'mfa' => 'optional bool',
));
}
$special_map = array();
foreach ($config as $key => $value) {
$special = idx($value, 'special');
if (!$special) {
continue;
}
if (isset($special_map[$special])) {
throw new Exception(
pht(
'Configuration has two statuses both marked with the special '.
'attribute "%s" ("%s" and "%s"). There should be only one.',
$special,
$special_map[$special],
$key));
}
switch ($special) {
case self::SPECIAL_DEFAULT:
if (!empty($value['closed'])) {
throw new Exception(
pht(
'Status "%s" is marked as default, but it is a closed '.
'status. The default status should be an open status.',
$key));
}
break;
case self::SPECIAL_CLOSED:
if (empty($value['closed'])) {
throw new Exception(
pht(
'Status "%s" is marked as the default status for closing '.
'tasks, but is not a closed status. It should be a closed '.
'status.',
$key));
}
break;
case self::SPECIAL_DUPLICATE:
if (empty($value['closed'])) {
throw new Exception(
pht(
'Status "%s" is marked as the status for closing tasks as '.
'duplicates, but it is not a closed status. It should '.
'be a closed status.',
$key));
}
break;
}
$special_map[$special] = $key;
}
// NOTE: We're not explicitly validating that we have at least one open
// and one closed status, because the DEFAULT and CLOSED specials imply
// that to be true. If those change in the future, that might become a
// reasonable thing to validate.
$required = array(
self::SPECIAL_DEFAULT,
self::SPECIAL_CLOSED,
self::SPECIAL_DUPLICATE,
);
foreach ($required as $required_special) {
if (!isset($special_map[$required_special])) {
throw new Exception(
pht(
'Configuration defines no task status with special attribute '.
'"%s", but you must specify a status which fills this special '.
'role.',
$required_special));
}
}
}
}
diff --git a/src/applications/maniphest/controller/ManiphestReportController.php b/src/applications/maniphest/controller/ManiphestReportController.php
index 4f546d04c..77bd6c0d5 100644
--- a/src/applications/maniphest/controller/ManiphestReportController.php
+++ b/src/applications/maniphest/controller/ManiphestReportController.php
@@ -1,872 +1,873 @@
<?php
final class ManiphestReportController extends ManiphestController {
private $view;
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$this->view = $request->getURIData('view');
if ($request->isFormPost()) {
$uri = $request->getRequestURI();
$project = head($request->getArr('set_project'));
$project = nonempty($project, null);
$uri = $uri->alter('project', $project);
$window = $request->getStr('set_window');
$uri = $uri->alter('window', $window);
return id(new AphrontRedirectResponse())->setURI($uri);
}
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI('/maniphest/report/'));
$nav->addLabel(pht('Open Tasks'));
$nav->addFilter('user', pht('By User'));
$nav->addFilter('project', pht('By Project'));
$nav->addLabel(pht('Burnup'));
$nav->addFilter('burn', pht('Burnup Rate'));
$this->view = $nav->selectFilter($this->view, 'user');
require_celerity_resource('maniphest-report-css');
switch ($this->view) {
case 'burn':
$core = $this->renderBurn();
break;
case 'user':
case 'project':
$core = $this->renderOpenTasks();
break;
default:
return new Aphront404Response();
}
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb(pht('Reports'));
$nav->appendChild($core);
$title = pht('Maniphest Reports');
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->setNavigation($nav);
}
public function renderBurn() {
$request = $this->getRequest();
$viewer = $request->getUser();
$handle = null;
$project_phid = $request->getStr('project');
if ($project_phid) {
$phids = array($project_phid);
$handles = $this->loadViewerHandles($phids);
$handle = $handles[$project_phid];
}
$table = new ManiphestTransaction();
$conn = $table->establishConnection('r');
- $joins = '';
- $create_joins = '';
if ($project_phid) {
$joins = qsprintf(
$conn,
'JOIN %T t ON x.objectPHID = t.phid
JOIN %T p ON p.src = t.phid AND p.type = %d AND p.dst = %s',
id(new ManiphestTask())->getTableName(),
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
$project_phid);
$create_joins = qsprintf(
$conn,
'JOIN %T p ON p.src = t.phid AND p.type = %d AND p.dst = %s',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
$project_phid);
+ } else {
+ $joins = qsprintf($conn, '');
+ $create_joins = qsprintf($conn, '');
}
$data = queryfx_all(
$conn,
'SELECT x.transactionType, x.oldValue, x.newValue, x.dateCreated
FROM %T x %Q
WHERE transactionType IN (%Ls)
ORDER BY x.dateCreated ASC',
$table->getTableName(),
$joins,
array(
ManiphestTaskStatusTransaction::TRANSACTIONTYPE,
ManiphestTaskMergedIntoTransaction::TRANSACTIONTYPE,
));
// See PHI273. After the move to EditEngine, we no longer create a
// "status" transaction if a task is created directly into the default
// status. This likely impacted API/email tasks after 2016 and all other
// tasks after late 2017. Until Facts can fix this properly, use the
// task creation dates to generate synthetic transactions which look like
// the older transactions that this page expects.
$default_status = ManiphestTaskStatus::getDefaultStatus();
$duplicate_status = ManiphestTaskStatus::getDuplicateStatus();
// Build synthetic transactions which take status from `null` to the
// default value.
$create_rows = queryfx_all(
$conn,
'SELECT t.dateCreated FROM %T t %Q',
id(new ManiphestTask())->getTableName(),
$create_joins);
foreach ($create_rows as $key => $create_row) {
$create_rows[$key] = array(
'transactionType' => 'status',
'oldValue' => null,
'newValue' => $default_status,
'dateCreated' => $create_row['dateCreated'],
);
}
// Remove any actual legacy status transactions which take status from
// `null` to any open status.
foreach ($data as $key => $row) {
if ($row['transactionType'] != 'status') {
continue;
}
$oldv = trim($row['oldValue'], '"');
$newv = trim($row['newValue'], '"');
// If this is a status change, preserve it.
if ($oldv != 'null') {
continue;
}
// If this task was created directly into a closed status, preserve
// the transaction.
if (!ManiphestTaskStatus::isOpenStatus($newv)) {
continue;
}
// If this is a legacy "create" transaction, discard it in favor of the
// synthetic one.
unset($data[$key]);
}
// Merge the synthetic rows into the real transactions.
$data = array_merge($create_rows, $data);
$data = array_values($data);
$data = isort($data, 'dateCreated');
$stats = array();
$day_buckets = array();
$open_tasks = array();
foreach ($data as $key => $row) {
switch ($row['transactionType']) {
case ManiphestTaskStatusTransaction::TRANSACTIONTYPE:
// NOTE: Hack to avoid json_decode().
$oldv = trim($row['oldValue'], '"');
$newv = trim($row['newValue'], '"');
break;
case ManiphestTaskMergedIntoTransaction::TRANSACTIONTYPE:
// NOTE: Merging a task does not generate a "status" transaction.
// We pretend it did. Note that this is not always accurate: it is
// possible to merge a task which was previously closed, but this
// fake transaction always counts a merge as a closure.
$oldv = $default_status;
$newv = $duplicate_status;
break;
}
if ($oldv == 'null') {
$old_is_open = false;
} else {
$old_is_open = ManiphestTaskStatus::isOpenStatus($oldv);
}
$new_is_open = ManiphestTaskStatus::isOpenStatus($newv);
$is_open = ($new_is_open && !$old_is_open);
$is_close = ($old_is_open && !$new_is_open);
$data[$key]['_is_open'] = $is_open;
$data[$key]['_is_close'] = $is_close;
if (!$is_open && !$is_close) {
// This is either some kind of bogus event, or a resolution change
// (e.g., resolved -> invalid). Just skip it.
continue;
}
$day_bucket = phabricator_format_local_time(
$row['dateCreated'],
$viewer,
'Yz');
$day_buckets[$day_bucket] = $row['dateCreated'];
if (empty($stats[$day_bucket])) {
$stats[$day_bucket] = array(
'open' => 0,
'close' => 0,
);
}
$stats[$day_bucket][$is_close ? 'close' : 'open']++;
}
$template = array(
'open' => 0,
'close' => 0,
);
$rows = array();
$rowc = array();
$last_month = null;
$last_month_epoch = null;
$last_week = null;
$last_week_epoch = null;
$week = null;
$month = null;
$last = last_key($stats) - 1;
$period = $template;
foreach ($stats as $bucket => $info) {
$epoch = $day_buckets[$bucket];
$week_bucket = phabricator_format_local_time(
$epoch,
$viewer,
'YW');
if ($week_bucket != $last_week) {
if ($week) {
$rows[] = $this->formatBurnRow(
pht('Week of %s', phabricator_date($last_week_epoch, $viewer)),
$week);
$rowc[] = 'week';
}
$week = $template;
$last_week = $week_bucket;
$last_week_epoch = $epoch;
}
$month_bucket = phabricator_format_local_time(
$epoch,
$viewer,
'Ym');
if ($month_bucket != $last_month) {
if ($month) {
$rows[] = $this->formatBurnRow(
phabricator_format_local_time($last_month_epoch, $viewer, 'F, Y'),
$month);
$rowc[] = 'month';
}
$month = $template;
$last_month = $month_bucket;
$last_month_epoch = $epoch;
}
$rows[] = $this->formatBurnRow(phabricator_date($epoch, $viewer), $info);
$rowc[] = null;
$week['open'] += $info['open'];
$week['close'] += $info['close'];
$month['open'] += $info['open'];
$month['close'] += $info['close'];
$period['open'] += $info['open'];
$period['close'] += $info['close'];
}
if ($week) {
$rows[] = $this->formatBurnRow(
pht('Week To Date'),
$week);
$rowc[] = 'week';
}
if ($month) {
$rows[] = $this->formatBurnRow(
pht('Month To Date'),
$month);
$rowc[] = 'month';
}
$rows[] = $this->formatBurnRow(
pht('All Time'),
$period);
$rowc[] = 'aggregate';
$rows = array_reverse($rows);
$rowc = array_reverse($rowc);
$table = new AphrontTableView($rows);
$table->setRowClasses($rowc);
$table->setHeaders(
array(
pht('Period'),
pht('Opened'),
pht('Closed'),
pht('Change'),
));
$table->setColumnClasses(
array(
'right wide',
'n',
'n',
'n',
));
if ($handle) {
$inst = pht(
'NOTE: This table reflects tasks currently in '.
'the project. If a task was opened in the past but added to '.
'the project recently, it is counted on the day it was '.
'opened, not the day it was categorized. If a task was part '.
'of this project in the past but no longer is, it is not '.
'counted at all.');
$header = pht('Task Burn Rate for Project %s', $handle->renderLink());
$caption = phutil_tag('p', array(), $inst);
} else {
$header = pht('Task Burn Rate for All Tasks');
$caption = null;
}
if ($caption) {
$caption = id(new PHUIInfoView())
->appendChild($caption)
->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
}
$panel = new PHUIObjectBoxView();
$panel->setHeaderText($header);
if ($caption) {
$panel->setInfoView($caption);
}
$panel->setTable($table);
$tokens = array();
if ($handle) {
$tokens = array($handle);
}
$filter = $this->renderReportFilters($tokens, $has_window = false);
$id = celerity_generate_unique_node_id();
$chart = phutil_tag(
'div',
array(
'id' => $id,
'style' => 'border: 1px solid #BFCFDA; '.
'background-color: #fff; '.
'margin: 8px 16px; '.
'height: 400px; ',
),
'');
list($burn_x, $burn_y) = $this->buildSeries($data);
require_celerity_resource('d3');
require_celerity_resource('phui-chart-css');
Javelin::initBehavior('line-chart', array(
'hardpoint' => $id,
'x' => array(
$burn_x,
),
'y' => array(
$burn_y,
),
'xformat' => 'epoch',
'yformat' => 'int',
));
$box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Burnup Rate'))
->appendChild($chart);
return array($filter, $box, $panel);
}
private function renderReportFilters(array $tokens, $has_window) {
$request = $this->getRequest();
$viewer = $request->getUser();
$form = id(new AphrontFormView())
->setUser($viewer)
->appendControl(
id(new AphrontFormTokenizerControl())
->setDatasource(new PhabricatorProjectDatasource())
->setLabel(pht('Project'))
->setLimit(1)
->setName('set_project')
// TODO: This is silly, but this is Maniphest reports.
->setValue(mpull($tokens, 'getPHID')));
if ($has_window) {
list($window_str, $ignored, $window_error) = $this->getWindow();
$form
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Recently Means'))
->setName('set_window')
->setCaption(
pht('Configure the cutoff for the "Recently Closed" column.'))
->setValue($window_str)
->setError($window_error));
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Filter By Project')));
$filter = new AphrontListFilterView();
$filter->appendChild($form);
return $filter;
}
private function buildSeries(array $data) {
$out = array();
$counter = 0;
foreach ($data as $row) {
$t = (int)$row['dateCreated'];
if ($row['_is_close']) {
--$counter;
$out[$t] = $counter;
} else if ($row['_is_open']) {
++$counter;
$out[$t] = $counter;
}
}
return array(array_keys($out), array_values($out));
}
private function formatBurnRow($label, $info) {
$delta = $info['open'] - $info['close'];
$fmt = number_format($delta);
if ($delta > 0) {
$fmt = '+'.$fmt;
$fmt = phutil_tag('span', array('class' => 'red'), $fmt);
} else {
$fmt = phutil_tag('span', array('class' => 'green'), $fmt);
}
return array(
$label,
number_format($info['open']),
number_format($info['close']),
$fmt,
);
}
public function renderOpenTasks() {
$request = $this->getRequest();
$viewer = $request->getUser();
$query = id(new ManiphestTaskQuery())
->setViewer($viewer)
->withStatuses(ManiphestTaskStatus::getOpenStatusConstants());
switch ($this->view) {
case 'project':
$query->needProjectPHIDs(true);
break;
}
$project_phid = $request->getStr('project');
$project_handle = null;
if ($project_phid) {
$phids = array($project_phid);
$handles = $this->loadViewerHandles($phids);
$project_handle = $handles[$project_phid];
$query->withEdgeLogicPHIDs(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
PhabricatorQueryConstraint::OPERATOR_OR,
$phids);
}
$tasks = $query->execute();
$recently_closed = $this->loadRecentlyClosedTasks();
$date = phabricator_date(time(), $viewer);
switch ($this->view) {
case 'user':
$result = mgroup($tasks, 'getOwnerPHID');
$leftover = idx($result, '', array());
unset($result['']);
$result_closed = mgroup($recently_closed, 'getOwnerPHID');
$leftover_closed = idx($result_closed, '', array());
unset($result_closed['']);
$base_link = '/maniphest/?assigned=';
$leftover_name = phutil_tag('em', array(), pht('(Up For Grabs)'));
$col_header = pht('User');
$header = pht('Open Tasks by User and Priority (%s)', $date);
break;
case 'project':
$result = array();
$leftover = array();
foreach ($tasks as $task) {
$phids = $task->getProjectPHIDs();
if ($phids) {
foreach ($phids as $project_phid) {
$result[$project_phid][] = $task;
}
} else {
$leftover[] = $task;
}
}
$result_closed = array();
$leftover_closed = array();
foreach ($recently_closed as $task) {
$phids = $task->getProjectPHIDs();
if ($phids) {
foreach ($phids as $project_phid) {
$result_closed[$project_phid][] = $task;
}
} else {
$leftover_closed[] = $task;
}
}
$base_link = '/maniphest/?projects=';
$leftover_name = phutil_tag('em', array(), pht('(No Project)'));
$col_header = pht('Project');
$header = pht('Open Tasks by Project and Priority (%s)', $date);
break;
}
$phids = array_keys($result);
$handles = $this->loadViewerHandles($phids);
$handles = msort($handles, 'getName');
$order = $request->getStr('order', 'name');
list($order, $reverse) = AphrontTableView::parseSort($order);
require_celerity_resource('aphront-tooltip-css');
Javelin::initBehavior('phabricator-tooltips', array());
$rows = array();
$pri_total = array();
foreach (array_merge($handles, array(null)) as $handle) {
if ($handle) {
if (($project_handle) &&
($project_handle->getPHID() == $handle->getPHID())) {
// If filtering by, e.g., "bugs", don't show a "bugs" group.
continue;
}
$tasks = idx($result, $handle->getPHID(), array());
$name = phutil_tag(
'a',
array(
'href' => $base_link.$handle->getPHID(),
),
$handle->getName());
$closed = idx($result_closed, $handle->getPHID(), array());
} else {
$tasks = $leftover;
$name = $leftover_name;
$closed = $leftover_closed;
}
$taskv = $tasks;
$tasks = mgroup($tasks, 'getPriority');
$row = array();
$row[] = $name;
$total = 0;
foreach (ManiphestTaskPriority::getTaskPriorityMap() as $pri => $label) {
$n = count(idx($tasks, $pri, array()));
if ($n == 0) {
$row[] = '-';
} else {
$row[] = number_format($n);
}
$total += $n;
}
$row[] = number_format($total);
list($link, $oldest_all) = $this->renderOldest($taskv);
$row[] = $link;
$normal_or_better = array();
foreach ($taskv as $id => $task) {
// TODO: This is sort of a hard-code for the default "normal" status.
// When reports are more powerful, this should be made more general.
if ($task->getPriority() < 50) {
continue;
}
$normal_or_better[$id] = $task;
}
list($link, $oldest_pri) = $this->renderOldest($normal_or_better);
$row[] = $link;
if ($closed) {
$task_ids = implode(',', mpull($closed, 'getID'));
$row[] = phutil_tag(
'a',
array(
'href' => '/maniphest/?ids='.$task_ids,
'target' => '_blank',
),
number_format(count($closed)));
} else {
$row[] = '-';
}
switch ($order) {
case 'total':
$row['sort'] = $total;
break;
case 'oldest-all':
$row['sort'] = $oldest_all;
break;
case 'oldest-pri':
$row['sort'] = $oldest_pri;
break;
case 'closed':
$row['sort'] = count($closed);
break;
case 'name':
default:
$row['sort'] = $handle ? $handle->getName() : '~';
break;
}
$rows[] = $row;
}
$rows = isort($rows, 'sort');
foreach ($rows as $k => $row) {
unset($rows[$k]['sort']);
}
if ($reverse) {
$rows = array_reverse($rows);
}
$cname = array($col_header);
$cclass = array('pri right wide');
$pri_map = ManiphestTaskPriority::getShortNameMap();
foreach ($pri_map as $pri => $label) {
$cname[] = $label;
$cclass[] = 'n';
}
$cname[] = pht('Total');
$cclass[] = 'n';
$cname[] = javelin_tag(
'span',
array(
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => pht('Oldest open task.'),
'size' => 200,
),
),
pht('Oldest (All)'));
$cclass[] = 'n';
$cname[] = javelin_tag(
'span',
array(
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => pht(
'Oldest open task, excluding those with Low or Wishlist priority.'),
'size' => 200,
),
),
pht('Oldest (Pri)'));
$cclass[] = 'n';
list($ignored, $window_epoch) = $this->getWindow();
$edate = phabricator_datetime($window_epoch, $viewer);
$cname[] = javelin_tag(
'span',
array(
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => pht('Closed after %s', $edate),
'size' => 260,
),
),
pht('Recently Closed'));
$cclass[] = 'n';
$table = new AphrontTableView($rows);
$table->setHeaders($cname);
$table->setColumnClasses($cclass);
$table->makeSortable(
$request->getRequestURI(),
'order',
$order,
$reverse,
array(
'name',
null,
null,
null,
null,
null,
null,
'total',
'oldest-all',
'oldest-pri',
'closed',
));
$panel = new PHUIObjectBoxView();
$panel->setHeaderText($header);
$panel->setTable($table);
$tokens = array();
if ($project_handle) {
$tokens = array($project_handle);
}
$filter = $this->renderReportFilters($tokens, $has_window = true);
return array($filter, $panel);
}
/**
* Load all the tasks that have been recently closed.
*/
private function loadRecentlyClosedTasks() {
list($ignored, $window_epoch) = $this->getWindow();
$table = new ManiphestTask();
$xtable = new ManiphestTransaction();
$conn_r = $table->establishConnection('r');
// TODO: Gross. This table is not meant to be queried like this. Build
// real stats tables.
$open_status_list = array();
foreach (ManiphestTaskStatus::getOpenStatusConstants() as $constant) {
$open_status_list[] = json_encode((string)$constant);
}
$rows = queryfx_all(
$conn_r,
'SELECT t.id FROM %T t JOIN %T x ON x.objectPHID = t.phid
WHERE t.status NOT IN (%Ls)
AND x.oldValue IN (null, %Ls)
AND x.newValue NOT IN (%Ls)
AND t.dateModified >= %d
AND x.dateCreated >= %d',
$table->getTableName(),
$xtable->getTableName(),
ManiphestTaskStatus::getOpenStatusConstants(),
$open_status_list,
$open_status_list,
$window_epoch,
$window_epoch);
if (!$rows) {
return array();
}
$ids = ipull($rows, 'id');
$query = id(new ManiphestTaskQuery())
->setViewer($this->getRequest()->getUser())
->withIDs($ids);
switch ($this->view) {
case 'project':
$query->needProjectPHIDs(true);
break;
}
return $query->execute();
}
/**
* Parse the "Recently Means" filter into:
*
* - A string representation, like "12 AM 7 days ago" (default);
* - a locale-aware epoch representation; and
* - a possible error.
*/
private function getWindow() {
$request = $this->getRequest();
$viewer = $request->getUser();
$window_str = $this->getRequest()->getStr('window', '12 AM 7 days ago');
$error = null;
$window_epoch = null;
// Do locale-aware parsing so that the user's timezone is assumed for
// time windows like "3 PM", rather than assuming the server timezone.
$window_epoch = PhabricatorTime::parseLocalTime($window_str, $viewer);
if (!$window_epoch) {
$error = 'Invalid';
$window_epoch = time() - (60 * 60 * 24 * 7);
}
// If the time ends up in the future, convert it to the corresponding time
// and equal distance in the past. This is so users can type "6 days" (which
// means "6 days from now") and get the behavior of "6 days ago", rather
// than no results (because the window epoch is in the future). This might
// be a little confusing because it causes "tomorrow" to mean "yesterday"
// and "2022" (or whatever) to mean "ten years ago", but these inputs are
// nonsense anyway.
if ($window_epoch > time()) {
$window_epoch = time() - ($window_epoch - time());
}
return array($window_str, $window_epoch, $error);
}
private function renderOldest(array $tasks) {
assert_instances_of($tasks, 'ManiphestTask');
$oldest = null;
foreach ($tasks as $id => $task) {
if (($oldest === null) ||
($task->getDateCreated() < $tasks[$oldest]->getDateCreated())) {
$oldest = $id;
}
}
if ($oldest === null) {
return array('-', 0);
}
$oldest = $tasks[$oldest];
$raw_age = (time() - $oldest->getDateCreated());
$age = number_format($raw_age / (24 * 60 * 60)).' d';
$link = javelin_tag(
'a',
array(
'href' => '/T'.$oldest->getID(),
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => 'T'.$oldest->getID().': '.$oldest->getTitle(),
),
'target' => '_blank',
),
$age);
return array($link, $raw_age);
}
}
diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
index 47a6b1b4f..0722e0e27 100644
--- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php
+++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
@@ -1,1015 +1,1015 @@
<?php
final class ManiphestTransactionEditor
extends PhabricatorApplicationTransactionEditor {
private $moreValidationErrors = array();
public function getEditorApplicationClass() {
return 'PhabricatorManiphestApplication';
}
public function getEditorObjectsDescription() {
return pht('Maniphest Tasks');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = PhabricatorTransactions::TYPE_COLUMNS;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this task.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created %s.', $author, $object);
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COLUMNS:
return null;
}
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COLUMNS:
return $xaction->getNewValue();
}
}
protected function transactionHasEffect(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COLUMNS:
return (bool)$new;
}
return parent::transactionHasEffect($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COLUMNS:
return;
}
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COLUMNS:
foreach ($xaction->getNewValue() as $move) {
$this->applyBoardMove($object, $move);
}
break;
}
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
// When we change the status of a task, update tasks this tasks blocks
// with a message to the effect of "alincoln resolved blocking task Txxx."
$unblock_xaction = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTaskStatusTransaction::TRANSACTIONTYPE:
$unblock_xaction = $xaction;
break;
}
}
if ($unblock_xaction !== null) {
$blocked_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
ManiphestTaskDependedOnByTaskEdgeType::EDGECONST);
if ($blocked_phids) {
// In theory we could apply these through policies, but that seems a
// little bit surprising. For now, use the actor's vision.
$blocked_tasks = id(new ManiphestTaskQuery())
->setViewer($this->getActor())
->withPHIDs($blocked_phids)
->needSubscriberPHIDs(true)
->needProjectPHIDs(true)
->execute();
$old = $unblock_xaction->getOldValue();
$new = $unblock_xaction->getNewValue();
foreach ($blocked_tasks as $blocked_task) {
$parent_xaction = id(new ManiphestTransaction())
->setTransactionType(
ManiphestTaskUnblockTransaction::TRANSACTIONTYPE)
->setOldValue(array($object->getPHID() => $old))
->setNewValue(array($object->getPHID() => $new));
if ($this->getIsNewObject()) {
$parent_xaction->setMetadataValue('blocker.new', true);
}
id(new ManiphestTransactionEditor())
->setActor($this->getActor())
->setActingAsPHID($this->getActingAsPHID())
->setContentSource($this->getContentSource())
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($blocked_task, array($parent_xaction));
}
}
}
return $xactions;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailSubjectPrefix() {
- return PhabricatorEnv::getEnvConfig('metamta.maniphest.subject-prefix');
+ return pht('[Maniphest]');
}
protected function getMailThreadID(PhabricatorLiskDAO $object) {
return 'maniphest-task-'.$object->getPHID();
}
protected function getMailTo(PhabricatorLiskDAO $object) {
$phids = array();
if ($object->getOwnerPHID()) {
$phids[] = $object->getOwnerPHID();
}
$phids[] = $this->getActingAsPHID();
return $phids;
}
public function getMailTagsMap() {
return array(
ManiphestTransaction::MAILTAG_STATUS =>
pht("A task's status changes."),
ManiphestTransaction::MAILTAG_OWNER =>
pht("A task's owner changes."),
ManiphestTransaction::MAILTAG_PRIORITY =>
pht("A task's priority changes."),
ManiphestTransaction::MAILTAG_CC =>
pht("A task's subscribers change."),
ManiphestTransaction::MAILTAG_PROJECTS =>
pht("A task's associated projects change."),
ManiphestTransaction::MAILTAG_UNBLOCK =>
pht("One of a task's subtasks changes status."),
ManiphestTransaction::MAILTAG_COLUMN =>
pht('A task is moved between columns on a workboard.'),
ManiphestTransaction::MAILTAG_COMMENT =>
pht('Someone comments on a task.'),
ManiphestTransaction::MAILTAG_OTHER =>
pht('Other task activity not listed above occurs.'),
);
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new ManiphestReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$title = $object->getTitle();
return id(new PhabricatorMetaMTAMail())
->setSubject("T{$id}: {$title}");
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
if ($this->getIsNewObject()) {
$body->addRemarkupSection(
pht('TASK DESCRIPTION'),
$object->getDescription());
}
$body->addLinkSection(
pht('TASK DETAIL'),
PhabricatorEnv::getProductionURI('/T'.$object->getID()));
$board_phids = array();
$type_columns = PhabricatorTransactions::TYPE_COLUMNS;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == $type_columns) {
$moves = $xaction->getNewValue();
foreach ($moves as $move) {
$board_phids[] = $move['boardPHID'];
}
}
}
if ($board_phids) {
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->requireActor())
->withPHIDs($board_phids)
->execute();
foreach ($projects as $project) {
$body->addLinkSection(
pht('WORKBOARD'),
PhabricatorEnv::getProductionURI(
'/project/board/'.$project->getID().'/'));
}
}
return $body;
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function supportsSearch() {
return true;
}
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
return id(new HeraldManiphestTaskAdapter())
->setTask($object);
}
protected function adjustObjectForPolicyChecks(
PhabricatorLiskDAO $object,
array $xactions) {
$copy = parent::adjustObjectForPolicyChecks($object, $xactions);
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE:
$copy->setOwnerPHID($xaction->getNewValue());
break;
default:
break;
}
}
return $copy;
}
/**
* Get priorities for moving a task to a new priority.
*/
public static function getEdgeSubpriority(
$priority,
$is_end) {
$query = id(new ManiphestTaskQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPriorities(array($priority))
->setLimit(1);
if ($is_end) {
$query->setOrderVector(array('-priority', '-subpriority', '-id'));
} else {
$query->setOrderVector(array('priority', 'subpriority', 'id'));
}
$result = $query->executeOne();
$step = (double)(2 << 32);
if ($result) {
$base = $result->getSubpriority();
if ($is_end) {
$sub = ($base - $step);
} else {
$sub = ($base + $step);
}
} else {
$sub = 0;
}
return array($priority, $sub);
}
/**
* Get priorities for moving a task before or after another task.
*/
public static function getAdjacentSubpriority(
ManiphestTask $dst,
$is_after) {
$query = id(new ManiphestTaskQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->setOrder(ManiphestTaskQuery::ORDER_PRIORITY)
->withPriorities(array($dst->getPriority()))
->setLimit(1);
if ($is_after) {
$query->setAfterID($dst->getID());
} else {
$query->setBeforeID($dst->getID());
}
$adjacent = $query->executeOne();
$base = $dst->getSubpriority();
$step = (double)(2 << 32);
// If we find an adjacent task, we average the two subpriorities and
// return the result.
if ($adjacent) {
$epsilon = 1.0;
// If the adjacent task has a subpriority that is identical or very
// close to the task we're looking at, we're going to spread out all
// the nearby tasks.
$adjacent_sub = $adjacent->getSubpriority();
if ((abs($adjacent_sub - $base) < $epsilon)) {
$base = self::disperseBlock(
$dst,
$epsilon * 2);
if ($is_after) {
$sub = $base - $epsilon;
} else {
$sub = $base + $epsilon;
}
} else {
$sub = ($adjacent_sub + $base) / 2;
}
} else {
// Otherwise, we take a step away from the target's subpriority and
// use that.
if ($is_after) {
$sub = ($base - $step);
} else {
$sub = ($base + $step);
}
}
return array($dst->getPriority(), $sub);
}
/**
* Distribute a cluster of tasks with similar subpriorities.
*/
private static function disperseBlock(
ManiphestTask $task,
$spacing) {
$conn = $task->establishConnection('w');
// Find a block of subpriority space which is, on average, sparse enough
// to hold all the tasks that are inside it with a reasonable level of
// separation between them.
// We'll start by looking near the target task for a range of numbers
// which has more space available than tasks. For example, if the target
// task has subpriority 33 and we want to separate each task by at least 1,
// we might start by looking in the range [23, 43].
// If we find fewer than 20 tasks there, we have room to reassign them
// with the desired level of separation. We space them out, then we're
// done.
// However: if we find more than 20 tasks, we don't have enough room to
// distribute them. We'll widen our search and look in a bigger range,
// maybe [13, 53]. This range has more space, so if we find fewer than
// 40 tasks in this range we can spread them out. If we still find too
// many tasks, we keep widening the search.
$base = $task->getSubpriority();
$scale = 4.0;
while (true) {
$range = ($spacing * $scale) / 2.0;
$min = ($base - $range);
$max = ($base + $range);
$result = queryfx_one(
$conn,
'SELECT COUNT(*) N FROM %T WHERE priority = %d AND
subpriority BETWEEN %f AND %f',
$task->getTableName(),
$task->getPriority(),
$min,
$max);
$count = $result['N'];
if ($count < $scale) {
// We have found a block which we can make sparse enough, so bail and
// continue below with our selection.
break;
}
// This block had too many tasks for its size, so try again with a
// bigger block.
$scale *= 2.0;
}
$rows = queryfx_all(
$conn,
'SELECT id FROM %T WHERE priority = %d AND
subpriority BETWEEN %f AND %f
ORDER BY priority, subpriority, id',
$task->getTableName(),
$task->getPriority(),
$min,
$max);
$task_id = $task->getID();
$result = null;
// NOTE: In strict mode (which we encourage enabling) we can't structure
// this bulk update as an "INSERT ... ON DUPLICATE KEY UPDATE" unless we
// provide default values for ALL of the columns that don't have defaults.
// This is gross, but we may be moving enough rows that individual
// queries are unreasonably slow. An alternate construction which might
// be worth evaluating is to use "CASE". Another approach is to disable
// strict mode for this query.
$default_str = qsprintf($conn, '%s', '');
$default_int = qsprintf($conn, '%d', 0);
$extra_columns = array(
'phid' => $default_str,
'authorPHID' => $default_str,
'status' => $default_str,
'priority' => $default_int,
'title' => $default_str,
'description' => $default_str,
'dateCreated' => $default_int,
'dateModified' => $default_int,
'mailKey' => $default_str,
'viewPolicy' => $default_str,
'editPolicy' => $default_str,
'ownerOrdering' => $default_str,
'spacePHID' => $default_str,
'bridgedObjectPHID' => $default_str,
'properties' => $default_str,
'points' => $default_int,
'subtype' => $default_str,
);
$sql = array();
$offset = 0;
// Often, we'll have more room than we need in the range. Distribute the
// tasks evenly over the whole range so that we're less likely to end up
// with tasks spaced exactly the minimum distance apart, which may
// get shifted again later. We have one fewer space to distribute than we
// have tasks.
$divisor = (double)(count($rows) - 1.0);
if ($divisor > 0) {
$available_distance = (($max - $min) / $divisor);
} else {
$available_distance = 0.0;
}
foreach ($rows as $row) {
$subpriority = $min + ($offset * $available_distance);
// If this is the task that we're spreading out relative to, keep track
// of where it is ending up so we can return the new subpriority.
$id = $row['id'];
if ($id == $task_id) {
$result = $subpriority;
}
$sql[] = qsprintf(
$conn,
'(%d, %LQ, %f)',
$id,
$extra_columns,
$subpriority);
$offset++;
}
foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
queryfx(
$conn,
'INSERT INTO %T (id, %LC, subpriority) VALUES %LQ
ON DUPLICATE KEY UPDATE subpriority = VALUES(subpriority)',
$task->getTableName(),
array_keys($extra_columns),
$chunk);
}
return $result;
}
protected function validateAllTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$errors = parent::validateAllTransactions($object, $xactions);
if ($this->moreValidationErrors) {
$errors = array_merge($errors, $this->moreValidationErrors);
}
return $errors;
}
protected function expandTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$actor = $this->getActor();
$actor_phid = $actor->getPHID();
$results = parent::expandTransactions($object, $xactions);
$is_unassigned = ($object->getOwnerPHID() === null);
$any_assign = false;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() ==
ManiphestTaskOwnerTransaction::TRANSACTIONTYPE) {
$any_assign = true;
break;
}
}
$is_open = !$object->isClosed();
$new_status = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTaskStatusTransaction::TRANSACTIONTYPE:
$new_status = $xaction->getNewValue();
break;
}
}
if ($new_status === null) {
$is_closing = false;
} else {
$is_closing = ManiphestTaskStatus::isClosedStatus($new_status);
}
// If the task is not assigned, not being assigned, currently open, and
// being closed, try to assign the actor as the owner.
if ($is_unassigned && !$any_assign && $is_open && $is_closing) {
$is_claim = ManiphestTaskStatus::isClaimStatus($new_status);
// Don't assign the actor if they aren't a real user.
// Don't claim the task if the status is configured to not claim.
if ($actor_phid && $is_claim) {
$results[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTaskOwnerTransaction::TRANSACTIONTYPE)
->setNewValue($actor_phid);
}
}
// Automatically subscribe the author when they create a task.
if ($this->getIsNewObject()) {
if ($actor_phid) {
$results[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setNewValue(
array(
'+' => array($actor_phid => $actor_phid),
));
}
}
return $results;
}
protected function expandTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$results = parent::expandTransaction($object, $xaction);
$type = $xaction->getTransactionType();
switch ($type) {
case PhabricatorTransactions::TYPE_COLUMNS:
try {
$more_xactions = $this->buildMoveTransaction($object, $xaction);
foreach ($more_xactions as $more_xaction) {
$results[] = $more_xaction;
}
} catch (Exception $ex) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
$ex->getMessage(),
$xaction);
$this->moreValidationErrors[] = $error;
}
break;
case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE:
// If this is a no-op update, don't expand it.
$old_value = $object->getOwnerPHID();
$new_value = $xaction->getNewValue();
if ($old_value === $new_value) {
break;
}
// When a task is reassigned, move the old owner to the subscriber
// list so they're still in the loop.
if ($old_value) {
$results[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setIgnoreOnNoEffect(true)
->setNewValue(
array(
'+' => array($old_value => $old_value),
));
}
break;
}
return $results;
}
private function buildMoveTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$new = $xaction->getNewValue();
if (!is_array($new)) {
$this->validateColumnPHID($new);
$new = array($new);
}
$nearby_phids = array();
foreach ($new as $key => $value) {
if (!is_array($value)) {
$this->validateColumnPHID($value);
$value = array(
'columnPHID' => $value,
);
}
PhutilTypeSpec::checkMap(
$value,
array(
'columnPHID' => 'string',
'beforePHID' => 'optional string',
'afterPHID' => 'optional string',
));
$new[$key] = $value;
if (!empty($value['beforePHID'])) {
$nearby_phids[] = $value['beforePHID'];
}
if (!empty($value['afterPHID'])) {
$nearby_phids[] = $value['afterPHID'];
}
}
if ($nearby_phids) {
$nearby_objects = id(new PhabricatorObjectQuery())
->setViewer($this->getActor())
->withPHIDs($nearby_phids)
->execute();
$nearby_objects = mpull($nearby_objects, null, 'getPHID');
} else {
$nearby_objects = array();
}
$column_phids = ipull($new, 'columnPHID');
if ($column_phids) {
$columns = id(new PhabricatorProjectColumnQuery())
->setViewer($this->getActor())
->withPHIDs($column_phids)
->execute();
$columns = mpull($columns, null, 'getPHID');
} else {
$columns = array();
}
$board_phids = mpull($columns, 'getProjectPHID');
$object_phid = $object->getPHID();
$object_phids = $nearby_phids;
// Note that we may not have an object PHID if we're creating a new
// object.
if ($object_phid) {
$object_phids[] = $object_phid;
}
if ($object_phids) {
$layout_engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($this->getActor())
->setBoardPHIDs($board_phids)
->setObjectPHIDs($object_phids)
->setFetchAllBoards(true)
->executeLayout();
}
foreach ($new as $key => $spec) {
$column_phid = $spec['columnPHID'];
$column = idx($columns, $column_phid);
if (!$column) {
throw new Exception(
pht(
'Column move transaction specifies column PHID "%s", but there '.
'is no corresponding column with this PHID.',
$column_phid));
}
$board_phid = $column->getProjectPHID();
$nearby = array();
if (!empty($spec['beforePHID'])) {
$nearby['beforePHID'] = $spec['beforePHID'];
}
if (!empty($spec['afterPHID'])) {
$nearby['afterPHID'] = $spec['afterPHID'];
}
if (count($nearby) > 1) {
throw new Exception(
pht(
'Column move transaction moves object to multiple positions. '.
'Specify only "beforePHID" or "afterPHID", not both.'));
}
foreach ($nearby as $where => $nearby_phid) {
if (empty($nearby_objects[$nearby_phid])) {
throw new Exception(
pht(
'Column move transaction specifies object "%s" as "%s", but '.
'there is no corresponding object with this PHID.',
$object_phid,
$where));
}
$nearby_columns = $layout_engine->getObjectColumns(
$board_phid,
$nearby_phid);
$nearby_columns = mpull($nearby_columns, null, 'getPHID');
if (empty($nearby_columns[$column_phid])) {
throw new Exception(
pht(
'Column move transaction specifies object "%s" as "%s" in '.
'column "%s", but this object is not in that column!',
$nearby_phid,
$where,
$column_phid));
}
}
if ($object_phid) {
$old_columns = $layout_engine->getObjectColumns(
$board_phid,
$object_phid);
$old_column_phids = mpull($old_columns, 'getPHID');
} else {
$old_column_phids = array();
}
$spec += array(
'boardPHID' => $board_phid,
'fromColumnPHIDs' => $old_column_phids,
);
// Check if the object is already in this column, and isn't being moved.
// We can just drop this column change if it has no effect.
$from_map = array_fuse($spec['fromColumnPHIDs']);
$already_here = isset($from_map[$column_phid]);
$is_reordering = (bool)$nearby;
if ($already_here && !$is_reordering) {
unset($new[$key]);
} else {
$new[$key] = $spec;
}
}
$new = array_values($new);
$xaction->setNewValue($new);
$more = array();
// If we're moving the object into a column and it does not already belong
// in the column, add the appropriate board. For normal columns, this
// is the board PHID. For proxy columns, it is the proxy PHID, unless the
// object is already a member of some descendant of the proxy PHID.
// The major case where this can happen is moves via the API, but it also
// happens when a user drags a task from the "Backlog" to a milestone
// column.
if ($object_phid) {
$current_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object_phid,
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
$current_phids = array_fuse($current_phids);
} else {
$current_phids = array();
}
$add_boards = array();
foreach ($new as $move) {
$column_phid = $move['columnPHID'];
$board_phid = $move['boardPHID'];
$column = $columns[$column_phid];
$proxy_phid = $column->getProxyPHID();
// If this is a normal column, add the board if the object isn't already
// associated.
if (!$proxy_phid) {
if (!isset($current_phids[$board_phid])) {
$add_boards[] = $board_phid;
}
continue;
}
// If this is a proxy column but the object is already associated with
// the proxy board, we don't need to do anything.
if (isset($current_phids[$proxy_phid])) {
continue;
}
// If this a proxy column and the object is already associated with some
// descendant of the proxy board, we also don't need to do anything.
$descendants = id(new PhabricatorProjectQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withAncestorProjectPHIDs(array($proxy_phid))
->execute();
$found_descendant = false;
foreach ($descendants as $descendant) {
if (isset($current_phids[$descendant->getPHID()])) {
$found_descendant = true;
break;
}
}
if ($found_descendant) {
continue;
}
// Otherwise, we're moving the object to a proxy column which it is not
// a member of yet, so add an association to the column's proxy board.
$add_boards[] = $proxy_phid;
}
if ($add_boards) {
$more[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)
->setIgnoreOnNoEffect(true)
->setNewValue(
array(
'+' => array_fuse($add_boards),
));
}
return $more;
}
private function applyBoardMove($object, array $move) {
$board_phid = $move['boardPHID'];
$column_phid = $move['columnPHID'];
$before_phid = idx($move, 'beforePHID');
$after_phid = idx($move, 'afterPHID');
$object_phid = $object->getPHID();
// We're doing layout with the omnipotent viewer to make sure we don't
// remove positions in columns that exist, but which the actual actor
// can't see.
$omnipotent_viewer = PhabricatorUser::getOmnipotentUser();
$select_phids = array($board_phid);
$descendants = id(new PhabricatorProjectQuery())
->setViewer($omnipotent_viewer)
->withAncestorProjectPHIDs($select_phids)
->execute();
foreach ($descendants as $descendant) {
$select_phids[] = $descendant->getPHID();
}
$board_tasks = id(new ManiphestTaskQuery())
->setViewer($omnipotent_viewer)
->withEdgeLogicPHIDs(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
PhabricatorQueryConstraint::OPERATOR_ANCESTOR,
array($select_phids))
->execute();
$board_tasks = mpull($board_tasks, null, 'getPHID');
$board_tasks[$object_phid] = $object;
// Make sure tasks are sorted by ID, so we lay out new positions in
// a consistent way.
$board_tasks = msort($board_tasks, 'getID');
$object_phids = array_keys($board_tasks);
$engine = id(new PhabricatorBoardLayoutEngine())
->setViewer($omnipotent_viewer)
->setBoardPHIDs(array($board_phid))
->setObjectPHIDs($object_phids)
->executeLayout();
// TODO: This logic needs to be revised when we legitimately support
// multiple column positions.
$columns = $engine->getObjectColumns($board_phid, $object_phid);
foreach ($columns as $column) {
$engine->queueRemovePosition(
$board_phid,
$column->getPHID(),
$object_phid);
}
if ($before_phid) {
$engine->queueAddPositionBefore(
$board_phid,
$column_phid,
$object_phid,
$before_phid);
} else if ($after_phid) {
$engine->queueAddPositionAfter(
$board_phid,
$column_phid,
$object_phid,
$after_phid);
} else {
$engine->queueAddPosition(
$board_phid,
$column_phid,
$object_phid);
}
$engine->applyPositionUpdates();
}
private function validateColumnPHID($value) {
if (phid_get_type($value) == PhabricatorProjectColumnPHIDType::TYPECONST) {
return;
}
throw new Exception(
pht(
'When moving objects between columns on a board, columns must '.
'be identified by PHIDs. This transaction uses "%s" to identify '.
'a column, but that is not a valid column PHID.',
$value));
}
}
diff --git a/src/applications/maniphest/engine/ManiphestTaskMFAEngine.php b/src/applications/maniphest/engine/ManiphestTaskMFAEngine.php
new file mode 100644
index 000000000..2aa0e303e
--- /dev/null
+++ b/src/applications/maniphest/engine/ManiphestTaskMFAEngine.php
@@ -0,0 +1,11 @@
+<?php
+
+final class ManiphestTaskMFAEngine
+ extends PhabricatorEditEngineMFAEngine {
+
+ public function shouldRequireMFA() {
+ $status = $this->getObject()->getStatus();
+ return ManiphestTaskStatus::isMFAStatus($status);
+ }
+
+}
diff --git a/src/applications/maniphest/mail/ManiphestCreateMailReceiver.php b/src/applications/maniphest/mail/ManiphestCreateMailReceiver.php
index 767a11d26..22c09fdf6 100644
--- a/src/applications/maniphest/mail/ManiphestCreateMailReceiver.php
+++ b/src/applications/maniphest/mail/ManiphestCreateMailReceiver.php
@@ -1,36 +1,36 @@
<?php
-final class ManiphestCreateMailReceiver extends PhabricatorMailReceiver {
+final class ManiphestCreateMailReceiver
+ extends PhabricatorApplicationMailReceiver {
- public function isEnabled() {
- return PhabricatorApplication::isClassInstalled(
- 'PhabricatorManiphestApplication');
- }
-
- public function canAcceptMail(PhabricatorMetaMTAReceivedMail $mail) {
- $maniphest_app = new PhabricatorManiphestApplication();
- return $this->canAcceptApplicationMail($maniphest_app, $mail);
+ protected function newApplication() {
+ return new PhabricatorManiphestApplication();
}
protected function processReceivedMail(
PhabricatorMetaMTAReceivedMail $mail,
- PhabricatorUser $sender) {
+ PhutilEmailAddress $target) {
- $task = ManiphestTask::initializeNewTask($sender);
- $task->setOriginalEmailSource($mail->getHeader('From'));
+ $author = $this->getAuthor();
+ $task = ManiphestTask::initializeNewTask($author);
+
+ $from_address = $mail->newFromAddress();
+ if ($from_address) {
+ $task->setOriginalEmailSource((string)$from_address);
+ }
$handler = new ManiphestReplyHandler();
$handler->setMailReceiver($task);
- $handler->setActor($sender);
+ $handler->setActor($author);
$handler->setExcludeMailRecipientPHIDs(
$mail->loadAllRecipientPHIDs());
if ($this->getApplicationEmail()) {
$handler->setApplicationEmail($this->getApplicationEmail());
}
$handler->processEmail($mail);
$mail->setRelatedPHID($task->getPHID());
}
}
diff --git a/src/applications/maniphest/mail/ManiphestTaskMailReceiver.php b/src/applications/maniphest/mail/ManiphestTaskMailReceiver.php
index e69ae8293..54ac72fd5 100644
--- a/src/applications/maniphest/mail/ManiphestTaskMailReceiver.php
+++ b/src/applications/maniphest/mail/ManiphestTaskMailReceiver.php
@@ -1,31 +1,29 @@
<?php
final class ManiphestTaskMailReceiver extends PhabricatorObjectMailReceiver {
public function isEnabled() {
return PhabricatorApplication::isClassInstalled(
'PhabricatorManiphestApplication');
}
protected function getObjectPattern() {
return 'T[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
- $id = (int)trim($pattern, 'T');
+ $id = (int)substr($pattern, 1);
- $results = id(new ManiphestTaskQuery())
+ return id(new ManiphestTaskQuery())
->setViewer($viewer)
->withIDs(array($id))
->needSubscriberPHIDs(true)
->needProjectPHIDs(true)
- ->execute();
-
- return head($results);
+ ->executeOne();
}
protected function getTransactionReplyHandler() {
return new ManiphestReplyHandler();
}
}
diff --git a/src/applications/maniphest/relationship/ManiphestTaskRelationship.php b/src/applications/maniphest/relationship/ManiphestTaskRelationship.php
index 04f78b852..a7d3e6f11 100644
--- a/src/applications/maniphest/relationship/ManiphestTaskRelationship.php
+++ b/src/applications/maniphest/relationship/ManiphestTaskRelationship.php
@@ -1,69 +1,68 @@
<?php
abstract class ManiphestTaskRelationship
extends PhabricatorObjectRelationship {
public function isEnabledForObject($object) {
$viewer = $this->getViewer();
$has_app = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorManiphestApplication',
$viewer);
if (!$has_app) {
return false;
}
return ($object instanceof ManiphestTask);
}
protected function newMergeIntoTransactions(ManiphestTask $task) {
return array(
id(new ManiphestTransaction())
->setTransactionType(
ManiphestTaskMergedIntoTransaction::TRANSACTIONTYPE)
->setNewValue($task->getPHID()),
);
}
protected function newMergeFromTransactions(array $tasks) {
$xactions = array();
$subscriber_phids = $this->loadMergeSubscriberPHIDs($tasks);
$xactions[] = id(new ManiphestTransaction())
- ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
- ->setNewValue(array('+' => $subscriber_phids));
+ ->setTransactionType(ManiphestTaskMergedFromTransaction::TRANSACTIONTYPE)
+ ->setNewValue(mpull($tasks, 'getPHID'));
$xactions[] = id(new ManiphestTransaction())
- ->setTransactionType(
- ManiphestTaskMergedFromTransaction::TRANSACTIONTYPE)
- ->setNewValue(mpull($tasks, 'getPHID'));
+ ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
+ ->setNewValue(array('+' => $subscriber_phids));
return $xactions;
}
private function loadMergeSubscriberPHIDs(array $tasks) {
$phids = array();
foreach ($tasks as $task) {
$phids[] = $task->getAuthorPHID();
$phids[] = $task->getOwnerPHID();
}
$subscribers = id(new PhabricatorSubscribersQuery())
->withObjectPHIDs(mpull($tasks, 'getPHID'))
->execute();
foreach ($subscribers as $phid => $subscriber_list) {
foreach ($subscriber_list as $subscriber) {
$phids[] = $subscriber;
}
}
$phids = array_unique($phids);
$phids = array_filter($phids);
return $phids;
}
}
diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php
index cdac563a1..ada537fa4 100644
--- a/src/applications/maniphest/storage/ManiphestTask.php
+++ b/src/applications/maniphest/storage/ManiphestTask.php
@@ -1,633 +1,631 @@
<?php
final class ManiphestTask extends ManiphestDAO
implements
PhabricatorSubscribableInterface,
PhabricatorMarkupInterface,
PhabricatorPolicyInterface,
PhabricatorTokenReceiverInterface,
PhabricatorFlaggableInterface,
PhabricatorMentionableInterface,
PhrequentTrackableInterface,
PhabricatorCustomFieldInterface,
PhabricatorDestructibleInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorProjectInterface,
PhabricatorSpacesInterface,
PhabricatorConduitResultInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface,
DoorkeeperBridgedObjectInterface,
PhabricatorEditEngineSubtypeInterface,
- PhabricatorEditEngineLockableInterface {
+ PhabricatorEditEngineLockableInterface,
+ PhabricatorEditEngineMFAInterface {
const MARKUP_FIELD_DESCRIPTION = 'markup:desc';
protected $authorPHID;
protected $ownerPHID;
protected $status;
protected $priority;
protected $subpriority = 0;
protected $title = '';
protected $description = '';
protected $originalEmailSource;
protected $mailKey;
protected $viewPolicy = PhabricatorPolicies::POLICY_USER;
protected $editPolicy = PhabricatorPolicies::POLICY_USER;
protected $ownerOrdering;
protected $spacePHID;
protected $bridgedObjectPHID;
protected $properties = array();
protected $points;
protected $subtype;
protected $closedEpoch;
protected $closerPHID;
private $subscriberPHIDs = self::ATTACHABLE;
private $groupByProjectPHID = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
private $edgeProjectPHIDs = self::ATTACHABLE;
private $bridgedObject = self::ATTACHABLE;
public static function initializeNewTask(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorManiphestApplication'))
->executeOne();
$view_policy = $app->getPolicy(ManiphestDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(ManiphestDefaultEditCapability::CAPABILITY);
return id(new ManiphestTask())
->setStatus(ManiphestTaskStatus::getDefaultStatus())
->setPriority(ManiphestTaskPriority::getDefaultPriority())
->setAuthorPHID($actor->getPHID())
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setSpacePHID($actor->getDefaultSpacePHID())
->setSubtype(PhabricatorEditEngineSubtype::SUBTYPE_DEFAULT)
->attachProjectPHIDs(array())
->attachSubscriberPHIDs(array());
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'ownerPHID' => 'phid?',
'status' => 'text64',
'priority' => 'uint32',
'title' => 'sort',
'description' => 'text',
'mailKey' => 'bytes20',
'ownerOrdering' => 'text64?',
'originalEmailSource' => 'text255?',
'subpriority' => 'double',
'points' => 'double?',
'bridgedObjectPHID' => 'phid?',
'subtype' => 'text64',
'closedEpoch' => 'epoch?',
'closerPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'priority' => array(
'columns' => array('priority', 'status'),
),
'status' => array(
'columns' => array('status'),
),
'ownerPHID' => array(
'columns' => array('ownerPHID', 'status'),
),
'authorPHID' => array(
'columns' => array('authorPHID', 'status'),
),
'ownerOrdering' => array(
'columns' => array('ownerOrdering'),
),
'priority_2' => array(
'columns' => array('priority', 'subpriority'),
),
'key_dateCreated' => array(
'columns' => array('dateCreated'),
),
'key_dateModified' => array(
'columns' => array('dateModified'),
),
'key_title' => array(
'columns' => array('title(64)'),
),
'key_bridgedobject' => array(
'columns' => array('bridgedObjectPHID'),
'unique' => true,
),
'key_subtype' => array(
'columns' => array('subtype'),
),
'key_closed' => array(
'columns' => array('closedEpoch'),
),
'key_closer' => array(
'columns' => array('closerPHID', 'closedEpoch'),
),
),
) + parent::getConfiguration();
}
public function loadDependsOnTaskPHIDs() {
return PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getPHID(),
ManiphestTaskDependsOnTaskEdgeType::EDGECONST);
}
public function loadDependedOnByTaskPHIDs() {
return PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getPHID(),
ManiphestTaskDependedOnByTaskEdgeType::EDGECONST);
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(ManiphestTaskPHIDType::TYPECONST);
}
public function getSubscriberPHIDs() {
return $this->assertAttached($this->subscriberPHIDs);
}
public function getProjectPHIDs() {
return $this->assertAttached($this->edgeProjectPHIDs);
}
public function attachProjectPHIDs(array $phids) {
$this->edgeProjectPHIDs = $phids;
return $this;
}
public function attachSubscriberPHIDs(array $phids) {
$this->subscriberPHIDs = $phids;
return $this;
}
public function setOwnerPHID($phid) {
$this->ownerPHID = nonempty($phid, null);
return $this;
}
public function getMonogram() {
return 'T'.$this->getID();
}
public function getURI() {
return '/'.$this->getMonogram();
}
public function attachGroupByProjectPHID($phid) {
$this->groupByProjectPHID = $phid;
return $this;
}
public function getGroupByProjectPHID() {
return $this->assertAttached($this->groupByProjectPHID);
}
public function save() {
if (!$this->mailKey) {
$this->mailKey = Filesystem::readRandomCharacters(20);
}
$result = parent::save();
return $result;
}
public function isClosed() {
return ManiphestTaskStatus::isClosedStatus($this->getStatus());
}
public function isLocked() {
return ManiphestTaskStatus::isLockedStatus($this->getStatus());
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function getCoverImageFilePHID() {
return idx($this->properties, 'cover.filePHID');
}
public function getCoverImageThumbnailPHID() {
return idx($this->properties, 'cover.thumbnailPHID');
}
public function getWorkboardOrderVectors() {
return array(
PhabricatorProjectColumn::ORDER_PRIORITY => array(
(int)-$this->getPriority(),
(double)-$this->getSubpriority(),
(int)-$this->getID(),
),
);
}
public function getPriorityKeyword() {
$priority = $this->getPriority();
$keyword = ManiphestTaskPriority::getKeywordForTaskPriority($priority);
if ($keyword !== null) {
return $keyword;
}
return ManiphestTaskPriority::UNKNOWN_PRIORITY_KEYWORD;
}
private function comparePriorityTo(ManiphestTask $other) {
$upri = $this->getPriority();
$vpri = $other->getPriority();
if ($upri != $vpri) {
return ($upri - $vpri);
}
$usub = $this->getSubpriority();
$vsub = $other->getSubpriority();
if ($usub != $vsub) {
return ($usub - $vsub);
}
$uid = $this->getID();
$vid = $other->getID();
if ($uid != $vid) {
return ($uid - $vid);
}
return 0;
}
public function isLowerPriorityThan(ManiphestTask $other) {
return ($this->comparePriorityTo($other) < 0);
}
public function isHigherPriorityThan(ManiphestTask $other) {
return ($this->comparePriorityTo($other) > 0);
}
public function getWorkboardProperties() {
return array(
'status' => $this->getStatus(),
'points' => (double)$this->getPoints(),
);
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return ($phid == $this->getOwnerPHID());
}
/* -( Markup Interface )--------------------------------------------------- */
/**
* @task markup
*/
public function getMarkupFieldKey($field) {
$content = $this->getMarkupText($field);
return PhabricatorMarkupEngine::digestRemarkupContent($this, $content);
}
/**
* @task markup
*/
public function getMarkupText($field) {
return $this->getDescription();
}
/**
* @task markup
*/
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newManiphestMarkupEngine();
}
/**
* @task markup
*/
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
/**
* @task markup
*/
public function shouldUseMarkupCache($field) {
return (bool)$this->getID();
}
/* -( Policy Interface )--------------------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_INTERACT,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_INTERACT:
if ($this->isLocked()) {
return PhabricatorPolicies::POLICY_NOONE;
} else {
return $this->getViewPolicy();
}
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
// The owner of a task can always view and edit it.
$owner_phid = $this->getOwnerPHID();
if ($owner_phid) {
$user_phid = $user->getPHID();
if ($user_phid == $owner_phid) {
return true;
}
}
return false;
}
public function describeAutomaticCapability($capability) {
return pht('The owner of a task can always view and edit it.');
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
// Sort of ambiguous who this was intended for; just let them both know.
return array_filter(
array_unique(
array(
$this->getAuthorPHID(),
$this->getOwnerPHID(),
)));
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('maniphest.fields');
}
public function getCustomFieldBaseClass() {
return 'ManiphestCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new ManiphestTransactionEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new ManiphestTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorSpacesInterface )----------------------------------------- */
public function getSpacePHID() {
return $this->spacePHID;
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('title')
->setType('string')
->setDescription(pht('The title of the task.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('description')
->setType('remarkup')
->setDescription(pht('The task description.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('authorPHID')
->setType('phid')
->setDescription(pht('Original task author.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('ownerPHID')
->setType('phid?')
->setDescription(pht('Current task owner, if task is assigned.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('status')
->setType('map<string, wild>')
->setDescription(pht('Information about task status.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('priority')
->setType('map<string, wild>')
->setDescription(pht('Information about task priority.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('points')
->setType('points')
->setDescription(pht('Point value of the task.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('subtype')
->setType('string')
->setDescription(pht('Subtype of the task.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('closerPHID')
->setType('phid?')
->setDescription(
pht('User who closed the task, if the task is closed.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('dateClosed')
->setType('int?')
->setDescription(
pht('Epoch timestamp when the task was closed.')),
);
}
public function getFieldValuesForConduit() {
$status_value = $this->getStatus();
$status_info = array(
'value' => $status_value,
'name' => ManiphestTaskStatus::getTaskStatusName($status_value),
'color' => ManiphestTaskStatus::getStatusColor($status_value),
);
$priority_value = (int)$this->getPriority();
$priority_info = array(
'value' => $priority_value,
'subpriority' => (double)$this->getSubpriority(),
'name' => ManiphestTaskPriority::getTaskPriorityName($priority_value),
'color' => ManiphestTaskPriority::getTaskPriorityColor($priority_value),
);
$closed_epoch = $this->getClosedEpoch();
if ($closed_epoch !== null) {
$closed_epoch = (int)$closed_epoch;
}
return array(
'name' => $this->getTitle(),
'description' => array(
'raw' => $this->getDescription(),
),
'authorPHID' => $this->getAuthorPHID(),
'ownerPHID' => $this->getOwnerPHID(),
'status' => $status_info,
'priority' => $priority_info,
'points' => $this->getPoints(),
'subtype' => $this->getSubtype(),
'closerPHID' => $this->getCloserPHID(),
'dateClosed' => $closed_epoch,
);
}
public function getConduitSearchAttachments() {
return array(
id(new PhabricatorBoardColumnsSearchEngineAttachment())
->setAttachmentKey('columns'),
);
}
public function newSubtypeObject() {
$subtype_key = $this->getEditEngineSubtype();
$subtype_map = $this->newEditEngineSubtypeMap();
return $subtype_map->getSubtype($subtype_key);
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new ManiphestTaskFulltextEngine();
}
/* -( DoorkeeperBridgedObjectInterface )----------------------------------- */
public function getBridgedObject() {
return $this->assertAttached($this->bridgedObject);
}
public function attachBridgedObject(
DoorkeeperExternalObject $object = null) {
$this->bridgedObject = $object;
return $this;
}
/* -( PhabricatorEditEngineSubtypeInterface )------------------------------ */
public function getEditEngineSubtype() {
return $this->getSubtype();
}
public function setEditEngineSubtype($value) {
return $this->setSubtype($value);
}
public function newEditEngineSubtypeMap() {
$config = PhabricatorEnv::getEnvConfig('maniphest.subtypes');
return PhabricatorEditEngineSubtype::newSubtypeMap($config);
}
/* -( PhabricatorEditEngineLockableInterface )----------------------------- */
public function newEditEngineLock() {
return new ManiphestTaskEditEngineLock();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new ManiphestTaskFerretEngine();
}
+
+/* -( PhabricatorEditEngineMFAInterface )---------------------------------- */
+
+
+ public function newEditEngineMFAEngine() {
+ return new ManiphestTaskMFAEngine();
+ }
+
}
diff --git a/src/applications/metamta/adapter/PhabricatorMailAdapter.php b/src/applications/metamta/adapter/PhabricatorMailAdapter.php
new file mode 100644
index 000000000..4fb262626
--- /dev/null
+++ b/src/applications/metamta/adapter/PhabricatorMailAdapter.php
@@ -0,0 +1,140 @@
+<?php
+
+abstract class PhabricatorMailAdapter
+ extends Phobject {
+
+ private $key;
+ private $priority;
+ private $media;
+ private $options = array();
+
+ private $supportsInbound = true;
+ private $supportsOutbound = true;
+ private $mediaMap;
+
+ final public function getAdapterType() {
+ return $this->getPhobjectClassConstant('ADAPTERTYPE');
+ }
+
+ final public static function getAllAdapters() {
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->setUniqueMethod('getAdapterType')
+ ->execute();
+ }
+
+ abstract public function getSupportedMessageTypes();
+ abstract public function sendMessage(PhabricatorMailExternalMessage $message);
+
+ /**
+ * Return true if this adapter supports setting a "Message-ID" when sending
+ * email.
+ *
+ * This is an ugly implementation detail because mail threading is a horrible
+ * mess, implemented differently by every client in existence.
+ */
+ public function supportsMessageIDHeader() {
+ return false;
+ }
+
+ final public function supportsMessageType($message_type) {
+ if ($this->mediaMap === null) {
+ $media_map = $this->getSupportedMessageTypes();
+ $media_map = array_fuse($media_map);
+
+ if ($this->media) {
+ $config_map = $this->media;
+ $config_map = array_fuse($config_map);
+
+ $media_map = array_intersect_key($media_map, $config_map);
+ }
+
+ $this->mediaMap = $media_map;
+ }
+
+ return isset($this->mediaMap[$message_type]);
+ }
+
+ final public function setMedia(array $media) {
+ $native_map = $this->getSupportedMessageTypes();
+ $native_map = array_fuse($native_map);
+
+ foreach ($media as $medium) {
+ if (!isset($native_map[$medium])) {
+ throw new Exception(
+ pht(
+ 'Adapter ("%s") is configured for medium "%s", but this is not '.
+ 'a supported delivery medium. Supported media are: %s.',
+ $medium,
+ implode(', ', $native_map)));
+ }
+ }
+
+ $this->media = $media;
+ $this->mediaMap = null;
+ return $this;
+ }
+
+ final public function getMedia() {
+ return $this->media;
+ }
+
+ final public function setKey($key) {
+ $this->key = $key;
+ return $this;
+ }
+
+ final public function getKey() {
+ return $this->key;
+ }
+
+ final public function setPriority($priority) {
+ $this->priority = $priority;
+ return $this;
+ }
+
+ final public function getPriority() {
+ return $this->priority;
+ }
+
+ final public function setSupportsInbound($supports_inbound) {
+ $this->supportsInbound = $supports_inbound;
+ return $this;
+ }
+
+ final public function getSupportsInbound() {
+ return $this->supportsInbound;
+ }
+
+ final public function setSupportsOutbound($supports_outbound) {
+ $this->supportsOutbound = $supports_outbound;
+ return $this;
+ }
+
+ final public function getSupportsOutbound() {
+ return $this->supportsOutbound;
+ }
+
+ final public function getOption($key) {
+ if (!array_key_exists($key, $this->options)) {
+ throw new Exception(
+ pht(
+ 'Mailer ("%s") is attempting to access unknown option ("%s").',
+ get_class($this),
+ $key));
+ }
+
+ return $this->options[$key];
+ }
+
+ final public function setOptions(array $options) {
+ $this->validateOptions($options);
+ $this->options = $options;
+ return $this;
+ }
+
+ abstract protected function validateOptions(array $options);
+
+ abstract public function newDefaultOptions();
+
+}
diff --git a/src/applications/metamta/adapter/PhabricatorMailAmazonSESAdapter.php b/src/applications/metamta/adapter/PhabricatorMailAmazonSESAdapter.php
new file mode 100644
index 000000000..a289e5bc7
--- /dev/null
+++ b/src/applications/metamta/adapter/PhabricatorMailAmazonSESAdapter.php
@@ -0,0 +1,71 @@
+<?php
+
+final class PhabricatorMailAmazonSESAdapter
+ extends PhabricatorMailAdapter {
+
+ const ADAPTERTYPE = 'ses';
+
+ public function getSupportedMessageTypes() {
+ return array(
+ PhabricatorMailEmailMessage::MESSAGETYPE,
+ );
+ }
+
+ public function supportsMessageIDHeader() {
+ return false;
+ }
+
+ protected function validateOptions(array $options) {
+ PhutilTypeSpec::checkMap(
+ $options,
+ array(
+ 'access-key' => 'string',
+ 'secret-key' => 'string',
+ 'endpoint' => 'string',
+ ));
+ }
+
+ public function newDefaultOptions() {
+ return array(
+ 'access-key' => null,
+ 'secret-key' => null,
+ 'endpoint' => null,
+ );
+ }
+
+ /**
+ * @phutil-external-symbol class PHPMailerLite
+ */
+ public function sendMessage(PhabricatorMailExternalMessage $message) {
+ $root = phutil_get_library_root('phabricator');
+ $root = dirname($root);
+ require_once $root.'/externals/phpmailer/class.phpmailer-lite.php';
+
+ $mailer = PHPMailerLite::newFromMessage($message);
+
+ $mailer->Mailer = 'amazon-ses';
+ $mailer->customMailer = $this;
+
+ $mailer->Send();
+ }
+
+
+
+ /**
+ * @phutil-external-symbol class SimpleEmailService
+ */
+ public function executeSend($body) {
+ $key = $this->getOption('access-key');
+ $secret = $this->getOption('secret-key');
+ $endpoint = $this->getOption('endpoint');
+
+ $root = phutil_get_library_root('phabricator');
+ $root = dirname($root);
+ require_once $root.'/externals/amazon-ses/ses.php';
+
+ $service = new SimpleEmailService($key, $secret, $endpoint);
+ $service->enableUseExceptions(true);
+ return $service->sendRawEmail($body);
+ }
+
+}
diff --git a/src/applications/metamta/adapter/PhabricatorMailAmazonSNSAdapter.php b/src/applications/metamta/adapter/PhabricatorMailAmazonSNSAdapter.php
new file mode 100644
index 000000000..b34e422db
--- /dev/null
+++ b/src/applications/metamta/adapter/PhabricatorMailAmazonSNSAdapter.php
@@ -0,0 +1,63 @@
+<?php
+
+final class PhabricatorMailAmazonSNSAdapter
+ extends PhabricatorMailAdapter {
+
+ const ADAPTERTYPE = 'sns';
+
+ public function getSupportedMessageTypes() {
+ return array(
+ PhabricatorMailSMSMessage::MESSAGETYPE,
+ );
+ }
+
+ protected function validateOptions(array $options) {
+ PhutilTypeSpec::checkMap(
+ $options,
+ array(
+ 'access-key' => 'string',
+ 'secret-key' => 'string',
+ 'endpoint' => 'string',
+ 'region' => 'string',
+ ));
+ }
+
+ public function newDefaultOptions() {
+ return array(
+ 'access-key' => null,
+ 'secret-key' => null,
+ 'endpoint' => null,
+ 'region' => null,
+ );
+ }
+
+ public function sendMessage(PhabricatorMailExternalMessage $message) {
+ $access_key = $this->getOption('access-key');
+
+ $secret_key = $this->getOption('secret-key');
+ $secret_key = new PhutilOpaqueEnvelope($secret_key);
+
+ $endpoint = $this->getOption('endpoint');
+ $region = $this->getOption('region');
+
+ $to_number = $message->getToNumber();
+ $text_body = $message->getTextBody();
+
+ $params = array(
+ 'Version' => '2010-03-31',
+ 'Action' => 'Publish',
+ 'PhoneNumber' => $to_number->toE164(),
+ 'Message' => $text_body,
+ );
+
+ return id(new PhabricatorAmazonSNSFuture())
+ ->setParameters($params)
+ ->setEndpoint($endpoint)
+ ->setAccessKey($access_key)
+ ->setSecretKey($secret_key)
+ ->setRegion($region)
+ ->setTimeout(60)
+ ->resolve();
+ }
+
+}
diff --git a/src/applications/metamta/adapter/PhabricatorMailMailgunAdapter.php b/src/applications/metamta/adapter/PhabricatorMailMailgunAdapter.php
new file mode 100644
index 000000000..9eb478efc
--- /dev/null
+++ b/src/applications/metamta/adapter/PhabricatorMailMailgunAdapter.php
@@ -0,0 +1,132 @@
+<?php
+
+/**
+ * Mail adapter that uses Mailgun's web API to deliver email.
+ */
+final class PhabricatorMailMailgunAdapter
+ extends PhabricatorMailAdapter {
+
+ const ADAPTERTYPE = 'mailgun';
+
+ public function getSupportedMessageTypes() {
+ return array(
+ PhabricatorMailEmailMessage::MESSAGETYPE,
+ );
+ }
+
+ public function supportsMessageIDHeader() {
+ return true;
+ }
+
+ protected function validateOptions(array $options) {
+ PhutilTypeSpec::checkMap(
+ $options,
+ array(
+ 'api-key' => 'string',
+ 'domain' => 'string',
+ ));
+ }
+
+ public function newDefaultOptions() {
+ return array(
+ 'api-key' => null,
+ 'domain' => null,
+ );
+ }
+
+ public function sendMessage(PhabricatorMailExternalMessage $message) {
+ $api_key = $this->getOption('api-key');
+ $domain = $this->getOption('domain');
+ $params = array();
+
+ $subject = $message->getSubject();
+ if ($subject !== null) {
+ $params['subject'] = $subject;
+ }
+
+ $from_address = $message->getFromAddress();
+ if ($from_address) {
+ $params['from'] = (string)$from_address;
+ }
+
+ $to_addresses = $message->getToAddresses();
+ if ($to_addresses) {
+ $to = array();
+ foreach ($to_addresses as $address) {
+ $to[] = (string)$address;
+ }
+ $params['to'] = implode(', ', $to);
+ }
+
+ $cc_addresses = $message->getCCAddresses();
+ if ($cc_addresses) {
+ $cc = array();
+ foreach ($cc_addresses as $address) {
+ $cc[] = (string)$address;
+ }
+ $params['cc'] = implode(', ', $cc);
+ }
+
+ $reply_address = $message->getReplyToAddress();
+ if ($reply_address) {
+ $params['h:reply-to'] = (string)$reply_address;
+ }
+
+ $headers = $message->getHeaders();
+ if ($headers) {
+ foreach ($headers as $header) {
+ $name = $header->getName();
+ $value = $header->getValue();
+ $params['h:'.$name] = $value;
+ }
+ }
+
+ $text_body = $message->getTextBody();
+ if ($text_body !== null) {
+ $params['text'] = $text_body;
+ }
+
+ $html_body = $message->getHTMLBody();
+ if ($html_body !== null) {
+ $params['html'] = $html_body;
+ }
+
+ $mailgun_uri = urisprintf(
+ 'https://api.mailgun.net/v2/%s/messages',
+ $domain);
+
+ $future = id(new HTTPSFuture($mailgun_uri, $params))
+ ->setMethod('POST')
+ ->setHTTPBasicAuthCredentials('api', new PhutilOpaqueEnvelope($api_key))
+ ->setTimeout(60);
+
+ $attachments = $message->getAttachments();
+ foreach ($attachments as $attachment) {
+ $future->attachFileData(
+ 'attachment',
+ $attachment->getData(),
+ $attachment->getFilename(),
+ $attachment->getMimeType());
+ }
+
+ list($body) = $future->resolvex();
+
+ $response = null;
+ try {
+ $response = phutil_json_decode($body);
+ } catch (PhutilJSONParserException $ex) {
+ throw new PhutilProxyException(
+ pht('Failed to JSON decode response.'),
+ $ex);
+ }
+
+ if (!idx($response, 'id')) {
+ $message = $response['message'];
+ throw new Exception(
+ pht(
+ 'Request failed with errors: %s.',
+ $message));
+ }
+ }
+
+}
diff --git a/src/applications/metamta/adapter/PhabricatorMailPostmarkAdapter.php b/src/applications/metamta/adapter/PhabricatorMailPostmarkAdapter.php
new file mode 100644
index 000000000..d84d8f8bf
--- /dev/null
+++ b/src/applications/metamta/adapter/PhabricatorMailPostmarkAdapter.php
@@ -0,0 +1,128 @@
+<?php
+
+final class PhabricatorMailPostmarkAdapter
+ extends PhabricatorMailAdapter {
+
+ const ADAPTERTYPE = 'postmark';
+
+ public function getSupportedMessageTypes() {
+ return array(
+ PhabricatorMailEmailMessage::MESSAGETYPE,
+ );
+ }
+
+ public function supportsMessageIDHeader() {
+ return true;
+ }
+
+ protected function validateOptions(array $options) {
+ PhutilTypeSpec::checkMap(
+ $options,
+ array(
+ 'access-token' => 'string',
+ 'inbound-addresses' => 'list<string>',
+ ));
+
+ // Make sure this is properly formatted.
+ PhutilCIDRList::newList($options['inbound-addresses']);
+ }
+
+ public function newDefaultOptions() {
+ return array(
+ 'access-token' => null,
+ 'inbound-addresses' => array(
+ // Via Postmark support circa February 2018, see:
+ //
+ // https://postmarkapp.com/support/article/800-ips-for-firewalls
+ //
+ // "Configuring Outbound Email" should be updated if this changes.
+ //
+ // These addresses were last updated in January 2019.
+ '50.31.156.6/32',
+ '50.31.156.77/32',
+ '18.217.206.57/32',
+ ),
+ );
+ }
+
+ public function sendMessage(PhabricatorMailExternalMessage $message) {
+ $access_token = $this->getOption('access-token');
+
+ $parameters = array();
+
+ $subject = $message->getSubject();
+ if ($subject !== null) {
+ $parameters['Subject'] = $subject;
+ }
+
+ $from_address = $message->getFromAddress();
+ if ($from_address) {
+ $parameters['From'] = (string)$from_address;
+ }
+
+ $to_addresses = $message->getToAddresses();
+ if ($to_addresses) {
+ $to = array();
+ foreach ($to_addresses as $address) {
+ $to[] = (string)$address;
+ }
+ $parameters['To'] = implode(', ', $to);
+ }
+
+ $cc_addresses = $message->getCCAddresses();
+ if ($cc_addresses) {
+ $cc = array();
+ foreach ($cc_addresses as $address) {
+ $cc[] = (string)$address;
+ }
+ $parameters['Cc'] = implode(', ', $cc);
+ }
+
+ $reply_address = $message->getReplyToAddress();
+ if ($reply_address) {
+ $parameters['ReplyTo'] = (string)$reply_address;
+ }
+
+ $headers = $message->getHeaders();
+ if ($headers) {
+ $list = array();
+ foreach ($headers as $header) {
+ $list[] = array(
+ 'Name' => $header->getName(),
+ 'Value' => $header->getValue(),
+ );
+ }
+ $parameters['Headers'] = $list;
+ }
+
+ $text_body = $message->getTextBody();
+ if ($text_body !== null) {
+ $parameters['TextBody'] = $text_body;
+ }
+
+ $html_body = $message->getHTMLBody();
+ if ($html_body !== null) {
+ $parameters['HtmlBody'] = $html_body;
+ }
+
+ $attachments = $message->getAttachments();
+ if ($attachments) {
+ $files = array();
+ foreach ($attachments as $attachment) {
+ $files[] = array(
+ 'Name' => $attachment->getFilename(),
+ 'ContentType' => $attachment->getMimeType(),
+ 'Content' => base64_encode($attachment->getData()),
+ );
+ }
+ $parameters['Attachments'] = $files;
+ }
+
+ id(new PhutilPostmarkFuture())
+ ->setAccessToken($access_token)
+ ->setMethod('email', $parameters)
+ ->setTimeout(60)
+ ->resolve();
+ }
+
+}
diff --git a/src/applications/metamta/adapter/PhabricatorMailSMTPAdapter.php b/src/applications/metamta/adapter/PhabricatorMailSMTPAdapter.php
new file mode 100644
index 000000000..a3c629827
--- /dev/null
+++ b/src/applications/metamta/adapter/PhabricatorMailSMTPAdapter.php
@@ -0,0 +1,154 @@
+<?php
+
+final class PhabricatorMailSMTPAdapter
+ extends PhabricatorMailAdapter {
+
+ const ADAPTERTYPE = 'smtp';
+
+ public function getSupportedMessageTypes() {
+ return array(
+ PhabricatorMailEmailMessage::MESSAGETYPE,
+ );
+ }
+
+ public function supportsMessageIDHeader() {
+ return true;
+ }
+
+ protected function validateOptions(array $options) {
+ PhutilTypeSpec::checkMap(
+ $options,
+ array(
+ 'host' => 'string|null',
+ 'port' => 'int',
+ 'user' => 'string|null',
+ 'password' => 'string|null',
+ 'protocol' => 'string|null',
+ ));
+ }
+
+ public function newDefaultOptions() {
+ return array(
+ 'host' => null,
+ 'port' => 25,
+ 'user' => null,
+ 'password' => null,
+ 'protocol' => null,
+ );
+ }
+
+ /**
+ * @phutil-external-symbol class PHPMailer
+ */
+ public function sendMessage(PhabricatorMailExternalMessage $message) {
+ $root = phutil_get_library_root('phabricator');
+ $root = dirname($root);
+ require_once $root.'/externals/phpmailer/class.phpmailer.php';
+ $smtp = new PHPMailer($use_exceptions = true);
+
+ $smtp->CharSet = 'utf-8';
+ $smtp->Encoding = 'base64';
+
+ // By default, PHPMailer sends one mail per recipient. We handle
+ // combining or separating To and Cc higher in the stack, so tell it to
+ // send mail exactly like we ask.
+ $smtp->SingleTo = false;
+
+ $smtp->IsSMTP();
+ $smtp->Host = $this->getOption('host');
+ $smtp->Port = $this->getOption('port');
+ $user = $this->getOption('user');
+ if (strlen($user)) {
+ $smtp->SMTPAuth = true;
+ $smtp->Username = $user;
+ $smtp->Password = $this->getOption('password');
+ }
+
+ $protocol = $this->getOption('protocol');
+ if ($protocol) {
+ $protocol = phutil_utf8_strtolower($protocol);
+ $smtp->SMTPSecure = $protocol;
+ }
+
+ $subject = $message->getSubject();
+ if ($subject !== null) {
+ $smtp->Subject = $subject;
+ }
+
+ $from_address = $message->getFromAddress();
+ if ($from_address) {
+ $smtp->SetFrom(
+ $from_address->getAddress(),
+ (string)$from_address->getDisplayName(),
+ $crazy_side_effects = false);
+ }
+
+ $reply_address = $message->getReplyToAddress();
+ if ($reply_address) {
+ $smtp->AddReplyTo(
+ $reply_address->getAddress(),
+ (string)$reply_address->getDisplayName());
+ }
+
+ $to_addresses = $message->getToAddresses();
+ if ($to_addresses) {
+ foreach ($to_addresses as $address) {
+ $smtp->AddAddress(
+ $address->getAddress(),
+ (string)$address->getDisplayName());
+ }
+ }
+
+ $cc_addresses = $message->getCCAddresses();
+ if ($cc_addresses) {
+ foreach ($cc_addresses as $address) {
+ $smtp->AddCC(
+ $address->getAddress(),
+ (string)$address->getDisplayName());
+ }
+ }
+
+ $headers = $message->getHeaders();
+ if ($headers) {
+ $list = array();
+ foreach ($headers as $header) {
+ $name = $header->getName();
+ $value = $header->getValue();
+
+ if (phutil_utf8_strtolower($name) === 'message-id') {
+ $smtp->MessageID = $value;
+ } else {
+ $smtp->AddCustomHeader("{$name}: {$value}");
+ }
+ }
+ }
+
+ $text_body = $message->getTextBody();
+ if ($text_body !== null) {
+ $smtp->Body = $text_body;
+ }
+
+ $html_body = $message->getHTMLBody();
+ if ($html_body !== null) {
+ $smtp->IsHTML(true);
+ $smtp->Body = $html_body;
+ if ($text_body !== null) {
+ $smtp->AltBody = $text_body;
+ }
+ }
+
+ $attachments = $message->getAttachments();
+ if ($attachments) {
+ foreach ($attachments as $attachment) {
+ $smtp->AddStringAttachment(
+ $attachment->getData(),
+ $attachment->getFilename(),
+ 'base64',
+ $attachment->getMimeType());
+ }
+ }
+
+ $smtp->Send();
+ }
+
+}
diff --git a/src/applications/metamta/adapter/PhabricatorMailSendGridAdapter.php b/src/applications/metamta/adapter/PhabricatorMailSendGridAdapter.php
new file mode 100644
index 000000000..133e82b62
--- /dev/null
+++ b/src/applications/metamta/adapter/PhabricatorMailSendGridAdapter.php
@@ -0,0 +1,144 @@
+<?php
+
+/**
+ * Mail adapter that uses SendGrid's web API to deliver email.
+ */
+final class PhabricatorMailSendGridAdapter
+ extends PhabricatorMailAdapter {
+
+ const ADAPTERTYPE = 'sendgrid';
+
+ public function getSupportedMessageTypes() {
+ return array(
+ PhabricatorMailEmailMessage::MESSAGETYPE,
+ );
+ }
+
+ protected function validateOptions(array $options) {
+ PhutilTypeSpec::checkMap(
+ $options,
+ array(
+ 'api-key' => 'string',
+ ));
+ }
+
+ public function newDefaultOptions() {
+ return array(
+ 'api-key' => null,
+ );
+ }
+
+ public function sendMessage(PhabricatorMailExternalMessage $message) {
+ $key = $this->getOption('api-key');
+
+ $parameters = array();
+
+ $subject = $message->getSubject();
+ if ($subject !== null) {
+ $parameters['subject'] = $subject;
+ }
+
+ $personalizations = array();
+
+ $to_addresses = $message->getToAddresses();
+ if ($to_addresses) {
+ $personalizations['to'] = array();
+ foreach ($to_addresses as $address) {
+ $personalizations['to'][] = $this->newPersonalization($address);
+ }
+ }
+
+ $cc_addresses = $message->getCCAddresses();
+ if ($cc_addresses) {
+ $personalizations['cc'] = array();
+ foreach ($cc_addresses as $address) {
+ $personalizations['cc'][] = $this->newPersonalization($address);
+ }
+ }
+
+ // This is a list of different sets of recipients who should receive copies
+ // of the mail. We handle "one message to each recipient" ourselves.
+ $parameters['personalizations'] = array(
+ $personalizations,
+ );
+
+ $from_address = $message->getFromAddress();
+ if ($from_address) {
+ $parameters['from'] = $this->newPersonalization($from_address);
+ }
+
+ $reply_address = $message->getReplyToAddress();
+ if ($reply_address) {
+ $parameters['reply_to'] = $this->newPersonalization($reply_address);
+ }
+
+ $headers = $message->getHeaders();
+ if ($headers) {
+ $map = array();
+ foreach ($headers as $header) {
+ $map[$header->getName()] = $header->getValue();
+ }
+ $parameters['headers'] = $map;
+ }
+
+ $content = array();
+ $text_body = $message->getTextBody();
+ if ($text_body !== null) {
+ $content[] = array(
+ 'type' => 'text/plain',
+ 'value' => $text_body,
+ );
+ }
+
+ $html_body = $message->getHTMLBody();
+ if ($html_body !== null) {
+ $content[] = array(
+ 'type' => 'text/html',
+ 'value' => $html_body,
+ );
+ }
+ $parameters['content'] = $content;
+
+ $attachments = $message->getAttachments();
+ if ($attachments) {
+ $files = array();
+ foreach ($attachments as $attachment) {
+ $files[] = array(
+ 'content' => base64_encode($attachment->getData()),
+ 'type' => $attachment->getMimeType(),
+ 'filename' => $attachment->getFilename(),
+ 'disposition' => 'attachment',
+ );
+ }
+ $parameters['attachments'] = $files;
+ }
+
+ $sendgrid_uri = 'https://api.sendgrid.com/v3/mail/send';
+ $json_parameters = phutil_json_encode($parameters);
+
+ id(new HTTPSFuture($sendgrid_uri))
+ ->setMethod('POST')
+ ->addHeader('Authorization', "Bearer {$key}")
+ ->addHeader('Content-Type', 'application/json')
+ ->setData($json_parameters)
+ ->setTimeout(60)
+ ->resolvex();
+
+ // The SendGrid v3 API does not return a JSON response body. We get a
+ // non-2XX HTTP response in the case of an error, which throws above.
+ }
+
+ private function newPersonalization(PhutilEmailAddress $address) {
+ $result = array(
+ 'email' => $address->getAddress(),
+ );
+
+ $display_name = $address->getDisplayName();
+ if ($display_name) {
+ $result['name'] = $display_name;
+ }
+
+ return $result;
+ }
+
+}
diff --git a/src/applications/metamta/adapter/PhabricatorMailSendmailAdapter.php b/src/applications/metamta/adapter/PhabricatorMailSendmailAdapter.php
new file mode 100644
index 000000000..05f3c909a
--- /dev/null
+++ b/src/applications/metamta/adapter/PhabricatorMailSendmailAdapter.php
@@ -0,0 +1,45 @@
+<?php
+
+final class PhabricatorMailSendmailAdapter
+ extends PhabricatorMailAdapter {
+
+ const ADAPTERTYPE = 'sendmail';
+
+
+ public function getSupportedMessageTypes() {
+ return array(
+ PhabricatorMailEmailMessage::MESSAGETYPE,
+ );
+ }
+
+ public function supportsMessageIDHeader() {
+ return true;
+ }
+
+ protected function validateOptions(array $options) {
+ PhutilTypeSpec::checkMap(
+ $options,
+ array(
+ 'encoding' => 'string',
+ ));
+ }
+
+ public function newDefaultOptions() {
+ return array(
+ 'encoding' => 'base64',
+ );
+ }
+
+ /**
+ * @phutil-external-symbol class PHPMailerLite
+ */
+ public function sendMessage(PhabricatorMailExternalMessage $message) {
+ $root = phutil_get_library_root('phabricator');
+ $root = dirname($root);
+ require_once $root.'/externals/phpmailer/class.phpmailer-lite.php';
+
+ $mailer = PHPMailerLite::newFromMessage($message);
+ $mailer->Send();
+ }
+
+}
diff --git a/src/applications/metamta/adapter/PhabricatorMailTestAdapter.php b/src/applications/metamta/adapter/PhabricatorMailTestAdapter.php
new file mode 100644
index 000000000..a6258a887
--- /dev/null
+++ b/src/applications/metamta/adapter/PhabricatorMailTestAdapter.php
@@ -0,0 +1,161 @@
+<?php
+
+/**
+ * Mail adapter that doesn't actually send any email, for writing unit tests
+ * against.
+ */
+final class PhabricatorMailTestAdapter
+ extends PhabricatorMailAdapter {
+
+ const ADAPTERTYPE = 'test';
+
+ private $guts = array();
+
+ private $supportsMessageID;
+ private $failPermanently;
+ private $failTemporarily;
+
+ public function setSupportsMessageID($support) {
+ $this->supportsMessageID = $support;
+ return $this;
+ }
+
+ public function setFailPermanently($fail) {
+ $this->failPermanently = true;
+ return $this;
+ }
+
+ public function setFailTemporarily($fail) {
+ $this->failTemporarily = true;
+ return $this;
+ }
+
+ public function getSupportedMessageTypes() {
+ return array(
+ PhabricatorMailEmailMessage::MESSAGETYPE,
+ PhabricatorMailSMSMessage::MESSAGETYPE,
+ );
+ }
+
+ protected function validateOptions(array $options) {
+ PhutilTypeSpec::checkMap($options, array());
+ }
+
+ public function newDefaultOptions() {
+ return array();
+ }
+
+ public function supportsMessageIDHeader() {
+ return $this->supportsMessageID;
+ }
+
+ public function getGuts() {
+ return $this->guts;
+ }
+
+ public function sendMessage(PhabricatorMailExternalMessage $message) {
+ if ($this->failPermanently) {
+ throw new PhabricatorMetaMTAPermanentFailureException(
+ pht('Unit Test (Permanent)'));
+ }
+
+ if ($this->failTemporarily) {
+ throw new Exception(
+ pht('Unit Test (Temporary)'));
+ }
+
+ switch ($message->getMessageType()) {
+ case PhabricatorMailEmailMessage::MESSAGETYPE:
+ $guts = $this->newEmailGuts($message);
+ break;
+ case PhabricatorMailSMSMessage::MESSAGETYPE:
+ $guts = $this->newSMSGuts($message);
+ break;
+ }
+
+ $guts['did-send'] = true;
+ $this->guts = $guts;
+ }
+
+ public function getBody() {
+ return idx($this->guts, 'body');
+ }
+
+ public function getHTMLBody() {
+ return idx($this->guts, 'html-body');
+ }
+
+ private function newEmailGuts(PhabricatorMailExternalMessage $message) {
+ $guts = array();
+
+ $from = $message->getFromAddress();
+ $guts['from'] = (string)$from;
+
+ $reply_to = $message->getReplyToAddress();
+ if ($reply_to) {
+ $guts['reply-to'] = (string)$reply_to;
+ }
+
+ $to_addresses = $message->getToAddresses();
+ $to = array();
+ foreach ($to_addresses as $address) {
+ $to[] = (string)$address;
+ }
+ $guts['tos'] = $to;
+
+ $cc_addresses = $message->getCCAddresses();
+ $cc = array();
+ foreach ($cc_addresses as $address) {
+ $cc[] = (string)$address;
+ }
+ $guts['ccs'] = $cc;
+
+ $subject = $message->getSubject();
+ if (strlen($subject)) {
+ $guts['subject'] = $subject;
+ }
+
+ $headers = $message->getHeaders();
+ $header_list = array();
+ foreach ($headers as $header) {
+ $header_list[] = array(
+ $header->getName(),
+ $header->getValue(),
+ );
+ }
+ $guts['headers'] = $header_list;
+
+ $text_body = $message->getTextBody();
+ if (strlen($text_body)) {
+ $guts['body'] = $text_body;
+ }
+
+ $html_body = $message->getHTMLBody();
+ if (strlen($html_body)) {
+ $guts['html-body'] = $html_body;
+ }
+
+ $attachments = $message->getAttachments();
+ $file_list = array();
+ foreach ($attachments as $attachment) {
+ $file_list[] = array(
+ 'data' => $attachment->getData(),
+ 'filename' => $attachment->getFilename(),
+ 'mimetype' => $attachment->getMimeType(),
+ );
+ }
+ $guts['attachments'] = $file_list;
+
+ return $guts;
+ }
+
+ private function newSMSGuts(PhabricatorMailExternalMessage $message) {
+ $guts = array();
+
+ $guts['to'] = $message->getToNumber();
+ $guts['body'] = $message->getTextBody();
+
+ return $guts;
+ }
+
+}
diff --git a/src/applications/metamta/adapter/PhabricatorMailTwilioAdapter.php b/src/applications/metamta/adapter/PhabricatorMailTwilioAdapter.php
new file mode 100644
index 000000000..cdeb8d422
--- /dev/null
+++ b/src/applications/metamta/adapter/PhabricatorMailTwilioAdapter.php
@@ -0,0 +1,61 @@
+<?php
+
+final class PhabricatorMailTwilioAdapter
+ extends PhabricatorMailAdapter {
+
+ const ADAPTERTYPE = 'twilio';
+
+ public function getSupportedMessageTypes() {
+ return array(
+ PhabricatorMailSMSMessage::MESSAGETYPE,
+ );
+ }
+
+ protected function validateOptions(array $options) {
+ PhutilTypeSpec::checkMap(
+ $options,
+ array(
+ 'account-sid' => 'string',
+ 'auth-token' => 'string',
+ 'from-number' => 'string',
+ ));
+
+ // Construct an object from the "from-number" to validate it.
+ $number = new PhabricatorPhoneNumber($options['from-number']);
+ }
+
+ public function newDefaultOptions() {
+ return array(
+ 'account-sid' => null,
+ 'auth-token' => null,
+ 'from-number' => null,
+ );
+ }
+
+ public function sendMessage(PhabricatorMailExternalMessage $message) {
+ $account_sid = $this->getOption('account-sid');
+
+ $auth_token = $this->getOption('auth-token');
+ $auth_token = new PhutilOpaqueEnvelope($auth_token);
+
+ $from_number = $this->getOption('from-number');
+ $from_number = new PhabricatorPhoneNumber($from_number);
+
+ $to_number = $message->getToNumber();
+ $text_body = $message->getTextBody();
+
+ $parameters = array(
+ 'From' => $from_number->toE164(),
+ 'To' => $to_number->toE164(),
+ 'Body' => $text_body,
+ );
+
+ $result = id(new PhabricatorTwilioFuture())
+ ->setAccountSID($account_sid)
+ ->setAuthToken($auth_token)
+ ->setMethod('Messages.json', $parameters)
+ ->setTimeout(60)
+ ->resolve();
+ }
+
+}
diff --git a/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php b/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php
index afde9fee2..c13835e6f 100644
--- a/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php
+++ b/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php
@@ -1,408 +1,423 @@
<?php
final class PhabricatorMetaMTAApplicationEmailPanel
extends PhabricatorApplicationConfigurationPanel {
public function getPanelKey() {
return 'email';
}
public function shouldShowForApplication(
PhabricatorApplication $application) {
return $application->supportsEmailIntegration();
}
public function buildConfigurationPagePanel() {
$viewer = $this->getViewer();
$application = $this->getApplication();
$table = $this->buildEmailTable($is_edit = false, null);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$application,
PhabricatorPolicyCapability::CAN_EDIT);
$header = id(new PHUIHeaderView())
->setHeader(pht('Application Emails'))
->addActionLink(
id(new PHUIButtonView())
->setTag('a')
->setText(pht('Edit Application Emails'))
->setIcon('fa-pencil')
->setHref($this->getPanelURI())
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($table);
return $box;
}
public function handlePanelRequest(
AphrontRequest $request,
PhabricatorController $controller) {
$viewer = $request->getViewer();
$application = $this->getApplication();
$path = $request->getURIData('path');
if (strlen($path)) {
return new Aphront404Response();
}
$uri = $request->getRequestURI();
$uri->setQueryParams(array());
$new = $request->getStr('new');
$edit = $request->getInt('edit');
$delete = $request->getInt('delete');
if ($new) {
return $this->returnNewAddressResponse($request, $uri, $application);
}
if ($edit) {
return $this->returnEditAddressResponse($request, $uri, $edit);
}
if ($delete) {
return $this->returnDeleteAddressResponse($request, $uri, $delete);
}
$table = $this->buildEmailTable(
$is_edit = true,
$request->getInt('id'));
$form = id(new AphrontFormView())
->setUser($viewer);
$crumbs = $controller->buildPanelCrumbs($this);
$crumbs->addTextCrumb(pht('Edit Application Emails'));
$crumbs->setBorder(true);
$header = id(new PHUIHeaderView())
->setHeader(pht('Edit Application Emails: %s', $application->getName()))
->setSubheader($application->getAppEmailBlurb())
->setHeaderIcon('fa-pencil');
$icon = id(new PHUIIconView())
->setIcon('fa-plus');
$button = new PHUIButtonView();
$button->setText(pht('Add New Address'));
$button->setTag('a');
$button->setHref($uri->alter('new', 'true'));
$button->setIcon($icon);
$button->addSigil('workflow');
$header->addActionLink($button);
$object_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Emails'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($table);
$title = $application->getName();
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter($object_box);
return $controller->buildPanelPage(
$this,
$title,
$crumbs,
$view);
}
private function returnNewAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
PhabricatorApplication $application) {
$viewer = $request->getUser();
$email_object =
PhabricatorMetaMTAApplicationEmail::initializeNewAppEmail($viewer)
->setApplicationPHID($application->getPHID());
return $this->returnSaveAddressResponse(
$request,
$uri,
$email_object,
$is_new = true);
}
private function returnEditAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
$email_object_id) {
$viewer = $request->getUser();
$email_object = id(new PhabricatorMetaMTAApplicationEmailQuery())
->setViewer($viewer)
->withIDs(array($email_object_id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$email_object) {
return new Aphront404Response();
}
return $this->returnSaveAddressResponse(
$request,
$uri,
$email_object,
$is_new = false);
}
private function returnSaveAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
PhabricatorMetaMTAApplicationEmail $email_object,
$is_new) {
$viewer = $request->getUser();
$config_default =
PhabricatorMetaMTAApplicationEmail::CONFIG_DEFAULT_AUTHOR;
$e_email = true;
$v_email = $email_object->getAddress();
$e_space = null;
$v_space = $email_object->getSpacePHID();
$v_default = $email_object->getConfigValue($config_default);
$validation_exception = null;
if ($request->isDialogFormPost()) {
$e_email = null;
$v_email = trim($request->getStr('email'));
$v_space = $request->getStr('spacePHID');
$v_default = $request->getArr($config_default);
$v_default = nonempty(head($v_default), null);
$type_address =
PhabricatorMetaMTAApplicationEmailTransaction::TYPE_ADDRESS;
$type_space = PhabricatorTransactions::TYPE_SPACE;
$type_config =
PhabricatorMetaMTAApplicationEmailTransaction::TYPE_CONFIG;
$key_config = PhabricatorMetaMTAApplicationEmailTransaction::KEY_CONFIG;
$xactions = array();
$xactions[] = id(new PhabricatorMetaMTAApplicationEmailTransaction())
->setTransactionType($type_address)
->setNewValue($v_email);
$xactions[] = id(new PhabricatorMetaMTAApplicationEmailTransaction())
->setTransactionType($type_space)
->setNewValue($v_space);
$xactions[] = id(new PhabricatorMetaMTAApplicationEmailTransaction())
->setTransactionType($type_config)
->setMetadataValue($key_config, $config_default)
->setNewValue($v_default);
$editor = id(new PhabricatorMetaMTAApplicationEmailEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true);
try {
$editor->applyTransactions($email_object, $xactions);
return id(new AphrontRedirectResponse())->setURI(
$uri->alter('highlight', $email_object->getID()));
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
$e_email = $ex->getShortMessage($type_address);
$e_space = $ex->getShortMessage($type_space);
}
}
if ($v_default) {
$v_default = array($v_default);
} else {
$v_default = array();
}
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Email'))
->setName('email')
->setValue($v_email)
->setError($e_email));
if (PhabricatorSpacesNamespaceQuery::getViewerSpacesExist($viewer)) {
$form->appendControl(
id(new AphrontFormSelectControl())
->setLabel(pht('Space'))
->setName('spacePHID')
->setValue($v_space)
->setError($e_space)
->setOptions(
PhabricatorSpacesNamespaceQuery::getSpaceOptionsForViewer(
$viewer,
$v_space)));
}
$form
->appendControl(
id(new AphrontFormTokenizerControl())
->setDatasource(new PhabricatorPeopleDatasource())
->setLabel(pht('Default Author'))
->setName($config_default)
->setLimit(1)
->setValue($v_default)
- ->setCaption(pht(
- 'Used if the "From:" address does not map to a known account.')));
+ ->setCaption(
+ pht(
+ 'Used if the "From:" address does not map to a user account. '.
+ 'Setting a default author will allow anyone on the public '.
+ 'internet to create objects in Phabricator by sending email to '.
+ 'this address.')));
if ($is_new) {
$title = pht('New Address');
} else {
$title = pht('Edit Address');
}
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setWidth(AphrontDialogView::WIDTH_FORM)
->setTitle($title)
->setValidationException($validation_exception)
->appendForm($form)
->addSubmitButton(pht('Save'))
->addCancelButton($uri);
if ($is_new) {
$dialog->addHiddenInput('new', 'true');
}
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function returnDeleteAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
$email_object_id) {
$viewer = $this->getViewer();
$email_object = id(new PhabricatorMetaMTAApplicationEmailQuery())
->setViewer($viewer)
->withIDs(array($email_object_id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$email_object) {
return new Aphront404Response();
}
if ($request->isDialogFormPost()) {
$engine = new PhabricatorDestructionEngine();
$engine->destroyObject($email_object);
return id(new AphrontRedirectResponse())->setURI($uri);
}
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->addHiddenInput('delete', $email_object_id)
->setTitle(pht('Delete Address'))
->appendParagraph(pht(
'Are you sure you want to delete this email address?'))
->addSubmitButton(pht('Delete'))
->addCancelButton($uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function buildEmailTable($is_edit, $highlight) {
$viewer = $this->getViewer();
$application = $this->getApplication();
$uri = new PhutilURI($this->getPanelURI());
$emails = id(new PhabricatorMetaMTAApplicationEmailQuery())
->setViewer($viewer)
->withApplicationPHIDs(array($application->getPHID()))
->execute();
$rowc = array();
$rows = array();
foreach ($emails as $email) {
$button_edit = javelin_tag(
'a',
array(
'class' => 'button small button-grey',
'href' => $uri->alter('edit', $email->getID()),
'sigil' => 'workflow',
),
pht('Edit'));
$button_remove = javelin_tag(
'a',
array(
'class' => 'button small button-grey',
'href' => $uri->alter('delete', $email->getID()),
'sigil' => 'workflow',
),
pht('Delete'));
if ($highlight == $email->getID()) {
$rowc[] = 'highlighted';
} else {
$rowc[] = null;
}
$space_phid = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID($email);
if ($space_phid) {
$email_space = $viewer->renderHandle($space_phid);
} else {
$email_space = null;
}
+ $default_author_phid = $email->getDefaultAuthorPHID();
+ if (!$default_author_phid) {
+ $default_author = phutil_tag('em', array(), pht('None'));
+ } else {
+ $default_author = $viewer->renderHandle($default_author_phid);
+ }
+
$rows[] = array(
$email_space,
$email->getAddress(),
+ $default_author,
$button_edit,
$button_remove,
);
}
$table = id(new AphrontTableView($rows))
->setNoDataString(pht('No application emails created yet.'));
$table->setHeaders(
array(
pht('Space'),
pht('Email'),
+ pht('Default'),
pht('Edit'),
pht('Delete'),
));
$table->setColumnClasses(
array(
+ '',
'',
'wide',
'action',
'action',
));
$table->setRowClasses($rowc);
$table->setColumnVisibility(
array(
PhabricatorSpacesNamespaceQuery::getViewerSpacesExist($viewer),
true,
+ true,
$is_edit,
$is_edit,
));
return $table;
}
}
diff --git a/src/applications/metamta/constants/MetaMTAReceivedMailStatus.php b/src/applications/metamta/constants/MetaMTAReceivedMailStatus.php
index 965bdbdbf..faacdc2cf 100644
--- a/src/applications/metamta/constants/MetaMTAReceivedMailStatus.php
+++ b/src/applications/metamta/constants/MetaMTAReceivedMailStatus.php
@@ -1,42 +1,42 @@
<?php
final class MetaMTAReceivedMailStatus
extends Phobject {
const STATUS_DUPLICATE = 'err:duplicate';
const STATUS_FROM_PHABRICATOR = 'err:self';
const STATUS_NO_RECEIVERS = 'err:no-receivers';
- const STATUS_ABUNDANT_RECEIVERS = 'err:multiple-receivers';
const STATUS_UNKNOWN_SENDER = 'err:unknown-sender';
const STATUS_DISABLED_SENDER = 'err:disabled-sender';
const STATUS_NO_PUBLIC_MAIL = 'err:no-public-mail';
const STATUS_USER_MISMATCH = 'err:bad-user';
const STATUS_POLICY_PROBLEM = 'err:policy';
const STATUS_NO_SUCH_OBJECT = 'err:not-found';
const STATUS_HASH_MISMATCH = 'err:bad-hash';
const STATUS_UNHANDLED_EXCEPTION = 'err:exception';
const STATUS_EMPTY = 'err:empty';
const STATUS_EMPTY_IGNORED = 'err:empty-ignored';
+ const STATUS_RESERVED = 'err:reserved-recipient';
public static function getHumanReadableName($status) {
$map = array(
self::STATUS_DUPLICATE => pht('Duplicate Message'),
self::STATUS_FROM_PHABRICATOR => pht('Phabricator Mail'),
self::STATUS_NO_RECEIVERS => pht('No Receivers'),
- self::STATUS_ABUNDANT_RECEIVERS => pht('Multiple Receivers'),
self::STATUS_UNKNOWN_SENDER => pht('Unknown Sender'),
self::STATUS_DISABLED_SENDER => pht('Disabled Sender'),
self::STATUS_NO_PUBLIC_MAIL => pht('No Public Mail'),
self::STATUS_USER_MISMATCH => pht('User Mismatch'),
self::STATUS_POLICY_PROBLEM => pht('Policy Error'),
self::STATUS_NO_SUCH_OBJECT => pht('No Such Object'),
self::STATUS_HASH_MISMATCH => pht('Bad Address'),
self::STATUS_UNHANDLED_EXCEPTION => pht('Unhandled Exception'),
self::STATUS_EMPTY => pht('Empty Mail'),
self::STATUS_EMPTY_IGNORED => pht('Ignored Empty Mail'),
+ self::STATUS_RESERVED => pht('Reserved Recipient'),
);
return idx($map, $status, pht('Processing Exception'));
}
}
diff --git a/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php b/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php
index 9b3339783..d7d31ba25 100644
--- a/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php
+++ b/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php
@@ -1,450 +1,450 @@
<?php
final class PhabricatorMetaMTAMailViewController
extends PhabricatorMetaMTAController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$mail = id(new PhabricatorMetaMTAMailQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->executeOne();
if (!$mail) {
return new Aphront404Response();
}
if ($mail->hasSensitiveContent()) {
$title = pht('Content Redacted');
} else {
$title = $mail->getSubject();
}
$header = id(new PHUIHeaderView())
->setHeader($title)
->setUser($viewer)
->setPolicyObject($mail)
->setHeaderIcon('fa-envelope');
$status = $mail->getStatus();
$name = PhabricatorMailOutboundStatus::getStatusName($status);
$icon = PhabricatorMailOutboundStatus::getStatusIcon($status);
$color = PhabricatorMailOutboundStatus::getStatusColor($status);
$header->setStatus($icon, $color, $name);
if ($mail->getMustEncrypt()) {
Javelin::initBehavior('phabricator-tooltips');
$header->addTag(
id(new PHUITagView())
->setType(PHUITagView::TYPE_SHADE)
->setColor('blue')
->setName(pht('Must Encrypt'))
->setIcon('fa-shield blue')
->addSigil('has-tooltip')
->setMetadata(
array(
'tip' => pht(
'Message content can only be transmitted over secure '.
'channels.'),
)));
}
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb(pht('Mail %d', $mail->getID()))
->setBorder(true);
$tab_group = id(new PHUITabGroupView())
->addTab(
id(new PHUITabView())
->setName(pht('Message'))
->setKey('message')
->appendChild($this->buildMessageProperties($mail)))
->addTab(
id(new PHUITabView())
->setName(pht('Headers'))
->setKey('headers')
->appendChild($this->buildHeaderProperties($mail)))
->addTab(
id(new PHUITabView())
->setName(pht('Delivery'))
->setKey('delivery')
->appendChild($this->buildDeliveryProperties($mail)))
->addTab(
id(new PHUITabView())
->setName(pht('Metadata'))
->setKey('metadata')
->appendChild($this->buildMetadataProperties($mail)));
$header_view = id(new PHUIHeaderView())
->setHeader(pht('Mail'));
$object_phid = $mail->getRelatedPHID();
if ($object_phid) {
$handles = $viewer->loadHandles(array($object_phid));
$handle = $handles[$object_phid];
if ($handle->isComplete() && $handle->getURI()) {
$view_button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('View Object'))
->setIcon('fa-chevron-right')
->setHref($handle->getURI());
$header_view->addActionLink($view_button);
}
}
$object_box = id(new PHUIObjectBoxView())
->setHeader($header_view)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addTabGroup($tab_group);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter($object_box);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->setPageObjectPHIDs(array($mail->getPHID()))
->appendChild($view);
}
private function buildMessageProperties(PhabricatorMetaMTAMail $mail) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($mail);
if ($mail->getFrom()) {
$from_str = $viewer->renderHandle($mail->getFrom());
} else {
$from_str = pht('Sent by Phabricator');
}
$properties->addProperty(
pht('From'),
$from_str);
if ($mail->getToPHIDs()) {
$to_list = $viewer->renderHandleList($mail->getToPHIDs());
} else {
$to_list = pht('None');
}
$properties->addProperty(
pht('To'),
$to_list);
if ($mail->getCcPHIDs()) {
$cc_list = $viewer->renderHandleList($mail->getCcPHIDs());
} else {
$cc_list = pht('None');
}
$properties->addProperty(
pht('Cc'),
$cc_list);
$properties->addProperty(
pht('Sent'),
phabricator_datetime($mail->getDateCreated(), $viewer));
$properties->addSectionHeader(
pht('Message'),
PHUIPropertyListView::ICON_SUMMARY);
if ($mail->hasSensitiveContent()) {
$body = phutil_tag(
'em',
array(),
pht(
'The content of this mail is sensitive and it can not be '.
'viewed from the web UI.'));
} else {
$body = phutil_tag(
'div',
array(
'style' => 'white-space: pre-wrap',
),
$mail->getBody());
}
$properties->addTextContent($body);
$file_phids = $mail->getAttachmentFilePHIDs();
if ($file_phids) {
$properties->addProperty(
pht('Attached Files'),
$viewer->loadHandles($file_phids)->renderList());
}
return $properties;
}
private function buildHeaderProperties(PhabricatorMetaMTAMail $mail) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setUser($viewer)
->setStacked(true);
$headers = $mail->getDeliveredHeaders();
- if ($headers === null) {
- $headers = $mail->generateHeaders();
+ if (!$headers) {
+ $headers = array();
}
// Sort headers by name.
$headers = isort($headers, 0);
foreach ($headers as $header) {
list($key, $value) = $header;
$properties->addProperty($key, $value);
}
$encrypt_phids = $mail->getMustEncryptReasons();
if ($encrypt_phids) {
$properties->addProperty(
pht('Must Encrypt'),
$viewer->loadHandles($encrypt_phids)
->renderList());
}
return $properties;
}
private function buildDeliveryProperties(PhabricatorMetaMTAMail $mail) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setUser($viewer);
$actors = $mail->getDeliveredActors();
$reasons = null;
if (!$actors) {
if ($mail->getStatus() == PhabricatorMailOutboundStatus::STATUS_QUEUE) {
$delivery = $this->renderEmptyMessage(
pht(
'This message has not been delivered yet, so delivery information '.
'is not available.'));
} else {
$delivery = $this->renderEmptyMessage(
pht(
'This is an older message that predates recording delivery '.
'information, so none is available.'));
}
} else {
$actor = idx($actors, $viewer->getPHID());
if (!$actor) {
$delivery = phutil_tag(
'em',
array(),
pht('This message was not delivered to you.'));
} else {
$deliverable = $actor['deliverable'];
if ($deliverable) {
$delivery = pht('Delivered');
} else {
$delivery = pht('Voided');
}
$reasons = id(new PHUIStatusListView());
$reason_codes = $actor['reasons'];
if (!$reason_codes) {
$reason_codes = array(
PhabricatorMetaMTAActor::REASON_NONE,
);
}
$icon_yes = 'fa-check green';
$icon_no = 'fa-times red';
foreach ($reason_codes as $reason) {
$target = phutil_tag(
'strong',
array(),
PhabricatorMetaMTAActor::getReasonName($reason));
if (PhabricatorMetaMTAActor::isDeliveryReason($reason)) {
$icon = $icon_yes;
} else {
$icon = $icon_no;
}
$item = id(new PHUIStatusItemView())
->setIcon($icon)
->setTarget($target)
->setNote(PhabricatorMetaMTAActor::getReasonDescription($reason));
$reasons->addItem($item);
}
}
}
$properties->addProperty(pht('Delivery'), $delivery);
if ($reasons) {
$properties->addProperty(pht('Reasons'), $reasons);
$properties->addProperty(
null,
$this->renderEmptyMessage(
pht(
'Delivery reasons are listed from weakest to strongest.')));
}
$properties->addSectionHeader(
pht('Routing Rules'), 'fa-paper-plane-o');
$map = $mail->getDeliveredRoutingMap();
$routing_detail = null;
if ($map === null) {
if ($mail->getStatus() == PhabricatorMailOutboundStatus::STATUS_QUEUE) {
$routing_result = $this->renderEmptyMessage(
pht(
'This message has not been sent yet, so routing rules have '.
'not been computed.'));
} else {
$routing_result = $this->renderEmptyMessage(
pht(
'This is an older message which predates routing rules.'));
}
} else {
$rule = idx($map, $viewer->getPHID());
if ($rule === null) {
$rule = idx($map, 'default');
}
if ($rule === null) {
$routing_result = $this->renderEmptyMessage(
pht(
'No routing rules applied when delivering this message to you.'));
} else {
$rule_const = $rule['rule'];
$reason_phid = $rule['reason'];
switch ($rule_const) {
case PhabricatorMailRoutingRule::ROUTE_AS_NOTIFICATION:
$routing_result = pht(
'This message was routed as a notification because it '.
'matched %s.',
$viewer->renderHandle($reason_phid)->render());
break;
case PhabricatorMailRoutingRule::ROUTE_AS_MAIL:
$routing_result = pht(
'This message was routed as an email because it matched %s.',
$viewer->renderHandle($reason_phid)->render());
break;
default:
$routing_result = pht('Unknown routing rule "%s".', $rule_const);
break;
}
}
$routing_rules = $mail->getDeliveredRoutingRules();
if ($routing_rules) {
$rules = array();
foreach ($routing_rules as $rule) {
$phids = idx($rule, 'phids');
if ($phids === null) {
$rules[] = $rule;
} else if (in_array($viewer->getPHID(), $phids)) {
$rules[] = $rule;
}
}
// Reorder rules by strength.
foreach ($rules as $key => $rule) {
$const = $rule['routingRule'];
$phids = $rule['phids'];
if ($phids === null) {
$type = 'A';
} else {
$type = 'B';
}
$rules[$key]['strength'] = sprintf(
'~%s%08d',
$type,
PhabricatorMailRoutingRule::getRuleStrength($const));
}
$rules = isort($rules, 'strength');
$routing_detail = id(new PHUIStatusListView());
foreach ($rules as $rule) {
$const = $rule['routingRule'];
$phids = $rule['phids'];
$name = PhabricatorMailRoutingRule::getRuleName($const);
$icon = PhabricatorMailRoutingRule::getRuleIcon($const);
$color = PhabricatorMailRoutingRule::getRuleColor($const);
if ($phids === null) {
$kind = pht('Global');
} else {
$kind = pht('Personal');
}
$target = array($kind, ': ', $name);
$target = phutil_tag('strong', array(), $target);
$item = id(new PHUIStatusItemView())
->setTarget($target)
->setNote($viewer->renderHandle($rule['reasonPHID']))
->setIcon($icon, $color);
$routing_detail->addItem($item);
}
}
}
$properties->addProperty(pht('Effective Rule'), $routing_result);
if ($routing_detail !== null) {
$properties->addProperty(pht('All Matching Rules'), $routing_detail);
$properties->addProperty(
null,
$this->renderEmptyMessage(
pht(
'Matching rules are listed from weakest to strongest.')));
}
return $properties;
}
private function buildMetadataProperties(PhabricatorMetaMTAMail $mail) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setUser($viewer);
$properties->addProperty(pht('Message PHID'), $mail->getPHID());
$details = $mail->getMessage();
if (!strlen($details)) {
$details = phutil_tag('em', array(), pht('None'));
}
$properties->addProperty(pht('Status Details'), $details);
$actor_phid = $mail->getActorPHID();
if ($actor_phid) {
$actor_str = $viewer->renderHandle($actor_phid);
} else {
$actor_str = pht('Generated by Phabricator');
}
$properties->addProperty(pht('Actor'), $actor_str);
$related_phid = $mail->getRelatedPHID();
if ($related_phid) {
$related = $viewer->renderHandle($mail->getRelatedPHID());
} else {
$related = phutil_tag('em', array(), pht('None'));
}
$properties->addProperty(pht('Related Object'), $related);
return $properties;
}
private function renderEmptyMessage($message) {
return phutil_tag('em', array(), $message);
}
}
diff --git a/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php b/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php
index 91f656cf9..8de908e4e 100644
--- a/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php
+++ b/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php
@@ -1,110 +1,110 @@
<?php
final class PhabricatorMetaMTAMailgunReceiveController
extends PhabricatorMetaMTAController {
public function shouldRequireLogin() {
return false;
}
private function verifyMessage() {
$request = $this->getRequest();
$timestamp = $request->getStr('timestamp');
$token = $request->getStr('token');
$sig = $request->getStr('signature');
// An install may configure multiple Mailgun mailers, and we might receive
// inbound mail from any of them. Test the signature to see if it matches
// any configured Mailgun mailer.
$mailers = PhabricatorMetaMTAMail::newMailers(
array(
'inbound' => true,
'types' => array(
- PhabricatorMailImplementationMailgunAdapter::ADAPTERTYPE,
+ PhabricatorMailMailgunAdapter::ADAPTERTYPE,
),
));
foreach ($mailers as $mailer) {
$api_key = $mailer->getOption('api-key');
$hash = hash_hmac('sha256', $timestamp.$token, $api_key);
if (phutil_hashes_are_identical($sig, $hash)) {
return true;
}
}
return false;
}
public function handleRequest(AphrontRequest $request) {
// No CSRF for Mailgun.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
if (!$this->verifyMessage()) {
throw new Exception(
pht('Mail signature is not valid. Check your Mailgun API key.'));
}
$raw_headers = $request->getStr('message-headers');
$raw_dict = array();
if (strlen($raw_headers)) {
$raw_headers = phutil_json_decode($raw_headers);
foreach ($raw_headers as $raw_header) {
list($name, $value) = $raw_header;
$raw_dict[$name] = $value;
}
}
$headers = array(
'to' => $request->getStr('recipient'),
'from' => $request->getStr('from'),
'subject' => $request->getStr('subject'),
) + $raw_dict;
$received = new PhabricatorMetaMTAReceivedMail();
$received->setHeaders($headers);
$received->setBodies(array(
'text' => $request->getStr('stripped-text'),
'html' => $request->getStr('stripped-html'),
));
$file_phids = array();
foreach ($_FILES as $file_raw) {
try {
$file = PhabricatorFile::newFromPHPUpload(
$file_raw,
array(
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
));
$file_phids[] = $file->getPHID();
} catch (Exception $ex) {
phlog($ex);
}
}
$received->setAttachments($file_phids);
try {
$received->save();
$received->processReceivedMail();
} catch (Exception $ex) {
// We can get exceptions here in two cases.
// First, saving the message may throw if we have already received a
// message with the same Message ID. In this case, we're declining to
// process a duplicate message, so failing silently is correct.
// Second, processing the message may throw (for example, if it contains
// an invalid !command). This will generate an email as a side effect,
// so we don't need to explicitly handle the exception here.
// In these cases, we want to return HTTP 200. If we do not, MailGun will
// re-transmit the message later.
phlog($ex);
}
$response = new AphrontWebpageResponse();
$response->setContent(pht("Got it! Thanks, Mailgun!\n"));
return $response;
}
}
diff --git a/src/applications/metamta/controller/PhabricatorMetaMTAPostmarkReceiveController.php b/src/applications/metamta/controller/PhabricatorMetaMTAPostmarkReceiveController.php
index 550a1366f..a5cc53373 100644
--- a/src/applications/metamta/controller/PhabricatorMetaMTAPostmarkReceiveController.php
+++ b/src/applications/metamta/controller/PhabricatorMetaMTAPostmarkReceiveController.php
@@ -1,105 +1,105 @@
<?php
final class PhabricatorMetaMTAPostmarkReceiveController
extends PhabricatorMetaMTAController {
public function shouldRequireLogin() {
return false;
}
/**
* @phutil-external-symbol class PhabricatorStartup
*/
public function handleRequest(AphrontRequest $request) {
// Don't process requests if we don't have a configured Postmark adapter.
$mailers = PhabricatorMetaMTAMail::newMailers(
array(
'inbound' => true,
'types' => array(
- PhabricatorMailImplementationPostmarkAdapter::ADAPTERTYPE,
+ PhabricatorMailPostmarkAdapter::ADAPTERTYPE,
),
));
if (!$mailers) {
return new Aphront404Response();
}
$remote_address = $request->getRemoteAddress();
$any_remote_match = false;
foreach ($mailers as $mailer) {
$inbound_addresses = $mailer->getOption('inbound-addresses');
$cidr_list = PhutilCIDRList::newList($inbound_addresses);
if ($cidr_list->containsAddress($remote_address)) {
$any_remote_match = true;
break;
}
}
if (!$any_remote_match) {
return new Aphront400Response();
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$raw_input = PhabricatorStartup::getRawInput();
try {
$data = phutil_json_decode($raw_input);
} catch (Exception $ex) {
return new Aphront400Response();
}
$raw_headers = array();
$header_items = idx($data, 'Headers', array());
foreach ($header_items as $header_item) {
$name = idx($header_item, 'Name');
$value = idx($header_item, 'Value');
$raw_headers[$name] = $value;
}
$headers = array(
'to' => idx($data, 'To'),
'from' => idx($data, 'From'),
'cc' => idx($data, 'Cc'),
'subject' => idx($data, 'Subject'),
) + $raw_headers;
$received = id(new PhabricatorMetaMTAReceivedMail())
->setHeaders($headers)
->setBodies(
array(
'text' => idx($data, 'TextBody'),
'html' => idx($data, 'HtmlBody'),
));
$file_phids = array();
$attachments = idx($data, 'Attachments', array());
foreach ($attachments as $attachment) {
$file_data = idx($attachment, 'Content');
$file_data = base64_decode($file_data);
try {
$file = PhabricatorFile::newFromFileData(
$file_data,
array(
'name' => idx($attachment, 'Name'),
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
));
$file_phids[] = $file->getPHID();
} catch (Exception $ex) {
phlog($ex);
}
}
$received->setAttachments($file_phids);
try {
$received->save();
$received->processReceivedMail();
} catch (Exception $ex) {
phlog($ex);
}
return id(new AphrontWebpageResponse())
->setContent(pht("Got it! Thanks, Postmark!\n"));
}
}
diff --git a/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php b/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php
index 9ec32f60a..28ebdbe0c 100644
--- a/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php
+++ b/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php
@@ -1,73 +1,73 @@
<?php
final class PhabricatorMetaMTASendGridReceiveController
extends PhabricatorMetaMTAController {
public function shouldRequireLogin() {
return false;
}
public function handleRequest(AphrontRequest $request) {
// SendGrid doesn't sign payloads so we can't be sure that SendGrid
// actually sent this request, but require a configured SendGrid mailer
// before we activate this endpoint.
$mailers = PhabricatorMetaMTAMail::newMailers(
array(
'inbound' => true,
'types' => array(
- PhabricatorMailImplementationSendGridAdapter::ADAPTERTYPE,
+ PhabricatorMailSendGridAdapter::ADAPTERTYPE,
),
));
if (!$mailers) {
return new Aphront404Response();
}
// No CSRF for SendGrid.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$user = $request->getUser();
$raw_headers = $request->getStr('headers');
$raw_headers = explode("\n", rtrim($raw_headers));
$raw_dict = array();
foreach (array_filter($raw_headers) as $header) {
list($name, $value) = explode(':', $header, 2);
$raw_dict[$name] = ltrim($value);
}
$headers = array(
'to' => $request->getStr('to'),
'from' => $request->getStr('from'),
'subject' => $request->getStr('subject'),
) + $raw_dict;
$received = new PhabricatorMetaMTAReceivedMail();
$received->setHeaders($headers);
$received->setBodies(array(
'text' => $request->getStr('text'),
'html' => $request->getStr('from'),
));
$file_phids = array();
foreach ($_FILES as $file_raw) {
try {
$file = PhabricatorFile::newFromPHPUpload(
$file_raw,
array(
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
));
$file_phids[] = $file->getPHID();
} catch (Exception $ex) {
phlog($ex);
}
}
$received->setAttachments($file_phids);
$received->save();
$received->processReceivedMail();
$response = new AphrontWebpageResponse();
$response->setContent(pht('Got it! Thanks, SendGrid!')."\n");
return $response;
}
}
diff --git a/src/applications/metamta/editor/PhabricatorMetaMTAApplicationEmailEditor.php b/src/applications/metamta/editor/PhabricatorMetaMTAApplicationEmailEditor.php
index 2cbd164a8..843e65303 100644
--- a/src/applications/metamta/editor/PhabricatorMetaMTAApplicationEmailEditor.php
+++ b/src/applications/metamta/editor/PhabricatorMetaMTAApplicationEmailEditor.php
@@ -1,145 +1,169 @@
<?php
final class PhabricatorMetaMTAApplicationEmailEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return pht('PhabricatorMetaMTAApplication');
}
public function getEditorObjectsDescription() {
return pht('Application Emails');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorMetaMTAApplicationEmailTransaction::TYPE_ADDRESS;
$types[] = PhabricatorMetaMTAApplicationEmailTransaction::TYPE_CONFIG;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorMetaMTAApplicationEmailTransaction::TYPE_ADDRESS:
return $object->getAddress();
case PhabricatorMetaMTAApplicationEmailTransaction::TYPE_CONFIG:
$key = $xaction->getMetadataValue(
PhabricatorMetaMTAApplicationEmailTransaction::KEY_CONFIG);
return $object->getConfigValue($key);
}
return parent::getCustomTransactionOldValue($object, $xaction);
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorMetaMTAApplicationEmailTransaction::TYPE_ADDRESS:
case PhabricatorMetaMTAApplicationEmailTransaction::TYPE_CONFIG:
return $xaction->getNewValue();
}
return parent::getCustomTransactionNewValue($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$new = $xaction->getNewValue();
switch ($xaction->getTransactionType()) {
case PhabricatorMetaMTAApplicationEmailTransaction::TYPE_ADDRESS:
$object->setAddress($new);
return;
case PhabricatorMetaMTAApplicationEmailTransaction::TYPE_CONFIG:
$key = $xaction->getMetadataValue(
PhabricatorMetaMTAApplicationEmailTransaction::KEY_CONFIG);
$object->setConfigValue($key, $new);
return;
}
return parent::applyCustomInternalTransaction($object, $xaction);
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorMetaMTAApplicationEmailTransaction::TYPE_ADDRESS:
case PhabricatorMetaMTAApplicationEmailTransaction::TYPE_CONFIG:
return;
}
return parent::applyCustomExternalTransaction($object, $xaction);
}
protected function validateTransaction(
PhabricatorLiskDAO $object,
$type,
array $xactions) {
$errors = parent::validateTransaction($object, $type, $xactions);
switch ($type) {
case PhabricatorMetaMTAApplicationEmailTransaction::TYPE_ADDRESS:
foreach ($xactions as $xaction) {
$email = $xaction->getNewValue();
if (!strlen($email)) {
// We'll deal with this below.
continue;
}
if (!PhabricatorUserEmail::isValidAddress($email)) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
pht('Email address is not formatted properly.'));
+ continue;
+ }
+
+ $address = new PhutilEmailAddress($email);
+ if (PhabricatorMailUtil::isReservedAddress($address)) {
+ $errors[] = new PhabricatorApplicationTransactionValidationError(
+ $type,
+ pht('Reserved'),
+ pht(
+ 'This email address is reserved. Choose a different '.
+ 'address.'));
+ continue;
+ }
+
+ // See T13234. Prevent use of user email addresses as application
+ // email addresses.
+ if (PhabricatorMailUtil::isUserAddress($address)) {
+ $errors[] = new PhabricatorApplicationTransactionValidationError(
+ $type,
+ pht('In Use'),
+ pht(
+ 'This email address is already in use by a user. Choose '.
+ 'a different address.'));
+ continue;
}
}
$missing = $this->validateIsEmptyTextField(
$object->getAddress(),
$xactions);
if ($missing) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Required'),
pht('You must provide an email address.'),
nonempty(last($xactions), null));
$error->setIsMissingFieldError(true);
$errors[] = $error;
}
break;
}
return $errors;
}
protected function didCatchDuplicateKeyException(
PhabricatorLiskDAO $object,
array $xactions,
Exception $ex) {
$errors = array();
$errors[] = new PhabricatorApplicationTransactionValidationError(
PhabricatorMetaMTAApplicationEmailTransaction::TYPE_ADDRESS,
pht('Duplicate'),
pht('This email address is already in use.'),
null);
throw new PhabricatorApplicationTransactionValidationException($errors);
}
}
diff --git a/src/applications/metamta/engine/PhabricatorMailEmailEngine.php b/src/applications/metamta/engine/PhabricatorMailEmailEngine.php
new file mode 100644
index 000000000..ef7b92a7d
--- /dev/null
+++ b/src/applications/metamta/engine/PhabricatorMailEmailEngine.php
@@ -0,0 +1,648 @@
+<?php
+
+final class PhabricatorMailEmailEngine
+ extends PhabricatorMailMessageEngine {
+
+ public function newMessage() {
+ $mailer = $this->getMailer();
+ $mail = $this->getMail();
+
+ $message = new PhabricatorMailEmailMessage();
+
+ $from_address = $this->newFromEmailAddress();
+ $message->setFromAddress($from_address);
+
+ $reply_address = $this->newReplyToEmailAddress();
+ if ($reply_address) {
+ $message->setReplyToAddress($reply_address);
+ }
+
+ $to_addresses = $this->newToEmailAddresses();
+ $cc_addresses = $this->newCCEmailAddresses();
+
+ if (!$to_addresses && !$cc_addresses) {
+ $mail->setMessage(
+ pht(
+ 'Message has no valid recipients: all To/CC are disabled, '.
+ 'invalid, or configured not to receive this mail.'));
+ return null;
+ }
+
+ // If this email describes a mail processing error, we rate limit outbound
+ // messages to each individual address. This prevents messes where
+ // something is stuck in a loop or dumps a ton of messages on us suddenly.
+ if ($mail->getIsErrorEmail()) {
+ $all_recipients = array();
+ foreach ($to_addresses as $to_address) {
+ $all_recipients[] = $to_address->getAddress();
+ }
+ foreach ($cc_addresses as $cc_address) {
+ $all_recipients[] = $cc_address->getAddress();
+ }
+ if ($this->shouldRateLimitMail($all_recipients)) {
+ $mail->setMessage(
+ pht(
+ 'This is an error email, but one or more recipients have '.
+ 'exceeded the error email rate limit. Declining to deliver '.
+ 'message.'));
+ return null;
+ }
+ }
+
+ // Some mailers require a valid "To:" in order to deliver mail. If we
+ // don't have any "To:", try to fill it in with a placeholder "To:".
+ // If that also fails, move the "Cc:" line to "To:".
+ if (!$to_addresses) {
+ $void_address = $this->newVoidEmailAddress();
+ $to_addresses = array($void_address);
+ }
+
+ $to_addresses = $this->getUniqueEmailAddresses($to_addresses);
+ $cc_addresses = $this->getUniqueEmailAddresses(
+ $cc_addresses,
+ $to_addresses);
+
+ $message->setToAddresses($to_addresses);
+ $message->setCCAddresses($cc_addresses);
+
+ $attachments = $this->newEmailAttachments();
+ $message->setAttachments($attachments);
+
+ $subject = $this->newEmailSubject();
+ $message->setSubject($subject);
+
+ $headers = $this->newEmailHeaders();
+ foreach ($this->newEmailThreadingHeaders($mailer) as $threading_header) {
+ $headers[] = $threading_header;
+ }
+
+ $stamps = $mail->getMailStamps();
+ if ($stamps) {
+ $headers[] = $this->newEmailHeader(
+ 'X-Phabricator-Stamps',
+ implode(' ', $stamps));
+ }
+
+ $must_encrypt = $mail->getMustEncrypt();
+
+ $raw_body = $mail->getBody();
+ $body = $raw_body;
+ if ($must_encrypt) {
+ $parts = array();
+
+ $encrypt_uri = $mail->getMustEncryptURI();
+ if (!strlen($encrypt_uri)) {
+ $encrypt_phid = $mail->getRelatedPHID();
+ if ($encrypt_phid) {
+ $encrypt_uri = urisprintf(
+ '/object/%s/',
+ $encrypt_phid);
+ }
+ }
+
+ if (strlen($encrypt_uri)) {
+ $parts[] = pht(
+ 'This secure message is notifying you of a change to this object:');
+ $parts[] = PhabricatorEnv::getProductionURI($encrypt_uri);
+ }
+
+ $parts[] = pht(
+ 'The content for this message can only be transmitted over a '.
+ 'secure channel. To view the message content, follow this '.
+ 'link:');
+
+ $parts[] = PhabricatorEnv::getProductionURI($mail->getURI());
+
+ $body = implode("\n\n", $parts);
+ } else {
+ $body = $raw_body;
+ }
+
+ $body_limit = PhabricatorEnv::getEnvConfig('metamta.email-body-limit');
+ if (strlen($body) > $body_limit) {
+ $body = id(new PhutilUTF8StringTruncator())
+ ->setMaximumBytes($body_limit)
+ ->truncateString($body);
+ $body .= "\n";
+ $body .= pht('(This email was truncated at %d bytes.)', $body_limit);
+ }
+ $message->setTextBody($body);
+ $body_limit -= strlen($body);
+
+ // If we sent a different message body than we were asked to, record
+ // what we actually sent to make debugging and diagnostics easier.
+ if ($body !== $raw_body) {
+ $mail->setDeliveredBody($body);
+ }
+
+ if ($must_encrypt) {
+ $send_html = false;
+ } else {
+ $send_html = $this->shouldSendHTML();
+ }
+
+ if ($send_html) {
+ $html_body = $mail->getHTMLBody();
+ if (strlen($html_body)) {
+ // NOTE: We just drop the entire HTML body if it won't fit. Safely
+ // truncating HTML is hard, and we already have the text body to fall
+ // back to.
+ if (strlen($html_body) <= $body_limit) {
+ $message->setHTMLBody($html_body);
+ $body_limit -= strlen($html_body);
+ }
+ }
+ }
+
+ // Pass the headers to the mailer, then save the state so we can show
+ // them in the web UI. If the mail must be encrypted, we remove headers
+ // which are not on a strict whitelist to avoid disclosing information.
+ $filtered_headers = $this->filterHeaders($headers, $must_encrypt);
+ $message->setHeaders($filtered_headers);
+
+ $mail->setUnfilteredHeaders($headers);
+ $mail->setDeliveredHeaders($headers);
+
+ if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
+ $mail->setMessage(
+ pht(
+ 'Phabricator is running in silent mode. See `%s` '.
+ 'in the configuration to change this setting.',
+ 'phabricator.silent'));
+
+ return null;
+ }
+
+ return $message;
+ }
+
+/* -( Message Components )------------------------------------------------- */
+
+ private function newFromEmailAddress() {
+ $from_address = $this->newDefaultEmailAddress();
+ $mail = $this->getMail();
+
+ // If the mail content must be encrypted, always disguise the sender.
+ $must_encrypt = $mail->getMustEncrypt();
+ if ($must_encrypt) {
+ return $from_address;
+ }
+
+ // If we have a raw "From" address, use that.
+ $raw_from = $mail->getRawFrom();
+ if ($raw_from) {
+ list($from_email, $from_name) = $raw_from;
+ return $this->newEmailAddress($from_email, $from_name);
+ }
+
+ // Otherwise, use as much of the information for any sending entity as
+ // we can.
+ $from_phid = $mail->getFrom();
+
+ $actor = $this->getActor($from_phid);
+ if ($actor) {
+ $actor_email = $actor->getEmailAddress();
+ $actor_name = $actor->getName();
+ } else {
+ $actor_email = null;
+ $actor_name = null;
+ }
+
+ $send_as_user = PhabricatorEnv::getEnvConfig('metamta.can-send-as-user');
+ if ($send_as_user) {
+ if ($actor_email !== null) {
+ $from_address->setAddress($actor_email);
+ }
+ }
+
+ if ($actor_name !== null) {
+ $from_address->setDisplayName($actor_name);
+ }
+
+ return $from_address;
+ }
+
+ private function newReplyToEmailAddress() {
+ $mail = $this->getMail();
+
+ $reply_raw = $mail->getReplyTo();
+ if (!strlen($reply_raw)) {
+ return null;
+ }
+
+ $reply_address = new PhutilEmailAddress($reply_raw);
+
+ // If we have a sending object, change the display name.
+ $from_phid = $mail->getFrom();
+ $actor = $this->getActor($from_phid);
+ if ($actor) {
+ $reply_address->setDisplayName($actor->getName());
+ }
+
+ // If we don't have a display name, fill in a default.
+ if (!strlen($reply_address->getDisplayName())) {
+ $reply_address->setDisplayName(pht('Phabricator'));
+ }
+
+ return $reply_address;
+ }
+
+ private function newToEmailAddresses() {
+ $mail = $this->getMail();
+
+ $phids = $mail->getToPHIDs();
+ $addresses = $this->newEmailAddressesFromActorPHIDs($phids);
+
+ foreach ($mail->getRawToAddresses() as $raw_address) {
+ $addresses[] = new PhutilEmailAddress($raw_address);
+ }
+
+ return $addresses;
+ }
+
+ private function newCCEmailAddresses() {
+ $mail = $this->getMail();
+ $phids = $mail->getCcPHIDs();
+ return $this->newEmailAddressesFromActorPHIDs($phids);
+ }
+
+ private function newEmailAddressesFromActorPHIDs(array $phids) {
+ $mail = $this->getMail();
+ $phids = $mail->expandRecipients($phids);
+
+ $addresses = array();
+ foreach ($phids as $phid) {
+ $actor = $this->getActor($phid);
+ if (!$actor) {
+ continue;
+ }
+
+ if (!$actor->isDeliverable()) {
+ continue;
+ }
+
+ $addresses[] = new PhutilEmailAddress($actor->getEmailAddress());
+ }
+
+ return $addresses;
+ }
+
+ private function newEmailSubject() {
+ $mail = $this->getMail();
+
+ $is_threaded = (bool)$mail->getThreadID();
+ $must_encrypt = $mail->getMustEncrypt();
+
+ $subject = array();
+
+ if ($is_threaded) {
+ if ($this->shouldAddRePrefix()) {
+ $subject[] = 'Re:';
+ }
+ }
+
+ $subject[] = trim($mail->getSubjectPrefix());
+
+ // If mail content must be encrypted, we replace the subject with
+ // a generic one.
+ if ($must_encrypt) {
+ $encrypt_subject = $mail->getMustEncryptSubject();
+ if (!strlen($encrypt_subject)) {
+ $encrypt_subject = pht('Object Updated');
+ }
+ $subject[] = $encrypt_subject;
+ } else {
+ $vary_prefix = $mail->getVarySubjectPrefix();
+ if (strlen($vary_prefix)) {
+ if ($this->shouldVarySubject()) {
+ $subject[] = $vary_prefix;
+ }
+ }
+
+ $subject[] = $mail->getSubject();
+ }
+
+ foreach ($subject as $key => $part) {
+ if (!strlen($part)) {
+ unset($subject[$key]);
+ }
+ }
+
+ $subject = implode(' ', $subject);
+ return $subject;
+ }
+
+ private function newEmailHeaders() {
+ $mail = $this->getMail();
+
+ $headers = array();
+
+ $headers[] = $this->newEmailHeader(
+ 'X-Phabricator-Sent-This-Message',
+ 'Yes');
+ $headers[] = $this->newEmailHeader(
+ 'X-Mail-Transport-Agent',
+ 'MetaMTA');
+
+ // Some clients respect this to suppress OOF and other auto-responses.
+ $headers[] = $this->newEmailHeader(
+ 'X-Auto-Response-Suppress',
+ 'All');
+
+ $mailtags = $mail->getMailTags();
+ if ($mailtags) {
+ $tag_header = array();
+ foreach ($mailtags as $mailtag) {
+ $tag_header[] = '<'.$mailtag.'>';
+ }
+ $tag_header = implode(', ', $tag_header);
+ $headers[] = $this->newEmailHeader(
+ 'X-Phabricator-Mail-Tags',
+ $tag_header);
+ }
+
+ $value = $mail->getHeaders();
+ foreach ($value as $pair) {
+ list($header_key, $header_value) = $pair;
+
+ // NOTE: If we have \n in a header, SES rejects the email.
+ $header_value = str_replace("\n", ' ', $header_value);
+ $headers[] = $this->newEmailHeader($header_key, $header_value);
+ }
+
+ $is_bulk = $mail->getIsBulk();
+ if ($is_bulk) {
+ $headers[] = $this->newEmailHeader('Precedence', 'bulk');
+ }
+
+ if ($mail->getMustEncrypt()) {
+ $headers[] = $this->newEmailHeader('X-Phabricator-Must-Encrypt', 'Yes');
+ }
+
+ $related_phid = $mail->getRelatedPHID();
+ if ($related_phid) {
+ $headers[] = $this->newEmailHeader('Thread-Topic', $related_phid);
+ }
+
+ $headers[] = $this->newEmailHeader(
+ 'X-Phabricator-Mail-ID',
+ $mail->getID());
+
+ $unique = Filesystem::readRandomCharacters(16);
+ $headers[] = $this->newEmailHeader(
+ 'X-Phabricator-Send-Attempt',
+ $unique);
+
+ return $headers;
+ }
+
+ private function newEmailThreadingHeaders() {
+ $mailer = $this->getMailer();
+ $mail = $this->getMail();
+
+ $headers = array();
+
+ $thread_id = $mail->getThreadID();
+ if (!strlen($thread_id)) {
+ return $headers;
+ }
+
+ $is_first = $mail->getIsFirstMessage();
+
+ // NOTE: Gmail freaks out about In-Reply-To and References which aren't in
+ // the form "<string@domain.tld>"; this is also required by RFC 2822,
+ // although some clients are more liberal in what they accept.
+ $domain = $this->newMailDomain();
+ $thread_id = '<'.$thread_id.'@'.$domain.'>';
+
+ if ($is_first && $mailer->supportsMessageIDHeader()) {
+ $headers[] = $this->newEmailHeader('Message-ID', $thread_id);
+ } else {
+ $in_reply_to = $thread_id;
+ $references = array($thread_id);
+ $parent_id = $mail->getParentMessageID();
+ if ($parent_id) {
+ $in_reply_to = $parent_id;
+ // By RFC 2822, the most immediate parent should appear last
+ // in the "References" header, so this order is intentional.
+ $references[] = $parent_id;
+ }
+ $references = implode(' ', $references);
+ $headers[] = $this->newEmailHeader('In-Reply-To', $in_reply_to);
+ $headers[] = $this->newEmailHeader('References', $references);
+ }
+ $thread_index = $this->generateThreadIndex($thread_id, $is_first);
+ $headers[] = $this->newEmailHeader('Thread-Index', $thread_index);
+
+ return $headers;
+ }
+
+ private function newEmailAttachments() {
+ $mail = $this->getMail();
+
+ // If the mail content must be encrypted, don't add attachments.
+ $must_encrypt = $mail->getMustEncrypt();
+ if ($must_encrypt) {
+ return array();
+ }
+
+ return $mail->getAttachments();
+ }
+
+/* -( Preferences )-------------------------------------------------------- */
+
+ private function shouldAddRePrefix() {
+ $preferences = $this->getPreferences();
+
+ $value = $preferences->getSettingValue(
+ PhabricatorEmailRePrefixSetting::SETTINGKEY);
+
+ return ($value == PhabricatorEmailRePrefixSetting::VALUE_RE_PREFIX);
+ }
+
+ private function shouldVarySubject() {
+ $preferences = $this->getPreferences();
+
+ $value = $preferences->getSettingValue(
+ PhabricatorEmailVarySubjectsSetting::SETTINGKEY);
+
+ return ($value == PhabricatorEmailVarySubjectsSetting::VALUE_VARY_SUBJECTS);
+ }
+
+ private function shouldSendHTML() {
+ $preferences = $this->getPreferences();
+
+ $value = $preferences->getSettingValue(
+ PhabricatorEmailFormatSetting::SETTINGKEY);
+
+ return ($value == PhabricatorEmailFormatSetting::VALUE_HTML_EMAIL);
+ }
+
+
+/* -( Utilities )---------------------------------------------------------- */
+
+ private function newEmailHeader($name, $value) {
+ return id(new PhabricatorMailHeader())
+ ->setName($name)
+ ->setValue($value);
+ }
+
+ private function newEmailAddress($address, $name = null) {
+ $object = id(new PhutilEmailAddress())
+ ->setAddress($address);
+
+ if (strlen($name)) {
+ $object->setDisplayName($name);
+ }
+
+ return $object;
+ }
+
+ public function newDefaultEmailAddress() {
+ $raw_address = PhabricatorEnv::getEnvConfig('metamta.default-address');
+
+ if (!strlen($raw_address)) {
+ $domain = $this->newMailDomain();
+ $raw_address = "noreply@{$domain}";
+ }
+
+ $address = new PhutilEmailAddress($raw_address);
+
+ if (!strlen($address->getDisplayName())) {
+ $address->setDisplayName(pht('Phabricator'));
+ }
+
+ return $address;
+ }
+
+ public function newVoidEmailAddress() {
+ return $this->newDefaultEmailAddress();
+ }
+
+ private function newMailDomain() {
+ $domain = PhabricatorEnv::getEnvConfig('metamta.reply-handler-domain');
+ if (strlen($domain)) {
+ return $domain;
+ }
+
+ $install_uri = PhabricatorEnv::getURI('/');
+ $install_uri = new PhutilURI($install_uri);
+
+ return $install_uri->getDomain();
+ }
+
+ private function filterHeaders(array $headers, $must_encrypt) {
+ assert_instances_of($headers, 'PhabricatorMailHeader');
+
+ if (!$must_encrypt) {
+ return $headers;
+ }
+
+ $whitelist = array(
+ 'In-Reply-To',
+ 'Message-ID',
+ 'Precedence',
+ 'References',
+ 'Thread-Index',
+ 'Thread-Topic',
+
+ 'X-Mail-Transport-Agent',
+ 'X-Auto-Response-Suppress',
+
+ 'X-Phabricator-Sent-This-Message',
+ 'X-Phabricator-Must-Encrypt',
+ 'X-Phabricator-Mail-ID',
+ 'X-Phabricator-Send-Attempt',
+ );
+
+ // NOTE: The major header we want to drop is "X-Phabricator-Mail-Tags".
+ // This header contains a significant amount of meaningful information
+ // about the object.
+
+ $whitelist_map = array();
+ foreach ($whitelist as $term) {
+ $whitelist_map[phutil_utf8_strtolower($term)] = true;
+ }
+
+ foreach ($headers as $key => $header) {
+ $name = $header->getName();
+ $name = phutil_utf8_strtolower($name);
+
+ if (!isset($whitelist_map[$name])) {
+ unset($headers[$key]);
+ }
+ }
+
+ return $headers;
+ }
+
+ private function getUniqueEmailAddresses(
+ array $addresses,
+ array $exclude = array()) {
+ assert_instances_of($addresses, 'PhutilEmailAddress');
+ assert_instances_of($exclude, 'PhutilEmailAddress');
+
+ $seen = array();
+
+ foreach ($exclude as $address) {
+ $seen[$address->getAddress()] = true;
+ }
+
+ foreach ($addresses as $key => $address) {
+ $raw_address = $address->getAddress();
+
+ if (isset($seen[$raw_address])) {
+ unset($addresses[$key]);
+ continue;
+ }
+
+ $seen[$raw_address] = true;
+ }
+
+ return array_values($addresses);
+ }
+
+ private function generateThreadIndex($seed, $is_first_mail) {
+ // When threading, Outlook ignores the 'References' and 'In-Reply-To'
+ // headers that most clients use. Instead, it uses a custom 'Thread-Index'
+ // header. The format of this header is something like this (from
+ // camel-exchange-folder.c in Evolution Exchange):
+
+ /* A new post to a folder gets a 27-byte-long thread index. (The value
+ * is apparently unique but meaningless.) Each reply to a post gets a
+ * 32-byte-long thread index whose first 27 bytes are the same as the
+ * parent's thread index. Each reply to any of those gets a
+ * 37-byte-long thread index, etc. The Thread-Index header contains a
+ * base64 representation of this value.
+ */
+
+ // The specific implementation uses a 27-byte header for the first email
+ // a recipient receives, and a random 5-byte suffix (32 bytes total)
+ // thereafter. This means that all the replies are (incorrectly) siblings,
+ // but it would be very difficult to keep track of the entire tree and this
+ // gets us reasonable client behavior.
+
+ $base = substr(md5($seed), 0, 27);
+ if (!$is_first_mail) {
+ // Not totally sure, but it seems like outlook orders replies by
+ // thread-index rather than timestamp, so to get these to show up in the
+ // right order we use the time as the last 4 bytes.
+ $base .= ' '.pack('N', time());
+ }
+
+ return base64_encode($base);
+ }
+
+ private function shouldRateLimitMail(array $all_recipients) {
+ try {
+ PhabricatorSystemActionEngine::willTakeAction(
+ $all_recipients,
+ new PhabricatorMetaMTAErrorMailAction(),
+ 1);
+ return false;
+ } catch (PhabricatorSystemActionRateLimitException $ex) {
+ return true;
+ }
+ }
+
+}
diff --git a/src/applications/metamta/engine/PhabricatorMailMessageEngine.php b/src/applications/metamta/engine/PhabricatorMailMessageEngine.php
new file mode 100644
index 000000000..c65346bf5
--- /dev/null
+++ b/src/applications/metamta/engine/PhabricatorMailMessageEngine.php
@@ -0,0 +1,54 @@
+<?php
+
+abstract class PhabricatorMailMessageEngine
+ extends Phobject {
+
+ private $mailer;
+ private $mail;
+ private $actors = array();
+ private $preferences;
+
+ final public function setMailer(PhabricatorMailAdapter $mailer) {
+
+ $this->mailer = $mailer;
+ return $this;
+ }
+
+ final public function getMailer() {
+ return $this->mailer;
+ }
+
+ final public function setMail(PhabricatorMetaMTAMail $mail) {
+ $this->mail = $mail;
+ return $this;
+ }
+
+ final public function getMail() {
+ return $this->mail;
+ }
+
+ final public function setActors(array $actors) {
+ assert_instances_of($actors, 'PhabricatorMetaMTAActor');
+ $this->actors = $actors;
+ return $this;
+ }
+
+ final public function getActors() {
+ return $this->actors;
+ }
+
+ final public function getActor($phid) {
+ return idx($this->actors, $phid);
+ }
+
+ final public function setPreferences(
+ PhabricatorUserPreferences $preferences) {
+ $this->preferences = $preferences;
+ return $this;
+ }
+
+ final public function getPreferences() {
+ return $this->preferences;
+ }
+
+}
diff --git a/src/applications/metamta/engine/PhabricatorMailSMSEngine.php b/src/applications/metamta/engine/PhabricatorMailSMSEngine.php
new file mode 100644
index 000000000..9f5c2fef3
--- /dev/null
+++ b/src/applications/metamta/engine/PhabricatorMailSMSEngine.php
@@ -0,0 +1,75 @@
+<?php
+
+final class PhabricatorMailSMSEngine
+ extends PhabricatorMailMessageEngine {
+
+ public function newMessage() {
+ $mailer = $this->getMailer();
+ $mail = $this->getMail();
+
+ $message = new PhabricatorMailSMSMessage();
+
+ $phids = $mail->getToPHIDs();
+ if (!$phids) {
+ $mail->setMessage(pht('Message has no "To" recipient.'));
+ return null;
+ }
+
+ if (count($phids) > 1) {
+ $mail->setMessage(pht('Message has more than one "To" recipient.'));
+ return null;
+ }
+
+ $phid = head($phids);
+
+ $actor = $this->getActor($phid);
+ if (!$actor) {
+ $mail->setMessage(pht('Message recipient has no mailable actor.'));
+ return null;
+ }
+
+ if (!$actor->isDeliverable()) {
+ $mail->setMessage(pht('Message recipient is not deliverable.'));
+ return null;
+ }
+
+ $omnipotent = PhabricatorUser::getOmnipotentUser();
+
+ $contact_numbers = id(new PhabricatorAuthContactNumberQuery())
+ ->setViewer($omnipotent)
+ ->withObjectPHIDs(array($phid))
+ ->withStatuses(
+ array(
+ PhabricatorAuthContactNumber::STATUS_ACTIVE,
+ ))
+ ->withIsPrimary(true)
+ ->execute();
+
+ if (!$contact_numbers) {
+ $mail->setMessage(
+ pht('Message recipient has no primary contact number.'));
+ return null;
+ }
+
+ // The database does not strictly guarantee that only one number is
+ // primary, so make sure no one has monkeyed with stuff.
+ if (count($contact_numbers) > 1) {
+ $mail->setMessage(
+ pht('Message recipient has more than one primary contact number.'));
+ return null;
+ }
+
+ $contact_number = head($contact_numbers);
+ $contact_number = $contact_number->getContactNumber();
+ $to_number = new PhabricatorPhoneNumber($contact_number);
+ $message->setToNumber($to_number);
+
+ $body = $mail->getBody();
+ if ($body !== null) {
+ $message->setTextBody($body);
+ }
+
+ return $message;
+ }
+
+}
diff --git a/src/applications/metamta/future/PhabricatorAmazonSNSFuture.php b/src/applications/metamta/future/PhabricatorAmazonSNSFuture.php
new file mode 100644
index 000000000..3be236eee
--- /dev/null
+++ b/src/applications/metamta/future/PhabricatorAmazonSNSFuture.php
@@ -0,0 +1,41 @@
+<?php
+
+final class PhabricatorAmazonSNSFuture extends PhutilAWSFuture {
+ private $parameters = array();
+ private $timeout;
+
+ public function setParameters($parameters) {
+ $this->parameters = $parameters;
+ return $this;
+ }
+
+ protected function getParameters() {
+ return $this->parameters;
+ }
+
+ public function getServiceName() {
+ return 'sns';
+ }
+
+ public function setTimeout($timeout) {
+ $this->timeout = $timeout;
+ return $this;
+ }
+
+ public function getTimeout() {
+ return $this->timeout;
+ }
+
+ protected function getProxiedFuture() {
+ $future = parent::getProxiedFuture();
+
+ $timeout = $this->getTimeout();
+ if ($timeout) {
+ $future->setTimeout($timeout);
+ }
+
+ return $future;
+
+ }
+
+}
diff --git a/src/applications/metamta/future/PhabricatorTwilioFuture.php b/src/applications/metamta/future/PhabricatorTwilioFuture.php
new file mode 100644
index 000000000..8dc70329f
--- /dev/null
+++ b/src/applications/metamta/future/PhabricatorTwilioFuture.php
@@ -0,0 +1,100 @@
+<?php
+
+final class PhabricatorTwilioFuture extends FutureProxy {
+
+ private $future;
+ private $accountSID;
+ private $authToken;
+ private $method;
+ private $parameters;
+ private $timeout;
+
+ public function __construct() {
+ parent::__construct(null);
+ }
+
+ public function setAccountSID($account_sid) {
+ $this->accountSID = $account_sid;
+ return $this;
+ }
+
+ public function setAuthToken(PhutilOpaqueEnvelope $token) {
+ $this->authToken = $token;
+ return $this;
+ }
+
+ public function setMethod($method, array $parameters) {
+ $this->method = $method;
+ $this->parameters = $parameters;
+ return $this;
+ }
+
+ public function setTimeout($timeout) {
+ $this->timeout = $timeout;
+ return $this;
+ }
+
+ public function getTimeout() {
+ return $this->timeout;
+ }
+
+ protected function getProxiedFuture() {
+ if (!$this->future) {
+ if ($this->accountSID === null) {
+ throw new PhutilInvalidStateException('setAccountSID');
+ }
+
+ if ($this->authToken === null) {
+ throw new PhutilInvalidStateException('setAuthToken');
+ }
+
+ if ($this->method === null || $this->parameters === null) {
+ throw new PhutilInvalidStateException('setMethod');
+ }
+
+ $path = urisprintf(
+ '/%s/Accounts/%s/%s',
+ '2010-04-01',
+ $this->accountSID,
+ $this->method);
+
+ $uri = id(new PhutilURI('https://api.twilio.com/'))
+ ->setPath($path);
+
+ $data = $this->parameters;
+
+ $future = id(new HTTPSFuture($uri, $data))
+ ->setHTTPBasicAuthCredentials($this->accountSID, $this->authToken)
+ ->setMethod('POST')
+ ->addHeader('Accept', 'application/json');
+
+ $timeout = $this->getTimeout();
+ if ($timeout) {
+ $future->setTimeout($timeout);
+ }
+
+ $this->future = $future;
+ }
+
+ return $this->future;
+ }
+
+ protected function didReceiveResult($result) {
+ list($status, $body, $headers) = $result;
+
+ if ($status->isError()) {
+ throw $status;
+ }
+
+ try {
+ $data = phutil_json_decode($body);
+ } catch (PhutilJSONParserException $ex) {
+ throw new PhutilProxyException(
+ pht('Expected JSON response from Twilio.'),
+ $ex);
+ }
+
+ return $data;
+ }
+
+}
diff --git a/src/applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php
index a83dafb0a..30939dd43 100644
--- a/src/applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php
+++ b/src/applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php
@@ -1,59 +1,60 @@
<?php
final class PhabricatorMailManagementListOutboundWorkflow
extends PhabricatorMailManagementWorkflow {
protected function didConstruct() {
$this
->setName('list-outbound')
->setSynopsis(pht('List outbound messages sent by Phabricator.'))
- ->setExamples(
- '**list-outbound**')
+ ->setExamples('**list-outbound**')
->setArguments(
array(
array(
'name' => 'limit',
'param' => 'N',
'default' => 100,
'help' => pht(
'Show a specific number of messages (default 100).'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$viewer = $this->getViewer();
$mails = id(new PhabricatorMetaMTAMail())->loadAllWhere(
'1 = 1 ORDER BY id DESC LIMIT %d',
$args->getArg('limit'));
if (!$mails) {
$console->writeErr("%s\n", pht('No sent mail.'));
return 0;
}
$table = id(new PhutilConsoleTable())
->setShowHeader(false)
->addColumn('id', array('title' => pht('ID')))
->addColumn('encrypt', array('title' => pht('#')))
->addColumn('status', array('title' => pht('Status')))
+ ->addColumn('type', array('title' => pht('Type')))
->addColumn('subject', array('title' => pht('Subject')));
foreach (array_reverse($mails) as $mail) {
$status = $mail->getStatus();
$table->addRow(array(
'id' => $mail->getID(),
'encrypt' => ($mail->getMustEncrypt() ? '#' : ' '),
'status' => PhabricatorMailOutboundStatus::getStatusName($status),
+ 'type' => $mail->getMessageType(),
'subject' => $mail->getSubject(),
));
}
$table->draw();
return 0;
}
}
diff --git a/src/applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php
index 46c444571..5c17b1132 100644
--- a/src/applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php
+++ b/src/applications/metamta/management/PhabricatorMailManagementReceiveTestWorkflow.php
@@ -1,166 +1,180 @@
<?php
final class PhabricatorMailManagementReceiveTestWorkflow
extends PhabricatorMailManagementWorkflow {
protected function didConstruct() {
$this
->setName('receive-test')
->setSynopsis(
pht(
'Simulate receiving mail. This is primarily useful if you are '.
'developing new mail receivers.'))
->setExamples(
'**receive-test** --as alincoln --to D123 < body.txt')
->setArguments(
array(
array(
'name' => 'as',
'param' => 'user',
'help' => pht('Act as the specified user.'),
),
array(
'name' => 'from',
'param' => 'email',
'help' => pht('Simulate mail delivery "From:" the given user.'),
),
array(
'name' => 'to',
'param' => 'object',
'help' => pht('Simulate mail delivery "To:" the given object.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
+ $viewer = $this->getViewer();
$console = PhutilConsole::getConsole();
$to = $args->getArg('to');
if (!$to) {
throw new PhutilArgumentUsageException(
pht(
"Use '%s' to specify the receiving object or email address.",
'--to'));
}
$to_application_email = id(new PhabricatorMetaMTAApplicationEmailQuery())
->setViewer($this->getViewer())
->withAddresses(array($to))
->executeOne();
$as = $args->getArg('as');
if (!$as && $to_application_email) {
$default_phid = $to_application_email->getConfigValue(
PhabricatorMetaMTAApplicationEmail::CONFIG_DEFAULT_AUTHOR);
if ($default_phid) {
$default_user = id(new PhabricatorPeopleQuery())
->setViewer($this->getViewer())
->withPHIDs(array($default_phid))
->executeOne();
if ($default_user) {
$as = $default_user->getUsername();
}
}
}
if (!$as) {
throw new PhutilArgumentUsageException(
pht("Use '--as' to specify the acting user."));
}
$user = id(new PhabricatorPeopleQuery())
->setViewer($this->getViewer())
->withUsernames(array($as))
->executeOne();
if (!$user) {
throw new PhutilArgumentUsageException(
pht("No such user '%s' exists.", $as));
}
$from = $args->getArg('from');
if (!$from) {
$from = $user->loadPrimaryEmail()->getAddress();
}
$console->writeErr("%s\n", pht('Reading message body from stdin...'));
$body = file_get_contents('php://stdin');
$received = new PhabricatorMetaMTAReceivedMail();
$header_content = array(
'Message-ID' => Filesystem::readRandomCharacters(12),
'From' => $from,
);
if (preg_match('/.+@.+/', $to)) {
$header_content['to'] = $to;
} else {
+
// We allow the user to use an object name instead of a real address
// as a convenience. To build the mail, we build a similar message and
// look for a receiver which will accept it.
+
+ // In the general case, mail may be processed by multiple receivers,
+ // but mail to objects only ever has one receiver today.
+
$pseudohash = PhabricatorObjectMailReceiver::computeMailHash('x', 'y');
+
+ $raw_target = $to.'+1+'.$pseudohash;
+ $target = new PhutilEmailAddress($raw_target.'@local.cli');
+
$pseudomail = id(new PhabricatorMetaMTAReceivedMail())
->setHeaders(
array(
- 'to' => $to.'+1+'.$pseudohash,
+ 'to' => $raw_target,
));
$receivers = id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorMailReceiver')
->setFilterMethod('isEnabled')
->execute();
$receiver = null;
foreach ($receivers as $possible_receiver) {
- if (!$possible_receiver->canAcceptMail($pseudomail)) {
+ $possible_receiver = id(clone $possible_receiver)
+ ->setViewer($viewer)
+ ->setSender($user);
+
+ if (!$possible_receiver->canAcceptMail($pseudomail, $target)) {
continue;
}
$receiver = $possible_receiver;
break;
}
if (!$receiver) {
throw new Exception(
pht("No configured mail receiver can accept mail to '%s'.", $to));
}
if (!($receiver instanceof PhabricatorObjectMailReceiver)) {
$class = get_class($receiver);
throw new Exception(
pht(
"Receiver '%s' accepts mail to '%s', but is not a ".
"subclass of PhabricatorObjectMailReceiver.",
$class,
$to));
}
$object = $receiver->loadMailReceiverObject($to, $user);
if (!$object) {
throw new Exception(pht("No such object '%s'!", $to));
}
$mail_key = PhabricatorMetaMTAMailProperties::loadMailKey($object);
$hash = PhabricatorObjectMailReceiver::computeMailHash(
$mail_key,
$user->getPHID());
$header_content['to'] = $to.'+'.$user->getID().'+'.$hash.'@test.com';
}
$received->setHeaders($header_content);
$received->setBodies(
array(
'text' => $body,
));
$received->save();
$received->processReceivedMail();
$console->writeErr(
"%s\n\n phabricator/ $ ./bin/mail show-inbound --id %d\n\n",
pht('Mail received! You can view details by running this command:'),
$received->getID());
}
}
diff --git a/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php
index 8140d64bd..f390ff27d 100644
--- a/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php
+++ b/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php
@@ -1,209 +1,236 @@
<?php
final class PhabricatorMailManagementSendTestWorkflow
extends PhabricatorMailManagementWorkflow {
protected function didConstruct() {
$this
->setName('send-test')
->setSynopsis(
pht(
'Simulate sending mail. This may be useful to test your mail '.
'configuration, or while developing new mail adapters.'))
->setExamples('**send-test** --to alincoln --subject hi < body.txt')
->setArguments(
array(
array(
'name' => 'from',
'param' => 'user',
'help' => pht('Send mail from the specified user.'),
),
array(
'name' => 'to',
'param' => 'user',
'help' => pht('Send mail "To:" the specified users.'),
'repeat' => true,
),
array(
'name' => 'cc',
'param' => 'user',
'help' => pht('Send mail which "Cc:"s the specified users.'),
'repeat' => true,
),
array(
'name' => 'subject',
'param' => 'text',
'help' => pht('Use the provided subject.'),
),
array(
'name' => 'tag',
'param' => 'text',
'help' => pht('Add the given mail tags.'),
'repeat' => true,
),
array(
'name' => 'attach',
'param' => 'file',
'help' => pht('Attach a file.'),
'repeat' => true,
),
array(
'name' => 'mailer',
'param' => 'key',
'help' => pht('Send with a specific configured mailer.'),
),
array(
'name' => 'html',
'help' => pht('Send as HTML mail.'),
),
array(
'name' => 'bulk',
'help' => pht('Send with bulk headers.'),
),
+ array(
+ 'name' => 'type',
+ 'param' => 'message-type',
+ 'help' => pht(
+ 'Send the specified type of message (email, sms, ...).'),
+ ),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$viewer = $this->getViewer();
+ $type = $args->getArg('type');
+ if (!strlen($type)) {
+ $type = PhabricatorMailEmailMessage::MESSAGETYPE;
+ }
+
+ $type_map = PhabricatorMailExternalMessage::getAllMessageTypes();
+ if (!isset($type_map[$type])) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Message type "%s" is unknown, supported message types are: %s.',
+ $type,
+ implode(', ', array_keys($type_map))));
+ }
+
$from = $args->getArg('from');
if ($from) {
$user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withUsernames(array($from))
->executeOne();
if (!$user) {
throw new PhutilArgumentUsageException(
pht("No such user '%s' exists.", $from));
}
$from = $user;
}
$tos = $args->getArg('to');
$ccs = $args->getArg('cc');
if (!$tos && !$ccs) {
throw new PhutilArgumentUsageException(
pht(
- 'Specify one or more users to send mail to with `%s` and `%s`.',
- '--to',
- '--cc'));
+ 'Specify one or more users to send a message to with "--to" and/or '.
+ '"--cc".'));
}
$names = array_merge($tos, $ccs);
$users = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withUsernames($names)
->execute();
$users = mpull($users, null, 'getUsername');
$raw_tos = array();
foreach ($tos as $key => $username) {
// If the recipient has an "@" in any noninitial position, treat this as
// a raw email address.
if (preg_match('/.@/', $username)) {
$raw_tos[] = $username;
unset($tos[$key]);
continue;
}
if (empty($users[$username])) {
throw new PhutilArgumentUsageException(
pht("No such user '%s' exists.", $username));
}
$tos[$key] = $users[$username]->getPHID();
}
foreach ($ccs as $key => $username) {
if (empty($users[$username])) {
throw new PhutilArgumentUsageException(
pht("No such user '%s' exists.", $username));
}
$ccs[$key] = $users[$username]->getPHID();
}
$subject = $args->getArg('subject');
if ($subject === null) {
$subject = pht('No Subject');
}
$tags = $args->getArg('tag');
$attach = $args->getArg('attach');
$is_bulk = $args->getArg('bulk');
$console->writeErr("%s\n", pht('Reading message body from stdin...'));
$body = file_get_contents('php://stdin');
$mail = id(new PhabricatorMetaMTAMail())
->addCCs($ccs)
->setSubject($subject)
->setBody($body)
->setIsBulk($is_bulk)
->setMailTags($tags);
if ($tos) {
$mail->addTos($tos);
}
if ($raw_tos) {
$mail->addRawTos($raw_tos);
}
if ($args->getArg('html')) {
$mail->setBody(
pht(
'(This is a placeholder plaintext email body for a test message '.
'sent with %s.)',
'--html'));
$mail->setHTMLBody($body);
} else {
$mail->setBody($body);
}
if ($from) {
$mail->setFrom($from->getPHID());
}
+ $mailers = PhabricatorMetaMTAMail::newMailers(
+ array(
+ 'media' => array($type),
+ 'outbound' => true,
+ ));
+ $mailers = mpull($mailers, null, 'getKey');
+
+ if (!$mailers) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'No configured mailers support outbound messages of type "%s".',
+ $type));
+ }
+
$mailer_key = $args->getArg('mailer');
if ($mailer_key !== null) {
- $mailers = PhabricatorMetaMTAMail::newMailers(array());
-
- $mailers = mpull($mailers, null, 'getKey');
if (!isset($mailers[$mailer_key])) {
throw new PhutilArgumentUsageException(
pht(
- 'Mailer key ("%s") is not configured. Available keys are: %s.',
+ 'Mailer key ("%s") is not configured, or does not support '.
+ 'outbound messages of type "%s". Available mailers are: %s.',
$mailer_key,
+ $type,
implode(', ', array_keys($mailers))));
}
- if (!$mailers[$mailer_key]->getSupportsOutbound()) {
- throw new PhutilArgumentUsageException(
- pht(
- 'Mailer ("%s") is not configured to support outbound mail.',
- $mailer_key));
- }
-
$mail->setTryMailers(array($mailer_key));
}
foreach ($attach as $attachment) {
$data = Filesystem::readFile($attachment);
$name = basename($attachment);
$mime = Filesystem::getMimeType($attachment);
- $file = new PhabricatorMetaMTAAttachment($data, $name, $mime);
+ $file = new PhabricatorMailAttachment($data, $name, $mime);
$mail->addAttachment($file);
}
+ $mail->setMessageType($type);
+
PhabricatorWorker::setRunAllTasksInProcess(true);
$mail->save();
$console->writeErr(
"%s\n\n phabricator/ $ ./bin/mail show-outbound --id %d\n\n",
pht('Mail sent! You can view details by running this command:'),
$mail->getID());
}
}
diff --git a/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php
index 0fc7dd14b..f29a63c2e 100644
--- a/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php
+++ b/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php
@@ -1,226 +1,230 @@
<?php
final class PhabricatorMailManagementShowOutboundWorkflow
extends PhabricatorMailManagementWorkflow {
protected function didConstruct() {
$this
->setName('show-outbound')
->setSynopsis(pht('Show diagnostic details about outbound mail.'))
->setExamples(
'**show-outbound** --id 1 --id 2')
->setArguments(
array(
array(
'name' => 'id',
'param' => 'id',
'help' => pht('Show details about outbound mail with given ID.'),
'repeat' => true,
),
array(
'name' => 'dump-html',
'help' => pht(
'Dump the HTML body of the mail. You can redirect it to a '.
'file and then open it in a browser.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$ids = $args->getArg('id');
if (!$ids) {
throw new PhutilArgumentUsageException(
pht(
"Use the '%s' flag to specify one or more messages to show.",
'--id'));
}
foreach ($ids as $id) {
if (!ctype_digit($id)) {
throw new PhutilArgumentUsageException(
pht(
'Argument "%s" is not a valid message ID.',
$id));
}
}
$messages = id(new PhabricatorMetaMTAMail())->loadAllWhere(
'id IN (%Ld)',
$ids);
if ($ids) {
$ids = array_fuse($ids);
$missing = array_diff_key($ids, $messages);
if ($missing) {
throw new PhutilArgumentUsageException(
pht(
'Some specified messages do not exist: %s',
implode(', ', array_keys($missing))));
}
}
$last_key = last_key($messages);
foreach ($messages as $message_key => $message) {
if ($args->getArg('dump-html')) {
$html_body = $message->getHTMLBody();
if (strlen($html_body)) {
$template =
"<!doctype html><html><body>{$html_body}</body></html>";
$console->writeOut("%s\n", $html_body);
} else {
$console->writeErr(
"%s\n",
pht('(This message has no HTML body.)'));
}
continue;
}
$info = array();
$info[] = $this->newSectionHeader(pht('PROPERTIES'));
$info[] = pht('ID: %d', $message->getID());
$info[] = pht('Status: %s', $message->getStatus());
$info[] = pht('Related PHID: %s', $message->getRelatedPHID());
$info[] = pht('Message: %s', $message->getMessage());
$ignore = array(
'body' => true,
'body.sent' => true,
'html-body' => true,
'headers' => true,
'attachments' => true,
'headers.sent' => true,
'headers.unfiltered' => true,
'authors.sent' => true,
);
$info[] = null;
$info[] = $this->newSectionHeader(pht('PARAMETERS'));
$parameters = $message->getParameters();
foreach ($parameters as $key => $value) {
if (isset($ignore[$key])) {
continue;
}
if (!is_scalar($value)) {
$value = json_encode($value);
}
$info[] = pht('%s: %s', $key, $value);
}
$info[] = null;
$info[] = $this->newSectionHeader(pht('HEADERS'));
$headers = $message->getDeliveredHeaders();
+ if (!$headers) {
+ $headers = array();
+ }
+
$unfiltered = $message->getUnfilteredHeaders();
if (!$unfiltered) {
- $headers = $message->generateHeaders();
- $unfiltered = $headers;
+ $unfiltered = array();
}
$header_map = array();
foreach ($headers as $header) {
list($name, $value) = $header;
$header_map[$name.':'.$value] = true;
}
foreach ($unfiltered as $header) {
list($name, $value) = $header;
$was_sent = isset($header_map[$name.':'.$value]);
if ($was_sent) {
$marker = ' ';
} else {
$marker = '#';
}
$info[] = "{$marker} {$name}: {$value}";
}
$attachments = idx($parameters, 'attachments');
if ($attachments) {
$info[] = null;
$info[] = $this->newSectionHeader(pht('ATTACHMENTS'));
foreach ($attachments as $attachment) {
$info[] = idx($attachment, 'filename', pht('Unnamed File'));
}
}
$all_actors = $message->loadAllActors();
$actors = $message->getDeliveredActors();
if ($actors) {
$info[] = null;
$info[] = $this->newSectionHeader(pht('RECIPIENTS'));
foreach ($actors as $actor_phid => $actor_info) {
$actor = idx($all_actors, $actor_phid);
if ($actor) {
$actor_name = coalesce($actor->getName(), $actor_phid);
} else {
$actor_name = $actor_phid;
}
$deliverable = $actor_info['deliverable'];
if ($deliverable) {
$info[] = ' '.$actor_name;
} else {
$info[] = '! '.$actor_name;
}
$reasons = $actor_info['reasons'];
foreach ($reasons as $reason) {
$name = PhabricatorMetaMTAActor::getReasonName($reason);
$desc = PhabricatorMetaMTAActor::getReasonDescription($reason);
$info[] = ' - '.$name.': '.$desc;
}
}
}
$info[] = null;
$info[] = $this->newSectionHeader(pht('TEXT BODY'));
if (strlen($message->getBody())) {
$info[] = tsprintf('%B', $message->getBody());
} else {
$info[] = pht('(This message has no text body.)');
}
$delivered_body = $message->getDeliveredBody();
if ($delivered_body !== null) {
$info[] = null;
$info[] = $this->newSectionHeader(pht('BODY AS DELIVERED'), true);
$info[] = tsprintf('%B', $delivered_body);
}
$info[] = null;
$info[] = $this->newSectionHeader(pht('HTML BODY'));
if (strlen($message->getHTMLBody())) {
$info[] = $message->getHTMLBody();
$info[] = null;
} else {
$info[] = pht('(This message has no HTML body.)');
+ $info[] = null;
}
$console->writeOut('%s', implode("\n", $info));
if ($message_key != $last_key) {
$console->writeOut("\n%s\n\n", str_repeat('-', 80));
}
}
}
private function newSectionHeader($label, $emphasize = false) {
if ($emphasize) {
return tsprintf('**<bg:yellow> %s </bg>**', $label);
} else {
return tsprintf('**<bg:blue> %s </bg>**', $label);
}
}
}
diff --git a/src/applications/metamta/message/PhabricatorMailAttachment.php b/src/applications/metamta/message/PhabricatorMailAttachment.php
new file mode 100644
index 000000000..7cc7f32fe
--- /dev/null
+++ b/src/applications/metamta/message/PhabricatorMailAttachment.php
@@ -0,0 +1,90 @@
+<?php
+
+final class PhabricatorMailAttachment extends Phobject {
+
+ private $data;
+ private $filename;
+ private $mimetype;
+ private $file;
+ private $filePHID;
+
+ public function __construct($data, $filename, $mimetype) {
+ $this->setData($data);
+ $this->setFilename($filename);
+ $this->setMimeType($mimetype);
+ }
+
+ public function getData() {
+ return $this->data;
+ }
+
+ public function setData($data) {
+ $this->data = $data;
+ return $this;
+ }
+
+ public function getFilename() {
+ return $this->filename;
+ }
+
+ public function setFilename($filename) {
+ $this->filename = $filename;
+ return $this;
+ }
+
+ public function getMimeType() {
+ return $this->mimetype;
+ }
+
+ public function setMimeType($mimetype) {
+ $this->mimetype = $mimetype;
+ return $this;
+ }
+
+ public function toDictionary() {
+ if (!$this->file) {
+ $iterator = new ArrayIterator(array($this->getData()));
+
+ $source = id(new PhabricatorIteratorFileUploadSource())
+ ->setName($this->getFilename())
+ ->setViewPolicy(PhabricatorPolicies::POLICY_NOONE)
+ ->setMIMEType($this->getMimeType())
+ ->setIterator($iterator);
+
+ $this->file = $source->uploadFile();
+ }
+
+ return array(
+ 'filename' => $this->getFilename(),
+ 'mimetype' => $this->getMimeType(),
+ 'filePHID' => $this->file->getPHID(),
+ );
+ }
+
+ public static function newFromDictionary(array $dict) {
+ $file = null;
+
+ $file_phid = idx($dict, 'filePHID');
+ if ($file_phid) {
+ $file = id(new PhabricatorFileQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->withPHIDs(array($file_phid))
+ ->executeOne();
+ if ($file) {
+ $dict['data'] = $file->loadFileData();
+ }
+ }
+
+ $attachment = new self(
+ idx($dict, 'data'),
+ idx($dict, 'filename'),
+ idx($dict, 'mimetype'));
+
+ if ($file) {
+ $attachment->file = $file;
+ }
+
+ return $attachment;
+ }
+
+}
diff --git a/src/applications/metamta/message/PhabricatorMailEmailMessage.php b/src/applications/metamta/message/PhabricatorMailEmailMessage.php
new file mode 100644
index 000000000..c98cdc2e3
--- /dev/null
+++ b/src/applications/metamta/message/PhabricatorMailEmailMessage.php
@@ -0,0 +1,107 @@
+<?php
+
+final class PhabricatorMailEmailMessage
+ extends PhabricatorMailExternalMessage {
+
+ const MESSAGETYPE = 'email';
+
+ private $fromAddress;
+ private $replyToAddress;
+ private $toAddresses = array();
+ private $ccAddresses = array();
+ private $headers = array();
+ private $attachments = array();
+ private $subject;
+ private $textBody;
+ private $htmlBody;
+
+ public function newMailMessageEngine() {
+ return new PhabricatorMailEmailEngine();
+ }
+
+ public function setFromAddress(PhutilEmailAddress $from_address) {
+ $this->fromAddress = $from_address;
+ return $this;
+ }
+
+ public function getFromAddress() {
+ return $this->fromAddress;
+ }
+
+ public function setReplyToAddress(PhutilEmailAddress $address) {
+ $this->replyToAddress = $address;
+ return $this;
+ }
+
+ public function getReplyToAddress() {
+ return $this->replyToAddress;
+ }
+
+ public function setToAddresses(array $addresses) {
+ assert_instances_of($addresses, 'PhutilEmailAddress');
+ $this->toAddresses = $addresses;
+ return $this;
+ }
+
+ public function getToAddresses() {
+ return $this->toAddresses;
+ }
+
+ public function setCCAddresses(array $addresses) {
+ assert_instances_of($addresses, 'PhutilEmailAddress');
+ $this->ccAddresses = $addresses;
+ return $this;
+ }
+
+ public function getCCAddresses() {
+ return $this->ccAddresses;
+ }
+
+ public function setHeaders(array $headers) {
+ assert_instances_of($headers, 'PhabricatorMailHeader');
+ $this->headers = $headers;
+ return $this;
+ }
+
+ public function getHeaders() {
+ return $this->headers;
+ }
+
+ public function setAttachments(array $attachments) {
+ assert_instances_of($attachments, 'PhabricatorMailAttachment');
+ $this->attachments = $attachments;
+ return $this;
+ }
+
+ public function getAttachments() {
+ return $this->attachments;
+ }
+
+ public function setSubject($subject) {
+ $this->subject = $subject;
+ return $this;
+ }
+
+ public function getSubject() {
+ return $this->subject;
+ }
+
+ public function setTextBody($text_body) {
+ $this->textBody = $text_body;
+ return $this;
+ }
+
+ public function getTextBody() {
+ return $this->textBody;
+ }
+
+ public function setHTMLBody($html_body) {
+ $this->htmlBody = $html_body;
+ return $this;
+ }
+
+ public function getHTMLBody() {
+ return $this->htmlBody;
+ }
+
+}
diff --git a/src/applications/metamta/message/PhabricatorMailExternalMessage.php b/src/applications/metamta/message/PhabricatorMailExternalMessage.php
new file mode 100644
index 000000000..048b20ab5
--- /dev/null
+++ b/src/applications/metamta/message/PhabricatorMailExternalMessage.php
@@ -0,0 +1,17 @@
+<?php
+
+abstract class PhabricatorMailExternalMessage
+ extends Phobject {
+
+ final public function getMessageType() {
+ return $this->getPhobjectClassConstant('MESSAGETYPE');
+ }
+
+ final public static function getAllMessageTypes() {
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->setUniqueMethod('getMessageType')
+ ->execute();
+ }
+
+}
diff --git a/src/applications/metamta/message/PhabricatorMailHeader.php b/src/applications/metamta/message/PhabricatorMailHeader.php
new file mode 100644
index 000000000..2e60ee5c7
--- /dev/null
+++ b/src/applications/metamta/message/PhabricatorMailHeader.php
@@ -0,0 +1,27 @@
+<?php
+
+final class PhabricatorMailHeader
+ extends Phobject {
+
+ private $name;
+ private $value;
+
+ public function setName($name) {
+ $this->name = $name;
+ return $this;
+ }
+
+ public function getName() {
+ return $this->name;
+ }
+
+ public function setValue($value) {
+ $this->value = $value;
+ return $this;
+ }
+
+ public function getValue() {
+ return $this->value;
+ }
+
+}
diff --git a/src/applications/metamta/message/PhabricatorMailSMSMessage.php b/src/applications/metamta/message/PhabricatorMailSMSMessage.php
new file mode 100644
index 000000000..ae7cd7122
--- /dev/null
+++ b/src/applications/metamta/message/PhabricatorMailSMSMessage.php
@@ -0,0 +1,33 @@
+<?php
+
+final class PhabricatorMailSMSMessage
+ extends PhabricatorMailExternalMessage {
+
+ const MESSAGETYPE = 'sms';
+
+ private $toNumber;
+ private $textBody;
+
+ public function newMailMessageEngine() {
+ return new PhabricatorMailSMSEngine();
+ }
+
+ public function setToNumber(PhabricatorPhoneNumber $to_number) {
+ $this->toNumber = $to_number;
+ return $this;
+ }
+
+ public function getToNumber() {
+ return $this->toNumber;
+ }
+
+ public function setTextBody($text_body) {
+ $this->textBody = $text_body;
+ return $this;
+ }
+
+ public function getTextBody() {
+ return $this->textBody;
+ }
+
+}
diff --git a/src/applications/metamta/message/PhabricatorPhoneNumber.php b/src/applications/metamta/message/PhabricatorPhoneNumber.php
new file mode 100644
index 000000000..9d8193685
--- /dev/null
+++ b/src/applications/metamta/message/PhabricatorPhoneNumber.php
@@ -0,0 +1,35 @@
+<?php
+
+final class PhabricatorPhoneNumber
+ extends Phobject {
+
+ private $number;
+
+ public function __construct($raw_number) {
+ $number = preg_replace('/[^\d]+/', '', $raw_number);
+
+ if (!preg_match('/^[1-9]\d{9,14}\z/', $number)) {
+ throw new Exception(
+ pht(
+ 'Phone number ("%s") is not in a recognized format: expected a '.
+ 'US number like "(555) 555-5555", or an international number '.
+ 'like "+55 5555 555555".',
+ $raw_number));
+ }
+
+ // If the number didn't start with "+" and has has 10 digits, assume it is
+ // a US number with no country code prefix, like "(555) 555-5555".
+ if (!preg_match('/^[+]/', $raw_number)) {
+ if (strlen($number) === 10) {
+ $number = '1'.$number;
+ }
+ }
+
+ $this->number = $number;
+ }
+
+ public function toE164() {
+ return '+'.$this->number;
+ }
+
+}
diff --git a/src/applications/metamta/message/__tests__/PhabricatorPhoneNumberTestCase.php b/src/applications/metamta/message/__tests__/PhabricatorPhoneNumberTestCase.php
new file mode 100644
index 000000000..4a5da3bcc
--- /dev/null
+++ b/src/applications/metamta/message/__tests__/PhabricatorPhoneNumberTestCase.php
@@ -0,0 +1,37 @@
+<?php
+
+final class PhabricatorPhoneNumberTestCase
+ extends PhabricatorTestCase {
+
+ public function testNumberNormalization() {
+ $map = array(
+ '+15555555555' => '+15555555555',
+ '+1 (555) 555-5555' => '+15555555555',
+ '(555) 555-5555' => '+15555555555',
+
+ '' => false,
+ '1-800-CALL-SAUL' => false,
+ );
+
+ foreach ($map as $input => $expect) {
+ $caught = null;
+ try {
+ $actual = id(new PhabricatorPhoneNumber($input))
+ ->toE164();
+ } catch (Exception $ex) {
+ $caught = $ex;
+ }
+
+ $this->assertEqual(
+ (bool)$caught,
+ ($expect === false),
+ pht('Exception raised by: %s', $input));
+
+ if ($expect !== false) {
+ $this->assertEqual($expect, $actual, pht('E164 of: %s', $input));
+ }
+ }
+
+ }
+
+}
diff --git a/src/applications/metamta/query/PhabricatorMetaMTAActorQuery.php b/src/applications/metamta/query/PhabricatorMetaMTAActorQuery.php
index 18b8063ee..269b9824a 100644
--- a/src/applications/metamta/query/PhabricatorMetaMTAActorQuery.php
+++ b/src/applications/metamta/query/PhabricatorMetaMTAActorQuery.php
@@ -1,166 +1,166 @@
<?php
final class PhabricatorMetaMTAActorQuery extends PhabricatorQuery {
private $phids = array();
private $viewer;
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function execute() {
$phids = array_fuse($this->phids);
$actors = array();
$type_map = array();
foreach ($phids as $phid) {
$type_map[phid_get_type($phid)][] = $phid;
$actors[$phid] = id(new PhabricatorMetaMTAActor())->setPHID($phid);
}
// TODO: Move this to PhabricatorPHIDType, or the objects, or some
// interface.
foreach ($type_map as $type => $phids) {
switch ($type) {
case PhabricatorPeopleUserPHIDType::TYPECONST:
$this->loadUserActors($actors, $phids);
break;
case PhabricatorPeopleExternalPHIDType::TYPECONST:
$this->loadExternalUserActors($actors, $phids);
break;
default:
$this->loadUnknownActors($actors, $phids);
break;
}
}
return $actors;
}
private function loadUserActors(array $actors, array $phids) {
assert_instances_of($actors, 'PhabricatorMetaMTAActor');
$emails = id(new PhabricatorUserEmail())->loadAllWhere(
'userPHID IN (%Ls) AND isPrimary = 1',
$phids);
$emails = mpull($emails, null, 'getUserPHID');
$users = id(new PhabricatorPeopleQuery())
->setViewer($this->getViewer())
->withPHIDs($phids)
->needUserSettings(true)
->execute();
$users = mpull($users, null, 'getPHID');
foreach ($phids as $phid) {
$actor = $actors[$phid];
$user = idx($users, $phid);
if (!$user) {
$actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_UNLOADABLE);
} else {
$actor->setName($this->getUserName($user));
if ($user->getIsDisabled()) {
$actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_DISABLED);
}
if ($user->getIsSystemAgent()) {
$actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_BOT);
}
// NOTE: We do send email to unapproved users, and to unverified users,
// because it would otherwise be impossible to get them to verify their
// email addresses. Possibly we should white-list this kind of mail and
// deny all other types of mail.
}
$email = idx($emails, $phid);
if (!$email) {
$actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_NO_ADDRESS);
} else {
$actor->setEmailAddress($email->getAddress());
$actor->setIsVerified($email->getIsVerified());
}
}
}
private function loadExternalUserActors(array $actors, array $phids) {
assert_instances_of($actors, 'PhabricatorMetaMTAActor');
$xusers = id(new PhabricatorExternalAccountQuery())
->setViewer($this->getViewer())
->withPHIDs($phids)
->execute();
$xusers = mpull($xusers, null, 'getPHID');
foreach ($phids as $phid) {
$actor = $actors[$phid];
$xuser = idx($xusers, $phid);
if (!$xuser) {
$actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_UNLOADABLE);
continue;
}
$actor->setName($xuser->getDisplayName());
if ($xuser->getAccountType() != 'email') {
$actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_EXTERNAL_TYPE);
continue;
}
$actor->setEmailAddress($xuser->getAccountID());
- // NOTE: This effectively drops all outbound mail to unrecognized
- // addresses unless "phabricator.allow-email-users" is set. See T12237
- // for context.
- $allow_key = 'phabricator.allow-email-users';
- $allow_value = PhabricatorEnv::getEnvConfig($allow_key);
- $actor->setIsVerified((bool)$allow_value);
+ // Circa T7477, it appears that we never intentionally send email to
+ // external users (even when they email "bugs@" to create a task).
+ // Mark these users as unverified so mail to them is always dropped.
+ // See also T12237. In the future, we might change this behavior.
+
+ $actor->setIsVerified(false);
}
}
private function loadUnknownActors(array $actors, array $phids) {
foreach ($phids as $phid) {
$actor = $actors[$phid];
$actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_UNMAILABLE);
}
}
/**
* Small helper function to make sure we format the username properly as
* specified by the `metamta.user-address-format` configuration value.
*/
private function getUserName(PhabricatorUser $user) {
$format = PhabricatorEnv::getEnvConfig('metamta.user-address-format');
switch ($format) {
case 'short':
$name = $user->getUserName();
break;
case 'real':
$name = strlen($user->getRealName()) ?
$user->getRealName() : $user->getUserName();
break;
case 'full':
default:
$name = $user->getFullName();
break;
}
return $name;
}
}
diff --git a/src/applications/metamta/query/PhabricatorMetaMTAMailQuery.php b/src/applications/metamta/query/PhabricatorMetaMTAMailQuery.php
index e0dadb5f1..a1fad69c0 100644
--- a/src/applications/metamta/query/PhabricatorMetaMTAMailQuery.php
+++ b/src/applications/metamta/query/PhabricatorMetaMTAMailQuery.php
@@ -1,137 +1,137 @@
<?php
final class PhabricatorMetaMTAMailQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $actorPHIDs;
private $recipientPHIDs;
private $createdMin;
private $createdMax;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withActorPHIDs(array $phids) {
$this->actorPHIDs = $phids;
return $this;
}
public function withRecipientPHIDs(array $phids) {
$this->recipientPHIDs = $phids;
return $this;
}
public function withDateCreatedBetween($min, $max) {
$this->createdMin = $min;
$this->createdMax = $max;
return $this;
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'mail.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'mail.phid IN (%Ls)',
$this->phids);
}
if ($this->actorPHIDs !== null) {
$where[] = qsprintf(
$conn,
'mail.actorPHID IN (%Ls)',
$this->actorPHIDs);
}
if ($this->recipientPHIDs !== null) {
$where[] = qsprintf(
$conn,
'recipient.dst IN (%Ls)',
$this->recipientPHIDs);
}
if ($this->actorPHIDs === null && $this->recipientPHIDs === null) {
$viewer = $this->getViewer();
if (!$viewer->isOmnipotent()) {
$where[] = qsprintf(
$conn,
'edge.dst = %s OR actorPHID = %s',
$viewer->getPHID(),
$viewer->getPHID());
}
}
if ($this->createdMin !== null) {
$where[] = qsprintf(
$conn,
'mail.dateCreated >= %d',
$this->createdMin);
}
if ($this->createdMax !== null) {
$where[] = qsprintf(
$conn,
'mail.dateCreated <= %d',
$this->createdMax);
}
return $where;
}
- protected function buildJoinClause(AphrontDatabaseConnection $conn) {
- $joins = array();
+ protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
+ $joins = parent::buildJoinClauseParts($conn);
if ($this->actorPHIDs === null && $this->recipientPHIDs === null) {
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T edge ON mail.phid = edge.src AND edge.type = %d',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorMetaMTAMailHasRecipientEdgeType::EDGECONST);
}
if ($this->recipientPHIDs !== null) {
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T recipient '.
'ON mail.phid = recipient.src AND recipient.type = %d',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorMetaMTAMailHasRecipientEdgeType::EDGECONST);
}
- return implode(' ', $joins);
+ return $joins;
}
protected function getPrimaryTableAlias() {
return 'mail';
}
public function newResultObject() {
return new PhabricatorMetaMTAMail();
}
public function getQueryApplicationClass() {
return 'PhabricatorMetaMTAApplication';
}
}
diff --git a/src/applications/metamta/receiver/PhabricatorApplicationMailReceiver.php b/src/applications/metamta/receiver/PhabricatorApplicationMailReceiver.php
new file mode 100644
index 000000000..546f622e1
--- /dev/null
+++ b/src/applications/metamta/receiver/PhabricatorApplicationMailReceiver.php
@@ -0,0 +1,107 @@
+<?php
+
+abstract class PhabricatorApplicationMailReceiver
+ extends PhabricatorMailReceiver {
+
+ private $applicationEmail;
+ private $emailList;
+ private $author;
+
+ abstract protected function newApplication();
+
+ final protected function setApplicationEmail(
+ PhabricatorMetaMTAApplicationEmail $email) {
+ $this->applicationEmail = $email;
+ return $this;
+ }
+
+ final protected function getApplicationEmail() {
+ return $this->applicationEmail;
+ }
+
+ final protected function setAuthor(PhabricatorUser $author) {
+ $this->author = $author;
+ return $this;
+ }
+
+ final protected function getAuthor() {
+ return $this->author;
+ }
+
+ final public function isEnabled() {
+ return $this->newApplication()->isInstalled();
+ }
+
+ final public function canAcceptMail(
+ PhabricatorMetaMTAReceivedMail $mail,
+ PhutilEmailAddress $target) {
+
+ $viewer = $this->getViewer();
+ $sender = $this->getSender();
+
+ foreach ($this->loadApplicationEmailList() as $application_email) {
+ $create_address = $application_email->newAddress();
+
+ if (!PhabricatorMailUtil::matchAddresses($create_address, $target)) {
+ continue;
+ }
+
+ if ($sender) {
+ $author = $sender;
+ } else {
+ $author_phid = $application_email->getDefaultAuthorPHID();
+
+ // If this mail isn't from a recognized sender and the target address
+ // does not have a default author, we can't accept it, and it's an
+ // error because you tried to send it here.
+
+ // You either need to be sending from a real address or be sending to
+ // an address which accepts mail from the public internet.
+
+ if (!$author_phid) {
+ throw new PhabricatorMetaMTAReceivedMailProcessingException(
+ MetaMTAReceivedMailStatus::STATUS_UNKNOWN_SENDER,
+ pht(
+ 'You are sending from an unrecognized email address to '.
+ 'an address which does not support public email ("%s").',
+ (string)$target));
+ }
+
+ $author = id(new PhabricatorPeopleQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($author_phid))
+ ->executeOne();
+ if (!$author) {
+ throw new Exception(
+ pht(
+ 'Application email ("%s") has an invalid default author ("%s").',
+ (string)$create_address,
+ $author_phid));
+ }
+ }
+
+ $this
+ ->setApplicationEmail($application_email)
+ ->setAuthor($author);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ private function loadApplicationEmailList() {
+ if ($this->emailList === null) {
+ $viewer = $this->getViewer();
+ $application = $this->newApplication();
+
+ $this->emailList = id(new PhabricatorMetaMTAApplicationEmailQuery())
+ ->setViewer($viewer)
+ ->withApplicationPHIDs(array($application->getPHID()))
+ ->execute();
+ }
+
+ return $this->emailList;
+ }
+
+}
diff --git a/src/applications/metamta/receiver/PhabricatorMailReceiver.php b/src/applications/metamta/receiver/PhabricatorMailReceiver.php
index 12483bf5b..738f8d9e2 100644
--- a/src/applications/metamta/receiver/PhabricatorMailReceiver.php
+++ b/src/applications/metamta/receiver/PhabricatorMailReceiver.php
@@ -1,275 +1,41 @@
<?php
abstract class PhabricatorMailReceiver extends Phobject {
- private $applicationEmail;
+ private $viewer;
+ private $sender;
- public function setApplicationEmail(
- PhabricatorMetaMTAApplicationEmail $email) {
- $this->applicationEmail = $email;
+ final public function setViewer(PhabricatorUser $viewer) {
+ $this->viewer = $viewer;
return $this;
}
- public function getApplicationEmail() {
- return $this->applicationEmail;
+ final public function getViewer() {
+ return $this->viewer;
}
- abstract public function isEnabled();
- abstract public function canAcceptMail(PhabricatorMetaMTAReceivedMail $mail);
- final protected function canAcceptApplicationMail(
- PhabricatorApplication $app,
- PhabricatorMetaMTAReceivedMail $mail) {
-
- $application_emails = id(new PhabricatorMetaMTAApplicationEmailQuery())
- ->setViewer($this->getViewer())
- ->withApplicationPHIDs(array($app->getPHID()))
- ->execute();
-
- foreach ($mail->getToAddresses() as $to_address) {
- foreach ($application_emails as $application_email) {
- $create_address = $application_email->getAddress();
- if ($this->matchAddresses($create_address, $to_address)) {
- $this->setApplicationEmail($application_email);
- return true;
- }
- }
- }
+ final public function setSender(PhabricatorUser $sender) {
+ $this->sender = $sender;
+ return $this;
+ }
- return false;
+ final public function getSender() {
+ return $this->sender;
}
+ abstract public function isEnabled();
+ abstract public function canAcceptMail(
+ PhabricatorMetaMTAReceivedMail $mail,
+ PhutilEmailAddress $target);
abstract protected function processReceivedMail(
PhabricatorMetaMTAReceivedMail $mail,
- PhabricatorUser $sender);
+ PhutilEmailAddress $target);
final public function receiveMail(
PhabricatorMetaMTAReceivedMail $mail,
- PhabricatorUser $sender) {
- $this->processReceivedMail($mail, $sender);
- }
-
- public function getViewer() {
- return PhabricatorUser::getOmnipotentUser();
- }
-
- public function validateSender(
- PhabricatorMetaMTAReceivedMail $mail,
- PhabricatorUser $sender) {
-
- $failure_reason = null;
- if ($sender->getIsDisabled()) {
- $failure_reason = pht(
- 'Your account (%s) is disabled, so you can not interact with '.
- 'Phabricator over email.',
- $sender->getUsername());
- } else if ($sender->getIsStandardUser()) {
- if (!$sender->getIsApproved()) {
- $failure_reason = pht(
- 'Your account (%s) has not been approved yet. You can not interact '.
- 'with Phabricator over email until your account is approved.',
- $sender->getUsername());
- } else if (PhabricatorUserEmail::isEmailVerificationRequired() &&
- !$sender->getIsEmailVerified()) {
- $failure_reason = pht(
- 'You have not verified the email address for your account (%s). '.
- 'You must verify your email address before you can interact '.
- 'with Phabricator over email.',
- $sender->getUsername());
- }
- }
-
- if ($failure_reason) {
- throw new PhabricatorMetaMTAReceivedMailProcessingException(
- MetaMTAReceivedMailStatus::STATUS_DISABLED_SENDER,
- $failure_reason);
- }
- }
-
- /**
- * Identifies the sender's user account for a piece of received mail. Note
- * that this method does not validate that the sender is who they say they
- * are, just that they've presented some credential which corresponds to a
- * recognizable user.
- */
- public function loadSender(PhabricatorMetaMTAReceivedMail $mail) {
- $raw_from = $mail->getHeader('From');
- $from = self::getRawAddress($raw_from);
-
- $reasons = array();
-
- // Try to find a user with this email address.
- $user = PhabricatorUser::loadOneWithEmailAddress($from);
- if ($user) {
- return $user;
- } else {
- $reasons[] = pht(
- 'This email was sent from "%s", but that address is not recognized by '.
- 'Phabricator and does not correspond to any known user account.',
- $raw_from);
- }
-
- // If we missed on "From", try "Reply-To" if we're configured for it.
- $raw_reply_to = $mail->getHeader('Reply-To');
- if (strlen($raw_reply_to)) {
- $reply_to_key = 'metamta.insecure-auth-with-reply-to';
- $allow_reply_to = PhabricatorEnv::getEnvConfig($reply_to_key);
- if ($allow_reply_to) {
- $reply_to = self::getRawAddress($raw_reply_to);
-
- $user = PhabricatorUser::loadOneWithEmailAddress($reply_to);
- if ($user) {
- return $user;
- } else {
- $reasons[] = pht(
- 'Phabricator is configured to authenticate users using the '.
- '"Reply-To" header, but the reply address ("%s") on this '.
- 'message does not correspond to any known user account.',
- $raw_reply_to);
- }
- } else {
- $reasons[] = pht(
- '(Phabricator is not configured to authenticate users using the '.
- '"Reply-To" header, so it was ignored.)');
- }
- }
-
- // If we don't know who this user is, load or create an external user
- // account for them if we're configured for it.
- $email_key = 'phabricator.allow-email-users';
- $allow_email_users = PhabricatorEnv::getEnvConfig($email_key);
- if ($allow_email_users) {
- $from_obj = new PhutilEmailAddress($from);
- $xuser = id(new PhabricatorExternalAccountQuery())
- ->setViewer($this->getViewer())
- ->withAccountTypes(array('email'))
- ->withAccountDomains(array($from_obj->getDomainName(), 'self'))
- ->withAccountIDs(array($from_obj->getAddress()))
- ->requireCapabilities(
- array(
- PhabricatorPolicyCapability::CAN_VIEW,
- PhabricatorPolicyCapability::CAN_EDIT,
- ))
- ->loadOneOrCreate();
- return $xuser->getPhabricatorUser();
- } else {
- // NOTE: Currently, we'll always drop this mail (since it's headed to
- // an unverified recipient). See T12237. These details are still useful
- // because they'll appear in the mail logs and Mail web UI.
-
- $reasons[] = pht(
- 'Phabricator is also not configured to allow unknown external users '.
- 'to send mail to the system using just an email address.');
- $reasons[] = pht(
- 'To interact with Phabricator, add this address ("%s") to your '.
- 'account.',
- $raw_from);
- }
-
- if ($this->getApplicationEmail()) {
- $application_email = $this->getApplicationEmail();
- $default_user_phid = $application_email->getConfigValue(
- PhabricatorMetaMTAApplicationEmail::CONFIG_DEFAULT_AUTHOR);
-
- if ($default_user_phid) {
- $user = id(new PhabricatorUser())->loadOneWhere(
- 'phid = %s',
- $default_user_phid);
- if ($user) {
- return $user;
- }
-
- $reasons[] = pht(
- 'Phabricator is misconfigured: the application email '.
- '"%s" is set to user "%s", but that user does not exist.',
- $application_email->getAddress(),
- $default_user_phid);
- }
- }
-
- $reasons = implode("\n\n", $reasons);
-
- throw new PhabricatorMetaMTAReceivedMailProcessingException(
- MetaMTAReceivedMailStatus::STATUS_UNKNOWN_SENDER,
- $reasons);
- }
-
- /**
- * Determine if two inbound email addresses are effectively identical. This
- * method strips and normalizes addresses so that equivalent variations are
- * correctly detected as identical. For example, these addresses are all
- * considered to match one another:
- *
- * "Abraham Lincoln" <alincoln@example.com>
- * alincoln@example.com
- * <ALincoln@example.com>
- * "Abraham" <phabricator+ALINCOLN@EXAMPLE.COM> # With configured prefix.
- *
- * @param string Email address.
- * @param string Another email address.
- * @return bool True if addresses match.
- */
- public static function matchAddresses($u, $v) {
- $u = self::getRawAddress($u);
- $v = self::getRawAddress($v);
-
- $u = self::stripMailboxPrefix($u);
- $v = self::stripMailboxPrefix($v);
-
- return ($u === $v);
- }
-
-
- /**
- * Strip a global mailbox prefix from an address if it is present. Phabricator
- * can be configured to prepend a prefix to all reply addresses, which can
- * make forwarding rules easier to write. A prefix looks like:
- *
- * example@phabricator.example.com # No Prefix
- * phabricator+example@phabricator.example.com # Prefix "phabricator"
- *
- * @param string Email address, possibly with a mailbox prefix.
- * @return string Email address with any prefix stripped.
- */
- public static function stripMailboxPrefix($address) {
- $address = id(new PhutilEmailAddress($address))->getAddress();
-
- $prefix_key = 'metamta.single-reply-handler-prefix';
- $prefix = PhabricatorEnv::getEnvConfig($prefix_key);
-
- $len = strlen($prefix);
-
- if ($len) {
- $prefix = $prefix.'+';
- $len = $len + 1;
- }
-
- if ($len) {
- if (!strncasecmp($address, $prefix, $len)) {
- $address = substr($address, strlen($prefix));
- }
- }
-
- return $address;
- }
-
-
- /**
- * Reduce an email address to its canonical form. For example, an address
- * like:
- *
- * "Abraham Lincoln" < ALincoln@example.com >
- *
- * ...will be reduced to:
- *
- * alincoln@example.com
- *
- * @param string Email address in noncanonical form.
- * @return string Canonical email address.
- */
- public static function getRawAddress($address) {
- $address = id(new PhutilEmailAddress($address))->getAddress();
- return trim(phutil_utf8_strtolower($address));
+ PhutilEmailAddress $target) {
+ $this->processReceivedMail($mail, $target);
}
}
diff --git a/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php b/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php
index 801f7a8e6..16950c157 100644
--- a/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php
+++ b/src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php
@@ -1,209 +1,190 @@
<?php
abstract class PhabricatorObjectMailReceiver extends PhabricatorMailReceiver {
/**
* Return a regular expression fragment which matches the name of an
* object which can receive mail. For example, Differential uses:
*
* D[1-9]\d*
*
* ...to match `D123`, etc., identifying Differential Revisions.
*
* @return string Regular expression fragment.
*/
abstract protected function getObjectPattern();
/**
* Load the object receiving mail, based on an identifying pattern. Normally
* this pattern is some sort of object ID.
*
* @param string A string matched by @{method:getObjectPattern}
* fragment.
* @param PhabricatorUser The viewing user.
* @return void
*/
abstract protected function loadObject($pattern, PhabricatorUser $viewer);
final protected function processReceivedMail(
PhabricatorMetaMTAReceivedMail $mail,
- PhabricatorUser $sender) {
-
- $object = $this->loadObjectFromMail($mail, $sender);
- $mail->setRelatedPHID($object->getPHID());
-
- $this->processReceivedObjectMail($mail, $object, $sender);
+ PhutilEmailAddress $target) {
- return $this;
- }
-
- protected function processReceivedObjectMail(
- PhabricatorMetaMTAReceivedMail $mail,
- PhabricatorLiskDAO $object,
- PhabricatorUser $sender) {
-
- $handler = $this->getTransactionReplyHandler();
- if ($handler) {
- return $handler
- ->setMailReceiver($object)
- ->setActor($sender)
- ->setExcludeMailRecipientPHIDs($mail->loadAllRecipientPHIDs())
- ->processEmail($mail);
+ $parts = $this->matchObjectAddress($target);
+ if (!$parts) {
+ // We should only make it here if we matched already in "canAcceptMail()",
+ // so this is a surprise.
+ throw new Exception(
+ pht(
+ 'Failed to parse object address ("%s") during processing.',
+ (string)$target));
}
- throw new PhutilMethodNotImplementedException();
- }
-
- protected function getTransactionReplyHandler() {
- return null;
- }
-
- public function loadMailReceiverObject($pattern, PhabricatorUser $viewer) {
- return $this->loadObject($pattern, $viewer);
- }
-
- public function validateSender(
- PhabricatorMetaMTAReceivedMail $mail,
- PhabricatorUser $sender) {
-
- parent::validateSender($mail, $sender);
-
- $parts = $this->matchObjectAddressInMail($mail);
$pattern = $parts['pattern'];
+ $sender = $this->getSender();
try {
- $object = $this->loadObjectFromMail($mail, $sender);
+ $object = $this->loadObject($pattern, $sender);
} catch (PhabricatorPolicyException $policy_exception) {
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_POLICY_PROBLEM,
pht(
'This mail is addressed to an object ("%s") you do not have '.
'permission to see: %s',
$pattern,
$policy_exception->getMessage()));
}
if (!$object) {
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_NO_SUCH_OBJECT,
pht(
'This mail is addressed to an object ("%s"), but that object '.
'does not exist.',
$pattern));
}
$sender_identifier = $parts['sender'];
-
if ($sender_identifier === 'public') {
if (!PhabricatorEnv::getEnvConfig('metamta.public-replies')) {
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_NO_PUBLIC_MAIL,
pht(
'This mail is addressed to the public email address of an object '.
'("%s"), but public replies are not enabled on this Phabricator '.
'install. An administrator may have recently disabled this '.
'setting, or you may have replied to an old message. Try '.
'replying to a more recent message instead.',
$pattern));
}
$check_phid = $object->getPHID();
} else {
if ($sender_identifier != $sender->getID()) {
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_USER_MISMATCH,
pht(
'This mail is addressed to the private email address of an object '.
'("%s"), but you are not the user who is authorized to use the '.
'address you sent mail to. Each private address is unique to the '.
'user who received the original mail. Try replying to a message '.
'which was sent directly to you instead.',
$pattern));
}
$check_phid = $sender->getPHID();
}
$mail_key = PhabricatorMetaMTAMailProperties::loadMailKey($object);
$expect_hash = self::computeMailHash($mail_key, $check_phid);
if (!phutil_hashes_are_identical($expect_hash, $parts['hash'])) {
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_HASH_MISMATCH,
pht(
'This mail is addressed to an object ("%s"), but the address is '.
'not correct (the security hash is wrong). Check that the address '.
'is correct.',
$pattern));
}
+
+ $mail->setRelatedPHID($object->getPHID());
+ $this->processReceivedObjectMail($mail, $object, $sender);
+
+ return $this;
}
+ protected function processReceivedObjectMail(
+ PhabricatorMetaMTAReceivedMail $mail,
+ PhabricatorLiskDAO $object,
+ PhabricatorUser $sender) {
- final public function canAcceptMail(PhabricatorMetaMTAReceivedMail $mail) {
- if ($this->matchObjectAddressInMail($mail)) {
- return true;
+ $handler = $this->getTransactionReplyHandler();
+ if ($handler) {
+ return $handler
+ ->setMailReceiver($object)
+ ->setActor($sender)
+ ->setExcludeMailRecipientPHIDs($mail->loadAllRecipientPHIDs())
+ ->processEmail($mail);
}
- return false;
+ throw new PhutilMethodNotImplementedException();
}
- private function matchObjectAddressInMail(
- PhabricatorMetaMTAReceivedMail $mail) {
+ protected function getTransactionReplyHandler() {
+ return null;
+ }
- foreach ($mail->getToAddresses() as $address) {
- $parts = $this->matchObjectAddress($address);
- if ($parts) {
- return $parts;
- }
+ public function loadMailReceiverObject($pattern, PhabricatorUser $viewer) {
+ return $this->loadObject($pattern, $viewer);
+ }
+
+ final public function canAcceptMail(
+ PhabricatorMetaMTAReceivedMail $mail,
+ PhutilEmailAddress $target) {
+
+ // If we don't have a valid sender user account, we can never accept
+ // mail to any object.
+ $sender = $this->getSender();
+ if (!$sender) {
+ return false;
}
- return null;
+ return (bool)$this->matchObjectAddress($target);
}
- private function matchObjectAddress($address) {
- $regexp = $this->getAddressRegexp();
-
- $address = self::stripMailboxPrefix($address);
- $local = id(new PhutilEmailAddress($address))->getLocalPart();
+ private function matchObjectAddress(PhutilEmailAddress $address) {
+ $address = PhabricatorMailUtil::normalizeAddress($address);
+ $local = $address->getLocalPart();
+ $regexp = $this->getAddressRegexp();
$matches = null;
if (!preg_match($regexp, $local, $matches)) {
return false;
}
return $matches;
}
private function getAddressRegexp() {
$pattern = $this->getObjectPattern();
$regexp =
'(^'.
'(?P<pattern>'.$pattern.')'.
'\\+'.
'(?P<sender>\w+)'.
'\\+'.
'(?P<hash>[a-f0-9]{16})'.
'$)Ui';
return $regexp;
}
- private function loadObjectFromMail(
- PhabricatorMetaMTAReceivedMail $mail,
- PhabricatorUser $sender) {
- $parts = $this->matchObjectAddressInMail($mail);
-
- return $this->loadObject(
- phutil_utf8_strtoupper($parts['pattern']),
- $sender);
- }
-
public static function computeMailHash($mail_key, $phid) {
- $global_mail_key = PhabricatorEnv::getEnvConfig('phabricator.mail-key');
-
- $hash = PhabricatorHash::weakDigest($mail_key.$global_mail_key.$phid);
+ $hash = PhabricatorHash::digestWithNamedKey(
+ $mail_key.$phid,
+ 'mail.object-address-key');
return substr($hash, 0, 16);
}
}
diff --git a/src/applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php b/src/applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php
index 37eb75761..391acb228 100644
--- a/src/applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php
+++ b/src/applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php
@@ -1,40 +1,70 @@
<?php
final class PhabricatorMailReceiverTestCase extends PhabricatorTestCase {
public function testAddressSimilarity() {
$env = PhabricatorEnv::beginScopedEnv();
$env->overrideEnvConfig('metamta.single-reply-handler-prefix', 'prefix');
$base = 'alincoln@example.com';
$same = array(
'alincoln@example.com',
'"Abrahamn Lincoln" <alincoln@example.com>',
'ALincoln@example.com',
'prefix+alincoln@example.com',
);
foreach ($same as $address) {
$this->assertTrue(
- PhabricatorMailReceiver::matchAddresses($base, $address),
+ PhabricatorMailUtil::matchAddresses(
+ new PhutilEmailAddress($base),
+ new PhutilEmailAddress($address)),
pht('Address %s', $address));
}
$diff = array(
'a.lincoln@example.com',
'aluncoln@example.com',
'prefixalincoln@example.com',
'badprefix+alincoln@example.com',
'bad+prefix+alincoln@example.com',
'prefix+alincoln+sufffix@example.com',
);
foreach ($diff as $address) {
$this->assertFalse(
- PhabricatorMailReceiver::matchAddresses($base, $address),
+ PhabricatorMailUtil::matchAddresses(
+ new PhutilEmailAddress($base),
+ new PhutilEmailAddress($address)),
pht('Address: %s', $address));
}
}
+ public function testReservedAddresses() {
+ $default_address = id(new PhabricatorMailEmailEngine())
+ ->newDefaultEmailAddress();
+
+ $void_address = id(new PhabricatorMailEmailEngine())
+ ->newVoidEmailAddress();
+
+ $map = array(
+ 'alincoln@example.com' => false,
+ 'sysadmin@example.com' => true,
+ 'hostmaster@example.com' => true,
+ '"Walter Ebmaster" <webmaster@example.com>' => true,
+ (string)$default_address => true,
+ (string)$void_address => true,
+ );
+
+ foreach ($map as $raw_address => $expect) {
+ $address = new PhutilEmailAddress($raw_address);
+
+ $this->assertEqual(
+ $expect,
+ PhabricatorMailUtil::isReservedAddress($address),
+ pht('Reserved: %s', $raw_address));
+ }
+ }
+
}
diff --git a/src/applications/metamta/receiver/__tests__/PhabricatorObjectMailReceiverTestCase.php b/src/applications/metamta/receiver/__tests__/PhabricatorObjectMailReceiverTestCase.php
index a697436d2..8cef83211 100644
--- a/src/applications/metamta/receiver/__tests__/PhabricatorObjectMailReceiverTestCase.php
+++ b/src/applications/metamta/receiver/__tests__/PhabricatorObjectMailReceiverTestCase.php
@@ -1,129 +1,134 @@
<?php
final class PhabricatorObjectMailReceiverTestCase
extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testDropUnconfiguredPublicMail() {
list($task, $user, $mail) = $this->buildMail('public');
$env = PhabricatorEnv::beginScopedEnv();
$env->overrideEnvConfig('metamta.public-replies', false);
$mail->save();
$mail->processReceivedMail();
$this->assertEqual(
MetaMTAReceivedMailStatus::STATUS_NO_PUBLIC_MAIL,
$mail->getStatus());
}
-/*
-
- TODO: Tasks don't support policies yet. Implement this once they do.
-
public function testDropPolicyViolationMail() {
- list($task, $user, $mail) = $this->buildMail('public');
+ list($task, $user, $mail) = $this->buildMail('policy');
- // TODO: Set task policy to "no one" here.
+ $task
+ ->setViewPolicy(PhabricatorPolicies::POLICY_NOONE)
+ ->setOwnerPHID(null)
+ ->save();
+
+ $env = PhabricatorEnv::beginScopedEnv();
+ $env->overrideEnvConfig('metamta.public-replies', true);
$mail->save();
$mail->processReceivedMail();
$this->assertEqual(
MetaMTAReceivedMailStatus::STATUS_POLICY_PROBLEM,
$mail->getStatus());
}
-*/
-
public function testDropInvalidObjectMail() {
list($task, $user, $mail) = $this->buildMail('404');
$mail->save();
$mail->processReceivedMail();
$this->assertEqual(
MetaMTAReceivedMailStatus::STATUS_NO_SUCH_OBJECT,
$mail->getStatus());
}
public function testDropUserMismatchMail() {
list($task, $user, $mail) = $this->buildMail('baduser');
$mail->save();
$mail->processReceivedMail();
$this->assertEqual(
MetaMTAReceivedMailStatus::STATUS_USER_MISMATCH,
$mail->getStatus());
}
public function testDropHashMismatchMail() {
list($task, $user, $mail) = $this->buildMail('badhash');
$mail->save();
$mail->processReceivedMail();
$this->assertEqual(
MetaMTAReceivedMailStatus::STATUS_HASH_MISMATCH,
$mail->getStatus());
}
private function buildMail($style) {
$user = $this->generateNewTestUser();
$task = id(new PhabricatorManiphestTaskTestDataGenerator())
->setViewer($user)
->generateObject();
$is_public = ($style === 'public');
$is_bad_hash = ($style == 'badhash');
$is_bad_user = ($style == 'baduser');
$is_404_object = ($style == '404');
if ($is_public) {
$user_identifier = 'public';
} else if ($is_bad_user) {
$user_identifier = $user->getID() + 1;
} else {
$user_identifier = $user->getID();
}
if ($is_bad_hash) {
$hash = PhabricatorObjectMailReceiver::computeMailHash('x', 'y');
} else {
$mail_key = PhabricatorMetaMTAMailProperties::loadMailKey($task);
$hash = PhabricatorObjectMailReceiver::computeMailHash(
$mail_key,
$is_public ? $task->getPHID() : $user->getPHID());
}
if ($is_404_object) {
$task_identifier = 'T'.($task->getID() + 1);
} else {
$task_identifier = 'T'.$task->getID();
}
$to = $task_identifier.'+'.$user_identifier.'+'.$hash.'@example.com';
$mail = new PhabricatorMetaMTAReceivedMail();
$mail->setHeaders(
array(
'Message-ID' => 'test@example.com',
'From' => $user->loadPrimaryEmail()->getAddress(),
'To' => $to,
));
+ $mail->setBodies(
+ array(
+ 'text' => 'test',
+ ));
+
return array($task, $user, $mail);
}
}
diff --git a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php
index 70b524b52..72e23ec2c 100644
--- a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php
+++ b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php
@@ -1,442 +1,411 @@
<?php
abstract class PhabricatorMailReplyHandler extends Phobject {
private $mailReceiver;
private $applicationEmail;
private $actor;
private $excludePHIDs = array();
private $unexpandablePHIDs = array();
final public function setMailReceiver($mail_receiver) {
$this->validateMailReceiver($mail_receiver);
$this->mailReceiver = $mail_receiver;
return $this;
}
final public function getMailReceiver() {
return $this->mailReceiver;
}
public function setApplicationEmail(
PhabricatorMetaMTAApplicationEmail $email) {
$this->applicationEmail = $email;
return $this;
}
public function getApplicationEmail() {
return $this->applicationEmail;
}
final public function setActor(PhabricatorUser $actor) {
$this->actor = $actor;
return $this;
}
final public function getActor() {
return $this->actor;
}
final public function setExcludeMailRecipientPHIDs(array $exclude) {
$this->excludePHIDs = $exclude;
return $this;
}
final public function getExcludeMailRecipientPHIDs() {
return $this->excludePHIDs;
}
public function setUnexpandablePHIDs(array $phids) {
$this->unexpandablePHIDs = $phids;
return $this;
}
public function getUnexpandablePHIDs() {
return $this->unexpandablePHIDs;
}
abstract public function validateMailReceiver($mail_receiver);
abstract public function getPrivateReplyHandlerEmailAddress(
PhabricatorUser $user);
public function getReplyHandlerDomain() {
return PhabricatorEnv::getEnvConfig('metamta.reply-handler-domain');
}
abstract protected function receiveEmail(
PhabricatorMetaMTAReceivedMail $mail);
public function processEmail(PhabricatorMetaMTAReceivedMail $mail) {
- $this->dropEmptyMail($mail);
-
return $this->receiveEmail($mail);
}
- private function dropEmptyMail(PhabricatorMetaMTAReceivedMail $mail) {
- $body = $mail->getCleanTextBody();
- $attachments = $mail->getAttachments();
-
- if (strlen($body) || $attachments) {
- return;
- }
-
- // Only send an error email if the user is talking to just Phabricator.
- // We can assume if there is only one "To" address it is a Phabricator
- // address since this code is running and everything.
- $is_direct_mail = (count($mail->getToAddresses()) == 1) &&
- (count($mail->getCCAddresses()) == 0);
-
- if ($is_direct_mail) {
- $status_code = MetaMTAReceivedMailStatus::STATUS_EMPTY;
- } else {
- $status_code = MetaMTAReceivedMailStatus::STATUS_EMPTY_IGNORED;
- }
-
- throw new PhabricatorMetaMTAReceivedMailProcessingException(
- $status_code,
- pht(
- 'Your message does not contain any body text or attachments, so '.
- 'Phabricator can not do anything useful with it. Make sure comment '.
- 'text appears at the top of your message: quoted replies, inline '.
- 'text, and signatures are discarded and ignored.'));
- }
-
public function supportsPrivateReplies() {
return (bool)$this->getReplyHandlerDomain() &&
!$this->supportsPublicReplies();
}
public function supportsPublicReplies() {
if (!PhabricatorEnv::getEnvConfig('metamta.public-replies')) {
return false;
}
if (!$this->getReplyHandlerDomain()) {
return false;
}
return (bool)$this->getPublicReplyHandlerEmailAddress();
}
final public function supportsReplies() {
return $this->supportsPrivateReplies() ||
$this->supportsPublicReplies();
}
public function getPublicReplyHandlerEmailAddress() {
return null;
}
protected function getDefaultPublicReplyHandlerEmailAddress($prefix) {
$receiver = $this->getMailReceiver();
$receiver_id = $receiver->getID();
$domain = $this->getReplyHandlerDomain();
// We compute a hash using the object's own PHID to prevent an attacker
// from blindly interacting with objects that they haven't ever received
// mail about by just sending to D1@, D2@, etc...
$mail_key = PhabricatorMetaMTAMailProperties::loadMailKey($receiver);
$hash = PhabricatorObjectMailReceiver::computeMailHash(
$mail_key,
$receiver->getPHID());
$address = "{$prefix}{$receiver_id}+public+{$hash}@{$domain}";
return $this->getSingleReplyHandlerPrefix($address);
}
protected function getSingleReplyHandlerPrefix($address) {
$single_handle_prefix = PhabricatorEnv::getEnvConfig(
'metamta.single-reply-handler-prefix');
return ($single_handle_prefix)
? $single_handle_prefix.'+'.$address
: $address;
}
protected function getDefaultPrivateReplyHandlerEmailAddress(
PhabricatorUser $user,
$prefix) {
$receiver = $this->getMailReceiver();
$receiver_id = $receiver->getID();
$user_id = $user->getID();
$mail_key = PhabricatorMetaMTAMailProperties::loadMailKey($receiver);
$hash = PhabricatorObjectMailReceiver::computeMailHash(
$mail_key,
$user->getPHID());
$domain = $this->getReplyHandlerDomain();
$address = "{$prefix}{$receiver_id}+{$user_id}+{$hash}@{$domain}";
return $this->getSingleReplyHandlerPrefix($address);
}
final protected function enhanceBodyWithAttachments(
$body,
array $attachments) {
if (!$attachments) {
return $body;
}
$files = id(new PhabricatorFileQuery())
->setViewer($this->getActor())
->withPHIDs($attachments)
->execute();
$output = array();
$output[] = $body;
// We're going to put all the non-images first in a list, then embed
// the images.
$head = array();
$tail = array();
foreach ($files as $file) {
if ($file->isViewableImage()) {
$tail[] = $file;
} else {
$head[] = $file;
}
}
if ($head) {
$list = array();
foreach ($head as $file) {
$list[] = ' - {'.$file->getMonogram().', layout=link}';
}
$output[] = implode("\n", $list);
}
if ($tail) {
$list = array();
foreach ($tail as $file) {
$list[] = '{'.$file->getMonogram().'}';
}
$output[] = implode("\n\n", $list);
}
$output = implode("\n\n", $output);
return rtrim($output);
}
/**
* Produce a list of mail targets for a given to/cc list.
*
* Each target should be sent a separate email, and contains the information
* required to generate it with appropriate permissions and configuration.
*
* @param list<phid> List of "To" PHIDs.
* @param list<phid> List of "CC" PHIDs.
* @return list<PhabricatorMailTarget> List of targets.
*/
final public function getMailTargets(array $raw_to, array $raw_cc) {
list($to, $cc) = $this->expandRecipientPHIDs($raw_to, $raw_cc);
list($to, $cc) = $this->loadRecipientUsers($to, $cc);
list($to, $cc) = $this->filterRecipientUsers($to, $cc);
if (!$to && !$cc) {
return array();
}
$template = id(new PhabricatorMailTarget())
->setRawToPHIDs($raw_to)
->setRawCCPHIDs($raw_cc);
// Set the public reply address as the default, if one exists. We
// might replace this with a private address later.
if ($this->supportsPublicReplies()) {
$reply_to = $this->getPublicReplyHandlerEmailAddress();
if ($reply_to) {
$template->setReplyTo($reply_to);
}
}
$supports_private_replies = $this->supportsPrivateReplies();
$mail_all = !PhabricatorEnv::getEnvConfig('metamta.one-mail-per-recipient');
$targets = array();
if ($mail_all) {
$target = id(clone $template)
->setViewer(PhabricatorUser::getOmnipotentUser())
->setToMap($to)
->setCCMap($cc);
$targets[] = $target;
} else {
$map = $to + $cc;
foreach ($map as $phid => $user) {
// Preserve the original To/Cc information on the target.
if (isset($to[$phid])) {
$target_to = array($phid => $user);
$target_cc = array();
} else {
$target_to = array();
$target_cc = array($phid => $user);
}
$target = id(clone $template)
->setViewer($user)
->setToMap($target_to)
->setCCMap($target_cc);
if ($supports_private_replies) {
$reply_to = $this->getPrivateReplyHandlerEmailAddress($user);
if ($reply_to) {
$target->setReplyTo($reply_to);
}
}
$targets[] = $target;
}
}
return $targets;
}
/**
* Expand lists of recipient PHIDs.
*
* This takes any compound recipients (like projects) and looks up all their
* members.
*
* @param list<phid> List of To PHIDs.
* @param list<phid> List of CC PHIDs.
* @return pair<list<phid>, list<phid>> Expanded PHID lists.
*/
private function expandRecipientPHIDs(array $to, array $cc) {
$to_result = array();
$cc_result = array();
// "Unexpandable" users have disengaged from an object (for example,
// by resigning from a revision).
// If such a user is still a direct recipient (for example, they're still
// on the Subscribers list) they're fair game, but group targets (like
// projects) will no longer include them when expanded.
$unexpandable = $this->getUnexpandablePHIDs();
$unexpandable = array_fuse($unexpandable);
$all_phids = array_merge($to, $cc);
if ($all_phids) {
$map = id(new PhabricatorMetaMTAMemberQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($all_phids)
->execute();
foreach ($to as $phid) {
foreach ($map[$phid] as $expanded) {
if ($expanded !== $phid) {
if (isset($unexpandable[$expanded])) {
continue;
}
}
$to_result[$expanded] = $expanded;
}
}
foreach ($cc as $phid) {
foreach ($map[$phid] as $expanded) {
if ($expanded !== $phid) {
if (isset($unexpandable[$expanded])) {
continue;
}
}
$cc_result[$expanded] = $expanded;
}
}
}
// Remove recipients from "CC" if they're also present in "To".
$cc_result = array_diff_key($cc_result, $to_result);
return array(array_values($to_result), array_values($cc_result));
}
/**
* Load @{class:PhabricatorUser} objects for each recipient.
*
* Invalid recipients are dropped from the results.
*
* @param list<phid> List of To PHIDs.
* @param list<phid> List of CC PHIDs.
* @return pair<wild, wild> Maps from PHIDs to users.
*/
private function loadRecipientUsers(array $to, array $cc) {
$to_result = array();
$cc_result = array();
$all_phids = array_merge($to, $cc);
if ($all_phids) {
// We need user settings here because we'll check translations later
// when generating mail.
$users = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($all_phids)
->needUserSettings(true)
->execute();
$users = mpull($users, null, 'getPHID');
foreach ($to as $phid) {
if (isset($users[$phid])) {
$to_result[$phid] = $users[$phid];
}
}
foreach ($cc as $phid) {
if (isset($users[$phid])) {
$cc_result[$phid] = $users[$phid];
}
}
}
return array($to_result, $cc_result);
}
/**
* Remove recipients who do not have permission to view the mail receiver.
*
* @param map<string, PhabricatorUser> Map of "To" users.
* @param map<string, PhabricatorUser> Map of "CC" users.
* @return pair<wild, wild> Filtered user maps.
*/
private function filterRecipientUsers(array $to, array $cc) {
$to_result = array();
$cc_result = array();
$all_users = $to + $cc;
if ($all_users) {
$can_see = array();
$object = $this->getMailReceiver();
foreach ($all_users as $phid => $user) {
$visible = PhabricatorPolicyFilter::hasCapability(
$user,
$object,
PhabricatorPolicyCapability::CAN_VIEW);
if ($visible) {
$can_see[$phid] = true;
}
}
foreach ($to as $phid => $user) {
if (!empty($can_see[$phid])) {
$to_result[$phid] = $all_users[$phid];
}
}
foreach ($cc as $phid => $user) {
if (!empty($can_see[$phid])) {
$cc_result[$phid] = $all_users[$phid];
}
}
}
return array($to_result, $cc_result);
}
}
diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php b/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php
index fb70b377a..f5673bed9 100644
--- a/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php
+++ b/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php
@@ -1,157 +1,154 @@
<?php
final class PhabricatorMetaMTAApplicationEmail
extends PhabricatorMetaMTADAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorDestructibleInterface,
PhabricatorSpacesInterface {
protected $applicationPHID;
protected $address;
protected $configData;
protected $spacePHID;
private $application = self::ATTACHABLE;
const CONFIG_DEFAULT_AUTHOR = 'config:default:author';
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'configData' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'address' => 'sort128',
),
self::CONFIG_KEY_SCHEMA => array(
'key_address' => array(
'columns' => array('address'),
'unique' => true,
),
'key_application' => array(
'columns' => array('applicationPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorMetaMTAApplicationEmailPHIDType::TYPECONST);
}
public static function initializeNewAppEmail(PhabricatorUser $actor) {
return id(new PhabricatorMetaMTAApplicationEmail())
->setSpacePHID($actor->getDefaultSpacePHID())
->setConfigData(array());
}
public function attachApplication(PhabricatorApplication $app) {
$this->application = $app;
return $this;
}
public function getApplication() {
return self::assertAttached($this->application);
}
public function setConfigValue($key, $value) {
$this->configData[$key] = $value;
return $this;
}
public function getConfigValue($key, $default = null) {
return idx($this->configData, $key, $default);
}
+ public function getDefaultAuthorPHID() {
+ return $this->getConfigValue(self::CONFIG_DEFAULT_AUTHOR);
+ }
public function getInUseMessage() {
$applications = PhabricatorApplication::getAllApplications();
$applications = mpull($applications, null, 'getPHID');
$application = idx(
$applications,
$this->getApplicationPHID());
if ($application) {
$message = pht(
'The address %s is configured to be used by the %s Application.',
$this->getAddress(),
$application->getName());
} else {
$message = pht(
'The address %s is configured to be used by an application.',
$this->getAddress());
}
return $message;
}
+ public function newAddress() {
+ return new PhutilEmailAddress($this->getAddress());
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return $this->getApplication()->getPolicy($capability);
}
public function hasAutomaticCapability(
$capability,
PhabricatorUser $viewer) {
return $this->getApplication()->hasAutomaticCapability(
$capability,
$viewer);
}
public function describeAutomaticCapability($capability) {
return $this->getApplication()->describeAutomaticCapability($capability);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorMetaMTAApplicationEmailEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorMetaMTAApplicationEmailTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
- return $timeline;
- }
-
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->delete();
}
/* -( PhabricatorSpacesInterface )----------------------------------------- */
public function getSpacePHID() {
return $this->spacePHID;
}
}
diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php
index e7ebf6dc8..cc3ae82be 100644
--- a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php
+++ b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php
@@ -1,1641 +1,1265 @@
<?php
/**
* @task recipients Managing Recipients
*/
final class PhabricatorMetaMTAMail
extends PhabricatorMetaMTADAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface {
const RETRY_DELAY = 5;
protected $actorPHID;
protected $parameters = array();
protected $status;
protected $message;
protected $relatedPHID;
private $recipientExpansionMap;
private $routingMap;
public function __construct() {
$this->status = PhabricatorMailOutboundStatus::STATUS_QUEUE;
$this->parameters = array(
'sensitive' => true,
'mustEncrypt' => false,
);
parent::__construct();
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'parameters' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'actorPHID' => 'phid?',
'status' => 'text32',
'relatedPHID' => 'phid?',
// T6203/NULLABILITY
// This should just be empty if there's no body.
'message' => 'text?',
),
self::CONFIG_KEY_SCHEMA => array(
'status' => array(
'columns' => array('status'),
),
'key_actorPHID' => array(
'columns' => array('actorPHID'),
),
'relatedPHID' => array(
'columns' => array('relatedPHID'),
),
'key_created' => array(
'columns' => array('dateCreated'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorMetaMTAMailPHIDType::TYPECONST);
}
protected function setParam($param, $value) {
$this->parameters[$param] = $value;
return $this;
}
protected function getParam($param, $default = null) {
// Some old mail was saved without parameters because no parameters were
// set or encoding failed. Recover in these cases so we can perform
// mail migrations, see T9251.
if (!is_array($this->parameters)) {
$this->parameters = array();
}
return idx($this->parameters, $param, $default);
}
/**
* These tags are used to allow users to opt out of receiving certain types
* of mail, like updates when a task's projects change.
*
* @param list<const>
* @return this
*/
public function setMailTags(array $tags) {
$this->setParam('mailtags', array_unique($tags));
return $this;
}
public function getMailTags() {
return $this->getParam('mailtags', array());
}
/**
* In Gmail, conversations will be broken if you reply to a thread and the
* server sends back a response without referencing your Message-ID, even if
* it references a Message-ID earlier in the thread. To avoid this, use the
* parent email's message ID explicitly if it's available. This overwrites the
* "In-Reply-To" and "References" headers we would otherwise generate. This
* needs to be set whenever an action is triggered by an email message. See
* T251 for more details.
*
* @param string The "Message-ID" of the email which precedes this one.
* @return this
*/
public function setParentMessageID($id) {
$this->setParam('parent-message-id', $id);
return $this;
}
public function getParentMessageID() {
return $this->getParam('parent-message-id');
}
public function getSubject() {
return $this->getParam('subject');
}
public function addTos(array $phids) {
$phids = array_unique($phids);
$this->setParam('to', $phids);
return $this;
}
public function addRawTos(array $raw_email) {
// Strip addresses down to bare emails, since the MailAdapter API currently
// requires we pass it just the address (like `alincoln@logcabin.org`), not
// a full string like `"Abraham Lincoln" <alincoln@logcabin.org>`.
foreach ($raw_email as $key => $email) {
$object = new PhutilEmailAddress($email);
$raw_email[$key] = $object->getAddress();
}
$this->setParam('raw-to', $raw_email);
return $this;
}
public function addCCs(array $phids) {
$phids = array_unique($phids);
$this->setParam('cc', $phids);
return $this;
}
public function setExcludeMailRecipientPHIDs(array $exclude) {
$this->setParam('exclude', $exclude);
return $this;
}
private function getExcludeMailRecipientPHIDs() {
return $this->getParam('exclude', array());
}
public function setMutedPHIDs(array $muted) {
$this->setParam('muted', $muted);
return $this;
}
private function getMutedPHIDs() {
return $this->getParam('muted', array());
}
public function setForceHeraldMailRecipientPHIDs(array $force) {
$this->setParam('herald-force-recipients', $force);
return $this;
}
private function getForceHeraldMailRecipientPHIDs() {
return $this->getParam('herald-force-recipients', array());
}
public function addPHIDHeaders($name, array $phids) {
$phids = array_unique($phids);
foreach ($phids as $phid) {
$this->addHeader($name, '<'.$phid.'>');
}
return $this;
}
public function addHeader($name, $value) {
$this->parameters['headers'][] = array($name, $value);
return $this;
}
- public function addAttachment(PhabricatorMetaMTAAttachment $attachment) {
+ public function getHeaders() {
+ return $this->getParam('headers', array());
+ }
+
+ public function addAttachment(PhabricatorMailAttachment $attachment) {
$this->parameters['attachments'][] = $attachment->toDictionary();
return $this;
}
public function getAttachments() {
- $dicts = $this->getParam('attachments');
+ $dicts = $this->getParam('attachments', array());
$result = array();
foreach ($dicts as $dict) {
- $result[] = PhabricatorMetaMTAAttachment::newFromDictionary($dict);
+ $result[] = PhabricatorMailAttachment::newFromDictionary($dict);
}
return $result;
}
public function getAttachmentFilePHIDs() {
$file_phids = array();
$dictionaries = $this->getParam('attachments');
if ($dictionaries) {
foreach ($dictionaries as $dictionary) {
$file_phid = idx($dictionary, 'filePHID');
if ($file_phid) {
$file_phids[] = $file_phid;
}
}
}
return $file_phids;
}
public function loadAttachedFiles(PhabricatorUser $viewer) {
$file_phids = $this->getAttachmentFilePHIDs();
if (!$file_phids) {
return array();
}
return id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs($file_phids)
->execute();
}
public function setAttachments(array $attachments) {
- assert_instances_of($attachments, 'PhabricatorMetaMTAAttachment');
+ assert_instances_of($attachments, 'PhabricatorMailAttachment');
$this->setParam('attachments', mpull($attachments, 'toDictionary'));
return $this;
}
public function setFrom($from) {
$this->setParam('from', $from);
$this->setActorPHID($from);
return $this;
}
public function getFrom() {
return $this->getParam('from');
}
public function setRawFrom($raw_email, $raw_name) {
$this->setParam('raw-from', array($raw_email, $raw_name));
return $this;
}
+ public function getRawFrom() {
+ return $this->getParam('raw-from');
+ }
+
public function setReplyTo($reply_to) {
$this->setParam('reply-to', $reply_to);
return $this;
}
+ public function getReplyTo() {
+ return $this->getParam('reply-to');
+ }
+
public function setSubject($subject) {
$this->setParam('subject', $subject);
return $this;
}
public function setSubjectPrefix($prefix) {
$this->setParam('subject-prefix', $prefix);
return $this;
}
+ public function getSubjectPrefix() {
+ return $this->getParam('subject-prefix');
+ }
+
public function setVarySubjectPrefix($prefix) {
$this->setParam('vary-subject-prefix', $prefix);
return $this;
}
+ public function getVarySubjectPrefix() {
+ return $this->getParam('vary-subject-prefix');
+ }
+
public function setBody($body) {
$this->setParam('body', $body);
return $this;
}
public function setSensitiveContent($bool) {
$this->setParam('sensitive', $bool);
return $this;
}
public function hasSensitiveContent() {
return $this->getParam('sensitive', true);
}
public function setMustEncrypt($bool) {
return $this->setParam('mustEncrypt', $bool);
}
public function getMustEncrypt() {
return $this->getParam('mustEncrypt', false);
}
public function setMustEncryptURI($uri) {
return $this->setParam('mustEncrypt.uri', $uri);
}
public function getMustEncryptURI() {
return $this->getParam('mustEncrypt.uri');
}
public function setMustEncryptSubject($subject) {
return $this->setParam('mustEncrypt.subject', $subject);
}
public function getMustEncryptSubject() {
return $this->getParam('mustEncrypt.subject');
}
public function setMustEncryptReasons(array $reasons) {
return $this->setParam('mustEncryptReasons', $reasons);
}
public function getMustEncryptReasons() {
return $this->getParam('mustEncryptReasons', array());
}
public function setMailStamps(array $stamps) {
return $this->setParam('stamps', $stamps);
}
public function getMailStamps() {
return $this->getParam('stamps', array());
}
public function setMailStampMetadata($metadata) {
return $this->setParam('stampMetadata', $metadata);
}
public function getMailStampMetadata() {
return $this->getParam('stampMetadata', array());
}
public function getMailerKey() {
return $this->getParam('mailer.key');
}
public function setTryMailers(array $mailers) {
return $this->setParam('mailers.try', $mailers);
}
public function setHTMLBody($html) {
$this->setParam('html-body', $html);
return $this;
}
public function getBody() {
return $this->getParam('body');
}
public function getHTMLBody() {
return $this->getParam('html-body');
}
public function setIsErrorEmail($is_error) {
$this->setParam('is-error', $is_error);
return $this;
}
public function getIsErrorEmail() {
return $this->getParam('is-error', false);
}
public function getToPHIDs() {
return $this->getParam('to', array());
}
public function getRawToAddresses() {
return $this->getParam('raw-to', array());
}
public function getCcPHIDs() {
return $this->getParam('cc', array());
}
+ public function setMessageType($message_type) {
+ return $this->setParam('message.type', $message_type);
+ }
+
+ public function getMessageType() {
+ return $this->getParam(
+ 'message.type',
+ PhabricatorMailEmailMessage::MESSAGETYPE);
+ }
+
+
+
/**
* Force delivery of a message, even if recipients have preferences which
* would otherwise drop the message.
*
* This is primarily intended to let users who don't want any email still
* receive things like password resets.
*
* @param bool True to force delivery despite user preferences.
* @return this
*/
public function setForceDelivery($force) {
$this->setParam('force', $force);
return $this;
}
public function getForceDelivery() {
return $this->getParam('force', false);
}
/**
* Flag that this is an auto-generated bulk message and should have bulk
* headers added to it if appropriate. Broadly, this means some flavor of
* "Precedence: bulk" or similar, but is implementation and configuration
* dependent.
*
* @param bool True if the mail is automated bulk mail.
* @return this
*/
public function setIsBulk($is_bulk) {
$this->setParam('is-bulk', $is_bulk);
return $this;
}
+ public function getIsBulk() {
+ return $this->getParam('is-bulk');
+ }
+
/**
* Use this method to set an ID used for message threading. MetaMTA will
* set appropriate headers (Message-ID, In-Reply-To, References and
* Thread-Index) based on the capabilities of the underlying mailer.
*
* @param string Unique identifier, appropriate for use in a Message-ID,
* In-Reply-To or References headers.
* @param bool If true, indicates this is the first message in the thread.
* @return this
*/
public function setThreadID($thread_id, $is_first_message = false) {
$this->setParam('thread-id', $thread_id);
$this->setParam('is-first-message', $is_first_message);
return $this;
}
+ public function getThreadID() {
+ return $this->getParam('thread-id');
+ }
+
+ public function getIsFirstMessage() {
+ return (bool)$this->getParam('is-first-message');
+ }
+
/**
* Save a newly created mail to the database. The mail will eventually be
* delivered by the MetaMTA daemon.
*
* @return this
*/
public function saveAndSend() {
return $this->save();
}
/**
* @return this
*/
public function save() {
if ($this->getID()) {
return parent::save();
}
// NOTE: When mail is sent from CLI scripts that run tasks in-process, we
// may re-enter this method from within scheduleTask(). The implementation
// is intended to avoid anything awkward if we end up reentering this
// method.
$this->openTransaction();
// Save to generate a mail ID and PHID.
$result = parent::save();
// Write the recipient edges.
$editor = new PhabricatorEdgeEditor();
$edge_type = PhabricatorMetaMTAMailHasRecipientEdgeType::EDGECONST;
$recipient_phids = array_merge(
$this->getToPHIDs(),
$this->getCcPHIDs());
$expanded_phids = $this->expandRecipients($recipient_phids);
$all_phids = array_unique(array_merge(
$recipient_phids,
$expanded_phids));
foreach ($all_phids as $curr_phid) {
$editor->addEdge($this->getPHID(), $edge_type, $curr_phid);
}
$editor->save();
$this->saveTransaction();
// Queue a task to send this mail.
$mailer_task = PhabricatorWorker::scheduleTask(
'PhabricatorMetaMTAWorker',
$this->getID(),
array(
'priority' => PhabricatorWorker::PRIORITY_ALERTS,
));
return $result;
}
/**
* Attempt to deliver an email immediately, in this process.
*
* @return void
*/
public function sendNow() {
if ($this->getStatus() != PhabricatorMailOutboundStatus::STATUS_QUEUE) {
throw new Exception(pht('Trying to send an already-sent mail!'));
}
$mailers = self::newMailers(
array(
'outbound' => true,
+ 'media' => array(
+ $this->getMessageType(),
+ ),
));
$try_mailers = $this->getParam('mailers.try');
if ($try_mailers) {
$mailers = mpull($mailers, null, 'getKey');
$mailers = array_select_keys($mailers, $try_mailers);
}
return $this->sendWithMailers($mailers);
}
public static function newMailers(array $constraints) {
PhutilTypeSpec::checkMap(
$constraints,
array(
'types' => 'optional list<string>',
'inbound' => 'optional bool',
'outbound' => 'optional bool',
+ 'media' => 'optional list<string>',
));
$mailers = array();
$config = PhabricatorEnv::getEnvConfig('cluster.mailers');
- if ($config === null) {
- $mailer = PhabricatorEnv::newObjectFromConfig('metamta.mail-adapter');
-
- $defaults = $mailer->newDefaultOptions();
- $options = $mailer->newLegacyOptions();
-
- $options = $options + $defaults;
- $mailer
- ->setKey('default')
- ->setPriority(-1)
- ->setOptions($options);
+ $adapters = PhabricatorMailAdapter::getAllAdapters();
+ $next_priority = -1;
- $mailers[] = $mailer;
- } else {
- $adapters = PhabricatorMailImplementationAdapter::getAllAdapters();
- $next_priority = -1;
-
- foreach ($config as $spec) {
- $type = $spec['type'];
- if (!isset($adapters[$type])) {
- throw new Exception(
- pht(
- 'Unknown mailer ("%s")!',
- $type));
- }
+ foreach ($config as $spec) {
+ $type = $spec['type'];
+ if (!isset($adapters[$type])) {
+ throw new Exception(
+ pht(
+ 'Unknown mailer ("%s")!',
+ $type));
+ }
- $key = $spec['key'];
- $mailer = id(clone $adapters[$type])
- ->setKey($key);
+ $key = $spec['key'];
+ $mailer = id(clone $adapters[$type])
+ ->setKey($key);
- $priority = idx($spec, 'priority');
- if (!$priority) {
- $priority = $next_priority;
- $next_priority--;
- }
- $mailer->setPriority($priority);
+ $priority = idx($spec, 'priority');
+ if (!$priority) {
+ $priority = $next_priority;
+ $next_priority--;
+ }
+ $mailer->setPriority($priority);
- $defaults = $mailer->newDefaultOptions();
- $options = idx($spec, 'options', array()) + $defaults;
- $mailer->setOptions($options);
+ $defaults = $mailer->newDefaultOptions();
+ $options = idx($spec, 'options', array()) + $defaults;
+ $mailer->setOptions($options);
- $mailer->setSupportsInbound(idx($spec, 'inbound', true));
- $mailer->setSupportsOutbound(idx($spec, 'outbound', true));
+ $mailer->setSupportsInbound(idx($spec, 'inbound', true));
+ $mailer->setSupportsOutbound(idx($spec, 'outbound', true));
- $mailers[] = $mailer;
+ $media = idx($spec, 'media');
+ if ($media !== null) {
+ $mailer->setMedia($media);
}
+
+ $mailers[] = $mailer;
}
// Remove mailers with the wrong types.
if (isset($constraints['types'])) {
$types = $constraints['types'];
$types = array_fuse($types);
foreach ($mailers as $key => $mailer) {
$mailer_type = $mailer->getAdapterType();
if (!isset($types[$mailer_type])) {
unset($mailers[$key]);
}
}
}
// If we're only looking for inbound mailers, remove mailers with inbound
// support disabled.
if (!empty($constraints['inbound'])) {
foreach ($mailers as $key => $mailer) {
if (!$mailer->getSupportsInbound()) {
unset($mailers[$key]);
}
}
}
// If we're only looking for outbound mailers, remove mailers with outbound
// support disabled.
if (!empty($constraints['outbound'])) {
foreach ($mailers as $key => $mailer) {
if (!$mailer->getSupportsOutbound()) {
unset($mailers[$key]);
}
}
}
+ // Select only the mailers which can transmit messages with requested media
+ // types.
+ if (!empty($constraints['media'])) {
+ foreach ($mailers as $key => $mailer) {
+ $supports_any = false;
+ foreach ($constraints['media'] as $medium) {
+ if ($mailer->supportsMessageType($medium)) {
+ $supports_any = true;
+ break;
+ }
+ }
+
+ if (!$supports_any) {
+ unset($mailers[$key]);
+ }
+ }
+ }
+
$sorted = array();
$groups = mgroup($mailers, 'getPriority');
krsort($groups);
foreach ($groups as $group) {
// Reorder services within the same priority group randomly.
shuffle($group);
foreach ($group as $mailer) {
$sorted[] = $mailer;
}
}
- foreach ($sorted as $mailer) {
- $mailer->prepareForSend();
- }
-
return $sorted;
}
public function sendWithMailers(array $mailers) {
if (!$mailers) {
$any_mailers = self::newMailers(array());
// NOTE: We can end up here with some custom list of "$mailers", like
// from a unit test. In that case, this message could be misleading. We
// can't really tell if the caller made up the list, so just assume they
// aren't tricking us.
if ($any_mailers) {
$void_message = pht(
'No configured mailers support sending outbound mail.');
} else {
$void_message = pht(
'No mailers are configured.');
}
return $this
->setStatus(PhabricatorMailOutboundStatus::STATUS_VOID)
->setMessage($void_message)
->save();
}
- $exceptions = array();
- foreach ($mailers as $template_mailer) {
- $mailer = null;
+ $actors = $this->loadAllActors();
+
+ // If we're sending one mail to everyone, 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($this->getToPHIDs());
+ if (!$target_phid) {
+ $target_phid = head($this->getCcPHIDs());
+ }
+ $preferences = $this->loadPreferences($target_phid);
+
+ // Attach any files we're about to send to this message, so the recipients
+ // can view them.
+ $viewer = PhabricatorUser::getOmnipotentUser();
+ $files = $this->loadAttachedFiles($viewer);
+ foreach ($files as $file) {
+ $file->attachToObject($this->getPHID());
+ }
+
+ $type_map = PhabricatorMailExternalMessage::getAllMessageTypes();
+ $type = idx($type_map, $this->getMessageType());
+ if (!$type) {
+ throw new Exception(
+ pht(
+ 'Unable to send message with unknown message type "%s".',
+ $type));
+ }
+ $exceptions = array();
+ foreach ($mailers as $mailer) {
try {
- $mailer = $this->buildMailer($template_mailer);
+ $message = $type->newMailMessageEngine()
+ ->setMailer($mailer)
+ ->setMail($this)
+ ->setActors($actors)
+ ->setPreferences($preferences)
+ ->newMessage($mailer);
} catch (Exception $ex) {
$exceptions[] = $ex;
continue;
}
- if (!$mailer) {
- // If we don't get a mailer back, that means the mail doesn't
- // actually need to be sent (for example, because recipients have
- // declined to receive the mail). Void it and return.
+ if (!$message) {
+ // If we don't get a message back, that means the mail doesn't actually
+ // need to be sent (for example, because recipients have declined to
+ // receive the mail). Void it and return.
return $this
->setStatus(PhabricatorMailOutboundStatus::STATUS_VOID)
->save();
}
try {
- $ok = $mailer->send();
- if (!$ok) {
- // TODO: At some point, we should clean this up and make all mailers
- // throw.
- throw new Exception(
- pht(
- 'Mail adapter encountered an unexpected, unspecified '.
- 'failure.'));
- }
+ $mailer->sendMessage($message);
} catch (PhabricatorMetaMTAPermanentFailureException $ex) {
// If any mailer raises a permanent failure, stop trying to send the
// mail with other mailers.
$this
->setStatus(PhabricatorMailOutboundStatus::STATUS_FAIL)
->setMessage($ex->getMessage())
->save();
throw $ex;
} catch (Exception $ex) {
$exceptions[] = $ex;
continue;
}
// Keep track of which mailer actually ended up accepting the message.
$mailer_key = $mailer->getKey();
if ($mailer_key !== null) {
$this->setParam('mailer.key', $mailer_key);
}
+ // Now that we sent the message, store the final deliverability outcomes
+ // and reasoning so we can explain why things happened the way they did.
+ $actor_list = array();
+ foreach ($actors as $actor) {
+ $actor_list[$actor->getPHID()] = array(
+ 'deliverable' => $actor->isDeliverable(),
+ 'reasons' => $actor->getDeliverabilityReasons(),
+ );
+ }
+ $this->setParam('actors.sent', $actor_list);
+ $this->setParam('routing.sent', $this->getParam('routing'));
+ $this->setParam('routingmap.sent', $this->getRoutingRuleMap());
+
return $this
->setStatus(PhabricatorMailOutboundStatus::STATUS_SENT)
->save();
}
// If we make it here, no mailer could send the mail but no mailer failed
// permanently either. We update the error message for the mail, but leave
// it in the current status (usually, STATUS_QUEUE) and try again later.
$messages = array();
foreach ($exceptions as $ex) {
$messages[] = $ex->getMessage();
}
$messages = implode("\n\n", $messages);
$this
->setMessage($messages)
->save();
if (count($exceptions) === 1) {
throw head($exceptions);
}
throw new PhutilAggregateException(
pht('Encountered multiple exceptions while transmitting mail.'),
$exceptions);
}
- private function buildMailer(PhabricatorMailImplementationAdapter $mailer) {
- $headers = $this->generateHeaders();
-
- $params = $this->parameters;
-
- $actors = $this->loadAllActors();
- $deliverable_actors = $this->filterDeliverableActors($actors);
-
- $default_from = PhabricatorEnv::getEnvConfig('metamta.default-address');
- if (empty($params['from'])) {
- $mailer->setFrom($default_from);
- }
-
- $is_first = idx($params, 'is-first-message');
- unset($params['is-first-message']);
-
- $is_threaded = (bool)idx($params, 'thread-id');
- $must_encrypt = $this->getMustEncrypt();
-
- $reply_to_name = idx($params, 'reply-to-name', '');
- unset($params['reply-to-name']);
-
- $add_cc = array();
- $add_to = array();
-
- // If we're sending one mail to everyone, 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()));
- }
-
- $preferences = $this->loadPreferences($target_phid);
-
- foreach ($params as $key => $value) {
- switch ($key) {
- case 'raw-from':
- list($from_email, $from_name) = $value;
- $mailer->setFrom($from_email, $from_name);
- break;
- case 'from':
- // If the mail content must be encrypted, disguise the sender.
- if ($must_encrypt) {
- $mailer->setFrom($default_from, pht('Phabricator'));
- break;
- }
-
- $from = $value;
- $actor_email = null;
- $actor_name = null;
- $actor = idx($actors, $from);
- if ($actor) {
- $actor_email = $actor->getEmailAddress();
- $actor_name = $actor->getName();
- }
- $can_send_as_user = $actor_email &&
- PhabricatorEnv::getEnvConfig('metamta.can-send-as-user');
-
- if ($can_send_as_user) {
- $mailer->setFrom($actor_email, $actor_name);
- } else {
- $from_email = coalesce($actor_email, $default_from);
- $from_name = coalesce($actor_name, pht('Phabricator'));
-
- if (empty($params['reply-to'])) {
- $params['reply-to'] = $from_email;
- $params['reply-to-name'] = $from_name;
- }
-
- $mailer->setFrom($default_from, $from_name);
- }
- break;
- case 'reply-to':
- $mailer->addReplyTo($value, $reply_to_name);
- break;
- case 'to':
- $to_phids = $this->expandRecipients($value);
- $to_actors = array_select_keys($deliverable_actors, $to_phids);
- $add_to = array_merge(
- $add_to,
- mpull($to_actors, 'getEmailAddress'));
- break;
- case 'raw-to':
- $add_to = array_merge($add_to, $value);
- break;
- case 'cc':
- $cc_phids = $this->expandRecipients($value);
- $cc_actors = array_select_keys($deliverable_actors, $cc_phids);
- $add_cc = array_merge(
- $add_cc,
- mpull($cc_actors, 'getEmailAddress'));
- break;
- case 'attachments':
- $attached_viewer = PhabricatorUser::getOmnipotentUser();
- $files = $this->loadAttachedFiles($attached_viewer);
- foreach ($files as $file) {
- $file->attachToObject($this->getPHID());
- }
-
- // If the mail content must be encrypted, don't add attachments.
- if ($must_encrypt) {
- break;
- }
-
- $value = $this->getAttachments();
- foreach ($value as $attachment) {
- $mailer->addAttachment(
- $attachment->getData(),
- $attachment->getFilename(),
- $attachment->getMimeType());
- }
- break;
- case 'subject':
- $subject = array();
-
- if ($is_threaded) {
- if ($this->shouldAddRePrefix($preferences)) {
- $subject[] = 'Re:';
- }
- }
-
- $subject[] = trim(idx($params, 'subject-prefix'));
-
- // If mail content must be encrypted, we replace the subject with
- // a generic one.
- if ($must_encrypt) {
- $encrypt_subject = $this->getMustEncryptSubject();
- if (!strlen($encrypt_subject)) {
- $encrypt_subject = pht('Object Updated');
- }
- $subject[] = $encrypt_subject;
- } else {
- $vary_prefix = idx($params, 'vary-subject-prefix');
- if ($vary_prefix != '') {
- if ($this->shouldVarySubject($preferences)) {
- $subject[] = $vary_prefix;
- }
- }
-
- $subject[] = $value;
- }
-
- $mailer->setSubject(implode(' ', array_filter($subject)));
- break;
- case 'thread-id':
-
- // NOTE: Gmail freaks out about In-Reply-To and References which
- // aren't in the form "<string@domain.tld>"; this is also required
- // by RFC 2822, although some clients are more liberal in what they
- // accept.
- $domain = PhabricatorEnv::getEnvConfig('metamta.domain');
- $value = '<'.$value.'@'.$domain.'>';
-
- if ($is_first && $mailer->supportsMessageIDHeader()) {
- $headers[] = array('Message-ID', $value);
- } else {
- $in_reply_to = $value;
- $references = array($value);
- $parent_id = $this->getParentMessageID();
- if ($parent_id) {
- $in_reply_to = $parent_id;
- // By RFC 2822, the most immediate parent should appear last
- // in the "References" header, so this order is intentional.
- $references[] = $parent_id;
- }
- $references = implode(' ', $references);
- $headers[] = array('In-Reply-To', $in_reply_to);
- $headers[] = array('References', $references);
- }
- $thread_index = $this->generateThreadIndex($value, $is_first);
- $headers[] = array('Thread-Index', $thread_index);
- break;
- default:
- // Other parameters are handled elsewhere or are not relevant to
- // constructing the message.
- break;
- }
- }
-
- $stamps = $this->getMailStamps();
- if ($stamps) {
- $headers[] = array('X-Phabricator-Stamps', implode(' ', $stamps));
- }
-
- $raw_body = idx($params, 'body', '');
- $body = $raw_body;
- if ($must_encrypt) {
- $parts = array();
-
- $encrypt_uri = $this->getMustEncryptURI();
- if (!strlen($encrypt_uri)) {
- $encrypt_phid = $this->getRelatedPHID();
- if ($encrypt_phid) {
- $encrypt_uri = urisprintf(
- '/object/%s/',
- $encrypt_phid);
- }
- }
-
- if (strlen($encrypt_uri)) {
- $parts[] = pht(
- 'This secure message is notifying you of a change to this object:');
- $parts[] = PhabricatorEnv::getProductionURI($encrypt_uri);
- }
-
- $parts[] = pht(
- 'The content for this message can only be transmitted over a '.
- 'secure channel. To view the message content, follow this '.
- 'link:');
-
- $parts[] = PhabricatorEnv::getProductionURI($this->getURI());
-
- $body = implode("\n\n", $parts);
- } else {
- $body = $raw_body;
- }
-
- $body_limit = PhabricatorEnv::getEnvConfig('metamta.email-body-limit');
- if (strlen($body) > $body_limit) {
- $body = id(new PhutilUTF8StringTruncator())
- ->setMaximumBytes($body_limit)
- ->truncateString($body);
- $body .= "\n";
- $body .= pht('(This email was truncated at %d bytes.)', $body_limit);
- }
- $mailer->setBody($body);
- $body_limit -= strlen($body);
-
- // If we sent a different message body than we were asked to, record
- // what we actually sent to make debugging and diagnostics easier.
- if ($body !== $raw_body) {
- $this->setParam('body.sent', $body);
- }
-
- if ($must_encrypt) {
- $send_html = false;
- } else {
- $send_html = $this->shouldSendHTML($preferences);
- }
-
- if ($send_html) {
- $html_body = idx($params, 'html-body');
- if (strlen($html_body)) {
- // NOTE: We just drop the entire HTML body if it won't fit. Safely
- // truncating HTML is hard, and we already have the text body to fall
- // back to.
- if (strlen($html_body) <= $body_limit) {
- $mailer->setHTMLBody($html_body);
- $body_limit -= strlen($html_body);
- }
- }
- }
-
- // Pass the headers to the mailer, then save the state so we can show
- // them in the web UI. If the mail must be encrypted, we remove headers
- // which are not on a strict whitelist to avoid disclosing information.
- $filtered_headers = $this->filterHeaders($headers, $must_encrypt);
- foreach ($filtered_headers as $header) {
- list($header_key, $header_value) = $header;
- $mailer->addHeader($header_key, $header_value);
- }
- $this->setParam('headers.unfiltered', $headers);
- $this->setParam('headers.sent', $filtered_headers);
-
- // Save the final deliverability outcomes and reasoning so we can
- // explain why things happened the way they did.
- $actor_list = array();
- foreach ($actors as $actor) {
- $actor_list[$actor->getPHID()] = array(
- 'deliverable' => $actor->isDeliverable(),
- 'reasons' => $actor->getDeliverabilityReasons(),
- );
- }
- $this->setParam('actors.sent', $actor_list);
-
- $this->setParam('routing.sent', $this->getParam('routing'));
- $this->setParam('routingmap.sent', $this->getRoutingRuleMap());
-
- if (!$add_to && !$add_cc) {
- $this->setMessage(
- pht(
- 'Message has no valid recipients: all To/Cc are disabled, '.
- 'invalid, or configured not to receive this mail.'));
-
- return null;
- }
-
- if ($this->getIsErrorEmail()) {
- $all_recipients = array_merge($add_to, $add_cc);
- if ($this->shouldRateLimitMail($all_recipients)) {
- $this->setMessage(
- pht(
- 'This is an error email, but one or more recipients have '.
- 'exceeded the error email rate limit. Declining to deliver '.
- 'message.'));
-
- return null;
- }
- }
-
- if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
- $this->setMessage(
- pht(
- 'Phabricator is running in silent mode. See `%s` '.
- 'in the configuration to change this setting.',
- 'phabricator.silent'));
-
- return null;
- }
-
- // Some mailers require a valid "To:" in order to deliver mail. If we
- // don't have any "To:", try to fill it in with a placeholder "To:".
- // If that also fails, move the "Cc:" line to "To:".
- if (!$add_to) {
- $placeholder_key = 'metamta.placeholder-to-recipient';
- $placeholder = PhabricatorEnv::getEnvConfig($placeholder_key);
- if ($placeholder !== null) {
- $add_to = array($placeholder);
- } else {
- $add_to = $add_cc;
- $add_cc = array();
- }
- }
-
- $add_to = array_unique($add_to);
- $add_cc = array_diff(array_unique($add_cc), $add_to);
-
- $mailer->addTos($add_to);
- if ($add_cc) {
- $mailer->addCCs($add_cc);
- }
-
- return $mailer;
- }
-
- private function generateThreadIndex($seed, $is_first_mail) {
- // When threading, Outlook ignores the 'References' and 'In-Reply-To'
- // headers that most clients use. Instead, it uses a custom 'Thread-Index'
- // header. The format of this header is something like this (from
- // camel-exchange-folder.c in Evolution Exchange):
-
- /* A new post to a folder gets a 27-byte-long thread index. (The value
- * is apparently unique but meaningless.) Each reply to a post gets a
- * 32-byte-long thread index whose first 27 bytes are the same as the
- * parent's thread index. Each reply to any of those gets a
- * 37-byte-long thread index, etc. The Thread-Index header contains a
- * base64 representation of this value.
- */
-
- // The specific implementation uses a 27-byte header for the first email
- // a recipient receives, and a random 5-byte suffix (32 bytes total)
- // thereafter. This means that all the replies are (incorrectly) siblings,
- // but it would be very difficult to keep track of the entire tree and this
- // gets us reasonable client behavior.
-
- $base = substr(md5($seed), 0, 27);
- if (!$is_first_mail) {
- // Not totally sure, but it seems like outlook orders replies by
- // thread-index rather than timestamp, so to get these to show up in the
- // right order we use the time as the last 4 bytes.
- $base .= ' '.pack('N', time());
- }
-
- return base64_encode($base);
- }
public static function shouldMailEachRecipient() {
return PhabricatorEnv::getEnvConfig('metamta.one-mail-per-recipient');
}
/* -( Managing Recipients )------------------------------------------------ */
/**
* Get all of the recipients for this mail, after preference filters are
* applied. This list has all objects to whom delivery will be attempted.
*
* Note that this expands recipients into their members, because delivery
* is never directly attempted to aggregate actors like projects.
*
* @return list<phid> A list of all recipients to whom delivery will be
* attempted.
* @task recipients
*/
public function buildRecipientList() {
$actors = $this->loadAllActors();
$actors = $this->filterDeliverableActors($actors);
return mpull($actors, 'getPHID');
}
public function loadAllActors() {
$actor_phids = $this->getExpandedRecipientPHIDs();
return $this->loadActors($actor_phids);
}
public function getExpandedRecipientPHIDs() {
$actor_phids = $this->getAllActorPHIDs();
return $this->expandRecipients($actor_phids);
}
private function getAllActorPHIDs() {
return array_merge(
array($this->getParam('from')),
$this->getToPHIDs(),
$this->getCcPHIDs());
}
/**
* Expand a list of recipient PHIDs (possibly including aggregate recipients
* like projects) into a deaggregated list of individual recipient PHIDs.
* For example, this will expand project PHIDs into a list of the project's
* members.
*
* @param list<phid> List of recipient PHIDs, possibly including aggregate
* recipients.
* @return list<phid> Deaggregated list of mailable recipients.
*/
- private function expandRecipients(array $phids) {
+ public function expandRecipients(array $phids) {
if ($this->recipientExpansionMap === null) {
$all_phids = $this->getAllActorPHIDs();
$this->recipientExpansionMap = id(new PhabricatorMetaMTAMemberQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($all_phids)
->execute();
}
$results = array();
foreach ($phids as $phid) {
foreach ($this->recipientExpansionMap[$phid] as $recipient_phid) {
$results[$recipient_phid] = $recipient_phid;
}
}
return array_keys($results);
}
private function filterDeliverableActors(array $actors) {
assert_instances_of($actors, 'PhabricatorMetaMTAActor');
$deliverable_actors = array();
foreach ($actors as $phid => $actor) {
if ($actor->isDeliverable()) {
$deliverable_actors[$phid] = $actor;
}
}
return $deliverable_actors;
}
private function loadActors(array $actor_phids) {
$actor_phids = array_filter($actor_phids);
$viewer = PhabricatorUser::getOmnipotentUser();
$actors = id(new PhabricatorMetaMTAActorQuery())
->setViewer($viewer)
->withPHIDs($actor_phids)
->execute();
if (!$actors) {
return array();
}
if ($this->getForceDelivery()) {
// If we're forcing delivery, skip all the opt-out checks. We don't
// bother annotating reasoning on the mail in this case because it should
// always be obvious why the mail hit this rule (e.g., it is a password
// reset mail).
foreach ($actors as $actor) {
$actor->setDeliverable(PhabricatorMetaMTAActor::REASON_FORCE);
}
return $actors;
}
// Exclude explicit recipients.
foreach ($this->getExcludeMailRecipientPHIDs() as $phid) {
$actor = idx($actors, $phid);
if (!$actor) {
continue;
}
$actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_RESPONSE);
}
// Before running more rules, save a list of the actors who were
// deliverable before we started running preference-based rules. This stops
// us from trying to send mail to disabled users just because a Herald rule
// added them, for example.
$deliverable = array();
foreach ($actors as $phid => $actor) {
if ($actor->isDeliverable()) {
$deliverable[] = $phid;
}
}
// Exclude muted recipients. We're doing this after saving deliverability
// so that Herald "Send me an email" actions can still punch through a
// mute.
foreach ($this->getMutedPHIDs() as $muted_phid) {
$muted_actor = idx($actors, $muted_phid);
if (!$muted_actor) {
continue;
}
$muted_actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_MUTED);
}
// For the rest of the rules, order matters. We're going to run all the
// possible rules in order from weakest to strongest, and let the strongest
// matching rule win. The weaker rules leave annotations behind which help
// users understand why the mail was routed the way it was.
// Exclude the actor if their preferences are set.
$from_phid = $this->getParam('from');
$from_actor = idx($actors, $from_phid);
if ($from_actor) {
$from_user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withPHIDs(array($from_phid))
->needUserSettings(true)
->execute();
$from_user = head($from_user);
if ($from_user) {
$pref_key = PhabricatorEmailSelfActionsSetting::SETTINGKEY;
$exclude_self = $from_user->getUserSetting($pref_key);
if ($exclude_self) {
$from_actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_SELF);
}
}
}
$all_prefs = id(new PhabricatorUserPreferencesQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withUserPHIDs($actor_phids)
->needSyntheticPreferences(true)
->execute();
$all_prefs = mpull($all_prefs, null, 'getUserPHID');
$value_email = PhabricatorEmailTagsSetting::VALUE_EMAIL;
// Exclude all recipients who have set preferences to not receive this type
// of email (for example, a user who says they don't want emails about task
// CC changes).
$tags = $this->getParam('mailtags');
if ($tags) {
foreach ($all_prefs as $phid => $prefs) {
$user_mailtags = $prefs->getSettingValue(
PhabricatorEmailTagsSetting::SETTINGKEY);
// The user must have elected to receive mail for at least one
// of the mailtags.
$send = false;
foreach ($tags as $tag) {
if (((int)idx($user_mailtags, $tag, $value_email)) == $value_email) {
$send = true;
break;
}
}
if (!$send) {
$actors[$phid]->setUndeliverable(
PhabricatorMetaMTAActor::REASON_MAILTAGS);
}
}
}
foreach ($deliverable as $phid) {
switch ($this->getRoutingRule($phid)) {
case PhabricatorMailRoutingRule::ROUTE_AS_NOTIFICATION:
$actors[$phid]->setUndeliverable(
PhabricatorMetaMTAActor::REASON_ROUTE_AS_NOTIFICATION);
break;
case PhabricatorMailRoutingRule::ROUTE_AS_MAIL:
$actors[$phid]->setDeliverable(
PhabricatorMetaMTAActor::REASON_ROUTE_AS_MAIL);
break;
default:
// No change.
break;
}
}
// If recipients were initially deliverable and were added by "Send me an
// email" Herald rules, annotate them as such and make them deliverable
// again, overriding any changes made by the "self mail" and "mail tags"
// settings.
$force_recipients = $this->getForceHeraldMailRecipientPHIDs();
$force_recipients = array_fuse($force_recipients);
if ($force_recipients) {
foreach ($deliverable as $phid) {
if (isset($force_recipients[$phid])) {
$actors[$phid]->setDeliverable(
PhabricatorMetaMTAActor::REASON_FORCE_HERALD);
}
}
}
// Exclude recipients who don't want any mail. This rule is very strong
// and runs last.
foreach ($all_prefs as $phid => $prefs) {
$exclude = $prefs->getSettingValue(
PhabricatorEmailNotificationsSetting::SETTINGKEY);
if ($exclude) {
$actors[$phid]->setUndeliverable(
PhabricatorMetaMTAActor::REASON_MAIL_DISABLED);
}
}
// Unless delivery was forced earlier (password resets, confirmation mail),
// never send mail to unverified addresses.
foreach ($actors as $phid => $actor) {
if ($actor->getIsVerified()) {
continue;
}
$actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_UNVERIFIED);
}
return $actors;
}
- private function shouldRateLimitMail(array $all_recipients) {
- try {
- PhabricatorSystemActionEngine::willTakeAction(
- $all_recipients,
- new PhabricatorMetaMTAErrorMailAction(),
- 1);
- return false;
- } catch (PhabricatorSystemActionRateLimitException $ex) {
- return true;
- }
- }
-
- public function generateHeaders() {
- $headers = array();
-
- $headers[] = array('X-Phabricator-Sent-This-Message', 'Yes');
- $headers[] = array('X-Mail-Transport-Agent', 'MetaMTA');
-
- // Some clients respect this to suppress OOF and other auto-responses.
- $headers[] = array('X-Auto-Response-Suppress', 'All');
-
- $mailtags = $this->getParam('mailtags');
- if ($mailtags) {
- $tag_header = array();
- foreach ($mailtags as $mailtag) {
- $tag_header[] = '<'.$mailtag.'>';
- }
- $tag_header = implode(', ', $tag_header);
- $headers[] = array('X-Phabricator-Mail-Tags', $tag_header);
- }
-
- $value = $this->getParam('headers', array());
- foreach ($value as $pair) {
- list($header_key, $header_value) = $pair;
-
- // NOTE: If we have \n in a header, SES rejects the email.
- $header_value = str_replace("\n", ' ', $header_value);
- $headers[] = array($header_key, $header_value);
- }
-
- $is_bulk = $this->getParam('is-bulk');
- if ($is_bulk) {
- $headers[] = array('Precedence', 'bulk');
- }
-
- if ($this->getMustEncrypt()) {
- $headers[] = array('X-Phabricator-Must-Encrypt', 'Yes');
- }
-
- $related_phid = $this->getRelatedPHID();
- if ($related_phid) {
- $headers[] = array('Thread-Topic', $related_phid);
- }
-
- $headers[] = array('X-Phabricator-Mail-ID', $this->getID());
-
- $unique = Filesystem::readRandomCharacters(16);
- $headers[] = array('X-Phabricator-Send-Attempt', $unique);
-
- return $headers;
- }
-
public function getDeliveredHeaders() {
return $this->getParam('headers.sent');
}
+ public function setDeliveredHeaders(array $headers) {
+ $headers = $this->flattenHeaders($headers);
+ return $this->setParam('headers.sent', $headers);
+ }
+
public function getUnfilteredHeaders() {
$unfiltered = $this->getParam('headers.unfiltered');
if ($unfiltered === null) {
// Older versions of Phabricator did not filter headers, and thus did
// not record unfiltered headers. If we don't have unfiltered header
// data just return the delivered headers for compatibility.
return $this->getDeliveredHeaders();
}
return $unfiltered;
}
+ public function setUnfilteredHeaders(array $headers) {
+ $headers = $this->flattenHeaders($headers);
+ return $this->setParam('headers.unfiltered', $headers);
+ }
+
+ private function flattenHeaders(array $headers) {
+ assert_instances_of($headers, 'PhabricatorMailHeader');
+
+ $list = array();
+ foreach ($list as $header) {
+ $list[] = array(
+ $header->getName(),
+ $header->getValue(),
+ );
+ }
+
+ return $list;
+ }
+
public function getDeliveredActors() {
return $this->getParam('actors.sent');
}
public function getDeliveredRoutingRules() {
return $this->getParam('routing.sent');
}
public function getDeliveredRoutingMap() {
return $this->getParam('routingmap.sent');
}
public function getDeliveredBody() {
return $this->getParam('body.sent');
}
- private function filterHeaders(array $headers, $must_encrypt) {
- if (!$must_encrypt) {
- return $headers;
- }
-
- $whitelist = array(
- 'In-Reply-To',
- 'Message-ID',
- 'Precedence',
- 'References',
- 'Thread-Index',
- 'Thread-Topic',
-
- 'X-Mail-Transport-Agent',
- 'X-Auto-Response-Suppress',
-
- 'X-Phabricator-Sent-This-Message',
- 'X-Phabricator-Must-Encrypt',
- 'X-Phabricator-Mail-ID',
- 'X-Phabricator-Send-Attempt',
- );
-
- // NOTE: The major header we want to drop is "X-Phabricator-Mail-Tags".
- // This header contains a significant amount of meaningful information
- // about the object.
-
- $whitelist_map = array();
- foreach ($whitelist as $term) {
- $whitelist_map[phutil_utf8_strtolower($term)] = true;
- }
-
- foreach ($headers as $key => $header) {
- list($name, $value) = $header;
- $name = phutil_utf8_strtolower($name);
-
- if (!isset($whitelist_map[$name])) {
- unset($headers[$key]);
- }
- }
-
- return $headers;
+ public function setDeliveredBody($body) {
+ return $this->setParam('body.sent', $body);
}
public function getURI() {
return '/mail/detail/'.$this->getID().'/';
}
/* -( Routing )------------------------------------------------------------ */
public function addRoutingRule($routing_rule, $phids, $reason_phid) {
$routing = $this->getParam('routing', array());
$routing[] = array(
'routingRule' => $routing_rule,
'phids' => $phids,
'reasonPHID' => $reason_phid,
);
$this->setParam('routing', $routing);
// Throw the routing map away so we rebuild it.
$this->routingMap = null;
return $this;
}
private function getRoutingRule($phid) {
$map = $this->getRoutingRuleMap();
$info = idx($map, $phid, idx($map, 'default'));
if ($info) {
return idx($info, 'rule');
}
return null;
}
private function getRoutingRuleMap() {
if ($this->routingMap === null) {
$map = array();
$routing = $this->getParam('routing', array());
foreach ($routing as $route) {
$phids = $route['phids'];
if ($phids === null) {
$phids = array('default');
}
foreach ($phids as $phid) {
$new_rule = $route['routingRule'];
$current_rule = idx($map, $phid);
if ($current_rule === null) {
$is_stronger = true;
} else {
$is_stronger = PhabricatorMailRoutingRule::isStrongerThan(
$new_rule,
$current_rule);
}
if ($is_stronger) {
$map[$phid] = array(
'rule' => $new_rule,
'reason' => $route['reasonPHID'],
);
}
}
}
$this->routingMap = $map;
}
return $this->routingMap;
}
/* -( Preferences )-------------------------------------------------------- */
private function loadPreferences($target_phid) {
$viewer = PhabricatorUser::getOmnipotentUser();
if (self::shouldMailEachRecipient()) {
$preferences = id(new PhabricatorUserPreferencesQuery())
->setViewer($viewer)
->withUserPHIDs(array($target_phid))
->needSyntheticPreferences(true)
->executeOne();
if ($preferences) {
return $preferences;
}
}
return PhabricatorUserPreferences::loadGlobalPreferences($viewer);
}
- private function shouldAddRePrefix(PhabricatorUserPreferences $preferences) {
- $value = $preferences->getSettingValue(
- PhabricatorEmailRePrefixSetting::SETTINGKEY);
-
- return ($value == PhabricatorEmailRePrefixSetting::VALUE_RE_PREFIX);
- }
-
- private function shouldVarySubject(PhabricatorUserPreferences $preferences) {
- $value = $preferences->getSettingValue(
- PhabricatorEmailVarySubjectsSetting::SETTINGKEY);
-
- return ($value == PhabricatorEmailVarySubjectsSetting::VALUE_VARY_SUBJECTS);
- }
-
- private function shouldSendHTML(PhabricatorUserPreferences $preferences) {
- $value = $preferences->getSettingValue(
- PhabricatorEmailFormatSetting::SETTINGKEY);
-
- return ($value == PhabricatorEmailFormatSetting::VALUE_HTML_EMAIL);
- }
-
public function shouldRenderMailStampsInBody($viewer) {
$preferences = $this->loadPreferences($viewer->getPHID());
$value = $preferences->getSettingValue(
PhabricatorEmailStampsSetting::SETTINGKEY);
return ($value == PhabricatorEmailStampsSetting::VALUE_BODY_STAMPS);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
return PhabricatorPolicies::POLICY_NOONE;
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
$actor_phids = $this->getExpandedRecipientPHIDs();
return in_array($viewer->getPHID(), $actor_phids);
}
public function describeAutomaticCapability($capability) {
return pht(
'The mail sender and message recipients can always see the mail.');
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$files = $this->loadAttachedFiles($engine->getViewer());
foreach ($files as $file) {
$engine->destroyObject($file);
}
$this->delete();
}
}
diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php
index fc98d1701..64528ea94 100644
--- a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php
+++ b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php
@@ -1,387 +1,573 @@
<?php
final class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO {
protected $headers = array();
protected $bodies = array();
protected $attachments = array();
protected $status = '';
protected $relatedPHID;
protected $authorPHID;
protected $message;
protected $messageIDHash = '';
protected function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'headers' => self::SERIALIZATION_JSON,
'bodies' => self::SERIALIZATION_JSON,
'attachments' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'relatedPHID' => 'phid?',
'authorPHID' => 'phid?',
'message' => 'text?',
'messageIDHash' => 'bytes12',
'status' => 'text32',
),
self::CONFIG_KEY_SCHEMA => array(
'relatedPHID' => array(
'columns' => array('relatedPHID'),
),
'authorPHID' => array(
'columns' => array('authorPHID'),
),
'key_messageIDHash' => array(
'columns' => array('messageIDHash'),
),
'key_created' => array(
'columns' => array('dateCreated'),
),
),
) + parent::getConfiguration();
}
public function setHeaders(array $headers) {
// Normalize headers to lowercase.
$normalized = array();
foreach ($headers as $name => $value) {
$name = $this->normalizeMailHeaderName($name);
if ($name == 'message-id') {
$this->setMessageIDHash(PhabricatorHash::digestForIndex($value));
}
$normalized[$name] = $value;
}
$this->headers = $normalized;
return $this;
}
public function getHeader($key, $default = null) {
$key = $this->normalizeMailHeaderName($key);
return idx($this->headers, $key, $default);
}
private function normalizeMailHeaderName($name) {
return strtolower($name);
}
public function getMessageID() {
return $this->getHeader('Message-ID');
}
public function getSubject() {
return $this->getHeader('Subject');
}
public function getCCAddresses() {
return $this->getRawEmailAddresses(idx($this->headers, 'cc'));
}
public function getToAddresses() {
return $this->getRawEmailAddresses(idx($this->headers, 'to'));
}
+ public function newTargetAddresses() {
+ $raw_addresses = array();
+
+ foreach ($this->getToAddresses() as $raw_address) {
+ $raw_addresses[] = $raw_address;
+ }
+
+ foreach ($this->getCCAddresses() as $raw_address) {
+ $raw_addresses[] = $raw_address;
+ }
+
+ $raw_addresses = array_unique($raw_addresses);
+
+ $addresses = array();
+ foreach ($raw_addresses as $raw_address) {
+ $addresses[] = new PhutilEmailAddress($raw_address);
+ }
+
+ return $addresses;
+ }
+
public function loadAllRecipientPHIDs() {
$addresses = array_merge(
$this->getToAddresses(),
$this->getCCAddresses());
return $this->loadPHIDsFromAddresses($addresses);
}
public function loadCCPHIDs() {
return $this->loadPHIDsFromAddresses($this->getCCAddresses());
}
private function loadPHIDsFromAddresses(array $addresses) {
if (empty($addresses)) {
return array();
}
$users = id(new PhabricatorUserEmail())
->loadAllWhere('address IN (%Ls)', $addresses);
return mpull($users, 'getUserPHID');
}
public function processReceivedMail() {
+ $viewer = $this->getViewer();
$sender = null;
try {
$this->dropMailFromPhabricator();
$this->dropMailAlreadyReceived();
+ $this->dropEmptyMail();
+
+ $sender = $this->loadSender();
+ if ($sender) {
+ $this->setAuthorPHID($sender->getPHID());
+
+ // If we've identified the sender, mark them as the author of any
+ // attached files. We do this before we validate them (below), since
+ // they still authored these files even if their account is not allowed
+ // to interact via email.
+
+ $attachments = $this->getAttachments();
+ if ($attachments) {
+ $files = id(new PhabricatorFileQuery())
+ ->setViewer($viewer)
+ ->withPHIDs($attachments)
+ ->execute();
+ foreach ($files as $file) {
+ $file->setAuthorPHID($sender->getPHID())->save();
+ }
+ }
+
+ $this->validateSender($sender);
+ }
+
+ $receivers = id(new PhutilClassMapQuery())
+ ->setAncestorClass('PhabricatorMailReceiver')
+ ->setFilterMethod('isEnabled')
+ ->execute();
+
+ $reserved_recipient = null;
+ $targets = $this->newTargetAddresses();
+ foreach ($targets as $key => $target) {
+ // Never accept any reserved address as a mail target. This prevents
+ // security issues around "hostmaster@" and bad behavior with
+ // "noreply@".
+ if (PhabricatorMailUtil::isReservedAddress($target)) {
+ if (!$reserved_recipient) {
+ $reserved_recipient = $target;
+ }
+ unset($targets[$key]);
+ continue;
+ }
+
+ // See T13234. Don't process mail if a user has attached this address
+ // to their account.
+ if (PhabricatorMailUtil::isUserAddress($target)) {
+ unset($targets[$key]);
+ continue;
+ }
+ }
- $receiver = $this->loadReceiver();
- $sender = $receiver->loadSender($this);
- $receiver->validateSender($this, $sender);
-
- $this->setAuthorPHID($sender->getPHID());
-
- // Now that we've identified the sender, mark them as the author of
- // any attached files.
- $attachments = $this->getAttachments();
- if ($attachments) {
- $files = id(new PhabricatorFileQuery())
- ->setViewer(PhabricatorUser::getOmnipotentUser())
- ->withPHIDs($attachments)
- ->execute();
- foreach ($files as $file) {
- $file->setAuthorPHID($sender->getPHID())->save();
+ $any_accepted = false;
+ $receiver_exception = null;
+ foreach ($receivers as $receiver) {
+ $receiver = id(clone $receiver)
+ ->setViewer($viewer);
+
+ if ($sender) {
+ $receiver->setSender($sender);
+ }
+
+ foreach ($targets as $target) {
+ try {
+ if (!$receiver->canAcceptMail($this, $target)) {
+ continue;
+ }
+
+ $any_accepted = true;
+
+ $receiver->receiveMail($this, $target);
+ } catch (Exception $ex) {
+ // If receivers raise exceptions, we'll keep the first one in hope
+ // that it points at a root cause.
+ if (!$receiver_exception) {
+ $receiver_exception = $ex;
+ }
+ }
}
}
- $receiver->receiveMail($this, $sender);
+ if ($receiver_exception) {
+ throw $receiver_exception;
+ }
+
+
+ if (!$any_accepted) {
+ if ($reserved_recipient) {
+ // If nothing accepted the mail, we normally raise an error to help
+ // users who mistakenly send mail to "barges@" instead of "bugs@".
+
+ // However, if the recipient list included a reserved recipient, we
+ // don't bounce the mail with an error.
+
+ // The intent here is that if a user does a "Reply All" and includes
+ // "From: noreply@phabricator" in the receipient list, we just want
+ // to drop the mail rather than send them an unhelpful bounce message.
+
+ throw new PhabricatorMetaMTAReceivedMailProcessingException(
+ MetaMTAReceivedMailStatus::STATUS_RESERVED,
+ pht(
+ 'No application handled this mail. This mail was sent to a '.
+ 'reserved recipient ("%s") so bounces are suppressed.',
+ (string)$reserved_recipient));
+ } else if (!$sender) {
+ // NOTE: Currently, we'll always drop this mail (since it's headed to
+ // an unverified recipient). See T12237. These details are still
+ // useful because they'll appear in the mail logs and Mail web UI.
+
+ throw new PhabricatorMetaMTAReceivedMailProcessingException(
+ MetaMTAReceivedMailStatus::STATUS_UNKNOWN_SENDER,
+ pht(
+ 'This email was sent from an email address ("%s") that is not '.
+ 'associated with a Phabricator account. To interact with '.
+ 'Phabricator via email, add this address to your account.',
+ (string)$this->newFromAddress()));
+ } else {
+ throw new PhabricatorMetaMTAReceivedMailProcessingException(
+ MetaMTAReceivedMailStatus::STATUS_NO_RECEIVERS,
+ pht(
+ 'Phabricator can not process this mail because no application '.
+ 'knows how to handle it. Check that the address you sent it to '.
+ 'is correct.'.
+ "\n\n".
+ '(No concrete, enabled subclass of PhabricatorMailReceiver can '.
+ 'accept this mail.)'));
+ }
+ }
} catch (PhabricatorMetaMTAReceivedMailProcessingException $ex) {
switch ($ex->getStatusCode()) {
case MetaMTAReceivedMailStatus::STATUS_DUPLICATE:
case MetaMTAReceivedMailStatus::STATUS_FROM_PHABRICATOR:
// Don't send an error email back in these cases, since they're
// very unlikely to be the sender's fault.
break;
+ case MetaMTAReceivedMailStatus::STATUS_RESERVED:
+ // This probably is the sender's fault, but it's likely an accident
+ // that we received the mail at all.
+ break;
case MetaMTAReceivedMailStatus::STATUS_EMPTY_IGNORED:
// This error is explicitly ignored.
break;
default:
$this->sendExceptionMail($ex, $sender);
break;
}
$this
->setStatus($ex->getStatusCode())
->setMessage($ex->getMessage())
->save();
return $this;
} catch (Exception $ex) {
$this->sendExceptionMail($ex, $sender);
$this
->setStatus(MetaMTAReceivedMailStatus::STATUS_UNHANDLED_EXCEPTION)
->setMessage(pht('Unhandled Exception: %s', $ex->getMessage()))
->save();
throw $ex;
}
return $this->setMessage('OK')->save();
}
public function getCleanTextBody() {
$body = $this->getRawTextBody();
$parser = new PhabricatorMetaMTAEmailBodyParser();
return $parser->stripTextBody($body);
}
public function parseBody() {
$body = $this->getRawTextBody();
$parser = new PhabricatorMetaMTAEmailBodyParser();
return $parser->parseBody($body);
}
public function getRawTextBody() {
return idx($this->bodies, 'text');
}
/**
* Strip an email address down to the actual user@domain.tld part if
* necessary, since sometimes it will have formatting like
* '"Abraham Lincoln" <alincoln@logcab.in>'.
*/
private function getRawEmailAddress($address) {
$matches = null;
$ok = preg_match('/<(.*)>/', $address, $matches);
if ($ok) {
$address = $matches[1];
}
return $address;
}
private function getRawEmailAddresses($addresses) {
$raw_addresses = array();
foreach (explode(',', $addresses) as $address) {
$raw_addresses[] = $this->getRawEmailAddress($address);
}
return array_filter($raw_addresses);
}
/**
* If Phabricator sent the mail, always drop it immediately. This prevents
* loops where, e.g., the public bug address is also a user email address
* and creating a bug sends them an email, which loops.
*/
private function dropMailFromPhabricator() {
if (!$this->getHeader('x-phabricator-sent-this-message')) {
return;
}
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_FROM_PHABRICATOR,
pht(
"Ignoring email with '%s' header to avoid loops.",
'X-Phabricator-Sent-This-Message'));
}
/**
* If this mail has the same message ID as some other mail, and isn't the
* first mail we we received with that message ID, we drop it as a duplicate.
*/
private function dropMailAlreadyReceived() {
$message_id_hash = $this->getMessageIDHash();
if (!$message_id_hash) {
// No message ID hash, so we can't detect duplicates. This should only
// happen with very old messages.
return;
}
$messages = $this->loadAllWhere(
'messageIDHash = %s ORDER BY id ASC LIMIT 2',
$message_id_hash);
$messages_count = count($messages);
if ($messages_count <= 1) {
// If we only have one copy of this message, we're good to process it.
return;
}
$first_message = reset($messages);
if ($first_message->getID() == $this->getID()) {
// If this is the first copy of the message, it is okay to process it.
// We may not have been able to to process it immediately when we received
// it, and could may have received several copies without processing any
// yet.
return;
}
$message = pht(
'Ignoring email with "Message-ID" hash "%s" that has been seen %d '.
'times, including this message.',
$message_id_hash,
$messages_count);
throw new PhabricatorMetaMTAReceivedMailProcessingException(
MetaMTAReceivedMailStatus::STATUS_DUPLICATE,
$message);
}
+ private function dropEmptyMail() {
+ $body = $this->getCleanTextBody();
+ $attachments = $this->getAttachments();
- /**
- * Load a concrete instance of the @{class:PhabricatorMailReceiver} which
- * accepts this mail, if one exists.
- */
- private function loadReceiver() {
- $receivers = id(new PhutilClassMapQuery())
- ->setAncestorClass('PhabricatorMailReceiver')
- ->setFilterMethod('isEnabled')
- ->execute();
-
- $accept = array();
- foreach ($receivers as $key => $receiver) {
- if ($receiver->canAcceptMail($this)) {
- $accept[$key] = $receiver;
- }
+ if (strlen($body) || $attachments) {
+ return;
}
- if (!$accept) {
- throw new PhabricatorMetaMTAReceivedMailProcessingException(
- MetaMTAReceivedMailStatus::STATUS_NO_RECEIVERS,
- pht(
- 'Phabricator can not process this mail because no application '.
- 'knows how to handle it. Check that the address you sent it to is '.
- 'correct.'.
- "\n\n".
- '(No concrete, enabled subclass of PhabricatorMailReceiver can '.
- 'accept this mail.)'));
- }
+ // Only send an error email if the user is talking to just Phabricator.
+ // We can assume if there is only one "To" address it is a Phabricator
+ // address since this code is running and everything.
+ $is_direct_mail = (count($this->getToAddresses()) == 1) &&
+ (count($this->getCCAddresses()) == 0);
- if (count($accept) > 1) {
- $names = implode(', ', array_keys($accept));
- throw new PhabricatorMetaMTAReceivedMailProcessingException(
- MetaMTAReceivedMailStatus::STATUS_ABUNDANT_RECEIVERS,
- pht(
- 'Phabricator is not able to process this mail because more than '.
- 'one application is willing to accept it, creating ambiguity. '.
- 'Mail needs to be accepted by exactly one receiving application.'.
- "\n\n".
- 'Accepting receivers: %s.',
- $names));
+ if ($is_direct_mail) {
+ $status_code = MetaMTAReceivedMailStatus::STATUS_EMPTY;
+ } else {
+ $status_code = MetaMTAReceivedMailStatus::STATUS_EMPTY_IGNORED;
}
- return head($accept);
+ throw new PhabricatorMetaMTAReceivedMailProcessingException(
+ $status_code,
+ pht(
+ 'Your message does not contain any body text or attachments, so '.
+ 'Phabricator can not do anything useful with it. Make sure comment '.
+ 'text appears at the top of your message: quoted replies, inline '.
+ 'text, and signatures are discarded and ignored.'));
}
private function sendExceptionMail(
Exception $ex,
PhabricatorUser $viewer = null) {
// If we've failed to identify a legitimate sender, we don't send them
// an error message back. We want to avoid sending mail to unverified
// addresses. See T12491.
if (!$viewer) {
return;
}
if ($ex instanceof PhabricatorMetaMTAReceivedMailProcessingException) {
$status_code = $ex->getStatusCode();
$status_name = MetaMTAReceivedMailStatus::getHumanReadableName(
$status_code);
$title = pht('Error Processing Mail (%s)', $status_name);
$description = $ex->getMessage();
} else {
$title = pht('Error Processing Mail (%s)', get_class($ex));
$description = pht('%s: %s', get_class($ex), $ex->getMessage());
}
// TODO: Since headers don't necessarily have unique names, this may not
// really be all the headers. It would be nice to pass the raw headers
// through from the upper layers where possible.
// On the MimeMailParser pathway, we arrive here with a list value for
// headers that appeared multiple times in the original mail. Be
// accommodating until header handling gets straightened out.
$headers = array();
foreach ($this->headers as $key => $values) {
if (!is_array($values)) {
$values = array($values);
}
foreach ($values as $value) {
$headers[] = pht('%s: %s', $key, $value);
}
}
$headers = implode("\n", $headers);
$body = pht(<<<EOBODY
Your email to Phabricator was not processed, because an error occurred while
trying to handle it:
%s
-- Original Message Body -----------------------------------------------------
%s
-- Original Message Headers --------------------------------------------------
%s
EOBODY
,
wordwrap($description, 78),
$this->getRawTextBody(),
$headers);
$mail = id(new PhabricatorMetaMTAMail())
->setIsErrorEmail(true)
->setSubject($title)
->addTos(array($viewer->getPHID()))
->setBody($body)
->saveAndSend();
}
public function newContentSource() {
return PhabricatorContentSource::newForSource(
PhabricatorEmailContentSource::SOURCECONST,
array(
'id' => $this->getID(),
));
}
+ public function newFromAddress() {
+ $raw_from = $this->getHeader('From');
+
+ if (strlen($raw_from)) {
+ return new PhutilEmailAddress($raw_from);
+ }
+
+ return null;
+ }
+
+ private function getViewer() {
+ return PhabricatorUser::getOmnipotentUser();
+ }
+
+ /**
+ * Identify the sender's user account for a piece of received mail.
+ *
+ * Note that this method does not validate that the sender is who they say
+ * they are, just that they've presented some credential which corresponds
+ * to a recognizable user.
+ */
+ private function loadSender() {
+ $viewer = $this->getViewer();
+
+ // Try to identify the user based on their "From" address.
+ $from_address = $this->newFromAddress();
+ if ($from_address) {
+ $user = id(new PhabricatorPeopleQuery())
+ ->setViewer($viewer)
+ ->withEmails(array($from_address->getAddress()))
+ ->executeOne();
+ if ($user) {
+ return $user;
+ }
+ }
+
+ return null;
+ }
+
+ private function validateSender(PhabricatorUser $sender) {
+ $failure_reason = null;
+ if ($sender->getIsDisabled()) {
+ $failure_reason = pht(
+ 'Your account ("%s") is disabled, so you can not interact with '.
+ 'Phabricator over email.',
+ $sender->getUsername());
+ } else if ($sender->getIsStandardUser()) {
+ if (!$sender->getIsApproved()) {
+ $failure_reason = pht(
+ 'Your account ("%s") has not been approved yet. You can not '.
+ 'interact with Phabricator over email until your account is '.
+ 'approved.',
+ $sender->getUsername());
+ } else if (PhabricatorUserEmail::isEmailVerificationRequired() &&
+ !$sender->getIsEmailVerified()) {
+ $failure_reason = pht(
+ 'You have not verified the email address for your account ("%s"). '.
+ 'You must verify your email address before you can interact '.
+ 'with Phabricator over email.',
+ $sender->getUsername());
+ }
+ }
+
+ if ($failure_reason) {
+ throw new PhabricatorMetaMTAReceivedMailProcessingException(
+ MetaMTAReceivedMailStatus::STATUS_DISABLED_SENDER,
+ $failure_reason);
+ }
+ }
+
}
diff --git a/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php b/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php
index d20a28fc1..7462aaf55 100644
--- a/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php
+++ b/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php
@@ -1,414 +1,422 @@
<?php
final class PhabricatorMetaMTAMailTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testMailSendFailures() {
$user = $this->generateNewTestUser();
$phid = $user->getPHID();
// Normally, the send should succeed.
$mail = new PhabricatorMetaMTAMail();
$mail->addTos(array($phid));
- $mailer = new PhabricatorMailImplementationTestAdapter();
+ $mailer = new PhabricatorMailTestAdapter();
$mail->sendWithMailers(array($mailer));
$this->assertEqual(
PhabricatorMailOutboundStatus::STATUS_SENT,
$mail->getStatus());
// When the mailer fails temporarily, the mail should remain queued.
$mail = new PhabricatorMetaMTAMail();
$mail->addTos(array($phid));
- $mailer = new PhabricatorMailImplementationTestAdapter();
+ $mailer = new PhabricatorMailTestAdapter();
$mailer->setFailTemporarily(true);
try {
$mail->sendWithMailers(array($mailer));
} catch (Exception $ex) {
// Ignore.
}
$this->assertEqual(
PhabricatorMailOutboundStatus::STATUS_QUEUE,
$mail->getStatus());
// When the mailer fails permanently, the mail should be failed.
$mail = new PhabricatorMetaMTAMail();
$mail->addTos(array($phid));
- $mailer = new PhabricatorMailImplementationTestAdapter();
+ $mailer = new PhabricatorMailTestAdapter();
$mailer->setFailPermanently(true);
try {
$mail->sendWithMailers(array($mailer));
} catch (Exception $ex) {
// Ignore.
}
$this->assertEqual(
PhabricatorMailOutboundStatus::STATUS_FAIL,
$mail->getStatus());
}
public function testRecipients() {
$user = $this->generateNewTestUser();
$phid = $user->getPHID();
- $mailer = new PhabricatorMailImplementationTestAdapter();
+ $mailer = new PhabricatorMailTestAdapter();
$mail = new PhabricatorMetaMTAMail();
$mail->addTos(array($phid));
$this->assertTrue(
in_array($phid, $mail->buildRecipientList()),
pht('"To" is a recipient.'));
// Test that the "No Self Mail" and "No Mail" preferences work correctly.
$mail->setFrom($phid);
$this->assertTrue(
in_array($phid, $mail->buildRecipientList()),
pht('"From" does not exclude recipients by default.'));
$user = $this->writeSetting(
$user,
PhabricatorEmailSelfActionsSetting::SETTINGKEY,
true);
$this->assertFalse(
in_array($phid, $mail->buildRecipientList()),
pht('"From" excludes recipients with no-self-mail set.'));
$user = $this->writeSetting(
$user,
PhabricatorEmailSelfActionsSetting::SETTINGKEY,
null);
$this->assertTrue(
in_array($phid, $mail->buildRecipientList()),
pht('"From" does not exclude recipients by default.'));
$user = $this->writeSetting(
$user,
PhabricatorEmailNotificationsSetting::SETTINGKEY,
true);
$this->assertFalse(
in_array($phid, $mail->buildRecipientList()),
pht('"From" excludes recipients with no-mail set.'));
$mail->setForceDelivery(true);
$this->assertTrue(
in_array($phid, $mail->buildRecipientList()),
pht('"From" includes no-mail recipients when forced.'));
$mail->setForceDelivery(false);
$user = $this->writeSetting(
$user,
PhabricatorEmailNotificationsSetting::SETTINGKEY,
null);
$this->assertTrue(
in_array($phid, $mail->buildRecipientList()),
pht('"From" does not exclude recipients by default.'));
// Test that explicit exclusion works correctly.
$mail->setExcludeMailRecipientPHIDs(array($phid));
$this->assertFalse(
in_array($phid, $mail->buildRecipientList()),
pht('Explicit exclude excludes recipients.'));
$mail->setExcludeMailRecipientPHIDs(array());
// Test that mail tag preferences exclude recipients.
$user = $this->writeSetting(
$user,
PhabricatorEmailTagsSetting::SETTINGKEY,
array(
'test-tag' => false,
));
$mail->setMailTags(array('test-tag'));
$this->assertFalse(
in_array($phid, $mail->buildRecipientList()),
pht('Tag preference excludes recipients.'));
$user = $this->writeSetting(
$user,
PhabricatorEmailTagsSetting::SETTINGKEY,
null);
$this->assertTrue(
in_array($phid, $mail->buildRecipientList()),
'Recipients restored after tag preference removed.');
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'userPHID = %s AND isPrimary = 1',
$phid);
$email->setIsVerified(0)->save();
$this->assertFalse(
in_array($phid, $mail->buildRecipientList()),
pht('Mail not sent to unverified address.'));
$email->setIsVerified(1)->save();
$this->assertTrue(
in_array($phid, $mail->buildRecipientList()),
pht('Mail sent to verified address.'));
}
public function testThreadIDHeaders() {
$this->runThreadIDHeadersWithConfiguration(true, true);
$this->runThreadIDHeadersWithConfiguration(true, false);
$this->runThreadIDHeadersWithConfiguration(false, true);
$this->runThreadIDHeadersWithConfiguration(false, false);
}
private function runThreadIDHeadersWithConfiguration(
$supports_message_id,
$is_first_mail) {
- $mailer = new PhabricatorMailImplementationTestAdapter();
+ $user = $this->generateNewTestUser();
+ $phid = $user->getPHID();
- $mailer->prepareForSend(
- array(
- 'supportsMessageIDHeader' => $supports_message_id,
- ));
+ $mailer = new PhabricatorMailTestAdapter();
- $thread_id = '<somethread-12345@somedomain.tld>';
+ $mailer->setSupportsMessageID($supports_message_id);
- $mail = new PhabricatorMetaMTAMail();
- $mail->setThreadID($thread_id, $is_first_mail);
- $mail->sendWithMailers(array($mailer));
+ $thread_id = 'somethread-12345';
+
+ $mail = id(new PhabricatorMetaMTAMail())
+ ->setThreadID($thread_id, $is_first_mail)
+ ->addTos(array($phid))
+ ->sendWithMailers(array($mailer));
$guts = $mailer->getGuts();
- $dict = ipull($guts['headers'], 1, 0);
+
+ $headers = idx($guts, 'headers', array());
+
+ $dict = array();
+ foreach ($headers as $header) {
+ list($name, $value) = $header;
+ $dict[$name] = $value;
+ }
if ($is_first_mail && $supports_message_id) {
$expect_message_id = true;
$expect_in_reply_to = false;
$expect_references = false;
} else {
$expect_message_id = false;
$expect_in_reply_to = true;
$expect_references = true;
}
$case = '<message-id = '.($supports_message_id ? 'Y' : 'N').', '.
'first = '.($is_first_mail ? 'Y' : 'N').'>';
$this->assertTrue(
isset($dict['Thread-Index']),
pht('Expect Thread-Index header for case %s.', $case));
$this->assertEqual(
$expect_message_id,
isset($dict['Message-ID']),
pht(
'Expectation about existence of Message-ID header for case %s.',
$case));
$this->assertEqual(
$expect_in_reply_to,
isset($dict['In-Reply-To']),
pht(
'Expectation about existence of In-Reply-To header for case %s.',
$case));
$this->assertEqual(
$expect_references,
isset($dict['References']),
pht(
'Expectation about existence of References header for case %s.',
$case));
}
private function writeSetting(PhabricatorUser $user, $key, $value) {
$preferences = PhabricatorUserPreferences::loadUserPreferences($user);
$editor = id(new PhabricatorUserPreferencesEditor())
->setActor($user)
->setContentSource($this->newContentSource())
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
$xactions = array();
$xactions[] = $preferences->newTransaction($key, $value);
$editor->applyTransactions($preferences, $xactions);
return id(new PhabricatorPeopleQuery())
->setViewer($user)
->withIDs(array($user->getID()))
->executeOne();
}
public function testMailerFailover() {
$user = $this->generateNewTestUser();
$phid = $user->getPHID();
$status_sent = PhabricatorMailOutboundStatus::STATUS_SENT;
$status_queue = PhabricatorMailOutboundStatus::STATUS_QUEUE;
$status_fail = PhabricatorMailOutboundStatus::STATUS_FAIL;
- $mailer1 = id(new PhabricatorMailImplementationTestAdapter())
+ $mailer1 = id(new PhabricatorMailTestAdapter())
->setKey('mailer1');
- $mailer2 = id(new PhabricatorMailImplementationTestAdapter())
+ $mailer2 = id(new PhabricatorMailTestAdapter())
->setKey('mailer2');
$mailers = array(
$mailer1,
$mailer2,
);
// Send mail with both mailers active. The first mailer should be used.
$mail = id(new PhabricatorMetaMTAMail())
->addTos(array($phid))
->sendWithMailers($mailers);
$this->assertEqual($status_sent, $mail->getStatus());
$this->assertEqual('mailer1', $mail->getMailerKey());
// If the first mailer fails, the mail should be sent with the second
// mailer. Since we transmitted the mail, this doesn't raise an exception.
$mailer1->setFailTemporarily(true);
$mail = id(new PhabricatorMetaMTAMail())
->addTos(array($phid))
->sendWithMailers($mailers);
$this->assertEqual($status_sent, $mail->getStatus());
$this->assertEqual('mailer2', $mail->getMailerKey());
// If both mailers fail, the mail should remain in queue.
$mailer2->setFailTemporarily(true);
$mail = id(new PhabricatorMetaMTAMail())
->addTos(array($phid));
$caught = null;
try {
$mail->sendWithMailers($mailers);
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertTrue($caught instanceof Exception);
$this->assertEqual($status_queue, $mail->getStatus());
$this->assertEqual(null, $mail->getMailerKey());
$mailer1->setFailTemporarily(false);
$mailer2->setFailTemporarily(false);
// If the first mailer fails permanently, the mail should fail even though
// the second mailer isn't configured to fail.
$mailer1->setFailPermanently(true);
$mail = id(new PhabricatorMetaMTAMail())
->addTos(array($phid));
$caught = null;
try {
$mail->sendWithMailers($mailers);
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertTrue($caught instanceof Exception);
$this->assertEqual($status_fail, $mail->getStatus());
$this->assertEqual(null, $mail->getMailerKey());
}
public function testMailSizeLimits() {
$env = PhabricatorEnv::beginScopedEnv();
$env->overrideEnvConfig('metamta.email-body-limit', 1024 * 512);
$user = $this->generateNewTestUser();
$phid = $user->getPHID();
$string_1kb = str_repeat('x', 1024);
$html_1kb = str_repeat('y', 1024);
$string_1mb = str_repeat('x', 1024 * 1024);
$html_1mb = str_repeat('y', 1024 * 1024);
// First, send a mail with a small text body and a small HTML body to make
// sure the basics work properly.
$mail = id(new PhabricatorMetaMTAMail())
->addTos(array($phid))
->setBody($string_1kb)
->setHTMLBody($html_1kb);
- $mailer = new PhabricatorMailImplementationTestAdapter();
+ $mailer = new PhabricatorMailTestAdapter();
$mail->sendWithMailers(array($mailer));
$this->assertEqual(
PhabricatorMailOutboundStatus::STATUS_SENT,
$mail->getStatus());
$text_body = $mailer->getBody();
$html_body = $mailer->getHTMLBody();
$this->assertEqual($string_1kb, $text_body);
$this->assertEqual($html_1kb, $html_body);
// Now, send a mail with a large text body and a large HTML body. We expect
// the text body to be truncated and the HTML body to be dropped.
$mail = id(new PhabricatorMetaMTAMail())
->addTos(array($phid))
->setBody($string_1mb)
->setHTMLBody($html_1mb);
- $mailer = new PhabricatorMailImplementationTestAdapter();
+ $mailer = new PhabricatorMailTestAdapter();
$mail->sendWithMailers(array($mailer));
$this->assertEqual(
PhabricatorMailOutboundStatus::STATUS_SENT,
$mail->getStatus());
$text_body = $mailer->getBody();
$html_body = $mailer->getHTMLBody();
// We expect the body was truncated, because it exceeded the body limit.
$this->assertTrue(
(strlen($text_body) < strlen($string_1mb)),
pht('Text Body Truncated'));
// We expect the HTML body was dropped completely after the text body was
// truncated.
$this->assertTrue(
!strlen($html_body),
pht('HTML Body Removed'));
// Next send a mail with a small text body and a large HTML body. We expect
// the text body to be intact and the HTML body to be dropped.
$mail = id(new PhabricatorMetaMTAMail())
->addTos(array($phid))
->setBody($string_1kb)
->setHTMLBody($html_1mb);
- $mailer = new PhabricatorMailImplementationTestAdapter();
+ $mailer = new PhabricatorMailTestAdapter();
$mail->sendWithMailers(array($mailer));
$this->assertEqual(
PhabricatorMailOutboundStatus::STATUS_SENT,
$mail->getStatus());
$text_body = $mailer->getBody();
$html_body = $mailer->getHTMLBody();
$this->assertEqual($string_1kb, $text_body);
$this->assertTrue(!strlen($html_body));
}
}
diff --git a/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAReceivedMailTestCase.php b/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAReceivedMailTestCase.php
index fbabe412c..2630a50fe 100644
--- a/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAReceivedMailTestCase.php
+++ b/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAReceivedMailTestCase.php
@@ -1,124 +1,136 @@
<?php
final class PhabricatorMetaMTAReceivedMailTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testDropSelfMail() {
$mail = new PhabricatorMetaMTAReceivedMail();
$mail->setHeaders(
array(
'X-Phabricator-Sent-This-Message' => 'yes',
));
$mail->save();
$mail->processReceivedMail();
$this->assertEqual(
MetaMTAReceivedMailStatus::STATUS_FROM_PHABRICATOR,
$mail->getStatus());
}
public function testDropDuplicateMail() {
$mail_a = new PhabricatorMetaMTAReceivedMail();
$mail_a->setHeaders(
array(
'Message-ID' => 'test@example.com',
));
$mail_a->save();
$mail_b = new PhabricatorMetaMTAReceivedMail();
$mail_b->setHeaders(
array(
'Message-ID' => 'test@example.com',
));
$mail_b->save();
$mail_a->processReceivedMail();
$mail_b->processReceivedMail();
$this->assertEqual(
MetaMTAReceivedMailStatus::STATUS_DUPLICATE,
$mail_b->getStatus());
}
public function testDropUnreceivableMail() {
+ $user = $this->generateNewTestUser()
+ ->save();
+
$mail = new PhabricatorMetaMTAReceivedMail();
$mail->setHeaders(
array(
'Message-ID' => 'test@example.com',
'To' => 'does+not+exist@example.com',
+ 'From' => $user->loadPrimaryEmail()->getAddress(),
+ ));
+ $mail->setBodies(
+ array(
+ 'text' => 'test',
));
$mail->save();
$mail->processReceivedMail();
$this->assertEqual(
MetaMTAReceivedMailStatus::STATUS_NO_RECEIVERS,
$mail->getStatus());
}
public function testDropUnknownSenderMail() {
$this->setManiphestCreateEmail();
- $env = PhabricatorEnv::beginScopedEnv();
- $env->overrideEnvConfig('phabricator.allow-email-users', false);
- $env->overrideEnvConfig('metamta.maniphest.default-public-author', null);
-
$mail = new PhabricatorMetaMTAReceivedMail();
$mail->setHeaders(
array(
'Message-ID' => 'test@example.com',
'To' => 'bugs@example.com',
'From' => 'does+not+exist@example.com',
));
+ $mail->setBodies(
+ array(
+ 'text' => 'test',
+ ));
$mail->save();
$mail->processReceivedMail();
$this->assertEqual(
MetaMTAReceivedMailStatus::STATUS_UNKNOWN_SENDER,
$mail->getStatus());
}
public function testDropDisabledSenderMail() {
$this->setManiphestCreateEmail();
$user = $this->generateNewTestUser()
->setIsDisabled(true)
->save();
$mail = new PhabricatorMetaMTAReceivedMail();
$mail->setHeaders(
array(
'Message-ID' => 'test@example.com',
'From' => $user->loadPrimaryEmail()->getAddress(),
'To' => 'bugs@example.com',
));
+ $mail->setBodies(
+ array(
+ 'text' => 'test',
+ ));
$mail->save();
$mail->processReceivedMail();
$this->assertEqual(
MetaMTAReceivedMailStatus::STATUS_DISABLED_SENDER,
$mail->getStatus());
}
private function setManiphestCreateEmail() {
$maniphest_app = new PhabricatorManiphestApplication();
try {
id(new PhabricatorMetaMTAApplicationEmail())
->setApplicationPHID($maniphest_app->getPHID())
->setAddress('bugs@example.com')
->setConfigData(array())
->save();
} catch (AphrontDuplicateKeyQueryException $ex) {}
}
}
diff --git a/src/applications/metamta/util/PhabricatorMailUtil.php b/src/applications/metamta/util/PhabricatorMailUtil.php
new file mode 100644
index 000000000..a5fbc7179
--- /dev/null
+++ b/src/applications/metamta/util/PhabricatorMailUtil.php
@@ -0,0 +1,119 @@
+<?php
+
+final class PhabricatorMailUtil
+ extends Phobject {
+
+ /**
+ * Normalize an email address for comparison or lookup.
+ *
+ * Phabricator can be configured to prepend a prefix to all reply addresses,
+ * which can make forwarding rules easier to write. This method strips the
+ * prefix if it is present, and normalizes casing and whitespace.
+ *
+ * @param PhutilEmailAddress Email address.
+ * @return PhutilEmailAddress Normalized address.
+ */
+ public static function normalizeAddress(PhutilEmailAddress $address) {
+ $raw_address = $address->getAddress();
+ $raw_address = phutil_utf8_strtolower($raw_address);
+ $raw_address = trim($raw_address);
+
+ // If a mailbox prefix is configured and present, strip it off.
+ $prefix_key = 'metamta.single-reply-handler-prefix';
+ $prefix = PhabricatorEnv::getEnvConfig($prefix_key);
+ $len = strlen($prefix);
+
+ if ($len) {
+ $prefix = $prefix.'+';
+ $len = $len + 1;
+
+ if (!strncasecmp($raw_address, $prefix, $len)) {
+ $raw_address = substr($raw_address, $len);
+ }
+ }
+
+ return id(clone $address)
+ ->setAddress($raw_address);
+ }
+
+ /**
+ * Determine if two inbound email addresses are effectively identical.
+ *
+ * This method strips and normalizes addresses so that equivalent variations
+ * are correctly detected as identical. For example, these addresses are all
+ * considered to match one another:
+ *
+ * "Abraham Lincoln" <alincoln@example.com>
+ * alincoln@example.com
+ * <ALincoln@example.com>
+ * "Abraham" <phabricator+ALINCOLN@EXAMPLE.COM> # With configured prefix.
+ *
+ * @param PhutilEmailAddress Email address.
+ * @param PhutilEmailAddress Another email address.
+ * @return bool True if addresses are effectively the same address.
+ */
+ public static function matchAddresses(
+ PhutilEmailAddress $u,
+ PhutilEmailAddress $v) {
+
+ $u = self::normalizeAddress($u);
+ $v = self::normalizeAddress($v);
+
+ return ($u->getAddress() === $v->getAddress());
+ }
+
+ public static function isReservedAddress(PhutilEmailAddress $address) {
+ $address = self::normalizeAddress($address);
+ $local = $address->getLocalPart();
+
+ $reserved = array(
+ 'admin',
+ 'administrator',
+ 'hostmaster',
+ 'list',
+ 'list-request',
+ 'majordomo',
+ 'postmaster',
+ 'root',
+ 'ssl-admin',
+ 'ssladmin',
+ 'ssladministrator',
+ 'sslwebmaster',
+ 'sysadmin',
+ 'uucp',
+ 'webmaster',
+
+ 'noreply',
+ 'no-reply',
+ );
+
+ $reserved = array_fuse($reserved);
+
+ if (isset($reserved[$local])) {
+ return true;
+ }
+
+ $default_address = id(new PhabricatorMailEmailEngine())
+ ->newDefaultEmailAddress();
+ if (self::matchAddresses($address, $default_address)) {
+ return true;
+ }
+
+ $void_address = id(new PhabricatorMailEmailEngine())
+ ->newVoidEmailAddress();
+ if (self::matchAddresses($address, $void_address)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ public static function isUserAddress(PhutilEmailAddress $address) {
+ $user_email = id(new PhabricatorUserEmail())->loadOneWhere(
+ 'address = %s',
+ $address->getAddress());
+
+ return (bool)$user_email;
+ }
+
+}
diff --git a/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php b/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php
index 778e05b05..6622f0afe 100644
--- a/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php
+++ b/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php
@@ -1,223 +1,223 @@
<?php
/**
* Render the body of an application email by building it up section-by-section.
*
* @task compose Composition
* @task render Rendering
*/
final class PhabricatorMetaMTAMailBody extends Phobject {
private $sections = array();
private $htmlSections = array();
private $attachments = array();
private $viewer;
private $contextObject;
public function getViewer() {
return $this->viewer;
}
public function setViewer($viewer) {
$this->viewer = $viewer;
return $this;
}
public function setContextObject($context_object) {
$this->contextObject = $context_object;
return $this;
}
public function getContextObject() {
return $this->contextObject;
}
/* -( Composition )-------------------------------------------------------- */
/**
* Add a raw block of text to the email. This will be rendered as-is.
*
* @param string Block of text.
* @return this
* @task compose
*/
public function addRawSection($text) {
if (strlen($text)) {
$text = rtrim($text);
$this->sections[] = $text;
$this->htmlSections[] = phutil_escape_html_newlines(
phutil_tag('div', array(), $text));
}
return $this;
}
public function addRemarkupSection($header, $text) {
try {
$engine = $this->newMarkupEngine()
->setMode(PhutilRemarkupEngine::MODE_TEXT);
$styled_text = $engine->markupText($text);
$this->addPlaintextSection($header, $styled_text);
} catch (Exception $ex) {
phlog($ex);
$this->addTextSection($header, $text);
}
try {
$mail_engine = $this->newMarkupEngine()
->setMode(PhutilRemarkupEngine::MODE_HTML_MAIL);
$html = $mail_engine->markupText($text);
$this->addHTMLSection($header, $html);
} catch (Exception $ex) {
phlog($ex);
$this->addHTMLSection($header, $text);
}
return $this;
}
public function addRawPlaintextSection($text) {
if (strlen($text)) {
$text = rtrim($text);
$this->sections[] = $text;
}
return $this;
}
public function addRawHTMLSection($html) {
$this->htmlSections[] = phutil_safe_html($html);
return $this;
}
/**
* Add a block of text with a section header. This is rendered like this:
*
* HEADER
* Text is indented.
*
* @param string Header text.
* @param string Section text.
* @return this
* @task compose
*/
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, $indent = true) {
if ($indent) {
$text = $this->indent($text);
}
$this->sections[] = $header."\n".$text;
return $this;
}
public function addHTMLSection($header, $html_fragment) {
if ($header !== null) {
$header = phutil_tag('strong', array(), $header);
}
$this->htmlSections[] = array(
phutil_tag(
'div',
array(),
array(
$header,
phutil_tag('div', array(), $html_fragment),
)),
);
return $this;
}
public function addLinkSection($header, $link) {
$html = phutil_tag('a', array('href' => $link), $link);
$this->addPlaintextSection($header, $link);
$this->addHTMLSection($header, $html);
return $this;
}
/**
* Add an attachment.
*
- * @param PhabricatorMetaMTAAttachment Attachment.
+ * @param PhabricatorMailAttachment Attachment.
* @return this
* @task compose
*/
- public function addAttachment(PhabricatorMetaMTAAttachment $attachment) {
+ public function addAttachment(PhabricatorMailAttachment $attachment) {
$this->attachments[] = $attachment;
return $this;
}
/* -( Rendering )---------------------------------------------------------- */
/**
* Render the email body.
*
* @return string Rendered body.
* @task render
*/
public function render() {
return implode("\n\n", $this->sections)."\n";
}
public function renderHTML() {
$br = phutil_tag('br');
$body = phutil_implode_html($br, $this->htmlSections);
return (string)hsprintf('%s', array($body, $br));
}
/**
* Retrieve attachments.
*
- * @return list<PhabricatorMetaMTAAttachment> Attachments.
+ * @return list<PhabricatorMailAttachment> Attachments.
* @task render
*/
public function getAttachments() {
return $this->attachments;
}
/**
* Indent a block of text for rendering under a section heading.
*
* @param string Text to indent.
* @return string Indented text.
* @task render
*/
private function indent($text) {
return rtrim(" ".str_replace("\n", "\n ", $text));
}
private function newMarkupEngine() {
$engine = PhabricatorMarkupEngine::newMarkupEngine(array())
->setConfig('viewer', $this->getViewer())
->setConfig('uri.base', PhabricatorEnv::getProductionURI('/'));
$context = $this->getContextObject();
if ($context) {
$engine->setConfig('contextObject', $context);
}
return $engine;
}
}
diff --git a/src/applications/nuance/storage/NuanceItem.php b/src/applications/nuance/storage/NuanceItem.php
index 09a106ca7..f182e1758 100644
--- a/src/applications/nuance/storage/NuanceItem.php
+++ b/src/applications/nuance/storage/NuanceItem.php
@@ -1,210 +1,200 @@
<?php
final class NuanceItem
extends NuanceDAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface {
const STATUS_IMPORTING = 'importing';
const STATUS_ROUTING = 'routing';
const STATUS_OPEN = 'open';
const STATUS_CLOSED = 'closed';
protected $status;
protected $ownerPHID;
protected $requestorPHID;
protected $sourcePHID;
protected $queuePHID;
protected $itemType;
protected $itemKey;
protected $itemContainerKey;
protected $data = array();
protected $mailKey;
private $queue = self::ATTACHABLE;
private $source = self::ATTACHABLE;
private $implementation = self::ATTACHABLE;
public static function initializeNewItem($item_type) {
// TODO: Validate that the type is valid, and construct and attach it.
return id(new NuanceItem())
->setItemType($item_type)
->setStatus(self::STATUS_OPEN);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'data' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'ownerPHID' => 'phid?',
'requestorPHID' => 'phid?',
'queuePHID' => 'phid?',
'itemType' => 'text64',
'itemKey' => 'text64?',
'itemContainerKey' => 'text64?',
'status' => 'text32',
'mailKey' => 'bytes20',
),
self::CONFIG_KEY_SCHEMA => array(
'key_source' => array(
'columns' => array('sourcePHID', 'status'),
),
'key_owner' => array(
'columns' => array('ownerPHID', 'status'),
),
'key_requestor' => array(
'columns' => array('requestorPHID', 'status'),
),
'key_queue' => array(
'columns' => array('queuePHID', 'status'),
),
'key_container' => array(
'columns' => array('sourcePHID', 'itemContainerKey'),
),
'key_item' => array(
'columns' => array('sourcePHID', 'itemKey'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
NuanceItemPHIDType::TYPECONST);
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public function getURI() {
return '/nuance/item/view/'.$this->getID().'/';
}
public function getSource() {
return $this->assertAttached($this->source);
}
public function attachSource(NuanceSource $source) {
$this->source = $source;
}
public function getItemProperty($key, $default = null) {
return idx($this->data, $key, $default);
}
public function setItemProperty($key, $value) {
$this->data[$key] = $value;
return $this;
}
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
// TODO - this should be based on the queues the item currently resides in
return PhabricatorPolicies::POLICY_USER;
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
// TODO - requestors should get auto access too!
return $viewer->getPHID() == $this->ownerPHID;
}
public function describeAutomaticCapability($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return pht('Owners of an item can always view it.');
case PhabricatorPolicyCapability::CAN_EDIT:
return pht('Owners of an item can always edit it.');
}
return null;
}
public function getDisplayName() {
return $this->getImplementation()->getItemDisplayName($this);
}
public function scheduleUpdate() {
PhabricatorWorker::scheduleTask(
'NuanceItemUpdateWorker',
array(
'itemPHID' => $this->getPHID(),
),
array(
'objectPHID' => $this->getPHID(),
));
}
public function issueCommand(
$author_phid,
$command,
array $parameters = array()) {
$command = id(NuanceItemCommand::initializeNewCommand())
->setItemPHID($this->getPHID())
->setAuthorPHID($author_phid)
->setCommand($command)
->setParameters($parameters)
->save();
$this->scheduleUpdate();
return $this;
}
public function getImplementation() {
return $this->assertAttached($this->implementation);
}
public function attachImplementation(NuanceItemType $type) {
$this->implementation = $type;
return $this;
}
public function getQueue() {
return $this->assertAttached($this->queue);
}
public function attachQueue(NuanceQueue $queue = null) {
$this->queue = $queue;
return $this;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new NuanceItemEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new NuanceItemTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
- return $timeline;
- }
-
}
diff --git a/src/applications/nuance/storage/NuanceQueue.php b/src/applications/nuance/storage/NuanceQueue.php
index f0ba5bb45..a19f1693b 100644
--- a/src/applications/nuance/storage/NuanceQueue.php
+++ b/src/applications/nuance/storage/NuanceQueue.php
@@ -1,96 +1,86 @@
<?php
final class NuanceQueue
extends NuanceDAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface {
protected $name;
protected $mailKey;
protected $viewPolicy;
protected $editPolicy;
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text255?',
'mailKey' => 'bytes20',
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
NuanceQueuePHIDType::TYPECONST);
}
public static function initializeNewQueue() {
return id(new self())
->setViewPolicy(PhabricatorPolicies::POLICY_USER)
->setEditPolicy(PhabricatorPolicies::POLICY_USER);
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public function getURI() {
return '/nuance/queue/view/'.$this->getID().'/';
}
public function getWorkURI() {
return '/nuance/queue/work/'.$this->getID().'/';
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
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 $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new NuanceQueueEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new NuanceQueueTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
- return $timeline;
- }
-
}
diff --git a/src/applications/nuance/storage/NuanceSource.php b/src/applications/nuance/storage/NuanceSource.php
index c3f1d57c9..5e06a5dc0 100644
--- a/src/applications/nuance/storage/NuanceSource.php
+++ b/src/applications/nuance/storage/NuanceSource.php
@@ -1,152 +1,141 @@
<?php
final class NuanceSource extends NuanceDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorNgramsInterface {
protected $name;
protected $type;
protected $data = array();
protected $mailKey;
protected $viewPolicy;
protected $editPolicy;
protected $defaultQueuePHID;
protected $isDisabled;
private $definition = self::ATTACHABLE;
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'data' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort255',
'type' => 'text32',
'mailKey' => 'bytes20',
'isDisabled' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_type' => array(
'columns' => array('type', 'dateModified'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(NuanceSourcePHIDType::TYPECONST);
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public function getURI() {
return '/nuance/source/view/'.$this->getID().'/';
}
public static function initializeNewSource(
PhabricatorUser $actor,
NuanceSourceDefinition $definition) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorNuanceApplication'))
->executeOne();
$view_policy = $app->getPolicy(
NuanceSourceDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(
NuanceSourceDefaultEditCapability::CAPABILITY);
return id(new NuanceSource())
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setType($definition->getSourceTypeConstant())
->attachDefinition($definition)
->setIsDisabled(0);
}
public function getDefinition() {
return $this->assertAttached($this->definition);
}
public function attachDefinition(NuanceSourceDefinition $definition) {
$this->definition = $definition;
return $this;
}
public function getSourceProperty($key, $default = null) {
return idx($this->data, $key, $default);
}
public function setSourceProperty($key, $value) {
$this->data[$key] = $value;
return $this;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new NuanceSourceEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new NuanceSourceTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorPolicyInterface )----------------------------------------- */
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 $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorNgramsInterface )----------------------------------------- */
public function newNgrams() {
return array(
id(new NuanceSourceNameNgrams())
->setValue($this->getName()),
);
}
}
diff --git a/src/applications/oauthserver/panel/PhabricatorOAuthServerAuthorizationsSettingsPanel.php b/src/applications/oauthserver/panel/PhabricatorOAuthServerAuthorizationsSettingsPanel.php
index 37e85ab53..89a1cc028 100644
--- a/src/applications/oauthserver/panel/PhabricatorOAuthServerAuthorizationsSettingsPanel.php
+++ b/src/applications/oauthserver/panel/PhabricatorOAuthServerAuthorizationsSettingsPanel.php
@@ -1,143 +1,147 @@
<?php
final class PhabricatorOAuthServerAuthorizationsSettingsPanel
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'oauthorizations';
}
public function getPanelName() {
return pht('OAuth Authorizations');
}
+ public function getPanelMenuIcon() {
+ return 'fa-exchange';
+ }
+
public function getPanelGroupKey() {
return PhabricatorSettingsLogsPanelGroup::PANELGROUPKEY;
}
public function isEnabled() {
return PhabricatorApplication::isClassInstalled(
'PhabricatorOAuthServerApplication');
}
public function processRequest(AphrontRequest $request) {
$viewer = $request->getUser();
// TODO: It would be nice to simply disable this panel, but we can't do
// viewer-based checks for enabled panels right now.
$app_class = 'PhabricatorOAuthServerApplication';
$installed = PhabricatorApplication::isClassInstalledForViewer(
$app_class,
$viewer);
if (!$installed) {
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle(pht('OAuth Not Available'))
->appendParagraph(
pht('You do not have access to OAuth authorizations.'))
->addCancelButton('/settings/');
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$authorizations = id(new PhabricatorOAuthClientAuthorizationQuery())
->setViewer($viewer)
->withUserPHIDs(array($viewer->getPHID()))
->execute();
$authorizations = mpull($authorizations, null, 'getID');
$panel_uri = $this->getPanelURI();
$revoke = $request->getInt('revoke');
if ($revoke) {
if (empty($authorizations[$revoke])) {
return new Aphront404Response();
}
if ($request->isFormPost()) {
$authorizations[$revoke]->delete();
return id(new AphrontRedirectResponse())->setURI($panel_uri);
}
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle(pht('Revoke Authorization?'))
->appendParagraph(
pht(
'This application will no longer be able to access Phabricator '.
'on your behalf.'))
->addSubmitButton(pht('Revoke Authorization'))
->addCancelButton($panel_uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$highlight = $request->getInt('id');
$rows = array();
$rowc = array();
foreach ($authorizations as $authorization) {
if ($highlight == $authorization->getID()) {
$rowc[] = 'highlighted';
} else {
$rowc[] = null;
}
$button = javelin_tag(
'a',
array(
'href' => $this->getPanelURI('?revoke='.$authorization->getID()),
'class' => 'small button button-grey',
'sigil' => 'workflow',
),
pht('Revoke'));
$rows[] = array(
phutil_tag(
'a',
array(
'href' => $authorization->getClient()->getViewURI(),
),
$authorization->getClient()->getName()),
$authorization->getScopeString(),
phabricator_datetime($authorization->getDateCreated(), $viewer),
phabricator_datetime($authorization->getDateModified(), $viewer),
$button,
);
}
$table = new AphrontTableView($rows);
$table->setNoDataString(
pht("You haven't authorized any OAuth applications."));
$table->setRowClasses($rowc);
$table->setHeaders(
array(
pht('Application'),
pht('Scope'),
pht('Created'),
pht('Updated'),
null,
));
$table->setColumnClasses(
array(
'pri',
'wide',
'right',
'right',
'action',
));
$header = id(new PHUIHeaderView())
->setHeader(pht('OAuth Application Authorizations'));
$panel = id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
->appendChild($table);
return $panel;
}
}
diff --git a/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php b/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php
index 4154383c3..a951bf578 100644
--- a/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php
+++ b/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php
@@ -1,139 +1,129 @@
<?php
final class PhabricatorOAuthServerClient
extends PhabricatorOAuthServerDAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorDestructibleInterface {
protected $secret;
protected $name;
protected $redirectURI;
protected $creatorPHID;
protected $isTrusted;
protected $viewPolicy;
protected $editPolicy;
protected $isDisabled;
public function getEditURI() {
$id = $this->getID();
return "/oauthserver/edit/{$id}/";
}
public function getViewURI() {
$id = $this->getID();
return "/oauthserver/client/view/{$id}/";
}
public static function initializeNewClient(PhabricatorUser $actor) {
return id(new PhabricatorOAuthServerClient())
->setCreatorPHID($actor->getPHID())
->setSecret(Filesystem::readRandomCharacters(32))
->setViewPolicy(PhabricatorPolicies::POLICY_USER)
->setEditPolicy($actor->getPHID())
->setIsDisabled(0)
->setIsTrusted(0);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text255',
'secret' => 'text32',
'redirectURI' => 'text255',
'isTrusted' => 'bool',
'isDisabled' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'creatorPHID' => array(
'columns' => array('creatorPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorOAuthServerClientPHIDType::TYPECONST);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
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 $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorOAuthServerEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorOAuthServerTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
- return $timeline;
- }
-
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$authorizations = id(new PhabricatorOAuthClientAuthorization())
->loadAllWhere('clientPHID = %s', $this->getPHID());
foreach ($authorizations as $authorization) {
$authorization->delete();
}
$tokens = id(new PhabricatorOAuthServerAccessToken())
->loadAllWhere('clientPHID = %s', $this->getPHID());
foreach ($tokens as $token) {
$token->delete();
}
$codes = id(new PhabricatorOAuthServerAuthorizationCode())
->loadAllWhere('clientPHID = %s', $this->getPHID());
foreach ($codes as $code) {
$code->delete();
}
$this->saveTransaction();
}
}
diff --git a/src/applications/owners/config/PhabricatorOwnersConfigOptions.php b/src/applications/owners/config/PhabricatorOwnersConfigOptions.php
index 0f827672b..330d465f3 100644
--- a/src/applications/owners/config/PhabricatorOwnersConfigOptions.php
+++ b/src/applications/owners/config/PhabricatorOwnersConfigOptions.php
@@ -1,55 +1,53 @@
<?php
final class PhabricatorOwnersConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Owners');
}
public function getDescription() {
return pht('Configure Owners.');
}
public function getIcon() {
return 'fa-gift';
}
public function getGroup() {
return 'apps';
}
public function getOptions() {
$custom_field_type = 'custom:PhabricatorCustomFieldConfigOptionType';
$default_fields = array();
$field_base_class = id(new PhabricatorOwnersPackage())
->getCustomFieldBaseClass();
$fields_example = array(
'mycompany:lore' => array(
'name' => pht('Package Lore'),
'type' => 'remarkup',
'caption' => pht('Tales of adventure for this package.'),
),
);
$fields_example = id(new PhutilJSON())->encodeFormatted($fields_example);
return array(
- $this->newOption('metamta.package.subject-prefix', 'string', '[Package]')
- ->setDescription(pht('Subject prefix for Owners email.')),
$this->newOption('owners.fields', $custom_field_type, $default_fields)
->setCustomData($field_base_class)
->setDescription(pht('Select and reorder package fields.')),
$this->newOption('owners.custom-field-definitions', 'wild', array())
->setSummary(pht('Custom Owners fields.'))
->setDescription(
pht(
'Map of custom fields for Owners packages. For details on '.
'adding custom fields to Owners, see "Configuring Custom '.
'Fields" in the documentation.'))
->addExample($fields_example, pht('Valid Setting')),
);
}
}
diff --git a/src/applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php b/src/applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php
index c6aad6e2b..888587270 100644
--- a/src/applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php
+++ b/src/applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php
@@ -1,74 +1,74 @@
<?php
final class PhabricatorOwnersPackageTransactionEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorOwnersApplication';
}
public function getEditorObjectsDescription() {
return pht('Owners Packages');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailSubjectPrefix() {
- return PhabricatorEnv::getEnvConfig('metamta.package.subject-prefix');
+ return pht('[Package]');
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$this->requireActor()->getPHID(),
);
}
protected function getMailCC(PhabricatorLiskDAO $object) {
return mpull($object->getOwners(), 'getUserPHID');
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new OwnersPackageReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$name = $object->getName();
return id(new PhabricatorMetaMTAMail())
->setSubject($name);
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$detail_uri = PhabricatorEnv::getProductionURI($object->getURI());
$body->addLinkSection(
pht('PACKAGE DETAIL'),
$detail_uri);
return $body;
}
protected function supportsSearch() {
return true;
}
}
diff --git a/src/applications/owners/storage/PhabricatorOwnersPackage.php b/src/applications/owners/storage/PhabricatorOwnersPackage.php
index 7e2a348c8..564fc8a28 100644
--- a/src/applications/owners/storage/PhabricatorOwnersPackage.php
+++ b/src/applications/owners/storage/PhabricatorOwnersPackage.php
@@ -1,815 +1,805 @@
<?php
final class PhabricatorOwnersPackage
extends PhabricatorOwnersDAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorCustomFieldInterface,
PhabricatorDestructibleInterface,
PhabricatorConduitResultInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface,
PhabricatorNgramsInterface {
protected $name;
protected $auditingEnabled;
protected $autoReview;
protected $description;
protected $status;
protected $viewPolicy;
protected $editPolicy;
protected $dominion;
protected $properties = array();
private $paths = self::ATTACHABLE;
private $owners = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
private $pathRepositoryMap = array();
const STATUS_ACTIVE = 'active';
const STATUS_ARCHIVED = 'archived';
const AUTOREVIEW_NONE = 'none';
const AUTOREVIEW_SUBSCRIBE = 'subscribe';
const AUTOREVIEW_SUBSCRIBE_ALWAYS = 'subscribe-always';
const AUTOREVIEW_REVIEW = 'review';
const AUTOREVIEW_REVIEW_ALWAYS = 'review-always';
const AUTOREVIEW_BLOCK = 'block';
const AUTOREVIEW_BLOCK_ALWAYS = 'block-always';
const DOMINION_STRONG = 'strong';
const DOMINION_WEAK = 'weak';
const PROPERTY_IGNORED = 'ignored';
public static function initializeNewPackage(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorOwnersApplication'))
->executeOne();
$view_policy = $app->getPolicy(
PhabricatorOwnersDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(
PhabricatorOwnersDefaultEditCapability::CAPABILITY);
return id(new PhabricatorOwnersPackage())
->setAuditingEnabled(0)
->setAutoReview(self::AUTOREVIEW_NONE)
->setDominion(self::DOMINION_STRONG)
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->attachPaths(array())
->setStatus(self::STATUS_ACTIVE)
->attachOwners(array())
->setDescription('');
}
public static function getStatusNameMap() {
return array(
self::STATUS_ACTIVE => pht('Active'),
self::STATUS_ARCHIVED => pht('Archived'),
);
}
public static function getAutoreviewOptionsMap() {
return array(
self::AUTOREVIEW_NONE => array(
'name' => pht('No Autoreview'),
),
self::AUTOREVIEW_REVIEW => array(
'name' => pht('Review Changes With Non-Owner Author'),
'authority' => true,
),
self::AUTOREVIEW_BLOCK => array(
'name' => pht('Review Changes With Non-Owner Author (Blocking)'),
'authority' => true,
),
self::AUTOREVIEW_SUBSCRIBE => array(
'name' => pht('Subscribe to Changes With Non-Owner Author'),
'authority' => true,
),
self::AUTOREVIEW_REVIEW_ALWAYS => array(
'name' => pht('Review All Changes'),
),
self::AUTOREVIEW_BLOCK_ALWAYS => array(
'name' => pht('Review All Changes (Blocking)'),
),
self::AUTOREVIEW_SUBSCRIBE_ALWAYS => array(
'name' => pht('Subscribe to All Changes'),
),
);
}
public static function getDominionOptionsMap() {
return array(
self::DOMINION_STRONG => array(
'name' => pht('Strong (Control All Paths)'),
'short' => pht('Strong'),
),
self::DOMINION_WEAK => array(
'name' => pht('Weak (Control Unowned Paths)'),
'short' => pht('Weak'),
),
);
}
protected function getConfiguration() {
return array(
// This information is better available from the history table.
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort',
'description' => 'text',
'auditingEnabled' => 'bool',
'status' => 'text32',
'autoReview' => 'text32',
'dominion' => 'text32',
),
) + parent::getConfiguration();
}
public function getPHIDType() {
return PhabricatorOwnersPackagePHIDType::TYPECONST;
}
public function isArchived() {
return ($this->getStatus() == self::STATUS_ARCHIVED);
}
public function getMustMatchUngeneratedPaths() {
$ignore_attributes = $this->getIgnoredPathAttributes();
return !empty($ignore_attributes['generated']);
}
public function getPackageProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setPackageProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getIgnoredPathAttributes() {
return $this->getPackageProperty(self::PROPERTY_IGNORED, array());
}
public function setIgnoredPathAttributes(array $attributes) {
return $this->setPackageProperty(self::PROPERTY_IGNORED, $attributes);
}
public function loadOwners() {
if (!$this->getID()) {
return array();
}
return id(new PhabricatorOwnersOwner())->loadAllWhere(
'packageID = %d',
$this->getID());
}
public function loadPaths() {
if (!$this->getID()) {
return array();
}
return id(new PhabricatorOwnersPath())->loadAllWhere(
'packageID = %d',
$this->getID());
}
public static function loadAffectedPackages(
PhabricatorRepository $repository,
array $paths) {
if (!$paths) {
return array();
}
return self::loadPackagesForPaths($repository, $paths);
}
public static function loadAffectedPackagesForChangesets(
PhabricatorRepository $repository,
DifferentialDiff $diff,
array $changesets) {
assert_instances_of($changesets, 'DifferentialChangeset');
$paths_all = array();
$paths_ungenerated = array();
foreach ($changesets as $changeset) {
$path = $changeset->getAbsoluteRepositoryPath($repository, $diff);
$paths_all[] = $path;
if (!$changeset->isGeneratedChangeset()) {
$paths_ungenerated[] = $path;
}
}
if (!$paths_all) {
return array();
}
$packages_all = self::loadAffectedPackages(
$repository,
$paths_all);
// If there are no generated changesets, we can't possibly need to throw
// away any packages for matching only generated paths. Just return the
// full set of packages.
if ($paths_ungenerated === $paths_all) {
return $packages_all;
}
$must_match_ungenerated = array();
foreach ($packages_all as $package) {
if ($package->getMustMatchUngeneratedPaths()) {
$must_match_ungenerated[] = $package;
}
}
// If no affected packages have the "Ignore Generated Paths" flag set, we
// can't possibly need to throw any away.
if (!$must_match_ungenerated) {
return $packages_all;
}
if ($paths_ungenerated) {
$packages_ungenerated = self::loadAffectedPackages(
$repository,
$paths_ungenerated);
} else {
$packages_ungenerated = array();
}
// We have some generated paths, and some packages that ignore generated
// paths. Take all the packages which:
//
// - ignore generated paths; and
// - didn't match any ungenerated paths
//
// ...and remove them from the list.
$must_match_ungenerated = mpull($must_match_ungenerated, null, 'getID');
$packages_ungenerated = mpull($packages_ungenerated, null, 'getID');
$packages_all = mpull($packages_all, null, 'getID');
foreach ($must_match_ungenerated as $package_id => $package) {
if (!isset($packages_ungenerated[$package_id])) {
unset($packages_all[$package_id]);
}
}
return $packages_all;
}
public static function loadOwningPackages($repository, $path) {
if (empty($path)) {
return array();
}
return self::loadPackagesForPaths($repository, array($path), 1);
}
private static function loadPackagesForPaths(
PhabricatorRepository $repository,
array $paths,
$limit = 0) {
$fragments = array();
foreach ($paths as $path) {
foreach (self::splitPath($path) as $fragment) {
$fragments[$fragment][$path] = true;
}
}
$package = new PhabricatorOwnersPackage();
$path = new PhabricatorOwnersPath();
$conn = $package->establishConnection('r');
$repository_clause = qsprintf(
$conn,
'AND p.repositoryPHID = %s',
$repository->getPHID());
// NOTE: The list of $paths may be very large if we're coming from
// the OwnersWorker and processing, e.g., an SVN commit which created a new
// branch. Break it apart so that it will fit within 'max_allowed_packet',
// and then merge results in PHP.
$rows = array();
foreach (array_chunk(array_keys($fragments), 1024) as $chunk) {
$indexes = array();
foreach ($chunk as $fragment) {
$indexes[] = PhabricatorHash::digestForIndex($fragment);
}
$rows[] = queryfx_all(
$conn,
'SELECT pkg.id, pkg.dominion, p.excluded, p.path
FROM %T pkg JOIN %T p ON p.packageID = pkg.id
WHERE p.pathIndex IN (%Ls) AND pkg.status IN (%Ls) %Q',
$package->getTableName(),
$path->getTableName(),
$indexes,
array(
self::STATUS_ACTIVE,
),
$repository_clause);
}
$rows = array_mergev($rows);
$ids = self::findLongestPathsPerPackage($rows, $fragments);
if (!$ids) {
return array();
}
arsort($ids);
if ($limit) {
$ids = array_slice($ids, 0, $limit, $preserve_keys = true);
}
$ids = array_keys($ids);
$packages = $package->loadAllWhere('id in (%Ld)', $ids);
$packages = array_select_keys($packages, $ids);
return $packages;
}
public static function loadPackagesForRepository($repository) {
$package = new PhabricatorOwnersPackage();
$ids = ipull(
queryfx_all(
$package->establishConnection('r'),
'SELECT DISTINCT packageID FROM %T WHERE repositoryPHID = %s',
id(new PhabricatorOwnersPath())->getTableName(),
$repository->getPHID()),
'packageID');
return $package->loadAllWhere('id in (%Ld)', $ids);
}
public static function findLongestPathsPerPackage(array $rows, array $paths) {
// Build a map from each path to all the package paths which match it.
$path_hits = array();
$weak = array();
foreach ($rows as $row) {
$id = $row['id'];
$path = $row['path'];
$length = strlen($path);
$excluded = $row['excluded'];
if ($row['dominion'] === self::DOMINION_WEAK) {
$weak[$id] = true;
}
$matches = $paths[$path];
foreach ($matches as $match => $ignored) {
$path_hits[$match][] = array(
'id' => $id,
'excluded' => $excluded,
'length' => $length,
);
}
}
// For each path, process the matching package paths to figure out which
// packages actually own it.
$path_packages = array();
foreach ($path_hits as $match => $hits) {
$hits = isort($hits, 'length');
$packages = array();
foreach ($hits as $hit) {
$package_id = $hit['id'];
if ($hit['excluded']) {
unset($packages[$package_id]);
} else {
$packages[$package_id] = $hit;
}
}
$path_packages[$match] = $packages;
}
// Remove packages with weak dominion rules that should cede control to
// a more specific package.
if ($weak) {
foreach ($path_packages as $match => $packages) {
// Group packages by length.
$length_map = array();
foreach ($packages as $package_id => $package) {
$length_map[$package['length']][$package_id] = $package;
}
// For each path length, remove all weak packages if there are any
// strong packages of the same length. This makes sure that if there
// are one or more strong claims on a particular path, only those
// claims stand.
foreach ($length_map as $package_list) {
$any_strong = false;
foreach ($package_list as $package_id => $package) {
if (!isset($weak[$package_id])) {
$any_strong = true;
break;
}
}
if ($any_strong) {
foreach ($package_list as $package_id => $package) {
if (isset($weak[$package_id])) {
unset($packages[$package_id]);
}
}
}
}
$packages = isort($packages, 'length');
$packages = array_reverse($packages, true);
$best_length = null;
foreach ($packages as $package_id => $package) {
// If this is the first package we've encountered, note its length.
// We're iterating over the packages from longest to shortest match,
// so packages of this length always have the best claim on the path.
if ($best_length === null) {
$best_length = $package['length'];
}
// If this package has the same length as the best length, its claim
// stands.
if ($package['length'] === $best_length) {
continue;
}
// If this is a weak package and does not have the best length,
// cede its claim to the stronger package.
if (isset($weak[$package_id])) {
unset($packages[$package_id]);
}
}
$path_packages[$match] = $packages;
}
}
// For each package that owns at least one path, identify the longest
// path it owns.
$package_lengths = array();
foreach ($path_packages as $match => $hits) {
foreach ($hits as $hit) {
$length = $hit['length'];
$id = $hit['id'];
if (empty($package_lengths[$id])) {
$package_lengths[$id] = $length;
} else {
$package_lengths[$id] = max($package_lengths[$id], $length);
}
}
}
return $package_lengths;
}
public static function splitPath($path) {
$result = array(
'/',
);
$parts = explode('/', $path);
$buffer = '/';
foreach ($parts as $part) {
if (!strlen($part)) {
continue;
}
$buffer = $buffer.$part.'/';
$result[] = $buffer;
}
return $result;
}
public function attachPaths(array $paths) {
assert_instances_of($paths, 'PhabricatorOwnersPath');
$this->paths = $paths;
// Drop this cache if we're attaching new paths.
$this->pathRepositoryMap = array();
return $this;
}
public function getPaths() {
return $this->assertAttached($this->paths);
}
public function getPathsForRepository($repository_phid) {
if (isset($this->pathRepositoryMap[$repository_phid])) {
return $this->pathRepositoryMap[$repository_phid];
}
$map = array();
foreach ($this->getPaths() as $path) {
if ($path->getRepositoryPHID() == $repository_phid) {
$map[] = $path;
}
}
$this->pathRepositoryMap[$repository_phid] = $map;
return $this->pathRepositoryMap[$repository_phid];
}
public function attachOwners(array $owners) {
assert_instances_of($owners, 'PhabricatorOwnersOwner');
$this->owners = $owners;
return $this;
}
public function getOwners() {
return $this->assertAttached($this->owners);
}
public function getOwnerPHIDs() {
return mpull($this->getOwners(), 'getUserPHID');
}
public function isOwnerPHID($phid) {
if (!$phid) {
return false;
}
$owner_phids = $this->getOwnerPHIDs();
$owner_phids = array_fuse($owner_phids);
return isset($owner_phids[$phid]);
}
public function getMonogram() {
return 'O'.$this->getID();
}
public function getURI() {
// TODO: Move these to "/O123" for consistency.
return '/owners/package/'.$this->getID().'/';
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
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 $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
if ($this->isOwnerPHID($viewer->getPHID())) {
return true;
}
break;
}
return false;
}
public function describeAutomaticCapability($capability) {
return pht('Owners of a package may always view it.');
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorOwnersPackageTransactionEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorOwnersPackageTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
- return $timeline;
- }
-
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('owners.fields');
}
public function getCustomFieldBaseClass() {
return 'PhabricatorOwnersCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$conn_w = $this->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE packageID = %d',
id(new PhabricatorOwnersPath())->getTableName(),
$this->getID());
queryfx(
$conn_w,
'DELETE FROM %T WHERE packageID = %d',
id(new PhabricatorOwnersOwner())->getTableName(),
$this->getID());
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of the package.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('description')
->setType('string')
->setDescription(pht('The package description.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('status')
->setType('string')
->setDescription(pht('Active or archived status of the package.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('owners')
->setType('list<map<string, wild>>')
->setDescription(pht('List of package owners.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('review')
->setType('map<string, wild>')
->setDescription(pht('Auto review information.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('audit')
->setType('map<string, wild>')
->setDescription(pht('Auto audit information.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('dominion')
->setType('map<string, wild>')
->setDescription(pht('Dominion setting information.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('ignored')
->setType('map<string, wild>')
->setDescription(pht('Ignored attribute information.')),
);
}
public function getFieldValuesForConduit() {
$owner_list = array();
foreach ($this->getOwners() as $owner) {
$owner_list[] = array(
'ownerPHID' => $owner->getUserPHID(),
);
}
$review_map = self::getAutoreviewOptionsMap();
$review_value = $this->getAutoReview();
if (isset($review_map[$review_value])) {
$review_label = $review_map[$review_value]['name'];
} else {
$review_label = pht('Unknown ("%s")', $review_value);
}
$review = array(
'value' => $review_value,
'label' => $review_label,
);
if ($this->getAuditingEnabled()) {
$audit_value = 'audit';
$audit_label = pht('Auditing Enabled');
} else {
$audit_value = 'none';
$audit_label = pht('No Auditing');
}
$audit = array(
'value' => $audit_value,
'label' => $audit_label,
);
$dominion_value = $this->getDominion();
$dominion_map = self::getDominionOptionsMap();
if (isset($dominion_map[$dominion_value])) {
$dominion_label = $dominion_map[$dominion_value]['name'];
$dominion_short = $dominion_map[$dominion_value]['short'];
} else {
$dominion_label = pht('Unknown ("%s")', $dominion_value);
$dominion_short = pht('Unknown ("%s")', $dominion_value);
}
$dominion = array(
'value' => $dominion_value,
'label' => $dominion_label,
'short' => $dominion_short,
);
// Force this to always emit as a JSON object even if empty, never as
// a JSON list.
$ignored = $this->getIgnoredPathAttributes();
if (!$ignored) {
$ignored = (object)array();
}
return array(
'name' => $this->getName(),
'description' => $this->getDescription(),
'status' => $this->getStatus(),
'owners' => $owner_list,
'review' => $review,
'audit' => $audit,
'dominion' => $dominion,
'ignored' => $ignored,
);
}
public function getConduitSearchAttachments() {
return array(
id(new PhabricatorOwnersPathsSearchEngineAttachment())
->setAttachmentKey('paths'),
);
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PhabricatorOwnersPackageFulltextEngine();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new PhabricatorOwnersPackageFerretEngine();
}
/* -( PhabricatorNgramsInterface )----------------------------------------- */
public function newNgrams() {
return array(
id(new PhabricatorOwnersPackageNameNgrams())
->setValue($this->getName()),
);
}
}
diff --git a/src/applications/packages/storage/PhabricatorPackagesPackage.php b/src/applications/packages/storage/PhabricatorPackagesPackage.php
index 53e040e5f..15748329e 100644
--- a/src/applications/packages/storage/PhabricatorPackagesPackage.php
+++ b/src/applications/packages/storage/PhabricatorPackagesPackage.php
@@ -1,257 +1,247 @@
<?php
final class PhabricatorPackagesPackage
extends PhabricatorPackagesDAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorDestructibleInterface,
PhabricatorSubscribableInterface,
PhabricatorProjectInterface,
PhabricatorConduitResultInterface,
PhabricatorNgramsInterface {
protected $name;
protected $publisherPHID;
protected $packageKey;
protected $viewPolicy;
protected $editPolicy;
private $publisher = self::ATTACHABLE;
public static function initializeNewPackage(PhabricatorUser $actor) {
$packages_application = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorPackagesApplication'))
->executeOne();
$view_policy = $packages_application->getPolicy(
PhabricatorPackagesPackageDefaultViewCapability::CAPABILITY);
$edit_policy = $packages_application->getPolicy(
PhabricatorPackagesPackageDefaultEditCapability::CAPABILITY);
return id(new self())
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort64',
'packageKey' => 'sort64',
),
self::CONFIG_KEY_SCHEMA => array(
'key_package' => array(
'columns' => array('publisherPHID', 'packageKey'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPackagesPackagePHIDType::TYPECONST);
}
public function getURI() {
$full_key = $this->getFullKey();
return "/package/{$full_key}/";
}
public function getFullKey() {
$publisher = $this->getPublisher();
$publisher_key = $publisher->getPublisherKey();
$package_key = $this->getPackageKey();
return "{$publisher_key}/{$package_key}";
}
public function attachPublisher(PhabricatorPackagesPublisher $publisher) {
$this->publisher = $publisher;
return $this;
}
public function getPublisher() {
return $this->assertAttached($this->publisher);
}
public static function assertValidPackageName($value) {
$length = phutil_utf8_strlen($value);
if (!$length) {
throw new Exception(
pht(
'Package name "%s" is not valid: package names are required.',
$value));
}
$max_length = 64;
if ($length > $max_length) {
throw new Exception(
pht(
'Package name "%s" is not valid: package names must not be '.
'more than %s characters long.',
$value,
new PhutilNumber($max_length)));
}
}
public static function assertValidPackageKey($value) {
$length = phutil_utf8_strlen($value);
if (!$length) {
throw new Exception(
pht(
'Package key "%s" is not valid: package keys are required.',
$value));
}
$max_length = 64;
if ($length > $max_length) {
throw new Exception(
pht(
'Package key "%s" is not valid: package keys must not be '.
'more than %s characters long.',
$value,
new PhutilNumber($max_length)));
}
if (!preg_match('/^[a-z]+\z/', $value)) {
throw new Exception(
pht(
'Package key "%s" is not valid: package keys may only contain '.
'lowercase latin letters.',
$value));
}
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return false;
}
/* -( Policy Interface )--------------------------------------------------- */
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 $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
return false;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$viewer = $engine->getViewer();
$this->openTransaction();
$versions = id(new PhabricatorPackagesVersionQuery())
->setViewer($viewer)
->withPackagePHIDs(array($this->getPHID()))
->execute();
foreach ($versions as $version) {
$engine->destroyObject($version);
}
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorPackagesPackageEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorPackagesPackageTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
- return $timeline;
- }
-
/* -( PhabricatorNgramsInterface )----------------------------------------- */
public function newNgrams() {
return array(
id(new PhabricatorPackagesPackageNameNgrams())
->setValue($this->getName()),
);
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of the package.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('packageKey')
->setType('string')
->setDescription(pht('The unique key of the package.')),
);
}
public function getFieldValuesForConduit() {
$publisher = $this->getPublisher();
$publisher_map = array(
'id' => (int)$publisher->getID(),
'phid' => $publisher->getPHID(),
'name' => $publisher->getName(),
'publisherKey' => $publisher->getPublisherKey(),
);
return array(
'name' => $this->getName(),
'packageKey' => $this->getPackageKey(),
'fullKey' => $this->getFullKey(),
'publisher' => $publisher_map,
);
}
public function getConduitSearchAttachments() {
return array();
}
}
diff --git a/src/applications/packages/storage/PhabricatorPackagesPublisher.php b/src/applications/packages/storage/PhabricatorPackagesPublisher.php
index 57737cdab..21b586dc1 100644
--- a/src/applications/packages/storage/PhabricatorPackagesPublisher.php
+++ b/src/applications/packages/storage/PhabricatorPackagesPublisher.php
@@ -1,222 +1,212 @@
<?php
final class PhabricatorPackagesPublisher
extends PhabricatorPackagesDAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorDestructibleInterface,
PhabricatorSubscribableInterface,
PhabricatorProjectInterface,
PhabricatorConduitResultInterface,
PhabricatorNgramsInterface {
protected $name;
protected $publisherKey;
protected $editPolicy;
public static function initializeNewPublisher(PhabricatorUser $actor) {
$packages_application = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorPackagesApplication'))
->executeOne();
$edit_policy = $packages_application->getPolicy(
PhabricatorPackagesPublisherDefaultEditCapability::CAPABILITY);
return id(new self())
->setEditPolicy($edit_policy);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort64',
'publisherKey' => 'sort64',
),
self::CONFIG_KEY_SCHEMA => array(
'key_publisher' => array(
'columns' => array('publisherKey'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPackagesPublisherPHIDType::TYPECONST);
}
public function getURI() {
$publisher_key = $this->getPublisherKey();
return "/package/{$publisher_key}/";
}
public static function assertValidPublisherName($value) {
$length = phutil_utf8_strlen($value);
if (!$length) {
throw new Exception(
pht(
'Publisher name "%s" is not valid: publisher names are required.',
$value));
}
$max_length = 64;
if ($length > $max_length) {
throw new Exception(
pht(
'Publisher name "%s" is not valid: publisher names must not be '.
'more than %s characters long.',
$value,
new PhutilNumber($max_length)));
}
}
public static function assertValidPublisherKey($value) {
$length = phutil_utf8_strlen($value);
if (!$length) {
throw new Exception(
pht(
'Publisher key "%s" is not valid: publisher keys are required.',
$value));
}
$max_length = 64;
if ($length > $max_length) {
throw new Exception(
pht(
'Publisher key "%s" is not valid: publisher keys must not be '.
'more than %s characters long.',
$value,
new PhutilNumber($max_length)));
}
if (!preg_match('/^[a-z]+\z/', $value)) {
throw new Exception(
pht(
'Publisher key "%s" is not valid: publisher keys may only contain '.
'lowercase latin letters.',
$value));
}
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return false;
}
/* -( Policy Interface )--------------------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::getMostOpenPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
return false;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$viewer = $engine->getViewer();
$this->openTransaction();
$packages = id(new PhabricatorPackagesPackageQuery())
->setViewer($viewer)
->withPublisherPHIDs(array($this->getPHID()))
->execute();
foreach ($packages as $package) {
$engine->destroyObject($package);
}
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorPackagesPublisherEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorPackagesPublisherTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
- return $timeline;
- }
-
/* -( PhabricatorNgramsInterface )----------------------------------------- */
public function newNgrams() {
return array(
id(new PhabricatorPackagesPublisherNameNgrams())
->setValue($this->getName()),
);
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of the publisher.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('publisherKey')
->setType('string')
->setDescription(pht('The unique key of the publisher.')),
);
}
public function getFieldValuesForConduit() {
return array(
'name' => $this->getName(),
'publisherKey' => $this->getPublisherKey(),
);
}
public function getConduitSearchAttachments() {
return array();
}
}
diff --git a/src/applications/packages/storage/PhabricatorPackagesVersion.php b/src/applications/packages/storage/PhabricatorPackagesVersion.php
index aba247307..cd5e2648f 100644
--- a/src/applications/packages/storage/PhabricatorPackagesVersion.php
+++ b/src/applications/packages/storage/PhabricatorPackagesVersion.php
@@ -1,208 +1,198 @@
<?php
final class PhabricatorPackagesVersion
extends PhabricatorPackagesDAO
implements
PhabricatorPolicyInterface,
PhabricatorExtendedPolicyInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorDestructibleInterface,
PhabricatorSubscribableInterface,
PhabricatorProjectInterface,
PhabricatorConduitResultInterface,
PhabricatorNgramsInterface {
protected $name;
protected $packagePHID;
private $package;
public static function initializeNewVersion(PhabricatorUser $actor) {
return id(new self());
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort64',
),
self::CONFIG_KEY_SCHEMA => array(
'key_package' => array(
'columns' => array('packagePHID', 'name'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPackagesVersionPHIDType::TYPECONST);
}
public function getURI() {
$package = $this->getPackage();
$full_key = $package->getFullKey();
$name = $this->getName();
return "/package/{$full_key}/{$name}/";
}
public function attachPackage(PhabricatorPackagesPackage $package) {
$this->package = $package;
return $this;
}
public function getPackage() {
return $this->assertAttached($this->package);
}
public static function assertValidVersionName($value) {
$length = phutil_utf8_strlen($value);
if (!$length) {
throw new Exception(
pht(
'Version name "%s" is not valid: version names are required.',
$value));
}
$max_length = 64;
if ($length > $max_length) {
throw new Exception(
pht(
'Version name "%s" is not valid: version names must not be '.
'more than %s characters long.',
$value,
new PhutilNumber($max_length)));
}
if (!preg_match('/^[A-Za-z0-9.-]+\z/', $value)) {
throw new Exception(
pht(
'Version name "%s" is not valid: version names may only contain '.
'latin letters, digits, periods, and hyphens.',
$value));
}
if (preg_match('/^[.-]|[.-]$/', $value)) {
throw new Exception(
pht(
'Version name "%s" is not valid: version names may not start or '.
'end with a period or hyphen.',
$value));
}
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return false;
}
/* -( Policy Interface )--------------------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::getMostOpenPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return PhabricatorPolicies::POLICY_USER;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
return false;
}
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
return array(
array(
$this->getPackage(),
$capability,
),
);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->delete();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorPackagesVersionEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorPackagesVersionTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
- return $timeline;
- }
-
/* -( PhabricatorNgramsInterface )----------------------------------------- */
public function newNgrams() {
return array(
id(new PhabricatorPackagesVersionNameNgrams())
->setValue($this->getName()),
);
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of the version.')),
);
}
public function getFieldValuesForConduit() {
return array(
'name' => $this->getName(),
);
}
public function getConduitSearchAttachments() {
return array();
}
}
diff --git a/src/applications/passphrase/controller/PassphraseCredentialRevealController.php b/src/applications/passphrase/controller/PassphraseCredentialRevealController.php
index 3a40d253c..99b6711ae 100644
--- a/src/applications/passphrase/controller/PassphraseCredentialRevealController.php
+++ b/src/applications/passphrase/controller/PassphraseCredentialRevealController.php
@@ -1,107 +1,104 @@
<?php
final class PassphraseCredentialRevealController
extends PassphraseController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$credential = id(new PassphraseCredentialQuery())
->setViewer($viewer)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->needSecrets(true)
->executeOne();
if (!$credential) {
return new Aphront404Response();
}
- $view_uri = '/K'.$credential->getID();
+ $view_uri = $credential->getURI();
- $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
- $viewer,
- $request,
- $view_uri);
$is_locked = $credential->getIsLocked();
if ($is_locked) {
return $this->newDialog()
->setUser($viewer)
->setTitle(pht('Credential is locked'))
->appendChild(
pht(
'This credential can not be shown, because it is locked.'))
->addCancelButton($view_uri);
}
- if ($request->isFormPost()) {
+ if ($request->isFormOrHisecPost()) {
$secret = $credential->getSecret();
if (!$secret) {
$body = pht('This credential has no associated secret.');
} else if (!strlen($secret->openEnvelope())) {
$body = pht('This credential has an empty secret.');
} else {
$body = id(new PHUIFormLayoutView())
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel(pht('Plaintext'))
->setReadOnly(true)
->setCustomClass('PhabricatorMonospaced')
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)
->setValue($secret->openEnvelope()));
}
// NOTE: Disable workflow on the cancel button to reload the page so
// the viewer can see that their view was logged.
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setWidth(AphrontDialogView::WIDTH_FORM)
->setTitle(pht('Credential Secret (%s)', $credential->getMonogram()))
->appendChild($body)
->setDisableWorkflowOnCancel(true)
->addCancelButton($view_uri, pht('Done'));
$type_secret = PassphraseCredentialLookedAtTransaction::TRANSACTIONTYPE;
$xactions = array(
id(new PassphraseCredentialTransaction())
->setTransactionType($type_secret)
->setNewValue(true),
);
$editor = id(new PassphraseCredentialTransactionEditor())
->setActor($viewer)
+ ->setCancelURI($view_uri)
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request)
->applyTransactions($credential, $xactions);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
if ($is_serious) {
$body = pht(
'The secret associated with this credential will be shown in plain '.
'text on your screen.');
} else {
$body = pht(
'The secret associated with this credential will be shown in plain '.
'text on your screen. Before continuing, wrap your arms around '.
'your monitor to create a human shield, keeping it safe from '.
'prying eyes. Protect company secrets!');
}
return $this->newDialog()
->setUser($viewer)
->setTitle(pht('Really show secret?'))
->appendChild($body)
->addSubmitButton(pht('Show Secret'))
->addCancelButton($view_uri);
}
}
diff --git a/src/applications/passphrase/storage/PassphraseCredential.php b/src/applications/passphrase/storage/PassphraseCredential.php
index 737a20c1e..c470ea661 100644
--- a/src/applications/passphrase/storage/PassphraseCredential.php
+++ b/src/applications/passphrase/storage/PassphraseCredential.php
@@ -1,209 +1,202 @@
<?php
final class PassphraseCredential extends PassphraseDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorFlaggableInterface,
PhabricatorSubscribableInterface,
PhabricatorDestructibleInterface,
PhabricatorSpacesInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface {
protected $name;
protected $credentialType;
protected $providesType;
protected $viewPolicy;
protected $editPolicy;
protected $description;
protected $username;
protected $secretID;
protected $isDestroyed;
protected $isLocked = 0;
protected $allowConduit = 0;
protected $authorPHID;
protected $spacePHID;
private $secret = self::ATTACHABLE;
private $implementation = self::ATTACHABLE;
public static function initializeNewCredential(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorPassphraseApplication'))
->executeOne();
$view_policy = $app->getPolicy(PassphraseDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(PassphraseDefaultEditCapability::CAPABILITY);
return id(new PassphraseCredential())
->setName('')
->setUsername('')
->setDescription('')
->setIsDestroyed(0)
->setAuthorPHID($actor->getPHID())
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setSpacePHID($actor->getDefaultSpacePHID());
}
public function getMonogram() {
return 'K'.$this->getID();
}
+ public function getURI() {
+ return '/'.$this->getMonogram();
+ }
+
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text255',
'credentialType' => 'text64',
'providesType' => 'text64',
'description' => 'text',
'username' => 'text255',
'secretID' => 'id?',
'isDestroyed' => 'bool',
'isLocked' => 'bool',
'allowConduit' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_secret' => array(
'columns' => array('secretID'),
'unique' => true,
),
'key_type' => array(
'columns' => array('credentialType'),
),
'key_provides' => array(
'columns' => array('providesType'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PassphraseCredentialPHIDType::TYPECONST);
}
public function attachSecret(PhutilOpaqueEnvelope $secret = null) {
$this->secret = $secret;
return $this;
}
public function getSecret() {
return $this->assertAttached($this->secret);
}
public function getCredentialTypeImplementation() {
$type = $this->getCredentialType();
return PassphraseCredentialType::getTypeByConstant($type);
}
public function attachImplementation(PassphraseCredentialType $impl) {
$this->implementation = $impl;
return $this;
}
public function getImplementation() {
return $this->assertAttached($this->implementation);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PassphraseCredentialTransactionEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PassphraseCredentialTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorPolicyInterface )----------------------------------------- */
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 $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return false;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$secrets = id(new PassphraseSecret())->loadAllWhere(
'id = %d',
$this->getSecretID());
foreach ($secrets as $secret) {
$secret->delete();
}
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorSpacesInterface )----------------------------------------- */
public function getSpacePHID() {
return $this->spacePHID;
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PassphraseCredentialFulltextEngine();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new PassphraseCredentialFerretEngine();
}
}
diff --git a/src/applications/passphrase/xaction/PassphraseCredentialLookedAtTransaction.php b/src/applications/passphrase/xaction/PassphraseCredentialLookedAtTransaction.php
index 3d8cb36f3..fc76ab0d5 100644
--- a/src/applications/passphrase/xaction/PassphraseCredentialLookedAtTransaction.php
+++ b/src/applications/passphrase/xaction/PassphraseCredentialLookedAtTransaction.php
@@ -1,33 +1,39 @@
<?php
final class PassphraseCredentialLookedAtTransaction
extends PassphraseCredentialTransactionType {
const TRANSACTIONTYPE = 'passphrase:lookedAtSecret';
public function generateOldValue($object) {
return null;
}
public function getTitle() {
return pht(
'%s examined the secret plaintext for this credential.',
$this->renderAuthor());
}
public function getTitleForFeed() {
return pht(
'%s examined the secret plaintext for credential %s.',
$this->renderAuthor(),
$this->renderObject());
}
public function getIcon() {
return 'fa-eye';
}
public function getColor() {
return 'blue';
}
+ public function shouldTryMFA(
+ $object,
+ PhabricatorApplicationTransaction $xaction) {
+ return true;
+ }
+
}
diff --git a/src/applications/paste/config/PhabricatorPasteConfigOptions.php b/src/applications/paste/config/PhabricatorPasteConfigOptions.php
deleted file mode 100644
index 15b32eeb0..000000000
--- a/src/applications/paste/config/PhabricatorPasteConfigOptions.php
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-
-final class PhabricatorPasteConfigOptions
- extends PhabricatorApplicationConfigOptions {
-
- public function getName() {
- return pht('Paste');
- }
-
- public function getDescription() {
- return pht('Configure Paste.');
- }
-
- public function getIcon() {
- return 'fa-paste';
- }
-
- public function getGroup() {
- return 'apps';
- }
-
- public function getOptions() {
- return array(
- $this->newOption(
- 'metamta.paste.subject-prefix',
- 'string',
- '[Paste]')
- ->setDescription(pht('Subject prefix for Paste email.')),
- );
- }
-
-}
diff --git a/src/applications/paste/editor/PhabricatorPasteEditor.php b/src/applications/paste/editor/PhabricatorPasteEditor.php
index c31291572..76ade878c 100644
--- a/src/applications/paste/editor/PhabricatorPasteEditor.php
+++ b/src/applications/paste/editor/PhabricatorPasteEditor.php
@@ -1,97 +1,97 @@
<?php
final class PhabricatorPasteEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorPasteApplication';
}
public function getEditorObjectsDescription() {
return pht('Pastes');
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this paste.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created %s.', $author, $object);
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
$types[] = PhabricatorTransactions::TYPE_COMMENT;
return $types;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
if ($this->getIsNewObject()) {
return false;
}
return true;
}
protected function getMailSubjectPrefix() {
- return PhabricatorEnv::getEnvConfig('metamta.paste.subject-prefix');
+ return pht('[Paste]');
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$object->getAuthorPHID(),
$this->getActingAsPHID(),
);
}
public function getMailTagsMap() {
return array(
PhabricatorPasteTransaction::MAILTAG_CONTENT =>
pht('Paste title, language or text changes.'),
PhabricatorPasteTransaction::MAILTAG_COMMENT =>
pht('Someone comments on a paste.'),
PhabricatorPasteTransaction::MAILTAG_OTHER =>
pht('Other paste activity not listed above occurs.'),
);
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new PasteReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$name = $object->getTitle();
return id(new PhabricatorMetaMTAMail())
->setSubject("P{$id}: {$name}");
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$body->addLinkSection(
pht('PASTE DETAIL'),
PhabricatorEnv::getProductionURI('/P'.$object->getID()));
return $body;
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
}
diff --git a/src/applications/paste/mail/PasteCreateMailReceiver.php b/src/applications/paste/mail/PasteCreateMailReceiver.php
index a8f7693d1..5562858b8 100644
--- a/src/applications/paste/mail/PasteCreateMailReceiver.php
+++ b/src/applications/paste/mail/PasteCreateMailReceiver.php
@@ -1,65 +1,64 @@
<?php
-final class PasteCreateMailReceiver extends PhabricatorMailReceiver {
+final class PasteCreateMailReceiver
+ extends PhabricatorApplicationMailReceiver {
- public function isEnabled() {
- $app_class = 'PhabricatorPasteApplication';
- return PhabricatorApplication::isClassInstalled($app_class);
- }
-
- public function canAcceptMail(PhabricatorMetaMTAReceivedMail $mail) {
- $paste_app = new PhabricatorPasteApplication();
- return $this->canAcceptApplicationMail($paste_app, $mail);
+ protected function newApplication() {
+ return new PhabricatorPasteApplication();
}
protected function processReceivedMail(
PhabricatorMetaMTAReceivedMail $mail,
- PhabricatorUser $sender) {
+ PhutilEmailAddress $target) {
+ $author = $this->getAuthor();
$title = $mail->getSubject();
if (!$title) {
$title = pht('Email Paste');
}
$xactions = array();
$xactions[] = id(new PhabricatorPasteTransaction())
->setTransactionType(PhabricatorPasteContentTransaction::TRANSACTIONTYPE)
->setNewValue($mail->getCleanTextBody());
$xactions[] = id(new PhabricatorPasteTransaction())
->setTransactionType(PhabricatorPasteTitleTransaction::TRANSACTIONTYPE)
->setNewValue($title);
- $paste = PhabricatorPaste::initializeNewPaste($sender);
+ $paste = PhabricatorPaste::initializeNewPaste($author);
$content_source = $mail->newContentSource();
$editor = id(new PhabricatorPasteEditor())
- ->setActor($sender)
+ ->setActor($author)
->setContentSource($content_source)
->setContinueOnNoEffect(true);
$xactions = $editor->applyTransactions($paste, $xactions);
$mail->setRelatedPHID($paste->getPHID());
- $subject_prefix =
- PhabricatorEnv::getEnvConfig('metamta.paste.subject-prefix');
+ $sender = $this->getSender();
+ if (!$sender) {
+ return;
+ }
+
+ $subject_prefix = pht('[Paste]');
$subject = pht('You successfully created a paste.');
$paste_uri = PhabricatorEnv::getProductionURI($paste->getURI());
$body = new PhabricatorMetaMTAMailBody();
$body->addRawSection($subject);
$body->addTextSection(pht('PASTE LINK'), $paste_uri);
id(new PhabricatorMetaMTAMail())
->addTos(array($sender->getPHID()))
->setSubject($subject)
->setSubjectPrefix($subject_prefix)
->setFrom($sender->getPHID())
->setRelatedPHID($paste->getPHID())
->setBody($body->render())
->saveAndSend();
}
-
}
diff --git a/src/applications/paste/mail/PasteMailReceiver.php b/src/applications/paste/mail/PasteMailReceiver.php
index cb5a6e898..f0b2f4674 100644
--- a/src/applications/paste/mail/PasteMailReceiver.php
+++ b/src/applications/paste/mail/PasteMailReceiver.php
@@ -1,27 +1,27 @@
<?php
final class PasteMailReceiver extends PhabricatorObjectMailReceiver {
public function isEnabled() {
$app_class = 'PhabricatorPasteApplication';
return PhabricatorApplication::isClassInstalled($app_class);
}
protected function getObjectPattern() {
return 'P[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
- $id = (int)trim($pattern, 'P');
+ $id = (int)substr($pattern, 1);
return id(new PhabricatorPasteQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
}
protected function getTransactionReplyHandler() {
return new PasteReplyHandler();
}
}
diff --git a/src/applications/paste/storage/PhabricatorPaste.php b/src/applications/paste/storage/PhabricatorPaste.php
index 19aabcf8e..79f1a953f 100644
--- a/src/applications/paste/storage/PhabricatorPaste.php
+++ b/src/applications/paste/storage/PhabricatorPaste.php
@@ -1,286 +1,275 @@
<?php
final class PhabricatorPaste extends PhabricatorPasteDAO
implements
PhabricatorSubscribableInterface,
PhabricatorTokenReceiverInterface,
PhabricatorFlaggableInterface,
PhabricatorMentionableInterface,
PhabricatorPolicyInterface,
PhabricatorProjectInterface,
PhabricatorDestructibleInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorSpacesInterface,
PhabricatorConduitResultInterface {
protected $title;
protected $authorPHID;
protected $filePHID;
protected $language;
protected $parentPHID;
protected $viewPolicy;
protected $editPolicy;
protected $mailKey;
protected $status;
protected $spacePHID;
const STATUS_ACTIVE = 'active';
const STATUS_ARCHIVED = 'archived';
private $content = self::ATTACHABLE;
private $rawContent = self::ATTACHABLE;
private $snippet = self::ATTACHABLE;
public static function initializeNewPaste(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorPasteApplication'))
->executeOne();
$view_policy = $app->getPolicy(PasteDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(PasteDefaultEditCapability::CAPABILITY);
return id(new PhabricatorPaste())
->setTitle('')
->setStatus(self::STATUS_ACTIVE)
->setAuthorPHID($actor->getPHID())
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setSpacePHID($actor->getDefaultSpacePHID())
->attachRawContent(null);
}
public static function getStatusNameMap() {
return array(
self::STATUS_ACTIVE => pht('Active'),
self::STATUS_ARCHIVED => pht('Archived'),
);
}
public function getURI() {
return '/'.$this->getMonogram();
}
public function getMonogram() {
return 'P'.$this->getID();
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'status' => 'text32',
'title' => 'text255',
'language' => 'text64?',
'mailKey' => 'bytes20',
'parentPHID' => 'phid?',
// T6203/NULLABILITY
// Pastes should always have a view policy.
'viewPolicy' => 'policy?',
),
self::CONFIG_KEY_SCHEMA => array(
'parentPHID' => array(
'columns' => array('parentPHID'),
),
'authorPHID' => array(
'columns' => array('authorPHID'),
),
'key_dateCreated' => array(
'columns' => array('dateCreated'),
),
'key_language' => array(
'columns' => array('language'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPastePastePHIDType::TYPECONST);
}
public function isArchived() {
return ($this->getStatus() == self::STATUS_ARCHIVED);
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public function getFullName() {
$title = $this->getTitle();
if (!$title) {
$title = pht('(An Untitled Masterwork)');
}
return 'P'.$this->getID().' '.$title;
}
public function getContent() {
return $this->assertAttached($this->content);
}
public function attachContent($content) {
$this->content = $content;
return $this;
}
public function getRawContent() {
return $this->assertAttached($this->rawContent);
}
public function attachRawContent($raw_content) {
$this->rawContent = $raw_content;
return $this;
}
public function getSnippet() {
return $this->assertAttached($this->snippet);
}
public function attachSnippet(PhabricatorPasteSnippet $snippet) {
$this->snippet = $snippet;
return $this;
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return ($this->authorPHID == $phid);
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getAuthorPHID(),
);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
return $this->viewPolicy;
} else if ($capability == PhabricatorPolicyCapability::CAN_EDIT) {
return $this->editPolicy;
}
return PhabricatorPolicies::POLICY_NOONE;
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
return ($user->getPHID() == $this->getAuthorPHID());
}
public function describeAutomaticCapability($capability) {
return pht('The author of a paste can always view and edit it.');
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
if ($this->filePHID) {
$file = id(new PhabricatorFileQuery())
->setViewer($engine->getViewer())
->withPHIDs(array($this->filePHID))
->executeOne();
if ($file) {
$engine->destroyObject($file);
}
}
$this->delete();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorPasteEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorPasteTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorSpacesInterface )----------------------------------------- */
public function getSpacePHID() {
return $this->spacePHID;
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('title')
->setType('string')
->setDescription(pht('The title of the paste.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('authorPHID')
->setType('phid')
->setDescription(pht('User PHID of the author.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('language')
->setType('string?')
->setDescription(pht('Language to use for syntax highlighting.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('status')
->setType('string')
->setDescription(pht('Active or archived status of the paste.')),
);
}
public function getFieldValuesForConduit() {
return array(
'title' => $this->getTitle(),
'authorPHID' => $this->getAuthorPHID(),
'language' => nonempty($this->getLanguage(), null),
'status' => $this->getStatus(),
);
}
public function getConduitSearchAttachments() {
return array(
id(new PhabricatorPasteContentSearchEngineAttachment())
->setAttachmentKey('content'),
);
}
}
diff --git a/src/applications/paste/xaction/PhabricatorPasteContentTransaction.php b/src/applications/paste/xaction/PhabricatorPasteContentTransaction.php
index c90892345..1e4ad2d26 100644
--- a/src/applications/paste/xaction/PhabricatorPasteContentTransaction.php
+++ b/src/applications/paste/xaction/PhabricatorPasteContentTransaction.php
@@ -1,138 +1,116 @@
<?php
final class PhabricatorPasteContentTransaction
extends PhabricatorPasteTransactionType {
const TRANSACTIONTYPE = 'paste.create';
- private $fileName;
-
public function generateOldValue($object) {
return $object->getFilePHID();
}
public function applyInternalEffects($object, $value) {
$object->setFilePHID($value);
}
public function extractFilePHIDs($object, $value) {
return array($value);
}
public function validateTransactions($object, array $xactions) {
if ($object->getFilePHID() || $xactions) {
return array();
}
$error = $this->newError(
pht('Required'),
pht('You must provide content to create a paste.'));
$error->setIsMissingFieldError(true);
return array($error);
}
- public function willApplyTransactions($object, array $xactions) {
- // Find the most user-friendly filename we can by examining the title of
- // the paste and the pending transactions. We'll use this if we create a
- // new file to store raw content later.
-
- $name = $object->getTitle();
- if (!strlen($name)) {
- $name = 'paste.raw';
- }
-
- $type_title = PhabricatorPasteTitleTransaction::TRANSACTIONTYPE;
- foreach ($xactions as $xaction) {
- if ($xaction->getTransactionType() == $type_title) {
- $name = $xaction->getNewValue();
- }
- }
-
- $this->fileName = $name;
- }
-
public function generateNewValue($object, $value) {
// If this transaction does not really change the paste content, return
// the current file PHID so this transaction no-ops.
$old_content = $object->getRawContent();
if ($value === $old_content) {
$file_phid = $object->getFilePHID();
if ($file_phid) {
return $file_phid;
}
}
$editor = $this->getEditor();
$actor = $editor->getActor();
- $file = $this->newFileForPaste($actor, $this->fileName, $value);
+ $file = $this->newFileForPaste($actor, $value);
return $file->getPHID();
}
- private function newFileForPaste(PhabricatorUser $actor, $name, $data) {
+ private function newFileForPaste(PhabricatorUser $actor, $data) {
return PhabricatorFile::newFromFileData(
$data,
array(
- 'name' => $name,
+ 'name' => 'raw.txt',
'mime-type' => 'text/plain; charset=utf-8',
'authorPHID' => $actor->getPHID(),
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
));
}
public function getIcon() {
return 'fa-plus';
}
public function getTitle() {
return pht(
'%s edited the content of this paste.',
$this->renderAuthor());
}
public function getTitleForFeed() {
return pht(
'%s edited %s.',
$this->renderAuthor(),
$this->renderObject());
}
public function hasChangeDetailView() {
return true;
}
public function getMailDiffSectionHeader() {
return pht('CHANGES TO PASTE CONTENT');
}
public function newChangeDetailView() {
$viewer = $this->getViewer();
$old = $this->getOldValue();
$new = $this->getNewValue();
$files = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($old, $new))
->execute();
$files = mpull($files, null, 'getPHID');
$old_text = '';
if (idx($files, $old)) {
$old_text = $files[$old]->loadFileData();
}
$new_text = '';
if (idx($files, $new)) {
$new_text = $files[$new]->loadFileData();
}
return id(new PhabricatorApplicationTransactionTextDiffDetailView())
->setViewer($viewer)
->setOldText($old_text)
->setNewText($new_text);
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleEmpowerController.php b/src/applications/people/controller/PhabricatorPeopleEmpowerController.php
index a49f8b3d1..09021bf73 100644
--- a/src/applications/people/controller/PhabricatorPeopleEmpowerController.php
+++ b/src/applications/people/controller/PhabricatorPeopleEmpowerController.php
@@ -1,70 +1,75 @@
<?php
final class PhabricatorPeopleEmpowerController
extends PhabricatorPeopleController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$user) {
return new Aphront404Response();
}
$done_uri = $this->getApplicationURI("manage/{$id}/");
id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
$done_uri);
- if ($user->getPHID() == $viewer->getPHID()) {
- return $this->newDialog()
- ->setTitle(pht('Your Way is Blocked'))
- ->appendParagraph(
- pht(
- 'After a time, your efforts fail. You can not adjust your own '.
- 'status as an administrator.'))
- ->addCancelButton($done_uri, pht('Accept Fate'));
- }
+ $validation_exception = null;
if ($request->isFormPost()) {
- id(new PhabricatorUserEditor())
+ $xactions = array();
+ $xactions[] = id(new PhabricatorUserTransaction())
+ ->setTransactionType(
+ PhabricatorUserEmpowerTransaction::TRANSACTIONTYPE)
+ ->setNewValue(!$user->getIsAdmin());
+
+ $editor = id(new PhabricatorUserTransactionEditor())
->setActor($viewer)
- ->makeAdminUser($user, !$user->getIsAdmin());
+ ->setContentSourceFromRequest($request)
+ ->setContinueOnMissingFields(true);
- return id(new AphrontRedirectResponse())->setURI($done_uri);
+ try {
+ $editor->applyTransactions($user, $xactions);
+ return id(new AphrontRedirectResponse())->setURI($done_uri);
+ } catch (PhabricatorApplicationTransactionValidationException $ex) {
+ $validation_exception = $ex;
+ }
}
if ($user->getIsAdmin()) {
$title = pht('Remove as Administrator?');
$short = pht('Remove Administrator');
$body = pht(
'Remove %s as an administrator? They will no longer be able to '.
'perform administrative functions on this Phabricator install.',
phutil_tag('strong', array(), $user->getUsername()));
$submit = pht('Remove Administrator');
} else {
$title = pht('Make Administrator?');
$short = pht('Make Administrator');
$body = pht(
'Empower %s as an administrator? They will be able to create users, '.
'approve users, make and remove administrators, delete accounts, and '.
'perform other administrative functions on this Phabricator install.',
phutil_tag('strong', array(), $user->getUsername()));
$submit = pht('Make Administrator');
}
return $this->newDialog()
+ ->setValidationException($validation_exception)
->setTitle($title)
->setShortTitle($short)
->appendParagraph($body)
->addCancelButton($done_uri)
->addSubmitButton($submit);
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleNewController.php b/src/applications/people/controller/PhabricatorPeopleNewController.php
index 3521fb840..44dfe0e8a 100644
--- a/src/applications/people/controller/PhabricatorPeopleNewController.php
+++ b/src/applications/people/controller/PhabricatorPeopleNewController.php
@@ -1,235 +1,240 @@
<?php
final class PhabricatorPeopleNewController
extends PhabricatorPeopleController {
public function handleRequest(AphrontRequest $request) {
$type = $request->getURIData('type');
$admin = $request->getUser();
id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$admin,
$request,
$this->getApplicationURI());
$is_bot = false;
$is_list = false;
switch ($type) {
case 'standard':
$this->requireApplicationCapability(
PeopleCreateUsersCapability::CAPABILITY);
break;
case 'bot':
$is_bot = true;
break;
case 'list':
$is_list = true;
break;
default:
return new Aphront404Response();
}
$user = new PhabricatorUser();
$require_real_name = PhabricatorEnv::getEnvConfig('user.require-real-name');
$e_username = true;
$e_realname = $require_real_name ? true : null;
$e_email = true;
$errors = array();
$welcome_checked = true;
$new_email = null;
if ($request->isFormPost()) {
$welcome_checked = $request->getInt('welcome');
$user->setUsername($request->getStr('username'));
$new_email = $request->getStr('email');
if (!strlen($new_email)) {
$errors[] = pht('Email is required.');
$e_email = pht('Required');
} else if (!PhabricatorUserEmail::isAllowedAddress($new_email)) {
$e_email = pht('Invalid');
$errors[] = PhabricatorUserEmail::describeAllowedAddresses();
} else {
$e_email = null;
}
$user->setRealName($request->getStr('realname'));
if (!strlen($user->getUsername())) {
$errors[] = pht('Username is required.');
$e_username = pht('Required');
} else if (!PhabricatorUser::validateUsername($user->getUsername())) {
$errors[] = PhabricatorUser::describeValidUsername();
$e_username = pht('Invalid');
} else {
$e_username = null;
}
if (!strlen($user->getRealName()) && $require_real_name) {
$errors[] = pht('Real name is required.');
$e_realname = pht('Required');
} else {
$e_realname = null;
}
if (!$errors) {
try {
$email = id(new PhabricatorUserEmail())
->setAddress($new_email)
->setIsVerified(0);
// Automatically approve the user, since an admin is creating them.
$user->setIsApproved(1);
// If the user is a bot or list, approve their email too.
if ($is_bot || $is_list) {
$email->setIsVerified(1);
}
id(new PhabricatorUserEditor())
->setActor($admin)
->createNewUser($user, $email);
if ($is_bot) {
id(new PhabricatorUserEditor())
->setActor($admin)
->makeSystemAgentUser($user, true);
}
if ($is_list) {
id(new PhabricatorUserEditor())
->setActor($admin)
->makeMailingListUser($user, true);
}
- if ($welcome_checked && !$is_bot && !$is_list) {
- $user->sendWelcomeEmail($admin);
+ if ($welcome_checked) {
+ $welcome_engine = id(new PhabricatorPeopleWelcomeMailEngine())
+ ->setSender($admin)
+ ->setRecipient($user);
+ if ($welcome_engine->canSendMail()) {
+ $welcome_engine->sendMail();
+ }
}
$response = id(new AphrontRedirectResponse())
->setURI('/p/'.$user->getUsername().'/');
return $response;
} catch (AphrontDuplicateKeyQueryException $ex) {
$errors[] = pht('Username and email must be unique.');
$same_username = id(new PhabricatorUser())
->loadOneWhere('username = %s', $user->getUsername());
$same_email = id(new PhabricatorUserEmail())
->loadOneWhere('address = %s', $new_email);
if ($same_username) {
$e_username = pht('Duplicate');
}
if ($same_email) {
$e_email = pht('Duplicate');
}
}
}
}
$form = id(new AphrontFormView())
->setUser($admin);
if ($is_bot) {
$title = pht('Create New Bot');
$form->appendRemarkupInstructions(
pht('You are creating a new **bot** user account.'));
} else if ($is_list) {
$title = pht('Create New Mailing List');
$form->appendRemarkupInstructions(
pht('You are creating a new **mailing list** user account.'));
} else {
$title = pht('Create New User');
$form->appendRemarkupInstructions(
pht('You are creating a new **standard** user account.'));
}
$form
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Username'))
->setName('username')
->setValue($user->getUsername())
->setError($e_username))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Real Name'))
->setName('realname')
->setValue($user->getRealName())
->setError($e_realname))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Email'))
->setName('email')
->setValue($new_email)
->setCaption(PhabricatorUserEmail::describeAllowedAddresses())
->setError($e_email));
if (!$is_bot && !$is_list) {
$form->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox(
'welcome',
1,
pht('Send "Welcome to Phabricator" email with login instructions.'),
$welcome_checked));
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($this->getApplicationURI())
->setValue(pht('Create User')));
if ($is_bot) {
$form
->appendChild(id(new AphrontFormDividerControl()))
->appendRemarkupInstructions(
pht(
'**Why do bot accounts need an email address?**'.
"\n\n".
'Although bots do not normally receive email from Phabricator, '.
'they can interact with other systems which require an email '.
'address. Examples include:'.
"\n\n".
" - If the account takes actions which //send// email, we need ".
" an address to use in the //From// header.\n".
" - If the account creates commits, Git and Mercurial require ".
" an email address for authorship.\n".
" - If you send email //to// Phabricator on behalf of the ".
" account, the address can identify the sender.\n".
" - Some internal authentication functions depend on accounts ".
" having an email address.\n".
"\n\n".
"The address will automatically be verified, so you do not need ".
"to be able to receive mail at this address, and can enter some ".
"invalid or nonexistent (but correctly formatted) address like ".
"`bot@yourcompany.com` if you prefer."));
}
$box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setFormErrors($errors)
->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
->setForm($form);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($title);
$crumbs->setBorder(true);
$view = id(new PHUITwoColumnView())
->setFooter($box);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleProfileManageController.php b/src/applications/people/controller/PhabricatorPeopleProfileManageController.php
index 69a643626..a0ab100ee 100644
--- a/src/applications/people/controller/PhabricatorPeopleProfileManageController.php
+++ b/src/applications/people/controller/PhabricatorPeopleProfileManageController.php
@@ -1,202 +1,268 @@
<?php
final class PhabricatorPeopleProfileManageController
extends PhabricatorPeopleProfileController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withIDs(array($id))
->needProfile(true)
->needProfileImage(true)
->needAvailability(true)
->executeOne();
if (!$user) {
return new Aphront404Response();
}
$this->setUser($user);
$header = $this->buildProfileHeader();
$curtain = $this->buildCurtain($user);
$properties = $this->buildPropertyView($user);
$name = $user->getUsername();
$nav = $this->getProfileMenu();
$nav->selectFilter(PhabricatorPeopleProfileMenuEngine::ITEM_MANAGE);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Manage'));
$crumbs->setBorder(true);
$timeline = $this->buildTransactionTimeline(
$user,
new PhabricatorPeopleTransactionQuery());
$timeline->setShouldTerminate(true);
$manage = id(new PHUITwoColumnView())
->setHeader($header)
->addClass('project-view-home')
->addClass('project-view-people-home')
->setCurtain($curtain)
->addPropertySection(pht('Details'), $properties)
->setMainColumn($timeline);
return $this->newPage()
->setTitle(
array(
pht('Manage User'),
$user->getUsername(),
))
->setNavigation($nav)
->setCrumbs($crumbs)
->appendChild($manage);
}
private function buildPropertyView(PhabricatorUser $user) {
$viewer = $this->getRequest()->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($user);
$field_list = PhabricatorCustomField::getObjectFields(
$user,
PhabricatorCustomField::ROLE_VIEW);
$field_list->appendFieldsToPropertyList($user, $viewer, $view);
return $view;
}
private function buildCurtain(PhabricatorUser $user) {
$viewer = $this->getViewer();
$is_self = ($user->getPHID() === $viewer->getPHID());
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$user,
PhabricatorPolicyCapability::CAN_EDIT);
$is_admin = $viewer->getIsAdmin();
$can_admin = ($is_admin && !$is_self);
$has_disable = $this->hasApplicationCapability(
PeopleDisableUsersCapability::CAPABILITY);
$can_disable = ($has_disable && !$is_self);
+<<<<<<< HEAD
$can_welcome = ($is_admin && $user->canEstablishWebSessions());
+=======
+ $welcome_engine = id(new PhabricatorPeopleWelcomeMailEngine())
+ ->setSender($viewer)
+ ->setRecipient($user);
+
+ $can_welcome = $welcome_engine->canSendMail();
+>>>>>>> upstream/stable
$curtain = $this->newCurtainView($user);
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit Profile'))
->setHref($this->getApplicationURI('editprofile/'.$user->getID().'/'))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-picture-o')
->setName(pht('Edit Profile Picture'))
->setHref($this->getApplicationURI('picture/'.$user->getID().'/'))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-wrench')
->setName(pht('Edit Settings'))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit)
->setHref('/settings/user/'.$user->getUsername().'/'));
if ($user->getIsAdmin()) {
$empower_icon = 'fa-arrow-circle-o-down';
$empower_name = pht('Remove Administrator');
} else {
$empower_icon = 'fa-arrow-circle-o-up';
$empower_name = pht('Make Administrator');
}
+<<<<<<< HEAD
$is_admin = $viewer->getIsAdmin();
$is_self = ($user->getPHID() === $viewer->getPHID());
$can_admin = ($is_admin && !$is_self);
// c4science custo
// Only show admin actions to admin user
if ($is_admin) {
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon($empower_icon)
->setName($empower_name)
->setDisabled(!$can_admin)
->setWorkflow(true)
->setHref($this->getApplicationURI('empower/'.$user->getID().'/')));
}
// c4science custo
if ($can_admin) {
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-tag')
->setName(pht('Change Username'))
->setDisabled(!$is_admin)
->setWorkflow(true)
->setHref($this->getApplicationURI('rename/'.$user->getID().'/')));
}
// c4science custo
if ($is_admin) {
if ($user->getIsDisabled()) {
$disable_icon = 'fa-check-circle-o';
$disable_name = pht('Enable User');
} else {
$disable_icon = 'fa-ban';
$disable_name = pht('Disable User');
}
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon($disable_icon)
->setName($disable_name)
->setDisabled(!$can_admin)
->setWorkflow(true)
->setHref($this->getApplicationURI('disable/'.$user->getID().'/')));
}
// c4science custo
if ($is_admin) {
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-times')
->setName(pht('Delete User'))
->setDisabled(!$can_admin)
->setWorkflow(true)
->setHref($this->getApplicationURI('delete/'.$user->getID().'/')));
$can_welcome = ($is_admin && $user->canEstablishWebSessions());
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-envelope')
->setName(pht('Send Welcome Email'))
->setWorkflow(true)
->setDisabled(!$can_welcome)
->setHref($this->getApplicationURI('welcome/'.$user->getID().'/')));
}
+=======
+ $curtain->addAction(
+ id(new PhabricatorActionView())
+ ->setIcon($empower_icon)
+ ->setName($empower_name)
+ ->setDisabled(!$can_admin)
+ ->setWorkflow(true)
+ ->setHref($this->getApplicationURI('empower/'.$user->getID().'/')));
+
+ $curtain->addAction(
+ id(new PhabricatorActionView())
+ ->setIcon('fa-tag')
+ ->setName(pht('Change Username'))
+ ->setDisabled(!$is_admin)
+ ->setWorkflow(true)
+ ->setHref($this->getApplicationURI('rename/'.$user->getID().'/')));
+
+ if ($user->getIsDisabled()) {
+ $disable_icon = 'fa-check-circle-o';
+ $disable_name = pht('Enable User');
+ } else {
+ $disable_icon = 'fa-ban';
+ $disable_name = pht('Disable User');
+ }
+
+ $curtain->addAction(
+ id(new PhabricatorActionView())
+ ->setIcon('fa-envelope')
+ ->setName(pht('Send Welcome Email'))
+ ->setWorkflow(true)
+ ->setDisabled(!$can_welcome)
+ ->setHref($this->getApplicationURI('welcome/'.$user->getID().'/')));
+
+ $curtain->addAction(
+ id(new PhabricatorActionView())
+ ->setType(PhabricatorActionView::TYPE_DIVIDER));
+
+ $curtain->addAction(
+ id(new PhabricatorActionView())
+ ->setIcon($disable_icon)
+ ->setName($disable_name)
+ ->setDisabled(!$can_disable)
+ ->setWorkflow(true)
+ ->setHref($this->getApplicationURI('disable/'.$user->getID().'/')));
+
+ $curtain->addAction(
+ id(new PhabricatorActionView())
+ ->setIcon('fa-times')
+ ->setName(pht('Delete User'))
+ ->setDisabled(!$can_admin)
+ ->setWorkflow(true)
+ ->setHref($this->getApplicationURI('delete/'.$user->getID().'/')));
+
+ $curtain->addAction(
+ id(new PhabricatorActionView())
+ ->setType(PhabricatorActionView::TYPE_DIVIDER));
+>>>>>>> upstream/stable
return $curtain;
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleRenameController.php b/src/applications/people/controller/PhabricatorPeopleRenameController.php
index 42ff2e798..42eebfc8a 100644
--- a/src/applications/people/controller/PhabricatorPeopleRenameController.php
+++ b/src/applications/people/controller/PhabricatorPeopleRenameController.php
@@ -1,101 +1,97 @@
<?php
final class PhabricatorPeopleRenameController
extends PhabricatorPeopleController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$user) {
return new Aphront404Response();
}
$done_uri = $this->getApplicationURI("manage/{$id}/");
- id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
- $viewer,
- $request,
- $done_uri);
-
$validation_exception = null;
$username = $user->getUsername();
- if ($request->isFormPost()) {
+ if ($request->isFormOrHisecPost()) {
$username = $request->getStr('username');
$xactions = array();
$xactions[] = id(new PhabricatorUserTransaction())
->setTransactionType(
PhabricatorUserUsernameTransaction::TRANSACTIONTYPE)
->setNewValue($username);
$editor = id(new PhabricatorUserTransactionEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
+ ->setCancelURI($done_uri)
->setContinueOnMissingFields(true);
try {
$editor->applyTransactions($user, $xactions);
return id(new AphrontRedirectResponse())->setURI($done_uri);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
}
}
$inst1 = pht(
'Be careful when renaming users!');
$inst2 = pht(
'The old username will no longer be tied to the user, so anything '.
'which uses it (like old commit messages) will no longer associate '.
'correctly. (And, if you give a user a username which some other user '.
'used to have, username lookups will begin returning the wrong user.)');
$inst3 = pht(
'It is generally safe to rename newly created users (and test users '.
'and so on), but less safe to rename established users and unsafe to '.
'reissue a username.');
$inst4 = pht(
'Users who rely on password authentication will need to reset their '.
'password after their username is changed (their username is part of '.
'the salt in the password hash).');
$inst5 = pht(
'The user will receive an email notifying them that you changed their '.
'username, with instructions for logging in and resetting their '.
'password if necessary.');
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('Old Username'))
->setValue($user->getUsername()))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('New Username'))
->setValue($username)
->setName('username'));
return $this->newDialog()
->setWidth(AphrontDialogView::WIDTH_FORM)
->setTitle(pht('Change Username'))
->setValidationException($validation_exception)
->appendParagraph($inst1)
->appendParagraph($inst2)
->appendParagraph($inst3)
->appendParagraph($inst4)
->appendParagraph($inst5)
->appendParagraph(null)
->appendForm($form)
->addSubmitButton(pht('Rename User'))
->addCancelButton($done_uri);
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleWelcomeController.php b/src/applications/people/controller/PhabricatorPeopleWelcomeController.php
index 14b1544b7..94d5e0bb0 100644
--- a/src/applications/people/controller/PhabricatorPeopleWelcomeController.php
+++ b/src/applications/people/controller/PhabricatorPeopleWelcomeController.php
@@ -1,53 +1,95 @@
<?php
final class PhabricatorPeopleWelcomeController
extends PhabricatorPeopleController {
+ public function shouldRequireAdmin() {
+ // You need to be an administrator to actually send welcome email, but
+ // we let anyone hit this page so they can get a nice error dialog
+ // explaining the issue.
+ return false;
+ }
+
public function handleRequest(AphrontRequest $request) {
$admin = $this->getViewer();
$user = id(new PhabricatorPeopleQuery())
->setViewer($admin)
->withIDs(array($request->getURIData('id')))
->executeOne();
if (!$user) {
return new Aphront404Response();
}
- $profile_uri = '/p/'.$user->getUsername().'/';
+ $id = $user->getID();
+ $profile_uri = "/people/manage/{$id}/";
- if (!$user->canEstablishWebSessions()) {
+ $welcome_engine = id(new PhabricatorPeopleWelcomeMailEngine())
+ ->setSender($admin)
+ ->setRecipient($user);
+
+ try {
+ $welcome_engine->validateMail();
+ } catch (PhabricatorPeopleMailEngineException $ex) {
return $this->newDialog()
- ->setTitle(pht('Not a Normal User'))
- ->appendParagraph(
- pht(
- 'You can not send this user a welcome mail because they are not '.
- 'a normal user and can not log in to the web interface. Special '.
- 'users (like bots and mailing lists) are unable to establish web '.
- 'sessions.'))
+ ->setTitle($ex->getTitle())
+ ->appendParagraph($ex->getBody())
->addCancelButton($profile_uri, pht('Done'));
}
+ $v_message = $request->getStr('message');
+
if ($request->isFormPost()) {
- $user->sendWelcomeEmail($admin);
+ if (strlen($v_message)) {
+ $welcome_engine->setWelcomeMessage($v_message);
+ }
+
+ $welcome_engine->sendMail();
return id(new AphrontRedirectResponse())->setURI($profile_uri);
}
- return $this->newDialog()
- ->setTitle(pht('Send Welcome Email'))
- ->appendParagraph(
+ $default_message = PhabricatorAuthMessage::loadMessage(
+ $admin,
+ PhabricatorAuthWelcomeMailMessageType::MESSAGEKEY);
+ if ($default_message && strlen($default_message->getMessageText())) {
+ $message_instructions = pht(
+ 'The email will identify you as the sender. You may optionally '.
+ 'replace the [[ %s | default custom mail body ]] with different text '.
+ 'by providing a message below.',
+ $default_message->getURI());
+ } else {
+ $message_instructions = pht(
+ 'The email will identify you as the sender. You may optionally '.
+ 'include additional text in the mail body by specifying it below.');
+ }
+
+ $form = id(new AphrontFormView())
+ ->setViewer($admin)
+ ->appendRemarkupInstructions(
pht(
- 'This will send the user another copy of the "Welcome to '.
+ 'This workflow will send this user ("%s") a copy of the "Welcome to '.
'Phabricator" email that users normally receive when their '.
- 'accounts are created.'))
- ->appendParagraph(
+ 'accounts are created by an administrator.',
+ $user->getUsername()))
+ ->appendRemarkupInstructions(
pht(
- 'The email contains a link to log in to their account. Sending '.
- 'another copy of the email can be useful if the original was lost '.
- 'or never sent.'))
- ->appendParagraph(pht('The email will identify you as the sender.'))
+ 'The email will contain a link that the user may use to log in '.
+ 'to their account. This link bypasses authentication requirements '.
+ 'and allows them to log in without credentials. Sending a copy of '.
+ 'this email can be useful if the original was lost or never sent.'))
+ ->appendRemarkupInstructions($message_instructions)
+ ->appendControl(
+ id(new PhabricatorRemarkupControl())
+ ->setName('message')
+ ->setLabel(pht('Custom Message'))
+ ->setValue($v_message));
+
+ return $this->newDialog()
+ ->setTitle(pht('Send Welcome Email'))
+ ->setWidth(AphrontDialogView::WIDTH_FORM)
+ ->appendForm($form)
->addSubmitButton(pht('Send Email'))
->addCancelButton($profile_uri);
}
}
diff --git a/src/applications/people/editor/PhabricatorUserEditor.php b/src/applications/people/editor/PhabricatorUserEditor.php
index 8092824a0..c8068858d 100644
--- a/src/applications/people/editor/PhabricatorUserEditor.php
+++ b/src/applications/people/editor/PhabricatorUserEditor.php
@@ -1,600 +1,561 @@
<?php
/**
* Editor class for creating and adjusting users. This class guarantees data
* integrity and writes logs when user information changes.
*
* @task config Configuration
* @task edit Creating and Editing Users
* @task role Editing Roles
* @task email Adding, Removing and Changing Email
* @task internal Internals
*/
final class PhabricatorUserEditor extends PhabricatorEditor {
private $logs = array();
/* -( Creating and Editing Users )----------------------------------------- */
/**
* @task edit
*/
public function createNewUser(
PhabricatorUser $user,
PhabricatorUserEmail $email,
$allow_reassign = false) {
if ($user->getID()) {
throw new Exception(pht('User has already been created!'));
}
$is_reassign = false;
if ($email->getID()) {
if ($allow_reassign) {
if ($email->getIsPrimary()) {
throw new Exception(
pht('Primary email addresses can not be reassigned.'));
}
$is_reassign = true;
} else {
throw new Exception(pht('Email has already been created!'));
}
}
if (!PhabricatorUser::validateUsername($user->getUsername())) {
$valid = PhabricatorUser::describeValidUsername();
throw new Exception(pht('Username is invalid! %s', $valid));
}
// Always set a new user's email address to primary.
$email->setIsPrimary(1);
// If the primary address is already verified, also set the verified flag
// on the user themselves.
if ($email->getIsVerified()) {
$user->setIsEmailVerified(1);
}
$this->willAddEmail($email);
$user->openTransaction();
try {
$user->save();
$email->setUserPHID($user->getPHID());
$email->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
// We might have written the user but failed to write the email; if
// so, erase the IDs we attached.
$user->setID(null);
$user->setPHID(null);
$user->killTransaction();
throw $ex;
}
$log = PhabricatorUserLog::initializeNewLog(
$this->requireActor(),
$user->getPHID(),
PhabricatorUserLog::ACTION_CREATE);
$log->setNewValue($email->getAddress());
$log->save();
if ($is_reassign) {
$log = PhabricatorUserLog::initializeNewLog(
$this->requireActor(),
$user->getPHID(),
PhabricatorUserLog::ACTION_EMAIL_REASSIGN);
$log->setNewValue($email->getAddress());
$log->save();
}
$user->saveTransaction();
if ($email->getIsVerified()) {
$this->didVerifyEmail($user, $email);
}
return $this;
}
/**
* @task edit
*/
public function updateUser(
PhabricatorUser $user,
PhabricatorUserEmail $email = null) {
if (!$user->getID()) {
throw new Exception(pht('User has not been created yet!'));
}
$user->openTransaction();
$user->save();
if ($email) {
$email->save();
}
$log = PhabricatorUserLog::initializeNewLog(
$this->requireActor(),
$user->getPHID(),
PhabricatorUserLog::ACTION_EDIT);
$log->save();
$user->saveTransaction();
return $this;
}
/* -( Editing Roles )------------------------------------------------------ */
-
- /**
- * @task role
- */
- public function makeAdminUser(PhabricatorUser $user, $admin) {
- $actor = $this->requireActor();
-
- if (!$user->getID()) {
- throw new Exception(pht('User has not been created yet!'));
- }
-
- $user->openTransaction();
- $user->beginWriteLocking();
-
- $user->reload();
- if ($user->getIsAdmin() == $admin) {
- $user->endWriteLocking();
- $user->killTransaction();
- return $this;
- }
-
- $log = PhabricatorUserLog::initializeNewLog(
- $actor,
- $user->getPHID(),
- PhabricatorUserLog::ACTION_ADMIN);
- $log->setOldValue($user->getIsAdmin());
- $log->setNewValue($admin);
-
- $user->setIsAdmin((int)$admin);
- $user->save();
-
- $log->save();
-
- $user->endWriteLocking();
- $user->saveTransaction();
-
- return $this;
- }
-
/**
* @task role
*/
public function makeSystemAgentUser(PhabricatorUser $user, $system_agent) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception(pht('User has not been created yet!'));
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
if ($user->getIsSystemAgent() == $system_agent) {
$user->endWriteLocking();
$user->killTransaction();
return $this;
}
$log = PhabricatorUserLog::initializeNewLog(
$actor,
$user->getPHID(),
PhabricatorUserLog::ACTION_SYSTEM_AGENT);
$log->setOldValue($user->getIsSystemAgent());
$log->setNewValue($system_agent);
$user->setIsSystemAgent((int)$system_agent);
$user->save();
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
return $this;
}
/**
* @task role
*/
public function makeMailingListUser(PhabricatorUser $user, $mailing_list) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception(pht('User has not been created yet!'));
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
if ($user->getIsMailingList() == $mailing_list) {
$user->endWriteLocking();
$user->killTransaction();
return $this;
}
$log = PhabricatorUserLog::initializeNewLog(
$actor,
$user->getPHID(),
PhabricatorUserLog::ACTION_MAILING_LIST);
$log->setOldValue($user->getIsMailingList());
$log->setNewValue($mailing_list);
$user->setIsMailingList((int)$mailing_list);
$user->save();
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
return $this;
}
/* -( Adding, Removing and Changing Email )-------------------------------- */
/**
* @task email
*/
public function addEmail(
PhabricatorUser $user,
PhabricatorUserEmail $email) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception(pht('User has not been created yet!'));
}
if ($email->getID()) {
throw new Exception(pht('Email has already been created!'));
}
// Use changePrimaryEmail() to change primary email.
$email->setIsPrimary(0);
$email->setUserPHID($user->getPHID());
$this->willAddEmail($email);
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
try {
$email->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
$user->endWriteLocking();
$user->killTransaction();
throw $ex;
}
$log = PhabricatorUserLog::initializeNewLog(
$actor,
$user->getPHID(),
PhabricatorUserLog::ACTION_EMAIL_ADD);
$log->setNewValue($email->getAddress());
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
// Try and match this new address against unclaimed `RepositoryIdentity`s
PhabricatorWorker::scheduleTask(
'PhabricatorRepositoryIdentityChangeWorker',
array('userPHID' => $user->getPHID()),
array('objectPHID' => $user->getPHID()));
return $this;
}
/**
* @task email
*/
public function removeEmail(
PhabricatorUser $user,
PhabricatorUserEmail $email) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception(pht('User has not been created yet!'));
}
if (!$email->getID()) {
throw new Exception(pht('Email has not been created yet!'));
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
$email->reload();
if ($email->getIsPrimary()) {
throw new Exception(pht("Can't remove primary email!"));
}
if ($email->getUserPHID() != $user->getPHID()) {
throw new Exception(pht('Email not owned by user!'));
}
$email->delete();
$log = PhabricatorUserLog::initializeNewLog(
$actor,
$user->getPHID(),
PhabricatorUserLog::ACTION_EMAIL_REMOVE);
$log->setOldValue($email->getAddress());
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
$this->revokePasswordResetLinks($user);
return $this;
}
/**
* @task email
*/
public function changePrimaryEmail(
PhabricatorUser $user,
PhabricatorUserEmail $email) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception(pht('User has not been created yet!'));
}
if (!$email->getID()) {
throw new Exception(pht('Email has not been created yet!'));
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
$email->reload();
if ($email->getUserPHID() != $user->getPHID()) {
throw new Exception(pht('User does not own email!'));
}
if ($email->getIsPrimary()) {
throw new Exception(pht('Email is already primary!'));
}
if (!$email->getIsVerified()) {
throw new Exception(pht('Email is not verified!'));
}
$old_primary = $user->loadPrimaryEmail();
if ($old_primary) {
$old_primary->setIsPrimary(0);
$old_primary->save();
}
$email->setIsPrimary(1);
$email->save();
// If the user doesn't have the verified flag set on their account
// yet, set it. We've made sure the email is verified above. See
// T12635 for discussion.
if (!$user->getIsEmailVerified()) {
$user->setIsEmailVerified(1);
$user->save();
}
$log = PhabricatorUserLog::initializeNewLog(
$actor,
$user->getPHID(),
PhabricatorUserLog::ACTION_EMAIL_PRIMARY);
$log->setOldValue($old_primary ? $old_primary->getAddress() : null);
$log->setNewValue($email->getAddress());
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
if ($old_primary) {
$old_primary->sendOldPrimaryEmail($user, $email);
}
$email->sendNewPrimaryEmail($user);
$this->revokePasswordResetLinks($user);
return $this;
}
/**
* Verify a user's email address.
*
* This verifies an individual email address. If the address is the user's
* primary address and their account was not previously verified, their
* account is marked as email verified.
*
* @task email
*/
public function verifyEmail(
PhabricatorUser $user,
PhabricatorUserEmail $email) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception(pht('User has not been created yet!'));
}
if (!$email->getID()) {
throw new Exception(pht('Email has not been created yet!'));
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
$email->reload();
if ($email->getUserPHID() != $user->getPHID()) {
throw new Exception(pht('User does not own email!'));
}
if (!$email->getIsVerified()) {
$email->setIsVerified(1);
$email->save();
$log = PhabricatorUserLog::initializeNewLog(
$actor,
$user->getPHID(),
PhabricatorUserLog::ACTION_EMAIL_VERIFY);
$log->setNewValue($email->getAddress());
$log->save();
}
if (!$user->getIsEmailVerified()) {
// If the user just verified their primary email address, mark their
// account as email verified.
$user_primary = $user->loadPrimaryEmail();
if ($user_primary->getID() == $email->getID()) {
$user->setIsEmailVerified(1);
$user->save();
}
}
$user->endWriteLocking();
$user->saveTransaction();
$this->didVerifyEmail($user, $email);
}
/**
* Reassign an unverified email address.
*/
public function reassignEmail(
PhabricatorUser $user,
PhabricatorUserEmail $email) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception(pht('User has not been created yet!'));
}
if (!$email->getID()) {
throw new Exception(pht('Email has not been created yet!'));
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
$email->reload();
$old_user = $email->getUserPHID();
if ($old_user != $user->getPHID()) {
if ($email->getIsVerified()) {
throw new Exception(
pht('Verified email addresses can not be reassigned.'));
}
if ($email->getIsPrimary()) {
throw new Exception(
pht('Primary email addresses can not be reassigned.'));
}
$email->setUserPHID($user->getPHID());
$email->save();
$log = PhabricatorUserLog::initializeNewLog(
$actor,
$user->getPHID(),
PhabricatorUserLog::ACTION_EMAIL_REASSIGN);
$log->setNewValue($email->getAddress());
$log->save();
}
$user->endWriteLocking();
$user->saveTransaction();
}
/* -( Internals )---------------------------------------------------------- */
/**
* @task internal
*/
private function willAddEmail(PhabricatorUserEmail $email) {
// Hard check before write to prevent creation of disallowed email
// addresses. Normally, the application does checks and raises more
// user friendly errors for us, but we omit the courtesy checks on some
// pathways like administrative scripts for simplicity.
if (!PhabricatorUserEmail::isValidAddress($email->getAddress())) {
throw new Exception(PhabricatorUserEmail::describeValidAddresses());
}
if (!PhabricatorUserEmail::isAllowedAddress($email->getAddress())) {
throw new Exception(PhabricatorUserEmail::describeAllowedAddresses());
}
$application_email = id(new PhabricatorMetaMTAApplicationEmailQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withAddresses(array($email->getAddress()))
->executeOne();
if ($application_email) {
throw new Exception($application_email->getInUseMessage());
}
}
public function revokePasswordResetLinks(PhabricatorUser $user) {
// Revoke any outstanding password reset links. If an attacker compromises
// an account, changes the email address, and sends themselves a password
// reset link, it could otherwise remain live for a short period of time
// and allow them to compromise the account again later.
PhabricatorAuthTemporaryToken::revokeTokens(
$user,
array($user->getPHID()),
array(
PhabricatorAuthOneTimeLoginTemporaryTokenType::TOKENTYPE,
PhabricatorAuthPasswordResetTemporaryTokenType::TOKENTYPE,
));
}
private function didVerifyEmail(
PhabricatorUser $user,
PhabricatorUserEmail $email) {
$event_type = PhabricatorEventType::TYPE_AUTH_DIDVERIFYEMAIL;
$event_data = array(
'user' => $user,
'email' => $email,
);
$event = id(new PhabricatorEvent($event_type, $event_data))
->setUser($user);
PhutilEventEngine::dispatchEvent($event);
}
}
diff --git a/src/applications/people/mail/PhabricatorPeopleMailEngine.php b/src/applications/people/mail/PhabricatorPeopleMailEngine.php
new file mode 100644
index 000000000..281009341
--- /dev/null
+++ b/src/applications/people/mail/PhabricatorPeopleMailEngine.php
@@ -0,0 +1,72 @@
+<?php
+
+abstract class PhabricatorPeopleMailEngine
+ extends Phobject {
+
+ private $sender;
+ private $recipient;
+
+ final public function setSender(PhabricatorUser $sender) {
+ $this->sender = $sender;
+ return $this;
+ }
+
+ final public function getSender() {
+ if (!$this->sender) {
+ throw new PhutilInvalidStateException('setSender');
+ }
+ return $this->sender;
+ }
+
+ final public function setRecipient(PhabricatorUser $recipient) {
+ $this->recipient = $recipient;
+ return $this;
+ }
+
+ final public function getRecipient() {
+ if (!$this->recipient) {
+ throw new PhutilInvalidStateException('setRecipient');
+ }
+ return $this->recipient;
+ }
+
+ final public function canSendMail() {
+ try {
+ $this->validateMail();
+ return true;
+ } catch (PhabricatorPeopleMailEngineException $ex) {
+ return false;
+ }
+ }
+
+ final public function sendMail() {
+ $this->validateMail();
+ $mail = $this->newMail();
+
+ $mail
+ ->setForceDelivery(true)
+ ->save();
+
+ return $mail;
+ }
+
+ abstract public function validateMail();
+ abstract protected function newMail();
+
+
+ final protected function throwValidationException($title, $body) {
+ throw new PhabricatorPeopleMailEngineException($title, $body);
+ }
+
+ final protected function newRemarkupText($text) {
+ $recipient = $this->getRecipient();
+
+ $engine = PhabricatorMarkupEngine::newMarkupEngine(array())
+ ->setConfig('viewer', $recipient)
+ ->setConfig('uri.base', PhabricatorEnv::getProductionURI('/'))
+ ->setMode(PhutilRemarkupEngine::MODE_TEXT);
+
+ return $engine->markupText($text);
+ }
+
+}
diff --git a/src/applications/people/mail/PhabricatorPeopleMailEngineException.php b/src/applications/people/mail/PhabricatorPeopleMailEngineException.php
new file mode 100644
index 000000000..fa19bdfa9
--- /dev/null
+++ b/src/applications/people/mail/PhabricatorPeopleMailEngineException.php
@@ -0,0 +1,24 @@
+<?php
+
+final class PhabricatorPeopleMailEngineException
+ extends Exception {
+
+ private $title;
+ private $body;
+
+ public function __construct($title, $body) {
+ $this->title = $title;
+ $this->body = $body;
+
+ parent::__construct(pht('%s: %s', $title, $body));
+ }
+
+ public function getTitle() {
+ return $this->title;
+ }
+
+ public function getBody() {
+ return $this->body;
+ }
+
+}
diff --git a/src/applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php b/src/applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php
new file mode 100644
index 000000000..ff7ee7127
--- /dev/null
+++ b/src/applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php
@@ -0,0 +1,135 @@
+<?php
+
+final class PhabricatorPeopleWelcomeMailEngine
+ extends PhabricatorPeopleMailEngine {
+
+ private $welcomeMessage;
+
+ public function setWelcomeMessage($welcome_message) {
+ $this->welcomeMessage = $welcome_message;
+ return $this;
+ }
+
+ public function getWelcomeMessage() {
+ return $this->welcomeMessage;
+ }
+
+ public function validateMail() {
+ $sender = $this->getSender();
+ $recipient = $this->getRecipient();
+
+ if (!$sender->getIsAdmin()) {
+ $this->throwValidationException(
+ pht('Not an Administrator'),
+ pht(
+ 'You can not send welcome mail because you are not an '.
+ 'administrator. Only administrators may send welcome mail.'));
+ }
+
+ if ($recipient->getIsDisabled()) {
+ $this->throwValidationException(
+ pht('User is Disabled'),
+ pht(
+ 'You can not send welcome mail to this user because their account '.
+ 'is disabled.'));
+ }
+
+ if (!$recipient->canEstablishWebSessions()) {
+ $this->throwValidationException(
+ pht('Not a Normal User'),
+ pht(
+ 'You can not send this user welcome mail because they are not '.
+ 'a normal user and can not log in to the web interface. Special '.
+ 'users (like bots and mailing lists) are unable to establish '.
+ 'web sessions.'));
+ }
+ }
+
+ protected function newMail() {
+ $sender = $this->getSender();
+ $recipient = $this->getRecipient();
+
+ $base_uri = PhabricatorEnv::getProductionURI('/');
+
+ $engine = new PhabricatorAuthSessionEngine();
+
+ $uri = $engine->getOneTimeLoginURI(
+ $recipient,
+ $recipient->loadPrimaryEmail(),
+ PhabricatorAuthSessionEngine::ONETIME_WELCOME);
+
+ $message = array();
+
+ $message[] = pht('Welcome to Phabricator!');
+
+ $message[] = pht(
+ '%s (%s) has created an account for you.',
+ $sender->getUsername(),
+ $sender->getRealName());
+
+ $message[] = pht(
+ ' Username: %s',
+ $recipient->getUsername());
+
+ // If password auth is enabled, give the user specific instructions about
+ // how to add a credential to their account.
+
+ // If we aren't sure what they're supposed to be doing and passwords are
+ // not enabled, just give them generic instructions.
+
+ $use_passwords = PhabricatorPasswordAuthProvider::getPasswordProvider();
+ if ($use_passwords) {
+ $message[] = pht(
+ 'To log in to Phabricator, follow this link and set a password:');
+ $message[] = pht(' %s', $uri);
+ $message[] = pht(
+ 'After you have set a password, you can log in to Phabricator in '.
+ 'the future by going here:');
+ $message[] = pht(' %s', $base_uri);
+ } else {
+ $message[] = pht(
+ 'To log in to your account for the first time, follow this link:');
+ $message[] = pht(' %s', $uri);
+ $message[] = pht(
+ 'After you set up your account, you can log in to Phabricator in '.
+ 'the future by going here:');
+ $message[] = pht(' %s', $base_uri);
+ }
+
+ $message_body = $this->newBody();
+ if ($message_body !== null) {
+ $message[] = $message_body;
+ }
+
+ $message = implode("\n\n", $message);
+
+ return id(new PhabricatorMetaMTAMail())
+ ->addTos(array($recipient->getPHID()))
+ ->setSubject(pht('[Phabricator] Welcome to Phabricator'))
+ ->setBody($message);
+ }
+
+ private function newBody() {
+ $recipient = $this->getRecipient();
+
+ $custom_body = $this->getWelcomeMessage();
+ if (strlen($custom_body)) {
+ return $this->newRemarkupText($custom_body);
+ }
+
+ $default_body = PhabricatorAuthMessage::loadMessageText(
+ $recipient,
+ PhabricatorAuthWelcomeMailMessageType::MESSAGEKEY);
+ if (strlen($default_body)) {
+ return $this->newRemarkupText($default_body);
+ }
+
+ $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
+ if (!$is_serious) {
+ return pht("Love,\nPhabricator");
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php
index d3c836ac0..42228f6bd 100644
--- a/src/applications/people/storage/PhabricatorUser.php
+++ b/src/applications/people/storage/PhabricatorUser.php
@@ -1,1696 +1,1582 @@
<?php
/**
* @task availability Availability
* @task image-cache Profile Image Cache
* @task factors Multi-Factor Authentication
* @task handles Managing Handles
* @task settings Settings
* @task cache User Cache
*/
final class PhabricatorUser
extends PhabricatorUserDAO
implements
PhutilPerson,
PhabricatorPolicyInterface,
PhabricatorCustomFieldInterface,
PhabricatorDestructibleInterface,
PhabricatorSSHPublicKeyInterface,
PhabricatorFlaggableInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface,
PhabricatorConduitResultInterface,
PhabricatorAuthPasswordHashInterface {
const SESSION_TABLE = 'phabricator_session';
const NAMETOKEN_TABLE = 'user_nametoken';
const MAXIMUM_USERNAME_LENGTH = 64;
protected $userName;
protected $realName;
protected $profileImagePHID;
protected $defaultProfileImagePHID;
protected $defaultProfileImageVersion;
protected $availabilityCache;
protected $availabilityCacheTTL;
protected $conduitCertificate;
protected $isSystemAgent = 0;
protected $isMailingList = 0;
protected $isAdmin = 0;
protected $isDisabled = 0;
protected $isEmailVerified = 0;
protected $isApproved = 0;
protected $isEnrolledInMultiFactor = 0;
protected $accountSecret;
private $profile = null;
private $availability = self::ATTACHABLE;
private $preferences = null;
private $omnipotent = false;
private $customFields = self::ATTACHABLE;
private $badgePHIDs = self::ATTACHABLE;
private $alternateCSRFString = self::ATTACHABLE;
private $session = self::ATTACHABLE;
private $rawCacheData = array();
private $usableCacheData = array();
private $authorities = array();
private $handlePool;
private $csrfSalt;
private $settingCacheKeys = array();
private $settingCache = array();
private $allowInlineCacheGeneration;
private $conduitClusterToken = self::ATTACHABLE;
protected function readField($field) {
switch ($field) {
// Make sure these return booleans.
case 'isAdmin':
return (bool)$this->isAdmin;
case 'isDisabled':
return (bool)$this->isDisabled;
case 'isSystemAgent':
return (bool)$this->isSystemAgent;
case 'isMailingList':
return (bool)$this->isMailingList;
case 'isEmailVerified':
return (bool)$this->isEmailVerified;
case 'isApproved':
return (bool)$this->isApproved;
default:
return parent::readField($field);
}
}
/**
* Is this a live account which has passed required approvals? Returns true
* if this is an enabled, verified (if required), approved (if required)
* account, and false otherwise.
*
* @return bool True if this is a standard, usable account.
*/
public function isUserActivated() {
if (!$this->isLoggedIn()) {
return false;
}
if ($this->isOmnipotent()) {
return true;
}
if ($this->getIsDisabled()) {
return false;
}
if (!$this->getIsApproved()) {
return false;
}
if (PhabricatorUserEmail::isEmailVerificationRequired()) {
if (!$this->getIsEmailVerified()) {
return false;
}
}
return true;
}
/**
* Is this a user who we can reasonably expect to respond to requests?
*
* This is used to provide a grey "disabled/unresponsive" dot cue when
* rendering handles and tags, so it isn't a surprise if you get ignored
* when you ask things of users who will not receive notifications or could
* not respond to them (because they are disabled, unapproved, do not have
* verified email addresses, etc).
*
* @return bool True if this user can receive and respond to requests from
* other humans.
*/
public function isResponsive() {
if (!$this->isUserActivated()) {
return false;
}
if (!$this->getIsEmailVerified()) {
return false;
}
return true;
}
public function canEstablishWebSessions() {
if ($this->getIsMailingList()) {
return false;
}
if ($this->getIsSystemAgent()) {
return false;
}
return true;
}
public function canEstablishAPISessions() {
if ($this->getIsDisabled()) {
return false;
}
// Intracluster requests are permitted even if the user is logged out:
// in particular, public users are allowed to issue intracluster requests
// when browsing Diffusion.
if (PhabricatorEnv::isClusterRemoteAddress()) {
if (!$this->isLoggedIn()) {
return true;
}
}
if (!$this->isUserActivated()) {
return false;
}
if ($this->getIsMailingList()) {
return false;
}
return true;
}
public function canEstablishSSHSessions() {
if (!$this->isUserActivated()) {
return false;
}
if ($this->getIsMailingList()) {
return false;
}
return true;
}
/**
* Returns `true` if this is a standard user who is logged in. Returns `false`
* for logged out, anonymous, or external users.
*
* @return bool `true` if the user is a standard user who is logged in with
* a normal session.
*/
public function getIsStandardUser() {
$type_user = PhabricatorPeopleUserPHIDType::TYPECONST;
return $this->getPHID() && (phid_get_type($this->getPHID()) == $type_user);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'userName' => 'sort64',
'realName' => 'text128',
'profileImagePHID' => 'phid?',
'conduitCertificate' => 'text255',
'isSystemAgent' => 'bool',
'isMailingList' => 'bool',
'isDisabled' => 'bool',
'isAdmin' => 'bool',
'isEmailVerified' => 'uint32',
'isApproved' => 'uint32',
'accountSecret' => 'bytes64',
'isEnrolledInMultiFactor' => 'bool',
'availabilityCache' => 'text255?',
'availabilityCacheTTL' => 'uint32?',
'defaultProfileImagePHID' => 'phid?',
'defaultProfileImageVersion' => 'text64?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'userName' => array(
'columns' => array('userName'),
'unique' => true,
),
'realName' => array(
'columns' => array('realName'),
),
'key_approved' => array(
'columns' => array('isApproved'),
),
),
self::CONFIG_NO_MUTATE => array(
'availabilityCache' => true,
'availabilityCacheTTL' => true,
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPeopleUserPHIDType::TYPECONST);
}
public function getMonogram() {
return '@'.$this->getUsername();
}
public function isLoggedIn() {
return !($this->getPHID() === null);
}
public function saveWithoutIndex() {
return parent::save();
}
public function save() {
if (!$this->getConduitCertificate()) {
$this->setConduitCertificate($this->generateConduitCertificate());
}
if (!strlen($this->getAccountSecret())) {
$this->setAccountSecret(Filesystem::readRandomCharacters(64));
}
$result = $this->saveWithoutIndex();
if ($this->profile) {
$this->profile->save();
}
$this->updateNameTokens();
PhabricatorSearchWorker::queueDocumentForIndexing($this->getPHID());
return $result;
}
public function attachSession(PhabricatorAuthSession $session) {
$this->session = $session;
return $this;
}
public function getSession() {
return $this->assertAttached($this->session);
}
public function hasSession() {
return ($this->session !== self::ATTACHABLE);
}
public function hasHighSecuritySession() {
if (!$this->hasSession()) {
return false;
}
return $this->getSession()->isHighSecuritySession();
}
private function generateConduitCertificate() {
return Filesystem::readRandomCharacters(255);
}
- const CSRF_CYCLE_FREQUENCY = 3600;
- const CSRF_SALT_LENGTH = 8;
- const CSRF_TOKEN_LENGTH = 16;
- const CSRF_BREACH_PREFIX = 'B@';
-
const EMAIL_CYCLE_FREQUENCY = 86400;
const EMAIL_TOKEN_LENGTH = 24;
- private function getRawCSRFToken($offset = 0) {
- return $this->generateToken(
- time() + (self::CSRF_CYCLE_FREQUENCY * $offset),
- self::CSRF_CYCLE_FREQUENCY,
- PhabricatorEnv::getEnvConfig('phabricator.csrf-key'),
- self::CSRF_TOKEN_LENGTH);
- }
-
- public function getCSRFToken() {
- // c4science custo
- // disable csrf for admin // FIXME: CHECK THIS
- if ($this->isOmnipotent() && !$this->getIsAdmin()) {
- // We may end up here when called from the daemons. The omnipotent user
- // has no meaningful CSRF token, so just return `null`.
- return null;
- }
-
- if ($this->csrfSalt === null) {
- $this->csrfSalt = Filesystem::readRandomCharacters(
- self::CSRF_SALT_LENGTH);
- }
-
- $salt = $this->csrfSalt;
-
- // Generate a token hash to mitigate BREACH attacks against SSL. See
- // discussion in T3684.
- $token = $this->getRawCSRFToken();
- $hash = PhabricatorHash::weakDigest($token, $salt);
- return self::CSRF_BREACH_PREFIX.$salt.substr(
- $hash, 0, self::CSRF_TOKEN_LENGTH);
- }
-
- public function validateCSRFToken($token) {
- // We expect a BREACH-mitigating token. See T3684.
- $breach_prefix = self::CSRF_BREACH_PREFIX;
- $breach_prelen = strlen($breach_prefix);
- if (strncmp($token, $breach_prefix, $breach_prelen) !== 0) {
- return false;
- }
-
- $salt = substr($token, $breach_prelen, self::CSRF_SALT_LENGTH);
- $token = substr($token, $breach_prelen + self::CSRF_SALT_LENGTH);
-
- // When the user posts a form, we check that it contains a valid CSRF token.
- // Tokens cycle each hour (every CSRF_CYCLE_FREQUENCY seconds) and we accept
- // either the current token, the next token (users can submit a "future"
- // token if you have two web frontends that have some clock skew) or any of
- // the last 6 tokens. This means that pages are valid for up to 7 hours.
- // There is also some Javascript which periodically refreshes the CSRF
- // tokens on each page, so theoretically pages should be valid indefinitely.
- // However, this code may fail to run (if the user loses their internet
- // connection, or there's a JS problem, or they don't have JS enabled).
- // Choosing the size of the window in which we accept old CSRF tokens is
- // an issue of balancing concerns between security and usability. We could
- // choose a very narrow (e.g., 1-hour) window to reduce vulnerability to
- // attacks using captured CSRF tokens, but it's also more likely that real
- // users will be affected by this, e.g. if they close their laptop for an
- // hour, open it back up, and try to submit a form before the CSRF refresh
- // can kick in. Since the user experience of submitting a form with expired
- // CSRF is often quite bad (you basically lose data, or it's a big pain to
- // recover at least) and I believe we gain little additional protection
- // by keeping the window very short (the overwhelming value here is in
- // preventing blind attacks, and most attacks which can capture CSRF tokens
- // can also just capture authentication information [sniffing networks]
- // or act as the user [xss]) the 7 hour default seems like a reasonable
- // balance. Other major platforms have much longer CSRF token lifetimes,
- // like Rails (session duration) and Django (forever), which suggests this
- // is a reasonable analysis.
- $csrf_window = 6;
-
- for ($ii = -$csrf_window; $ii <= 1; $ii++) {
- $valid = $this->getRawCSRFToken($ii);
-
- $digest = PhabricatorHash::weakDigest($valid, $salt);
- $digest = substr($digest, 0, self::CSRF_TOKEN_LENGTH);
- if (phutil_hashes_are_identical($digest, $token)) {
- return true;
- }
- }
-
- return false;
- }
-
- private function generateToken($epoch, $frequency, $key, $len) {
- if ($this->getPHID()) {
- $vec = $this->getPHID().$this->getAccountSecret();
- } else {
- $vec = $this->getAlternateCSRFString();
- }
-
- if ($this->hasSession()) {
- $vec = $vec.$this->getSession()->getSessionKey();
- }
-
- $time_block = floor($epoch / $frequency);
- $vec = $vec.$key.$time_block;
-
- return substr(PhabricatorHash::weakDigest($vec), 0, $len);
- }
-
public function getUserProfile() {
return $this->assertAttached($this->profile);
}
public function attachUserProfile(PhabricatorUserProfile $profile) {
$this->profile = $profile;
return $this;
}
public function loadUserProfile() {
if ($this->profile) {
return $this->profile;
}
$profile_dao = new PhabricatorUserProfile();
$this->profile = $profile_dao->loadOneWhere('userPHID = %s',
$this->getPHID());
if (!$this->profile) {
$this->profile = PhabricatorUserProfile::initializeNewProfile($this);
}
return $this->profile;
}
public function loadPrimaryEmailAddress() {
$email = $this->loadPrimaryEmail();
if (!$email) {
throw new Exception(pht('User has no primary email address!'));
}
return $email->getAddress();
}
public function loadPrimaryEmail() {
return id(new PhabricatorUserEmail())->loadOneWhere(
'userPHID = %s AND isPrimary = 1',
$this->getPHID());
}
/* -( Settings )----------------------------------------------------------- */
public function getUserSetting($key) {
// NOTE: We store available keys and cached values separately to make it
// faster to check for `null` in the cache, which is common.
if (isset($this->settingCacheKeys[$key])) {
return $this->settingCache[$key];
}
$settings_key = PhabricatorUserPreferencesCacheType::KEY_PREFERENCES;
if ($this->getPHID()) {
$settings = $this->requireCacheData($settings_key);
} else {
$settings = $this->loadGlobalSettings();
}
if (array_key_exists($key, $settings)) {
$value = $settings[$key];
return $this->writeUserSettingCache($key, $value);
}
$cache = PhabricatorCaches::getRuntimeCache();
$cache_key = "settings.defaults({$key})";
$cache_map = $cache->getKeys(array($cache_key));
if ($cache_map) {
$value = $cache_map[$cache_key];
} else {
$defaults = PhabricatorSetting::getAllSettings();
if (isset($defaults[$key])) {
$value = id(clone $defaults[$key])
->setViewer($this)
->getSettingDefaultValue();
} else {
$value = null;
}
$cache->setKey($cache_key, $value);
}
return $this->writeUserSettingCache($key, $value);
}
/**
* Test if a given setting is set to a particular value.
*
* @param const Setting key.
* @param wild Value to compare.
* @return bool True if the setting has the specified value.
* @task settings
*/
public function compareUserSetting($key, $value) {
$actual = $this->getUserSetting($key);
return ($actual == $value);
}
private function writeUserSettingCache($key, $value) {
$this->settingCacheKeys[$key] = true;
$this->settingCache[$key] = $value;
return $value;
}
public function getTranslation() {
return $this->getUserSetting(PhabricatorTranslationSetting::SETTINGKEY);
}
public function getTimezoneIdentifier() {
return $this->getUserSetting(PhabricatorTimezoneSetting::SETTINGKEY);
}
public static function getGlobalSettingsCacheKey() {
return 'user.settings.globals.v1';
}
private function loadGlobalSettings() {
$cache_key = self::getGlobalSettingsCacheKey();
$cache = PhabricatorCaches::getMutableStructureCache();
$settings = $cache->getKey($cache_key);
if (!$settings) {
$preferences = PhabricatorUserPreferences::loadGlobalPreferences($this);
$settings = $preferences->getPreferences();
$cache->setKey($cache_key, $settings);
}
return $settings;
}
/**
* Override the user's timezone identifier.
*
* This is primarily useful for unit tests.
*
* @param string New timezone identifier.
* @return this
* @task settings
*/
public function overrideTimezoneIdentifier($identifier) {
$timezone_key = PhabricatorTimezoneSetting::SETTINGKEY;
$this->settingCacheKeys[$timezone_key] = true;
$this->settingCache[$timezone_key] = $identifier;
return $this;
}
public function getGender() {
return $this->getUserSetting(PhabricatorPronounSetting::SETTINGKEY);
}
public function loadEditorLink(
$path,
$line,
PhabricatorRepository $repository = null) {
$editor = $this->getUserSetting(PhabricatorEditorSetting::SETTINGKEY);
if (is_array($path)) {
$multi_key = PhabricatorEditorMultipleSetting::SETTINGKEY;
$multiedit = $this->getUserSetting($multi_key);
switch ($multiedit) {
case PhabricatorEditorMultipleSetting::VALUE_SPACES:
$path = implode(' ', $path);
break;
case PhabricatorEditorMultipleSetting::VALUE_SINGLE:
default:
return null;
}
}
if (!strlen($editor)) {
return null;
}
if ($repository) {
$callsign = $repository->getCallsign();
} else {
$callsign = null;
}
$uri = strtr($editor, array(
'%%' => '%',
'%f' => phutil_escape_uri($path),
'%l' => phutil_escape_uri($line),
'%r' => phutil_escape_uri($callsign),
));
// The resulting URI must have an allowed protocol. Otherwise, we'll return
// a link to an error page explaining the misconfiguration.
$ok = PhabricatorHelpEditorProtocolController::hasAllowedProtocol($uri);
if (!$ok) {
return '/help/editorprotocol/';
}
return (string)$uri;
}
- public function getAlternateCSRFString() {
- return $this->assertAttached($this->alternateCSRFString);
- }
-
- public function attachAlternateCSRFString($string) {
- $this->alternateCSRFString = $string;
- return $this;
- }
-
/**
* Populate the nametoken table, which used to fetch typeahead results. When
* a user types "linc", we want to match "Abraham Lincoln" from on-demand
* typeahead sources. To do this, we need a separate table of name fragments.
*/
public function updateNameTokens() {
$table = self::NAMETOKEN_TABLE;
$conn_w = $this->establishConnection('w');
$tokens = PhabricatorTypeaheadDatasource::tokenizeString(
$this->getUserName().' '.$this->getRealName());
$sql = array();
foreach ($tokens as $token) {
$sql[] = qsprintf(
$conn_w,
'(%d, %s)',
$this->getID(),
$token);
}
queryfx(
$conn_w,
'DELETE FROM %T WHERE userID = %d',
$table,
$this->getID());
if ($sql) {
queryfx(
$conn_w,
'INSERT INTO %T (userID, token) VALUES %LQ',
$table,
$sql);
}
}
- public function sendWelcomeEmail(PhabricatorUser $admin) {
- if (!$this->canEstablishWebSessions()) {
- throw new Exception(
- pht(
- 'Can not send welcome mail to users who can not establish '.
- 'web sessions!'));
- }
-
- $admin_username = $admin->getUserName();
- $admin_realname = $admin->getRealName();
- $user_username = $this->getUserName();
- $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
-
- $base_uri = PhabricatorEnv::getProductionURI('/');
-
- $engine = new PhabricatorAuthSessionEngine();
- $uri = $engine->getOneTimeLoginURI(
- $this,
- $this->loadPrimaryEmail(),
- PhabricatorAuthSessionEngine::ONETIME_WELCOME);
-
- $body = pht(
- "Welcome to Phabricator!\n\n".
- "%s (%s) has created an account for you.\n\n".
- " Username: %s\n\n".
- "To login to Phabricator, follow this link and set a password:\n\n".
- " %s\n\n".
- "After you have set a password, you can login in the future by ".
- "going here:\n\n".
- " %s\n",
- $admin_username,
- $admin_realname,
- $user_username,
- $uri,
- $base_uri);
-
- if (!$is_serious) {
- $body .= sprintf(
- "\n%s\n",
- pht("Love,\nPhabricator"));
- }
-
- $mail = id(new PhabricatorMetaMTAMail())
- ->addTos(array($this->getPHID()))
- ->setForceDelivery(true)
- ->setSubject(pht('[Phabricator] Welcome to Phabricator'))
- ->setBody($body)
- ->saveAndSend();
- }
-
public function sendUsernameChangeEmail(
PhabricatorUser $admin,
$old_username) {
$admin_username = $admin->getUserName();
$admin_realname = $admin->getRealName();
$new_username = $this->getUserName();
$password_instructions = null;
if (PhabricatorPasswordAuthProvider::getPasswordProvider()) {
$engine = new PhabricatorAuthSessionEngine();
$uri = $engine->getOneTimeLoginURI(
$this,
null,
PhabricatorAuthSessionEngine::ONETIME_USERNAME);
$password_instructions = sprintf(
"%s\n\n %s\n\n%s\n",
pht(
"If you use a password to login, you'll need to reset it ".
"before you can login again. You can reset your password by ".
"following this link:"),
$uri,
pht(
"And, of course, you'll need to use your new username to login ".
"from now on. If you use OAuth to login, nothing should change."));
}
$body = sprintf(
"%s\n\n %s\n %s\n\n%s",
pht(
'%s (%s) has changed your Phabricator username.',
$admin_username,
$admin_realname),
pht(
'Old Username: %s',
$old_username),
pht(
'New Username: %s',
$new_username),
$password_instructions);
$mail = id(new PhabricatorMetaMTAMail())
->addTos(array($this->getPHID()))
->setForceDelivery(true)
->setSubject(pht('[Phabricator] Username Changed'))
->setBody($body)
->saveAndSend();
}
public static function describeValidUsername() {
return pht(
'Usernames must contain only numbers, letters, period, underscore and '.
'hyphen, and can not end with a period. They must have no more than %d '.
'characters.',
new PhutilNumber(self::MAXIMUM_USERNAME_LENGTH));
}
public static function validateUsername($username) {
// NOTE: If you update this, make sure to update:
//
// - Remarkup rule for @mentions.
// - Routing rule for "/p/username/".
// - Unit tests, obviously.
// - describeValidUsername() method, above.
if (strlen($username) > self::MAXIMUM_USERNAME_LENGTH) {
return false;
}
return (bool)preg_match('/^[a-zA-Z0-9._-]*[a-zA-Z0-9_-]\z/', $username);
}
public static function getDefaultProfileImageURI() {
return celerity_get_resource_uri('/rsrc/image/avatar.png');
}
public function getProfileImageURI() {
$uri_key = PhabricatorUserProfileImageCacheType::KEY_URI;
return $this->requireCacheData($uri_key);
}
public function getUnreadNotificationCount() {
$notification_key = PhabricatorUserNotificationCountCacheType::KEY_COUNT;
return $this->requireCacheData($notification_key);
}
public function getUnreadMessageCount() {
$message_key = PhabricatorUserMessageCountCacheType::KEY_COUNT;
return $this->requireCacheData($message_key);
}
public function getRecentBadgeAwards() {
$badges_key = PhabricatorUserBadgesCacheType::KEY_BADGES;
return $this->requireCacheData($badges_key);
}
public function getFullName() {
if (strlen($this->getRealName())) {
return $this->getUsername().' ('.$this->getRealName().')';
} else {
return $this->getUsername();
}
}
public function getTimeZone() {
return new DateTimeZone($this->getTimezoneIdentifier());
}
public function getTimeZoneOffset() {
$timezone = $this->getTimeZone();
$now = new DateTime('@'.PhabricatorTime::getNow());
$offset = $timezone->getOffset($now);
// Javascript offsets are in minutes and have the opposite sign.
$offset = -(int)($offset / 60);
return $offset;
}
public function getTimeZoneOffsetInHours() {
$offset = $this->getTimeZoneOffset();
$offset = (int)round($offset / 60);
$offset = -$offset;
return $offset;
}
public function formatShortDateTime($when, $now = null) {
if ($now === null) {
$now = PhabricatorTime::getNow();
}
try {
$when = new DateTime('@'.$when);
$now = new DateTime('@'.$now);
} catch (Exception $ex) {
return null;
}
$zone = $this->getTimeZone();
$when->setTimeZone($zone);
$now->setTimeZone($zone);
if ($when->format('Y') !== $now->format('Y')) {
// Different year, so show "Feb 31 2075".
$format = 'M j Y';
} else if ($when->format('Ymd') !== $now->format('Ymd')) {
// Same year but different month and day, so show "Feb 31".
$format = 'M j';
} else {
// Same year, month and day so show a time of day.
$pref_time = PhabricatorTimeFormatSetting::SETTINGKEY;
$format = $this->getUserSetting($pref_time);
}
return $when->format($format);
}
public function __toString() {
return $this->getUsername();
}
public static function loadOneWithEmailAddress($address) {
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$address);
if (!$email) {
return null;
}
return id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$email->getUserPHID());
}
public function getDefaultSpacePHID() {
// TODO: We might let the user switch which space they're "in" later on;
// for now just use the global space if one exists.
// If the viewer has access to the default space, use that.
$spaces = PhabricatorSpacesNamespaceQuery::getViewerActiveSpaces($this);
foreach ($spaces as $space) {
if ($space->getIsDefaultNamespace()) {
return $space->getPHID();
}
}
// Otherwise, use the space with the lowest ID that they have access to.
// This just tends to keep the default stable and predictable over time,
// so adding a new space won't change behavior for users.
if ($spaces) {
$spaces = msort($spaces, 'getID');
return head($spaces)->getPHID();
}
return null;
}
/**
* Grant a user a source of authority, to let them bypass policy checks they
* could not otherwise.
*/
public function grantAuthority($authority) {
$this->authorities[] = $authority;
return $this;
}
/**
* Get authorities granted to the user.
*/
public function getAuthorities() {
return $this->authorities;
}
public function hasConduitClusterToken() {
return ($this->conduitClusterToken !== self::ATTACHABLE);
}
public function attachConduitClusterToken(PhabricatorConduitToken $token) {
$this->conduitClusterToken = $token;
return $this;
}
public function getConduitClusterToken() {
return $this->assertAttached($this->conduitClusterToken);
}
/* -( Availability )------------------------------------------------------- */
/**
* @task availability
*/
public function attachAvailability(array $availability) {
$this->availability = $availability;
return $this;
}
/**
* Get the timestamp the user is away until, if they are currently away.
*
* @return int|null Epoch timestamp, or `null` if the user is not away.
* @task availability
*/
public function getAwayUntil() {
$availability = $this->availability;
$this->assertAttached($availability);
if (!$availability) {
return null;
}
return idx($availability, 'until');
}
public function getDisplayAvailability() {
$availability = $this->availability;
$this->assertAttached($availability);
if (!$availability) {
return null;
}
$busy = PhabricatorCalendarEventInvitee::AVAILABILITY_BUSY;
return idx($availability, 'availability', $busy);
}
public function getAvailabilityEventPHID() {
$availability = $this->availability;
$this->assertAttached($availability);
if (!$availability) {
return null;
}
return idx($availability, 'eventPHID');
}
/**
* Get cached availability, if present.
*
* @return wild|null Cache data, or null if no cache is available.
* @task availability
*/
public function getAvailabilityCache() {
$now = PhabricatorTime::getNow();
if ($this->availabilityCacheTTL <= $now) {
return null;
}
try {
return phutil_json_decode($this->availabilityCache);
} catch (Exception $ex) {
return null;
}
}
/**
* Write to the availability cache.
*
* @param wild Availability cache data.
* @param int|null Cache TTL.
* @return this
* @task availability
*/
public function writeAvailabilityCache(array $availability, $ttl) {
if (PhabricatorEnv::isReadOnly()) {
return $this;
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
queryfx(
$this->establishConnection('w'),
'UPDATE %T SET availabilityCache = %s, availabilityCacheTTL = %nd
WHERE id = %d',
$this->getTableName(),
phutil_json_encode($availability),
$ttl,
$this->getID());
unset($unguarded);
return $this;
}
/* -( Multi-Factor Authentication )---------------------------------------- */
/**
* Update the flag storing this user's enrollment in multi-factor auth.
*
* With certain settings, we need to check if a user has MFA on every page,
* so we cache MFA enrollment on the user object for performance. Calling this
* method synchronizes the cache by examining enrollment records. After
* updating the cache, use @{method:getIsEnrolledInMultiFactor} to check if
* the user is enrolled.
*
* This method should be called after any changes are made to a given user's
* multi-factor configuration.
*
* @return void
* @task factors
*/
public function updateMultiFactorEnrollment() {
- $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
- 'userPHID = %s',
- $this->getPHID());
+ $factors = id(new PhabricatorAuthFactorConfigQuery())
+ ->setViewer($this)
+ ->withUserPHIDs(array($this->getPHID()))
+ ->withFactorProviderStatuses(
+ array(
+ PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE,
+ PhabricatorAuthFactorProviderStatus::STATUS_DEPRECATED,
+ ))
+ ->execute();
$enrolled = count($factors) ? 1 : 0;
if ($enrolled !== $this->isEnrolledInMultiFactor) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
queryfx(
$this->establishConnection('w'),
'UPDATE %T SET isEnrolledInMultiFactor = %d WHERE id = %d',
$this->getTableName(),
$enrolled,
$this->getID());
unset($unguarded);
$this->isEnrolledInMultiFactor = $enrolled;
}
}
/**
* Check if the user is enrolled in multi-factor authentication.
*
* Enrolled users have one or more multi-factor authentication sources
* attached to their account. For performance, this value is cached. You
* can use @{method:updateMultiFactorEnrollment} to update the cache.
*
* @return bool True if the user is enrolled.
* @task factors
*/
public function getIsEnrolledInMultiFactor() {
return $this->isEnrolledInMultiFactor;
}
/* -( Omnipotence )-------------------------------------------------------- */
/**
* Returns true if this user is omnipotent. Omnipotent users bypass all policy
* checks.
*
* @return bool True if the user bypasses policy checks.
*/
public function isOmnipotent() {
// c4science custo
// Allow administrators to bypass all policies.
if ($this->getIsAdmin()) {
return true;
}
return $this->omnipotent;
}
/**
* Get an omnipotent user object for use in contexts where there is no acting
* user, notably daemons.
*
* @return PhabricatorUser An omnipotent user.
*/
public static function getOmnipotentUser() {
static $user = null;
if (!$user) {
$user = new PhabricatorUser();
$user->omnipotent = true;
$user->makeEphemeral();
}
return $user;
}
/**
* Get a scalar string identifying this user.
*
* This is similar to using the PHID, but distinguishes between omnipotent
* and public users explicitly. This allows safe construction of cache keys
* or cache buckets which do not conflate public and omnipotent users.
*
* @return string Scalar identifier.
*/
public function getCacheFragment() {
if ($this->isOmnipotent()) {
return 'u.omnipotent';
}
$phid = $this->getPHID();
if ($phid) {
return 'u.'.$phid;
}
return 'u.public';
}
/* -( Managing Handles )--------------------------------------------------- */
/**
* Get a @{class:PhabricatorHandleList} which benefits from this viewer's
* internal handle pool.
*
* @param list<phid> List of PHIDs to load.
* @return PhabricatorHandleList Handle list object.
* @task handle
*/
public function loadHandles(array $phids) {
if ($this->handlePool === null) {
$this->handlePool = id(new PhabricatorHandlePool())
->setViewer($this);
}
return $this->handlePool->newHandleList($phids);
}
/**
* Get a @{class:PHUIHandleView} for a single handle.
*
* This benefits from the viewer's internal handle pool.
*
* @param phid PHID to render a handle for.
* @return PHUIHandleView View of the handle.
* @task handle
*/
public function renderHandle($phid) {
return $this->loadHandles(array($phid))->renderHandle($phid);
}
/**
* Get a @{class:PHUIHandleListView} for a list of handles.
*
* This benefits from the viewer's internal handle pool.
*
* @param list<phid> List of PHIDs to render.
* @return PHUIHandleListView View of the handles.
* @task handle
*/
public function renderHandleList(array $phids) {
return $this->loadHandles($phids)->renderList();
}
public function attachBadgePHIDs(array $phids) {
$this->badgePHIDs = $phids;
return $this;
}
public function getBadgePHIDs() {
return $this->assertAttached($this->badgePHIDs);
}
+/* -( CSRF )--------------------------------------------------------------- */
+
+
+ public function getCSRFToken() {
+ // c4science custo
+ // disable csrf for admin // FIXME: CHECK THIS
+ if ($this->isOmnipotent() && !$this->getIsAdmin()) {
+ // We may end up here when called from the daemons. The omnipotent user
+ // has no meaningful CSRF token, so just return `null`.
+ return null;
+ }
+
+ return $this->newCSRFEngine()
+ ->newToken();
+ }
+
+ public function validateCSRFToken($token) {
+ return $this->newCSRFengine()
+ ->isValidToken($token);
+ }
+
+ public function getAlternateCSRFString() {
+ return $this->assertAttached($this->alternateCSRFString);
+ }
+
+ public function attachAlternateCSRFString($string) {
+ $this->alternateCSRFString = $string;
+ return $this;
+ }
+
+ private function newCSRFEngine() {
+ if ($this->getPHID()) {
+ $vec = $this->getPHID().$this->getAccountSecret();
+ } else {
+ $vec = $this->getAlternateCSRFString();
+ }
+
+ if ($this->hasSession()) {
+ $vec = $vec.$this->getSession()->getSessionKey();
+ }
+
+ $engine = new PhabricatorAuthCSRFEngine();
+
+ if ($this->csrfSalt === null) {
+ $this->csrfSalt = $engine->newSalt();
+ }
+
+ $engine
+ ->setSalt($this->csrfSalt)
+ ->setSecret(new PhutilOpaqueEnvelope($vec));
+
+ return $engine;
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::POLICY_PUBLIC;
case PhabricatorPolicyCapability::CAN_EDIT:
if ($this->getIsSystemAgent() || $this->getIsMailingList()) {
return PhabricatorPolicies::POLICY_ADMIN;
} else {
return PhabricatorPolicies::POLICY_NOONE;
}
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getPHID() && ($viewer->getPHID() === $this->getPHID());
}
public function describeAutomaticCapability($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_EDIT:
return pht('Only you can edit your information.');
default:
return null;
}
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('user.fields');
}
public function getCustomFieldBaseClass() {
return 'PhabricatorUserCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$externals = id(new PhabricatorExternalAccount())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($externals as $external) {
$external->delete();
}
$prefs = id(new PhabricatorUserPreferencesQuery())
->setViewer($engine->getViewer())
->withUsers(array($this))
->execute();
foreach ($prefs as $pref) {
$engine->destroyObject($pref);
}
$profiles = id(new PhabricatorUserProfile())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($profiles as $profile) {
$profile->delete();
}
$keys = id(new PhabricatorAuthSSHKeyQuery())
->setViewer($engine->getViewer())
->withObjectPHIDs(array($this->getPHID()))
->execute();
foreach ($keys as $key) {
$engine->destroyObject($key);
}
$emails = id(new PhabricatorUserEmail())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($emails as $email) {
$email->delete();
}
$sessions = id(new PhabricatorAuthSession())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($sessions as $session) {
$session->delete();
}
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($factors as $factor) {
$factor->delete();
}
$this->saveTransaction();
}
/* -( PhabricatorSSHPublicKeyInterface )----------------------------------- */
public function getSSHPublicKeyManagementURI(PhabricatorUser $viewer) {
if ($viewer->getPHID() == $this->getPHID()) {
// If the viewer is managing their own keys, take them to the normal
// panel.
return '/settings/panel/ssh/';
} else {
// Otherwise, take them to the administrative panel for this user.
return '/settings/user/'.$this->getUsername().'/page/ssh/';
}
}
public function getSSHKeyDefaultName() {
return 'id_rsa_phabricator';
}
public function getSSHKeyNotifyPHIDs() {
return array(
$this->getPHID(),
);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorUserTransactionEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorUserTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
- return $timeline;
- }
-
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PhabricatorUserFulltextEngine();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new PhabricatorUserFerretEngine();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('username')
->setType('string')
->setDescription(pht("The user's username.")),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('realName')
->setType('string')
->setDescription(pht("The user's real name.")),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('roles')
->setType('list<string>')
->setDescription(pht('List of account roles.')),
);
}
public function getFieldValuesForConduit() {
$roles = array();
if ($this->getIsDisabled()) {
$roles[] = 'disabled';
}
if ($this->getIsSystemAgent()) {
$roles[] = 'bot';
}
if ($this->getIsMailingList()) {
$roles[] = 'list';
}
if ($this->getIsAdmin()) {
$roles[] = 'admin';
}
if ($this->getIsEmailVerified()) {
$roles[] = 'verified';
}
if ($this->getIsApproved()) {
$roles[] = 'approved';
}
if ($this->isUserActivated()) {
$roles[] = 'activated';
}
return array(
'username' => $this->getUsername(),
'realName' => $this->getRealName(),
'roles' => $roles,
);
}
public function getConduitSearchAttachments() {
return array(
id(new PhabricatorPeopleAvailabilitySearchEngineAttachment())
->setAttachmentKey('availability'),
);
}
/* -( User Cache )--------------------------------------------------------- */
/**
* @task cache
*/
public function attachRawCacheData(array $data) {
$this->rawCacheData = $data + $this->rawCacheData;
return $this;
}
public function setAllowInlineCacheGeneration($allow_cache_generation) {
$this->allowInlineCacheGeneration = $allow_cache_generation;
return $this;
}
/**
* @task cache
*/
protected function requireCacheData($key) {
if (isset($this->usableCacheData[$key])) {
return $this->usableCacheData[$key];
}
$type = PhabricatorUserCacheType::requireCacheTypeForKey($key);
if (isset($this->rawCacheData[$key])) {
$raw_value = $this->rawCacheData[$key];
$usable_value = $type->getValueFromStorage($raw_value);
$this->usableCacheData[$key] = $usable_value;
return $usable_value;
}
// By default, we throw if a cache isn't available. This is consistent
// with the standard `needX()` + `attachX()` + `getX()` interaction.
if (!$this->allowInlineCacheGeneration) {
throw new PhabricatorDataNotAttachedException($this);
}
$user_phid = $this->getPHID();
// Try to read the actual cache before we generate a new value. We can
// end up here via Conduit, which does not use normal sessions and can
// not pick up a free cache load during session identification.
if ($user_phid) {
$raw_data = PhabricatorUserCache::readCaches(
$type,
$key,
array($user_phid));
if (array_key_exists($user_phid, $raw_data)) {
$raw_value = $raw_data[$user_phid];
$usable_value = $type->getValueFromStorage($raw_value);
$this->rawCacheData[$key] = $raw_value;
$this->usableCacheData[$key] = $usable_value;
return $usable_value;
}
}
$usable_value = $type->getDefaultValue();
if ($user_phid) {
$map = $type->newValueForUsers($key, array($this));
if (array_key_exists($user_phid, $map)) {
$raw_value = $map[$user_phid];
$usable_value = $type->getValueFromStorage($raw_value);
$this->rawCacheData[$key] = $raw_value;
PhabricatorUserCache::writeCache(
$type,
$key,
$user_phid,
$raw_value);
}
}
$this->usableCacheData[$key] = $usable_value;
return $usable_value;
}
/**
* @task cache
*/
public function clearCacheData($key) {
unset($this->rawCacheData[$key]);
unset($this->usableCacheData[$key]);
return $this;
}
public function getCSSValue($variable_key) {
$preference = PhabricatorAccessibilitySetting::SETTINGKEY;
$key = $this->getUserSetting($preference);
$postprocessor = CelerityPostprocessor::getPostprocessor($key);
$variables = $postprocessor->getVariables();
if (!isset($variables[$variable_key])) {
throw new Exception(
pht(
'Unknown CSS variable "%s"!',
$variable_key));
}
return $variables[$variable_key];
}
/* -( PhabricatorAuthPasswordHashInterface )------------------------------- */
public function newPasswordDigest(
PhutilOpaqueEnvelope $envelope,
PhabricatorAuthPassword $password) {
// Before passwords are hashed, they are digested. The goal of digestion
// is twofold: to reduce the length of very long passwords to something
// reasonable; and to salt the password in case the best available hasher
// does not include salt automatically.
// Users may choose arbitrarily long passwords, and attackers may try to
// attack the system by probing it with very long passwords. When large
// inputs are passed to hashers -- which are intentionally slow -- it
// can result in unacceptably long runtimes. The classic attack here is
// to try to log in with a 64MB password and see if that locks up the
// machine for the next century. By digesting passwords to a standard
// length first, the length of the raw input does not impact the runtime
// of the hashing algorithm.
// Some hashers like bcrypt are self-salting, while other hashers are not.
// Applying salt while digesting passwords ensures that hashes are salted
// whether we ultimately select a self-salting hasher or not.
// For legacy compatibility reasons, old VCS and Account password digest
// algorithms are significantly more complicated than necessary to achieve
// these goals. This is because they once used a different hashing and
// salting process. When we upgraded to the modern modular hasher
// infrastructure, we just bolted it onto the end of the existing pipelines
// so that upgrading didn't break all users' credentials.
// New implementations can (and, generally, should) safely select the
// simple HMAC SHA256 digest at the bottom of the function, which does
// everything that a digest callback should without any needless legacy
// baggage on top.
if ($password->getLegacyDigestFormat() == 'v1') {
switch ($password->getPasswordType()) {
case PhabricatorAuthPassword::PASSWORD_TYPE_VCS:
// Old VCS passwords use an iterated HMAC SHA1 as a digest algorithm.
// They originally used this as a hasher, but it became a digest
// algorithm once hashing was upgraded to include bcrypt.
$digest = $envelope->openEnvelope();
$salt = $this->getPHID();
for ($ii = 0; $ii < 1000; $ii++) {
$digest = PhabricatorHash::weakDigest($digest, $salt);
}
return new PhutilOpaqueEnvelope($digest);
case PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT:
// Account passwords previously used this weird mess of salt and did
// not digest the input to a standard length.
// Beyond this being a weird special case, there are two actual
// problems with this, although neither are particularly severe:
// First, because we do not normalize the length of passwords, this
// algorithm may make us vulnerable to DOS attacks where an attacker
// attempts to use a very long input to slow down hashers.
// Second, because the username is part of the hash algorithm,
// renaming a user breaks their password. This isn't a huge deal but
// it's pretty silly. There's no security justification for this
// behavior, I just didn't think about the implication when I wrote
// it originally.
$parts = array(
$this->getUsername(),
$envelope->openEnvelope(),
$this->getPHID(),
$password->getPasswordSalt(),
);
return new PhutilOpaqueEnvelope(implode('', $parts));
}
}
// For passwords which do not have some crazy legacy reason to use some
// other digest algorithm, HMAC SHA256 is an excellent choice. It satisfies
// the digest requirements and is simple.
$digest = PhabricatorHash::digestHMACSHA256(
$envelope->openEnvelope(),
$password->getPasswordSalt());
return new PhutilOpaqueEnvelope($digest);
}
public function newPasswordBlocklist(
PhabricatorUser $viewer,
PhabricatorAuthPasswordEngine $engine) {
$list = array();
$list[] = $this->getUsername();
$list[] = $this->getRealName();
$emails = id(new PhabricatorUserEmail())->loadAllWhere(
'userPHID = %s',
$this->getPHID());
foreach ($emails as $email) {
$list[] = $email->getAddress();
}
return $list;
}
}
diff --git a/src/applications/people/xaction/PhabricatorUserEmpowerTransaction.php b/src/applications/people/xaction/PhabricatorUserEmpowerTransaction.php
new file mode 100644
index 000000000..1b561d323
--- /dev/null
+++ b/src/applications/people/xaction/PhabricatorUserEmpowerTransaction.php
@@ -0,0 +1,98 @@
+<?php
+
+final class PhabricatorUserEmpowerTransaction
+ extends PhabricatorUserTransactionType {
+
+ const TRANSACTIONTYPE = 'user.admin';
+
+ public function generateOldValue($object) {
+ return (bool)$object->getIsAdmin();
+ }
+
+ public function generateNewValue($object, $value) {
+ return (bool)$value;
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setIsAdmin((int)$value);
+ }
+
+ public function applyExternalEffects($object, $value) {
+ $user = $object;
+
+ $this->newUserLog(PhabricatorUserLog::ACTION_ADMIN)
+ ->setOldValue($this->getOldValue())
+ ->setNewValue($value)
+ ->save();
+ }
+
+ public function validateTransactions($object, array $xactions) {
+ $user = $object;
+ $actor = $this->getActor();
+
+ $errors = array();
+ foreach ($xactions as $xaction) {
+ $old = $xaction->getOldValue();
+ $new = $xaction->getNewValue();
+
+ if ($old === $new) {
+ continue;
+ }
+
+ if ($user->getPHID() === $actor->getPHID()) {
+ $errors[] = $this->newInvalidError(
+ pht('After a time, your efforts fail. You can not adjust your own '.
+ 'status as an administrator.'), $xaction);
+ }
+
+ $is_admin = $actor->getIsAdmin();
+ $is_omnipotent = $actor->isOmnipotent();
+
+ if (!$is_admin && !$is_omnipotent) {
+ $errors[] = $this->newInvalidError(
+ pht('You must be an administrator to create administrators.'),
+ $xaction);
+ }
+ }
+
+ return $errors;
+ }
+
+ public function getTitle() {
+ $new = $this->getNewValue();
+ if ($new) {
+ return pht(
+ '%s empowered this user as an administrator.',
+ $this->renderAuthor());
+ } else {
+ return pht(
+ '%s defrocked this user.',
+ $this->renderAuthor());
+ }
+ }
+
+ public function getTitleForFeed() {
+ $new = $this->getNewValue();
+ if ($new) {
+ return pht(
+ '%s empowered %s as an administrator.',
+ $this->renderAuthor(),
+ $this->renderObject());
+ } else {
+ return pht(
+ '%s defrocked %s.',
+ $this->renderAuthor(),
+ $this->renderObject());
+ }
+ }
+
+ public function getRequiredCapabilities(
+ $object,
+ PhabricatorApplicationTransaction $xaction) {
+
+ // Unlike normal user edits, admin promotions require admin
+ // permissions, which is enforced by validateTransactions().
+
+ return null;
+ }
+}
diff --git a/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php b/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php
index db134a5c7..b6d23b351 100644
--- a/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php
+++ b/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php
@@ -1,92 +1,99 @@
<?php
final class PhabricatorUserUsernameTransaction
extends PhabricatorUserTransactionType {
const TRANSACTIONTYPE = 'user.rename';
public function generateOldValue($object) {
return $object->getUsername();
}
public function generateNewValue($object, $value) {
return $value;
}
public function applyInternalEffects($object, $value) {
$object->setUsername($value);
}
public function applyExternalEffects($object, $value) {
$user = $object;
$this->newUserLog(PhabricatorUserLog::ACTION_CHANGE_USERNAME)
->setOldValue($this->getOldValue())
->setNewValue($value)
->save();
// The SSH key cache currently includes usernames, so dirty it. See T12554
// for discussion.
PhabricatorAuthSSHKeyQuery::deleteSSHKeyCache();
$user->sendUsernameChangeEmail($this->getActor(), $this->getOldValue());
}
public function getTitle() {
return pht(
'%s renamed this user from %s to %s.',
$this->renderAuthor(),
$this->renderOldValue(),
$this->renderNewValue());
}
public function validateTransactions($object, array $xactions) {
$actor = $this->getActor();
$errors = array();
foreach ($xactions as $xaction) {
$new = $xaction->getNewValue();
$old = $xaction->getOldValue();
if ($old === $new) {
continue;
}
if (!$actor->getIsAdmin()) {
$errors[] = $this->newInvalidError(
pht('You must be an administrator to rename users.'));
}
if (!strlen($new)) {
$errors[] = $this->newRequiredError(
pht('New username is required.'), $xaction);
} else if (!PhabricatorUser::validateUsername($new)) {
$errors[] = $this->newInvalidError(
PhabricatorUser::describeValidUsername(), $xaction);
}
$user = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withUsernames(array($new))
->executeOne();
if ($user) {
$errors[] = $this->newInvalidError(
pht('Another user already has that username.'), $xaction);
}
}
return $errors;
}
public function getRequiredCapabilities(
$object,
PhabricatorApplicationTransaction $xaction) {
// Unlike normal user edits, renames require admin permissions, which
// is enforced by validateTransactions().
return null;
}
+
+ public function shouldTryMFA(
+ $object,
+ PhabricatorApplicationTransaction $xaction) {
+ return true;
+ }
+
}
diff --git a/src/applications/phame/mail/PhamePostMailReceiver.php b/src/applications/phame/mail/PhamePostMailReceiver.php
index 3655e7390..3e2f493d3 100644
--- a/src/applications/phame/mail/PhamePostMailReceiver.php
+++ b/src/applications/phame/mail/PhamePostMailReceiver.php
@@ -1,28 +1,28 @@
<?php
final class PhamePostMailReceiver
extends PhabricatorObjectMailReceiver {
public function isEnabled() {
return PhabricatorApplication::isClassInstalled(
'PhabricatorPhameApplication');
}
protected function getObjectPattern() {
return 'J[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
- $id = (int)substr($pattern, 4);
+ $id = (int)substr($pattern, 1);
return id(new PhamePostQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
}
protected function getTransactionReplyHandler() {
return new PhamePostReplyHandler();
}
}
diff --git a/src/applications/phame/storage/PhameBlog.php b/src/applications/phame/storage/PhameBlog.php
index fa2ac0045..71a518622 100644
--- a/src/applications/phame/storage/PhameBlog.php
+++ b/src/applications/phame/storage/PhameBlog.php
@@ -1,404 +1,394 @@
<?php
final class PhameBlog extends PhameDAO
implements
PhabricatorPolicyInterface,
PhabricatorMarkupInterface,
PhabricatorSubscribableInterface,
PhabricatorFlaggableInterface,
PhabricatorProjectInterface,
PhabricatorDestructibleInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorConduitResultInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface {
const MARKUP_FIELD_DESCRIPTION = 'markup:description';
protected $name;
protected $subtitle;
protected $description;
protected $domain;
protected $domainFullURI;
protected $parentSite;
protected $parentDomain;
protected $configData;
protected $creatorPHID;
protected $viewPolicy;
protected $editPolicy;
protected $status;
protected $mailKey;
protected $profileImagePHID;
protected $headerImagePHID;
private $profileImageFile = self::ATTACHABLE;
private $headerImageFile = self::ATTACHABLE;
const STATUS_ACTIVE = 'active';
const STATUS_ARCHIVED = 'archived';
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'configData' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text64',
'subtitle' => 'text64',
'description' => 'text',
'domain' => 'text128?',
'domainFullURI' => 'text128?',
'parentSite' => 'text128?',
'parentDomain' => 'text128?',
'status' => 'text32',
'mailKey' => 'bytes20',
'profileImagePHID' => 'phid?',
'headerImagePHID' => 'phid?',
// T6203/NULLABILITY
// These policies should always be non-null.
'editPolicy' => 'policy?',
'viewPolicy' => 'policy?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'domain' => array(
'columns' => array('domain'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPhameBlogPHIDType::TYPECONST);
}
public static function initializeNewBlog(PhabricatorUser $actor) {
$blog = id(new PhameBlog())
->setCreatorPHID($actor->getPHID())
->setStatus(self::STATUS_ACTIVE)
->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy())
->setEditPolicy(PhabricatorPolicies::POLICY_USER);
return $blog;
}
public function isArchived() {
return ($this->getStatus() == self::STATUS_ARCHIVED);
}
public static function getStatusNameMap() {
return array(
self::STATUS_ACTIVE => pht('Active'),
self::STATUS_ARCHIVED => pht('Archived'),
);
}
/**
* Makes sure a given custom blog uri is properly configured in DNS
* to point at this Phabricator instance. If there is an error in
* the configuration, return a string describing the error and how
* to fix it. If there is no error, return an empty string.
*
* @return string
*/
public function validateCustomDomain($domain_full_uri) {
$example_domain = 'http://blog.example.com/';
$label = pht('Invalid');
// note this "uri" should be pretty busted given the desired input
// so just use it to test if there's a protocol specified
$uri = new PhutilURI($domain_full_uri);
$domain = $uri->getDomain();
$protocol = $uri->getProtocol();
$path = $uri->getPath();
$supported_protocols = array('http', 'https');
if (!in_array($protocol, $supported_protocols)) {
return pht(
'The custom domain should include a valid protocol in the URI '.
'(for example, "%s"). Valid protocols are "http" or "https".',
$example_domain);
}
if (strlen($path) && $path != '/') {
return pht(
'The custom domain should not specify a path (hosting a Phame '.
'blog at a path is currently not supported). Instead, just provide '.
'the bare domain name (for example, "%s").',
$example_domain);
}
if (strpos($domain, '.') === false) {
return pht(
'The custom domain should contain at least one dot (.) because '.
'some browsers fail to set cookies on domains without a dot. '.
'Instead, use a normal looking domain name like "%s".',
$example_domain);
}
if (!PhabricatorEnv::getEnvConfig('policy.allow-public')) {
$href = PhabricatorEnv::getProductionURI(
'/config/edit/policy.allow-public/');
return pht(
'For custom domains to work, this Phabricator instance must be '.
'configured to allow the public access policy. Configure this '.
'setting %s, or ask an administrator to configure this setting. '.
'The domain can be specified later once this setting has been '.
'changed.',
phutil_tag(
'a',
array('href' => $href),
pht('here')));
}
return null;
}
public function getLiveURI() {
if (strlen($this->getDomain())) {
return $this->getExternalLiveURI();
} else {
return $this->getInternalLiveURI();
}
}
public function getExternalLiveURI() {
$uri = new PhutilURI($this->getDomainFullURI());
PhabricatorEnv::requireValidRemoteURIForLink($uri);
return (string)$uri;
}
public function getExternalParentURI() {
$uri = $this->getParentDomain();
PhabricatorEnv::requireValidRemoteURIForLink($uri);
return (string)$uri;
}
public function getInternalLiveURI() {
return '/phame/live/'.$this->getID().'/';
}
public function getViewURI() {
return '/phame/blog/view/'.$this->getID().'/';
}
public function getManageURI() {
return '/phame/blog/manage/'.$this->getID().'/';
}
public function getProfileImageURI() {
return $this->getProfileImageFile()->getBestURI();
}
public function attachProfileImageFile(PhabricatorFile $file) {
$this->profileImageFile = $file;
return $this;
}
public function getProfileImageFile() {
return $this->assertAttached($this->profileImageFile);
}
public function getHeaderImageURI() {
return $this->getHeaderImageFile()->getBestURI();
}
public function attachHeaderImageFile(PhabricatorFile $file) {
$this->headerImageFile = $file;
return $this;
}
public function getHeaderImageFile() {
return $this->assertAttached($this->headerImageFile);
}
/* -( 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 $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
$can_edit = PhabricatorPolicyCapability::CAN_EDIT;
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
// Users who can edit or post to a blog can always view it.
if (PhabricatorPolicyFilter::hasCapability($user, $this, $can_edit)) {
return true;
}
break;
}
return false;
}
public function describeAutomaticCapability($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return pht(
'Users who can edit a blog can always view it.');
}
return null;
}
/* -( PhabricatorMarkupInterface Implementation )-------------------------- */
public function getMarkupFieldKey($field) {
$content = $this->getMarkupText($field);
return PhabricatorMarkupEngine::digestRemarkupContent($this, $content);
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newPhameMarkupEngine();
}
public function getMarkupText($field) {
return $this->getDescription();
}
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
public function shouldUseMarkupCache($field) {
return (bool)$this->getPHID();
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$posts = id(new PhamePostQuery())
->setViewer($engine->getViewer())
->withBlogPHIDs(array($this->getPHID()))
->execute();
foreach ($posts as $post) {
$engine->destroyObject($post);
}
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhameBlogEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhameBlogTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
- return $timeline;
- }
-
/* -( PhabricatorSubscribableInterface Implementation )-------------------- */
public function isAutomaticallySubscribed($phid) {
return false;
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of the blog.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('description')
->setType('string')
->setDescription(pht('Blog description.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('status')
->setType('string')
->setDescription(pht('Archived or active status.')),
);
}
public function getFieldValuesForConduit() {
return array(
'name' => $this->getName(),
'description' => $this->getDescription(),
'status' => $this->getStatus(),
);
}
public function getConduitSearchAttachments() {
return array();
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PhameBlogFulltextEngine();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new PhameBlogFerretEngine();
}
}
diff --git a/src/applications/phame/storage/PhamePost.php b/src/applications/phame/storage/PhamePost.php
index 8380a18f4..300579b08 100644
--- a/src/applications/phame/storage/PhamePost.php
+++ b/src/applications/phame/storage/PhamePost.php
@@ -1,399 +1,388 @@
<?php
final class PhamePost extends PhameDAO
implements
PhabricatorPolicyInterface,
PhabricatorMarkupInterface,
PhabricatorFlaggableInterface,
PhabricatorProjectInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorSubscribableInterface,
PhabricatorDestructibleInterface,
PhabricatorTokenReceiverInterface,
PhabricatorConduitResultInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface {
const MARKUP_FIELD_BODY = 'markup:body';
const MARKUP_FIELD_SUMMARY = 'markup:summary';
protected $bloggerPHID;
protected $title;
protected $subtitle;
protected $phameTitle;
protected $body;
protected $visibility;
protected $configData;
protected $datePublished;
protected $blogPHID;
protected $mailKey;
protected $headerImagePHID;
private $blog = self::ATTACHABLE;
private $headerImageFile = self::ATTACHABLE;
public static function initializePost(
PhabricatorUser $blogger,
PhameBlog $blog) {
$post = id(new PhamePost())
->setBloggerPHID($blogger->getPHID())
->setBlogPHID($blog->getPHID())
->attachBlog($blog)
->setDatePublished(PhabricatorTime::getNow())
->setVisibility(PhameConstants::VISIBILITY_PUBLISHED);
return $post;
}
public function attachBlog(PhameBlog $blog) {
$this->blog = $blog;
return $this;
}
public function getBlog() {
return $this->assertAttached($this->blog);
}
public function getMonogram() {
return 'J'.$this->getID();
}
public function getLiveURI() {
$blog = $this->getBlog();
$is_draft = $this->isDraft();
$is_archived = $this->isArchived();
if (strlen($blog->getDomain()) && !$is_draft && !$is_archived) {
return $this->getExternalLiveURI();
} else {
return $this->getInternalLiveURI();
}
}
public function getExternalLiveURI() {
$id = $this->getID();
$slug = $this->getSlug();
$path = "/post/{$id}/{$slug}/";
$domain = $this->getBlog()->getDomain();
return (string)id(new PhutilURI('http://'.$domain))
->setPath($path);
}
public function getInternalLiveURI() {
$id = $this->getID();
$slug = $this->getSlug();
$blog_id = $this->getBlog()->getID();
return "/phame/live/{$blog_id}/post/{$id}/{$slug}/";
}
public function getViewURI() {
$id = $this->getID();
$slug = $this->getSlug();
return "/phame/post/view/{$id}/{$slug}/";
}
public function getBestURI($is_live, $is_external) {
if ($is_live) {
if ($is_external) {
return $this->getExternalLiveURI();
} else {
return $this->getInternalLiveURI();
}
} else {
return $this->getViewURI();
}
}
public function getEditURI() {
return '/phame/post/edit/'.$this->getID().'/';
}
public function isDraft() {
return ($this->getVisibility() == PhameConstants::VISIBILITY_DRAFT);
}
public function isArchived() {
return ($this->getVisibility() == PhameConstants::VISIBILITY_ARCHIVED);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'configData' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'title' => 'text255',
'subtitle' => 'text64',
'phameTitle' => 'sort64?',
'visibility' => 'uint32',
'mailKey' => 'bytes20',
'headerImagePHID' => 'phid?',
// T6203/NULLABILITY
// These seem like they should always be non-null?
'blogPHID' => 'phid?',
'body' => 'text?',
'configData' => 'text?',
// T6203/NULLABILITY
// This one probably should be nullable?
'datePublished' => 'epoch',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'bloggerPosts' => array(
'columns' => array(
'bloggerPHID',
'visibility',
'datePublished',
'id',
),
),
),
) + parent::getConfiguration();
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPhamePostPHIDType::TYPECONST);
}
public function getSlug() {
return PhabricatorSlug::normalizeProjectSlug($this->getTitle());
}
public function getHeaderImageURI() {
return $this->getHeaderImageFile()->getBestURI();
}
public function attachHeaderImageFile(PhabricatorFile $file) {
$this->headerImageFile = $file;
return $this;
}
public function getHeaderImageFile() {
return $this->assertAttached($this->headerImageFile);
}
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
// Draft and archived posts are visible only to the author and other
// users who can edit the blog. Published posts are visible to whoever
// the blog is visible to.
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
if (!$this->isDraft() && !$this->isArchived() && $this->getBlog()) {
return $this->getBlog()->getViewPolicy();
} else if ($this->getBlog()) {
return $this->getBlog()->getEditPolicy();
} else {
return PhabricatorPolicies::POLICY_NOONE;
}
break;
case PhabricatorPolicyCapability::CAN_EDIT:
if ($this->getBlog()) {
return $this->getBlog()->getEditPolicy();
} else {
return PhabricatorPolicies::POLICY_NOONE;
}
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
// A blog post's author can always view it.
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
case PhabricatorPolicyCapability::CAN_EDIT:
return ($user->getPHID() == $this->getBloggerPHID());
}
}
public function describeAutomaticCapability($capability) {
return pht('The author of a blog post can always view and edit it.');
}
/* -( PhabricatorMarkupInterface Implementation )-------------------------- */
public function getMarkupFieldKey($field) {
$content = $this->getMarkupText($field);
return PhabricatorMarkupEngine::digestRemarkupContent($this, $content);
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newPhameMarkupEngine();
}
public function getMarkupText($field) {
switch ($field) {
case self::MARKUP_FIELD_BODY:
return $this->getBody();
case self::MARKUP_FIELD_SUMMARY:
return PhabricatorMarkupEngine::summarize($this->getBody());
}
}
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
public function shouldUseMarkupCache($field) {
return (bool)$this->getPHID();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhamePostEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhamePostTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getBloggerPHID(),
);
}
/* -( PhabricatorSubscribableInterface Implementation )-------------------- */
public function isAutomaticallySubscribed($phid) {
return ($this->bloggerPHID == $phid);
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('title')
->setType('string')
->setDescription(pht('Title of the post.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('slug')
->setType('string')
->setDescription(pht('Slug for the post.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('blogPHID')
->setType('phid')
->setDescription(pht('PHID of the blog that the post belongs to.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('authorPHID')
->setType('phid')
->setDescription(pht('PHID of the author of the post.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('body')
->setType('string')
->setDescription(pht('Body of the post.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('datePublished')
->setType('epoch?')
->setDescription(pht('Publish date, if the post has been published.')),
);
}
public function getFieldValuesForConduit() {
if ($this->isDraft()) {
$date_published = null;
} else if ($this->isArchived()) {
$date_published = null;
} else {
$date_published = (int)$this->getDatePublished();
}
return array(
'title' => $this->getTitle(),
'slug' => $this->getSlug(),
'blogPHID' => $this->getBlogPHID(),
'authorPHID' => $this->getBloggerPHID(),
'body' => $this->getBody(),
'datePublished' => $date_published,
);
}
public function getConduitSearchAttachments() {
return array();
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PhamePostFulltextEngine();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new PhamePostFerretEngine();
}
}
diff --git a/src/applications/phlux/storage/PhluxVariable.php b/src/applications/phlux/storage/PhluxVariable.php
index 875b3dee9..fb0d5c2f8 100644
--- a/src/applications/phlux/storage/PhluxVariable.php
+++ b/src/applications/phlux/storage/PhluxVariable.php
@@ -1,83 +1,72 @@
<?php
final class PhluxVariable extends PhluxDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorFlaggableInterface,
PhabricatorPolicyInterface {
protected $variableKey;
protected $variableValue;
protected $viewPolicy;
protected $editPolicy;
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'variableValue' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'variableKey' => 'text64',
),
self::CONFIG_KEY_SCHEMA => array(
'key_key' => array(
'columns' => array('variableKey'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(PhluxVariablePHIDType::TYPECONST);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhluxVariableEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhluxTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->viewPolicy;
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->editPolicy;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
}
diff --git a/src/applications/pholio/config/PhabricatorPholioConfigOptions.php b/src/applications/pholio/config/PhabricatorPholioConfigOptions.php
deleted file mode 100644
index 30bea9855..000000000
--- a/src/applications/pholio/config/PhabricatorPholioConfigOptions.php
+++ /dev/null
@@ -1,29 +0,0 @@
-<?php
-
-final class PhabricatorPholioConfigOptions
- extends PhabricatorApplicationConfigOptions {
-
- public function getName() {
- return pht('Pholio');
- }
-
- public function getDescription() {
- return pht('Configure Pholio.');
- }
-
- public function getIcon() {
- return 'fa-camera-retro';
- }
-
- public function getGroup() {
- return 'apps';
- }
-
- public function getOptions() {
- return array(
- $this->newOption('metamta.pholio.subject-prefix', 'string', '[Pholio]')
- ->setDescription(pht('Subject prefix for Pholio email.')),
- );
- }
-
-}
diff --git a/src/applications/pholio/controller/PholioImageUploadController.php b/src/applications/pholio/controller/PholioImageUploadController.php
index 0329d3eb1..0ff5e061f 100644
--- a/src/applications/pholio/controller/PholioImageUploadController.php
+++ b/src/applications/pholio/controller/PholioImageUploadController.php
@@ -1,43 +1,44 @@
<?php
final class PholioImageUploadController extends PholioController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$phid = $request->getStr('filePHID');
$replaces_phid = $request->getStr('replacesPHID');
$title = $request->getStr('title');
$description = $request->getStr('description');
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($phid))
->executeOne();
if (!$file) {
return new Aphront404Response();
}
if (!strlen($title)) {
$title = $file->getName();
}
- $image = id(new PholioImage())
+ $image = PholioImage::initializeNewImage()
+ ->setAuthorPHID($viewer->getPHID())
->attachFile($file)
->setName($title)
->setDescription($description)
->makeEphemeral();
$view = id(new PholioUploadedImageView())
->setUser($viewer)
->setImage($image)
->setReplacesPHID($replaces_phid);
$content = array(
'markup' => $view,
);
return id(new AphrontAjaxResponse())->setContent($content);
}
}
diff --git a/src/applications/pholio/controller/PholioMockCommentController.php b/src/applications/pholio/controller/PholioMockCommentController.php
index 1888c7369..a4f9daf88 100644
--- a/src/applications/pholio/controller/PholioMockCommentController.php
+++ b/src/applications/pholio/controller/PholioMockCommentController.php
@@ -1,84 +1,81 @@
<?php
final class PholioMockCommentController extends PholioController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
if (!$request->isFormPost()) {
return new Aphront400Response();
}
$mock = id(new PholioMockQuery())
->setViewer($viewer)
->withIDs(array($id))
->needImages(true)
->executeOne();
if (!$mock) {
return new Aphront404Response();
}
$is_preview = $request->isPreviewRequest();
$draft = PhabricatorDraft::buildFromRequest($request);
- $mock_uri = '/M'.$mock->getID();
+ $mock_uri = $mock->getURI();
$comment = $request->getStr('comment');
$xactions = array();
$inline_comments = id(new PholioTransactionComment())->loadAllWhere(
'authorphid = %s AND transactionphid IS NULL AND imageid IN (%Ld)',
$viewer->getPHID(),
- mpull($mock->getImages(), 'getID'));
+ mpull($mock->getActiveImages(), 'getID'));
if (!$inline_comments || strlen($comment)) {
$xactions[] = id(new PholioTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new PholioTransactionComment())
->setContent($comment));
}
foreach ($inline_comments as $inline_comment) {
$xactions[] = id(new PholioTransaction())
->setTransactionType(PholioMockInlineTransaction::TRANSACTIONTYPE)
->attachComment($inline_comment);
}
$editor = id(new PholioMockEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect($request->isContinueRequest())
->setIsPreview($is_preview);
try {
$xactions = $editor->applyTransactions($mock, $xactions);
} catch (PhabricatorApplicationTransactionNoEffectException $ex) {
return id(new PhabricatorApplicationTransactionNoEffectResponse())
->setCancelURI($mock_uri)
->setException($ex);
}
if ($draft) {
$draft->replaceOrDelete();
}
if ($request->isAjax() && $is_preview) {
- $xaction_view = id(new PholioTransactionView())
- ->setMock($mock);
-
return id(new PhabricatorApplicationTransactionResponse())
+ ->setObject($mock)
->setViewer($viewer)
->setTransactions($xactions)
- ->setTransactionView($xaction_view)
->setIsPreview($is_preview);
} else {
return id(new AphrontRedirectResponse())->setURI($mock_uri);
}
}
}
diff --git a/src/applications/pholio/controller/PholioMockEditController.php b/src/applications/pholio/controller/PholioMockEditController.php
index 89d1fe2a5..3e33a5eff 100644
--- a/src/applications/pholio/controller/PholioMockEditController.php
+++ b/src/applications/pholio/controller/PholioMockEditController.php
@@ -1,371 +1,374 @@
<?php
final class PholioMockEditController extends PholioController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
if ($id) {
$mock = id(new PholioMockQuery())
->setViewer($viewer)
->needImages(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->withIDs(array($id))
->executeOne();
if (!$mock) {
return new Aphront404Response();
}
$title = pht('Edit Mock: %s', $mock->getName());
$is_new = false;
- $mock_images = $mock->getImages();
+ $mock_images = $mock->getActiveImages();
$files = mpull($mock_images, 'getFile');
$mock_images = mpull($mock_images, null, 'getFilePHID');
} else {
$mock = PholioMock::initializeNewMock($viewer);
$title = pht('Create Mock');
$is_new = true;
$files = array();
$mock_images = array();
}
if ($is_new) {
$v_projects = array();
} else {
$v_projects = PhabricatorEdgeQuery::loadDestinationPHIDs(
$mock->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
$v_projects = array_reverse($v_projects);
}
$e_name = true;
$e_images = count($mock_images) ? null : true;
$errors = array();
$posted_mock_images = array();
$v_name = $mock->getName();
$v_desc = $mock->getDescription();
$v_view = $mock->getViewPolicy();
$v_edit = $mock->getEditPolicy();
$v_cc = PhabricatorSubscribersQuery::loadSubscribersForPHID(
$mock->getPHID());
$v_space = $mock->getSpacePHID();
if ($request->isFormPost()) {
$xactions = array();
$type_name = PholioMockNameTransaction::TRANSACTIONTYPE;
$type_desc = PholioMockDescriptionTransaction::TRANSACTIONTYPE;
$type_view = PhabricatorTransactions::TYPE_VIEW_POLICY;
$type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY;
$type_cc = PhabricatorTransactions::TYPE_SUBSCRIBERS;
$type_space = PhabricatorTransactions::TYPE_SPACE;
$v_name = $request->getStr('name');
$v_desc = $request->getStr('description');
$v_view = $request->getStr('can_view');
$v_edit = $request->getStr('can_edit');
$v_cc = $request->getArr('cc');
$v_projects = $request->getArr('projects');
$v_space = $request->getStr('spacePHID');
$mock_xactions = array();
$mock_xactions[$type_name] = $v_name;
$mock_xactions[$type_desc] = $v_desc;
$mock_xactions[$type_view] = $v_view;
$mock_xactions[$type_edit] = $v_edit;
$mock_xactions[$type_cc] = array('=' => $v_cc);
$mock_xactions[$type_space] = $v_space;
$file_phids = $request->getArr('file_phids');
if ($file_phids) {
$files = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
$files = array_select_keys($files, $file_phids);
} else {
$files = array();
}
if (!$files) {
$e_images = pht('Required');
$errors[] = pht('You must add at least one image to the mock.');
} else {
$mock->setCoverPHID(head($files)->getPHID());
}
foreach ($mock_xactions as $type => $value) {
$xactions[$type] = id(new PholioTransaction())
->setTransactionType($type)
->setNewValue($value);
}
$order = $request->getStrList('imageOrder');
$sequence_map = array_flip($order);
$replaces = $request->getArr('replaces');
$replaces_map = array_flip($replaces);
/**
* Foreach file posted, check to see whether we are replacing an image,
* adding an image, or simply updating image metadata. Create
* transactions for these cases as appropos.
*/
foreach ($files as $file_phid => $file) {
$replaces_image_phid = null;
if (isset($replaces_map[$file_phid])) {
$old_file_phid = $replaces_map[$file_phid];
if ($old_file_phid != $file_phid) {
$old_image = idx($mock_images, $old_file_phid);
if ($old_image) {
$replaces_image_phid = $old_image->getPHID();
}
}
}
$existing_image = idx($mock_images, $file_phid);
$title = (string)$request->getStr('title_'.$file_phid);
$description = (string)$request->getStr('description_'.$file_phid);
$sequence = $sequence_map[$file_phid];
if ($replaces_image_phid) {
- $replace_image = id(new PholioImage())
+ $replace_image = PholioImage::initializeNewImage()
+ ->setAuthorPHID($viewer->getPHID())
->setReplacesImagePHID($replaces_image_phid)
- ->setFilePhid($file_phid)
+ ->setFilePHID($file_phid)
->attachFile($file)
->setName(strlen($title) ? $title : $file->getName())
->setDescription($description)
- ->setSequence($sequence);
+ ->setSequence($sequence)
+ ->save();
+
$xactions[] = id(new PholioTransaction())
- ->setTransactionType(
- PholioImageReplaceTransaction::TRANSACTIONTYPE)
- ->setNewValue($replace_image);
+ ->setTransactionType(PholioImageReplaceTransaction::TRANSACTIONTYPE)
+ ->setNewValue($replace_image->getPHID());
+
$posted_mock_images[] = $replace_image;
} else if (!$existing_image) { // this is an add
- $add_image = id(new PholioImage())
- ->setFilePhid($file_phid)
+ $add_image = PholioImage::initializeNewImage()
+ ->setAuthorPHID($viewer->getPHID())
+ ->setFilePHID($file_phid)
->attachFile($file)
->setName(strlen($title) ? $title : $file->getName())
->setDescription($description)
- ->setSequence($sequence);
+ ->setSequence($sequence)
+ ->save();
+
$xactions[] = id(new PholioTransaction())
->setTransactionType(PholioImageFileTransaction::TRANSACTIONTYPE)
->setNewValue(
- array('+' => array($add_image)));
+ array('+' => array($add_image->getPHID())));
$posted_mock_images[] = $add_image;
} else {
$xactions[] = id(new PholioTransaction())
->setTransactionType(PholioImageNameTransaction::TRANSACTIONTYPE)
->setNewValue(
array($existing_image->getPHID() => $title));
$xactions[] = id(new PholioTransaction())
->setTransactionType(
PholioImageDescriptionTransaction::TRANSACTIONTYPE)
->setNewValue(
array($existing_image->getPHID() => $description));
$xactions[] = id(new PholioTransaction())
->setTransactionType(
PholioImageSequenceTransaction::TRANSACTIONTYPE)
->setNewValue(
array($existing_image->getPHID() => $sequence));
$posted_mock_images[] = $existing_image;
}
}
foreach ($mock_images as $file_phid => $mock_image) {
if (!isset($files[$file_phid]) && !isset($replaces[$file_phid])) {
// this is an outright delete
$xactions[] = id(new PholioTransaction())
->setTransactionType(PholioImageFileTransaction::TRANSACTIONTYPE)
->setNewValue(
- array('-' => array($mock_image)));
+ array('-' => array($mock_image->getPHID())));
}
}
if (!$errors) {
$proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
$xactions[] = id(new PholioTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $proj_edge_type)
->setNewValue(array('=' => array_fuse($v_projects)));
- $mock->openTransaction();
$editor = id(new PholioMockEditor())
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setActor($viewer);
$xactions = $editor->applyTransactions($mock, $xactions);
- $mock->saveTransaction();
-
return id(new AphrontRedirectResponse())
->setURI('/M'.$mock->getID());
}
}
if ($id) {
$submit = id(new AphrontFormSubmitControl())
->addCancelButton('/M'.$id)
->setValue(pht('Save'));
} else {
$submit = id(new AphrontFormSubmitControl())
->addCancelButton($this->getApplicationURI())
->setValue(pht('Create'));
}
$policies = id(new PhabricatorPolicyQuery())
->setViewer($viewer)
->setObject($mock)
->execute();
// NOTE: Make this show up correctly on the rendered form.
$mock->setViewPolicy($v_view);
$mock->setEditPolicy($v_edit);
$image_elements = array();
if ($posted_mock_images) {
$display_mock_images = $posted_mock_images;
} else {
$display_mock_images = $mock_images;
}
foreach ($display_mock_images as $mock_image) {
$image_elements[] = id(new PholioUploadedImageView())
->setUser($viewer)
->setImage($mock_image)
->setReplacesPHID($mock_image->getFilePHID());
}
$list_id = celerity_generate_unique_node_id();
$drop_id = celerity_generate_unique_node_id();
$order_id = celerity_generate_unique_node_id();
$list_control = phutil_tag(
'div',
array(
'id' => $list_id,
'class' => 'pholio-edit-list',
),
$image_elements);
$drop_control = phutil_tag(
'a',
array(
'id' => $drop_id,
'class' => 'pholio-edit-drop',
),
pht('Click here, or drag and drop images to add them to the mock.'));
$order_control = phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => 'imageOrder',
'id' => $order_id,
));
Javelin::initBehavior(
'pholio-mock-edit',
array(
'listID' => $list_id,
'dropID' => $drop_id,
'orderID' => $order_id,
'uploadURI' => '/file/dropupload/',
'renderURI' => $this->getApplicationURI('image/upload/'),
'pht' => array(
'uploading' => pht('Uploading Image...'),
'uploaded' => pht('Upload Complete...'),
'undo' => pht('Undo'),
'removed' => pht('This image will be removed from the mock.'),
),
));
require_celerity_resource('pholio-edit-css');
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild($order_control)
->appendChild(
id(new AphrontFormTextControl())
->setName('name')
->setValue($v_name)
->setLabel(pht('Name'))
->setError($e_name))
->appendChild(
id(new PhabricatorRemarkupControl())
->setName('description')
->setValue($v_desc)
->setLabel(pht('Description'))
->setUser($viewer))
->appendControl(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Tags'))
->setName('projects')
->setValue($v_projects)
->setDatasource(new PhabricatorProjectDatasource()))
->appendControl(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Subscribers'))
->setName('cc')
->setValue($v_cc)
->setUser($viewer)
->setDatasource(new PhabricatorMetaMTAMailableDatasource()))
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($viewer)
->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
->setPolicyObject($mock)
->setPolicies($policies)
->setSpacePHID($v_space)
->setName('can_view'))
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($viewer)
->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
->setPolicyObject($mock)
->setPolicies($policies)
->setName('can_edit'))
->appendChild(
id(new AphrontFormMarkupControl())
->setValue($list_control))
->appendChild(
id(new AphrontFormMarkupControl())
->setValue($drop_control)
->setError($e_images))
->appendChild($submit);
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setFormErrors($errors)
->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
->setForm($form);
$crumbs = $this->buildApplicationCrumbs();
if (!$is_new) {
$crumbs->addTextCrumb($mock->getMonogram(), '/'.$mock->getMonogram());
}
$crumbs->addTextCrumb($title);
$crumbs->setBorder(true);
$view = id(new PHUITwoColumnView())
->setFooter($form_box);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->addQuicksandConfig(
array('mockEditConfig' => true))
->appendChild($view);
}
}
diff --git a/src/applications/pholio/controller/PholioMockViewController.php b/src/applications/pholio/controller/PholioMockViewController.php
index 2ab31d0d9..e24889875 100644
--- a/src/applications/pholio/controller/PholioMockViewController.php
+++ b/src/applications/pholio/controller/PholioMockViewController.php
@@ -1,238 +1,234 @@
<?php
final class PholioMockViewController extends PholioController {
private $maniphestTaskPHIDs = array();
private function setManiphestTaskPHIDs($maniphest_task_phids) {
$this->maniphestTaskPHIDs = $maniphest_task_phids;
return $this;
}
private function getManiphestTaskPHIDs() {
return $this->maniphestTaskPHIDs;
}
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$image_id = $request->getURIData('imageID');
$mock = id(new PholioMockQuery())
->setViewer($viewer)
->withIDs(array($id))
->needImages(true)
->needInlineComments(true)
->executeOne();
if (!$mock) {
return new Aphront404Response();
}
$phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$mock->getPHID(),
PholioMockHasTaskEdgeType::EDGECONST);
$this->setManiphestTaskPHIDs($phids);
- $engine = id(new PhabricatorMarkupEngine())
- ->setViewer($viewer);
- $engine->addObject($mock, PholioMock::MARKUP_FIELD_DESCRIPTION);
-
$title = $mock->getName();
if ($mock->isClosed()) {
$header_icon = 'fa-ban';
$header_name = pht('Closed');
$header_color = 'dark';
} else {
$header_icon = 'fa-square-o';
$header_name = pht('Open');
$header_color = 'bluegrey';
}
$header = id(new PHUIHeaderView())
->setHeader($title)
->setUser($viewer)
->setStatus($header_icon, $header_color, $header_name)
->setPolicyObject($mock)
->setHeaderIcon('fa-camera-retro');
$timeline = $this->buildTransactionTimeline(
$mock,
- new PholioTransactionQuery(),
- $engine);
+ new PholioTransactionQuery());
$timeline->setMock($mock);
$curtain = $this->buildCurtainView($mock);
$details = $this->buildDescriptionView($mock);
require_celerity_resource('pholio-css');
require_celerity_resource('pholio-inline-comments-css');
$comment_form_id = celerity_generate_unique_node_id();
$mock_view = id(new PholioMockImagesView())
->setRequestURI($request->getRequestURI())
->setCommentFormID($comment_form_id)
->setUser($viewer)
->setMock($mock)
->setImageID($image_id);
$output = id(new PHUIObjectBoxView())
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($mock_view);
$add_comment = $this->buildAddCommentView($mock, $comment_form_id);
$crumbs = $this->buildApplicationCrumbs();
- $crumbs->addTextCrumb('M'.$mock->getID(), '/M'.$mock->getID());
+ $crumbs->addTextCrumb($mock->getMonogram(), $mock->getURI());
$crumbs->setBorder(true);
$thumb_grid = id(new PholioMockThumbGridView())
->setUser($viewer)
->setMock($mock);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setCurtain($curtain)
- ->setMainColumn(array(
- $output,
- $thumb_grid,
- $details,
- $timeline,
- $add_comment,
- ));
+ ->setMainColumn(
+ array(
+ $output,
+ $thumb_grid,
+ $details,
+ $timeline,
+ $add_comment,
+ ));
return $this->newPage()
- ->setTitle('M'.$mock->getID().' '.$title)
+ ->setTitle(pht('%s %s', $mock->getMonogram(), $title))
->setCrumbs($crumbs)
->setPageObjectPHIDs(array($mock->getPHID()))
->addQuicksandConfig(
array('mockViewConfig' => $mock_view->getBehaviorConfig()))
->appendChild($view);
}
private function buildCurtainView(PholioMock $mock) {
$viewer = $this->getViewer();
$curtain = $this->newCurtainView($mock);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$mock,
PhabricatorPolicyCapability::CAN_EDIT);
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit Mock'))
->setHref($this->getApplicationURI('/edit/'.$mock->getID().'/'))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
if ($mock->isClosed()) {
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-check')
->setName(pht('Open Mock'))
->setHref($this->getApplicationURI('/archive/'.$mock->getID().'/'))
->setDisabled(!$can_edit)
->setWorkflow(true));
} else {
$curtain->addAction(
id(new PhabricatorActionView())
->setIcon('fa-ban')
->setName(pht('Close Mock'))
->setHref($this->getApplicationURI('/archive/'.$mock->getID().'/'))
->setDisabled(!$can_edit)
->setWorkflow(true));
}
$relationship_list = PhabricatorObjectRelationshipList::newForObject(
$viewer,
$mock);
$relationship_submenu = $relationship_list->newActionMenu();
if ($relationship_submenu) {
$curtain->addAction($relationship_submenu);
}
if ($this->getManiphestTaskPHIDs()) {
$curtain->newPanel()
->setHeaderText(pht('Maniphest Tasks'))
->appendChild(
$viewer->renderHandleList($this->getManiphestTaskPHIDs()));
}
$curtain->newPanel()
->setHeaderText(pht('Authored By'))
->appendChild($this->buildAuthorPanel($mock));
return $curtain;
}
private function buildDescriptionView(PholioMock $mock) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setUser($viewer);
$description = $mock->getDescription();
if (strlen($description)) {
$properties->addTextContent(
new PHUIRemarkupView($viewer, $description));
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Mock Description'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($properties);
}
return null;
}
private function buildAuthorPanel(PholioMock $mock) {
$viewer = $this->getViewer();
$author_phid = $mock->getAuthorPHID();
$handles = $viewer->loadHandles(array($author_phid));
$author_uri = $handles[$author_phid]->getImageURI();
$author_href = $handles[$author_phid]->getURI();
$author = $viewer->renderHandle($author_phid)->render();
$content = phutil_tag('strong', array(), $author);
$date = phabricator_date($mock->getDateCreated(), $viewer);
$content = pht('%s, %s', $content, $date);
$authored_by = id(new PHUIHeadThingView())
->setImage($author_uri)
->setImageHref($author_href)
->setContent($content);
return $authored_by;
}
private function buildAddCommentView(PholioMock $mock, $comment_form_id) {
$viewer = $this->getViewer();
$draft = PhabricatorDraft::newFromUserAndKey($viewer, $mock->getPHID());
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
$title = $is_serious
? pht('Add Comment')
: pht('History Beckons');
$form = id(new PhabricatorApplicationTransactionCommentView())
->setUser($viewer)
->setObjectPHID($mock->getPHID())
->setFormID($comment_form_id)
->setDraft($draft)
->setHeaderText($title)
->setSubmitButtonName(pht('Add Comment'))
->setAction($this->getApplicationURI('/comment/'.$mock->getID().'/'))
->setRequestURI($this->getRequest()->getRequestURI());
return $form;
}
}
diff --git a/src/applications/pholio/editor/PholioMockEditor.php b/src/applications/pholio/editor/PholioMockEditor.php
index d1f7daf3a..df5d55672 100644
--- a/src/applications/pholio/editor/PholioMockEditor.php
+++ b/src/applications/pholio/editor/PholioMockEditor.php
@@ -1,278 +1,244 @@
<?php
final class PholioMockEditor extends PhabricatorApplicationTransactionEditor {
- private $newImages = array();
+ private $images = array();
public function getEditorApplicationClass() {
return 'PhabricatorPholioApplication';
}
public function getEditorObjectsDescription() {
return pht('Pholio Mocks');
}
- private function setNewImages(array $new_images) {
- assert_instances_of($new_images, 'PholioImage');
- $this->newImages = $new_images;
- return $this;
- }
-
- public function getNewImages() {
- return $this->newImages;
- }
-
public function getCreateObjectTitle($author, $object) {
return pht('%s created this mock.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created %s.', $author, $object);
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
- protected function shouldApplyInitialEffects(
- PhabricatorLiskDAO $object,
- array $xactions) {
-
- foreach ($xactions as $xaction) {
- switch ($xaction->getTransactionType()) {
- case PholioImageFileTransaction::TRANSACTIONTYPE:
- case PholioImageReplaceTransaction::TRANSACTIONTYPE:
- return true;
- break;
- }
- }
- return false;
- }
-
- protected function applyInitialEffects(
- PhabricatorLiskDAO $object,
- array $xactions) {
-
- $new_images = array();
- foreach ($xactions as $xaction) {
- switch ($xaction->getTransactionType()) {
- case PholioImageFileTransaction::TRANSACTIONTYPE:
- $new_value = $xaction->getNewValue();
- foreach ($new_value as $key => $txn_images) {
- if ($key != '+') {
- continue;
- }
- foreach ($txn_images as $image) {
- $image->save();
- $new_images[] = $image;
- }
- }
- break;
- case PholioImageReplaceTransaction::TRANSACTIONTYPE:
- $image = $xaction->getNewValue();
- $image->save();
- $new_images[] = $image;
- break;
- }
- }
- $this->setNewImages($new_images);
- }
-
- protected function applyFinalEffects(
- PhabricatorLiskDAO $object,
- array $xactions) {
-
- $images = $this->getNewImages();
- foreach ($images as $image) {
- $image->setMockID($object->getID());
- $image->save();
- }
-
- return $xactions;
- }
-
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new PholioReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
- $id = $object->getID();
+ $monogram = $object->getMonogram();
$name = $object->getName();
return id(new PhabricatorMetaMTAMail())
- ->setSubject("M{$id}: {$name}");
+ ->setSubject("{$monogram}: {$name}");
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$object->getAuthorPHID(),
$this->requireActor()->getPHID(),
);
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$viewer = $this->requireActor();
$body = id(new PhabricatorMetaMTAMailBody())
->setViewer($viewer);
$mock_uri = $object->getURI();
$mock_uri = PhabricatorEnv::getProductionURI($mock_uri);
$this->addHeadersAndCommentsToMailBody(
$body,
$xactions,
pht('View Mock'),
$mock_uri);
$type_inline = PholioMockInlineTransaction::TRANSACTIONTYPE;
$inlines = array();
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == $type_inline) {
$inlines[] = $xaction;
}
}
$this->appendInlineCommentsForMail($object, $inlines, $body);
$body->addLinkSection(
pht('MOCK DETAIL'),
- PhabricatorEnv::getProductionURI('/M'.$object->getID()));
+ PhabricatorEnv::getProductionURI($object->getURI()));
return $body;
}
private function appendInlineCommentsForMail(
$object,
array $inlines,
PhabricatorMetaMTAMailBody $body) {
if (!$inlines) {
return;
}
$viewer = $this->requireActor();
$header = pht('INLINE COMMENTS');
$body->addRawPlaintextSection($header);
$body->addRawHTMLSection(phutil_tag('strong', array(), $header));
$image_ids = array();
foreach ($inlines as $inline) {
$comment = $inline->getComment();
$image_id = $comment->getImageID();
$image_ids[$image_id] = $image_id;
}
$images = id(new PholioImageQuery())
->setViewer($viewer)
->withIDs($image_ids)
->execute();
$images = mpull($images, null, 'getID');
foreach ($inlines as $inline) {
$comment = $inline->getComment();
$content = $comment->getContent();
$image_id = $comment->getImageID();
$image = idx($images, $image_id);
if ($image) {
$image_name = $image->getName();
} else {
$image_name = pht('Unknown (ID %d)', $image_id);
}
$body->addRemarkupSection(
pht('Image "%s":', $image_name),
$content);
}
}
protected function getMailSubjectPrefix() {
- return PhabricatorEnv::getEnvConfig('metamta.pholio.subject-prefix');
+ return pht('[Pholio]');
}
public function getMailTagsMap() {
return array(
PholioTransaction::MAILTAG_STATUS =>
pht("A mock's status changes."),
PholioTransaction::MAILTAG_COMMENT =>
pht('Someone comments on a mock.'),
PholioTransaction::MAILTAG_UPDATED =>
pht('Mock images or descriptions change.'),
PholioTransaction::MAILTAG_OTHER =>
pht('Other mock activity not listed above occurs.'),
);
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function supportsSearch() {
return true;
}
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
return id(new HeraldPholioMockAdapter())
->setMock($object);
}
protected function sortTransactions(array $xactions) {
$head = array();
$tail = array();
// Move inline comments to the end, so the comments precede them.
foreach ($xactions as $xaction) {
$type = $xaction->getTransactionType();
if ($type == PholioMockInlineTransaction::TRANSACTIONTYPE) {
$tail[] = $xaction;
} else {
$head[] = $xaction;
}
}
return array_values(array_merge($head, $tail));
}
protected function shouldImplyCC(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PholioMockInlineTransaction::TRANSACTIONTYPE:
return true;
}
return parent::shouldImplyCC($object, $xaction);
}
+ public function loadPholioImage($object, $phid) {
+ if (!isset($this->images[$phid])) {
+
+ $image = id(new PholioImageQuery())
+ ->setViewer($this->getActor())
+ ->withPHIDs(array($phid))
+ ->executeOne();
+
+ if (!$image) {
+ throw new Exception(
+ pht(
+ 'No image exists with PHID "%s".',
+ $phid));
+ }
+
+ $mock_phid = $image->getMockPHID();
+ if ($mock_phid) {
+ if ($mock_phid !== $object->getPHID()) {
+ throw new Exception(
+ pht(
+ 'Image ("%s") belongs to the wrong object ("%s", expected "%s").',
+ $phid,
+ $mock_phid,
+ $object->getPHID()));
+ }
+ }
+
+ $this->images[$phid] = $image;
+ }
+
+ return $this->images[$phid];
+ }
+
}
diff --git a/src/applications/pholio/engine/PholioMockTimelineEngine.php b/src/applications/pholio/engine/PholioMockTimelineEngine.php
new file mode 100644
index 000000000..8543649e6
--- /dev/null
+++ b/src/applications/pholio/engine/PholioMockTimelineEngine.php
@@ -0,0 +1,22 @@
+<?php
+
+final class PholioMockTimelineEngine
+ extends PhabricatorTimelineEngine {
+
+ protected function newTimelineView() {
+ $viewer = $this->getViewer();
+ $object = $this->getObject();
+
+ $images = id(new PholioImageQuery())
+ ->setViewer($viewer)
+ ->withMocks(array($object))
+ ->needInlineComments(true)
+ ->execute();
+
+ $object->attachImages($images);
+
+ return id(new PholioTransactionView())
+ ->setMock($object);
+ }
+
+}
diff --git a/src/applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php b/src/applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php
index 039b0ddee..b97a5fc3c 100644
--- a/src/applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php
+++ b/src/applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php
@@ -1,119 +1,121 @@
<?php
final class PhabricatorPholioMockTestDataGenerator
extends PhabricatorTestDataGenerator {
const GENERATORKEY = 'mocks';
public function getGeneratorName() {
return pht('Pholio Mocks');
}
public function generateObject() {
$author_phid = $this->loadPhabricatorUserPHID();
$author = id(new PhabricatorUser())
->loadOneWhere('phid = %s', $author_phid);
$mock = PholioMock::initializeNewMock($author);
$content_source = $this->getLipsumContentSource();
$template = id(new PholioTransaction())
->setContentSource($content_source);
// Accumulate Transactions
$changes = array();
$changes[PholioMockNameTransaction::TRANSACTIONTYPE] =
$this->generateTitle();
$changes[PholioMockDescriptionTransaction::TRANSACTIONTYPE] =
$this->generateDescription();
$changes[PhabricatorTransactions::TYPE_VIEW_POLICY] =
PhabricatorPolicies::POLICY_PUBLIC;
$changes[PhabricatorTransactions::TYPE_SUBSCRIBERS] =
array('=' => $this->getCCPHIDs());
// Get Files and make Images
$file_phids = $this->generateImages();
$files = id(new PhabricatorFileQuery())
->setViewer($author)
->withPHIDs($file_phids)
->execute();
$mock->setCoverPHID(head($files)->getPHID());
$sequence = 0;
$images = array();
foreach ($files as $file) {
- $image = new PholioImage();
- $image->setFilePHID($file->getPHID());
- $image->setSequence($sequence++);
- $image->attachMock($mock);
+ $image = PholioImage::initializeNewImage()
+ ->setAuthorPHID($author_phid)
+ ->setFilePHID($file->getPHID())
+ ->setSequence($sequence++)
+ ->attachMock($mock);
+
$images[] = $image;
}
// Apply Transactions
$transactions = array();
foreach ($changes as $type => $value) {
$transaction = clone $template;
$transaction->setTransactionType($type);
$transaction->setNewValue($value);
$transactions[] = $transaction;
}
$mock->openTransaction();
$editor = id(new PholioMockEditor())
->setContentSource($content_source)
->setContinueOnNoEffect(true)
->setActor($author)
->applyTransactions($mock, $transactions);
foreach ($images as $image) {
- $image->setMockID($mock->getID());
+ $image->setMockPHID($mock->getPHID());
$image->save();
}
$mock->saveTransaction();
return $mock->save();
}
public function generateTitle() {
return id(new PhutilLipsumContextFreeGrammar())
->generate();
}
public function generateDescription() {
return id(new PhutilLipsumContextFreeGrammar())
->generateSeveral(rand(30, 40));
}
public function getCCPHIDs() {
$ccs = array();
for ($i = 0; $i < rand(1, 4);$i++) {
$ccs[] = $this->loadPhabricatorUserPHID();
}
return $ccs;
}
public function generateImages() {
$images = newv('PhabricatorFile', array())
->loadAllWhere('mimeType = %s', 'image/jpeg');
$rand_images = array();
$quantity = rand(2, 10);
$quantity = min($quantity, count($images));
if ($quantity) {
$random_images = $quantity === 1 ?
array(array_rand($images, $quantity)) :
array_rand($images, $quantity);
foreach ($random_images as $random) {
$rand_images[] = $images[$random]->getPHID();
}
}
// This means you don't have any JPEGs yet. We'll just use a built-in image.
if (empty($rand_images)) {
$default = PhabricatorFile::loadBuiltin(
PhabricatorUser::getOmnipotentUser(),
'profile.png');
$rand_images[] = $default->getPHID();
}
return $rand_images;
}
}
diff --git a/src/applications/pholio/mail/PholioMockMailReceiver.php b/src/applications/pholio/mail/PholioMockMailReceiver.php
index 09c0eb305..13fdc559e 100644
--- a/src/applications/pholio/mail/PholioMockMailReceiver.php
+++ b/src/applications/pholio/mail/PholioMockMailReceiver.php
@@ -1,27 +1,27 @@
<?php
final class PholioMockMailReceiver extends PhabricatorObjectMailReceiver {
public function isEnabled() {
$app_class = 'PhabricatorPholioApplication';
return PhabricatorApplication::isClassInstalled($app_class);
}
protected function getObjectPattern() {
return 'M[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
- $id = (int)trim($pattern, 'M');
+ $id = (int)substr($pattern, 1);
return id(new PholioMockQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
}
protected function getTransactionReplyHandler() {
return new PholioReplyHandler();
}
}
diff --git a/src/applications/pholio/phid/PholioImagePHIDType.php b/src/applications/pholio/phid/PholioImagePHIDType.php
index b28dbd64d..d7a9984fd 100644
--- a/src/applications/pholio/phid/PholioImagePHIDType.php
+++ b/src/applications/pholio/phid/PholioImagePHIDType.php
@@ -1,45 +1,41 @@
<?php
final class PholioImagePHIDType extends PhabricatorPHIDType {
const TYPECONST = 'PIMG';
public function getTypeName() {
return pht('Image');
}
public function newObject() {
return new PholioImage();
}
public function getPHIDTypeApplicationClass() {
return 'PhabricatorPholioApplication';
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return id(new PholioImageQuery())
->withPHIDs($phids);
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
foreach ($handles as $phid => $handle) {
$image = $objects[$phid];
- $id = $image->getID();
- $mock_id = $image->getMockID();
- $name = $image->getName();
-
- $handle->setURI("/M{$mock_id}/{$id}/");
- $handle->setName($name);
- $handle->setFullName($name);
+ $handle
+ ->setName($image->getName())
+ ->setURI($image->getURI());
}
}
}
diff --git a/src/applications/pholio/query/PholioImageQuery.php b/src/applications/pholio/query/PholioImageQuery.php
index 79ffdc56d..0d64540f9 100644
--- a/src/applications/pholio/query/PholioImageQuery.php
+++ b/src/applications/pholio/query/PholioImageQuery.php
@@ -1,168 +1,162 @@
<?php
final class PholioImageQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
- private $mockIDs;
- private $obsolete;
+ private $mockPHIDs;
+ private $mocks;
private $needInlineComments;
- private $mockCache = array();
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
- public function withMockIDs(array $mock_ids) {
- $this->mockIDs = $mock_ids;
+ public function withMocks(array $mocks) {
+ assert_instances_of($mocks, 'PholioMock');
+
+ $mocks = mpull($mocks, null, 'getPHID');
+ $this->mocks = $mocks;
+ $this->mockPHIDs = array_keys($mocks);
+
return $this;
}
- public function withObsolete($obsolete) {
- $this->obsolete = $obsolete;
+ public function withMockPHIDs(array $mock_phids) {
+ $this->mockPHIDs = $mock_phids;
return $this;
}
public function needInlineComments($need_inline_comments) {
$this->needInlineComments = $need_inline_comments;
return $this;
}
- public function setMockCache($mock_cache) {
- $this->mockCache = $mock_cache;
- return $this;
- }
- public function getMockCache() {
- return $this->mockCache;
+ public function newResultObject() {
+ return new PholioImage();
}
protected function loadPage() {
- $table = new PholioImage();
- $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));
-
- $images = $table->loadAllFromArray($data);
-
- return $images;
+ return $this->loadStandardPage($this->newResultObject());
}
- protected function buildWhereClause(AphrontDatabaseConnection $conn) {
- $where = array();
-
- $where[] = $this->buildPagingClause($conn);
+ protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
+ $where = parent::buildWhereClauseParts($conn);
- if ($this->ids) {
+ if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
- if ($this->phids) {
+ if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
- if ($this->mockIDs) {
- $where[] = qsprintf(
- $conn,
- 'mockID IN (%Ld)',
- $this->mockIDs);
- }
-
- if ($this->obsolete !== null) {
+ if ($this->mockPHIDs !== null) {
$where[] = qsprintf(
$conn,
- 'isObsolete = %d',
- $this->obsolete);
+ 'mockPHID IN (%Ls)',
+ $this->mockPHIDs);
}
- return $this->formatWhereClause($conn, $where);
+ return $where;
}
protected function willFilterPage(array $images) {
assert_instances_of($images, 'PholioImage');
- if ($this->getMockCache()) {
- $mocks = $this->getMockCache();
- } else {
- $mock_ids = mpull($images, 'getMockID');
- // DO NOT set needImages to true; recursion results!
- $mocks = id(new PholioMockQuery())
- ->setViewer($this->getViewer())
- ->withIDs($mock_ids)
- ->execute();
- $mocks = mpull($mocks, null, 'getID');
+ $mock_phids = array();
+ foreach ($images as $image) {
+ if (!$image->hasMock()) {
+ continue;
+ }
+
+ $mock_phids[] = $image->getMockPHID();
}
- foreach ($images as $index => $image) {
- $mock = idx($mocks, $image->getMockID());
- if ($mock) {
- $image->attachMock($mock);
+
+ if ($mock_phids) {
+ if ($this->mocks) {
+ $mocks = $this->mocks;
} else {
- // mock is missing or we can't see it
- unset($images[$index]);
+ $mocks = id(new PholioMockQuery())
+ ->setViewer($this->getViewer())
+ ->withPHIDs($mock_phids)
+ ->execute();
+ }
+
+ $mocks = mpull($mocks, null, 'getPHID');
+
+ foreach ($images as $key => $image) {
+ if (!$image->hasMock()) {
+ continue;
+ }
+
+ $mock = idx($mocks, $image->getMockPHID());
+ if (!$mock) {
+ unset($images[$key]);
+ $this->didRejectResult($image);
+ continue;
+ }
+
+ $image->attachMock($mock);
}
}
return $images;
}
protected function didFilterPage(array $images) {
assert_instances_of($images, 'PholioImage');
$file_phids = mpull($images, 'getFilePHID');
$all_files = id(new PhabricatorFileQuery())
->setParentQuery($this)
->setViewer($this->getViewer())
->withPHIDs($file_phids)
->execute();
$all_files = mpull($all_files, null, 'getPHID');
if ($this->needInlineComments) {
// Only load inline comments the viewer has permission to see.
$all_inline_comments = id(new PholioTransactionComment())->loadAllWhere(
'imageID IN (%Ld)
AND (transactionPHID IS NOT NULL OR authorPHID = %s)',
mpull($images, 'getID'),
$this->getViewer()->getPHID());
$all_inline_comments = mgroup($all_inline_comments, 'getImageID');
}
foreach ($images as $image) {
$file = idx($all_files, $image->getFilePHID());
if (!$file) {
$file = PhabricatorFile::loadBuiltin($this->getViewer(), 'missing.png');
}
$image->attachFile($file);
if ($this->needInlineComments) {
$inlines = idx($all_inline_comments, $image->getID(), array());
$image->attachInlineComments($inlines);
}
}
return $images;
}
public function getQueryApplicationClass() {
return 'PhabricatorPholioApplication';
}
}
diff --git a/src/applications/pholio/query/PholioMockQuery.php b/src/applications/pholio/query/PholioMockQuery.php
index 5f1711def..4820ffd8e 100644
--- a/src/applications/pholio/query/PholioMockQuery.php
+++ b/src/applications/pholio/query/PholioMockQuery.php
@@ -1,176 +1,162 @@
<?php
final class PholioMockQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $authorPHIDs;
private $statuses;
private $needCoverFiles;
private $needImages;
private $needInlineComments;
private $needTokenCounts;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withAuthorPHIDs(array $author_phids) {
$this->authorPHIDs = $author_phids;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function needCoverFiles($need_cover_files) {
$this->needCoverFiles = $need_cover_files;
return $this;
}
public function needImages($need_images) {
$this->needImages = $need_images;
return $this;
}
public function needInlineComments($need_inline_comments) {
$this->needInlineComments = $need_inline_comments;
return $this;
}
public function needTokenCounts($need) {
$this->needTokenCounts = $need;
return $this;
}
public function newResultObject() {
return new PholioMock();
}
protected function loadPage() {
- $mocks = $this->loadStandardPage(new PholioMock());
-
- if ($mocks && $this->needImages) {
- self::loadImages($this->getViewer(), $mocks, $this->needInlineComments);
- }
-
- if ($mocks && $this->needCoverFiles) {
- $this->loadCoverFiles($mocks);
- }
-
- if ($mocks && $this->needTokenCounts) {
- $this->loadTokenCounts($mocks);
+ if ($this->needInlineComments && !$this->needImages) {
+ throw new Exception(
+ pht(
+ 'You can not query for inline comments without also querying for '.
+ 'images.'));
}
- return $mocks;
+ return $this->loadStandardPage(new PholioMock());
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'mock.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'mock.phid IN (%Ls)',
$this->phids);
}
if ($this->authorPHIDs !== null) {
$where[] = qsprintf(
$conn,
'mock.authorPHID in (%Ls)',
$this->authorPHIDs);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'mock.status IN (%Ls)',
$this->statuses);
}
return $where;
}
- public static function loadImages(
- PhabricatorUser $viewer,
- array $mocks,
- $need_inline_comments) {
- assert_instances_of($mocks, 'PholioMock');
-
- $mock_map = mpull($mocks, null, 'getID');
- $all_images = id(new PholioImageQuery())
- ->setViewer($viewer)
- ->setMockCache($mock_map)
- ->withMockIDs(array_keys($mock_map))
- ->needInlineComments($need_inline_comments)
- ->execute();
-
- $image_groups = mgroup($all_images, 'getMockID');
-
- foreach ($mocks as $mock) {
- $mock_images = idx($image_groups, $mock->getID(), array());
- $mock->attachAllImages($mock_images);
- $active_images = mfilter($mock_images, 'getIsObsolete', true);
- $mock->attachImages(msort($active_images, 'getSequence'));
- }
- }
+ protected function didFilterPage(array $mocks) {
+ $viewer = $this->getViewer();
- private function loadCoverFiles(array $mocks) {
- assert_instances_of($mocks, 'PholioMock');
- $cover_file_phids = mpull($mocks, 'getCoverPHID');
- $cover_files = id(new PhabricatorFileQuery())
- ->setViewer($this->getViewer())
- ->withPHIDs($cover_file_phids)
- ->execute();
+ if ($this->needImages) {
+ $images = id(new PholioImageQuery())
+ ->setViewer($viewer)
+ ->withMocks($mocks)
+ ->needInlineComments($this->needInlineComments)
+ ->execute();
- $cover_files = mpull($cover_files, null, 'getPHID');
-
- foreach ($mocks as $mock) {
- $file = idx($cover_files, $mock->getCoverPHID());
- if (!$file) {
- $file = PhabricatorFile::loadBuiltin($this->getViewer(), 'missing.png');
+ $image_groups = mgroup($images, 'getMockPHID');
+ foreach ($mocks as $mock) {
+ $images = idx($image_groups, $mock->getPHID(), array());
+ $mock->attachImages($images);
}
- $mock->attachCoverFile($file);
}
- }
- private function loadTokenCounts(array $mocks) {
- assert_instances_of($mocks, 'PholioMock');
+ if ($this->needCoverFiles) {
+ $cover_files = id(new PhabricatorFileQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(mpull($mocks, 'getCoverPHID'))
+ ->execute();
+ $cover_files = mpull($cover_files, null, 'getPHID');
+
+ foreach ($mocks as $mock) {
+ $file = idx($cover_files, $mock->getCoverPHID());
+ if (!$file) {
+ $file = PhabricatorFile::loadBuiltin(
+ $viewer,
+ 'missing.png');
+ }
+ $mock->attachCoverFile($file);
+ }
+ }
- $phids = mpull($mocks, 'getPHID');
- $counts = id(new PhabricatorTokenCountQuery())
- ->withObjectPHIDs($phids)
- ->execute();
+ if ($this->needTokenCounts) {
+ $counts = id(new PhabricatorTokenCountQuery())
+ ->withObjectPHIDs(mpull($mocks, 'getPHID'))
+ ->execute();
- foreach ($mocks as $mock) {
- $mock->attachTokenCount(idx($counts, $mock->getPHID(), 0));
+ foreach ($mocks as $mock) {
+ $token_count = idx($counts, $mock->getPHID(), 0);
+ $mock->attachTokenCount($token_count);
+ }
}
+
+ return $mocks;
}
public function getQueryApplicationClass() {
return 'PhabricatorPholioApplication';
}
protected function getPrimaryTableAlias() {
return 'mock';
}
}
diff --git a/src/applications/pholio/query/PholioMockSearchEngine.php b/src/applications/pholio/query/PholioMockSearchEngine.php
index 2433484d6..cbb175b2d 100644
--- a/src/applications/pholio/query/PholioMockSearchEngine.php
+++ b/src/applications/pholio/query/PholioMockSearchEngine.php
@@ -1,153 +1,153 @@
<?php
final class PholioMockSearchEngine extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Pholio Mocks');
}
public function getApplicationClassName() {
return 'PhabricatorPholioApplication';
}
public function newQuery() {
return id(new PholioMockQuery())
->needCoverFiles(true)
->needImages(true)
->needTokenCounts(true);
}
protected function buildCustomSearchFields() {
return array(
id(new PhabricatorUsersSearchField())
->setKey('authorPHIDs')
->setAliases(array('authors'))
->setLabel(pht('Authors')),
id(new PhabricatorSearchCheckboxesField())
->setKey('statuses')
->setLabel(pht('Status'))
->setOptions(
id(new PholioMock())
->getStatuses()),
);
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
if ($map['authorPHIDs']) {
$query->withAuthorPHIDs($map['authorPHIDs']);
}
if ($map['statuses']) {
$query->withStatuses($map['statuses']);
}
return $query;
}
protected function getURI($path) {
return '/pholio/'.$path;
}
protected function getBuiltinQueryNames() {
$names = array(
'open' => pht('Open Mocks'),
'all' => pht('All Mocks'),
);
if ($this->requireViewer()->isLoggedIn()) {
$names['authored'] = pht('Authored');
}
return $names;
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
switch ($query_key) {
case 'open':
return $query->setParameter(
'statuses',
array('open'));
case 'all':
return $query;
case 'authored':
return $query->setParameter(
'authorPHIDs',
array($this->requireViewer()->getPHID()));
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
protected function renderResultList(
array $mocks,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($mocks, 'PholioMock');
$viewer = $this->requireViewer();
$handles = $viewer->loadHandles(mpull($mocks, 'getAuthorPHID'));
$xform = PhabricatorFileTransform::getTransformByKey(
PhabricatorFileThumbnailTransform::TRANSFORM_PINBOARD);
$board = new PHUIPinboardView();
foreach ($mocks as $mock) {
$image = $mock->getCoverFile();
$image_uri = $image->getURIForTransform($xform);
list($x, $y) = $xform->getTransformedDimensions($image);
$header = 'M'.$mock->getID().' '.$mock->getName();
$item = id(new PHUIPinboardItemView())
->setUser($viewer)
->setHeader($header)
->setObject($mock)
->setURI('/M'.$mock->getID())
->setImageURI($image_uri)
->setImageSize($x, $y)
->setDisabled($mock->isClosed())
- ->addIconCount('fa-picture-o', count($mock->getImages()))
+ ->addIconCount('fa-picture-o', count($mock->getActiveImages()))
->addIconCount('fa-trophy', $mock->getTokenCount());
if ($mock->getAuthorPHID()) {
$author_handle = $handles[$mock->getAuthorPHID()];
$datetime = phabricator_date($mock->getDateCreated(), $viewer);
$item->appendChild(
pht('By %s on %s', $author_handle->renderLink(), $datetime));
}
$board->addItem($item);
}
$result = new PhabricatorApplicationSearchResultView();
$result->setContent($board);
return $result;
}
protected function getNewUserBody() {
$create_button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('Create a Mock'))
->setHref('/pholio/create/')
->setColor(PHUIButtonView::GREEN);
$icon = $this->getApplication()->getIcon();
$app_name = $this->getApplication()->getName();
$view = id(new PHUIBigInfoView())
->setIcon($icon)
->setTitle(pht('Welcome to %s', $app_name))
->setDescription(
pht('Upload sets of images for review with revision history and '.
'inline comments.'))
->addAction($create_button);
return $view;
}
}
diff --git a/src/applications/pholio/storage/PholioImage.php b/src/applications/pholio/storage/PholioImage.php
index 70f6e8b8c..782f4d0d4 100644
--- a/src/applications/pholio/storage/PholioImage.php
+++ b/src/applications/pholio/storage/PholioImage.php
@@ -1,122 +1,143 @@
<?php
final class PholioImage extends PholioDAO
implements
- PhabricatorMarkupInterface,
- PhabricatorPolicyInterface {
+ PhabricatorPolicyInterface,
+ PhabricatorExtendedPolicyInterface {
- const MARKUP_FIELD_DESCRIPTION = 'markup:description';
-
- protected $mockID;
+ protected $authorPHID;
+ protected $mockPHID;
protected $filePHID;
- protected $name = '';
- protected $description = '';
+ protected $name;
+ protected $description;
protected $sequence;
- protected $isObsolete = 0;
+ protected $isObsolete;
protected $replacesImagePHID = null;
private $inlineComments = self::ATTACHABLE;
private $file = self::ATTACHABLE;
private $mock = self::ATTACHABLE;
+ public static function initializeNewImage() {
+ return id(new self())
+ ->setName('')
+ ->setDescription('')
+ ->setIsObsolete(0);
+ }
+
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
- 'mockID' => 'id?',
+ 'mockPHID' => 'phid?',
'name' => 'text128',
'description' => 'text',
'sequence' => 'uint32',
'isObsolete' => 'bool',
'replacesImagePHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
- 'key_phid' => null,
- 'keyPHID' => array(
- 'columns' => array('phid'),
- 'unique' => true,
- ),
- 'mockID' => array(
- 'columns' => array('mockID', 'isObsolete', 'sequence'),
+ 'key_mock' => array(
+ 'columns' => array('mockPHID'),
),
),
) + parent::getConfiguration();
}
- public function generatePHID() {
- return PhabricatorPHID::generateNewPHID(PholioImagePHIDType::TYPECONST);
+ public function getPHIDType() {
+ return PholioImagePHIDType::TYPECONST;
}
public function attachFile(PhabricatorFile $file) {
$this->file = $file;
return $this;
}
public function getFile() {
- $this->assertAttached($this->file);
- return $this->file;
+ return $this->assertAttached($this->file);
}
public function attachMock(PholioMock $mock) {
$this->mock = $mock;
return $this;
}
public function getMock() {
- $this->assertAttached($this->mock);
- return $this->mock;
+ return $this->assertAttached($this->mock);
}
+ public function hasMock() {
+ return (bool)$this->getMockPHID();
+ }
public function attachInlineComments(array $inline_comments) {
assert_instances_of($inline_comments, 'PholioTransactionComment');
$this->inlineComments = $inline_comments;
return $this;
}
public function getInlineComments() {
$this->assertAttached($this->inlineComments);
return $this->inlineComments;
}
+ public function getURI() {
+ if ($this->hasMock()) {
+ $mock = $this->getMock();
-/* -( PhabricatorMarkupInterface )----------------------------------------- */
+ $mock_uri = $mock->getURI();
+ $image_id = $this->getID();
+ return "{$mock_uri}/{$image_id}/";
+ }
- public function getMarkupFieldKey($field) {
- $content = $this->getMarkupText($field);
- return PhabricatorMarkupEngine::digestRemarkupContent($this, $content);
- }
+ // For now, standalone images have no URI. We could provide one at some
+ // point, although it's not clear that there's any motivation to do so.
- public function newMarkupEngine($field) {
- return PhabricatorMarkupEngine::newMarkupEngine(array());
+ return null;
}
- public function getMarkupText($field) {
- return $this->getDescription();
- }
- public function didMarkupText($field, $output, PhutilMarkupEngine $engine) {
- return $output;
- }
-
- public function shouldUseMarkupCache($field) {
- return (bool)$this->getID();
- }
+/* -( PhabricatorPolicyInterface )----------------------------------------- */
-/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
public function getCapabilities() {
- return $this->getMock()->getCapabilities();
+ return array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ );
}
public function getPolicy($capability) {
- return $this->getMock()->getPolicy($capability);
+ // If the image is attached to a mock, we use an extended policy to match
+ // the mock's permissions.
+ if ($this->hasMock()) {
+ return PhabricatorPolicies::getMostOpenPolicy();
+ }
+
+ // If the image is not attached to a mock, only the author can see it.
+ return $this->getAuthorPHID();
}
- // really the *mock* controls who can see an image
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
- return $this->getMock()->hasAutomaticCapability($capability, $viewer);
+ return false;
+ }
+
+
+/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
+
+
+ public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
+ if ($this->hasMock()) {
+ return array(
+ array(
+ $this->getMock(),
+ $capability,
+ ),
+ );
+ }
+
+ return array();
}
}
diff --git a/src/applications/pholio/storage/PholioMock.php b/src/applications/pholio/storage/PholioMock.php
index 569513cb4..25b216ba5 100644
--- a/src/applications/pholio/storage/PholioMock.php
+++ b/src/applications/pholio/storage/PholioMock.php
@@ -1,333 +1,271 @@
<?php
final class PholioMock extends PholioDAO
implements
- PhabricatorMarkupInterface,
PhabricatorPolicyInterface,
PhabricatorSubscribableInterface,
PhabricatorTokenReceiverInterface,
PhabricatorFlaggableInterface,
PhabricatorApplicationTransactionInterface,
+ PhabricatorTimelineInterface,
PhabricatorProjectInterface,
PhabricatorDestructibleInterface,
PhabricatorSpacesInterface,
PhabricatorMentionableInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface {
- const MARKUP_FIELD_DESCRIPTION = 'markup:description';
-
const STATUS_OPEN = 'open';
const STATUS_CLOSED = 'closed';
protected $authorPHID;
protected $viewPolicy;
protected $editPolicy;
protected $name;
protected $description;
protected $coverPHID;
- protected $mailKey;
protected $status;
protected $spacePHID;
private $images = self::ATTACHABLE;
- private $allImages = self::ATTACHABLE;
private $coverFile = self::ATTACHABLE;
private $tokenCount = self::ATTACHABLE;
public static function initializeNewMock(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorPholioApplication'))
->executeOne();
$view_policy = $app->getPolicy(PholioDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(PholioDefaultEditCapability::CAPABILITY);
return id(new PholioMock())
->setAuthorPHID($actor->getPHID())
->attachImages(array())
->setStatus(self::STATUS_OPEN)
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setSpacePHID($actor->getDefaultSpacePHID());
}
public function getMonogram() {
return 'M'.$this->getID();
}
public function getURI() {
return '/'.$this->getMonogram();
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text128',
'description' => 'text',
- 'mailKey' => 'bytes20',
'status' => 'text12',
),
self::CONFIG_KEY_SCHEMA => array(
- 'key_phid' => null,
- 'phid' => array(
- 'columns' => array('phid'),
- 'unique' => true,
- ),
'authorPHID' => array(
'columns' => array('authorPHID'),
),
),
) + parent::getConfiguration();
}
- public function generatePHID() {
- return PhabricatorPHID::generateNewPHID('MOCK');
- }
-
- public function save() {
- if (!$this->getMailKey()) {
- $this->setMailKey(Filesystem::readRandomCharacters(20));
- }
- return parent::save();
+ public function getPHIDType() {
+ return PholioMockPHIDType::TYPECONST;
}
- /**
- * These should be the images currently associated with the Mock.
- */
public function attachImages(array $images) {
assert_instances_of($images, 'PholioImage');
+ $images = mpull($images, null, 'getPHID');
+ $images = msort($images, 'getSequence');
$this->images = $images;
return $this;
}
public function getImages() {
- $this->assertAttached($this->images);
- return $this->images;
+ return $this->assertAttached($this->images);
}
- /**
- * These should be *all* images associated with the Mock. This includes
- * images which have been removed and / or replaced from the Mock.
- */
- public function attachAllImages(array $images) {
- assert_instances_of($images, 'PholioImage');
- $this->allImages = $images;
- return $this;
- }
+ public function getActiveImages() {
+ $images = $this->getImages();
- public function getAllImages() {
- $this->assertAttached($this->images);
- return $this->allImages;
+ foreach ($images as $phid => $image) {
+ if ($image->getIsObsolete()) {
+ unset($images[$phid]);
+ }
+ }
+
+ return $images;
}
public function attachCoverFile(PhabricatorFile $file) {
$this->coverFile = $file;
return $this;
}
public function getCoverFile() {
$this->assertAttached($this->coverFile);
return $this->coverFile;
}
public function getTokenCount() {
$this->assertAttached($this->tokenCount);
return $this->tokenCount;
}
public function attachTokenCount($count) {
$this->tokenCount = $count;
return $this;
}
public function getImageHistorySet($image_id) {
- $images = $this->getAllImages();
+ $images = $this->getImages();
$images = mpull($images, null, 'getID');
$selected_image = $images[$image_id];
$replace_map = mpull($images, null, 'getReplacesImagePHID');
$phid_map = mpull($images, null, 'getPHID');
// find the earliest image
$image = $selected_image;
while (isset($phid_map[$image->getReplacesImagePHID()])) {
$image = $phid_map[$image->getReplacesImagePHID()];
}
// now build history moving forward
$history = array($image->getID() => $image);
while (isset($replace_map[$image->getPHID()])) {
$image = $replace_map[$image->getPHID()];
$history[$image->getID()] = $image;
}
return $history;
}
public function getStatuses() {
$options = array();
$options[self::STATUS_OPEN] = pht('Open');
$options[self::STATUS_CLOSED] = pht('Closed');
return $options;
}
public function isClosed() {
return ($this->getStatus() == 'closed');
}
/* -( 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 $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return ($viewer->getPHID() == $this->getAuthorPHID());
}
public function describeAutomaticCapability($capability) {
return pht("A mock's owner can always view and edit it.");
}
-/* -( PhabricatorMarkupInterface )----------------------------------------- */
-
-
- public function getMarkupFieldKey($field) {
- $content = $this->getMarkupText($field);
- return PhabricatorMarkupEngine::digestRemarkupContent($this, $content);
- }
-
- public function newMarkupEngine($field) {
- return PhabricatorMarkupEngine::newMarkupEngine(array());
- }
-
- public function getMarkupText($field) {
- if ($this->getDescription()) {
- return $this->getDescription();
- }
-
- return null;
- }
-
- public function didMarkupText($field, $output, PhutilMarkupEngine $engine) {
- require_celerity_resource('phabricator-remarkup-css');
- return phutil_tag(
- 'div',
- array(
- 'class' => 'phabricator-remarkup',
- ),
- $output);
- }
-
- public function shouldUseMarkupCache($field) {
- return (bool)$this->getID();
- }
-
-
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PholioMockEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PholioTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- PholioMockQuery::loadImages(
- $request->getUser(),
- array($this),
- $need_inline_comments = true);
- $timeline->setMock($this);
- return $timeline;
- }
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getAuthorPHID(),
);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
- $images = id(new PholioImage())->loadAllWhere(
- 'mockID = %d',
- $this->getID());
+ $images = id(new PholioImageQuery())
+ ->setViewer($engine->getViewer())
+ ->withMockIDs(array($this->getPHID()))
+ ->execute();
foreach ($images as $image) {
$image->delete();
}
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorSpacesInterface )----------------------------------------- */
public function getSpacePHID() {
return $this->spacePHID;
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PholioMockFulltextEngine();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
+
public function newFerretEngine() {
return new PholioMockFerretEngine();
}
+/* -( PhabricatorTimelineInterace )---------------------------------------- */
+
+
+ public function newTimelineEngine() {
+ return new PholioMockTimelineEngine();
+ }
+
+
}
diff --git a/src/applications/pholio/storage/PholioTransaction.php b/src/applications/pholio/storage/PholioTransaction.php
index 240b9d93e..e4656dc7f 100644
--- a/src/applications/pholio/storage/PholioTransaction.php
+++ b/src/applications/pholio/storage/PholioTransaction.php
@@ -1,65 +1,61 @@
<?php
final class PholioTransaction extends PhabricatorModularTransaction {
const MAILTAG_STATUS = 'pholio-status';
const MAILTAG_COMMENT = 'pholio-comment';
const MAILTAG_UPDATED = 'pholio-updated';
const MAILTAG_OTHER = 'pholio-other';
public function getApplicationName() {
return 'pholio';
}
public function getBaseTransactionClass() {
return 'PholioTransactionType';
}
public function getApplicationTransactionType() {
return PholioMockPHIDType::TYPECONST;
}
public function getApplicationTransactionCommentObject() {
return new PholioTransactionComment();
}
- public function getApplicationTransactionViewObject() {
- return new PholioTransactionView();
- }
-
public function getMailTags() {
$tags = array();
switch ($this->getTransactionType()) {
case PholioMockInlineTransaction::TRANSACTIONTYPE:
case PhabricatorTransactions::TYPE_COMMENT:
$tags[] = self::MAILTAG_COMMENT;
break;
case PholioMockStatusTransaction::TRANSACTIONTYPE:
$tags[] = self::MAILTAG_STATUS;
break;
case PholioMockNameTransaction::TRANSACTIONTYPE:
case PholioMockDescriptionTransaction::TRANSACTIONTYPE:
case PholioImageNameTransaction::TRANSACTIONTYPE:
case PholioImageDescriptionTransaction::TRANSACTIONTYPE:
case PholioImageSequenceTransaction::TRANSACTIONTYPE:
case PholioImageFileTransaction::TRANSACTIONTYPE:
case PholioImageReplaceTransaction::TRANSACTIONTYPE:
$tags[] = self::MAILTAG_UPDATED;
break;
default:
$tags[] = self::MAILTAG_OTHER;
break;
}
return $tags;
}
public function isInlineCommentTransaction() {
switch ($this->getTransactionType()) {
case PholioMockInlineTransaction::TRANSACTIONTYPE:
return true;
}
return parent::isInlineCommentTransaction();
}
}
diff --git a/src/applications/pholio/view/PholioMockEmbedView.php b/src/applications/pholio/view/PholioMockEmbedView.php
index 88f2f2ac5..38b375b68 100644
--- a/src/applications/pholio/view/PholioMockEmbedView.php
+++ b/src/applications/pholio/view/PholioMockEmbedView.php
@@ -1,63 +1,63 @@
<?php
final class PholioMockEmbedView extends AphrontView {
private $mock;
private $images = array();
public function setMock(PholioMock $mock) {
$this->mock = $mock;
return $this;
}
public function setImages(array $images) {
$this->images = $images;
return $this;
}
public function render() {
if (!$this->mock) {
throw new PhutilInvalidStateException('setMock');
}
$mock = $this->mock;
$images_to_show = array();
$thumbnail = null;
if (!empty($this->images)) {
$images_to_show = array_intersect_key(
- $this->mock->getImages(), array_flip($this->images));
+ $this->mock->getActiveImages(), array_flip($this->images));
}
$xform = PhabricatorFileTransform::getTransformByKey(
PhabricatorFileThumbnailTransform::TRANSFORM_PINBOARD);
if ($images_to_show) {
$image = head($images_to_show);
$thumbfile = $image->getFile();
$header = 'M'.$mock->getID().' '.$mock->getName().
' (#'.$image->getID().')';
$uri = '/M'.$this->mock->getID().'/'.$image->getID().'/';
} else {
$thumbfile = $mock->getCoverFile();
$header = 'M'.$mock->getID().' '.$mock->getName();
$uri = '/M'.$this->mock->getID();
}
$thumbnail = $thumbfile->getURIForTransform($xform);
list($x, $y) = $xform->getTransformedDimensions($thumbfile);
$item = id(new PHUIPinboardItemView())
->setUser($this->getUser())
->setObject($mock)
->setHeader($header)
->setURI($uri)
->setImageURI($thumbnail)
->setImageSize($x, $y)
->setDisabled($mock->isClosed())
- ->addIconCount('fa-picture-o', count($mock->getImages()))
+ ->addIconCount('fa-picture-o', count($mock->getActiveImages()))
->addIconCount('fa-trophy', $mock->getTokenCount());
return $item;
}
}
diff --git a/src/applications/pholio/view/PholioMockImagesView.php b/src/applications/pholio/view/PholioMockImagesView.php
index c95d8827d..99645c4a9 100644
--- a/src/applications/pholio/view/PholioMockImagesView.php
+++ b/src/applications/pholio/view/PholioMockImagesView.php
@@ -1,235 +1,223 @@
<?php
final class PholioMockImagesView extends AphrontView {
private $mock;
private $imageID;
private $requestURI;
private $commentFormID;
private $panelID;
private $viewportID;
private $behaviorConfig;
public function setCommentFormID($comment_form_id) {
$this->commentFormID = $comment_form_id;
return $this;
}
public function getCommentFormID() {
return $this->commentFormID;
}
public function setRequestURI(PhutilURI $request_uri) {
$this->requestURI = $request_uri;
return $this;
}
public function getRequestURI() {
return $this->requestURI;
}
public function setImageID($image_id) {
$this->imageID = $image_id;
return $this;
}
public function getImageID() {
return $this->imageID;
}
public function setMock(PholioMock $mock) {
$this->mock = $mock;
return $this;
}
public function getMock() {
return $this->mock;
}
public function __construct() {
$this->panelID = celerity_generate_unique_node_id();
$this->viewportID = celerity_generate_unique_node_id();
}
public function getBehaviorConfig() {
if (!$this->getMock()) {
throw new PhutilInvalidStateException('setMock');
}
if ($this->behaviorConfig === null) {
$this->behaviorConfig = $this->calculateBehaviorConfig();
}
return $this->behaviorConfig;
}
private function calculateBehaviorConfig() {
$mock = $this->getMock();
// TODO: We could maybe do a better job with tailoring this, which is the
// image shown on the review stage.
$viewer = $this->getUser();
$default = PhabricatorFile::loadBuiltin($viewer, 'image-100x100.png');
- $engine = id(new PhabricatorMarkupEngine())
- ->setViewer($this->getUser());
- foreach ($mock->getAllImages() as $image) {
- $engine->addObject($image, 'default');
- }
- $engine->process();
-
$images = array();
$current_set = 0;
- foreach ($mock->getAllImages() as $image) {
+ foreach ($mock->getImages() as $image) {
$file = $image->getFile();
$metadata = $file->getMetadata();
$x = idx($metadata, PhabricatorFile::METADATA_IMAGE_WIDTH);
$y = idx($metadata, PhabricatorFile::METADATA_IMAGE_HEIGHT);
$is_obs = (bool)$image->getIsObsolete();
if (!$is_obs) {
$current_set++;
}
- $description = $engine->getOutput($image, 'default');
+ $description = $image->getDescription();
if (strlen($description)) {
- $description = phutil_tag(
- 'div',
- array(
- 'class' => 'phabricator-remarkup',
- ),
- $description);
+ $description = new PHUIRemarkupView($viewer, $description);
}
$history_uri = '/pholio/image/history/'.$image->getID().'/';
$images[] = array(
'id' => $image->getID(),
'fullURI' => $file->getBestURI(),
'stageURI' => ($file->isViewableImage()
? $file->getBestURI()
: $default->getBestURI()),
'pageURI' => $this->getImagePageURI($image, $mock),
'downloadURI' => $file->getDownloadURI(),
'historyURI' => $history_uri,
'width' => $x,
'height' => $y,
'title' => $image->getName(),
'descriptionMarkup' => $description,
'isObsolete' => (bool)$image->getIsObsolete(),
'isImage' => $file->isViewableImage(),
'isViewable' => $file->isViewableInBrowser(),
);
}
- $ids = mpull($mock->getImages(), 'getID');
+ $ids = mpull($mock->getActiveImages(), null, 'getID');
if ($this->imageID && isset($ids[$this->imageID])) {
$selected_id = $this->imageID;
} else {
$selected_id = head_key($ids);
}
$navsequence = array();
- foreach ($mock->getImages() as $image) {
+ foreach ($mock->getActiveImages() as $image) {
$navsequence[] = $image->getID();
}
$full_icon = array(
javelin_tag('span', array('aural' => true), pht('View Raw File')),
id(new PHUIIconView())->setIcon('fa-file-image-o'),
);
$download_icon = array(
javelin_tag('span', array('aural' => true), pht('Download File')),
id(new PHUIIconView())->setIcon('fa-download'),
);
$login_uri = id(new PhutilURI('/login/'))
->setQueryParam('next', (string)$this->getRequestURI());
$config = array(
'mockID' => $mock->getID(),
'panelID' => $this->panelID,
'viewportID' => $this->viewportID,
'commentFormID' => $this->getCommentFormID(),
'images' => $images,
'selectedID' => $selected_id,
'loggedIn' => $this->getUser()->isLoggedIn(),
'logInLink' => (string)$login_uri,
'navsequence' => $navsequence,
'fullIcon' => hsprintf('%s', $full_icon),
'downloadIcon' => hsprintf('%s', $download_icon),
'currentSetSize' => $current_set,
);
return $config;
}
public function render() {
if (!$this->getMock()) {
throw new PhutilInvalidStateException('setMock');
}
$mock = $this->getMock();
require_celerity_resource('javelin-behavior-pholio-mock-view');
$panel_id = $this->panelID;
$viewport_id = $this->viewportID;
$config = $this->getBehaviorConfig();
Javelin::initBehavior(
'pholio-mock-view',
$this->getBehaviorConfig());
$mock_wrapper = javelin_tag(
'div',
array(
'id' => $this->viewportID,
'sigil' => 'mock-viewport',
'class' => 'pholio-mock-image-viewport',
),
'');
$image_header = javelin_tag(
'div',
array(
'id' => 'mock-image-header',
'class' => 'pholio-mock-image-header',
),
'');
$mock_wrapper = javelin_tag(
'div',
array(
'id' => $this->panelID,
'sigil' => 'mock-panel touchable',
'class' => 'pholio-mock-image-panel',
),
array(
$image_header,
$mock_wrapper,
));
$inline_comments_holder = javelin_tag(
'div',
array(
'id' => 'mock-image-description',
'sigil' => 'mock-image-description',
'class' => 'mock-image-description',
),
'');
return phutil_tag(
'div',
array(
'class' => 'pholio-mock-image-container',
'id' => 'pholio-mock-image-container',
),
array($mock_wrapper, $inline_comments_holder));
}
private function getImagePageURI(PholioImage $image, PholioMock $mock) {
$uri = '/M'.$mock->getID().'/'.$image->getID().'/';
return $uri;
}
}
diff --git a/src/applications/pholio/view/PholioMockThumbGridView.php b/src/applications/pholio/view/PholioMockThumbGridView.php
index 457d700f1..b859eaaa9 100644
--- a/src/applications/pholio/view/PholioMockThumbGridView.php
+++ b/src/applications/pholio/view/PholioMockThumbGridView.php
@@ -1,181 +1,181 @@
<?php
final class PholioMockThumbGridView extends AphrontView {
private $mock;
public function setMock(PholioMock $mock) {
$this->mock = $mock;
return $this;
}
public function render() {
$mock = $this->mock;
- $all_images = $mock->getAllImages();
+ $all_images = $mock->getImages();
$all_images = mpull($all_images, null, 'getPHID');
$history = mpull($all_images, 'getReplacesImagePHID', 'getPHID');
$replaced = array();
foreach ($history as $phid => $replaces_phid) {
if ($replaces_phid) {
$replaced[$replaces_phid] = true;
}
}
// Figure out the columns. Start with all the active images.
- $images = mpull($mock->getImages(), null, 'getPHID');
+ $images = mpull($mock->getActiveImages(), null, 'getPHID');
// Now, find deleted images: obsolete images which were not replaced.
- foreach ($mock->getAllImages() as $image) {
+ foreach ($mock->getImages() as $image) {
if (!$image->getIsObsolete()) {
// Image is current.
continue;
}
if (isset($replaced[$image->getPHID()])) {
// Image was replaced.
continue;
}
// This is an obsolete image which was not replaced, so it must be
// a deleted image.
$images[$image->getPHID()] = $image;
}
$cols = array();
$depth = 0;
foreach ($images as $image) {
$phid = $image->getPHID();
$col = array();
// If this is a deleted image, null out the final column.
if ($image->getIsObsolete()) {
$col[] = null;
}
$col[] = $phid;
while ($phid && isset($history[$phid])) {
$col[] = $history[$phid];
$phid = $history[$phid];
}
$cols[] = $col;
$depth = max($depth, count($col));
}
$grid = array();
$jj = $depth;
for ($ii = 0; $ii < $depth; $ii++) {
$row = array();
if ($depth == $jj) {
$row[] = phutil_tag(
'th',
array(
'valign' => 'middle',
'class' => 'pholio-history-header',
),
pht('Current Revision'));
} else {
$row[] = phutil_tag('th', array(), null);
}
foreach ($cols as $col) {
if (empty($col[$ii])) {
$row[] = phutil_tag('td', array(), null);
} else {
$thumb = $this->renderThumbnail($all_images[$col[$ii]]);
$row[] = phutil_tag('td', array(), $thumb);
}
}
$grid[] = phutil_tag('tr', array(), $row);
$jj--;
}
$grid = phutil_tag(
'table',
array(
'id' => 'pholio-mock-thumb-grid',
'class' => 'pholio-mock-thumb-grid',
),
$grid);
$grid = id(new PHUIBoxView())
->addClass('pholio-mock-thumb-grid-container')
->appendChild($grid);
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Mock History'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($grid);
}
private function renderThumbnail(PholioImage $image) {
$thumbfile = $image->getFile();
$preview_key = PhabricatorFileThumbnailTransform::TRANSFORM_THUMBGRID;
$xform = PhabricatorFileTransform::getTransformByKey($preview_key);
Javelin::initBehavior('phabricator-tooltips');
$attributes = array(
'class' => 'pholio-mock-thumb-grid-image',
'src' => $thumbfile->getURIForTransform($xform),
);
if ($image->getFile()->isViewableImage()) {
$dimensions = $xform->getTransformedDimensions($thumbfile);
if ($dimensions) {
list($x, $y) = $dimensions;
$attributes += array(
'width' => $x,
'height' => $y,
'style' => 'top: '.floor((100 - $y) / 2).'px',
);
}
} else {
// If this is a PDF or a text file or something, we'll end up using a
// generic thumbnail which is always sized correctly.
$attributes += array(
'width' => 100,
'height' => 100,
);
}
$tag = phutil_tag('img', $attributes);
$classes = array('pholio-mock-thumb-grid-item');
if ($image->getIsObsolete()) {
$classes[] = 'pholio-mock-thumb-grid-item-obsolete';
}
$inline_count = null;
if ($image->getInlineComments()) {
$inline_count[] = phutil_tag(
'span',
array(
'class' => 'pholio-mock-thumb-grid-comment-count',
),
pht('%s', phutil_count($image->getInlineComments())));
}
return javelin_tag(
'a',
array(
'sigil' => 'mock-thumbnail has-tooltip',
'class' => implode(' ', $classes),
'href' => '#',
'meta' => array(
'imageID' => $image->getID(),
'tip' => $image->getName(),
'align' => 'N',
),
),
array(
$tag,
$inline_count,
));
}
}
diff --git a/src/applications/pholio/view/PholioTransactionView.php b/src/applications/pholio/view/PholioTransactionView.php
index 69613c542..1126db9c8 100644
--- a/src/applications/pholio/view/PholioTransactionView.php
+++ b/src/applications/pholio/view/PholioTransactionView.php
@@ -1,141 +1,141 @@
<?php
final class PholioTransactionView
extends PhabricatorApplicationTransactionView {
private $mock;
public function setMock($mock) {
$this->mock = $mock;
return $this;
}
public function getMock() {
return $this->mock;
}
protected function shouldGroupTransactions(
PhabricatorApplicationTransaction $u,
PhabricatorApplicationTransaction $v) {
if ($u->getAuthorPHID() != $v->getAuthorPHID()) {
// Don't group transactions by different authors.
return false;
}
if (($v->getDateCreated() - $u->getDateCreated()) > 60) {
// Don't group if transactions happened more than 60s apart.
return false;
}
switch ($u->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
case PholioMockInlineTransaction::TRANSACTIONTYPE:
break;
default:
return false;
}
switch ($v->getTransactionType()) {
case PholioMockInlineTransaction::TRANSACTIONTYPE:
return true;
}
return parent::shouldGroupTransactions($u, $v);
}
protected function renderTransactionContent(
PhabricatorApplicationTransaction $xaction) {
$out = array();
$group = $xaction->getTransactionGroup();
$type = $xaction->getTransactionType();
if ($type == PholioMockInlineTransaction::TRANSACTIONTYPE) {
array_unshift($group, $xaction);
} else {
$out[] = parent::renderTransactionContent($xaction);
}
if (!$group) {
return $out;
}
$inlines = array();
foreach ($group as $xaction) {
switch ($xaction->getTransactionType()) {
case PholioMockInlineTransaction::TRANSACTIONTYPE:
$inlines[] = $xaction;
break;
default:
throw new Exception(pht('Unknown grouped transaction type!'));
}
}
if ($inlines) {
$icon = id(new PHUIIconView())
->setIcon('fa-comment bluegrey msr');
$header = phutil_tag(
'div',
array(
'class' => 'phabricator-transaction-subheader',
),
array($icon, pht('Inline Comments')));
$out[] = $header;
foreach ($inlines as $inline) {
if (!$inline->getComment()) {
continue;
}
$out[] = $this->renderInlineContent($inline);
}
}
return $out;
}
private function renderInlineContent(PholioTransaction $inline) {
$comment = $inline->getComment();
$mock = $this->getMock();
- $images = $mock->getAllImages();
+ $images = $mock->getImages();
$images = mpull($images, null, 'getID');
$image = idx($images, $comment->getImageID());
if (!$image) {
throw new Exception(pht('No image attached!'));
}
$file = $image->getFile();
if (!$file->isViewableImage()) {
throw new Exception(pht('File is not viewable.'));
}
$image_uri = $file->getBestURI();
$thumb = id(new PHUIImageMaskView())
->addClass('mrl')
->setImage($image_uri)
->setDisplayHeight(100)
->setDisplayWidth(200)
->withMask(true)
->centerViewOnPoint(
$comment->getX(), $comment->getY(),
$comment->getHeight(), $comment->getWidth());
$link = phutil_tag(
'a',
array(
'href' => '#',
'class' => 'pholio-transaction-inline-image-anchor',
),
$thumb);
$inline_comment = parent::renderTransactionContent($inline);
return phutil_tag(
'div',
array('class' => 'pholio-transaction-inline-comment'),
array($link, $inline_comment));
}
}
diff --git a/src/applications/pholio/xaction/PholioImageFileTransaction.php b/src/applications/pholio/xaction/PholioImageFileTransaction.php
index 5f68dad9f..e18fca28e 100644
--- a/src/applications/pholio/xaction/PholioImageFileTransaction.php
+++ b/src/applications/pholio/xaction/PholioImageFileTransaction.php
@@ -1,120 +1,128 @@
<?php
final class PholioImageFileTransaction
extends PholioImageTransactionType {
const TRANSACTIONTYPE = 'image-file';
public function generateOldValue($object) {
- $images = $object->getImages();
+ $images = $object->getActiveImages();
return array_values(mpull($images, 'getPHID'));
}
public function generateNewValue($object, $value) {
- $new_value = array();
- foreach ($value as $key => $images) {
- $new_value[$key] = mpull($images, 'getPHID');
- }
- $old = array_fuse($this->getOldValue());
- return $this->getEditor()->getPHIDList($old, $new_value);
+ $editor = $this->getEditor();
+
+ $old_value = $this->getOldValue();
+ $new_value = $value;
+
+ return $editor->getPHIDList($old_value, $new_value);
}
- public function applyInternalEffects($object, $value) {
+ public function applyExternalEffects($object, $value) {
$old_map = array_fuse($this->getOldValue());
$new_map = array_fuse($this->getNewValue());
- $obsolete_map = array_diff_key($old_map, $new_map);
- $images = $object->getImages();
- foreach ($images as $seq => $image) {
- if (isset($obsolete_map[$image->getPHID()])) {
- $image->setIsObsolete(1);
- $image->save();
- unset($images[$seq]);
- }
+ $add_map = array_diff_key($new_map, $old_map);
+ $rem_map = array_diff_key($old_map, $new_map);
+
+ $editor = $this->getEditor();
+
+ foreach ($rem_map as $phid) {
+ $editor->loadPholioImage($object, $phid)
+ ->setIsObsolete(1)
+ ->save();
+ }
+
+ foreach ($add_map as $phid) {
+ $editor->loadPholioImage($object, $phid)
+ ->setMockPHID($object->getPHID())
+ ->save();
}
- $object->attachImages($images);
}
public function getTitle() {
$old = $this->getOldValue();
$new = $this->getNewValue();
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
if ($add && $rem) {
return pht(
'%s edited image(s), added %d: %s; removed %d: %s.',
$this->renderAuthor(),
count($add),
$this->renderHandleList($add),
count($rem),
$this->renderHandleList($rem));
} else if ($add) {
return pht(
'%s added %d image(s): %s.',
$this->renderAuthor(),
count($add),
$this->renderHandleList($add));
} else {
return pht(
'%s removed %d image(s): %s.',
$this->renderAuthor(),
count($rem),
$this->renderHandleList($rem));
}
}
public function getTitleForFeed() {
$old = $this->getOldValue();
$new = $this->getNewValue();
return pht(
'%s updated images of %s.',
$this->renderAuthor(),
$this->renderObject());
}
public function getIcon() {
return 'fa-picture-o';
}
public function getColor() {
$old = $this->getOldValue();
$new = $this->getNewValue();
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
if ($add && $rem) {
return PhabricatorTransactions::COLOR_YELLOW;
} else if ($add) {
return PhabricatorTransactions::COLOR_GREEN;
} else {
return PhabricatorTransactions::COLOR_RED;
}
}
public function extractFilePHIDs($object, $value) {
- $images = $this->getEditor()->getNewImages();
- $images = mpull($images, null, 'getPHID');
+ $editor = $this->getEditor();
+ // NOTE: This method is a little weird (and includes ALL the file PHIDs,
+ // including old file PHIDs) because we currently don't have a storage
+ // object when called. This might change at some point.
+
+ $new_phids = $value;
$file_phids = array();
- foreach ($value as $image_phid) {
- $image = idx($images, $image_phid);
- if (!$image) {
- continue;
- }
- $file_phids[] = $image->getFilePHID();
+ foreach ($new_phids as $phid) {
+ $file_phids[] = $editor->loadPholioImage($object, $phid)
+ ->getFilePHID();
}
+
return $file_phids;
}
public function mergeTransactions(
$object,
PhabricatorApplicationTransaction $u,
PhabricatorApplicationTransaction $v) {
- return $this->getEditor()->mergePHIDOrEdgeTransactions($u, $v);
+ return $this->getEditor()->mergePHIDOrEdgeTransactions($u, $v);
}
}
diff --git a/src/applications/pholio/xaction/PholioImageReplaceTransaction.php b/src/applications/pholio/xaction/PholioImageReplaceTransaction.php
index 4978fa976..a0e62bc1e 100644
--- a/src/applications/pholio/xaction/PholioImageReplaceTransaction.php
+++ b/src/applications/pholio/xaction/PholioImageReplaceTransaction.php
@@ -1,80 +1,140 @@
<?php
final class PholioImageReplaceTransaction
extends PholioImageTransactionType {
const TRANSACTIONTYPE = 'image-replace';
public function generateOldValue($object) {
- $new_image = $this->getNewValue();
- return $new_image->getReplacesImagePHID();
- }
+ $editor = $this->getEditor();
+ $new_phid = $this->getNewValue();
- public function generateNewValue($object, $value) {
- return $value->getPHID();
+ return $editor->loadPholioImage($object, $new_phid)
+ ->getReplacesImagePHID();
}
- public function applyInternalEffects($object, $value) {
- $old = $this->getOldValue();
- $images = $object->getImages();
- foreach ($images as $seq => $image) {
- if ($image->getPHID() == $old) {
- $image->setIsObsolete(1);
- $image->save();
- unset($images[$seq]);
- }
- }
- $object->attachImages($images);
+ public function applyExternalEffects($object, $value) {
+ $editor = $this->getEditor();
+ $old_phid = $this->getOldValue();
+
+ $old_image = $editor->loadPholioImage($object, $old_phid)
+ ->setIsObsolete(1)
+ ->save();
+
+ $editor->loadPholioImage($object, $value)
+ ->setMockPHID($object->getPHID())
+ ->setSequence($old_image->getSequence())
+ ->save();
}
public function getTitle() {
return pht(
'%s replaced %s with %s.',
$this->renderAuthor(),
$this->renderOldHandle(),
$this->renderNewHandle());
}
public function getTitleForFeed() {
return pht(
'%s updated images of %s.',
$this->renderAuthor(),
$this->renderObject());
}
public function getIcon() {
return 'fa-picture-o';
}
public function getColor() {
return PhabricatorTransactions::COLOR_YELLOW;
}
public function mergeTransactions(
$object,
PhabricatorApplicationTransaction $u,
PhabricatorApplicationTransaction $v) {
- $u_img = $u->getNewValue();
- $v_img = $v->getNewValue();
- if ($u_img->getReplacesImagePHID() == $v_img->getReplacesImagePHID()) {
+
+ $u_phid = $u->getOldValue();
+ $v_phid = $v->getOldValue();
+
+ if ($u_phid === $v_phid) {
return $v;
}
+
+ return null;
}
public function extractFilePHIDs($object, $value) {
- $file_phids = array();
+ $editor = $this->getEditor();
+
+ $file_phid = $editor->loadPholioImage($object, $value)
+ ->getFilePHID();
+
+ return array($file_phid);
+ }
+
+ public function validateTransactions($object, array $xactions) {
+ $errors = array();
+
+ $mock_phid = $object->getPHID();
$editor = $this->getEditor();
- $images = $editor->getNewImages();
- foreach ($images as $image) {
- if ($image->getPHID() !== $value) {
+ foreach ($xactions as $xaction) {
+ $new_phid = $xaction->getNewValue();
+
+ try {
+ $new_image = $editor->loadPholioImage($object, $new_phid);
+ } catch (Exception $ex) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Unable to load replacement image ("%s"): %s',
+ $new_phid,
+ $ex->getMessage()),
+ $xaction);
continue;
}
- $file_phids[] = $image->getFilePHID();
+ $old_phid = $new_image->getReplacesImagePHID();
+ if (!$old_phid) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Image ("%s") does not specify which image it replaces.',
+ $new_phid),
+ $xaction);
+ continue;
+ }
+
+ try {
+ $old_image = $editor->loadPholioImage($object, $old_phid);
+ } catch (Exception $ex) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Unable to load replaced image ("%s"): %s',
+ $old_phid,
+ $ex->getMessage()),
+ $xaction);
+ continue;
+ }
+
+ if ($old_image->getMockPHID() !== $mock_phid) {
+ $errors[] = $this->newInvalidError(
+ pht(
+ 'Replaced image ("%s") belongs to the wrong mock ("%s", expected '.
+ '"%s").',
+ $old_phid,
+ $old_image->getMockPHID(),
+ $mock_phid),
+ $xaction);
+ continue;
+ }
+
+ // TODO: You shouldn't be able to replace an image if it has already
+ // been replaced.
+
}
- return $file_phids;
+ return $errors;
}
}
diff --git a/src/applications/phortune/controller/cart/PhortuneCartViewController.php b/src/applications/phortune/controller/cart/PhortuneCartViewController.php
index 8387cacb0..8108c83b3 100644
--- a/src/applications/phortune/controller/cart/PhortuneCartViewController.php
+++ b/src/applications/phortune/controller/cart/PhortuneCartViewController.php
@@ -1,374 +1,386 @@
<?php
final class PhortuneCartViewController
extends PhortuneCartController {
private $action = null;
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$this->action = $request->getURIData('action');
$authority = $this->loadMerchantAuthority();
require_celerity_resource('phortune-css');
$query = id(new PhortuneCartQuery())
->setViewer($viewer)
->withIDs(array($id))
->needPurchases(true);
if ($authority) {
$query->withMerchantPHIDs(array($authority->getPHID()));
}
$cart = $query->executeOne();
if (!$cart) {
return new Aphront404Response();
}
$cart_table = $this->buildCartContentTable($cart);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$cart,
PhabricatorPolicyCapability::CAN_EDIT);
$errors = array();
$error_view = null;
$resume_uri = null;
switch ($cart->getStatus()) {
case PhortuneCart::STATUS_READY:
if ($authority && $cart->getIsInvoice()) {
// We arrived here by following the ad-hoc invoice workflow, and
// are acting with merchant authority.
$checkout_uri = PhabricatorEnv::getURI($cart->getCheckoutURI());
$invoice_message = array(
pht(
'Manual invoices do not automatically notify recipients yet. '.
'Send the payer this checkout link:'),
' ',
phutil_tag(
'a',
array(
'href' => $checkout_uri,
),
$checkout_uri),
);
$error_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors(array($invoice_message));
}
break;
case PhortuneCart::STATUS_PURCHASING:
if ($can_edit) {
$resume_uri = $cart->getMetadataValue('provider.checkoutURI');
if ($resume_uri) {
$errors[] = pht(
'The checkout process has been started, but not yet completed. '.
'You can continue checking out by clicking %s, or cancel the '.
'order, or contact the merchant for assistance.',
phutil_tag('strong', array(), pht('Continue Checkout')));
} else {
$errors[] = pht(
'The checkout process has been started, but an error occurred. '.
'You can cancel the order or contact the merchant for '.
'assistance.');
}
}
break;
case PhortuneCart::STATUS_CHARGED:
if ($can_edit) {
$errors[] = pht(
'You have been charged, but processing could not be completed. '.
'You can cancel your order, or contact the merchant for '.
'assistance.');
}
break;
case PhortuneCart::STATUS_HOLD:
if ($can_edit) {
$errors[] = pht(
'Payment for this order is on hold. You can click %s to check '.
'for updates, cancel the order, or contact the merchant for '.
'assistance.',
phutil_tag('strong', array(), pht('Update Status')));
}
break;
case PhortuneCart::STATUS_REVIEW:
if ($authority) {
$errors[] = pht(
'This order has been flagged for manual review. Review the order '.
'and choose %s to accept it or %s to reject it.',
phutil_tag('strong', array(), pht('Accept Order')),
phutil_tag('strong', array(), pht('Refund Order')));
} else if ($can_edit) {
$errors[] = pht(
'This order requires manual processing and will complete once '.
'the merchant accepts it.');
}
break;
case PhortuneCart::STATUS_PURCHASED:
$error_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_SUCCESS)
->appendChild(pht('This purchase has been completed.'));
break;
}
if ($errors) {
$error_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->appendChild($errors);
}
$details = $this->buildDetailsView($cart);
$curtain = $this->buildCurtainView(
$cart,
$can_edit,
$authority,
$resume_uri);
$header = id(new PHUIHeaderView())
->setUser($viewer)
->setHeader($cart->getName())
->setHeaderIcon('fa-shopping-cart');
if ($cart->getStatus() == PhortuneCart::STATUS_PURCHASED) {
$done_uri = $cart->getDoneURI();
if ($done_uri) {
$header->addActionLink(
id(new PHUIButtonView())
->setTag('a')
->setHref($done_uri)
->setIcon('fa-check-square green')
->setText($cart->getDoneActionName()));
}
}
$cart_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Cart Items'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($cart_table);
$description = $this->renderCartDescription($cart);
$charges = id(new PhortuneChargeQuery())
->setViewer($viewer)
->withCartPHIDs(array($cart->getPHID()))
->needCarts(true)
->execute();
$phids = array();
foreach ($charges as $charge) {
$phids[] = $charge->getProviderPHID();
$phids[] = $charge->getCartPHID();
$phids[] = $charge->getMerchantPHID();
$phids[] = $charge->getPaymentMethodPHID();
}
$handles = $this->loadViewerHandles($phids);
$charges_table = id(new PhortuneChargeTableView())
->setUser($viewer)
->setHandles($handles)
->setCharges($charges)
->setShowOrder(false);
$charges = id(new PHUIObjectBoxView())
->setHeaderText(pht('Charges'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($charges_table);
$account = $cart->getAccount();
$crumbs = $this->buildApplicationCrumbs();
if ($authority) {
$this->addMerchantCrumb($crumbs, $authority);
} else {
$this->addAccountCrumb($crumbs, $cart->getAccount());
}
$crumbs->addTextCrumb(pht('Cart %d', $cart->getID()));
$crumbs->setBorder(true);
if (!$this->action) {
$class = 'phortune-cart-page';
$timeline = $this->buildTransactionTimeline(
$cart,
new PhortuneCartTransactionQuery());
$timeline
->setShouldTerminate(true);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setCurtain($curtain)
->setMainColumn(array(
$error_view,
$details,
$cart_box,
$description,
$charges,
$timeline,
));
} else {
$class = 'phortune-invoice-view';
$crumbs = null;
$merchant_phid = $cart->getMerchantPHID();
$buyer_phid = $cart->getAuthorPHID();
$merchant = id(new PhortuneMerchantQuery())
->setViewer($viewer)
->withPHIDs(array($merchant_phid))
->needProfileImage(true)
->executeOne();
$buyer = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withPHIDs(array($buyer_phid))
->needProfileImage(true)
->executeOne();
- // TODO: Add account "Contact" info
$merchant_contact = new PHUIRemarkupView(
- $viewer, $merchant->getContactInfo());
- $description = null;
+ $viewer,
+ $merchant->getContactInfo());
+
+ $account_name = $account->getBillingName();
+ if (!strlen($account_name)) {
+ $account_name = $buyer->getRealName();
+ }
+
+ $account_contact = $account->getBillingAddress();
+ if (strlen($account_contact)) {
+ $account_contact = new PHUIRemarkupView(
+ $viewer,
+ $account_contact);
+ }
$view = id(new PhortuneInvoiceView())
->setMerchantName($merchant->getName())
->setMerchantLogo($merchant->getProfileImageURI())
->setMerchantContact($merchant_contact)
->setMerchantFooter($merchant->getInvoiceFooter())
- ->setAccountName($buyer->getRealName())
+ ->setAccountName($account_name)
+ ->setAccountContact($account_contact)
->setStatus($error_view)
- ->setContent(array(
- $description,
- $details,
- $cart_box,
- $charges,
- ));
+ ->setContent(
+ array(
+ $details,
+ $cart_box,
+ $charges,
+ ));
}
$page = $this->newPage()
->setTitle(pht('Cart %d', $cart->getID()))
->addClass($class)
->appendChild($view);
if ($crumbs) {
$page->setCrumbs($crumbs);
}
return $page;
}
private function buildDetailsView(PhortuneCart $cart) {
$viewer = $this->getViewer();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($cart);
$handles = $this->loadViewerHandles(
array(
$cart->getAccountPHID(),
$cart->getAuthorPHID(),
$cart->getMerchantPHID(),
));
if ($this->action == 'print') {
$view->addProperty(pht('Order Name'), $cart->getName());
}
$view->addProperty(
pht('Account'),
$handles[$cart->getAccountPHID()]->renderLink());
$view->addProperty(
pht('Authorized By'),
$handles[$cart->getAuthorPHID()]->renderLink());
$view->addProperty(
pht('Merchant'),
$handles[$cart->getMerchantPHID()]->renderLink());
$view->addProperty(
pht('Status'),
PhortuneCart::getNameForStatus($cart->getStatus()));
$view->addProperty(
pht('Updated'),
phabricator_datetime($cart->getDateModified(), $viewer));
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Details'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($view);
}
private function buildCurtainView(
PhortuneCart $cart,
$can_edit,
$authority,
$resume_uri) {
$viewer = $this->getViewer();
$id = $cart->getID();
$curtain = $this->newCurtainView($cart);
$can_cancel = ($can_edit && $cart->canCancelOrder());
if ($authority) {
$prefix = 'merchant/'.$authority->getID().'/';
} else {
$prefix = '';
}
$cancel_uri = $this->getApplicationURI("{$prefix}cart/{$id}/cancel/");
$refund_uri = $this->getApplicationURI("{$prefix}cart/{$id}/refund/");
$update_uri = $this->getApplicationURI("{$prefix}cart/{$id}/update/");
$accept_uri = $this->getApplicationURI("{$prefix}cart/{$id}/accept/");
$print_uri = $this->getApplicationURI("{$prefix}cart/{$id}/print/");
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Cancel Order'))
->setIcon('fa-times')
->setDisabled(!$can_cancel)
->setWorkflow(true)
->setHref($cancel_uri));
if ($authority) {
if ($cart->getStatus() == PhortuneCart::STATUS_REVIEW) {
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Accept Order'))
->setIcon('fa-check')
->setWorkflow(true)
->setHref($accept_uri));
}
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Refund Order'))
->setIcon('fa-reply')
->setWorkflow(true)
->setHref($refund_uri));
}
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Update Status'))
->setIcon('fa-refresh')
->setHref($update_uri));
if ($can_edit && $resume_uri) {
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Continue Checkout'))
->setIcon('fa-shopping-cart')
->setHref($resume_uri));
}
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Printable Version'))
->setHref($print_uri)
->setOpenInNewWindow(true)
->setIcon('fa-print'));
return $curtain;
}
}
diff --git a/src/applications/phortune/editor/PhortuneAccountEditEngine.php b/src/applications/phortune/editor/PhortuneAccountEditEngine.php
index ed0e4b01b..1b6f9a504 100644
--- a/src/applications/phortune/editor/PhortuneAccountEditEngine.php
+++ b/src/applications/phortune/editor/PhortuneAccountEditEngine.php
@@ -1,108 +1,127 @@
<?php
final class PhortuneAccountEditEngine
extends PhabricatorEditEngine {
const ENGINECONST = 'phortune.account';
public function getEngineName() {
return pht('Phortune Accounts');
}
public function getEngineApplicationClass() {
return 'PhabricatorPhortuneApplication';
}
public function getSummaryHeader() {
return pht('Configure Phortune Account Forms');
}
public function getSummaryText() {
return pht('Configure creation and editing forms in Phortune Accounts.');
}
public function isEngineConfigurable() {
return false;
}
protected function newEditableObject() {
return PhortuneAccount::initializeNewAccount($this->getViewer());
}
protected function newObjectQuery() {
return new PhortuneAccountQuery();
}
protected function getObjectCreateTitleText($object) {
return pht('Create Payment Account');
}
protected function getObjectEditTitleText($object) {
return pht('Edit Account: %s', $object->getName());
}
protected function getObjectEditShortText($object) {
return $object->getName();
}
protected function getObjectCreateShortText() {
return pht('Create Account');
}
protected function getObjectName() {
return pht('Account');
}
protected function getObjectCreateCancelURI($object) {
return $this->getApplication()->getApplicationURI('/');
}
protected function getEditorURI() {
return $this->getApplication()->getApplicationURI('edit/');
}
protected function getObjectViewURI($object) {
return $object->getURI();
}
protected function buildCustomEditFields($object) {
$viewer = $this->getViewer();
if ($this->getIsCreate()) {
$member_phids = array($viewer->getPHID());
} else {
$member_phids = $object->getMemberPHIDs();
}
$fields = array(
id(new PhabricatorTextEditField())
->setKey('name')
->setLabel(pht('Name'))
->setDescription(pht('Account name.'))
->setConduitTypeDescription(pht('New account name.'))
->setTransactionType(
PhortuneAccountNameTransaction::TRANSACTIONTYPE)
->setValue($object->getName())
->setIsRequired(true),
id(new PhabricatorUsersEditField())
->setKey('managers')
->setAliases(array('memberPHIDs', 'managerPHIDs'))
->setLabel(pht('Managers'))
->setUseEdgeTransactions(true)
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
PhortuneAccountHasMemberEdgeType::EDGECONST)
->setDescription(pht('Initial account managers.'))
->setConduitDescription(pht('Set account managers.'))
->setConduitTypeDescription(pht('New list of managers.'))
->setInitialValue($object->getMemberPHIDs())
->setValue($member_phids),
+
+ id(new PhabricatorTextEditField())
+ ->setKey('billingName')
+ ->setLabel(pht('Billing Name'))
+ ->setDescription(pht('Account name for billing purposes.'))
+ ->setConduitTypeDescription(pht('New account billing name.'))
+ ->setTransactionType(
+ PhortuneAccountBillingNameTransaction::TRANSACTIONTYPE)
+ ->setValue($object->getBillingName()),
+
+ id(new PhabricatorTextAreaEditField())
+ ->setKey('billingAddress')
+ ->setLabel(pht('Billing Address'))
+ ->setDescription(pht('Account billing address.'))
+ ->setConduitTypeDescription(pht('New account billing address.'))
+ ->setTransactionType(
+ PhortuneAccountBillingAddressTransaction::TRANSACTIONTYPE)
+ ->setValue($object->getBillingAddress()),
+
);
return $fields;
}
}
diff --git a/src/applications/phortune/storage/PhortuneAccount.php b/src/applications/phortune/storage/PhortuneAccount.php
index ff3b0d8a8..ade98d327 100644
--- a/src/applications/phortune/storage/PhortuneAccount.php
+++ b/src/applications/phortune/storage/PhortuneAccount.php
@@ -1,171 +1,166 @@
<?php
/**
* An account represents a purchasing entity. An account may have multiple users
* on it (e.g., several employees of a company have access to the company
* account), and a user may have several accounts (e.g., a company account and
* a personal account).
*/
final class PhortuneAccount extends PhortuneDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface {
protected $name;
+ protected $billingName;
+ protected $billingAddress;
private $memberPHIDs = self::ATTACHABLE;
public static function initializeNewAccount(PhabricatorUser $actor) {
return id(new self())
+ ->setBillingName('')
+ ->setBillingAddress('')
->attachMemberPHIDs(array());
}
public static function createNewAccount(
PhabricatorUser $actor,
PhabricatorContentSource $content_source) {
$account = self::initializeNewAccount($actor);
$xactions = array();
$xactions[] = id(new PhortuneAccountTransaction())
->setTransactionType(PhortuneAccountNameTransaction::TRANSACTIONTYPE)
->setNewValue(pht('Default Account'));
$xactions[] = id(new PhortuneAccountTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
PhortuneAccountHasMemberEdgeType::EDGECONST)
->setNewValue(
array(
'=' => array($actor->getPHID() => $actor->getPHID()),
));
$editor = id(new PhortuneAccountEditor())
->setActor($actor)
->setContentSource($content_source);
// We create an account for you the first time you visit Phortune.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$editor->applyTransactions($account, $xactions);
unset($unguarded);
return $account;
}
public function newCart(
PhabricatorUser $actor,
PhortuneCartImplementation $implementation,
PhortuneMerchant $merchant) {
$cart = PhortuneCart::initializeNewCart($actor, $this, $merchant);
$cart->setCartClass(get_class($implementation));
$cart->attachImplementation($implementation);
$implementation->willCreateCart($actor, $cart);
return $cart->save();
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text255',
+ 'billingName' => 'text255',
+ 'billingAddress' => 'text',
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhortuneAccountPHIDType::TYPECONST);
}
public function getMemberPHIDs() {
return $this->assertAttached($this->memberPHIDs);
}
public function attachMemberPHIDs(array $phids) {
$this->memberPHIDs = $phids;
return $this;
}
public function getURI() {
return '/phortune/'.$this->getID().'/';
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhortuneAccountEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhortuneAccountTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
case PhabricatorPolicyCapability::CAN_EDIT:
if ($this->getPHID() === null) {
// Allow a user to create an account for themselves.
return PhabricatorPolicies::POLICY_USER;
} else {
return PhabricatorPolicies::POLICY_NOONE;
}
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
$members = array_fuse($this->getMemberPHIDs());
if (isset($members[$viewer->getPHID()])) {
return true;
}
// If the viewer is acting on behalf of a merchant, they can see
// payment accounts.
if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
foreach ($viewer->getAuthorities() as $authority) {
if ($authority instanceof PhortuneMerchant) {
return true;
}
}
}
return false;
}
public function describeAutomaticCapability($capability) {
return pht('Members of an account can always view and edit it.');
}
}
diff --git a/src/applications/phortune/storage/PhortuneCart.php b/src/applications/phortune/storage/PhortuneCart.php
index 07554ccb8..2b121a3b0 100644
--- a/src/applications/phortune/storage/PhortuneCart.php
+++ b/src/applications/phortune/storage/PhortuneCart.php
@@ -1,700 +1,689 @@
<?php
final class PhortuneCart extends PhortuneDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface {
const STATUS_BUILDING = 'cart:building';
const STATUS_READY = 'cart:ready';
const STATUS_PURCHASING = 'cart:purchasing';
const STATUS_CHARGED = 'cart:charged';
const STATUS_HOLD = 'cart:hold';
const STATUS_REVIEW = 'cart:review';
const STATUS_PURCHASED = 'cart:purchased';
protected $accountPHID;
protected $authorPHID;
protected $merchantPHID;
protected $subscriptionPHID;
protected $cartClass;
protected $status;
protected $metadata = array();
protected $mailKey;
protected $isInvoice;
private $account = self::ATTACHABLE;
private $purchases = self::ATTACHABLE;
private $implementation = self::ATTACHABLE;
private $merchant = self::ATTACHABLE;
public static function initializeNewCart(
PhabricatorUser $actor,
PhortuneAccount $account,
PhortuneMerchant $merchant) {
$cart = id(new PhortuneCart())
->setAuthorPHID($actor->getPHID())
->setStatus(self::STATUS_BUILDING)
->setAccountPHID($account->getPHID())
->setIsInvoice(0)
->attachAccount($account)
->setMerchantPHID($merchant->getPHID())
->attachMerchant($merchant);
$cart->account = $account;
$cart->purchases = array();
return $cart;
}
public function newPurchase(
PhabricatorUser $actor,
PhortuneProduct $product) {
$purchase = PhortunePurchase::initializeNewPurchase($actor, $product)
->setAccountPHID($this->getAccount()->getPHID())
->setCartPHID($this->getPHID())
->save();
$this->purchases[] = $purchase;
return $purchase;
}
public static function getStatusNameMap() {
return array(
self::STATUS_BUILDING => pht('Building'),
self::STATUS_READY => pht('Ready'),
self::STATUS_PURCHASING => pht('Purchasing'),
self::STATUS_CHARGED => pht('Charged'),
self::STATUS_HOLD => pht('Hold'),
self::STATUS_REVIEW => pht('Review'),
self::STATUS_PURCHASED => pht('Purchased'),
);
}
public static function getNameForStatus($status) {
return idx(self::getStatusNameMap(), $status, $status);
}
public function activateCart() {
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if ($copy->getStatus() !== self::STATUS_BUILDING) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call %s.',
$copy->getStatus(),
'willApplyCharge()'));
}
$this->setStatus(self::STATUS_READY)->save();
$this->endReadLocking();
$this->saveTransaction();
$this->recordCartTransaction(PhortuneCartTransaction::TYPE_CREATED);
return $this;
}
public function willApplyCharge(
PhabricatorUser $actor,
PhortunePaymentProvider $provider,
PhortunePaymentMethod $method = null) {
$account = $this->getAccount();
$charge = PhortuneCharge::initializeNewCharge()
->setAccountPHID($account->getPHID())
->setCartPHID($this->getPHID())
->setAuthorPHID($actor->getPHID())
->setMerchantPHID($this->getMerchant()->getPHID())
->setProviderPHID($provider->getProviderConfig()->getPHID())
->setAmountAsCurrency($this->getTotalPriceAsCurrency());
if ($method) {
if (!$method->isActive()) {
throw new Exception(
pht(
'Attempting to apply a charge using an inactive '.
'payment method ("%s")!',
$method->getPHID()));
}
$charge->setPaymentMethodPHID($method->getPHID());
}
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if ($copy->getStatus() !== self::STATUS_READY) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call %s, expected "%s".',
$copy->getStatus(),
'willApplyCharge()',
self::STATUS_READY));
}
$charge->save();
$this->setStatus(self::STATUS_PURCHASING)->save();
$this->endReadLocking();
$this->saveTransaction();
return $charge;
}
public function didHoldCharge(PhortuneCharge $charge) {
$charge->setStatus(PhortuneCharge::STATUS_HOLD);
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if ($copy->getStatus() !== self::STATUS_PURCHASING) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call %s, expected "%s".',
$copy->getStatus(),
'didHoldCharge()',
self::STATUS_PURCHASING));
}
$charge->save();
$this->setStatus(self::STATUS_HOLD)->save();
$this->endReadLocking();
$this->saveTransaction();
$this->recordCartTransaction(PhortuneCartTransaction::TYPE_HOLD);
}
public function didApplyCharge(PhortuneCharge $charge) {
$charge->setStatus(PhortuneCharge::STATUS_CHARGED);
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if (($copy->getStatus() !== self::STATUS_PURCHASING) &&
($copy->getStatus() !== self::STATUS_HOLD)) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call %s.',
$copy->getStatus(),
'didApplyCharge()'));
}
$charge->save();
$this->setStatus(self::STATUS_CHARGED)->save();
$this->endReadLocking();
$this->saveTransaction();
// TODO: Perform purchase review. Here, we would apply rules to determine
// whether the charge needs manual review (maybe making the decision via
// Herald, configuration, or by examining provider fraud data). For now,
// don't require review.
$needs_review = false;
if ($needs_review) {
$this->willReviewCart();
} else {
$this->didReviewCart();
}
return $this;
}
public function willReviewCart() {
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if (($copy->getStatus() !== self::STATUS_CHARGED)) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call %s!',
$copy->getStatus(),
'willReviewCart()'));
}
$this->setStatus(self::STATUS_REVIEW)->save();
$this->endReadLocking();
$this->saveTransaction();
$this->recordCartTransaction(PhortuneCartTransaction::TYPE_REVIEW);
return $this;
}
public function didReviewCart() {
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if (($copy->getStatus() !== self::STATUS_CHARGED) &&
($copy->getStatus() !== self::STATUS_REVIEW)) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call %s!',
$copy->getStatus(),
'didReviewCart()'));
}
foreach ($this->purchases as $purchase) {
$purchase->getProduct()->didPurchaseProduct($purchase);
}
$this->setStatus(self::STATUS_PURCHASED)->save();
$this->endReadLocking();
$this->saveTransaction();
$this->recordCartTransaction(PhortuneCartTransaction::TYPE_PURCHASED);
return $this;
}
public function didFailCharge(PhortuneCharge $charge) {
$charge->setStatus(PhortuneCharge::STATUS_FAILED);
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if (($copy->getStatus() !== self::STATUS_PURCHASING) &&
($copy->getStatus() !== self::STATUS_HOLD)) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call %s.',
$copy->getStatus(),
'didFailCharge()'));
}
$charge->save();
// Move the cart back into STATUS_READY so the user can try
// making the purchase again.
$this->setStatus(self::STATUS_READY)->save();
$this->endReadLocking();
$this->saveTransaction();
return $this;
}
public function willRefundCharge(
PhabricatorUser $actor,
PhortunePaymentProvider $provider,
PhortuneCharge $charge,
PhortuneCurrency $amount) {
if (!$amount->isPositive()) {
throw new Exception(
pht('Trying to refund non-positive amount of money!'));
}
if ($amount->isGreaterThan($charge->getAmountRefundableAsCurrency())) {
throw new Exception(
pht('Trying to refund more money than remaining on charge!'));
}
if ($charge->getRefundedChargePHID()) {
throw new Exception(
pht('Trying to refund a refund!'));
}
if (($charge->getStatus() !== PhortuneCharge::STATUS_CHARGED) &&
($charge->getStatus() !== PhortuneCharge::STATUS_HOLD)) {
throw new Exception(
pht('Trying to refund an uncharged charge!'));
}
$refund_charge = PhortuneCharge::initializeNewCharge()
->setAccountPHID($this->getAccount()->getPHID())
->setCartPHID($this->getPHID())
->setAuthorPHID($actor->getPHID())
->setMerchantPHID($this->getMerchant()->getPHID())
->setProviderPHID($provider->getProviderConfig()->getPHID())
->setPaymentMethodPHID($charge->getPaymentMethodPHID())
->setRefundedChargePHID($charge->getPHID())
->setAmountAsCurrency($amount->negate());
$charge->openTransaction();
$charge->beginReadLocking();
$copy = clone $charge;
$copy->reload();
if ($copy->getRefundingPHID() !== null) {
throw new Exception(
pht('Trying to refund a charge which is already refunding!'));
}
$refund_charge->save();
$charge->setRefundingPHID($refund_charge->getPHID());
$charge->save();
$charge->endReadLocking();
$charge->saveTransaction();
return $refund_charge;
}
public function didRefundCharge(
PhortuneCharge $charge,
PhortuneCharge $refund) {
$refund->setStatus(PhortuneCharge::STATUS_CHARGED);
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $charge;
$copy->reload();
if ($charge->getRefundingPHID() !== $refund->getPHID()) {
throw new Exception(
pht('Charge is in the wrong refunding state!'));
}
$charge->setRefundingPHID(null);
// NOTE: There's some trickiness here to get the signs right. Both
// these values are positive but the refund has a negative value.
$total_refunded = $charge
->getAmountRefundedAsCurrency()
->add($refund->getAmountAsCurrency()->negate());
$charge->setAmountRefundedAsCurrency($total_refunded);
$charge->save();
$refund->save();
$this->endReadLocking();
$this->saveTransaction();
$amount = $refund->getAmountAsCurrency()->negate();
foreach ($this->purchases as $purchase) {
$purchase->getProduct()->didRefundProduct($purchase, $amount);
}
return $this;
}
public function didFailRefund(
PhortuneCharge $charge,
PhortuneCharge $refund) {
$refund->setStatus(PhortuneCharge::STATUS_FAILED);
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $charge;
$copy->reload();
if ($charge->getRefundingPHID() !== $refund->getPHID()) {
throw new Exception(
pht('Charge is in the wrong refunding state!'));
}
$charge->setRefundingPHID(null);
$charge->save();
$refund->save();
$this->endReadLocking();
$this->saveTransaction();
}
private function recordCartTransaction($type) {
$omnipotent_user = PhabricatorUser::getOmnipotentUser();
$phortune_phid = id(new PhabricatorPhortuneApplication())->getPHID();
$xactions = array();
$xactions[] = id(new PhortuneCartTransaction())
->setTransactionType($type)
->setNewValue(true);
$content_source = PhabricatorContentSource::newForSource(
PhabricatorPhortuneContentSource::SOURCECONST);
$editor = id(new PhortuneCartEditor())
->setActor($omnipotent_user)
->setActingAsPHID($phortune_phid)
->setContentSource($content_source)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true);
$editor->applyTransactions($this, $xactions);
}
public function getName() {
return $this->getImplementation()->getName($this);
}
public function getDoneURI() {
return $this->getImplementation()->getDoneURI($this);
}
public function getDoneActionName() {
return $this->getImplementation()->getDoneActionName($this);
}
public function getCancelURI() {
return $this->getImplementation()->getCancelURI($this);
}
public function getDescription() {
return $this->getImplementation()->getDescription($this);
}
public function getDetailURI(PhortuneMerchant $authority = null) {
if ($authority) {
$prefix = 'merchant/'.$authority->getID().'/';
} else {
$prefix = '';
}
return '/phortune/'.$prefix.'cart/'.$this->getID().'/';
}
public function getCheckoutURI() {
return '/phortune/cart/'.$this->getID().'/checkout/';
}
public function canCancelOrder() {
try {
$this->assertCanCancelOrder();
return true;
} catch (Exception $ex) {
return false;
}
}
public function canRefundOrder() {
try {
$this->assertCanRefundOrder();
return true;
} catch (Exception $ex) {
return false;
}
}
public function assertCanCancelOrder() {
switch ($this->getStatus()) {
case self::STATUS_BUILDING:
throw new Exception(
pht(
'This order can not be cancelled because the application has not '.
'finished building it yet.'));
case self::STATUS_READY:
throw new Exception(
pht(
'This order can not be cancelled because it has not been placed.'));
}
return $this->getImplementation()->assertCanCancelOrder($this);
}
public function assertCanRefundOrder() {
switch ($this->getStatus()) {
case self::STATUS_BUILDING:
throw new Exception(
pht(
'This order can not be refunded because the application has not '.
'finished building it yet.'));
case self::STATUS_READY:
throw new Exception(
pht(
'This order can not be refunded because it has not been placed.'));
}
return $this->getImplementation()->assertCanRefundOrder($this);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'metadata' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'status' => 'text32',
'cartClass' => 'text128',
'mailKey' => 'bytes20',
'subscriptionPHID' => 'phid?',
'isInvoice' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_account' => array(
'columns' => array('accountPHID'),
),
'key_merchant' => array(
'columns' => array('merchantPHID'),
),
'key_subscription' => array(
'columns' => array('subscriptionPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhortuneCartPHIDType::TYPECONST);
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public function attachPurchases(array $purchases) {
assert_instances_of($purchases, 'PhortunePurchase');
$this->purchases = $purchases;
return $this;
}
public function getPurchases() {
return $this->assertAttached($this->purchases);
}
public function attachAccount(PhortuneAccount $account) {
$this->account = $account;
return $this;
}
public function getAccount() {
return $this->assertAttached($this->account);
}
public function attachMerchant(PhortuneMerchant $merchant) {
$this->merchant = $merchant;
return $this;
}
public function getMerchant() {
return $this->assertAttached($this->merchant);
}
public function attachImplementation(
PhortuneCartImplementation $implementation) {
$this->implementation = $implementation;
return $this;
}
public function getImplementation() {
return $this->assertAttached($this->implementation);
}
public function getTotalPriceAsCurrency() {
$prices = array();
foreach ($this->getPurchases() as $purchase) {
$prices[] = $purchase->getTotalPriceAsCurrency();
}
return PhortuneCurrency::newFromList($prices);
}
public function setMetadataValue($key, $value) {
$this->metadata[$key] = $value;
return $this;
}
public function getMetadataValue($key, $default = null) {
return idx($this->metadata, $key, $default);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhortuneCartEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhortuneCartTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
// NOTE: Both view and edit use the account's edit policy. We punch a hole
// through this for merchants, below.
return $this
->getAccount()
->getPolicy(PhabricatorPolicyCapability::CAN_EDIT);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($this->getAccount()->hasAutomaticCapability($capability, $viewer)) {
return true;
}
// If the viewer controls the merchant this order was placed with, they
// can view the order.
if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
$can_admin = PhabricatorPolicyFilter::hasCapability(
$viewer,
$this->getMerchant(),
PhabricatorPolicyCapability::CAN_EDIT);
if ($can_admin) {
return true;
}
}
return false;
}
public function describeAutomaticCapability($capability) {
return array(
pht('Orders inherit the policies of the associated account.'),
pht('The merchant you placed an order with can review and manage it.'),
);
}
}
diff --git a/src/applications/phortune/storage/PhortuneMerchant.php b/src/applications/phortune/storage/PhortuneMerchant.php
index 81d039137..4916cfede 100644
--- a/src/applications/phortune/storage/PhortuneMerchant.php
+++ b/src/applications/phortune/storage/PhortuneMerchant.php
@@ -1,129 +1,118 @@
<?php
final class PhortuneMerchant extends PhortuneDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface {
protected $name;
protected $viewPolicy;
protected $description;
protected $contactInfo;
protected $invoiceEmail;
protected $invoiceFooter;
protected $profileImagePHID;
private $memberPHIDs = self::ATTACHABLE;
private $profileImageFile = self::ATTACHABLE;
public static function initializeNewMerchant(PhabricatorUser $actor) {
return id(new PhortuneMerchant())
->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy())
->attachMemberPHIDs(array())
->setContactInfo('')
->setInvoiceEmail('')
->setInvoiceFooter('');
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text255',
'description' => 'text',
'contactInfo' => 'text',
'invoiceEmail' => 'text255',
'invoiceFooter' => 'text',
'profileImagePHID' => 'phid?',
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhortuneMerchantPHIDType::TYPECONST);
}
public function getMemberPHIDs() {
return $this->assertAttached($this->memberPHIDs);
}
public function attachMemberPHIDs(array $member_phids) {
$this->memberPHIDs = $member_phids;
return $this;
}
public function getURI() {
return '/phortune/merchant/'.$this->getID().'/';
}
public function getProfileImageURI() {
return $this->getProfileImageFile()->getBestURI();
}
public function attachProfileImageFile(PhabricatorFile $file) {
$this->profileImageFile = $file;
return $this;
}
public function getProfileImageFile() {
return $this->assertAttached($this->profileImageFile);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhortuneMerchantEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhortuneMerchantTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorPolicyInterface )----------------------------------------- */
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 hasAutomaticCapability($capability, PhabricatorUser $viewer) {
$members = array_fuse($this->getMemberPHIDs());
if (isset($members[$viewer->getPHID()])) {
return true;
}
return false;
}
public function describeAutomaticCapability($capability) {
return pht("A merchant's members an always view and edit it.");
}
}
diff --git a/src/applications/phortune/storage/PhortunePaymentProviderConfig.php b/src/applications/phortune/storage/PhortunePaymentProviderConfig.php
index b358b51d0..1e151b3fb 100644
--- a/src/applications/phortune/storage/PhortunePaymentProviderConfig.php
+++ b/src/applications/phortune/storage/PhortunePaymentProviderConfig.php
@@ -1,124 +1,113 @@
<?php
final class PhortunePaymentProviderConfig extends PhortuneDAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface {
protected $merchantPHID;
protected $providerClassKey;
protected $providerClass;
protected $isEnabled;
protected $metadata = array();
private $merchant = self::ATTACHABLE;
public static function initializeNewProvider(
PhortuneMerchant $merchant) {
return id(new PhortunePaymentProviderConfig())
->setMerchantPHID($merchant->getPHID())
->setIsEnabled(1);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'metadata' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'providerClassKey' => 'bytes12',
'providerClass' => 'text128',
'isEnabled' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_merchant' => array(
'columns' => array('merchantPHID', 'providerClassKey'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function save() {
$this->providerClassKey = PhabricatorHash::digestForIndex(
$this->providerClass);
return parent::save();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhortunePaymentProviderPHIDType::TYPECONST);
}
public function attachMerchant(PhortuneMerchant $merchant) {
$this->merchant = $merchant;
return $this;
}
public function getMerchant() {
return $this->assertAttached($this->merchant);
}
public function getMetadataValue($key, $default = null) {
return idx($this->metadata, $key, $default);
}
public function setMetadataValue($key, $value) {
$this->metadata[$key] = $value;
return $this;
}
public function buildProvider() {
return newv($this->getProviderClass(), array())
->setProviderConfig($this);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return $this->getMerchant()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getMerchant()->hasAutomaticCapability($capability, $viewer);
}
public function describeAutomaticCapability($capability) {
return pht('Providers have the policies of their merchant.');
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhortunePaymentProviderConfigEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhortunePaymentProviderConfigTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
}
diff --git a/src/applications/phortune/view/PhortuneInvoiceView.php b/src/applications/phortune/view/PhortuneInvoiceView.php
index 89ad3fd1b..65da418cc 100644
--- a/src/applications/phortune/view/PhortuneInvoiceView.php
+++ b/src/applications/phortune/view/PhortuneInvoiceView.php
@@ -1,159 +1,159 @@
<?php
final class PhortuneInvoiceView extends AphrontTagView {
private $merchantName;
private $merchantLogo;
private $merchantContact;
private $merchantFooter;
private $accountName;
private $accountContact;
private $status;
private $content;
public function setMerchantName($name) {
$this->merchantName = $name;
return $this;
}
public function setMerchantLogo($logo) {
$this->merchantLogo = $logo;
return $this;
}
public function setMerchantContact($contact) {
$this->merchantContact = $contact;
return $this;
}
public function setMerchantFooter($footer) {
$this->merchantFooter = $footer;
return $this;
}
public function setAccountName($name) {
$this->accountName = $name;
return $this;
}
public function setAccountContact($contact) {
$this->accountContact = $contact;
return $this;
}
public function setStatus($status) {
$this->status = $status;
return $this;
}
public function setContent($content) {
$this->content = $content;
return $this;
}
protected function getTagAttributes() {
$classes = array();
$classes[] = 'phortune-invoice-view';
return array(
'class' => implode(' ', $classes),
);
}
protected function getTagContent() {
require_celerity_resource('phortune-invoice-css');
$logo = phutil_tag(
'div',
array(
'class' => 'phortune-invoice-logo',
),
phutil_tag(
'img',
array(
'height' => '50',
'width' => '50',
'alt' => $this->merchantName,
'src' => $this->merchantLogo,
)));
$to_title = phutil_tag(
'div',
array(
'class' => 'phortune-mini-header',
),
- pht('To:'));
+ pht('Bill To:'));
$bill_to = phutil_tag(
'td',
array(
'class' => 'phortune-invoice-to',
'width' => '50%',
),
array(
$to_title,
phutil_tag('strong', array(), $this->accountName),
phutil_tag('br', array()),
$this->accountContact,
));
$from_title = phutil_tag(
'div',
array(
'class' => 'phortune-mini-header',
),
pht('From:'));
$bill_from = phutil_tag(
'td',
array(
'class' => 'phortune-invoice-from',
'width' => '50%',
),
array(
$from_title,
phutil_tag('strong', array(), $this->merchantName),
phutil_tag('br', array()),
$this->merchantContact,
));
$contact = phutil_tag(
'table',
array(
'class' => 'phortune-invoice-contact',
'width' => '100%',
),
phutil_tag(
'tr',
array(),
array(
$bill_to,
$bill_from,
)));
$status = null;
if ($this->status) {
$status = phutil_tag(
'div',
array(
'class' => 'phortune-invoice-status',
),
$this->status);
}
$footer = phutil_tag(
'div',
array(
'class' => 'phortune-invoice-footer',
),
$this->merchantFooter);
return array(
$logo,
$contact,
$status,
$this->content,
$footer,
);
}
}
diff --git a/src/applications/phortune/xaction/PhortuneAccountBillingAddressTransaction.php b/src/applications/phortune/xaction/PhortuneAccountBillingAddressTransaction.php
new file mode 100644
index 000000000..f4d62b1dc
--- /dev/null
+++ b/src/applications/phortune/xaction/PhortuneAccountBillingAddressTransaction.php
@@ -0,0 +1,39 @@
+<?php
+
+final class PhortuneAccountBillingAddressTransaction
+ extends PhortuneAccountTransactionType {
+
+ const TRANSACTIONTYPE = 'billing-address';
+
+ public function generateOldValue($object) {
+ return $object->getBillingAddress();
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setBillingAddress($value);
+ }
+
+ public function getTitle() {
+ return pht(
+ '%s updated the account billing address.',
+ $this->renderAuthor());
+ }
+
+ public function hasChangeDetailView() {
+ return true;
+ }
+
+ public function getMailDiffSectionHeader() {
+ return pht('CHANGES TO BILLING ADDRESS');
+ }
+
+ public function newChangeDetailView() {
+ $viewer = $this->getViewer();
+
+ return id(new PhabricatorApplicationTransactionTextDiffDetailView())
+ ->setViewer($viewer)
+ ->setOldText($this->getOldValue())
+ ->setNewText($this->getNewValue());
+ }
+
+}
diff --git a/src/applications/phortune/xaction/PhortuneAccountBillingNameTransaction.php b/src/applications/phortune/xaction/PhortuneAccountBillingNameTransaction.php
new file mode 100644
index 000000000..6c2cde6c9
--- /dev/null
+++ b/src/applications/phortune/xaction/PhortuneAccountBillingNameTransaction.php
@@ -0,0 +1,56 @@
+<?php
+
+final class PhortuneAccountBillingNameTransaction
+ extends PhortuneAccountTransactionType {
+
+ const TRANSACTIONTYPE = 'billing-name';
+
+ public function generateOldValue($object) {
+ return $object->getBillingName();
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setBillingName($value);
+ }
+
+ public function getTitle() {
+ $old = $this->getOldValue();
+ $new = $this->getNewValue();
+
+ if (strlen($old) && strlen($new)) {
+ return pht(
+ '%s changed the billing name for this account from %s to %s.',
+ $this->renderAuthor(),
+ $this->renderOldValue(),
+ $this->renderNewValue());
+ } else if (strlen($old)) {
+ return pht(
+ '%s removed the billing name for this account (was %s).',
+ $this->renderAuthor(),
+ $this->renderOldValue());
+ } else {
+ return pht(
+ '%s set the billing name for this account to %s.',
+ $this->renderAuthor(),
+ $this->renderNewValue());
+ }
+ }
+
+ public function validateTransactions($object, array $xactions) {
+ $errors = array();
+
+ $max_length = $object->getColumnMaximumByteLength('billingName');
+ foreach ($xactions as $xaction) {
+ $new_value = $xaction->getNewValue();
+ $new_length = strlen($new_value);
+ if ($new_length > $max_length) {
+ $errors[] = $this->newRequiredError(
+ pht('The billing name can be no longer than %s characters.',
+ new PhutilNumber($max_length)));
+ }
+ }
+
+ return $errors;
+ }
+
+}
diff --git a/src/applications/phrequent/engineextension/PhrequentCurtainExtension.php b/src/applications/phrequent/engineextension/PhrequentCurtainExtension.php
index 25d0e424a..ea284e9a4 100644
--- a/src/applications/phrequent/engineextension/PhrequentCurtainExtension.php
+++ b/src/applications/phrequent/engineextension/PhrequentCurtainExtension.php
@@ -1,87 +1,92 @@
<?php
final class PhrequentCurtainExtension
extends PHUICurtainExtension {
const EXTENSIONKEY = 'phrequent.time';
public function shouldEnableForObject($object) {
return ($object instanceof PhrequentTrackableInterface);
}
public function getExtensionApplication() {
return new PhabricatorPhrequentApplication();
}
public function buildCurtainPanel($object) {
$viewer = $this->getViewer();
$events = id(new PhrequentUserTimeQuery())
->setViewer($viewer)
->withObjectPHIDs(array($object->getPHID()))
->needPreemptingEvents(true)
->execute();
$event_groups = mgroup($events, 'getUserPHID');
if (!$events) {
return;
}
$handles = $viewer->loadHandles(array_keys($event_groups));
$status_view = new PHUIStatusListView();
+ $now = PhabricatorTime::getNow();
foreach ($event_groups as $user_phid => $event_group) {
$item = new PHUIStatusItemView();
$item->setTarget($handles[$user_phid]->renderLink());
$state = 'stopped';
foreach ($event_group as $event) {
if ($event->getDateEnded() === null) {
if ($event->isPreempted()) {
$state = 'suspended';
} else {
$state = 'active';
break;
}
}
}
switch ($state) {
case 'active':
$item->setIcon(
PHUIStatusItemView::ICON_CLOCK,
'green',
pht('Working Now'));
break;
case 'suspended':
$item->setIcon(
PHUIStatusItemView::ICON_CLOCK,
'yellow',
pht('Interrupted'));
break;
case 'stopped':
$item->setIcon(
PHUIStatusItemView::ICON_CLOCK,
'bluegrey',
pht('Not Working Now'));
break;
}
$block = new PhrequentTimeBlock($event_group);
- $item->setNote(
- phutil_format_relative_time(
- $block->getTimeSpentOnObject(
- $object->getPHID(),
- time())));
+
+ $duration = $block->getTimeSpentOnObject(
+ $object->getPHID(),
+ $now);
+
+ $duration_display = phutil_format_relative_time_detailed(
+ $duration,
+ $levels = 3);
+
+ $item->setNote($duration_display);
$status_view->addItem($item);
}
-
return $this->newPanel()
->setHeaderText(pht('Time Spent'))
->setOrder(40000)
->appendChild($status_view);
}
}
diff --git a/src/applications/phrequent/event/PhrequentUIEventListener.php b/src/applications/phrequent/event/PhrequentUIEventListener.php
index 9987bcfd0..e876559ef 100644
--- a/src/applications/phrequent/event/PhrequentUIEventListener.php
+++ b/src/applications/phrequent/event/PhrequentUIEventListener.php
@@ -1,151 +1,60 @@
<?php
final class PhrequentUIEventListener
extends PhabricatorEventListener {
public function register() {
$this->listen(PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS);
- $this->listen(PhabricatorEventType::TYPE_UI_WILLRENDERPROPERTIES);
}
public function handleEvent(PhutilEvent $event) {
switch ($event->getType()) {
case PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS:
$this->handleActionEvent($event);
break;
- case PhabricatorEventType::TYPE_UI_WILLRENDERPROPERTIES:
- $this->handlePropertyEvent($event);
- break;
}
}
private function handleActionEvent($event) {
$user = $event->getUser();
$object = $event->getValue('object');
if (!$object || !$object->getPHID()) {
// No object, or the object has no PHID yet..
return;
}
if (!($object instanceof PhrequentTrackableInterface)) {
// This object isn't a time trackable object.
return;
}
if (!$this->canUseApplication($event->getUser())) {
return;
}
$tracking = PhrequentUserTimeQuery::isUserTrackingObject(
$user,
$object->getPHID());
if (!$tracking) {
$track_action = id(new PhabricatorActionView())
->setName(pht('Start Tracking Time'))
->setIcon('fa-clock-o')
->setWorkflow(true)
->setHref('/phrequent/track/start/'.$object->getPHID().'/');
} else {
$track_action = id(new PhabricatorActionView())
->setName(pht('Stop Tracking Time'))
->setIcon('fa-clock-o red')
->setWorkflow(true)
->setHref('/phrequent/track/stop/'.$object->getPHID().'/');
}
if (!$user->isLoggedIn()) {
$track_action->setDisabled(true);
}
$this->addActionMenuItems($event, $track_action);
}
- private function handlePropertyEvent($ui_event) {
- $user = $ui_event->getUser();
- $object = $ui_event->getValue('object');
-
- if (!$object || !$object->getPHID()) {
- // No object, or the object has no PHID yet..
- return;
- }
-
- if (!($object instanceof PhrequentTrackableInterface)) {
- // This object isn't a time trackable object.
- return;
- }
-
- if (!$this->canUseApplication($ui_event->getUser())) {
- return;
- }
-
- $events = id(new PhrequentUserTimeQuery())
- ->setViewer($user)
- ->withObjectPHIDs(array($object->getPHID()))
- ->needPreemptingEvents(true)
- ->execute();
- $event_groups = mgroup($events, 'getUserPHID');
-
- if (!$events) {
- return;
- }
-
- $handles = id(new PhabricatorHandleQuery())
- ->setViewer($user)
- ->withPHIDs(array_keys($event_groups))
- ->execute();
-
- $status_view = new PHUIStatusListView();
-
- foreach ($event_groups as $user_phid => $event_group) {
- $item = new PHUIStatusItemView();
- $item->setTarget($handles[$user_phid]->renderLink());
-
- $state = 'stopped';
- foreach ($event_group as $event) {
- if ($event->getDateEnded() === null) {
- if ($event->isPreempted()) {
- $state = 'suspended';
- } else {
- $state = 'active';
- break;
- }
- }
- }
-
- switch ($state) {
- case 'active':
- $item->setIcon(
- PHUIStatusItemView::ICON_CLOCK,
- 'green',
- pht('Working Now'));
- break;
- case 'suspended':
- $item->setIcon(
- PHUIStatusItemView::ICON_CLOCK,
- 'yellow',
- pht('Interrupted'));
- break;
- case 'stopped':
- $item->setIcon(
- PHUIStatusItemView::ICON_CLOCK,
- 'bluegrey',
- pht('Not Working Now'));
- break;
- }
-
- $block = new PhrequentTimeBlock($event_group);
- $item->setNote(
- phutil_format_relative_time(
- $block->getTimeSpentOnObject(
- $object->getPHID(),
- time())));
-
- $status_view->addItem($item);
- }
-
- $view = $ui_event->getValue('view');
- $view->addProperty(pht('Time Spent'), $status_view);
- }
-
}
diff --git a/src/applications/phriction/config/PhabricatorPhrictionConfigOptions.php b/src/applications/phriction/config/PhabricatorPhrictionConfigOptions.php
deleted file mode 100644
index 9fada90d3..000000000
--- a/src/applications/phriction/config/PhabricatorPhrictionConfigOptions.php
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-
-final class PhabricatorPhrictionConfigOptions
- extends PhabricatorApplicationConfigOptions {
-
- public function getName() {
- return pht('Phriction');
- }
-
- public function getDescription() {
- return pht('Options related to Phriction (wiki).');
- }
-
- public function getIcon() {
- return 'fa-book';
- }
-
- public function getGroup() {
- return 'apps';
- }
-
- public function getOptions() {
- return array(
- $this->newOption(
- 'metamta.phriction.subject-prefix', 'string', '[Phriction]')
- ->setDescription(pht('Subject prefix for Phriction email.')),
- );
- }
-
-}
diff --git a/src/applications/phriction/editor/PhrictionTransactionEditor.php b/src/applications/phriction/editor/PhrictionTransactionEditor.php
index 6d72fc5b8..d09ff5d55 100644
--- a/src/applications/phriction/editor/PhrictionTransactionEditor.php
+++ b/src/applications/phriction/editor/PhrictionTransactionEditor.php
@@ -1,574 +1,574 @@
<?php
final class PhrictionTransactionEditor
extends PhabricatorApplicationTransactionEditor {
const VALIDATE_CREATE_ANCESTRY = 'create';
const VALIDATE_MOVE_ANCESTRY = 'move';
private $description;
private $oldContent;
private $newContent;
private $moveAwayDocument;
private $skipAncestorCheck;
private $contentVersion;
private $processContentVersionError = true;
private $contentDiffURI;
public function setDescription($description) {
$this->description = $description;
return $this;
}
private function getDescription() {
return $this->description;
}
private function setOldContent(PhrictionContent $content) {
$this->oldContent = $content;
return $this;
}
public function getOldContent() {
return $this->oldContent;
}
private function setNewContent(PhrictionContent $content) {
$this->newContent = $content;
return $this;
}
public function getNewContent() {
return $this->newContent;
}
public function setSkipAncestorCheck($bool) {
$this->skipAncestorCheck = $bool;
return $this;
}
public function getSkipAncestorCheck() {
return $this->skipAncestorCheck;
}
public function setContentVersion($version) {
$this->contentVersion = $version;
return $this;
}
public function getContentVersion() {
return $this->contentVersion;
}
public function setProcessContentVersionError($process) {
$this->processContentVersionError = $process;
return $this;
}
public function getProcessContentVersionError() {
return $this->processContentVersionError;
}
public function setMoveAwayDocument(PhrictionDocument $document) {
$this->moveAwayDocument = $document;
return $this;
}
public function setShouldPublishContent(
PhrictionDocument $object,
$publish) {
if ($publish) {
$content_phid = $this->getNewContent()->getPHID();
} else {
$content_phid = $this->getOldContent()->getPHID();
}
$object->setContentPHID($content_phid);
return $this;
}
public function getEditorApplicationClass() {
return 'PhabricatorPhrictionApplication';
}
public function getEditorObjectsDescription() {
return pht('Phriction Documents');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
protected function expandTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$this->setOldContent($object->getContent());
return parent::expandTransactions($object, $xactions);
}
protected function expandTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$xactions = parent::expandTransaction($object, $xaction);
switch ($xaction->getTransactionType()) {
case PhrictionDocumentContentTransaction::TRANSACTIONTYPE:
if ($this->getIsNewObject()) {
break;
}
$content = $xaction->getNewValue();
if ($content === '') {
$xactions[] = id(new PhrictionTransaction())
->setTransactionType(
PhrictionDocumentDeleteTransaction::TRANSACTIONTYPE)
->setNewValue(true)
->setMetadataValue('contentDelete', true);
}
break;
case PhrictionDocumentMoveToTransaction::TRANSACTIONTYPE:
$document = $xaction->getNewValue();
$xactions[] = id(new PhrictionTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue($document->getViewPolicy());
$xactions[] = id(new PhrictionTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY)
->setNewValue($document->getEditPolicy());
break;
default:
break;
}
return $xactions;
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
if ($this->hasNewDocumentContent()) {
$content = $this->getNewDocumentContent($object);
$content
->setDocumentPHID($object->getPHID())
->save();
}
if ($this->getIsNewObject() && !$this->getSkipAncestorCheck()) {
// Stub out empty parent documents if they don't exist
$ancestral_slugs = PhabricatorSlug::getAncestry($object->getSlug());
if ($ancestral_slugs) {
$ancestors = id(new PhrictionDocumentQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withSlugs($ancestral_slugs)
->needContent(true)
->execute();
$ancestors = mpull($ancestors, null, 'getSlug');
$stub_type = PhrictionChangeType::CHANGE_STUB;
foreach ($ancestral_slugs as $slug) {
$ancestor_doc = idx($ancestors, $slug);
// We check for change type to prevent near-infinite recursion
if (!$ancestor_doc && $content->getChangeType() != $stub_type) {
$ancestor_doc = PhrictionDocument::initializeNewDocument(
$this->getActor(),
$slug);
$stub_xactions = array();
$stub_xactions[] = id(new PhrictionTransaction())
->setTransactionType(
PhrictionDocumentTitleTransaction::TRANSACTIONTYPE)
->setNewValue(PhabricatorSlug::getDefaultTitle($slug))
->setMetadataValue('stub:create:phid', $object->getPHID());
$stub_xactions[] = id(new PhrictionTransaction())
->setTransactionType(
PhrictionDocumentContentTransaction::TRANSACTIONTYPE)
->setNewValue('')
->setMetadataValue('stub:create:phid', $object->getPHID());
$stub_xactions[] = id(new PhrictionTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue($object->getViewPolicy());
$stub_xactions[] = id(new PhrictionTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY)
->setNewValue($object->getEditPolicy());
$sub_editor = id(new PhrictionTransactionEditor())
->setActor($this->getActor())
->setContentSource($this->getContentSource())
->setContinueOnNoEffect($this->getContinueOnNoEffect())
->setSkipAncestorCheck(true)
->setDescription(pht('Empty Parent Document'))
->applyTransactions($ancestor_doc, $stub_xactions);
}
}
}
}
if ($this->moveAwayDocument !== null) {
$move_away_xactions = array();
$move_away_xactions[] = id(new PhrictionTransaction())
->setTransactionType(
PhrictionDocumentMoveAwayTransaction::TRANSACTIONTYPE)
->setNewValue($object);
$sub_editor = id(new PhrictionTransactionEditor())
->setActor($this->getActor())
->setContentSource($this->getContentSource())
->setContinueOnNoEffect($this->getContinueOnNoEffect())
->setDescription($this->getDescription())
->applyTransactions($this->moveAwayDocument, $move_away_xactions);
}
// Compute the content diff URI for the publishing phase.
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhrictionDocumentContentTransaction::TRANSACTIONTYPE:
$uri = id(new PhutilURI('/phriction/diff/'.$object->getID().'/'))
->alter('l', $this->getOldContent()->getVersion())
->alter('r', $this->getNewContent()->getVersion());
$this->contentDiffURI = (string)$uri;
break 2;
default:
break;
}
}
return $xactions;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailSubjectPrefix() {
return '[Phriction]';
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$this->getActingAsPHID(),
);
}
public function getMailTagsMap() {
return array(
PhrictionTransaction::MAILTAG_TITLE =>
pht("A document's title changes."),
PhrictionTransaction::MAILTAG_CONTENT =>
pht("A document's content changes."),
PhrictionTransaction::MAILTAG_DELETE =>
pht('A document is deleted.'),
PhrictionTransaction::MAILTAG_SUBSCRIBERS =>
pht('A document\'s subscribers change.'),
PhrictionTransaction::MAILTAG_OTHER =>
pht('Other document activity not listed above occurs.'),
);
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new PhrictionReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$title = $object->getContent()->getTitle();
return id(new PhabricatorMetaMTAMail())
->setSubject($title);
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
if ($this->getIsNewObject()) {
$body->addRemarkupSection(
pht('DOCUMENT CONTENT'),
$object->getContent()->getContent());
} else if ($this->contentDiffURI) {
$body->addLinkSection(
pht('DOCUMENT DIFF'),
PhabricatorEnv::getProductionURI($this->contentDiffURI));
}
$description = $object->getContent()->getDescription();
if (strlen($description)) {
$body->addTextSection(
pht('EDIT NOTES'),
$description);
}
$body->addLinkSection(
pht('DOCUMENT DETAIL'),
PhabricatorEnv::getProductionURI(
PhrictionDocument::getSlugURI($object->getSlug())));
return $body;
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return $this->shouldSendMail($object, $xactions);
}
protected function getFeedRelatedPHIDs(
PhabricatorLiskDAO $object,
array $xactions) {
$phids = parent::getFeedRelatedPHIDs($object, $xactions);
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhrictionDocumentMoveToTransaction::TRANSACTIONTYPE:
$dict = $xaction->getNewValue();
$phids[] = $dict['phid'];
break;
}
}
return $phids;
}
protected function validateTransaction(
PhabricatorLiskDAO $object,
$type,
array $xactions) {
$errors = parent::validateTransaction($object, $type, $xactions);
foreach ($xactions as $xaction) {
switch ($type) {
case PhrictionDocumentContentTransaction::TRANSACTIONTYPE:
if ($xaction->getMetadataValue('stub:create:phid')) {
- continue;
+ break;
}
if ($this->getProcessContentVersionError()) {
$error = $this->validateContentVersion($object, $type, $xaction);
if ($error) {
$this->setProcessContentVersionError(false);
$errors[] = $error;
}
}
if ($this->getIsNewObject()) {
$ancestry_errors = $this->validateAncestry(
$object,
$type,
$xaction,
self::VALIDATE_CREATE_ANCESTRY);
if ($ancestry_errors) {
$errors = array_merge($errors, $ancestry_errors);
}
}
break;
case PhrictionDocumentMoveToTransaction::TRANSACTIONTYPE:
$source_document = $xaction->getNewValue();
$ancestry_errors = $this->validateAncestry(
$object,
$type,
$xaction,
self::VALIDATE_MOVE_ANCESTRY);
if ($ancestry_errors) {
$errors = array_merge($errors, $ancestry_errors);
}
$target_document = id(new PhrictionDocumentQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withSlugs(array($object->getSlug()))
->needContent(true)
->executeOne();
// Prevent overwrites and no-op moves.
$exists = PhrictionDocumentStatus::STATUS_EXISTS;
if ($target_document) {
$message = null;
if ($target_document->getSlug() == $source_document->getSlug()) {
$message = pht(
'You can not move a document to its existing location. '.
'Choose a different location to move the document to.');
} else if ($target_document->getStatus() == $exists) {
$message = pht(
'You can not move this document there, because it would '.
'overwrite an existing document which is already at that '.
'location. Move or delete the existing document first.');
}
if ($message !== null) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Invalid'),
$message,
$xaction);
$errors[] = $error;
}
}
break;
}
}
return $errors;
}
public function validateAncestry(
PhabricatorLiskDAO $object,
$type,
PhabricatorApplicationTransaction $xaction,
$verb) {
$errors = array();
// NOTE: We use the omnipotent user for these checks because policy
// doesn't matter; existence does.
$other_doc_viewer = PhabricatorUser::getOmnipotentUser();
$ancestral_slugs = PhabricatorSlug::getAncestry($object->getSlug());
if ($ancestral_slugs) {
$ancestors = id(new PhrictionDocumentQuery())
->setViewer($other_doc_viewer)
->withSlugs($ancestral_slugs)
->execute();
$ancestors = mpull($ancestors, null, 'getSlug');
foreach ($ancestral_slugs as $slug) {
$ancestor_doc = idx($ancestors, $slug);
if (!$ancestor_doc) {
$create_uri = '/phriction/edit/?slug='.$slug;
$create_link = phutil_tag(
'a',
array(
'href' => $create_uri,
),
$slug);
switch ($verb) {
case self::VALIDATE_MOVE_ANCESTRY:
$message = pht(
'Can not move document because the parent document with '.
'slug %s does not exist!',
$create_link);
break;
case self::VALIDATE_CREATE_ANCESTRY:
$message = pht(
'Can not create document because the parent document with '.
'slug %s does not exist!',
$create_link);
break;
}
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Missing Ancestor'),
$message,
$xaction);
$errors[] = $error;
}
}
}
return $errors;
}
private function validateContentVersion(
PhabricatorLiskDAO $object,
$type,
PhabricatorApplicationTransaction $xaction) {
$error = null;
if ($this->getContentVersion() &&
($object->getMaxVersion() != $this->getContentVersion())) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
pht('Edit Conflict'),
pht(
'Another user made changes to this document after you began '.
'editing it. Do you want to overwrite their changes? '.
'(If you choose to overwrite their changes, you should review '.
'the document edit history to see what you overwrote, and '.
'then make another edit to merge the changes if necessary.)'),
$xaction);
}
return $error;
}
protected function supportsSearch() {
return true;
}
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
return id(new PhrictionDocumentHeraldAdapter())
->setDocument($object);
}
private function hasNewDocumentContent() {
return (bool)$this->newContent;
}
public function getNewDocumentContent(PhrictionDocument $document) {
if (!$this->hasNewDocumentContent()) {
$content = $this->newDocumentContent($document);
// Generate a PHID now so we can populate "contentPHID" before saving
// the document to the database: the column is not nullable so we need
// a value.
$content_phid = $content->generatePHID();
$content->setPHID($content_phid);
$document->setContentPHID($content_phid);
$document->attachContent($content);
$document->setEditedEpoch(PhabricatorTime::getNow());
$document->setMaxVersion($content->getVersion());
$this->newContent = $content;
}
return $this->newContent;
}
private function newDocumentContent(PhrictionDocument $document) {
$content = id(new PhrictionContent())
->setSlug($document->getSlug())
->setAuthorPHID($this->getActingAsPHID())
->setChangeType(PhrictionChangeType::CHANGE_EDIT)
->setTitle($this->getOldContent()->getTitle())
->setContent($this->getOldContent()->getContent())
->setDescription('');
if (strlen($this->getDescription())) {
$content->setDescription($this->getDescription());
}
$content->setVersion($document->getMaxVersion() + 1);
return $content;
}
protected function getCustomWorkerState() {
return array(
'contentDiffURI' => $this->contentDiffURI,
);
}
protected function loadCustomWorkerState(array $state) {
$this->contentDiffURI = idx($state, 'contentDiffURI');
return $this;
}
}
diff --git a/src/applications/phriction/storage/PhrictionDocument.php b/src/applications/phriction/storage/PhrictionDocument.php
index b6dcd6d56..9f8a4a475 100644
--- a/src/applications/phriction/storage/PhrictionDocument.php
+++ b/src/applications/phriction/storage/PhrictionDocument.php
@@ -1,331 +1,320 @@
<?php
final class PhrictionDocument extends PhrictionDAO
implements
PhabricatorPolicyInterface,
PhabricatorSubscribableInterface,
PhabricatorFlaggableInterface,
PhabricatorTokenReceiverInterface,
PhabricatorDestructibleInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface,
PhabricatorProjectInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorConduitResultInterface,
PhabricatorPolicyCodexInterface,
PhabricatorSpacesInterface {
protected $slug;
protected $depth;
protected $contentPHID;
protected $status;
protected $viewPolicy;
protected $editPolicy;
protected $spacePHID;
protected $editedEpoch;
protected $maxVersion;
private $contentObject = self::ATTACHABLE;
private $ancestors = array();
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_COLUMN_SCHEMA => array(
'slug' => 'sort128',
'depth' => 'uint32',
'status' => 'text32',
'editedEpoch' => 'epoch',
'maxVersion' => 'uint32',
),
self::CONFIG_KEY_SCHEMA => array(
'slug' => array(
'columns' => array('slug'),
'unique' => true,
),
'depth' => array(
'columns' => array('depth', 'slug'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function getPHIDType() {
return PhrictionDocumentPHIDType::TYPECONST;
}
public static function initializeNewDocument(PhabricatorUser $actor, $slug) {
$document = id(new self())
->setSlug($slug);
$content = id(new PhrictionContent())
->setSlug($slug);
$default_title = PhabricatorSlug::getDefaultTitle($slug);
$content->setTitle($default_title);
$document->attachContent($content);
$parent_doc = null;
$ancestral_slugs = PhabricatorSlug::getAncestry($slug);
if ($ancestral_slugs) {
$parent = end($ancestral_slugs);
$parent_doc = id(new PhrictionDocumentQuery())
->setViewer($actor)
->withSlugs(array($parent))
->executeOne();
}
if ($parent_doc) {
$document
->setViewPolicy($parent_doc->getViewPolicy())
->setEditPolicy($parent_doc->getEditPolicy())
->setSpacePHID($parent_doc->getSpacePHID());
} else {
$default_view_policy = PhabricatorPolicies::getMostOpenPolicy();
$document
->setViewPolicy($default_view_policy)
->setEditPolicy(PhabricatorPolicies::POLICY_USER)
->setSpacePHID($actor->getDefaultSpacePHID());
}
$document->setEditedEpoch(PhabricatorTime::getNow());
$document->setMaxVersion(0);
return $document;
}
public static function getSlugURI($slug, $type = 'document') {
static $types = array(
'document' => '/w/',
'history' => '/phriction/history/',
);
if (empty($types[$type])) {
throw new Exception(pht("Unknown URI type '%s'!", $type));
}
$prefix = $types[$type];
if ($slug == '/') {
return $prefix;
} else {
// NOTE: The effect here is to escape non-latin characters, since modern
// browsers deal with escaped UTF8 characters in a reasonable way (showing
// the user a readable URI) but older programs may not.
$slug = phutil_escape_uri($slug);
return $prefix.$slug;
}
}
public function setSlug($slug) {
$this->slug = PhabricatorSlug::normalize($slug);
$this->depth = PhabricatorSlug::getDepth($slug);
return $this;
}
public function attachContent(PhrictionContent $content) {
$this->contentObject = $content;
return $this;
}
public function getContent() {
return $this->assertAttached($this->contentObject);
}
public function getAncestors() {
return $this->ancestors;
}
public function getAncestor($slug) {
return $this->assertAttachedKey($this->ancestors, $slug);
}
public function attachAncestor($slug, $ancestor) {
$this->ancestors[$slug] = $ancestor;
return $this;
}
public function getURI() {
return self::getSlugURI($this->getSlug());
}
/* -( Status )------------------------------------------------------------- */
public function getStatusObject() {
return PhrictionDocumentStatus::newStatusObject($this->getStatus());
}
public function getStatusIcon() {
return $this->getStatusObject()->getIcon();
}
public function getStatusColor() {
return $this->getStatusObject()->getColor();
}
public function getStatusDisplayName() {
return $this->getStatusObject()->getDisplayName();
}
public function isActive() {
return $this->getStatusObject()->isActive();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
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 $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
return false;
}
/* -( PhabricatorSpacesInterface )----------------------------------------- */
public function getSpacePHID() {
return $this->spacePHID;
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return false;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhrictionTransactionEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhrictionTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return PhabricatorSubscribersQuery::loadSubscribersForPHID($this->phid);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$contents = id(new PhrictionContentQuery())
->setViewer($engine->getViewer())
->withDocumentPHIDs(array($this->getPHID()))
->execute();
foreach ($contents as $content) {
$engine->destroyObject($content);
}
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PhrictionDocumentFulltextEngine();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new PhrictionDocumentFerretEngine();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('path')
->setType('string')
->setDescription(pht('The path to the document.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('status')
->setType('map<string, wild>')
->setDescription(pht('Status information about the document.')),
);
}
public function getFieldValuesForConduit() {
$status = array(
'value' => $this->getStatus(),
'name' => $this->getStatusDisplayName(),
);
return array(
'path' => $this->getSlug(),
'status' => $status,
);
}
public function getConduitSearchAttachments() {
return array(
id(new PhrictionContentSearchEngineAttachment())
->setAttachmentKey('content'),
);
}
/* -( PhabricatorPolicyCodexInterface )------------------------------------ */
public function newPolicyCodex() {
return new PhrictionDocumentPolicyCodex();
}
}
diff --git a/src/applications/phurl/storage/PhabricatorPhurlURL.php b/src/applications/phurl/storage/PhabricatorPhurlURL.php
index c36bf6e8c..4f3ee36de 100644
--- a/src/applications/phurl/storage/PhabricatorPhurlURL.php
+++ b/src/applications/phurl/storage/PhabricatorPhurlURL.php
@@ -1,258 +1,248 @@
<?php
final class PhabricatorPhurlURL extends PhabricatorPhurlDAO
implements PhabricatorPolicyInterface,
PhabricatorProjectInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorSubscribableInterface,
PhabricatorTokenReceiverInterface,
PhabricatorDestructibleInterface,
PhabricatorMentionableInterface,
PhabricatorFlaggableInterface,
PhabricatorSpacesInterface,
PhabricatorConduitResultInterface,
PhabricatorNgramsInterface {
protected $name;
protected $alias;
protected $longURL;
protected $description;
protected $viewPolicy;
protected $editPolicy;
protected $authorPHID;
protected $spacePHID;
protected $mailKey;
const DEFAULT_ICON = 'fa-compress';
public static function initializeNewPhurlURL(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorPhurlApplication'))
->executeOne();
return id(new PhabricatorPhurlURL())
->setAuthorPHID($actor->getPHID())
->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy())
->setEditPolicy($actor->getPHID())
->setSpacePHID($actor->getDefaultSpacePHID());
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text',
'alias' => 'sort64?',
'longURL' => 'text',
'description' => 'text',
'mailKey' => 'bytes20',
),
self::CONFIG_KEY_SCHEMA => array(
'key_instance' => array(
'columns' => array('alias'),
'unique' => true,
),
'key_author' => array(
'columns' => array('authorPHID'),
),
),
) + parent::getConfiguration();
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPhurlURLPHIDType::TYPECONST);
}
public function getMonogram() {
return 'U'.$this->getID();
}
public function getURI() {
$uri = '/'.$this->getMonogram();
return $uri;
}
public function isValid() {
$allowed_protocols = PhabricatorEnv::getEnvConfig('uri.allowed-protocols');
$uri = new PhutilURI($this->getLongURL());
return isset($allowed_protocols[$uri->getProtocol()]);
}
public function getDisplayName() {
if ($this->getName()) {
return $this->getName();
} else {
return $this->getLongURL();
}
}
public function getRedirectURI() {
if (strlen($this->getAlias())) {
$path = '/u/'.$this->getAlias();
} else {
$path = '/u/'.$this->getID();
}
$domain = PhabricatorEnv::getEnvConfig('phurl.short-uri');
if (!$domain) {
$domain = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
}
$uri = new PhutilURI($domain);
$uri->setPath($path);
return (string)$uri;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
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 $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
$user_phid = $this->getAuthorPHID();
if ($user_phid) {
$viewer_phid = $viewer->getPHID();
if ($viewer_phid == $user_phid) {
return true;
}
}
return false;
}
public function describeAutomaticCapability($capability) {
return pht('The owner of a URL can always view and edit it.');
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorPhurlURLEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorPhurlURLTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return ($phid == $this->getAuthorPHID());
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array($this->getAuthorPHID());
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorSpacesInterface )----------------------------------------- */
public function getSpacePHID() {
return $this->spacePHID;
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('URL name.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('alias')
->setType('string')
->setDescription(pht('The alias for the URL.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('longurl')
->setType('string')
->setDescription(pht('The pre-shortened URL.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('description')
->setType('string')
->setDescription(pht('A description of the URL.')),
);
}
public function getFieldValuesForConduit() {
return array(
'name' => $this->getName(),
'alias' => $this->getAlias(),
'description' => $this->getDescription(),
'urls' => array(
'long' => $this->getLongURL(),
'short' => $this->getRedirectURI(),
),
);
}
public function getConduitSearchAttachments() {
return array();
}
/* -( PhabricatorNgramInterface )------------------------------------------ */
public function newNgrams() {
return array(
id(new PhabricatorPhurlURLNameNgrams())
->setValue($this->getName()),
);
}
}
diff --git a/src/applications/ponder/controller/PonderAnswerCommentController.php b/src/applications/ponder/controller/PonderAnswerCommentController.php
index 4b60fb939..3d3e3a139 100644
--- a/src/applications/ponder/controller/PonderAnswerCommentController.php
+++ b/src/applications/ponder/controller/PonderAnswerCommentController.php
@@ -1,62 +1,63 @@
<?php
final class PonderAnswerCommentController extends PonderController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
if (!$request->isFormPost()) {
return new Aphront400Response();
}
$answer = id(new PonderAnswerQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$answer) {
return new Aphront404Response();
}
$is_preview = $request->isPreviewRequest();
$qid = $answer->getQuestion()->getID();
$aid = $answer->getID();
// TODO, this behaves badly when redirecting to the answer
$view_uri = "/Q{$qid}";
$xactions = array();
$xactions[] = id(new PonderAnswerTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new PonderAnswerTransactionComment())
->setContent($request->getStr('comment')));
$editor = id(new PonderAnswerEditor())
->setActor($viewer)
->setContinueOnNoEffect($request->isContinueRequest())
->setContentSourceFromRequest($request)
->setIsPreview($is_preview);
try {
$xactions = $editor->applyTransactions($answer, $xactions);
} catch (PhabricatorApplicationTransactionNoEffectException $ex) {
return id(new PhabricatorApplicationTransactionNoEffectResponse())
->setCancelURI($view_uri)
->setException($ex);
}
if ($request->isAjax() && $is_preview) {
return id(new PhabricatorApplicationTransactionResponse())
+ ->setObject($answer)
->setViewer($viewer)
->setTransactions($xactions)
->setIsPreview($is_preview);
} else {
return id(new AphrontRedirectResponse())
->setURI($view_uri);
}
}
}
diff --git a/src/applications/ponder/controller/PonderQuestionCommentController.php b/src/applications/ponder/controller/PonderQuestionCommentController.php
index 84c276cd5..d50cd637c 100644
--- a/src/applications/ponder/controller/PonderQuestionCommentController.php
+++ b/src/applications/ponder/controller/PonderQuestionCommentController.php
@@ -1,58 +1,59 @@
<?php
final class PonderQuestionCommentController extends PonderController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
if (!$request->isFormPost()) {
return new Aphront400Response();
}
$question = id(new PonderQuestionQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$question) {
return new Aphront404Response();
}
$is_preview = $request->isPreviewRequest();
$qid = $question->getID();
$view_uri = "/Q{$qid}";
$xactions = array();
$xactions[] = id(new PonderQuestionTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new PonderQuestionTransactionComment())
->setContent($request->getStr('comment')));
$editor = id(new PonderQuestionEditor())
->setActor($viewer)
->setContinueOnNoEffect($request->isContinueRequest())
->setContentSourceFromRequest($request)
->setIsPreview($is_preview);
try {
$xactions = $editor->applyTransactions($question, $xactions);
} catch (PhabricatorApplicationTransactionNoEffectException $ex) {
return id(new PhabricatorApplicationTransactionNoEffectResponse())
->setCancelURI($view_uri)
->setException($ex);
}
if ($request->isAjax() && $is_preview) {
return id(new PhabricatorApplicationTransactionResponse())
+ ->setObject($question)
->setViewer($viewer)
->setTransactions($xactions)
->setIsPreview($is_preview);
} else {
return id(new AphrontRedirectResponse())
->setURI($view_uri);
}
}
}
diff --git a/src/applications/ponder/mail/PonderAnswerMailReceiver.php b/src/applications/ponder/mail/PonderAnswerMailReceiver.php
index d7269ac86..dfa252c4a 100644
--- a/src/applications/ponder/mail/PonderAnswerMailReceiver.php
+++ b/src/applications/ponder/mail/PonderAnswerMailReceiver.php
@@ -1,27 +1,27 @@
<?php
final class PonderAnswerMailReceiver extends PhabricatorObjectMailReceiver {
public function isEnabled() {
$app_class = 'PhabricatorPonderApplication';
return PhabricatorApplication::isClassInstalled($app_class);
}
protected function getObjectPattern() {
return 'ANSR[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
- $id = (int)trim($pattern, 'ANSR');
+ $id = (int)substr($pattern, 4);
return id(new PonderAnswerQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
}
protected function getTransactionReplyHandler() {
return new PonderAnswerReplyHandler();
}
}
diff --git a/src/applications/ponder/mail/PonderQuestionCreateMailReceiver.php b/src/applications/ponder/mail/PonderQuestionCreateMailReceiver.php
index 4176e87f6..32669855e 100644
--- a/src/applications/ponder/mail/PonderQuestionCreateMailReceiver.php
+++ b/src/applications/ponder/mail/PonderQuestionCreateMailReceiver.php
@@ -1,49 +1,44 @@
<?php
-final class PonderQuestionCreateMailReceiver extends PhabricatorMailReceiver {
+final class PonderQuestionCreateMailReceiver
+ extends PhabricatorApplicationMailReceiver {
- public function isEnabled() {
- $app_class = 'PhabricatorPonderApplication';
- return PhabricatorApplication::isClassInstalled($app_class);
- }
-
- public function canAcceptMail(PhabricatorMetaMTAReceivedMail $mail) {
- $ponder_app = new PhabricatorPonderApplication();
- return $this->canAcceptApplicationMail($ponder_app, $mail);
+ protected function newApplication() {
+ return new PhabricatorPonderApplication();
}
protected function processReceivedMail(
PhabricatorMetaMTAReceivedMail $mail,
- PhabricatorUser $sender) {
+ PhutilEmailAddress $target) {
+ $author = $this->getAuthor();
$title = $mail->getSubject();
if (!strlen($title)) {
$title = pht('New Question');
}
$xactions = array();
$xactions[] = id(new PonderQuestionTransaction())
->setTransactionType(PonderQuestionTransaction::TYPE_TITLE)
->setNewValue($title);
$xactions[] = id(new PonderQuestionTransaction())
->setTransactionType(PonderQuestionTransaction::TYPE_CONTENT)
->setNewValue($mail->getCleanTextBody());
- $question = PonderQuestion::initializeNewQuestion($sender);
+ $question = PonderQuestion::initializeNewQuestion($author);
$content_source = $mail->newContentSource();
$editor = id(new PonderQuestionEditor())
- ->setActor($sender)
+ ->setActor($author)
->setContentSource($content_source)
->setContinueOnNoEffect(true);
$xactions = $editor->applyTransactions($question, $xactions);
$mail->setRelatedPHID($question->getPHID());
-
}
}
diff --git a/src/applications/ponder/mail/PonderQuestionMailReceiver.php b/src/applications/ponder/mail/PonderQuestionMailReceiver.php
index 6388837af..e68d6c0ec 100644
--- a/src/applications/ponder/mail/PonderQuestionMailReceiver.php
+++ b/src/applications/ponder/mail/PonderQuestionMailReceiver.php
@@ -1,27 +1,27 @@
<?php
final class PonderQuestionMailReceiver extends PhabricatorObjectMailReceiver {
public function isEnabled() {
$app_class = 'PhabricatorPonderApplication';
return PhabricatorApplication::isClassInstalled($app_class);
}
protected function getObjectPattern() {
return 'Q[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
- $id = (int)trim($pattern, 'Q');
+ $id = (int)substr($pattern, 1);
return id(new PonderQuestionQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
}
protected function getTransactionReplyHandler() {
return new PonderQuestionReplyHandler();
}
}
diff --git a/src/applications/ponder/storage/PonderAnswer.php b/src/applications/ponder/storage/PonderAnswer.php
index f9e3e8eb8..9179e22a4 100644
--- a/src/applications/ponder/storage/PonderAnswer.php
+++ b/src/applications/ponder/storage/PonderAnswer.php
@@ -1,233 +1,222 @@
<?php
final class PonderAnswer extends PonderDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorMarkupInterface,
PhabricatorPolicyInterface,
PhabricatorFlaggableInterface,
PhabricatorSubscribableInterface,
PhabricatorDestructibleInterface {
const MARKUP_FIELD_CONTENT = 'markup:content';
protected $authorPHID;
protected $questionID;
protected $content;
protected $mailKey;
protected $status;
protected $voteCount;
private $question = self::ATTACHABLE;
private $comments;
public static function initializeNewAnswer(
PhabricatorUser $actor,
PonderQuestion $question) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorPonderApplication'))
->executeOne();
return id(new PonderAnswer())
->setQuestionID($question->getID())
->setContent('')
->attachQuestion($question)
->setAuthorPHID($actor->getPHID())
->setVoteCount(0)
->setStatus(PonderAnswerStatus::ANSWER_STATUS_VISIBLE);
}
public function attachQuestion(PonderQuestion $question = null) {
$this->question = $question;
return $this;
}
public function getQuestion() {
return $this->assertAttached($this->question);
}
public function getURI() {
return '/Q'.$this->getQuestionID().'#A'.$this->getID();
}
public function setComments($comments) {
$this->comments = $comments;
return $this;
}
public function getComments() {
return $this->comments;
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'voteCount' => 'sint32',
'content' => 'text',
'status' => 'text32',
'mailKey' => 'bytes20',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'key_oneanswerperquestion' => array(
'columns' => array('questionID', 'authorPHID'),
'unique' => true,
),
'questionID' => array(
'columns' => array('questionID'),
),
'authorPHID' => array(
'columns' => array('authorPHID'),
),
'status' => array(
'columns' => array('status'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(PonderAnswerPHIDType::TYPECONST);
}
public function getMarkupField() {
return self::MARKUP_FIELD_CONTENT;
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PonderAnswerEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PonderAnswerTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
// Markup interface
public function getMarkupFieldKey($field) {
$content = $this->getMarkupText($field);
return PhabricatorMarkupEngine::digestRemarkupContent($this, $content);
}
public function getMarkupText($field) {
return $this->getContent();
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::getEngine();
}
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
public function shouldUseMarkupCache($field) {
return (bool)$this->getID();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getQuestion()->getPolicy($capability);
case PhabricatorPolicyCapability::CAN_EDIT:
$app = PhabricatorApplication::getByClass(
'PhabricatorPonderApplication');
return $app->getPolicy(PonderModerateCapability::CAPABILITY);
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
if ($this->getAuthorPHID() == $viewer->getPHID()) {
return true;
}
return $this->getQuestion()->hasAutomaticCapability(
$capability,
$viewer);
case PhabricatorPolicyCapability::CAN_EDIT:
return ($this->getAuthorPHID() == $viewer->getPHID());
}
}
public function describeAutomaticCapability($capability) {
$out = array();
$out[] = pht('The author of an answer can always view and edit it.');
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$out[] = pht(
'The user who asks a question can always view the answers.');
$out[] = pht(
'A moderator can always view the answers.');
break;
}
return $out;
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return ($phid == $this->getAuthorPHID());
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/ponder/storage/PonderQuestion.php b/src/applications/ponder/storage/PonderQuestion.php
index 17f7ee3fd..40ec9fbee 100644
--- a/src/applications/ponder/storage/PonderQuestion.php
+++ b/src/applications/ponder/storage/PonderQuestion.php
@@ -1,308 +1,297 @@
<?php
final class PonderQuestion extends PonderDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorMarkupInterface,
PhabricatorSubscribableInterface,
PhabricatorFlaggableInterface,
PhabricatorPolicyInterface,
PhabricatorTokenReceiverInterface,
PhabricatorProjectInterface,
PhabricatorDestructibleInterface,
PhabricatorSpacesInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface {
const MARKUP_FIELD_CONTENT = 'markup:content';
protected $title;
protected $phid;
protected $authorPHID;
protected $status;
protected $content;
protected $answerWiki;
protected $contentSource;
protected $viewPolicy;
protected $spacePHID;
protected $answerCount;
protected $mailKey;
private $answers;
private $comments;
private $projectPHIDs = self::ATTACHABLE;
public static function initializeNewQuestion(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorPonderApplication'))
->executeOne();
$view_policy = $app->getPolicy(
PonderDefaultViewCapability::CAPABILITY);
return id(new PonderQuestion())
->setAuthorPHID($actor->getPHID())
->setViewPolicy($view_policy)
->setStatus(PonderQuestionStatus::STATUS_OPEN)
->setAnswerCount(0)
->setAnswerWiki('')
->setSpacePHID($actor->getDefaultSpacePHID());
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'title' => 'text255',
'status' => 'text32',
'content' => 'text',
'answerWiki' => 'text',
'answerCount' => 'uint32',
'mailKey' => 'bytes20',
// T6203/NULLABILITY
// This should always exist.
'contentSource' => 'text?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'authorPHID' => array(
'columns' => array('authorPHID'),
),
'status' => array(
'columns' => array('status'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(PonderQuestionPHIDType::TYPECONST);
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source->serialize();
return $this;
}
public function getContentSource() {
return PhabricatorContentSource::newFromSerialized($this->contentSource);
}
public function setComments($comments) {
$this->comments = $comments;
return $this;
}
public function getComments() {
return $this->comments;
}
public function getMonogram() {
return 'Q'.$this->getID();
}
public function getViewURI() {
return '/'.$this->getMonogram();
}
public function attachAnswers(array $answers) {
assert_instances_of($answers, 'PonderAnswer');
$this->answers = $answers;
return $this;
}
public function getAnswers() {
return $this->answers;
}
public function getProjectPHIDs() {
return $this->assertAttached($this->projectPHIDs);
}
public function attachProjectPHIDs(array $phids) {
$this->projectPHIDs = $phids;
return $this;
}
public function getMarkupField() {
return self::MARKUP_FIELD_CONTENT;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PonderQuestionEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PonderQuestionTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
// Markup interface
public function getMarkupFieldKey($field) {
$content = $this->getMarkupText($field);
return PhabricatorMarkupEngine::digestRemarkupContent($this, $content);
}
public function getMarkupText($field) {
return $this->getContent();
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::getEngine();
}
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
public function shouldUseMarkupCache($field) {
return (bool)$this->getID();
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public function getFullTitle() {
$id = $this->getID();
$title = $this->getTitle();
return "Q{$id}: {$title}";
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
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:
$app = PhabricatorApplication::getByClass(
'PhabricatorPonderApplication');
return $app->getPolicy(PonderModerateCapability::CAPABILITY);
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
if (PhabricatorPolicyFilter::hasCapability(
$viewer, $this, PhabricatorPolicyCapability::CAN_EDIT)) {
return true;
}
}
return ($viewer->getPHID() == $this->getAuthorPHID());
}
public function describeAutomaticCapability($capability) {
$out = array();
$out[] = pht('The user who asked a question can always view and edit it.');
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$out[] = pht(
'A moderator can always view the question.');
break;
}
return $out;
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return ($phid == $this->getAuthorPHID());
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getAuthorPHID(),
);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$answers = id(new PonderAnswer())->loadAllWhere(
'questionID = %d',
$this->getID());
foreach ($answers as $answer) {
$engine->destroyObject($answer);
}
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorSpacesInterface )----------------------------------------- */
public function getSpacePHID() {
return $this->spacePHID;
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PonderQuestionFulltextEngine();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new PonderQuestionFerretEngine();
}
}
diff --git a/src/applications/project/config/PhabricatorProjectConfigOptions.php b/src/applications/project/config/PhabricatorProjectConfigOptions.php
index c61faa64f..2d87bf159 100644
--- a/src/applications/project/config/PhabricatorProjectConfigOptions.php
+++ b/src/applications/project/config/PhabricatorProjectConfigOptions.php
@@ -1,108 +1,141 @@
<?php
final class PhabricatorProjectConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Projects');
}
public function getDescription() {
return pht('Configure Projects.');
}
public function getIcon() {
return 'fa-briefcase';
}
public function getGroup() {
return 'apps';
}
public function getOptions() {
$default_icons = PhabricatorProjectIconSet::getDefaultConfiguration();
$icons_type = 'project.icons';
$icons_description = $this->deformat(pht(<<<EOTEXT
Allows you to change and customize the available project icons.
You can find a list of available icons in {nav UIExamples > Icons and Images}.
Configure a list of icon specifications. Each icon specification should be
a dictionary, which may contain these keys:
- `key` //Required string.// Internal key identifying the icon.
- `name` //Required string.// Human-readable icon name.
- `icon` //Required string.// Specifies which actual icon image to use.
- `image` //Optional string.// Selects a default image. Select an image from
`resources/builtins/projects/`.
- `default` //Optional bool.// Selects a default icon. Exactly one icon must
be selected as the default.
- `disabled` //Optional bool.// If true, this icon will no longer be
available for selection when creating or editing projects.
- `special` //Optional string.// Marks an icon as a special icon:
- `milestone` This is the icon for milestones. Exactly one icon must be
selected as the milestone icon.
You can look at the default configuration below for an example of a valid
configuration.
EOTEXT
));
$default_colors = PhabricatorProjectIconSet::getDefaultColorMap();
$colors_type = 'project.colors';
$colors_description = $this->deformat(pht(<<<EOTEXT
Allows you to relabel project colors.
The list of available colors can not be expanded, but the existing colors may
be given labels.
Configure a list of color specifications. Each color specification should be a
dictionary, which may contain these keys:
- `key` //Required string.// The internal key identifying the color.
- `name` //Required string.// Human-readable label for the color.
- `default` //Optional bool.// Selects the default color used when creating
new projects. Exactly one color must be selected as the default.
You can look at the default configuration below for an example of a valid
configuration.
EOTEXT
));
$default_fields = array(
'std:project:internal:description' => true,
);
foreach ($default_fields as $key => $enabled) {
$default_fields[$key] = array(
'disabled' => !$enabled,
);
}
$custom_field_type = 'custom:PhabricatorCustomFieldConfigOptionType';
+
+ $subtype_type = 'projects.subtypes';
+ $subtype_default_key = PhabricatorEditEngineSubtype::SUBTYPE_DEFAULT;
+ $subtype_example = array(
+ array(
+ 'key' => $subtype_default_key,
+ 'name' => pht('Project'),
+ ),
+ array(
+ 'key' => 'team',
+ 'name' => pht('Team'),
+ ),
+ );
+ $subtype_example = id(new PhutilJSON())->encodeAsList($subtype_example);
+
+ $subtype_default = array(
+ array(
+ 'key' => $subtype_default_key,
+ 'name' => pht('Project'),
+ ),
+ );
+
+ $subtype_description = $this->deformat(pht(<<<EOTEXT
+Allows you to define project subtypes. For a more detailed description of
+subtype configuration, see @{config:maniphest.subtypes}.
+EOTEXT
+ ));
+
return array(
$this->newOption('projects.custom-field-definitions', 'wild', array())
->setSummary(pht('Custom Projects fields.'))
->setDescription(
pht(
'Array of custom fields for Projects.'))
->addExample(
'{"mycompany:motto": {"name": "Project Motto", '.
'"type": "text"}}',
pht('Valid Setting')),
$this->newOption('projects.fields', $custom_field_type, $default_fields)
->setCustomData(id(new PhabricatorProject())->getCustomFieldBaseClass())
->setDescription(pht('Select and reorder project fields.')),
$this->newOption('projects.icons', $icons_type, $default_icons)
->setSummary(pht('Adjust project icons.'))
->setDescription($icons_description),
$this->newOption('projects.colors', $colors_type, $default_colors)
->setSummary(pht('Adjust project colors.'))
->setDescription($colors_description),
+ $this->newOption('projects.subtypes', $subtype_type, $subtype_default)
+ ->setSummary(pht('Define project subtypes.'))
+ ->setDescription($subtype_description)
+ ->addExample($subtype_example, pht('Simple Subtypes')),
+
);
}
}
diff --git a/src/applications/project/config/PhabricatorProjectSubtypesConfigType.php b/src/applications/project/config/PhabricatorProjectSubtypesConfigType.php
new file mode 100644
index 000000000..7603ad768
--- /dev/null
+++ b/src/applications/project/config/PhabricatorProjectSubtypesConfigType.php
@@ -0,0 +1,14 @@
+<?php
+
+final class PhabricatorProjectSubtypesConfigType
+ extends PhabricatorJSONConfigType {
+
+ const TYPEKEY = 'projects.subtypes';
+
+ public function validateStoredValue(
+ PhabricatorConfigOption $option,
+ $value) {
+ PhabricatorEditEngineSubtype::validateConfiguration($value);
+ }
+
+}
diff --git a/src/applications/project/controller/PhabricatorProjectBoardImportController.php b/src/applications/project/controller/PhabricatorProjectBoardImportController.php
index c344bc0af..67bddaaa5 100644
--- a/src/applications/project/controller/PhabricatorProjectBoardImportController.php
+++ b/src/applications/project/controller/PhabricatorProjectBoardImportController.php
@@ -1,98 +1,113 @@
<?php
final class PhabricatorProjectBoardImportController
extends PhabricatorProjectBoardController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$project_id = $request->getURIData('projectID');
$project = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->withIDs(array($project_id))
->executeOne();
if (!$project) {
return new Aphront404Response();
}
$this->setProject($project);
+ $project_id = $project->getID();
+ $board_uri = $this->getApplicationURI("board/{$project_id}/");
+
+ // See PHI1025. We only want to prevent the import if the board already has
+ // real columns. If it has proxy columns (for example, for milestones) you
+ // can still import columns from another board.
$columns = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withProjectPHIDs(array($project->getPHID()))
+ ->withIsProxyColumn(false)
->execute();
if ($columns) {
- return new Aphront400Response();
+ return $this->newDialog()
+ ->setTitle(pht('Workboard Already Has Columns'))
+ ->appendParagraph(
+ pht(
+ 'You can not import columns into this workboard because it '.
+ 'already has columns. You can only import into an empty '.
+ 'workboard.'))
+ ->addCancelButton($board_uri);
}
- $project_id = $project->getID();
- $board_uri = $this->getApplicationURI("board/{$project_id}/");
-
if ($request->isFormPost()) {
$import_phid = $request->getArr('importProjectPHID');
$import_phid = reset($import_phid);
$import_columns = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withProjectPHIDs(array($import_phid))
+ ->withIsProxyColumn(false)
->execute();
if (!$import_columns) {
- return new Aphront400Response();
+ return $this->newDialog()
+ ->setTitle(pht('Source Workboard Has No Columns'))
+ ->appendParagraph(
+ pht(
+ 'You can not import columns from that workboard because it has '.
+ 'no importable columns.'))
+ ->addCancelButton($board_uri);
}
$table = id(new PhabricatorProjectColumn())
->openTransaction();
foreach ($import_columns as $import_column) {
if ($import_column->isHidden()) {
continue;
}
- if ($import_column->getProxy()) {
- continue;
- }
$new_column = PhabricatorProjectColumn::initializeNewColumn($viewer)
->setSequence($import_column->getSequence())
->setProjectPHID($project->getPHID())
->setName($import_column->getName())
->setProperties($import_column->getProperties())
->save();
}
$xactions = array();
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(
PhabricatorProjectWorkboardTransaction::TRANSACTIONTYPE)
->setNewValue(1);
id(new PhabricatorProjectTransactionEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($project, $xactions);
$table->saveTransaction();
return id(new AphrontRedirectResponse())->setURI($board_uri);
}
$proj_selector = id(new AphrontFormTokenizerControl())
->setName('importProjectPHID')
->setUser($viewer)
->setDatasource(id(new PhabricatorProjectDatasource())
->setParameters(array('mustHaveColumns' => true))
->setLimit(1));
return $this->newDialog()
->setTitle(pht('Import Columns'))
->setWidth(AphrontDialogView::WIDTH_FORM)
->appendParagraph(pht('Choose a project to import columns from:'))
->appendChild($proj_selector)
->addCancelButton($board_uri)
->addSubmitButton(pht('Import'));
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php
index 3f9fbd222..df7e2a0d9 100644
--- a/src/applications/project/controller/PhabricatorProjectProfileController.php
+++ b/src/applications/project/controller/PhabricatorProjectProfileController.php
@@ -1,504 +1,520 @@
<?php
final class PhabricatorProjectProfileController
extends PhabricatorProjectController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$response = $this->loadProject();
if ($response) {
return $response;
}
$viewer = $request->getUser();
$project = $this->getProject();
$id = $project->getID();
$picture = $project->getProfileImageURI();
$icon = $project->getDisplayIconIcon();
$icon_name = $project->getDisplayIconName();
$tag = id(new PHUITagView())
->setIcon($icon)
->setName($icon_name)
->addClass('project-view-header-tag')
->setType(PHUITagView::TYPE_SHADE);
$header = id(new PHUIHeaderView())
->setHeader(array($project->getDisplayName(), $tag))
->setUser($viewer)
->setPolicyObject($project)
->setProfileHeader(true);
if ($project->getStatus() == PhabricatorProjectStatus::STATUS_ACTIVE) {
$header->setStatus('fa-check', 'bluegrey', pht('Active'));
} else {
$header->setStatus('fa-ban', 'red', pht('Archived'));
}
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$project,
PhabricatorPolicyCapability::CAN_EDIT);
if ($can_edit) {
$header->setImageEditURL($this->getApplicationURI("picture/{$id}/"));
}
$properties = $this->buildPropertyListView($project);
$watch_action = $this->renderWatchAction($project);
$header->addActionLink($watch_action);
// c4science custo
// Create and list Phriction pages, repositories and
// Jenkins Job linked to this project
if($can_edit){
$wiki_action = $this->renderWikiAction($project);
$header->addActionLink($wiki_action);
$have_jenkins = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorJenkinsJobApplication', $viewer);
if ($have_jenkins) {
$job_action = $this->renderJobAction($project);
$header->addActionLink($job_action);
}
}
$repo_list = $this->buildRepoList($project);
$jobs_list = $this->buildJobsList($project);
$wiki_list = $this->buildWikiList($project);
// end of c4science custo
+
+ $subtype = $project->newSubtypeObject();
+ if ($subtype && $subtype->hasTagView()) {
+ $subtype_tag = $subtype->newTagView();
+ $header->addTag($subtype_tag);
+ }
+
$milestone_list = $this->buildMilestoneList($project);
$subproject_list = $this->buildSubprojectList($project);
$member_list = id(new PhabricatorProjectMemberListView())
->setUser($viewer)
->setProject($project)
->setLimit(5)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setUserPHIDs($project->getMemberPHIDs());
+<<<<<<< HEAD
// c4science custo
// $watcher_list = id(new PhabricatorProjectWatcherListView())
// ->setUser($viewer)
// ->setProject($project)
// ->setLimit(5)
// ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
// ->setUserPHIDs($project->getWatcherPHIDs());
+=======
+ $watcher_list = id(new PhabricatorProjectWatcherListView())
+ ->setUser($viewer)
+ ->setProject($project)
+ ->setLimit(5)
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->setUserPHIDs($project->getWatcherPHIDs());
+>>>>>>> upstream/stable
$nav = $this->getProfileMenu();
$nav->selectFilter(PhabricatorProject::ITEM_PROFILE);
$stories = id(new PhabricatorFeedQuery())
->setViewer($viewer)
->withFilterPHIDs(
array(
$project->getPHID(),
))
->setLimit(50)
->execute();
$view_all = id(new PHUIButtonView())
->setTag('a')
->setIcon(
id(new PHUIIconView())
->setIcon('fa-list-ul'))
->setText(pht('View All'))
->setHref('/feed/?projectPHIDs='.$project->getPHID());
$feed_header = id(new PHUIHeaderView())
->setHeader(pht('Recent Activity'))
->addActionLink($view_all);
$feed = $this->renderStories($stories);
$feed = id(new PHUIObjectBoxView())
->setHeader($feed_header)
->addClass('project-view-feed')
->appendChild($feed);
require_celerity_resource('project-view-css');
$home = id(new PHUITwoColumnView())
->setHeader($header)
->addClass('project-view-home')
->addClass('project-view-people-home')
->setMainColumn(
array(
$properties,
$feed,
))
->setSideColumn(
array(
$repo_list, // c4s custo
$jobs_list, // c4s custo
$wiki_list, // c4s custo
$milestone_list,
$subproject_list,
$member_list,
// $watcher_list, // c4s custo
));
$crumbs = $this->buildApplicationCrumbs();
$crumbs->setBorder(true);
return $this->newPage()
->setNavigation($nav)
->setCrumbs($crumbs)
->setTitle($project->getDisplayName())
->setPageObjectPHIDs(array($project->getPHID()))
->appendChild($home);
}
private function buildPropertyListView(
PhabricatorProject $project) {
$request = $this->getRequest();
$viewer = $request->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($project);
$field_list = PhabricatorCustomField::getObjectFields(
$project,
PhabricatorCustomField::ROLE_VIEW);
$field_list->appendFieldsToPropertyList($project, $viewer, $view);
if (!$view->hasAnyProperties()) {
return null;
}
$header = id(new PHUIHeaderView())
->setHeader(pht('Details'));
$view = id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild($view)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addClass('project-view-properties');
return $view;
}
private function renderStories(array $stories) {
assert_instances_of($stories, 'PhabricatorFeedStory');
$builder = new PhabricatorFeedBuilder($stories);
$builder->setUser($this->getRequest()->getUser());
$builder->setShowHovercards(true);
$view = $builder->buildView();
return $view;
}
private function renderWatchAction(PhabricatorProject $project) {
$viewer = $this->getViewer();
$id = $project->getID();
if (!$viewer->isLoggedIn()) {
$is_watcher = false;
$is_ancestor = false;
} else {
$viewer_phid = $viewer->getPHID();
$is_watcher = $project->isUserWatcher($viewer_phid);
$is_ancestor = $project->isUserAncestorWatcher($viewer_phid);
}
if ($is_ancestor && !$is_watcher) {
$watch_icon = 'fa-eye';
$watch_text = pht('Watching Ancestor');
$watch_href = "/project/watch/{$id}/?via=profile";
$watch_disabled = true;
} else if (!$is_watcher) {
$watch_icon = 'fa-eye';
$watch_text = pht('Watch Project');
$watch_href = "/project/watch/{$id}/?via=profile";
$watch_disabled = false;
} else {
$watch_icon = 'fa-eye-slash';
$watch_text = pht('Unwatch Project');
$watch_href = "/project/unwatch/{$id}/?via=profile";
$watch_disabled = false;
}
$watch_icon = id(new PHUIIconView())
->setIcon($watch_icon);
return id(new PHUIButtonView())
->setTag('a')
->setWorkflow(true)
->setIcon($watch_icon)
->setText($watch_text)
->setHref($watch_href)
->setDisabled($watch_disabled);
}
// c4science custo
private function renderWikiAction(PhabricatorProject $project) {
$id = $project->getID();
$wiki_icon = id(new PHUIIconView())->setIcon('fa-book');
$wiki_text = pht('Create wiki page');
$wiki_href = "/project/wiki/create/{$id}/?via=profile";
return id(new PHUIButtonView())
->setTag('a')
->setWorkflow(true)
->setIcon($wiki_icon)
->setText($wiki_text)
->setHref($wiki_href);
}
// c4science custo
private function renderJobAction(PhabricatorProject $project) {
$phid = $project->getPHID();
$job_icon = id(new PHUIIconView())->setIcon('fa-tasks');
$job_text = pht('Create Jenkins Job');
$job_href = "/jobs/create/?with_project={$phid}";
return id(new PHUIButtonView())
->setTag('a')
->setWorkflow(true)
->setIcon($job_icon)
->setText($job_text)
->setHref($job_href);
}
// c4science custo
private function getHasWiki(PhabricatorProject $project) {
$viewer = $this->getViewer();
$slug = PhabricatorProjectWikiCreate::getAllSlugs($project);
return id(new PhrictionDocumentQuery())
->setViewer($viewer)
->withStatuses(array(PhrictionDocumentStatus::STATUS_EXISTS))
->withSlugPrefix($slug)
->setLimit(1)
->execute();
}
// c4science custo
private function buildWikiList(PhabricatorProject $project) {
if (!$this->getHasWiki($project)) {
return null;
}
$viewer = $this->getViewer();
$id = $project->getID();
$slug = PhabricatorProjectWikiCreate::getAllSlugs($project);
$query = id(new PhrictionDocumentQuery())
->setViewer($viewer)
->needContent(true)
->withStatuses(array(PhrictionDocumentStatus::STATUS_EXISTS))
->withSlugPrefix($slug)
->setLimit(5)
->execute();
$wiki_list = new PHUIObjectItemListView();
foreach($query as $w){
$wiki_list->addItem(
id(new PHUIObjectItemView())
->setHeader($w->getContent()->getTitle())
->addAttribute($w->getSlug())
->setHref($w->getSlugURI($w->getSlug())));
}
$view_all = id(new PHUIButtonView())
->setTag('a')
->setIcon(
id(new PHUIIconView())
->setIcon('fa-list-ul'))
->setText(pht('View All'))
->setHref("/project/wiki/view/{$id}/");
$header = id(new PHUIHeaderView())
->setHeader(pht('Wiki pages'))
->addActionLink($view_all);
return id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setObjectList($wiki_list);
}
// c4science custo
private function buildJobsList(PhabricatorProject $project) {
$viewer = $this->getViewer();
$have_jenkins = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorJenkinsJobApplication', $viewer);
if (!$have_jenkins) {
return;
}
$query = id(new PhabricatorJenkinsJobQuery())
->setViewer($viewer)
->withProjects(array($project->getPHID()))
->execute();
if (!$query) {
return null;
}
$jobs_list = PhabricatorJenkinsJobController::createObjectList(
$viewer, $project->getPHID(), null);
$header = id(new PHUIHeaderView())
->setHeader(pht('Jobs'));
return id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setObjectList($jobs_list);
}
// c4science custo
private function buildRepoList(PhabricatorProject $project) {
$viewer = $this->getViewer();
$datasource = id(new PhabricatorProjectLogicalDatasource())
->setViewer($viewer);
$constraints = $datasource->evaluateTokens(array($project->getPHID()));
$query = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->needProjectPHIDs(true)
->withStatus(PhabricatorRepositoryQuery::STATUS_OPEN)
->setOrder('committed')
->setLimit(5)
->withEdgeLogicConstraints(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
$constraints)
->execute();
if(!$query){
return null;
}
$repo_list = new PHUIObjectItemListView();
foreach($query as $r){
$repo_list->addItem(
id(new PHUIObjectItemView())
->setHeader($r->getName())
->addAttribute($r->getDisplayName())
->setHref($r->getURI())
);
}
$view_all = id(new PHUIButtonView())
->setTag('a')
->setIcon(
id(new PHUIIconView())
->setIcon('fa-list-ul'))
->setText(pht('View All'))
->setHref("/diffusion/?status=open&projectPHIDs=" . $project->getPHID() . '#R');
$header = id(new PHUIHeaderView())
->setHeader(pht('Repositories'))
->addActionLink($view_all);
return id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setObjectList($repo_list);
}
private function buildMilestoneList(PhabricatorProject $project) {
if (!$project->getHasMilestones()) {
return null;
}
$viewer = $this->getViewer();
$id = $project->getID();
$milestones = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withParentProjectPHIDs(array($project->getPHID()))
->needImages(true)
->withIsMilestone(true)
->withStatuses(
array(
PhabricatorProjectStatus::STATUS_ACTIVE,
))
->setOrderVector(array('milestoneNumber', 'id'))
->execute();
if (!$milestones) {
return null;
}
$milestone_list = id(new PhabricatorProjectListView())
->setUser($viewer)
->setProjects($milestones)
->renderList();
$view_all = id(new PHUIButtonView())
->setTag('a')
->setIcon(
id(new PHUIIconView())
->setIcon('fa-list-ul'))
->setText(pht('View All'))
->setHref("/project/subprojects/{$id}/");
$header = id(new PHUIHeaderView())
->setHeader(pht('Milestones'))
->addActionLink($view_all);
return id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setObjectList($milestone_list);
}
private function buildSubprojectList(PhabricatorProject $project) {
if (!$project->getHasSubprojects()) {
return null;
}
$viewer = $this->getViewer();
$id = $project->getID();
$limit = 25;
$subprojects = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withParentProjectPHIDs(array($project->getPHID()))
->needImages(true)
->withStatuses(
array(
PhabricatorProjectStatus::STATUS_ACTIVE,
))
->withIsMilestone(false)
->setLimit($limit)
->execute();
if (!$subprojects) {
return null;
}
$subproject_list = id(new PhabricatorProjectListView())
->setUser($viewer)
->setProjects($subprojects)
->renderList();
$view_all = id(new PHUIButtonView())
->setTag('a')
->setIcon(
id(new PHUIIconView())
->setIcon('fa-list-ul'))
->setText(pht('View All'))
->setHref("/project/subprojects/{$id}/");
$header = id(new PHUIHeaderView())
->setHeader(pht('Subprojects'))
->addActionLink($view_all);
return id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setObjectList($subproject_list);
}
}
diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
index 581ec2315..b714f6683 100644
--- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
+++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
@@ -1,419 +1,429 @@
<?php
final class PhabricatorProjectTransactionEditor
extends PhabricatorApplicationTransactionEditor {
private $isMilestone;
private function setIsMilestone($is_milestone) {
$this->isMilestone = $is_milestone;
return $this;
}
public function getIsMilestone() {
return $this->isMilestone;
}
public function getEditorApplicationClass() {
return 'PhabricatorProjectApplication';
}
public function getEditorObjectsDescription() {
return pht('Projects');
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this project.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created %s.', $author, $object);
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
$types[] = PhabricatorTransactions::TYPE_JOIN_POLICY;
return $types;
}
protected function validateAllTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$errors = array();
// Prevent creating projects which are both subprojects and milestones,
// since this does not make sense, won't work, and will break everything.
$parent_xaction = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorProjectParentTransaction::TRANSACTIONTYPE:
case PhabricatorProjectMilestoneTransaction::TRANSACTIONTYPE:
if ($xaction->getNewValue() === null) {
- continue;
+ continue 2;
}
if (!$parent_xaction) {
$parent_xaction = $xaction;
- continue;
+ continue 2;
}
$errors[] = new PhabricatorApplicationTransactionValidationError(
$xaction->getTransactionType(),
pht('Invalid'),
pht(
'When creating a project, specify a maximum of one parent '.
'project or milestone project. A project can not be both a '.
'subproject and a milestone.'),
$xaction);
- break;
- break;
+ break 2;
}
}
$is_milestone = $this->getIsMilestone();
$is_parent = $object->getHasSubprojects();
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_EDGE:
$type = $xaction->getMetadataValue('edge:type');
if ($type != PhabricatorProjectProjectHasMemberEdgeType::EDGECONST) {
break;
}
if ($is_parent) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$xaction->getTransactionType(),
pht('Invalid'),
pht(
'You can not change members of a project with subprojects '.
'directly. Members of any subproject are automatically '.
'members of the parent project.'),
$xaction);
}
if ($is_milestone) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$xaction->getTransactionType(),
pht('Invalid'),
pht(
'You can not change members of a milestone. Members of the '.
'parent project are automatically members of the milestone.'),
$xaction);
}
break;
}
}
return $errors;
}
protected function willPublish(PhabricatorLiskDAO $object, array $xactions) {
// NOTE: We're using the omnipotent user here because the original actor
// may no longer have permission to view the object.
return id(new PhabricatorProjectQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($object->getPHID()))
->needAncestorMembers(true)
->executeOne();
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailSubjectPrefix() {
return pht('[Project]');
}
protected function getMailTo(PhabricatorLiskDAO $object) {
return array(
$this->getActingAsPHID(),
);
}
protected function getMailCc(PhabricatorLiskDAO $object) {
return array();
}
public function getMailTagsMap() {
return array(
PhabricatorProjectTransaction::MAILTAG_METADATA =>
pht('Project name, hashtags, icon, image, or color changes.'),
PhabricatorProjectTransaction::MAILTAG_MEMBERS =>
pht('Project membership changes.'),
PhabricatorProjectTransaction::MAILTAG_WATCHERS =>
pht('Project watcher list changes.'),
PhabricatorProjectTransaction::MAILTAG_OTHER =>
pht('Other project activity not listed above occurs.'),
);
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new ProjectReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$name = $object->getName();
return id(new PhabricatorMetaMTAMail())
->setSubject("{$name}");
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
$uri = '/project/profile/'.$object->getID().'/';
$body->addLinkSection(
pht('PROJECT DETAIL'),
PhabricatorEnv::getProductionURI($uri));
return $body;
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function supportsSearch() {
return true;
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
$materialize = false;
$new_parent = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_EDGE:
switch ($xaction->getMetadataValue('edge:type')) {
case PhabricatorProjectProjectHasMemberEdgeType::EDGECONST:
$materialize = true;
break;
}
break;
case PhabricatorProjectParentTransaction::TRANSACTIONTYPE:
case PhabricatorProjectMilestoneTransaction::TRANSACTIONTYPE:
$materialize = true;
$new_parent = $object->getParentProject();
break;
}
}
if ($new_parent) {
// If we just created the first subproject of this parent, we want to
// copy all of the real members to the subproject.
if (!$new_parent->getHasSubprojects()) {
$member_type = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST;
$project_members = PhabricatorEdgeQuery::loadDestinationPHIDs(
$new_parent->getPHID(),
$member_type);
if ($project_members) {
$editor = id(new PhabricatorEdgeEditor());
foreach ($project_members as $phid) {
$editor->addEdge($object->getPHID(), $member_type, $phid);
}
$editor->save();
}
}
}
// TODO: We should dump an informational transaction onto the parent
// project to show that we created the sub-thing.
if ($materialize) {
id(new PhabricatorProjectsMembershipIndexEngineExtension())
->rematerialize($object);
}
if ($new_parent) {
id(new PhabricatorProjectsMembershipIndexEngineExtension())
->rematerialize($new_parent);
}
+ // See PHI1046. Milestones are always in the Space of their parent project.
+ // Synchronize the database values to match the application values.
+ $conn = $object->establishConnection('w');
+ queryfx(
+ $conn,
+ 'UPDATE %R SET spacePHID = %ns
+ WHERE parentProjectPHID = %s AND milestoneNumber IS NOT NULL',
+ $object,
+ $object->getSpacePHID(),
+ $object->getPHID());
+
return parent::applyFinalEffects($object, $xactions);
}
public function addSlug(PhabricatorProject $project, $slug, $force) {
$slug = PhabricatorSlug::normalizeProjectSlug($slug);
$table = new PhabricatorProjectSlug();
$project_phid = $project->getPHID();
if ($force) {
// If we have the `$force` flag set, we only want to ignore an existing
// slug if it's for the same project. We'll error on collisions with
// other projects.
$current = $table->loadOneWhere(
'slug = %s AND projectPHID = %s',
$slug,
$project_phid);
} else {
// Without the `$force` flag, we'll just return without doing anything
// if any other project already has the slug.
$current = $table->loadOneWhere(
'slug = %s',
$slug);
}
if ($current) {
return;
}
return id(new PhabricatorProjectSlug())
->setSlug($slug)
->setProjectPHID($project_phid)
->save();
}
public function removeSlugs(PhabricatorProject $project, array $slugs) {
if (!$slugs) {
return;
}
// We're going to try to delete both the literal and normalized versions
// of all slugs. This allows us to destroy old slugs that are no longer
// valid.
foreach ($this->normalizeSlugs($slugs) as $slug) {
$slugs[] = $slug;
}
$objects = id(new PhabricatorProjectSlug())->loadAllWhere(
'projectPHID = %s AND slug IN (%Ls)',
$project->getPHID(),
$slugs);
foreach ($objects as $object) {
$object->delete();
}
}
public function normalizeSlugs(array $slugs) {
foreach ($slugs as $key => $slug) {
$slugs[$key] = PhabricatorSlug::normalizeProjectSlug($slug);
}
$slugs = array_unique($slugs);
$slugs = array_values($slugs);
return $slugs;
}
protected function adjustObjectForPolicyChecks(
PhabricatorLiskDAO $object,
array $xactions) {
$copy = parent::adjustObjectForPolicyChecks($object, $xactions);
$type_edge = PhabricatorTransactions::TYPE_EDGE;
$edgetype_member = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST;
$member_xaction = null;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() !== $type_edge) {
continue;
}
$edgetype = $xaction->getMetadataValue('edge:type');
if ($edgetype !== $edgetype_member) {
continue;
}
$member_xaction = $xaction;
}
if ($member_xaction) {
$object_phid = $object->getPHID();
if ($object_phid) {
$project = id(new PhabricatorProjectQuery())
->setViewer($this->getActor())
->withPHIDs(array($object_phid))
->needMembers(true)
->executeOne();
$members = $project->getMemberPHIDs();
} else {
$members = array();
}
$clone_xaction = clone $member_xaction;
$hint = $this->getPHIDTransactionNewValue($clone_xaction, $members);
$rule = new PhabricatorProjectMembersPolicyRule();
$hint = array_fuse($hint);
PhabricatorPolicyRule::passTransactionHintToRule(
$copy,
$rule,
$hint);
}
return $copy;
}
protected function expandTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$actor = $this->getActor();
$actor_phid = $actor->getPHID();
$results = parent::expandTransactions($object, $xactions);
$is_milestone = $object->isMilestone();
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorProjectMilestoneTransaction::TRANSACTIONTYPE:
if ($xaction->getNewValue() !== null) {
$is_milestone = true;
}
break;
}
}
$this->setIsMilestone($is_milestone);
return $results;
}
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
// Herald rules may run on behalf of other users and need to execute
// membership checks against ancestors.
$project = id(new PhabricatorProjectQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($object->getPHID()))
->needAncestorMembers(true)
->executeOne();
return id(new PhabricatorProjectHeraldAdapter())
->setProject($project);
}
}
diff --git a/src/applications/project/query/PhabricatorProjectColumnQuery.php b/src/applications/project/query/PhabricatorProjectColumnQuery.php
index 13f2f52a4..441c33e8c 100644
--- a/src/applications/project/query/PhabricatorProjectColumnQuery.php
+++ b/src/applications/project/query/PhabricatorProjectColumnQuery.php
@@ -1,166 +1,180 @@
<?php
final class PhabricatorProjectColumnQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $projectPHIDs;
private $proxyPHIDs;
private $statuses;
+ private $isProxyColumn;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withProjectPHIDs(array $project_phids) {
$this->projectPHIDs = $project_phids;
return $this;
}
public function withProxyPHIDs(array $proxy_phids) {
$this->proxyPHIDs = $proxy_phids;
return $this;
}
public function withStatuses(array $status) {
$this->statuses = $status;
return $this;
}
+ public function withIsProxyColumn($is_proxy) {
+ $this->isProxyColumn = $is_proxy;
+ return $this;
+ }
+
public function newResultObject() {
return new PhabricatorProjectColumn();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $page) {
$projects = array();
$project_phids = array_filter(mpull($page, 'getProjectPHID'));
if ($project_phids) {
$projects = id(new PhabricatorProjectQuery())
->setParentQuery($this)
->setViewer($this->getViewer())
->withPHIDs($project_phids)
->execute();
$projects = mpull($projects, null, 'getPHID');
}
foreach ($page as $key => $column) {
$phid = $column->getProjectPHID();
$project = idx($projects, $phid);
if (!$project) {
$this->didRejectResult($page[$key]);
unset($page[$key]);
continue;
}
$column->attachProject($project);
}
$proxy_phids = array_filter(mpull($page, 'getProjectPHID'));
return $page;
}
protected function didFilterPage(array $page) {
$proxy_phids = array();
foreach ($page as $column) {
$proxy_phid = $column->getProxyPHID();
if ($proxy_phid !== null) {
$proxy_phids[$proxy_phid] = $proxy_phid;
}
}
if ($proxy_phids) {
$proxies = id(new PhabricatorObjectQuery())
->setParentQuery($this)
->setViewer($this->getViewer())
->withPHIDs($proxy_phids)
->execute();
$proxies = mpull($proxies, null, 'getPHID');
} else {
$proxies = array();
}
foreach ($page as $key => $column) {
$proxy_phid = $column->getProxyPHID();
if ($proxy_phid !== null) {
$proxy = idx($proxies, $proxy_phid);
// Only attach valid proxies, so we don't end up getting surprised if
// an install somehow gets junk into their database.
if (!($proxy instanceof PhabricatorColumnProxyInterface)) {
$proxy = null;
}
if (!$proxy) {
$this->didRejectResult($column);
unset($page[$key]);
continue;
}
} else {
$proxy = null;
}
$column->attachProxy($proxy);
}
return $page;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->projectPHIDs !== null) {
$where[] = qsprintf(
$conn,
'projectPHID IN (%Ls)',
$this->projectPHIDs);
}
if ($this->proxyPHIDs !== null) {
$where[] = qsprintf(
$conn,
'proxyPHID IN (%Ls)',
$this->proxyPHIDs);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'status IN (%Ld)',
$this->statuses);
}
+ if ($this->isProxyColumn !== null) {
+ if ($this->isProxyColumn) {
+ $where[] = qsprintf($conn, 'proxyPHID IS NOT NULL');
+ } else {
+ $where[] = qsprintf($conn, 'proxyPHID IS NULL');
+ }
+ }
+
return $where;
}
public function getQueryApplicationClass() {
return 'PhabricatorProjectApplication';
}
}
diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php
index 9b051c00d..f6087f7d2 100644
--- a/src/applications/project/query/PhabricatorProjectQuery.php
+++ b/src/applications/project/query/PhabricatorProjectQuery.php
@@ -1,859 +1,878 @@
<?php
final class PhabricatorProjectQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $memberPHIDs;
private $watcherPHIDs;
private $slugs;
private $slugNormals;
private $slugMap;
private $allSlugs;
private $names;
private $namePrefixes;
private $nameTokens;
private $icons;
private $colors;
private $ancestorPHIDs;
private $parentPHIDs;
private $isMilestone;
private $hasSubprojects;
private $minDepth;
private $maxDepth;
private $minMilestoneNumber;
private $maxMilestoneNumber;
+ private $subtypes;
private $status = 'status-any';
const STATUS_ANY = 'status-any';
const STATUS_OPEN = 'status-open';
const STATUS_CLOSED = 'status-closed';
const STATUS_ACTIVE = 'status-active';
const STATUS_ARCHIVED = 'status-archived';
private $statuses;
private $needSlugs;
private $needMembers;
private $needAncestorMembers;
private $needWatchers;
private $needImages;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function withMemberPHIDs(array $member_phids) {
$this->memberPHIDs = $member_phids;
return $this;
}
public function withWatcherPHIDs(array $watcher_phids) {
$this->watcherPHIDs = $watcher_phids;
return $this;
}
public function withSlugs(array $slugs) {
$this->slugs = $slugs;
return $this;
}
public function withNames(array $names) {
$this->names = $names;
return $this;
}
public function withNamePrefixes(array $prefixes) {
$this->namePrefixes = $prefixes;
return $this;
}
public function withNameTokens(array $tokens) {
$this->nameTokens = array_values($tokens);
return $this;
}
public function withIcons(array $icons) {
$this->icons = $icons;
return $this;
}
public function withColors(array $colors) {
$this->colors = $colors;
return $this;
}
public function withParentProjectPHIDs($parent_phids) {
$this->parentPHIDs = $parent_phids;
return $this;
}
public function withAncestorProjectPHIDs($ancestor_phids) {
$this->ancestorPHIDs = $ancestor_phids;
return $this;
}
public function withIsMilestone($is_milestone) {
$this->isMilestone = $is_milestone;
return $this;
}
public function withHasSubprojects($has_subprojects) {
$this->hasSubprojects = $has_subprojects;
return $this;
}
public function withDepthBetween($min, $max) {
$this->minDepth = $min;
$this->maxDepth = $max;
return $this;
}
public function withMilestoneNumberBetween($min, $max) {
$this->minMilestoneNumber = $min;
$this->maxMilestoneNumber = $max;
return $this;
}
+ public function withSubtypes(array $subtypes) {
+ $this->subtypes = $subtypes;
+ return $this;
+ }
+
public function needMembers($need_members) {
$this->needMembers = $need_members;
return $this;
}
public function needAncestorMembers($need_ancestor_members) {
$this->needAncestorMembers = $need_ancestor_members;
return $this;
}
public function needWatchers($need_watchers) {
$this->needWatchers = $need_watchers;
return $this;
}
public function needImages($need_images) {
$this->needImages = $need_images;
return $this;
}
public function needSlugs($need_slugs) {
$this->needSlugs = $need_slugs;
return $this;
}
public function newResultObject() {
return new PhabricatorProject();
}
protected function getDefaultOrderVector() {
return array('name');
}
public function getBuiltinOrders() {
return array(
'name' => array(
'vector' => array('name'),
'name' => pht('Name'),
),
) + parent::getBuiltinOrders();
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'name',
'reverse' => true,
'type' => 'string',
'unique' => true,
),
'milestoneNumber' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'milestoneNumber',
'type' => 'int',
),
+ 'status' => array(
+ 'table' => $this->getPrimaryTableAlias(),
+ 'column' => 'status',
+ 'type' => 'int',
+ ),
);
}
protected function getPagingValueMap($cursor, array $keys) {
$project = $this->loadCursorObject($cursor);
return array(
'id' => $project->getID(),
'name' => $project->getName(),
+ 'status' => $project->getStatus(),
);
}
public function getSlugMap() {
if ($this->slugMap === null) {
throw new PhutilInvalidStateException('execute');
}
return $this->slugMap;
}
protected function willExecute() {
$this->slugMap = array();
$this->slugNormals = array();
$this->allSlugs = array();
if ($this->slugs) {
foreach ($this->slugs as $slug) {
if (PhabricatorSlug::isValidProjectSlug($slug)) {
$normal = PhabricatorSlug::normalizeProjectSlug($slug);
$this->slugNormals[$slug] = $normal;
$this->allSlugs[$normal] = $normal;
}
// NOTE: At least for now, we query for the normalized slugs but also
// for the slugs exactly as entered. This allows older projects with
// slugs that are no longer valid to continue to work.
$this->allSlugs[$slug] = $slug;
}
}
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $projects) {
$ancestor_paths = array();
foreach ($projects as $project) {
foreach ($project->getAncestorProjectPaths() as $path) {
$ancestor_paths[$path] = $path;
}
}
if ($ancestor_paths) {
$ancestors = id(new PhabricatorProject())->loadAllWhere(
'projectPath IN (%Ls)',
$ancestor_paths);
} else {
$ancestors = array();
}
$projects = $this->linkProjectGraph($projects, $ancestors);
$viewer_phid = $this->getViewer()->getPHID();
$material_type = PhabricatorProjectMaterializedMemberEdgeType::EDGECONST;
$watcher_type = PhabricatorObjectHasWatcherEdgeType::EDGECONST;
$types = array();
$types[] = $material_type;
if ($this->needWatchers) {
$types[] = $watcher_type;
}
$all_graph = $this->getAllReachableAncestors($projects);
// NOTE: Although we may not need much information about ancestors, we
// always need to test if the viewer is a member, because we will return
// ancestor projects to the policy filter via ExtendedPolicy calls. If
// we skip populating membership data on a parent, the policy framework
// will think the user is not a member of the parent project.
$all_sources = array();
foreach ($all_graph as $project) {
// For milestones, we need parent members.
if ($project->isMilestone()) {
$parent_phid = $project->getParentProjectPHID();
$all_sources[$parent_phid] = $parent_phid;
}
$phid = $project->getPHID();
$all_sources[$phid] = $phid;
}
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($all_sources)
->withEdgeTypes($types);
$need_all_edges =
$this->needMembers ||
$this->needWatchers ||
$this->needAncestorMembers;
// If we only need to know if the viewer is a member, we can restrict
// the query to just their PHID.
$any_edges = true;
if (!$need_all_edges) {
if ($viewer_phid) {
$edge_query->withDestinationPHIDs(array($viewer_phid));
} else {
// If we don't need members or watchers and don't have a viewer PHID
// (viewer is logged-out or omnipotent), they'll never be a member
// so we don't need to issue this query at all.
$any_edges = false;
}
}
if ($any_edges) {
$edge_query->execute();
}
$membership_projects = array();
foreach ($all_graph as $project) {
$project_phid = $project->getPHID();
if ($project->isMilestone()) {
$source_phids = array($project->getParentProjectPHID());
} else {
$source_phids = array($project_phid);
}
if ($any_edges) {
$member_phids = $edge_query->getDestinationPHIDs(
$source_phids,
array($material_type));
} else {
$member_phids = array();
}
if (in_array($viewer_phid, $member_phids)) {
$membership_projects[$project_phid] = $project;
}
if ($this->needMembers || $this->needAncestorMembers) {
$project->attachMemberPHIDs($member_phids);
}
if ($this->needWatchers) {
$watcher_phids = $edge_query->getDestinationPHIDs(
array($project_phid),
array($watcher_type));
$project->attachWatcherPHIDs($watcher_phids);
$project->setIsUserWatcher(
$viewer_phid,
in_array($viewer_phid, $watcher_phids));
}
}
// If we loaded ancestor members, we've already populated membership
// lists above, so we can skip this step.
if (!$this->needAncestorMembers) {
$member_graph = $this->getAllReachableAncestors($membership_projects);
foreach ($all_graph as $phid => $project) {
$is_member = isset($member_graph[$phid]);
$project->setIsUserMember($viewer_phid, $is_member);
}
}
return $projects;
}
protected function didFilterPage(array $projects) {
$viewer = $this->getViewer();
if ($this->needImages) {
$need_images = $projects;
// First, try to load custom profile images for any projects with custom
// images.
$file_phids = array();
foreach ($need_images as $key => $project) {
$image_phid = $project->getProfileImagePHID();
if ($image_phid) {
$file_phids[$key] = $image_phid;
}
}
if ($file_phids) {
$files = id(new PhabricatorFileQuery())
->setParentQuery($this)
->setViewer($viewer)
->withPHIDs($file_phids)
->execute();
$files = mpull($files, null, 'getPHID');
foreach ($file_phids as $key => $image_phid) {
$file = idx($files, $image_phid);
if (!$file) {
continue;
}
$need_images[$key]->attachProfileImageFile($file);
unset($need_images[$key]);
}
}
// For projects with default images, or projects where the custom image
// failed to load, load a builtin image.
if ($need_images) {
$builtin_map = array();
$builtins = array();
foreach ($need_images as $key => $project) {
$icon = $project->getIcon();
$builtin_name = PhabricatorProjectIconSet::getIconImage($icon);
$builtin_name = 'projects/'.$builtin_name;
$builtin = id(new PhabricatorFilesOnDiskBuiltinFile())
->setName($builtin_name);
$builtin_key = $builtin->getBuiltinFileKey();
$builtins[] = $builtin;
$builtin_map[$key] = $builtin_key;
}
$builtin_files = PhabricatorFile::loadBuiltins(
$viewer,
$builtins);
foreach ($need_images as $key => $project) {
$builtin_key = $builtin_map[$key];
$builtin_file = $builtin_files[$builtin_key];
$project->attachProfileImageFile($builtin_file);
}
}
}
$this->loadSlugs($projects);
return $projects;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->status != self::STATUS_ANY) {
switch ($this->status) {
case self::STATUS_OPEN:
case self::STATUS_ACTIVE:
$filter = array(
PhabricatorProjectStatus::STATUS_ACTIVE,
);
break;
case self::STATUS_CLOSED:
case self::STATUS_ARCHIVED:
$filter = array(
PhabricatorProjectStatus::STATUS_ARCHIVED,
);
break;
default:
throw new Exception(
pht(
"Unknown project status '%s'!",
$this->status));
}
$where[] = qsprintf(
$conn,
'status IN (%Ld)',
$filter);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'status IN (%Ls)',
$this->statuses);
}
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->memberPHIDs !== null) {
$where[] = qsprintf(
$conn,
'e.dst IN (%Ls)',
$this->memberPHIDs);
}
if ($this->watcherPHIDs !== null) {
$where[] = qsprintf(
$conn,
'w.dst IN (%Ls)',
$this->watcherPHIDs);
}
if ($this->slugs !== null) {
$where[] = qsprintf(
$conn,
'slug.slug IN (%Ls)',
$this->allSlugs);
}
if ($this->names !== null) {
$where[] = qsprintf(
$conn,
'name IN (%Ls)',
$this->names);
}
if ($this->namePrefixes) {
$parts = array();
foreach ($this->namePrefixes as $name_prefix) {
$parts[] = qsprintf(
$conn,
'name LIKE %>',
$name_prefix);
}
$where[] = qsprintf($conn, '%LO', $parts);
}
if ($this->icons !== null) {
$where[] = qsprintf(
$conn,
'icon IN (%Ls)',
$this->icons);
}
if ($this->colors !== null) {
$where[] = qsprintf(
$conn,
'color IN (%Ls)',
$this->colors);
}
if ($this->parentPHIDs !== null) {
$where[] = qsprintf(
$conn,
'parentProjectPHID IN (%Ls)',
$this->parentPHIDs);
}
if ($this->ancestorPHIDs !== null) {
$ancestor_paths = queryfx_all(
$conn,
'SELECT projectPath, projectDepth FROM %T WHERE phid IN (%Ls)',
id(new PhabricatorProject())->getTableName(),
$this->ancestorPHIDs);
if (!$ancestor_paths) {
throw new PhabricatorEmptyQueryException();
}
$sql = array();
foreach ($ancestor_paths as $ancestor_path) {
$sql[] = qsprintf(
$conn,
'(projectPath LIKE %> AND projectDepth > %d)',
$ancestor_path['projectPath'],
$ancestor_path['projectDepth']);
}
$where[] = qsprintf($conn, '%LO', $sql);
$where[] = qsprintf(
$conn,
'parentProjectPHID IS NOT NULL');
}
if ($this->isMilestone !== null) {
if ($this->isMilestone) {
$where[] = qsprintf(
$conn,
'milestoneNumber IS NOT NULL');
} else {
$where[] = qsprintf(
$conn,
'milestoneNumber IS NULL');
}
}
if ($this->hasSubprojects !== null) {
$where[] = qsprintf(
$conn,
'hasSubprojects = %d',
(int)$this->hasSubprojects);
}
if ($this->minDepth !== null) {
$where[] = qsprintf(
$conn,
'projectDepth >= %d',
$this->minDepth);
}
if ($this->maxDepth !== null) {
$where[] = qsprintf(
$conn,
'projectDepth <= %d',
$this->maxDepth);
}
if ($this->minMilestoneNumber !== null) {
$where[] = qsprintf(
$conn,
'milestoneNumber >= %d',
$this->minMilestoneNumber);
}
if ($this->maxMilestoneNumber !== null) {
$where[] = qsprintf(
$conn,
'milestoneNumber <= %d',
$this->maxMilestoneNumber);
}
+ if ($this->subtypes !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'subtype IN (%Ls)',
+ $this->subtypes);
+ }
+
return $where;
}
protected function shouldGroupQueryResultRows() {
if ($this->memberPHIDs || $this->watcherPHIDs || $this->nameTokens) {
return true;
}
return parent::shouldGroupQueryResultRows();
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
if ($this->memberPHIDs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T e ON e.src = p.phid AND e.type = %d',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorProjectMaterializedMemberEdgeType::EDGECONST);
}
if ($this->watcherPHIDs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T w ON w.src = p.phid AND w.type = %d',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorObjectHasWatcherEdgeType::EDGECONST);
}
if ($this->slugs !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %T slug on slug.projectPHID = p.phid',
id(new PhabricatorProjectSlug())->getTableName());
}
if ($this->nameTokens !== null) {
$name_tokens = $this->getNameTokensForQuery($this->nameTokens);
foreach ($name_tokens as $key => $token) {
$token_table = 'token_'.$key;
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON %T.projectID = p.id AND %T.token LIKE %>',
PhabricatorProject::TABLE_DATASOURCE_TOKEN,
$token_table,
$token_table,
$token_table,
$token);
}
}
return $joins;
}
public function getQueryApplicationClass() {
return 'PhabricatorProjectApplication';
}
protected function getPrimaryTableAlias() {
return 'p';
}
private function linkProjectGraph(array $projects, array $ancestors) {
$ancestor_map = mpull($ancestors, null, 'getPHID');
$projects_map = mpull($projects, null, 'getPHID');
$all_map = $projects_map + $ancestor_map;
$done = array();
foreach ($projects as $key => $project) {
$seen = array($project->getPHID() => true);
if (!$this->linkProject($project, $all_map, $done, $seen)) {
$this->didRejectResult($project);
unset($projects[$key]);
continue;
}
foreach ($project->getAncestorProjects() as $ancestor) {
$seen[$ancestor->getPHID()] = true;
}
}
return $projects;
}
private function linkProject($project, array $all, array $done, array $seen) {
$parent_phid = $project->getParentProjectPHID();
// This project has no parent, so just attach `null` and return.
if (!$parent_phid) {
$project->attachParentProject(null);
return true;
}
// This project has a parent, but it failed to load.
if (empty($all[$parent_phid])) {
return false;
}
// Test for graph cycles. If we encounter one, we're going to hide the
// entire cycle since we can't meaningfully resolve it.
if (isset($seen[$parent_phid])) {
return false;
}
$seen[$parent_phid] = true;
$parent = $all[$parent_phid];
$project->attachParentProject($parent);
if (!empty($done[$parent_phid])) {
return true;
}
return $this->linkProject($parent, $all, $done, $seen);
}
private function getAllReachableAncestors(array $projects) {
$ancestors = array();
$seen = mpull($projects, null, 'getPHID');
$stack = $projects;
while ($stack) {
$project = array_pop($stack);
$phid = $project->getPHID();
$ancestors[$phid] = $project;
$parent_phid = $project->getParentProjectPHID();
if (!$parent_phid) {
continue;
}
if (isset($seen[$parent_phid])) {
continue;
}
$seen[$parent_phid] = true;
$stack[] = $project->getParentProject();
}
return $ancestors;
}
private function loadSlugs(array $projects) {
// Build a map from primary slugs to projects.
$primary_map = array();
foreach ($projects as $project) {
$primary_slug = $project->getPrimarySlug();
if ($primary_slug === null) {
continue;
}
$primary_map[$primary_slug] = $project;
}
// Link up all of the queried slugs which correspond to primary
// slugs. If we can link up everything from this (no slugs were queried,
// or only primary slugs were queried) we don't need to load anything
// else.
$unknown = $this->slugNormals;
foreach ($unknown as $input => $normal) {
if (isset($primary_map[$input])) {
$match = $input;
} else if (isset($primary_map[$normal])) {
$match = $normal;
} else {
continue;
}
$this->slugMap[$input] = array(
'slug' => $match,
'projectPHID' => $primary_map[$match]->getPHID(),
);
unset($unknown[$input]);
}
// If we need slugs, we have to load everything.
// If we still have some queried slugs which we haven't mapped, we only
// need to look for them.
// If we've mapped everything, we don't have to do any work.
$project_phids = mpull($projects, 'getPHID');
if ($this->needSlugs) {
$slugs = id(new PhabricatorProjectSlug())->loadAllWhere(
'projectPHID IN (%Ls)',
$project_phids);
} else if ($unknown) {
$slugs = id(new PhabricatorProjectSlug())->loadAllWhere(
'projectPHID IN (%Ls) AND slug IN (%Ls)',
$project_phids,
$unknown);
} else {
$slugs = array();
}
// Link up any slugs we were not able to link up earlier.
$extra_map = mpull($slugs, 'getProjectPHID', 'getSlug');
foreach ($unknown as $input => $normal) {
if (isset($extra_map[$input])) {
$match = $input;
} else if (isset($extra_map[$normal])) {
$match = $normal;
} else {
continue;
}
$this->slugMap[$input] = array(
'slug' => $match,
'projectPHID' => $extra_map[$match],
);
unset($unknown[$input]);
}
if ($this->needSlugs) {
$slug_groups = mgroup($slugs, 'getProjectPHID');
foreach ($projects as $project) {
$project_slugs = idx($slug_groups, $project->getPHID(), array());
$project->attachSlugs($project_slugs);
}
}
}
private function getNameTokensForQuery(array $tokens) {
// When querying for projects by name, only actually search for the five
// longest tokens. MySQL can get grumpy with a large number of JOINs
// with LIKEs and queries for more than 5 tokens are essentially never
// legitimate searches for projects, but users copy/pasting nonsense.
// See also PHI47.
$length_map = array();
foreach ($tokens as $token) {
$length_map[$token] = strlen($token);
}
arsort($length_map);
$length_map = array_slice($length_map, 0, 5, true);
return array_keys($length_map);
}
}
diff --git a/src/applications/project/query/PhabricatorProjectSearchEngine.php b/src/applications/project/query/PhabricatorProjectSearchEngine.php
index 02e795395..88a35676c 100644
--- a/src/applications/project/query/PhabricatorProjectSearchEngine.php
+++ b/src/applications/project/query/PhabricatorProjectSearchEngine.php
@@ -1,279 +1,294 @@
<?php
final class PhabricatorProjectSearchEngine
extends PhabricatorApplicationSearchEngine {
public function getResultTypeDescription() {
return pht('Projects');
}
public function getApplicationClassName() {
return 'PhabricatorProjectApplication';
}
public function newQuery() {
return id(new PhabricatorProjectQuery())
->needImages(true)
->needMembers(true)
->needWatchers(true);
}
protected function buildCustomSearchFields() {
+ $subtype_map = id(new PhabricatorProject())->newEditEngineSubtypeMap();
+ $hide_subtypes = ($subtype_map->getCount() == 1);
+
return array(
id(new PhabricatorSearchTextField())
->setLabel(pht('Name'))
->setKey('name')
->setDescription(
pht(
'(Deprecated.) Search for projects with a given name or '.
'hashtag using tokenizer/datasource query matching rules. This '.
'is deprecated in favor of the more powerful "query" '.
'constraint.')),
id(new PhabricatorSearchStringListField())
->setLabel(pht('Slugs'))
->setIsHidden(true)
->setKey('slugs')
->setDescription(
pht(
'Search for projects with particular slugs. (Slugs are the same '.
'as project hashtags.)')),
id(new PhabricatorUsersSearchField())
->setLabel(pht('Members'))
->setKey('memberPHIDs')
->setConduitKey('members')
->setAliases(array('member', 'members')),
id(new PhabricatorUsersSearchField())
->setLabel(pht('Watchers'))
->setKey('watcherPHIDs')
->setConduitKey('watchers')
->setAliases(array('watcher', 'watchers')),
id(new PhabricatorSearchSelectField())
->setLabel(pht('Status'))
->setKey('status')
->setOptions($this->getStatusOptions()),
id(new PhabricatorSearchThreeStateField())
->setLabel(pht('Milestones'))
->setKey('isMilestone')
->setOptions(
pht('(Show All)'),
pht('Show Only Milestones'),
pht('Hide Milestones'))
->setDescription(
pht(
'Pass true to find only milestones, or false to omit '.
'milestones.')),
+ id(new PhabricatorSearchDatasourceField())
+ ->setLabel(pht('Subtypes'))
+ ->setKey('subtypes')
+ ->setAliases(array('subtype'))
+ ->setDescription(
+ pht('Search for projects with given subtypes.'))
+ ->setDatasource(new PhabricatorProjectSubtypeDatasource())
+ ->setIsHidden($hide_subtypes),
id(new PhabricatorSearchCheckboxesField())
->setLabel(pht('Icons'))
->setKey('icons')
->setOptions($this->getIconOptions()),
id(new PhabricatorSearchCheckboxesField())
->setLabel(pht('Colors'))
->setKey('colors')
->setOptions($this->getColorOptions()),
id(new PhabricatorPHIDsSearchField())
->setLabel(pht('Parent Projects'))
->setKey('parentPHIDs')
->setConduitKey('parents')
->setAliases(array('parent', 'parents', 'parentPHID'))
->setDescription(pht('Find direct subprojects of specified parents.')),
id(new PhabricatorPHIDsSearchField())
->setLabel(pht('Ancestor Projects'))
->setKey('ancestorPHIDs')
->setConduitKey('ancestors')
->setAliases(array('ancestor', 'ancestors', 'ancestorPHID'))
->setDescription(
pht('Find all subprojects beneath specified ancestors.')),
);
}
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
if (strlen($map['name'])) {
$tokens = PhabricatorTypeaheadDatasource::tokenizeString($map['name']);
$query->withNameTokens($tokens);
}
if ($map['slugs']) {
$query->withSlugs($map['slugs']);
}
if ($map['memberPHIDs']) {
$query->withMemberPHIDs($map['memberPHIDs']);
}
if ($map['watcherPHIDs']) {
$query->withWatcherPHIDs($map['watcherPHIDs']);
}
if ($map['status']) {
$status = idx($this->getStatusValues(), $map['status']);
if ($status) {
$query->withStatus($status);
}
}
if ($map['icons']) {
$query->withIcons($map['icons']);
}
if ($map['colors']) {
$query->withColors($map['colors']);
}
if ($map['isMilestone'] !== null) {
$query->withIsMilestone($map['isMilestone']);
}
if ($map['parentPHIDs']) {
$query->withParentProjectPHIDs($map['parentPHIDs']);
}
if ($map['ancestorPHIDs']) {
$query->withAncestorProjectPHIDs($map['ancestorPHIDs']);
}
+ if ($map['subtypes']) {
+ $query->withSubtypes($map['subtypes']);
+ }
+
return $query;
}
protected function getURI($path) {
return '/project/'.$path;
}
protected function getBuiltinQueryNames() {
$names = array();
if ($this->requireViewer()->isLoggedIn()) {
$names['joined'] = pht('Joined');
}
if ($this->requireViewer()->isLoggedIn()) {
$names['watching'] = pht('Watching');
}
$names['active'] = pht('Active');
$names['all'] = pht('All');
return $names;
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
$viewer_phid = $this->requireViewer()->getPHID();
// By default, do not show milestones in the list view.
$query->setParameter('isMilestone', false);
switch ($query_key) {
case 'all':
return $query;
case 'active':
return $query
->setParameter('status', 'active');
case 'joined':
return $query
->setParameter('memberPHIDs', array($viewer_phid))
->setParameter('status', 'active');
case 'watching':
return $query
->setParameter('watcherPHIDs', array($viewer_phid))
->setParameter('status', 'active');
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
private function getStatusOptions() {
return array(
'active' => pht('Show Only Active Projects'),
'archived' => pht('Show Only Archived Projects'),
'all' => pht('Show All Projects'),
);
}
private function getStatusValues() {
return array(
'active' => PhabricatorProjectQuery::STATUS_ACTIVE,
'archived' => PhabricatorProjectQuery::STATUS_ARCHIVED,
'all' => PhabricatorProjectQuery::STATUS_ANY,
);
}
private function getIconOptions() {
$options = array();
$set = new PhabricatorProjectIconSet();
foreach ($set->getIcons() as $icon) {
if ($icon->getIsDisabled()) {
continue;
}
$options[$icon->getKey()] = array(
id(new PHUIIconView())
->setIcon($icon->getIcon()),
' ',
$icon->getLabel(),
);
}
return $options;
}
private function getColorOptions() {
$options = array();
foreach (PhabricatorProjectIconSet::getColorMap() as $color => $name) {
$options[$color] = array(
id(new PHUITagView())
->setType(PHUITagView::TYPE_SHADE)
->setColor($color)
->setName($name),
);
}
return $options;
}
protected function renderResultList(
array $projects,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($projects, 'PhabricatorProject');
$viewer = $this->requireViewer();
$list = id(new PhabricatorProjectListView())
->setUser($viewer)
->setProjects($projects)
->setShowWatching(true)
->setShowMember(true)
->renderList();
return id(new PhabricatorApplicationSearchResultView())
->setObjectList($list)
->setNoDataString(pht('No projects found.'));
}
protected function getNewUserBody() {
$create_button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('Create a Project'))
->setHref('/project/edit/')
->setColor(PHUIButtonView::GREEN);
$icon = $this->getApplication()->getIcon();
$app_name = $this->getApplication()->getName();
$view = id(new PHUIBigInfoView())
->setIcon($icon)
->setTitle(pht('Welcome to %s', $app_name))
->setDescription(
pht('Projects are flexible storage containers used as '.
'tags, teams, projects, or anything you need to group.'))
->addAction($create_button);
return $view;
}
}
diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php
index f134b2c63..5182a941b 100644
--- a/src/applications/project/storage/PhabricatorProject.php
+++ b/src/applications/project/storage/PhabricatorProject.php
@@ -1,887 +1,907 @@
<?php
final class PhabricatorProject extends PhabricatorProjectDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorFlaggableInterface,
PhabricatorPolicyInterface,
PhabricatorExtendedPolicyInterface,
PhabricatorCustomFieldInterface,
PhabricatorDestructibleInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface,
PhabricatorConduitResultInterface,
PhabricatorColumnProxyInterface,
- PhabricatorSpacesInterface {
+ PhabricatorSpacesInterface,
+ PhabricatorEditEngineSubtypeInterface {
protected $name;
protected $status = PhabricatorProjectStatus::STATUS_ACTIVE;
protected $authorPHID;
protected $primarySlug;
protected $profileImagePHID;
protected $icon;
protected $color;
protected $mailKey;
protected $viewPolicy;
protected $editPolicy;
protected $joinPolicy;
protected $isMembershipLocked;
protected $parentProjectPHID;
protected $hasWorkboard;
protected $hasMilestones;
protected $hasSubprojects;
protected $milestoneNumber;
protected $projectPath;
protected $projectDepth;
protected $projectPathKey;
protected $properties = array();
protected $spacePHID;
+ protected $subtype;
private $memberPHIDs = self::ATTACHABLE;
private $watcherPHIDs = self::ATTACHABLE;
private $sparseWatchers = self::ATTACHABLE;
private $sparseMembers = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
private $profileImageFile = self::ATTACHABLE;
private $slugs = self::ATTACHABLE;
private $parentProject = self::ATTACHABLE;
const TABLE_DATASOURCE_TOKEN = 'project_datasourcetoken';
const ITEM_PICTURE = 'project.picture';
const ITEM_PROFILE = 'project.profile';
const ITEM_POINTS = 'project.points';
const ITEM_WORKBOARD = 'project.workboard';
const ITEM_MEMBERS = 'project.members';
const ITEM_MANAGE = 'project.manage';
const ITEM_MILESTONES = 'project.milestones';
const ITEM_SUBPROJECTS = 'project.subprojects';
public static function initializeNewProject(
PhabricatorUser $actor,
PhabricatorProject $parent = null) {
$app = id(new PhabricatorApplicationQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withClasses(array('PhabricatorProjectApplication'))
->executeOne();
$view_policy = $app->getPolicy(
ProjectDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(
ProjectDefaultEditCapability::CAPABILITY);
$join_policy = $app->getPolicy(
ProjectDefaultJoinCapability::CAPABILITY);
// If this is the child of some other project, default the Space to the
// Space of the parent.
if ($parent) {
$space_phid = $parent->getSpacePHID();
} else {
$space_phid = $actor->getDefaultSpacePHID();
}
$default_icon = PhabricatorProjectIconSet::getDefaultIconKey();
$default_color = PhabricatorProjectIconSet::getDefaultColorKey();
return id(new PhabricatorProject())
->setAuthorPHID($actor->getPHID())
->setIcon($default_icon)
->setColor($default_color)
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setJoinPolicy($join_policy)
->setSpacePHID($space_phid)
->setIsMembershipLocked(0)
->attachMemberPHIDs(array())
->attachSlugs(array())
->setHasWorkboard(0)
->setHasMilestones(0)
->setHasSubprojects(0)
+ ->setSubtype(PhabricatorEditEngineSubtype::SUBTYPE_DEFAULT)
->attachParentProject(null);
}
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
PhabricatorPolicyCapability::CAN_JOIN,
);
}
public function getPolicy($capability) {
if ($this->isMilestone()) {
return $this->getParentProject()->getPolicy($capability);
}
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
case PhabricatorPolicyCapability::CAN_JOIN:
return $this->getJoinPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($this->isMilestone()) {
return $this->getParentProject()->hasAutomaticCapability(
$capability,
$viewer);
}
$can_edit = PhabricatorPolicyCapability::CAN_EDIT;
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
if ($this->isUserMember($viewer->getPHID())) {
// Project members can always view a project.
return true;
}
break;
case PhabricatorPolicyCapability::CAN_EDIT:
$parent = $this->getParentProject();
if ($parent) {
$can_edit_parent = PhabricatorPolicyFilter::hasCapability(
$viewer,
$parent,
$can_edit);
if ($can_edit_parent) {
return true;
}
}
break;
case PhabricatorPolicyCapability::CAN_JOIN:
if (PhabricatorPolicyFilter::hasCapability($viewer, $this, $can_edit)) {
// Project editors can always join a project.
return true;
}
break;
}
return false;
}
public function describeAutomaticCapability($capability) {
// TODO: Clarify the additional rules that parent and subprojects imply.
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return pht('Members of a project can always view it.');
case PhabricatorPolicyCapability::CAN_JOIN:
return pht('Users who can edit a project can always join it.');
}
return null;
}
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
$extended = array();
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$parent = $this->getParentProject();
if ($parent) {
$extended[] = array(
$parent,
PhabricatorPolicyCapability::CAN_VIEW,
);
}
break;
}
return $extended;
}
public function isUserMember($user_phid) {
if ($this->memberPHIDs !== self::ATTACHABLE) {
return in_array($user_phid, $this->memberPHIDs);
}
return $this->assertAttachedKey($this->sparseMembers, $user_phid);
}
public function setIsUserMember($user_phid, $is_member) {
if ($this->sparseMembers === self::ATTACHABLE) {
$this->sparseMembers = array();
}
$this->sparseMembers[$user_phid] = $is_member;
return $this;
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort128',
'status' => 'text32',
'primarySlug' => 'text128?',
'isMembershipLocked' => 'bool',
'profileImagePHID' => 'phid?',
'icon' => 'text32',
'color' => 'text32',
'mailKey' => 'bytes20',
'joinPolicy' => 'policy',
'parentProjectPHID' => 'phid?',
'hasWorkboard' => 'bool',
'hasMilestones' => 'bool',
'hasSubprojects' => 'bool',
'milestoneNumber' => 'uint32?',
'projectPath' => 'hashpath64',
'projectDepth' => 'uint32',
'projectPathKey' => 'bytes4',
+ 'subtype' => 'text64',
),
self::CONFIG_KEY_SCHEMA => array(
'key_icon' => array(
'columns' => array('icon'),
),
'key_color' => array(
'columns' => array('color'),
),
'key_milestone' => array(
'columns' => array('parentProjectPHID', 'milestoneNumber'),
'unique' => true,
),
'key_primaryslug' => array(
'columns' => array('primarySlug'),
'unique' => true,
),
'key_path' => array(
'columns' => array('projectPath', 'projectDepth'),
),
'key_pathkey' => array(
'columns' => array('projectPathKey'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorProjectProjectPHIDType::TYPECONST);
}
public function attachMemberPHIDs(array $phids) {
$this->memberPHIDs = $phids;
return $this;
}
public function getMemberPHIDs() {
return $this->assertAttached($this->memberPHIDs);
}
public function isArchived() {
return ($this->getStatus() == PhabricatorProjectStatus::STATUS_ARCHIVED);
}
public function getProfileImageURI() {
return $this->getProfileImageFile()->getBestURI();
}
public function attachProfileImageFile(PhabricatorFile $file) {
$this->profileImageFile = $file;
return $this;
}
public function getProfileImageFile() {
return $this->assertAttached($this->profileImageFile);
}
public function isUserWatcher($user_phid) {
if ($this->watcherPHIDs !== self::ATTACHABLE) {
return in_array($user_phid, $this->watcherPHIDs);
}
return $this->assertAttachedKey($this->sparseWatchers, $user_phid);
}
public function isUserAncestorWatcher($user_phid) {
$is_watcher = $this->isUserWatcher($user_phid);
if (!$is_watcher) {
$parent = $this->getParentProject();
if ($parent) {
return $parent->isUserWatcher($user_phid);
}
}
return $is_watcher;
}
public function getWatchedAncestorPHID($user_phid) {
if ($this->isUserWatcher($user_phid)) {
return $this->getPHID();
}
$parent = $this->getParentProject();
if ($parent) {
return $parent->getWatchedAncestorPHID($user_phid);
}
return null;
}
public function setIsUserWatcher($user_phid, $is_watcher) {
if ($this->sparseWatchers === self::ATTACHABLE) {
$this->sparseWatchers = array();
}
$this->sparseWatchers[$user_phid] = $is_watcher;
return $this;
}
public function attachWatcherPHIDs(array $phids) {
$this->watcherPHIDs = $phids;
return $this;
}
public function getWatcherPHIDs() {
return $this->assertAttached($this->watcherPHIDs);
}
public function getAllAncestorWatcherPHIDs() {
$parent = $this->getParentProject();
if ($parent) {
$watchers = $parent->getAllAncestorWatcherPHIDs();
} else {
$watchers = array();
}
foreach ($this->getWatcherPHIDs() as $phid) {
$watchers[$phid] = $phid;
}
return $watchers;
}
public function attachSlugs(array $slugs) {
$this->slugs = $slugs;
return $this;
}
public function getSlugs() {
return $this->assertAttached($this->slugs);
}
public function getColor() {
if ($this->isArchived()) {
return PHUITagView::COLOR_DISABLED;
}
return $this->color;
}
public function getURI() {
$id = $this->getID();
return "/project/view/{$id}/";
}
public function getProfileURI() {
$id = $this->getID();
return "/project/profile/{$id}/";
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
if (!strlen($this->getPHID())) {
$this->setPHID($this->generatePHID());
}
if (!strlen($this->getProjectPathKey())) {
$hash = PhabricatorHash::digestForIndex($this->getPHID());
$hash = substr($hash, 0, 4);
$this->setProjectPathKey($hash);
}
$path = array();
$depth = 0;
if ($this->parentProjectPHID) {
$parent = $this->getParentProject();
$path[] = $parent->getProjectPath();
$depth = $parent->getProjectDepth() + 1;
}
$path[] = $this->getProjectPathKey();
$path = implode('', $path);
$limit = self::getProjectDepthLimit();
if ($depth >= $limit) {
throw new Exception(pht('Project depth is too great.'));
}
$this->setProjectPath($path);
$this->setProjectDepth($depth);
$this->openTransaction();
$result = parent::save();
$this->updateDatasourceTokens();
$this->saveTransaction();
return $result;
}
public static function getProjectDepthLimit() {
// This is limited by how many path hashes we can fit in the path
// column.
return 16;
}
public function updateDatasourceTokens() {
$table = self::TABLE_DATASOURCE_TOKEN;
$conn_w = $this->establishConnection('w');
$id = $this->getID();
$slugs = queryfx_all(
$conn_w,
'SELECT * FROM %T WHERE projectPHID = %s',
id(new PhabricatorProjectSlug())->getTableName(),
$this->getPHID());
$all_strings = ipull($slugs, 'slug');
$all_strings[] = $this->getDisplayName();
$all_strings = implode(' ', $all_strings);
$tokens = PhabricatorTypeaheadDatasource::tokenizeString($all_strings);
$sql = array();
foreach ($tokens as $token) {
$sql[] = qsprintf($conn_w, '(%d, %s)', $id, $token);
}
$this->openTransaction();
queryfx(
$conn_w,
'DELETE FROM %T WHERE projectID = %d',
$table,
$id);
foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
queryfx(
$conn_w,
'INSERT INTO %T (projectID, token) VALUES %LQ',
$table,
$chunk);
}
$this->saveTransaction();
}
public function isMilestone() {
return ($this->getMilestoneNumber() !== null);
}
public function getParentProject() {
return $this->assertAttached($this->parentProject);
}
public function attachParentProject(PhabricatorProject $project = null) {
$this->parentProject = $project;
return $this;
}
public function getAncestorProjectPaths() {
$parts = array();
$path = $this->getProjectPath();
$parent_length = (strlen($path) - 4);
for ($ii = $parent_length; $ii > 0; $ii -= 4) {
$parts[] = substr($path, 0, $ii);
}
return $parts;
}
public function getAncestorProjects() {
$ancestors = array();
$cursor = $this->getParentProject();
while ($cursor) {
$ancestors[] = $cursor;
$cursor = $cursor->getParentProject();
}
return $ancestors;
}
public function supportsEditMembers() {
if ($this->isMilestone()) {
return false;
}
if ($this->getHasSubprojects()) {
return false;
}
return true;
}
public function supportsMilestones() {
if ($this->isMilestone()) {
return false;
}
return true;
}
public function supportsSubprojects() {
if ($this->isMilestone()) {
return false;
}
return true;
}
public function loadNextMilestoneNumber() {
$current = queryfx_one(
$this->establishConnection('w'),
'SELECT MAX(milestoneNumber) n
FROM %T
WHERE parentProjectPHID = %s',
$this->getTableName(),
$this->getPHID());
if (!$current) {
$number = 1;
} else {
$number = (int)$current['n'] + 1;
}
return $number;
}
public function getDisplayName() {
$name = $this->getName();
// If this is a milestone, show it as "Parent > Sprint 99".
if ($this->isMilestone()) {
$name = pht(
'%s (%s)',
$this->getParentProject()->getName(),
$name);
}
return $name;
}
public function getDisplayIconKey() {
if ($this->isMilestone()) {
$key = PhabricatorProjectIconSet::getMilestoneIconKey();
} else {
$key = $this->getIcon();
}
return $key;
}
public function getDisplayIconIcon() {
$key = $this->getDisplayIconKey();
return PhabricatorProjectIconSet::getIconIcon($key);
}
public function getDisplayIconName() {
$key = $this->getDisplayIconKey();
return PhabricatorProjectIconSet::getIconName($key);
}
public function getDisplayColor() {
if ($this->isMilestone()) {
return $this->getParentProject()->getColor();
}
return $this->getColor();
}
public function getDisplayIconComposeIcon() {
$icon = $this->getDisplayIconIcon();
return $icon;
}
public function getDisplayIconComposeColor() {
$color = $this->getDisplayColor();
$map = array(
'grey' => 'charcoal',
'checkered' => 'backdrop',
);
return idx($map, $color, $color);
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getDefaultWorkboardSort() {
return $this->getProperty('workboard.sort.default');
}
public function setDefaultWorkboardSort($sort) {
return $this->setProperty('workboard.sort.default', $sort);
}
public function getDefaultWorkboardFilter() {
return $this->getProperty('workboard.filter.default');
}
public function setDefaultWorkboardFilter($filter) {
return $this->setProperty('workboard.filter.default', $filter);
}
public function getWorkboardBackgroundColor() {
return $this->getProperty('workboard.background');
}
public function setWorkboardBackgroundColor($color) {
return $this->setProperty('workboard.background', $color);
}
public function getDisplayWorkboardBackgroundColor() {
$color = $this->getWorkboardBackgroundColor();
if ($color === null) {
$parent = $this->getParentProject();
if ($parent) {
return $parent->getDisplayWorkboardBackgroundColor();
}
}
if ($color === 'none') {
$color = null;
}
return $color;
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('projects.fields');
}
public function getCustomFieldBaseClass() {
return 'PhabricatorProjectCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorProjectTransactionEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorProjectTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorSpacesInterface )----------------------------------------- */
public function getSpacePHID() {
if ($this->isMilestone()) {
return $this->getParentProject()->getSpacePHID();
}
return $this->spacePHID;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$columns = id(new PhabricatorProjectColumn())
->loadAllWhere('projectPHID = %s', $this->getPHID());
foreach ($columns as $column) {
$engine->destroyObject($column);
}
$slugs = id(new PhabricatorProjectSlug())
->loadAllWhere('projectPHID = %s', $this->getPHID());
foreach ($slugs as $slug) {
$slug->delete();
}
$this->saveTransaction();
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PhabricatorProjectFulltextEngine();
}
/* -( PhabricatorFerretInterface )--------------------------------------- */
public function newFerretEngine() {
return new PhabricatorProjectFerretEngine();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of the project.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('slug')
->setType('string')
->setDescription(pht('Primary slug/hashtag.')),
+ id(new PhabricatorConduitSearchFieldSpecification())
+ ->setKey('subtype')
+ ->setType('string')
+ ->setDescription(pht('Subtype of the project.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('milestone')
->setType('int?')
->setDescription(pht('For milestones, milestone sequence number.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('parent')
->setType('map<string, wild>?')
->setDescription(
pht(
'For subprojects and milestones, a brief description of the '.
'parent project.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('depth')
->setType('int')
->setDescription(
pht(
'For subprojects and milestones, depth of this project in the '.
'tree. Root projects have depth 0.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('icon')
->setType('map<string, wild>')
->setDescription(pht('Information about the project icon.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('color')
->setType('map<string, wild>')
->setDescription(pht('Information about the project color.')),
);
}
public function getFieldValuesForConduit() {
$color_key = $this->getColor();
$color_name = PhabricatorProjectIconSet::getColorName($color_key);
if ($this->isMilestone()) {
$milestone = (int)$this->getMilestoneNumber();
} else {
$milestone = null;
}
$parent = $this->getParentProject();
if ($parent) {
$parent_ref = $parent->getRefForConduit();
} else {
$parent_ref = null;
}
return array(
'name' => $this->getName(),
'slug' => $this->getPrimarySlug(),
+ 'subtype' => $this->getSubtype(),
'milestone' => $milestone,
'depth' => (int)$this->getProjectDepth(),
'parent' => $parent_ref,
'icon' => array(
'key' => $this->getDisplayIconKey(),
'name' => $this->getDisplayIconName(),
'icon' => $this->getDisplayIconIcon(),
),
'color' => array(
'key' => $color_key,
'name' => $color_name,
),
);
}
public function getConduitSearchAttachments() {
return array(
id(new PhabricatorProjectsMembersSearchEngineAttachment())
->setAttachmentKey('members'),
id(new PhabricatorProjectsWatchersSearchEngineAttachment())
->setAttachmentKey('watchers'),
id(new PhabricatorProjectsAncestorsSearchEngineAttachment())
->setAttachmentKey('ancestors'),
);
}
/**
* Get an abbreviated representation of this project for use in providing
* "parent" and "ancestor" information.
*/
public function getRefForConduit() {
return array(
'id' => (int)$this->getID(),
'phid' => $this->getPHID(),
'name' => $this->getName(),
);
}
/* -( PhabricatorColumnProxyInterface )------------------------------------ */
public function getProxyColumnName() {
return $this->getName();
}
public function getProxyColumnIcon() {
return $this->getDisplayIconIcon();
}
public function getProxyColumnClass() {
if ($this->isMilestone()) {
return 'phui-workboard-column-milestone';
}
return null;
}
+/* -( PhabricatorEditEngineSubtypeInterface )------------------------------ */
+
+
+ public function getEditEngineSubtype() {
+ return $this->getSubtype();
+ }
+
+ public function setEditEngineSubtype($value) {
+ return $this->setSubtype($value);
+ }
+
+ public function newEditEngineSubtypeMap() {
+ $config = PhabricatorEnv::getEnvConfig('projects.subtypes');
+ return PhabricatorEditEngineSubtype::newSubtypeMap($config);
+ }
+
+ public function newSubtypeObject() {
+ $subtype_key = $this->getEditEngineSubtype();
+ $subtype_map = $this->newEditEngineSubtypeMap();
+ return $subtype_map->getSubtype($subtype_key);
+ }
+
}
diff --git a/src/applications/project/storage/PhabricatorProjectColumn.php b/src/applications/project/storage/PhabricatorProjectColumn.php
index 7ddfb4351..756c356ee 100644
--- a/src/applications/project/storage/PhabricatorProjectColumn.php
+++ b/src/applications/project/storage/PhabricatorProjectColumn.php
@@ -1,305 +1,294 @@
<?php
final class PhabricatorProjectColumn
extends PhabricatorProjectDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface,
PhabricatorExtendedPolicyInterface,
PhabricatorConduitResultInterface {
const STATUS_ACTIVE = 0;
const STATUS_HIDDEN = 1;
const DEFAULT_ORDER = 'natural';
const ORDER_NATURAL = 'natural';
const ORDER_PRIORITY = 'priority';
protected $name;
protected $status;
protected $projectPHID;
protected $proxyPHID;
protected $sequence;
protected $properties = array();
private $project = self::ATTACHABLE;
private $proxy = self::ATTACHABLE;
public static function initializeNewColumn(PhabricatorUser $user) {
return id(new PhabricatorProjectColumn())
->setName('')
->setStatus(self::STATUS_ACTIVE)
->attachProxy(null);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text255',
'status' => 'uint32',
'sequence' => 'uint32',
'proxyPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_status' => array(
'columns' => array('projectPHID', 'status', 'sequence'),
),
'key_sequence' => array(
'columns' => array('projectPHID', 'sequence'),
),
'key_proxy' => array(
'columns' => array('projectPHID', 'proxyPHID'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorProjectColumnPHIDType::TYPECONST);
}
public function attachProject(PhabricatorProject $project) {
$this->project = $project;
return $this;
}
public function getProject() {
return $this->assertAttached($this->project);
}
public function attachProxy($proxy) {
$this->proxy = $proxy;
return $this;
}
public function getProxy() {
return $this->assertAttached($this->proxy);
}
public function isDefaultColumn() {
return (bool)$this->getProperty('isDefault');
}
public function isHidden() {
$proxy = $this->getProxy();
if ($proxy) {
return $proxy->isArchived();
}
return ($this->getStatus() == self::STATUS_HIDDEN);
}
public function getDisplayName() {
$proxy = $this->getProxy();
if ($proxy) {
return $proxy->getProxyColumnName();
}
$name = $this->getName();
if (strlen($name)) {
return $name;
}
if ($this->isDefaultColumn()) {
return pht('Backlog');
}
return pht('Unnamed Column');
}
public function getDisplayType() {
if ($this->isDefaultColumn()) {
return pht('(Default)');
}
if ($this->isHidden()) {
return pht('(Hidden)');
}
return null;
}
public function getDisplayClass() {
$proxy = $this->getProxy();
if ($proxy) {
return $proxy->getProxyColumnClass();
}
return null;
}
public function getHeaderIcon() {
$proxy = $this->getProxy();
if ($proxy) {
return $proxy->getProxyColumnIcon();
}
if ($this->isHidden()) {
return 'fa-eye-slash';
}
return null;
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getPointLimit() {
return $this->getProperty('pointLimit');
}
public function setPointLimit($limit) {
$this->setProperty('pointLimit', $limit);
return $this;
}
public function getOrderingKey() {
$proxy = $this->getProxy();
// Normal columns and subproject columns go first, in a user-controlled
// order.
// All the milestone columns go last, in their sequential order.
if (!$proxy || !$proxy->isMilestone()) {
$group = 'A';
$sequence = $this->getSequence();
} else {
$group = 'B';
$sequence = $proxy->getMilestoneNumber();
}
return sprintf('%s%012d', $group, $sequence);
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The display name of the column.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('project')
->setType('map<string, wild>')
->setDescription(pht('The project the column belongs to.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('proxyPHID')
->setType('phid?')
->setDescription(
pht(
'For columns that proxy another object (like a subproject or '.
'milestone), the PHID of the object they proxy.')),
);
}
public function getFieldValuesForConduit() {
return array(
'name' => $this->getDisplayName(),
'proxyPHID' => $this->getProxyPHID(),
'project' => $this->getProject()->getRefForConduit(),
);
}
public function getConduitSearchAttachments() {
return array();
}
public function getRefForConduit() {
return array(
'id' => (int)$this->getID(),
'phid' => $this->getPHID(),
'name' => $this->getDisplayName(),
);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorProjectColumnTransactionEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorProjectColumnTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
// NOTE: Column policies are enforced as an extended policy which makes
// them the same as the project's policies.
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::getMostOpenPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return PhabricatorPolicies::POLICY_USER;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getProject()->hasAutomaticCapability(
$capability,
$viewer);
}
public function describeAutomaticCapability($capability) {
return pht('Users must be able to see a project to see its board.');
}
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
return array(
array($this->getProject(), $capability),
);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/project/typeahead/PhabricatorProjectDatasource.php b/src/applications/project/typeahead/PhabricatorProjectDatasource.php
index d0abcad2c..5b999a997 100644
--- a/src/applications/project/typeahead/PhabricatorProjectDatasource.php
+++ b/src/applications/project/typeahead/PhabricatorProjectDatasource.php
@@ -1,155 +1,157 @@
<?php
final class PhabricatorProjectDatasource
extends PhabricatorTypeaheadDatasource {
public function getBrowseTitle() {
return pht('Browse Projects');
}
public function getPlaceholderText() {
return pht('Type a project name...');
}
public function getDatasourceApplicationClass() {
return 'PhabricatorProjectApplication';
}
public function loadResults() {
$viewer = $this->getViewer();
$raw_query = $this->getRawQuery();
// Allow users to type "#qa" or "qa" to find "Quality Assurance".
$raw_query = ltrim($raw_query, '#');
$tokens = self::tokenizeString($raw_query);
$query = id(new PhabricatorProjectQuery())
->needImages(true)
- ->needSlugs(true);
+ ->needSlugs(true)
+ ->setOrderVector(array('-status', 'id'));
if ($this->getPhase() == self::PHASE_PREFIX) {
$prefix = $this->getPrefixQuery();
$query->withNamePrefixes(array($prefix));
} else if ($tokens) {
$query->withNameTokens($tokens);
}
// If this is for policy selection, prevent users from using milestones.
$for_policy = $this->getParameter('policy');
if ($for_policy) {
$query->withIsMilestone(false);
}
$for_autocomplete = $this->getParameter('autocomplete');
$projs = $this->executeQuery($query);
$projs = mpull($projs, null, 'getPHID');
$must_have_cols = $this->getParameter('mustHaveColumns', false);
if ($must_have_cols) {
$columns = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withProjectPHIDs(array_keys($projs))
+ ->withIsProxyColumn(false)
->execute();
$has_cols = mgroup($columns, 'getProjectPHID');
} else {
$has_cols = array_fill_keys(array_keys($projs), true);
}
$is_browse = $this->getIsBrowse();
if ($is_browse && $projs) {
// TODO: This is a little ad-hoc, but we don't currently have
// infrastructure for bulk querying custom fields efficiently.
$table = new PhabricatorProjectCustomFieldStorage();
$descriptions = $table->loadAllWhere(
'objectPHID IN (%Ls) AND fieldIndex = %s',
array_keys($projs),
PhabricatorHash::digestForIndex('std:project:internal:description'));
$descriptions = mpull($descriptions, 'getFieldValue', 'getObjectPHID');
} else {
$descriptions = array();
}
$results = array();
foreach ($projs as $proj) {
$phid = $proj->getPHID();
if (!isset($has_cols[$phid])) {
continue;
}
$slug = $proj->getPrimarySlug();
if (!strlen($slug)) {
foreach ($proj->getSlugs() as $slug_object) {
$slug = $slug_object->getSlug();
if (strlen($slug)) {
break;
}
}
}
// If we're building results for the autocompleter and this project
// doesn't have any usable slugs, don't return it as a result.
if ($for_autocomplete && !strlen($slug)) {
continue;
}
$closed = null;
if ($proj->isArchived()) {
$closed = pht('Archived');
}
$all_strings = array();
// NOTE: We list the project's name first because results will be
// sorted into prefix vs content phases incorrectly if we don't: it
// will look like "Parent (Milestone)" matched "Parent" as a prefix,
// but it did not.
$all_strings[] = $proj->getName();
if ($proj->isMilestone()) {
$all_strings[] = $proj->getParentProject()->getName();
}
foreach ($proj->getSlugs() as $project_slug) {
$all_strings[] = $project_slug->getSlug();
}
$all_strings = implode("\n", $all_strings);
$proj_result = id(new PhabricatorTypeaheadResult())
->setName($all_strings)
->setDisplayName($proj->getDisplayName())
->setDisplayType($proj->getDisplayIconName())
->setURI($proj->getURI())
->setPHID($phid)
->setIcon($proj->getDisplayIconIcon())
->setColor($proj->getColor())
->setPriorityType('proj')
->setClosed($closed);
if (strlen($slug)) {
$proj_result->setAutocomplete('#'.$slug);
}
$proj_result->setImageURI($proj->getProfileImageURI());
if ($is_browse) {
$proj_result->addAttribute($proj->getDisplayIconName());
$description = idx($descriptions, $phid);
if (strlen($description)) {
$summary = PhabricatorMarkupEngine::summarizeSentence($description);
$proj_result->addAttribute($summary);
}
}
$results[] = $proj_result;
}
return $results;
}
}
diff --git a/src/applications/project/typeahead/PhabricatorProjectSubtypeDatasource.php b/src/applications/project/typeahead/PhabricatorProjectSubtypeDatasource.php
new file mode 100644
index 000000000..68de11e63
--- /dev/null
+++ b/src/applications/project/typeahead/PhabricatorProjectSubtypeDatasource.php
@@ -0,0 +1,45 @@
+<?php
+
+final class PhabricatorProjectSubtypeDatasource
+ extends PhabricatorTypeaheadDatasource {
+
+ public function getBrowseTitle() {
+ return pht('Browse Subtypes');
+ }
+
+ public function getPlaceholderText() {
+ return pht('Type a project subtype name...');
+ }
+
+ public function getDatasourceApplicationClass() {
+ return 'PhabricatorProjectApplication';
+ }
+
+ public function loadResults() {
+ $results = $this->buildResults();
+ return $this->filterResultsAgainstTokens($results);
+ }
+
+ protected function renderSpecialTokens(array $values) {
+ return $this->renderTokensFromResults($this->buildResults(), $values);
+ }
+
+ private function buildResults() {
+ $results = array();
+
+ $subtype_map = id(new PhabricatorProject())->newEditEngineSubtypeMap();
+ foreach ($subtype_map->getSubtypes() as $key => $subtype) {
+
+ $result = id(new PhabricatorTypeaheadResult())
+ ->setIcon($subtype->getIcon())
+ ->setColor($subtype->getColor())
+ ->setPHID($key)
+ ->setName($subtype->getName());
+
+ $results[$key] = $result;
+ }
+
+ return $results;
+ }
+
+}
diff --git a/src/applications/project/view/PhabricatorProjectListView.php b/src/applications/project/view/PhabricatorProjectListView.php
index 645440d83..d8fb011c2 100644
--- a/src/applications/project/view/PhabricatorProjectListView.php
+++ b/src/applications/project/view/PhabricatorProjectListView.php
@@ -1,100 +1,107 @@
<?php
final class PhabricatorProjectListView extends AphrontView {
private $projects;
private $showMember;
private $showWatching;
private $noDataString;
public function setProjects(array $projects) {
$this->projects = $projects;
return $this;
}
public function getProjects() {
return $this->projects;
}
public function setShowWatching($watching) {
$this->showWatching = $watching;
return $this;
}
public function setShowMember($member) {
$this->showMember = $member;
return $this;
}
public function setNoDataString($text) {
$this->noDataString = $text;
return $this;
}
public function renderList() {
$viewer = $this->getUser();
$viewer_phid = $viewer->getPHID();
$projects = $this->getProjects();
$handles = $viewer->loadHandles(mpull($projects, 'getPHID'));
$no_data = pht('No projects found.');
if ($this->noDataString) {
$no_data = $this->noDataString;
}
$list = id(new PHUIObjectItemListView())
->setUser($viewer)
->setNoDataString($no_data);
foreach ($projects as $key => $project) {
$id = $project->getID();
$icon = $project->getDisplayIconIcon();
$icon_icon = id(new PHUIIconView())
->setIcon($icon);
$icon_name = $project->getDisplayIconName();
$item = id(new PHUIObjectItemView())
->setObject($project)
->setHeader($project->getName())
->setHref("/project/view/{$id}/")
->setImageURI($project->getProfileImageURI())
->addAttribute(
array(
$icon_icon,
' ',
$icon_name,
));
if ($project->getStatus() == PhabricatorProjectStatus::STATUS_ARCHIVED) {
$item->addIcon('fa-ban', pht('Archived'));
$item->setDisabled(true);
}
if ($this->showMember) {
$is_member = $project->isUserMember($viewer_phid);
if ($is_member) {
$item->addIcon('fa-user', pht('Member'));
}
}
if ($this->showWatching) {
$is_watcher = $project->isUserWatcher($viewer_phid);
if ($is_watcher) {
$item->addIcon('fa-eye', pht('Watching'));
}
}
+ $subtype = $project->newSubtypeObject();
+ if ($subtype && $subtype->hasTagView()) {
+ $subtype_tag = $subtype->newTagView()
+ ->setSlimShady(true);
+ $item->addAttribute($subtype_tag);
+ }
+
$list->addItem($item);
}
return $list;
}
public function render() {
return $this->renderList();
}
}
diff --git a/src/applications/releeph/controller/request/ReleephRequestCommentController.php b/src/applications/releeph/controller/request/ReleephRequestCommentController.php
index 0a31261a1..96500e8bc 100644
--- a/src/applications/releeph/controller/request/ReleephRequestCommentController.php
+++ b/src/applications/releeph/controller/request/ReleephRequestCommentController.php
@@ -1,63 +1,64 @@
<?php
final class ReleephRequestCommentController
extends ReleephRequestController {
public function handleRequest(AphrontRequest $request) {
$id = $request->getURIData('requestID');
$viewer = $request->getViewer();
if (!$request->isFormPost()) {
return new Aphront400Response();
}
$pull = id(new ReleephRequestQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$pull) {
return new Aphront404Response();
}
$is_preview = $request->isPreviewRequest();
$draft = PhabricatorDraft::buildFromRequest($request);
$view_uri = $this->getApplicationURI('/'.$pull->getMonogram());
$xactions = array();
$xactions[] = id(new ReleephRequestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new ReleephRequestTransactionComment())
->setContent($request->getStr('comment')));
$editor = id(new ReleephRequestTransactionalEditor())
->setActor($viewer)
->setContinueOnNoEffect($request->isContinueRequest())
->setContentSourceFromRequest($request)
->setIsPreview($is_preview);
try {
$xactions = $editor->applyTransactions($pull, $xactions);
} catch (PhabricatorApplicationTransactionNoEffectException $ex) {
return id(new PhabricatorApplicationTransactionNoEffectResponse())
->setCancelURI($view_uri)
->setException($ex);
}
if ($draft) {
$draft->replaceOrDelete();
}
if ($request->isAjax() && $is_preview) {
return id(new PhabricatorApplicationTransactionResponse())
+ ->setObject($pull)
->setViewer($viewer)
->setTransactions($xactions)
->setIsPreview($is_preview);
} else {
return id(new AphrontRedirectResponse())
->setURI($view_uri);
}
}
}
diff --git a/src/applications/releeph/storage/ReleephBranch.php b/src/applications/releeph/storage/ReleephBranch.php
index cef3f16ed..28323a998 100644
--- a/src/applications/releeph/storage/ReleephBranch.php
+++ b/src/applications/releeph/storage/ReleephBranch.php
@@ -1,199 +1,188 @@
<?php
final class ReleephBranch extends ReleephDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface {
protected $releephProjectID;
protected $isActive;
protected $createdByUserPHID;
// The immutable name of this branch ('releases/foo-2013.01.24')
protected $name;
protected $basename;
// The symbolic name of this branch (LATEST, PRODUCTION, RC, ...)
// See SYMBOLIC_NAME_NOTE below
protected $symbolicName;
// Where to cut the branch
protected $cutPointCommitPHID;
protected $details = array();
private $project = self::ATTACHABLE;
private $cutPointCommit = self::ATTACHABLE;
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'details' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'basename' => 'text64',
'isActive' => 'bool',
'symbolicName' => 'text64?',
'name' => 'text128',
),
self::CONFIG_KEY_SCHEMA => array(
'releephProjectID' => array(
'columns' => array('releephProjectID', 'symbolicName'),
'unique' => true,
),
'releephProjectID_2' => array(
'columns' => array('releephProjectID', 'basename'),
'unique' => true,
),
'releephProjectID_name' => array(
'columns' => array('releephProjectID', 'name'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(ReleephBranchPHIDType::TYPECONST);
}
public function getDetail($key, $default = null) {
return idx($this->getDetails(), $key, $default);
}
public function setDetail($key, $value) {
$this->details[$key] = $value;
return $this;
}
protected function willWriteData(array &$data) {
// If symbolicName is omitted, set it to the basename.
//
// This means that we can enforce symbolicName as a UNIQUE column in the
// DB. We'll interpret symbolicName === basename as meaning "no symbolic
// name".
//
// SYMBOLIC_NAME_NOTE
if (!$data['symbolicName']) {
$data['symbolicName'] = $data['basename'];
}
parent::willWriteData($data);
}
public function getSymbolicName() {
// See SYMBOLIC_NAME_NOTE above for why this is needed
if ($this->symbolicName == $this->getBasename()) {
return '';
}
return $this->symbolicName;
}
public function setSymbolicName($name) {
if ($name) {
parent::setSymbolicName($name);
} else {
parent::setSymbolicName($this->getBasename());
}
return $this;
}
public function getDisplayName() {
if ($sn = $this->getSymbolicName()) {
return $sn;
}
return $this->getBasename();
}
public function getDisplayNameWithDetail() {
$n = $this->getBasename();
if ($sn = $this->getSymbolicName()) {
return "{$sn} ({$n})";
} else {
return $n;
}
}
public function getURI($path = null) {
$components = array(
'/releeph/branch',
$this->getID(),
$path,
);
return implode('/', $components);
}
public function isActive() {
return $this->getIsActive();
}
public function attachProject(ReleephProject $project) {
$this->project = $project;
return $this;
}
public function getProject() {
return $this->assertAttached($this->project);
}
public function getProduct() {
return $this->getProject();
}
public function attachCutPointCommit(
PhabricatorRepositoryCommit $commit = null) {
$this->cutPointCommit = $commit;
return $this;
}
public function getCutPointCommit() {
return $this->assertAttached($this->cutPointCommit);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new ReleephBranchEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new ReleephBranchTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return $this->getProduct()->getCapabilities();
}
public function getPolicy($capability) {
return $this->getProduct()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getProduct()->hasAutomaticCapability($capability, $viewer);
}
public function describeAutomaticCapability($capability) {
return pht(
'Release branches have the same policies as the product they are a '.
'part of.');
}
}
diff --git a/src/applications/releeph/storage/ReleephProject.php b/src/applications/releeph/storage/ReleephProject.php
index bda0d0372..db47ac5d3 100644
--- a/src/applications/releeph/storage/ReleephProject.php
+++ b/src/applications/releeph/storage/ReleephProject.php
@@ -1,156 +1,145 @@
<?php
final class ReleephProject extends ReleephDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface {
const DEFAULT_BRANCH_NAMESPACE = 'releeph-releases';
const SYSTEM_AGENT_USERNAME_PREFIX = 'releeph-agent-';
protected $name;
// Specifying the place to pick from is a requirement for svn, though not
// for git. It's always useful though for reasoning about what revs have
// been picked and which haven't.
protected $trunkBranch;
protected $repositoryPHID;
protected $isActive;
protected $createdByUserPHID;
protected $details = array();
private $repository = self::ATTACHABLE;
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'details' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text128',
'trunkBranch' => 'text255',
'isActive' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'projectName' => array(
'columns' => array('name'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(ReleephProductPHIDType::TYPECONST);
}
public function getDetail($key, $default = null) {
return idx($this->details, $key, $default);
}
public function getURI($path = null) {
$components = array(
'/releeph/product',
$this->getID(),
$path,
);
return implode('/', $components);
}
public function setDetail($key, $value) {
$this->details[$key] = $value;
return $this;
}
public function getPushers() {
return $this->getDetail('pushers', array());
}
public function isPusher(PhabricatorUser $user) {
// TODO Deprecate this once `isPusher` is out of the Facebook codebase.
return $this->isAuthoritative($user);
}
public function isAuthoritative(PhabricatorUser $user) {
return $this->isAuthoritativePHID($user->getPHID());
}
public function isAuthoritativePHID($phid) {
$pushers = $this->getPushers();
if (!$pushers) {
return true;
} else {
return in_array($phid, $pushers);
}
}
public function attachRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
public function getRepository() {
return $this->assertAttached($this->repository);
}
public function getReleephFieldSelector() {
return new ReleephDefaultFieldSelector();
}
public function isTestFile($filename) {
$test_paths = $this->getDetail('testPaths', array());
foreach ($test_paths as $test_path) {
if (preg_match($test_path, $filename)) {
return true;
}
}
return false;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new ReleephProductEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new ReleephProductTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return PhabricatorPolicies::POLICY_USER;
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
}
diff --git a/src/applications/releeph/storage/ReleephRequest.php b/src/applications/releeph/storage/ReleephRequest.php
index a87719210..c21f9b28c 100644
--- a/src/applications/releeph/storage/ReleephRequest.php
+++ b/src/applications/releeph/storage/ReleephRequest.php
@@ -1,365 +1,354 @@
<?php
final class ReleephRequest extends ReleephDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorCustomFieldInterface {
protected $branchID;
protected $requestUserPHID;
protected $details = array();
protected $userIntents = array();
protected $inBranch;
protected $pickStatus;
protected $mailKey;
/**
* The object which is being requested. Normally this is a commit, but it
* might also be a revision. In the future, it could be a repository branch
* or an external object (like a GitHub pull request).
*/
protected $requestedObjectPHID;
// Information about the thing being requested
protected $requestCommitPHID;
// Information about the last commit to the releeph branch
protected $commitIdentifier;
protected $commitPHID;
private $customFields = self::ATTACHABLE;
private $branch = self::ATTACHABLE;
private $requestedObject = self::ATTACHABLE;
/* -( Constants and helper methods )--------------------------------------- */
const INTENT_WANT = 'want';
const INTENT_PASS = 'pass';
const PICK_PENDING = 1; // old
const PICK_FAILED = 2;
const PICK_OK = 3;
const PICK_MANUAL = 4; // old
const REVERT_OK = 5;
const REVERT_FAILED = 6;
public function shouldBeInBranch() {
return
$this->getPusherIntent() == self::INTENT_WANT &&
/**
* We use "!= pass" instead of "== want" in case the requestor intent is
* not present. In other words, only revert if the requestor explicitly
* passed.
*/
$this->getRequestorIntent() != self::INTENT_PASS;
}
/**
* Will return INTENT_WANT if any pusher wants this request, and no pusher
* passes on this request.
*/
public function getPusherIntent() {
$product = $this->getBranch()->getProduct();
if (!$product->getPushers()) {
return self::INTENT_WANT;
}
$found_pusher_want = false;
foreach ($this->userIntents as $phid => $intent) {
if ($product->isAuthoritativePHID($phid)) {
if ($intent == self::INTENT_PASS) {
return self::INTENT_PASS;
}
$found_pusher_want = true;
}
}
if ($found_pusher_want) {
return self::INTENT_WANT;
} else {
return null;
}
}
public function getRequestorIntent() {
return idx($this->userIntents, $this->requestUserPHID);
}
public function getStatus() {
return $this->calculateStatus();
}
public function getMonogram() {
return 'Y'.$this->getID();
}
public function getBranch() {
return $this->assertAttached($this->branch);
}
public function attachBranch(ReleephBranch $branch) {
$this->branch = $branch;
return $this;
}
public function getRequestedObject() {
return $this->assertAttached($this->requestedObject);
}
public function attachRequestedObject($object) {
$this->requestedObject = $object;
return $this;
}
private function calculateStatus() {
if ($this->shouldBeInBranch()) {
if ($this->getInBranch()) {
return ReleephRequestStatus::STATUS_PICKED;
} else {
return ReleephRequestStatus::STATUS_NEEDS_PICK;
}
} else {
if ($this->getInBranch()) {
return ReleephRequestStatus::STATUS_NEEDS_REVERT;
} else {
$intent_pass = self::INTENT_PASS;
$intent_want = self::INTENT_WANT;
$has_been_in_branch = $this->getCommitIdentifier();
// Regardless of why we reverted something, always say reverted if it
// was once in the branch.
if ($has_been_in_branch) {
return ReleephRequestStatus::STATUS_REVERTED;
} else if ($this->getPusherIntent() === $intent_pass) {
// Otherwise, if it has never been in the branch, explicitly say why:
return ReleephRequestStatus::STATUS_REJECTED;
} else if ($this->getRequestorIntent() === $intent_want) {
return ReleephRequestStatus::STATUS_REQUESTED;
} else {
return ReleephRequestStatus::STATUS_ABANDONED;
}
}
}
}
/* -( Lisk mechanics )----------------------------------------------------- */
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'details' => self::SERIALIZATION_JSON,
'userIntents' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'requestCommitPHID' => 'phid?',
'commitIdentifier' => 'text40?',
'commitPHID' => 'phid?',
'pickStatus' => 'uint32?',
'inBranch' => 'bool',
'mailKey' => 'bytes20',
'userIntents' => 'text?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'requestIdentifierBranch' => array(
'columns' => array('requestCommitPHID', 'branchID'),
'unique' => true,
),
'branchID' => array(
'columns' => array('branchID'),
),
'key_requestedObject' => array(
'columns' => array('requestedObjectPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
ReleephRequestPHIDType::TYPECONST);
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
/* -( Helpful accessors )--------------------------------------------------- */
public function getDetail($key, $default = null) {
return idx($this->getDetails(), $key, $default);
}
public function setDetail($key, $value) {
$this->details[$key] = $value;
return $this;
}
/**
* Get the commit PHIDs this request is requesting.
*
* NOTE: For now, this always returns one PHID.
*
* @return list<phid> Commit PHIDs requested by this request.
*/
public function getCommitPHIDs() {
return array(
$this->requestCommitPHID,
);
}
public function getReason() {
// Backward compatibility: reason used to be called comments
$reason = $this->getDetail('reason');
if (!$reason) {
return $this->getDetail('comments');
}
return $reason;
}
/**
* Allow a null summary, and fall back to the title of the commit.
*/
public function getSummaryForDisplay() {
$summary = $this->getDetail('summary');
if (!strlen($summary)) {
$commit = $this->loadPhabricatorRepositoryCommit();
if ($commit) {
$summary = $commit->getSummary();
}
}
if (!strlen($summary)) {
$summary = pht('None');
}
return $summary;
}
/* -( Loading external objects )------------------------------------------- */
public function loadPhabricatorRepositoryCommit() {
return id(new PhabricatorRepositoryCommit())->loadOneWhere(
'phid = %s',
$this->getRequestCommitPHID());
}
public function loadPhabricatorRepositoryCommitData() {
$commit = $this->loadPhabricatorRepositoryCommit();
if ($commit) {
return id(new PhabricatorRepositoryCommitData())->loadOneWhere(
'commitID = %d',
$commit->getID());
}
}
/* -( State change helpers )----------------------------------------------- */
public function setUserIntent(PhabricatorUser $user, $intent) {
$this->userIntents[$user->getPHID()] = $intent;
return $this;
}
/* -( Migrating to status-less ReleephRequests )--------------------------- */
protected function didReadData() {
if ($this->userIntents === null) {
$this->userIntents = array();
}
}
public function setStatus($value) {
throw new Exception(pht('`%s` is now deprecated!', 'status'));
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new ReleephRequestTransactionalEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new ReleephRequestTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return $this->getBranch()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getBranch()->hasAutomaticCapability($capability, $viewer);
}
public function describeAutomaticCapability($capability) {
return pht(
'Pull requests have the same policies as the branches they are '.
'requested against.');
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('releeph.fields');
}
public function getCustomFieldBaseClass() {
return 'ReleephFieldSpecification';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
}
diff --git a/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php b/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php
index ee0fcfea7..125ee833f 100644
--- a/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php
+++ b/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php
@@ -1,524 +1,595 @@
<?php
/**
* Run pull commands on local working copies to keep them up to date. This
* daemon handles all repository types.
*
* By default, the daemon pulls **every** repository. If you want it to be
* responsible for only some repositories, you can launch it with a list of
* repositories:
*
* ./phd launch repositorypulllocal -- X Q Z
*
* You can also launch a daemon which is responsible for all //but// one or
* more repositories:
*
* ./phd launch repositorypulllocal -- --not A --not B
*
* If you have a very large number of repositories and some aren't being pulled
* as frequently as you'd like, you can either change the pull frequency of
* the less-important repositories to a larger number (so the daemon will skip
* them more often) or launch one daemon for all the less-important repositories
* and one for the more important repositories (or one for each more important
* repository).
*
* @task pull Pulling Repositories
*/
final class PhabricatorRepositoryPullLocalDaemon
extends PhabricatorDaemon {
private $statusMessageCursor = 0;
/* -( Pulling Repositories )----------------------------------------------- */
/**
* @task pull
*/
protected function run() {
$argv = $this->getArgv();
array_unshift($argv, __CLASS__);
$args = new PhutilArgumentParser($argv);
$args->parse(
array(
array(
'name' => 'no-discovery',
'help' => pht('Pull only, without discovering commits.'),
),
array(
'name' => 'not',
'param' => 'repository',
'repeat' => true,
'help' => pht('Do not pull __repository__.'),
),
array(
'name' => 'repositories',
'wildcard' => true,
'help' => pht('Pull specific __repositories__ instead of all.'),
),
));
$no_discovery = $args->getArg('no-discovery');
$include = $args->getArg('repositories');
$exclude = $args->getArg('not');
// Each repository has an individual pull frequency; after we pull it,
// wait that long to pull it again. When we start up, try to pull everything
// serially.
$retry_after = array();
- $min_sleep = 30; // c4s custo
+ $min_sleep = 15;
$max_sleep = phutil_units('5 minutes in seconds');
- $max_futures = 5; // c4s custo
+ $max_futures = 4;
$futures = array();
$queue = array();
+ $sync_wait = phutil_units('2 minutes in seconds');
+ $last_sync = array();
+
while (!$this->shouldExit()) {
PhabricatorCaches::destroyRequestCache();
$device = AlmanacKeys::getLiveDevice();
$pullable = $this->loadPullableRepositories($include, $exclude, $device);
// If any repositories have the NEEDS_UPDATE flag set, pull them
// as soon as possible.
$need_update_messages = $this->loadRepositoryUpdateMessages(true);
foreach ($need_update_messages as $message) {
$repo = idx($pullable, $message->getRepositoryID());
if (!$repo) {
continue;
}
$this->log(
pht(
'Got an update message for repository "%s"!',
$repo->getMonogram()));
$retry_after[$message->getRepositoryID()] = time();
}
+ if ($device) {
+ $unsynchronized = $this->loadUnsynchronizedRepositories($device);
+ $now = PhabricatorTime::getNow();
+ foreach ($unsynchronized as $repository) {
+ $id = $repository->getID();
+
+ $this->log(
+ pht(
+ 'Cluster repository ("%s") is out of sync on this node ("%s").',
+ $repository->getDisplayName(),
+ $device->getName()));
+
+ // Don't let out-of-sync conditions trigger updates too frequently,
+ // since we don't want to get trapped in a death spiral if sync is
+ // failing.
+ $sync_at = idx($last_sync, $id, 0);
+ $wait_duration = ($now - $sync_at);
+ if ($wait_duration < $sync_wait) {
+ $this->log(
+ pht(
+ 'Skipping forced out-of-sync update because the last update '.
+ 'was too recent (%s seconds ago).',
+ $wait_duration));
+ continue;
+ }
+
+ $last_sync[$id] = $now;
+ $retry_after[$id] = $now;
+ }
+ }
+
// If any repositories were deleted, remove them from the retry timer map
// so we don't end up with a retry timer that never gets updated and
// causes us to sleep for the minimum amount of time.
$retry_after = array_select_keys(
$retry_after,
array_keys($pullable));
// Figure out which repositories we need to queue for an update.
foreach ($pullable as $id => $repository) {
$now = PhabricatorTime::getNow();
$display_name = $repository->getDisplayName();
if (isset($futures[$id])) {
$this->log(
pht(
'Repository "%s" is currently updating.',
$display_name));
continue;
}
if (isset($queue[$id])) {
$this->log(
pht(
'Repository "%s" is already queued.',
$display_name));
continue;
}
$after = idx($retry_after, $id);
if (!$after) {
$smart_wait = $repository->loadUpdateInterval($min_sleep);
$last_update = $this->loadLastUpdate($repository);
$after = $last_update + $smart_wait;
$retry_after[$id] = $after;
$this->log(
pht(
'Scheduling repository "%s" with an update window of %s '.
'second(s). Last update was %s second(s) ago.',
$display_name,
new PhutilNumber($smart_wait),
new PhutilNumber($now - $last_update)));
}
if ($after > time()) {
$this->log(
pht(
'Repository "%s" is not due for an update for %s second(s).',
$display_name,
new PhutilNumber($after - $now)));
continue;
}
$this->log(
pht(
'Scheduling repository "%s" for an update (%s seconds overdue).',
$display_name,
new PhutilNumber($now - $after)));
$queue[$id] = $after;
}
// Process repositories in the order they became candidates for updates.
asort($queue);
// Dequeue repositories until we hit maximum parallelism.
while ($queue && (count($futures) < $max_futures)) {
foreach ($queue as $id => $time) {
$repository = idx($pullable, $id);
if (!$repository) {
$this->log(
pht('Repository %s is no longer pullable; skipping.', $id));
unset($queue[$id]);
continue;
}
$display_name = $repository->getDisplayName();
$this->log(
pht(
'Starting update for repository "%s".',
$display_name));
unset($queue[$id]);
$futures[$id] = $this->buildUpdateFuture(
$repository,
$no_discovery);
break;
}
}
if ($queue) {
$this->log(
pht(
'Not enough process slots to schedule the other %s '.
'repository(s) for updates yet.',
phutil_count($queue)));
}
if ($futures) {
$iterator = id(new FutureIterator($futures))
->setUpdateInterval($min_sleep);
foreach ($iterator as $id => $future) {
$this->stillWorking();
if ($future === null) {
$this->log(pht('Waiting for updates to complete...'));
$this->stillWorking();
if ($this->loadRepositoryUpdateMessages()) {
$this->log(pht('Interrupted by pending updates!'));
break;
}
continue;
}
unset($futures[$id]);
$retry_after[$id] = $this->resolveUpdateFuture(
$pullable[$id],
$future,
$min_sleep);
// We have a free slot now, so go try to fill it.
break;
}
// Jump back into prioritization if we had any futures to deal with.
continue;
}
$should_hibernate = $this->waitForUpdates($max_sleep, $retry_after);
if ($should_hibernate) {
break;
}
}
}
/**
* @task pull
*/
private function buildUpdateFuture(
PhabricatorRepository $repository,
$no_discovery) {
$bin = dirname(phutil_get_library_root('phabricator')).'/bin/repository';
$flags = array();
if ($no_discovery) {
$flags[] = '--no-discovery';
}
$monogram = $repository->getMonogram();
$future = new ExecFuture('%s update %Ls -- %s', $bin, $flags, $monogram);
// Sometimes, the underlying VCS commands will hang indefinitely. We've
// observed this occasionally with GitHub, and other users have observed
// it with other VCS servers.
// To limit the damage this can cause, kill the update out after a
// reasonable amount of time, under the assumption that it has hung.
// Since it's hard to know what a "reasonable" amount of time is given that
// users may be downloading a repository full of pirated movies over a
// potato, these limits are fairly generous. Repositories exceeding these
// limits can be manually pulled with `bin/repository update X`, which can
// just run for as long as it wants.
if ($repository->isImporting()) {
$timeout = phutil_units('4 hours in seconds');
} else {
$timeout = phutil_units('15 minutes in seconds');
}
$future->setTimeout($timeout);
// The default TERM inherited by this process is "unknown", which causes PHP
// to produce a warning upon startup. Override it to squash this output to
// STDERR.
$future->updateEnv('TERM', 'dumb');
return $future;
}
/**
* Check for repositories that should be updated immediately.
*
* With the `$consume` flag, an internal cursor will also be incremented so
* that these messages are not returned by subsequent calls.
*
* @param bool Pass `true` to consume these messages, so the process will
* not see them again.
* @return list<wild> Pending update messages.
*
* @task pull
*/
private function loadRepositoryUpdateMessages($consume = false) {
$type_need_update = PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE;
$messages = id(new PhabricatorRepositoryStatusMessage())->loadAllWhere(
'statusType = %s AND id > %d',
$type_need_update,
$this->statusMessageCursor);
// Keep track of messages we've seen so that we don't load them again.
// If we reload messages, we can get stuck a loop if we have a failing
// repository: we update immediately in response to the message, but do
// not clear the message because the update does not succeed. We then
// immediately retry. Instead, messages are only permitted to trigger
// an immediate update once.
if ($consume) {
foreach ($messages as $message) {
$this->statusMessageCursor = max(
$this->statusMessageCursor,
$message->getID());
}
}
return $messages;
}
/**
* @task pull
*/
private function loadLastUpdate(PhabricatorRepository $repository) {
$table = new PhabricatorRepositoryStatusMessage();
$conn = $table->establishConnection('r');
$epoch = queryfx_one(
$conn,
'SELECT MAX(epoch) last_update FROM %T
WHERE repositoryID = %d
AND statusType IN (%Ls)',
$table->getTableName(),
$repository->getID(),
array(
PhabricatorRepositoryStatusMessage::TYPE_INIT,
PhabricatorRepositoryStatusMessage::TYPE_FETCH,
));
if ($epoch) {
return (int)$epoch['last_update'];
}
return PhabricatorTime::getNow();
}
/**
* @task pull
*/
private function loadPullableRepositories(
array $include,
array $exclude,
AlmanacDevice $device = null) {
$query = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer());
if ($include) {
$query->withIdentifiers($include);
}
$repositories = $query->execute();
$repositories = mpull($repositories, null, 'getPHID');
if ($include) {
$map = $query->getIdentifierMap();
foreach ($include as $identifier) {
if (empty($map[$identifier])) {
throw new Exception(
pht(
'No repository "%s" exists!',
$identifier));
}
}
}
if ($exclude) {
$xquery = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->withIdentifiers($exclude);
$excluded_repos = $xquery->execute();
$xmap = $xquery->getIdentifierMap();
foreach ($exclude as $identifier) {
if (empty($xmap[$identifier])) {
throw new Exception(
pht(
'No repository "%s" exists!',
$identifier));
}
}
foreach ($excluded_repos as $excluded_repo) {
unset($repositories[$excluded_repo->getPHID()]);
}
}
foreach ($repositories as $key => $repository) {
if (!$repository->isTracked()) {
unset($repositories[$key]);
}
}
$viewer = $this->getViewer();
$filter = id(new DiffusionLocalRepositoryFilter())
->setViewer($viewer)
->setDevice($device)
->setRepositories($repositories);
$repositories = $filter->execute();
foreach ($filter->getRejectionReasons() as $reason) {
$this->log($reason);
}
// Shuffle the repositories, then re-key the array since shuffle()
// discards keys. This is mostly for startup, we'll use soft priorities
// later.
shuffle($repositories);
$repositories = mpull($repositories, null, 'getID');
return $repositories;
}
/**
* @task pull
*/
private function resolveUpdateFuture(
PhabricatorRepository $repository,
ExecFuture $future,
$min_sleep) {
$display_name = $repository->getDisplayName();
$this->log(pht('Resolving update for "%s".', $display_name));
try {
list($stdout, $stderr) = $future->resolvex();
} catch (Exception $ex) {
$proxy = new PhutilProxyException(
pht(
'Error while updating the "%s" repository.',
$display_name),
$ex);
phlog($proxy);
$smart_wait = $repository->loadUpdateInterval($min_sleep);
return PhabricatorTime::getNow() + $smart_wait;
}
if (strlen($stderr)) {
$stderr_msg = pht(
'Unexpected output while updating repository "%s": %s',
$display_name,
$stderr);
phlog($stderr_msg);
}
$smart_wait = $repository->loadUpdateInterval($min_sleep);
$this->log(
pht(
'Based on activity in repository "%s", considering a wait of %s '.
'seconds before update.',
$display_name,
new PhutilNumber($smart_wait)));
return PhabricatorTime::getNow() + $smart_wait;
}
/**
* Sleep for a short period of time, waiting for update messages from the
*
*
* @task pull
*/
private function waitForUpdates($min_sleep, array $retry_after) {
$this->log(
pht('No repositories need updates right now, sleeping...'));
$sleep_until = time() + $min_sleep;
if ($retry_after) {
$sleep_until = min($sleep_until, min($retry_after));
}
while (($sleep_until - time()) > 0) {
$sleep_duration = ($sleep_until - time());
if ($this->shouldHibernate($sleep_duration)) {
return true;
}
$this->log(
pht(
'Sleeping for %s more second(s)...',
new PhutilNumber($sleep_duration)));
$this->sleep(1);
if ($this->shouldExit()) {
$this->log(pht('Awakened from sleep by graceful shutdown!'));
return false;
}
if ($this->loadRepositoryUpdateMessages()) {
$this->log(pht('Awakened from sleep by pending updates!'));
break;
}
}
return false;
}
+ private function loadUnsynchronizedRepositories(AlmanacDevice $device) {
+ $viewer = $this->getViewer();
+ $table = new PhabricatorRepositoryWorkingCopyVersion();
+ $conn = $table->establishConnection('r');
+
+ $our_versions = queryfx_all(
+ $conn,
+ 'SELECT repositoryPHID, repositoryVersion FROM %R WHERE devicePHID = %s',
+ $table,
+ $device->getPHID());
+ $our_versions = ipull($our_versions, 'repositoryVersion', 'repositoryPHID');
+
+ $max_versions = queryfx_all(
+ $conn,
+ 'SELECT repositoryPHID, MAX(repositoryVersion) maxVersion FROM %R
+ GROUP BY repositoryPHID',
+ $table);
+ $max_versions = ipull($max_versions, 'maxVersion', 'repositoryPHID');
+
+ $unsynchronized_phids = array();
+ foreach ($max_versions as $repository_phid => $max_version) {
+ $our_version = idx($our_versions, $repository_phid);
+ if (($our_version === null) || ($our_version < $max_version)) {
+ $unsynchronized_phids[] = $repository_phid;
+ }
+ }
+
+ if (!$unsynchronized_phids) {
+ return array();
+ }
+
+ return id(new PhabricatorRepositoryQuery())
+ ->setViewer($viewer)
+ ->withPHIDs($unsynchronized_phids)
+ ->execute();
+ }
+
}
diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementWorkflow.php
index 654a974e6..6d48d7c5d 100644
--- a/src/applications/repository/management/PhabricatorRepositoryManagementWorkflow.php
+++ b/src/applications/repository/management/PhabricatorRepositoryManagementWorkflow.php
@@ -1,95 +1,95 @@
<?php
abstract class PhabricatorRepositoryManagementWorkflow
extends PhabricatorManagementWorkflow {
protected function loadRepositories(PhutilArgumentParser $args, $param) {
$identifiers = $args->getArg($param);
if (!$identifiers) {
- return null;
+ return array();
}
$query = id(new PhabricatorRepositoryQuery())
->setViewer($this->getViewer())
->needURIs(true)
->withIdentifiers($identifiers);
$query->execute();
$map = $query->getIdentifierMap();
foreach ($identifiers as $identifier) {
if (empty($map[$identifier])) {
throw new PhutilArgumentUsageException(
pht(
'Repository "%s" does not exist!',
$identifier));
}
}
// Reorder repositories according to argument order.
$repositories = array_select_keys($map, $identifiers);
return array_values($repositories);
}
protected function loadLocalRepositories(
PhutilArgumentParser $args,
$param) {
$repositories = $this->loadRepositories($args, $param);
if (!$repositories) {
return $repositories;
}
$device = AlmanacKeys::getLiveDevice();
$viewer = $this->getViewer();
$filter = id(new DiffusionLocalRepositoryFilter())
->setViewer($viewer)
->setDevice($device)
->setRepositories($repositories);
$repositories = $filter->execute();
foreach ($filter->getRejectionReasons() as $reason) {
throw new PhutilArgumentUsageException($reason);
}
return $repositories;
}
protected function loadCommits(PhutilArgumentParser $args, $param) {
$names = $args->getArg($param);
if (!$names) {
return null;
}
return $this->loadNamedCommits($names);
}
protected function loadNamedCommit($name) {
$map = $this->loadNamedCommits(array($name));
return $map[$name];
}
protected function loadNamedCommits(array $names) {
$query = id(new DiffusionCommitQuery())
->setViewer($this->getViewer())
->withIdentifiers($names);
$query->execute();
$map = $query->getIdentifierMap();
foreach ($names as $name) {
if (empty($map[$name])) {
throw new PhutilArgumentUsageException(
pht('Commit "%s" does not exist or is ambiguous.', $name));
}
}
return $map;
}
}
diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php
index d57bf0894..cc7b00e81 100644
--- a/src/applications/repository/storage/PhabricatorRepository.php
+++ b/src/applications/repository/storage/PhabricatorRepository.php
@@ -1,2833 +1,2856 @@
<?php
/**
* @task uri Repository URI Management
* @task autoclose Autoclose
* @task sync Cluster Synchronization
*/
final class PhabricatorRepository extends PhabricatorRepositoryDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorFlaggableInterface,
PhabricatorMarkupInterface,
PhabricatorDestructibleInterface,
PhabricatorDestructibleCodexInterface,
PhabricatorProjectInterface,
PhabricatorSpacesInterface,
PhabricatorConduitResultInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface {
/**
* Shortest hash we'll recognize in raw "a829f32" form.
*/
const MINIMUM_UNQUALIFIED_HASH = 7;
/**
* Shortest hash we'll recognize in qualified "rXab7ef2f8" form.
*/
const MINIMUM_QUALIFIED_HASH = 5;
/**
* Minimum number of commits to an empty repository to trigger "import" mode.
*/
const IMPORT_THRESHOLD = 7;
const TABLE_PATH = 'repository_path';
const TABLE_PATHCHANGE = 'repository_pathchange';
const TABLE_FILESYSTEM = 'repository_filesystem';
const TABLE_SUMMARY = 'repository_summary';
const TABLE_LINTMESSAGE = 'repository_lintmessage';
const TABLE_PARENTS = 'repository_parents';
const TABLE_COVERAGE = 'repository_coverage';
const BECAUSE_REPOSITORY_IMPORTING = 'auto/importing';
const BECAUSE_AUTOCLOSE_DISABLED = 'auto/disabled';
const BECAUSE_NOT_ON_AUTOCLOSE_BRANCH = 'auto/nobranch';
const BECAUSE_BRANCH_UNTRACKED = 'auto/notrack';
const BECAUSE_BRANCH_NOT_AUTOCLOSE = 'auto/noclose';
const BECAUSE_AUTOCLOSE_FORCED = 'auto/forced';
const STATUS_ACTIVE = 'active';
const STATUS_INACTIVE = 'inactive';
protected $name;
protected $callsign;
protected $repositorySlug;
protected $uuid;
protected $viewPolicy;
protected $editPolicy;
protected $pushPolicy;
protected $profileImagePHID;
protected $versionControlSystem;
protected $details = array();
protected $credentialPHID;
protected $almanacServicePHID;
protected $spacePHID;
protected $localPath;
private $commitCount = self::ATTACHABLE;
private $mostRecentCommit = self::ATTACHABLE;
private $projectPHIDs = self::ATTACHABLE;
private $uris = self::ATTACHABLE;
private $profileImageFile = self::ATTACHABLE;
public static function initializeNewRepository(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorDiffusionApplication'))
->executeOne();
$view_policy = $app->getPolicy(DiffusionDefaultViewCapability::CAPABILITY);
$edit_policy = $app->getPolicy(DiffusionDefaultEditCapability::CAPABILITY);
$push_policy = $app->getPolicy(DiffusionDefaultPushCapability::CAPABILITY);
$repository = id(new PhabricatorRepository())
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setPushPolicy($push_policy)
->setSpacePHID($actor->getDefaultSpacePHID());
// Put the repository in "Importing" mode until we finish
// parsing it.
$repository->setDetail('importing', true);
// c4science customization
if($repository->getVersionControlSystem() == 'git') {
$repository->setFilesizeLimit(104900000);
}
return $repository;
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'details' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort255',
'callsign' => 'sort32?',
'repositorySlug' => 'sort64?',
'versionControlSystem' => 'text32',
'uuid' => 'text64?',
'pushPolicy' => 'policy',
'credentialPHID' => 'phid?',
'almanacServicePHID' => 'phid?',
'localPath' => 'text128?',
'profileImagePHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'callsign' => array(
'columns' => array('callsign'),
'unique' => true,
),
'key_name' => array(
'columns' => array('name(128)'),
),
'key_vcs' => array(
'columns' => array('versionControlSystem'),
),
'key_slug' => array(
'columns' => array('repositorySlug'),
'unique' => true,
),
'key_local' => array(
'columns' => array('localPath'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorRepositoryRepositoryPHIDType::TYPECONST);
}
public static function getStatusMap() {
return array(
self::STATUS_ACTIVE => array(
'name' => pht('Active'),
'isTracked' => 1,
),
self::STATUS_INACTIVE => array(
'name' => pht('Inactive'),
'isTracked' => 0,
),
);
}
public static function getStatusNameMap() {
return ipull(self::getStatusMap(), 'name');
}
public function getStatus() {
if ($this->isTracked()) {
return self::STATUS_ACTIVE;
} else {
return self::STATUS_INACTIVE;
}
}
public function toDictionary() {
return array(
'id' => $this->getID(),
'name' => $this->getName(),
'phid' => $this->getPHID(),
'callsign' => $this->getCallsign(),
'monogram' => $this->getMonogram(),
'vcs' => $this->getVersionControlSystem(),
'uri' => PhabricatorEnv::getProductionURI($this->getURI()),
'remoteURI' => (string)$this->getRemoteURI(),
'description' => $this->getDetail('description'),
'isActive' => $this->isTracked(),
'isHosted' => $this->isHosted(),
'isImporting' => $this->isImporting(),
'encoding' => $this->getDefaultTextEncoding(),
'staging' => array(
'supported' => $this->supportsStaging(),
'prefix' => 'phabricator',
'uri' => $this->getStagingURI(),
),
);
}
public function getDefaultTextEncoding() {
return $this->getDetail('encoding', 'UTF-8');
}
public function getMonogram() {
$callsign = $this->getCallsign();
if (strlen($callsign)) {
return "r{$callsign}";
}
$id = $this->getID();
return "R{$id}";
}
public function getDisplayName() {
$slug = $this->getRepositorySlug();
if (strlen($slug)) {
return $slug;
}
return $this->getMonogram();
}
public function getAllMonograms() {
$monograms = array();
$monograms[] = 'R'.$this->getID();
$callsign = $this->getCallsign();
if (strlen($callsign)) {
$monograms[] = 'r'.$callsign;
}
return $monograms;
}
public function setLocalPath($path) {
// Convert any extra slashes ("//") in the path to a single slash ("/").
$path = preg_replace('(//+)', '/', $path);
return parent::setLocalPath($path);
}
public function getDetail($key, $default = null) {
return idx($this->details, $key, $default);
}
public function setDetail($key, $value) {
$this->details[$key] = $value;
return $this;
}
public function attachCommitCount($count) {
$this->commitCount = $count;
return $this;
}
public function getCommitCount() {
return $this->assertAttached($this->commitCount);
}
public function attachMostRecentCommit(
PhabricatorRepositoryCommit $commit = null) {
$this->mostRecentCommit = $commit;
return $this;
}
public function getMostRecentCommit() {
return $this->assertAttached($this->mostRecentCommit);
}
public function getDiffusionBrowseURIForPath(
PhabricatorUser $user,
$path,
$line = null,
$branch = null) {
$drequest = DiffusionRequest::newFromDictionary(
array(
'user' => $user,
'repository' => $this,
'path' => $path,
'branch' => $branch,
));
return $drequest->generateURI(
array(
'action' => 'browse',
'line' => $line,
));
}
public function getSubversionBaseURI($commit = null) {
$subpath = $this->getDetail('svn-subpath');
if (!strlen($subpath)) {
$subpath = null;
}
return $this->getSubversionPathURI($subpath, $commit);
}
public function getSubversionPathURI($path = null, $commit = null) {
$vcs = $this->getVersionControlSystem();
if ($vcs != PhabricatorRepositoryType::REPOSITORY_TYPE_SVN) {
throw new Exception(pht('Not a subversion repository!'));
}
if ($this->isHosted()) {
$uri = 'file://'.$this->getLocalPath();
} else {
$uri = $this->getDetail('remote-uri');
}
$uri = rtrim($uri, '/');
if (strlen($path)) {
$path = rawurlencode($path);
$path = str_replace('%2F', '/', $path);
$uri = $uri.'/'.ltrim($path, '/');
}
if ($path !== null || $commit !== null) {
$uri .= '@';
}
if ($commit !== null) {
$uri .= $commit;
}
return $uri;
}
public function attachProjectPHIDs(array $project_phids) {
$this->projectPHIDs = $project_phids;
return $this;
}
public function getProjectPHIDs() {
return $this->assertAttached($this->projectPHIDs);
}
/**
* Get the name of the directory this repository should clone or checkout
* into. For example, if the repository name is "Example Repository", a
* reasonable name might be "example-repository". This is used to help users
* get reasonable results when cloning repositories, since they generally do
* not want to clone into directories called "X/" or "Example Repository/".
*
* @return string
*/
public function getCloneName() {
$name = $this->getRepositorySlug();
// Make some reasonable effort to produce reasonable default directory
// names from repository names.
if (!strlen($name)) {
$name = $this->getName();
$name = phutil_utf8_strtolower($name);
$name = preg_replace('@[ -/:->]+@', '-', $name);
$name = trim($name, '-');
if (!strlen($name)) {
$name = $this->getCallsign();
}
}
return $name;
}
public static function isValidRepositorySlug($slug) {
try {
self::assertValidRepositorySlug($slug);
return true;
} catch (Exception $ex) {
return false;
}
}
public static function assertValidRepositorySlug($slug) {
if (!strlen($slug)) {
throw new Exception(
pht(
'The empty string is not a valid repository short name. '.
'Repository short names must be at least one character long.'));
}
if (strlen($slug) > 64) {
throw new Exception(
pht(
'The name "%s" is not a valid repository short name. Repository '.
'short names must not be longer than 64 characters.',
$slug));
}
if (preg_match('/[^a-zA-Z0-9._-]/', $slug)) {
throw new Exception(
pht(
'The name "%s" is not a valid repository short name. Repository '.
'short names may only contain letters, numbers, periods, hyphens '.
'and underscores.',
$slug));
}
if (!preg_match('/^[a-zA-Z0-9]/', $slug)) {
throw new Exception(
pht(
'The name "%s" is not a valid repository short name. Repository '.
'short names must begin with a letter or number.',
$slug));
}
if (!preg_match('/[a-zA-Z0-9]\z/', $slug)) {
throw new Exception(
pht(
'The name "%s" is not a valid repository short name. Repository '.
'short names must end with a letter or number.',
$slug));
}
if (preg_match('/__|--|\\.\\./', $slug)) {
throw new Exception(
pht(
'The name "%s" is not a valid repository short name. Repository '.
'short names must not contain multiple consecutive underscores, '.
'hyphens, or periods.',
$slug));
}
if (preg_match('/^[A-Z]+\z/', $slug)) {
throw new Exception(
pht(
'The name "%s" is not a valid repository short name. Repository '.
'short names may not contain only uppercase letters.',
$slug));
}
if (preg_match('/^\d+\z/', $slug)) {
throw new Exception(
pht(
'The name "%s" is not a valid repository short name. Repository '.
'short names may not contain only numbers.',
$slug));
}
if (preg_match('/\\.git/', $slug)) {
throw new Exception(
pht(
'The name "%s" is not a valid repository short name. Repository '.
'short names must not end in ".git". This suffix will be added '.
'automatically in appropriate contexts.',
$slug));
}
}
public static function assertValidCallsign($callsign) {
if (!strlen($callsign)) {
throw new Exception(
pht(
'A repository callsign must be at least one character long.'));
}
if (strlen($callsign) > 32) {
throw new Exception(
pht(
'The callsign "%s" is not a valid repository callsign. Callsigns '.
'must be no more than 32 bytes long.',
$callsign));
}
if (!preg_match('/^[A-Z]+\z/', $callsign)) {
throw new Exception(
pht(
'The callsign "%s" is not a valid repository callsign. Callsigns '.
'may only contain UPPERCASE letters.',
$callsign));
}
}
public function getProfileImageURI() {
return $this->getProfileImageFile()->getBestURI();
}
public function attachProfileImageFile(PhabricatorFile $file) {
$this->profileImageFile = $file;
return $this;
}
public function getProfileImageFile() {
return $this->assertAttached($this->profileImageFile);
}
/* -( Remote Command Execution )------------------------------------------- */
public function execRemoteCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newRemoteCommandFuture($args)->resolve();
}
public function execxRemoteCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newRemoteCommandFuture($args)->resolvex();
}
public function getRemoteCommandFuture($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newRemoteCommandFuture($args);
}
public function passthruRemoteCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newRemoteCommandPassthru($args)->execute();
}
private function newRemoteCommandFuture(array $argv) {
return $this->newRemoteCommandEngine($argv)
->newFuture();
}
private function newRemoteCommandPassthru(array $argv) {
return $this->newRemoteCommandEngine($argv)
->setPassthru(true)
->newFuture();
}
private function newRemoteCommandEngine(array $argv) {
return DiffusionCommandEngine::newCommandEngine($this)
->setArgv($argv)
->setCredentialPHID($this->getCredentialPHID())
->setURI($this->getRemoteURIObject());
}
/* -( Local Command Execution )-------------------------------------------- */
public function execLocalCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newLocalCommandFuture($args)->resolve();
}
public function execxLocalCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newLocalCommandFuture($args)->resolvex();
}
public function getLocalCommandFuture($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newLocalCommandFuture($args);
}
public function passthruLocalCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
return $this->newLocalCommandPassthru($args)->execute();
}
private function newLocalCommandFuture(array $argv) {
$this->assertLocalExists();
$future = DiffusionCommandEngine::newCommandEngine($this)
->setArgv($argv)
->newFuture();
if ($this->usesLocalWorkingCopy()) {
$future->setCWD($this->getLocalPath());
}
return $future;
}
private function newLocalCommandPassthru(array $argv) {
$this->assertLocalExists();
$future = DiffusionCommandEngine::newCommandEngine($this)
->setArgv($argv)
->setPassthru(true)
->newFuture();
if ($this->usesLocalWorkingCopy()) {
$future->setCWD($this->getLocalPath());
}
return $future;
}
public function getURI() {
$short_name = $this->getRepositorySlug();
if (strlen($short_name)) {
return "/source/{$short_name}/";
}
$callsign = $this->getCallsign();
if (strlen($callsign)) {
return "/diffusion/{$callsign}/";
}
$id = $this->getID();
return "/diffusion/{$id}/";
}
public function getPathURI($path) {
return $this->getURI().ltrim($path, '/');
}
public function getCommitURI($identifier) {
$callsign = $this->getCallsign();
if (strlen($callsign)) {
return "/r{$callsign}{$identifier}";
}
$id = $this->getID();
return "/R{$id}:{$identifier}";
}
public static function parseRepositoryServicePath($request_path, $vcs) {
$is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
$patterns = array(
'(^'.
'(?P<base>/?(?:diffusion|source)/(?P<identifier>[^/]+))'.
'(?P<path>.*)'.
'\z)',
);
$identifier = null;
foreach ($patterns as $pattern) {
$matches = null;
if (!preg_match($pattern, $request_path, $matches)) {
continue;
}
$identifier = $matches['identifier'];
if ($is_git) {
$identifier = preg_replace('/\\.git\z/', '', $identifier);
}
$base = $matches['base'];
$path = $matches['path'];
break;
}
if ($identifier === null) {
return null;
}
return array(
'identifier' => $identifier,
'base' => $base,
'path' => $path,
);
}
public function getCanonicalPath($request_path) {
$standard_pattern =
'(^'.
'(?P<prefix>/(?:diffusion|source)/)'.
'(?P<identifier>[^/]+)'.
'(?P<suffix>(?:/.*)?)'.
'\z)';
$matches = null;
if (preg_match($standard_pattern, $request_path, $matches)) {
$suffix = $matches['suffix'];
return $this->getPathURI($suffix);
}
$commit_pattern =
'(^'.
'(?P<prefix>/)'.
'(?P<monogram>'.
'(?:'.
'r(?P<repositoryCallsign>[A-Z]+)'.
'|'.
'R(?P<repositoryID>[1-9]\d*):'.
')'.
'(?P<commit>[a-f0-9]+)'.
')'.
'\z)';
$matches = null;
if (preg_match($commit_pattern, $request_path, $matches)) {
$commit = $matches['commit'];
return $this->getCommitURI($commit);
}
return null;
}
public function generateURI(array $params) {
$req_branch = false;
$req_commit = false;
$action = idx($params, 'action');
switch ($action) {
case 'history':
case 'graph':
case 'clone':
case 'jobs': // c4science custo
case 'blame':
case 'browse':
case 'document':
case 'change':
case 'lastmodified':
case 'tags':
case 'branches':
case 'lint':
case 'pathtree':
case 'refs':
case 'compare':
break;
case 'branch':
// NOTE: This does not actually require a branch, and won't have one
// in Subversion. Possibly this should be more clear.
break;
case 'commit':
case 'rendering-ref':
$req_commit = true;
break;
default:
throw new Exception(
pht(
'Action "%s" is not a valid repository URI action.',
$action));
}
$path = idx($params, 'path');
$branch = idx($params, 'branch');
$commit = idx($params, 'commit');
$line = idx($params, 'line');
$head = idx($params, 'head');
$against = idx($params, 'against');
if ($req_commit && !strlen($commit)) {
throw new Exception(
pht(
'Diffusion URI action "%s" requires commit!',
$action));
}
if ($req_branch && !strlen($branch)) {
throw new Exception(
pht(
'Diffusion URI action "%s" requires branch!',
$action));
}
if ($action === 'commit') {
return $this->getCommitURI($commit);
}
if (strlen($path)) {
$path = ltrim($path, '/');
$path = str_replace(array(';', '$'), array(';;', '$$'), $path);
$path = phutil_escape_uri($path);
}
$raw_branch = $branch;
if (strlen($branch)) {
$branch = phutil_escape_uri_path_component($branch);
$path = "{$branch}/{$path}";
}
$raw_commit = $commit;
if (strlen($commit)) {
$commit = str_replace('$', '$$', $commit);
$commit = ';'.phutil_escape_uri($commit);
}
if (strlen($line)) {
$line = '$'.phutil_escape_uri($line);
}
$query = array();
switch ($action) {
case 'change':
case 'history':
case 'graph':
case 'jobs': // c4science custo
case 'blame':
case 'browse':
case 'document':
case 'lastmodified':
case 'tags':
case 'branches':
case 'lint':
case 'pathtree':
case 'refs':
$uri = $this->getPathURI("/{$action}/{$path}{$commit}{$line}");
break;
case 'compare':
$uri = $this->getPathURI("/{$action}/");
if (strlen($head)) {
$query['head'] = $head;
} else if (strlen($raw_commit)) {
$query['commit'] = $raw_commit;
} else if (strlen($raw_branch)) {
$query['head'] = $raw_branch;
}
if (strlen($against)) {
$query['against'] = $against;
}
break;
case 'branch':
if (strlen($path)) {
$uri = $this->getPathURI("/repository/{$path}");
} else {
$uri = $this->getPathURI('/');
}
break;
case 'external':
$commit = ltrim($commit, ';');
$uri = "/diffusion/external/{$commit}/";
break;
case 'rendering-ref':
// This isn't a real URI per se, it's passed as a query parameter to
// the ajax changeset stuff but then we parse it back out as though
// it came from a URI.
$uri = rawurldecode("{$path}{$commit}");
break;
case 'clone':
$uri = $this->getPathURI("/{$action}/");
break;
}
if ($action == 'rendering-ref') {
return $uri;
}
$uri = new PhutilURI($uri);
if (isset($params['lint'])) {
$params['params'] = idx($params, 'params', array()) + array(
'lint' => $params['lint'],
);
}
$query = idx($params, 'params', array()) + $query;
if ($query) {
$uri->setQueryParams($query);
}
return $uri;
}
public function updateURIIndex() {
$indexes = array();
$uris = $this->getURIs();
foreach ($uris as $uri) {
if ($uri->getIsDisabled()) {
continue;
}
$indexes[] = $uri->getNormalizedURI();
}
PhabricatorRepositoryURIIndex::updateRepositoryURIs(
$this->getPHID(),
$indexes);
return $this;
}
public function isTracked() {
$status = $this->getDetail('tracking-enabled');
$map = self::getStatusMap();
$spec = idx($map, $status);
if (!$spec) {
if ($status) {
$status = self::STATUS_ACTIVE;
} else {
$status = self::STATUS_INACTIVE;
}
$spec = idx($map, $status);
}
return (bool)idx($spec, 'isTracked', false);
}
public function getDefaultBranch() {
$default = $this->getDetail('default-branch');
if (strlen($default)) {
return $default;
}
$default_branches = array(
PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'master',
PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL => 'default',
);
return idx($default_branches, $this->getVersionControlSystem());
}
public function getDefaultArcanistBranch() {
return coalesce($this->getDefaultBranch(), 'svn');
}
private function isBranchInFilter($branch, $filter_key) {
$vcs = $this->getVersionControlSystem();
$is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
$use_filter = ($is_git);
if (!$use_filter) {
// If this VCS doesn't use filters, pass everything through.
return true;
}
$filter = $this->getDetail($filter_key, array());
// If there's no filter set, let everything through.
if (!$filter) {
return true;
}
// If this branch isn't literally named `regexp(...)`, and it's in the
// filter list, let it through.
if (isset($filter[$branch])) {
if (self::extractBranchRegexp($branch) === null) {
return true;
}
}
// If the branch matches a regexp, let it through.
foreach ($filter as $pattern => $ignored) {
$regexp = self::extractBranchRegexp($pattern);
if ($regexp !== null) {
if (preg_match($regexp, $branch)) {
return true;
}
}
}
// Nothing matched, so filter this branch out.
return false;
}
public static function extractBranchRegexp($pattern) {
$matches = null;
if (preg_match('/^regexp\\((.*)\\)\z/', $pattern, $matches)) {
return $matches[1];
}
return null;
}
public function shouldTrackRef(DiffusionRepositoryRef $ref) {
// At least for now, don't track the staging area tags.
if ($ref->isTag()) {
if (preg_match('(^phabricator/)', $ref->getShortName())) {
return false;
}
}
if (!$ref->isBranch()) {
return true;
}
return $this->shouldTrackBranch($ref->getShortName());
}
public function shouldTrackBranch($branch) {
return $this->isBranchInFilter($branch, 'branch-filter');
}
public function formatCommitName($commit_identifier, $local = false) {
$vcs = $this->getVersionControlSystem();
$type_git = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
$type_hg = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL;
$is_git = ($vcs == $type_git);
$is_hg = ($vcs == $type_hg);
if ($is_git || $is_hg) {
$name = substr($commit_identifier, 0, 12);
$need_scope = false;
} else {
$name = $commit_identifier;
$need_scope = true;
}
if (!$local) {
$need_scope = true;
}
if ($need_scope) {
$callsign = $this->getCallsign();
if ($callsign) {
$scope = "r{$callsign}";
} else {
$id = $this->getID();
$scope = "R{$id}:";
}
$name = $scope.$name;
}
return $name;
}
public function isImporting() {
return (bool)$this->getDetail('importing', false);
}
public function isNewlyInitialized() {
return (bool)$this->getDetail('newly-initialized', false);
}
public function loadImportProgress() {
$progress = queryfx_all(
$this->establishConnection('r'),
'SELECT importStatus, count(*) N FROM %T WHERE repositoryID = %d
GROUP BY importStatus',
id(new PhabricatorRepositoryCommit())->getTableName(),
$this->getID());
$done = 0;
$total = 0;
foreach ($progress as $row) {
$total += $row['N'] * 4;
$status = $row['importStatus'];
if ($status & PhabricatorRepositoryCommit::IMPORTED_MESSAGE) {
$done += $row['N'];
}
if ($status & PhabricatorRepositoryCommit::IMPORTED_CHANGE) {
$done += $row['N'];
}
if ($status & PhabricatorRepositoryCommit::IMPORTED_OWNERS) {
$done += $row['N'];
}
if ($status & PhabricatorRepositoryCommit::IMPORTED_HERALD) {
$done += $row['N'];
}
}
if ($total) {
$ratio = ($done / $total);
} else {
$ratio = 0;
}
// Cap this at "99.99%", because it's confusing to users when the actual
// fraction is "99.996%" and it rounds up to "100.00%".
if ($ratio > 0.9999) {
$ratio = 0.9999;
}
return $ratio;
}
/**
* Should this repository publish feed, notifications, audits, and email?
*
* We do not publish information about repositories during initial import,
* or if the repository has been set not to publish.
*/
public function shouldPublish() {
if ($this->isImporting()) {
return false;
}
if ($this->getDetail('herald-disabled')) {
return false;
}
return true;
}
/* -( Autoclose )---------------------------------------------------------- */
public function shouldAutocloseRef(DiffusionRepositoryRef $ref) {
if (!$ref->isBranch()) {
return false;
}
return $this->shouldAutocloseBranch($ref->getShortName());
}
/**
* Determine if autoclose is active for a branch.
*
* For more details about why, use @{method:shouldSkipAutocloseBranch}.
*
* @param string Branch name to check.
* @return bool True if autoclose is active for the branch.
* @task autoclose
*/
public function shouldAutocloseBranch($branch) {
return ($this->shouldSkipAutocloseBranch($branch) === null);
}
/**
* Determine if autoclose is active for a commit.
*
* For more details about why, use @{method:shouldSkipAutocloseCommit}.
*
* @param PhabricatorRepositoryCommit Commit to check.
* @return bool True if autoclose is active for the commit.
* @task autoclose
*/
public function shouldAutocloseCommit(PhabricatorRepositoryCommit $commit) {
return ($this->shouldSkipAutocloseCommit($commit) === null);
}
/**
* Determine why autoclose should be skipped for a branch.
*
* This method gives a detailed reason why autoclose will be skipped. To
* perform a simple test, use @{method:shouldAutocloseBranch}.
*
* @param string Branch name to check.
* @return const|null Constant identifying reason to skip this branch, or null
* if autoclose is active.
* @task autoclose
*/
public function shouldSkipAutocloseBranch($branch) {
$all_reason = $this->shouldSkipAllAutoclose();
if ($all_reason) {
return $all_reason;
}
if (!$this->shouldTrackBranch($branch)) {
return self::BECAUSE_BRANCH_UNTRACKED;
}
if (!$this->isBranchInFilter($branch, 'close-commits-filter')) {
return self::BECAUSE_BRANCH_NOT_AUTOCLOSE;
}
return null;
}
/**
* Determine why autoclose should be skipped for a commit.
*
* This method gives a detailed reason why autoclose will be skipped. To
* perform a simple test, use @{method:shouldAutocloseCommit}.
*
* @param PhabricatorRepositoryCommit Commit to check.
* @return const|null Constant identifying reason to skip this commit, or null
* if autoclose is active.
* @task autoclose
*/
public function shouldSkipAutocloseCommit(
PhabricatorRepositoryCommit $commit) {
$all_reason = $this->shouldSkipAllAutoclose();
if ($all_reason) {
return $all_reason;
}
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
return null;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
break;
default:
throw new Exception(pht('Unrecognized version control system.'));
}
$closeable_flag = PhabricatorRepositoryCommit::IMPORTED_CLOSEABLE;
if (!$commit->isPartiallyImported($closeable_flag)) {
return self::BECAUSE_NOT_ON_AUTOCLOSE_BRANCH;
}
return null;
}
/**
* Determine why all autoclose operations should be skipped for this
* repository.
*
* @return const|null Constant identifying reason to skip all autoclose
* operations, or null if autoclose operations are not blocked at the
* repository level.
* @task autoclose
*/
private function shouldSkipAllAutoclose() {
if ($this->isImporting()) {
return self::BECAUSE_REPOSITORY_IMPORTING;
}
if ($this->getDetail('disable-autoclose', false)) {
return self::BECAUSE_AUTOCLOSE_DISABLED;
}
return null;
}
public function getAutocloseOnlyRules() {
return array_keys($this->getDetail('close-commits-filter', array()));
}
public function setAutocloseOnlyRules(array $rules) {
$rules = array_fill_keys($rules, true);
$this->setDetail('close-commits-filter', $rules);
return $this;
}
public function getTrackOnlyRules() {
return array_keys($this->getDetail('branch-filter', array()));
}
public function setTrackOnlyRules(array $rules) {
$rules = array_fill_keys($rules, true);
$this->setDetail('branch-filter', $rules);
return $this;
}
/* -( Repository URI Management )------------------------------------------ */
/**
* Get the remote URI for this repository.
*
* @return string
* @task uri
*/
public function getRemoteURI() {
return (string)$this->getRemoteURIObject();
}
/**
* Get the remote URI for this repository, including credentials if they're
* used by this repository.
*
* @return PhutilOpaqueEnvelope URI, possibly including credentials.
* @task uri
*/
public function getRemoteURIEnvelope() {
$uri = $this->getRemoteURIObject();
$remote_protocol = $this->getRemoteProtocol();
if ($remote_protocol == 'http' || $remote_protocol == 'https') {
// For SVN, we use `--username` and `--password` flags separately, so
// don't add any credentials here.
if (!$this->isSVN()) {
$credential_phid = $this->getCredentialPHID();
if ($credential_phid) {
$key = PassphrasePasswordKey::loadFromPHID(
$credential_phid,
PhabricatorUser::getOmnipotentUser());
$uri->setUser($key->getUsernameEnvelope()->openEnvelope());
$uri->setPass($key->getPasswordEnvelope()->openEnvelope());
}
}
}
return new PhutilOpaqueEnvelope((string)$uri);
}
/**
* Get the clone (or checkout) URI for this repository, without authentication
* information.
*
* @return string Repository URI.
* @task uri
*/
public function getPublicCloneURI() {
return (string)$this->getCloneURIObject();
}
/**
* Get the protocol for the repository's remote.
*
* @return string Protocol, like "ssh" or "git".
* @task uri
*/
public function getRemoteProtocol() {
$uri = $this->getRemoteURIObject();
return $uri->getProtocol();
}
/**
* Get a parsed object representation of the repository's remote URI..
*
* @return wild A @{class@libphutil:PhutilURI}.
* @task uri
*/
public function getRemoteURIObject() {
$raw_uri = $this->getDetail('remote-uri');
if (!strlen($raw_uri)) {
return new PhutilURI('');
}
if (!strncmp($raw_uri, '/', 1)) {
return new PhutilURI('file://'.$raw_uri);
}
return new PhutilURI($raw_uri);
}
/**
* Get the "best" clone/checkout URI for this repository, on any protocol.
*/
public function getCloneURIObject() {
if (!$this->isHosted()) {
if ($this->isSVN()) {
// Make sure we pick up the "Import Only" path for Subversion, so
// the user clones the repository starting at the correct path, not
// from the root.
$base_uri = $this->getSubversionBaseURI();
$base_uri = new PhutilURI($base_uri);
$path = $base_uri->getPath();
if (!$path) {
$path = '/';
}
// If the trailing "@" is not required to escape the URI, strip it for
// readability.
if (!preg_match('/@.*@/', $path)) {
$path = rtrim($path, '@');
}
$base_uri->setPath($path);
return $base_uri;
} else {
return $this->getRemoteURIObject();
}
}
// TODO: This should be cleaned up to deal with all the new URI handling.
$another_copy = id(new PhabricatorRepositoryQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($this->getPHID()))
->needURIs(true)
->executeOne();
$clone_uris = $another_copy->getCloneURIs();
if (!$clone_uris) {
return null;
}
return head($clone_uris)->getEffectiveURI();
}
private function getRawHTTPCloneURIObject() {
$uri = PhabricatorEnv::getProductionURI($this->getURI());
$uri = new PhutilURI($uri);
if ($this->isGit()) {
$uri->setPath($uri->getPath().$this->getCloneName().'.git');
} else if ($this->isHg()) {
$uri->setPath($uri->getPath().$this->getCloneName().'/');
}
return $uri;
}
/**
* Determine if we should connect to the remote using SSH flags and
* credentials.
*
* @return bool True to use the SSH protocol.
* @task uri
*/
private function shouldUseSSH() {
if ($this->isHosted()) {
return false;
}
$protocol = $this->getRemoteProtocol();
if ($this->isSSHProtocol($protocol)) {
return true;
}
return false;
}
/**
* Determine if we should connect to the remote using HTTP flags and
* credentials.
*
* @return bool True to use the HTTP protocol.
* @task uri
*/
private function shouldUseHTTP() {
if ($this->isHosted()) {
return false;
}
$protocol = $this->getRemoteProtocol();
return ($protocol == 'http' || $protocol == 'https');
}
/**
* Determine if we should connect to the remote using SVN flags and
* credentials.
*
* @return bool True to use the SVN protocol.
* @task uri
*/
private function shouldUseSVNProtocol() {
if ($this->isHosted()) {
return false;
}
$protocol = $this->getRemoteProtocol();
return ($protocol == 'svn');
}
/**
* Determine if a protocol is SSH or SSH-like.
*
* @param string A protocol string, like "http" or "ssh".
* @return bool True if the protocol is SSH-like.
* @task uri
*/
private function isSSHProtocol($protocol) {
return ($protocol == 'ssh' || $protocol == 'svn+ssh');
}
public function delete() {
$this->openTransaction();
$paths = id(new PhabricatorOwnersPath())
->loadAllWhere('repositoryPHID = %s', $this->getPHID());
foreach ($paths as $path) {
$path->delete();
}
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE repositoryPHID = %s',
id(new PhabricatorRepositorySymbol())->getTableName(),
$this->getPHID());
$commits = id(new PhabricatorRepositoryCommit())
->loadAllWhere('repositoryID = %d', $this->getID());
foreach ($commits as $commit) {
// note PhabricatorRepositoryAuditRequests and
// PhabricatorRepositoryCommitData are deleted here too.
$commit->delete();
}
$uris = id(new PhabricatorRepositoryURI())
->loadAllWhere('repositoryPHID = %s', $this->getPHID());
foreach ($uris as $uri) {
$uri->delete();
}
$ref_cursors = id(new PhabricatorRepositoryRefCursor())
->loadAllWhere('repositoryPHID = %s', $this->getPHID());
foreach ($ref_cursors as $cursor) {
$cursor->delete();
}
$conn_w = $this->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE repositoryID = %d',
self::TABLE_FILESYSTEM,
$this->getID());
queryfx(
$conn_w,
'DELETE FROM %T WHERE repositoryID = %d',
self::TABLE_PATHCHANGE,
$this->getID());
queryfx(
$conn_w,
'DELETE FROM %T WHERE repositoryID = %d',
self::TABLE_SUMMARY,
$this->getID());
$result = parent::delete();
$this->saveTransaction();
return $result;
}
public function isGit() {
$vcs = $this->getVersionControlSystem();
return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
}
public function isSVN() {
$vcs = $this->getVersionControlSystem();
return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_SVN);
}
public function isHg() {
$vcs = $this->getVersionControlSystem();
return ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL);
}
public function isHosted() {
return (bool)$this->getDetail('hosting-enabled', false);
}
public function setHosted($enabled) {
return $this->setDetail('hosting-enabled', $enabled);
}
- public function canServeProtocol($protocol, $write) {
- if (!$this->isTracked()) {
- return false;
+ public function canServeProtocol(
+ $protocol,
+ $write,
+ $is_intracluster = false) {
+
+ // See T13192. If a repository is inactive, don't serve it to users. We
+ // still synchronize it within the cluster and serve it to other repository
+ // nodes.
+ if (!$is_intracluster) {
+ if (!$this->isTracked()) {
+ return false;
+ }
}
$clone_uris = $this->getCloneURIs();
foreach ($clone_uris as $uri) {
if ($uri->getBuiltinProtocol() !== $protocol) {
continue;
}
$io_type = $uri->getEffectiveIoType();
if ($io_type == PhabricatorRepositoryURI::IO_READWRITE) {
return true;
}
if (!$write) {
if ($io_type == PhabricatorRepositoryURI::IO_READ) {
return true;
}
}
}
return false;
}
public function hasLocalWorkingCopy() {
try {
self::assertLocalExists();
return true;
} catch (Exception $ex) {
return false;
}
}
/**
* Raise more useful errors when there are basic filesystem problems.
*/
private function assertLocalExists() {
if (!$this->usesLocalWorkingCopy()) {
return;
}
$local = $this->getLocalPath();
Filesystem::assertExists($local);
Filesystem::assertIsDirectory($local);
Filesystem::assertReadable($local);
}
/**
* Determine if the working copy is bare or not. In Git, this corresponds
* to `--bare`. In Mercurial, `--noupdate`.
*/
public function isWorkingCopyBare() {
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
return false;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$local = $this->getLocalPath();
if (Filesystem::pathExists($local.'/.git')) {
return false;
} else {
return true;
}
}
}
public function usesLocalWorkingCopy() {
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
return $this->isHosted();
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
return true;
}
}
public function getHookDirectories() {
$directories = array();
if (!$this->isHosted()) {
return $directories;
}
$root = $this->getLocalPath();
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
if ($this->isWorkingCopyBare()) {
$directories[] = $root.'/hooks/pre-receive-phabricator.d/';
} else {
$directories[] = $root.'/.git/hooks/pre-receive-phabricator.d/';
}
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$directories[] = $root.'/hooks/pre-commit-phabricator.d/';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
// NOTE: We don't support custom Mercurial hooks for now because they're
// messy and we can't easily just drop a `hooks.d/` directory next to
// the hooks.
break;
}
return $directories;
}
public function canDestroyWorkingCopy() {
if ($this->isHosted()) {
// Never destroy hosted working copies.
return false;
}
$default_path = PhabricatorEnv::getEnvConfig(
'repository.default-local-path');
return Filesystem::isDescendant($this->getLocalPath(), $default_path);
}
public function canUsePathTree() {
return !$this->isSVN();
}
public function canUseGitLFS() {
if (!$this->isGit()) {
return false;
}
if (!$this->isHosted()) {
return false;
}
if (!PhabricatorEnv::getEnvConfig('diffusion.allow-git-lfs')) {
return false;
}
return true;
}
public function getGitLFSURI($path = null) {
if (!$this->canUseGitLFS()) {
throw new Exception(
pht(
'This repository does not support Git LFS, so Git LFS URIs can '.
'not be generated for it.'));
}
$uri = $this->getRawHTTPCloneURIObject();
$uri = (string)$uri;
$uri = $uri.'/'.$path;
return $uri;
}
public function canMirror() {
if ($this->isGit() || $this->isHg()) {
return true;
}
return false;
}
public function canAllowDangerousChanges() {
if (!$this->isHosted()) {
return false;
}
// In Git and Mercurial, ref deletions and rewrites are dangerous.
// In Subversion, editing revprops is dangerous.
return true;
}
public function shouldAllowDangerousChanges() {
return (bool)$this->getDetail('allow-dangerous-changes');
}
public function canAllowEnormousChanges() {
if (!$this->isHosted()) {
return false;
}
return true;
}
public function shouldAllowEnormousChanges() {
return (bool)$this->getDetail('allow-enormous-changes');
}
public function writeStatusMessage(
$status_type,
$status_code,
array $parameters = array()) {
$table = new PhabricatorRepositoryStatusMessage();
$conn_w = $table->establishConnection('w');
$table_name = $table->getTableName();
if ($status_code === null) {
queryfx(
$conn_w,
'DELETE FROM %T WHERE repositoryID = %d AND statusType = %s',
$table_name,
$this->getID(),
$status_type);
} else {
// If the existing message has the same code (e.g., we just hit an
// error and also previously hit an error) we increment the message
// count. This allows us to determine how many times in a row we've
// run into an error.
// NOTE: The assignments in "ON DUPLICATE KEY UPDATE" are evaluated
// in order, so the "messageCount" assignment must occur before the
// "statusCode" assignment. See T11705.
queryfx(
$conn_w,
'INSERT INTO %T
(repositoryID, statusType, statusCode, parameters, epoch,
messageCount)
VALUES (%d, %s, %s, %s, %d, %d)
ON DUPLICATE KEY UPDATE
messageCount =
IF(
statusCode = VALUES(statusCode),
messageCount + VALUES(messageCount),
VALUES(messageCount)),
statusCode = VALUES(statusCode),
parameters = VALUES(parameters),
epoch = VALUES(epoch)',
$table_name,
$this->getID(),
$status_type,
$status_code,
json_encode($parameters),
time(),
1);
}
return $this;
}
public static function assertValidRemoteURI($uri) {
if (trim($uri) != $uri) {
throw new Exception(
pht('The remote URI has leading or trailing whitespace.'));
}
$uri_object = new PhutilURI($uri);
$protocol = $uri_object->getProtocol();
// Catch confusion between Git/SCP-style URIs and normal URIs. See T3619
// for discussion. This is usually a user adding "ssh://" to an implicit
// SSH Git URI.
if ($protocol == 'ssh') {
if (preg_match('(^[^:@]+://[^/:]+:[^\d])', $uri)) {
throw new Exception(
pht(
"The remote URI is not formatted correctly. Remote URIs ".
"with an explicit protocol should be in the form ".
"'%s', not '%s'. The '%s' syntax is only valid in SCP-style URIs.",
'proto://domain/path',
'proto://domain:/path',
':/path'));
}
}
switch ($protocol) {
case 'ssh':
case 'http':
case 'https':
case 'git':
case 'svn':
case 'svn+ssh':
break;
default:
// NOTE: We're explicitly rejecting 'file://' because it can be
// used to clone from the working copy of another repository on disk
// that you don't normally have permission to access.
throw new Exception(
pht(
'The URI protocol is unrecognized. It should begin with '.
'"%s", "%s", "%s", "%s", "%s", "%s", or be in the form "%s".',
'ssh://',
'http://',
'https://',
'git://',
'svn://',
'svn+ssh://',
'git@domain.com:path'));
}
return true;
}
/**
* Load the pull frequency for this repository, based on the time since the
* last activity.
*
* We pull rarely used repositories less frequently. This finds the most
* recent commit which is older than the current time (which prevents us from
* spinning on repositories with a silly commit post-dated to some time in
* 2037). We adjust the pull frequency based on when the most recent commit
* occurred.
*
* @param int The minimum update interval to use, in seconds.
* @return int Repository update interval, in seconds.
*/
public function loadUpdateInterval($minimum = 15) {
// First, check if we've hit errors recently. If we have, wait one period
// for each consecutive error. Normally, this corresponds to a backoff of
// 15s, 30s, 45s, etc.
$message_table = new PhabricatorRepositoryStatusMessage();
$conn = $message_table->establishConnection('r');
$error_count = queryfx_one(
$conn,
'SELECT MAX(messageCount) error_count FROM %T
WHERE repositoryID = %d
AND statusType IN (%Ls)
AND statusCode IN (%Ls)',
$message_table->getTableName(),
$this->getID(),
array(
PhabricatorRepositoryStatusMessage::TYPE_INIT,
PhabricatorRepositoryStatusMessage::TYPE_FETCH,
),
array(
PhabricatorRepositoryStatusMessage::CODE_ERROR,
));
$error_count = (int)$error_count['error_count'];
if ($error_count > 0) {
return (int)($minimum * $error_count);
}
// If a repository is still importing, always pull it as frequently as
// possible. This prevents us from hanging for a long time at 99.9% when
// importing an inactive repository.
if ($this->isImporting()) {
return $minimum;
}
$window_start = (PhabricatorTime::getNow() + $minimum);
$table = id(new PhabricatorRepositoryCommit());
$last_commit = queryfx_one(
$table->establishConnection('r'),
'SELECT epoch FROM %T
WHERE repositoryID = %d AND epoch <= %d
ORDER BY epoch DESC LIMIT 1',
$table->getTableName(),
$this->getID(),
$window_start);
if ($last_commit) {
$time_since_commit = ($window_start - $last_commit['epoch']);
} else {
// If the repository has no commits, treat the creation date as
// though it were the date of the last commit. This makes empty
// repositories update quickly at first but slow down over time
// if they don't see any activity.
$time_since_commit = ($window_start - $this->getDateCreated());
}
$last_few_days = phutil_units('3 days in seconds');
if ($time_since_commit <= $last_few_days) {
// For repositories with activity in the recent past, we wait one
// extra second for every 10 minutes since the last commit. This
// shorter backoff is intended to handle weekends and other short
// breaks from development.
$smart_wait = ($time_since_commit / 600);
} else {
// For repositories without recent activity, we wait one extra second
// for every 4 minutes since the last commit. This longer backoff
// handles rarely used repositories, up to the maximum.
$smart_wait = ($time_since_commit / 240);
}
// We'll never wait more than 6 hours to pull a repository.
$longest_wait = phutil_units('6 hours in seconds');
$smart_wait = min($smart_wait, $longest_wait);
$smart_wait = max($minimum, $smart_wait);
return (int)$smart_wait;
}
/**
* Time limit for cloning or copying this repository.
*
* This limit is used to timeout operations like `git clone` or `git fetch`
* when doing intracluster synchronization, building working copies, etc.
*
* @return int Maximum number of seconds to spend copying this repository.
*/
public function getCopyTimeLimit() {
return $this->getDetail('limit.copy');
}
public function setCopyTimeLimit($limit) {
return $this->setDetail('limit.copy', $limit);
}
public function getDefaultCopyTimeLimit() {
return phutil_units('15 minutes in seconds');
}
public function getEffectiveCopyTimeLimit() {
$limit = $this->getCopyTimeLimit();
if ($limit) {
return $limit;
}
return $this->getDefaultCopyTimeLimit();
}
public function getFilesizeLimit() {
return $this->getDetail('limit.filesize');
}
public function setFilesizeLimit($limit) {
return $this->setDetail('limit.filesize', $limit);
}
public function getTouchLimit() {
return $this->getDetail('limit.touch');
}
public function setTouchLimit($limit) {
return $this->setDetail('limit.touch', $limit);
}
/**
* Retrieve the service URI for the device hosting this repository.
*
* See @{method:newConduitClient} for a general discussion of interacting
* with repository services. This method provides lower-level resolution of
* services, returning raw URIs.
*
* @param PhabricatorUser Viewing user.
* @param map<string, wild> Constraints on selectable services.
* @return string|null URI, or `null` for local repositories.
*/
public function getAlmanacServiceURI(
PhabricatorUser $viewer,
array $options) {
PhutilTypeSpec::checkMap(
$options,
array(
'neverProxy' => 'bool',
'protocols' => 'list<string>',
'writable' => 'optional bool',
));
$never_proxy = $options['neverProxy'];
$protocols = $options['protocols'];
$writable = idx($options, 'writable', false);
$cache_key = $this->getAlmanacServiceCacheKey();
if (!$cache_key) {
return null;
}
$cache = PhabricatorCaches::getMutableStructureCache();
$uris = $cache->getKey($cache_key, false);
// If we haven't built the cache yet, build it now.
if ($uris === false) {
$uris = $this->buildAlmanacServiceURIs();
$cache->setKey($cache_key, $uris);
}
if ($uris === null) {
return null;
}
$local_device = AlmanacKeys::getDeviceID();
if ($never_proxy && !$local_device) {
throw new Exception(
pht(
'Unable to handle proxied service request. This device is not '.
'registered, so it can not identify local services. Register '.
'this device before sending requests here.'));
}
$protocol_map = array_fuse($protocols);
$results = array();
foreach ($uris as $uri) {
// If we're never proxying this and it's locally satisfiable, return
// `null` to tell the caller to handle it locally. If we're allowed to
// proxy, we skip this check and may proxy the request to ourselves.
// (That proxied request will end up here with proxying forbidden,
// return `null`, and then the request will actually run.)
if ($local_device && $never_proxy) {
if ($uri['device'] == $local_device) {
return null;
}
}
if (isset($protocol_map[$uri['protocol']])) {
$results[] = $uri;
}
}
if (!$results) {
throw new Exception(
pht(
'The Almanac service for this repository is not bound to any '.
'interfaces which support the required protocols (%s).',
implode(', ', $protocols)));
}
if ($never_proxy) {
+ // See PHI1030. This error can arise from various device name/address
+ // mismatches which are hard to detect, so try to provide as much
+ // information as we can.
+
+ if ($writable) {
+ $request_type = pht('(This is a write request.)');
+ } else {
+ $request_type = pht('(This is a read request.)');
+ }
+
throw new Exception(
pht(
- 'Refusing to proxy a repository request from a cluster host. '.
- 'Cluster hosts must correctly route their intracluster requests.'));
+ 'This repository request (for repository "%s") has been '.
+ 'incorrectly routed to a cluster host (with device name "%s", '.
+ 'and hostname "%s") which can not serve the request.'.
+ "\n\n".
+ 'The Almanac device address for the correct device may improperly '.
+ 'point at this host, or the "device.id" configuration file on '.
+ 'this host may be incorrect.'.
+ "\n\n".
+ 'Requests routed within the cluster by Phabricator are always '.
+ 'expected to be sent to a node which can serve the request. To '.
+ 'prevent loops, this request will not be proxied again.'.
+ "\n\n".
+ "%s",
+ $this->getDisplayName(),
+ $local_device,
+ php_uname('n'),
+ $request_type));
}
if (count($results) > 1) {
if (!$this->supportsSynchronization()) {
throw new Exception(
pht(
'Repository "%s" is bound to multiple active repository hosts, '.
'but this repository does not support cluster synchronization. '.
'Declusterize this repository or move it to a service with only '.
'one host.',
$this->getDisplayName()));
}
}
// If we require a writable device, remove URIs which aren't writable.
if ($writable) {
foreach ($results as $key => $uri) {
if (!$uri['writable']) {
unset($results[$key]);
}
}
if (!$results) {
throw new Exception(
pht(
'This repository ("%s") is not writable with the given '.
'protocols (%s). The Almanac service for this repository has no '.
'writable bindings that support these protocols.',
$this->getDisplayName(),
implode(', ', $protocols)));
}
}
if ($writable) {
$results = $this->sortWritableAlmanacServiceURIs($results);
} else {
shuffle($results);
}
$result = head($results);
return $result['uri'];
}
private function sortWritableAlmanacServiceURIs(array $results) {
// See T13109 for discussion of how this method routes requests.
// In the absence of other rules, we'll send traffic to devices randomly.
// We also want to select randomly among nodes which are equally good
// candidates to receive the write, and accomplish that by shuffling the
// list up front.
shuffle($results);
$order = array();
// If some device is currently holding the write lock, send all requests
// to that device. We're trying to queue writes on a single device so they
// do not need to wait for read synchronization after earlier writes
// complete.
$writer = PhabricatorRepositoryWorkingCopyVersion::loadWriter(
$this->getPHID());
if ($writer) {
$device_phid = $writer->getWriteProperty('devicePHID');
foreach ($results as $key => $result) {
if ($result['devicePHID'] === $device_phid) {
$order[] = $key;
}
}
}
// If no device is currently holding the write lock, try to send requests
// to a device which is already up to date and will not need to synchronize
// before it can accept the write.
$versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions(
$this->getPHID());
if ($versions) {
$max_version = (int)max(mpull($versions, 'getRepositoryVersion'));
$max_devices = array();
foreach ($versions as $version) {
if ($version->getRepositoryVersion() == $max_version) {
$max_devices[] = $version->getDevicePHID();
}
}
$max_devices = array_fuse($max_devices);
foreach ($results as $key => $result) {
if (isset($max_devices[$result['devicePHID']])) {
$order[] = $key;
}
}
}
// Reorder the results, putting any we've selected as preferred targets for
// the write at the head of the list.
$results = array_select_keys($results, $order) + $results;
return $results;
}
public function supportsSynchronization() {
// TODO: For now, this is only supported for Git.
if (!$this->isGit()) {
return false;
}
return true;
}
public function getAlmanacServiceCacheKey() {
$service_phid = $this->getAlmanacServicePHID();
if (!$service_phid) {
return null;
}
$repository_phid = $this->getPHID();
$parts = array(
"repo({$repository_phid})",
"serv({$service_phid})",
- 'v3',
+ 'v4',
);
return implode('.', $parts);
}
private function buildAlmanacServiceURIs() {
$service = $this->loadAlmanacService();
if (!$service) {
return null;
}
$bindings = $service->getActiveBindings();
if (!$bindings) {
throw new Exception(
pht(
'The Almanac service for this repository is not bound to any '.
'interfaces.'));
}
$uris = array();
foreach ($bindings as $binding) {
$iface = $binding->getInterface();
$uri = $this->getClusterRepositoryURIFromBinding($binding);
$protocol = $uri->getProtocol();
$device_name = $iface->getDevice()->getName();
$device_phid = $iface->getDevice()->getPHID();
$uris[] = array(
'protocol' => $protocol,
'uri' => (string)$uri,
'device' => $device_name,
'writable' => (bool)$binding->getAlmanacPropertyValue('writable'),
'devicePHID' => $device_phid,
);
}
return $uris;
}
/**
* Build a new Conduit client in order to make a service call to this
* repository.
*
* If the repository is hosted locally, this method may return `null`. The
* caller should use `ConduitCall` or other local logic to complete the
* request.
*
* By default, we will return a @{class:ConduitClient} for any repository with
* a service, even if that service is on the current device.
*
* We do this because this configuration does not make very much sense in a
* production context, but is very common in a test/development context
* (where the developer's machine is both the web host and the repository
* service). By proxying in development, we get more consistent behavior
* between development and production, and don't have a major untested
* codepath.
*
* The `$never_proxy` parameter can be used to prevent this local proxying.
* If the flag is passed:
*
* - The method will return `null` (implying a local service call)
* if the repository service is hosted on the current device.
* - The method will throw if it would need to return a client.
*
* This is used to prevent loops in Conduit: the first request will proxy,
* even in development, but the second request will be identified as a
* cluster request and forced not to proxy.
*
* For lower-level service resolution, see @{method:getAlmanacServiceURI}.
*
* @param PhabricatorUser Viewing user.
* @param bool `true` to throw if a client would be returned.
* @return ConduitClient|null Client, or `null` for local repositories.
*/
public function newConduitClient(
PhabricatorUser $viewer,
$never_proxy = false) {
$uri = $this->getAlmanacServiceURI(
$viewer,
array(
'neverProxy' => $never_proxy,
'protocols' => array(
'http',
'https',
),
// At least today, no Conduit call can ever write to a repository,
// so it's fine to send anything to a read-only node.
'writable' => false,
));
if ($uri === null) {
return null;
}
$domain = id(new PhutilURI(PhabricatorEnv::getURI('/')))->getDomain();
$client = id(new ConduitClient($uri))
->setHost($domain);
if ($viewer->isOmnipotent()) {
// If the caller is the omnipotent user (normally, a daemon), we will
// sign the request with this host's asymmetric keypair.
$public_path = AlmanacKeys::getKeyPath('device.pub');
try {
$public_key = Filesystem::readFile($public_path);
} catch (Exception $ex) {
throw new PhutilAggregateException(
pht(
'Unable to read device public key while attempting to make '.
'authenticated method call within the Phabricator cluster. '.
'Use `%s` to register keys for this device. Exception: %s',
'bin/almanac register',
$ex->getMessage()),
array($ex));
}
$private_path = AlmanacKeys::getKeyPath('device.key');
try {
$private_key = Filesystem::readFile($private_path);
$private_key = new PhutilOpaqueEnvelope($private_key);
} catch (Exception $ex) {
throw new PhutilAggregateException(
pht(
'Unable to read device private key while attempting to make '.
'authenticated method call within the Phabricator cluster. '.
'Use `%s` to register keys for this device. Exception: %s',
'bin/almanac register',
$ex->getMessage()),
array($ex));
}
$client->setSigningKeys($public_key, $private_key);
} else {
// If the caller is a normal user, we generate or retrieve a cluster
// API token.
$token = PhabricatorConduitToken::loadClusterTokenForUser($viewer);
if ($token) {
$client->setConduitToken($token->getToken());
}
}
return $client;
}
public function getPassthroughEnvironmentalVariables() {
$env = $_ENV;
if ($this->isGit()) {
// $_ENV does not populate in CLI contexts if "E" is missing from
// "variables_order" in PHP config. Currently, we do not require this
// to be configured. Since it may not be, explicitly bring expected Git
// environmental variables into scope. This list is not exhaustive, but
// only lists variables with a known impact on commit hook behavior.
// This can be removed if we later require "E" in "variables_order".
$git_env = array(
'GIT_OBJECT_DIRECTORY',
'GIT_ALTERNATE_OBJECT_DIRECTORIES',
'GIT_QUARANTINE_PATH',
);
foreach ($git_env as $key) {
$value = getenv($key);
if (strlen($value)) {
$env[$key] = $value;
}
}
$key = 'GIT_PUSH_OPTION_COUNT';
$git_count = getenv($key);
if (strlen($git_count)) {
$git_count = (int)$git_count;
$env[$key] = $git_count;
for ($ii = 0; $ii < $git_count; $ii++) {
$key = 'GIT_PUSH_OPTION_'.$ii;
$env[$key] = getenv($key);
}
}
}
$result = array();
foreach ($env as $key => $value) {
// In Git, pass anything matching "GIT_*" though. Some of these variables
// need to be preserved to allow `git` operations to work properly when
// running from commit hooks.
if ($this->isGit()) {
if (preg_match('/^GIT_/', $key)) {
$result[$key] = $value;
}
}
}
return $result;
}
public function supportsBranchComparison() {
return $this->isGit();
}
/* -( Repository URIs )---------------------------------------------------- */
public function attachURIs(array $uris) {
$custom_map = array();
foreach ($uris as $key => $uri) {
$builtin_key = $uri->getRepositoryURIBuiltinKey();
if ($builtin_key !== null) {
$custom_map[$builtin_key] = $key;
}
}
$builtin_uris = $this->newBuiltinURIs();
$seen_builtins = array();
foreach ($builtin_uris as $builtin_uri) {
$builtin_key = $builtin_uri->getRepositoryURIBuiltinKey();
$seen_builtins[$builtin_key] = true;
// If this builtin URI is disabled, don't attach it and remove the
// persisted version if it exists.
if ($builtin_uri->getIsDisabled()) {
if (isset($custom_map[$builtin_key])) {
unset($uris[$custom_map[$builtin_key]]);
}
continue;
}
// If the URI exists, make sure it's marked as not being disabled.
if (isset($custom_map[$builtin_key])) {
$uris[$custom_map[$builtin_key]]->setIsDisabled(false);
}
}
// Remove any builtins which no longer exist.
foreach ($custom_map as $builtin_key => $key) {
if (empty($seen_builtins[$builtin_key])) {
unset($uris[$key]);
}
}
$this->uris = $uris;
return $this;
}
public function getURIs() {
return $this->assertAttached($this->uris);
}
public function getCloneURIs() {
$uris = $this->getURIs();
$clone = array();
foreach ($uris as $uri) {
if (!$uri->isBuiltin()) {
continue;
}
if ($uri->getIsDisabled()) {
continue;
}
$io_type = $uri->getEffectiveIoType();
$is_clone =
($io_type == PhabricatorRepositoryURI::IO_READ) ||
($io_type == PhabricatorRepositoryURI::IO_READWRITE);
if (!$is_clone) {
continue;
}
$clone[] = $uri;
}
$clone = msort($clone, 'getURIScore');
$clone = array_reverse($clone);
return $clone;
}
public function newBuiltinURIs() {
$has_callsign = ($this->getCallsign() !== null);
$has_shortname = ($this->getRepositorySlug() !== null);
$identifier_map = array(
PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_CALLSIGN => $has_callsign,
PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_SHORTNAME => $has_shortname,
PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_ID => true,
);
// If the view policy of the repository is public, support anonymous HTTP
// even if authenticated HTTP is not supported.
if ($this->getViewPolicy() === PhabricatorPolicies::POLICY_PUBLIC) {
$allow_http = true;
} else {
$allow_http = PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth');
}
$base_uri = PhabricatorEnv::getURI('/');
$base_uri = new PhutilURI($base_uri);
$has_https = ($base_uri->getProtocol() == 'https');
$has_https = ($has_https && $allow_http);
$has_http = !PhabricatorEnv::getEnvConfig('security.require-https');
$has_http = ($has_http && $allow_http);
// HTTP is not supported for Subversion.
if ($this->isSVN()) {
$has_http = false;
$has_https = false;
}
$has_ssh = (bool)strlen(PhabricatorEnv::getEnvConfig('phd.user'));
$protocol_map = array(
PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH => $has_ssh,
PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTPS => $has_https,
PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTP => $has_http,
);
$uris = array();
foreach ($protocol_map as $protocol => $proto_supported) {
foreach ($identifier_map as $identifier => $id_supported) {
// This is just a dummy value because it can't be empty; we'll force
// it to a proper value when using it in the UI.
$builtin_uri = "{$protocol}://{$identifier}";
$uris[] = PhabricatorRepositoryURI::initializeNewURI()
->setRepositoryPHID($this->getPHID())
->attachRepository($this)
->setBuiltinProtocol($protocol)
->setBuiltinIdentifier($identifier)
->setURI($builtin_uri)
->setIsDisabled((int)(!$proto_supported || !$id_supported));
}
}
return $uris;
}
public function getClusterRepositoryURIFromBinding(
AlmanacBinding $binding) {
$protocol = $binding->getAlmanacPropertyValue('protocol');
if ($protocol === null) {
$protocol = 'https';
}
$iface = $binding->getInterface();
$address = $iface->renderDisplayAddress();
$path = $this->getURI();
return id(new PhutilURI("{$protocol}://{$address}"))
->setPath($path);
}
public function loadAlmanacService() {
$service_phid = $this->getAlmanacServicePHID();
if (!$service_phid) {
// No service, so this is a local repository.
return null;
}
$service = id(new AlmanacServiceQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($service_phid))
->needBindings(true)
->needProperties(true)
->executeOne();
if (!$service) {
throw new Exception(
pht(
'The Almanac service for this repository is invalid or could not '.
'be loaded.'));
}
$service_type = $service->getServiceImplementation();
if (!($service_type instanceof AlmanacClusterRepositoryServiceType)) {
throw new Exception(
pht(
'The Almanac service for this repository does not have the correct '.
'service type.'));
}
return $service;
}
public function markImporting() {
$this->openTransaction();
$this->beginReadLocking();
$repository = $this->reload();
$repository->setDetail('importing', true);
$repository->save();
$this->endReadLocking();
$this->saveTransaction();
return $repository;
}
/* -( Symbols )-------------------------------------------------------------*/
public function getSymbolSources() {
return $this->getDetail('symbol-sources', array());
}
public function getSymbolLanguages() {
return $this->getDetail('symbol-languages', array());
}
/* -( Staging )------------------------------------------------------------ */
public function supportsStaging() {
return $this->isGit();
}
public function getStagingURI() {
if (!$this->supportsStaging()) {
return null;
}
return $this->getDetail('staging-uri', null);
}
/* -( Automation )--------------------------------------------------------- */
public function supportsAutomation() {
return $this->isGit();
}
public function canPerformAutomation() {
if (!$this->supportsAutomation()) {
return false;
}
if (!$this->getAutomationBlueprintPHIDs()) {
return false;
}
return true;
}
public function getAutomationBlueprintPHIDs() {
if (!$this->supportsAutomation()) {
return array();
}
return $this->getDetail('automation.blueprintPHIDs', array());
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorRepositoryEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorRepositoryTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
DiffusionPushCapability::CAPABILITY,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
case DiffusionPushCapability::CAPABILITY:
return $this->getPushPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
return false;
}
/* -( PhabricatorMarkupInterface )----------------------------------------- */
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digestForIndex($this->getMarkupText($field));
return "repo:{$hash}";
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newMarkupEngine(array());
}
public function getMarkupText($field) {
return $this->getDetail('description');
}
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
require_celerity_resource('phabricator-remarkup-css');
return phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
$output);
}
public function shouldUseMarkupCache($field) {
return true;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$phid = $this->getPHID();
$this->openTransaction();
$this->delete();
PhabricatorRepositoryURIIndex::updateRepositoryURIs($phid, array());
$books = id(new DivinerBookQuery())
->setViewer($engine->getViewer())
->withRepositoryPHIDs(array($phid))
->execute();
foreach ($books as $book) {
$engine->destroyObject($book);
}
$atoms = id(new DivinerAtomQuery())
->setViewer($engine->getViewer())
->withRepositoryPHIDs(array($phid))
->execute();
foreach ($atoms as $atom) {
$engine->destroyObject($atom);
}
$lfs_refs = id(new PhabricatorRepositoryGitLFSRefQuery())
->setViewer($engine->getViewer())
->withRepositoryPHIDs(array($phid))
->execute();
foreach ($lfs_refs as $ref) {
$engine->destroyObject($ref);
}
$this->saveTransaction();
}
/* -( PhabricatorDestructibleCodexInterface )------------------------------ */
public function newDestructibleCodex() {
return new PhabricatorRepositoryDestructibleCodex();
}
/* -( PhabricatorSpacesInterface )----------------------------------------- */
public function getSpacePHID() {
return $this->spacePHID;
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The repository name.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('vcs')
->setType('string')
->setDescription(
pht('The VCS this repository uses ("git", "hg" or "svn").')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('callsign')
->setType('string')
->setDescription(pht('The repository callsign, if it has one.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('shortName')
->setType('string')
->setDescription(pht('Unique short name, if the repository has one.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('status')
->setType('string')
->setDescription(pht('Active or inactive status.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('isImporting')
->setType('bool')
->setDescription(
pht(
'True if the repository is importing initial commits.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('almanacServicePHID')
->setType('phid?')
->setDescription(
pht(
'The Almanac Service that hosts this repository, if the '.
'repository is clustered.')),
);
}
public function getFieldValuesForConduit() {
return array(
'name' => $this->getName(),
'vcs' => $this->getVersionControlSystem(),
'callsign' => $this->getCallsign(),
'shortName' => $this->getRepositorySlug(),
'status' => $this->getStatus(),
'isImporting' => (bool)$this->isImporting(),
'almanacServicePHID' => $this->getAlmanacServicePHID(),
);
}
public function getConduitSearchAttachments() {
return array(
id(new DiffusionRepositoryURIsSearchEngineAttachment())
->setAttachmentKey('uris'),
);
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PhabricatorRepositoryFulltextEngine();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new PhabricatorRepositoryFerretEngine();
}
}
diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommit.php b/src/applications/repository/storage/PhabricatorRepositoryCommit.php
index b5fe08b6e..fecb1762b 100644
--- a/src/applications/repository/storage/PhabricatorRepositoryCommit.php
+++ b/src/applications/repository/storage/PhabricatorRepositoryCommit.php
@@ -1,919 +1,897 @@
<?php
final class PhabricatorRepositoryCommit
extends PhabricatorRepositoryDAO
implements
PhabricatorPolicyInterface,
PhabricatorFlaggableInterface,
PhabricatorProjectInterface,
PhabricatorTokenReceiverInterface,
PhabricatorSubscribableInterface,
PhabricatorMentionableInterface,
HarbormasterBuildableInterface,
HarbormasterCircleCIBuildableInterface,
HarbormasterBuildkiteBuildableInterface,
PhabricatorCustomFieldInterface,
PhabricatorApplicationTransactionInterface,
+ PhabricatorTimelineInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface,
PhabricatorConduitResultInterface,
PhabricatorDraftInterface {
protected $repositoryID;
protected $phid;
protected $authorIdentityPHID;
protected $committerIdentityPHID;
protected $commitIdentifier;
protected $epoch;
protected $authorPHID;
protected $auditStatus = DiffusionCommitAuditStatus::NONE;
protected $summary = '';
protected $importStatus = 0;
const IMPORTED_MESSAGE = 1;
const IMPORTED_CHANGE = 2;
const IMPORTED_OWNERS = 4;
const IMPORTED_HERALD = 8;
const IMPORTED_ALL = 15;
const IMPORTED_CLOSEABLE = 1024;
const IMPORTED_UNREACHABLE = 2048;
private $commitData = self::ATTACHABLE;
private $audits = self::ATTACHABLE;
private $repository = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
private $authorIdentity = self::ATTACHABLE;
private $committerIdentity = self::ATTACHABLE;
private $drafts = array();
private $auditAuthorityPHIDs = array();
public function attachRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
public function getRepository($assert_attached = true) {
if ($assert_attached) {
return $this->assertAttached($this->repository);
}
return $this->repository;
}
public function isPartiallyImported($mask) {
return (($mask & $this->getImportStatus()) == $mask);
}
public function isImported() {
return $this->isPartiallyImported(self::IMPORTED_ALL);
}
public function isUnreachable() {
return $this->isPartiallyImported(self::IMPORTED_UNREACHABLE);
}
public function writeImportStatusFlag($flag) {
return $this->adjustImportStatusFlag($flag, true);
}
public function clearImportStatusFlag($flag) {
return $this->adjustImportStatusFlag($flag, false);
}
private function adjustImportStatusFlag($flag, $set) {
$conn_w = $this->establishConnection('w');
$table_name = $this->getTableName();
$id = $this->getID();
if ($set) {
queryfx(
$conn_w,
'UPDATE %T SET importStatus = (importStatus | %d) WHERE id = %d',
$table_name,
$flag,
$id);
$this->setImportStatus($this->getImportStatus() | $flag);
} else {
queryfx(
$conn_w,
'UPDATE %T SET importStatus = (importStatus & ~%d) WHERE id = %d',
$table_name,
$flag,
$id);
$this->setImportStatus($this->getImportStatus() & ~$flag);
}
return $this;
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_COLUMN_SCHEMA => array(
'commitIdentifier' => 'text40',
'authorPHID' => 'phid?',
'authorIdentityPHID' => 'phid?',
'committerIdentityPHID' => 'phid?',
'auditStatus' => 'text32',
'summary' => 'text255',
'importStatus' => 'uint32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'repositoryID' => array(
'columns' => array('repositoryID', 'importStatus'),
),
'authorPHID' => array(
'columns' => array('authorPHID', 'auditStatus', 'epoch'),
),
'repositoryID_2' => array(
'columns' => array('repositoryID', 'epoch'),
),
'key_commit_identity' => array(
'columns' => array('commitIdentifier', 'repositoryID'),
'unique' => true,
),
'key_epoch' => array(
'columns' => array('epoch'),
),
'key_author' => array(
'columns' => array('authorPHID', 'epoch'),
),
),
self::CONFIG_NO_MUTATE => array(
'importStatus',
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorRepositoryCommitPHIDType::TYPECONST);
}
public function loadCommitData() {
if (!$this->getID()) {
return null;
}
return id(new PhabricatorRepositoryCommitData())->loadOneWhere(
'commitID = %d',
$this->getID());
}
public function attachCommitData(
PhabricatorRepositoryCommitData $data = null) {
$this->commitData = $data;
return $this;
}
public function getCommitData() {
return $this->assertAttached($this->commitData);
}
public function attachAudits(array $audits) {
assert_instances_of($audits, 'PhabricatorRepositoryAuditRequest');
$this->audits = $audits;
return $this;
}
public function getAudits() {
return $this->assertAttached($this->audits);
}
public function hasAttachedAudits() {
return ($this->audits !== self::ATTACHABLE);
}
public function attachIdentities(
PhabricatorRepositoryIdentity $author = null,
PhabricatorRepositoryIdentity $committer = null) {
$this->authorIdentity = $author;
$this->committerIdentity = $committer;
return $this;
}
public function getAuthorIdentity() {
return $this->assertAttached($this->authorIdentity);
}
public function getCommitterIdentity() {
return $this->assertAttached($this->committerIdentity);
}
public function attachAuditAuthority(
PhabricatorUser $user,
array $authority) {
$user_phid = $user->getPHID();
if (!$user->getPHID()) {
throw new Exception(
pht('You can not attach audit authority for a user with no PHID.'));
}
$this->auditAuthorityPHIDs[$user_phid] = $authority;
return $this;
}
public function hasAuditAuthority(
PhabricatorUser $user,
PhabricatorRepositoryAuditRequest $audit) {
$user_phid = $user->getPHID();
if (!$user_phid) {
return false;
}
$map = $this->assertAttachedKey($this->auditAuthorityPHIDs, $user_phid);
return isset($map[$audit->getAuditorPHID()]);
}
public function writeOwnersEdges(array $package_phids) {
$src_phid = $this->getPHID();
$edge_type = DiffusionCommitHasPackageEdgeType::EDGECONST;
$editor = new PhabricatorEdgeEditor();
$dst_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$src_phid,
$edge_type);
foreach ($dst_phids as $dst_phid) {
$editor->removeEdge($src_phid, $edge_type, $dst_phid);
}
foreach ($package_phids as $package_phid) {
$editor->addEdge($src_phid, $edge_type, $package_phid);
}
$editor->save();
return $this;
}
public function getAuditorPHIDsForEdit() {
$audits = $this->getAudits();
return mpull($audits, 'getAuditorPHID');
}
public function delete() {
$data = $this->loadCommitData();
$audits = id(new PhabricatorRepositoryAuditRequest())
->loadAllWhere('commitPHID = %s', $this->getPHID());
$this->openTransaction();
if ($data) {
$data->delete();
}
foreach ($audits as $audit) {
$audit->delete();
}
$result = parent::delete();
$this->saveTransaction();
return $result;
}
public function getDateCreated() {
// This is primarily to make analysis of commits with the Fact engine work.
return $this->getEpoch();
}
public function getURI() {
return '/'.$this->getMonogram();
}
/**
* Synchronize a commit's overall audit status with the individual audit
* triggers.
*/
public function updateAuditStatus(array $requests) {
assert_instances_of($requests, 'PhabricatorRepositoryAuditRequest');
$any_concern = false;
$any_accept = false;
$any_need = false;
foreach ($requests as $request) {
switch ($request->getAuditStatus()) {
case PhabricatorAuditStatusConstants::AUDIT_REQUIRED:
case PhabricatorAuditStatusConstants::AUDIT_REQUESTED:
$any_need = true;
break;
case PhabricatorAuditStatusConstants::ACCEPTED:
$any_accept = true;
break;
case PhabricatorAuditStatusConstants::CONCERNED:
$any_concern = true;
break;
}
}
if ($any_concern) {
if ($this->isAuditStatusNeedsVerification()) {
// If the change is in "Needs Verification", we keep it there as
// long as any auditors still have concerns.
$status = DiffusionCommitAuditStatus::NEEDS_VERIFICATION;
} else {
$status = DiffusionCommitAuditStatus::CONCERN_RAISED;
}
} else if ($any_accept) {
if ($any_need) {
$status = DiffusionCommitAuditStatus::PARTIALLY_AUDITED;
} else {
$status = DiffusionCommitAuditStatus::AUDITED;
}
} else if ($any_need) {
$status = DiffusionCommitAuditStatus::NEEDS_AUDIT;
} else {
$status = DiffusionCommitAuditStatus::NONE;
}
return $this->setAuditStatus($status);
}
public function getMonogram() {
$repository = $this->getRepository();
$callsign = $repository->getCallsign();
$identifier = $this->getCommitIdentifier();
if ($callsign !== null) {
return "r{$callsign}{$identifier}";
} else {
$id = $repository->getID();
return "R{$id}:{$identifier}";
}
}
public function getDisplayName() {
$repository = $this->getRepository();
$identifier = $this->getCommitIdentifier();
return $repository->formatCommitName($identifier);
}
/**
* Return a local display name for use in the context of the containing
* repository.
*
* In Git and Mercurial, this returns only a short hash, like "abcdef012345".
* See @{method:getDisplayName} for a short name that always includes
* repository context.
*
* @return string Short human-readable name for use inside a repository.
*/
public function getLocalName() {
$repository = $this->getRepository();
$identifier = $this->getCommitIdentifier();
return $repository->formatCommitName($identifier, $local = true);
}
/**
* Make a strong effort to find a way to render this commit's committer.
* This currently attempts to use @{PhabricatorRepositoryIdentity}, and
* falls back to examining the commit detail information. After we force
* the migration to using identities, update this method to remove the
* fallback. See T12164 for details.
*/
public function renderAnyCommitter(PhabricatorUser $viewer, $handles) {
$committer = $this->renderCommitter($viewer, $handles);
if ($committer) {
return $committer;
}
return $this->renderAuthor($viewer, $handles);
}
public function renderCommitter(PhabricatorUser $viewer, $handles) {
$committer_phid = $this->getCommitterDisplayPHID();
if ($committer_phid) {
return $handles[$committer_phid]->renderLink();
}
$data = $this->getCommitData();
$committer_name = $data->getCommitDetail('committer');
if (strlen($committer_name)) {
return DiffusionView::renderName($committer_name);
}
return null;
}
public function renderAuthor(PhabricatorUser $viewer, $handles) {
$author_phid = $this->getAuthorDisplayPHID();
if ($author_phid) {
return $handles[$author_phid]->renderLink();
}
$data = $this->getCommitData();
$author_name = $data->getAuthorName();
if (strlen($author_name)) {
return DiffusionView::renderName($author_name);
}
return null;
}
public function loadIdentities(PhabricatorUser $viewer) {
if ($this->authorIdentity !== self::ATTACHABLE) {
return $this;
}
$commit = id(new DiffusionCommitQuery())
->setViewer($viewer)
->withIDs(array($this->getID()))
->needIdentities(true)
->executeOne();
$author_identity = $commit->getAuthorIdentity();
$committer_identity = $commit->getCommitterIdentity();
return $this->attachIdentities($author_identity, $committer_identity);
}
public function hasCommitterIdentity() {
return ($this->getCommitterIdentity() !== null);
}
public function hasAuthorIdentity() {
return ($this->getAuthorIdentity() !== null);
}
public function getCommitterDisplayPHID() {
if ($this->hasCommitterIdentity()) {
return $this->getCommitterIdentity()->getIdentityDisplayPHID();
}
$data = $this->getCommitData();
return $data->getCommitDetail('committerPHID');
}
public function getAuthorDisplayPHID() {
if ($this->hasAuthorIdentity()) {
return $this->getAuthorIdentity()->getIdentityDisplayPHID();
}
$data = $this->getCommitData();
return $data->getCommitDetail('authorPHID');
}
public function getAuditStatusObject() {
$status = $this->getAuditStatus();
return DiffusionCommitAuditStatus::newForStatus($status);
}
public function isAuditStatusNoAudit() {
return $this->getAuditStatusObject()->isNoAudit();
}
public function isAuditStatusNeedsAudit() {
return $this->getAuditStatusObject()->isNeedsAudit();
}
public function isAuditStatusConcernRaised() {
return $this->getAuditStatusObject()->isConcernRaised();
}
public function isAuditStatusNeedsVerification() {
return $this->getAuditStatusObject()->isNeedsVerification();
}
public function isAuditStatusPartiallyAudited() {
return $this->getAuditStatusObject()->isPartiallyAudited();
}
public function isAuditStatusAudited() {
return $this->getAuditStatusObject()->isAudited();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getRepository()->getPolicy($capability);
case PhabricatorPolicyCapability::CAN_EDIT:
return PhabricatorPolicies::POLICY_USER;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getRepository()->hasAutomaticCapability($capability, $viewer);
}
public function describeAutomaticCapability($capability) {
return pht(
'Commits inherit the policies of the repository they belong to.');
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getAuthorPHID(),
);
}
/* -( Stuff for serialization )---------------------------------------------- */
/**
* NOTE: this is not a complete serialization; only the 'protected' fields are
* involved. This is due to ease of (ab)using the Lisk abstraction to get this
* done, as well as complexity of the other fields.
*/
public function toDictionary() {
return array(
'repositoryID' => $this->getRepositoryID(),
'phid' => $this->getPHID(),
'commitIdentifier' => $this->getCommitIdentifier(),
'epoch' => $this->getEpoch(),
'authorPHID' => $this->getAuthorPHID(),
'auditStatus' => $this->getAuditStatus(),
'summary' => $this->getSummary(),
'importStatus' => $this->getImportStatus(),
);
}
public static function newFromDictionary(array $dict) {
return id(new PhabricatorRepositoryCommit())
->loadFromArray($dict);
}
/* -( HarbormasterBuildableInterface )------------------------------------- */
public function getHarbormasterBuildableDisplayPHID() {
return $this->getHarbormasterBuildablePHID();
}
public function getHarbormasterBuildablePHID() {
return $this->getPHID();
}
public function getHarbormasterContainerPHID() {
return $this->getRepository()->getPHID();
}
public function getBuildVariables() {
$results = array();
$results['buildable.commit'] = $this->getCommitIdentifier();
$repo = $this->getRepository();
$results['repository.callsign'] = $repo->getCallsign();
$results['repository.phid'] = $repo->getPHID();
$results['repository.vcs'] = $repo->getVersionControlSystem();
$results['repository.uri'] = $repo->getPublicCloneURI();
return $results;
}
public function getAvailableBuildVariables() {
return array(
'buildable.commit' => pht('The commit identifier, if applicable.'),
'repository.callsign' =>
pht('The callsign of the repository in Phabricator.'),
'repository.phid' =>
pht('The PHID of the repository in Phabricator.'),
'repository.vcs' =>
pht('The version control system, either "svn", "hg" or "git".'),
'repository.uri' =>
pht('The URI to clone or checkout the repository from.'),
);
}
public function newBuildableEngine() {
return new DiffusionBuildableEngine();
}
/* -( HarbormasterCircleCIBuildableInterface )----------------------------- */
public function getCircleCIGitHubRepositoryURI() {
$repository = $this->getRepository();
$commit_phid = $this->getPHID();
$repository_phid = $repository->getPHID();
if ($repository->isHosted()) {
throw new Exception(
pht(
'This commit ("%s") is associated with a hosted repository '.
'("%s"). Repositories must be imported from GitHub to be built '.
'with CircleCI.',
$commit_phid,
$repository_phid));
}
$remote_uri = $repository->getRemoteURI();
$path = HarbormasterCircleCIBuildStepImplementation::getGitHubPath(
$remote_uri);
if (!$path) {
throw new Exception(
pht(
'This commit ("%s") is associated with a repository ("%s") that '.
'with a remote URI ("%s") that does not appear to be hosted on '.
'GitHub. Repositories must be hosted on GitHub to be built with '.
'CircleCI.',
$commit_phid,
$repository_phid,
$remote_uri));
}
return $remote_uri;
}
public function getCircleCIBuildIdentifierType() {
return 'revision';
}
public function getCircleCIBuildIdentifier() {
return $this->getCommitIdentifier();
}
/* -( HarbormasterBuildkiteBuildableInterface )---------------------------- */
public function getBuildkiteBranch() {
$viewer = PhabricatorUser::getOmnipotentUser();
$repository = $this->getRepository();
$branches = DiffusionQuery::callConduitWithDiffusionRequest(
$viewer,
DiffusionRequest::newFromDictionary(
array(
'repository' => $repository,
'user' => $viewer,
)),
'diffusion.branchquery',
array(
'contains' => $this->getCommitIdentifier(),
'repository' => $repository->getPHID(),
));
if (!$branches) {
throw new Exception(
pht(
'Commit "%s" is not an ancestor of any branch head, so it can not '.
'be built with Buildkite.',
$this->getCommitIdentifier()));
}
$branch = head($branches);
return 'refs/heads/'.$branch['shortName'];
}
public function getBuildkiteCommit() {
return $this->getCommitIdentifier();
}
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
public function getCustomFieldSpecificationForRole($role) {
return PhabricatorEnv::getEnvConfig('diffusion.fields');
}
public function getCustomFieldBaseClass() {
return 'PhabricatorCommitCustomField';
}
public function getCustomFields() {
return $this->assertAttached($this->customFields);
}
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
$this->customFields = $fields;
return $this;
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
// TODO: This should also list auditors, but handling that is a bit messy
// right now because we are not guaranteed to have the data. (It should not
// include resigned auditors.)
return ($phid == $this->getAuthorPHID());
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorAuditEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorAuditTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- $xactions = $timeline->getTransactions();
-
- $path_ids = array();
- foreach ($xactions as $xaction) {
- if ($xaction->hasComment()) {
- $path_id = $xaction->getComment()->getPathID();
- if ($path_id) {
- $path_ids[] = $path_id;
- }
- }
- }
-
- $path_map = array();
- if ($path_ids) {
- $path_map = id(new DiffusionPathQuery())
- ->withPathIDs($path_ids)
- ->execute();
- $path_map = ipull($path_map, 'path', 'id');
- }
-
- return $timeline->setPathMap($path_map);
- }
-
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new DiffusionCommitFulltextEngine();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new DiffusionCommitFerretEngine();
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('identifier')
->setType('string')
->setDescription(pht('The commit identifier.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('repositoryPHID')
->setType('phid')
->setDescription(pht('The repository this commit belongs to.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('author')
->setType('map<string, wild>')
->setDescription(pht('Information about the commit author.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('committer')
->setType('map<string, wild>')
->setDescription(pht('Information about the committer.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('isImported')
->setType('bool')
->setDescription(pht('True if the commit is fully imported.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('isUnreachable')
->setType('bool')
->setDescription(
pht(
'True if the commit is not the ancestor of any tag, branch, or '.
'ref.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('auditStatus')
->setType('map<string, wild>')
->setDescription(pht('Information about the current audit status.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('message')
->setType('string')
->setDescription(pht('The commit message.')),
);
}
public function getFieldValuesForConduit() {
$data = $this->getCommitData();
$author_identity = $this->getAuthorIdentity();
if ($author_identity) {
$author_name = $author_identity->getIdentityDisplayName();
$author_email = $author_identity->getIdentityEmailAddress();
$author_raw = $author_identity->getIdentityName();
$author_identity_phid = $author_identity->getPHID();
$author_user_phid = $author_identity->getCurrentEffectiveUserPHID();
} else {
$author_name = null;
$author_email = null;
$author_raw = null;
$author_identity_phid = null;
$author_user_phid = null;
}
$committer_identity = $this->getCommitterIdentity();
if ($committer_identity) {
$committer_name = $committer_identity->getIdentityDisplayName();
$committer_email = $committer_identity->getIdentityEmailAddress();
$committer_raw = $committer_identity->getIdentityName();
$committer_identity_phid = $committer_identity->getPHID();
$committer_user_phid = $committer_identity->getCurrentEffectiveUserPHID();
} else {
$committer_name = null;
$committer_email = null;
$committer_raw = null;
$committer_identity_phid = null;
$committer_user_phid = null;
}
$author_epoch = $data->getCommitDetail('authorEpoch');
if ($author_epoch) {
$author_epoch = (int)$author_epoch;
} else {
$author_epoch = null;
}
$audit_status = $this->getAuditStatusObject();
return array(
'identifier' => $this->getCommitIdentifier(),
'repositoryPHID' => $this->getRepository()->getPHID(),
'author' => array(
'name' => $author_name,
'email' => $author_email,
'raw' => $author_raw,
'epoch' => $author_epoch,
'identityPHID' => $author_identity_phid,
'userPHID' => $author_user_phid,
),
'committer' => array(
'name' => $committer_name,
'email' => $committer_email,
'raw' => $committer_raw,
'epoch' => (int)$this->getEpoch(),
'identityPHID' => $committer_identity_phid,
'userPHID' => $committer_user_phid,
),
'isUnreachable' => (bool)$this->isUnreachable(),
'isImported' => (bool)$this->isImported(),
'auditStatus' => array(
'value' => $audit_status->getKey(),
'name' => $audit_status->getName(),
'closed' => (bool)$audit_status->getIsClosed(),
'color.ansi' => $audit_status->getAnsiColor(),
),
'message' => $data->getCommitMessage(),
);
}
public function getConduitSearchAttachments() {
return array();
}
/* -( PhabricatorDraftInterface )------------------------------------------ */
public function newDraftEngine() {
return new DiffusionCommitDraftEngine();
}
public function getHasDraft(PhabricatorUser $viewer) {
return $this->assertAttachedKey($this->drafts, $viewer->getCacheFragment());
}
public function attachHasDraft(PhabricatorUser $viewer, $has_draft) {
$this->drafts[$viewer->getCacheFragment()] = $has_draft;
return $this;
}
+
+/* -( PhabricatorTimelineInterface )--------------------------------------- */
+
+
+ public function newTimelineEngine() {
+ return new DiffusionCommitTimelineEngine();
+ }
+
}
diff --git a/src/applications/repository/storage/PhabricatorRepositoryIdentity.php b/src/applications/repository/storage/PhabricatorRepositoryIdentity.php
index 416d1a3cd..76c6aed9e 100644
--- a/src/applications/repository/storage/PhabricatorRepositoryIdentity.php
+++ b/src/applications/repository/storage/PhabricatorRepositoryIdentity.php
@@ -1,152 +1,141 @@
<?php
final class PhabricatorRepositoryIdentity
extends PhabricatorRepositoryDAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface {
protected $authorPHID;
protected $identityNameHash;
protected $identityNameRaw;
protected $identityNameEncoding;
protected $automaticGuessedUserPHID;
protected $manuallySetUserPHID;
protected $currentEffectiveUserPHID;
private $effectiveUser = self::ATTACHABLE;
public function attachEffectiveUser(PhabricatorUser $user) {
$this->effectiveUser = $user;
return $this;
}
public function getEffectiveUser() {
return $this->assertAttached($this->effectiveUser);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_BINARY => array(
'identityNameRaw' => true,
),
self::CONFIG_COLUMN_SCHEMA => array(
'identityNameHash' => 'bytes12',
'identityNameEncoding' => 'text16?',
'automaticGuessedUserPHID' => 'phid?',
'manuallySetUserPHID' => 'phid?',
'currentEffectiveUserPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_identity' => array(
'columns' => array('identityNameHash'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function getPHIDType() {
return PhabricatorRepositoryIdentityPHIDType::TYPECONST;
}
public function setIdentityName($name_raw) {
$this->setIdentityNameRaw($name_raw);
$this->setIdentityNameHash(PhabricatorHash::digestForIndex($name_raw));
$this->setIdentityNameEncoding($this->detectEncodingForStorage($name_raw));
return $this;
}
public function getIdentityName() {
return $this->getUTF8StringFromStorage(
$this->getIdentityNameRaw(),
$this->getIdentityNameEncoding());
}
public function getIdentityEmailAddress() {
$address = new PhutilEmailAddress($this->getIdentityName());
return $address->getAddress();
}
public function getIdentityDisplayName() {
$address = new PhutilEmailAddress($this->getIdentityName());
return $address->getDisplayName();
}
public function getIdentityShortName() {
// TODO
return $this->getIdentityName();
}
public function getURI() {
return '/diffusion/identity/view/'.$this->getID().'/';
}
public function hasEffectiveUser() {
return ($this->currentEffectiveUserPHID != null);
}
public function getIdentityDisplayPHID() {
if ($this->hasEffectiveUser()) {
return $this->getCurrentEffectiveUserPHID();
} else {
return $this->getPHID();
}
}
public function save() {
if ($this->manuallySetUserPHID) {
$this->currentEffectiveUserPHID = $this->manuallySetUserPHID;
} else {
$this->currentEffectiveUserPHID = $this->automaticGuessedUserPHID;
}
return parent::save();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return PhabricatorPolicies::getMostOpenPolicy();
}
public function hasAutomaticCapability(
$capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new DiffusionRepositoryIdentityEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorRepositoryIdentityTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
}
diff --git a/src/applications/repository/storage/PhabricatorRepositoryURI.php b/src/applications/repository/storage/PhabricatorRepositoryURI.php
index c8d560705..8b1de62dd 100644
--- a/src/applications/repository/storage/PhabricatorRepositoryURI.php
+++ b/src/applications/repository/storage/PhabricatorRepositoryURI.php
@@ -1,748 +1,738 @@
<?php
final class PhabricatorRepositoryURI
extends PhabricatorRepositoryDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorExtendedPolicyInterface,
PhabricatorConduitResultInterface {
protected $repositoryPHID;
protected $uri;
protected $builtinProtocol;
protected $builtinIdentifier;
protected $credentialPHID;
protected $ioType;
protected $displayType;
protected $isDisabled;
private $repository = self::ATTACHABLE;
const BUILTIN_PROTOCOL_SSH = 'ssh';
const BUILTIN_PROTOCOL_HTTP = 'http';
const BUILTIN_PROTOCOL_HTTPS = 'https';
const BUILTIN_IDENTIFIER_ID = 'id';
const BUILTIN_IDENTIFIER_SHORTNAME = 'shortname';
const BUILTIN_IDENTIFIER_CALLSIGN = 'callsign';
const DISPLAY_DEFAULT = 'default';
const DISPLAY_NEVER = 'never';
const DISPLAY_ALWAYS = 'always';
const IO_DEFAULT = 'default';
const IO_OBSERVE = 'observe';
const IO_MIRROR = 'mirror';
const IO_NONE = 'none';
const IO_READ = 'read';
const IO_READWRITE = 'readwrite';
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'uri' => 'text255',
'builtinProtocol' => 'text32?',
'builtinIdentifier' => 'text32?',
'credentialPHID' => 'phid?',
'ioType' => 'text32',
'displayType' => 'text32',
'isDisabled' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_builtin' => array(
'columns' => array(
'repositoryPHID',
'builtinProtocol',
'builtinIdentifier',
),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public static function initializeNewURI() {
return id(new self())
->setIoType(self::IO_DEFAULT)
->setDisplayType(self::DISPLAY_DEFAULT)
->setIsDisabled(0);
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorRepositoryURIPHIDType::TYPECONST);
}
public function attachRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
public function getRepository() {
return $this->assertAttached($this->repository);
}
public function getRepositoryURIBuiltinKey() {
if (!$this->getBuiltinProtocol()) {
return null;
}
$parts = array(
$this->getBuiltinProtocol(),
$this->getBuiltinIdentifier(),
);
return implode('.', $parts);
}
public function isBuiltin() {
return (bool)$this->getBuiltinProtocol();
}
public function getEffectiveDisplayType() {
$display = $this->getDisplayType();
if ($display != self::DISPLAY_DEFAULT) {
return $display;
}
return $this->getDefaultDisplayType();
}
public function getDefaultDisplayType() {
switch ($this->getEffectiveIOType()) {
case self::IO_MIRROR:
case self::IO_OBSERVE:
case self::IO_NONE:
return self::DISPLAY_NEVER;
case self::IO_READ:
case self::IO_READWRITE:
// By default, only show the "best" version of the builtin URI, not the
// other redundant versions.
$repository = $this->getRepository();
$other_uris = $repository->getURIs();
$identifier_value = array(
self::BUILTIN_IDENTIFIER_SHORTNAME => 3,
self::BUILTIN_IDENTIFIER_CALLSIGN => 2,
self::BUILTIN_IDENTIFIER_ID => 1,
);
$have_identifiers = array();
foreach ($other_uris as $other_uri) {
if ($other_uri->getIsDisabled()) {
continue;
}
$identifier = $other_uri->getBuiltinIdentifier();
if (!$identifier) {
continue;
}
$have_identifiers[$identifier] = $identifier_value[$identifier];
}
$best_identifier = max($have_identifiers);
$this_identifier = $identifier_value[$this->getBuiltinIdentifier()];
if ($this_identifier < $best_identifier) {
return self::DISPLAY_NEVER;
}
return self::DISPLAY_ALWAYS;
}
return self::DISPLAY_NEVER;
}
public function getEffectiveIOType() {
$io = $this->getIoType();
if ($io != self::IO_DEFAULT) {
return $io;
}
return $this->getDefaultIOType();
}
public function getDefaultIOType() {
if ($this->isBuiltin()) {
$repository = $this->getRepository();
$other_uris = $repository->getURIs();
$any_observe = false;
foreach ($other_uris as $other_uri) {
if ($other_uri->getIoType() == self::IO_OBSERVE) {
$any_observe = true;
break;
}
}
if ($any_observe) {
return self::IO_READ;
} else {
return self::IO_READWRITE;
}
}
return self::IO_NONE;
}
public function getNormalizedURI() {
$vcs = $this->getRepository()->getVersionControlSystem();
$map = array(
PhabricatorRepositoryType::REPOSITORY_TYPE_GIT =>
PhabricatorRepositoryURINormalizer::TYPE_GIT,
PhabricatorRepositoryType::REPOSITORY_TYPE_SVN =>
PhabricatorRepositoryURINormalizer::TYPE_SVN,
PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL =>
PhabricatorRepositoryURINormalizer::TYPE_MERCURIAL,
);
$type = $map[$vcs];
$display = (string)$this->getDisplayURI();
$normal_uri = new PhabricatorRepositoryURINormalizer($type, $display);
return $normal_uri->getNormalizedURI();
}
public function getDisplayURI() {
return $this->getURIObject();
}
public function getEffectiveURI() {
return $this->getURIObject();
}
public function getURIEnvelope() {
$uri = $this->getEffectiveURI();
$command_engine = $this->newCommandEngine();
$is_http = $command_engine->isAnyHTTPProtocol();
// For SVN, we use `--username` and `--password` flags separately in the
// CommandEngine, so we don't need to add any credentials here.
$is_svn = $this->getRepository()->isSVN();
$credential_phid = $this->getCredentialPHID();
if ($is_http && !$is_svn && $credential_phid) {
$key = PassphrasePasswordKey::loadFromPHID(
$credential_phid,
PhabricatorUser::getOmnipotentUser());
$uri->setUser($key->getUsernameEnvelope()->openEnvelope());
$uri->setPass($key->getPasswordEnvelope()->openEnvelope());
}
return new PhutilOpaqueEnvelope((string)$uri);
}
private function getURIObject() {
// Users can provide Git/SCP-style URIs in the form "user@host:path".
// In the general case, these are not equivalent to any "ssh://..." form
// because the path is relative.
if ($this->isBuiltin()) {
$builtin_protocol = $this->getForcedProtocol();
$builtin_domain = $this->getForcedHost();
$raw_uri = "{$builtin_protocol}://{$builtin_domain}";
} else {
$raw_uri = $this->getURI();
}
$port = $this->getForcedPort();
$default_ports = array(
'ssh' => 22,
'http' => 80,
'https' => 443,
);
$uri = new PhutilURI($raw_uri);
// Make sure to remove any password from the URI before we do anything
// with it; this should always be provided by the associated credential.
$uri->setPass(null);
$protocol = $this->getForcedProtocol();
if ($protocol) {
$uri->setProtocol($protocol);
}
if ($port) {
$uri->setPort($port);
}
// Remove any explicitly set default ports.
$uri_port = $uri->getPort();
$uri_protocol = $uri->getProtocol();
$uri_default = idx($default_ports, $uri_protocol);
if ($uri_default && ($uri_default == $uri_port)) {
$uri->setPort(null);
}
$user = $this->getForcedUser();
if ($user) {
$uri->setUser($user);
}
$host = $this->getForcedHost();
if ($host) {
$uri->setDomain($host);
}
$path = $this->getForcedPath();
if ($path) {
$uri->setPath($path);
}
return $uri;
}
private function getForcedProtocol() {
$repository = $this->getRepository();
switch ($this->getBuiltinProtocol()) {
case self::BUILTIN_PROTOCOL_SSH:
if ($repository->isSVN()) {
return 'svn+ssh';
} else {
return 'ssh';
}
case self::BUILTIN_PROTOCOL_HTTP:
return 'http';
case self::BUILTIN_PROTOCOL_HTTPS:
return 'https';
default:
return null;
}
}
private function getForcedUser() {
switch ($this->getBuiltinProtocol()) {
case self::BUILTIN_PROTOCOL_SSH:
return AlmanacKeys::getClusterSSHUser();
default:
return null;
}
}
private function getForcedHost() {
$phabricator_uri = PhabricatorEnv::getURI('/');
$phabricator_uri = new PhutilURI($phabricator_uri);
$phabricator_host = $phabricator_uri->getDomain();
switch ($this->getBuiltinProtocol()) {
case self::BUILTIN_PROTOCOL_SSH:
$ssh_host = PhabricatorEnv::getEnvConfig('diffusion.ssh-host');
if ($ssh_host !== null) {
return $ssh_host;
}
return $phabricator_host;
case self::BUILTIN_PROTOCOL_HTTP:
case self::BUILTIN_PROTOCOL_HTTPS:
return $phabricator_host;
default:
return null;
}
}
private function getForcedPort() {
$protocol = $this->getBuiltinProtocol();
if ($protocol == self::BUILTIN_PROTOCOL_SSH) {
return PhabricatorEnv::getEnvConfig('diffusion.ssh-port');
}
// If Phabricator is running on a nonstandard port, use that as the default
// port for URIs with the same protocol.
$is_http = ($protocol == self::BUILTIN_PROTOCOL_HTTP);
$is_https = ($protocol == self::BUILTIN_PROTOCOL_HTTPS);
if ($is_http || $is_https) {
$uri = PhabricatorEnv::getURI('/');
$uri = new PhutilURI($uri);
$port = $uri->getPort();
if (!$port) {
return null;
}
$uri_protocol = $uri->getProtocol();
$use_port =
($is_http && ($uri_protocol == 'http')) ||
($is_https && ($uri_protocol == 'https'));
if (!$use_port) {
return null;
}
return $port;
}
return null;
}
private function getForcedPath() {
if (!$this->isBuiltin()) {
return null;
}
$repository = $this->getRepository();
$id = $repository->getID();
$callsign = $repository->getCallsign();
$short_name = $repository->getRepositorySlug();
$clone_name = $repository->getCloneName();
if ($repository->isGit()) {
$suffix = '.git';
} else if ($repository->isHg()) {
$suffix = '/';
} else {
$suffix = '';
$clone_name = '';
}
switch ($this->getBuiltinIdentifier()) {
case self::BUILTIN_IDENTIFIER_ID:
return "/diffusion/{$id}/{$clone_name}{$suffix}";
case self::BUILTIN_IDENTIFIER_SHORTNAME:
return "/source/{$short_name}{$suffix}";
case self::BUILTIN_IDENTIFIER_CALLSIGN:
return "/diffusion/{$callsign}/{$clone_name}{$suffix}";
default:
return null;
}
}
public function getViewURI() {
$id = $this->getID();
return $this->getRepository()->getPathURI("uri/view/{$id}/");
}
public function getEditURI() {
$id = $this->getID();
return $this->getRepository()->getPathURI("uri/edit/{$id}/");
}
public function getAvailableIOTypeOptions() {
$options = array(
self::IO_DEFAULT,
self::IO_NONE,
);
if ($this->isBuiltin()) {
$options[] = self::IO_READ;
$options[] = self::IO_READWRITE;
} else {
$options[] = self::IO_OBSERVE;
$options[] = self::IO_MIRROR;
}
$map = array();
$io_map = self::getIOTypeMap();
foreach ($options as $option) {
$spec = idx($io_map, $option, array());
$label = idx($spec, 'label', $option);
$short = idx($spec, 'short');
$name = pht('%s: %s', $label, $short);
$map[$option] = $name;
}
return $map;
}
public function getAvailableDisplayTypeOptions() {
$options = array(
self::DISPLAY_DEFAULT,
self::DISPLAY_ALWAYS,
self::DISPLAY_NEVER,
);
$map = array();
$display_map = self::getDisplayTypeMap();
foreach ($options as $option) {
$spec = idx($display_map, $option, array());
$label = idx($spec, 'label', $option);
$short = idx($spec, 'short');
$name = pht('%s: %s', $label, $short);
$map[$option] = $name;
}
return $map;
}
public static function getIOTypeMap() {
return array(
self::IO_DEFAULT => array(
'label' => pht('Default'),
'short' => pht('Use default behavior.'),
),
self::IO_OBSERVE => array(
'icon' => 'fa-download',
'color' => 'green',
'label' => pht('Observe'),
'note' => pht(
'Phabricator will observe changes to this URI and copy them.'),
'short' => pht('Copy from a remote.'),
),
self::IO_MIRROR => array(
'icon' => 'fa-upload',
'color' => 'green',
'label' => pht('Mirror'),
'note' => pht(
'Phabricator will push a copy of any changes to this URI.'),
'short' => pht('Push a copy to a remote.'),
),
self::IO_NONE => array(
'icon' => 'fa-times',
'color' => 'grey',
'label' => pht('No I/O'),
'note' => pht(
'Phabricator will not push or pull any changes to this URI.'),
'short' => pht('Do not perform any I/O.'),
),
self::IO_READ => array(
'icon' => 'fa-folder',
'color' => 'blue',
'label' => pht('Read Only'),
'note' => pht(
'Phabricator will serve a read-only copy of the repository from '.
'this URI.'),
'short' => pht('Serve repository in read-only mode.'),
),
self::IO_READWRITE => array(
'icon' => 'fa-folder-open',
'color' => 'blue',
'label' => pht('Read/Write'),
'note' => pht(
'Phabricator will serve a read/write copy of the repository from '.
'this URI.'),
'short' => pht('Serve repository in read/write mode.'),
),
);
}
public static function getDisplayTypeMap() {
return array(
self::DISPLAY_DEFAULT => array(
'label' => pht('Default'),
'short' => pht('Use default behavior.'),
),
self::DISPLAY_ALWAYS => array(
'icon' => 'fa-eye',
'color' => 'green',
'label' => pht('Visible'),
'note' => pht('This URI will be shown to users as a clone URI.'),
'short' => pht('Show as a clone URI.'),
),
self::DISPLAY_NEVER => array(
'icon' => 'fa-eye-slash',
'color' => 'grey',
'label' => pht('Hidden'),
'note' => pht(
'This URI will be hidden from users.'),
'short' => pht('Do not show as a clone URI.'),
),
);
}
public function newCommandEngine() {
$repository = $this->getRepository();
return DiffusionCommandEngine::newCommandEngine($repository)
->setCredentialPHID($this->getCredentialPHID())
->setURI($this->getEffectiveURI());
}
public function getURIScore() {
$score = 0;
$io_points = array(
self::IO_READWRITE => 200,
self::IO_READ => 100,
);
$score += idx($io_points, $this->getEffectiveIOType(), 0);
$protocol_points = array(
self::BUILTIN_PROTOCOL_SSH => 30,
self::BUILTIN_PROTOCOL_HTTPS => 20,
self::BUILTIN_PROTOCOL_HTTP => 10,
);
$score += idx($protocol_points, $this->getBuiltinProtocol(), 0);
$identifier_points = array(
self::BUILTIN_IDENTIFIER_SHORTNAME => 3,
self::BUILTIN_IDENTIFIER_CALLSIGN => 2,
self::BUILTIN_IDENTIFIER_ID => 1,
);
$score += idx($identifier_points, $this->getBuiltinIdentifier(), 0);
return $score;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new DiffusionURIEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorRepositoryURITransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
- return $timeline;
- }
-
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
case PhabricatorPolicyCapability::CAN_EDIT:
return PhabricatorPolicies::getMostOpenPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
$extended = array();
switch ($capability) {
case PhabricatorPolicyCapability::CAN_EDIT:
// To edit a repository URI, you must be able to edit the
// corresponding repository.
$extended[] = array($this->getRepository(), $capability);
break;
}
return $extended;
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('repositoryPHID')
->setType('phid')
->setDescription(pht('The associated repository PHID.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('uri')
->setType('map<string, string>')
->setDescription(pht('The raw and effective URI.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('io')
->setType('map<string, const>')
->setDescription(
pht('The raw, default, and effective I/O Type settings.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('display')
->setType('map<string, const>')
->setDescription(
pht('The raw, default, and effective Display Type settings.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('credentialPHID')
->setType('phid?')
->setDescription(
pht('The associated credential PHID, if one exists.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('disabled')
->setType('bool')
->setDescription(pht('True if the URI is disabled.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('builtin')
->setType('map<string, string>')
->setDescription(
pht('Information about builtin URIs.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('dateCreated')
->setType('int')
->setDescription(
pht('Epoch timestamp when the object was created.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('dateModified')
->setType('int')
->setDescription(
pht('Epoch timestamp when the object was last updated.')),
);
}
public function getFieldValuesForConduit() {
return array(
'repositoryPHID' => $this->getRepositoryPHID(),
'uri' => array(
'raw' => $this->getURI(),
'display' => (string)$this->getDisplayURI(),
'effective' => (string)$this->getEffectiveURI(),
'normalized' => (string)$this->getNormalizedURI(),
),
'io' => array(
'raw' => $this->getIOType(),
'default' => $this->getDefaultIOType(),
'effective' => $this->getEffectiveIOType(),
),
'display' => array(
'raw' => $this->getDisplayType(),
'default' => $this->getDefaultDisplayType(),
'effective' => $this->getEffectiveDisplayType(),
),
'credentialPHID' => $this->getCredentialPHID(),
'disabled' => (bool)$this->getIsDisabled(),
'builtin' => array(
'protocol' => $this->getBuiltinProtocol(),
'identifier' => $this->getBuiltinIdentifier(),
),
'dateCreated' => $this->getDateCreated(),
'dateModified' => $this->getDateModified(),
);
}
public function getConduitSearchAttachments() {
return array();
}
}
diff --git a/src/applications/repository/worker/PhabricatorRepositoryPushMailWorker.php b/src/applications/repository/worker/PhabricatorRepositoryPushMailWorker.php
index 554c2cf77..7459073ca 100644
--- a/src/applications/repository/worker/PhabricatorRepositoryPushMailWorker.php
+++ b/src/applications/repository/worker/PhabricatorRepositoryPushMailWorker.php
@@ -1,226 +1,226 @@
<?php
final class PhabricatorRepositoryPushMailWorker
extends PhabricatorWorker {
protected function doWork() {
$viewer = PhabricatorUser::getOmnipotentUser();
$task_data = $this->getTaskData();
$email_phids = idx($task_data, 'emailPHIDs');
if (!$email_phids) {
// If we don't have any email targets, don't send any email.
return;
}
$event_phid = idx($task_data, 'eventPHID');
$event = id(new PhabricatorRepositoryPushEventQuery())
->setViewer($viewer)
->withPHIDs(array($event_phid))
->needLogs(true)
->executeOne();
$repository = $event->getRepository();
if (!$repository->shouldPublish()) {
// If the repository is still importing, don't send email.
return;
}
$targets = id(new PhabricatorRepositoryPushReplyHandler())
->setMailReceiver($repository)
->getMailTargets($email_phids, array());
$messages = array();
foreach ($targets as $target) {
$messages[] = $this->sendMail($target, $repository, $event);
}
foreach ($messages as $message) {
$message->save();
}
}
private function sendMail(
PhabricatorMailTarget $target,
PhabricatorRepository $repository,
PhabricatorRepositoryPushEvent $event) {
$task_data = $this->getTaskData();
$viewer = $target->getViewer();
$locale = PhabricatorEnv::beginScopedLocale($viewer->getTranslation());
$logs = $event->getLogs();
list($ref_lines, $ref_list) = $this->renderRefs($logs);
list($commit_lines, $subject_line) = $this->renderCommits(
$repository,
$logs,
idx($task_data, 'info', array()));
$ref_count = count($ref_lines);
$commit_count = count($commit_lines);
$handles = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(array($event->getPusherPHID()))
->execute();
$pusher_name = $handles[$event->getPusherPHID()]->getName();
$repo_name = $repository->getMonogram();
if ($commit_count) {
$overview = pht(
'%s pushed %d commit(s) to %s.',
$pusher_name,
$commit_count,
$repo_name);
} else {
$overview = pht(
'%s pushed to %s.',
$pusher_name,
$repo_name);
}
$details_uri = PhabricatorEnv::getProductionURI(
'/diffusion/pushlog/view/'.$event->getID().'/');
$body = new PhabricatorMetaMTAMailBody();
$body->addRawSection($overview);
$body->addLinkSection(pht('DETAILS'), $details_uri);
if ($commit_lines) {
$body->addTextSection(pht('COMMITS'), implode("\n", $commit_lines));
}
if ($ref_lines) {
$body->addTextSection(pht('REFERENCES'), implode("\n", $ref_lines));
}
- $prefix = PhabricatorEnv::getEnvConfig('metamta.diffusion.subject-prefix');
+ $prefix = pht('[Diffusion]');
$parts = array();
if ($commit_count) {
$parts[] = pht('%s commit(s)', $commit_count);
}
if ($ref_count) {
$parts[] = implode(', ', $ref_list);
}
$parts = implode(', ', $parts);
if ($subject_line) {
$subject = pht('(%s) %s', $parts, $subject_line);
} else {
$subject = pht('(%s)', $parts);
}
$mail = id(new PhabricatorMetaMTAMail())
->setRelatedPHID($event->getPHID())
->setSubjectPrefix($prefix)
->setVarySubjectPrefix(pht('[Push]'))
->setSubject($subject)
->setFrom($event->getPusherPHID())
->setBody($body->render())
->setHTMLBody($body->renderHTML())
->setThreadID($event->getPHID(), $is_new = true)
->setIsBulk(true);
return $target->willSendMail($mail);
}
private function renderRefs(array $logs) {
$ref_lines = array();
$ref_list = array();
foreach ($logs as $log) {
$type_name = null;
$type_prefix = null;
switch ($log->getRefType()) {
case PhabricatorRepositoryPushLog::REFTYPE_BRANCH:
$type_name = pht('branch');
break;
case PhabricatorRepositoryPushLog::REFTYPE_TAG:
$type_name = pht('tag');
$type_prefix = pht('tag:');
break;
case PhabricatorRepositoryPushLog::REFTYPE_BOOKMARK:
$type_name = pht('bookmark');
$type_prefix = pht('bookmark:');
break;
case PhabricatorRepositoryPushLog::REFTYPE_COMMIT:
default:
break;
}
if ($type_name === null) {
continue;
}
$flags = $log->getChangeFlags();
if ($flags & PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS) {
$action = '!';
} else if ($flags & PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE) {
$action = '-';
} else if ($flags & PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE) {
$action = '~';
} else if ($flags & PhabricatorRepositoryPushLog::CHANGEFLAG_APPEND) {
$action = ' ';
} else if ($flags & PhabricatorRepositoryPushLog::CHANGEFLAG_ADD) {
$action = '+';
} else {
$action = '?';
}
$old = nonempty($log->getRefOldShort(), pht('<null>'));
$new = nonempty($log->getRefNewShort(), pht('<null>'));
$name = $log->getRefName();
$ref_lines[] = "{$action} {$type_name} {$name} {$old} > {$new}";
$ref_list[] = $type_prefix.$name;
}
return array(
$ref_lines,
array_unique($ref_list),
);
}
private function renderCommits(
PhabricatorRepository $repository,
array $logs,
array $info) {
$commit_lines = array();
$subject_line = null;
foreach ($logs as $log) {
if ($log->getRefType() != PhabricatorRepositoryPushLog::REFTYPE_COMMIT) {
continue;
}
$commit_info = idx($info, $log->getRefNew(), array());
$name = $repository->formatCommitName($log->getRefNew());
$branches = null;
if (idx($commit_info, 'branches')) {
$branches = ' ('.implode(', ', $commit_info['branches']).')';
}
$summary = null;
if (strlen(idx($commit_info, 'summary'))) {
$summary = ' '.$commit_info['summary'];
}
$commit_lines[] = "{$name}{$branches}{$summary}";
if ($subject_line === null) {
$subject_line = "{$name}{$summary}";
}
}
return array($commit_lines, $subject_line);
}
}
diff --git a/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php b/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php
index 37124393f..8520cac1b 100644
--- a/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php
+++ b/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php
@@ -1,283 +1,272 @@
<?php
final class PhabricatorProfileMenuItemConfiguration
extends PhabricatorSearchDAO
implements
PhabricatorPolicyInterface,
PhabricatorExtendedPolicyInterface,
PhabricatorApplicationTransactionInterface {
protected $profilePHID;
protected $menuItemKey;
protected $builtinKey;
protected $menuItemOrder;
protected $visibility;
protected $customPHID;
protected $menuItemProperties = array();
private $profileObject = self::ATTACHABLE;
private $menuItem = self::ATTACHABLE;
const VISIBILITY_DEFAULT = 'default';
const VISIBILITY_VISIBLE = 'visible';
const VISIBILITY_DISABLED = 'disabled';
public function getTableName() {
// For now, this class uses an older table name.
return 'search_profilepanelconfiguration';
}
public static function initializeNewBuiltin() {
return id(new self())
->setVisibility(self::VISIBILITY_VISIBLE);
}
public static function initializeNewItem(
$profile_object,
PhabricatorProfileMenuItem $item,
$custom_phid) {
return self::initializeNewBuiltin()
->setProfilePHID($profile_object->getPHID())
->setMenuItemKey($item->getMenuItemKey())
->attachMenuItem($item)
->attachProfileObject($profile_object)
->setCustomPHID($custom_phid);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'menuItemProperties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'menuItemKey' => 'text64',
'builtinKey' => 'text64?',
'menuItemOrder' => 'uint32?',
'customPHID' => 'phid?',
'visibility' => 'text32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_profile' => array(
'columns' => array('profilePHID', 'menuItemOrder'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorProfileMenuItemPHIDType::TYPECONST);
}
public function attachMenuItem(PhabricatorProfileMenuItem $item) {
$this->menuItem = $item;
return $this;
}
public function getMenuItem() {
return $this->assertAttached($this->menuItem);
}
public function attachProfileObject($profile_object) {
$this->profileObject = $profile_object;
return $this;
}
public function getProfileObject() {
return $this->assertAttached($this->profileObject);
}
public function setMenuItemProperty($key, $value) {
$this->menuItemProperties[$key] = $value;
return $this;
}
public function getMenuItemProperty($key, $default = null) {
return idx($this->menuItemProperties, $key, $default);
}
public function buildNavigationMenuItems() {
return $this->getMenuItem()->buildNavigationMenuItems($this);
}
public function getMenuItemTypeName() {
return $this->getMenuItem()->getMenuItemTypeName();
}
public function getDisplayName() {
return $this->getMenuItem()->getDisplayName($this);
}
public function canMakeDefault() {
return $this->getMenuItem()->canMakeDefault($this);
}
public function canHideMenuItem() {
return $this->getMenuItem()->canHideMenuItem($this);
}
public function shouldEnableForObject($object) {
return $this->getMenuItem()->shouldEnableForObject($object);
}
public function willBuildNavigationItems(array $items) {
return $this->getMenuItem()->willBuildNavigationItems($items);
}
public function validateTransactions(array $map) {
$item = $this->getMenuItem();
$fields = $item->buildEditEngineFields($this);
$errors = array();
foreach ($fields as $field) {
$field_key = $field->getKey();
$xactions = idx($map, $field_key, array());
$value = $this->getMenuItemProperty($field_key);
$field_errors = $item->validateTransactions(
$this,
$field_key,
$value,
$xactions);
foreach ($field_errors as $error) {
$errors[] = $error;
}
}
return $errors;
}
public function getSortVector() {
// Sort custom items above global items.
if ($this->getCustomPHID()) {
$is_global = 0;
} else {
$is_global = 1;
}
// Sort items with an explicit order above items without an explicit order,
// so any newly created builtins go to the bottom.
$order = $this->getMenuItemOrder();
if ($order !== null) {
$has_order = 0;
} else {
$has_order = 1;
}
return id(new PhutilSortVector())
->addInt($is_global)
->addInt($has_order)
->addInt((int)$order)
->addInt((int)$this->getID());
}
public function isDisabled() {
if (!$this->canHideMenuItem()) {
return false;
}
return ($this->getVisibility() === self::VISIBILITY_DISABLED);
}
public function isDefault() {
return ($this->getVisibility() === self::VISIBILITY_DEFAULT);
}
public function getItemIdentifier() {
$id = $this->getID();
if ($id) {
return (int)$id;
}
return $this->getBuiltinKey();
}
public function getDefaultMenuItemKey() {
if ($this->getBuiltinKey()) {
return $this->getBuiltinKey();
}
return $this->getPHID();
}
public function newPageContent() {
return $this->getMenuItem()->newPageContent($this);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return PhabricatorPolicies::getMostOpenPolicy();
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getProfileObject()->hasAutomaticCapability(
$capability,
$viewer);
}
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
// If this is an item with a custom PHID (like a personal menu item),
// we only require that the user can edit the corresponding custom
// object (usually their own user profile), not the object that the
// menu appears on (which may be an Application like Favorites or Home).
if ($capability == PhabricatorPolicyCapability::CAN_EDIT) {
if ($this->getCustomPHID()) {
return array(
array(
$this->getCustomPHID(),
$capability,
),
);
}
}
return array(
array(
$this->getProfileObject(),
$capability,
),
);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorProfileMenuEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorProfileMenuItemConfigurationTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
}
diff --git a/src/applications/search/view/PhabricatorSearchResultView.php b/src/applications/search/view/PhabricatorSearchResultView.php
index 6c527733e..b209b4422 100644
--- a/src/applications/search/view/PhabricatorSearchResultView.php
+++ b/src/applications/search/view/PhabricatorSearchResultView.php
@@ -1,186 +1,186 @@
<?php
final class PhabricatorSearchResultView extends AphrontView {
private $handle;
private $object;
private $tokens;
public function setHandle(PhabricatorObjectHandle $handle) {
$this->handle = $handle;
return $this;
}
public function setTokens(array $tokens) {
assert_instances_of($tokens, 'PhabricatorFulltextToken');
$this->tokens = $tokens;
return $this;
}
public function setObject($object) {
$this->object = $object;
return $this;
}
public function render() {
$handle = $this->handle;
if (!$handle->isComplete()) {
return;
}
require_celerity_resource('phabricator-search-results-css');
$type_name = nonempty($handle->getTypeName(), pht('Document'));
$raw_title = $handle->getFullName();
$title = $this->emboldenQuery($raw_title);
$item = id(new PHUIObjectItemView())
->setHeader($title)
->setTitleText($raw_title)
->setHref($handle->getURI())
->setImageURI($handle->getImageURI())
->addAttribute($type_name);
if ($handle->getStatus() == PhabricatorObjectHandle::STATUS_CLOSED) {
$item->setDisabled(true);
$item->addAttribute(pht('Closed'));
}
return $item;
}
/**
* Find the words which are part of the query string, and bold them in a
* result string. This makes it easier for users to see why a result
* matched their query.
*/
private function emboldenQuery($str) {
$tokens = $this->tokens;
if (!$tokens) {
return $str;
}
if (count($tokens) > 16) {
return $str;
}
if (!strlen($str)) {
return $str;
}
if (strlen($str) > 2048) {
return $str;
}
$patterns = array();
foreach ($tokens as $token) {
$raw_token = $token->getToken();
$operator = $raw_token->getOperator();
$value = $raw_token->getValue();
switch ($operator) {
case PhutilSearchQueryCompiler::OPERATOR_SUBSTRING:
case PhutilSearchQueryCompiler::OPERATOR_EXACT:
$patterns[] = '(('.preg_quote($value).'))ui';
break;
case PhutilSearchQueryCompiler::OPERATOR_AND:
$patterns[] = '((?<=\W|^)('.preg_quote($value).')(?=\W|\z))ui';
break;
default:
// Don't highlight anything else, particularly "NOT".
break;
}
}
// Find all matches for all query terms in the document title, then reduce
// them to a map from offsets to highlighted sequence lengths. If two terms
// match at the same position, we choose the longer one.
$all_matches = array();
foreach ($patterns as $pattern) {
$matches = null;
$ok = preg_match_all(
$pattern,
$str,
$matches,
PREG_OFFSET_CAPTURE);
if (!$ok) {
continue;
}
foreach ($matches[1] as $match) {
$match_text = $match[0];
$match_offset = $match[1];
if (!isset($all_matches[$match_offset])) {
$all_matches[$match_offset] = 0;
}
$all_matches[$match_offset] = max(
$all_matches[$match_offset],
strlen($match_text));
}
}
// Go through the string one display glyph at a time. If a glyph starts
- // on a highlighted byte position, turn on highlighting for the nubmer
+ // on a highlighted byte position, turn on highlighting for the number
// of matching bytes. If a query searches for "e" and the document contains
// an "e" followed by a bunch of combining marks, this will correctly
// highlight the entire glyph.
$parts = array();
$highlight = 0;
$offset = 0;
foreach (phutil_utf8v_combined($str) as $character) {
$length = strlen($character);
if (isset($all_matches[$offset])) {
$highlight = $all_matches[$offset];
}
if ($highlight > 0) {
$is_highlighted = true;
$highlight -= $length;
} else {
$is_highlighted = false;
}
$parts[] = array(
'text' => $character,
'highlighted' => $is_highlighted,
);
$offset += $length;
}
// Combine all the sequences together so we aren't emitting a tag around
// every individual character.
$last = null;
foreach ($parts as $key => $part) {
if ($last !== null) {
if ($part['highlighted'] == $parts[$last]['highlighted']) {
$parts[$last]['text'] .= $part['text'];
unset($parts[$key]);
continue;
}
}
$last = $key;
}
// Finally, add tags.
$result = array();
foreach ($parts as $part) {
if ($part['highlighted']) {
$result[] = phutil_tag('strong', array(), $part['text']);
} else {
$result[] = $part['text'];
}
}
return $result;
}
}
diff --git a/src/applications/settings/controller/PhabricatorSettingsMainController.php b/src/applications/settings/controller/PhabricatorSettingsMainController.php
index 9dc84a9bd..ded20a8e9 100644
--- a/src/applications/settings/controller/PhabricatorSettingsMainController.php
+++ b/src/applications/settings/controller/PhabricatorSettingsMainController.php
@@ -1,240 +1,242 @@
<?php
final class PhabricatorSettingsMainController
extends PhabricatorController {
private $user;
private $builtinKey;
private $preferences;
private function getUser() {
return $this->user;
}
private function isSelf() {
$user = $this->getUser();
if (!$user) {
return false;
}
$user_phid = $user->getPHID();
$viewer_phid = $this->getViewer()->getPHID();
return ($viewer_phid == $user_phid);
}
private function isTemplate() {
return ($this->builtinKey !== null);
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
// Redirect "/panel/XYZ/" to the viewer's personal settings panel. This
// was the primary URI before global settings were introduced and allows
// generation of viewer-agnostic URIs for email and logged-out users.
$panel = $request->getURIData('panel');
if ($panel) {
$panel = phutil_escape_uri($panel);
$username = $viewer->getUsername();
$panel_uri = "/user/{$username}/page/{$panel}/";
$panel_uri = $this->getApplicationURI($panel_uri);
return id(new AphrontRedirectResponse())->setURI($panel_uri);
}
$username = $request->getURIData('username');
$builtin = $request->getURIData('builtin');
$key = $request->getURIData('pageKey');
if ($builtin) {
$this->builtinKey = $builtin;
$preferences = id(new PhabricatorUserPreferencesQuery())
->setViewer($viewer)
->withBuiltinKeys(array($builtin))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$preferences) {
$preferences = id(new PhabricatorUserPreferences())
->attachUser(null)
->setBuiltinKey($builtin);
}
} else {
$user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withUsernames(array($username))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$user) {
return new Aphront404Response();
}
$preferences = PhabricatorUserPreferences::loadUserPreferences($user);
$this->user = $user;
}
if (!$preferences) {
return new Aphront404Response();
}
PhabricatorPolicyFilter::requireCapability(
$viewer,
$preferences,
PhabricatorPolicyCapability::CAN_EDIT);
$this->preferences = $preferences;
$panels = $this->buildPanels($preferences);
$nav = $this->renderSideNav($panels);
$key = $nav->selectFilter($key, head($panels)->getPanelKey());
$panel = $panels[$key]
->setController($this)
->setNavigation($nav);
$response = $panel->processRequest($request);
if (($response instanceof AphrontResponse) ||
($response instanceof AphrontResponseProducerInterface)) {
return $response;
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($panel->getPanelName());
$crumbs->setBorder(true);
if ($this->user) {
- $header_text = pht('Edit Settings (%s)', $user->getUserName());
+ $header_text = pht('Edit Settings: %s', $user->getUserName());
} else {
$header_text = pht('Edit Global Settings');
}
$header = id(new PHUIHeaderView())
->setHeader($header_text);
$title = $panel->getPanelName();
$view = id(new PHUITwoColumnView())
->setHeader($header)
- ->setFixed(true)
- ->setNavigation($nav)
- ->setMainColumn($response);
+ ->setFooter($response);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
+ ->setNavigation($nav)
->appendChild($view);
-
}
private function buildPanels(PhabricatorUserPreferences $preferences) {
$viewer = $this->getViewer();
$panels = PhabricatorSettingsPanel::getAllDisplayPanels();
$result = array();
foreach ($panels as $key => $panel) {
$panel
->setPreferences($preferences)
->setViewer($viewer);
if ($this->user) {
$panel->setUser($this->user);
}
if (!$panel->isEnabled()) {
continue;
}
if ($this->isTemplate()) {
if (!$panel->isTemplatePanel()) {
continue;
}
} else {
if (!$this->isSelf() && !$panel->isManagementPanel()) {
continue;
}
if ($this->isSelf() && !$panel->isUserPanel()) {
continue;
}
}
if (!empty($result[$key])) {
throw new Exception(pht(
"Two settings panels share the same panel key ('%s'): %s, %s.",
$key,
get_class($panel),
get_class($result[$key])));
}
$result[$key] = $panel;
}
if (!$result) {
throw new Exception(pht('No settings panels are available.'));
}
return $result;
}
private function renderSideNav(array $panels) {
$nav = new AphrontSideNavFilterView();
if ($this->isTemplate()) {
$base_uri = 'builtin/'.$this->builtinKey.'/page/';
} else {
$user = $this->getUser();
$base_uri = 'user/'.$user->getUsername().'/page/';
}
$nav->setBaseURI(new PhutilURI($this->getApplicationURI($base_uri)));
$group_key = null;
foreach ($panels as $panel) {
if ($panel->getPanelGroupKey() != $group_key) {
$group_key = $panel->getPanelGroupKey();
$group = $panel->getPanelGroup();
$panel_name = $group->getPanelGroupName();
if ($panel_name) {
$nav->addLabel($panel_name);
}
}
- $nav->addFilter($panel->getPanelKey(), $panel->getPanelName());
+ $nav->addFilter(
+ $panel->getPanelKey(),
+ $panel->getPanelName(),
+ null,
+ $panel->getPanelMenuIcon());
}
return $nav;
}
public function buildApplicationMenu() {
if ($this->preferences) {
$panels = $this->buildPanels($this->preferences);
return $this->renderSideNav($panels)->getMenu();
}
return parent::buildApplicationMenu();
}
protected function buildApplicationCrumbs() {
$crumbs = parent::buildApplicationCrumbs();
$user = $this->getUser();
if (!$this->isSelf() && $user) {
$username = $user->getUsername();
$crumbs->addTextCrumb($username, "/p/{$username}/");
}
return $crumbs;
}
}
diff --git a/src/applications/settings/editor/PhabricatorSettingsEditEngine.php b/src/applications/settings/editor/PhabricatorSettingsEditEngine.php
index 30e831543..34a6132d8 100644
--- a/src/applications/settings/editor/PhabricatorSettingsEditEngine.php
+++ b/src/applications/settings/editor/PhabricatorSettingsEditEngine.php
@@ -1,256 +1,256 @@
<?php
final class PhabricatorSettingsEditEngine
extends PhabricatorEditEngine {
const ENGINECONST = 'settings.settings';
private $isSelfEdit;
private $profileURI;
public function setIsSelfEdit($is_self_edit) {
$this->isSelfEdit = $is_self_edit;
return $this;
}
public function getIsSelfEdit() {
return $this->isSelfEdit;
}
public function setProfileURI($profile_uri) {
$this->profileURI = $profile_uri;
return $this;
}
public function getProfileURI() {
return $this->profileURI;
}
public function isEngineConfigurable() {
return false;
}
public function getEngineName() {
return pht('Settings');
}
public function getSummaryHeader() {
return pht('Edit Settings Configurations');
}
public function getSummaryText() {
return pht('This engine is used to edit settings.');
}
public function getEngineApplicationClass() {
return 'PhabricatorSettingsApplication';
}
protected function newEditableObject() {
return new PhabricatorUserPreferences();
}
protected function newObjectQuery() {
return new PhabricatorUserPreferencesQuery();
}
protected function getObjectCreateTitleText($object) {
return pht('Create Settings');
}
protected function getObjectCreateButtonText($object) {
return pht('Create Settings');
}
protected function getObjectEditTitleText($object) {
$page = $this->getSelectedPage();
if ($page) {
return $page->getLabel();
}
return pht('Settings');
}
protected function getObjectEditShortText($object) {
if (!$object->getUser()) {
return pht('Global Defaults');
} else {
if ($this->getIsSelfEdit()) {
return pht('Personal Settings');
} else {
return pht('Account Settings');
}
}
}
protected function getObjectCreateShortText() {
return pht('Create Settings');
}
protected function getObjectName() {
$page = $this->getSelectedPage();
if ($page) {
return $page->getLabel();
}
return pht('Settings');
}
protected function getPageHeader($object) {
$user = $object->getUser();
if ($user) {
- $text = pht('Edit Settings (%s)', $user->getUserName());
+ $text = pht('Edit Settings: %s', $user->getUserName());
} else {
$text = pht('Edit Global Settings');
}
$header = id(new PHUIHeaderView())
->setHeader($text);
return $header;
}
protected function getEditorURI() {
throw new PhutilMethodNotImplementedException();
}
protected function getObjectCreateCancelURI($object) {
return '/settings/';
}
protected function getObjectViewURI($object) {
return $object->getEditURI();
}
protected function getCreateNewObjectPolicy() {
return PhabricatorPolicies::POLICY_ADMIN;
}
public function getEffectiveObjectEditDoneURI($object) {
return parent::getEffectiveObjectViewURI($object).'saved/';
}
public function getEffectiveObjectEditCancelURI($object) {
if (!$object->getUser()) {
return '/settings/';
}
if ($this->getIsSelfEdit()) {
return null;
}
if ($this->getProfileURI()) {
return $this->getProfileURI();
}
return parent::getEffectiveObjectEditCancelURI($object);
}
protected function newPages($object) {
$viewer = $this->getViewer();
$user = $object->getUser();
- $panels = PhabricatorSettingsPanel::getAllPanels();
+ $panels = PhabricatorSettingsPanel::getAllDisplayPanels();
foreach ($panels as $key => $panel) {
if (!($panel instanceof PhabricatorEditEngineSettingsPanel)) {
unset($panels[$key]);
continue;
}
$panel->setViewer($viewer);
if ($user) {
$panel->setUser($user);
}
}
$pages = array();
$uris = array();
foreach ($panels as $key => $panel) {
$uris[$key] = $panel->getPanelURI();
$page = $panel->newEditEnginePage();
if (!$page) {
continue;
}
$pages[] = $page;
}
$more_pages = array(
id(new PhabricatorEditPage())
->setKey('extra')
->setLabel(pht('Extra Settings'))
->setIsDefault(true),
);
foreach ($more_pages as $page) {
$pages[] = $page;
}
return $pages;
}
protected function buildCustomEditFields($object) {
$viewer = $this->getViewer();
$settings = PhabricatorSetting::getAllEnabledSettings($viewer);
foreach ($settings as $key => $setting) {
$setting = clone $setting;
$setting->setViewer($viewer);
$settings[$key] = $setting;
}
$settings = msortv($settings, 'getSettingOrderVector');
$fields = array();
foreach ($settings as $setting) {
foreach ($setting->newCustomEditFields($object) as $field) {
$fields[] = $field;
}
}
return $fields;
}
protected function getValidationExceptionShortMessage(
PhabricatorApplicationTransactionValidationException $ex,
PhabricatorEditField $field) {
// Settings fields all have the same transaction type so we need to make
// sure the transaction is changing the same setting before matching an
// error to a given field.
$xaction_type = $field->getTransactionType();
if ($xaction_type == PhabricatorUserPreferencesTransaction::TYPE_SETTING) {
$property = PhabricatorUserPreferencesTransaction::PROPERTY_SETTING;
$field_setting = idx($field->getMetadata(), $property);
foreach ($ex->getErrors() as $error) {
if ($error->getType() !== $xaction_type) {
continue;
}
$xaction = $error->getTransaction();
if (!$xaction) {
continue;
}
$xaction_setting = $xaction->getMetadataValue($property);
if ($xaction_setting != $field_setting) {
continue;
}
$short_message = $error->getShortMessage();
if ($short_message !== null) {
return $short_message;
}
}
return null;
}
return parent::getValidationExceptionShortMessage($ex, $field);
}
}
diff --git a/src/applications/settings/panel/PhabricatorActivitySettingsPanel.php b/src/applications/settings/panel/PhabricatorActivitySettingsPanel.php
index 2759f3a26..a3654a438 100644
--- a/src/applications/settings/panel/PhabricatorActivitySettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorActivitySettingsPanel.php
@@ -1,46 +1,50 @@
<?php
final class PhabricatorActivitySettingsPanel extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'activity';
}
public function getPanelName() {
return pht('Activity Logs');
}
+ public function getPanelMenuIcon() {
+ return 'fa-list';
+ }
+
public function getPanelGroupKey() {
return PhabricatorSettingsLogsPanelGroup::PANELGROUPKEY;
}
public function processRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$user = $this->getUser();
$pager = id(new AphrontCursorPagerView())
->readFromRequest($request);
$logs = id(new PhabricatorPeopleLogQuery())
->setViewer($viewer)
->withRelatedPHIDs(array($user->getPHID()))
->executeWithCursorPager($pager);
$table = id(new PhabricatorUserLogView())
->setUser($viewer)
->setLogs($logs);
$panel = $this->newBox(pht('Account Activity Logs'), $table);
$pager_box = id(new PHUIBoxView())
->addMargin(PHUI::MARGIN_LARGE)
->appendChild($pager);
return array($panel, $pager_box);
}
public function isManagementPanel() {
return true;
}
}
diff --git a/src/applications/settings/panel/PhabricatorConpherencePreferencesSettingsPanel.php b/src/applications/settings/panel/PhabricatorConpherencePreferencesSettingsPanel.php
index 6ed6325d6..3ce72af2f 100644
--- a/src/applications/settings/panel/PhabricatorConpherencePreferencesSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorConpherencePreferencesSettingsPanel.php
@@ -1,20 +1,24 @@
<?php
final class PhabricatorConpherencePreferencesSettingsPanel
extends PhabricatorEditEngineSettingsPanel {
const PANELKEY = 'conpherence';
public function getPanelName() {
return pht('Conpherence');
}
+ public function getPanelMenuIcon() {
+ return 'fa-comment-o';
+ }
+
public function getPanelGroupKey() {
return PhabricatorSettingsApplicationsPanelGroup::PANELGROUPKEY;
}
public function isTemplatePanel() {
return true;
}
}
diff --git a/src/applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php b/src/applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php
new file mode 100644
index 000000000..7056fd02d
--- /dev/null
+++ b/src/applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php
@@ -0,0 +1,91 @@
+<?php
+
+final class PhabricatorContactNumbersSettingsPanel
+ extends PhabricatorSettingsPanel {
+
+ public function getPanelKey() {
+ return 'contact';
+ }
+
+ public function getPanelName() {
+ return pht('Contact Numbers');
+ }
+
+ public function getPanelMenuIcon() {
+ return 'fa-hashtag';
+ }
+
+ public function getPanelGroupKey() {
+ return PhabricatorSettingsAuthenticationPanelGroup::PANELGROUPKEY;
+ }
+
+ public function isMultiFactorEnrollmentPanel() {
+ return true;
+ }
+
+ public function processRequest(AphrontRequest $request) {
+ $user = $this->getUser();
+ $viewer = $request->getUser();
+
+ $numbers = id(new PhabricatorAuthContactNumberQuery())
+ ->setViewer($viewer)
+ ->withObjectPHIDs(array($user->getPHID()))
+ ->execute();
+ $numbers = msortv($numbers, 'getSortVector');
+
+ $rows = array();
+ $row_classes = array();
+ foreach ($numbers as $number) {
+ if ($number->getIsPrimary()) {
+ $primary_display = pht('Primary');
+ $row_classes[] = 'highlighted';
+ } else {
+ $primary_display = null;
+ $row_classes[] = null;
+ }
+
+ $rows[] = array(
+ $number->newIconView(),
+ phutil_tag(
+ 'a',
+ array(
+ 'href' => $number->getURI(),
+ ),
+ $number->getDisplayName()),
+ $primary_display,
+ phabricator_datetime($number->getDateCreated(), $viewer),
+ );
+ }
+
+ $table = id(new AphrontTableView($rows))
+ ->setNoDataString(
+ pht("You haven't added any contact numbers to your account."))
+ ->setRowClasses($row_classes)
+ ->setHeaders(
+ array(
+ null,
+ pht('Number'),
+ pht('Status'),
+ pht('Created'),
+ ))
+ ->setColumnClasses(
+ array(
+ null,
+ 'wide pri',
+ null,
+ 'right',
+ ));
+
+ $buttons = array();
+
+ $buttons[] = id(new PHUIButtonView())
+ ->setTag('a')
+ ->setIcon('fa-plus')
+ ->setText(pht('Add Contact Number'))
+ ->setHref('/auth/contact/edit/')
+ ->setColor(PHUIButtonView::GREY);
+
+ return $this->newBox(pht('Contact Numbers'), $table, $buttons);
+ }
+
+}
diff --git a/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php b/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php
index e5ca46510..285bc6989 100644
--- a/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php
@@ -1,24 +1,28 @@
<?php
final class PhabricatorDateTimeSettingsPanel
extends PhabricatorEditEngineSettingsPanel {
const PANELKEY = 'datetime';
public function getPanelName() {
return pht('Date and Time');
}
+ public function getPanelMenuIcon() {
+ return 'fa-calendar';
+ }
+
public function getPanelGroupKey() {
return PhabricatorSettingsAccountPanelGroup::PANELGROUPKEY;
}
public function isManagementPanel() {
return true;
}
public function isTemplatePanel() {
return true;
}
}
diff --git a/src/applications/settings/panel/PhabricatorDeveloperPreferencesSettingsPanel.php b/src/applications/settings/panel/PhabricatorDeveloperPreferencesSettingsPanel.php
index 384f7e3be..e6ed8e756 100644
--- a/src/applications/settings/panel/PhabricatorDeveloperPreferencesSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorDeveloperPreferencesSettingsPanel.php
@@ -1,20 +1,24 @@
<?php
final class PhabricatorDeveloperPreferencesSettingsPanel
extends PhabricatorEditEngineSettingsPanel {
const PANELKEY = 'developer';
public function getPanelName() {
return pht('Developer Settings');
}
+ public function getPanelMenuIcon() {
+ return 'fa-magic';
+ }
+
public function getPanelGroupKey() {
return PhabricatorSettingsDeveloperPanelGroup::PANELGROUPKEY;
}
public function isTemplatePanel() {
return true;
}
}
diff --git a/src/applications/settings/panel/PhabricatorDiffPreferencesSettingsPanel.php b/src/applications/settings/panel/PhabricatorDiffPreferencesSettingsPanel.php
index 2e055c340..acb7f5054 100644
--- a/src/applications/settings/panel/PhabricatorDiffPreferencesSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorDiffPreferencesSettingsPanel.php
@@ -1,20 +1,24 @@
<?php
final class PhabricatorDiffPreferencesSettingsPanel
extends PhabricatorEditEngineSettingsPanel {
const PANELKEY = 'diff';
public function getPanelName() {
return pht('Diff Preferences');
}
+ public function getPanelMenuIcon() {
+ return 'fa-cog';
+ }
+
public function getPanelGroupKey() {
return PhabricatorSettingsApplicationsPanelGroup::PANELGROUPKEY;
}
public function isTemplatePanel() {
return true;
}
}
diff --git a/src/applications/settings/panel/PhabricatorDisplayPreferencesSettingsPanel.php b/src/applications/settings/panel/PhabricatorDisplayPreferencesSettingsPanel.php
index 6033ef79e..7c17a9fea 100644
--- a/src/applications/settings/panel/PhabricatorDisplayPreferencesSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorDisplayPreferencesSettingsPanel.php
@@ -1,20 +1,24 @@
<?php
final class PhabricatorDisplayPreferencesSettingsPanel
extends PhabricatorEditEngineSettingsPanel {
const PANELKEY = 'display';
public function getPanelName() {
return pht('Display Preferences');
}
+ public function getPanelMenuIcon() {
+ return 'fa-desktop';
+ }
+
public function getPanelGroupKey() {
return PhabricatorSettingsApplicationsPanelGroup::PANELGROUPKEY;
}
public function isTemplatePanel() {
return true;
}
}
diff --git a/src/applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php b/src/applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php
index cd1bfba54..1b69adcd6 100644
--- a/src/applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php
@@ -1,411 +1,415 @@
<?php
final class PhabricatorEmailAddressesSettingsPanel
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'email';
}
public function getPanelName() {
return pht('Email Addresses');
}
+ public function getPanelMenuIcon() {
+ return 'fa-at';
+ }
+
public function getPanelGroupKey() {
return PhabricatorSettingsEmailPanelGroup::PANELGROUPKEY;
}
public function isEditableByAdministrators() {
if ($this->getUser()->getIsMailingList()) {
return true;
}
return false;
}
public function processRequest(AphrontRequest $request) {
$user = $this->getUser();
$editable = PhabricatorEnv::getEnvConfig('account.editable');
$uri = $request->getRequestURI();
$uri->setQueryParams(array());
if ($editable) {
$new = $request->getStr('new');
if ($new) {
return $this->returnNewAddressResponse($request, $uri, $new);
}
$delete = $request->getInt('delete');
if ($delete) {
return $this->returnDeleteAddressResponse($request, $uri, $delete);
}
}
$verify = $request->getInt('verify');
if ($verify) {
return $this->returnVerifyAddressResponse($request, $uri, $verify);
}
$primary = $request->getInt('primary');
if ($primary) {
return $this->returnPrimaryAddressResponse($request, $uri, $primary);
}
$emails = id(new PhabricatorUserEmail())->loadAllWhere(
'userPHID = %s ORDER BY address',
$user->getPHID());
$rowc = array();
$rows = array();
foreach ($emails as $email) {
$button_verify = javelin_tag(
'a',
array(
'class' => 'button small button-grey',
'href' => $uri->alter('verify', $email->getID()),
'sigil' => 'workflow',
),
pht('Verify'));
$button_make_primary = javelin_tag(
'a',
array(
'class' => 'button small button-grey',
'href' => $uri->alter('primary', $email->getID()),
'sigil' => 'workflow',
),
pht('Make Primary'));
$button_remove = javelin_tag(
'a',
array(
'class' => 'button small button-grey',
'href' => $uri->alter('delete', $email->getID()),
'sigil' => 'workflow',
),
pht('Remove'));
$button_primary = phutil_tag(
'a',
array(
'class' => 'button small disabled',
),
pht('Primary'));
if (!$email->getIsVerified()) {
$action = $button_verify;
} else if ($email->getIsPrimary()) {
$action = $button_primary;
} else {
$action = $button_make_primary;
}
if ($email->getIsPrimary()) {
$remove = $button_primary;
$rowc[] = 'highlighted';
} else {
$remove = $button_remove;
$rowc[] = null;
}
$rows[] = array(
$email->getAddress(),
$action,
$remove,
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
pht('Email'),
pht('Status'),
pht('Remove'),
));
$table->setColumnClasses(
array(
'wide',
'action',
'action',
));
$table->setRowClasses($rowc);
$table->setColumnVisibility(
array(
true,
true,
$editable,
));
$buttons = array();
if ($editable) {
$buttons[] = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-plus')
->setText(pht('Add New Address'))
->setHref($uri->alter('new', 'true'))
->addSigil('workflow')
->setColor(PHUIButtonView::GREY);
}
return $this->newBox(pht('Email Addresses'), $table, $buttons);
}
private function returnNewAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
$new) {
$user = $this->getUser();
$viewer = $this->getViewer();
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
$this->getPanelURI());
$e_email = true;
$email = null;
$errors = array();
if ($request->isDialogFormPost()) {
$email = trim($request->getStr('email'));
if ($new == 'verify') {
// The user clicked "Done" from the "an email has been sent" dialog.
return id(new AphrontReloadResponse())->setURI($uri);
}
PhabricatorSystemActionEngine::willTakeAction(
array($viewer->getPHID()),
new PhabricatorSettingsAddEmailAction(),
1);
if (!strlen($email)) {
$e_email = pht('Required');
$errors[] = pht('Email is required.');
} else if (!PhabricatorUserEmail::isValidAddress($email)) {
$e_email = pht('Invalid');
$errors[] = PhabricatorUserEmail::describeValidAddresses();
} else if (!PhabricatorUserEmail::isAllowedAddress($email)) {
$e_email = pht('Disallowed');
$errors[] = PhabricatorUserEmail::describeAllowedAddresses();
}
if ($e_email === true) {
$application_email = id(new PhabricatorMetaMTAApplicationEmailQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withAddresses(array($email))
->executeOne();
if ($application_email) {
$e_email = pht('In Use');
$errors[] = $application_email->getInUseMessage();
}
}
if (!$errors) {
$object = id(new PhabricatorUserEmail())
->setAddress($email)
->setIsVerified(0);
// If an administrator is editing a mailing list, automatically verify
// the address.
if ($viewer->getPHID() != $user->getPHID()) {
if ($viewer->getIsAdmin()) {
$object->setIsVerified(1);
}
}
try {
id(new PhabricatorUserEditor())
->setActor($viewer)
->addEmail($user, $object);
if ($object->getIsVerified()) {
// If we autoverified the address, just reload the page.
return id(new AphrontReloadResponse())->setURI($uri);
}
$object->sendVerificationEmail($user);
$dialog = $this->newDialog()
->addHiddenInput('new', 'verify')
->setTitle(pht('Verification Email Sent'))
->appendChild(phutil_tag('p', array(), pht(
'A verification email has been sent. Click the link in the '.
'email to verify your address.')))
->setSubmitURI($uri)
->addSubmitButton(pht('Done'));
return id(new AphrontDialogResponse())->setDialog($dialog);
} catch (AphrontDuplicateKeyQueryException $ex) {
$e_email = pht('Duplicate');
$errors[] = pht('Another user already has this email.');
}
}
}
if ($errors) {
$errors = id(new PHUIInfoView())
->setErrors($errors);
}
$form = id(new PHUIFormLayoutView())
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Email'))
->setName('email')
->setValue($email)
->setCaption(PhabricatorUserEmail::describeAllowedAddresses())
->setError($e_email));
$dialog = $this->newDialog()
->addHiddenInput('new', 'true')
->setTitle(pht('New Address'))
->appendChild($errors)
->appendChild($form)
->addSubmitButton(pht('Save'))
->addCancelButton($uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function returnDeleteAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
$email_id) {
$user = $this->getUser();
$viewer = $this->getViewer();
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
$this->getPanelURI());
// NOTE: You can only delete your own email addresses, and you can not
// delete your primary address.
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'id = %d AND userPHID = %s AND isPrimary = 0',
$email_id,
$user->getPHID());
if (!$email) {
return new Aphront404Response();
}
if ($request->isFormPost()) {
id(new PhabricatorUserEditor())
->setActor($viewer)
->removeEmail($user, $email);
return id(new AphrontRedirectResponse())->setURI($uri);
}
$address = $email->getAddress();
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->addHiddenInput('delete', $email_id)
->setTitle(pht("Really delete address '%s'?", $address))
->appendParagraph(
pht(
'Are you sure you want to delete this address? You will no '.
'longer be able to use it to login.'))
->appendParagraph(
pht(
'Note: Removing an email address from your account will invalidate '.
'any outstanding password reset links.'))
->addSubmitButton(pht('Delete'))
->addCancelButton($uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function returnVerifyAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
$email_id) {
$user = $this->getUser();
$viewer = $this->getViewer();
// NOTE: You can only send more email for your unverified addresses.
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'id = %d AND userPHID = %s AND isVerified = 0',
$email_id,
$user->getPHID());
if (!$email) {
return new Aphront404Response();
}
if ($request->isFormPost()) {
$email->sendVerificationEmail($user);
return id(new AphrontRedirectResponse())->setURI($uri);
}
$address = $email->getAddress();
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->addHiddenInput('verify', $email_id)
->setTitle(pht('Send Another Verification Email?'))
->appendChild(phutil_tag('p', array(), pht(
'Send another copy of the verification email to %s?',
$address)))
->addSubmitButton(pht('Send Email'))
->addCancelButton($uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function returnPrimaryAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
$email_id) {
$user = $this->getUser();
$viewer = $this->getViewer();
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
$this->getPanelURI());
// NOTE: You can only make your own verified addresses primary.
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'id = %d AND userPHID = %s AND isVerified = 1 AND isPrimary = 0',
$email_id,
$user->getPHID());
if (!$email) {
return new Aphront404Response();
}
if ($request->isFormPost()) {
id(new PhabricatorUserEditor())
->setActor($viewer)
->changePrimaryEmail($user, $email);
return id(new AphrontRedirectResponse())->setURI($uri);
}
$address = $email->getAddress();
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->addHiddenInput('primary', $email_id)
->setTitle(pht('Change primary email address?'))
->appendParagraph(
pht(
'If you change your primary address, Phabricator will send all '.
'email to %s.',
$address))
->appendParagraph(
pht(
'Note: Changing your primary email address will invalidate any '.
'outstanding password reset links.'))
->addSubmitButton(pht('Change Primary Address'))
->addCancelButton($uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/settings/panel/PhabricatorEmailDeliverySettingsPanel.php b/src/applications/settings/panel/PhabricatorEmailDeliverySettingsPanel.php
index 86260c1b5..55932aa49 100644
--- a/src/applications/settings/panel/PhabricatorEmailDeliverySettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorEmailDeliverySettingsPanel.php
@@ -1,28 +1,32 @@
<?php
final class PhabricatorEmailDeliverySettingsPanel
extends PhabricatorEditEngineSettingsPanel {
const PANELKEY = 'emaildelivery';
public function getPanelName() {
return pht('Email Delivery');
}
+ public function getPanelMenuIcon() {
+ return 'fa-envelope-o';
+ }
+
public function getPanelGroupKey() {
return PhabricatorSettingsEmailPanelGroup::PANELGROUPKEY;
}
public function isManagementPanel() {
if ($this->getUser()->getIsMailingList()) {
return true;
}
return false;
}
public function isTemplatePanel() {
return true;
}
}
diff --git a/src/applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php b/src/applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php
index 51ff40ed9..5a4a707a0 100644
--- a/src/applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php
@@ -1,39 +1,32 @@
<?php
final class PhabricatorEmailFormatSettingsPanel
extends PhabricatorEditEngineSettingsPanel {
const PANELKEY = 'emailformat';
public function getPanelName() {
return pht('Email Format');
}
+ public function getPanelMenuIcon() {
+ return 'fa-font';
+ }
+
public function getPanelGroupKey() {
return PhabricatorSettingsEmailPanelGroup::PANELGROUPKEY;
}
public function isUserPanel() {
return PhabricatorMetaMTAMail::shouldMailEachRecipient();
}
public function isManagementPanel() {
return false;
-/*
- if (!$this->isUserPanel()) {
- return false;
- }
-
- if ($this->getUser()->getIsMailingList()) {
- return true;
- }
-
- return false;
-*/
}
public function isTemplatePanel() {
return true;
}
}
diff --git a/src/applications/settings/panel/PhabricatorEmailPreferencesSettingsPanel.php b/src/applications/settings/panel/PhabricatorEmailPreferencesSettingsPanel.php
index faa79889e..defee7339 100644
--- a/src/applications/settings/panel/PhabricatorEmailPreferencesSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorEmailPreferencesSettingsPanel.php
@@ -1,213 +1,217 @@
<?php
final class PhabricatorEmailPreferencesSettingsPanel
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'emailpreferences';
}
public function getPanelName() {
return pht('Email Preferences');
}
+ public function getPanelMenuIcon() {
+ return 'fa-envelope-open-o';
+ }
+
public function getPanelGroupKey() {
return PhabricatorSettingsEmailPanelGroup::PANELGROUPKEY;
}
public function isManagementPanel() {
if ($this->getUser()->getIsMailingList()) {
return true;
}
return false;
}
public function isTemplatePanel() {
return true;
}
public function processRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$user = $this->getUser();
$preferences = $this->getPreferences();
$value_email = PhabricatorEmailTagsSetting::VALUE_EMAIL;
$errors = array();
if ($request->isFormPost()) {
$new_tags = $request->getArr('mailtags');
$mailtags = $preferences->getPreference('mailtags', array());
$all_tags = $this->getAllTags($user);
foreach ($all_tags as $key => $label) {
$mailtags[$key] = (int)idx($new_tags, $key, $value_email);
}
$this->writeSetting(
$preferences,
PhabricatorEmailTagsSetting::SETTINGKEY,
$mailtags);
return id(new AphrontRedirectResponse())
->setURI($this->getPanelURI('?saved=true'));
}
$mailtags = $preferences->getSettingValue(
PhabricatorEmailTagsSetting::SETTINGKEY);
$form = id(new AphrontFormView())
->setUser($viewer);
$form->appendRemarkupInstructions(
pht(
'You can adjust **Application Settings** here to customize when '.
'you are emailed and notified.'.
"\n\n".
"| Setting | Effect\n".
"| ------- | -------\n".
"| Email | You will receive an email and a notification, but the ".
"notification will be marked \"read\".\n".
"| Notify | You will receive an unread notification only.\n".
"| Ignore | You will receive nothing.\n".
"\n\n".
'If an update makes several changes (like adding CCs to a task, '.
'closing it, and adding a comment) you will receive the strongest '.
'notification any of the changes is configured to deliver.'.
"\n\n".
'These preferences **only** apply to objects you are connected to '.
'(for example, Revisions where you are a reviewer or tasks you are '.
'CC\'d on). To receive email alerts when other objects are created, '.
'configure [[ /herald/ | Herald Rules ]].'));
$editors = $this->getAllEditorsWithTags($user);
// Find all the tags shared by more than one application, and put them
// in a "common" group.
$all_tags = array();
foreach ($editors as $editor) {
foreach ($editor->getMailTagsMap() as $tag => $name) {
if (empty($all_tags[$tag])) {
$all_tags[$tag] = array(
'count' => 0,
'name' => $name,
);
}
$all_tags[$tag]['count'];
}
}
$common_tags = array();
foreach ($all_tags as $tag => $info) {
if ($info['count'] > 1) {
$common_tags[$tag] = $info['name'];
}
}
// Build up the groups of application-specific options.
$tag_groups = array();
foreach ($editors as $editor) {
$tag_groups[] = array(
$editor->getEditorObjectsDescription(),
array_diff_key($editor->getMailTagsMap(), $common_tags),
);
}
// Sort them, then put "Common" at the top.
$tag_groups = isort($tag_groups, 0);
if ($common_tags) {
array_unshift($tag_groups, array(pht('Common'), $common_tags));
}
// Finally, build the controls.
foreach ($tag_groups as $spec) {
list($label, $map) = $spec;
$control = $this->buildMailTagControl($label, $map, $mailtags);
$form->appendChild($control);
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Save Preferences')));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Email Preferences'))
->setFormSaved($request->getStr('saved'))
->setFormErrors($errors)
->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
->setForm($form);
return $form_box;
}
private function getAllEditorsWithTags(PhabricatorUser $user = null) {
$editors = id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorApplicationTransactionEditor')
->setFilterMethod('getMailTagsMap')
->execute();
foreach ($editors as $key => $editor) {
// Remove editors for applications which are not installed.
$app = $editor->getEditorApplicationClass();
if ($app !== null && $user !== null) {
if (!PhabricatorApplication::isClassInstalledForViewer($app, $user)) {
unset($editors[$key]);
}
}
}
return $editors;
}
private function getAllTags(PhabricatorUser $user = null) {
$tags = array();
foreach ($this->getAllEditorsWithTags($user) as $editor) {
$tags += $editor->getMailTagsMap();
}
return $tags;
}
private function buildMailTagControl(
$control_label,
array $tags,
array $prefs) {
$value_email = PhabricatorEmailTagsSetting::VALUE_EMAIL;
$value_notify = PhabricatorEmailTagsSetting::VALUE_NOTIFY;
$value_ignore = PhabricatorEmailTagsSetting::VALUE_IGNORE;
$content = array();
foreach ($tags as $key => $label) {
$select = AphrontFormSelectControl::renderSelectTag(
(int)idx($prefs, $key, $value_email),
array(
$value_email => pht("\xE2\x9A\xAB Email"),
$value_notify => pht("\xE2\x97\x90 Notify"),
$value_ignore => pht("\xE2\x9A\xAA Ignore"),
),
array(
'name' => 'mailtags['.$key.']',
));
$content[] = phutil_tag(
'div',
array(
'class' => 'psb',
),
array(
$select,
' ',
$label,
));
}
$control = new AphrontFormStaticControl();
$control->setLabel($control_label);
$control->setValue($content);
return $control;
}
}
diff --git a/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php b/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php
index e380248a8..121548720 100644
--- a/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php
@@ -1,137 +1,141 @@
<?php
final class PhabricatorExternalAccountsSettingsPanel
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'external';
}
public function getPanelName() {
return pht('External Accounts');
}
+ public function getPanelMenuIcon() {
+ return 'fa-users';
+ }
+
public function getPanelGroupKey() {
return PhabricatorSettingsAuthenticationPanelGroup::PANELGROUPKEY;
}
public function processRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$providers = PhabricatorAuthProvider::getAllProviders();
$accounts = id(new PhabricatorExternalAccountQuery())
->setViewer($viewer)
->withUserPHIDs(array($viewer->getPHID()))
->needImages(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->execute();
$linked_head = pht('Linked Accounts and Authentication');
$linked = id(new PHUIObjectItemListView())
->setUser($viewer)
->setNoDataString(pht('You have no linked accounts.'));
$login_accounts = 0;
foreach ($accounts as $account) {
if ($account->isUsableForLogin()) {
$login_accounts++;
}
}
foreach ($accounts as $account) {
$item = new PHUIObjectItemView();
$provider = idx($providers, $account->getProviderKey());
if ($provider) {
$item->setHeader($provider->getProviderName());
$can_unlink = $provider->shouldAllowAccountUnlink();
if (!$can_unlink) {
$item->addAttribute(pht('Permanently Linked'));
}
} else {
$item->setHeader(
pht('Unknown Account ("%s")', $account->getProviderKey()));
$can_unlink = true;
}
$can_login = $account->isUsableForLogin();
if (!$can_login) {
$item->addAttribute(
pht(
'Disabled (an administrator has disabled login for this '.
'account provider).'));
}
$can_unlink = $can_unlink && (!$can_login || ($login_accounts > 1));
$can_refresh = $provider && $provider->shouldAllowAccountRefresh();
if ($can_refresh) {
$item->addAction(
id(new PHUIListItemView())
->setIcon('fa-refresh')
->setHref('/auth/refresh/'.$account->getProviderKey().'/'));
}
$item->addAction(
id(new PHUIListItemView())
->setIcon('fa-times')
->setWorkflow(true)
->setDisabled(!$can_unlink)
->setHref('/auth/unlink/'.$account->getProviderKey().'/'));
if ($provider) {
$provider->willRenderLinkedAccount($viewer, $item, $account);
}
$linked->addItem($item);
}
$linkable_head = pht('Add External Account');
$linkable = id(new PHUIObjectItemListView())
->setUser($viewer)
->setNoDataString(
pht('Your account is linked with all available providers.'));
$accounts = mpull($accounts, null, 'getProviderKey');
$providers = PhabricatorAuthProvider::getAllEnabledProviders();
$providers = msort($providers, 'getProviderName');
foreach ($providers as $key => $provider) {
if (isset($accounts[$key])) {
continue;
}
if (!$provider->shouldAllowAccountLink()) {
continue;
}
$link_uri = '/auth/link/'.$provider->getProviderKey().'/';
$item = id(new PHUIObjectItemView())
->setHeader($provider->getProviderName())
->setHref($link_uri)
->addAction(
id(new PHUIListItemView())
->setIcon('fa-link')
->setHref($link_uri));
$linkable->addItem($item);
}
$linked_box = $this->newBox($linked_head, $linked);
$linkable_box = $this->newBox($linkable_head, $linkable);
return array(
$linked_box,
$linkable_box,
);
}
}
diff --git a/src/applications/settings/panel/PhabricatorAccountSettingsPanel.php b/src/applications/settings/panel/PhabricatorLanguageSettingsPanel.php
similarity index 57%
rename from src/applications/settings/panel/PhabricatorAccountSettingsPanel.php
rename to src/applications/settings/panel/PhabricatorLanguageSettingsPanel.php
index a7eb3ad09..39bd5deac 100644
--- a/src/applications/settings/panel/PhabricatorAccountSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorLanguageSettingsPanel.php
@@ -1,24 +1,32 @@
<?php
-final class PhabricatorAccountSettingsPanel
+final class PhabricatorLanguageSettingsPanel
extends PhabricatorEditEngineSettingsPanel {
- const PANELKEY = 'account';
+ const PANELKEY = 'language';
public function getPanelName() {
- return pht('Account');
+ return pht('Language');
+ }
+
+ public function getPanelMenuIcon() {
+ return 'fa-globe';
}
public function getPanelGroupKey() {
return PhabricatorSettingsAccountPanelGroup::PANELGROUPKEY;
}
public function isManagementPanel() {
return true;
}
public function isTemplatePanel() {
return true;
}
+ public function isMultiFactorEnrollmentPanel() {
+ return true;
+ }
+
}
diff --git a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php
index 5fada0bbe..6809b5133 100644
--- a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php
@@ -1,320 +1,468 @@
<?php
final class PhabricatorMultiFactorSettingsPanel
extends PhabricatorSettingsPanel {
+ private $isEnrollment;
+
public function getPanelKey() {
return 'multifactor';
}
public function getPanelName() {
return pht('Multi-Factor Auth');
}
+ public function getPanelMenuIcon() {
+ return 'fa-lock';
+ }
+
public function getPanelGroupKey() {
return PhabricatorSettingsAuthenticationPanelGroup::PANELGROUPKEY;
}
+ public function isMultiFactorEnrollmentPanel() {
+ return true;
+ }
+
+ public function setIsEnrollment($is_enrollment) {
+ $this->isEnrollment = $is_enrollment;
+ return $this;
+ }
+
+ public function getIsEnrollment() {
+ return $this->isEnrollment;
+ }
+
public function processRequest(AphrontRequest $request) {
- if ($request->getExists('new')) {
+ if ($request->getExists('new') || $request->getExists('providerPHID')) {
return $this->processNew($request);
}
if ($request->getExists('edit')) {
return $this->processEdit($request);
}
if ($request->getExists('delete')) {
return $this->processDelete($request);
}
$user = $this->getUser();
$viewer = $request->getUser();
- $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
- 'userPHID = %s',
- $user->getPHID());
+ $factors = id(new PhabricatorAuthFactorConfigQuery())
+ ->setViewer($viewer)
+ ->withUserPHIDs(array($user->getPHID()))
+ ->execute();
+ $factors = msort($factors, 'newSortVector');
$rows = array();
$rowc = array();
$highlight_id = $request->getInt('id');
foreach ($factors as $factor) {
-
- $impl = $factor->getImplementation();
- if ($impl) {
- $type = $impl->getFactorName();
- } else {
- $type = $factor->getFactorKey();
- }
+ $provider = $factor->getFactorProvider();
if ($factor->getID() == $highlight_id) {
$rowc[] = 'highlighted';
} else {
$rowc[] = null;
}
+ $status = $provider->newStatus();
+ $status_icon = $status->getFactorIcon();
+ $status_color = $status->getFactorColor();
+
+ $icon = id(new PHUIIconView())
+ ->setIcon("{$status_icon} {$status_color}")
+ ->setTooltip(pht('Provider: %s', $status->getName()));
+
+ $details = $provider->getConfigurationListDetails($factor, $viewer);
+
$rows[] = array(
+ $icon,
javelin_tag(
'a',
array(
'href' => $this->getPanelURI('?edit='.$factor->getID()),
'sigil' => 'workflow',
),
$factor->getFactorName()),
- $type,
+ $provider->getFactor()->getFactorShortName(),
+ $provider->getDisplayName(),
+ $details,
phabricator_datetime($factor->getDateCreated(), $viewer),
javelin_tag(
'a',
array(
'href' => $this->getPanelURI('?delete='.$factor->getID()),
'sigil' => 'workflow',
'class' => 'small button button-grey',
),
pht('Remove')),
);
}
$table = new AphrontTableView($rows);
$table->setNoDataString(
pht("You haven't added any authentication factors to your account yet."));
$table->setHeaders(
array(
+ null,
pht('Name'),
pht('Type'),
+ pht('Provider'),
+ pht('Details'),
pht('Created'),
- '',
+ null,
));
$table->setColumnClasses(
array(
+ null,
'wide pri',
- '',
+ null,
+ null,
+ null,
'right',
'action',
));
$table->setRowClasses($rowc);
$table->setDeviceVisibility(
array(
true,
+ true,
+ false,
+ false,
false,
false,
true,
));
$help_uri = PhabricatorEnv::getDoclink(
'User Guide: Multi-Factor Authentication');
$buttons = array();
+ // If we're enrolling a new account in MFA, provide a small visual hint
+ // that this is the button they want to click.
+ if ($this->getIsEnrollment()) {
+ $add_color = PHUIButtonView::BLUE;
+ } else {
+ $add_color = PHUIButtonView::GREY;
+ }
+
+ $can_add = (bool)$this->loadActiveMFAProviders();
+
$buttons[] = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-plus')
->setText(pht('Add Auth Factor'))
->setHref($this->getPanelURI('?new=true'))
->setWorkflow(true)
- ->setColor(PHUIButtonView::GREY);
+ ->setDisabled(!$can_add)
+ ->setColor($add_color);
$buttons[] = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-book')
->setText(pht('Help'))
->setHref($help_uri)
->setColor(PHUIButtonView::GREY);
return $this->newBox(pht('Authentication Factors'), $table, $buttons);
}
private function processNew(AphrontRequest $request) {
$viewer = $request->getUser();
$user = $this->getUser();
+ $cancel_uri = $this->getPanelURI();
+
+ // Check that we have providers before we send the user through the MFA
+ // gate, so you don't authenticate and then immediately get roadblocked.
+ $providers = $this->loadActiveMFAProviders();
+
+ if (!$providers) {
+ return $this->newDialog()
+ ->setTitle(pht('No MFA Providers'))
+ ->appendParagraph(
+ pht(
+ 'This install does not have any active MFA providers configured. '.
+ 'At least one provider must be configured and active before you '.
+ 'can add new MFA factors.'))
+ ->addCancelButton($cancel_uri);
+ }
+
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
- $this->getPanelURI());
-
- $factors = PhabricatorAuthFactor::getAllFactors();
+ $cancel_uri);
- $form = id(new AphrontFormView())
- ->setUser($viewer);
-
- $type = $request->getStr('type');
- if (empty($factors[$type]) || !$request->isFormPost()) {
- $factor = null;
+ $selected_phid = $request->getStr('providerPHID');
+ if (empty($providers[$selected_phid])) {
+ $selected_provider = null;
} else {
- $factor = $factors[$type];
+ $selected_provider = $providers[$selected_phid];
+
+ // Only let the user continue creating a factor for a given provider if
+ // they actually pass the provider's checks.
+ if (!$selected_provider->canCreateNewConfiguration($viewer)) {
+ $selected_provider = null;
+ }
}
- $dialog = id(new AphrontDialogView())
- ->setUser($viewer)
- ->addHiddenInput('new', true);
-
- if ($factor === null) {
- $choice_control = id(new AphrontFormRadioButtonControl())
- ->setName('type')
- ->setValue(key($factors));
-
- foreach ($factors as $available_factor) {
- $choice_control->addButton(
- $available_factor->getFactorKey(),
- $available_factor->getFactorName(),
- $available_factor->getFactorDescription());
+ if (!$selected_provider) {
+ $menu = id(new PHUIObjectItemListView())
+ ->setViewer($viewer)
+ ->setBig(true)
+ ->setFlush(true);
+
+ foreach ($providers as $provider_phid => $provider) {
+ $provider_uri = id(new PhutilURI($this->getPanelURI()))
+ ->setQueryParam('providerPHID', $provider_phid);
+
+ $is_enabled = $provider->canCreateNewConfiguration($viewer);
+
+ $item = id(new PHUIObjectItemView())
+ ->setHeader($provider->getDisplayName())
+ ->setImageIcon($provider->newIconView())
+ ->addAttribute($provider->getDisplayDescription());
+
+ if ($is_enabled) {
+ $item
+ ->setHref($provider_uri)
+ ->setClickable(true);
+ } else {
+ $item->setDisabled(true);
+ }
+
+ $create_description = $provider->getConfigurationCreateDescription(
+ $viewer);
+ if ($create_description) {
+ $item->appendChild($create_description);
+ }
+
+ $menu->addItem($item);
}
- $dialog->appendParagraph(
- pht(
- 'Adding an additional authentication factor improves the security '.
- 'of your account. Choose the type of factor to add:'));
+ return $this->newDialog()
+ ->setTitle(pht('Choose Factor Type'))
+ ->appendChild($menu)
+ ->addCancelButton($cancel_uri);
+ }
- $form
- ->appendChild($choice_control);
+ // NOTE: Beyond providing guidance, this step is also providing a CSRF gate
+ // on this endpoint, since prompting the user to respond to a challenge
+ // sometimes requires us to push a challenge to them as a side effect (for
+ // example, with SMS).
+ if (!$request->isFormPost() || !$request->getBool('mfa.start')) {
+ $enroll = $selected_provider->getEnrollMessage();
+ if (!strlen($enroll)) {
+ $enroll = $selected_provider->getEnrollDescription($viewer);
+ }
- } else {
- $dialog->addHiddenInput('type', $type);
+ return $this->newDialog()
+ ->addHiddenInput('providerPHID', $selected_provider->getPHID())
+ ->addHiddenInput('mfa.start', 1)
+ ->setTitle(pht('Add Authentication Factor'))
+ ->appendChild(new PHUIRemarkupView($viewer, $enroll))
+ ->addCancelButton($cancel_uri)
+ ->addSubmitButton($selected_provider->getEnrollButtonText($viewer));
+ }
- $config = $factor->processAddFactorForm(
- $form,
- $request,
- $user);
+ $form = id(new AphrontFormView())
+ ->setViewer($viewer);
+
+ if ($request->getBool('mfa.enroll')) {
+ // Subject users to rate limiting so that it's difficult to add factors
+ // by pure brute force. This is normally not much of an attack, but push
+ // factor types may have side effects.
+ PhabricatorSystemActionEngine::willTakeAction(
+ array($viewer->getPHID()),
+ new PhabricatorAuthNewFactorAction(),
+ 1);
+ } else {
+ // Test the limit before showing the user a form, so we don't give them
+ // a form which can never possibly work because it will always hit rate
+ // limiting.
+ PhabricatorSystemActionEngine::willTakeAction(
+ array($viewer->getPHID()),
+ new PhabricatorAuthNewFactorAction(),
+ 0);
+ }
- if ($config) {
- $config->save();
+ $config = $selected_provider->processAddFactorForm(
+ $form,
+ $request,
+ $user);
+
+ if ($config) {
+ // If the user added a factor, give them a rate limiting point back.
+ PhabricatorSystemActionEngine::willTakeAction(
+ array($viewer->getPHID()),
+ new PhabricatorAuthNewFactorAction(),
+ -1);
+
+ $config->save();
+
+ // If we used a temporary token to handle synchronizing the factor,
+ // revoke it now.
+ $sync_token = $config->getMFASyncToken();
+ if ($sync_token) {
+ $sync_token->revokeToken();
+ }
- $log = PhabricatorUserLog::initializeNewLog(
- $viewer,
- $user->getPHID(),
- PhabricatorUserLog::ACTION_MULTI_ADD);
- $log->save();
+ $log = PhabricatorUserLog::initializeNewLog(
+ $viewer,
+ $user->getPHID(),
+ PhabricatorUserLog::ACTION_MULTI_ADD);
+ $log->save();
- $user->updateMultiFactorEnrollment();
+ $user->updateMultiFactorEnrollment();
- // Terminate other sessions so they must log in and survive the
- // multi-factor auth check.
+ // Terminate other sessions so they must log in and survive the
+ // multi-factor auth check.
- id(new PhabricatorAuthSessionEngine())->terminateLoginSessions(
- $user,
- new PhutilOpaqueEnvelope(
- $request->getCookie(PhabricatorCookies::COOKIE_SESSION)));
+ id(new PhabricatorAuthSessionEngine())->terminateLoginSessions(
+ $user,
+ new PhutilOpaqueEnvelope(
+ $request->getCookie(PhabricatorCookies::COOKIE_SESSION)));
- return id(new AphrontRedirectResponse())
- ->setURI($this->getPanelURI('?id='.$config->getID()));
- }
+ return id(new AphrontRedirectResponse())
+ ->setURI($this->getPanelURI('?id='.$config->getID()));
}
- $dialog
+ return $this->newDialog()
+ ->addHiddenInput('providerPHID', $selected_provider->getPHID())
+ ->addHiddenInput('mfa.start', 1)
+ ->addHiddenInput('mfa.enroll', 1)
->setWidth(AphrontDialogView::WIDTH_FORM)
->setTitle(pht('Add Authentication Factor'))
->appendChild($form->buildLayoutView())
->addSubmitButton(pht('Continue'))
- ->addCancelButton($this->getPanelURI());
-
- return id(new AphrontDialogResponse())
- ->setDialog($dialog);
+ ->addCancelButton($cancel_uri);
}
private function processEdit(AphrontRequest $request) {
$viewer = $request->getUser();
$user = $this->getUser();
$factor = id(new PhabricatorAuthFactorConfig())->loadOneWhere(
'id = %d AND userPHID = %s',
$request->getInt('edit'),
$user->getPHID());
if (!$factor) {
return new Aphront404Response();
}
$e_name = true;
$errors = array();
if ($request->isFormPost()) {
$name = $request->getStr('name');
if (!strlen($name)) {
$e_name = pht('Required');
$errors[] = pht(
'Authentication factors must have a name to identify them.');
}
if (!$errors) {
$factor->setFactorName($name);
$factor->save();
$user->updateMultiFactorEnrollment();
return id(new AphrontRedirectResponse())
->setURI($this->getPanelURI('?id='.$factor->getID()));
}
} else {
$name = $factor->getFactorName();
}
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild(
id(new AphrontFormTextControl())
->setName('name')
->setLabel(pht('Name'))
->setValue($name)
->setError($e_name));
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->addHiddenInput('edit', $factor->getID())
->setTitle(pht('Edit Authentication Factor'))
->setErrors($errors)
->appendChild($form->buildLayoutView())
->addSubmitButton(pht('Save'))
->addCancelButton($this->getPanelURI());
return id(new AphrontDialogResponse())
->setDialog($dialog);
}
private function processDelete(AphrontRequest $request) {
$viewer = $request->getUser();
$user = $this->getUser();
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
$this->getPanelURI());
$factor = id(new PhabricatorAuthFactorConfig())->loadOneWhere(
'id = %d AND userPHID = %s',
$request->getInt('delete'),
$user->getPHID());
if (!$factor) {
return new Aphront404Response();
}
if ($request->isFormPost()) {
$factor->delete();
$log = PhabricatorUserLog::initializeNewLog(
$viewer,
$user->getPHID(),
PhabricatorUserLog::ACTION_MULTI_REMOVE);
$log->save();
$user->updateMultiFactorEnrollment();
return id(new AphrontRedirectResponse())
->setURI($this->getPanelURI());
}
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->addHiddenInput('delete', $factor->getID())
->setTitle(pht('Delete Authentication Factor'))
->appendParagraph(
pht(
'Really remove the authentication factor %s from your account?',
phutil_tag('strong', array(), $factor->getFactorName())))
->addSubmitButton(pht('Remove Factor'))
->addCancelButton($this->getPanelURI());
return id(new AphrontDialogResponse())
->setDialog($dialog);
}
+ private function loadActiveMFAProviders() {
+ $viewer = $this->getViewer();
+
+ $providers = id(new PhabricatorAuthFactorProviderQuery())
+ ->setViewer($viewer)
+ ->withStatuses(
+ array(
+ PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE,
+ ))
+ ->execute();
+
+ $providers = mpull($providers, null, 'getPHID');
+ $providers = msortv($providers, 'newSortVector');
+
+ return $providers;
+ }
+
}
diff --git a/src/applications/settings/panel/PhabricatorNotificationsSettingsPanel.php b/src/applications/settings/panel/PhabricatorNotificationsSettingsPanel.php
index 797bcafcb..d0165dc3f 100644
--- a/src/applications/settings/panel/PhabricatorNotificationsSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorNotificationsSettingsPanel.php
@@ -1,179 +1,183 @@
<?php
final class PhabricatorNotificationsSettingsPanel
extends PhabricatorSettingsPanel {
public function isEnabled() {
$servers = PhabricatorNotificationServerRef::getEnabledAdminServers();
if (!$servers) {
return false;
}
return PhabricatorApplication::isClassInstalled(
'PhabricatorNotificationsApplication');
}
public function getPanelKey() {
return 'notifications';
}
public function getPanelName() {
return pht('Notifications');
}
+ public function getPanelMenuIcon() {
+ return 'fa-bell-o';
+ }
+
public function getPanelGroupKey() {
return PhabricatorSettingsApplicationsPanelGroup::PANELGROUPKEY;
}
public function processRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$preferences = $this->getPreferences();
$notifications_key = PhabricatorNotificationsSetting::SETTINGKEY;
$notifications_value = $preferences->getSettingValue($notifications_key);
if ($request->isFormPost()) {
$this->writeSetting(
$preferences,
$notifications_key,
$request->getInt($notifications_key));
return id(new AphrontRedirectResponse())
->setURI($this->getPanelURI('?saved=true'));
}
$title = pht('Notifications');
$control_id = celerity_generate_unique_node_id();
$status_id = celerity_generate_unique_node_id();
$browser_status_id = celerity_generate_unique_node_id();
$cancel_ask = pht(
'The dialog asking for permission to send desktop notifications was '.
'closed without granting permission. Only application notifications '.
'will be sent.');
$accept_ask = pht(
'Click "Save Preference" to persist these changes.');
$reject_ask = pht(
'Permission for desktop notifications was denied. Only application '.
'notifications will be sent.');
$no_support = pht(
'This web browser does not support desktop notifications. Only '.
'application notifications will be sent for this browser regardless of '.
'this preference.');
$default_status = phutil_tag(
'span',
array(),
array(
pht('This browser has not yet granted permission to send desktop '.
'notifications for this Phabricator instance.'),
phutil_tag('br'),
phutil_tag('br'),
javelin_tag(
'button',
array(
'sigil' => 'desktop-notifications-permission-button',
'class' => 'green',
),
pht('Grant Permission')),
));
$granted_status = phutil_tag(
'span',
array(),
pht('This browser has been granted permission to send desktop '.
'notifications for this Phabricator instance.'));
$denied_status = phutil_tag(
'span',
array(),
pht('This browser has denied permission to send desktop notifications '.
'for this Phabricator instance. Consult your browser settings / '.
'documentation to figure out how to clear this setting, do so, '.
'and then re-visit this page to grant permission.'));
$message_id = celerity_generate_unique_node_id();
$message_container = phutil_tag(
'span',
array(
'id' => $message_id,
));
$saved_box = null;
if ($request->getBool('saved')) {
$saved_box = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->appendChild(pht('Changes saved.'));
}
$status_box = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->setID($status_id)
->setIsHidden(true)
->appendChild($message_container);
$status_box = id(new PHUIBoxView())
->addClass('mll mlr')
->appendChild($status_box);
$control_config = array(
'controlID' => $control_id,
'statusID' => $status_id,
'messageID' => $message_id,
'browserStatusID' => $browser_status_id,
'defaultMode' => 0,
'desktop' => 1,
'desktopOnly' => 2,
'cancelAsk' => $cancel_ask,
'grantedAsk' => $accept_ask,
'deniedAsk' => $reject_ask,
'defaultStatus' => $default_status,
'deniedStatus' => $denied_status,
'grantedStatus' => $granted_status,
'noSupport' => $no_support,
);
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild(
id(new AphrontFormSelectControl())
->setLabel($title)
->setControlID($control_id)
->setName($notifications_key)
->setValue($notifications_value)
->setOptions(PhabricatorNotificationsSetting::getOptionsMap())
->setCaption(
pht(
'Phabricator can send real-time notifications to your web browser '.
'or to your desktop. Select where you want to receive these '.
'real-time updates.'))
->initBehavior(
'desktop-notifications-control',
$control_config))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Save Preference')));
$button = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-send-o')
->setWorkflow(true)
->setText(pht('Send Test Notification'))
->setHref('/notification/test/')
->setColor(PHUIButtonView::GREY);
$form_content = array($saved_box, $status_box, $form);
$form_box = $this->newBox(
pht('Notifications'), $form_content, array($button));
$browser_status_box = id(new PHUIInfoView())
->setID($browser_status_id)
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->setIsHidden(true)
->appendChild($default_status);
return array(
$form_box,
$browser_status_box,
);
}
}
diff --git a/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php b/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php
index 79d7610f2..37393d5d4 100644
--- a/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php
@@ -1,218 +1,222 @@
<?php
final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'password';
}
public function getPanelName() {
return pht('Password');
}
+ public function getPanelMenuIcon() {
+ return 'fa-key';
+ }
+
public function getPanelGroupKey() {
return PhabricatorSettingsAuthenticationPanelGroup::PANELGROUPKEY;
}
public function isEnabled() {
// There's no sense in showing a change password panel if this install
// doesn't support password authentication.
if (!PhabricatorPasswordAuthProvider::getPasswordProvider()) {
return false;
}
return true;
}
public function processRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$user = $this->getUser();
$content_source = PhabricatorContentSource::newFromRequest($request);
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
'/settings/');
$min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length');
$min_len = (int)$min_len;
// NOTE: Users can also change passwords through the separate "set/reset"
// interface which is reached by logging in with a one-time token after
// registration or password reset. If this flow changes, that flow may
// also need to change.
$account_type = PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT;
$password_objects = id(new PhabricatorAuthPasswordQuery())
->setViewer($viewer)
->withObjectPHIDs(array($user->getPHID()))
->withPasswordTypes(array($account_type))
->withIsRevoked(false)
->execute();
if ($password_objects) {
$password_object = head($password_objects);
} else {
$password_object = PhabricatorAuthPassword::initializeNewPassword(
$user,
$account_type);
}
$e_old = true;
$e_new = true;
$e_conf = true;
$errors = array();
if ($request->isFormPost()) {
// Rate limit guesses about the old password. This page requires MFA and
// session compromise already, so this is mostly just to stop researchers
// from reporting this as a vulnerability.
PhabricatorSystemActionEngine::willTakeAction(
array($viewer->getPHID()),
new PhabricatorAuthChangePasswordAction(),
1);
$envelope = new PhutilOpaqueEnvelope($request->getStr('old_pw'));
$engine = id(new PhabricatorAuthPasswordEngine())
->setViewer($viewer)
->setContentSource($content_source)
->setPasswordType($account_type)
->setObject($user);
if (!strlen($envelope->openEnvelope())) {
$errors[] = pht('You must enter your current password.');
$e_old = pht('Required');
} else if (!$engine->isValidPassword($envelope)) {
$errors[] = pht('The old password you entered is incorrect.');
$e_old = pht('Invalid');
} else {
$e_old = null;
// Refund the user an action credit for getting the password right.
PhabricatorSystemActionEngine::willTakeAction(
array($viewer->getPHID()),
new PhabricatorAuthChangePasswordAction(),
-1);
}
$pass = $request->getStr('new_pw');
$conf = $request->getStr('conf_pw');
$password_envelope = new PhutilOpaqueEnvelope($pass);
$confirm_envelope = new PhutilOpaqueEnvelope($conf);
try {
$engine->checkNewPassword($password_envelope, $confirm_envelope);
$e_new = null;
$e_conf = null;
} catch (PhabricatorAuthPasswordException $ex) {
$errors[] = $ex->getMessage();
$e_new = $ex->getPasswordError();
$e_conf = $ex->getConfirmError();
}
if (!$errors) {
$password_object
->setPassword($password_envelope, $user)
->save();
$next = $this->getPanelURI('?saved=true');
id(new PhabricatorAuthSessionEngine())->terminateLoginSessions(
$user,
new PhutilOpaqueEnvelope(
$request->getCookie(PhabricatorCookies::COOKIE_SESSION)));
return id(new AphrontRedirectResponse())->setURI($next);
}
}
if ($password_object->getID()) {
try {
$can_upgrade = $password_object->canUpgrade();
} catch (PhabricatorPasswordHasherUnavailableException $ex) {
$can_upgrade = false;
$errors[] = pht(
'Your password is currently hashed using an algorithm which is '.
'no longer available on this install.');
$errors[] = pht(
'Because the algorithm implementation is missing, your password '.
'can not be used or updated.');
$errors[] = pht(
'To set a new password, request a password reset link from the '.
'login screen and then follow the instructions.');
}
if ($can_upgrade) {
$errors[] = pht(
'The strength of your stored password hash can be upgraded. '.
'To upgrade, either: log out and log in using your password; or '.
'change your password.');
}
}
$len_caption = null;
if ($min_len) {
$len_caption = pht('Minimum password length: %d characters.', $min_len);
}
$form = id(new AphrontFormView())
->setViewer($viewer)
->appendChild(
id(new AphrontFormPasswordControl())
->setLabel(pht('Old Password'))
->setError($e_old)
->setName('old_pw'))
->appendChild(
id(new AphrontFormPasswordControl())
->setDisableAutocomplete(true)
->setLabel(pht('New Password'))
->setError($e_new)
->setName('new_pw'))
->appendChild(
id(new AphrontFormPasswordControl())
->setDisableAutocomplete(true)
->setLabel(pht('Confirm Password'))
->setCaption($len_caption)
->setError($e_conf)
->setName('conf_pw'))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Change Password')));
$properties = id(new PHUIPropertyListView());
$properties->addProperty(
pht('Current Algorithm'),
PhabricatorPasswordHasher::getCurrentAlgorithmName(
$password_object->newPasswordEnvelope()));
$properties->addProperty(
pht('Best Available Algorithm'),
PhabricatorPasswordHasher::getBestAlgorithmName());
$info_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->appendChild(
pht('Changing your password will terminate any other outstanding '.
'login sessions.'));
$algo_box = $this->newBox(pht('Password Algorithms'), $properties);
$form_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Change Password'))
->setFormSaved($request->getStr('saved'))
->setFormErrors($errors)
->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
->setForm($form);
return array(
$form_box,
$algo_box,
$info_view,
);
}
}
diff --git a/src/applications/settings/panel/PhabricatorSSHKeysSettingsPanel.php b/src/applications/settings/panel/PhabricatorSSHKeysSettingsPanel.php
index 13944411e..131f60297 100644
--- a/src/applications/settings/panel/PhabricatorSSHKeysSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorSSHKeysSettingsPanel.php
@@ -1,51 +1,55 @@
<?php
final class PhabricatorSSHKeysSettingsPanel extends PhabricatorSettingsPanel {
public function isManagementPanel() {
if ($this->getUser()->getIsMailingList()) {
return false;
}
return true;
}
public function getPanelKey() {
return 'ssh';
}
public function getPanelName() {
return pht('SSH Public Keys');
}
+ public function getPanelMenuIcon() {
+ return 'fa-file-text-o';
+ }
+
public function getPanelGroupKey() {
return PhabricatorSettingsAuthenticationPanelGroup::PANELGROUPKEY;
}
public function processRequest(AphrontRequest $request) {
$user = $this->getUser();
$viewer = $request->getUser();
$keys = id(new PhabricatorAuthSSHKeyQuery())
->setViewer($viewer)
->withObjectPHIDs(array($user->getPHID()))
->withIsActive(true)
->execute();
$table = id(new PhabricatorAuthSSHKeyTableView())
->setUser($viewer)
->setKeys($keys)
->setCanEdit(true)
->setNoDataString(pht("You haven't added any SSH Public Keys."));
$panel = new PHUIObjectBoxView();
$header = new PHUIHeaderView();
$ssh_actions = PhabricatorAuthSSHKeyTableView::newKeyActionsMenu(
$viewer,
$user);
return $this->newBox(pht('SSH Public Keys'), $table, array($ssh_actions));
}
}
diff --git a/src/applications/settings/panel/PhabricatorSessionsSettingsPanel.php b/src/applications/settings/panel/PhabricatorSessionsSettingsPanel.php
index 314d68f69..fb10572e1 100644
--- a/src/applications/settings/panel/PhabricatorSessionsSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorSessionsSettingsPanel.php
@@ -1,139 +1,143 @@
<?php
final class PhabricatorSessionsSettingsPanel extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'sessions';
}
public function getPanelName() {
return pht('Sessions');
}
+ public function getPanelMenuIcon() {
+ return 'fa-user';
+ }
+
public function getPanelGroupKey() {
return PhabricatorSettingsLogsPanelGroup::PANELGROUPKEY;
}
public function isEnabled() {
return true;
}
public function processRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$accounts = id(new PhabricatorExternalAccountQuery())
->setViewer($viewer)
->withUserPHIDs(array($viewer->getPHID()))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->execute();
$identity_phids = mpull($accounts, 'getPHID');
$identity_phids[] = $viewer->getPHID();
$sessions = id(new PhabricatorAuthSessionQuery())
->setViewer($viewer)
->withIdentityPHIDs($identity_phids)
->execute();
$handles = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs($identity_phids)
->execute();
$current_key = PhabricatorAuthSession::newSessionDigest(
new PhutilOpaqueEnvelope(
$request->getCookie(PhabricatorCookies::COOKIE_SESSION)));
$rows = array();
$rowc = array();
foreach ($sessions as $session) {
$is_current = phutil_hashes_are_identical(
$session->getSessionKey(),
$current_key);
if ($is_current) {
$rowc[] = 'highlighted';
$button = phutil_tag(
'a',
array(
'class' => 'small button button-grey disabled',
),
pht('Current'));
} else {
$rowc[] = null;
$button = javelin_tag(
'a',
array(
'href' => '/auth/session/terminate/'.$session->getID().'/',
'class' => 'small button button-grey',
'sigil' => 'workflow',
),
pht('Terminate'));
}
$hisec = ($session->getHighSecurityUntil() - time());
$rows[] = array(
$handles[$session->getUserPHID()]->renderLink(),
substr($session->getSessionKey(), 0, 6),
$session->getType(),
($hisec > 0)
? phutil_format_relative_time($hisec)
: null,
phabricator_datetime($session->getSessionStart(), $viewer),
phabricator_date($session->getSessionExpires(), $viewer),
$button,
);
}
$table = new AphrontTableView($rows);
$table->setNoDataString(pht("You don't have any active sessions."));
$table->setRowClasses($rowc);
$table->setHeaders(
array(
pht('Identity'),
pht('Session'),
pht('Type'),
pht('HiSec'),
pht('Created'),
pht('Expires'),
pht(''),
));
$table->setColumnClasses(
array(
'wide',
'n',
'',
'right',
'right',
'right',
'action',
));
$buttons = array();
$buttons[] = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-warning')
->setText(pht('Terminate All Sessions'))
->setHref('/auth/session/terminate/all/')
->setWorkflow(true)
->setColor(PHUIButtonView::RED);
$hisec = ($viewer->getSession()->getHighSecurityUntil() - time());
if ($hisec > 0) {
$buttons[] = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-lock')
->setText(pht('Leave High Security'))
->setHref('/auth/session/downgrade/')
->setWorkflow(true)
->setColor(PHUIButtonView::RED);
}
return $this->newBox(pht('Active Login Sessions'), $table, $buttons);
}
}
diff --git a/src/applications/settings/panel/PhabricatorSettingsPanel.php b/src/applications/settings/panel/PhabricatorSettingsPanel.php
index 19ac6fec6..e2efd9209 100644
--- a/src/applications/settings/panel/PhabricatorSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorSettingsPanel.php
@@ -1,301 +1,322 @@
<?php
/**
* Defines a settings panel. Settings panels appear in the Settings application,
* and behave like lightweight controllers -- generally, they render some sort
* of form with options in it, and then update preferences when the user
* submits the form. By extending this class, you can add new settings
* panels.
*
* @task config Panel Configuration
* @task panel Panel Implementation
* @task internal Internals
*/
abstract class PhabricatorSettingsPanel extends Phobject {
private $user;
private $viewer;
private $controller;
private $navigation;
private $overrideURI;
private $preferences;
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function getUser() {
return $this->user;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setOverrideURI($override_uri) {
$this->overrideURI = $override_uri;
return $this;
}
final public function setController(PhabricatorController $controller) {
$this->controller = $controller;
return $this;
}
final public function getController() {
return $this->controller;
}
final public function setNavigation(AphrontSideNavFilterView $navigation) {
$this->navigation = $navigation;
return $this;
}
final public function getNavigation() {
return $this->navigation;
}
public function setPreferences(PhabricatorUserPreferences $preferences) {
$this->preferences = $preferences;
return $this;
}
public function getPreferences() {
return $this->preferences;
}
final public static function getAllPanels() {
$panels = id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getPanelKey')
->execute();
return msortv($panels, 'getPanelOrderVector');
}
final public static function getAllDisplayPanels() {
$panels = array();
$groups = PhabricatorSettingsPanelGroup::getAllPanelGroupsWithPanels();
foreach ($groups as $group) {
foreach ($group->getPanels() as $key => $panel) {
$panels[$key] = $panel;
}
}
return $panels;
}
final public function getPanelGroup() {
$group_key = $this->getPanelGroupKey();
$groups = PhabricatorSettingsPanelGroup::getAllPanelGroupsWithPanels();
$group = idx($groups, $group_key);
if (!$group) {
throw new Exception(
pht(
'No settings panel group with key "%s" exists!',
$group_key));
}
return $group;
}
/* -( Panel Configuration )------------------------------------------------ */
/**
* Return a unique string used in the URI to identify this panel, like
* "example".
*
* @return string Unique panel identifier (used in URIs).
* @task config
*/
public function getPanelKey() {
return $this->getPhobjectClassConstant('PANELKEY');
}
/**
* Return a human-readable description of the panel's contents, like
* "Example Settings".
*
* @return string Human-readable panel name.
* @task config
*/
abstract public function getPanelName();
+ /**
+ * Return an icon for the panel in the menu.
+ *
+ * @return string Icon identifier.
+ * @task config
+ */
+ public function getPanelMenuIcon() {
+ return 'fa-wrench';
+ }
+
/**
* Return a panel group key constant for this panel.
*
* @return const Panel group key.
* @task config
*/
abstract public function getPanelGroupKey();
/**
* Return false to prevent this panel from being displayed or used. You can
* do, e.g., configuration checks here, to determine if the feature your
* panel controls is unavailable in this install. By default, all panels are
* enabled.
*
* @return bool True if the panel should be shown.
* @task config
*/
public function isEnabled() {
return true;
}
/**
* Return true if this panel is available to users while editing their own
* settings.
*
* @return bool True to enable management on behalf of a user.
* @task config
*/
public function isUserPanel() {
return true;
}
/**
* Return true if this panel is available to administrators while managing
* bot and mailing list accounts.
*
* @return bool True to enable management on behalf of accounts.
* @task config
*/
public function isManagementPanel() {
return false;
}
/**
* Return true if this panel is available while editing settings templates.
*
* @return bool True to allow editing in templates.
* @task config
*/
public function isTemplatePanel() {
return false;
}
+ /**
+ * Return true if this panel should be available when enrolling in MFA on
+ * a new account with MFA requiredd.
+ *
+ * @return bool True to allow configuration during MFA enrollment.
+ * @task config
+ */
+ public function isMultiFactorEnrollmentPanel() {
+ return false;
+ }
+
/* -( Panel Implementation )----------------------------------------------- */
/**
* Process a user request for this settings panel. Implement this method like
* a lightweight controller. If you return an @{class:AphrontResponse}, the
* response will be used in whole. If you return anything else, it will be
* treated as a view and composed into a normal settings page.
*
* Generally, render your settings panel by returning a form, then return
* a redirect when the user saves settings.
*
* @param AphrontRequest Incoming request.
* @return wild Response to request, either as an
* @{class:AphrontResponse} or something which can
* be composed into a @{class:AphrontView}.
* @task panel
*/
abstract public function processRequest(AphrontRequest $request);
/**
* Get the URI for this panel.
*
* @param string? Optional path to append.
* @return string Relative URI for the panel.
* @task panel
*/
final public function getPanelURI($path = '') {
$path = ltrim($path, '/');
if ($this->overrideURI) {
return rtrim($this->overrideURI, '/').'/'.$path;
}
$key = $this->getPanelKey();
$key = phutil_escape_uri($key);
$user = $this->getUser();
if ($user) {
if ($user->isLoggedIn()) {
$username = $user->getUsername();
return "/settings/user/{$username}/page/{$key}/{$path}";
} else {
// For logged-out users, we can't put their username in the URI. This
// page will prompt them to login, then redirect them to the correct
// location.
return "/settings/panel/{$key}/";
}
} else {
$builtin = $this->getPreferences()->getBuiltinKey();
return "/settings/builtin/{$builtin}/page/{$key}/{$path}";
}
}
/* -( Internals )---------------------------------------------------------- */
/**
* Generates a key to sort the list of panels.
*
* @return string Sortable key.
* @task internal
*/
final public function getPanelOrderVector() {
return id(new PhutilSortVector())
->addString($this->getPanelName());
}
protected function newDialog() {
return $this->getController()->newDialog();
}
protected function writeSetting(
PhabricatorUserPreferences $preferences,
$key,
$value) {
$viewer = $this->getViewer();
$request = $this->getController()->getRequest();
$editor = id(new PhabricatorUserPreferencesEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
$xactions = array();
$xactions[] = $preferences->newTransaction($key, $value);
$editor->applyTransactions($preferences, $xactions);
}
public function newBox($title, $content, $actions = array()) {
$header = id(new PHUIHeaderView())
->setHeader($title);
foreach ($actions as $action) {
$header->addActionLink($action);
}
$view = id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild($content)
->setBackground(PHUIObjectBoxView::WHITE_CONFIG);
return $view;
}
}
diff --git a/src/applications/settings/panel/PhabricatorTokensSettingsPanel.php b/src/applications/settings/panel/PhabricatorTokensSettingsPanel.php
index f2021bafa..91064a432 100644
--- a/src/applications/settings/panel/PhabricatorTokensSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorTokensSettingsPanel.php
@@ -1,85 +1,89 @@
<?php
final class PhabricatorTokensSettingsPanel extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'tokens';
}
public function getPanelName() {
return pht('Temporary Tokens');
}
+ public function getPanelMenuIcon() {
+ return 'fa-ticket';
+ }
+
public function getPanelGroupKey() {
return PhabricatorSettingsLogsPanelGroup::PANELGROUPKEY;
}
public function processRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$tokens = id(new PhabricatorAuthTemporaryTokenQuery())
->setViewer($viewer)
->withTokenResources(array($viewer->getPHID()))
->execute();
$rows = array();
foreach ($tokens as $token) {
if ($token->isRevocable()) {
$button = javelin_tag(
'a',
array(
'href' => '/auth/token/revoke/'.$token->getID().'/',
'class' => 'small button button-grey',
'sigil' => 'workflow',
),
pht('Revoke'));
} else {
$button = javelin_tag(
'a',
array(
'class' => 'small button button-grey disabled',
),
pht('Revoke'));
}
if ($token->getTokenExpires() >= time()) {
$expiry = phabricator_datetime($token->getTokenExpires(), $viewer);
} else {
$expiry = pht('Expired');
}
$rows[] = array(
$token->getTokenReadableTypeName(),
$expiry,
$button,
);
}
$table = new AphrontTableView($rows);
$table->setNoDataString(pht("You don't have any active tokens."));
$table->setHeaders(
array(
pht('Type'),
pht('Expires'),
pht(''),
));
$table->setColumnClasses(
array(
'wide',
'right',
'action',
));
$button = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-warning')
->setText(pht('Revoke All'))
->setHref('/auth/token/revoke/all/')
->setWorkflow(true)
->setColor(PHUIButtonView::RED);
return $this->newBox(pht('Temporary Tokens'), $table, array($button));
}
}
diff --git a/src/applications/settings/setting/PhabricatorPronounSetting.php b/src/applications/settings/setting/PhabricatorPronounSetting.php
index 2ab30f0ab..51425f3cf 100644
--- a/src/applications/settings/setting/PhabricatorPronounSetting.php
+++ b/src/applications/settings/setting/PhabricatorPronounSetting.php
@@ -1,46 +1,46 @@
<?php
final class PhabricatorPronounSetting
extends PhabricatorSelectSetting {
const SETTINGKEY = 'pronoun';
public function getSettingName() {
return pht('Pronoun');
}
public function getSettingPanelKey() {
- return PhabricatorAccountSettingsPanel::PANELKEY;
+ return PhabricatorLanguageSettingsPanel::PANELKEY;
}
protected function getSettingOrder() {
return 200;
}
protected function getControlInstructions() {
return pht('Choose the pronoun you prefer.');
}
public function getSettingDefaultValue() {
return PhutilPerson::GENDER_UNKNOWN;
}
protected function getSelectOptions() {
// TODO: When editing another user's settings as an administrator, this
// is not the best username: the user's username would be better.
$viewer = $this->getViewer();
$username = $viewer->getUsername();
$label_unknown = pht('%s updated their profile', $username);
$label_her = pht('%s updated her profile', $username);
$label_his = pht('%s updated his profile', $username);
return array(
PhutilPerson::GENDER_UNKNOWN => $label_unknown,
PhutilPerson::GENDER_MASCULINE => $label_his,
PhutilPerson::GENDER_FEMININE => $label_her,
);
}
}
diff --git a/src/applications/settings/setting/PhabricatorTranslationSetting.php b/src/applications/settings/setting/PhabricatorTranslationSetting.php
index 6c0bec879..09f77c2ba 100644
--- a/src/applications/settings/setting/PhabricatorTranslationSetting.php
+++ b/src/applications/settings/setting/PhabricatorTranslationSetting.php
@@ -1,119 +1,119 @@
<?php
final class PhabricatorTranslationSetting
extends PhabricatorOptionGroupSetting {
const SETTINGKEY = 'translation';
public function getSettingName() {
return pht('Translation');
}
public function getSettingPanelKey() {
- return PhabricatorAccountSettingsPanel::PANELKEY;
+ return PhabricatorLanguageSettingsPanel::PANELKEY;
}
protected function getSettingOrder() {
return 100;
}
public function getSettingDefaultValue() {
return 'en_US';
}
protected function getControlInstructions() {
return pht(
'Choose which language you would like the Phabricator UI to use.');
}
public function assertValidValue($value) {
$locales = PhutilLocale::loadAllLocales();
return isset($locales[$value]);
}
protected function getSelectOptionGroups() {
$locales = PhutilLocale::loadAllLocales();
$group_labels = array(
'normal' => pht('Translations'),
'limited' => pht('Limited Translations'),
'silly' => pht('Silly Translations'),
'test' => pht('Developer/Test Translations'),
);
$groups = array_fill_keys(array_keys($group_labels), array());
$translations = array();
foreach ($locales as $locale) {
$code = $locale->getLocaleCode();
// Get the locale's localized name if it's available. For example,
// "Deutsch" instead of "German". This helps users who do not speak the
// current language to find the correct setting.
$raw_scope = PhabricatorEnv::beginScopedLocale($code);
$name = $locale->getLocaleName();
unset($raw_scope);
if ($locale->isSillyLocale()) {
$groups['silly'][$code] = $name;
continue;
}
if ($locale->isTestLocale()) {
$groups['test'][$code] = $name;
continue;
}
$strings = PhutilTranslation::getTranslationMapForLocale($code);
$size = count($strings);
// If a translation is English, assume it can fall back to the default
// strings and don't caveat its completeness.
$is_english = (substr($code, 0, 3) == 'en_');
// Arbitrarily pick some number of available strings to promote a
// translation out of the "limited" group. The major goal is just to
// keep locales with very few strings out of the main group, so users
// aren't surprised if a locale has no upstream translations available.
if ($size > 512 || $is_english) {
$type = 'normal';
} else {
$type = 'limited';
}
$groups[$type][$code] = $name;
}
// Omit silly locales on serious business installs.
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
if ($is_serious) {
unset($groups['silly']);
}
// Omit limited and test translations if Phabricator is not in developer
// mode.
$is_dev = PhabricatorEnv::getEnvConfig('phabricator.developer-mode');
if (!$is_dev) {
unset($groups['limited']);
unset($groups['test']);
}
$results = array();
foreach ($groups as $key => $group) {
$label = $group_labels[$key];
if (!$group) {
continue;
}
asort($group);
$results[] = array(
'label' => $label,
'options' => $group,
);
}
return $results;
}
}
diff --git a/src/applications/settings/storage/PhabricatorUserPreferences.php b/src/applications/settings/storage/PhabricatorUserPreferences.php
index 63b2bd3a0..0af6b4553 100644
--- a/src/applications/settings/storage/PhabricatorUserPreferences.php
+++ b/src/applications/settings/storage/PhabricatorUserPreferences.php
@@ -1,262 +1,252 @@
<?php
final class PhabricatorUserPreferences
extends PhabricatorUserDAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface,
PhabricatorApplicationTransactionInterface {
const BUILTIN_GLOBAL_DEFAULT = 'global';
protected $userPHID;
protected $preferences = array();
protected $builtinKey;
private $user = self::ATTACHABLE;
private $defaultSettings;
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'preferences' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'userPHID' => 'phid?',
'builtinKey' => 'text32?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_user' => array(
'columns' => array('userPHID'),
'unique' => true,
),
'key_builtin' => array(
'columns' => array('builtinKey'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorUserPreferencesPHIDType::TYPECONST);
}
public function getPreference($key, $default = null) {
return idx($this->preferences, $key, $default);
}
public function setPreference($key, $value) {
$this->preferences[$key] = $value;
return $this;
}
public function unsetPreference($key) {
unset($this->preferences[$key]);
return $this;
}
public function getDefaultValue($key) {
if ($this->defaultSettings) {
return $this->defaultSettings->getSettingValue($key);
}
$setting = self::getSettingObject($key);
if (!$setting) {
return null;
}
$setting = id(clone $setting)
->setViewer($this->getUser());
return $setting->getSettingDefaultValue();
}
public function getSettingValue($key) {
if (array_key_exists($key, $this->preferences)) {
return $this->preferences[$key];
}
return $this->getDefaultValue($key);
}
private static function getSettingObject($key) {
$settings = PhabricatorSetting::getAllSettings();
return idx($settings, $key);
}
public function attachDefaultSettings(PhabricatorUserPreferences $settings) {
$this->defaultSettings = $settings;
return $this;
}
public function attachUser(PhabricatorUser $user = null) {
$this->user = $user;
return $this;
}
public function getUser() {
return $this->assertAttached($this->user);
}
public function hasManagedUser() {
$user_phid = $this->getUserPHID();
if (!$user_phid) {
return false;
}
$user = $this->getUser();
if ($user->getIsSystemAgent() || $user->getIsMailingList()) {
return true;
}
return false;
}
/**
* Load or create a preferences object for the given user.
*
* @param PhabricatorUser User to load or create preferences for.
*/
public static function loadUserPreferences(PhabricatorUser $user) {
return id(new PhabricatorUserPreferencesQuery())
->setViewer($user)
->withUsers(array($user))
->needSyntheticPreferences(true)
->executeOne();
}
/**
* Load or create a global preferences object.
*
* If no global preferences exist, an empty preferences object is returned.
*
* @param PhabricatorUser Viewing user.
*/
public static function loadGlobalPreferences(PhabricatorUser $viewer) {
$global = id(new PhabricatorUserPreferencesQuery())
->setViewer($viewer)
->withBuiltinKeys(
array(
self::BUILTIN_GLOBAL_DEFAULT,
))
->executeOne();
if (!$global) {
$global = id(new self())
->attachUser(new PhabricatorUser());
}
return $global;
}
public function newTransaction($key, $value) {
$setting_property = PhabricatorUserPreferencesTransaction::PROPERTY_SETTING;
$xaction_type = PhabricatorUserPreferencesTransaction::TYPE_SETTING;
return id(clone $this->getApplicationTransactionTemplate())
->setTransactionType($xaction_type)
->setMetadataValue($setting_property, $key)
->setNewValue($value);
}
public function getEditURI() {
if ($this->getUser()) {
return '/settings/user/'.$this->getUser()->getUsername().'/';
} else {
return '/settings/builtin/'.$this->getBuiltinKey().'/';
}
}
public function getDisplayName() {
if ($this->getBuiltinKey()) {
return pht('Global Default Settings');
}
return pht('Personal Settings');
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$user_phid = $this->getUserPHID();
if ($user_phid) {
return $user_phid;
}
return PhabricatorPolicies::getMostOpenPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
if ($this->hasManagedUser()) {
return PhabricatorPolicies::POLICY_ADMIN;
}
$user_phid = $this->getUserPHID();
if ($user_phid) {
return $user_phid;
}
return PhabricatorPolicies::POLICY_ADMIN;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($this->hasManagedUser()) {
if ($viewer->getIsAdmin()) {
return true;
}
}
switch ($this->getBuiltinKey()) {
case self::BUILTIN_GLOBAL_DEFAULT:
// NOTE: Without this policy exception, the logged-out viewer can not
// see global preferences.
return true;
}
return false;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->delete();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorUserPreferencesEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorUserPreferencesTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
- return $timeline;
- }
-
}
diff --git a/src/applications/slowvote/controller/PhabricatorSlowvoteCommentController.php b/src/applications/slowvote/controller/PhabricatorSlowvoteCommentController.php
index 035eb577d..3d48c3186 100644
--- a/src/applications/slowvote/controller/PhabricatorSlowvoteCommentController.php
+++ b/src/applications/slowvote/controller/PhabricatorSlowvoteCommentController.php
@@ -1,63 +1,64 @@
<?php
final class PhabricatorSlowvoteCommentController
extends PhabricatorSlowvoteController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
if (!$request->isFormPost()) {
return new Aphront400Response();
}
$poll = id(new PhabricatorSlowvoteQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$poll) {
return new Aphront404Response();
}
$is_preview = $request->isPreviewRequest();
$draft = PhabricatorDraft::buildFromRequest($request);
$view_uri = '/V'.$poll->getID();
$xactions = array();
$xactions[] = id(new PhabricatorSlowvoteTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new PhabricatorSlowvoteTransactionComment())
->setContent($request->getStr('comment')));
$editor = id(new PhabricatorSlowvoteEditor())
->setActor($viewer)
->setContinueOnNoEffect($request->isContinueRequest())
->setContentSourceFromRequest($request)
->setIsPreview($is_preview);
try {
$xactions = $editor->applyTransactions($poll, $xactions);
} catch (PhabricatorApplicationTransactionNoEffectException $ex) {
return id(new PhabricatorApplicationTransactionNoEffectResponse())
->setCancelURI($view_uri)
->setException($ex);
}
if ($draft) {
$draft->replaceOrDelete();
}
if ($request->isAjax() && $is_preview) {
return id(new PhabricatorApplicationTransactionResponse())
+ ->setObject($poll)
->setViewer($viewer)
->setTransactions($xactions)
->setIsPreview($is_preview);
} else {
return id(new AphrontRedirectResponse())
->setURI($view_uri);
}
}
}
diff --git a/src/applications/slowvote/mail/PhabricatorSlowvoteMailReceiver.php b/src/applications/slowvote/mail/PhabricatorSlowvoteMailReceiver.php
index 78e608231..7b7459d4c 100644
--- a/src/applications/slowvote/mail/PhabricatorSlowvoteMailReceiver.php
+++ b/src/applications/slowvote/mail/PhabricatorSlowvoteMailReceiver.php
@@ -1,28 +1,28 @@
<?php
final class PhabricatorSlowvoteMailReceiver
extends PhabricatorObjectMailReceiver {
public function isEnabled() {
return PhabricatorApplication::isClassInstalled(
'PhabricatorSlowvoteApplication');
}
protected function getObjectPattern() {
return 'V[1-9]\d*';
}
protected function loadObject($pattern, PhabricatorUser $viewer) {
- $id = (int)substr($pattern, 4);
+ $id = (int)substr($pattern, 1);
return id(new PhabricatorSlowvoteQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
}
protected function getTransactionReplyHandler() {
return new PhabricatorSlowvoteReplyHandler();
}
}
diff --git a/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php b/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php
index 3b642256d..b8355c058 100644
--- a/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php
+++ b/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php
@@ -1,222 +1,211 @@
<?php
final class PhabricatorSlowvotePoll extends PhabricatorSlowvoteDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorSubscribableInterface,
PhabricatorFlaggableInterface,
PhabricatorTokenReceiverInterface,
PhabricatorProjectInterface,
PhabricatorDestructibleInterface,
PhabricatorSpacesInterface {
const RESPONSES_VISIBLE = 0;
const RESPONSES_VOTERS = 1;
const RESPONSES_OWNER = 2;
const METHOD_PLURALITY = 0;
const METHOD_APPROVAL = 1;
protected $question;
protected $description;
protected $authorPHID;
protected $responseVisibility = 0;
protected $shuffle = 0;
protected $method;
protected $mailKey;
protected $viewPolicy;
protected $isClosed = 0;
protected $spacePHID;
private $options = self::ATTACHABLE;
private $choices = self::ATTACHABLE;
private $viewerChoices = self::ATTACHABLE;
public static function initializeNewPoll(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorSlowvoteApplication'))
->executeOne();
$view_policy = $app->getPolicy(
PhabricatorSlowvoteDefaultViewCapability::CAPABILITY);
return id(new PhabricatorSlowvotePoll())
->setAuthorPHID($actor->getPHID())
->setViewPolicy($view_policy)
->setSpacePHID($actor->getDefaultSpacePHID());
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'question' => 'text255',
'responseVisibility' => 'uint32',
'shuffle' => 'bool',
'method' => 'uint32',
'description' => 'text',
'isClosed' => 'bool',
'mailKey' => 'bytes20',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorSlowvotePollPHIDType::TYPECONST);
}
public function getOptions() {
return $this->assertAttached($this->options);
}
public function attachOptions(array $options) {
assert_instances_of($options, 'PhabricatorSlowvoteOption');
$this->options = $options;
return $this;
}
public function getChoices() {
return $this->assertAttached($this->choices);
}
public function attachChoices(array $choices) {
assert_instances_of($choices, 'PhabricatorSlowvoteChoice');
$this->choices = $choices;
return $this;
}
public function getViewerChoices(PhabricatorUser $viewer) {
return $this->assertAttachedKey($this->viewerChoices, $viewer->getPHID());
}
public function attachViewerChoices(PhabricatorUser $viewer, array $choices) {
if ($this->viewerChoices === self::ATTACHABLE) {
$this->viewerChoices = array();
}
assert_instances_of($choices, 'PhabricatorSlowvoteChoice');
$this->viewerChoices[$viewer->getPHID()] = $choices;
return $this;
}
public function getMonogram() {
return 'V'.$this->getID();
}
public function getURI() {
return '/'.$this->getMonogram();
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorSlowvoteEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorSlowvoteTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->viewPolicy;
case PhabricatorPolicyCapability::CAN_EDIT:
return PhabricatorPolicies::POLICY_NOONE;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return ($viewer->getPHID() == $this->getAuthorPHID());
}
public function describeAutomaticCapability($capability) {
return pht('The author of a poll can always view and edit it.');
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return ($phid == $this->getAuthorPHID());
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array($this->getAuthorPHID());
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$choices = id(new PhabricatorSlowvoteChoice())->loadAllWhere(
'pollID = %d',
$this->getID());
foreach ($choices as $choice) {
$choice->delete();
}
$options = id(new PhabricatorSlowvoteOption())->loadAllWhere(
'pollID = %d',
$this->getID());
foreach ($options as $option) {
$option->delete();
}
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorSpacesInterface )--------------------------------------- */
public function getSpacePHID() {
return $this->spacePHID;
}
}
diff --git a/src/applications/spaces/storage/PhabricatorSpacesNamespace.php b/src/applications/spaces/storage/PhabricatorSpacesNamespace.php
index 5f0137684..e8b2afb43 100644
--- a/src/applications/spaces/storage/PhabricatorSpacesNamespace.php
+++ b/src/applications/spaces/storage/PhabricatorSpacesNamespace.php
@@ -1,117 +1,107 @@
<?php
final class PhabricatorSpacesNamespace
extends PhabricatorSpacesDAO
implements
PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorDestructibleInterface {
protected $namespaceName;
protected $viewPolicy;
protected $editPolicy;
protected $isDefaultNamespace;
protected $description;
protected $isArchived;
public static function initializeNewNamespace(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
->withClasses(array('PhabricatorSpacesApplication'))
->executeOne();
$view_policy = $app->getPolicy(
PhabricatorSpacesCapabilityDefaultView::CAPABILITY);
$edit_policy = $app->getPolicy(
PhabricatorSpacesCapabilityDefaultEdit::CAPABILITY);
return id(new PhabricatorSpacesNamespace())
->setIsDefaultNamespace(null)
->setViewPolicy($view_policy)
->setEditPolicy($edit_policy)
->setDescription('')
->setIsArchived(0);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'namespaceName' => 'text255',
'isDefaultNamespace' => 'bool?',
'description' => 'text',
'isArchived' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_default' => array(
'columns' => array('isDefaultNamespace'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorSpacesNamespacePHIDType::TYPECONST);
}
public function getMonogram() {
return 'S'.$this->getID();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
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 $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorSpacesNamespaceEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorSpacesNamespaceTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
- return $timeline;
- }
-
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->delete();
}
}
diff --git a/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php b/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php
index 817e92ce3..941c0c581 100644
--- a/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php
+++ b/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php
@@ -1,119 +1,118 @@
<?php
final class PhabricatorSubscriptionsEditController
extends PhabricatorController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$phid = $request->getURIData('phid');
$action = $request->getURIData('action');
- if (!$request->isFormPost()) {
+ if (!$request->isFormOrHisecPost()) {
return new Aphront400Response();
}
switch ($action) {
case 'add':
$is_add = true;
break;
case 'delete':
$is_add = false;
break;
default:
return new Aphront400Response();
}
$handle = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(array($phid))
->executeOne();
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($phid))
->executeOne();
if (!($object instanceof PhabricatorSubscribableInterface)) {
return $this->buildErrorResponse(
pht('Bad Object'),
pht('This object is not subscribable.'),
$handle->getURI());
}
if ($object->isAutomaticallySubscribed($viewer->getPHID())) {
return $this->buildErrorResponse(
pht('Automatically Subscribed'),
pht('You are automatically subscribed to this object.'),
$handle->getURI());
}
if (!PhabricatorPolicyFilter::canInteract($viewer, $object)) {
$lock = PhabricatorEditEngineLock::newForObject($viewer, $object);
$dialog = $this->newDialog()
->addCancelButton($handle->getURI());
return $lock->willBlockUserInteractionWithDialog($dialog);
}
if ($object instanceof PhabricatorApplicationTransactionInterface) {
if ($is_add) {
$xaction_value = array(
'+' => array($viewer->getPHID()),
);
} else {
$xaction_value = array(
'-' => array($viewer->getPHID()),
);
}
$xaction = id($object->getApplicationTransactionTemplate())
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setNewValue($xaction_value);
$editor = id($object->getApplicationTransactionEditor())
->setActor($viewer)
+ ->setCancelURI($handle->getURI())
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setContentSourceFromRequest($request);
- $editor->applyTransactions(
- $object->getApplicationTransactionObject(),
- array($xaction));
+ $editor->applyTransactions($object, array($xaction));
} else {
// TODO: Eventually, get rid of this once everything implements
// PhabricatorApplicationTransactionInterface.
$editor = id(new PhabricatorSubscriptionsEditor())
->setActor($viewer)
->setObject($object);
if ($is_add) {
$editor->subscribeExplicit(array($viewer->getPHID()), $explicit = true);
} else {
$editor->unsubscribe(array($viewer->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();
$viewer = $request->getUser();
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle($title)
->appendChild($message)
->addCancelButton($uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php b/src/applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php
index e29d5e380..929a4985d 100644
--- a/src/applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php
+++ b/src/applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php
@@ -1,90 +1,88 @@
<?php
final class PhabricatorSubscriptionsMuteController
extends PhabricatorController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$phid = $request->getURIData('phid');
$handle = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(array($phid))
->executeOne();
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($phid))
->executeOne();
if (!($object instanceof PhabricatorSubscribableInterface)) {
return new Aphront400Response();
}
$muted_type = PhabricatorMutedByEdgeType::EDGECONST;
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($object->getPHID()))
->withEdgeTypes(array($muted_type))
->withDestinationPHIDs(array($viewer->getPHID()));
$edge_query->execute();
$is_mute = !$edge_query->getDestinationPHIDs();
$object_uri = $handle->getURI();
if ($request->isFormPost()) {
if ($is_mute) {
$xaction_value = array(
'+' => array_fuse(array($viewer->getPHID())),
);
} else {
$xaction_value = array(
'-' => array_fuse(array($viewer->getPHID())),
);
}
$xaction = id($object->getApplicationTransactionTemplate())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $muted_type)
->setNewValue($xaction_value);
$editor = id($object->getApplicationTransactionEditor())
->setActor($viewer)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setContentSourceFromRequest($request);
- $editor->applyTransactions(
- $object->getApplicationTransactionObject(),
- array($xaction));
+ $editor->applyTransactions($object, array($xaction));
return id(new AphrontReloadResponse())->setURI($object_uri);
}
$dialog = $this->newDialog()
->addCancelButton($object_uri);
if ($is_mute) {
$dialog
->setTitle(pht('Mute Notifications'))
->appendParagraph(
pht(
'Mute this object? You will no longer receive notifications or '.
'email about it.'))
->addSubmitButton(pht('Mute'));
} else {
$dialog
->setTitle(pht('Unmute Notifications'))
->appendParagraph(
pht(
'Unmute this object? You will receive notifications and email '.
'again.'))
->addSubmitButton(pht('Unmute'));
}
return $dialog;
}
}
diff --git a/src/applications/tokens/controller/PhabricatorTokenGiveController.php b/src/applications/tokens/controller/PhabricatorTokenGiveController.php
index c7c47c41f..ef8dc7599 100644
--- a/src/applications/tokens/controller/PhabricatorTokenGiveController.php
+++ b/src/applications/tokens/controller/PhabricatorTokenGiveController.php
@@ -1,142 +1,144 @@
<?php
final class PhabricatorTokenGiveController extends PhabricatorTokenController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$phid = $request->getURIData('phid');
$handle = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(array($phid))
->executeOne();
if (!$handle->isComplete()) {
return new Aphront404Response();
}
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($phid))
->executeOne();
if (!($object instanceof PhabricatorTokenReceiverInterface)) {
return new Aphront400Response();
}
if (!PhabricatorPolicyFilter::canInteract($viewer, $object)) {
$lock = PhabricatorEditEngineLock::newForObject($viewer, $object);
$dialog = $this->newDialog()
->addCancelButton($handle->getURI());
return $lock->willBlockUserInteractionWithDialog($dialog);
}
$current = id(new PhabricatorTokenGivenQuery())
->setViewer($viewer)
->withAuthorPHIDs(array($viewer->getPHID()))
->withObjectPHIDs(array($handle->getPHID()))
->execute();
if ($current) {
$is_give = false;
$title = pht('Rescind Token');
} else {
$is_give = true;
$title = pht('Give Token');
}
$done_uri = $handle->getURI();
- if ($request->isDialogFormPost()) {
+ if ($request->isFormOrHisecPost()) {
$content_source = PhabricatorContentSource::newFromRequest($request);
$editor = id(new PhabricatorTokenGivenEditor())
->setActor($viewer)
+ ->setRequest($request)
+ ->setCancelURI($handle->getURI())
->setContentSource($content_source);
if ($is_give) {
$token_phid = $request->getStr('tokenPHID');
$editor->addToken($handle->getPHID(), $token_phid);
} else {
$editor->deleteToken($handle->getPHID());
}
return id(new AphrontReloadResponse())->setURI($done_uri);
}
if ($is_give) {
$dialog = $this->buildGiveTokenDialog();
} else {
$dialog = $this->buildRescindTokenDialog(head($current));
}
$dialog->setUser($viewer);
$dialog->addCancelButton($done_uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function buildGiveTokenDialog() {
$viewer = $this->getViewer();
$tokens = id(new PhabricatorTokenQuery())
->setViewer($viewer)
->execute();
$buttons = array();
$ii = 0;
foreach ($tokens as $token) {
$aural = javelin_tag(
'span',
array(
'aural' => true,
),
pht('Award "%s" Token', $token->getName()));
$buttons[] = javelin_tag(
'button',
array(
'class' => 'token-button',
'name' => 'tokenPHID',
'value' => $token->getPHID(),
'type' => 'submit',
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $token->getName(),
),
),
array(
$aural,
$token->renderIcon(),
));
if ((++$ii % 6) == 0) {
$buttons[] = phutil_tag('br');
}
}
$buttons = phutil_tag(
'div',
array(
'class' => 'token-grid',
),
$buttons);
$dialog = new AphrontDialogView();
$dialog->setTitle(pht('Give Token'));
$dialog->appendChild($buttons);
return $dialog;
}
private function buildRescindTokenDialog(PhabricatorTokenGiven $token_given) {
$dialog = new AphrontDialogView();
$dialog->setTitle(pht('Rescind Token'));
$dialog->appendChild(
pht('Really rescind this lovely token?'));
$dialog->addSubmitButton(pht('Rescind Token'));
return $dialog;
}
}
diff --git a/src/applications/tokens/editor/PhabricatorTokenGivenEditor.php b/src/applications/tokens/editor/PhabricatorTokenGivenEditor.php
index ea4e8367c..08a4cbf9b 100644
--- a/src/applications/tokens/editor/PhabricatorTokenGivenEditor.php
+++ b/src/applications/tokens/editor/PhabricatorTokenGivenEditor.php
@@ -1,174 +1,216 @@
<?php
final class PhabricatorTokenGivenEditor
extends PhabricatorEditor {
private $contentSource;
+ private $request;
+ private $cancelURI;
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source;
return $this;
}
public function getContentSource() {
return $this->contentSource;
}
+ public function setRequest(AphrontRequest $request) {
+ $this->request = $request;
+ return $this;
+ }
+
+ public function getRequest() {
+ return $this->request;
+ }
+
+ public function setCancelURI($cancel_uri) {
+ $this->cancelURI = $cancel_uri;
+ return $this;
+ }
+
+ public function getCancelURI() {
+ return $this->cancelURI;
+ }
+
public function addToken($object_phid, $token_phid) {
$token = $this->validateToken($token_phid);
$object = $this->validateObject($object_phid);
$current_token = $this->loadCurrentToken($object);
$actor = $this->requireActor();
$token_given = id(new PhabricatorTokenGiven())
->setAuthorPHID($actor->getPHID())
->setObjectPHID($object->getPHID())
->setTokenPHID($token->getPHID());
$token_given->openTransaction();
if ($current_token) {
$this->executeDeleteToken($object, $current_token);
}
$token_given->save();
queryfx(
$token_given->establishConnection('w'),
'INSERT INTO %T (objectPHID, tokenCount) VALUES (%s, 1)
ON DUPLICATE KEY UPDATE tokenCount = tokenCount + 1',
id(new PhabricatorTokenCount())->getTableName(),
$object->getPHID());
- $token_given->saveTransaction();
+ $current_token_phid = null;
+ if ($current_token) {
+ $current_token_phid = $current_token->getTokenPHID();
+ }
- $current_token_phid = null;
- if ($current_token) {
- $current_token_phid = $current_token->getTokenPHID();
- }
+ try {
+ $this->publishTransaction(
+ $object,
+ $current_token_phid,
+ $token->getPHID());
+ } catch (Exception $ex) {
+ $token_given->killTransaction();
+ throw $ex;
+ }
- $this->publishTransaction(
- $object,
- $current_token_phid,
- $token->getPHID());
+ $token_given->saveTransaction();
$subscribed_phids = $object->getUsersToNotifyOfTokenGiven();
if ($subscribed_phids) {
$related_phids = $subscribed_phids;
$related_phids[] = $actor->getPHID();
$story_type = 'PhabricatorTokenGivenFeedStory';
$story_data = array(
'authorPHID' => $actor->getPHID(),
'tokenPHID' => $token->getPHID(),
'objectPHID' => $object->getPHID(),
);
id(new PhabricatorFeedStoryPublisher())
->setStoryType($story_type)
->setStoryData($story_data)
->setStoryTime(time())
->setStoryAuthorPHID($actor->getPHID())
->setRelatedPHIDs($related_phids)
->setPrimaryObjectPHID($object->getPHID())
->setSubscribedPHIDs($subscribed_phids)
->publish();
}
return $token_given;
}
public function deleteToken($object_phid) {
$object = $this->validateObject($object_phid);
$token_given = $this->loadCurrentToken($object);
if (!$token_given) {
return;
}
- $this->executeDeleteToken($object, $token_given);
- $this->publishTransaction(
- $object,
- $token_given->getTokenPHID(),
- null);
+ $token_given->openTransaction();
+ $this->executeDeleteToken($object, $token_given);
+
+ try {
+ $this->publishTransaction(
+ $object,
+ $token_given->getTokenPHID(),
+ null);
+ } catch (Exception $ex) {
+ $token_given->killTransaction();
+ throw $ex;
+ }
+
+ $token_given->saveTransaction();
}
private function executeDeleteToken(
PhabricatorTokenReceiverInterface $object,
PhabricatorTokenGiven $token_given) {
$token_given->openTransaction();
$token_given->delete();
queryfx(
$token_given->establishConnection('w'),
'INSERT INTO %T (objectPHID, tokenCount) VALUES (%s, 0)
ON DUPLICATE KEY UPDATE tokenCount = tokenCount - 1',
id(new PhabricatorTokenCount())->getTableName(),
$object->getPHID());
$token_given->saveTransaction();
}
private function validateToken($token_phid) {
$token = id(new PhabricatorTokenQuery())
->setViewer($this->requireActor())
->withPHIDs(array($token_phid))
->executeOne();
if (!$token) {
throw new Exception(pht('No such token "%s"!', $token_phid));
}
return $token;
}
private function validateObject($object_phid) {
$object = id(new PhabricatorObjectQuery())
->setViewer($this->requireActor())
->withPHIDs(array($object_phid))
->executeOne();
if (!$object) {
throw new Exception(pht('No such object "%s"!', $object_phid));
}
return $object;
}
private function loadCurrentToken(PhabricatorTokenReceiverInterface $object) {
return id(new PhabricatorTokenGiven())->loadOneWhere(
'authorPHID = %s AND objectPHID = %s',
$this->requireActor()->getPHID(),
$object->getPHID());
}
private function publishTransaction(
PhabricatorTokenReceiverInterface $object,
$old_token_phid,
$new_token_phid) {
if (!($object instanceof PhabricatorApplicationTransactionInterface)) {
return;
}
$actor = $this->requireActor();
$xactions = array();
$xactions[] = id($object->getApplicationTransactionTemplate())
->setTransactionType(PhabricatorTransactions::TYPE_TOKEN)
->setOldValue($old_token_phid)
->setNewValue($new_token_phid);
$editor = $object->getApplicationTransactionEditor()
->setActor($actor)
->setContentSource($this->getContentSource())
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
- $editor->applyTransactions(
- $object->getApplicationTransactionObject(),
- $xactions);
+ $request = $this->getRequest();
+ if ($request) {
+ $editor->setRequest($request);
+ }
+
+ $cancel_uri = $this->getCancelURI();
+ if ($cancel_uri) {
+ $editor->setCancelURI($cancel_uri);
+ }
+
+ $editor->applyTransactions($object, $xactions);
}
}
diff --git a/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php b/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php
index 0394432ff..0edc0b3f5 100644
--- a/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php
+++ b/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php
@@ -1,242 +1,243 @@
<?php
final class TransactionSearchConduitAPIMethod
extends ConduitAPIMethod {
public function getAPIMethodName() {
return 'transaction.search';
}
public function getMethodDescription() {
return pht('Read transactions for an object.');
}
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodStatusDescription() {
return pht('This method is new and experimental.');
}
protected function defineParamTypes() {
return array(
'objectIdentifier' => 'phid|string',
'constraints' => 'map<string, wild>',
) + $this->getPagerParamTypes();
}
protected function defineReturnType() {
return 'list<dict>';
}
protected function defineErrorTypes() {
return array();
}
protected function execute(ConduitAPIRequest $request) {
$viewer = $request->getUser();
$pager = $this->newPager($request);
$object_name = $request->getValue('objectIdentifier', null);
if (!strlen($object_name)) {
throw new Exception(
pht(
'When calling "transaction.search", you must provide an object to '.
'retrieve transactions for.'));
}
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withNames(array($object_name))
->executeOne();
if (!$object) {
throw new Exception(
pht(
'No object "%s" exists.',
$object_name));
}
if (!($object instanceof PhabricatorApplicationTransactionInterface)) {
throw new Exception(
pht(
'Object "%s" does not implement "%s", so transactions can not '.
'be loaded for it.'));
}
$xaction_query = PhabricatorApplicationTransactionQuery::newQueryForObject(
$object);
$xaction_query
+ ->needHandles(false)
->withObjectPHIDs(array($object->getPHID()))
->setViewer($viewer);
$constraints = $request->getValue('constraints', array());
PhutilTypeSpec::checkMap(
$constraints,
array(
'phids' => 'optional list<string>',
));
$with_phids = idx($constraints, 'phids');
if ($with_phids === array()) {
throw new Exception(
pht(
'Constraint "phids" to "transaction.search" requires nonempty list, '.
'empty list provided.'));
}
if ($with_phids) {
$xaction_query->withPHIDs($with_phids);
}
$xactions = $xaction_query->executeWithCursorPager($pager);
$comment_map = array();
if ($xactions) {
$template = head($xactions)->getApplicationTransactionCommentObject();
if ($template) {
$query = new PhabricatorApplicationTransactionTemplatedCommentQuery();
$comment_map = $query
->setViewer($viewer)
->setTemplate($template)
->withTransactionPHIDs(mpull($xactions, 'getPHID'))
->execute();
$comment_map = msort($comment_map, 'getCommentVersion');
$comment_map = array_reverse($comment_map);
$comment_map = mgroup($comment_map, 'getTransactionPHID');
}
}
$modular_classes = array();
$modular_objects = array();
$modular_xactions = array();
foreach ($xactions as $xaction) {
if (!$xaction instanceof PhabricatorModularTransaction) {
continue;
}
// TODO: Hack things so certain transactions which don't have a modular
// type yet can use a pseudotype until they modularize. Some day, we'll
// modularize everything and remove this.
switch ($xaction->getTransactionType()) {
case DifferentialTransaction::TYPE_INLINE:
$modular_template = new DifferentialRevisionInlineTransaction();
break;
default:
$modular_template = $xaction->getModularType();
break;
}
$modular_class = get_class($modular_template);
if (!isset($modular_objects[$modular_class])) {
try {
$modular_object = newv($modular_class, array());
$modular_objects[$modular_class] = $modular_object;
} catch (Exception $ex) {
continue;
}
}
$modular_classes[$xaction->getPHID()] = $modular_class;
$modular_xactions[$modular_class][] = $xaction;
}
$modular_data_map = array();
foreach ($modular_objects as $class => $modular_type) {
$modular_data_map[$class] = $modular_type
->setViewer($viewer)
->loadTransactionTypeConduitData($modular_xactions[$class]);
}
$data = array();
foreach ($xactions as $xaction) {
$comments = idx($comment_map, $xaction->getPHID());
$comment_data = array();
if ($comments) {
$removed = head($comments)->getIsDeleted();
foreach ($comments as $comment) {
if ($removed) {
// If the most recent version of the comment has been removed,
// don't show the history. This is for consistency with the web
// UI, which also prevents users from retrieving the content of
// removed comments.
$content = array(
'raw' => '',
);
} else {
$content = array(
'raw' => (string)$comment->getContent(),
);
}
$comment_data[] = array(
'id' => (int)$comment->getID(),
'phid' => (string)$comment->getPHID(),
'version' => (int)$comment->getCommentVersion(),
'authorPHID' => (string)$comment->getAuthorPHID(),
'dateCreated' => (int)$comment->getDateCreated(),
'dateModified' => (int)$comment->getDateModified(),
'removed' => (bool)$comment->getIsDeleted(),
'content' => $content,
);
}
}
$fields = array();
$type = null;
if (isset($modular_classes[$xaction->getPHID()])) {
$modular_class = $modular_classes[$xaction->getPHID()];
$modular_object = $modular_objects[$modular_class];
$modular_data = $modular_data_map[$modular_class];
$type = $modular_object->getTransactionTypeForConduit($xaction);
$fields = $modular_object->getFieldValuesForConduit(
$xaction,
$modular_data);
}
if (!$fields) {
$fields = (object)$fields;
}
// If we haven't found a modular type, fallback for some simple core
// types. Ideally, we'll modularize everything some day.
if ($type === null) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
$type = 'comment';
break;
case PhabricatorTransactions::TYPE_CREATE:
$type = 'create';
break;
}
}
$data[] = array(
'id' => (int)$xaction->getID(),
'phid' => (string)$xaction->getPHID(),
'type' => $type,
'authorPHID' => (string)$xaction->getAuthorPHID(),
'objectPHID' => (string)$xaction->getObjectPHID(),
'dateCreated' => (int)$xaction->getDateCreated(),
'dateModified' => (int)$xaction->getDateModified(),
'comments' => $comment_data,
'fields' => $fields,
);
}
$results = array(
'data' => $data,
);
return $this->addPagerResults($results, $pager);
}
}
diff --git a/src/applications/transactions/constants/PhabricatorTransactions.php b/src/applications/transactions/constants/PhabricatorTransactions.php
index 7a606fe65..3b523417d 100644
--- a/src/applications/transactions/constants/PhabricatorTransactions.php
+++ b/src/applications/transactions/constants/PhabricatorTransactions.php
@@ -1,41 +1,42 @@
<?php
final class PhabricatorTransactions extends Phobject {
const TYPE_COMMENT = 'core:comment';
const TYPE_SUBSCRIBERS = 'core:subscribers';
const TYPE_VIEW_POLICY = 'core:view-policy';
const TYPE_EDIT_POLICY = 'core:edit-policy';
const TYPE_JOIN_POLICY = 'core:join-policy';
const TYPE_EDGE = 'core:edge';
const TYPE_CUSTOMFIELD = 'core:customfield';
const TYPE_TOKEN = 'token:give';
const TYPE_INLINESTATE = 'core:inlinestate';
const TYPE_SPACE = 'core:space';
const TYPE_CREATE = 'core:create';
const TYPE_COLUMNS = 'core:columns';
const TYPE_SUBTYPE = 'core:subtype';
const TYPE_HISTORY = 'core:history';
+ const TYPE_MFA = 'core:mfa';
const COLOR_RED = 'red';
const COLOR_ORANGE = 'orange';
const COLOR_YELLOW = 'yellow';
const COLOR_GREEN = 'green';
const COLOR_SKY = 'sky';
const COLOR_BLUE = 'blue';
const COLOR_INDIGO = 'indigo';
const COLOR_VIOLET = 'violet';
const COLOR_GREY = 'grey';
const COLOR_BLACK = 'black';
public static function getInlineStateMap() {
return array(
PhabricatorInlineCommentInterface::STATE_DRAFT =>
PhabricatorInlineCommentInterface::STATE_DONE,
PhabricatorInlineCommentInterface::STATE_UNDRAFT =>
PhabricatorInlineCommentInterface::STATE_UNDONE,
);
}
}
diff --git a/src/applications/transactions/controller/PhabricatorApplicationTransactionShowOlderController.php b/src/applications/transactions/controller/PhabricatorApplicationTransactionShowOlderController.php
index cdbdbf1ba..741e1e6f9 100644
--- a/src/applications/transactions/controller/PhabricatorApplicationTransactionShowOlderController.php
+++ b/src/applications/transactions/controller/PhabricatorApplicationTransactionShowOlderController.php
@@ -1,42 +1,53 @@
<?php
final class PhabricatorApplicationTransactionShowOlderController
extends PhabricatorApplicationTransactionController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$object = id(new PhabricatorObjectQuery())
->withPHIDs(array($request->getURIData('phid')))
->setViewer($viewer)
->executeOne();
if (!$object) {
return new Aphront404Response();
}
if (!$object instanceof PhabricatorApplicationTransactionInterface) {
return new Aphront404Response();
}
$query = PhabricatorApplicationTransactionQuery::newQueryForObject($object);
if (!$query) {
return new Aphront404Response();
}
- $timeline = $this->buildTransactionTimeline($object, $query);
+ $raw_view_data = $request->getStr('viewData');
+ try {
+ $view_data = phutil_json_decode($raw_view_data);
+ } catch (Exception $ex) {
+ $view_data = array();
+ }
+
+ $timeline = $this->buildTransactionTimeline(
+ $object,
+ $query,
+ null,
+ $view_data);
$phui_timeline = $timeline->buildPHUITimelineView($with_hiding = false);
$phui_timeline->setShouldAddSpacers(false);
$events = $phui_timeline->buildEvents();
return id(new AphrontAjaxResponse())
->setContent(array(
'timeline' => hsprintf('%s', $events),
));
}
}
diff --git a/src/applications/transactions/editengine/PhabricatorEditEngine.php b/src/applications/transactions/editengine/PhabricatorEditEngine.php
index 85d4897a1..465738687 100644
--- a/src/applications/transactions/editengine/PhabricatorEditEngine.php
+++ b/src/applications/transactions/editengine/PhabricatorEditEngine.php
@@ -1,2628 +1,2674 @@
<?php
/**
* @task fields Managing Fields
* @task text Display Text
* @task config Edit Engine Configuration
* @task uri Managing URIs
* @task load Creating and Loading Objects
* @task web Responding to Web Requests
* @task edit Responding to Edit Requests
* @task http Responding to HTTP Parameter Requests
* @task conduit Responding to Conduit Requests
*/
abstract class PhabricatorEditEngine
extends Phobject
implements PhabricatorPolicyInterface {
const EDITENGINECONFIG_DEFAULT = 'default';
const SUBTYPE_DEFAULT = 'default';
private $viewer;
private $controller;
private $isCreate;
private $editEngineConfiguration;
private $contextParameters = array();
private $targetObject;
private $page;
private $pages;
private $navigation;
final public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
final public function getViewer() {
return $this->viewer;
}
final public function setController(PhabricatorController $controller) {
$this->controller = $controller;
$this->setViewer($controller->getViewer());
return $this;
}
final public function getController() {
return $this->controller;
}
final public function getEngineKey() {
$key = $this->getPhobjectClassConstant('ENGINECONST', 64);
if (strpos($key, '/') !== false) {
throw new Exception(
pht(
'EditEngine ("%s") contains an invalid key character "/".',
get_class($this)));
}
return $key;
}
final public function getApplication() {
$app_class = $this->getEngineApplicationClass();
return PhabricatorApplication::getByClass($app_class);
}
final public function addContextParameter($key) {
$this->contextParameters[] = $key;
return $this;
}
public function isEngineConfigurable() {
return true;
}
public function isEngineExtensible() {
return true;
}
public function isDefaultQuickCreateEngine() {
return false;
}
public function getDefaultQuickCreateFormKeys() {
$keys = array();
if ($this->isDefaultQuickCreateEngine()) {
$keys[] = self::EDITENGINECONFIG_DEFAULT;
}
foreach ($keys as $idx => $key) {
$keys[$idx] = $this->getEngineKey().'/'.$key;
}
return $keys;
}
public static function splitFullKey($full_key) {
return explode('/', $full_key, 2);
}
public function getQuickCreateOrderVector() {
return id(new PhutilSortVector())
->addString($this->getObjectCreateShortText());
}
/**
* Force the engine to edit a particular object.
*/
public function setTargetObject($target_object) {
$this->targetObject = $target_object;
return $this;
}
public function getTargetObject() {
return $this->targetObject;
}
public function setNavigation(AphrontSideNavFilterView $navigation) {
$this->navigation = $navigation;
return $this;
}
public function getNavigation() {
return $this->navigation;
}
/* -( Managing Fields )---------------------------------------------------- */
abstract public function getEngineApplicationClass();
abstract protected function buildCustomEditFields($object);
public function getFieldsForConfig(
PhabricatorEditEngineConfiguration $config) {
$object = $this->newEditableObject();
$this->editEngineConfiguration = $config;
// This is mostly making sure that we fill in default values.
$this->setIsCreate(true);
return $this->buildEditFields($object);
}
final protected function buildEditFields($object) {
$viewer = $this->getViewer();
$fields = $this->buildCustomEditFields($object);
foreach ($fields as $field) {
$field
->setViewer($viewer)
->setObject($object);
}
$fields = mpull($fields, null, 'getKey');
if ($this->isEngineExtensible()) {
$extensions = PhabricatorEditEngineExtension::getAllEnabledExtensions();
} else {
$extensions = array();
}
foreach ($extensions as $extension) {
$extension->setViewer($viewer);
if (!$extension->supportsObject($this, $object)) {
continue;
}
$extension_fields = $extension->buildCustomEditFields($this, $object);
// TODO: Validate this in more detail with a more tailored error.
assert_instances_of($extension_fields, 'PhabricatorEditField');
foreach ($extension_fields as $field) {
$field
->setViewer($viewer)
->setObject($object);
$group_key = $field->getBulkEditGroupKey();
if ($group_key === null) {
$field->setBulkEditGroupKey('extension');
}
}
$extension_fields = mpull($extension_fields, null, 'getKey');
foreach ($extension_fields as $key => $field) {
$fields[$key] = $field;
}
}
$config = $this->getEditEngineConfiguration();
$fields = $this->willConfigureFields($object, $fields);
$fields = $config->applyConfigurationToFields($this, $object, $fields);
$fields = $this->applyPageToFields($object, $fields);
return $fields;
}
protected function willConfigureFields($object, array $fields) {
return $fields;
}
final public function supportsSubtypes() {
try {
$object = $this->newEditableObject();
} catch (Exception $ex) {
return false;
}
return ($object instanceof PhabricatorEditEngineSubtypeInterface);
}
final public function newSubtypeMap() {
return $this->newEditableObject()->newEditEngineSubtypeMap();
}
/* -( Display Text )------------------------------------------------------- */
/**
* @task text
*/
abstract public function getEngineName();
/**
* @task text
*/
abstract protected function getObjectCreateTitleText($object);
/**
* @task text
*/
protected function getFormHeaderText($object) {
$config = $this->getEditEngineConfiguration();
return $config->getName();
}
/**
* @task text
*/
abstract protected function getObjectEditTitleText($object);
/**
* @task text
*/
abstract protected function getObjectCreateShortText();
/**
* @task text
*/
abstract protected function getObjectName();
/**
* @task text
*/
abstract protected function getObjectEditShortText($object);
/**
* @task text
*/
protected function getObjectCreateButtonText($object) {
return $this->getObjectCreateTitleText($object);
}
/**
* @task text
*/
protected function getObjectEditButtonText($object) {
return pht('Save Changes');
}
/**
* @task text
*/
protected function getCommentViewSeriousHeaderText($object) {
return pht('Take Action');
}
/**
* @task text
*/
protected function getCommentViewSeriousButtonText($object) {
return pht('Submit');
}
/**
* @task text
*/
protected function getCommentViewHeaderText($object) {
return $this->getCommentViewSeriousHeaderText($object);
}
/**
* @task text
*/
protected function getCommentViewButtonText($object) {
return $this->getCommentViewSeriousButtonText($object);
}
/**
* @task text
*/
protected function getPageHeader($object) {
return null;
}
/**
* Return a human-readable header describing what this engine is used to do,
* like "Configure Maniphest Task Forms".
*
* @return string Human-readable description of the engine.
* @task text
*/
abstract public function getSummaryHeader();
/**
* Return a human-readable summary of what this engine is used to do.
*
* @return string Human-readable description of the engine.
* @task text
*/
abstract public function getSummaryText();
/* -( Edit Engine Configuration )------------------------------------------ */
protected function supportsEditEngineConfiguration() {
return true;
}
final protected function getEditEngineConfiguration() {
return $this->editEngineConfiguration;
}
public function newConfigurationQuery() {
return id(new PhabricatorEditEngineConfigurationQuery())
->setViewer($this->getViewer())
->withEngineKeys(array($this->getEngineKey()));
}
private function loadEditEngineConfigurationWithQuery(
PhabricatorEditEngineConfigurationQuery $query,
$sort_method) {
if ($sort_method) {
$results = $query->execute();
$results = msort($results, $sort_method);
$result = head($results);
} else {
$result = $query->executeOne();
}
if (!$result) {
return null;
}
$this->editEngineConfiguration = $result;
return $result;
}
private function loadEditEngineConfigurationWithIdentifier($identifier) {
$query = $this->newConfigurationQuery()
->withIdentifiers(array($identifier));
return $this->loadEditEngineConfigurationWithQuery($query, null);
}
private function loadDefaultConfiguration() {
$query = $this->newConfigurationQuery()
->withIdentifiers(
array(
self::EDITENGINECONFIG_DEFAULT,
))
->withIgnoreDatabaseConfigurations(true);
return $this->loadEditEngineConfigurationWithQuery($query, null);
}
private function loadDefaultCreateConfiguration() {
$query = $this->newConfigurationQuery()
->withIsDefault(true)
->withIsDisabled(false);
return $this->loadEditEngineConfigurationWithQuery(
$query,
'getCreateSortKey');
}
public function loadDefaultEditConfiguration($object) {
$query = $this->newConfigurationQuery()
->withIsEdit(true)
->withIsDisabled(false);
// If this object supports subtyping, we edit it with a form of the same
// subtype: so "bug" tasks get edited with "bug" forms.
if ($object instanceof PhabricatorEditEngineSubtypeInterface) {
$query->withSubtypes(
array(
$object->getEditEngineSubtype(),
));
}
return $this->loadEditEngineConfigurationWithQuery(
$query,
'getEditSortKey');
}
final public function getBuiltinEngineConfigurations() {
$configurations = $this->newBuiltinEngineConfigurations();
if (!$configurations) {
throw new Exception(
pht(
'EditEngine ("%s") returned no builtin engine configurations, but '.
'an edit engine must have at least one configuration.',
get_class($this)));
}
assert_instances_of($configurations, 'PhabricatorEditEngineConfiguration');
$has_default = false;
foreach ($configurations as $config) {
if ($config->getBuiltinKey() == self::EDITENGINECONFIG_DEFAULT) {
$has_default = true;
}
}
if (!$has_default) {
$first = head($configurations);
if (!$first->getBuiltinKey()) {
$first
->setBuiltinKey(self::EDITENGINECONFIG_DEFAULT)
->setIsDefault(true)
->setIsEdit(true);
if (!strlen($first->getName())) {
$first->setName($this->getObjectCreateShortText());
}
} else {
throw new Exception(
pht(
'EditEngine ("%s") returned builtin engine configurations, '.
'but none are marked as default and the first configuration has '.
'a different builtin key already. Mark a builtin as default or '.
'omit the key from the first configuration',
get_class($this)));
}
}
$builtins = array();
foreach ($configurations as $key => $config) {
$builtin_key = $config->getBuiltinKey();
if ($builtin_key === null) {
throw new Exception(
pht(
'EditEngine ("%s") returned builtin engine configurations, '.
'but one (with key "%s") is missing a builtin key. Provide a '.
'builtin key for each configuration (you can omit it from the '.
'first configuration in the list to automatically assign the '.
'default key).',
get_class($this),
$key));
}
if (isset($builtins[$builtin_key])) {
throw new Exception(
pht(
'EditEngine ("%s") returned builtin engine configurations, '.
'but at least two specify the same builtin key ("%s"). Engines '.
'must have unique builtin keys.',
get_class($this),
$builtin_key));
}
$builtins[$builtin_key] = $config;
}
return $builtins;
}
protected function newBuiltinEngineConfigurations() {
return array(
$this->newConfiguration(),
);
}
final protected function newConfiguration() {
return PhabricatorEditEngineConfiguration::initializeNewConfiguration(
$this->getViewer(),
$this);
}
/* -( Managing URIs )------------------------------------------------------ */
/**
* @task uri
*/
abstract protected function getObjectViewURI($object);
/**
* @task uri
*/
protected function getObjectCreateCancelURI($object) {
return $this->getApplication()->getApplicationURI();
}
/**
* @task uri
*/
protected function getEditorURI() {
return $this->getApplication()->getApplicationURI('edit/');
}
/**
* @task uri
*/
protected function getObjectEditCancelURI($object) {
return $this->getObjectViewURI($object);
}
/**
* @task uri
*/
public function getEditURI($object = null, $path = null) {
$parts = array();
$parts[] = $this->getEditorURI();
if ($object && $object->getID()) {
$parts[] = $object->getID().'/';
}
if ($path !== null) {
$parts[] = $path;
}
return implode('', $parts);
}
public function getEffectiveObjectViewURI($object) {
if ($this->getIsCreate()) {
return $this->getObjectViewURI($object);
}
$page = $this->getSelectedPage();
if ($page) {
$view_uri = $page->getViewURI();
if ($view_uri !== null) {
return $view_uri;
}
}
return $this->getObjectViewURI($object);
}
public function getEffectiveObjectEditDoneURI($object) {
return $this->getEffectiveObjectViewURI($object);
}
public function getEffectiveObjectEditCancelURI($object) {
$page = $this->getSelectedPage();
if ($page) {
$view_uri = $page->getViewURI();
if ($view_uri !== null) {
return $view_uri;
}
}
return $this->getObjectEditCancelURI($object);
}
/* -( Creating and Loading Objects )--------------------------------------- */
/**
* Initialize a new object for creation.
*
* @return object Newly initialized object.
* @task load
*/
abstract protected function newEditableObject();
/**
* Build an empty query for objects.
*
* @return PhabricatorPolicyAwareQuery Query.
* @task load
*/
abstract protected function newObjectQuery();
/**
* Test if this workflow is creating a new object or editing an existing one.
*
* @return bool True if a new object is being created.
* @task load
*/
final public function getIsCreate() {
return $this->isCreate;
}
/**
* Initialize a new object for object creation via Conduit.
*
* @return object Newly initialized object.
* @param list<wild> Raw transactions.
* @task load
*/
protected function newEditableObjectFromConduit(array $raw_xactions) {
return $this->newEditableObject();
}
/**
* Initialize a new object for documentation creation.
*
* @return object Newly initialized object.
* @task load
*/
protected function newEditableObjectForDocumentation() {
return $this->newEditableObject();
}
/**
* Flag this workflow as a create or edit.
*
* @param bool True if this is a create workflow.
* @return this
* @task load
*/
private function setIsCreate($is_create) {
$this->isCreate = $is_create;
return $this;
}
/**
* Try to load an object by ID, PHID, or monogram. This is done primarily
* to make Conduit a little easier to use.
*
* @param wild ID, PHID, or monogram.
* @param list<const> List of required capability constants, or omit for
* defaults.
* @return object Corresponding editable object.
* @task load
*/
private function newObjectFromIdentifier(
$identifier,
array $capabilities = array()) {
if (is_int($identifier) || ctype_digit($identifier)) {
$object = $this->newObjectFromID($identifier, $capabilities);
if (!$object) {
throw new Exception(
pht(
'No object exists with ID "%s".',
$identifier));
}
return $object;
}
$type_unknown = PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN;
if (phid_get_type($identifier) != $type_unknown) {
$object = $this->newObjectFromPHID($identifier, $capabilities);
if (!$object) {
throw new Exception(
pht(
'No object exists with PHID "%s".',
$identifier));
}
return $object;
}
$target = id(new PhabricatorObjectQuery())
->setViewer($this->getViewer())
->withNames(array($identifier))
->executeOne();
if (!$target) {
throw new Exception(
pht(
'Monogram "%s" does not identify a valid object.',
$identifier));
}
$expect = $this->newEditableObject();
$expect_class = get_class($expect);
$target_class = get_class($target);
if ($expect_class !== $target_class) {
throw new Exception(
pht(
'Monogram "%s" identifies an object of the wrong type. Loaded '.
'object has class "%s", but this editor operates on objects of '.
'type "%s".',
$identifier,
$target_class,
$expect_class));
}
// Load the object by PHID using this engine's standard query. This makes
// sure it's really valid, goes through standard policy check logic, and
// picks up any `need...()` clauses we want it to load with.
$object = $this->newObjectFromPHID($target->getPHID(), $capabilities);
if (!$object) {
throw new Exception(
pht(
'Failed to reload object identified by monogram "%s" when '.
'querying by PHID.',
$identifier));
}
return $object;
}
/**
* Load an object by ID.
*
* @param int Object ID.
* @param list<const> List of required capability constants, or omit for
* defaults.
* @return object|null Object, or null if no such object exists.
* @task load
*/
private function newObjectFromID($id, array $capabilities = array()) {
$query = $this->newObjectQuery()
->withIDs(array($id));
return $this->newObjectFromQuery($query, $capabilities);
}
/**
* Load an object by PHID.
*
* @param phid Object PHID.
* @param list<const> List of required capability constants, or omit for
* defaults.
* @return object|null Object, or null if no such object exists.
* @task load
*/
private function newObjectFromPHID($phid, array $capabilities = array()) {
$query = $this->newObjectQuery()
->withPHIDs(array($phid));
return $this->newObjectFromQuery($query, $capabilities);
}
/**
* Load an object given a configured query.
*
* @param PhabricatorPolicyAwareQuery Configured query.
* @param list<const> List of required capability constants, or omit for
* defaults.
* @return object|null Object, or null if no such object exists.
* @task load
*/
private function newObjectFromQuery(
PhabricatorPolicyAwareQuery $query,
array $capabilities = array()) {
$viewer = $this->getViewer();
if (!$capabilities) {
$capabilities = array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
$object = $query
->setViewer($viewer)
->requireCapabilities($capabilities)
->executeOne();
if (!$object) {
return null;
}
return $object;
}
/**
* Verify that an object is appropriate for editing.
*
* @param wild Loaded value.
* @return void
* @task load
*/
private function validateObject($object) {
if (!$object || !is_object($object)) {
throw new Exception(
pht(
'EditEngine "%s" created or loaded an invalid object: object must '.
'actually be an object, but is of some other type ("%s").',
get_class($this),
gettype($object)));
}
if (!($object instanceof PhabricatorApplicationTransactionInterface)) {
throw new Exception(
pht(
'EditEngine "%s" created or loaded an invalid object: object (of '.
'class "%s") must implement "%s", but does not.',
get_class($this),
get_class($object),
'PhabricatorApplicationTransactionInterface'));
}
}
/* -( Responding to Web Requests )----------------------------------------- */
final public function buildResponse() {
$viewer = $this->getViewer();
$controller = $this->getController();
$request = $controller->getRequest();
$action = $this->getEditAction();
$capabilities = array();
$use_default = false;
$require_create = true;
switch ($action) {
case 'comment':
$capabilities = array(
PhabricatorPolicyCapability::CAN_VIEW,
);
$use_default = true;
break;
case 'parameters':
$use_default = true;
break;
case 'nodefault':
case 'nocreate':
case 'nomanage':
$require_create = false;
break;
default:
break;
}
$object = $this->getTargetObject();
if (!$object) {
$id = $request->getURIData('id');
if ($id) {
$this->setIsCreate(false);
$object = $this->newObjectFromID($id, $capabilities);
if (!$object) {
return new Aphront404Response();
}
} else {
// Make sure the viewer has permission to create new objects of
// this type if we're going to create a new object.
if ($require_create) {
$this->requireCreateCapability();
}
$this->setIsCreate(true);
$object = $this->newEditableObject();
}
} else {
$id = $object->getID();
}
$this->validateObject($object);
if ($use_default) {
$config = $this->loadDefaultConfiguration();
if (!$config) {
return new Aphront404Response();
}
} else {
$form_key = $request->getURIData('formKey');
if (strlen($form_key)) {
$config = $this->loadEditEngineConfigurationWithIdentifier($form_key);
if (!$config) {
return new Aphront404Response();
}
if ($id && !$config->getIsEdit()) {
return $this->buildNotEditFormRespose($object, $config);
}
} else {
if ($id) {
$config = $this->loadDefaultEditConfiguration($object);
if (!$config) {
return $this->buildNoEditResponse($object);
}
} else {
$config = $this->loadDefaultCreateConfiguration();
if (!$config) {
return $this->buildNoCreateResponse($object);
}
}
}
}
if ($config->getIsDisabled()) {
return $this->buildDisabledFormResponse($object, $config);
}
$page_key = $request->getURIData('pageKey');
if (!strlen($page_key)) {
$pages = $this->getPages($object);
if ($pages) {
$page_key = head_key($pages);
}
}
if (strlen($page_key)) {
$page = $this->selectPage($object, $page_key);
if (!$page) {
return new Aphront404Response();
}
}
switch ($action) {
case 'parameters':
return $this->buildParametersResponse($object);
case 'nodefault':
return $this->buildNoDefaultResponse($object);
case 'nocreate':
return $this->buildNoCreateResponse($object);
case 'nomanage':
return $this->buildNoManageResponse($object);
case 'comment':
return $this->buildCommentResponse($object);
default:
return $this->buildEditResponse($object);
}
}
private function buildCrumbs($object, $final = false) {
$controller = $this->getController();
$crumbs = $controller->buildApplicationCrumbsForEditEngine();
if ($this->getIsCreate()) {
$create_text = $this->getObjectCreateShortText();
if ($final) {
$crumbs->addTextCrumb($create_text);
} else {
$edit_uri = $this->getEditURI($object);
$crumbs->addTextCrumb($create_text, $edit_uri);
}
} else {
$crumbs->addTextCrumb(
$this->getObjectEditShortText($object),
$this->getEffectiveObjectViewURI($object));
$edit_text = pht('Edit');
if ($final) {
$crumbs->addTextCrumb($edit_text);
} else {
$edit_uri = $this->getEditURI($object);
$crumbs->addTextCrumb($edit_text, $edit_uri);
}
}
return $crumbs;
}
private function buildEditResponse($object) {
$viewer = $this->getViewer();
$controller = $this->getController();
$request = $controller->getRequest();
$fields = $this->buildEditFields($object);
$template = $object->getApplicationTransactionTemplate();
if ($this->getIsCreate()) {
$cancel_uri = $this->getObjectCreateCancelURI($object);
$submit_button = $this->getObjectCreateButtonText($object);
} else {
$cancel_uri = $this->getEffectiveObjectEditCancelURI($object);
$submit_button = $this->getObjectEditButtonText($object);
}
$config = $this->getEditEngineConfiguration()
->attachEngine($this);
// NOTE: Don't prompt users to override locks when creating objects,
// even if the default settings would create a locked object.
$can_interact = PhabricatorPolicyFilter::canInteract($viewer, $object);
if (!$can_interact &&
!$this->getIsCreate() &&
!$request->getBool('editEngine') &&
!$request->getBool('overrideLock')) {
$lock = PhabricatorEditEngineLock::newForObject($viewer, $object);
$dialog = $this->getController()
->newDialog()
->addHiddenInput('overrideLock', true)
->setDisableWorkflowOnSubmit(true)
->addCancelButton($cancel_uri);
return $lock->willPromptUserForLockOverrideWithDialog($dialog);
}
$validation_exception = null;
- if ($request->isFormPost() && $request->getBool('editEngine')) {
+ if ($request->isFormOrHisecPost() && $request->getBool('editEngine')) {
$submit_fields = $fields;
foreach ($submit_fields as $key => $field) {
if (!$field->shouldGenerateTransactionsFromSubmit()) {
unset($submit_fields[$key]);
continue;
}
}
// Before we read the submitted values, store a copy of what we would
// use if the form was empty so we can figure out which transactions are
// just setting things to their default values for the current form.
$defaults = array();
foreach ($submit_fields as $key => $field) {
$defaults[$key] = $field->getValueForTransaction();
}
foreach ($submit_fields as $key => $field) {
$field->setIsSubmittedForm(true);
if (!$field->shouldReadValueFromSubmit()) {
continue;
}
$field->readValueFromSubmit($request);
}
$xactions = array();
if ($this->getIsCreate()) {
$xactions[] = id(clone $template)
->setTransactionType(PhabricatorTransactions::TYPE_CREATE);
if ($this->supportsSubtypes()) {
$xactions[] = id(clone $template)
->setTransactionType(PhabricatorTransactions::TYPE_SUBTYPE)
->setNewValue($config->getSubtype());
}
}
foreach ($submit_fields as $key => $field) {
$field_value = $field->getValueForTransaction();
$type_xactions = $field->generateTransactions(
clone $template,
array(
'value' => $field_value,
));
foreach ($type_xactions as $type_xaction) {
$default = $defaults[$key];
if ($default === $field->getValueForTransaction()) {
$type_xaction->setIsDefaultTransaction(true);
}
$xactions[] = $type_xaction;
}
}
$editor = $object->getApplicationTransactionEditor()
->setActor($viewer)
->setContentSourceFromRequest($request)
+ ->setCancelURI($cancel_uri)
->setContinueOnNoEffect(true);
try {
$xactions = $this->willApplyTransactions($object, $xactions);
$editor->applyTransactions($object, $xactions);
$this->didApplyTransactions($object, $xactions);
return $this->newEditResponse($request, $object, $xactions);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
foreach ($fields as $field) {
$message = $this->getValidationExceptionShortMessage($ex, $field);
if ($message === null) {
continue;
}
$field->setControlError($message);
}
}
} else {
if ($this->getIsCreate()) {
$template = $request->getStr('template');
if (strlen($template)) {
$template_object = $this->newObjectFromIdentifier(
$template,
array(
PhabricatorPolicyCapability::CAN_VIEW,
));
if (!$template_object) {
return new Aphront404Response();
}
} else {
$template_object = null;
}
if ($template_object) {
$copy_fields = $this->buildEditFields($template_object);
$copy_fields = mpull($copy_fields, null, 'getKey');
foreach ($copy_fields as $copy_key => $copy_field) {
if (!$copy_field->getIsCopyable()) {
unset($copy_fields[$copy_key]);
}
}
} else {
$copy_fields = array();
}
foreach ($fields as $field) {
if (!$field->shouldReadValueFromRequest()) {
continue;
}
$field_key = $field->getKey();
if (isset($copy_fields[$field_key])) {
$field->readValueFromField($copy_fields[$field_key]);
}
$field->readValueFromRequest($request);
}
}
}
$action_button = $this->buildEditFormActionButton($object);
if ($this->getIsCreate()) {
$header_text = $this->getFormHeaderText($object);
} else {
$header_text = $this->getObjectEditTitleText($object);
}
$show_preview = !$request->isAjax();
if ($show_preview) {
$previews = array();
foreach ($fields as $field) {
$preview = $field->getPreviewPanel();
if (!$preview) {
continue;
}
$control_id = $field->getControlID();
$preview
->setControlID($control_id)
->setPreviewURI('/transactions/remarkuppreview/');
$previews[] = $preview;
}
} else {
$previews = array();
}
$form = $this->buildEditForm($object, $fields);
$crumbs = $this->buildCrumbs($object, $final = true);
$crumbs->setBorder(true);
if ($request->isAjax()) {
return $this->getController()
->newDialog()
->setWidth(AphrontDialogView::WIDTH_FULL)
->setTitle($header_text)
->setValidationException($validation_exception)
->appendForm($form)
->addCancelButton($cancel_uri)
->addSubmitButton($submit_button);
}
$box_header = id(new PHUIHeaderView())
->setHeader($header_text);
if ($action_button) {
$box_header->addActionLink($action_button);
}
$box = id(new PHUIObjectBoxView())
->setUser($viewer)
->setHeader($box_header)
->setValidationException($validation_exception)
->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
->appendChild($form);
// This is fairly questionable, but in use by Settings.
if ($request->getURIData('formSaved')) {
$box->setFormSaved(true);
}
$content = array(
$box,
$previews,
);
$view = new PHUITwoColumnView();
$page_header = $this->getPageHeader($object);
if ($page_header) {
$view->setHeader($page_header);
}
+ $view->setFooter($content);
+
$page = $controller->newPage()
->setTitle($header_text)
->setCrumbs($crumbs)
->appendChild($view);
$navigation = $this->getNavigation();
if ($navigation) {
- $view->setFixed(true);
- $view->setNavigation($navigation);
- $view->setMainColumn($content);
- } else {
- $view->setFooter($content);
+ $page->setNavigation($navigation);
}
return $page;
}
protected function newEditResponse(
AphrontRequest $request,
$object,
array $xactions) {
return id(new AphrontRedirectResponse())
->setURI($this->getEffectiveObjectEditDoneURI($object));
}
private function buildEditForm($object, array $fields) {
$viewer = $this->getViewer();
$controller = $this->getController();
$request = $controller->getRequest();
$fields = $this->willBuildEditForm($object, $fields);
+ $request_path = $request->getRequestURI()
+ ->setQueryParams(array());
+
$form = id(new AphrontFormView())
->setUser($viewer)
+ ->setAction($request_path)
->addHiddenInput('editEngine', 'true');
foreach ($this->contextParameters as $param) {
$form->addHiddenInput($param, $request->getStr($param));
}
+ $requires_mfa = false;
+ if ($object instanceof PhabricatorEditEngineMFAInterface) {
+ $mfa_engine = PhabricatorEditEngineMFAEngine::newEngineForObject($object)
+ ->setViewer($viewer);
+ $requires_mfa = $mfa_engine->shouldRequireMFA();
+ }
+
+ if ($requires_mfa) {
+ $message = pht(
+ 'You will be required to provide multi-factor credentials to make '.
+ 'changes.');
+ $form->appendChild(
+ id(new PHUIInfoView())
+ ->setSeverity(PHUIInfoView::SEVERITY_MFA)
+ ->setErrors(array($message)));
+
+ // TODO: This should also set workflow on the form, so the user doesn't
+ // lose any form data if they "Cancel". However, Maniphest currently
+ // overrides "newEditResponse()" if the request is Ajax and returns a
+ // bag of view data. This can reasonably be cleaned up when workboards
+ // get their next iteration.
+ }
+
foreach ($fields as $field) {
if (!$field->getIsFormField()) {
continue;
}
$field->appendToForm($form);
}
if ($this->getIsCreate()) {
$cancel_uri = $this->getObjectCreateCancelURI($object);
$submit_button = $this->getObjectCreateButtonText($object);
} else {
$cancel_uri = $this->getEffectiveObjectEditCancelURI($object);
$submit_button = $this->getObjectEditButtonText($object);
}
if (!$request->isAjax()) {
$buttons = id(new AphrontFormSubmitControl())
->setValue($submit_button);
if ($cancel_uri) {
$buttons->addCancelButton($cancel_uri);
}
$form->appendControl($buttons);
}
return $form;
}
protected function willBuildEditForm($object, array $fields) {
return $fields;
}
private function buildEditFormActionButton($object) {
if (!$this->isEngineConfigurable()) {
return null;
}
$viewer = $this->getViewer();
$action_view = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($this->buildEditFormActions($object) as $action) {
$action_view->addAction($action);
}
$action_button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('Configure Form'))
->setHref('#')
->setIcon('fa-gear')
->setDropdownMenu($action_view);
return $action_button;
}
private function buildEditFormActions($object) {
$actions = array();
// c4science custo (moved)
$config = $this->getEditEngineConfiguration();
$can_manage = PhabricatorPolicyFilter::hasCapability(
$this->getViewer(),
$config,
PhabricatorPolicyCapability::CAN_EDIT);
// end of c4s custo
if ($this->supportsEditEngineConfiguration()) {
$engine_key = $this->getEngineKey();
if ($can_manage) {
$manage_uri = $config->getURI();
} else {
$manage_uri = $this->getEditURI(null, 'nomanage/');
}
$view_uri = "/transactions/editengine/{$engine_key}/";
if($can_manage) { // c4science custo
$actions[] = id(new PhabricatorActionView())
->setLabel(true)
->setName(pht('Configuration'));
$actions[] = id(new PhabricatorActionView())
->setName(pht('View Form Configurations'))
->setIcon('fa-list-ul')
->setHref($view_uri);
$actions[] = id(new PhabricatorActionView())
->setName(pht('Edit Form Configuration'))
->setIcon('fa-pencil')
->setHref($manage_uri)
->setDisabled(!$can_manage)
->setWorkflow(!$can_manage);
} // end of c4s custo
}
$actions[] = id(new PhabricatorActionView())
->setLabel(true)
->setName(pht('Documentation'));
$actions[] = id(new PhabricatorActionView())
->setName(pht('Using HTTP Parameters'))
->setIcon('fa-book')
->setHref($this->getEditURI($object, 'parameters/'));
if($can_manage) { // c4science custo
$doc_href = PhabricatorEnv::getDoclink('User Guide: Customizing Forms');
$actions[] = id(new PhabricatorActionView())
->setName(pht('User Guide: Customizing Forms'))
->setIcon('fa-book')
->setHref($doc_href);
} // end of c4s custo
return $actions;
}
public function newNUXButton($text) {
$specs = $this->newCreateActionSpecifications(array());
$head = head($specs);
return id(new PHUIButtonView())
->setTag('a')
->setText($text)
->setHref($head['uri'])
->setDisabled($head['disabled'])
->setWorkflow($head['workflow'])
->setColor(PHUIButtonView::GREEN);
}
final public function addActionToCrumbs(
PHUICrumbsView $crumbs,
array $parameters = array()) {
$viewer = $this->getViewer();
$specs = $this->newCreateActionSpecifications($parameters);
$head = head($specs);
$menu_uri = $head['uri'];
$dropdown = null;
if (count($specs) > 1) {
$menu_icon = 'fa-caret-square-o-down';
$menu_name = $this->getObjectCreateShortText();
$workflow = false;
$disabled = false;
$dropdown = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($specs as $spec) {
$dropdown->addAction(
id(new PhabricatorActionView())
->setName($spec['name'])
->setIcon($spec['icon'])
->setHref($spec['uri'])
->setDisabled($head['disabled'])
->setWorkflow($head['workflow']));
}
} else {
$menu_icon = $head['icon'];
$menu_name = $head['name'];
$workflow = $head['workflow'];
$disabled = $head['disabled'];
}
$action = id(new PHUIListItemView())
->setName($menu_name)
->setHref($menu_uri)
->setIcon($menu_icon)
->setWorkflow($workflow)
->setDisabled($disabled);
if ($dropdown) {
$action->setDropdownMenu($dropdown);
}
$crumbs->addAction($action);
}
/**
* Build a raw description of available "Create New Object" UI options so
* other methods can build menus or buttons.
*/
public function newCreateActionSpecifications(array $parameters) {
$viewer = $this->getViewer();
$can_create = $this->hasCreateCapability();
if ($can_create) {
$configs = $this->loadUsableConfigurationsForCreate();
} else {
$configs = array();
}
$disabled = false;
$workflow = false;
$menu_icon = 'fa-plus-square';
$specs = array();
if (!$configs) {
if ($viewer->isLoggedIn()) {
$disabled = true;
} else {
// If the viewer isn't logged in, assume they'll get hit with a login
// dialog and are likely able to create objects after they log in.
$disabled = false;
}
$workflow = true;
if ($can_create) {
$create_uri = $this->getEditURI(null, 'nodefault/');
} else {
$create_uri = $this->getEditURI(null, 'nocreate/');
}
$specs[] = array(
'name' => $this->getObjectCreateShortText(),
'uri' => $create_uri,
'icon' => $menu_icon,
'disabled' => $disabled,
'workflow' => $workflow,
);
} else {
foreach ($configs as $config) {
$config_uri = $config->getCreateURI();
if ($parameters) {
$config_uri = (string)id(new PhutilURI($config_uri))
->setQueryParams($parameters);
}
$specs[] = array(
'name' => $config->getDisplayName(),
'uri' => $config_uri,
'icon' => 'fa-plus',
'disabled' => false,
'workflow' => false,
);
}
}
return $specs;
}
final public function buildEditEngineCommentView($object) {
$config = $this->loadDefaultEditConfiguration($object);
if (!$config) {
// TODO: This just nukes the entire comment form if you don't have access
// to any edit forms. We might want to tailor this UX a bit.
return id(new PhabricatorApplicationTransactionCommentView())
->setNoPermission(true);
}
$viewer = $this->getViewer();
$can_interact = PhabricatorPolicyFilter::canInteract($viewer, $object);
if (!$can_interact) {
$lock = PhabricatorEditEngineLock::newForObject($viewer, $object);
return id(new PhabricatorApplicationTransactionCommentView())
->setEditEngineLock($lock);
}
$object_phid = $object->getPHID();
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
if ($is_serious) {
$header_text = $this->getCommentViewSeriousHeaderText($object);
$button_text = $this->getCommentViewSeriousButtonText($object);
} else {
$header_text = $this->getCommentViewHeaderText($object);
$button_text = $this->getCommentViewButtonText($object);
}
$comment_uri = $this->getEditURI($object, 'comment/');
+ $requires_mfa = false;
+ if ($object instanceof PhabricatorEditEngineMFAInterface) {
+ $mfa_engine = PhabricatorEditEngineMFAEngine::newEngineForObject($object)
+ ->setViewer($viewer);
+ $requires_mfa = $mfa_engine->shouldRequireMFA();
+ }
+
$view = id(new PhabricatorApplicationTransactionCommentView())
->setUser($viewer)
->setObjectPHID($object_phid)
->setHeaderText($header_text)
->setAction($comment_uri)
+ ->setRequiresMFA($requires_mfa)
->setSubmitButtonName($button_text);
$draft = PhabricatorVersionedDraft::loadDraft(
$object_phid,
$viewer->getPHID());
if ($draft) {
$view->setVersionedDraft($draft);
}
$view->setCurrentVersion($this->loadDraftVersion($object));
$fields = $this->buildEditFields($object);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
$comment_actions = array();
foreach ($fields as $field) {
if (!$field->shouldGenerateTransactionsFromComment()) {
continue;
}
if (!$can_edit) {
if (!$field->getCanApplyWithoutEditCapability()) {
continue;
}
}
$comment_action = $field->getCommentAction();
if (!$comment_action) {
continue;
}
$key = $comment_action->getKey();
// TODO: Validate these better.
$comment_actions[$key] = $comment_action;
}
$comment_actions = msortv($comment_actions, 'getSortVector');
$view->setCommentActions($comment_actions);
$comment_groups = $this->newCommentActionGroups();
$view->setCommentActionGroups($comment_groups);
return $view;
}
protected function loadDraftVersion($object) {
$viewer = $this->getViewer();
if (!$viewer->isLoggedIn()) {
return null;
}
$template = $object->getApplicationTransactionTemplate();
$conn_r = $template->establishConnection('r');
// Find the most recent transaction the user has written. We'll use this
// as a version number to make sure that out-of-date drafts get discarded.
$result = queryfx_one(
$conn_r,
'SELECT id AS version FROM %T
WHERE objectPHID = %s AND authorPHID = %s
ORDER BY id DESC LIMIT 1',
$template->getTableName(),
$object->getPHID(),
$viewer->getPHID());
if ($result) {
return (int)$result['version'];
} else {
return null;
}
}
/* -( Responding to HTTP Parameter Requests )------------------------------ */
/**
* Respond to a request for documentation on HTTP parameters.
*
* @param object Editable object.
* @return AphrontResponse Response object.
* @task http
*/
private function buildParametersResponse($object) {
$controller = $this->getController();
$viewer = $this->getViewer();
$request = $controller->getRequest();
$fields = $this->buildEditFields($object);
$crumbs = $this->buildCrumbs($object);
$crumbs->addTextCrumb(pht('HTTP Parameters'));
$crumbs->setBorder(true);
$header_text = pht(
'HTTP Parameters: %s',
$this->getObjectCreateShortText());
$header = id(new PHUIHeaderView())
->setHeader($header_text);
$help_view = id(new PhabricatorApplicationEditHTTPParameterHelpView())
->setUser($viewer)
->setFields($fields);
$document = id(new PHUIDocumentView())
->setUser($viewer)
->setHeader($header)
->appendChild($help_view);
return $controller->newPage()
->setTitle(pht('HTTP Parameters'))
->setCrumbs($crumbs)
->appendChild($document);
}
private function buildError($object, $title, $body) {
$cancel_uri = $this->getObjectCreateCancelURI($object);
$dialog = $this->getController()
->newDialog()
->addCancelButton($cancel_uri);
if ($title !== null) {
$dialog->setTitle($title);
}
if ($body !== null) {
$dialog->appendParagraph($body);
}
return $dialog;
}
private function buildNoDefaultResponse($object) {
return $this->buildError(
$object,
pht('No Default Create Forms'),
pht(
'This application is not configured with any forms for creating '.
'objects that are visible to you and enabled.'));
}
private function buildNoCreateResponse($object) {
return $this->buildError(
$object,
pht('No Create Permission'),
pht('You do not have permission to create these objects.'));
}
private function buildNoManageResponse($object) {
return $this->buildError(
$object,
pht('No Manage Permission'),
pht(
'You do not have permission to configure forms for this '.
'application.'));
}
private function buildNoEditResponse($object) {
return $this->buildError(
$object,
pht('No Edit Forms'),
pht(
'You do not have access to any forms which are enabled and marked '.
'as edit forms.'));
}
private function buildNotEditFormRespose($object, $config) {
return $this->buildError(
$object,
pht('Not an Edit Form'),
pht(
'This form ("%s") is not marked as an edit form, so '.
'it can not be used to edit objects.',
$config->getName()));
}
private function buildDisabledFormResponse($object, $config) {
return $this->buildError(
$object,
pht('Form Disabled'),
pht(
'This form ("%s") has been disabled, so it can not be used.',
$config->getName()));
}
private function buildLockedObjectResponse($object) {
$dialog = $this->buildError($object, null, null);
$viewer = $this->getViewer();
$lock = PhabricatorEditEngineLock::newForObject($viewer, $object);
return $lock->willBlockUserInteractionWithDialog($dialog);
}
private function buildCommentResponse($object) {
$viewer = $this->getViewer();
if ($this->getIsCreate()) {
return new Aphront404Response();
}
$controller = $this->getController();
$request = $controller->getRequest();
- if (!$request->isFormPost()) {
+ // NOTE: We handle hisec inside the transaction editor with "Sign With MFA"
+ // comment actions.
+ if (!$request->isFormOrHisecPost()) {
return new Aphront400Response();
}
$can_interact = PhabricatorPolicyFilter::canInteract($viewer, $object);
if (!$can_interact) {
return $this->buildLockedObjectResponse($object);
}
$config = $this->loadDefaultEditConfiguration($object);
if (!$config) {
return new Aphront404Response();
}
$fields = $this->buildEditFields($object);
$is_preview = $request->isPreviewRequest();
$view_uri = $this->getEffectiveObjectViewURI($object);
$template = $object->getApplicationTransactionTemplate();
$comment_template = $template->getApplicationTransactionCommentObject();
$comment_text = $request->getStr('comment');
$actions = $request->getStr('editengine.actions');
if ($actions) {
$actions = phutil_json_decode($actions);
}
if ($is_preview) {
$version_key = PhabricatorVersionedDraft::KEY_VERSION;
$request_version = $request->getInt($version_key);
$current_version = $this->loadDraftVersion($object);
if ($request_version >= $current_version) {
$draft = PhabricatorVersionedDraft::loadOrCreateDraft(
$object->getPHID(),
$viewer->getPHID(),
$current_version);
$is_empty = (!strlen($comment_text) && !$actions);
$draft
->setProperty('comment', $comment_text)
->setProperty('actions', $actions)
->save();
$draft_engine = $this->newDraftEngine($object);
if ($draft_engine) {
$draft_engine
->setVersionedDraft($draft)
->synchronize();
}
}
}
$xactions = array();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
if ($actions) {
$action_map = array();
foreach ($actions as $action) {
$type = idx($action, 'type');
if (!$type) {
continue;
}
if (empty($fields[$type])) {
continue;
}
$action_map[$type] = $action;
}
foreach ($action_map as $type => $action) {
$field = $fields[$type];
if (!$field->shouldGenerateTransactionsFromComment()) {
continue;
}
// If you don't have edit permission on the object, you're limited in
// which actions you can take via the comment form. Most actions
// need edit permission, but some actions (like "Accept Revision")
// can be applied by anyone with view permission.
if (!$can_edit) {
if (!$field->getCanApplyWithoutEditCapability()) {
// We know the user doesn't have the capability, so this will
// raise a policy exception.
PhabricatorPolicyFilter::requireCapability(
$viewer,
$object,
PhabricatorPolicyCapability::CAN_EDIT);
}
}
if (array_key_exists('initialValue', $action)) {
$field->setInitialValue($action['initialValue']);
}
$field->readValueFromComment(idx($action, 'value'));
$type_xactions = $field->generateTransactions(
clone $template,
array(
'value' => $field->getValueForTransaction(),
));
foreach ($type_xactions as $type_xaction) {
$xactions[] = $type_xaction;
}
}
}
$auto_xactions = $this->newAutomaticCommentTransactions($object);
foreach ($auto_xactions as $xaction) {
$xactions[] = $xaction;
}
if (strlen($comment_text) || !$xactions) {
$xactions[] = id(clone $template)
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(clone $comment_template)
->setContent($comment_text));
}
$editor = $object->getApplicationTransactionEditor()
->setActor($viewer)
->setContinueOnNoEffect($request->isContinueRequest())
->setContinueOnMissingFields(true)
->setContentSourceFromRequest($request)
+ ->setCancelURI($view_uri)
->setRaiseWarnings(!$request->getBool('editEngine.warnings'))
->setIsPreview($is_preview);
try {
$xactions = $editor->applyTransactions($object, $xactions);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
return id(new PhabricatorApplicationTransactionValidationResponse())
->setCancelURI($view_uri)
->setException($ex);
} catch (PhabricatorApplicationTransactionNoEffectException $ex) {
return id(new PhabricatorApplicationTransactionNoEffectResponse())
->setCancelURI($view_uri)
->setException($ex);
} catch (PhabricatorApplicationTransactionWarningException $ex) {
return id(new PhabricatorApplicationTransactionWarningResponse())
->setCancelURI($view_uri)
->setException($ex);
}
if (!$is_preview) {
PhabricatorVersionedDraft::purgeDrafts(
$object->getPHID(),
$viewer->getPHID());
$draft_engine = $this->newDraftEngine($object);
if ($draft_engine) {
$draft_engine
->setVersionedDraft(null)
->synchronize();
}
}
if ($request->isAjax() && $is_preview) {
$preview_content = $this->newCommentPreviewContent($object, $xactions);
+ $raw_view_data = $request->getStr('viewData');
+ try {
+ $view_data = phutil_json_decode($raw_view_data);
+ } catch (Exception $ex) {
+ $view_data = array();
+ }
+
return id(new PhabricatorApplicationTransactionResponse())
+ ->setObject($object)
->setViewer($viewer)
->setTransactions($xactions)
->setIsPreview($is_preview)
+ ->setViewData($view_data)
->setPreviewContent($preview_content);
} else {
return id(new AphrontRedirectResponse())
->setURI($view_uri);
}
}
protected function newDraftEngine($object) {
$viewer = $this->getViewer();
if ($object instanceof PhabricatorDraftInterface) {
$engine = $object->newDraftEngine();
} else {
$engine = new PhabricatorBuiltinDraftEngine();
}
return $engine
->setObject($object)
->setViewer($viewer);
}
/* -( Conduit )------------------------------------------------------------ */
/**
* Respond to a Conduit edit request.
*
* This method accepts a list of transactions to apply to an object, and
* either edits an existing object or creates a new one.
*
* @task conduit
*/
final public function buildConduitResponse(ConduitAPIRequest $request) {
$viewer = $this->getViewer();
$config = $this->loadDefaultConfiguration();
if (!$config) {
throw new Exception(
pht(
'Unable to load configuration for this EditEngine ("%s").',
get_class($this)));
}
$raw_xactions = $this->getRawConduitTransactions($request);
$identifier = $request->getValue('objectIdentifier');
if ($identifier) {
$this->setIsCreate(false);
// After T13186, each transaction can individually weaken or replace the
// capabilities required to apply it, so we no longer need CAN_EDIT to
// attempt to apply transactions to objects. In practice, almost all
// transactions require CAN_EDIT so we won't get very far if we don't
// have it.
$capabilities = array(
PhabricatorPolicyCapability::CAN_VIEW,
);
$object = $this->newObjectFromIdentifier(
$identifier,
$capabilities);
} else {
$this->requireCreateCapability();
$this->setIsCreate(true);
$object = $this->newEditableObjectFromConduit($raw_xactions);
}
$this->validateObject($object);
$fields = $this->buildEditFields($object);
$types = $this->getConduitEditTypesFromFields($fields);
$template = $object->getApplicationTransactionTemplate();
$xactions = $this->getConduitTransactions(
$request,
$raw_xactions,
$types,
$template);
$editor = $object->getApplicationTransactionEditor()
->setActor($viewer)
->setContentSource($request->newContentSource())
->setContinueOnNoEffect(true);
if (!$this->getIsCreate()) {
$editor->setContinueOnMissingFields(true);
}
$xactions = $editor->applyTransactions($object, $xactions);
$xactions_struct = array();
foreach ($xactions as $xaction) {
$xactions_struct[] = array(
'phid' => $xaction->getPHID(),
);
}
return array(
'object' => array(
'id' => (int)$object->getID(),
'phid' => $object->getPHID(),
),
'transactions' => $xactions_struct,
);
}
private function getRawConduitTransactions(ConduitAPIRequest $request) {
$transactions_key = 'transactions';
$xactions = $request->getValue($transactions_key);
if (!is_array($xactions)) {
throw new Exception(
pht(
'Parameter "%s" is not a list of transactions.',
$transactions_key));
}
foreach ($xactions as $key => $xaction) {
if (!is_array($xaction)) {
throw new Exception(
pht(
'Parameter "%s" must contain a list of transaction descriptions, '.
'but item with key "%s" is not a dictionary.',
$transactions_key,
$key));
}
if (!array_key_exists('type', $xaction)) {
throw new Exception(
pht(
'Parameter "%s" must contain a list of transaction descriptions, '.
'but item with key "%s" is missing a "type" field. Each '.
'transaction must have a type field.',
$transactions_key,
$key));
}
}
return $xactions;
}
/**
* Generate transactions which can be applied from edit actions in a Conduit
* request.
*
* @param ConduitAPIRequest The request.
* @param list<wild> Raw conduit transactions.
* @param list<PhabricatorEditType> Supported edit types.
* @param PhabricatorApplicationTransaction Template transaction.
* @return list<PhabricatorApplicationTransaction> Generated transactions.
* @task conduit
*/
private function getConduitTransactions(
ConduitAPIRequest $request,
array $xactions,
array $types,
PhabricatorApplicationTransaction $template) {
$viewer = $request->getUser();
$results = array();
foreach ($xactions as $key => $xaction) {
$type = $xaction['type'];
if (empty($types[$type])) {
throw new Exception(
pht(
'Transaction with key "%s" has invalid type "%s". This type is '.
'not recognized. Valid types are: %s.',
$key,
$type,
implode(', ', array_keys($types))));
}
}
if ($this->getIsCreate()) {
$results[] = id(clone $template)
->setTransactionType(PhabricatorTransactions::TYPE_CREATE);
}
$is_strict = $request->getIsStrictlyTyped();
foreach ($xactions as $xaction) {
$type = $types[$xaction['type']];
// Let the parameter type interpret the value. This allows you to
// use usernames in list<user> fields, for example.
$parameter_type = $type->getConduitParameterType();
$parameter_type->setViewer($viewer);
try {
$value = $xaction['value'];
$value = $parameter_type->getValue($xaction, 'value', $is_strict);
$value = $type->getTransactionValueFromConduit($value);
$xaction['value'] = $value;
} catch (Exception $ex) {
throw new PhutilProxyException(
pht(
'Exception when processing transaction of type "%s": %s',
$xaction['type'],
$ex->getMessage()),
$ex);
}
$type_xactions = $type->generateTransactions(
clone $template,
$xaction);
foreach ($type_xactions as $type_xaction) {
$results[] = $type_xaction;
}
}
return $results;
}
/**
* @return map<string, PhabricatorEditType>
* @task conduit
*/
private function getConduitEditTypesFromFields(array $fields) {
$types = array();
foreach ($fields as $field) {
$field_types = $field->getConduitEditTypes();
if ($field_types === null) {
continue;
}
foreach ($field_types as $field_type) {
$types[$field_type->getEditType()] = $field_type;
}
}
return $types;
}
public function getConduitEditTypes() {
$config = $this->loadDefaultConfiguration();
if (!$config) {
return array();
}
$object = $this->newEditableObjectForDocumentation();
$fields = $this->buildEditFields($object);
return $this->getConduitEditTypesFromFields($fields);
}
final public static function getAllEditEngines() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getEngineKey')
->execute();
}
final public static function getByKey(PhabricatorUser $viewer, $key) {
return id(new PhabricatorEditEngineQuery())
->setViewer($viewer)
->withEngineKeys(array($key))
->executeOne();
}
public function getIcon() {
$application = $this->getApplication();
return $application->getIcon();
}
private function loadUsableConfigurationsForCreate() {
$viewer = $this->getViewer();
$configs = id(new PhabricatorEditEngineConfigurationQuery())
->setViewer($viewer)
->withEngineKeys(array($this->getEngineKey()))
->withIsDefault(true)
->withIsDisabled(false)
->execute();
$configs = msort($configs, 'getCreateSortKey');
// Attach this specific engine to configurations we load so they can access
// any runtime configuration. For example, this allows us to generate the
// correct "Create Form" buttons when editing forms, see T12301.
foreach ($configs as $config) {
$config->attachEngine($this);
}
return $configs;
}
protected function getValidationExceptionShortMessage(
PhabricatorApplicationTransactionValidationException $ex,
PhabricatorEditField $field) {
$xaction_type = $field->getTransactionType();
if ($xaction_type === null) {
return null;
}
return $ex->getShortMessage($xaction_type);
}
protected function getCreateNewObjectPolicy() {
return PhabricatorPolicies::POLICY_USER;
}
private function requireCreateCapability() {
PhabricatorPolicyFilter::requireCapability(
$this->getViewer(),
$this,
PhabricatorPolicyCapability::CAN_EDIT);
}
private function hasCreateCapability() {
return PhabricatorPolicyFilter::hasCapability(
$this->getViewer(),
$this,
PhabricatorPolicyCapability::CAN_EDIT);
}
public function isCommentAction() {
return ($this->getEditAction() == 'comment');
}
public function getEditAction() {
$controller = $this->getController();
$request = $controller->getRequest();
return $request->getURIData('editAction');
}
protected function newCommentActionGroups() {
return array();
}
protected function newAutomaticCommentTransactions($object) {
return array();
}
protected function newCommentPreviewContent($object, array $xactions) {
return null;
}
/* -( Form Pages )--------------------------------------------------------- */
public function getSelectedPage() {
return $this->page;
}
private function selectPage($object, $page_key) {
$pages = $this->getPages($object);
if (empty($pages[$page_key])) {
return null;
}
$this->page = $pages[$page_key];
return $this->page;
}
protected function newPages($object) {
return array();
}
protected function getPages($object) {
if ($this->pages === null) {
$pages = $this->newPages($object);
assert_instances_of($pages, 'PhabricatorEditPage');
$pages = mpull($pages, null, 'getKey');
$this->pages = $pages;
}
return $this->pages;
}
private function applyPageToFields($object, array $fields) {
$pages = $this->getPages($object);
if (!$pages) {
return $fields;
}
if (!$this->getSelectedPage()) {
return $fields;
}
$page_picks = array();
$default_key = head($pages)->getKey();
foreach ($pages as $page_key => $page) {
foreach ($page->getFieldKeys() as $field_key) {
$page_picks[$field_key] = $page_key;
}
if ($page->getIsDefault()) {
$default_key = $page_key;
}
}
$page_map = array_fill_keys(array_keys($pages), array());
foreach ($fields as $field_key => $field) {
if (isset($page_picks[$field_key])) {
$page_map[$page_picks[$field_key]][$field_key] = $field;
continue;
}
// TODO: Maybe let the field pick a page to associate itself with so
// extensions can force themselves onto a particular page?
$page_map[$default_key][$field_key] = $field;
}
$page = $this->getSelectedPage();
if (!$page) {
$page = head($pages);
}
$selected_key = $page->getKey();
return $page_map[$selected_key];
}
protected function willApplyTransactions($object, array $xactions) {
return $xactions;
}
protected function didApplyTransactions($object, array $xactions) {
return;
}
/* -( Bulk Edits )--------------------------------------------------------- */
final public function newBulkEditGroupMap() {
$groups = $this->newBulkEditGroups();
$map = array();
foreach ($groups as $group) {
$key = $group->getKey();
if (isset($map[$key])) {
throw new Exception(
pht(
'Two bulk edit groups have the same key ("%s"). Each bulk edit '.
'group must have a unique key.',
$key));
}
$map[$key] = $group;
}
if ($this->isEngineExtensible()) {
$extensions = PhabricatorEditEngineExtension::getAllEnabledExtensions();
} else {
$extensions = array();
}
foreach ($extensions as $extension) {
$extension_groups = $extension->newBulkEditGroups($this);
foreach ($extension_groups as $group) {
$key = $group->getKey();
if (isset($map[$key])) {
throw new Exception(
pht(
'Extension "%s" defines a bulk edit group with the same key '.
'("%s") as the main editor or another extension. Each bulk '.
'edit group must have a unique key.'));
}
$map[$key] = $group;
}
}
return $map;
}
protected function newBulkEditGroups() {
return array(
id(new PhabricatorBulkEditGroup())
->setKey('default')
->setLabel(pht('Primary Fields')),
id(new PhabricatorBulkEditGroup())
->setKey('extension')
->setLabel(pht('Support Applications')),
);
}
final public function newBulkEditMap() {
$config = $this->loadDefaultConfiguration();
if (!$config) {
throw new Exception(
pht('No default edit engine configuration for bulk edit.'));
}
$object = $this->newEditableObject();
$fields = $this->buildEditFields($object);
$groups = $this->newBulkEditGroupMap();
$edit_types = $this->getBulkEditTypesFromFields($fields);
$map = array();
foreach ($edit_types as $key => $type) {
$bulk_type = $type->getBulkParameterType();
if ($bulk_type === null) {
continue;
}
$bulk_label = $type->getBulkEditLabel();
if ($bulk_label === null) {
continue;
}
$group_key = $type->getBulkEditGroupKey();
if (!$group_key) {
$group_key = 'default';
}
if (!isset($groups[$group_key])) {
throw new Exception(
pht(
'Field "%s" has a bulk edit group key ("%s") with no '.
'corresponding bulk edit group.',
$key,
$group_key));
}
$map[] = array(
'label' => $bulk_label,
'xaction' => $key,
'group' => $group_key,
'control' => array(
'type' => $bulk_type->getPHUIXControlType(),
'spec' => (object)$bulk_type->getPHUIXControlSpecification(),
),
);
}
return $map;
}
final public function newRawBulkTransactions(array $xactions) {
$config = $this->loadDefaultConfiguration();
if (!$config) {
throw new Exception(
pht('No default edit engine configuration for bulk edit.'));
}
$object = $this->newEditableObject();
$fields = $this->buildEditFields($object);
$edit_types = $this->getBulkEditTypesFromFields($fields);
$template = $object->getApplicationTransactionTemplate();
$raw_xactions = array();
foreach ($xactions as $key => $xaction) {
PhutilTypeSpec::checkMap(
$xaction,
array(
'type' => 'string',
'value' => 'optional wild',
));
$type = $xaction['type'];
if (!isset($edit_types[$type])) {
throw new Exception(
pht(
'Unsupported bulk edit type "%s".',
$type));
}
$edit_type = $edit_types[$type];
// Replace the edit type with the underlying transaction type. Usually
// these are 1:1 and the transaction type just has more internal noise,
// but it's possible that this isn't the case.
$xaction['type'] = $edit_type->getTransactionType();
$value = $xaction['value'];
$value = $edit_type->getTransactionValueFromBulkEdit($value);
$xaction['value'] = $value;
$xaction_objects = $edit_type->generateTransactions(
clone $template,
$xaction);
foreach ($xaction_objects as $xaction_object) {
$raw_xaction = array(
'type' => $xaction_object->getTransactionType(),
'metadata' => $xaction_object->getMetadata(),
'new' => $xaction_object->getNewValue(),
);
if ($xaction_object->hasOldValue()) {
$raw_xaction['old'] = $xaction_object->getOldValue();
}
if ($xaction_object->hasComment()) {
$comment = $xaction_object->getComment();
$raw_xaction['comment'] = $comment->getContent();
}
$raw_xactions[] = $raw_xaction;
}
}
return $raw_xactions;
}
private function getBulkEditTypesFromFields(array $fields) {
$types = array();
foreach ($fields as $field) {
$field_types = $field->getBulkEditTypes();
if ($field_types === null) {
continue;
}
foreach ($field_types as $field_type) {
$types[$field_type->getEditType()] = $field_type;
}
}
return $types;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getPHID() {
return get_class($this);
}
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::getMostOpenPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getCreateNewObjectPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
}
diff --git a/src/applications/transactions/editengine/PhabricatorEditEngineMFAEngine.php b/src/applications/transactions/editengine/PhabricatorEditEngineMFAEngine.php
new file mode 100644
index 000000000..271ec6f44
--- /dev/null
+++ b/src/applications/transactions/editengine/PhabricatorEditEngineMFAEngine.php
@@ -0,0 +1,61 @@
+<?php
+
+abstract class PhabricatorEditEngineMFAEngine
+ extends Phobject {
+
+ private $object;
+ private $viewer;
+
+ public function setObject(PhabricatorEditEngineMFAInterface $object) {
+ $this->object = $object;
+ return $this;
+ }
+
+ public function getObject() {
+ return $this->object;
+ }
+
+ public function setViewer(PhabricatorUser $viewer) {
+ $this->viewer = $viewer;
+ return $this;
+ }
+
+ public function getViewer() {
+ if (!$this->viewer) {
+ throw new PhutilInvalidStateException('setViewer');
+ }
+
+ return $this->viewer;
+ }
+
+ final public static function newEngineForObject(
+ PhabricatorEditEngineMFAInterface $object) {
+ return $object->newEditEngineMFAEngine()
+ ->setObject($object);
+ }
+
+ /**
+ * Do edits to this object REQUIRE that the user submit MFA?
+ *
+ * This is a strict requirement: users will need to add MFA to their accounts
+ * if they don't already have it.
+ *
+ * @return bool True to strictly require MFA.
+ */
+ public function shouldRequireMFA() {
+ return false;
+ }
+
+ /**
+ * Should edits to this object prompt for MFA if it's available?
+ *
+ * This is advisory: users without MFA on their accounts will be able to
+ * perform edits without being required to add MFA.
+ *
+ * @return bool True to prompt for MFA if available.
+ */
+ public function shouldTryMFA() {
+ return false;
+ }
+
+}
diff --git a/src/applications/transactions/editengine/PhabricatorEditEngineMFAInterface.php b/src/applications/transactions/editengine/PhabricatorEditEngineMFAInterface.php
new file mode 100644
index 000000000..48acb43c5
--- /dev/null
+++ b/src/applications/transactions/editengine/PhabricatorEditEngineMFAInterface.php
@@ -0,0 +1,7 @@
+<?php
+
+interface PhabricatorEditEngineMFAInterface {
+
+ public function newEditEngineMFAEngine();
+
+}
diff --git a/src/applications/transactions/editfield/PhabricatorCredentialEditField.php b/src/applications/transactions/editfield/PhabricatorCredentialEditField.php
new file mode 100644
index 000000000..7c70bf288
--- /dev/null
+++ b/src/applications/transactions/editfield/PhabricatorCredentialEditField.php
@@ -0,0 +1,43 @@
+<?php
+
+final class PhabricatorCredentialEditField
+ extends PhabricatorEditField {
+
+ private $credentialType;
+ private $credentials;
+
+ public function setCredentialType($credential_type) {
+ $this->credentialType = $credential_type;
+ return $this;
+ }
+
+ public function getCredentialType() {
+ return $this->credentialType;
+ }
+
+ public function setCredentials(array $credentials) {
+ $this->credentials = $credentials;
+ return $this;
+ }
+
+ public function getCredentials() {
+ return $this->credentials;
+ }
+
+ protected function newControl() {
+ $control = id(new PassphraseCredentialControl())
+ ->setCredentialType($this->getCredentialType())
+ ->setOptions($this->getCredentials());
+
+ return $control;
+ }
+
+ protected function newHTTPParameterType() {
+ return new AphrontPHIDHTTPParameterType();
+ }
+
+ protected function newConduitParameterType() {
+ return new ConduitPHIDParameterType();
+ }
+
+}
diff --git a/src/applications/transactions/editfield/PhabricatorSpaceEditField.php b/src/applications/transactions/editfield/PhabricatorSpaceEditField.php
index ee15f0b19..c15213bd9 100644
--- a/src/applications/transactions/editfield/PhabricatorSpaceEditField.php
+++ b/src/applications/transactions/editfield/PhabricatorSpaceEditField.php
@@ -1,40 +1,39 @@
<?php
final class PhabricatorSpaceEditField
extends PhabricatorEditField {
private $policyField;
public function setPolicyField(PhabricatorPolicyEditField $policy_field) {
$this->policyField = $policy_field;
return $this;
}
public function getPolicyField() {
return $this->policyField;
}
protected function newControl() {
// NOTE: This field doesn't do anything on its own, it just serves as a
// companion to the associated View Policy field.
return null;
}
protected function newHTTPParameterType() {
return new AphrontPHIDHTTPParameterType();
}
protected function newConduitParameterType() {
return new ConduitPHIDParameterType();
}
-
public function shouldReadValueFromRequest() {
return $this->getPolicyField()->shouldReadValueFromRequest();
}
public function shouldReadValueFromSubmit() {
return $this->getPolicyField()->shouldReadValueFromSubmit();
}
}
diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
index 738fcf098..91825eb73 100644
--- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
+++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
@@ -1,4711 +1,5099 @@
<?php
/**
*
* Publishing and Managing State
* ======
*
* After applying changes, the Editor queues a worker to publish mail, feed,
* and notifications, and to perform other background work like updating search
* indexes. This allows it to do this work without impacting performance for
* users.
*
* When work is moved to the daemons, the Editor state is serialized by
* @{method:getWorkerState}, then reloaded in a daemon process by
* @{method:loadWorkerState}. **This is fragile.**
*
* State is not persisted into the daemons by default, because we can not send
* arbitrary objects into the queue. This means the default behavior of any
* state properties is to reset to their defaults without warning prior to
* publishing.
*
* The easiest way to avoid this is to keep Editors stateless: the overwhelming
* majority of Editors can be written statelessly. If you need to maintain
* state, you can either:
*
* - not require state to exist during publishing; or
* - pass state to the daemons by implementing @{method:getCustomWorkerState}
* and @{method:loadCustomWorkerState}.
*
* This architecture isn't ideal, and we may eventually split this class into
* "Editor" and "Publisher" parts to make it more robust. See T6367 for some
* discussion and context.
*
* @task mail Sending Mail
* @task feed Publishing Feed Stories
* @task search Search Index
* @task files Integration with Files
* @task workers Managing Workers
*/
abstract class PhabricatorApplicationTransactionEditor
extends PhabricatorEditor {
private $contentSource;
private $object;
private $xactions;
private $isNewObject;
private $mentionedPHIDs;
private $continueOnNoEffect;
private $continueOnMissingFields;
private $raiseWarnings;
private $parentMessageID;
private $heraldAdapter;
private $heraldTranscript;
private $subscribers;
private $unmentionablePHIDMap = array();
private $applicationEmail;
private $isPreview;
private $isHeraldEditor;
private $isInverseEdgeEditor;
private $actingAsPHID;
private $heraldEmailPHIDs = array();
private $heraldForcedEmailPHIDs = array();
private $heraldHeader;
private $mailToPHIDs = array();
private $mailCCPHIDs = array();
private $feedNotifyPHIDs = array();
private $feedRelatedPHIDs = array();
private $feedShouldPublish = false;
private $mailShouldSend = false;
private $modularTypes;
private $silent;
private $mustEncrypt;
private $stampTemplates = array();
private $mailStamps = array();
private $oldTo = array();
private $oldCC = array();
private $mailRemovedPHIDs = array();
private $mailUnexpandablePHIDs = array();
private $mailMutedPHIDs = array();
private $webhookMap = array();
private $transactionQueue = array();
private $sendHistory = false;
+ private $shouldRequireMFA = false;
+ private $hasRequiredMFA = false;
+ private $request;
+ private $cancelURI;
+ private $extensions;
const STORAGE_ENCODING_BINARY = 'binary';
/**
* Get the class name for the application this editor is a part of.
*
* Uninstalling the application will disable the editor.
*
* @return string Editor's application class name.
*/
abstract public function getEditorApplicationClass();
/**
* Get a description of the objects this editor edits, like "Differential
* Revisions".
*
* @return string Human readable description of edited objects.
*/
abstract public function getEditorObjectsDescription();
public function setActingAsPHID($acting_as_phid) {
$this->actingAsPHID = $acting_as_phid;
return $this;
}
public function getActingAsPHID() {
if ($this->actingAsPHID) {
return $this->actingAsPHID;
}
return $this->getActor()->getPHID();
}
/**
* When the editor tries to apply transactions that have no effect, should
* it raise an exception (default) or drop them and continue?
*
* Generally, you will set this flag for edits coming from "Edit" interfaces,
* and leave it cleared for edits coming from "Comment" interfaces, so the
* user will get a useful error if they try to submit a comment that does
* nothing (e.g., empty comment with a status change that has already been
* performed by another user).
*
* @param bool True to drop transactions without effect and continue.
* @return this
*/
public function setContinueOnNoEffect($continue) {
$this->continueOnNoEffect = $continue;
return $this;
}
public function getContinueOnNoEffect() {
return $this->continueOnNoEffect;
}
/**
* When the editor tries to apply transactions which don't populate all of
* an object's required fields, should it raise an exception (default) or
* drop them and continue?
*
* For example, if a user adds a new required custom field (like "Severity")
* to a task, all existing tasks won't have it populated. When users
* manually edit existing tasks, it's usually desirable to have them provide
* a severity. However, other operations (like batch editing just the
* owner of a task) will fail by default.
*
* By setting this flag for edit operations which apply to specific fields
* (like the priority, batch, and merge editors in Maniphest), these
* operations can continue to function even if an object is outdated.
*
* @param bool True to continue when transactions don't completely satisfy
* all required fields.
* @return this
*/
public function setContinueOnMissingFields($continue_on_missing_fields) {
$this->continueOnMissingFields = $continue_on_missing_fields;
return $this;
}
public function getContinueOnMissingFields() {
return $this->continueOnMissingFields;
}
/**
* Not strictly necessary, but reply handlers ideally set this value to
* make email threading work better.
*/
public function setParentMessageID($parent_message_id) {
$this->parentMessageID = $parent_message_id;
return $this;
}
public function getParentMessageID() {
return $this->parentMessageID;
}
public function getIsNewObject() {
return $this->isNewObject;
}
public function getMentionedPHIDs() {
return $this->mentionedPHIDs;
}
public function setIsPreview($is_preview) {
$this->isPreview = $is_preview;
return $this;
}
public function getIsPreview() {
return $this->isPreview;
}
public function setIsSilent($silent) {
$this->silent = $silent;
return $this;
}
public function getIsSilent() {
return $this->silent;
}
public function getMustEncrypt() {
return $this->mustEncrypt;
}
public function getHeraldRuleMonograms() {
// Convert the stored "<123>, <456>" string into a list: "H123", "H456".
$list = $this->heraldHeader;
$list = preg_split('/[, ]+/', $list);
foreach ($list as $key => $item) {
$item = trim($item, '<>');
if (!is_numeric($item)) {
unset($list[$key]);
continue;
}
$list[$key] = 'H'.$item;
}
return $list;
}
public function setIsInverseEdgeEditor($is_inverse_edge_editor) {
$this->isInverseEdgeEditor = $is_inverse_edge_editor;
return $this;
}
public function getIsInverseEdgeEditor() {
return $this->isInverseEdgeEditor;
}
public function setIsHeraldEditor($is_herald_editor) {
$this->isHeraldEditor = $is_herald_editor;
return $this;
}
public function getIsHeraldEditor() {
return $this->isHeraldEditor;
}
public function setUnmentionablePHIDMap(array $map) {
$this->unmentionablePHIDMap = $map;
return $this;
}
public function getUnmentionablePHIDMap() {
return $this->unmentionablePHIDMap;
}
protected function shouldEnableMentions(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
public function setApplicationEmail(
PhabricatorMetaMTAApplicationEmail $email) {
$this->applicationEmail = $email;
return $this;
}
public function getApplicationEmail() {
return $this->applicationEmail;
}
public function setRaiseWarnings($raise_warnings) {
$this->raiseWarnings = $raise_warnings;
return $this;
}
public function getRaiseWarnings() {
return $this->raiseWarnings;
}
+ public function setShouldRequireMFA($should_require_mfa) {
+ if ($this->hasRequiredMFA) {
+ throw new Exception(
+ pht(
+ 'Call to setShouldRequireMFA() is too late: this Editor has already '.
+ 'checked for MFA requirements.'));
+ }
+
+ $this->shouldRequireMFA = $should_require_mfa;
+ return $this;
+ }
+
+ public function getShouldRequireMFA() {
+ return $this->shouldRequireMFA;
+ }
+
public function getTransactionTypesForObject($object) {
$old = $this->object;
try {
$this->object = $object;
$result = $this->getTransactionTypes();
$this->object = $old;
} catch (Exception $ex) {
$this->object = $old;
throw $ex;
}
return $result;
}
public function getTransactionTypes() {
$types = array();
$types[] = PhabricatorTransactions::TYPE_CREATE;
$types[] = PhabricatorTransactions::TYPE_HISTORY;
if ($this->object instanceof PhabricatorEditEngineSubtypeInterface) {
$types[] = PhabricatorTransactions::TYPE_SUBTYPE;
}
if ($this->object instanceof PhabricatorSubscribableInterface) {
$types[] = PhabricatorTransactions::TYPE_SUBSCRIBERS;
}
if ($this->object instanceof PhabricatorCustomFieldInterface) {
$types[] = PhabricatorTransactions::TYPE_CUSTOMFIELD;
}
if ($this->object instanceof PhabricatorTokenReceiverInterface) {
$types[] = PhabricatorTransactions::TYPE_TOKEN;
}
if ($this->object instanceof PhabricatorProjectInterface ||
$this->object instanceof PhabricatorMentionableInterface) {
$types[] = PhabricatorTransactions::TYPE_EDGE;
}
if ($this->object instanceof PhabricatorSpacesInterface) {
$types[] = PhabricatorTransactions::TYPE_SPACE;
}
+ $types[] = PhabricatorTransactions::TYPE_MFA;
+
$template = $this->object->getApplicationTransactionTemplate();
if ($template instanceof PhabricatorModularTransaction) {
$xtypes = $template->newModularTransactionTypes();
foreach ($xtypes as $xtype) {
$types[] = $xtype->getTransactionTypeConstant();
}
}
if ($template) {
try {
$comment = $template->getApplicationTransactionCommentObject();
} catch (PhutilMethodNotImplementedException $ex) {
$comment = null;
}
if ($comment) {
$types[] = PhabricatorTransactions::TYPE_COMMENT;
}
}
return $types;
}
private function adjustTransactionValues(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
if ($xaction->shouldGenerateOldValue()) {
$old = $this->getTransactionOldValue($object, $xaction);
$xaction->setOldValue($old);
}
$new = $this->getTransactionNewValue($object, $xaction);
$xaction->setNewValue($new);
}
private function getTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$xtype = clone $xtype;
$xtype->setStorage($xaction);
return $xtype->generateOldValue($object);
}
switch ($type) {
case PhabricatorTransactions::TYPE_CREATE:
case PhabricatorTransactions::TYPE_HISTORY:
return null;
case PhabricatorTransactions::TYPE_SUBTYPE:
return $object->getEditEngineSubtype();
+ case PhabricatorTransactions::TYPE_MFA:
+ return null;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return array_values($this->subscribers);
case PhabricatorTransactions::TYPE_VIEW_POLICY:
if ($this->getIsNewObject()) {
return null;
}
return $object->getViewPolicy();
case PhabricatorTransactions::TYPE_EDIT_POLICY:
if ($this->getIsNewObject()) {
return null;
}
return $object->getEditPolicy();
case PhabricatorTransactions::TYPE_JOIN_POLICY:
if ($this->getIsNewObject()) {
return null;
}
return $object->getJoinPolicy();
case PhabricatorTransactions::TYPE_SPACE:
if ($this->getIsNewObject()) {
return null;
}
$space_phid = $object->getSpacePHID();
if ($space_phid === null) {
$default_space = PhabricatorSpacesNamespaceQuery::getDefaultSpace();
if ($default_space) {
$space_phid = $default_space->getPHID();
}
}
return $space_phid;
case PhabricatorTransactions::TYPE_EDGE:
$edge_type = $xaction->getMetadataValue('edge:type');
if (!$edge_type) {
throw new Exception(
pht(
"Edge transaction has no '%s'!",
'edge:type'));
}
+ // See T13082. If this is an inverse edit, the parent editor has
+ // already populated the transaction values correctly.
+ if ($this->getIsInverseEdgeEditor()) {
+ return $xaction->getOldValue();
+ }
+
$old_edges = array();
if ($object->getPHID()) {
$edge_src = $object->getPHID();
$old_edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($edge_src))
->withEdgeTypes(array($edge_type))
->needEdgeData(true)
->execute();
$old_edges = $old_edges[$edge_src][$edge_type];
}
return $old_edges;
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
// NOTE: Custom fields have their old value pre-populated when they are
// built by PhabricatorCustomFieldList.
return $xaction->getOldValue();
case PhabricatorTransactions::TYPE_COMMENT:
return null;
default:
return $this->getCustomTransactionOldValue($object, $xaction);
}
}
private function getTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$xtype = clone $xtype;
$xtype->setStorage($xaction);
return $xtype->generateNewValue($object, $xaction->getNewValue());
}
switch ($type) {
case PhabricatorTransactions::TYPE_CREATE:
return null;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return $this->getPHIDTransactionNewValue($xaction);
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
case PhabricatorTransactions::TYPE_TOKEN:
case PhabricatorTransactions::TYPE_INLINESTATE:
case PhabricatorTransactions::TYPE_SUBTYPE:
case PhabricatorTransactions::TYPE_HISTORY:
return $xaction->getNewValue();
+ case PhabricatorTransactions::TYPE_MFA:
+ return true;
case PhabricatorTransactions::TYPE_SPACE:
$space_phid = $xaction->getNewValue();
if (!strlen($space_phid)) {
// If an install has no Spaces or the Spaces controls are not visible
// to the viewer, we might end up with the empty string here instead
// of a strict `null`, because some controller just used `getStr()`
// to read the space PHID from the request.
// Just make this work like callers might reasonably expect so we
// don't need to handle this specially in every EditController.
return $this->getActor()->getDefaultSpacePHID();
} else {
return $space_phid;
}
case PhabricatorTransactions::TYPE_EDGE:
+ // See T13082. If this is an inverse edit, the parent editor has
+ // already populated appropriate transaction values.
+ if ($this->getIsInverseEdgeEditor()) {
+ return $xaction->getNewValue();
+ }
+
$new_value = $this->getEdgeTransactionNewValue($xaction);
$edge_type = $xaction->getMetadataValue('edge:type');
$type_project = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
if ($edge_type == $type_project) {
$new_value = $this->applyProjectConflictRules($new_value);
}
return $new_value;
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getCustomFieldForTransaction($object, $xaction);
return $field->getNewValueFromApplicationTransactions($xaction);
case PhabricatorTransactions::TYPE_COMMENT:
return null;
default:
return $this->getCustomTransactionNewValue($object, $xaction);
}
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
throw new Exception(pht('Capability not supported!'));
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
throw new Exception(pht('Capability not supported!'));
}
protected function transactionHasEffect(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_CREATE:
case PhabricatorTransactions::TYPE_HISTORY:
return true;
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getCustomFieldForTransaction($object, $xaction);
return $field->getApplicationTransactionHasEffect($xaction);
case PhabricatorTransactions::TYPE_EDGE:
// A straight value comparison here doesn't always get the right
// result, because newly added edges aren't fully populated. Instead,
// compare the changes in a more granular way.
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$old_dst = array_keys($old);
$new_dst = array_keys($new);
// NOTE: For now, we don't consider edge reordering to be a change.
// We have very few order-dependent edges and effectively no order
// oriented UI. This might change in the future.
sort($old_dst);
sort($new_dst);
if ($old_dst !== $new_dst) {
// We've added or removed edges, so this transaction definitely
// has an effect.
return true;
}
// We haven't added or removed edges, but we might have changed
// edge data.
foreach ($old as $key => $old_value) {
$new_value = $new[$key];
if ($old_value['data'] !== $new_value['data']) {
return true;
}
}
return false;
}
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
return $xtype->getTransactionHasEffect(
$object,
$xaction->getOldValue(),
$xaction->getNewValue());
}
if ($xaction->hasComment()) {
return true;
}
return ($xaction->getOldValue() !== $xaction->getNewValue());
}
protected function shouldApplyInitialEffects(
PhabricatorLiskDAO $object,
array $xactions) {
return false;
}
protected function applyInitialEffects(
PhabricatorLiskDAO $object,
array $xactions) {
throw new PhutilMethodNotImplementedException();
}
private function applyInternalEffects(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$xtype = clone $xtype;
$xtype->setStorage($xaction);
return $xtype->applyInternalEffects($object, $xaction->getNewValue());
}
switch ($type) {
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getCustomFieldForTransaction($object, $xaction);
return $field->applyApplicationTransactionInternalEffects($xaction);
case PhabricatorTransactions::TYPE_CREATE:
case PhabricatorTransactions::TYPE_HISTORY:
case PhabricatorTransactions::TYPE_SUBTYPE:
+ case PhabricatorTransactions::TYPE_MFA:
case PhabricatorTransactions::TYPE_TOKEN:
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
case PhabricatorTransactions::TYPE_INLINESTATE:
case PhabricatorTransactions::TYPE_EDGE:
case PhabricatorTransactions::TYPE_SPACE:
case PhabricatorTransactions::TYPE_COMMENT:
return $this->applyBuiltinInternalTransaction($object, $xaction);
}
return $this->applyCustomInternalTransaction($object, $xaction);
}
private function applyExternalEffects(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$xtype = clone $xtype;
$xtype->setStorage($xaction);
return $xtype->applyExternalEffects($object, $xaction->getNewValue());
}
switch ($type) {
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$subeditor = id(new PhabricatorSubscriptionsEditor())
->setObject($object)
->setActor($this->requireActor());
$old_map = array_fuse($xaction->getOldValue());
$new_map = array_fuse($xaction->getNewValue());
$subeditor->unsubscribe(
array_keys(
array_diff_key($old_map, $new_map)));
$subeditor->subscribeExplicit(
array_keys(
array_diff_key($new_map, $old_map)));
$subeditor->save();
// for the rest of these edits, subscribers should include those just
// added as well as those just removed.
$subscribers = array_unique(array_merge(
$this->subscribers,
$xaction->getOldValue(),
$xaction->getNewValue()));
$this->subscribers = $subscribers;
return $this->applyBuiltinExternalTransaction($object, $xaction);
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getCustomFieldForTransaction($object, $xaction);
return $field->applyApplicationTransactionExternalEffects($xaction);
case PhabricatorTransactions::TYPE_CREATE:
case PhabricatorTransactions::TYPE_HISTORY:
case PhabricatorTransactions::TYPE_SUBTYPE:
+ case PhabricatorTransactions::TYPE_MFA:
case PhabricatorTransactions::TYPE_EDGE:
case PhabricatorTransactions::TYPE_TOKEN:
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
case PhabricatorTransactions::TYPE_INLINESTATE:
case PhabricatorTransactions::TYPE_SPACE:
case PhabricatorTransactions::TYPE_COMMENT:
return $this->applyBuiltinExternalTransaction($object, $xaction);
}
return $this->applyCustomExternalTransaction($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
throw new Exception(
pht(
"Transaction type '%s' is missing an internal apply implementation!",
$type));
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
throw new Exception(
pht(
"Transaction type '%s' is missing an external apply implementation!",
$type));
}
/**
* @{class:PhabricatorTransactions} provides many built-in transactions
* which should not require much - if any - code in specific applications.
*
* This method is a hook for the exceedingly-rare cases where you may need
* to do **additional** work for built-in transactions. Developers should
* extend this method, making sure to return the parent implementation
* regardless of handling any transactions.
*
* See also @{method:applyBuiltinExternalTransaction}.
*/
protected function applyBuiltinInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_VIEW_POLICY:
$object->setViewPolicy($xaction->getNewValue());
break;
case PhabricatorTransactions::TYPE_EDIT_POLICY:
$object->setEditPolicy($xaction->getNewValue());
break;
case PhabricatorTransactions::TYPE_JOIN_POLICY:
$object->setJoinPolicy($xaction->getNewValue());
break;
case PhabricatorTransactions::TYPE_SPACE:
$object->setSpacePHID($xaction->getNewValue());
break;
case PhabricatorTransactions::TYPE_SUBTYPE:
$object->setEditEngineSubtype($xaction->getNewValue());
break;
}
}
/**
* See @{method::applyBuiltinInternalTransaction}.
*/
protected function applyBuiltinExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_EDGE:
if ($this->getIsInverseEdgeEditor()) {
// If we're writing an inverse edge transaction, don't actually
// do anything. The initiating editor on the other side of the
// transaction will take care of the edge writes.
break;
}
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$src = $object->getPHID();
$const = $xaction->getMetadataValue('edge:type');
- $type = PhabricatorEdgeType::getByConstant($const);
- if ($type->shouldWriteInverseTransactions()) {
- $this->applyInverseEdgeTransactions(
- $object,
- $xaction,
- $type->getInverseEdgeConstant());
- }
-
foreach ($new as $dst_phid => $edge) {
$new[$dst_phid]['src'] = $src;
}
$editor = new PhabricatorEdgeEditor();
foreach ($old as $dst_phid => $edge) {
if (!empty($new[$dst_phid])) {
if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) {
continue;
}
}
$editor->removeEdge($src, $const, $dst_phid);
}
foreach ($new as $dst_phid => $edge) {
if (!empty($old[$dst_phid])) {
if ($old[$dst_phid]['data'] === $new[$dst_phid]['data']) {
continue;
}
}
$data = array(
'data' => $edge['data'],
);
$editor->addEdge($src, $const, $dst_phid, $data);
}
$editor->save();
$this->updateWorkboardColumns($object, $const, $old, $new);
break;
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_SPACE:
$this->scrambleFileSecrets($object);
break;
case PhabricatorTransactions::TYPE_HISTORY:
$this->sendHistory = true;
break;
}
}
/**
* Fill in a transaction's common values, like author and content source.
*/
protected function populateTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$actor = $this->getActor();
// TODO: This needs to be more sophisticated once we have meta-policies.
$xaction->setViewPolicy(PhabricatorPolicies::POLICY_PUBLIC);
if ($actor->isOmnipotent()) {
$xaction->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
} else {
$xaction->setEditPolicy($this->getActingAsPHID());
}
// If the transaction already has an explicit author PHID, allow it to
// stand. This is used by applications like Owners that hook into the
// post-apply change pipeline.
if (!$xaction->getAuthorPHID()) {
$xaction->setAuthorPHID($this->getActingAsPHID());
}
$xaction->setContentSource($this->getContentSource());
$xaction->attachViewer($actor);
$xaction->attachObject($object);
if ($object->getPHID()) {
$xaction->setObjectPHID($object->getPHID());
}
if ($this->getIsSilent()) {
$xaction->setIsSilentTransaction(true);
}
- if ($actor->hasHighSecuritySession()) {
- $xaction->setIsMFATransaction(true);
- }
-
return $xaction;
}
protected function didApplyInternalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
return $xactions;
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
return $xactions;
}
final protected function didCommitTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
foreach ($xactions as $xaction) {
$type = $xaction->getTransactionType();
+ // See T13082. When we're writing edges that imply corresponding inverse
+ // transactions, apply those inverse transactions now. We have to wait
+ // until the object we're editing (with this editor) has committed its
+ // transactions to do this. If we don't, the inverse editor may race,
+ // build a mail before we actually commit this object, and render "alice
+ // added an edge: Unknown Object".
+
+ if ($type === PhabricatorTransactions::TYPE_EDGE) {
+ // Don't do anything if we're already an inverse edge editor.
+ if ($this->getIsInverseEdgeEditor()) {
+ continue;
+ }
+
+ $edge_const = $xaction->getMetadataValue('edge:type');
+ $edge_type = PhabricatorEdgeType::getByConstant($edge_const);
+ if ($edge_type->shouldWriteInverseTransactions()) {
+ $this->applyInverseEdgeTransactions(
+ $object,
+ $xaction,
+ $edge_type->getInverseEdgeConstant());
+ }
+ continue;
+ }
+
$xtype = $this->getModularTransactionType($type);
if (!$xtype) {
continue;
}
$xtype = clone $xtype;
$xtype->setStorage($xaction);
$xtype->didCommitTransaction($object, $xaction->getNewValue());
}
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source;
return $this;
}
public function setContentSourceFromRequest(AphrontRequest $request) {
+ $this->setRequest($request);
return $this->setContentSource(
PhabricatorContentSource::newFromRequest($request));
}
public function getContentSource() {
return $this->contentSource;
}
+ public function setRequest(AphrontRequest $request) {
+ $this->request = $request;
+ return $this;
+ }
+
+ public function getRequest() {
+ return $this->request;
+ }
+
+ public function setCancelURI($cancel_uri) {
+ $this->cancelURI = $cancel_uri;
+ return $this;
+ }
+
+ public function getCancelURI() {
+ return $this->cancelURI;
+ }
+
final public function applyTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$this->object = $object;
$this->xactions = $xactions;
$this->isNewObject = ($object->getPHID() === null);
$this->validateEditParameters($object, $xactions);
+ $xactions = $this->newMFATransactions($object, $xactions);
$actor = $this->requireActor();
// NOTE: Some transaction expansion requires that the edited object be
// attached.
foreach ($xactions as $xaction) {
$xaction->attachObject($object);
$xaction->attachViewer($actor);
}
$xactions = $this->expandTransactions($object, $xactions);
$xactions = $this->expandSupportTransactions($object, $xactions);
$xactions = $this->combineTransactions($xactions);
foreach ($xactions as $xaction) {
$xaction = $this->populateTransaction($object, $xaction);
}
$is_preview = $this->getIsPreview();
$read_locking = false;
$transaction_open = false;
if (!$is_preview) {
$errors = array();
$type_map = mgroup($xactions, 'getTransactionType');
foreach ($this->getTransactionTypes() as $type) {
$type_xactions = idx($type_map, $type, array());
$errors[] = $this->validateTransaction($object, $type, $type_xactions);
}
$errors[] = $this->validateAllTransactions($object, $xactions);
+ $errors[] = $this->validateTransactionsWithExtensions($object, $xactions);
$errors = array_mergev($errors);
$continue_on_missing = $this->getContinueOnMissingFields();
foreach ($errors as $key => $error) {
if ($continue_on_missing && $error->getIsMissingFieldError()) {
unset($errors[$key]);
}
}
if ($errors) {
throw new PhabricatorApplicationTransactionValidationException($errors);
}
if ($this->raiseWarnings) {
$warnings = array();
foreach ($xactions as $xaction) {
if ($this->hasWarnings($object, $xaction)) {
$warnings[] = $xaction;
}
}
if ($warnings) {
throw new PhabricatorApplicationTransactionWarningException(
$warnings);
}
}
+ }
- $this->willApplyTransactions($object, $xactions);
+ foreach ($xactions as $xaction) {
+ $this->adjustTransactionValues($object, $xaction);
+ }
+
+ // Now that we've merged and combined transactions, check for required
+ // capabilities. Note that we're doing this before filtering
+ // transactions: if you try to apply an edit which you do not have
+ // permission to apply, we want to give you a permissions error even
+ // if the edit would have no effect.
+ $this->applyCapabilityChecks($object, $xactions);
+
+ $xactions = $this->filterTransactions($object, $xactions);
+
+ if (!$is_preview) {
+ $this->hasRequiredMFA = true;
+ if ($this->getShouldRequireMFA()) {
+ $this->requireMFA($object, $xactions);
+ }
if ($object->getID()) {
$this->buildOldRecipientLists($object, $xactions);
$object->openTransaction();
$transaction_open = true;
$object->beginReadLocking();
$read_locking = true;
$object->reload();
}
if ($this->shouldApplyInitialEffects($object, $xactions)) {
if (!$transaction_open) {
$object->openTransaction();
$transaction_open = true;
}
}
}
try {
if ($this->shouldApplyInitialEffects($object, $xactions)) {
$this->applyInitialEffects($object, $xactions);
}
- foreach ($xactions as $xaction) {
- $this->adjustTransactionValues($object, $xaction);
- }
-
- // Now that we've merged and combined transactions, check for required
- // capabilities. Note that we're doing this before filtering
- // transactions: if you try to apply an edit which you do not have
- // permission to apply, we want to give you a permissions error even
- // if the edit would have no effect.
- $this->applyCapabilityChecks($object, $xactions);
-
- // See T13186. Fatal hard if this object has an older
- // "requireCapabilities()" method. The code may rely on this method being
- // called to apply policy checks, so err on the side of safety and fatal.
- // TODO: Remove this check after some time has passed.
- if (method_exists($this, 'requireCapabilities')) {
- throw new Exception(
- pht(
- 'Editor (of class "%s") implements obsolete policy method '.
- 'requireCapabilities(). The implementation for this Editor '.
- 'MUST be updated. See <%s> for discussion.',
- get_class($this),
- 'https://secure.phabricator.com/T13186'));
- }
-
- $xactions = $this->filterTransactions($object, $xactions);
-
// TODO: Once everything is on EditEngine, just use getIsNewObject() to
// figure this out instead.
$mark_as_create = false;
$create_type = PhabricatorTransactions::TYPE_CREATE;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == $create_type) {
$mark_as_create = true;
}
}
if ($mark_as_create) {
foreach ($xactions as $xaction) {
$xaction->setIsCreateTransaction(true);
}
}
$xactions = $this->sortTransactions($xactions);
$file_phids = $this->extractFilePHIDs($object, $xactions);
if ($is_preview) {
$this->loadHandles($xactions);
return $xactions;
}
$comment_editor = id(new PhabricatorApplicationTransactionCommentEditor())
->setActor($actor)
->setActingAsPHID($this->getActingAsPHID())
->setContentSource($this->getContentSource());
if (!$transaction_open) {
$object->openTransaction();
$transaction_open = true;
}
foreach ($xactions as $xaction) {
$this->applyInternalEffects($object, $xaction);
}
$xactions = $this->didApplyInternalEffects($object, $xactions);
try {
$object->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
// This callback has an opportunity to throw a better exception,
// so execution may end here.
$this->didCatchDuplicateKeyException($object, $xactions, $ex);
throw $ex;
}
foreach ($xactions as $xaction) {
$xaction->setObjectPHID($object->getPHID());
if ($xaction->getComment()) {
$xaction->setPHID($xaction->generatePHID());
$comment_editor->applyEdit($xaction, $xaction->getComment());
} else {
// TODO: This is a transitional hack to let us migrate edge
// transactions to a more efficient storage format. For now, we're
// going to write a new slim format to the database but keep the old
// bulky format on the objects so we don't have to upgrade all the
// edit logic to the new format yet. See T13051.
$edge_type = PhabricatorTransactions::TYPE_EDGE;
if ($xaction->getTransactionType() == $edge_type) {
$bulky_old = $xaction->getOldValue();
$bulky_new = $xaction->getNewValue();
$record = PhabricatorEdgeChangeRecord::newFromTransaction($xaction);
$slim_old = $record->getModernOldEdgeTransactionData();
$slim_new = $record->getModernNewEdgeTransactionData();
$xaction->setOldValue($slim_old);
$xaction->setNewValue($slim_new);
$xaction->save();
$xaction->setOldValue($bulky_old);
$xaction->setNewValue($bulky_new);
} else {
$xaction->save();
}
}
}
if ($file_phids) {
$this->attachFiles($object, $file_phids);
}
foreach ($xactions as $xaction) {
$this->applyExternalEffects($object, $xaction);
}
$xactions = $this->applyFinalEffects($object, $xactions);
if ($read_locking) {
$object->endReadLocking();
$read_locking = false;
}
if ($transaction_open) {
$object->saveTransaction();
$transaction_open = false;
}
$this->didCommitTransactions($object, $xactions);
} catch (Exception $ex) {
if ($read_locking) {
$object->endReadLocking();
$read_locking = false;
}
if ($transaction_open) {
$object->killTransaction();
$transaction_open = false;
}
throw $ex;
}
// If we need to perform cache engine updates, execute them now.
id(new PhabricatorCacheEngine())
->updateObject($object);
// Now that we've completely applied the core transaction set, try to apply
// Herald rules. Herald rules are allowed to either take direct actions on
// the database (like writing flags), or take indirect actions (like saving
// some targets for CC when we generate mail a little later), or return
// transactions which we'll apply normally using another Editor.
// First, check if *this* is a sub-editor which is itself applying Herald
// rules: if it is, stop working and return so we don't descend into
// madness.
// Otherwise, we're not a Herald editor, so process Herald rules (possibly
// using a Herald editor to apply resulting transactions) and then send out
// mail, notifications, and feed updates about everything.
if ($this->getIsHeraldEditor()) {
// We are the Herald editor, so stop work here and return the updated
// transactions.
return $xactions;
} else if ($this->getIsInverseEdgeEditor()) {
// Do not run Herald if we're just recording that this object was
// mentioned elsewhere. This tends to create Herald side effects which
// feel arbitrary, and can really slow down edits which mention a large
// number of other objects. See T13114.
} else if ($this->shouldApplyHeraldRules($object, $xactions)) {
// We are not the Herald editor, so try to apply Herald rules.
$herald_xactions = $this->applyHeraldRules($object, $xactions);
if ($herald_xactions) {
$xscript_id = $this->getHeraldTranscript()->getID();
foreach ($herald_xactions as $herald_xaction) {
// Don't set a transcript ID if this is a transaction from another
// application or source, like Owners.
if ($herald_xaction->getAuthorPHID()) {
continue;
}
$herald_xaction->setMetadataValue('herald:transcriptID', $xscript_id);
}
// NOTE: We're acting as the omnipotent user because rules deal with
// their own policy issues. We use a synthetic author PHID (the
// Herald application) as the author of record, so that transactions
// will render in a reasonable way ("Herald assigned this task ...").
$herald_actor = PhabricatorUser::getOmnipotentUser();
$herald_phid = id(new PhabricatorHeraldApplication())->getPHID();
// TODO: It would be nice to give transactions a more specific source
// which points at the rule which generated them. You can figure this
// out from transcripts, but it would be cleaner if you didn't have to.
$herald_source = PhabricatorContentSource::newForSource(
PhabricatorHeraldContentSource::SOURCECONST);
$herald_editor = newv(get_class($this), array())
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setParentMessageID($this->getParentMessageID())
->setIsHeraldEditor(true)
->setActor($herald_actor)
->setActingAsPHID($herald_phid)
->setContentSource($herald_source);
$herald_xactions = $herald_editor->applyTransactions(
$object,
$herald_xactions);
// Merge the new transactions into the transaction list: we want to
// send email and publish feed stories about them, too.
$xactions = array_merge($xactions, $herald_xactions);
}
// If Herald did not generate transactions, we may still need to handle
// "Send an Email" rules.
$adapter = $this->getHeraldAdapter();
$this->heraldEmailPHIDs = $adapter->getEmailPHIDs();
$this->heraldForcedEmailPHIDs = $adapter->getForcedEmailPHIDs();
$this->webhookMap = $adapter->getWebhookMap();
}
$xactions = $this->didApplyTransactions($object, $xactions);
if ($object instanceof PhabricatorCustomFieldInterface) {
// Maybe this makes more sense to move into the search index itself? For
// now I'm putting it here since I think we might end up with things that
// need it to be up to date once the next page loads, but if we don't go
// there we could move it into search once search moves to the daemons.
// It now happens in the search indexer as well, but the search indexer is
// always daemonized, so the logic above still potentially holds. We could
// possibly get rid of this. The major motivation for putting it in the
// indexer was to enable reindexing to work.
$fields = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
$fields->readFieldsFromStorage($object);
$fields->rebuildIndexes($object);
}
$herald_xscript = $this->getHeraldTranscript();
if ($herald_xscript) {
$herald_header = $herald_xscript->getXHeraldRulesHeader();
$herald_header = HeraldTranscript::saveXHeraldRulesHeader(
$object->getPHID(),
$herald_header);
} else {
$herald_header = HeraldTranscript::loadXHeraldRulesHeader(
$object->getPHID());
}
$this->heraldHeader = $herald_header;
// We're going to compute some of the data we'll use to publish these
// transactions here, before queueing a worker.
//
// Primarily, this is more correct: we want to publish the object as it
// exists right now. The worker may not execute for some time, and we want
// to use the current To/CC list, not respect any changes which may occur
// between now and when the worker executes.
//
// As a secondary benefit, this tends to reduce the amount of state that
// Editors need to pass into workers.
$object = $this->willPublish($object, $xactions);
if (!$this->getIsSilent()) {
if ($this->shouldSendMail($object, $xactions)) {
$this->mailShouldSend = true;
$this->mailToPHIDs = $this->getMailTo($object);
$this->mailCCPHIDs = $this->getMailCC($object);
$this->mailUnexpandablePHIDs = $this->newMailUnexpandablePHIDs($object);
// Add any recipients who were previously on the notification list
// but were removed by this change.
$this->applyOldRecipientLists();
if ($object instanceof PhabricatorSubscribableInterface) {
$this->mailMutedPHIDs = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
PhabricatorMutedByEdgeType::EDGECONST);
} else {
$this->mailMutedPHIDs = array();
}
$mail_xactions = $this->getTransactionsForMail($object, $xactions);
$stamps = $this->newMailStamps($object, $xactions);
foreach ($stamps as $stamp) {
$this->mailStamps[] = $stamp->toDictionary();
}
}
if ($this->shouldPublishFeedStory($object, $xactions)) {
$this->feedShouldPublish = true;
$this->feedRelatedPHIDs = $this->getFeedRelatedPHIDs(
$object,
$xactions);
$this->feedNotifyPHIDs = $this->getFeedNotifyPHIDs(
$object,
$xactions);
}
}
PhabricatorWorker::scheduleTask(
'PhabricatorApplicationTransactionPublishWorker',
array(
'objectPHID' => $object->getPHID(),
'actorPHID' => $this->getActingAsPHID(),
'xactionPHIDs' => mpull($xactions, 'getPHID'),
'state' => $this->getWorkerState(),
),
array(
'objectPHID' => $object->getPHID(),
'priority' => PhabricatorWorker::PRIORITY_ALERTS,
));
$this->flushTransactionQueue($object);
return $xactions;
}
protected function didCatchDuplicateKeyException(
PhabricatorLiskDAO $object,
array $xactions,
Exception $ex) {
return;
}
public function publishTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$this->object = $object;
$this->xactions = $xactions;
// Hook for edges or other properties that may need (re-)loading
$object = $this->willPublish($object, $xactions);
// The object might have changed, so reassign it.
$this->object = $object;
$messages = array();
if ($this->mailShouldSend) {
$messages = $this->buildMail($object, $xactions);
}
if ($this->supportsSearch()) {
PhabricatorSearchWorker::queueDocumentForIndexing(
$object->getPHID(),
array(
'transactionPHIDs' => mpull($xactions, 'getPHID'),
));
}
if ($this->feedShouldPublish) {
$mailed = array();
foreach ($messages as $mail) {
foreach ($mail->buildRecipientList() as $phid) {
$mailed[$phid] = $phid;
}
}
$this->publishFeedStory($object, $xactions, $mailed);
}
if ($this->sendHistory) {
$history_mail = $this->buildHistoryMail($object);
if ($history_mail) {
$messages[] = $history_mail;
}
}
// NOTE: This actually sends the mail. We do this last to reduce the chance
// that we send some mail, hit an exception, then send the mail again when
// retrying.
foreach ($messages as $mail) {
$mail->save();
}
$this->queueWebhooks($object, $xactions);
return $xactions;
}
protected function didApplyTransactions($object, array $xactions) {
// Hook for subclasses.
return $xactions;
}
private function loadHandles(array $xactions) {
$phids = array();
foreach ($xactions as $key => $xaction) {
$phids[$key] = $xaction->getRequiredHandlePHIDs();
}
$handles = array();
$merged = array_mergev($phids);
if ($merged) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->requireActor())
->withPHIDs($merged)
->execute();
}
foreach ($xactions as $key => $xaction) {
$xaction->setHandles(array_select_keys($handles, $phids[$key]));
}
}
private function loadSubscribers(PhabricatorLiskDAO $object) {
if ($object->getPHID() &&
($object instanceof PhabricatorSubscribableInterface)) {
$subs = PhabricatorSubscribersQuery::loadSubscribersForPHID(
$object->getPHID());
$this->subscribers = array_fuse($subs);
} else {
$this->subscribers = array();
}
}
private function validateEditParameters(
PhabricatorLiskDAO $object,
array $xactions) {
if (!$this->getContentSource()) {
throw new PhutilInvalidStateException('setContentSource');
}
// Do a bunch of sanity checks that the incoming transactions are fresh.
// They should be unsaved and have only "transactionType" and "newValue"
// set.
$types = array_fill_keys($this->getTransactionTypes(), true);
assert_instances_of($xactions, 'PhabricatorApplicationTransaction');
foreach ($xactions as $xaction) {
if ($xaction->getPHID() || $xaction->getID()) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht('You can not apply transactions which already have IDs/PHIDs!'));
}
if ($xaction->getObjectPHID()) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'You can not apply transactions which already have %s!',
'objectPHIDs'));
}
if ($xaction->getCommentPHID()) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'You can not apply transactions which already have %s!',
'commentPHIDs'));
}
if ($xaction->getCommentVersion() !== 0) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'You can not apply transactions which already have '.
'commentVersions!'));
}
$expect_value = !$xaction->shouldGenerateOldValue();
$has_value = $xaction->hasOldValue();
+ // See T13082. In the narrow case of applying inverse edge edits, we
+ // expect the old value to be populated.
+ if ($this->getIsInverseEdgeEditor()) {
+ $expect_value = true;
+ }
+
if ($expect_value && !$has_value) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'This transaction is supposed to have an %s set, but it does not!',
'oldValue'));
}
if ($has_value && !$expect_value) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'This transaction should generate its %s automatically, '.
'but has already had one set!',
'oldValue'));
}
$type = $xaction->getTransactionType();
if (empty($types[$type])) {
throw new PhabricatorApplicationTransactionStructureException(
$xaction,
pht(
'Transaction has type "%s", but that transaction type is not '.
'supported by this editor (%s).',
$type,
get_class($this)));
}
}
}
private function applyCapabilityChecks(
PhabricatorLiskDAO $object,
array $xactions) {
assert_instances_of($xactions, 'PhabricatorApplicationTransaction');
$can_edit = PhabricatorPolicyCapability::CAN_EDIT;
if ($this->getIsNewObject()) {
// If we're creating a new object, we don't need any special capabilities
// on the object. The actor has already made it through creation checks,
// and objects which haven't been created yet often can not be
// meaningfully tested for capabilities anyway.
$required_capabilities = array();
} else {
if (!$xactions && !$this->xactions) {
// If we aren't doing anything, require CAN_EDIT to improve consistency.
$required_capabilities = array($can_edit);
} else {
$required_capabilities = array();
foreach ($xactions as $xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if (!$xtype) {
$capabilities = $this->getLegacyRequiredCapabilities($xaction);
} else {
$capabilities = $xtype->getRequiredCapabilities($object, $xaction);
}
// For convenience, we allow flexibility in the return types because
// it's very unusual that a transaction actually requires multiple
// capability checks.
if ($capabilities === null) {
$capabilities = array();
} else {
$capabilities = (array)$capabilities;
}
foreach ($capabilities as $capability) {
$required_capabilities[$capability] = $capability;
}
}
}
}
$required_capabilities = array_fuse($required_capabilities);
$actor = $this->getActor();
if ($required_capabilities) {
id(new PhabricatorPolicyFilter())
->setViewer($actor)
->requireCapabilities($required_capabilities)
->raisePolicyExceptions(true)
->apply(array($object));
}
}
private function getLegacyRequiredCapabilities(
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
switch ($type) {
case PhabricatorTransactions::TYPE_COMMENT:
// TODO: Comments technically require CAN_INTERACT, but this is
// currently somewhat special and handled through EditEngine. For now,
// don't enforce it here.
return null;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
// TODO: Removing subscribers other than yourself should probably
// require CAN_EDIT permission. You can do this via the API but
// generally can not via the web interface.
return null;
case PhabricatorTransactions::TYPE_TOKEN:
// TODO: This technically requires CAN_INTERACT, like comments.
return null;
case PhabricatorTransactions::TYPE_HISTORY:
// This is a special magic transaction which sends you history via
// email and is only partially supported in the upstream. You don't
// need any capabilities to apply it.
return null;
+ case PhabricatorTransactions::TYPE_MFA:
+ // Signing a transaction group with MFA does not require permissions
+ // on its own.
+ return null;
case PhabricatorTransactions::TYPE_EDGE:
return $this->getLegacyRequiredEdgeCapabilities($xaction);
default:
// For other older (non-modular) transactions, always require exactly
// CAN_EDIT. Transactions which do not need CAN_EDIT or need additional
// capabilities must move to ModularTransactions.
return PhabricatorPolicyCapability::CAN_EDIT;
}
}
private function getLegacyRequiredEdgeCapabilities(
PhabricatorApplicationTransaction $xaction) {
// You don't need to have edit permission on an object to mention it or
// otherwise add a relationship pointing toward it.
if ($this->getIsInverseEdgeEditor()) {
return null;
}
$edge_type = $xaction->getMetadataValue('edge:type');
switch ($edge_type) {
case PhabricatorMutedByEdgeType::EDGECONST:
// At time of writing, you can only write this edge for yourself, so
// you don't need permissions. If you can eventually mute an object
// for other users, this would need to be revisited.
return null;
case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
return null;
case PhabricatorProjectProjectHasMemberEdgeType::EDGECONST:
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$add = array_keys(array_diff_key($new, $old));
$rem = array_keys(array_diff_key($old, $new));
$actor_phid = $this->requireActor()->getPHID();
$is_join = (($add === array($actor_phid)) && !$rem);
$is_leave = (($rem === array($actor_phid)) && !$add);
if ($is_join) {
// You need CAN_JOIN to join a project.
return PhabricatorPolicyCapability::CAN_JOIN;
}
if ($is_leave) {
$object = $this->object;
// You usually don't need any capabilities to leave a project...
if ($object->getIsMembershipLocked()) {
// ...you must be able to edit to leave locked projects, though.
return PhabricatorPolicyCapability::CAN_EDIT;
} else {
return null;
}
}
// You need CAN_EDIT to change members other than yourself.
return PhabricatorPolicyCapability::CAN_EDIT;
+ case PhabricatorObjectHasWatcherEdgeType::EDGECONST:
+ // See PHI1024. Watching a project does not require CAN_EDIT.
+ return null;
default:
return PhabricatorPolicyCapability::CAN_EDIT;
}
}
private function buildSubscribeTransaction(
PhabricatorLiskDAO $object,
array $xactions,
array $changes) {
if (!($object instanceof PhabricatorSubscribableInterface)) {
return null;
}
if ($this->shouldEnableMentions($object, $xactions)) {
// Identify newly mentioned users. We ignore users who were previously
// mentioned so that we don't re-subscribe users after an edit of text
// which mentions them.
$old_texts = mpull($changes, 'getOldValue');
$new_texts = mpull($changes, 'getNewValue');
$old_phids = PhabricatorMarkupEngine::extractPHIDsFromMentions(
$this->getActor(),
$old_texts);
$new_phids = PhabricatorMarkupEngine::extractPHIDsFromMentions(
$this->getActor(),
$new_texts);
$phids = array_diff($new_phids, $old_phids);
} else {
$phids = array();
}
$this->mentionedPHIDs = $phids;
if ($object->getPHID()) {
// Don't try to subscribe already-subscribed mentions: we want to generate
// a dialog about an action having no effect if the user explicitly adds
// existing CCs, but not if they merely mention existing subscribers.
$phids = array_diff($phids, $this->subscribers);
}
if ($phids) {
$users = id(new PhabricatorPeopleQuery())
->setViewer($this->getActor())
->withPHIDs($phids)
->execute();
$users = mpull($users, null, 'getPHID');
foreach ($phids as $key => $phid) {
// Do not subscribe mentioned users
// who do not have VIEW Permissions
if ($object instanceof PhabricatorPolicyInterface
&& !PhabricatorPolicyFilter::hasCapability(
$users[$phid],
$object,
PhabricatorPolicyCapability::CAN_VIEW)
) {
unset($phids[$key]);
} else {
if ($object->isAutomaticallySubscribed($phid)) {
unset($phids[$key]);
}
}
}
$phids = array_values($phids);
}
// No else here to properly return null should we unset all subscriber
if (!$phids) {
return null;
}
$xaction = newv(get_class(head($xactions)), array());
$xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS);
$xaction->setNewValue(array('+' => $phids));
return $xaction;
}
protected function mergeTransactions(
PhabricatorApplicationTransaction $u,
PhabricatorApplicationTransaction $v) {
$type = $u->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$object = $this->object;
return $xtype->mergeTransactions($object, $u, $v);
}
switch ($type) {
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return $this->mergePHIDOrEdgeTransactions($u, $v);
case PhabricatorTransactions::TYPE_EDGE:
$u_type = $u->getMetadataValue('edge:type');
$v_type = $v->getMetadataValue('edge:type');
if ($u_type == $v_type) {
return $this->mergePHIDOrEdgeTransactions($u, $v);
}
return null;
}
// By default, do not merge the transactions.
return null;
}
/**
* Optionally expand transactions which imply other effects. For example,
* resigning from a revision in Differential implies removing yourself as
* a reviewer.
*/
protected function expandTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$results = array();
foreach ($xactions as $xaction) {
foreach ($this->expandTransaction($object, $xaction) as $expanded) {
$results[] = $expanded;
}
}
return $results;
}
protected function expandTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
return array($xaction);
}
public function getExpandedSupportTransactions(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$xactions = array($xaction);
$xactions = $this->expandSupportTransactions(
$object,
$xactions);
if (count($xactions) == 1) {
return array();
}
foreach ($xactions as $index => $cxaction) {
if ($cxaction === $xaction) {
unset($xactions[$index]);
break;
}
}
return $xactions;
}
private function expandSupportTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$this->loadSubscribers($object);
$xactions = $this->applyImplicitCC($object, $xactions);
$changes = $this->getRemarkupChanges($xactions);
$subscribe_xaction = $this->buildSubscribeTransaction(
$object,
$xactions,
$changes);
if ($subscribe_xaction) {
$xactions[] = $subscribe_xaction;
}
// TODO: For now, this is just a placeholder.
$engine = PhabricatorMarkupEngine::getEngine('extract');
$engine->setConfig('viewer', $this->requireActor());
$block_xactions = $this->expandRemarkupBlockTransactions(
$object,
$xactions,
$changes,
$engine);
foreach ($block_xactions as $xaction) {
$xactions[] = $xaction;
}
return $xactions;
}
private function getRemarkupChanges(array $xactions) {
$changes = array();
foreach ($xactions as $key => $xaction) {
foreach ($this->getRemarkupChangesFromTransaction($xaction) as $change) {
$changes[] = $change;
}
}
return $changes;
}
private function getRemarkupChangesFromTransaction(
PhabricatorApplicationTransaction $transaction) {
return $transaction->getRemarkupChanges();
}
private function expandRemarkupBlockTransactions(
PhabricatorLiskDAO $object,
array $xactions,
array $changes,
PhutilMarkupEngine $engine) {
$block_xactions = $this->expandCustomRemarkupBlockTransactions(
$object,
$xactions,
$changes,
$engine);
$mentioned_phids = array();
if ($this->shouldEnableMentions($object, $xactions)) {
foreach ($changes as $change) {
// Here, we don't care about processing only new mentions after an edit
// because there is no way for an object to ever "unmention" itself on
// another object, so we can ignore the old value.
$engine->markupText($change->getNewValue());
$mentioned_phids += $engine->getTextMetadata(
PhabricatorObjectRemarkupRule::KEY_MENTIONED_OBJECTS,
array());
}
}
if (!$mentioned_phids) {
return $block_xactions;
}
$mentioned_objects = id(new PhabricatorObjectQuery())
->setViewer($this->getActor())
->withPHIDs($mentioned_phids)
->execute();
$mentionable_phids = array();
if ($this->shouldEnableMentions($object, $xactions)) {
foreach ($mentioned_objects as $mentioned_object) {
if ($mentioned_object instanceof PhabricatorMentionableInterface) {
$mentioned_phid = $mentioned_object->getPHID();
if (idx($this->getUnmentionablePHIDMap(), $mentioned_phid)) {
continue;
}
// don't let objects mention themselves
if ($object->getPHID() && $mentioned_phid == $object->getPHID()) {
continue;
}
$mentionable_phids[$mentioned_phid] = $mentioned_phid;
}
}
}
if ($mentionable_phids) {
$edge_type = PhabricatorObjectMentionsObjectEdgeType::EDGECONST;
$block_xactions[] = newv(get_class(head($xactions)), array())
->setIgnoreOnNoEffect(true)
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $edge_type)
->setNewValue(array('+' => $mentionable_phids));
}
return $block_xactions;
}
protected function expandCustomRemarkupBlockTransactions(
PhabricatorLiskDAO $object,
array $xactions,
array $changes,
PhutilMarkupEngine $engine) {
return array();
}
/**
* Attempt to combine similar transactions into a smaller number of total
* transactions. For example, two transactions which edit the title of an
* object can be merged into a single edit.
*/
private function combineTransactions(array $xactions) {
$stray_comments = array();
$result = array();
$types = array();
foreach ($xactions as $key => $xaction) {
$type = $xaction->getTransactionType();
if (isset($types[$type])) {
foreach ($types[$type] as $other_key) {
$other_xaction = $result[$other_key];
// Don't merge transactions with different authors. For example,
// don't merge Herald transactions and owners transactions.
if ($other_xaction->getAuthorPHID() != $xaction->getAuthorPHID()) {
continue;
}
$merged = $this->mergeTransactions($result[$other_key], $xaction);
if ($merged) {
$result[$other_key] = $merged;
if ($xaction->getComment() &&
($xaction->getComment() !== $merged->getComment())) {
$stray_comments[] = $xaction->getComment();
}
if ($result[$other_key]->getComment() &&
($result[$other_key]->getComment() !== $merged->getComment())) {
$stray_comments[] = $result[$other_key]->getComment();
}
// Move on to the next transaction.
continue 2;
}
}
}
$result[$key] = $xaction;
$types[$type][] = $key;
}
// If we merged any comments away, restore them.
foreach ($stray_comments as $comment) {
$xaction = newv(get_class(head($result)), array());
$xaction->setTransactionType(PhabricatorTransactions::TYPE_COMMENT);
$xaction->setComment($comment);
$result[] = $xaction;
}
return array_values($result);
}
public function mergePHIDOrEdgeTransactions(
PhabricatorApplicationTransaction $u,
PhabricatorApplicationTransaction $v) {
$result = $u->getNewValue();
foreach ($v->getNewValue() as $key => $value) {
if ($u->getTransactionType() == PhabricatorTransactions::TYPE_EDGE) {
if (empty($result[$key])) {
$result[$key] = $value;
} else {
// We're merging two lists of edge adds, sets, or removes. Merge
// them by merging individual PHIDs within them.
$merged = $result[$key];
foreach ($value as $dst => $v_spec) {
if (empty($merged[$dst])) {
$merged[$dst] = $v_spec;
} else {
// Two transactions are trying to perform the same operation on
// the same edge. Normalize the edge data and then merge it. This
// allows transactions to specify how data merges execute in a
// precise way.
$u_spec = $merged[$dst];
if (!is_array($u_spec)) {
$u_spec = array('dst' => $u_spec);
}
if (!is_array($v_spec)) {
$v_spec = array('dst' => $v_spec);
}
$ux_data = idx($u_spec, 'data', array());
$vx_data = idx($v_spec, 'data', array());
$merged_data = $this->mergeEdgeData(
$u->getMetadataValue('edge:type'),
$ux_data,
$vx_data);
$u_spec['data'] = $merged_data;
$merged[$dst] = $u_spec;
}
}
$result[$key] = $merged;
}
} else {
$result[$key] = array_merge($value, idx($result, $key, array()));
}
}
$u->setNewValue($result);
// When combining an "ignore" transaction with a normal transaction, make
// sure we don't propagate the "ignore" flag.
if (!$v->getIgnoreOnNoEffect()) {
$u->setIgnoreOnNoEffect(false);
}
return $u;
}
protected function mergeEdgeData($type, array $u, array $v) {
return $v + $u;
}
protected function getPHIDTransactionNewValue(
PhabricatorApplicationTransaction $xaction,
$old = null) {
if ($old !== null) {
$old = array_fuse($old);
} else {
$old = array_fuse($xaction->getOldValue());
}
return $this->getPHIDList($old, $xaction->getNewValue());
}
public function getPHIDList(array $old, array $new) {
$new_add = idx($new, '+', array());
unset($new['+']);
$new_rem = idx($new, '-', array());
unset($new['-']);
$new_set = idx($new, '=', null);
if ($new_set !== null) {
$new_set = array_fuse($new_set);
}
unset($new['=']);
if ($new) {
throw new Exception(
pht(
"Invalid '%s' value for PHID transaction. Value should contain only ".
"keys '%s' (add PHIDs), '%s' (remove PHIDs) and '%s' (set PHIDS).",
'new',
'+',
'-',
'='));
}
$result = array();
foreach ($old as $phid) {
if ($new_set !== null && empty($new_set[$phid])) {
continue;
}
$result[$phid] = $phid;
}
if ($new_set !== null) {
foreach ($new_set as $phid) {
$result[$phid] = $phid;
}
}
foreach ($new_add as $phid) {
$result[$phid] = $phid;
}
foreach ($new_rem as $phid) {
unset($result[$phid]);
}
return array_values($result);
}
protected function getEdgeTransactionNewValue(
PhabricatorApplicationTransaction $xaction) {
$new = $xaction->getNewValue();
$new_add = idx($new, '+', array());
unset($new['+']);
$new_rem = idx($new, '-', array());
unset($new['-']);
$new_set = idx($new, '=', null);
unset($new['=']);
if ($new) {
throw new Exception(
pht(
"Invalid '%s' value for Edge transaction. Value should contain only ".
"keys '%s' (add edges), '%s' (remove edges) and '%s' (set edges).",
'new',
'+',
'-',
'='));
}
$old = $xaction->getOldValue();
$lists = array($new_set, $new_add, $new_rem);
foreach ($lists as $list) {
$this->checkEdgeList($list, $xaction->getMetadataValue('edge:type'));
}
$result = array();
foreach ($old as $dst_phid => $edge) {
if ($new_set !== null && empty($new_set[$dst_phid])) {
continue;
}
$result[$dst_phid] = $this->normalizeEdgeTransactionValue(
$xaction,
$edge,
$dst_phid);
}
if ($new_set !== null) {
foreach ($new_set as $dst_phid => $edge) {
$result[$dst_phid] = $this->normalizeEdgeTransactionValue(
$xaction,
$edge,
$dst_phid);
}
}
foreach ($new_add as $dst_phid => $edge) {
$result[$dst_phid] = $this->normalizeEdgeTransactionValue(
$xaction,
$edge,
$dst_phid);
}
foreach ($new_rem as $dst_phid => $edge) {
unset($result[$dst_phid]);
}
return $result;
}
private function checkEdgeList($list, $edge_type) {
if (!$list) {
return;
}
foreach ($list as $key => $item) {
if (phid_get_type($key) === PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
throw new Exception(
pht(
'Edge transactions must have destination PHIDs as in edge '.
'lists (found key "%s" on transaction of type "%s").',
$key,
$edge_type));
}
if (!is_array($item) && $item !== $key) {
throw new Exception(
pht(
'Edge transactions must have PHIDs or edge specs as values '.
'(found value "%s" on transaction of type "%s").',
$item,
$edge_type));
}
}
}
private function normalizeEdgeTransactionValue(
PhabricatorApplicationTransaction $xaction,
$edge,
$dst_phid) {
if (!is_array($edge)) {
if ($edge != $dst_phid) {
throw new Exception(
pht(
'Transaction edge data must either be the edge PHID or an edge '.
'specification dictionary.'));
}
$edge = array();
} else {
foreach ($edge as $key => $value) {
switch ($key) {
case 'src':
case 'dst':
case 'type':
case 'data':
case 'dateCreated':
case 'dateModified':
case 'seq':
case 'dataID':
break;
default:
throw new Exception(
pht(
'Transaction edge specification contains unexpected key "%s".',
$key));
}
}
}
$edge['dst'] = $dst_phid;
$edge_type = $xaction->getMetadataValue('edge:type');
if (empty($edge['type'])) {
$edge['type'] = $edge_type;
} else {
if ($edge['type'] != $edge_type) {
$this_type = $edge['type'];
throw new Exception(
pht(
"Edge transaction includes edge of type '%s', but ".
"transaction is of type '%s'. Each edge transaction ".
"must alter edges of only one type.",
$this_type,
$edge_type));
}
}
if (!isset($edge['data'])) {
$edge['data'] = array();
}
return $edge;
}
protected function sortTransactions(array $xactions) {
$head = array();
$tail = array();
// Move bare comments to the end, so the actions precede them.
foreach ($xactions as $xaction) {
$type = $xaction->getTransactionType();
if ($type == PhabricatorTransactions::TYPE_COMMENT) {
$tail[] = $xaction;
} else {
$head[] = $xaction;
}
}
return array_values(array_merge($head, $tail));
}
protected function filterTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
$type_comment = PhabricatorTransactions::TYPE_COMMENT;
+ $type_mfa = PhabricatorTransactions::TYPE_MFA;
$no_effect = array();
$has_comment = false;
$any_effect = false;
+
+ $meta_xactions = array();
foreach ($xactions as $key => $xaction) {
+ if ($xaction->getTransactionType() === $type_mfa) {
+ $meta_xactions[$key] = $xaction;
+ continue;
+ }
+
if ($this->transactionHasEffect($object, $xaction)) {
if ($xaction->getTransactionType() != $type_comment) {
$any_effect = true;
}
} else if ($xaction->getIgnoreOnNoEffect()) {
unset($xactions[$key]);
} else {
$no_effect[$key] = $xaction;
}
+
if ($xaction->hasComment()) {
$has_comment = true;
}
}
+ // If every transaction is a meta-transaction applying to the transaction
+ // group, these transactions are junk.
+ if (count($meta_xactions) == count($xactions)) {
+ $no_effect = $xactions;
+ $any_effect = false;
+ }
+
if (!$no_effect) {
return $xactions;
}
+ // If none of the transactions have an effect, the meta-transactions also
+ // have no effect. Add them to the "no effect" list so we get a full set
+ // of errors for everything.
+ if (!$any_effect) {
+ $no_effect += $meta_xactions;
+ }
+
if (!$this->getContinueOnNoEffect() && !$this->getIsPreview()) {
throw new PhabricatorApplicationTransactionNoEffectException(
$no_effect,
$any_effect,
$has_comment);
}
if (!$any_effect && !$has_comment) {
// If we only have empty comment transactions, just drop them all.
return array();
}
foreach ($no_effect as $key => $xaction) {
if ($xaction->hasComment()) {
$xaction->setTransactionType($type_comment);
$xaction->setOldValue(null);
$xaction->setNewValue(null);
} else {
unset($xactions[$key]);
}
}
return $xactions;
}
/**
* Hook for validating transactions. This callback will be invoked for each
* available transaction type, even if an edit does not apply any transactions
* of that type. This allows you to raise exceptions when required fields are
* missing, by detecting that the object has no field value and there is no
* transaction which sets one.
*
* @param PhabricatorLiskDAO Object being edited.
* @param string Transaction type to validate.
* @param list<PhabricatorApplicationTransaction> Transactions of given type,
* which may be empty if the edit does not apply any transactions of the
* given type.
* @return list<PhabricatorApplicationTransactionValidationError> List of
* validation errors.
*/
protected function validateTransaction(
PhabricatorLiskDAO $object,
$type,
array $xactions) {
$errors = array();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$errors[] = $xtype->validateTransactions($object, $xactions);
}
switch ($type) {
case PhabricatorTransactions::TYPE_VIEW_POLICY:
$errors[] = $this->validatePolicyTransaction(
$object,
$xactions,
$type,
PhabricatorPolicyCapability::CAN_VIEW);
break;
case PhabricatorTransactions::TYPE_EDIT_POLICY:
$errors[] = $this->validatePolicyTransaction(
$object,
$xactions,
$type,
PhabricatorPolicyCapability::CAN_EDIT);
break;
case PhabricatorTransactions::TYPE_SPACE:
$errors[] = $this->validateSpaceTransactions(
$object,
$xactions,
$type);
break;
case PhabricatorTransactions::TYPE_SUBTYPE:
$errors[] = $this->validateSubtypeTransactions(
$object,
$xactions,
$type);
break;
+ case PhabricatorTransactions::TYPE_MFA:
+ $errors[] = $this->validateMFATransactions(
+ $object,
+ $xactions,
+ $type);
+ break;
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$groups = array();
foreach ($xactions as $xaction) {
$groups[$xaction->getMetadataValue('customfield:key')][] = $xaction;
}
$field_list = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_EDIT);
$field_list->setViewer($this->getActor());
$role_xactions = PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS;
foreach ($field_list->getFields() as $field) {
if (!$field->shouldEnableForRole($role_xactions)) {
continue;
}
$errors[] = $field->validateApplicationTransactions(
$this,
$type,
idx($groups, $field->getFieldKey(), array()));
}
break;
}
return array_mergev($errors);
}
public function validatePolicyTransaction(
PhabricatorLiskDAO $object,
array $xactions,
$transaction_type,
$capability) {
$actor = $this->requireActor();
$errors = array();
// Note $this->xactions is necessary; $xactions is $this->xactions of
// $transaction_type
$policy_object = $this->adjustObjectForPolicyChecks(
$object,
$this->xactions);
// Make sure the user isn't editing away their ability to $capability this
// object.
foreach ($xactions as $xaction) {
try {
PhabricatorPolicyFilter::requireCapabilityWithForcedPolicy(
$actor,
$policy_object,
$capability,
$xaction->getNewValue());
} catch (PhabricatorPolicyException $ex) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$transaction_type,
pht('Invalid'),
pht(
'You can not select this %s policy, because you would no longer '.
'be able to %s the object.',
$capability,
$capability),
$xaction);
}
}
if ($this->getIsNewObject()) {
if (!$xactions) {
$has_capability = PhabricatorPolicyFilter::hasCapability(
$actor,
$policy_object,
$capability);
if (!$has_capability) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$transaction_type,
pht('Invalid'),
pht(
'The selected %s policy excludes you. Choose a %s policy '.
'which allows you to %s the object.',
$capability,
$capability,
$capability));
}
}
}
return $errors;
}
private function validateSpaceTransactions(
PhabricatorLiskDAO $object,
array $xactions,
$transaction_type) {
$errors = array();
$actor = $this->getActor();
$has_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpacesExist($actor);
$actor_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces($actor);
$active_spaces = PhabricatorSpacesNamespaceQuery::getViewerActiveSpaces(
$actor);
foreach ($xactions as $xaction) {
$space_phid = $xaction->getNewValue();
if ($space_phid === null) {
if (!$has_spaces) {
// The install doesn't have any spaces, so this is fine.
continue;
}
// The install has some spaces, so every object needs to be put
// in a valid space.
$errors[] = new PhabricatorApplicationTransactionValidationError(
$transaction_type,
pht('Invalid'),
pht('You must choose a space for this object.'),
$xaction);
continue;
}
// If the PHID isn't `null`, it needs to be a valid space that the
// viewer can see.
if (empty($actor_spaces[$space_phid])) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$transaction_type,
pht('Invalid'),
pht(
'You can not shift this object in the selected space, because '.
'the space does not exist or you do not have access to it.'),
$xaction);
} else if (empty($active_spaces[$space_phid])) {
// It's OK to edit objects in an archived space, so just move on if
// we aren't adjusting the value.
$old_space_phid = $this->getTransactionOldValue($object, $xaction);
if ($space_phid == $old_space_phid) {
continue;
}
$errors[] = new PhabricatorApplicationTransactionValidationError(
$transaction_type,
pht('Archived'),
pht(
'You can not shift this object into the selected space, because '.
'the space is archived. Objects can not be created inside (or '.
'moved into) archived spaces.'),
$xaction);
}
}
return $errors;
}
private function validateSubtypeTransactions(
PhabricatorLiskDAO $object,
array $xactions,
$transaction_type) {
$errors = array();
$map = $object->newEditEngineSubtypeMap();
$old = $object->getEditEngineSubtype();
foreach ($xactions as $xaction) {
$new = $xaction->getNewValue();
if ($old == $new) {
continue;
}
if (!$map->isValidSubtype($new)) {
$errors[] = new PhabricatorApplicationTransactionValidationError(
$transaction_type,
pht('Invalid'),
pht(
'The subtype "%s" is not a valid subtype.',
$new),
$xaction);
continue;
}
}
return $errors;
}
+ private function validateMFATransactions(
+ PhabricatorLiskDAO $object,
+ array $xactions,
+ $transaction_type) {
+ $errors = array();
+
+ $factors = id(new PhabricatorAuthFactorConfigQuery())
+ ->setViewer($this->getActor())
+ ->withUserPHIDs(array($this->getActingAsPHID()))
+ ->withFactorProviderStatuses(
+ array(
+ PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE,
+ PhabricatorAuthFactorProviderStatus::STATUS_DEPRECATED,
+ ))
+ ->execute();
+
+ foreach ($xactions as $xaction) {
+ if (!$factors) {
+ $errors[] = new PhabricatorApplicationTransactionValidationError(
+ $transaction_type,
+ pht('No MFA'),
+ pht(
+ 'You do not have any MFA factors attached to your account, so '.
+ 'you can not sign this transaction group with MFA. Add MFA to '.
+ 'your account in Settings.'),
+ $xaction);
+ }
+ }
+
+ if ($xactions) {
+ $this->setShouldRequireMFA(true);
+ }
+
+ return $errors;
+ }
+
protected function adjustObjectForPolicyChecks(
PhabricatorLiskDAO $object,
array $xactions) {
$copy = clone $object;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$clone_xaction = clone $xaction;
$clone_xaction->setOldValue(array_values($this->subscribers));
$clone_xaction->setNewValue(
$this->getPHIDTransactionNewValue(
$clone_xaction));
PhabricatorPolicyRule::passTransactionHintToRule(
$copy,
new PhabricatorSubscriptionsSubscribersPolicyRule(),
array_fuse($clone_xaction->getNewValue()));
break;
case PhabricatorTransactions::TYPE_SPACE:
$space_phid = $this->getTransactionNewValue($object, $xaction);
$copy->setSpacePHID($space_phid);
break;
}
}
return $copy;
}
protected function validateAllTransactions(
PhabricatorLiskDAO $object,
array $xactions) {
return array();
}
/**
* Check for a missing text field.
*
* A text field is missing if the object has no value and there are no
* transactions which set a value, or if the transactions remove the value.
* This method is intended to make implementing @{method:validateTransaction}
* more convenient:
*
* $missing = $this->validateIsEmptyTextField(
* $object->getName(),
* $xactions);
*
* This will return `true` if the net effect of the object and transactions
* is an empty field.
*
* @param wild Current field value.
* @param list<PhabricatorApplicationTransaction> Transactions editing the
* field.
* @return bool True if the field will be an empty text field after edits.
*/
protected function validateIsEmptyTextField($field_value, array $xactions) {
if (strlen($field_value) && empty($xactions)) {
return false;
}
if ($xactions && strlen(last($xactions)->getNewValue())) {
return false;
}
return true;
}
/* -( Implicit CCs )------------------------------------------------------- */
/**
* When a user interacts with an object, we might want to add them to CC.
*/
final public function applyImplicitCC(
PhabricatorLiskDAO $object,
array $xactions) {
if (!($object instanceof PhabricatorSubscribableInterface)) {
// If the object isn't subscribable, we can't CC them.
return $xactions;
}
$actor_phid = $this->getActingAsPHID();
$type_user = PhabricatorPeopleUserPHIDType::TYPECONST;
if (phid_get_type($actor_phid) != $type_user) {
// Transactions by application actors like Herald, Harbormaster and
// Diffusion should not CC the applications.
return $xactions;
}
if ($object->isAutomaticallySubscribed($actor_phid)) {
// If they're auto-subscribed, don't CC them.
return $xactions;
}
$should_cc = false;
foreach ($xactions as $xaction) {
if ($this->shouldImplyCC($object, $xaction)) {
$should_cc = true;
break;
}
}
if (!$should_cc) {
// Only some types of actions imply a CC (like adding a comment).
return $xactions;
}
if ($object->getPHID()) {
if (isset($this->subscribers[$actor_phid])) {
// If the user is already subscribed, don't implicitly CC them.
return $xactions;
}
$unsub = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
PhabricatorObjectHasUnsubscriberEdgeType::EDGECONST);
$unsub = array_fuse($unsub);
if (isset($unsub[$actor_phid])) {
// If the user has previously unsubscribed from this object explicitly,
// don't implicitly CC them.
return $xactions;
}
}
$xaction = newv(get_class(head($xactions)), array());
$xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS);
$xaction->setNewValue(array('+' => array($actor_phid)));
array_unshift($xactions, $xaction);
return $xactions;
}
protected function shouldImplyCC(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
return $xaction->isCommentTransaction();
}
/* -( Sending Mail )------------------------------------------------------- */
/**
* @task mail
*/
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return false;
}
/**
* @task mail
*/
private function buildMail(
PhabricatorLiskDAO $object,
array $xactions) {
$email_to = $this->mailToPHIDs;
$email_cc = $this->mailCCPHIDs;
$email_cc = array_merge($email_cc, $this->heraldEmailPHIDs);
$unexpandable = $this->mailUnexpandablePHIDs;
if (!is_array($unexpandable)) {
$unexpandable = array();
}
$messages = $this->buildMailWithRecipients(
$object,
$xactions,
$email_to,
$email_cc,
$unexpandable);
$this->runHeraldMailRules($messages);
return $messages;
}
private function buildMailWithRecipients(
PhabricatorLiskDAO $object,
array $xactions,
array $email_to,
array $email_cc,
array $unexpandable) {
$targets = $this->buildReplyHandler($object)
->setUnexpandablePHIDs($unexpandable)
->getMailTargets($email_to, $email_cc);
// Set this explicitly before we start swapping out the effective actor.
$this->setActingAsPHID($this->getActingAsPHID());
$messages = array();
foreach ($targets as $target) {
$original_actor = $this->getActor();
$viewer = $target->getViewer();
$this->setActor($viewer);
$locale = PhabricatorEnv::beginScopedLocale($viewer->getTranslation());
$caught = null;
$mail = null;
try {
// Reload handles for the new viewer.
$this->loadHandles($xactions);
$mail = $this->buildMailForTarget($object, $xactions, $target);
if ($mail) {
if ($this->mustEncrypt) {
$mail
->setMustEncrypt(true)
->setMustEncryptReasons($this->mustEncrypt);
}
}
} catch (Exception $ex) {
$caught = $ex;
}
$this->setActor($original_actor);
unset($locale);
if ($caught) {
throw $ex;
}
if ($mail) {
$messages[] = $mail;
}
}
return $messages;
}
protected function getTransactionsForMail(
PhabricatorLiskDAO $object,
array $xactions) {
return $xactions;
}
private function buildMailForTarget(
PhabricatorLiskDAO $object,
array $xactions,
PhabricatorMailTarget $target) {
// Check if any of the transactions are visible for this viewer. If we
// don't have any visible transactions, don't send the mail.
$any_visible = false;
foreach ($xactions as $xaction) {
if (!$xaction->shouldHideForMail($xactions)) {
$any_visible = true;
break;
}
}
if (!$any_visible) {
return null;
}
$mail_xactions = $this->getTransactionsForMail($object, $xactions);
$mail = $this->buildMailTemplate($object);
$body = $this->buildMailBody($object, $mail_xactions);
$mail_tags = $this->getMailTags($object, $mail_xactions);
$action = $this->getMailAction($object, $mail_xactions);
$stamps = $this->generateMailStamps($object, $this->mailStamps);
if (PhabricatorEnv::getEnvConfig('metamta.email-preferences')) {
$this->addEmailPreferenceSectionToMailBody(
$body,
$object,
$mail_xactions);
}
$muted_phids = $this->mailMutedPHIDs;
if (!is_array($muted_phids)) {
$muted_phids = array();
}
$mail
->setSensitiveContent(false)
->setFrom($this->getActingAsPHID())
->setSubjectPrefix($this->getMailSubjectPrefix())
->setVarySubjectPrefix('['.$action.']')
->setThreadID($this->getMailThreadID($object), $this->getIsNewObject())
->setRelatedPHID($object->getPHID())
->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs())
->setMutedPHIDs($muted_phids)
->setForceHeraldMailRecipientPHIDs($this->heraldForcedEmailPHIDs)
->setMailTags($mail_tags)
->setIsBulk(true)
->setBody($body->render())
->setHTMLBody($body->renderHTML());
foreach ($body->getAttachments() as $attachment) {
$mail->addAttachment($attachment);
}
if ($this->heraldHeader) {
$mail->addHeader('X-Herald-Rules', $this->heraldHeader);
}
if ($object instanceof PhabricatorProjectInterface) {
$this->addMailProjectMetadata($object, $mail);
}
if ($this->getParentMessageID()) {
$mail->setParentMessageID($this->getParentMessageID());
}
// If we have stamps, attach the raw dictionary version (not the actual
// objects) to the mail so that debugging tools can see what we used to
// render the final list.
if ($this->mailStamps) {
$mail->setMailStampMetadata($this->mailStamps);
}
// If we have rendered stamps, attach them to the mail.
if ($stamps) {
$mail->setMailStamps($stamps);
}
return $target->willSendMail($mail);
}
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();
}
/**
* @task mail
*/
protected function getStrongestAction(
PhabricatorLiskDAO $object,
array $xactions) {
return last(msort($xactions, 'getActionStrength'));
}
/**
* @task mail
*/
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
throw new Exception(pht('Capability not supported.'));
}
/**
* @task mail
*/
protected function getMailSubjectPrefix() {
throw new Exception(pht('Capability not supported.'));
}
/**
* @task mail
*/
protected function getMailTags(
PhabricatorLiskDAO $object,
array $xactions) {
$tags = array();
foreach ($xactions as $xaction) {
$tags[] = $xaction->getMailTags();
}
return array_mergev($tags);
}
/**
* @task mail
*/
public function getMailTagsMap() {
// TODO: We should move shared mail tags, like "comment", here.
return array();
}
/**
* @task mail
*/
protected function getMailAction(
PhabricatorLiskDAO $object,
array $xactions) {
return $this->getStrongestAction($object, $xactions)->getActionName();
}
/**
* @task mail
*/
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
throw new Exception(pht('Capability not supported.'));
}
/**
* @task mail
*/
protected function getMailTo(PhabricatorLiskDAO $object) {
throw new Exception(pht('Capability not supported.'));
}
protected function newMailUnexpandablePHIDs(PhabricatorLiskDAO $object) {
return array();
}
/**
* @task mail
*/
protected function getMailCC(PhabricatorLiskDAO $object) {
$phids = array();
$has_support = false;
if ($object instanceof PhabricatorSubscribableInterface) {
$phid = $object->getPHID();
$phids[] = PhabricatorSubscribersQuery::loadSubscribersForPHID($phid);
$has_support = true;
}
if ($object instanceof PhabricatorProjectInterface) {
$project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
if ($project_phids) {
$projects = id(new PhabricatorProjectQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($project_phids)
->needWatchers(true)
->execute();
$watcher_phids = array();
foreach ($projects as $project) {
foreach ($project->getAllAncestorWatcherPHIDs() as $phid) {
$watcher_phids[$phid] = $phid;
}
}
if ($watcher_phids) {
// We need to do a visibility check for all the watchers, as
// watching a project is not a guarantee that you can see objects
// associated with it.
$users = id(new PhabricatorPeopleQuery())
->setViewer($this->requireActor())
->withPHIDs($watcher_phids)
->execute();
$watchers = array();
foreach ($users as $user) {
$can_see = PhabricatorPolicyFilter::hasCapability(
$user,
$object,
PhabricatorPolicyCapability::CAN_VIEW);
if ($can_see) {
$watchers[] = $user->getPHID();
}
}
$phids[] = $watchers;
}
}
$has_support = true;
}
if (!$has_support) {
throw new Exception(
pht('The object being edited does not implement any standard '.
'interfaces (like PhabricatorSubscribableInterface) which allow '.
'CCs to be generated automatically. Override the "getMailCC()" '.
'method and generate CCs explicitly.'));
}
return array_mergev($phids);
}
/**
* @task mail
*/
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = id(new PhabricatorMetaMTAMailBody())
->setViewer($this->requireActor())
->setContextObject($object);
$this->addHeadersAndCommentsToMailBody($body, $xactions);
$this->addCustomFieldsToMailBody($body, $object, $xactions);
return $body;
}
/**
* @task mail
*/
protected function addEmailPreferenceSectionToMailBody(
PhabricatorMetaMTAMailBody $body,
PhabricatorLiskDAO $object,
array $xactions) {
$href = PhabricatorEnv::getProductionURI(
'/settings/panel/emailpreferences/');
$body->addLinkSection(pht('EMAIL PREFERENCES'), $href);
}
/**
* @task mail
*/
protected function addHeadersAndCommentsToMailBody(
PhabricatorMetaMTAMailBody $body,
array $xactions,
$object_label = null,
$object_href = null) {
// First, remove transactions which shouldn't be rendered in mail.
foreach ($xactions as $key => $xaction) {
if ($xaction->shouldHideForMail($xactions)) {
unset($xactions[$key]);
}
}
$headers = array();
$headers_html = array();
$comments = array();
$details = array();
$seen_comment = false;
foreach ($xactions as $xaction) {
// Most mail has zero or one comments. In these cases, we render the
// "alice added a comment." transaction in the header, like a normal
// transaction.
// Some mail, like Differential undraft mail or "!history" mail, may
// have two or more comments. In these cases, we'll put the first
// "alice added a comment." transaction in the header normally, but
// move the other transactions down so they provide context above the
// actual comment.
- $comment = $xaction->getBodyForMail();
+ $comment = $this->getBodyForTextMail($xaction);
if ($comment !== null) {
$is_comment = true;
$comments[] = array(
'xaction' => $xaction,
'comment' => $comment,
'initial' => !$seen_comment,
);
} else {
$is_comment = false;
}
if (!$is_comment || !$seen_comment) {
- $header = $xaction->getTitleForMail();
+ $header = $this->getTitleForTextMail($xaction);
if ($header !== null) {
$headers[] = $header;
}
- $header_html = $xaction->getTitleForHTMLMail();
+ $header_html = $this->getTitleForHTMLMail($xaction);
if ($header_html !== null) {
$headers_html[] = $header_html;
}
}
if ($xaction->hasChangeDetailsForMail()) {
$details[] = $xaction;
}
if ($is_comment) {
$seen_comment = true;
}
}
$headers_text = implode("\n", $headers);
$body->addRawPlaintextSection($headers_text);
$headers_html = phutil_implode_html(phutil_tag('br'), $headers_html);
$header_button = null;
if ($object_label !== null) {
$button_style = array(
'text-decoration: none;',
'padding: 4px 8px;',
'margin: 0 8px 8px;',
'float: right;',
'color: #464C5C;',
'font-weight: bold;',
'border-radius: 3px;',
'background-color: #F7F7F9;',
'background-image: linear-gradient(to bottom,#fff,#f1f0f1);',
'display: inline-block;',
'border: 1px solid rgba(71,87,120,.2);',
);
$header_button = phutil_tag(
'a',
array(
'style' => implode(' ', $button_style),
'href' => $object_href,
),
$object_label);
}
$xactions_style = array();
$header_action = phutil_tag(
'td',
array(),
$header_button);
$header_action = phutil_tag(
'td',
array(
'style' => implode(' ', $xactions_style),
),
array(
$headers_html,
// Add an extra newline to prevent the "View Object" button from
// running into the transaction text in Mail.app text snippet
// previews.
"\n",
));
$headers_html = phutil_tag(
'table',
array(),
phutil_tag('tr', array(), array($header_action, $header_button)));
$body->addRawHTMLSection($headers_html);
foreach ($comments as $spec) {
$xaction = $spec['xaction'];
$comment = $spec['comment'];
$is_initial = $spec['initial'];
// If this is not the first comment in the mail, add the header showing
// who wrote the comment immediately above the comment.
if (!$is_initial) {
- $header = $xaction->getTitleForMail();
+ $header = $this->getTitleForTextMail($xaction);
if ($header !== null) {
$body->addRawPlaintextSection($header);
}
- $header_html = $xaction->getTitleForHTMLMail();
+ $header_html = $this->getTitleForHTMLMail($xaction);
if ($header_html !== null) {
$body->addRawHTMLSection($header_html);
}
}
$body->addRemarkupSection(null, $comment);
}
foreach ($details as $xaction) {
$details = $xaction->renderChangeDetailsForMail($body->getViewer());
if ($details !== null) {
$label = $this->getMailDiffSectionHeader($xaction);
$body->addHTMLSection($label, $details);
}
}
}
private function getMailDiffSectionHeader($xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
return $xtype->getMailDiffSectionHeader();
}
return pht('EDIT DETAILS');
}
/**
* @task mail
*/
protected function addCustomFieldsToMailBody(
PhabricatorMetaMTAMailBody $body,
PhabricatorLiskDAO $object,
array $xactions) {
if ($object instanceof PhabricatorCustomFieldInterface) {
$field_list = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_TRANSACTIONMAIL);
$field_list->setViewer($this->getActor());
$field_list->readFieldsFromStorage($object);
foreach ($field_list->getFields() as $field) {
$field->updateTransactionMailBody(
$body,
$this,
$xactions);
}
}
}
/**
* @task mail
*/
private function runHeraldMailRules(array $messages) {
foreach ($messages as $message) {
$engine = new HeraldEngine();
$adapter = id(new PhabricatorMailOutboundMailHeraldAdapter())
->setObject($message);
$rules = $engine->loadRulesForAdapter($adapter);
$effects = $engine->applyRules($rules, $adapter);
$engine->applyEffects($effects, $adapter, $rules);
}
}
/* -( Publishing Feed Stories )-------------------------------------------- */
/**
* @task feed
*/
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return false;
}
/**
* @task feed
*/
protected function getFeedStoryType() {
return 'PhabricatorApplicationTransactionFeedStory';
}
/**
* @task feed
*/
protected function getFeedRelatedPHIDs(
PhabricatorLiskDAO $object,
array $xactions) {
$phids = array(
$object->getPHID(),
$this->getActingAsPHID(),
);
if ($object instanceof PhabricatorProjectInterface) {
$project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
foreach ($project_phids as $project_phid) {
$phids[] = $project_phid;
}
}
return $phids;
}
/**
* @task feed
*/
protected function getFeedNotifyPHIDs(
PhabricatorLiskDAO $object,
array $xactions) {
// If some transactions are forcing notification delivery, add the forced
// recipients to the notify list.
$force_list = array();
foreach ($xactions as $xaction) {
$force_phids = $xaction->getForceNotifyPHIDs();
if (!$force_phids) {
continue;
}
foreach ($force_phids as $force_phid) {
$force_list[] = $force_phid;
}
}
$to_list = $this->getMailTo($object);
$cc_list = $this->getMailCC($object);
$full_list = array_merge($force_list, $to_list, $cc_list);
$full_list = array_fuse($full_list);
return array_keys($full_list);
}
/**
* @task feed
*/
protected function getFeedStoryData(
PhabricatorLiskDAO $object,
array $xactions) {
$xactions = msort($xactions, 'getActionStrength');
$xactions = array_reverse($xactions);
return array(
'objectPHID' => $object->getPHID(),
'transactionPHIDs' => mpull($xactions, 'getPHID'),
);
}
/**
* @task feed
*/
protected function publishFeedStory(
PhabricatorLiskDAO $object,
array $xactions,
array $mailed_phids) {
// Remove transactions which don't publish feed stories or notifications.
// These never show up anywhere, so we don't need to do anything with them.
foreach ($xactions as $key => $xaction) {
if (!$xaction->shouldHideForFeed()) {
continue;
}
if (!$xaction->shouldHideForNotifications()) {
continue;
}
unset($xactions[$key]);
}
if (!$xactions) {
return;
}
$related_phids = $this->feedRelatedPHIDs;
$subscribed_phids = $this->feedNotifyPHIDs;
// Remove muted users from the subscription list so they don't get
// notifications, either.
$muted_phids = $this->mailMutedPHIDs;
if (!is_array($muted_phids)) {
$muted_phids = array();
}
$subscribed_phids = array_fuse($subscribed_phids);
foreach ($muted_phids as $muted_phid) {
unset($subscribed_phids[$muted_phid]);
}
$subscribed_phids = array_values($subscribed_phids);
$story_type = $this->getFeedStoryType();
$story_data = $this->getFeedStoryData($object, $xactions);
$unexpandable_phids = $this->mailUnexpandablePHIDs;
if (!is_array($unexpandable_phids)) {
$unexpandable_phids = array();
}
id(new PhabricatorFeedStoryPublisher())
->setStoryType($story_type)
->setStoryData($story_data)
->setStoryTime(time())
->setStoryAuthorPHID($this->getActingAsPHID())
->setRelatedPHIDs($related_phids)
->setPrimaryObjectPHID($object->getPHID())
->setSubscribedPHIDs($subscribed_phids)
->setUnexpandablePHIDs($unexpandable_phids)
->setMailRecipientPHIDs($mailed_phids)
->setMailTags($this->getMailTags($object, $xactions))
->publish();
}
/* -( Search Index )------------------------------------------------------- */
/**
* @task search
*/
protected function supportsSearch() {
return false;
}
/* -( Herald Integration )-------------------------------------------------- */
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return false;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
throw new Exception(pht('No herald adapter specified.'));
}
private function setHeraldAdapter(HeraldAdapter $adapter) {
$this->heraldAdapter = $adapter;
return $this;
}
protected function getHeraldAdapter() {
return $this->heraldAdapter;
}
private function setHeraldTranscript(HeraldTranscript $transcript) {
$this->heraldTranscript = $transcript;
return $this;
}
protected function getHeraldTranscript() {
return $this->heraldTranscript;
}
private function applyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
$adapter = $this->buildHeraldAdapter($object, $xactions)
->setContentSource($this->getContentSource())
->setIsNewObject($this->getIsNewObject())
->setActingAsPHID($this->getActingAsPHID())
->setAppliedTransactions($xactions);
if ($this->getApplicationEmail()) {
$adapter->setApplicationEmail($this->getApplicationEmail());
}
// If this editor is operating in silent mode, tell Herald that we aren't
// going to send any mail. This allows it to skip "the first time this
// rule matches, send me an email" rules which would otherwise match even
// though we aren't going to send any mail.
if ($this->getIsSilent()) {
$adapter->setForbiddenAction(
HeraldMailableState::STATECONST,
HeraldCoreStateReasons::REASON_SILENT);
}
$xscript = HeraldEngine::loadAndApplyRules($adapter);
$this->setHeraldAdapter($adapter);
$this->setHeraldTranscript($xscript);
if ($adapter instanceof HarbormasterBuildableAdapterInterface) {
$buildable_phid = $adapter->getHarbormasterBuildablePHID();
HarbormasterBuildable::applyBuildPlans(
$buildable_phid,
$adapter->getHarbormasterContainerPHID(),
$adapter->getQueuedHarbormasterBuildRequests());
// Whether we queued any builds or not, any automatic buildable for this
// object is now done preparing builds and can transition into a
// completed status.
$buildables = id(new HarbormasterBuildableQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withManualBuildables(false)
->withBuildablePHIDs(array($buildable_phid))
->execute();
foreach ($buildables as $buildable) {
// If this buildable has already moved beyond preparation, we don't
// need to nudge it again.
if (!$buildable->isPreparing()) {
continue;
}
$buildable->sendMessage(
$this->getActor(),
HarbormasterMessageType::BUILDABLE_BUILD,
true);
}
}
$this->mustEncrypt = $adapter->getMustEncryptReasons();
return array_merge(
$this->didApplyHeraldRules($object, $adapter, $xscript),
$adapter->getQueuedTransactions());
}
protected function didApplyHeraldRules(
PhabricatorLiskDAO $object,
HeraldAdapter $adapter,
HeraldTranscript $transcript) {
return array();
}
/* -( Custom Fields )------------------------------------------------------ */
/**
* @task customfield
*/
private function getCustomFieldForTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$field_key = $xaction->getMetadataValue('customfield:key');
if (!$field_key) {
throw new Exception(
pht(
"Custom field transaction has no '%s'!",
'customfield:key'));
}
$field = PhabricatorCustomField::getObjectField(
$object,
PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS,
$field_key);
if (!$field) {
throw new Exception(
pht(
"Custom field transaction has invalid '%s'; field '%s' ".
"is disabled or does not exist.",
'customfield:key',
$field_key));
}
if (!$field->shouldAppearInApplicationTransactions()) {
throw new Exception(
pht(
"Custom field transaction '%s' does not implement ".
"integration for %s.",
$field_key,
'ApplicationTransactions'));
}
$field->setViewer($this->getActor());
return $field;
}
/* -( Files )-------------------------------------------------------------- */
/**
* Extract the PHIDs of any files which these transactions attach.
*
* @task files
*/
private function extractFilePHIDs(
PhabricatorLiskDAO $object,
array $xactions) {
$changes = $this->getRemarkupChanges($xactions);
$blocks = mpull($changes, 'getNewValue');
$phids = array();
if ($blocks) {
$phids[] = PhabricatorMarkupEngine::extractFilePHIDsFromEmbeddedFiles(
$this->getActor(),
$blocks);
}
foreach ($xactions as $xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$phids[] = $xtype->extractFilePHIDs($object, $xaction->getNewValue());
} else {
$phids[] = $this->extractFilePHIDsFromCustomTransaction(
$object,
$xaction);
}
}
$phids = array_unique(array_filter(array_mergev($phids)));
if (!$phids) {
return array();
}
// Only let a user attach files they can actually see, since this would
// otherwise let you access any file by attaching it to an object you have
// view permission on.
$files = id(new PhabricatorFileQuery())
->setViewer($this->getActor())
->withPHIDs($phids)
->execute();
return mpull($files, 'getPHID');
}
/**
* @task files
*/
protected function extractFilePHIDsFromCustomTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
return array();
}
/**
* @task files
*/
private function attachFiles(
PhabricatorLiskDAO $object,
array $file_phids) {
if (!$file_phids) {
return;
}
$editor = new PhabricatorEdgeEditor();
$src = $object->getPHID();
$type = PhabricatorObjectHasFileEdgeType::EDGECONST;
foreach ($file_phids as $dst) {
$editor->addEdge($src, $type, $dst);
}
$editor->save();
}
private function applyInverseEdgeTransactions(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction,
$inverse_type) {
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$add = array_keys(array_diff_key($new, $old));
$rem = array_keys(array_diff_key($old, $new));
$add = array_fuse($add);
$rem = array_fuse($rem);
$all = $add + $rem;
$nodes = id(new PhabricatorObjectQuery())
->setViewer($this->requireActor())
->withPHIDs($all)
->execute();
+ $object_phid = $object->getPHID();
+
foreach ($nodes as $node) {
if (!($node instanceof PhabricatorApplicationTransactionInterface)) {
continue;
}
if ($node instanceof PhabricatorUser) {
// TODO: At least for now, don't record inverse edge transactions
// for users (for example, "alincoln joined project X"): Feed fills
// this role instead.
continue;
}
+ $node_phid = $node->getPHID();
$editor = $node->getApplicationTransactionEditor();
$template = $node->getApplicationTransactionTemplate();
- $target = $node->getApplicationTransactionObject();
- if (isset($add[$node->getPHID()])) {
- $edge_edit_type = '+';
+ // See T13082. We have to build these transactions with synthetic values
+ // because we've already applied the actual edit to the edge database
+ // table. If we try to apply this transaction naturally, it will no-op
+ // itself because it doesn't have any effect.
+
+ $edge_query = id(new PhabricatorEdgeQuery())
+ ->withSourcePHIDs(array($node_phid))
+ ->withEdgeTypes(array($inverse_type));
+
+ $edge_query->execute();
+
+ $edge_phids = $edge_query->getDestinationPHIDs();
+ $edge_phids = array_fuse($edge_phids);
+
+ $new_phids = $edge_phids;
+ $old_phids = $edge_phids;
+
+ if (isset($add[$node_phid])) {
+ unset($old_phids[$object_phid]);
} else {
- $edge_edit_type = '-';
+ $old_phids[$object_phid] = $object_phid;
}
$template
->setTransactionType($xaction->getTransactionType())
->setMetadataValue('edge:type', $inverse_type)
- ->setNewValue(
- array(
- $edge_edit_type => array($object->getPHID() => $object->getPHID()),
- ));
+ ->setOldValue($old_phids)
+ ->setNewValue($new_phids);
$editor
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setParentMessageID($this->getParentMessageID())
->setIsInverseEdgeEditor(true)
->setIsSilent($this->getIsSilent())
->setActor($this->requireActor())
->setActingAsPHID($this->getActingAsPHID())
->setContentSource($this->getContentSource());
- $editor->applyTransactions($target, array($template));
+ $editor->applyTransactions($node, array($template));
}
}
/* -( Workers )------------------------------------------------------------ */
/**
* Load any object state which is required to publish transactions.
*
* This hook is invoked in the main process before we compute data related
* to publishing transactions (like email "To" and "CC" lists), and again in
* the worker before publishing occurs.
*
* @return object Publishable object.
* @task workers
*/
protected function willPublish(PhabricatorLiskDAO $object, array $xactions) {
return $object;
}
/**
* Convert the editor state to a serializable dictionary which can be passed
* to a worker.
*
* This data will be loaded with @{method:loadWorkerState} in the worker.
*
* @return dict<string, wild> Serializable editor state.
* @task workers
*/
final private function getWorkerState() {
$state = array();
foreach ($this->getAutomaticStateProperties() as $property) {
$state[$property] = $this->$property;
}
$custom_state = $this->getCustomWorkerState();
$custom_encoding = $this->getCustomWorkerStateEncoding();
$state += array(
'excludeMailRecipientPHIDs' => $this->getExcludeMailRecipientPHIDs(),
'custom' => $this->encodeStateForStorage($custom_state, $custom_encoding),
'custom.encoding' => $custom_encoding,
);
return $state;
}
/**
* Hook; return custom properties which need to be passed to workers.
*
* @return dict<string, wild> Custom properties.
* @task workers
*/
protected function getCustomWorkerState() {
return array();
}
/**
* Hook; return storage encoding for custom properties which need to be
* passed to workers.
*
* This primarily allows binary data to be passed to workers and survive
* JSON encoding.
*
* @return dict<string, string> Property encodings.
* @task workers
*/
protected function getCustomWorkerStateEncoding() {
return array();
}
/**
* Load editor state using a dictionary emitted by @{method:getWorkerState}.
*
* This method is used to load state when running worker operations.
*
* @param dict<string, wild> Editor state, from @{method:getWorkerState}.
* @return this
* @task workers
*/
final public function loadWorkerState(array $state) {
foreach ($this->getAutomaticStateProperties() as $property) {
$this->$property = idx($state, $property);
}
$exclude = idx($state, 'excludeMailRecipientPHIDs', array());
$this->setExcludeMailRecipientPHIDs($exclude);
$custom_state = idx($state, 'custom', array());
$custom_encodings = idx($state, 'custom.encoding', array());
$custom = $this->decodeStateFromStorage($custom_state, $custom_encodings);
$this->loadCustomWorkerState($custom);
return $this;
}
/**
* Hook; set custom properties on the editor from data emitted by
* @{method:getCustomWorkerState}.
*
* @param dict<string, wild> Custom state,
* from @{method:getCustomWorkerState}.
* @return this
* @task workers
*/
protected function loadCustomWorkerState(array $state) {
return $this;
}
/**
* Get a list of object properties which should be automatically sent to
* workers in the state data.
*
* These properties will be automatically stored and loaded by the editor in
* the worker.
*
* @return list<string> List of properties.
* @task workers
*/
private function getAutomaticStateProperties() {
return array(
'parentMessageID',
'isNewObject',
'heraldEmailPHIDs',
'heraldForcedEmailPHIDs',
'heraldHeader',
'mailToPHIDs',
'mailCCPHIDs',
'feedNotifyPHIDs',
'feedRelatedPHIDs',
'feedShouldPublish',
'mailShouldSend',
'mustEncrypt',
'mailStamps',
'mailUnexpandablePHIDs',
'mailMutedPHIDs',
'webhookMap',
'silent',
'sendHistory',
);
}
/**
* Apply encodings prior to storage.
*
* See @{method:getCustomWorkerStateEncoding}.
*
* @param map<string, wild> Map of values to encode.
* @param map<string, string> Map of encodings to apply.
* @return map<string, wild> Map of encoded values.
* @task workers
*/
final private function encodeStateForStorage(
array $state,
array $encodings) {
foreach ($state as $key => $value) {
$encoding = idx($encodings, $key);
switch ($encoding) {
case self::STORAGE_ENCODING_BINARY:
// The mechanics of this encoding (serialize + base64) are a little
// awkward, but it allows us encode arrays and still be JSON-safe
// with binary data.
$value = @serialize($value);
if ($value === false) {
throw new Exception(
pht(
'Failed to serialize() value for key "%s".',
$key));
}
$value = base64_encode($value);
if ($value === false) {
throw new Exception(
pht(
'Failed to base64 encode value for key "%s".',
$key));
}
break;
}
$state[$key] = $value;
}
return $state;
}
/**
* Undo storage encoding applied when storing state.
*
* See @{method:getCustomWorkerStateEncoding}.
*
* @param map<string, wild> Map of encoded values.
* @param map<string, string> Map of encodings.
* @return map<string, wild> Map of decoded values.
* @task workers
*/
final private function decodeStateFromStorage(
array $state,
array $encodings) {
foreach ($state as $key => $value) {
$encoding = idx($encodings, $key);
switch ($encoding) {
case self::STORAGE_ENCODING_BINARY:
$value = base64_decode($value);
if ($value === false) {
throw new Exception(
pht(
'Failed to base64_decode() value for key "%s".',
$key));
}
$value = unserialize($value);
break;
}
$state[$key] = $value;
}
return $state;
}
/**
* Remove conflicts from a list of projects.
*
* Objects aren't allowed to be tagged with multiple milestones in the same
* group, nor projects such that one tag is the ancestor of any other tag.
* If the list of PHIDs include mutually exclusive projects, remove the
* conflicting projects.
*
* @param list<phid> List of project PHIDs.
* @return list<phid> List with conflicts removed.
*/
private function applyProjectConflictRules(array $phids) {
if (!$phids) {
return array();
}
// Overall, the last project in the list wins in cases of conflict (so when
// you add something, the thing you just added sticks and removes older
// values).
// Beyond that, there are two basic cases:
// Milestones: An object can't be in "A > Sprint 3" and "A > Sprint 4".
// If multiple projects are milestones of the same parent, we only keep the
// last one.
// Ancestor: You can't be in "A" and "A > B". If "A > B" comes later
// in the list, we remove "A" and keep "A > B". If "A" comes later, we
// remove "A > B" and keep "A".
// Note that it's OK to be in "A > B" and "A > C". There's only a conflict
// if one project is an ancestor of another. It's OK to have something
// tagged with multiple projects which share a common ancestor, so long as
// they are not mutual ancestors.
$viewer = PhabricatorUser::getOmnipotentUser();
$projects = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withPHIDs(array_keys($phids))
->execute();
$projects = mpull($projects, null, 'getPHID');
// We're going to build a map from each project with milestones to the last
// milestone in the list. This last milestone is the milestone we'll keep.
$milestone_map = array();
// We're going to build a set of the projects which have no descendants
// later in the list. This allows us to apply both ancestor rules.
$ancestor_map = array();
foreach ($phids as $phid => $ignored) {
$project = idx($projects, $phid);
if (!$project) {
continue;
}
// This is the last milestone we've seen, so set it as the selection for
// the project's parent. This might be setting a new value or overwriting
// an earlier value.
if ($project->isMilestone()) {
$parent_phid = $project->getParentProjectPHID();
$milestone_map[$parent_phid] = $phid;
}
// Since this is the last item in the list we've examined so far, add it
// to the set of projects with no later descendants.
$ancestor_map[$phid] = $phid;
// Remove any ancestors from the set, since this is a later descendant.
foreach ($project->getAncestorProjects() as $ancestor) {
$ancestor_phid = $ancestor->getPHID();
unset($ancestor_map[$ancestor_phid]);
}
}
// Now that we've built the maps, we can throw away all the projects which
// have conflicts.
foreach ($phids as $phid => $ignored) {
$project = idx($projects, $phid);
if (!$project) {
// If a PHID is invalid, we just leave it as-is. We could clean it up,
// but leaving it untouched is less likely to cause collateral damage.
continue;
}
// If this was a milestone, check if it was the last milestone from its
// group in the list. If not, remove it from the list.
if ($project->isMilestone()) {
$parent_phid = $project->getParentProjectPHID();
if ($milestone_map[$parent_phid] !== $phid) {
unset($phids[$phid]);
continue;
}
}
// If a later project in the list is a subproject of this one, it will
// have removed ancestors from the map. If this project does not point
// at itself in the ancestor map, it should be discarded in favor of a
// subproject that comes later.
if (idx($ancestor_map, $phid) !== $phid) {
unset($phids[$phid]);
continue;
}
// If a later project in the list is an ancestor of this one, it will
// have added itself to the map. If any ancestor of this project points
// at itself in the map, this project should be discarded in favor of
// that later ancestor.
foreach ($project->getAncestorProjects() as $ancestor) {
$ancestor_phid = $ancestor->getPHID();
if (isset($ancestor_map[$ancestor_phid])) {
unset($phids[$phid]);
continue 2;
}
}
}
return $phids;
}
/**
* When the view policy for an object is changed, scramble the secret keys
* for attached files to invalidate existing URIs.
*/
private function scrambleFileSecrets($object) {
// If this is a newly created object, we don't need to scramble anything
// since it couldn't have been previously published.
if ($this->getIsNewObject()) {
return;
}
// If the object is a file itself, scramble it.
if ($object instanceof PhabricatorFile) {
if ($this->shouldScramblePolicy($object->getViewPolicy())) {
$object->scrambleSecret();
$object->save();
}
}
$phid = $object->getPHID();
$attached_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$phid,
PhabricatorObjectHasFileEdgeType::EDGECONST);
if (!$attached_phids) {
return;
}
$omnipotent_viewer = PhabricatorUser::getOmnipotentUser();
$files = id(new PhabricatorFileQuery())
->setViewer($omnipotent_viewer)
->withPHIDs($attached_phids)
->execute();
foreach ($files as $file) {
$view_policy = $file->getViewPolicy();
if ($this->shouldScramblePolicy($view_policy)) {
$file->scrambleSecret();
$file->save();
}
}
}
/**
* Check if a policy is strong enough to justify scrambling. Objects which
* are set to very open policies don't need to scramble their files, and
* files with very open policies don't need to be scrambled when associated
* objects change.
*/
private function shouldScramblePolicy($policy) {
switch ($policy) {
case PhabricatorPolicies::POLICY_PUBLIC:
case PhabricatorPolicies::POLICY_USER:
return false;
}
return true;
}
private function updateWorkboardColumns($object, $const, $old, $new) {
// If an object is removed from a project, remove it from any proxy
// columns for that project. This allows a task which is moved up from a
// milestone to the parent to move back into the "Backlog" column on the
// parent workboard.
if ($const != PhabricatorProjectObjectHasProjectEdgeType::EDGECONST) {
return;
}
// TODO: This should likely be some future WorkboardInterface.
$appears_on_workboards = ($object instanceof ManiphestTask);
if (!$appears_on_workboards) {
return;
}
$removed_phids = array_keys(array_diff_key($old, $new));
if (!$removed_phids) {
return;
}
// Find any proxy columns for the removed projects.
$proxy_columns = id(new PhabricatorProjectColumnQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withProxyPHIDs($removed_phids)
->execute();
if (!$proxy_columns) {
return array();
}
$proxy_phids = mpull($proxy_columns, 'getPHID');
$position_table = new PhabricatorProjectColumnPosition();
$conn_w = $position_table->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE objectPHID = %s AND columnPHID IN (%Ls)',
$position_table->getTableName(),
$object->getPHID(),
$proxy_phids);
}
private function getModularTransactionTypes() {
if ($this->modularTypes === null) {
$template = $this->object->getApplicationTransactionTemplate();
if ($template instanceof PhabricatorModularTransaction) {
$xtypes = $template->newModularTransactionTypes();
foreach ($xtypes as $key => $xtype) {
$xtype = clone $xtype;
$xtype->setEditor($this);
$xtypes[$key] = $xtype;
}
} else {
$xtypes = array();
}
$this->modularTypes = $xtypes;
}
return $this->modularTypes;
}
private function getModularTransactionType($type) {
$types = $this->getModularTransactionTypes();
return idx($types, $type);
}
- private function willApplyTransactions($object, array $xactions) {
- foreach ($xactions as $xaction) {
- $type = $xaction->getTransactionType();
-
- $xtype = $this->getModularTransactionType($type);
- if (!$xtype) {
- continue;
- }
-
- $xtype->willApplyTransactions($object, $xactions);
- }
- }
-
public function getCreateObjectTitle($author, $object) {
return pht('%s created this object.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created an object: %s.', $author, $object);
}
/* -( Queue )-------------------------------------------------------------- */
protected function queueTransaction(
PhabricatorApplicationTransaction $xaction) {
$this->transactionQueue[] = $xaction;
return $this;
}
private function flushTransactionQueue($object) {
if (!$this->transactionQueue) {
return;
}
$xactions = $this->transactionQueue;
$this->transactionQueue = array();
$editor = $this->newQueueEditor();
return $editor->applyTransactions($object, $xactions);
}
private function newQueueEditor() {
$editor = id(newv(get_class($this), array()))
->setActor($this->getActor())
->setContentSource($this->getContentSource())
->setContinueOnNoEffect($this->getContinueOnNoEffect())
->setContinueOnMissingFields($this->getContinueOnMissingFields())
->setIsSilent($this->getIsSilent());
if ($this->actingAsPHID !== null) {
$editor->setActingAsPHID($this->actingAsPHID);
}
return $editor;
}
/* -( Stamps )------------------------------------------------------------- */
public function newMailStampTemplates($object) {
$actor = $this->getActor();
$templates = array();
$extensions = $this->newMailExtensions($object);
foreach ($extensions as $extension) {
$stamps = $extension->newMailStampTemplates($object);
foreach ($stamps as $stamp) {
$key = $stamp->getKey();
if (isset($templates[$key])) {
throw new Exception(
pht(
'Mail extension ("%s") defines a stamp template with the '.
'same key ("%s") as another template. Each stamp template '.
'must have a unique key.',
get_class($extension),
$key));
}
$stamp->setViewer($actor);
$templates[$key] = $stamp;
}
}
return $templates;
}
final public function getMailStamp($key) {
if (!isset($this->stampTemplates)) {
throw new PhutilInvalidStateException('newMailStampTemplates');
}
if (!isset($this->stampTemplates[$key])) {
throw new Exception(
pht(
'Editor ("%s") has no mail stamp template with provided key ("%s").',
get_class($this),
$key));
}
return $this->stampTemplates[$key];
}
private function newMailStamps($object, array $xactions) {
$actor = $this->getActor();
$this->stampTemplates = $this->newMailStampTemplates($object);
$extensions = $this->newMailExtensions($object);
$stamps = array();
foreach ($extensions as $extension) {
$extension->newMailStamps($object, $xactions);
}
return $this->stampTemplates;
}
private function newMailExtensions($object) {
$actor = $this->getActor();
$all_extensions = PhabricatorMailEngineExtension::getAllExtensions();
$extensions = array();
foreach ($all_extensions as $key => $template) {
$extension = id(clone $template)
->setViewer($actor)
->setEditor($this);
if ($extension->supportsObject($object)) {
$extensions[$key] = $extension;
}
}
return $extensions;
}
private function generateMailStamps($object, $data) {
if (!$data || !is_array($data)) {
return null;
}
$templates = $this->newMailStampTemplates($object);
foreach ($data as $spec) {
if (!is_array($spec)) {
continue;
}
$key = idx($spec, 'key');
if (!isset($templates[$key])) {
continue;
}
$type = idx($spec, 'type');
if ($templates[$key]->getStampType() !== $type) {
continue;
}
$value = idx($spec, 'value');
$templates[$key]->setValueFromDictionary($value);
}
$results = array();
foreach ($templates as $template) {
$value = $template->getValueForRendering();
$rendered = $template->renderStamps($value);
if ($rendered === null) {
continue;
}
$rendered = (array)$rendered;
foreach ($rendered as $stamp) {
$results[] = $stamp;
}
}
natcasesort($results);
return $results;
}
public function getRemovedRecipientPHIDs() {
return $this->mailRemovedPHIDs;
}
private function buildOldRecipientLists($object, $xactions) {
// See T4776. Before we start making any changes, build a list of the old
// recipients. If a change removes a user from the recipient list for an
// object we still want to notify the user about that change. This allows
// them to respond if they didn't want to be removed.
if (!$this->shouldSendMail($object, $xactions)) {
return;
}
$this->oldTo = $this->getMailTo($object);
$this->oldCC = $this->getMailCC($object);
return $this;
}
private function applyOldRecipientLists() {
$actor_phid = $this->getActingAsPHID();
// If you took yourself off the recipient list (for example, by
// unsubscribing or resigning) assume that you know what you did and
// don't need to be notified.
// If you just moved from "To" to "Cc" (or vice versa), you're still a
// recipient so we don't need to add you back in.
$map = array_fuse($this->mailToPHIDs) + array_fuse($this->mailCCPHIDs);
foreach ($this->oldTo as $phid) {
if ($phid === $actor_phid) {
continue;
}
if (isset($map[$phid])) {
continue;
}
$this->mailToPHIDs[] = $phid;
$this->mailRemovedPHIDs[] = $phid;
}
foreach ($this->oldCC as $phid) {
if ($phid === $actor_phid) {
continue;
}
if (isset($map[$phid])) {
continue;
}
$this->mailCCPHIDs[] = $phid;
$this->mailRemovedPHIDs[] = $phid;
}
return $this;
}
private function queueWebhooks($object, array $xactions) {
$hook_viewer = PhabricatorUser::getOmnipotentUser();
$webhook_map = $this->webhookMap;
if (!is_array($webhook_map)) {
$webhook_map = array();
}
// Add any "Firehose" hooks to the list of hooks we're going to call.
$firehose_hooks = id(new HeraldWebhookQuery())
->setViewer($hook_viewer)
->withStatuses(
array(
HeraldWebhook::HOOKSTATUS_FIREHOSE,
))
->execute();
foreach ($firehose_hooks as $firehose_hook) {
// This is "the hook itself is the reason this hook is being called",
// since we're including it because it's configured as a firehose
// hook.
$hook_phid = $firehose_hook->getPHID();
$webhook_map[$hook_phid][] = $hook_phid;
}
if (!$webhook_map) {
return;
}
// NOTE: We're going to queue calls to disabled webhooks, they'll just
// immediately fail in the worker queue. This makes the behavior more
// visible.
$call_hooks = id(new HeraldWebhookQuery())
->setViewer($hook_viewer)
->withPHIDs(array_keys($webhook_map))
->execute();
foreach ($call_hooks as $call_hook) {
$trigger_phids = idx($webhook_map, $call_hook->getPHID());
$request = HeraldWebhookRequest::initializeNewWebhookRequest($call_hook)
->setObjectPHID($object->getPHID())
->setTransactionPHIDs(mpull($xactions, 'getPHID'))
->setTriggerPHIDs($trigger_phids)
->setRetryMode(HeraldWebhookRequest::RETRY_FOREVER)
->setIsSilentAction((bool)$this->getIsSilent())
->setIsSecureAction((bool)$this->getMustEncrypt())
->save();
$request->queueCall();
}
}
private function hasWarnings($object, $xaction) {
// TODO: For the moment, this is a very un-modular hack to support
// exactly one type of warning (mentioning users on a draft revision)
// that we want to show. See PHI433.
if (!($object instanceof DifferentialRevision)) {
return false;
}
if (!$object->isDraft()) {
return false;
}
$type = $xaction->getTransactionType();
if ($type != PhabricatorTransactions::TYPE_SUBSCRIBERS) {
return false;
}
// NOTE: This will currently warn even if you're only removing
// subscribers.
return true;
}
private function buildHistoryMail(PhabricatorLiskDAO $object) {
$viewer = $this->requireActor();
$recipient_phid = $this->getActingAsPHID();
// Load every transaction so we can build a mail message with a complete
// history for the object.
$query = PhabricatorApplicationTransactionQuery::newQueryForObject($object);
$xactions = $query
->setViewer($viewer)
->withObjectPHIDs(array($object->getPHID()))
->execute();
$xactions = array_reverse($xactions);
$mail_messages = $this->buildMailWithRecipients(
$object,
$xactions,
array($recipient_phid),
array(),
array());
$mail = head($mail_messages);
// Since the user explicitly requested "!history", force delivery of this
// message regardless of their other mail settings.
$mail->setForceDelivery(true);
return $mail;
}
public function newAutomaticInlineTransactions(
PhabricatorLiskDAO $object,
array $inlines,
$transaction_type,
PhabricatorCursorPagedPolicyAwareQuery $query_template) {
$xactions = array();
foreach ($inlines as $inline) {
$xactions[] = $object->getApplicationTransactionTemplate()
->setTransactionType($transaction_type)
->attachComment($inline);
}
$state_xaction = $this->newInlineStateTransaction(
$object,
$query_template);
if ($state_xaction) {
$xactions[] = $state_xaction;
}
return $xactions;
}
protected function newInlineStateTransaction(
PhabricatorLiskDAO $object,
PhabricatorCursorPagedPolicyAwareQuery $query_template) {
$actor_phid = $this->getActingAsPHID();
$author_phid = $object->getAuthorPHID();
$actor_is_author = ($actor_phid == $author_phid);
$state_map = PhabricatorTransactions::getInlineStateMap();
$query = id(clone $query_template)
->setViewer($this->getActor())
->withFixedStates(array_keys($state_map));
$inlines = array();
$inlines[] = id(clone $query)
->withAuthorPHIDs(array($actor_phid))
->withHasTransaction(false)
->execute();
if ($actor_is_author) {
$inlines[] = id(clone $query)
->withHasTransaction(true)
->execute();
}
$inlines = array_mergev($inlines);
if (!$inlines) {
return null;
}
$old_value = mpull($inlines, 'getFixedState', 'getPHID');
$new_value = array();
foreach ($old_value as $key => $state) {
$new_value[$key] = $state_map[$state];
}
// See PHI995. Copy some information about the inlines into the transaction
// so we can tailor rendering behavior. In particular, we don't want to
// render transactions about users marking their own inlines as "Done".
$inline_details = array();
foreach ($inlines as $inline) {
$inline_details[$inline->getPHID()] = array(
'authorPHID' => $inline->getAuthorPHID(),
);
}
return $object->getApplicationTransactionTemplate()
->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE)
->setIgnoreOnNoEffect(true)
->setMetadataValue('inline.details', $inline_details)
->setOldValue($old_value)
->setNewValue($new_value);
}
+ private function requireMFA(PhabricatorLiskDAO $object, array $xactions) {
+ $actor = $this->getActor();
+
+ // Let omnipotent editors skip MFA. This is mostly aimed at scripts.
+ if ($actor->isOmnipotent()) {
+ return;
+ }
+
+ $editor_class = get_class($this);
+
+ $object_phid = $object->getPHID();
+ if ($object_phid) {
+ $workflow_key = sprintf(
+ 'editor(%s).phid(%s)',
+ $editor_class,
+ $object_phid);
+ } else {
+ $workflow_key = sprintf(
+ 'editor(%s).new()',
+ $editor_class);
+ }
+
+ $request = $this->getRequest();
+ if ($request === null) {
+ $source_type = $this->getContentSource()->getSourceTypeConstant();
+ $conduit_type = PhabricatorConduitContentSource::SOURCECONST;
+ $is_conduit = ($source_type === $conduit_type);
+ if ($is_conduit) {
+ throw new Exception(
+ pht(
+ 'This transaction group requires MFA to apply, but you can not '.
+ 'provide an MFA response via Conduit. Edit this object via the '.
+ 'web UI.'));
+ } else {
+ throw new Exception(
+ pht(
+ 'This transaction group requires MFA to apply, but the Editor was '.
+ 'not configured with a Request. This workflow can not perform an '.
+ 'MFA check.'));
+ }
+ }
+
+ $cancel_uri = $this->getCancelURI();
+ if ($cancel_uri === null) {
+ throw new Exception(
+ pht(
+ 'This transaction group requires MFA to apply, but the Editor was '.
+ 'not configured with a Cancel URI. This workflow can not perform '.
+ 'an MFA check.'));
+ }
+
+ id(new PhabricatorAuthSessionEngine())
+ ->setWorkflowKey($workflow_key)
+ ->requireHighSecurityToken($actor, $request, $cancel_uri);
+
+ foreach ($xactions as $xaction) {
+ $xaction->setIsMFATransaction(true);
+ }
+ }
+
+ private function newMFATransactions(
+ PhabricatorLiskDAO $object,
+ array $xactions) {
+
+ $has_engine = ($object instanceof PhabricatorEditEngineMFAInterface);
+ if ($has_engine) {
+ $engine = PhabricatorEditEngineMFAEngine::newEngineForObject($object)
+ ->setViewer($this->getActor());
+ $require_mfa = $engine->shouldRequireMFA();
+ $try_mfa = $engine->shouldTryMFA();
+ } else {
+ $require_mfa = false;
+ $try_mfa = false;
+ }
+
+ // If the user is mentioning an MFA object on another object or creating
+ // a relationship like "parent" or "child" to this object, we always
+ // allow the edit to move forward without requiring MFA.
+ if ($this->getIsInverseEdgeEditor()) {
+ return $xactions;
+ }
+
+ if (!$require_mfa) {
+ // If the object hasn't already opted into MFA, see if any of the
+ // transactions want it.
+ if (!$try_mfa) {
+ foreach ($xactions as $xaction) {
+ $type = $xaction->getTransactionType();
+
+ $xtype = $this->getModularTransactionType($type);
+ if ($xtype) {
+ $xtype = clone $xtype;
+ $xtype->setStorage($xaction);
+ if ($xtype->shouldTryMFA($object, $xaction)) {
+ $try_mfa = true;
+ break;
+ }
+ }
+ }
+ }
+
+ if ($try_mfa) {
+ $this->setShouldRequireMFA(true);
+ }
+
+ return $xactions;
+ }
+
+ $type_mfa = PhabricatorTransactions::TYPE_MFA;
+
+ $has_mfa = false;
+ foreach ($xactions as $xaction) {
+ if ($xaction->getTransactionType() === $type_mfa) {
+ $has_mfa = true;
+ break;
+ }
+ }
+
+ if ($has_mfa) {
+ return $xactions;
+ }
+
+ $template = $object->getApplicationTransactionTemplate();
+
+ $mfa_xaction = id(clone $template)
+ ->setTransactionType($type_mfa)
+ ->setNewValue(true);
+
+ array_unshift($xactions, $mfa_xaction);
+
+ return $xactions;
+ }
+
+ private function getTitleForTextMail(
+ PhabricatorApplicationTransaction $xaction) {
+ $type = $xaction->getTransactionType();
+
+ $xtype = $this->getModularTransactionType($type);
+ if ($xtype) {
+ $xtype = clone $xtype;
+ $xtype->setStorage($xaction);
+ $comment = $xtype->getTitleForTextMail();
+ if ($comment !== false) {
+ return $comment;
+ }
+ }
+
+ return $xaction->getTitleForTextMail();
+ }
+
+ private function getTitleForHTMLMail(
+ PhabricatorApplicationTransaction $xaction) {
+ $type = $xaction->getTransactionType();
+
+ $xtype = $this->getModularTransactionType($type);
+ if ($xtype) {
+ $xtype = clone $xtype;
+ $xtype->setStorage($xaction);
+ $comment = $xtype->getTitleForHTMLMail();
+ if ($comment !== false) {
+ return $comment;
+ }
+ }
+
+ return $xaction->getTitleForHTMLMail();
+ }
+
+
+ private function getBodyForTextMail(
+ PhabricatorApplicationTransaction $xaction) {
+ $type = $xaction->getTransactionType();
+
+ $xtype = $this->getModularTransactionType($type);
+ if ($xtype) {
+ $xtype = clone $xtype;
+ $xtype->setStorage($xaction);
+ $comment = $xtype->getBodyForTextMail();
+ if ($comment !== false) {
+ return $comment;
+ }
+ }
+
+ return $xaction->getBodyForMail();
+ }
+
+
+/* -( Extensions )--------------------------------------------------------- */
+
+
+ private function validateTransactionsWithExtensions(
+ PhabricatorLiskDAO $object,
+ array $xactions) {
+ $errors = array();
+
+ $extensions = $this->getEditorExtensions();
+ foreach ($extensions as $extension) {
+ $extension_errors = $extension
+ ->setObject($object)
+ ->validateTransactions($object, $xactions);
+
+ assert_instances_of(
+ $extension_errors,
+ 'PhabricatorApplicationTransactionValidationError');
+
+ $errors[] = $extension_errors;
+ }
+
+ return array_mergev($errors);
+ }
+
+ private function getEditorExtensions() {
+ if ($this->extensions === null) {
+ $this->extensions = $this->newEditorExtensions();
+ }
+ return $this->extensions;
+ }
+
+ private function newEditorExtensions() {
+ $extensions = PhabricatorEditorExtension::getAllExtensions();
+
+ $actor = $this->getActor();
+ $object = $this->object;
+ foreach ($extensions as $key => $extension) {
+
+ $extension = id(clone $extension)
+ ->setViewer($actor)
+ ->setEditor($this)
+ ->setObject($object);
+
+ if (!$extension->supportsObject($this, $object)) {
+ unset($extensions[$key]);
+ continue;
+ }
+
+ $extensions[$key] = $extension;
+ }
+
+ return $extensions;
+ }
+
+
}
diff --git a/src/applications/transactions/engine/PhabricatorStandardTimelineEngine.php b/src/applications/transactions/engine/PhabricatorStandardTimelineEngine.php
new file mode 100644
index 000000000..a9f6ea4ba
--- /dev/null
+++ b/src/applications/transactions/engine/PhabricatorStandardTimelineEngine.php
@@ -0,0 +1,4 @@
+<?php
+
+final class PhabricatorStandardTimelineEngine
+ extends PhabricatorTimelineEngine {}
diff --git a/src/applications/transactions/engine/PhabricatorTimelineEngine.php b/src/applications/transactions/engine/PhabricatorTimelineEngine.php
new file mode 100644
index 000000000..c6c6cd44a
--- /dev/null
+++ b/src/applications/transactions/engine/PhabricatorTimelineEngine.php
@@ -0,0 +1,95 @@
+<?php
+
+abstract class PhabricatorTimelineEngine
+ extends Phobject {
+
+ private $viewer;
+ private $object;
+ private $xactions;
+ private $viewData;
+
+ final public static function newForObject($object) {
+ if ($object instanceof PhabricatorTimelineInterface) {
+ $engine = $object->newTimelineEngine();
+ } else {
+ $engine = new PhabricatorStandardTimelineEngine();
+ }
+
+ $engine->setObject($object);
+
+ return $engine;
+ }
+
+ final public function setViewer(PhabricatorUser $viewer) {
+ $this->viewer = $viewer;
+ return $this;
+ }
+
+ final public function getViewer() {
+ return $this->viewer;
+ }
+
+ final public function setObject($object) {
+ $this->object = $object;
+ return $this;
+ }
+
+ final public function getObject() {
+ return $this->object;
+ }
+
+ final public function setTransactions(array $xactions) {
+ assert_instances_of($xactions, 'PhabricatorApplicationTransaction');
+ $this->xactions = $xactions;
+ return $this;
+ }
+
+ final public function getTransactions() {
+ return $this->xactions;
+ }
+
+ final public function setRequest(AphrontRequest $request) {
+ $this->request = $request;
+ return $this;
+ }
+
+ final public function getRequest() {
+ return $this->request;
+ }
+
+ final public function setViewData(array $view_data) {
+ $this->viewData = $view_data;
+ return $this;
+ }
+
+ final public function getViewData() {
+ return $this->viewData;
+ }
+
+ final public function buildTimelineView() {
+ $view = $this->newTimelineView();
+
+ if (!($view instanceof PhabricatorApplicationTransactionView)) {
+ throw new Exception(
+ pht(
+ 'Expected "newTimelineView()" to return an object of class "%s" '.
+ '(in engine "%s").',
+ 'PhabricatorApplicationTransactionView',
+ get_class($this)));
+ }
+
+ $viewer = $this->getViewer();
+ $object = $this->getObject();
+ $xactions = $this->getTransactions();
+
+ return $view
+ ->setViewer($viewer)
+ ->setObjectPHID($object->getPHID())
+ ->setTransactions($xactions);
+ }
+
+ protected function newTimelineView() {
+ return new PhabricatorApplicationTransactionView();
+ }
+
+}
diff --git a/src/applications/transactions/engineextension/PhabricatorEditorExtension.php b/src/applications/transactions/engineextension/PhabricatorEditorExtension.php
new file mode 100644
index 000000000..6ffac522c
--- /dev/null
+++ b/src/applications/transactions/engineextension/PhabricatorEditorExtension.php
@@ -0,0 +1,83 @@
+<?php
+
+abstract class PhabricatorEditorExtension
+ extends Phobject {
+
+ private $viewer;
+ private $editor;
+ private $object;
+
+ final public function getExtensionKey() {
+ return $this->getPhobjectClassConstant('EXTENSIONKEY');
+ }
+
+ final public function setEditor(
+ PhabricatorApplicationTransactionEditor $editor) {
+ $this->editor = $editor;
+ return $this;
+ }
+
+ final public function getEditor() {
+ return $this->editor;
+ }
+
+ final public function setViewer(PhabricatorUser $viewer) {
+ $this->viewer = $viewer;
+ return $this;
+ }
+
+ final public function getViewer() {
+ return $this->viewer;
+ }
+
+ final public function setObject(
+ PhabricatorApplicationTransactionInterface $object) {
+ $this->object = $object;
+ return $this;
+ }
+
+ final public static function getAllExtensions() {
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->setUniqueMethod('getExtensionKey')
+ ->execute();
+ }
+
+ abstract public function getExtensionName();
+
+ public function supportsObject(
+ PhabricatorApplicationTransactionEditor $editor,
+ PhabricatorApplicationTransactionInterface $object) {
+ return true;
+ }
+
+ public function validateTransactions($object, array $xactions) {
+ return array();
+ }
+
+ final protected function newTransactionError(
+ PhabricatorApplicationTransaction $xaction,
+ $title,
+ $message) {
+ return new PhabricatorApplicationTransactionValidationError(
+ $xaction->getTransactionType(),
+ $title,
+ $message,
+ $xaction);
+ }
+
+ final protected function newRequiredTransasctionError(
+ PhabricatorApplicationTransaction $xaction,
+ $message) {
+ return $this->newError($xaction, pht('Required'), $message)
+ ->setIsMissingFieldError(true);
+ }
+
+ final protected function newInvalidTransactionError(
+ PhabricatorApplicationTransaction $xaction,
+ $message) {
+ return $this->newTransactionError($xaction, pht('Invalid'), $message);
+ }
+
+
+}
diff --git a/src/applications/transactions/engineextension/PhabricatorEditorExtensionModule.php b/src/applications/transactions/engineextension/PhabricatorEditorExtensionModule.php
new file mode 100644
index 000000000..e34a0bb3a
--- /dev/null
+++ b/src/applications/transactions/engineextension/PhabricatorEditorExtensionModule.php
@@ -0,0 +1,40 @@
+<?php
+
+final class PhabricatorEditorExtensionModule
+ extends PhabricatorConfigModule {
+
+ public function getModuleKey() {
+ return 'editor';
+ }
+
+ public function getModuleName() {
+ return pht('Engine: Editor');
+ }
+
+ public function renderModuleStatus(AphrontRequest $request) {
+ $viewer = $request->getViewer();
+
+ $extensions = PhabricatorEditorExtension::getAllExtensions();
+
+ $rows = array();
+ foreach ($extensions as $extension) {
+ $rows[] = array(
+ get_class($extension),
+ $extension->getExtensionName(),
+ );
+ }
+
+ return id(new AphrontTableView($rows))
+ ->setHeaders(
+ array(
+ pht('Class'),
+ pht('Name'),
+ ))
+ ->setColumnClasses(
+ array(
+ null,
+ 'wide pri',
+ ));
+ }
+
+}
diff --git a/src/applications/transactions/interface/PhabricatorApplicationTransactionInterface.php b/src/applications/transactions/interface/PhabricatorApplicationTransactionInterface.php
index fdccf7b77..205253ba2 100644
--- a/src/applications/transactions/interface/PhabricatorApplicationTransactionInterface.php
+++ b/src/applications/transactions/interface/PhabricatorApplicationTransactionInterface.php
@@ -1,74 +1,43 @@
<?php
/**
* Allow infrastructure to apply transactions to the implementing object.
*
* For example, implementing this interface allows Subscriptions to apply CC
* transactions, and allows Harbormaster to apply build result notifications.
*/
interface PhabricatorApplicationTransactionInterface {
/**
* Return a @{class:PhabricatorApplicationTransactionEditor} which can be
* used to apply transactions to this object.
*
* @return PhabricatorApplicationTransactionEditor Editor for this object.
*/
public function getApplicationTransactionEditor();
- /**
- * Return the object to apply transactions to. Normally this is the current
- * object (that is, `$this`), but in some cases transactions may apply to
- * a different object: for example, @{class:DifferentialDiff} applies
- * transactions to the associated @{class:DifferentialRevision}.
- *
- * @return PhabricatorLiskDAO Object to apply transactions to.
- */
- public function getApplicationTransactionObject();
-
-
/**
* Return a template transaction for this object.
*
* @return PhabricatorApplicationTransaction
*/
public function getApplicationTransactionTemplate();
- /**
- * Hook to augment the $timeline with additional data for rendering.
- *
- * @return PhabricatorApplicationTransactionView
- */
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request);
-
}
// TEMPLATE IMPLEMENTATION /////////////////////////////////////////////////////
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
/*
public function getApplicationTransactionEditor() {
return new <<<???>>>Editor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new <<<???>>>Transaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
-
- return $timeline;
- }
-
*/
diff --git a/src/applications/transactions/interface/PhabricatorTimelineInterface.php b/src/applications/transactions/interface/PhabricatorTimelineInterface.php
new file mode 100644
index 000000000..2ec9fb210
--- /dev/null
+++ b/src/applications/transactions/interface/PhabricatorTimelineInterface.php
@@ -0,0 +1,7 @@
+<?php
+
+interface PhabricatorTimelineInterface {
+
+ public function newTimelineEngine();
+
+}
diff --git a/src/applications/transactions/replyhandler/PhabricatorApplicationTransactionReplyHandler.php b/src/applications/transactions/replyhandler/PhabricatorApplicationTransactionReplyHandler.php
index 545770895..9a369717f 100644
--- a/src/applications/transactions/replyhandler/PhabricatorApplicationTransactionReplyHandler.php
+++ b/src/applications/transactions/replyhandler/PhabricatorApplicationTransactionReplyHandler.php
@@ -1,173 +1,171 @@
<?php
abstract class PhabricatorApplicationTransactionReplyHandler
extends PhabricatorMailReplyHandler {
abstract public function getObjectPrefix();
public function getPrivateReplyHandlerEmailAddress(
PhabricatorUser $user) {
return $this->getDefaultPrivateReplyHandlerEmailAddress(
$user,
$this->getObjectPrefix());
}
public function getPublicReplyHandlerEmailAddress() {
return $this->getDefaultPublicReplyHandlerEmailAddress(
$this->getObjectPrefix());
}
private function newEditor(PhabricatorMetaMTAReceivedMail $mail) {
$content_source = $mail->newContentSource();
$editor = $this->getMailReceiver()
->getApplicationTransactionEditor()
->setActor($this->getActor())
->setContentSource($content_source)
->setContinueOnMissingFields(true)
->setParentMessageID($mail->getMessageID())
->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs());
if ($this->getApplicationEmail()) {
$editor->setApplicationEmail($this->getApplicationEmail());
}
return $editor;
}
protected function newTransaction() {
return $this->getMailReceiver()->getApplicationTransactionTemplate();
}
protected function didReceiveMail(
PhabricatorMetaMTAReceivedMail $mail,
$body) {
return array();
}
protected function shouldCreateCommentFromMailBody() {
return (bool)$this->getMailReceiver()->getID();
}
final protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
$viewer = $this->getActor();
$object = $this->getMailReceiver();
$app_email = $this->getApplicationEmail();
$is_new = !$object->getID();
// If this is a new object which implements the Spaces interface and was
// created by sending mail to an ApplicationEmail address, put the object
// in the same Space the address is in.
if ($is_new) {
if ($object instanceof PhabricatorSpacesInterface) {
if ($app_email) {
$space_phid = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID(
$app_email);
$object->setSpacePHID($space_phid);
}
}
}
$body_data = $mail->parseBody();
$body = $body_data['body'];
$body = $this->enhanceBodyWithAttachments($body, $mail->getAttachments());
$xactions = $this->didReceiveMail($mail, $body);
// If this object is subscribable, subscribe all the users who were
// recipients on the message.
if ($object instanceof PhabricatorSubscribableInterface) {
$subscriber_phids = $mail->loadAllRecipientPHIDs();
if ($subscriber_phids) {
$xactions[] = $this->newTransaction()
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setNewValue(
array(
'+' => $subscriber_phids,
));
}
}
$command_xactions = $this->processMailCommands(
$mail,
$body_data['commands']);
foreach ($command_xactions as $xaction) {
$xactions[] = $xaction;
}
if ($this->shouldCreateCommentFromMailBody()) {
$comment = $this
->newTransaction()
->getApplicationTransactionCommentObject()
->setContent($body);
$xactions[] = $this->newTransaction()
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment($comment);
}
- $target = $object->getApplicationTransactionObject();
-
$this->newEditor($mail)
->setContinueOnNoEffect(true)
- ->applyTransactions($target, $xactions);
+ ->applyTransactions($object, $xactions);
}
private function processMailCommands(
PhabricatorMetaMTAReceivedMail $mail,
array $command_list) {
$viewer = $this->getActor();
$object = $this->getMailReceiver();
$list = MetaMTAEmailTransactionCommand::getAllCommandsForObject($object);
$map = MetaMTAEmailTransactionCommand::getCommandMap($list);
$xactions = array();
foreach ($command_list as $command_argv) {
$command = head($command_argv);
$argv = array_slice($command_argv, 1);
$handler = idx($map, phutil_utf8_strtolower($command));
if ($handler) {
$results = $handler->buildTransactions(
$viewer,
$object,
$mail,
$command,
$argv);
foreach ($results as $result) {
$xactions[] = $result;
}
} else {
$valid_commands = array();
foreach ($list as $valid_command) {
$aliases = $valid_command->getCommandAliases();
if ($aliases) {
foreach ($aliases as $key => $alias) {
$aliases[$key] = '!'.$alias;
}
$aliases = implode(', ', $aliases);
$valid_commands[] = pht(
'!%s (or %s)',
$valid_command->getCommand(),
$aliases);
} else {
$valid_commands[] = '!'.$valid_command->getCommand();
}
}
throw new Exception(
pht(
'The command "!%s" is not a supported mail command. Valid '.
'commands for this object are: %s.',
$command,
implode(', ', $valid_commands)));
}
}
return $xactions;
}
}
diff --git a/src/applications/transactions/response/PhabricatorApplicationTransactionResponse.php b/src/applications/transactions/response/PhabricatorApplicationTransactionResponse.php
index 43a2fbc1b..00adb3a38 100644
--- a/src/applications/transactions/response/PhabricatorApplicationTransactionResponse.php
+++ b/src/applications/transactions/response/PhabricatorApplicationTransactionResponse.php
@@ -1,104 +1,113 @@
<?php
final class PhabricatorApplicationTransactionResponse
extends AphrontProxyResponse {
private $viewer;
private $transactions;
private $isPreview;
- private $transactionView;
private $previewContent;
-
- public function setTransactionView($transaction_view) {
- $this->transactionView = $transaction_view;
- return $this;
- }
-
- public function getTransactionView() {
- return $this->transactionView;
- }
+ private $object;
+ private $viewData = array();
protected function buildProxy() {
return new AphrontAjaxResponse();
}
public function setTransactions($transactions) {
assert_instances_of($transactions, 'PhabricatorApplicationTransaction');
$this->transactions = $transactions;
return $this;
}
public function getTransactions() {
return $this->transactions;
}
+ public function setObject($object) {
+ $this->object = $object;
+ return $this;
+ }
+
+ public function getObject() {
+ return $this->object;
+ }
+
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setIsPreview($is_preview) {
$this->isPreview = $is_preview;
return $this;
}
public function setPreviewContent($preview_content) {
$this->previewContent = $preview_content;
return $this;
}
public function getPreviewContent() {
return $this->previewContent;
}
+ public function setViewData(array $view_data) {
+ $this->viewData = $view_data;
+ return $this;
+ }
+
+ public function getViewData() {
+ return $this->viewData;
+ }
+
public function reduceProxyResponse() {
- if ($this->transactionView) {
- $view = $this->transactionView;
- } else if ($this->getTransactions()) {
- $view = head($this->getTransactions())
- ->getApplicationTransactionViewObject();
- } else {
- $view = new PhabricatorApplicationTransactionView();
- }
+ $object = $this->getObject();
+ $viewer = $this->getViewer();
+ $xactions = $this->getTransactions();
+
+ $timeline_engine = PhabricatorTimelineEngine::newForObject($object)
+ ->setViewer($viewer)
+ ->setTransactions($xactions)
+ ->setViewData($this->viewData);
+
+ $view = $timeline_engine->buildTimelineView();
- $view
- ->setUser($this->getViewer())
- ->setTransactions($this->getTransactions())
- ->setIsPreview($this->isPreview);
+ $view->setIsPreview($this->isPreview);
if ($this->isPreview) {
$xactions = mpull($view->buildEvents(), 'render');
} else {
$xactions = mpull($view->buildEvents(), 'render', 'getTransactionPHID');
}
// Force whatever the underlying views built to render into HTML for
// the Javascript.
foreach ($xactions as $key => $xaction) {
$xactions[$key] = hsprintf('%s', $xaction);
}
$aural = phutil_tag(
'h3',
array(
'class' => 'aural-only',
),
pht('Comment Preview'));
$content = array(
'header' => hsprintf('%s', $aural),
'xactions' => $xactions,
'spacer' => PHUITimelineView::renderSpacer(),
'previewContent' => hsprintf('%s', $this->getPreviewContent()),
);
return $this->getProxy()->setContent($content);
}
}
diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php
index f38c56acb..6d047fc82 100644
--- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php
+++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php
@@ -1,1707 +1,1755 @@
<?php
abstract class PhabricatorApplicationTransaction
extends PhabricatorLiskDAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface {
const TARGET_TEXT = 'text';
const TARGET_HTML = 'html';
protected $phid;
protected $objectPHID;
protected $authorPHID;
protected $viewPolicy;
protected $editPolicy;
protected $commentPHID;
protected $commentVersion = 0;
protected $transactionType;
protected $oldValue;
protected $newValue;
protected $metadata = array();
protected $contentSource;
private $comment;
private $commentNotLoaded;
private $handles;
private $renderingTarget = self::TARGET_HTML;
private $transactionGroup = array();
private $viewer = self::ATTACHABLE;
private $object = self::ATTACHABLE;
private $oldValueHasBeenSet = false;
private $ignoreOnNoEffect;
/**
* Flag this transaction as a pure side-effect which should be ignored when
* applying transactions if it has no effect, even if transaction application
* would normally fail. This both provides users with better error messages
* and allows transactions to perform optional side effects.
*/
public function setIgnoreOnNoEffect($ignore) {
$this->ignoreOnNoEffect = $ignore;
return $this;
}
public function getIgnoreOnNoEffect() {
return $this->ignoreOnNoEffect;
}
public function shouldGenerateOldValue() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_TOKEN:
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
case PhabricatorTransactions::TYPE_INLINESTATE:
return false;
}
return true;
}
abstract public function getApplicationTransactionType();
private function getApplicationObjectTypeName() {
$types = PhabricatorPHIDType::getAllTypes();
$type = idx($types, $this->getApplicationTransactionType());
if ($type) {
return $type->getTypeName();
}
return pht('Object');
}
public function getApplicationTransactionCommentObject() {
throw new PhutilMethodNotImplementedException();
}
- public function getApplicationTransactionViewObject() {
- return new PhabricatorApplicationTransactionView();
- }
-
public function getMetadataValue($key, $default = null) {
return idx($this->metadata, $key, $default);
}
public function setMetadataValue($key, $value) {
$this->metadata[$key] = $value;
return $this;
}
public function generatePHID() {
$type = PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST;
$subtype = $this->getApplicationTransactionType();
return PhabricatorPHID::generateNewPHID($type, $subtype);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'oldValue' => self::SERIALIZATION_JSON,
'newValue' => self::SERIALIZATION_JSON,
'metadata' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'commentPHID' => 'phid?',
'commentVersion' => 'uint32',
'contentSource' => 'text',
'transactionType' => 'text32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_object' => array(
'columns' => array('objectPHID'),
),
),
) + 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 hasComment() {
return $this->getComment() && strlen($this->getComment()->getContent());
}
public function getComment() {
if ($this->commentNotLoaded) {
throw new Exception(pht('Comment for this transaction was not loaded.'));
}
return $this->comment;
}
public function setIsCreateTransaction($create) {
return $this->setMetadataValue('core.create', $create);
}
public function getIsCreateTransaction() {
return (bool)$this->getMetadataValue('core.create', false);
}
public function setIsDefaultTransaction($default) {
return $this->setMetadataValue('core.default', $default);
}
public function getIsDefaultTransaction() {
return (bool)$this->getMetadataValue('core.default', false);
}
public function setIsSilentTransaction($silent) {
return $this->setMetadataValue('core.silent', $silent);
}
public function getIsSilentTransaction() {
return (bool)$this->getMetadataValue('core.silent', false);
}
public function setIsMFATransaction($mfa) {
return $this->setMetadataValue('core.mfa', $mfa);
}
public function getIsMFATransaction() {
return (bool)$this->getMetadataValue('core.mfa', false);
}
public function attachComment(
PhabricatorApplicationTransactionComment $comment) {
$this->comment = $comment;
$this->commentNotLoaded = false;
return $this;
}
public function setCommentNotLoaded($not_loaded) {
$this->commentNotLoaded = $not_loaded;
return $this;
}
public function attachObject($object) {
$this->object = $object;
return $this;
}
public function getObject() {
return $this->assertAttached($this->object);
}
public function getRemarkupChanges() {
$changes = $this->newRemarkupChanges();
assert_instances_of($changes, 'PhabricatorTransactionRemarkupChange');
// Convert older-style remarkup blocks into newer-style remarkup changes.
// This builds changes that do not have the correct "old value", so rules
// that operate differently against edits (like @user mentions) won't work
// properly.
foreach ($this->getRemarkupBlocks() as $block) {
$changes[] = $this->newRemarkupChange()
->setOldValue(null)
->setNewValue($block);
}
$comment = $this->getComment();
if ($comment) {
if ($comment->hasOldComment()) {
$old_value = $comment->getOldComment()->getContent();
} else {
$old_value = null;
}
$new_value = $comment->getContent();
$changes[] = $this->newRemarkupChange()
->setOldValue($old_value)
->setNewValue($new_value);
}
return $changes;
}
protected function newRemarkupChanges() {
return array();
}
protected function newRemarkupChange() {
return id(new PhabricatorTransactionRemarkupChange())
->setTransaction($this);
}
/**
* @deprecated
*/
public function getRemarkupBlocks() {
$blocks = array();
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getTransactionCustomField();
if ($field) {
$custom_blocks = $field->getApplicationTransactionRemarkupBlocks(
$this);
foreach ($custom_blocks as $custom_block) {
$blocks[] = $custom_block;
}
}
break;
}
return $blocks;
}
public function setOldValue($value) {
$this->oldValueHasBeenSet = true;
$this->writeField('oldValue', $value);
return $this;
}
public function hasOldValue() {
return $this->oldValueHasBeenSet;
}
public function newChronologicalSortVector() {
return id(new PhutilSortVector())
->addInt((int)$this->getDateCreated())
->addInt((int)$this->getID());
}
/* -( Rendering )---------------------------------------------------------- */
public function setRenderingTarget($rendering_target) {
$this->renderingTarget = $rendering_target;
return $this;
}
public function getRenderingTarget() {
return $this->renderingTarget;
}
public function attachViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->assertAttached($this->viewer);
}
public function getRequiredHandlePHIDs() {
$phids = array();
$old = $this->getOldValue();
$new = $this->getNewValue();
$phids[] = array($this->getAuthorPHID());
$phids[] = array($this->getObjectPHID());
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getTransactionCustomField();
if ($field) {
$phids[] = $field->getApplicationTransactionRequiredHandlePHIDs(
$this);
}
break;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$phids[] = $old;
$phids[] = $new;
break;
case PhabricatorTransactions::TYPE_EDGE:
$record = PhabricatorEdgeChangeRecord::newFromTransaction($this);
$phids[] = $record->getChangedPHIDs();
break;
case PhabricatorTransactions::TYPE_COLUMNS:
foreach ($new as $move) {
$phids[] = array(
$move['columnPHID'],
$move['boardPHID'],
);
$phids[] = $move['fromColumnPHIDs'];
}
break;
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
if (!PhabricatorPolicyQuery::isSpecialPolicy($old)) {
$phids[] = array($old);
}
if (!PhabricatorPolicyQuery::isSpecialPolicy($new)) {
$phids[] = array($new);
}
break;
case PhabricatorTransactions::TYPE_SPACE:
if ($old) {
$phids[] = array($old);
}
if ($new) {
$phids[] = array($new);
}
break;
case PhabricatorTransactions::TYPE_TOKEN:
break;
}
if ($this->getComment()) {
$phids[] = array($this->getComment()->getAuthorPHID());
}
return array_mergev($phids);
}
public function setHandles(array $handles) {
$this->handles = $handles;
return $this;
}
public function getHandle($phid) {
if (empty($this->handles[$phid])) {
throw new Exception(
pht(
'Transaction ("%s", of type "%s") requires a handle ("%s") that it '.
'did not load.',
$this->getPHID(),
$this->getTransactionType(),
$phid));
}
return $this->handles[$phid];
}
public function getHandleIfExists($phid) {
return idx($this->handles, $phid);
}
public function getHandles() {
if ($this->handles === null) {
throw new Exception(
pht('Transaction requires handles and it did not load them.'));
}
return $this->handles;
}
public function renderHandleLink($phid) {
if ($this->renderingTarget == self::TARGET_HTML) {
return $this->getHandle($phid)->renderLink();
} else {
return $this->getHandle($phid)->getLinkName();
}
}
public function renderHandleList(array $phids) {
$links = array();
foreach ($phids as $phid) {
$links[] = $this->renderHandleLink($phid);
}
if ($this->renderingTarget == self::TARGET_HTML) {
return phutil_implode_html(', ', $links);
} else {
return implode(', ', $links);
}
}
private function renderSubscriberList(array $phids, $change_type) {
if ($this->getRenderingTarget() == self::TARGET_TEXT) {
return $this->renderHandleList($phids);
} else {
$handles = array_select_keys($this->getHandles(), $phids);
return id(new SubscriptionListStringBuilder())
->setHandles($handles)
->setObjectPHID($this->getPHID())
->buildTransactionString($change_type);
}
}
protected function renderPolicyName($phid, $state = 'old') {
$policy = PhabricatorPolicy::newFromPolicyAndHandle(
$phid,
$this->getHandleIfExists($phid));
if ($this->renderingTarget == self::TARGET_HTML) {
switch ($policy->getType()) {
case PhabricatorPolicyType::TYPE_CUSTOM:
$policy->setHref('/transactions/'.$state.'/'.$this->getPHID().'/');
$policy->setWorkflow(true);
break;
default:
break;
}
$output = $policy->renderDescription();
} else {
$output = hsprintf('%s', $policy->getFullName());
}
return $output;
}
public function getIcon() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
$comment = $this->getComment();
if ($comment && $comment->getIsRemoved()) {
return 'fa-trash';
}
return 'fa-comment';
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$old = $this->getOldValue();
$new = $this->getNewValue();
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
if ($add && $rem) {
return 'fa-user';
} else if ($add) {
return 'fa-user-plus';
} else if ($rem) {
return 'fa-user-times';
} else {
return 'fa-user';
}
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
return 'fa-lock';
case PhabricatorTransactions::TYPE_EDGE:
switch ($this->getMetadataValue('edge:type')) {
case DiffusionCommitRevertedByCommitEdgeType::EDGECONST:
return 'fa-undo';
case DiffusionCommitRevertsCommitEdgeType::EDGECONST:
return 'fa-ambulance';
}
return 'fa-link';
case PhabricatorTransactions::TYPE_TOKEN:
return 'fa-trophy';
case PhabricatorTransactions::TYPE_SPACE:
return 'fa-th-large';
case PhabricatorTransactions::TYPE_COLUMNS:
return 'fa-columns';
+ case PhabricatorTransactions::TYPE_MFA:
+ return 'fa-vcard';
}
return 'fa-pencil';
}
public function getToken() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_TOKEN:
$old = $this->getOldValue();
$new = $this->getNewValue();
if ($new) {
$icon = substr($new, 10);
} else {
$icon = substr($old, 10);
}
return array($icon, !$this->getNewValue());
}
return array(null, null);
}
public function getColor() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT;
$comment = $this->getComment();
if ($comment && $comment->getIsRemoved()) {
return 'black';
}
break;
case PhabricatorTransactions::TYPE_EDGE:
switch ($this->getMetadataValue('edge:type')) {
case DiffusionCommitRevertedByCommitEdgeType::EDGECONST:
return 'pink';
case DiffusionCommitRevertsCommitEdgeType::EDGECONST:
return 'sky';
}
break;
+ case PhabricatorTransactions::TYPE_MFA;
+ return 'pink';
}
return null;
}
protected function getTransactionCustomField() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$key = $this->getMetadataValue('customfield:key');
if (!$key) {
return null;
}
$object = $this->getObject();
if (!($object instanceof PhabricatorCustomFieldInterface)) {
return null;
}
$field = PhabricatorCustomField::getObjectField(
$object,
PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS,
$key);
if (!$field) {
return null;
}
$field->setViewer($this->getViewer());
return $field;
}
return null;
}
public function shouldHide() {
// Never hide comments.
if ($this->hasComment()) {
return false;
}
$xaction_type = $this->getTransactionType();
// Always hide requests for object history.
if ($xaction_type === PhabricatorTransactions::TYPE_HISTORY) {
return true;
}
// Hide creation transactions if the old value is empty. These are
// transactions like "alice set the task title to: ...", which are
// essentially never interesting.
if ($this->getIsCreateTransaction()) {
switch ($xaction_type) {
case PhabricatorTransactions::TYPE_CREATE:
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
case PhabricatorTransactions::TYPE_SPACE:
break;
case PhabricatorTransactions::TYPE_SUBTYPE:
return true;
default:
$old = $this->getOldValue();
if (is_array($old) && !$old) {
return true;
}
if (!is_array($old)) {
if (!strlen($old)) {
return true;
}
// The integer 0 is also uninteresting by default; this is often
// an "off" flag for something like "All Day Event".
if ($old === 0) {
return true;
}
}
break;
}
}
// Hide creation transactions setting values to defaults, even if
// the old value is not empty. For example, tasks may have a global
// default view policy of "All Users", but a particular form sets the
// policy to "Administrators". The transaction corresponding to this
// change is not interesting, since it is the default behavior of the
// form.
if ($this->getIsCreateTransaction()) {
if ($this->getIsDefaultTransaction()) {
return true;
}
}
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
case PhabricatorTransactions::TYPE_SPACE:
if ($this->getIsCreateTransaction()) {
break;
}
// TODO: Remove this eventually, this is handling old changes during
// object creation prior to the introduction of "create" and "default"
// transaction display flags.
// NOTE: We can also hit this case with Space transactions that later
// update a default space (`null`) to an explicit space, so handling
// the Space case may require some finesse.
if ($this->getOldValue() === null) {
return true;
} else {
return false;
}
break;
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getTransactionCustomField();
if ($field) {
return $field->shouldHideInApplicationTransactions($this);
}
break;
case PhabricatorTransactions::TYPE_COLUMNS:
return !$this->getInterestingMoves($this->getNewValue());
case PhabricatorTransactions::TYPE_EDGE:
$edge_type = $this->getMetadataValue('edge:type');
switch ($edge_type) {
case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
case ManiphestTaskHasDuplicateTaskEdgeType::EDGECONST:
case ManiphestTaskIsDuplicateOfTaskEdgeType::EDGECONST:
case PhabricatorMutedEdgeType::EDGECONST:
case PhabricatorMutedByEdgeType::EDGECONST:
return true;
break;
case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST:
$record = PhabricatorEdgeChangeRecord::newFromTransaction($this);
$add = $record->getAddedPHIDs();
$add_value = reset($add);
$add_handle = $this->getHandle($add_value);
if ($add_handle->getPolicyFiltered()) {
return true;
}
return false;
break;
default:
break;
}
break;
case PhabricatorTransactions::TYPE_INLINESTATE:
list($done, $undone) = $this->getInterestingInlineStateChangeCounts();
if (!$done && !$undone) {
return true;
}
break;
}
return false;
}
public function shouldHideForMail(array $xactions) {
if ($this->isSelfSubscription()) {
return true;
}
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_TOKEN:
return true;
case PhabricatorTransactions::TYPE_EDGE:
$edge_type = $this->getMetadataValue('edge:type');
switch ($edge_type) {
case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST:
return true;
case PhabricatorProjectObjectHasProjectEdgeType::EDGECONST:
// When an object is first created, we hide any corresponding
// project transactions in the web UI because you can just look at
// the UI element elsewhere on screen to see which projects it
// is tagged with. However, in mail there's no other way to get
// this information, and it has some amount of value to users, so
// we keep the transaction. See T10493.
return false;
default:
break;
}
break;
}
if ($this->isInlineCommentTransaction()) {
$inlines = array();
// If there's a normal comment, we don't need to publish the inline
// transaction, since the normal comment covers things.
foreach ($xactions as $xaction) {
if ($xaction->isInlineCommentTransaction()) {
$inlines[] = $xaction;
continue;
}
// We found a normal comment, so hide this inline transaction.
if ($xaction->hasComment()) {
return true;
}
}
// If there are several inline comments, only publish the first one.
if ($this !== head($inlines)) {
return true;
}
}
return $this->shouldHide();
}
public function shouldHideForFeed() {
if ($this->isSelfSubscription()) {
return true;
}
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_TOKEN:
+ case PhabricatorTransactions::TYPE_MFA:
return true;
- case PhabricatorTransactions::TYPE_EDGE:
+ case PhabricatorTransactions::TYPE_EDGE:
$edge_type = $this->getMetadataValue('edge:type');
switch ($edge_type) {
case PhabricatorObjectMentionsObjectEdgeType::EDGECONST:
case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST:
return true;
break;
default:
break;
}
break;
case PhabricatorTransactions::TYPE_INLINESTATE:
return true;
}
return $this->shouldHide();
}
public function shouldHideForNotifications() {
return $this->shouldHideForFeed();
}
+ private function getTitleForMailWithRenderingTarget($new_target) {
+ $old_target = $this->getRenderingTarget();
+ try {
+ $this->setRenderingTarget($new_target);
+ $result = $this->getTitleForMail();
+ } catch (Exception $ex) {
+ $this->setRenderingTarget($old_target);
+ throw $ex;
+ }
+ $this->setRenderingTarget($old_target);
+ return $result;
+ }
+
public function getTitleForMail() {
- return id(clone $this)->setRenderingTarget('text')->getTitle();
+ return $this->getTitle();
+ }
+
+ public function getTitleForTextMail() {
+ return $this->getTitleForMailWithRenderingTarget(self::TARGET_TEXT);
}
public function getTitleForHTMLMail() {
- $title = $this->getTitleForMail();
+ // TODO: For now, rendering this with TARGET_HTML generates links with
+ // bad targets ("/x/y/" instead of "https://dev.example.com/x/y/"). Throw
+ // a rug over the issue for the moment. See T12921.
+
+ $title = $this->getTitleForMailWithRenderingTarget(self::TARGET_TEXT);
if ($title === null) {
return null;
}
if ($this->hasChangeDetails()) {
$details_uri = $this->getChangeDetailsURI();
$details_uri = PhabricatorEnv::getProductionURI($details_uri);
$show_details = phutil_tag(
'a',
array(
'href' => $details_uri,
),
pht('(Show Details)'));
$title = array($title, ' ', $show_details);
}
return $title;
}
public function getChangeDetailsURI() {
return '/transactions/detail/'.$this->getPHID().'/';
}
public function getBodyForMail() {
if ($this->isInlineCommentTransaction()) {
// We don't return inline comment content as mail body content, because
// applications need to contextualize it (by adding line numbers, for
// example) in order for it to make sense.
return null;
}
$comment = $this->getComment();
if ($comment && strlen($comment->getContent())) {
return $comment->getContent();
}
return null;
}
public function getNoEffectDescription() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
return pht('You can not post an empty comment.');
case PhabricatorTransactions::TYPE_VIEW_POLICY:
return pht(
'This %s already has that view policy.',
$this->getApplicationObjectTypeName());
case PhabricatorTransactions::TYPE_EDIT_POLICY:
return pht(
'This %s already has that edit policy.',
$this->getApplicationObjectTypeName());
case PhabricatorTransactions::TYPE_JOIN_POLICY:
return pht(
'This %s already has that join policy.',
$this->getApplicationObjectTypeName());
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return pht(
'All users are already subscribed to this %s.',
$this->getApplicationObjectTypeName());
case PhabricatorTransactions::TYPE_SPACE:
return pht('This object is already in that space.');
case PhabricatorTransactions::TYPE_EDGE:
return pht('Edges already exist; transaction has no effect.');
case PhabricatorTransactions::TYPE_COLUMNS:
return pht(
'You have not moved this object to any columns it is not '.
'already in.');
+ case PhabricatorTransactions::TYPE_MFA:
+ return pht(
+ 'You can not sign a transaction group that has no other '.
+ 'effects.');
}
return pht(
'Transaction (of type "%s") has no effect.',
$this->getTransactionType());
}
public function getTitle() {
$author_phid = $this->getAuthorPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CREATE:
return pht(
'%s created this object.',
$this->renderHandleLink($author_phid));
case PhabricatorTransactions::TYPE_COMMENT:
return pht(
'%s added a comment.',
$this->renderHandleLink($author_phid));
case PhabricatorTransactions::TYPE_VIEW_POLICY:
if ($this->getIsCreateTransaction()) {
return pht(
'%s created this object with visibility "%s".',
$this->renderHandleLink($author_phid),
$this->renderPolicyName($new, 'new'));
} else {
return pht(
'%s changed the visibility from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$this->renderPolicyName($old, 'old'),
$this->renderPolicyName($new, 'new'));
}
case PhabricatorTransactions::TYPE_EDIT_POLICY:
if ($this->getIsCreateTransaction()) {
return pht(
'%s created this object with edit policy "%s".',
$this->renderHandleLink($author_phid),
$this->renderPolicyName($new, 'new'));
} else {
return pht(
'%s changed the edit policy from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$this->renderPolicyName($old, 'old'),
$this->renderPolicyName($new, 'new'));
}
case PhabricatorTransactions::TYPE_JOIN_POLICY:
if ($this->getIsCreateTransaction()) {
return pht(
'%s created this object with join policy "%s".',
$this->renderHandleLink($author_phid),
$this->renderPolicyName($new, 'new'));
} else {
return pht(
'%s changed the join policy from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$this->renderPolicyName($old, 'old'),
$this->renderPolicyName($new, 'new'));
}
case PhabricatorTransactions::TYPE_SPACE:
if ($this->getIsCreateTransaction()) {
return pht(
'%s created this object in space %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($new));
} else {
return pht(
'%s shifted this object from the %s space to the %s space.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($old),
$this->renderHandleLink($new));
}
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
if ($add && $rem) {
return pht(
'%s edited subscriber(s), added %d: %s; removed %d: %s.',
$this->renderHandleLink($author_phid),
count($add),
$this->renderSubscriberList($add, 'add'),
count($rem),
$this->renderSubscriberList($rem, 'rem'));
} else if ($add) {
return pht(
'%s added %d subscriber(s): %s.',
$this->renderHandleLink($author_phid),
count($add),
$this->renderSubscriberList($add, 'add'));
} else if ($rem) {
return pht(
'%s removed %d subscriber(s): %s.',
$this->renderHandleLink($author_phid),
count($rem),
$this->renderSubscriberList($rem, 'rem'));
} else {
// This is used when rendering previews, before the user actually
// selects any CCs.
return pht(
'%s updated subscribers...',
$this->renderHandleLink($author_phid));
}
break;
case PhabricatorTransactions::TYPE_EDGE:
$record = PhabricatorEdgeChangeRecord::newFromTransaction($this);
$add = $record->getAddedPHIDs();
$rem = $record->getRemovedPHIDs();
$type = $this->getMetadata('edge:type');
$type = head($type);
try {
$type_obj = PhabricatorEdgeType::getByConstant($type);
} catch (Exception $ex) {
// Recover somewhat gracefully from edge transactions which
// we don't have the classes for.
return pht(
'%s edited an edge.',
$this->renderHandleLink($author_phid));
}
if ($add && $rem) {
return $type_obj->getTransactionEditString(
$this->renderHandleLink($author_phid),
new PhutilNumber(count($add) + count($rem)),
phutil_count($add),
$this->renderHandleList($add),
phutil_count($rem),
$this->renderHandleList($rem));
} else if ($add) {
return $type_obj->getTransactionAddString(
$this->renderHandleLink($author_phid),
phutil_count($add),
$this->renderHandleList($add));
} else if ($rem) {
return $type_obj->getTransactionRemoveString(
$this->renderHandleLink($author_phid),
phutil_count($rem),
$this->renderHandleList($rem));
} else {
return $type_obj->getTransactionPreviewString(
$this->renderHandleLink($author_phid));
}
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getTransactionCustomField();
if ($field) {
return $field->getApplicationTransactionTitle($this);
} else {
$developer_mode = 'phabricator.developer-mode';
$is_developer = PhabricatorEnv::getEnvConfig($developer_mode);
if ($is_developer) {
return pht(
'%s edited a custom field (with key "%s").',
$this->renderHandleLink($author_phid),
$this->getMetadata('customfield:key'));
} else {
return pht(
'%s edited a custom field.',
$this->renderHandleLink($author_phid));
}
}
case PhabricatorTransactions::TYPE_TOKEN:
if ($old && $new) {
return pht(
'%s updated a token.',
$this->renderHandleLink($author_phid));
} else if ($old) {
return pht(
'%s rescinded a token.',
$this->renderHandleLink($author_phid));
} else {
return pht(
'%s awarded a token.',
$this->renderHandleLink($author_phid));
}
case PhabricatorTransactions::TYPE_INLINESTATE:
list($done, $undone) = $this->getInterestingInlineStateChangeCounts();
if ($done && $undone) {
return pht(
'%s marked %s inline comment(s) as done and %s inline comment(s) '.
'as not done.',
$this->renderHandleLink($author_phid),
new PhutilNumber($done),
new PhutilNumber($undone));
} else if ($done) {
return pht(
'%s marked %s inline comment(s) as done.',
$this->renderHandleLink($author_phid),
new PhutilNumber($done));
} else {
return pht(
'%s marked %s inline comment(s) as not done.',
$this->renderHandleLink($author_phid),
new PhutilNumber($undone));
}
break;
case PhabricatorTransactions::TYPE_COLUMNS:
$moves = $this->getInterestingMoves($new);
if (count($moves) == 1) {
$move = head($moves);
$from_columns = $move['fromColumnPHIDs'];
$to_column = $move['columnPHID'];
$board_phid = $move['boardPHID'];
if (count($from_columns) == 1) {
return pht(
'%s moved this task from %s to %s on the %s board.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink(head($from_columns)),
$this->renderHandleLink($to_column),
$this->renderHandleLink($board_phid));
} else {
return pht(
'%s moved this task to %s on the %s board.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($to_column),
$this->renderHandleLink($board_phid));
}
} else {
$fragments = array();
foreach ($moves as $move) {
$fragments[] = pht(
'%s (%s)',
$this->renderHandleLink($board_phid),
$this->renderHandleLink($to_column));
}
return pht(
'%s moved this task on %s board(s): %s.',
$this->renderHandleLink($author_phid),
phutil_count($moves),
phutil_implode_html(', ', $fragments));
}
break;
+
+ case PhabricatorTransactions::TYPE_MFA:
+ return pht(
+ '%s signed these changes with MFA.',
+ $this->renderHandleLink($author_phid));
+
default:
// In developer mode, provide a better hint here about which string
// we're missing.
$developer_mode = 'phabricator.developer-mode';
$is_developer = PhabricatorEnv::getEnvConfig($developer_mode);
if ($is_developer) {
return pht(
'%s edited this object (transaction type "%s").',
$this->renderHandleLink($author_phid),
$this->getTransactionType());
} else {
return pht(
'%s edited this %s.',
$this->renderHandleLink($author_phid),
$this->getApplicationObjectTypeName());
}
}
}
public function getTitleForFeed() {
$author_phid = $this->getAuthorPHID();
$object_phid = $this->getObjectPHID();
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CREATE:
return pht(
'%s created %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case PhabricatorTransactions::TYPE_COMMENT:
return pht(
'%s added a comment to %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case PhabricatorTransactions::TYPE_VIEW_POLICY:
return pht(
'%s changed the visibility for %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case PhabricatorTransactions::TYPE_EDIT_POLICY:
return pht(
'%s changed the edit policy for %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case PhabricatorTransactions::TYPE_JOIN_POLICY:
return pht(
'%s changed the join policy for %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return pht(
'%s updated subscribers of %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
case PhabricatorTransactions::TYPE_SPACE:
if ($this->getIsCreateTransaction()) {
return pht(
'%s created %s in the %s space.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$this->renderHandleLink($new));
} else {
return pht(
'%s shifted %s from the %s space to the %s space.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$this->renderHandleLink($old),
$this->renderHandleLink($new));
}
case PhabricatorTransactions::TYPE_EDGE:
$record = PhabricatorEdgeChangeRecord::newFromTransaction($this);
$add = $record->getAddedPHIDs();
$rem = $record->getRemovedPHIDs();
$type = $this->getMetadata('edge:type');
$type = head($type);
$type_obj = PhabricatorEdgeType::getByConstant($type);
if ($add && $rem) {
return $type_obj->getFeedEditString(
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
new PhutilNumber(count($add) + count($rem)),
phutil_count($add),
$this->renderHandleList($add),
phutil_count($rem),
$this->renderHandleList($rem));
} else if ($add) {
return $type_obj->getFeedAddString(
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
phutil_count($add),
$this->renderHandleList($add));
} else if ($rem) {
return $type_obj->getFeedRemoveString(
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
phutil_count($rem),
$this->renderHandleList($rem));
} else {
return pht(
'%s edited edge metadata for %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
}
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getTransactionCustomField();
if ($field) {
return $field->getApplicationTransactionTitleForFeed($this);
} else {
return pht(
'%s edited a custom field on %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid));
}
case PhabricatorTransactions::TYPE_COLUMNS:
$moves = $this->getInterestingMoves($new);
if (count($moves) == 1) {
$move = head($moves);
$from_columns = $move['fromColumnPHIDs'];
$to_column = $move['columnPHID'];
$board_phid = $move['boardPHID'];
if (count($from_columns) == 1) {
return pht(
'%s moved %s from %s to %s on the %s board.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$this->renderHandleLink(head($from_columns)),
$this->renderHandleLink($to_column),
$this->renderHandleLink($board_phid));
} else {
return pht(
'%s moved %s to %s on the %s board.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
$this->renderHandleLink($to_column),
$this->renderHandleLink($board_phid));
}
} else {
$fragments = array();
foreach ($moves as $move) {
$fragments[] = pht(
'%s (%s)',
$this->renderHandleLink($board_phid),
$this->renderHandleLink($to_column));
}
return pht(
'%s moved %s on %s board(s): %s.',
$this->renderHandleLink($author_phid),
$this->renderHandleLink($object_phid),
phutil_count($moves),
phutil_implode_html(', ', $fragments));
}
break;
+ case PhabricatorTransactions::TYPE_MFA:
+ return null;
+
}
return $this->getTitle();
}
public function getMarkupFieldsForFeed(PhabricatorFeedStory $story) {
$fields = array();
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
$text = $this->getComment()->getContent();
if (strlen($text)) {
$fields[] = 'comment/'.$this->getID();
}
break;
}
return $fields;
}
public function getMarkupTextForFeed(PhabricatorFeedStory $story, $field) {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
$text = $this->getComment()->getContent();
return PhabricatorMarkupEngine::summarize($text);
}
return null;
}
public function getBodyForFeed(PhabricatorFeedStory $story) {
$remarkup = $this->getRemarkupBodyForFeed($story);
if ($remarkup !== null) {
$remarkup = PhabricatorMarkupEngine::summarize($remarkup);
return new PHUIRemarkupView($this->viewer, $remarkup);
}
$old = $this->getOldValue();
$new = $this->getNewValue();
$body = null;
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
$text = $this->getComment()->getContent();
if (strlen($text)) {
$body = $story->getMarkupFieldOutput('comment/'.$this->getID());
}
break;
}
return $body;
}
public function getRemarkupBodyForFeed(PhabricatorFeedStory $story) {
return null;
}
public function getActionStrength() {
if ($this->isInlineCommentTransaction()) {
return 0.25;
}
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
return 0.5;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
if ($this->isSelfSubscription()) {
// Make this weaker than TYPE_COMMENT.
return 0.25;
}
if ($this->isApplicationAuthor()) {
// When applications (most often: Herald) change subscriptions it
// is very uninteresting.
return 0.000000001;
}
// In other cases, subscriptions are more interesting than comments
// (which are shown anyway) but less interesting than any other type of
// transaction.
return 0.75;
+ case PhabricatorTransactions::TYPE_MFA:
+ // We want MFA signatures to render at the top of transaction groups,
+ // on top of the things they signed.
+ return 10;
}
return 1.0;
}
public function isCommentTransaction() {
if ($this->hasComment()) {
return true;
}
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
return true;
}
return false;
}
public function isInlineCommentTransaction() {
return false;
}
public function getActionName() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_COMMENT:
return pht('Commented On');
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
return pht('Changed Policy');
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
return pht('Changed Subscribers');
default:
return pht('Updated');
}
}
public function getMailTags() {
return array();
}
public function hasChangeDetails() {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getTransactionCustomField();
if ($field) {
return $field->getApplicationTransactionHasChangeDetails($this);
}
break;
}
return false;
}
public function hasChangeDetailsForMail() {
return $this->hasChangeDetails();
}
public function renderChangeDetailsForMail(PhabricatorUser $viewer) {
$view = $this->renderChangeDetails($viewer);
if ($view instanceof PhabricatorApplicationTransactionTextDiffDetailView) {
return $view->renderForMail();
}
return null;
}
public function renderChangeDetails(PhabricatorUser $viewer) {
switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_CUSTOMFIELD:
$field = $this->getTransactionCustomField();
if ($field) {
return $field->getApplicationTransactionChangeDetails($this, $viewer);
}
break;
}
return $this->renderTextCorpusChangeDetails(
$viewer,
$this->getOldValue(),
$this->getNewValue());
}
public function renderTextCorpusChangeDetails(
PhabricatorUser $viewer,
$old,
$new) {
return id(new PhabricatorApplicationTransactionTextDiffDetailView())
->setUser($viewer)
->setOldText($old)
->setNewText($new);
}
public function attachTransactionGroup(array $group) {
assert_instances_of($group, __CLASS__);
$this->transactionGroup = $group;
return $this;
}
public function getTransactionGroup() {
return $this->transactionGroup;
}
/**
* Should this transaction be visually grouped with an existing transaction
* group?
*
* @param list<PhabricatorApplicationTransaction> List of transactions.
* @return bool True to display in a group with the other transactions.
*/
public function shouldDisplayGroupWith(array $group) {
$this_source = null;
if ($this->getContentSource()) {
$this_source = $this->getContentSource()->getSource();
}
+ $type_mfa = PhabricatorTransactions::TYPE_MFA;
+
foreach ($group as $xaction) {
// Don't group transactions by different authors.
if ($xaction->getAuthorPHID() != $this->getAuthorPHID()) {
return false;
}
// Don't group transactions for different objects.
if ($xaction->getObjectPHID() != $this->getObjectPHID()) {
return false;
}
// Don't group anything into a group which already has a comment.
if ($xaction->isCommentTransaction()) {
return false;
}
// Don't group transactions from different content sources.
$other_source = null;
if ($xaction->getContentSource()) {
$other_source = $xaction->getContentSource()->getSource();
}
if ($other_source != $this_source) {
return false;
}
// Don't group transactions which happened more than 2 minutes apart.
$apart = abs($xaction->getDateCreated() - $this->getDateCreated());
if ($apart > (60 * 2)) {
return false;
}
// Don't group silent and nonsilent transactions together.
$is_silent = $this->getIsSilentTransaction();
if ($is_silent != $xaction->getIsSilentTransaction()) {
return false;
}
// Don't group MFA and non-MFA transactions together.
$is_mfa = $this->getIsMFATransaction();
if ($is_mfa != $xaction->getIsMFATransaction()) {
return false;
}
+
+ // Don't group two "Sign with MFA" transactions together.
+ if ($this->getTransactionType() === $type_mfa) {
+ if ($xaction->getTransactionType() === $type_mfa) {
+ return false;
+ }
+ }
}
return true;
}
public function renderExtraInformationLink() {
$herald_xscript_id = $this->getMetadataValue('herald:transcriptID');
if ($herald_xscript_id) {
return phutil_tag(
'a',
array(
'href' => '/herald/transcript/'.$herald_xscript_id.'/',
),
pht('View Herald Transcript'));
}
return null;
}
public function renderAsTextForDoorkeeper(
DoorkeeperFeedStoryPublisher $publisher,
PhabricatorFeedStory $story,
array $xactions) {
$text = array();
$body = array();
foreach ($xactions as $xaction) {
$xaction_body = $xaction->getBodyForMail();
if ($xaction_body !== null) {
$body[] = $xaction_body;
}
if ($xaction->shouldHideForMail($xactions)) {
continue;
}
$old_target = $xaction->getRenderingTarget();
$new_target = self::TARGET_TEXT;
$xaction->setRenderingTarget($new_target);
if ($publisher->getRenderWithImpliedContext()) {
$text[] = $xaction->getTitle();
} else {
$text[] = $xaction->getTitleForFeed();
}
$xaction->setRenderingTarget($old_target);
}
$text = implode("\n", $text);
$body = implode("\n\n", $body);
return rtrim($text."\n\n".$body);
}
/**
* Test if this transaction is just a user subscribing or unsubscribing
* themselves.
*/
private function isSelfSubscription() {
$type = $this->getTransactionType();
if ($type != PhabricatorTransactions::TYPE_SUBSCRIBERS) {
return false;
}
$old = $this->getOldValue();
$new = $this->getNewValue();
$add = array_diff($old, $new);
$rem = array_diff($new, $old);
if ((count($add) + count($rem)) != 1) {
// More than one user affected.
return false;
}
$affected_phid = head(array_merge($add, $rem));
if ($affected_phid != $this->getAuthorPHID()) {
// Affected user is someone else.
return false;
}
return true;
}
private function isApplicationAuthor() {
$author_phid = $this->getAuthorPHID();
$author_type = phid_get_type($author_phid);
$application_type = PhabricatorApplicationApplicationPHIDType::TYPECONST;
return ($author_type == $application_type);
}
private function getInterestingMoves(array $moves) {
// Remove moves which only shift the position of a task within a column.
foreach ($moves as $key => $move) {
$from_phids = array_fuse($move['fromColumnPHIDs']);
if (isset($from_phids[$move['columnPHID']])) {
unset($moves[$key]);
}
}
return $moves;
}
private function getInterestingInlineStateChangeCounts() {
// See PHI995. Newer inline state transactions have additional details
// which we use to tailor the rendering behavior. These details are not
// present on older transactions.
$details = $this->getMetadataValue('inline.details', array());
$new = $this->getNewValue();
$done = 0;
$undone = 0;
foreach ($new as $phid => $state) {
$is_done = ($state == PhabricatorInlineCommentInterface::STATE_DONE);
// See PHI995. If you're marking your own inline comments as "Done",
// don't count them when rendering a timeline story. In the case where
// you're only affecting your own comments, this will hide the
// "alice marked X comments as done" story entirely.
// Usually, this happens when you pre-mark inlines as "done" and submit
// them yourself. We'll still generate an "alice added inline comments"
// story (in most cases/contexts), but the state change story is largely
// just clutter and slightly confusing/misleading.
$inline_details = idx($details, $phid, array());
$inline_author_phid = idx($inline_details, 'authorPHID');
if ($inline_author_phid) {
if ($inline_author_phid == $this->getAuthorPHID()) {
if ($is_done) {
continue;
}
}
}
if ($is_done) {
$done++;
} else {
$undone++;
}
}
return array($done, $undone);
}
/* -( 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 $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return ($viewer->getPHID() == $this->getAuthorPHID());
}
public function describeAutomaticCapability($capability) {
return pht(
'Transactions are visible to users that can see the object which was '.
'acted upon. Some transactions - in particular, comments - are '.
'editable by the transaction author.');
}
public function getModularType() {
return null;
}
public function setForceNotifyPHIDs(array $phids) {
$this->setMetadataValue('notify.force', $phids);
return $this;
}
public function getForceNotifyPHIDs() {
return $this->getMetadataValue('notify.force', array());
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$comment_template = null;
try {
$comment_template = $this->getApplicationTransactionCommentObject();
} catch (Exception $ex) {
// Continue; no comments for these transactions.
}
if ($comment_template) {
$comments = $comment_template->loadAllWhere(
'transactionPHID = %s',
$this->getPHID());
foreach ($comments as $comment) {
$engine->destroyObject($comment);
}
}
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php b/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php
index 3a1c8ec60..6c9f3a50a 100644
--- a/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php
+++ b/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php
@@ -1,356 +1,346 @@
<?php
final class PhabricatorEditEngineConfiguration
extends PhabricatorSearchDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface {
protected $engineKey;
protected $builtinKey;
protected $name;
protected $viewPolicy;
protected $properties = array();
protected $isDisabled = 0;
protected $isDefault = 0;
protected $isEdit = 0;
protected $createOrder = 0;
protected $editOrder = 0;
protected $subtype;
private $engine = self::ATTACHABLE;
const LOCK_VISIBLE = 'visible';
const LOCK_LOCKED = 'locked';
const LOCK_HIDDEN = 'hidden';
public function getTableName() {
return 'search_editengineconfiguration';
}
public static function initializeNewConfiguration(
PhabricatorUser $actor,
PhabricatorEditEngine $engine) {
return id(new PhabricatorEditEngineConfiguration())
->setSubtype(PhabricatorEditEngine::SUBTYPE_DEFAULT)
->setEngineKey($engine->getEngineKey())
->attachEngine($engine)
->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy());
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorEditEngineConfigurationPHIDType::TYPECONST);
}
public function getCreateSortKey() {
return $this->getSortKey($this->createOrder);
}
public function getEditSortKey() {
return $this->getSortKey($this->editOrder);
}
private function getSortKey($order) {
// Put objects at the bottom by default if they haven't previously been
// reordered. When they're explicitly reordered, the smallest sort key we
// assign is 1, so if the object has a value of 0 it means it hasn't been
// ordered yet.
if ($order != 0) {
$group = 'A';
} else {
$group = 'B';
}
return sprintf(
"%s%012d%s\0%012d",
$group,
$order,
$this->getName(),
$this->getID());
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'engineKey' => 'text64',
'builtinKey' => 'text64?',
'name' => 'text255',
'isDisabled' => 'bool',
'isDefault' => 'bool',
'isEdit' => 'bool',
'createOrder' => 'uint32',
'editOrder' => 'uint32',
'subtype' => 'text64',
),
self::CONFIG_KEY_SCHEMA => array(
'key_engine' => array(
'columns' => array('engineKey', 'builtinKey'),
'unique' => true,
),
'key_default' => array(
'columns' => array('engineKey', 'isDefault', 'isDisabled'),
),
'key_edit' => array(
'columns' => array('engineKey', 'isEdit', 'isDisabled'),
),
),
) + parent::getConfiguration();
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function setBuiltinKey($key) {
if (strpos($key, '/') !== false) {
throw new Exception(
pht('EditEngine BuiltinKey contains an invalid key character "/".'));
}
return parent::setBuiltinKey($key);
}
public function attachEngine(PhabricatorEditEngine $engine) {
$this->engine = $engine;
return $this;
}
public function getEngine() {
return $this->assertAttached($this->engine);
}
public function applyConfigurationToFields(
PhabricatorEditEngine $engine,
$object,
array $fields) {
$fields = mpull($fields, null, 'getKey');
$is_new = !$object->getID();
$values = $this->getProperty('defaults', array());
foreach ($fields as $key => $field) {
if (!$field->getIsFormField()) {
continue;
}
if (!$field->getIsDefaultable()) {
continue;
}
if ($is_new) {
if (array_key_exists($key, $values)) {
$field->readDefaultValueFromConfiguration($values[$key]);
}
}
}
$locks = $this->getFieldLocks();
foreach ($fields as $field) {
$key = $field->getKey();
switch (idx($locks, $key)) {
case self::LOCK_LOCKED:
$field->setIsHidden(false);
if ($field->getIsLockable()) {
$field->setIsLocked(true);
}
break;
case self::LOCK_HIDDEN:
$field->setIsHidden(true);
if ($field->getIsLockable()) {
$field->setIsLocked(false);
}
break;
case self::LOCK_VISIBLE:
$field->setIsHidden(false);
if ($field->getIsLockable()) {
$field->setIsLocked(false);
}
break;
default:
// If we don't have an explicit value, don't make any adjustments.
break;
}
}
$fields = $this->reorderFields($fields);
$preamble = $this->getPreamble();
if (strlen($preamble)) {
$fields = array(
'config.preamble' => id(new PhabricatorInstructionsEditField())
->setKey('config.preamble')
->setIsReorderable(false)
->setIsDefaultable(false)
->setIsLockable(false)
->setValue($preamble),
) + $fields;
}
return $fields;
}
private function reorderFields(array $fields) {
// Fields which can not be reordered are fixed in order at the top of the
// form. These are used to show instructions or contextual information.
$fixed = array();
foreach ($fields as $key => $field) {
if (!$field->getIsReorderable()) {
$fixed[$key] = $field;
}
}
$keys = $this->getFieldOrder();
$fields = $fixed + array_select_keys($fields, $keys) + $fields;
return $fields;
}
public function getURI() {
$engine_key = $this->getEngineKey();
$key = $this->getIdentifier();
return "/transactions/editengine/{$engine_key}/view/{$key}/";
}
public function getCreateURI() {
$form_key = $this->getIdentifier();
$engine = $this->getEngine();
try {
$create_uri = $engine->getEditURI(null, "form/{$form_key}/");
} catch (Exception $ex) {
$create_uri = null;
}
return $create_uri;
}
public function getIdentifier() {
$key = $this->getID();
if (!$key) {
$key = $this->getBuiltinKey();
}
return $key;
}
public function getDisplayName() {
$name = $this->getName();
if (strlen($name)) {
return $name;
}
$builtin = $this->getBuiltinKey();
if ($builtin !== null) {
return pht('Builtin Form "%s"', $builtin);
}
return pht('Untitled Form');
}
public function getPreamble() {
return $this->getProperty('preamble');
}
public function setPreamble($preamble) {
return $this->setProperty('preamble', $preamble);
}
public function setFieldOrder(array $field_order) {
return $this->setProperty('order', $field_order);
}
public function getFieldOrder() {
return $this->getProperty('order', array());
}
public function setFieldLocks(array $field_locks) {
return $this->setProperty('locks', $field_locks);
}
public function getFieldLocks() {
return $this->getProperty('locks', array());
}
public function getFieldDefault($key) {
$defaults = $this->getProperty('defaults', array());
return idx($defaults, $key);
}
public function setFieldDefault($key, $value) {
$defaults = $this->getProperty('defaults', array());
$defaults[$key] = $value;
return $this->setProperty('defaults', $defaults);
}
public function getIcon() {
return $this->getEngine()->getIcon();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
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 $this->getEngine()
->getApplication()
->getPolicy($capability);
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicyFilter::hasCapability(
$viewer,
$this->getEngine()->getApplication(),
PhabricatorPolicyCapability::CAN_EDIT);
}
return false;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorEditEngineConfigurationEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorEditEngineConfigurationTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
- return $timeline;
- }
-
}
diff --git a/src/applications/transactions/storage/PhabricatorModularTransaction.php b/src/applications/transactions/storage/PhabricatorModularTransaction.php
index 6f9fd4125..4ee2e69b3 100644
--- a/src/applications/transactions/storage/PhabricatorModularTransaction.php
+++ b/src/applications/transactions/storage/PhabricatorModularTransaction.php
@@ -1,214 +1,200 @@
<?php
// TODO: Some "final" modifiers have been VERY TEMPORARILY moved aside to
// allow DifferentialTransaction to extend this class without converting
// fully to ModularTransactions.
abstract class PhabricatorModularTransaction
extends PhabricatorApplicationTransaction {
private $implementation;
abstract public function getBaseTransactionClass();
public function getModularType() {
return $this->getTransactionImplementation();
}
final protected function getTransactionImplementation() {
if (!$this->implementation) {
$this->implementation = $this->newTransactionImplementation();
}
return $this->implementation;
}
public function newModularTransactionTypes() {
$base_class = $this->getBaseTransactionClass();
$types = id(new PhutilClassMapQuery())
->setAncestorClass($base_class)
->setUniqueMethod('getTransactionTypeConstant')
->execute();
// Add core transaction types.
$types += id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorCoreTransactionType')
->setUniqueMethod('getTransactionTypeConstant')
->execute();
return $types;
}
private function newTransactionImplementation() {
$types = $this->newModularTransactionTypes();
$key = $this->getTransactionType();
if (empty($types[$key])) {
$type = $this->newFallbackModularTransactionType();
} else {
$type = clone $types[$key];
}
$type->setStorage($this);
return $type;
}
protected function newFallbackModularTransactionType() {
return new PhabricatorCoreVoidTransaction();
}
final public function generateOldValue($object) {
return $this->getTransactionImplementation()->generateOldValue($object);
}
final public function generateNewValue($object) {
return $this->getTransactionImplementation()
->generateNewValue($object, $this->getNewValue());
}
- final public function willApplyTransactions($object, array $xactions) {
- return $this->getTransactionImplementation()
- ->willApplyTransactions($object, $xactions);
- }
-
final public function applyInternalEffects($object) {
return $this->getTransactionImplementation()
->applyInternalEffects($object);
}
final public function applyExternalEffects($object) {
return $this->getTransactionImplementation()
->applyExternalEffects($object);
}
/* final */ public function shouldHide() {
if ($this->getTransactionImplementation()->shouldHide()) {
return true;
}
return parent::shouldHide();
}
// c4s custo
/* final */ public function shouldHideForFeed() {
if ($this->getTransactionImplementation()->shouldHideForFeed()) {
return true;
}
return parent::shouldHideForFeed();
}
/* final */ public function shouldHideForMail(array $xactions) {
if ($this->getTransactionImplementation()->shouldHideForMail()) {
return true;
}
return parent::shouldHideForMail($xactions);
}
final public function shouldHideForNotifications() {
$hide = $this->getTransactionImplementation()->shouldHideForNotifications();
// Returning "null" means "use the default behavior".
if ($hide === null) {
return parent::shouldHideForNotifications();
}
return $hide;
}
/* final */ public function getIcon() {
$icon = $this->getTransactionImplementation()->getIcon();
if ($icon !== null) {
return $icon;
}
return parent::getIcon();
}
/* final */ public function getTitle() {
$title = $this->getTransactionImplementation()->getTitle();
if ($title !== null) {
return $title;
}
return parent::getTitle();
}
/* final */ public function getActionName() {
$action = $this->getTransactionImplementation()->getActionName();
if ($action !== null) {
return $action;
}
return parent::getActionName();
}
/* final */ public function getActionStrength() {
$strength = $this->getTransactionImplementation()->getActionStrength();
if ($strength !== null) {
return $strength;
}
return parent::getActionStrength();
}
- public function getTitleForMail() {
- $old_target = $this->getRenderingTarget();
- $new_target = self::TARGET_TEXT;
- $this->setRenderingTarget($new_target);
- $title = $this->getTitle();
- $this->setRenderingTarget($old_target);
- return $title;
- }
-
/* final */ public function getTitleForFeed() {
$title = $this->getTransactionImplementation()->getTitleForFeed();
if ($title !== null) {
return $title;
}
return parent::getTitleForFeed();
}
/* final */ public function getColor() {
$color = $this->getTransactionImplementation()->getColor();
if ($color !== null) {
return $color;
}
return parent::getColor();
}
public function attachViewer(PhabricatorUser $viewer) {
$this->getTransactionImplementation()->setViewer($viewer);
return parent::attachViewer($viewer);
}
final public function hasChangeDetails() {
if ($this->getTransactionImplementation()->hasChangeDetailView()) {
return true;
}
return parent::hasChangeDetails();
}
final public function renderChangeDetails(PhabricatorUser $viewer) {
$impl = $this->getTransactionImplementation();
$impl->setViewer($viewer);
$view = $impl->newChangeDetailView();
if ($view !== null) {
return $view;
}
return parent::renderChangeDetails($viewer);
}
final protected function newRemarkupChanges() {
return $this->getTransactionImplementation()->newRemarkupChanges();
}
}
diff --git a/src/applications/transactions/storage/PhabricatorModularTransactionType.php b/src/applications/transactions/storage/PhabricatorModularTransactionType.php
index 35dd3ac19..2d0cb8e7c 100644
--- a/src/applications/transactions/storage/PhabricatorModularTransactionType.php
+++ b/src/applications/transactions/storage/PhabricatorModularTransactionType.php
@@ -1,432 +1,498 @@
<?php
abstract class PhabricatorModularTransactionType
extends Phobject {
private $storage;
private $viewer;
private $editor;
final public function getTransactionTypeConstant() {
return $this->getPhobjectClassConstant('TRANSACTIONTYPE');
}
public function generateOldValue($object) {
throw new PhutilMethodNotImplementedException();
}
public function generateNewValue($object, $value) {
return $value;
}
public function validateTransactions($object, array $xactions) {
return array();
}
- public function willApplyTransactions($object, array $xactions) {
- return;
- }
-
public function applyInternalEffects($object, $value) {
return;
}
public function applyExternalEffects($object, $value) {
return;
}
public function didCommitTransaction($object, $value) {
return;
}
public function getTransactionHasEffect($object, $old, $new) {
return ($old !== $new);
}
public function extractFilePHIDs($object, $value) {
return array();
}
public function shouldHide() {
return false;
}
public function shouldHideForFeed() {
return false;
}
public function shouldHideForMail() {
return false;
}
public function shouldHideForNotifications() {
return null;
}
public function getIcon() {
return null;
}
public function getTitle() {
return null;
}
public function getTitleForFeed() {
return null;
}
public function getActionName() {
return null;
}
public function getActionStrength() {
return null;
}
public function getColor() {
return null;
}
public function hasChangeDetailView() {
return false;
}
public function newChangeDetailView() {
return null;
}
public function getMailDiffSectionHeader() {
return pht('EDIT DETAILS');
}
public function newRemarkupChanges() {
return array();
}
public function mergeTransactions(
$object,
PhabricatorApplicationTransaction $u,
PhabricatorApplicationTransaction $v) {
return null;
}
final public function setStorage(
PhabricatorApplicationTransaction $xaction) {
$this->storage = $xaction;
return $this;
}
private function getStorage() {
return $this->storage;
}
final public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
final protected function getViewer() {
return $this->viewer;
}
final public function getActor() {
return $this->getEditor()->getActor();
}
final public function getActingAsPHID() {
return $this->getEditor()->getActingAsPHID();
}
final public function setEditor(
PhabricatorApplicationTransactionEditor $editor) {
$this->editor = $editor;
return $this;
}
final protected function getEditor() {
if (!$this->editor) {
throw new PhutilInvalidStateException('setEditor');
}
return $this->editor;
}
final protected function hasEditor() {
return (bool)$this->editor;
}
final protected function getAuthorPHID() {
return $this->getStorage()->getAuthorPHID();
}
final protected function getObjectPHID() {
return $this->getStorage()->getObjectPHID();
}
final protected function getObject() {
return $this->getStorage()->getObject();
}
final protected function getOldValue() {
return $this->getStorage()->getOldValue();
}
final protected function getNewValue() {
return $this->getStorage()->getNewValue();
}
final protected function renderAuthor() {
$author_phid = $this->getAuthorPHID();
return $this->getStorage()->renderHandleLink($author_phid);
}
final protected function renderObject() {
$object_phid = $this->getObjectPHID();
return $this->getStorage()->renderHandleLink($object_phid);
}
final protected function renderHandle($phid) {
$viewer = $this->getViewer();
$display = $viewer->renderHandle($phid);
if ($this->isTextMode()) {
$display->setAsText(true);
}
return $display;
}
final protected function renderOldHandle() {
return $this->renderHandle($this->getOldValue());
}
final protected function renderNewHandle() {
return $this->renderHandle($this->getNewValue());
}
final protected function renderOldPolicy() {
return $this->renderPolicy($this->getOldValue(), 'old');
}
final protected function renderNewPolicy() {
return $this->renderPolicy($this->getNewValue(), 'new');
}
final protected function renderPolicy($phid, $mode) {
$viewer = $this->getViewer();
$handles = $viewer->loadHandles(array($phid));
$policy = PhabricatorPolicy::newFromPolicyAndHandle(
$phid,
$handles[$phid]);
if ($this->isTextMode()) {
return $this->renderValue($policy->getFullName());
}
$storage = $this->getStorage();
if ($policy->getType() == PhabricatorPolicyType::TYPE_CUSTOM) {
$policy->setHref('/transactions/'.$mode.'/'.$storage->getPHID().'/');
$policy->setWorkflow(true);
}
return $this->renderValue($policy->renderDescription());
}
final protected function renderHandleList(array $phids) {
$viewer = $this->getViewer();
$display = $viewer->renderHandleList($phids)
->setAsInline(true);
if ($this->isTextMode()) {
$display->setAsText(true);
}
return $display;
}
final protected function renderValue($value) {
if ($this->isTextMode()) {
return sprintf('"%s"', $value);
}
return phutil_tag(
'span',
array(
'class' => 'phui-timeline-value',
),
$value);
}
final protected function renderValueList(array $values) {
$result = array();
foreach ($values as $value) {
$result[] = $this->renderValue($value);
}
if ($this->isTextMode()) {
return implode(', ', $result);
}
return phutil_implode_html(', ', $result);
}
final protected function renderOldValue() {
return $this->renderValue($this->getOldValue());
}
final protected function renderNewValue() {
return $this->renderValue($this->getNewValue());
}
final protected function renderDate($epoch) {
$viewer = $this->getViewer();
// We accept either epoch timestamps or dictionaries describing a
// PhutilCalendarDateTime.
if (is_array($epoch)) {
$datetime = PhutilCalendarAbsoluteDateTime::newFromDictionary($epoch)
->setViewerTimezone($viewer->getTimezoneIdentifier());
$all_day = $datetime->getIsAllDay();
$epoch = $datetime->getEpoch();
} else {
$all_day = false;
}
if ($all_day) {
$display = phabricator_date($epoch, $viewer);
} else if ($this->isRenderingTargetExternal()) {
// When rendering to text, we explicitly render the offset from UTC to
// provide context to the date: the mail may be generating with the
// server's settings, or the user may later refer back to it after
// changing timezones.
$display = phabricator_datetimezone($epoch, $viewer);
} else {
$display = phabricator_datetime($epoch, $viewer);
}
return $this->renderValue($display);
}
final protected function renderOldDate() {
return $this->renderDate($this->getOldValue());
}
final protected function renderNewDate() {
return $this->renderDate($this->getNewValue());
}
final protected function newError($title, $message, $xaction = null) {
return new PhabricatorApplicationTransactionValidationError(
$this->getTransactionTypeConstant(),
$title,
$message,
$xaction);
}
final protected function newRequiredError($message, $xaction = null) {
return $this->newError(pht('Required'), $message, $xaction)
->setIsMissingFieldError(true);
}
final protected function newInvalidError($message, $xaction = null) {
return $this->newError(pht('Invalid'), $message, $xaction);
}
final protected function isNewObject() {
return $this->getEditor()->getIsNewObject();
}
final protected function isEmptyTextTransaction($value, array $xactions) {
foreach ($xactions as $xaction) {
$value = $xaction->getNewValue();
}
return !strlen($value);
}
/**
* When rendering to external targets (Email/Asana/etc), we need to include
* more information that users can't obtain later.
*/
final protected function isRenderingTargetExternal() {
// Right now, this is our best proxy for this:
return $this->isTextMode();
// "TARGET_TEXT" means "EMail" and "TARGET_HTML" means "Web".
}
final protected function isTextMode() {
$target = $this->getStorage()->getRenderingTarget();
return ($target == PhabricatorApplicationTransaction::TARGET_TEXT);
}
final protected function newRemarkupChange() {
return id(new PhabricatorTransactionRemarkupChange())
->setTransaction($this->getStorage());
}
final protected function isCreateTransaction() {
return $this->getStorage()->getIsCreateTransaction();
}
final protected function getPHIDList(array $old, array $new) {
$editor = $this->getEditor();
return $editor->getPHIDList($old, $new);
}
public function getMetadataValue($key, $default = null) {
return $this->getStorage()->getMetadataValue($key, $default);
}
public function loadTransactionTypeConduitData(array $xactions) {
return null;
}
public function getTransactionTypeForConduit($xaction) {
return null;
}
public function getFieldValuesForConduit($xaction, $data) {
return array();
}
protected function requireApplicationCapability($capability) {
$application_class = $this->getEditor()->getEditorApplicationClass();
$application = newv($application_class, array());
PhabricatorPolicyFilter::requireCapability(
$this->getActor(),
$application,
$capability);
}
/**
* Get a list of capabilities the actor must have on the object to apply
* a transaction to it.
*
* Usually, you should use this to reduce capability requirements when a
* transaction (like leaving a Conpherence thread) can be applied without
* having edit permission on the object. You can override this method to
* remove the CAN_EDIT requirement, or to replace it with a different
* requirement.
*
* If you are increasing capability requirements and need to add an
* additional capability or policy requirement above and beyond CAN_EDIT, it
* is usually better implemented as a validation check.
*
* @param object Object being edited.
* @param PhabricatorApplicationTransaction Transaction being applied.
* @return null|const|list<const> A capability constant (or list of
* capability constants) which the actor must have on the object. You can
* return `null` as a shorthand for "no capabilities are required".
*/
public function getRequiredCapabilities(
$object,
PhabricatorApplicationTransaction $xaction) {
return PhabricatorPolicyCapability::CAN_EDIT;
}
+ public function shouldTryMFA(
+ $object,
+ PhabricatorApplicationTransaction $xaction) {
+ return false;
+ }
+
+ // NOTE: See T12921. These APIs are somewhat aspirational. For now, all of
+ // these use "TARGET_TEXT" (even the HTML methods!) and the body methods
+ // actually return Remarkup, not text or HTML.
+
+ final public function getTitleForTextMail() {
+ return $this->getTitleForMailWithRenderingTarget(
+ PhabricatorApplicationTransaction::TARGET_TEXT);
+ }
+
+ final public function getTitleForHTMLMail() {
+ return $this->getTitleForMailWithRenderingTarget(
+ PhabricatorApplicationTransaction::TARGET_TEXT);
+ }
+
+ final public function getBodyForTextMail() {
+ return $this->getBodyForMailWithRenderingTarget(
+ PhabricatorApplicationTransaction::TARGET_TEXT);
+ }
+
+ final public function getBodyForHTMLMail() {
+ return $this->getBodyForMailWithRenderingTarget(
+ PhabricatorApplicationTransaction::TARGET_TEXT);
+ }
+
+ private function getTitleForMailWithRenderingTarget($target) {
+ $storage = $this->getStorage();
+
+ $old_target = $storage->getRenderingTarget();
+ try {
+ $storage->setRenderingTarget($target);
+ $result = $this->getTitleForMail();
+ } catch (Exception $ex) {
+ $storage->setRenderingTarget($old_target);
+ throw $ex;
+ }
+ $storage->setRenderingTarget($old_target);
+
+ return $result;
+ }
+
+ private function getBodyForMailWithRenderingTarget($target) {
+ $storage = $this->getStorage();
+
+ $old_target = $storage->getRenderingTarget();
+ try {
+ $storage->setRenderingTarget($target);
+ $result = $this->getBodyForMail();
+ } catch (Exception $ex) {
+ $storage->setRenderingTarget($old_target);
+ throw $ex;
+ }
+ $storage->setRenderingTarget($old_target);
+
+ return $result;
+ }
+
+ protected function getTitleForMail() {
+ return false;
+ }
+
+ protected function getBodyForMail() {
+ return false;
+ }
+
}
diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php
index 227854c79..f6a27d4bc 100644
--- a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php
+++ b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php
@@ -1,571 +1,617 @@
<?php
-/**
- * @concrete-extensible
- */
-class PhabricatorApplicationTransactionCommentView extends AphrontView {
+final class PhabricatorApplicationTransactionCommentView
+ extends AphrontView {
private $submitButtonName;
private $action;
private $previewPanelID;
private $previewTimelineID;
private $previewToggleID;
private $formID;
private $statusID;
private $commentID;
private $draft;
private $requestURI;
private $showPreview = true;
private $objectPHID;
private $headerText;
private $noPermission;
private $fullWidth;
private $infoView;
private $editEngineLock;
private $noBorder;
+ private $requiresMFA;
private $currentVersion;
private $versionedDraft;
private $commentActions;
private $commentActionGroups = array();
private $transactionTimeline;
public function setObjectPHID($object_phid) {
$this->objectPHID = $object_phid;
return $this;
}
public function getObjectPHID() {
return $this->objectPHID;
}
public function setShowPreview($show_preview) {
$this->showPreview = $show_preview;
return $this;
}
public function getShowPreview() {
return $this->showPreview;
}
public function setRequestURI(PhutilURI $request_uri) {
$this->requestURI = $request_uri;
return $this;
}
public function getRequestURI() {
return $this->requestURI;
}
public function setCurrentVersion($current_version) {
$this->currentVersion = $current_version;
return $this;
}
public function getCurrentVersion() {
return $this->currentVersion;
}
public function setVersionedDraft(
PhabricatorVersionedDraft $versioned_draft) {
$this->versionedDraft = $versioned_draft;
return $this;
}
public function getVersionedDraft() {
return $this->versionedDraft;
}
public function setDraft(PhabricatorDraft $draft) {
$this->draft = $draft;
return $this;
}
public function getDraft() {
return $this->draft;
}
public function setSubmitButtonName($submit_button_name) {
$this->submitButtonName = $submit_button_name;
return $this;
}
public function getSubmitButtonName() {
return $this->submitButtonName;
}
public function setAction($action) {
$this->action = $action;
return $this;
}
public function getAction() {
return $this->action;
}
public function setHeaderText($text) {
$this->headerText = $text;
return $this;
}
public function setFullWidth($fw) {
$this->fullWidth = $fw;
return $this;
}
public function setInfoView(PHUIInfoView $info_view) {
$this->infoView = $info_view;
return $this;
}
public function getInfoView() {
return $this->infoView;
}
public function setCommentActions(array $comment_actions) {
assert_instances_of($comment_actions, 'PhabricatorEditEngineCommentAction');
$this->commentActions = $comment_actions;
return $this;
}
public function getCommentActions() {
return $this->commentActions;
}
public function setCommentActionGroups(array $groups) {
assert_instances_of($groups, 'PhabricatorEditEngineCommentActionGroup');
$this->commentActionGroups = $groups;
return $this;
}
public function getCommentActionGroups() {
return $this->commentActionGroups;
}
public function setNoPermission($no_permission) {
$this->noPermission = $no_permission;
return $this;
}
public function getNoPermission() {
return $this->noPermission;
}
public function setEditEngineLock(PhabricatorEditEngineLock $lock) {
$this->editEngineLock = $lock;
return $this;
}
public function getEditEngineLock() {
return $this->editEngineLock;
}
+ public function setRequiresMFA($requires_mfa) {
+ $this->requiresMFA = $requires_mfa;
+ return $this;
+ }
+
+ public function getRequiresMFA() {
+ return $this->requiresMFA;
+ }
+
public function setTransactionTimeline(
PhabricatorApplicationTransactionView $timeline) {
$timeline->setQuoteTargetID($this->getCommentID());
if ($this->getNoPermission() || $this->getEditEngineLock()) {
$timeline->setShouldTerminate(true);
}
$this->transactionTimeline = $timeline;
return $this;
}
public function render() {
if ($this->getNoPermission()) {
return null;
}
$lock = $this->getEditEngineLock();
if ($lock) {
return id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors(
array(
$lock->getLockedObjectDisplayText(),
));
}
- $user = $this->getUser();
- if (!$user->isLoggedIn()) {
+ $viewer = $this->getViewer();
+ if (!$viewer->isLoggedIn()) {
$uri = id(new PhutilURI('/login/'))
->setQueryParam('next', (string)$this->getRequestURI());
return id(new PHUIObjectBoxView())
->setFlush(true)
->appendChild(
javelin_tag(
'a',
array(
'class' => 'login-to-comment button',
'href' => $uri,
),
pht('Log In to Comment')));
}
+ if ($this->getRequiresMFA()) {
+ if (!$viewer->getIsEnrolledInMultiFactor()) {
+ $viewer->updateMultiFactorEnrollment();
+ if (!$viewer->getIsEnrolledInMultiFactor()) {
+ $messages = array();
+ $messages[] = pht(
+ 'You must provide multi-factor credentials to comment or make '.
+ 'changes, but you do not have multi-factor authentication '.
+ 'configured on your account.');
+ $messages[] = pht(
+ 'To continue, configure multi-factor authentication in Settings.');
+
+ return id(new PHUIInfoView())
+ ->setSeverity(PHUIInfoView::SEVERITY_MFA)
+ ->setErrors($messages);
+ }
+ }
+ }
+
$data = array();
$comment = $this->renderCommentPanel();
if ($this->getShowPreview()) {
$preview = $this->renderPreviewPanel();
} else {
$preview = null;
}
if (!$this->getCommentActions()) {
Javelin::initBehavior(
'phabricator-transaction-comment-form',
array(
'formID' => $this->getFormID(),
'timelineID' => $this->getPreviewTimelineID(),
'panelID' => $this->getPreviewPanelID(),
'showPreview' => $this->getShowPreview(),
'actionURI' => $this->getAction(),
));
}
require_celerity_resource('phui-comment-form-css');
- $image_uri = $user->getProfileImageURI();
+ $image_uri = $viewer->getProfileImageURI();
$image = phutil_tag(
'div',
array(
'style' => 'background-image: url('.$image_uri.')',
'class' => 'phui-comment-image visual-only',
));
$wedge = phutil_tag(
'div',
array(
'class' => 'phui-timeline-wedge',
),
'');
$badge_view = $this->renderBadgeView();
$comment_box = id(new PHUIObjectBoxView())
->setFlush(true)
->addClass('phui-comment-form-view')
->addSigil('phui-comment-form')
->appendChild(
phutil_tag(
'h3',
array(
'class' => 'aural-only',
),
pht('Add Comment')))
->appendChild($image)
->appendChild($badge_view)
->appendChild($wedge)
->appendChild($comment);
return array($comment_box, $preview);
}
private function renderCommentPanel() {
$draft_comment = '';
$draft_key = null;
if ($this->getDraft()) {
$draft_comment = $this->getDraft()->getDraft();
$draft_key = $this->getDraft()->getDraftKey();
}
$versioned_draft = $this->getVersionedDraft();
if ($versioned_draft) {
$draft_comment = $versioned_draft->getProperty('comment', '');
}
if (!$this->getObjectPHID()) {
throw new PhutilInvalidStateException('setObjectPHID', 'render');
}
$version_key = PhabricatorVersionedDraft::KEY_VERSION;
$version_value = $this->getCurrentVersion();
$form = id(new AphrontFormView())
->setUser($this->getUser())
->addSigil('transaction-append')
->setWorkflow(true)
->setFullWidth($this->fullWidth)
->setMetadata(
array(
'objectPHID' => $this->getObjectPHID(),
))
->setAction($this->getAction())
->setID($this->getFormID())
->addHiddenInput('__draft__', $draft_key)
->addHiddenInput($version_key, $version_value);
$comment_actions = $this->getCommentActions();
if ($comment_actions) {
$action_map = array();
$type_map = array();
$comment_actions = mpull($comment_actions, null, 'getKey');
$draft_actions = array();
$draft_keys = array();
if ($versioned_draft) {
$draft_actions = $versioned_draft->getProperty('actions', array());
if (!is_array($draft_actions)) {
$draft_actions = array();
}
foreach ($draft_actions as $action) {
$type = idx($action, 'type');
$comment_action = idx($comment_actions, $type);
if (!$comment_action) {
continue;
}
$value = idx($action, 'value');
$comment_action->setValue($value);
$draft_keys[] = $type;
}
}
foreach ($comment_actions as $key => $comment_action) {
$key = $comment_action->getKey();
$label = $comment_action->getLabel();
$action_map[$key] = array(
'key' => $key,
'label' => $label,
'type' => $comment_action->getPHUIXControlType(),
'spec' => $comment_action->getPHUIXControlSpecification(),
'initialValue' => $comment_action->getInitialValue(),
'groupKey' => $comment_action->getGroupKey(),
'conflictKey' => $comment_action->getConflictKey(),
'auralLabel' => pht('Remove Action: %s', $label),
'buttonText' => $comment_action->getSubmitButtonText(),
);
$type_map[$key] = $comment_action;
}
$options = $this->newCommentActionOptions($action_map);
$action_id = celerity_generate_unique_node_id();
$input_id = celerity_generate_unique_node_id();
$place_id = celerity_generate_unique_node_id();
$form->appendChild(
phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => 'editengine.actions',
'id' => $input_id,
)));
$invisi_bar = phutil_tag(
'div',
array(
'id' => $place_id,
'class' => 'phui-comment-control-stack',
));
$action_select = id(new AphrontFormSelectControl())
->addClass('phui-comment-fullwidth-control')
->addClass('phui-comment-action-control')
->setID($action_id)
->setOptions($options);
$action_bar = phutil_tag(
'div',
array(
'class' => 'phui-comment-action-bar grouped',
),
array(
$action_select,
));
$form->appendChild($action_bar);
$info_view = $this->getInfoView();
if ($info_view) {
$form->appendChild($info_view);
}
+ if ($this->getRequiresMFA()) {
+ $message = pht(
+ 'You will be required to provide multi-factor credentials to '.
+ 'comment or make changes.');
+
+ $form->appendChild(
+ id(new PHUIInfoView())
+ ->setSeverity(PHUIInfoView::SEVERITY_MFA)
+ ->setErrors(array($message)));
+ }
+
$form->appendChild($invisi_bar);
$form->addClass('phui-comment-has-actions');
+ $timeline = $this->transactionTimeline;
+
+ $view_data = array();
+ if ($timeline) {
+ $view_data = $timeline->getViewData();
+ }
+
Javelin::initBehavior(
'comment-actions',
array(
'actionID' => $action_id,
'inputID' => $input_id,
'formID' => $this->getFormID(),
'placeID' => $place_id,
'panelID' => $this->getPreviewPanelID(),
'timelineID' => $this->getPreviewTimelineID(),
'actions' => $action_map,
'showPreview' => $this->getShowPreview(),
'actionURI' => $this->getAction(),
'drafts' => $draft_keys,
'defaultButtonText' => $this->getSubmitButtonName(),
+ 'viewData' => $view_data,
));
}
$submit_button = id(new AphrontFormSubmitControl())
->addClass('phui-comment-fullwidth-control')
->addClass('phui-comment-submit-control')
->setValue($this->getSubmitButtonName());
$form
->appendChild(
id(new PhabricatorRemarkupControl())
->setID($this->getCommentID())
->addClass('phui-comment-fullwidth-control')
->addClass('phui-comment-textarea-control')
->setCanPin(true)
->setName('comment')
->setUser($this->getUser())
->setValue($draft_comment))
->appendChild(
id(new AphrontFormSubmitControl())
->addClass('phui-comment-fullwidth-control')
->addClass('phui-comment-submit-control')
->addSigil('submit-transactions')
->setValue($this->getSubmitButtonName()));
return $form;
}
private function renderPreviewPanel() {
$preview = id(new PHUITimelineView())
->setID($this->getPreviewTimelineID());
return phutil_tag(
'div',
array(
'id' => $this->getPreviewPanelID(),
'style' => 'display: none',
'class' => 'phui-comment-preview-view',
),
$preview);
}
private function getPreviewPanelID() {
if (!$this->previewPanelID) {
$this->previewPanelID = celerity_generate_unique_node_id();
}
return $this->previewPanelID;
}
private function getPreviewTimelineID() {
if (!$this->previewTimelineID) {
$this->previewTimelineID = celerity_generate_unique_node_id();
}
return $this->previewTimelineID;
}
public function setFormID($id) {
$this->formID = $id;
return $this;
}
private function getFormID() {
if (!$this->formID) {
$this->formID = celerity_generate_unique_node_id();
}
return $this->formID;
}
private function getStatusID() {
if (!$this->statusID) {
$this->statusID = celerity_generate_unique_node_id();
}
return $this->statusID;
}
private function getCommentID() {
if (!$this->commentID) {
$this->commentID = celerity_generate_unique_node_id();
}
return $this->commentID;
}
private function newCommentActionOptions(array $action_map) {
$options = array();
$options['+'] = pht('Add Action...');
// Merge options into groups.
$groups = array();
foreach ($action_map as $key => $item) {
$group_key = $item['groupKey'];
if (!isset($groups[$group_key])) {
$groups[$group_key] = array();
}
$groups[$group_key][$key] = $item;
}
$group_specs = $this->getCommentActionGroups();
$group_labels = mpull($group_specs, 'getLabel', 'getKey');
// Reorder groups to put them in the same order as the recognized
// group definitions.
$groups = array_select_keys($groups, array_keys($group_labels)) + $groups;
// Move options with no group to the end.
$default_group = idx($groups, '');
if ($default_group) {
unset($groups['']);
$groups[''] = $default_group;
}
foreach ($groups as $group_key => $group_items) {
if (strlen($group_key)) {
$group_label = idx($group_labels, $group_key, $group_key);
$options[$group_label] = ipull($group_items, 'label');
} else {
foreach ($group_items as $key => $item) {
$options[$key] = $item['label'];
}
}
}
return $options;
}
private function renderBadgeView() {
$user = $this->getUser();
$can_use_badges = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorBadgesApplication',
$user);
if (!$can_use_badges) {
return null;
}
// Pull Badges from UserCache
$badges = $user->getRecentBadgeAwards();
$badge_view = null;
if ($badges) {
$badge_list = array();
foreach ($badges as $badge) {
$badge_view = id(new PHUIBadgeMiniView())
->setIcon($badge['icon'])
->setQuality($badge['quality'])
->setHeader($badge['name'])
->setTipDirection('E')
->setHref('/badges/view/'.$badge['id'].'/');
$badge_list[] = $badge_view;
}
$flex = new PHUIBadgeBoxView();
$flex->addItems($badge_list);
$flex->setCollapsed(true);
$badge_view = phutil_tag(
'div',
array(
'class' => 'phui-timeline-badges',
),
$flex);
}
return $badge_view;
}
}
diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php
index 9916628ed..c2b32aa19 100644
--- a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php
+++ b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php
@@ -1,550 +1,541 @@
<?php
/**
* @concrete-extensible
*/
class PhabricatorApplicationTransactionView extends AphrontView {
private $transactions;
private $engine;
private $showEditActions = true;
private $isPreview;
private $objectPHID;
private $shouldTerminate = false;
private $quoteTargetID;
private $quoteRef;
private $pager;
private $renderAsFeed;
- private $renderData = array();
private $hideCommentOptions = false;
+ private $viewData = array();
public function setRenderAsFeed($feed) {
$this->renderAsFeed = $feed;
return $this;
}
public function setQuoteRef($quote_ref) {
$this->quoteRef = $quote_ref;
return $this;
}
public function getQuoteRef() {
return $this->quoteRef;
}
public function setQuoteTargetID($quote_target_id) {
$this->quoteTargetID = $quote_target_id;
return $this;
}
public function getQuoteTargetID() {
return $this->quoteTargetID;
}
public function setObjectPHID($object_phid) {
$this->objectPHID = $object_phid;
return $this;
}
public function getObjectPHID() {
return $this->objectPHID;
}
public function setIsPreview($is_preview) {
$this->isPreview = $is_preview;
return $this;
}
public function getIsPreview() {
return $this->isPreview;
}
public function setShowEditActions($show_edit_actions) {
$this->showEditActions = $show_edit_actions;
return $this;
}
public function getShowEditActions() {
return $this->showEditActions;
}
public function setMarkupEngine(PhabricatorMarkupEngine $engine) {
$this->engine = $engine;
return $this;
}
public function setTransactions(array $transactions) {
assert_instances_of($transactions, 'PhabricatorApplicationTransaction');
$this->transactions = $transactions;
return $this;
}
public function getTransactions() {
return $this->transactions;
}
public function setShouldTerminate($term) {
$this->shouldTerminate = $term;
return $this;
}
public function setPager(AphrontCursorPagerView $pager) {
$this->pager = $pager;
return $this;
}
public function getPager() {
return $this->pager;
}
- /**
- * This is additional data that may be necessary to render the next set
- * of transactions. Objects that implement
- * PhabricatorApplicationTransactionInterface use this data in
- * willRenderTimeline.
- */
- public function setRenderData(array $data) {
- $this->renderData = $data;
+ public function setHideCommentOptions($hide_comment_options) {
+ $this->hideCommentOptions = $hide_comment_options;
return $this;
}
- public function getRenderData() {
- return $this->renderData;
+ public function getHideCommentOptions() {
+ return $this->hideCommentOptions;
}
- public function setHideCommentOptions($hide_comment_options) {
- $this->hideCommentOptions = $hide_comment_options;
+ public function setViewData(array $view_data) {
+ $this->viewData = $view_data;
return $this;
}
- public function getHideCommentOptions() {
- return $this->hideCommentOptions;
+ public function getViewData() {
+ return $this->viewData;
}
public function buildEvents($with_hiding = false) {
$user = $this->getUser();
$xactions = $this->transactions;
$xactions = $this->filterHiddenTransactions($xactions);
$xactions = $this->groupRelatedTransactions($xactions);
$groups = $this->groupDisplayTransactions($xactions);
// If the viewer has interacted with this object, we hide things from
// before their most recent interaction by default. This tends to make
// very long threads much more manageable, because you don't have to
// scroll through a lot of history and can focus on just new stuff.
$show_group = null;
if ($with_hiding) {
// Find the most recent comment by the viewer.
$group_keys = array_keys($groups);
$group_keys = array_reverse($group_keys);
// If we would only hide a small number of transactions, don't hide
// anything. Just don't examine the last few keys. Also, we always
// want to show the most recent pieces of activity, so don't examine
// the first few keys either.
$group_keys = array_slice($group_keys, 2, -2);
$type_comment = PhabricatorTransactions::TYPE_COMMENT;
foreach ($group_keys as $group_key) {
$group = $groups[$group_key];
foreach ($group as $xaction) {
if ($xaction->getAuthorPHID() == $user->getPHID() &&
$xaction->getTransactionType() == $type_comment) {
// This is the most recent group where the user commented.
$show_group = $group_key;
break 2;
}
}
}
}
$events = array();
$hide_by_default = ($show_group !== null);
$set_next_page_id = false;
foreach ($groups as $group_key => $group) {
if ($hide_by_default && ($show_group === $group_key)) {
$hide_by_default = false;
$set_next_page_id = true;
}
$group_event = null;
foreach ($group as $xaction) {
$event = $this->renderEvent($xaction, $group);
$event->setHideByDefault($hide_by_default);
if (!$group_event) {
$group_event = $event;
} else {
$group_event->addEventToGroup($event);
}
if ($set_next_page_id) {
$set_next_page_id = false;
$pager = $this->getPager();
if ($pager) {
$pager->setNextPageID($xaction->getID());
}
}
}
$events[] = $group_event;
}
return $events;
}
public function render() {
if (!$this->getObjectPHID()) {
throw new PhutilInvalidStateException('setObjectPHID');
}
$view = $this->buildPHUITimelineView();
if ($this->getShowEditActions()) {
Javelin::initBehavior('phabricator-transaction-list');
}
return $view->render();
}
public function buildPHUITimelineView($with_hiding = true) {
if (!$this->getObjectPHID()) {
throw new PhutilInvalidStateException('setObjectPHID');
}
$view = id(new PHUITimelineView())
- ->setUser($this->getUser())
+ ->setViewer($this->getViewer())
->setShouldTerminate($this->shouldTerminate)
->setQuoteTargetID($this->getQuoteTargetID())
- ->setQuoteRef($this->getQuoteRef());
+ ->setQuoteRef($this->getQuoteRef())
+ ->setViewData($this->getViewData());
$events = $this->buildEvents($with_hiding);
foreach ($events as $event) {
$view->addEvent($event);
}
if ($this->getPager()) {
$view->setPager($this->getPager());
}
- if ($this->getRenderData()) {
- $view->setRenderData($this->getRenderData());
- }
-
return $view;
}
public function isTimelineEmpty() {
return !count($this->buildEvents(true));
}
protected function getOrBuildEngine() {
if (!$this->engine) {
$field = PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT;
$engine = id(new PhabricatorMarkupEngine())
- ->setViewer($this->getUser());
+ ->setViewer($this->getViewer());
foreach ($this->transactions as $xaction) {
if (!$xaction->hasComment()) {
continue;
}
$engine->addObject($xaction->getComment(), $field);
}
$engine->process();
$this->engine = $engine;
}
return $this->engine;
}
private function buildChangeDetailsLink(
PhabricatorApplicationTransaction $xaction) {
return javelin_tag(
'a',
array(
'href' => $xaction->getChangeDetailsURI(),
'sigil' => 'workflow',
),
pht('(Show Details)'));
}
private function buildExtraInformationLink(
PhabricatorApplicationTransaction $xaction) {
$link = $xaction->renderExtraInformationLink();
if (!$link) {
return null;
}
return phutil_tag(
'span',
array(
'class' => 'phui-timeline-extra-information',
),
array(" \xC2\xB7 ", $link));
}
protected function shouldGroupTransactions(
PhabricatorApplicationTransaction $u,
PhabricatorApplicationTransaction $v) {
return false;
}
protected function renderTransactionContent(
PhabricatorApplicationTransaction $xaction) {
$field = PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT;
$engine = $this->getOrBuildEngine();
$comment = $xaction->getComment();
if ($comment) {
if ($comment->getIsRemoved()) {
return javelin_tag(
'span',
array(
'class' => 'comment-deleted',
'sigil' => 'transaction-comment',
'meta' => array('phid' => $comment->getTransactionPHID()),
),
pht(
'This comment was removed by %s.',
$xaction->getHandle($comment->getAuthorPHID())->renderLink()));
} else if ($comment->getIsDeleted()) {
return javelin_tag(
'span',
array(
'class' => 'comment-deleted',
'sigil' => 'transaction-comment',
'meta' => array('phid' => $comment->getTransactionPHID()),
),
pht('This comment has been deleted.'));
} else if ($xaction->hasComment()) {
return javelin_tag(
'span',
array(
'class' => 'transaction-comment',
'sigil' => 'transaction-comment',
'meta' => array('phid' => $comment->getTransactionPHID()),
),
$engine->getOutput($comment, $field));
} else {
// This is an empty, non-deleted comment. Usually this happens when
// rendering previews.
return null;
}
}
return null;
}
private function filterHiddenTransactions(array $xactions) {
foreach ($xactions as $key => $xaction) {
if ($xaction->shouldHide()) {
unset($xactions[$key]);
}
}
return $xactions;
}
private function groupRelatedTransactions(array $xactions) {
$last = null;
$last_key = null;
$groups = array();
foreach ($xactions as $key => $xaction) {
if ($last && $this->shouldGroupTransactions($last, $xaction)) {
$groups[$last_key][] = $xaction;
unset($xactions[$key]);
} else {
$last = $xaction;
$last_key = $key;
}
}
foreach ($xactions as $key => $xaction) {
$xaction->attachTransactionGroup(idx($groups, $key, array()));
}
return $xactions;
}
private function groupDisplayTransactions(array $xactions) {
$groups = array();
$group = array();
foreach ($xactions as $xaction) {
if ($xaction->shouldDisplayGroupWith($group)) {
$group[] = $xaction;
} else {
if ($group) {
$groups[] = $group;
}
$group = array($xaction);
}
}
if ($group) {
$groups[] = $group;
}
foreach ($groups as $key => $group) {
$results = array();
// Sort transactions within the group by action strength, then by
// chronological order. This makes sure that multiple actions of the
// same type (like a close, then a reopen) render in the order they
// were performed.
$strength_groups = mgroup($group, 'getActionStrength');
krsort($strength_groups);
foreach ($strength_groups as $strength_group) {
foreach (msort($strength_group, 'getID') as $xaction) {
$results[] = $xaction;
}
}
$groups[$key] = $results;
}
return $groups;
}
private function renderEvent(
PhabricatorApplicationTransaction $xaction,
array $group) {
- $viewer = $this->getUser();
+ $viewer = $this->getViewer();
$event = id(new PHUITimelineEventView())
- ->setUser($viewer)
+ ->setViewer($viewer)
->setAuthorPHID($xaction->getAuthorPHID())
->setTransactionPHID($xaction->getPHID())
->setUserHandle($xaction->getHandle($xaction->getAuthorPHID()))
->setIcon($xaction->getIcon())
->setColor($xaction->getColor())
->setHideCommentOptions($this->getHideCommentOptions())
->setIsSilent($xaction->getIsSilentTransaction())
->setIsMFA($xaction->getIsMFATransaction());
list($token, $token_removed) = $xaction->getToken();
if ($token) {
$event->setToken($token, $token_removed);
}
if (!$this->shouldSuppressTitle($xaction, $group)) {
if ($this->renderAsFeed) {
$title = $xaction->getTitleForFeed();
} else {
$title = $xaction->getTitle();
}
if ($xaction->hasChangeDetails()) {
if (!$this->isPreview) {
$details = $this->buildChangeDetailsLink($xaction);
$title = array(
$title,
' ',
$details,
);
}
}
if (!$this->isPreview) {
$more = $this->buildExtraInformationLink($xaction);
if ($more) {
$title = array($title, ' ', $more);
}
}
$event->setTitle($title);
}
if ($this->isPreview) {
$event->setIsPreview(true);
} else {
$event
->setDateCreated($xaction->getDateCreated())
->setContentSource($xaction->getContentSource())
->setAnchor($xaction->getID());
}
$transaction_type = $xaction->getTransactionType();
$comment_type = PhabricatorTransactions::TYPE_COMMENT;
$is_normal_comment = ($transaction_type == $comment_type);
if ($this->getShowEditActions() &&
!$this->isPreview &&
$is_normal_comment) {
$has_deleted_comment =
$xaction->getComment() &&
$xaction->getComment()->getIsDeleted();
$has_removed_comment =
$xaction->getComment() &&
$xaction->getComment()->getIsRemoved();
if ($xaction->getCommentVersion() > 1 && !$has_removed_comment) {
$event->setIsEdited(true);
}
if (!$has_removed_comment) {
$event->setIsNormalComment(true);
}
// If we have a place for quoted text to go and this is a quotable
// comment, pass the quote target ID to the event view.
if ($this->getQuoteTargetID()) {
if ($xaction->hasComment()) {
if (!$has_removed_comment && !$has_deleted_comment) {
$event->setQuoteTargetID($this->getQuoteTargetID());
$event->setQuoteRef($this->getQuoteRef());
}
}
}
$can_edit = PhabricatorPolicyCapability::CAN_EDIT;
if ($xaction->hasComment() || $has_deleted_comment) {
$has_edit_capability = PhabricatorPolicyFilter::hasCapability(
$viewer,
$xaction,
$can_edit);
if ($has_edit_capability && !$has_removed_comment) {
$event->setIsEditable(true);
}
if ($has_edit_capability || $viewer->getIsAdmin()) {
if (!$has_removed_comment) {
$event->setIsRemovable(true);
}
}
}
}
$comment = $this->renderTransactionContent($xaction);
if ($comment) {
$event->appendChild($comment);
}
return $event;
}
private function shouldSuppressTitle(
PhabricatorApplicationTransaction $xaction,
array $group) {
// This is a little hard-coded, but we don't have any other reasonable
// cases for now. Suppress "commented on" if there are other actions in
// the display group.
if (count($group) > 1) {
$type_comment = PhabricatorTransactions::TYPE_COMMENT;
if ($xaction->getTransactionType() == $type_comment) {
return true;
}
}
return false;
}
}
diff --git a/src/docs/user/cluster/cluster_repositories.diviner b/src/docs/user/cluster/cluster_repositories.diviner
index 13307d0b7..9e4b324a5 100644
--- a/src/docs/user/cluster/cluster_repositories.diviner
+++ b/src/docs/user/cluster/cluster_repositories.diviner
@@ -1,520 +1,531 @@
@title Cluster: Repositories
@group cluster
Configuring Phabricator to use multiple repository hosts.
Overview
========
If you use Git, you can deploy Phabricator with multiple repository hosts,
configured so that each host is readable and writable. The advantages of doing
this are:
- you can completely survive the loss of repository hosts;
- reads and writes can scale across multiple machines; and
- read and write performance across multiple geographic regions may improve.
This configuration is complex, and many installs do not need to pursue it.
This configuration is not currently supported with Subversion or Mercurial.
How Reads and Writes Work
=========================
Phabricator repository replicas are multi-master: every node is readable and
writable, and a cluster of nodes can (almost always) survive the loss of any
arbitrary subset of nodes so long as at least one node is still alive.
Phabricator maintains an internal version for each repository, and increments
it when the repository is mutated.
Before responding to a read, replicas make sure their version of the repository
is up to date (no node in the cluster has a newer version of the repository).
If it isn't, they block the read until they can complete a fetch.
Before responding to a write, replicas obtain a global lock, perform the same
version check and fetch if necessary, then allow the write to continue.
Additionally, repositories passively check other nodes for updates and
replicate changes in the background. After you push a change to a repository,
it will usually spread passively to all other repository nodes within a few
minutes.
Even if passive replication is slow, the active replication makes acknowledged
changes sequential to all observers: after a write is acknowledged, all
subsequent reads are guaranteed to see it. The system does not permit stale
reads, and you do not need to wait for a replication delay to see a consistent
view of the repository no matter which node you ask.
HTTP vs HTTPS
=============
Intracluster requests (from the daemons to repository servers, or from
webservers to repository servers) are permitted to use HTTP, even if you have
set `security.require-https` in your configuration.
It is common to terminate SSL at a load balancer and use plain HTTP beyond
that, and the `security.require-https` feature is primarily focused on making
client browser behavior more convenient for users, so it does not apply to
intracluster traffic.
Using HTTP within the cluster leaves you vulnerable to attackers who can
observe traffic within a datacenter, or observe traffic between datacenters.
This is normally very difficult, but within reach for state-level adversaries
like the NSA.
If you are concerned about these attackers, you can terminate HTTPS on
repository hosts and bind to them with the "https" protocol. Just be aware that
the `security.require-https` setting won't prevent you from making
configuration mistakes, as it doesn't cover intracluster traffic.
Other mitigations are possible, but securing a network against the NSA and
similar agents of other rogue nations is beyond the scope of this document.
Repository Hosts
================
Repository hosts must run a complete, fully configured copy of Phabricator,
including a webserver. They must also run a properly configured `sshd`.
If you are converting existing hosts into cluster hosts, you may need to
revisit @{article:Diffusion User Guide: Repository Hosting} and make sure
the system user accounts have all the necessary `sudo` permissions. In
particular, cluster devices need `sudo` access to `ssh` so they can read
device keys.
Generally, these hosts will run the same set of services and configuration that
web hosts run. If you prefer, you can overlay these services and put web and
repository services on the same hosts. See @{article:Clustering Introduction}
for some guidance on overlaying services.
When a user requests information about a repository that can only be satisfied
by examining a repository working copy, the webserver receiving the request
will make an HTTP service call to a repository server which hosts the
repository to retrieve the data it needs. It will use the result of this query
to respond to the user.
-Setting up a Cluster Services
+Setting up Cluster Services
=============================
To set up clustering, first register the devices that you want to use as part
of the cluster with Almanac. For details, see @{article:Cluster: Devices}.
NOTE: Once you create a service, new repositories will immediately allocate
on it. You may want to disable repository creation during initial setup.
+NOTE: To create clustered services, your account must have the "Can Manage
+Cluster Services" capability. By default, no accounts have this capability,
+and you must enable it by changing the configuration of the Almanac
+application. Navigate to the Alamanc application configuration as follows:
+{nav icon=home, name=Home >
+Applications >
+Almanac >
+Configure >
+Edit Policies >
+Can Manage Cluster Services }
+
Once the hosts are registered as devices, you can create a new service in
Almanac:
- First, register at least one device according to the device clustering
instructions.
- Create a new service of type **Phabricator Cluster: Repository** in
Almanac.
- Bind this service to all the interfaces on the device or devices.
- For each binding, add a `protocol` key with one of these values:
`ssh`, `http`, `https`.
For example, a service might look like this:
- Service: `repos001.mycompany.net`
- Binding: `repo001.mycompany.net:80`, `protocol=http`
- Binding: `repo001.mycompany.net:2222`, `protocol=ssh`
The service itself has a `closed` property. You can set this to `true` to
disable new repository allocations on this service (for example, if it is
reaching capacity).
Migrating to Clustered Services
===============================
To convert existing repositories on an install into cluster repositories, you
will generally perform these steps:
- Register the existing host as a cluster device.
- Configure a single host repository service using //only// that host.
This puts you in a transitional state where repositories on the host can work
as either on-host repositories or cluster repositories. You can move forward
from here slowly and make sure services still work, with a quick path back to
safety if you run into trouble.
To move forward, migrate one repository to the service and make sure things
work correctly. If you run into issues, you can back out by migrating the
repository off the service.
To migrate a repository onto a cluster service, use this command:
```
$ ./bin/repository clusterize <repository> --service <service>
```
To migrate a repository back off a service, use this command:
```
$ ./bin/repository clusterize <repository> --remove-service
```
This command only changes how Phabricator connects to the repository; it does
not move any data or make any complex structural changes.
When Phabricator needs information about a non-clustered repository, it just
runs a command like `git log` directly on disk. When Phabricator needs
information about a clustered repository, it instead makes a service call to
another server, asking that server to run `git log` instead.
In a single-host cluster the server will make this service call to itself, so
nothing will really change. But this //is// an effective test for most
possible configuration mistakes.
If your canary repository works well, you can migrate the rest of your
repositories when ready (you can use `bin/repository list` to quickly get a
list of all repository monograms).
Once all repositories are migrated, you've reached a stable state and can
remain here as long as you want. This state is sufficient to convert daemons,
SSH, and web services into clustered versions and spread them across multiple
machines if those goals are more interesting.
Obviously, your single-device "cluster" will not be able to survive the loss of
the single repository host, but you can take as long as you want to expand the
cluster and add redundancy.
After creating a service, you do not need to `clusterize` new repositories:
they will automatically allocate onto an open service.
When you're ready to expand the cluster, continue below.
Expanding a Cluster
===================
To expand an existing cluster, follow these general steps:
- Register new devices in Almanac.
- Add bindings to the new devices to the repository service, also in Almanac.
- Start the daemons on the new devices.
For instructions on configuring and registering devices, see
@{article:Cluster: Devices}.
As soon as you add active bindings to a service, Phabricator will begin
synchronizing repositories and sending traffic to the new device. You do not
need to copy any repository data to the device: Phabricator will automatically
synchronize it.
If you have a large amount of repository data, you may want to help this
process along by copying the repository directory from an existing cluster
device before bringing the new host online. This is optional, but can reduce
the amount of time required to fully synchronize the cluster.
You do not need to synchronize the most up-to-date data or stop writes during
this process. For example, loading the most recent backup snapshot onto the new
device will substantially reduce the amount of data that needs to be
synchronized.
Contracting a Cluster
=====================
If you want to remove working devices from a cluster (for example, to take
hosts down for maintenance), first do this for each device:
- Change the `writable` property on the bindings to "Prevent Writes".
- Wait a few moments until the cluster synchronizes (see
"Monitoring Services" below).
This will ensure that the device you're about to remove is not the only cluster
leader, even if the cluster is receiving a high write volume. You can skip this
step if the device isn't working property to start with.
Once you've stopped writes and waited for synchronization (or if the hosts are
not working in the first place) do this for each device:
- Disable the bindings from the service to the device in Almanac.
If you are removing a device because it failed abruptly (or removing several
devices at once; or you skip the "Prevent Writes" step), it is possible that
some repositories will have lost all their leaders. See "Loss of Leaders" below
to understand and resolve this.
If you want to put the hosts back in service later:
- Enable the bindings again.
- Change `writable` back to "Allow Writes".
This will restore the cluster to the original state.
Monitoring Services
===================
You can get an overview of repository cluster status from the
{nav Config > Repository Servers} screen. This table shows a high-level
overview of all active repository services.
**Repos**: The number of repositories hosted on this service.
**Sync**: Synchronization status of repositories on this service. This is an
at-a-glance view of service health, and can show these values:
- **Synchronized**: All nodes are fully synchronized and have the latest
version of all repositories.
- **Partial**: All repositories either have at least two leaders, or have
a very recent write which is not expected to have propagated yet.
- **Unsynchronized**: At least one repository has changes which are
only available on one node and were not pushed very recently. Data may
be at risk.
- **No Repositories**: This service has no repositories.
- **Ambiguous Leader**: At least one repository has an ambiguous leader.
If this screen identifies problems, you can drill down into repository details
to get more information about them. See the next section for details.
Monitoring Repositories
=======================
You can get a more detailed view the current status of a specific repository on
cluster devices in {nav Diffusion > (Repository) > Manage Repository > Cluster
Configuration}.
This screen shows all the configured devices which are hosting the repository
and the available version on that device.
**Version**: When a repository is mutated by a push, Phabricator increases
an internal version number for the repository. This column shows which version
is on disk on the corresponding device.
After a change is pushed, the device which received the change will have a
larger version number than the other devices. The change should be passively
replicated to the remaining devices after a brief period of time, although this
can take a while if the change was large or the network connection between
devices is slow or unreliable.
You can click the version number to see the corresponding push logs for that
change. The logs contain details about what was changed, and can help you
identify if replication is slow because a change is large or for some other
reason.
**Writing**: This shows that the device is currently holding a write lock. This
normally means that it is actively receiving a push, but can also mean that
there was a write interruption. See "Write Interruptions" below for details.
**Last Writer**: This column identifies the user who most recently pushed a
change to this device. If the write lock is currently held, this user is
the user whose change is holding the lock.
**Last Write At**: When the most recent write started. If the write lock is
currently held, this shows when the lock was acquired.
Cluster Failure Modes
=====================
There are three major cluster failure modes:
- **Write Interruptions**: A write started but did not complete, leaving
the disk state and cluster state out of sync.
- **Loss of Leaders**: None of the devices with the most up-to-date data
are reachable.
- **Ambiguous Leaders**: The internal state of the repository is unclear.
Phabricator can detect these issues, and responds by freezing the repository
(usually preventing all reads and writes) until the issue is resolved. These
conditions are normally rare and very little data is at risk, but Phabricator
errs on the side of caution and requires decisions which may result in data
loss to be confirmed by a human.
The next sections cover these failure modes and appropriate responses in
more detail. In general, you will respond to these issues by assessing the
situation and then possibly choosing to discard some data.
Write Interruptions
===================
A repository cluster can be put into an inconsistent state by an interruption
in a brief window during and immediately after a write. This looks like this:
- A change is pushed to a server.
- The server acquires a write lock and begins writing the change.
- During or immediately after the write, lightning strikes the server
and destroys it.
Phabricator can not commit changes to a working copy (stored on disk) and to
the global state (stored in a database) atomically, so there is necessarily a
narrow window between committing these two different states when some tragedy
can befall a server, leaving the global and local views of the repository state
possibly divergent.
In these cases, Phabricator fails into a frozen state where further writes
are not permitted until the failure is investigated and resolved. When a
repository is frozen in this way it remains readable.
You can use the monitoring console to review the state of a frozen repository
with a held write lock. The **Writing** column will show which device is
holding the lock, and whoever is named in the **Last Writer** column may be
able to help you figure out what happened by providing more information about
what they were doing and what they observed.
Because the push was not acknowledged, it is normally safe to resolve this
issue by demoting the device. Demoting the device will undo any changes
committed by the push, and they will be lost forever.
However, the user should have received an error anyway, and should not expect
their push to have worked. Still, data is technically at risk and you may want
to investigate further and try to understand the issue in more detail before
continuing.
There is no way to explicitly keep the write, but if it was committed to disk
you can recover it manually from the working copy on the device (for example,
by using `git format-patch`) and then push it again after recovering.
If you demote the device, the in-process write will be thrown away, even if it
was complete on disk. To demote the device and release the write lock, run this
command:
```
phabricator/ $ ./bin/repository thaw <repository> --demote <device>
```
{icon exclamation-triangle, color="yellow"} Any committed but unacknowledged
data on the device will be lost.
Loss of Leaders
===============
A more straightforward failure condition is the loss of all servers in a
cluster which have the most up-to-date copy of a repository. This looks like
this:
- There is a cluster setup with two devices, X and Y.
- A new change is pushed to server X.
- Before the change can propagate to server Y, lightning strikes server X
and destroys it.
Here, all of the "leader" devices with the most up-to-date copy of the
repository have been lost. Phabricator will freeze the repository refuse to
serve requests because it can not serve reads consistently and can not accept
new writes without data loss.
The most straightforward way to resolve this issue is to restore any leader to
service. The change will be able to replicate to other devices once a leader
comes back online.
If you are unable to restore a leader or unsure that you can restore one
quickly, you can use the monitoring console to review which changes are
present on the leaders but not present on the followers by examining the
push logs.
If you are comfortable discarding these changes, you can instruct Phabricator
that it can forget about the leaders by doing this:
- Disable the service bindings to all of the leader devices so they are no
longer part of the cluster.
- Then, use `bin/repository thaw` to `--demote` the leaders explicitly.
To demote a device, run this command:
```
phabricator/ $ ./bin/repository thaw rXYZ --demote repo002.corp.net
```
{icon exclamation-triangle, color="red"} Any data which is only present on
the demoted device will be lost.
If you do this, **you will lose unreplicated data**. You will discard any
changes on the affected leaders which have not replicated to other devices
in the cluster.
If you have lost an entire cluster and replaced it with new devices that you
have restored from backups, you can aggressively wipe all memory of the old
devices by using `--demote <service>` and `--all-repositories`. **This is
dangerous and discards all unreplicated data in any repository on any device.**
```
phabricator/ $ ./bin/repository thaw --demote repo.corp.net --all-repositories
```
After you do this, continue below to promote a leader and restore the cluster
to service.
Ambiguous Leaders
=================
Repository clusters can also freeze if the leader devices are ambiguous. This
can happen if you replace an entire cluster with new devices suddenly, or make
a mistake with the `--demote` flag. This may arise from some kind of operator
error, like these:
- Someone accidentally uses `bin/repository thaw ... --demote` to demote
every device in a cluster.
- Someone accidentally deletes all the version information for a repository
from the database by making a mistake with a `DELETE` or `UPDATE` query.
- Someone accidentally disables all of the devices in a cluster, then adds
entirely new ones before repositories can propagate.
If you are moving repositories into cluster services, you can also reach this
state if you use `clusterize` to associate a repository with a service that is
bound to multiple active devices. In this case, Phabricator will not know which
device or devices have up-to-date information.
When Phabricator can not tell which device in a cluster is a leader, it freezes
the cluster because it is possible that some devices have less data and others
have more, and if it chooses a leader arbitrarily it may destroy some data
which you would prefer to retain.
To resolve this, you need to tell Phabricator which device has the most
up-to-date data and promote that device to become a leader. If you know all
devices have the same data, you are free to promote any device.
If you promote a device, **you may lose data** if you promote the wrong device
and some other device really had more up-to-date data. If you want to double
check, you can examine the working copies on disk before promoting by
connecting to the machines and using commands like `git log` to inspect state.
Once you have identified a device which has data you're happy with, use
`bin/repository thaw` to `--promote` the device. The data on the chosen
device will become authoritative:
```
phabricator/ $ ./bin/repository thaw rXYZ --promote repo002.corp.net
```
{icon exclamation-triangle, color="red"} Any data which is only present on
**other** devices will be lost.
Backups
======
Even if you configure clustering, you should still consider retaining separate
backup snapshots. Replicas protect you from data loss if you lose a host, but
they do not let you rewind time to recover from data mutation mistakes.
If something issues a `--force` push that destroys branch heads, the mutation
will propagate to the replicas.
You may be able to manually restore the branches by using tools like the
Phabricator push log or the Git reflog so it is less important to retain
repository snapshots than database snapshots, but it is still possible for
data to be lost permanently, especially if you don't notice the problem for
some time.
Retaining separate backup snapshots will improve your ability to recover more
data more easily in a wider range of disaster situations.
Next Steps
==========
Continue by:
- returning to @{article:Clustering Introduction}.
diff --git a/src/docs/user/configuration/configuration_locked.diviner b/src/docs/user/configuration/configuration_locked.diviner
index fff0da9bd..958124c38 100644
--- a/src/docs/user/configuration/configuration_locked.diviner
+++ b/src/docs/user/configuration/configuration_locked.diviner
@@ -1,121 +1,121 @@
@title Configuration Guide: Locked and Hidden Configuration
@group config
Details about locked and hidden configuration.
Overview
========
Some configuration options are **Locked** or **Hidden**. If an option has one
of these attributes, it means:
- **Locked Configuration**: This setting can not be written from the web UI.
- **Hidden Configuration**: This setting can not be read or written from
the web UI.
This document explains these attributes in more detail.
Locked Configuration
====================
**Locked Configuration** can not be edited from the web UI. In general, you
can edit it from the CLI instead, with `bin/config`:
```
phabricator/ $ ./bin/config set <key> <value>
```
Some configuration options take complicated values which can be difficult
to escape properly for the shell. The easiest way to set these options is
to use the `--stdin` flag. First, put your desired value in a `config.json`
file:
```name=config.json, lang=json
{
"duck": "quack",
"cow": "moo"
}
```
Then, set it with `--stdin` like this:
```
phabricator/ $ ./bin/config set <key> --stdin < config.json
```
A few settings have alternate CLI tools. Refer to the setting page for
details.
Note that these settings can not be written to the database, even from the
CLI.
Locked values can not be unlocked: they are locked because of what the setting
does or how the setting operates. Some of the reasons configuration options are
locked include:
**Required for bootstrapping**: Some options, like `mysql.host`, must be
available before Phabricator can read configuration from the database.
If you stored `mysql.host` only in the database, Phabricator would not know how
to connect to the database in order to read the value in the first place.
These options must be provided in a configuration source which is read earlier
in the bootstrapping process, before Phabricator connects to the database.
**Errors could not be fixed from the web UI**: Some options, like
`phabricator.base-uri`, can effectively disable the web UI if they are
configured incorrectly.
If these options could be configured from the web UI, you could not fix them if
you made a mistake (because the web UI would no longer work, so you could not
load the page to change the value).
We require these options to be edited from the CLI to make sure the editor has
access to fix any mistakes.
**Attackers could gain greater access**: Some options could be modified by an
attacker who has gained access to an administrator account in order to gain
greater access.
-For example, an attacker who could modify `metamta.mail-adapter` (and other
+For example, an attacker who could modify `cluster.mailers` (and other
similar options), could potentially reconfigure Phabricator to send mail
through an evil server they controlled, then trigger password resets on other
user accounts to compromise them.
We require these options to be edited from the CLI to make sure the editor
has full access to the install.
Hidden Configuration
====================
**Hidden Configuration** is similar to locked configuration, but also can not
be //read// from the web UI.
In almost all cases, configuration is hidden because it is some sort of secret
key or access token for an external service. These values are hidden from the
web UI to prevent administrators (or attackers who have compromised
administrator accounts) from reading them.
You can review (and edit) hidden configuration from the CLI:
```
phabricator/ $ ./bin/config get <key>
phabricator/ $ ./bin/config set <key> <value>
```
Next Steps
==========
Continue by:
- learning more about advanced options with
@{Configuration User Guide: Advanced Configuration}; or
- returning to the @{article: Configuration Guide}.
diff --git a/src/docs/user/configuration/configuring_accounts_and_registration.diviner b/src/docs/user/configuration/configuring_accounts_and_registration.diviner
index 8a4c59b19..05d11b11f 100644
--- a/src/docs/user/configuration/configuring_accounts_and_registration.diviner
+++ b/src/docs/user/configuration/configuring_accounts_and_registration.diviner
@@ -1,69 +1,67 @@
@title Configuring Accounts and Registration
@group config
Describes how to configure user access to Phabricator.
= Overview =
Phabricator supports a number of login systems. You can enable or disable these
systems to configure who can register for and access your install, and how users
with existing accounts can login.
Methods of logging in are called **Authentication Providers**. For example,
there is a "Username/Password" authentication provider available, which allows
users to log in with a traditional username and password. Other providers
support logging in with other credentials. For example:
- - **Username/Password:** Users use a username and password to log in or
- register.
- **LDAP:** Users use LDAP credentials to log in or register.
- **OAuth:** Users use accounts on a supported OAuth2 provider (like
GitHub, Facebook, or Google) to log in or register.
- **Other Providers:** More providers are available, and Phabricator
can be extended with custom providers. See the "Auth" application for
a list of available providers.
By default, no providers are enabled. You must use the "Auth" application to
add one or more providers after you complete the installation process.
After you add a provider, you can link it to existing accounts (for example,
associate an existing Phabricator account with a GitHub OAuth account) or users
can use it to register new accounts (assuming you enable these options).
-= Recovering Administrator Accounts =
+= Recovering Inaccessible Accounts =
-If you accidentally lock yourself out of Phabricator, you can use the `bin/auth`
-script to recover access to an administrator account. To recover access, run:
+If you accidentally lock yourself out of Phabricator (for example, by disabling
+all authentication providers), you can use the `bin/auth`
+script to recover access to an account. To recover access, run:
phabricator/ $ ./bin/auth recover <username>
-...where `<username>` is the admin account username you want to recover access
-to. This will give you a link which will log you in as the specified
-administrative user.
+...where `<username>` is the account username you want to recover access
+to. This will generate a link which will log you in as the specified user.
= Managing Accounts with the Web Console =
To manage accounts from the web, login as an administrator account and go to
`/people/` or click "People" on the homepage. Provided you're an admin,
you'll see options to create or edit accounts.
= Manually Creating New Accounts =
There are two ways to manually create new accounts: via the web UI using
the "People" application (this is easiest), or via the CLI using the
`accountadmin` binary (this has a few more options).
To use the CLI script, run:
phabricator/ $ ./bin/accountadmin
-Some options (like setting passwords and changing certain account flags) are
-only available from the CLI. You can also use this script to make a user
-an administrator (if you accidentally remove your admin flag) or create an
+Some options (like changing certain account flags) are only available from
+the CLI. You can also use this script to make a user
+an administrator (if you accidentally remove your admin flag) or to create an
administrative account.
= Next Steps =
Continue by:
- returning to the @{article:Configuration Guide}.
diff --git a/src/docs/user/configuration/configuring_inbound_email.diviner b/src/docs/user/configuration/configuring_inbound_email.diviner
index f4f367d57..b1ad08b7d 100644
--- a/src/docs/user/configuration/configuring_inbound_email.diviner
+++ b/src/docs/user/configuration/configuring_inbound_email.diviner
@@ -1,234 +1,291 @@
@title Configuring Inbound Email
@group config
This document contains instructions for configuring inbound email, so users
may interact with some Phabricator applications via email.
-= Preamble =
+Preamble
+========
-This can be extremely difficult to configure correctly. This is doubly true if
-you use a local MTA.
+Phabricator can process inbound mail in two general ways:
-There are a few approaches available:
+**Handling Replies**: When users reply to email notifications about changes,
+Phabricator can turn email into comments on the relevant discussion thread.
+
+**Creating Objects**: You can configure an address like `bugs@yourcompany.com`
+to create new objects (like tasks) when users send email.
+
+In either case, users can interact with objects via mail commands to apply a
+broader set of changes to objects beyond commenting. (For example, you can use
+`!close` to close a task or `!priority` to change task priority.)
+
+To configure inbound mail, you will generally:
+
+ - Configure some mail domain to submit mail to Phabricator for processing.
+ - For handling replies, set `metamta.reply-handler-domain` in your
+ configuration.
+ - For handling email that creates objects, configure inbound addresses in the
+ relevant application.
+
+See below for details on each of these steps.
+
+
+Configuration Overview
+======================
+
+Usually, the most challenging part of configuring inbound mail is getting mail
+delivered to Phabricator for processing. This step can be made much easier if
+you use a third-party mail service which can submit mail to Phabricator via
+webhooks.
+
+Some available approaches for delivering mail to Phabricator are:
| Receive Mail With | Setup | Cost | Notes |
|--------|-------|------|-------|
| Mailgun | Easy | Cheap | Recommended |
| Postmark | Easy | Cheap | Recommended |
| SendGrid | Easy | Cheap | |
-| Local MTA | Extremely Difficult | Free | Strongly discouraged! |
+| Local MTA | Difficult | Free | Discouraged |
The remainder of this document walks through configuring Phabricator to
receive mail, and then configuring your chosen transport to deliver mail
to Phabricator.
-= Configuring Phabricator =
+
+Configuring "Reply" Email
+=========================
By default, Phabricator uses a `noreply@phabricator.example.com` email address
-as the 'From' (configurable with `metamta.default-address`) and sets
-'Reply-To' to the user generating the email (e.g., by making a comment), if the
-mail was generated by a user action. This means that users can reply (or
-reply-all) to email to discuss changes, but the conversation won't be recorded
-in Phabricator and users will not be able to take actions like claiming tasks or
-requesting changes to revisions.
+as the "From" address when it sends mail. The exact address it uses can be
+configured with `metamta.default-address`.
+
+When a user takes an action that generates mail, Phabricator sets the
+"Reply-To" addresss for the mail to that user's name and address. This means
+that users can reply to email to discuss changes, but: the conversation won't
+be recorded in Phabricator; and users will not be able to use email commands
+to take actions or make edits.
To change this behavior so that users can interact with objects in Phabricator
over email, change the configuration key `metamta.reply-handler-domain` to some
domain you configure according to the instructions below, e.g.
-`phabricator.example.com`. Once you set this key, emails will use a
-'Reply-To' like `T123+273+af310f9220ad@phabricator.example.com`, which -- when
+`phabricator.example.com`. Once you set this key, email will use a
+"Reply-To" like `T123+273+af310f9220ad@phabricator.example.com`, which -- when
configured correctly, according to the instructions below -- will parse incoming
email and allow users to interact with Differential revisions, Maniphest tasks,
etc. over email.
If you don't want Phabricator to take up an entire domain (or subdomain) you
can configure a general prefix so you can use a single mailbox to receive mail
on. To make use of this set `metamta.single-reply-handler-prefix` to the
-prefix of your choice, and Phabricator will prepend this to the 'Reply-To'
+prefix of your choice, and Phabricator will prepend this to the "Reply-To"
mail address. This works because everything up to the first (optional) '+'
-character in an email-address is considered the receiver, and everything
+character in an email address is considered the receiver, and everything
after is essentially ignored.
-You can also set up application email addresses to allow users to create
-application objects via email. For example, you could configure
-`bugs@phabricator.example.com` to create a Maniphest task out of any email
-which is sent to it. To do this, see application settings for a given
-application at
+
+Configuring "Create" Email
+==========================
+
+You can set up application email addresses to allow users to create objects via
+email. For example, you could configure `bugs@phabricator.example.com` to
+create a Maniphest task out of any email which is sent to it.
+
+You can find application email settings for each application at:
{nav icon=home, name=Home >
-name=Applications >
-icon=cog, name=Settings}
+Applications >
+type=instructions, name="Select an Application" >
+icon=cog, name=Configure}
+
+Not all applications support creating objects via email.
+
+In some applications, including Maniphest, you can also configure Herald rules
+with the `[ Content source ]` and/or `[ Receiving email address ]` fields to
+route or handle objects based on which address mail was sent to.
+
+You'll also need to configure the actual mail domain to submit mail to
+Phabricator by following the instructions below. Phabricator will let you add
+any address as an application address, but can only process mail which is
+actually delivered to it.
+
-= Security =
+Security
+========
The email reply channel is "somewhat" authenticated. Each reply-to address is
unique to the recipient and includes a hash of user information and a unique
object ID, so it can only be used to update that object and only be used to act
on behalf of the recipient.
However, if an address is leaked (which is fairly easy -- for instance,
forwarding an email will leak a live reply address, or a user might take a
screenshot), //anyone// who can send mail to your reply-to domain may interact
with the object the email relates to as the user who leaked the mail. Because
the authentication around email has this weakness, some actions (like accepting
revisions) are not permitted over email.
This implementation is an attempt to balance utility and security, but makes
some sacrifices on both sides to achieve it because of the difficulty of
authenticating senders in the general case (e.g., where you are an open source
project and need to interact with users whose email accounts you have no control
over).
-If you leak a bunch of reply-to addresses by accident, you can change
-`phabricator.mail-key` in your configuration to invalidate all the old hashes.
-
You can also set `metamta.public-replies`, which will change how Phabricator
delivers email. Instead of sending each recipient a unique mail with a personal
reply-to address, it will send a single email to everyone with a public reply-to
address. This decreases security because anyone who can spoof a "From" address
can act as another user, but increases convenience if you use mailing lists and,
practically, is a reasonable setting for many installs. The reply-to address
will still contain a hash unique to the object it represents, so users who have
not received an email about an object can not blindly interact with it.
If you enable application email addresses, those addresses also use the weaker
"From" authentication mechanism.
NOTE: Phabricator does not currently attempt to verify "From" addresses because
this is technically complex, seems unreasonably difficult in the general case,
and no installs have had a need for it yet. If you have a specific case where a
reasonable mechanism exists to provide sender verification (e.g., DKIM
signatures are sufficient to authenticate the sender under your configuration,
or you are willing to require all users to sign their email), file a feature
request.
-= Testing and Debugging Inbound Email =
+
+Testing and Debugging Inbound Email
+===================================
You can use the `bin/mail` utility to test and review inbound mail. This can
help you determine if mail is being delivered to Phabricator or not:
phabricator/ $ ./bin/mail list-inbound # List inbound messages.
phabricator/ $ ./bin/mail show-inbound # Show details about a message.
You can also test receiving mail, but note that this just simulates receiving
the mail and doesn't send any information over the network. It is
primarily aimed at developing email handlers: it will still work properly
if your inbound email configuration is incorrect or even disabled.
phabricator/ $ ./bin/mail receive-test # Receive test message.
Run `bin/mail help <command>` for detailed help on using these commands.
-= Mailgun Setup =
+
+Mailgun Setup
+=============
To use Mailgun, you need a Mailgun account. You can sign up at
<http://www.mailgun.com>. Provided you have such an account, configure it
like this:
- Configure a mail domain according to Mailgun's instructions.
- Add a Mailgun route with a `catch_all()` rule which takes the action
`forward("https://phabricator.example.com/mail/mailgun/")`. Replace the
example domain with your actual domain.
- - Set the `mailgun.api-key` config key to your Mailgun API key.
+ - Configure a mailer in `cluster.mailers` with your Mailgun API key.
+
Postmark Setup
==============
To process inbound mail from Postmark, configure this URI as your inbound
webhook URI in the Postmark control panel:
```
https://<phabricator.yourdomain.com>/mail/postmark/
```
See also the Postmark section in @{article:Configuring Outbound Email} for
discussion of the remote address whitelist used to verify that requests this
endpoint receives are authentic requests originating from Postmark.
-= SendGrid Setup =
+SendGrid Setup
+==============
To use SendGrid, you need a SendGrid account with access to the "Parse API" for
inbound email. Provided you have such an account, configure it like this:
- Configure an MX record according to SendGrid's instructions, i.e. add
`phabricator.example.com MX 10 mx.sendgrid.net.` or similar.
- Go to the "Parse Incoming Emails" page on SendGrid
(<http://sendgrid.com/developer/reply>) and add the domain as the
"Hostname".
- Add the URL `https://phabricator.example.com/mail/sendgrid/` as the "Url",
using your domain (and HTTP instead of HTTPS if you are not configured with
SSL).
- If you get an error that the hostname "can't be located or verified", it
means your MX record is either incorrectly configured or hasn't propagated
yet.
- - Set `metamta.reply-handler-domain` to `phabricator.example.com`"
+ - Set `metamta.reply-handler-domain` to `phabricator.example.com`
(whatever you configured the MX record for).
That's it! If everything is working properly you should be able to send email
to `anything@phabricator.example.com` and it should appear in
`bin/mail list-inbound` within a few seconds.
-= Local MTA: Installing Mailparse =
+
+Local MTA: Installing Mailparse
+===============================
If you're going to run your own MTA, you need to install the PECL mailparse
extension. In theory, you can do that with:
$ sudo pecl install mailparse
You may run into an error like "needs mbstring". If so, try:
$ sudo yum install php-mbstring # or equivalent
$ sudo pecl install -n mailparse
If you get a linker error like this:
COUNTEREXAMPLE
PHP Warning: PHP Startup: Unable to load dynamic library
'/usr/lib64/php/modules/mailparse.so' - /usr/lib64/php/modules/mailparse.so:
undefined symbol: mbfl_name2no_encoding in Unknown on line 0
...you need to edit your php.ini file so that mbstring.so is loaded **before**
mailparse.so. This is not the default if you have individual files in
`php.d/`.
-= Local MTA: Configuring Sendmail =
+Local MTA: Configuring Sendmail
+===============================
Before you can configure Sendmail, you need to install Mailparse. See the
section "Installing Mailparse" above.
Sendmail is very difficult to configure. First, you need to configure it for
your domain so that mail can be delivered correctly. In broad strokes, this
probably means something like this:
- add an MX record;
- make sendmail listen on external interfaces;
- open up port 25 if necessary (e.g., in your EC2 security policy);
- add your host to /etc/mail/local-host-names; and
- restart sendmail.
Now, you can actually configure sendmail to deliver to Phabricator. In
`/etc/aliases`, add an entry like this:
phabricator: "| /path/to/phabricator/scripts/mail/mail_handler.php"
If you use the `PHABRICATOR_ENV` environmental variable to select a
configuration, you can pass the value to the script as an argument:
.../path/to/mail_handler.php <ENV>
This is an advanced feature which is rarely used. Most installs should run
without an argument.
After making this change, run `sudo newaliases`. Now you likely need to symlink
this script into `/etc/smrsh/`:
sudo ln -s /path/to/phabricator/scripts/mail/mail_handler.php /etc/smrsh/
Finally, edit `/etc/mail/virtusertable` and add an entry like this:
@yourdomain.com phabricator@localhost
That will forward all mail to @yourdomain.com to the Phabricator processing
script. Run `sudo /etc/mail/make` or similar and then restart sendmail with
`sudo /etc/init.d/sendmail restart`.
diff --git a/src/docs/user/configuration/configuring_outbound_email.diviner b/src/docs/user/configuration/configuring_outbound_email.diviner
index db04c2187..4d18ba0eb 100644
--- a/src/docs/user/configuration/configuring_outbound_email.diviner
+++ b/src/docs/user/configuration/configuring_outbound_email.diviner
@@ -1,334 +1,441 @@
@title Configuring Outbound Email
@group config
-Instructions for configuring Phabricator to send mail.
+Instructions for configuring Phabricator to send email and other types of
+messages, like text messages.
Overview
========
-Phabricator can send outbound email through several different mail services,
+Phabricator sends outbound messages through "mailers". Most mailers send
+email and most messages are email messages, but mailers may also send other
+types of messages (like text messages).
+
+Phabricator can send outbound messages through multiple different mailers,
including a local mailer or various third-party services. Options include:
-| Send Mail With | Setup | Cost | Inbound | Notes |
-|---------|-------|------|---------|-------|
-| Mailgun | Easy | Cheap | Yes | Recommended |
-| Postmark | Easy | Cheap | Yes | Recommended |
-| Amazon SES | Easy | Cheap | No | Recommended |
-| SendGrid | Medium | Cheap | Yes | Discouraged |
-| External SMTP | Medium | Varies | No | Gmail, etc. |
-| Local SMTP | Hard | Free | No | sendmail, postfix, etc |
-| Custom | Hard | Free | No | Write a custom mailer for some other service. |
-| Drop in a Hole | Easy | Free | No | Drops mail in a deep, dark hole. |
+| Send Mail With | Setup | Cost | Inbound | Media | Notes |
+|----------------|-------|------|---------|-------|-------|
+| Postmark | Easy | Cheap | Yes | Email | Recommended |
+| Mailgun | Easy | Cheap | Yes | Email | Recommended |
+| Amazon SES | Easy | Cheap | No | Email | |
+| SendGrid | Medium | Cheap | Yes | Email | |
+| Twilio | Easy | Cheap | No | SMS | Recommended |
+| Amazon SNS | Easy | Cheap | No | SMS | Recommended |
+| External SMTP | Medium | Varies | No | Email | Gmail, etc. |
+| Local SMTP | Hard | Free | No | Email | sendmail, postfix, etc |
+| Custom | Hard | Free | No | All | Write a custom mailer. |
+| Drop in a Hole | Easy | Free | No | All | Drops mail in a deep, dark hole. |
See below for details on how to select and configure mail delivery for each
mailer.
-Overall, Mailgun and SES are much easier to set up, and using one of them is
-recommended. In particular, Mailgun will also let you set up inbound email
-easily.
+For email, Postmark or Mailgun are recommended because they make it easy to
+set up inbound and outbound mail and have good track records in our production
+services. Other services will also generally work well, but they may be more
+difficult to set up.
+
+For SMS, Twilio or SNS are recommended. They're also your only upstream
+options.
-If you have some internal mail service you'd like to use you can also
-write a custom mailer, but this requires digging into the code.
+If you have some internal mail or messaging service you'd like to use you can
+also write a custom mailer, but this requires digging into the code.
Phabricator sends mail in the background, so the daemons need to be running for
it to be able to deliver mail. You should receive setup warnings if they are
not. For more information on using daemons, see
@{article:Managing Daemons with phd}.
Basics
======
-Regardless of how outbound email is delivered, you should configure these keys
-in your configuration:
+Before configuring outbound mail, you should first set up
+`metamta.default-address` in Configuration. This determines where mail is sent
+"From" by default.
- - **metamta.default-address** determines where mail is sent "From" by
- default. If your domain is `example.org`, set this to something like
- `noreply@example.org`.
- - **metamta.domain** should be set to your domain, e.g. `example.org`.
- - **metamta.can-send-as-user** should be left as `false` in most cases,
- but see the documentation for details.
+If your domain is `example.org`, set this to something
+like `noreply@example.org`.
+
+Ideally, this should be a valid, deliverable address that doesn't bounce if
+users accidentally send mail to it.
Configuring Mailers
===================
Configure one or more mailers by listing them in the the `cluster.mailers`
configuration option. Most installs only need to configure one mailer, but you
can configure multiple mailers to provide greater availability in the event of
a service disruption.
A valid `cluster.mailers` configuration looks something like this:
```lang=json
[
{
"key": "mycompany-mailgun",
"type": "mailgun",
"options": {
"domain": "mycompany.com",
"api-key": "..."
}
},
...
]
```
The supported keys for each mailer are:
- `key`: Required string. A unique name for this mailer.
- `type`: Required string. Identifies the type of mailer. See below for
options.
- `priority`: Optional string. Advanced option which controls load balancing
and failover behavior. See below for details.
- `options`: Optional map. Additional options for the mailer type.
- `inbound`: Optional bool. Use `false` to prevent this mailer from being
used to receive inbound mail.
- `outbound`: Optional bool. Use `false` to prevent this mailer from being
used to send outbound mail.
+ - `media`: Optional list<string>. Some mailers support delivering multiple
+ types of messages (like Email and SMS). If you want to configure a mailer
+ to support only a subset of possible message types, list only those message
+ types. Normally, you do not need to configure this. See below for a list
+ of media types.
-The `type` field can be used to select these third-party mailers:
+The `type` field can be used to select these mailer services:
- `mailgun`: Use Mailgun.
- `ses`: Use Amazon SES.
- - `sendgrid`: Use Sendgrid.
+ - `sendgrid`: Use SendGrid.
+ - `postmark`: Use Postmark.
+ - `twilio`: Use Twilio.
+ - `sns`: Use Amazon SNS.
It also supports these local mailers:
- `sendmail`: Use the local `sendmail` binary.
- `smtp`: Connect directly to an SMTP server.
- `test`: Internal mailer for testing. Does not send mail.
-You can also write your own mailer by extending
-`PhabricatorMailImplementationAdapter`.
+You can also write your own mailer by extending `PhabricatorMailAdapter`.
+
+The `media` field supports these values:
+
+ - `email`: Configure this mailer for email.
+ - `sms`: Configure this mailer for SMS.
Once you've selected a mailer, find the corresponding section below for
instructions on configuring it.
Setting Complex Configuration
=============================
Mailers can not be edited from the web UI. If mailers could be edited from
the web UI, it would give an attacker who compromised an administrator account
a lot of power: they could redirect mail to a server they control and then
intercept mail for any other account, including password reset mail.
For more information about locked configuration options, see
@{article:Configuration Guide: Locked and Hidden Configuration}.
Setting `cluster.mailers` from the command line using `bin/config set` can be
tricky because of shell escaping. The easiest way to do it is to use the
`--stdin` flag. First, put your desired configuration in a file like this:
```lang=json, name=mailers.json
[
{
"key": "test-mailer",
"type": "test"
}
]
```
Then set the value like this:
```
phabricator/ $ ./bin/config set --stdin cluster.mailers < mailers.json
```
For alternatives and more information on configuration, see
@{article:Configuration User Guide: Advanced Configuration}
-Mailer: Mailgun
-===============
-
-Mailgun is a third-party email delivery service. You can learn more at
-<http://www.mailgun.com>. Mailgun is easy to configure and works well.
-
-To use this mailer, set `type` to `mailgun`, then configure these `options`:
-
- - `api-key`: Required string. Your Mailgun API key.
- - `domain`: Required string. Your Mailgun domain.
-
-
Mailer: Postmark
================
-Postmark is a third-party email delivery serivice. You can learn more at
+| Media | Email
+|---------|
+| Inbound | Yes
+|---------|
+
+
+Postmark is a third-party email delivery service. You can learn more at
<https://www.postmarkapp.com/>.
To use this mailer, set `type` to `postmark`, then configure these `options`:
- `access-token`: Required string. Your Postmark access token.
- `inbound-addresses`: Optional list<string>. Address ranges which you
will accept inbound Postmark HTTP webook requests from.
The default address list is preconfigured with Postmark's address range, so
you generally will not need to set or adjust it.
The option accepts a list of CIDR ranges, like `1.2.3.4/16` (IPv4) or
`::ffff:0:0/96` (IPv6). The default ranges are:
```lang=json
[
- "50.31.156.6/32"
+ "50.31.156.6/32",
+ "50.31.156.77/32",
+ "18.217.206.57/32"
]
```
-The default address ranges were last updated in February 2018, and were
+The default address ranges were last updated in January 2019, and were
documented at: <https://postmarkapp.com/support/article/800-ips-for-firewalls>
+Mailer: Mailgun
+===============
+
+| Media | Email
+|---------|
+| Inbound | Yes
+|---------|
+
+Mailgun is a third-party email delivery service. You can learn more at
+<https://www.mailgun.com>. Mailgun is easy to configure and works well.
+
+To use this mailer, set `type` to `mailgun`, then configure these `options`:
+
+ - `api-key`: Required string. Your Mailgun API key.
+ - `domain`: Required string. Your Mailgun domain.
+
+
Mailer: Amazon SES
==================
+| Media | Email
+|---------|
+| Inbound | No
+|---------|
+
Amazon SES is Amazon's cloud email service. You can learn more at
-<http://aws.amazon.com/ses/>.
+<https://aws.amazon.com/ses/>.
To use this mailer, set `type` to `ses`, then configure these `options`:
- `access-key`: Required string. Your Amazon SES access key.
- `secret-key`: Required string. Your Amazon SES secret key.
- `endpoint`: Required string. Your Amazon SES endpoint.
NOTE: Amazon SES **requires you to verify your "From" address**. Configure
-which "From" address to use by setting "`metamta.default-address`" in your
+which "From" address to use by setting `metamta.default-address` in your
config, then follow the Amazon SES verification process to verify it. You
won't be able to send email until you do this!
+Mailer: Twilio
+==================
+
+| Media | SMS
+|---------|
+| Inbound | No
+|---------|
+
+Twilio is a third-party notification service. You can learn more at
+<https://www.twilio.com/>.
+
+
+To use this mailer, set `type` to `twilio`, then configure these options:
+
+ - `account-sid`: Your Twilio Account SID.
+ - `auth-token`: Your Twilio Auth Token.
+ - `from-number`: Number to send text messages from, in E.164 format
+ (like `+15551237890`).
+
+Mailer: Amazon SNS
+==================
+
+| Media | SMS
+|---------|
+| Inbound | No
+|---------|
+
+
+Amazon SNS is Amazon's cloud notification service. You can learn more at
+<https://aws.amazon.com/sns/>. Note that this mailer is only able to send
+SMS messages, not emails.
+
+To use this mailer, set `type` to `sns`, then configure these options:
+
+ - `access-key`: Required string. Your Amazon SNS access key.
+ - `secret-key`: Required string. Your Amazon SNS secret key.
+ - `endpoint`: Required string. Your Amazon SNS endpoint.
+ - `region`: Required string. Your Amazon SNS region.
+
+You can find the correct `region` value for your endpoint in the SNS
+documentation.
Mailer: SendGrid
================
+| Media | Email
+|---------|
+| Inbound | Yes
+|---------|
+
SendGrid is a third-party email delivery service. You can learn more at
-<http://sendgrid.com/>.
+<https://sendgrid.com/>.
You can configure SendGrid in two ways: you can send via SMTP or via the REST
API. To use SMTP, configure Phabricator to use an `smtp` mailer.
To use the REST API mailer, set `type` to `sendgrid`, then configure
these `options`:
- - `api-user`: Required string. Your SendGrid login name.
- `api-key`: Required string. Your SendGrid API key.
-NOTE: Users have experienced a number of odd issues with SendGrid, compared to
-fewer issues with other mailers. We discourage SendGrid unless you're already
-using it.
+Older versions of the SendGrid API used different sets of credentials,
+including an "API User". Make sure you're configuring your "API Key".
Mailer: Sendmail
================
-This requires a `sendmail` binary to be installed on
-the system. Most MTAs (e.g., sendmail, qmail, postfix) should do this, but your
-machine may not have one installed by default. For install instructions, consult
-the documentation for your favorite MTA.
+| Media | Email
+|---------|
+| Inbound | Requires Configuration
+|---------|
+
+
+This requires a `sendmail` binary to be installed on the system. Most MTAs
+(e.g., sendmail, qmail, postfix) should install one for you, but your machine
+may not have one installed by default. For install instructions, consult the
+documentation for your favorite MTA.
Since you'll be sending the mail yourself, you are subject to things like SPF
rules, blackholes, and MTA configuration which are beyond the scope of this
document. If you can already send outbound email from the command line or know
how to configure it, this option is straightforward. If you have no idea how to
-do any of this, strongly consider using Mailgun or Amazon SES instead.
+do any of this, strongly consider using Postmark or Mailgun instead.
To use this mailer, set `type` to `sendmail`. There are no `options` to
configure.
-Mailer: STMP
+Mailer: SMTP
============
+| Media | Email
+|---------|
+| Inbound | Requires Configuration
+|---------|
+
You can use this adapter to send mail via an external SMTP server, like Gmail.
To use this mailer, set `type` to `smtp`, then configure these `options`:
- `host`: Required string. The hostname of your SMTP server.
- `port`: Optional int. The port to connect to on your SMTP server.
- `user`: Optional string. Username used for authentication.
- `password`: Optional string. Password for authentication.
- `protocol`: Optional string. Set to `tls` or `ssl` if necessary. Use
`ssl` for Gmail.
Disable Mail
============
-To disable mail, just don't configure any mailers.
+| Media | All
+|---------|
+| Inbound | No
+|---------|
+
+
+To disable mail, just don't configure any mailers. (You can safely ignore the
+setup warning reminding you to set up mailers if you don't plan to configure
+any.)
Testing and Debugging Outbound Email
====================================
You can use the `bin/mail` utility to test, debug, and examine outbound mail. In
particular:
phabricator/ $ ./bin/mail list-outbound # List outbound mail.
phabricator/ $ ./bin/mail show-outbound # Show details about messages.
phabricator/ $ ./bin/mail send-test # Send test messages.
Run `bin/mail help <command>` for more help on using these commands.
+By default, `bin/mail send-test` sends email messages, but you can use
+the `--type` flag to send different types of messages.
+
You can monitor daemons using the Daemon Console (`/daemon/`, or click
**Daemon Console** from the homepage).
Priorities
==========
By default, Phabricator will try each mailer in order: it will try the first
mailer first. If that fails (for example, because the service is not available
at the moment) it will try the second mailer, and so on.
If you want to load balance between multiple mailers instead of using one as
a primary, you can set `priority`. Phabricator will start with mailers in the
highest priority group and go through them randomly, then fall back to the
next group.
For example, if you have two SMTP servers and you want to balance requests
between them and then fall back to Mailgun if both fail, configure priorities
like this:
```lang=json
[
{
"key": "smtp-uswest",
"type": "smtp",
"priority": 300,
"options": "..."
},
{
"key": "smtp-useast",
"type": "smtp",
"priority": 300,
"options": "..."
},
{
"key": "mailgun-fallback",
"type": "mailgun",
"options": "..."
}
}
```
Phabricator will start with servers in the highest priority group (the group
with the **largest** `priority` number). In this example, the highest group is
`300`, which has the two SMTP servers. They'll be tried in random order first.
If both fail, Phabricator will move on to the next priority group. In this
example, there are no other priority groups.
If it still hasn't sent the mail, Phabricator will try servers which are not
in any priority group, in the configured order. In this example there is
only one such server, so it will try to send via Mailgun.
Next Steps
==========
Continue by:
- @{article:Configuring Inbound Email} so users can reply to email they
receive about revisions and tasks to interact with them; or
- learning about daemons with @{article:Managing Daemons with phd}; or
- returning to the @{article:Configuration Guide}.
diff --git a/src/docs/user/userguide/multi_factor_auth.diviner b/src/docs/user/userguide/multi_factor_auth.diviner
index c17c80d29..eca85d0f9 100644
--- a/src/docs/user/userguide/multi_factor_auth.diviner
+++ b/src/docs/user/userguide/multi_factor_auth.diviner
@@ -1,134 +1,222 @@
@title User Guide: Multi-Factor Authentication
@group userguide
Explains how multi-factor authentication works in Phabricator.
Overview
========
Multi-factor authentication allows you to add additional credentials to your
account to make it more secure.
-This sounds complicated, but in most cases it just means that Phabricator will
-make sure you have your mobile phone (by sending you a text message or having
-you enter a code from a mobile application) before allowing you to log in or
-take certain "high security" actions (like changing your password).
+Once multi-factor authentication is configured on your account, you'll usually
+use your mobile phone to provide an authorization code or an extra confirmation
+when you try to log in to a new session or take certain actions (like changing
+your password).
Requiring you to prove you're really you by asking for something you know (your
password) //and// something you have (your mobile phone) makes it much harder
for attackers to access your account. The phone is an additional "factor" which
protects your account from attacks.
-Requiring re-authentication before performing high security actions further
-limits the damage an attacker can do even if they manage to compromise a
-login session.
-
How Multi-Factor Authentication Works
=====================================
If you've configured multi-factor authentication and try to log in to your
-account or take certain high security actions (like changing your password),
+account or take certain sensitive actions (like changing your password),
you'll be stopped and asked to enter additional credentials.
-Usually, this means you'll receive an SMS with a security code on your phone, or
-you'll open an app on your phone which will show you a security code.
-In both cases, you'll enter the security code into Phabricator.
+Usually, this means you'll receive an SMS with a authorization code on your
+phone, or you'll open an app on your phone which will show you a authorization
+code or ask you to confirm the action. If you're given a authorization code,
+you'll enter it into Phabricator.
If you're logging in, Phabricator will log you in after you enter the code.
-If you're taking a high security action, Phabricator will put your account in
-"high security" mode for a few minutes. In this mode, you can take high security
-actions like changing passwords or SSH keys freely without entering any more
-credentials. You can explicitly leave high security once you're done performing
-account management, or your account will naturally return to normal security
-after a short period of time.
+If you're taking a sensitive action, Phabricator will sometimes put your
+account in "high security" mode for a few minutes. In this mode, you can take
+sensitive actions like changing passwords or SSH keys freely, without
+entering any more credentials.
+
+You can explicitly leave high security once you're done performing account
+management, or your account will naturally return to normal security after a
+short period of time.
While your account is in high security, you'll see a notification on screen
with instructions for returning to normal security.
Configuring Multi-Factor Authentication
=======================================
To manage authentication factors for your account, go to
-Settings > Multi-Factor Auth. You can use this control panel to add or remove
-authentication factors from your account.
+{nav Settings > Multi-Factor Auth}. You can use this control panel to add
+or remove authentication factors from your account.
You can also rename a factor by clicking the name. This can help you identify
factors if you have several similar factors attached to your account.
For a description of the available factors, see the next few sections.
Factor: Mobile Phone App (TOTP)
===============================
TOTP stands for "Time-based One-Time Password". This factor operates by having
-you enter security codes from your mobile phone into Phabricator. The codes
+you enter authorization codes from your mobile phone into Phabricator. The codes
change every 30 seconds, so you will need to have your phone with you in order
to enter them.
To use this factor, you'll download an application onto your smartphone which
can compute these codes. Two applications which work well are **Authy** and
**Google Authenticator**. These applications are free, and you can find and
download them from the appropriate store on your device.
Your company may have a preferred application, or may use some other
application, so check any in-house documentation for details. In general, any
TOTP application should work properly.
After you've downloaded the application onto your phone, use the Phabricator
-settings panel to add a factor to your account. You'll be prompted to enter a
-master key into your phone, and then read a security code from your phone and
-type it into Phabricator.
+settings panel to add a factor to your account. You'll be prompted to scan a
+QR code, and then read an authorization code from your phone and type it into
+Phabricator.
Later, when you need to authenticate, you'll follow this same process: launch
-the application, read the security code, and type it into Phabricator. This will
-prove you have your phone.
+the application, read the authorization code, and type it into Phabricator.
+This will prove you have your phone.
Don't lose your phone! You'll need it to log into Phabricator in the future.
-Recovering from Lost Factors
-============================
+Factor: SMS
+===========
+
+This factor operates by texting you a short authorization code when you try to
+log in or perform a sensitive action.
+
+To use SMS, first add your phone number in {nav Settings > Contact Numbers}.
+Once a primary contact number is configured on your account, you'll be able
+to add an SMS factor.
+
+To enroll in SMS, you'll be sent a confirmation code to make sure your contact
+number is correct and SMS is being delivered properly. Enter it when prompted.
+
+When you're asked to confirm your identity in the future, you'll be texted
+an authorization code to enter into the prompt.
+
+(WARNING) SMS is a very weak factor and can be compromised or intercepted. For
+details, see: <https://phurl.io/u/sms>.
+
+
+Factor: Duo
+===========
+
+This factor supports integration with [[ https://duo.com/ | Duo Security ]], a
+third-party authentication service popular with enterprises that have a lot of
+policies to enforce.
+
+To use Duo, you'll install the Duo application on your phone. When you try
+to take a sensitive action, you'll be asked to confirm it in the application.
+
+
+Administration: Configuration
+=============================
+
+New Phabricator installs start without any multi-factor providers enabled.
+Users won't be able to add new factors until you set up multi-factor
+authentication by configuring at least one provider.
+
+Configure new providers in {nav Auth > Multi-Factor}.
+
+Providers may be in these states:
+
+ - **Active**: Users may add new factors. Users will be prompted to respond
+ to challenges from these providers when they take a sensitive action.
+ - **Deprecated**: Users may not add new factors, but they will still be
+ asked to respond to challenges from exising factors.
+ - **Disabled**: Users may not add new factors, and existing factors will
+ not be used. If MFA is required and a user only has disabled factors,
+ they will be forced to add a new factor.
-If you've lost a factor associated with your account (for example, your phone
-has been lost or damaged), an administrator can strip the factor off your
-account so that you can log in without it.
+If you want to change factor types for your organization, the process will
+normally look something like this:
+
+ - Configure and test a new provider.
+ - Deprecate the old provider.
+ - Notify users that the old provider is deprecated and that they should move
+ to the new provider at their convenience, but before some upcoming
+ deadline.
+ - Once the deadline arrives, disable the old provider.
+
+
+Administration: Requiring MFA
+=============================
+
+As an administrator, you can require all users to add MFA to their accounts by
+setting the `security.require-multi-factor-auth` option in Config.
+
+
+Administration: Recovering from Lost Factors
+============================================
+
+If a user has lost a factor associated with their account (for example, their
+phone has been lost or damaged), an administrator with host access can strip
+the factor off their account so that they can log in without it.
IMPORTANT: Before stripping factors from a user account, be absolutely certain
that the user is who they claim to be!
It is important to verify the user is who they claim they are before stripping
factors because an attacker might pretend to be a user who has lost their phone
in order to bypass multi-factor authentication. It is much easier for a typical
attacker to spoof an email with a sad story in it than it is for a typical
attacker to gain access to a mobile phone.
A good way to verify user identity is to meet them in person and have them
solemnly swear an oath that they lost their phone and are very sorry and
definitely won't do it again. You can also work out a secret handshake in
advance and require them to perform it. But no matter what you do, be certain
the user (not an attacker //pretending// to be the user) is really the one
making the request before stripping factors.
-After verifying identity, administrators can strip authentication factors from
-user accounts using the `bin/auth strip` command. For example, to strip all
-factors from the account of a user who has lost their phone, run this command:
+After verifying identity, administrators with host access can strip
+authentication factors from user accounts using the `bin/auth strip` command.
+For example, to strip all factors from the account of a user who has lost
+their phone, run this command:
```lang=console
# Strip all factors from a given user account.
phabricator/ $ ./bin/auth strip --user <username> --all-types
```
You can run `bin/auth help strip` for more detail and all available flags and
arguments.
-This command can selectively strip types of factors. You can use
-`bin/auth list-factors` for a list of available factor types.
+This command can selectively strip factors by factor type. You can use
+`bin/auth list-factors` to get a list of available factor types.
```lang=console
# Show supported factor types.
phabricator/ $ ./bin/auth list-factors
```
+
+Once you've identified the factor types you want to strip, you can strip
+matching factors by using the `--type` flag to specify one or more factor
+types:
+
+```lang=console
+# Strip all SMS and TOTP factors for a user.
+phabricator/ $ ./bin/auth strip --user <username> --type sms --type totp
+```
+
+The `bin/auth strip` command can also selectively strip factors for certain
+providers. This is more granular than stripping all factors of a given type.
+You can use `bin/auth list-mfa-providers` to get a list of providers.
+
+Once you have a provider PHID, use `--provider` to select factors to strip:
+
+```lang=console
+# Strip all factors for a particular provider.
+phabricator/ $ ./bin/auth strip --user <username> --provider <providerPHID>
+```
diff --git a/src/infrastructure/cluster/config/PhabricatorClusterMailersConfigType.php b/src/infrastructure/cluster/config/PhabricatorClusterMailersConfigType.php
index 387014289..e9b01f8cb 100644
--- a/src/infrastructure/cluster/config/PhabricatorClusterMailersConfigType.php
+++ b/src/infrastructure/cluster/config/PhabricatorClusterMailersConfigType.php
@@ -1,104 +1,105 @@
<?php
final class PhabricatorClusterMailersConfigType
extends PhabricatorJSONConfigType {
const TYPEKEY = 'cluster.mailers';
public function validateStoredValue(
PhabricatorConfigOption $option,
$value) {
if ($value === null) {
return;
}
if (!is_array($value)) {
throw $this->newException(
pht(
'Mailer cluster configuration is not valid: it should be a list '.
'of mailer configurations.'));
}
foreach ($value as $index => $spec) {
if (!is_array($spec)) {
throw $this->newException(
pht(
'Mailer cluster configuration is not valid: each entry in the '.
'list must be a dictionary describing a mailer, but the value '.
'with index "%s" is not a dictionary.',
$index));
}
}
- $adapters = PhabricatorMailImplementationAdapter::getAllAdapters();
+ $adapters = PhabricatorMailAdapter::getAllAdapters();
$map = array();
foreach ($value as $index => $spec) {
try {
PhutilTypeSpec::checkMap(
$spec,
array(
'key' => 'string',
'type' => 'string',
'priority' => 'optional int',
'options' => 'optional wild',
'inbound' => 'optional bool',
'outbound' => 'optional bool',
+ 'media' => 'optional list<string>',
));
} catch (Exception $ex) {
throw $this->newException(
pht(
'Mailer configuration has an invalid mailer specification '.
'(at index "%s"): %s.',
$index,
$ex->getMessage()));
}
$key = $spec['key'];
if (isset($map[$key])) {
throw $this->newException(
pht(
'Mailer configuration is invalid: multiple mailers have the same '.
'key ("%s"). Each mailer must have a unique key.',
$key));
}
$map[$key] = true;
$priority = idx($spec, 'priority');
if ($priority !== null && $priority <= 0) {
throw $this->newException(
pht(
'Mailer configuration ("%s") is invalid: priority must be '.
'greater than 0.',
$key));
}
$type = $spec['type'];
if (!isset($adapters[$type])) {
throw $this->newException(
pht(
'Mailer configuration ("%s") is invalid: mailer type ("%s") is '.
'unknown. Supported mailer types are: %s.',
$key,
$type,
implode(', ', array_keys($adapters))));
}
$options = idx($spec, 'options', array());
try {
$defaults = $adapters[$type]->newDefaultOptions();
$options = $options + $defaults;
id(clone $adapters[$type])->setOptions($options);
} catch (Exception $ex) {
throw $this->newException(
pht(
'Mailer configuration ("%s") specifies invalid options for '.
'mailer: %s',
$key,
$ex->getMessage()));
}
}
}
}
diff --git a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php
index 312281617..68263fccb 100644
--- a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php
+++ b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php
@@ -1,275 +1,266 @@
<?php
/**
* @task implementation Job Implementation
*/
final class PhabricatorWorkerBulkJob
extends PhabricatorWorkerDAO
implements
PhabricatorPolicyInterface,
PhabricatorSubscribableInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorDestructibleInterface {
const STATUS_CONFIRM = 'confirm';
const STATUS_WAITING = 'waiting';
const STATUS_RUNNING = 'running';
const STATUS_COMPLETE = 'complete';
protected $authorPHID;
protected $jobTypeKey;
protected $status;
protected $parameters = array();
protected $size;
protected $isSilent;
private $jobImplementation = self::ATTACHABLE;
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'parameters' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'jobTypeKey' => 'text32',
'status' => 'text32',
'size' => 'uint32',
'isSilent' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_type' => array(
'columns' => array('jobTypeKey'),
),
'key_author' => array(
'columns' => array('authorPHID'),
),
'key_status' => array(
'columns' => array('status'),
),
),
) + parent::getConfiguration();
}
public static function initializeNewJob(
PhabricatorUser $actor,
PhabricatorWorkerBulkJobType $type,
array $parameters) {
$job = id(new PhabricatorWorkerBulkJob())
->setAuthorPHID($actor->getPHID())
->setJobTypeKey($type->getBulkJobTypeKey())
->setParameters($parameters)
->attachJobImplementation($type)
->setIsSilent(0);
$job->setSize($job->computeSize());
return $job;
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorWorkerBulkJobPHIDType::TYPECONST);
}
public function getMonitorURI() {
return '/daemon/bulk/monitor/'.$this->getID().'/';
}
public function getManageURI() {
return '/daemon/bulk/view/'.$this->getID().'/';
}
public function getParameter($key, $default = null) {
return idx($this->parameters, $key, $default);
}
public function setParameter($key, $value) {
$this->parameters[$key] = $value;
return $this;
}
public function loadTaskStatusCounts() {
$table = new PhabricatorWorkerBulkTask();
$conn_r = $table->establishConnection('r');
$rows = queryfx_all(
$conn_r,
'SELECT status, COUNT(*) N FROM %T WHERE bulkJobPHID = %s
GROUP BY status',
$table->getTableName(),
$this->getPHID());
return ipull($rows, 'N', 'status');
}
public function newContentSource() {
return PhabricatorContentSource::newForSource(
PhabricatorBulkContentSource::SOURCECONST,
array(
'jobID' => $this->getID(),
));
}
public function getStatusIcon() {
$map = array(
self::STATUS_CONFIRM => 'fa-question',
self::STATUS_WAITING => 'fa-clock-o',
self::STATUS_RUNNING => 'fa-clock-o',
self::STATUS_COMPLETE => 'fa-check grey',
);
return idx($map, $this->getStatus(), 'none');
}
public function getStatusName() {
$map = array(
self::STATUS_CONFIRM => pht('Confirming'),
self::STATUS_WAITING => pht('Waiting'),
self::STATUS_RUNNING => pht('Running'),
self::STATUS_COMPLETE => pht('Complete'),
);
return idx($map, $this->getStatus(), $this->getStatus());
}
public function isConfirming() {
return ($this->getStatus() == self::STATUS_CONFIRM);
}
/* -( Job Implementation )------------------------------------------------- */
protected function getJobImplementation() {
return $this->assertAttached($this->jobImplementation);
}
public function attachJobImplementation(PhabricatorWorkerBulkJobType $type) {
$this->jobImplementation = $type;
return $this;
}
private function computeSize() {
return $this->getJobImplementation()->getJobSize($this);
}
public function getCancelURI() {
return $this->getJobImplementation()->getCancelURI($this);
}
public function getDoneURI() {
return $this->getJobImplementation()->getDoneURI($this);
}
public function getDescriptionForConfirm() {
return $this->getJobImplementation()->getDescriptionForConfirm($this);
}
public function createTasks() {
return $this->getJobImplementation()->createTasks($this);
}
public function runTask(
PhabricatorUser $actor,
PhabricatorWorkerBulkTask $task) {
return $this->getJobImplementation()->runTask($actor, $this, $task);
}
public function getJobName() {
return $this->getJobImplementation()->getJobName($this);
}
public function getCurtainActions(PhabricatorUser $viewer) {
return $this->getJobImplementation()->getCurtainActions($viewer, $this);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::getMostOpenPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getAuthorPHID();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function describeAutomaticCapability($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_EDIT:
return pht('Only the owner of a bulk job can edit it.');
default:
return null;
}
}
/* -( PhabricatorSubscribableInterface )----------------------------------- */
public function isAutomaticallySubscribed($phid) {
return false;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorWorkerBulkJobEditor();
}
- public function getApplicationTransactionObject() {
- return $this;
- }
-
public function getApplicationTransactionTemplate() {
return new PhabricatorWorkerBulkJobTransaction();
}
- public function willRenderTimeline(
- PhabricatorApplicationTransactionView $timeline,
- AphrontRequest $request) {
- return $timeline;
- }
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
// We're only removing the actual task objects. This may leave stranded
// workers in the queue itself, but they'll just flush out automatically
// when they can't load bulk job data.
$task_table = new PhabricatorWorkerBulkTask();
$conn_w = $task_table->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE bulkJobPHID = %s',
$task_table->getPHID(),
$this->getPHID());
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/infrastructure/events/constant/PhabricatorEventType.php b/src/infrastructure/events/constant/PhabricatorEventType.php
index c92503376..3dea7b36e 100644
--- a/src/infrastructure/events/constant/PhabricatorEventType.php
+++ b/src/infrastructure/events/constant/PhabricatorEventType.php
@@ -1,28 +1,27 @@
<?php
/**
* For detailed explanations of these events, see
* @{article:Events User Guide: Installing Event Listeners}.
*/
final class PhabricatorEventType extends PhutilEventType {
const TYPE_DIFFERENTIAL_WILLMARKGENERATED = 'differential.willMarkGenerated';
const TYPE_DIFFUSION_DIDDISCOVERCOMMIT = 'diffusion.didDiscoverCommit';
const TYPE_DIFFUSION_LOOKUPUSER = 'diffusion.lookupUser';
const TYPE_TEST_DIDRUNTEST = 'test.didRunTest';
const TYPE_UI_DIDRENDERACTIONS = 'ui.didRenderActions';
const TYPE_UI_WILLRENDEROBJECTS = 'ui.willRenderObjects';
const TYPE_UI_DDIDRENDEROBJECT = 'ui.didRenderObject';
const TYPE_UI_DIDRENDEROBJECTS = 'ui.didRenderObjects';
const TYPE_UI_WILLRENDERPROPERTIES = 'ui.willRenderProperties';
const TYPE_PEOPLE_DIDRENDERMENU = 'people.didRenderMenu';
const TYPE_AUTH_WILLREGISTERUSER = 'auth.willRegisterUser';
- const TYPE_AUTH_WILLLOGINUSER = 'auth.willLoginUser';
const TYPE_AUTH_DIDVERIFYEMAIL = 'auth.didVerifyEmail';
}
diff --git a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php
index 5169c08ff..ec07d018b 100644
--- a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php
+++ b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php
@@ -1,1679 +1,1724 @@
<?php
final class PhabricatorUSEnglishTranslation
extends PhutilTranslation {
public function getLocaleCode() {
return 'en_US';
}
protected function getTranslations() {
return array(
'These %d configuration value(s) are related:' => array(
'This configuration value is related:',
'These configuration values are related:',
),
'%s Task(s)' => array('Task', 'Tasks'),
'%s ERROR(S)' => array('ERROR', 'ERRORS'),
'%d Error(s)' => array('%d Error', '%d Errors'),
'%d Warning(s)' => array('%d Warning', '%d Warnings'),
'%d Auto-Fix(es)' => array('%d Auto-Fix', '%d Auto-Fixes'),
'%d Advice(s)' => array('%d Advice', '%d Pieces of Advice'),
'%d Detail(s)' => array('%d Detail', '%d Details'),
'(%d line(s))' => array('(%d line)', '(%d lines)'),
'%d line(s)' => array('%d line', '%d lines'),
'%d path(s)' => array('%d path', '%d paths'),
'%d diff(s)' => array('%d diff', '%d diffs'),
'%s Answer(s)' => array('%s Answer', '%s Answers'),
'Show %d Comment(s)' => array('Show %d Comment', 'Show %d Comments'),
'%s DIFF LINK(S)' => array('DIFF LINK', 'DIFF LINKS'),
'You successfully created %d diff(s).' => array(
'You successfully created %d diff.',
'You successfully created %d diffs.',
),
'Diff creation failed; see body for %s error(s).' => array(
'Diff creation failed; see body for error.',
'Diff creation failed; see body for errors.',
),
'There are %d raw fact(s) in storage.' => array(
'There is %d raw fact in storage.',
'There are %d raw facts in storage.',
),
'There are %d aggregate fact(s) in storage.' => array(
'There is %d aggregate fact in storage.',
'There are %d aggregate facts in storage.',
),
'%s Commit(s) Awaiting Audit' => array(
'%s Commit Awaiting Audit',
'%s Commits Awaiting Audit',
),
'%s Problem Commit(s)' => array(
'%s Problem Commit',
'%s Problem Commits',
),
'%s Review(s) Blocking Others' => array(
'%s Review Blocking Others',
'%s Reviews Blocking Others',
),
'%s Review(s) Need Attention' => array(
'%s Review Needs Attention',
'%s Reviews Need Attention',
),
'%s Review(s) Waiting on Others' => array(
'%s Review Waiting on Others',
'%s Reviews Waiting on Others',
),
'%s Active Review(s)' => array(
'%s Active Review',
'%s Active Reviews',
),
'%s Flagged Object(s)' => array(
'%s Flagged Object',
'%s Flagged Objects',
),
'%s Object(s) Tracked' => array(
'%s Object Tracked',
'%s Objects Tracked',
),
'%s Assigned Task(s)' => array(
'%s Assigned Task',
'%s Assigned Tasks',
),
'Show %d Lint Message(s)' => array(
'Show %d Lint Message',
'Show %d Lint Messages',
),
'Hide %d Lint Message(s)' => array(
'Hide %d Lint Message',
'Hide %d Lint Messages',
),
'This is a binary file. It is %s byte(s) in length.' => array(
'This is a binary file. It is %s byte in length.',
'This is a binary file. It is %s bytes in length.',
),
'%s Action(s) Have No Effect' => array(
'Action Has No Effect',
'Actions Have No Effect',
),
'%s Action(s) With No Effect' => array(
'Action With No Effect',
'Actions With No Effect',
),
'Some of your %s action(s) have no effect:' => array(
'One of your actions has no effect:',
'Some of your actions have no effect:',
),
'Apply remaining %d action(s)?' => array(
'Apply remaining action?',
'Apply remaining actions?',
),
'Apply %d Other Action(s)' => array(
'Apply Remaining Action',
'Apply Remaining Actions',
),
'The %s action(s) you are taking have no effect:' => array(
'The action you are taking has no effect:',
'The actions you are taking have no effect:',
),
'%s edited member(s), added %d: %s; removed %d: %s.' =>
'%s edited members, added: %3$s; removed: %5$s.',
'%s added %s member(s): %s.' => array(
array(
'%s added a member: %3$s.',
'%s added members: %3$s.',
),
),
'%s removed %s member(s): %s.' => array(
array(
'%s removed a member: %3$s.',
'%s removed members: %3$s.',
),
),
'%s edited project(s), added %s: %s; removed %s: %s.' =>
'%s edited projects, added: %3$s; removed: %5$s.',
'%s added %s project(s): %s.' => array(
array(
'%s added a project: %3$s.',
'%s added projects: %3$s.',
),
),
'%s removed %s project(s): %s.' => array(
array(
'%s removed a project: %3$s.',
'%s removed projects: %3$s.',
),
),
'%s merged %s task(s): %s.' => array(
array(
'%s merged a task: %3$s.',
'%s merged tasks: %3$s.',
),
),
'%s merged %s task(s) %s into %s.' => array(
array(
'%s merged %3$s into %4$s.',
'%s merged tasks %3$s into %4$s.',
),
),
'%s added %s voting user(s): %s.' => array(
array(
'%s added a voting user: %3$s.',
'%s added voting users: %3$s.',
),
),
'%s removed %s voting user(s): %s.' => array(
array(
'%s removed a voting user: %3$s.',
'%s removed voting users: %3$s.',
),
),
'%s added %s subtask(s): %s.' => array(
array(
'%s added a subtask: %3$s.',
'%s added subtasks: %3$s.',
),
),
'%s added %s parent task(s): %s.' => array(
array(
'%s added a parent task: %3$s.',
'%s added parent tasks: %3$s.',
),
),
'%s removed %s subtask(s): %s.' => array(
array(
'%s removed a subtask: %3$s.',
'%s removed subtasks: %3$s.',
),
),
'%s removed %s parent task(s): %s.' => array(
array(
'%s removed a parent task: %3$s.',
'%s removed parent tasks: %3$s.',
),
),
'%s added %s subtask(s) for %s: %s.' => array(
array(
'%s added a subtask for %3$s: %4$s.',
'%s added subtasks for %3$s: %4$s.',
),
),
'%s added %s parent task(s) for %s: %s.' => array(
array(
'%s added a parent task for %3$s: %4$s.',
'%s added parent tasks for %3$s: %4$s.',
),
),
'%s removed %s subtask(s) for %s: %s.' => array(
array(
'%s removed a subtask for %3$s: %4$s.',
'%s removed subtasks for %3$s: %4$s.',
),
),
'%s removed %s parent task(s) for %s: %s.' => array(
array(
'%s removed a parent task for %3$s: %4$s.',
'%s removed parent tasks for %3$s: %4$s.',
),
),
'%s edited subtask(s), added %s: %s; removed %s: %s.' =>
'%s edited subtasks, added: %3$s; removed: %5$s.',
'%s edited subtask(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited subtasks for %s, added: %4$s; removed: %6$s.',
'%s edited parent task(s), added %s: %s; removed %s: %s.' =>
'%s edited parent tasks, added: %3$s; removed: %5$s.',
'%s edited parent task(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited parent tasks for %s, added: %4$s; removed: %6$s.',
'%s edited answer(s), added %s: %s; removed %d: %s.' =>
'%s edited answers, added: %3$s; removed: %5$s.',
'%s added %s answer(s): %s.' => array(
array(
'%s added an answer: %3$s.',
'%s added answers: %3$s.',
),
),
'%s removed %s answer(s): %s.' => array(
array(
'%s removed a answer: %3$s.',
'%s removed answers: %3$s.',
),
),
'%s edited question(s), added %s: %s; removed %s: %s.' =>
'%s edited questions, added: %3$s; removed: %5$s.',
'%s added %s question(s): %s.' => array(
array(
'%s added a question: %3$s.',
'%s added questions: %3$s.',
),
),
'%s removed %s question(s): %s.' => array(
array(
'%s removed a question: %3$s.',
'%s removed questions: %3$s.',
),
),
'%s edited mock(s), added %s: %s; removed %s: %s.' =>
'%s edited mocks, added: %3$s; removed: %5$s.',
'%s added %s mock(s): %s.' => array(
array(
'%s added a mock: %3$s.',
'%s added mocks: %3$s.',
),
),
'%s removed %s mock(s): %s.' => array(
array(
'%s removed a mock: %3$s.',
'%s removed mocks: %3$s.',
),
),
'%s added %s task(s): %s.' => array(
array(
'%s added a task: %3$s.',
'%s added tasks: %3$s.',
),
),
'%s removed %s task(s): %s.' => array(
array(
'%s removed a task: %3$s.',
'%s removed tasks: %3$s.',
),
),
'%s edited file(s), added %s: %s; removed %s: %s.' =>
'%s edited files, added: %3$s; removed: %5$s.',
'%s added %s file(s): %s.' => array(
array(
'%s added a file: %3$s.',
'%s added files: %3$s.',
),
),
'%s removed %s file(s): %s.' => array(
array(
'%s removed a file: %3$s.',
'%s removed files: %3$s.',
),
),
'%s edited contributor(s), added %s: %s; removed %s: %s.' =>
'%s edited contributors, added: %3$s; removed: %5$s.',
'%s added %s contributor(s): %s.' => array(
array(
'%s added a contributor: %3$s.',
'%s added contributors: %3$s.',
),
),
'%s removed %s contributor(s): %s.' => array(
array(
'%s removed a contributor: %3$s.',
'%s removed contributors: %3$s.',
),
),
'%s edited %s reviewer(s), added %s: %s; removed %s: %s.' =>
'%s edited reviewers, added: %4$s; removed: %6$s.',
'%s edited %s reviewer(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited reviewers for %3$s, added: %5$s; removed: %7$s.',
'%s added %s reviewer(s): %s.' => array(
array(
'%s added a reviewer: %3$s.',
'%s added reviewers: %3$s.',
),
),
'%s added %s reviewer(s) for %s: %s.' => array(
array(
'%s added a reviewer for %3$s: %4$s.',
'%s added reviewers for %3$s: %4$s.',
),
),
'%s removed %s reviewer(s): %s.' => array(
array(
'%s removed a reviewer: %3$s.',
'%s removed reviewers: %3$s.',
),
),
'%s removed %s reviewer(s) for %s: %s.' => array(
array(
'%s removed a reviewer for %3$s: %4$s.',
'%s removed reviewers for %3$s: %4$s.',
),
),
'%d other(s)' => array(
'1 other',
'%d others',
),
'%s edited subscriber(s), added %d: %s; removed %d: %s.' =>
'%s edited subscribers, added: %3$s; removed: %5$s.',
'%s added %d subscriber(s): %s.' => array(
array(
'%s added a subscriber: %3$s.',
'%s added subscribers: %3$s.',
),
),
'%s removed %d subscriber(s): %s.' => array(
array(
'%s removed a subscriber: %3$s.',
'%s removed subscribers: %3$s.',
),
),
'%s edited watcher(s), added %s: %s; removed %d: %s.' =>
'%s edited watchers, added: %3$s; removed: %5$s.',
'%s added %s watcher(s): %s.' => array(
array(
'%s added a watcher: %3$s.',
'%s added watchers: %3$s.',
),
),
'%s removed %s watcher(s): %s.' => array(
array(
'%s removed a watcher: %3$s.',
'%s removed watchers: %3$s.',
),
),
'%s edited participant(s), added %d: %s; removed %d: %s.' =>
'%s edited participants, added: %3$s; removed: %5$s.',
'%s added %d participant(s): %s.' => array(
array(
'%s added a participant: %3$s.',
'%s added participants: %3$s.',
),
),
'%s removed %d participant(s): %s.' => array(
array(
'%s removed a participant: %3$s.',
'%s removed participants: %3$s.',
),
),
'%s edited image(s), added %d: %s; removed %d: %s.' =>
'%s edited images, added: %3$s; removed: %5$s',
'%s added %d image(s): %s.' => array(
array(
'%s added an image: %3$s.',
'%s added images: %3$s.',
),
),
'%s removed %d image(s): %s.' => array(
array(
'%s removed an image: %3$s.',
'%s removed images: %3$s.',
),
),
'%s Line(s)' => array(
'%s Line',
'%s Lines',
),
'Indexing %d object(s) of type %s.' => array(
'Indexing %d object of type %s.',
'Indexing %d object of type %s.',
),
'Run these %d command(s):' => array(
'Run this command:',
'Run these commands:',
),
'Install these %d PHP extension(s):' => array(
'Install this PHP extension:',
'Install these PHP extensions:',
),
'The current Phabricator configuration has these %d value(s):' => array(
'The current Phabricator configuration has this value:',
'The current Phabricator configuration has these values:',
),
'The current MySQL configuration has these %d value(s):' => array(
'The current MySQL configuration has this value:',
'The current MySQL configuration has these values:',
),
'You can update these %d value(s) here:' => array(
'You can update this value here:',
'You can update these values here:',
),
'The current PHP configuration has these %d value(s):' => array(
'The current PHP configuration has this value:',
'The current PHP configuration has these values:',
),
'To update these %d value(s), edit your PHP configuration file.' => array(
'To update this %d value, edit your PHP configuration file.',
'To update these %d values, edit your PHP configuration file.',
),
'To update these %d value(s), edit your PHP configuration file, located '.
'here:' => array(
'To update this value, edit your PHP configuration file, located '.
'here:',
'To update these values, edit your PHP configuration file, located '.
'here:',
),
'PHP also loaded these %s configuration file(s):' => array(
'PHP also loaded this configuration file:',
'PHP also loaded these configuration files:',
),
'%s added %d inline comment(s).' => array(
array(
'%s added an inline comment.',
'%s added inline comments.',
),
),
'%s comment(s)' => array('%s comment', '%s comments'),
'%s rejection(s)' => array('%s rejection', '%s rejections'),
'%s update(s)' => array('%s update', '%s updates'),
'This configuration value is defined in these %d '.
'configuration source(s): %s.' => array(
'This configuration value is defined in this '.
'configuration source: %2$s.',
'This configuration value is defined in these %d '.
'configuration sources: %s.',
),
'%s Open Pull Request(s)' => array(
'%s Open Pull Request',
'%s Open Pull Requests',
),
'Stale (%s day(s))' => array(
'Stale (%s day)',
'Stale (%s days)',
),
'Old (%s day(s))' => array(
'Old (%s day)',
'Old (%s days)',
),
'%s Commit(s)' => array(
'%s Commit',
'%s Commits',
),
'%s attached %d file(s): %s.' => array(
array(
'%s attached a file: %3$s.',
'%s attached files: %3$s.',
),
),
'%s detached %d file(s): %s.' => array(
array(
'%s detached a file: %3$s.',
'%s detached files: %3$s.',
),
),
'%s changed file(s), attached %d: %s; detached %d: %s.' =>
'%s changed files, attached: %3$s; detached: %5$s.',
'%s added %s parent revision(s): %s.' => array(
array(
'%s added a parent revision: %3$s.',
'%s added parent revisions: %3$s.',
),
),
'%s added %s parent revision(s) for %s: %s.' => array(
array(
'%s added a parent revision for %3$s: %4$s.',
'%s added parent revisions for %3$s: %4$s.',
),
),
'%s removed %s parent revision(s): %s.' => array(
array(
'%s removed a parent revision: %3$s.',
'%s removed parent revisions: %3$s.',
),
),
'%s removed %s parent revision(s) for %s: %s.' => array(
array(
'%s removed a parent revision for %3$s: %4$s.',
'%s removed parent revisions for %3$s: %4$s.',
),
),
'%s edited parent revision(s), added %s: %s; removed %s: %s.' => array(
'%s edited parent revisions, added: %3$s; removed: %5$s.',
),
'%s edited parent revision(s) for %s, '.
'added %s: %s; removed %s: %s.' => array(
'%s edited parent revisions for %s, added: %3$s; removed: %5$s.',
),
'%s added %s child revision(s): %s.' => array(
array(
'%s added a child revision: %3$s.',
'%s added child revisions: %3$s.',
),
),
'%s added %s child revision(s) for %s: %s.' => array(
array(
'%s added a child revision for %3$s: %4$s.',
'%s added child revisions for %3$s: %4$s.',
),
),
'%s removed %s child revision(s): %s.' => array(
array(
'%s removed a child revision: %3$s.',
'%s removed child revisions: %3$s.',
),
),
'%s removed %s child revision(s) for %s: %s.' => array(
array(
'%s removed a child revision for %3$s: %4$s.',
'%s removed child revisions for %3$s: %4$s.',
),
),
'%s edited child revision(s), added %s: %s; removed %s: %s.' => array(
'%s edited child revisions, added: %3$s; removed: %5$s.',
),
'%s edited child revision(s) for %s, '.
'added %s: %s; removed %s: %s.' => array(
'%s edited child revisions for %s, added: %3$s; removed: %5$s.',
),
'%s added %s commit(s): %s.' => array(
array(
'%s added a commit: %3$s.',
'%s added commits: %3$s.',
),
),
'%s removed %s commit(s): %s.' => array(
array(
'%s removed a commit: %3$s.',
'%s removed commits: %3$s.',
),
),
'%s edited commit(s), added %s: %s; removed %s: %s.' =>
'%s edited commits, added %3$s; removed %5$s.',
'%s added %s reverted change(s): %s.' => array(
array(
'%s added a reverted change: %3$s.',
'%s added reverted changes: %3$s.',
),
),
'%s removed %s reverted change(s): %s.' => array(
array(
'%s removed a reverted change: %3$s.',
'%s removed reverted changes: %3$s.',
),
),
'%s edited reverted change(s), added %s: %s; removed %s: %s.' =>
'%s edited reverted changes, added %3$s; removed %5$s.',
'%s added %s reverted change(s) for %s: %s.' => array(
array(
'%s added a reverted change for %3$s: %4$s.',
'%s added reverted changes for %3$s: %4$s.',
),
),
'%s removed %s reverted change(s) for %s: %s.' => array(
array(
'%s removed a reverted change for %3$s: %4$s.',
'%s removed reverted changes for %3$s: %4$s.',
),
),
'%s edited reverted change(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited reverted changes for %2$s, added %4$s; removed %6$s.',
'%s added %s reverting change(s): %s.' => array(
array(
'%s added a reverting change: %3$s.',
'%s added reverting changes: %3$s.',
),
),
'%s removed %s reverting change(s): %s.' => array(
array(
'%s removed a reverting change: %3$s.',
'%s removed reverting changes: %3$s.',
),
),
'%s edited reverting change(s), added %s: %s; removed %s: %s.' =>
'%s edited reverting changes, added %3$s; removed %5$s.',
'%s added %s reverting change(s) for %s: %s.' => array(
array(
'%s added a reverting change for %3$s: %4$s.',
'%s added reverting changes for %3$s: %4$s.',
),
),
'%s removed %s reverting change(s) for %s: %s.' => array(
array(
'%s removed a reverting change for %3$s: %4$s.',
'%s removed reverting changes for %3$s: %4$s.',
),
),
'%s edited reverting change(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited reverting changes for %s, added %4$s; removed %6$s.',
'%s changed project member(s), added %d: %s; removed %d: %s.' =>
'%s changed project members, added %3$s; removed %5$s.',
'%s added %d project member(s): %s.' => array(
array(
'%s added a member: %3$s.',
'%s added members: %3$s.',
),
),
'%s removed %d project member(s): %s.' => array(
array(
'%s removed a member: %3$s.',
'%s removed members: %3$s.',
),
),
'%s project hashtag(s) are already used by other projects: %s.' => array(
'Project hashtag "%2$s" is already used by another project.',
'Some project hashtags are already used by other projects: %2$s.',
),
'%s changed project hashtag(s), added %d: %s; removed %d: %s.' =>
'%s changed project hashtags, added %3$s; removed %5$s.',
'Hashtags must contain at least one letter or number. %s '.
'project hashtag(s) are invalid: %s.' => array(
'Hashtags must contain at least one letter or number. The '.
'hashtag "%2$s" is not valid.',
'Hashtags must contain at least one letter or number. These '.
'hashtags are invalid: %2$s.',
),
'%s added %d project hashtag(s): %s.' => array(
array(
'%s added a hashtag: %3$s.',
'%s added hashtags: %3$s.',
),
),
'%s removed %d project hashtag(s): %s.' => array(
array(
'%s removed a hashtag: %3$s.',
'%s removed hashtags: %3$s.',
),
),
'%s changed %s hashtag(s), added %d: %s; removed %d: %s.' =>
'%s changed hashtags for %s, added %4$s; removed %6$s.',
'%s added %d %s hashtag(s): %s.' => array(
array(
'%s added a hashtag to %3$s: %4$s.',
'%s added hashtags to %3$s: %4$s.',
),
),
'%s removed %d %s hashtag(s): %s.' => array(
array(
'%s removed a hashtag from %3$s: %4$s.',
'%s removed hashtags from %3$s: %4$s.',
),
),
'%d User(s) Need Approval' => array(
'%d User Needs Approval',
'%d Users Need Approval',
),
'%s, %s line(s)' => array(
array(
'%s, %s line',
'%s, %s lines',
),
),
'%s pushed %d commit(s) to %s.' => array(
array(
'%s pushed a commit to %3$s.',
'%s pushed %d commits to %s.',
),
),
'%s commit(s)' => array(
'1 commit',
'%s commits',
),
'%s removed %s JIRA issue(s): %s.' => array(
array(
'%s removed a JIRA issue: %3$s.',
'%s removed JIRA issues: %3$s.',
),
),
'%s added %s JIRA issue(s): %s.' => array(
array(
'%s added a JIRA issue: %3$s.',
'%s added JIRA issues: %3$s.',
),
),
'%s added %s required legal document(s): %s.' => array(
array(
'%s added a required legal document: %3$s.',
'%s added required legal documents: %3$s.',
),
),
'%s updated JIRA issue(s): added %s %s; removed %d %s.' =>
'%s updated JIRA issues: added %3$s; removed %5$s.',
'%s edited %s task(s), added %s: %s; removed %s: %s.' =>
'%s edited tasks, added %4$s; removed %6$s.',
'%s added %s task(s) to %s: %s.' => array(
array(
'%s added a task to %3$s: %4$s.',
'%s added tasks to %3$s: %4$s.',
),
),
'%s removed %s task(s) from %s: %s.' => array(
array(
'%s removed a task from %3$s: %4$s.',
'%s removed tasks from %3$s: %4$s.',
),
),
'%s edited %s task(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited tasks for %3$s, added: %5$s; removed %7$s.',
'%s edited %s commit(s), added %s: %s; removed %s: %s.' =>
'%s edited commits, added %4$s; removed %6$s.',
'%s added %s commit(s) to %s: %s.' => array(
array(
'%s added a commit to %3$s: %4$s.',
'%s added commits to %3$s: %4$s.',
),
),
'%s removed %s commit(s) from %s: %s.' => array(
array(
'%s removed a commit from %3$s: %4$s.',
'%s removed commits from %3$s: %4$s.',
),
),
'%s edited %s commit(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited commits for %3$s, added: %5$s; removed %7$s.',
'%s added %s revision(s): %s.' => array(
array(
'%s added a revision: %3$s.',
'%s added revisions: %3$s.',
),
),
'%s removed %s revision(s): %s.' => array(
array(
'%s removed a revision: %3$s.',
'%s removed revisions: %3$s.',
),
),
'%s edited %s revision(s), added %s: %s; removed %s: %s.' =>
'%s edited revisions, added %4$s; removed %6$s.',
'%s added %s revision(s) to %s: %s.' => array(
array(
'%s added a revision to %3$s: %4$s.',
'%s added revisions to %3$s: %4$s.',
),
),
'%s removed %s revision(s) from %s: %s.' => array(
array(
'%s removed a revision from %3$s: %4$s.',
'%s removed revisions from %3$s: %4$s.',
),
),
'%s edited %s revision(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited revisions for %3$s, added: %5$s; removed %7$s.',
'%s edited %s project(s), added %s: %s; removed %s: %s.' =>
'%s edited projects, added %4$s; removed %6$s.',
'%s added %s project(s) to %s: %s.' => array(
array(
'%s added a project to %3$s: %4$s.',
'%s added projects to %3$s: %4$s.',
),
),
'%s removed %s project(s) from %s: %s.' => array(
array(
'%s removed a project from %3$s: %4$s.',
'%s removed projects from %3$s: %4$s.',
),
),
'%s edited %s project(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited projects for %3$s, added: %5$s; removed %7$s.',
'%s added %s panel(s): %s.' => array(
array(
'%s added a panel: %3$s.',
'%s added panels: %3$s.',
),
),
'%s removed %s panel(s): %s.' => array(
array(
'%s removed a panel: %3$s.',
'%s removed panels: %3$s.',
),
),
'%s edited %s panel(s), added %s: %s; removed %s: %s.' =>
'%s edited panels, added %4$s; removed %6$s.',
'%s added %s dashboard(s): %s.' => array(
array(
'%s added a dashboard: %3$s.',
'%s added dashboards: %3$s.',
),
),
'%s removed %s dashboard(s): %s.' => array(
array(
'%s removed a dashboard: %3$s.',
'%s removed dashboards: %3$s.',
),
),
'%s edited %s dashboard(s), added %s: %s; removed %s: %s.' =>
'%s edited dashboards, added %4$s; removed %6$s.',
'%s added %s edge(s): %s.' => array(
array(
'%s added an edge: %3$s.',
'%s added edges: %3$s.',
),
),
'%s added %s edge(s) to %s: %s.' => array(
array(
'%s added an edge to %3$s: %4$s.',
'%s added edges to %3$s: %4$s.',
),
),
'%s removed %s edge(s): %s.' => array(
array(
'%s removed an edge: %3$s.',
'%s removed edges: %3$s.',
),
),
'%s removed %s edge(s) from %s: %s.' => array(
array(
'%s removed an edge from %3$s: %4$s.',
'%s removed edges from %3$s: %4$s.',
),
),
'%s edited edge(s), added %s: %s; removed %s: %s.' =>
'%s edited edges, added: %3$s; removed: %5$s.',
'%s edited %s edge(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited edges for %3$s, added: %5$s; removed %7$s.',
'%s added %s member(s) for %s: %s.' => array(
array(
'%s added a member for %3$s: %4$s.',
'%s added members for %3$s: %4$s.',
),
),
'%s removed %s member(s) for %s: %s.' => array(
array(
'%s removed a member for %3$s: %4$s.',
'%s removed members for %3$s: %4$s.',
),
),
'%s edited %s member(s) for %s, added %s: %s; removed %s: %s.' =>
'%s edited members for %3$s, added: %5$s; removed %7$s.',
'%d related link(s):' => array(
'Related link:',
'Related links:',
),
'You have %d unpaid invoice(s).' => array(
'You have an unpaid invoice.',
'You have unpaid invoices.',
),
'The configurations differ in the following %s way(s):' => array(
'The configurations differ:',
'The configurations differ in these ways:',
),
'Phabricator is configured with an email domain whitelist (in %s), so '.
'only users with a verified email address at one of these %s '.
'allowed domain(s) will be able to register an account: %s' => array(
array(
'Phabricator is configured with an email domain whitelist (in %s), '.
'so only users with a verified email address at %3$s will be '.
'allowed to register an account.',
'Phabricator is configured with an email domain whitelist (in %s), '.
'so only users with a verified email address at one of these '.
'allowed domains will be able to register an account: %3$s',
),
),
'Show First %d Line(s)' => array(
'Show First Line',
'Show First %d Lines',
),
"\xE2\x96\xB2 Show %d Line(s)" => array(
"\xE2\x96\xB2 Show Line",
"\xE2\x96\xB2 Show %d Lines",
),
'Show All %d Line(s)' => array(
'Show Line',
'Show All %d Lines',
),
"\xE2\x96\xBC Show %d Line(s)" => array(
"\xE2\x96\xBC Show Line",
"\xE2\x96\xBC Show %d Lines",
),
'Show Last %d Line(s)' => array(
'Show Last Line',
'Show Last %d Lines',
),
'%s marked %s inline comment(s) as done and %s inline comment(s) as '.
'not done.' => array(
array(
array(
'%s marked an inline comment as done and an inline comment '.
'as not done.',
'%s marked an inline comment as done and %3$s inline comments '.
'as not done.',
),
array(
'%s marked %s inline comments as done and an inline comment '.
'as not done.',
'%s marked %s inline comments as done and %s inline comments '.
'as done.',
),
),
),
'%s marked %s inline comment(s) as done.' => array(
array(
'%s marked an inline comment as done.',
'%s marked %s inline comments as done.',
),
),
'%s marked %s inline comment(s) as not done.' => array(
array(
'%s marked an inline comment as not done.',
'%s marked %s inline comments as not done.',
),
),
'These %s object(s) will be destroyed forever:' => array(
'This object will be destroyed forever:',
'These objects will be destroyed forever:',
),
'Are you absolutely certain you want to destroy these %s '.
'object(s)?' => array(
'Are you absolutely certain you want to destroy this object?',
'Are you absolutely certain you want to destroy these objects?',
),
'%s added %s owner(s): %s.' => array(
array(
'%s added an owner: %3$s.',
'%s added owners: %3$s.',
),
),
'%s removed %s owner(s): %s.' => array(
array(
'%s removed an owner: %3$s.',
'%s removed owners: %3$s.',
),
),
'%s changed %s package owner(s), added %s: %s; removed %s: %s.' => array(
'%s changed package owners, added: %4$s; removed: %6$s.',
),
'Found %s book(s).' => array(
'Found %s book.',
'Found %s books.',
),
'Found %s file(s)...' => array(
'Found %s file...',
'Found %s files...',
),
'Found %s file(s) in project.' => array(
'Found %s file in project.',
'Found %s files in project.',
),
'Found %s unatomized, uncached file(s).' => array(
'Found %s unatomized, uncached file.',
'Found %s unatomized, uncached files.',
),
'Found %s file(s) to atomize.' => array(
'Found %s file to atomize.',
'Found %s files to atomize.',
),
'Atomizing %s file(s).' => array(
'Atomizing %s file.',
'Atomizing %s files.',
),
'Creating %s document(s).' => array(
'Creating %s document.',
'Creating %s documents.',
),
'Deleting %s document(s).' => array(
'Deleting %s document.',
'Deleting %s documents.',
),
'Found %s obsolete atom(s) in graph.' => array(
'Found %s obsolete atom in graph.',
'Found %s obsolete atoms in graph.',
),
'Found %s new atom(s) in graph.' => array(
'Found %s new atom in graph.',
'Found %s new atoms in graph.',
),
'This call takes %s parameter(s), but only %s are documented.' => array(
array(
'This call takes %s parameter, but only %s is documented.',
'This call takes %s parameter, but only %s are documented.',
),
array(
'This call takes %s parameters, but only %s is documented.',
'This call takes %s parameters, but only %s are documented.',
),
),
'%s Passed Test(s)' => '%s Passed',
'%s Failed Test(s)' => '%s Failed',
'%s Skipped Test(s)' => '%s Skipped',
'%s Broken Test(s)' => '%s Broken',
'%s Unsound Test(s)' => '%s Unsound',
'%s Other Test(s)' => '%s Other',
'%s Bulk Task(s)' => array(
'%s Task',
'%s Tasks',
),
'%s added %s badge(s) for %s: %s.' => array(
array(
'%s added a badge for %s: %3$s.',
'%s added badges for %s: %3$s.',
),
),
'%s added %s badge(s): %s.' => array(
array(
'%s added a badge: %3$s.',
'%s added badges: %3$s.',
),
),
'%s awarded %s recipient(s) for %s: %s.' => array(
array(
'%s awarded %3$s to %4$s.',
'%s awarded %3$s to multiple recipients: %4$s.',
),
),
'%s awarded %s recipients(s): %s.' => array(
array(
'%s awarded a recipient: %3$s.',
'%s awarded multiple recipients: %3$s.',
),
),
'%s edited badge(s) for %s, added %s: %s; revoked %s: %s.' => array(
array(
'%s edited badges for %s, added %s: %s; revoked %s: %s.',
'%s edited badges for %s, added %s: %s; revoked %s: %s.',
),
),
'%s edited badge(s), added %s: %s; revoked %s: %s.' => array(
array(
'%s edited badges, added %s: %s; revoked %s: %s.',
'%s edited badges, added %s: %s; revoked %s: %s.',
),
),
'%s edited recipient(s) for %s, awarded %s: %s; revoked %s: %s.' => array(
array(
'%s edited recipients for %s, awarded %s: %s; revoked %s: %s.',
'%s edited recipients for %s, awarded %s: %s; revoked %s: %s.',
),
),
'%s edited recipient(s), awarded %s: %s; revoked %s: %s.' => array(
array(
'%s edited recipients, awarded %s: %s; revoked %s: %s.',
'%s edited recipients, awarded %s: %s; revoked %s: %s.',
),
),
'%s revoked %s badge(s) for %s: %s.' => array(
array(
'%s revoked a badge for %3$s: %4$s.',
'%s revoked multiple badges for %3$s: %4$s.',
),
),
'%s revoked %s badge(s): %s.' => array(
array(
'%s revoked a badge: %3$s.',
'%s revoked multiple badges: %3$s.',
),
),
'%s revoked %s recipient(s) for %s: %s.' => array(
array(
'%s revoked %3$s from %4$s.',
'%s revoked multiple recipients for %3$s: %4$s.',
),
),
'%s revoked %s recipients(s): %s.' => array(
array(
'%s revoked a recipient: %3$s.',
'%s revoked multiple recipients: %3$s.',
),
),
'%s automatically subscribed target(s) were not affected: %s.' => array(
'An automatically subscribed target was not affected: %2$s.',
'Automatically subscribed targets were not affected: %2$s.',
),
'Declined to resubscribe %s target(s) because they previously '.
'unsubscribed: %s.' => array(
'Delined to resubscribe a target because they previously '.
'unsubscribed: %2$s.',
'Declined to resubscribe targets because they previously '.
'unsubscribed: %2$s.',
),
'%s target(s) are not subscribed: %s.' => array(
'A target is not subscribed: %2$s.',
'Targets are not subscribed: %2$s.',
),
'%s target(s) are already subscribed: %s.' => array(
'A target is already subscribed: %2$s.',
'Targets are already subscribed: %2$s.',
),
'Added %s subscriber(s): %s.' => array(
'Added a subscriber: %2$s.',
'Added subscribers: %2$s.',
),
'Removed %s subscriber(s): %s.' => array(
'Removed a subscriber: %2$s.',
'Removed subscribers: %2$s.',
),
'Queued email to be delivered to %s target(s): %s.' => array(
'Queued email to be delivered to target: %2$s.',
'Queued email to be delivered to targets: %2$s.',
),
'Queued email to be delivered to %s target(s), ignoring their '.
'notification preferences: %s.' => array(
'Queued email to be delivered to target, ignoring notification '.
'preferences: %2$s.',
'Queued email to be delivered to targets, ignoring notification '.
'preferences: %2$s.',
),
'%s project(s) are not associated: %s.' => array(
'A project is not associated: %2$s.',
'Projects are not associated: %2$s.',
),
'%s project(s) are already associated: %s.' => array(
'A project is already associated: %2$s.',
'Projects are already associated: %2$s.',
),
'Added %s project(s): %s.' => array(
'Added a project: %2$s.',
'Added projects: %2$s.',
),
'Removed %s project(s): %s.' => array(
'Removed a project: %2$s.',
'Removed projects: %2$s.',
),
'Added %s reviewer(s): %s.' => array(
'Added a reviewer: %2$s.',
'Added reviewers: %2$s.',
),
'Added %s blocking reviewer(s): %s.' => array(
'Added a blocking reviewer: %2$s.',
'Added blocking reviewers: %2$s.',
),
'Required %s signature(s): %s.' => array(
'Required a signature: %2$s.',
'Required signatures: %2$s.',
),
'Started %s build(s): %s.' => array(
'Started a build: %2$s.',
'Started builds: %2$s.',
),
'Added %s auditor(s): %s.' => array(
'Added an auditor: %2$s.',
'Added auditors: %2$s.',
),
'%s target(s) do not have permission to see this object: %s.' => array(
'A target does not have permission to see this object: %2$s.',
'Targets do not have permission to see this object: %2$s.',
),
'This action has no effect on %s target(s): %s.' => array(
'This action has no effect on a target: %2$s.',
'This action has no effect on targets: %2$s.',
),
'Mail sent in the last %s day(s).' => array(
'Mail sent in the last day.',
'Mail sent in the last %s days.',
),
'%s Day(s)' => array(
'%s Day',
'%s Days',
),
'%s Day(s) Ago' => array(
'%s Day Ago',
'%s Days Ago',
),
'Setting retention policy for "%s" to %s day(s).' => array(
array(
'Setting retention policy for "%s" to one day.',
'Setting retention policy for "%s" to %s days.',
),
),
'Waiting %s second(s) for lease to activate.' => array(
'Waiting a second for lease to activate.',
'Waiting %s seconds for lease to activate.',
),
'%s changed %s automation blueprint(s), added %s: %s; removed %s: %s.' =>
'%s changed automation blueprints, added: %4$s; removed: %6$s.',
'%s added %s automation blueprint(s): %s.' => array(
array(
'%s added an automation blueprint: %3$s.',
'%s added automation blueprints: %3$s.',
),
),
'%s removed %s automation blueprint(s): %s.' => array(
array(
'%s removed an automation blueprint: %3$s.',
'%s removed automation blueprints: %3$s.',
),
),
'WARNING: There are %s unapproved authorization(s)!' => array(
'WARNING: There is an unapproved authorization!',
'WARNING: There are unapproved authorizations!',
),
'Found %s Open Resource(s)' => array(
'Found %s Open Resource',
'Found %s Open Resources',
),
'%s Open Resource(s) Remain' => array(
'%s Open Resource Remain',
'%s Open Resources Remain',
),
'Found %s Blueprint(s)' => array(
'Found %s Blueprint',
'Found %s Blueprints',
),
'%s Blueprint(s) Can Allocate' => array(
'%s Blueprint Can Allocate',
'%s Blueprints Can Allocate',
),
'%s Blueprint(s) Enabled' => array(
'%s Blueprint Enabled',
'%s Blueprints Enabled',
),
'%s Event(s)' => array(
'%s Event',
'%s Events',
),
'%s Unit(s)' => array(
'%s Unit',
'%s Units',
),
'QUEUEING TASKS (%s Commit(s)):' => array(
'QUEUEING TASKS (%s Commit):',
'QUEUEING TASKS (%s Commits):',
),
'Found %s total commit(s); updating...' => array(
'Found %s total commit; updating...',
'Found %s total commits; updating...',
),
'Not enough process slots to schedule the other %s '.
'repository(s) for updates yet.' => array(
'Not enough process slots to schedule the other '.'
repository for update yet.',
'Not enough process slots to schedule the other %s '.
'repositories for updates yet.',
),
'%s updated %s, added %d: %s.' =>
'%s updated %s, added: %4$s.',
'%s updated %s, removed %s: %s.' =>
'%s updated %s, removed: %4$s.',
'%s updated %s, added %s: %s; removed %s: %s.' =>
'%s updated %s, added: %4$s; removed: %6$s.',
'%s updated %s for %s, added %d: %s.' =>
'%s updated %s for %s, added: %5$s.',
'%s updated %s for %s, removed %s: %s.' =>
'%s updated %s for %s, removed: %5$s.',
'%s updated %s for %s, added %s: %s; removed %s: %s.' =>
'%s updated %s for %s, added: %5$s; removed; %7$s.',
'Permanently destroyed %s object(s).' => array(
'Permanently destroyed %s object.',
'Permanently destroyed %s objects.',
),
'%s added %s watcher(s) for %s: %s.' => array(
array(
'%s added a watcher for %3$s: %4$s.',
'%s added watchers for %3$s: %4$s.',
),
),
'%s removed %s watcher(s) for %s: %s.' => array(
array(
'%s removed a watcher for %3$s: %4$s.',
'%s removed watchers for %3$s: %4$s.',
),
),
'%s awarded this badge to %s recipient(s): %s.' => array(
array(
'%s awarded this badge to recipient: %3$s.',
'%s awarded this badge to recipients: %3$s.',
),
),
'%s revoked this badge from %s recipient(s): %s.' => array(
array(
'%s revoked this badge from recipient: %3$s.',
'%s revoked this badge from recipients: %3$s.',
),
),
'%s awarded %s to %s recipient(s): %s.' => array(
array(
array(
'%s awarded %s to recipient: %4$s.',
'%s awarded %s to recipients: %4$s.',
),
),
),
'%s revoked %s from %s recipient(s): %s.' => array(
array(
array(
'%s revoked %s from recipient: %4$s.',
'%s revoked %s from recipients: %4$s.',
),
),
),
'%s invited %s attendee(s): %s.' =>
'%s invited: %3$s.',
'%s uninvited %s attendee(s): %s.' =>
'%s uninvited: %3$s.',
'%s invited %s attendee(s): %s; uninvited %s attendee(s): %s.' =>
'%s invited: %3$s; uninvited: %5$s.',
'%s invited %s attendee(s) to %s: %s.' =>
'%s added invites for %3$s: %4$s.',
'%s uninvited %s attendee(s) to %s: %s.' =>
'%s removed invites for %3$s: %4$s.',
'%s updated the invite list for %s, invited %s: %s; uninvited %s: %s.' =>
'%s updated the invite list for %s, invited: %4$s; uninvited: %6$s.',
'Restart %s build(s)?' => array(
'Restart %s build?',
'Restart %s builds?',
),
'%s is starting in %s minute(s), at %s.' => array(
array(
'%s is starting in one minute, at %3$s.',
'%s is starting in %s minutes, at %s.',
),
),
'%s added %s auditor(s): %s.' => array(
array(
'%s added an auditor: %3$s.',
'%s added auditors: %3$s.',
),
),
'%s removed %s auditor(s): %s.' => array(
array(
'%s removed an auditor: %3$s.',
'%s removed auditors: %3$s.',
),
),
'%s edited %s auditor(s), removed %s: %s; added %s: %s.' => array(
array(
'%s edited auditors, removed: %4$s; added: %6$s.',
),
),
'%s accepted this revision as %s reviewer(s): %s.' =>
'%s accepted this revision as: %3$s.',
'%s added %s merchant manager(s): %s.' => array(
array(
'%s added a merchant manager: %3$s.',
'%s added merchant managers: %3$s.',
),
),
'%s removed %s merchant manager(s): %s.' => array(
array(
'%s removed a merchant manager: %3$s.',
'%s removed merchant managers: %3$s.',
),
),
'%s added %s account manager(s): %s.' => array(
array(
'%s added an account manager: %3$s.',
'%s added account managers: %3$s.',
),
),
'%s removed %s account manager(s): %s.' => array(
array(
'%s removed an account manager: %3$s.',
'%s removed account managers: %3$s.',
),
),
'You are about to apply a bulk edit which will affect '.
'%s object(s).' => array(
'You are about to apply a bulk edit to a single object.',
'You are about to apply a bulk edit which will affect '.
'%s objects.',
),
'Destroyed %s credential(s) of type "%s".' => array(
'Destroyed one credential of type "%2$s".',
'Destroyed %s credentials of type "%s".',
),
'%s notification(s) about objects which no longer exist or which '.
'you can no longer see were discarded.' => array(
'One notification about an object which no longer exists or which '.
'you can no longer see was discarded.',
'%s notifications about objects which no longer exist or which '.
'you can no longer see were discarded.',
),
'This draft revision will be sent for review once %s '.
'build(s) pass: %s.' => array(
'This draft revision will be sent for review once this build '.
'passes: %2$s.',
'This draft revision will be sent for review once these builds '.
'pass: %2$s.',
),
+ 'This factor recently issued a challenge to a different login '.
+ 'session. Wait %s second(s) for the code to cycle, then try '.
+ 'again.' => array(
+ 'This factor recently issued a challenge to a different login '.
+ 'session. Wait %s second for the code to cycle, then try '.
+ 'again.',
+ 'This factor recently issued a challenge to a different login '.
+ 'session. Wait %s seconds for the code to cycle, then try '.
+ 'again.',
+ ),
+
+ 'This factor recently issued a challenge for a different '.
+ 'workflow. Wait %s second(s) for the code to cycle, then try '.
+ 'again.' => array(
+ 'This factor recently issued a challenge for a different '.
+ 'workflow. Wait %s second for the code to cycle, then try '.
+ 'again.',
+ 'This factor recently issued a challenge for a different '.
+ 'workflow. Wait %s seconds for the code to cycle, then try '.
+ 'again.',
+ ),
+
+
+ 'This factor recently issued a challenge which has expired. '.
+ 'A new challenge can not be issued yet. Wait %s second(s) for '.
+ 'the code to cycle, then try again.' => array(
+ 'This factor recently issued a challenge which has expired. '.
+ 'A new challenge can not be issued yet. Wait %s second for '.
+ 'the code to cycle, then try again.',
+ 'This factor recently issued a challenge which has expired. '.
+ 'A new challenge can not be issued yet. Wait %s seconds for '.
+ 'the code to cycle, then try again.',
+ ),
+
+ 'You recently provided a response to this factor. Responses '.
+ 'may not be reused. Wait %s second(s) for the code to cycle, '.
+ 'then try again.' => array(
+ 'You recently provided a response to this factor. Responses '.
+ 'may not be reused. Wait %s second for the code to cycle, '.
+ 'then try again.',
+ 'You recently provided a response to this factor. Responses '.
+ 'may not be reused. Wait %s seconds for the code to cycle, '.
+ 'then try again.',
+ ),
+
);
}
}
diff --git a/src/infrastructure/markup/PhabricatorMarkupEngine.php b/src/infrastructure/markup/PhabricatorMarkupEngine.php
index 0c56d9ed4..868bbc567 100644
--- a/src/infrastructure/markup/PhabricatorMarkupEngine.php
+++ b/src/infrastructure/markup/PhabricatorMarkupEngine.php
@@ -1,712 +1,713 @@
<?php
/**
* Manages markup engine selection, configuration, application, caching and
* pipelining.
*
* @{class:PhabricatorMarkupEngine} can be used to render objects which
* implement @{interface:PhabricatorMarkupInterface} in a batched, cache-aware
* way. For example, if you have a list of comments written in remarkup (and
* the objects implement the correct interface) you can render them by first
* building an engine and adding the fields with @{method:addObject}.
*
* $field = 'field:body'; // Field you want to render. Each object exposes
* // one or more fields of markup.
*
* $engine = new PhabricatorMarkupEngine();
* foreach ($comments as $comment) {
* $engine->addObject($comment, $field);
* }
*
* Now, call @{method:process} to perform the actual cache/rendering
* step. This is a heavyweight call which does batched data access and
* transforms the markup into output.
*
* $engine->process();
*
* Finally, do something with the results:
*
* $results = array();
* foreach ($comments as $comment) {
* $results[] = $engine->getOutput($comment, $field);
* }
*
* If you have a single object to render, you can use the convenience method
* @{method:renderOneObject}.
*
* @task markup Markup Pipeline
* @task engine Engine Construction
*/
final class PhabricatorMarkupEngine extends Phobject {
private $objects = array();
private $viewer;
private $contextObject;
private $version = 17;
private $engineCaches = array();
private $auxiliaryConfig = array();
/* -( Markup Pipeline )---------------------------------------------------- */
/**
* Convenience method for pushing a single object through the markup
* pipeline.
*
* @param PhabricatorMarkupInterface The object to render.
* @param string The field to render.
* @param PhabricatorUser User viewing the markup.
* @param object A context object for policy checks
* @return string Marked up output.
* @task markup
*/
public static function renderOneObject(
PhabricatorMarkupInterface $object,
$field,
PhabricatorUser $viewer,
$context_object = null) {
return id(new PhabricatorMarkupEngine())
->setViewer($viewer)
->setContextObject($context_object)
->addObject($object, $field)
->process()
->getOutput($object, $field);
}
/**
* Queue an object for markup generation when @{method:process} is
* called. You can retrieve the output later with @{method:getOutput}.
*
* @param PhabricatorMarkupInterface The object to render.
* @param string The field to render.
* @return this
* @task markup
*/
public function addObject(PhabricatorMarkupInterface $object, $field) {
$key = $this->getMarkupFieldKey($object, $field);
$this->objects[$key] = array(
'object' => $object,
'field' => $field,
);
return $this;
}
/**
* Process objects queued with @{method:addObject}. You can then retrieve
* the output with @{method:getOutput}.
*
* @return this
* @task markup
*/
public function process() {
$keys = array();
foreach ($this->objects as $key => $info) {
if (!isset($info['markup'])) {
$keys[] = $key;
}
}
if (!$keys) {
return $this;
}
$objects = array_select_keys($this->objects, $keys);
// Build all the markup engines. We need an engine for each field whether
// we have a cache or not, since we still need to postprocess the cache.
$engines = array();
foreach ($objects as $key => $info) {
$engines[$key] = $info['object']->newMarkupEngine($info['field']);
$engines[$key]->setConfig('viewer', $this->viewer);
$engines[$key]->setConfig('contextObject', $this->contextObject);
foreach ($this->auxiliaryConfig as $aux_key => $aux_value) {
$engines[$key]->setConfig($aux_key, $aux_value);
}
}
// Load or build the preprocessor caches.
$blocks = $this->loadPreprocessorCaches($engines, $objects);
$blocks = mpull($blocks, 'getCacheData');
$this->engineCaches = $blocks;
// Finalize the output.
foreach ($objects as $key => $info) {
$engine = $engines[$key];
$field = $info['field'];
$object = $info['object'];
$output = $engine->postprocessText($blocks[$key]);
$output = $object->didMarkupText($field, $output, $engine);
$this->objects[$key]['output'] = $output;
}
return $this;
}
/**
* Get the output of markup processing for a field queued with
* @{method:addObject}. Before you can call this method, you must call
* @{method:process}.
*
* @param PhabricatorMarkupInterface The object to retrieve.
* @param string The field to retrieve.
* @return string Processed output.
* @task markup
*/
public function getOutput(PhabricatorMarkupInterface $object, $field) {
$key = $this->getMarkupFieldKey($object, $field);
$this->requireKeyProcessed($key);
return $this->objects[$key]['output'];
}
/**
* Retrieve engine metadata for a given field.
*
* @param PhabricatorMarkupInterface The object to retrieve.
* @param string The field to retrieve.
* @param string The engine metadata field to retrieve.
* @param wild Optional default value.
* @task markup
*/
public function getEngineMetadata(
PhabricatorMarkupInterface $object,
$field,
$metadata_key,
$default = null) {
$key = $this->getMarkupFieldKey($object, $field);
$this->requireKeyProcessed($key);
return idx($this->engineCaches[$key]['metadata'], $metadata_key, $default);
}
/**
* @task markup
*/
private function requireKeyProcessed($key) {
if (empty($this->objects[$key])) {
throw new Exception(
pht(
"Call %s before using results (key = '%s').",
'addObject()',
$key));
}
if (!isset($this->objects[$key]['output'])) {
throw new PhutilInvalidStateException('process');
}
}
/**
* @task markup
*/
private function getMarkupFieldKey(
PhabricatorMarkupInterface $object,
$field) {
static $custom;
if ($custom === null) {
$custom = array_merge(
self::loadCustomInlineRules(),
self::loadCustomBlockRules());
$custom = mpull($custom, 'getRuleVersion', null);
ksort($custom);
$custom = PhabricatorHash::digestForIndex(serialize($custom));
}
return $object->getMarkupFieldKey($field).'@'.$this->version.'@'.$custom;
}
/**
* @task markup
*/
private function loadPreprocessorCaches(array $engines, array $objects) {
$blocks = array();
$use_cache = array();
foreach ($objects as $key => $info) {
if ($info['object']->shouldUseMarkupCache($info['field'])) {
$use_cache[$key] = true;
}
}
if ($use_cache) {
try {
$blocks = id(new PhabricatorMarkupCache())->loadAllWhere(
'cacheKey IN (%Ls)',
array_keys($use_cache));
$blocks = mpull($blocks, null, 'getCacheKey');
} catch (Exception $ex) {
phlog($ex);
}
}
$is_readonly = PhabricatorEnv::isReadOnly();
foreach ($objects as $key => $info) {
// False check in case MySQL doesn't support unicode characters
// in the string (T1191), resulting in unserialize returning false.
if (isset($blocks[$key]) && $blocks[$key]->getCacheData() !== false) {
// If we already have a preprocessing cache, we don't need to rebuild
// it.
continue;
}
$text = $info['object']->getMarkupText($info['field']);
$data = $engines[$key]->preprocessText($text);
// NOTE: This is just debugging information to help sort out cache issues.
// If one machine is misconfigured and poisoning caches you can use this
// field to hunt it down.
$metadata = array(
'host' => php_uname('n'),
);
$blocks[$key] = id(new PhabricatorMarkupCache())
->setCacheKey($key)
->setCacheData($data)
->setMetadata($metadata);
if (isset($use_cache[$key]) && !$is_readonly) {
// This is just filling a cache and always safe, even on a read pathway.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$blocks[$key]->replace();
unset($unguarded);
}
}
return $blocks;
}
/**
* Set the viewing user. Used to implement object permissions.
*
* @param PhabricatorUser The viewing user.
* @return this
* @task markup
*/
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
/**
* Set the context object. Used to implement object permissions.
*
* @param The object in which context this remarkup is used.
* @return this
* @task markup
*/
public function setContextObject($object) {
$this->contextObject = $object;
return $this;
}
public function setAuxiliaryConfig($key, $value) {
// TODO: This is gross and should be removed. Avoid use.
$this->auxiliaryConfig[$key] = $value;
return $this;
}
/* -( Engine Construction )------------------------------------------------ */
/**
* @task engine
*/
public static function newManiphestMarkupEngine() {
return self::newMarkupEngine(array(
));
}
/**
* @task engine
*/
public static function newPhrictionMarkupEngine() {
return self::newMarkupEngine(array(
'header.generate-toc' => true,
));
}
/**
* @task engine
*/
public static function newPhameMarkupEngine() {
return self::newMarkupEngine(
array(
'macros' => false,
'uri.full' => true,
'uri.same-window' => true,
'uri.base' => PhabricatorEnv::getURI('/'),
));
}
/**
* @task engine
*/
public static function newFeedMarkupEngine() {
return self::newMarkupEngine(
array(
'macros' => false,
'youtube' => false,
));
}
/**
* @task engine
*/
public static function newCalendarMarkupEngine() {
return self::newMarkupEngine(array(
));
}
/**
* @task engine
*/
public static function newDifferentialMarkupEngine(array $options = array()) {
return self::newMarkupEngine(array(
'differential.diff' => idx($options, 'differential.diff'),
));
}
/**
* @task engine
*/
public static function newDiffusionMarkupEngine(array $options = array()) {
return self::newMarkupEngine(array(
'header.generate-toc' => true,
));
}
/**
* @task engine
*/
public static function getEngine($ruleset = 'default') {
static $engines = array();
if (isset($engines[$ruleset])) {
return $engines[$ruleset];
}
$engine = null;
switch ($ruleset) {
case 'default':
$engine = self::newMarkupEngine(array());
break;
case 'feed':
$engine = self::newMarkupEngine(array());
$engine->setConfig('autoplay.disable', true);
break;
case 'nolinebreaks':
$engine = self::newMarkupEngine(array());
$engine->setConfig('preserve-linebreaks', false);
break;
case 'diffusion-readme':
$engine = self::newMarkupEngine(array());
$engine->setConfig('preserve-linebreaks', false);
$engine->setConfig('header.generate-toc', true);
break;
case 'diviner':
$engine = self::newMarkupEngine(array());
$engine->setConfig('preserve-linebreaks', false);
// $engine->setConfig('diviner.renderer', new DivinerDefaultRenderer());
$engine->setConfig('header.generate-toc', true);
break;
case 'extract':
// Engine used for reference/edge extraction. Turn off anything which
// is slow and doesn't change reference extraction.
$engine = self::newMarkupEngine(array());
$engine->setConfig('pygments.enabled', false);
break;
default:
throw new Exception(pht('Unknown engine ruleset: %s!', $ruleset));
}
$engines[$ruleset] = $engine;
return $engine;
}
/**
* @task engine
*/
private static function getMarkupEngineDefaultConfiguration() {
return array(
'pygments' => PhabricatorEnv::getEnvConfig('pygments.enabled'),
'youtube' => PhabricatorEnv::getEnvConfig(
'remarkup.enable-embedded-youtube'),
'differential.diff' => null,
'header.generate-toc' => false,
'macros' => true,
'uri.allowed-protocols' => PhabricatorEnv::getEnvConfig(
'uri.allowed-protocols'),
'uri.full' => false,
'syntax-highlighter.engine' => PhabricatorEnv::getEnvConfig(
'syntax-highlighter.engine'),
'preserve-linebreaks' => true,
);
}
/**
* @task engine
*/
public static function newMarkupEngine(array $options) {
$options += self::getMarkupEngineDefaultConfiguration();
$engine = new PhutilRemarkupEngine();
$engine->setConfig('preserve-linebreaks', $options['preserve-linebreaks']);
$engine->setConfig('pygments.enabled', $options['pygments']);
$engine->setConfig(
'uri.allowed-protocols',
$options['uri.allowed-protocols']);
$engine->setConfig('differential.diff', $options['differential.diff']);
$engine->setConfig('header.generate-toc', $options['header.generate-toc']);
$engine->setConfig(
'syntax-highlighter.engine',
$options['syntax-highlighter.engine']);
$style_map = id(new PhabricatorDefaultSyntaxStyle())
->getRemarkupStyleMap();
$engine->setConfig('phutil.codeblock.style-map', $style_map);
$engine->setConfig('uri.full', $options['uri.full']);
if (isset($options['uri.base'])) {
$engine->setConfig('uri.base', $options['uri.base']);
}
if (isset($options['uri.same-window'])) {
$engine->setConfig('uri.same-window', $options['uri.same-window']);
}
$rules = array();
$rules[] = new PhutilRemarkupEscapeRemarkupRule();
$rules[] = new PhutilRemarkupMonospaceRule();
$rules[] = new PhutilRemarkupDocumentLinkRule();
$rules[] = new PhabricatorNavigationRemarkupRule();
$rules[] = new PhabricatorKeyboardRemarkupRule();
+ $rules[] = new PhabricatorConfigRemarkupRule();
if ($options['youtube']) {
$rules[] = new PhabricatorYoutubeRemarkupRule();
}
$rules[] = new PhabricatorIconRemarkupRule();
$rules[] = new PhabricatorEmojiRemarkupRule();
$rules[] = new PhabricatorHandleRemarkupRule();
$applications = PhabricatorApplication::getAllInstalledApplications();
foreach ($applications as $application) {
foreach ($application->getRemarkupRules() as $rule) {
$rules[] = $rule;
}
}
$rules[] = new PhutilRemarkupHyperlinkRule();
if ($options['macros']) {
$rules[] = new PhabricatorImageMacroRemarkupRule();
$rules[] = new PhabricatorMemeRemarkupRule();
}
$rules[] = new PhutilRemarkupBoldRule();
$rules[] = new PhutilRemarkupItalicRule();
$rules[] = new PhutilRemarkupDelRule();
$rules[] = new PhutilRemarkupUnderlineRule();
$rules[] = new PhutilRemarkupHighlightRule();
foreach (self::loadCustomInlineRules() as $rule) {
$rules[] = clone $rule;
}
$blocks = array();
$blocks[] = new PhutilRemarkupQuotesBlockRule();
$blocks[] = new PhutilRemarkupReplyBlockRule();
$blocks[] = new PhutilRemarkupLiteralBlockRule();
$blocks[] = new PhutilRemarkupHeaderBlockRule();
$blocks[] = new PhutilRemarkupHorizontalRuleBlockRule();
$blocks[] = new PhutilRemarkupListBlockRule();
$blocks[] = new PhutilRemarkupCodeBlockRule();
$blocks[] = new PhutilRemarkupNoteBlockRule();
$blocks[] = new PhutilRemarkupTableBlockRule();
$blocks[] = new PhutilRemarkupSimpleTableBlockRule();
$blocks[] = new PhutilRemarkupInterpreterBlockRule();
$blocks[] = new PhutilRemarkupDefaultBlockRule();
foreach (self::loadCustomBlockRules() as $rule) {
$blocks[] = $rule;
}
foreach ($blocks as $block) {
$block->setMarkupRules($rules);
}
$engine->setBlockRules($blocks);
return $engine;
}
public static function extractPHIDsFromMentions(
PhabricatorUser $viewer,
array $content_blocks) {
$mentions = array();
$engine = self::newDifferentialMarkupEngine();
$engine->setConfig('viewer', $viewer);
foreach ($content_blocks as $content_block) {
$engine->markupText($content_block);
$phids = $engine->getTextMetadata(
PhabricatorMentionRemarkupRule::KEY_MENTIONED,
array());
$mentions += $phids;
}
return $mentions;
}
public static function extractFilePHIDsFromEmbeddedFiles(
PhabricatorUser $viewer,
array $content_blocks) {
$files = array();
$engine = self::newDifferentialMarkupEngine();
$engine->setConfig('viewer', $viewer);
foreach ($content_blocks as $content_block) {
$engine->markupText($content_block);
$phids = $engine->getTextMetadata(
PhabricatorEmbedFileRemarkupRule::KEY_EMBED_FILE_PHIDS,
array());
foreach ($phids as $phid) {
$files[$phid] = $phid;
}
}
return array_values($files);
}
public static function summarizeSentence($corpus) {
$corpus = trim($corpus);
$blocks = preg_split('/\n+/', $corpus, 2);
$block = head($blocks);
$sentences = preg_split(
'/\b([.?!]+)\B/u',
$block,
2,
PREG_SPLIT_DELIM_CAPTURE);
if (count($sentences) > 1) {
$result = $sentences[0].$sentences[1];
} else {
$result = head($sentences);
}
return id(new PhutilUTF8StringTruncator())
->setMaximumGlyphs(128)
->truncateString($result);
}
/**
* Produce a corpus summary, in a way that shortens the underlying text
* without truncating it somewhere awkward.
*
* TODO: We could do a better job of this.
*
* @param string Remarkup corpus to summarize.
* @return string Summarized corpus.
*/
public static function summarize($corpus) {
// Major goals here are:
// - Don't split in the middle of a character (utf-8).
// - Don't split in the middle of, e.g., **bold** text, since
// we end up with hanging '**' in the summary.
// - Try not to pick an image macro, header, embedded file, etc.
// - Hopefully don't return too much text. We don't explicitly limit
// this right now.
$blocks = preg_split("/\n *\n\s*/", $corpus);
$best = null;
foreach ($blocks as $block) {
// This is a test for normal spaces in the block, i.e. a heuristic to
// distinguish standard paragraphs from things like image macros. It may
// not work well for non-latin text. We prefer to summarize with a
// paragraph of normal words over an image macro, if possible.
$has_space = preg_match('/\w\s\w/', $block);
// This is a test to find embedded images and headers. We prefer to
// summarize with a normal paragraph over a header or an embedded object,
// if possible.
$has_embed = preg_match('/^[{=]/', $block);
if ($has_space && !$has_embed) {
// This seems like a good summary, so return it.
return $block;
}
if (!$best) {
// This is the first block we found; if everything is garbage just
// use the first block.
$best = $block;
}
}
return $best;
}
private static function loadCustomInlineRules() {
return id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorRemarkupCustomInlineRule')
->execute();
}
private static function loadCustomBlockRules() {
return id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorRemarkupCustomBlockRule')
->execute();
}
public static function digestRemarkupContent($object, $content) {
$parts = array();
$parts[] = get_class($object);
if ($object instanceof PhabricatorLiskDAO) {
$parts[] = $object->getID();
}
$parts[] = $content;
$message = implode("\n", $parts);
return PhabricatorHash::digestWithNamedKey($message, 'remarkup');
}
}
diff --git a/src/infrastructure/markup/rule/PhabricatorConfigRemarkupRule.php b/src/infrastructure/markup/rule/PhabricatorConfigRemarkupRule.php
new file mode 100644
index 000000000..f353f8c1e
--- /dev/null
+++ b/src/infrastructure/markup/rule/PhabricatorConfigRemarkupRule.php
@@ -0,0 +1,50 @@
+<?php
+
+final class PhabricatorConfigRemarkupRule
+ extends PhutilRemarkupRule {
+
+ public function apply($text) {
+ return preg_replace_callback(
+ '(@{config:([^}]+)})',
+ array($this, 'markupConfig'),
+ $text);
+ }
+
+ public function getPriority() {
+ // We're reusing the Diviner atom syntax, so make sure we evaluate before
+ // the Diviner rule evaluates.
+ return id(new DivinerSymbolRemarkupRule())->getPriority() - 1;
+ }
+
+ public function markupConfig(array $matches) {
+ if (!$this->isFlatText($matches[0])) {
+ return $matches[0];
+ }
+
+ $config_key = $matches[1];
+
+ try {
+ $option = PhabricatorEnv::getEnvConfig($config_key);
+ } catch (Exception $ex) {
+ return $matches[0];
+ }
+
+ $is_text = $this->getEngine()->isTextMode();
+ $is_html_mail = $this->getEngine()->isHTMLMailMode();
+
+ if ($is_text || $is_html_mail) {
+ return pht('"%s"', $config_key);
+ }
+
+ $link = phutil_tag(
+ 'a',
+ array(
+ 'href' => urisprintf('/config/edit/%s/', $config_key),
+ 'target' => '_blank',
+ ),
+ $config_key);
+
+ return $this->getEngine()->storeText($link);
+ }
+
+}
diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
index 931840e8f..9f7a69909 100644
--- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
+++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
@@ -1,2955 +1,2962 @@
<?php
/**
* A query class which uses cursor-based paging. This paging is much more
* performant than offset-based paging in the presence of policy filtering.
*
* @task clauses Building Query Clauses
* @task appsearch Integration with ApplicationSearch
* @task customfield Integration with CustomField
* @task paging Paging
* @task order Result Ordering
* @task edgelogic Working with Edge Logic
* @task spaces Working with Spaces
*/
abstract class PhabricatorCursorPagedPolicyAwareQuery
extends PhabricatorPolicyAwareQuery {
private $afterID;
private $beforeID;
private $applicationSearchConstraints = array();
private $internalPaging;
private $orderVector;
private $groupVector;
private $builtinOrder;
private $edgeLogicConstraints = array();
private $edgeLogicConstraintsAreValid = false;
private $spacePHIDs;
private $spaceIsArchived;
private $ngrams = array();
private $ferretEngine;
private $ferretTokens = array();
private $ferretTables = array();
private $ferretQuery;
private $ferretMetadata = array();
protected function getPageCursors(array $page) {
return array(
$this->getResultCursor(head($page)),
$this->getResultCursor(last($page)),
);
}
protected function getResultCursor($object) {
if (!is_object($object)) {
throw new Exception(
pht(
'Expected object, got "%s".',
gettype($object)));
}
return $object->getID();
}
protected function nextPage(array $page) {
// See getPagingViewer() for a description of this flag.
$this->internalPaging = true;
if ($this->beforeID !== null) {
$page = array_reverse($page, $preserve_keys = true);
list($before, $after) = $this->getPageCursors($page);
$this->beforeID = $before;
} else {
list($before, $after) = $this->getPageCursors($page);
$this->afterID = $after;
}
}
final public function setAfterID($object_id) {
$this->afterID = $object_id;
return $this;
}
final protected function getAfterID() {
return $this->afterID;
}
final public function setBeforeID($object_id) {
$this->beforeID = $object_id;
return $this;
}
final protected function getBeforeID() {
return $this->beforeID;
}
final public function getFerretMetadata() {
if (!$this->supportsFerretEngine()) {
throw new Exception(
pht(
'Unable to retrieve Ferret engine metadata, this class ("%s") does '.
'not support the Ferret engine.',
get_class($this)));
}
return $this->ferretMetadata;
}
protected function loadStandardPage(PhabricatorLiskDAO $table) {
$rows = $this->loadStandardPageRows($table);
return $table->loadAllFromArray($rows);
}
protected function loadStandardPageRows(PhabricatorLiskDAO $table) {
$conn = $table->establishConnection('r');
return $this->loadStandardPageRowsWithConnection(
$conn,
$table->getTableName());
}
protected function loadStandardPageRowsWithConnection(
AphrontDatabaseConnection $conn,
$table_name) {
$query = $this->buildStandardPageQuery($conn, $table_name);
$rows = queryfx_all($conn, '%Q', $query);
$rows = $this->didLoadRawRows($rows);
return $rows;
}
protected function buildStandardPageQuery(
AphrontDatabaseConnection $conn,
$table_name) {
$table_alias = $this->getPrimaryTableAlias();
if ($table_alias === null) {
$table_alias = qsprintf($conn, '');
} else {
$table_alias = qsprintf($conn, '%T', $table_alias);
}
return qsprintf(
$conn,
'%Q FROM %T %Q %Q %Q %Q %Q %Q %Q',
$this->buildSelectClause($conn),
$table_name,
$table_alias,
$this->buildJoinClause($conn),
$this->buildWhereClause($conn),
$this->buildGroupClause($conn),
$this->buildHavingClause($conn),
$this->buildOrderClause($conn),
$this->buildLimitClause($conn));
}
protected function didLoadRawRows(array $rows) {
if ($this->ferretEngine) {
foreach ($rows as $row) {
$phid = $row['phid'];
$metadata = id(new PhabricatorFerretMetadata())
->setPHID($phid)
->setEngine($this->ferretEngine)
->setRelevance(idx($row, '_ft_rank'));
$this->ferretMetadata[$phid] = $metadata;
unset($row['_ft_rank']);
}
}
return $rows;
}
/**
* Get the viewer for making cursor paging queries.
*
* NOTE: You should ONLY use this viewer to load cursor objects while
* building paging queries.
*
* Cursor paging can happen in two ways. First, the user can request a page
* like `/stuff/?after=33`, which explicitly causes paging. Otherwise, we
* can fall back to implicit paging if we filter some results out of a
* result list because the user can't see them and need to go fetch some more
* results to generate a large enough result list.
*
* In the first case, want to use the viewer's policies to load the object.
* This prevents an attacker from figuring out information about an object
* they can't see by executing queries like `/stuff/?after=33&order=name`,
* which would otherwise give them a hint about the name of the object.
* Generally, if a user can't see an object, they can't use it to page.
*
* In the second case, we need to load the object whether the user can see
* it or not, because we need to examine new results. For example, if a user
* loads `/stuff/` and we run a query for the first 100 items that they can
* see, but the first 100 rows in the database aren't visible, we need to
* be able to issue a query for the next 100 results. If we can't load the
* cursor object, we'll fail or issue the same query over and over again.
* So, generally, internal paging must bypass policy controls.
*
* This method returns the appropriate viewer, based on the context in which
* the paging is occurring.
*
* @return PhabricatorUser Viewer for executing paging queries.
*/
final protected function getPagingViewer() {
if ($this->internalPaging) {
return PhabricatorUser::getOmnipotentUser();
} else {
return $this->getViewer();
}
}
final protected function buildLimitClause(AphrontDatabaseConnection $conn) {
if ($this->shouldLimitResults()) {
$limit = $this->getRawResultLimit();
if ($limit) {
return qsprintf($conn, 'LIMIT %d', $limit);
}
}
return qsprintf($conn, '');
}
protected function shouldLimitResults() {
return true;
}
final protected function didLoadResults(array $results) {
if ($this->beforeID) {
$results = array_reverse($results, $preserve_keys = true);
}
return $results;
}
final public function executeWithCursorPager(AphrontCursorPagerView $pager) {
$limit = $pager->getPageSize();
$this->setLimit($limit + 1);
if ($pager->getAfterID()) {
$this->setAfterID($pager->getAfterID());
} else if ($pager->getBeforeID()) {
$this->setBeforeID($pager->getBeforeID());
}
$results = $this->execute();
$count = count($results);
$sliced_results = $pager->sliceResults($results);
if ($sliced_results) {
list($before, $after) = $this->getPageCursors($sliced_results);
if ($pager->getBeforeID() || ($count > $limit)) {
$pager->setNextPageID($after);
}
if ($pager->getAfterID() ||
($pager->getBeforeID() && ($count > $limit))) {
$pager->setPrevPageID($before);
}
}
return $sliced_results;
}
/**
* Return the alias this query uses to identify the primary table.
*
* Some automatic query constructions may need to be qualified with a table
* alias if the query performs joins which make column names ambiguous. If
* this is the case, return the alias for the primary table the query
* uses; generally the object table which has `id` and `phid` columns.
*
* @return string Alias for the primary table.
*/
protected function getPrimaryTableAlias() {
return null;
}
public function newResultObject() {
return null;
}
/* -( Building Query Clauses )--------------------------------------------- */
/**
* @task clauses
*/
protected function buildSelectClause(AphrontDatabaseConnection $conn) {
$parts = $this->buildSelectClauseParts($conn);
return $this->formatSelectClause($conn, $parts);
}
/**
* @task clauses
*/
protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) {
$select = array();
$alias = $this->getPrimaryTableAlias();
if ($alias) {
$select[] = qsprintf($conn, '%T.*', $alias);
} else {
$select[] = qsprintf($conn, '*');
}
$select[] = $this->buildEdgeLogicSelectClause($conn);
$select[] = $this->buildFerretSelectClause($conn);
return $select;
}
/**
* @task clauses
*/
protected function buildJoinClause(AphrontDatabaseConnection $conn) {
$joins = $this->buildJoinClauseParts($conn);
return $this->formatJoinClause($conn, $joins);
}
/**
* @task clauses
*/
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = array();
$joins[] = $this->buildEdgeLogicJoinClause($conn);
$joins[] = $this->buildApplicationSearchJoinClause($conn);
$joins[] = $this->buildNgramsJoinClause($conn);
$joins[] = $this->buildFerretJoinClause($conn);
return $joins;
}
/**
* @task clauses
*/
protected function buildWhereClause(AphrontDatabaseConnection $conn) {
$where = $this->buildWhereClauseParts($conn);
return $this->formatWhereClause($conn, $where);
}
/**
* @task clauses
*/
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = array();
$where[] = $this->buildPagingClause($conn);
$where[] = $this->buildEdgeLogicWhereClause($conn);
$where[] = $this->buildSpacesWhereClause($conn);
$where[] = $this->buildNgramsWhereClause($conn);
$where[] = $this->buildFerretWhereClause($conn);
$where[] = $this->buildApplicationSearchWhereClause($conn);
return $where;
}
/**
* @task clauses
*/
protected function buildHavingClause(AphrontDatabaseConnection $conn) {
$having = $this->buildHavingClauseParts($conn);
return $this->formatHavingClause($conn, $having);
}
/**
* @task clauses
*/
protected function buildHavingClauseParts(AphrontDatabaseConnection $conn) {
$having = array();
$having[] = $this->buildEdgeLogicHavingClause($conn);
return $having;
}
/**
* @task clauses
*/
protected function buildGroupClause(AphrontDatabaseConnection $conn) {
if (!$this->shouldGroupQueryResultRows()) {
return qsprintf($conn, '');
}
return qsprintf(
$conn,
'GROUP BY %Q',
$this->getApplicationSearchObjectPHIDColumn($conn));
}
/**
* @task clauses
*/
protected function shouldGroupQueryResultRows() {
if ($this->shouldGroupEdgeLogicResultRows()) {
return true;
}
if ($this->getApplicationSearchMayJoinMultipleRows()) {
return true;
}
if ($this->shouldGroupNgramResultRows()) {
return true;
}
if ($this->shouldGroupFerretResultRows()) {
return true;
}
return false;
}
/* -( Paging )------------------------------------------------------------- */
/**
* @task paging
*/
protected function buildPagingClause(AphrontDatabaseConnection $conn) {
$orderable = $this->getOrderableColumns();
$vector = $this->getOrderVector();
if ($this->beforeID !== null) {
$cursor = $this->beforeID;
$reversed = true;
} else if ($this->afterID !== null) {
$cursor = $this->afterID;
$reversed = false;
} else {
// No paging is being applied to this query so we do not need to
// construct a paging clause.
return qsprintf($conn, '');
}
$keys = array();
foreach ($vector as $order) {
$keys[] = $order->getOrderKey();
}
$value_map = $this->getPagingValueMap($cursor, $keys);
$columns = array();
foreach ($vector as $order) {
$key = $order->getOrderKey();
if (!array_key_exists($key, $value_map)) {
throw new Exception(
pht(
'Query "%s" failed to return a value from getPagingValueMap() '.
'for column "%s".',
get_class($this),
$key));
}
$column = $orderable[$key];
$column['value'] = $value_map[$key];
// If the vector component is reversed, we need to reverse whatever the
// order of the column is.
if ($order->getIsReversed()) {
$column['reverse'] = !idx($column, 'reverse', false);
}
$columns[] = $column;
}
return $this->buildPagingClauseFromMultipleColumns(
$conn,
$columns,
array(
'reversed' => $reversed,
));
}
/**
* @task paging
*/
protected function getPagingValueMap($cursor, array $keys) {
return array(
'id' => $cursor,
);
}
/**
* @task paging
*/
protected function loadCursorObject($cursor) {
$query = newv(get_class($this), array())
->setViewer($this->getPagingViewer())
->withIDs(array((int)$cursor));
$this->willExecuteCursorQuery($query);
$object = $query->executeOne();
if (!$object) {
throw new Exception(
pht(
'Cursor "%s" does not identify a valid object in query "%s".',
$cursor,
get_class($this)));
}
return $object;
}
/**
* @task paging
*/
protected function willExecuteCursorQuery(
PhabricatorCursorPagedPolicyAwareQuery $query) {
return;
}
/**
* Simplifies the task of constructing a paging clause across multiple
* columns. In the general case, this looks like:
*
* A > a OR (A = a AND B > b) OR (A = a AND B = b AND C > c)
*
* To build a clause, specify the name, type, and value of each column
* to include:
*
* $this->buildPagingClauseFromMultipleColumns(
* $conn_r,
* array(
* array(
* 'table' => 't',
* 'column' => 'title',
* 'type' => 'string',
* 'value' => $cursor->getTitle(),
* 'reverse' => true,
* ),
* array(
* 'table' => 't',
* 'column' => 'id',
* 'type' => 'int',
* 'value' => $cursor->getID(),
* ),
* ),
* array(
* 'reversed' => $is_reversed,
* ));
*
* This method will then return a composable clause for inclusion in WHERE.
*
* @param AphrontDatabaseConnection Connection query will execute on.
* @param list<map> Column description dictionaries.
* @param map Additional construction options.
* @return string Query clause.
* @task paging
*/
final protected function buildPagingClauseFromMultipleColumns(
AphrontDatabaseConnection $conn,
array $columns,
array $options) {
foreach ($columns as $column) {
PhutilTypeSpec::checkMap(
$column,
array(
'table' => 'optional string|null',
'column' => 'string',
'value' => 'wild',
'type' => 'string',
'reverse' => 'optional bool',
'unique' => 'optional bool',
'null' => 'optional string|null',
));
}
PhutilTypeSpec::checkMap(
$options,
array(
'reversed' => 'optional bool',
));
$is_query_reversed = idx($options, 'reversed', false);
$clauses = array();
$accumulated = array();
$last_key = last_key($columns);
foreach ($columns as $key => $column) {
$type = $column['type'];
$null = idx($column, 'null');
if ($column['value'] === null) {
if ($null) {
$value = null;
} else {
throw new Exception(
pht(
'Column "%s" has null value, but does not specify a null '.
'behavior.',
$key));
}
} else {
switch ($type) {
case 'int':
$value = qsprintf($conn, '%d', $column['value']);
break;
case 'float':
$value = qsprintf($conn, '%f', $column['value']);
break;
case 'string':
$value = qsprintf($conn, '%s', $column['value']);
break;
default:
throw new Exception(
pht(
'Column "%s" has unknown column type "%s".',
$column['column'],
$type));
}
}
$is_column_reversed = idx($column, 'reverse', false);
$reverse = ($is_query_reversed xor $is_column_reversed);
$clause = $accumulated;
$table_name = idx($column, 'table');
$column_name = $column['column'];
if ($table_name !== null) {
$field = qsprintf($conn, '%T.%T', $table_name, $column_name);
} else {
$field = qsprintf($conn, '%T', $column_name);
}
$parts = array();
if ($null) {
$can_page_if_null = ($null === 'head');
$can_page_if_nonnull = ($null === 'tail');
if ($reverse) {
$can_page_if_null = !$can_page_if_null;
$can_page_if_nonnull = !$can_page_if_nonnull;
}
$subclause = null;
if ($can_page_if_null && $value === null) {
$parts[] = qsprintf(
$conn,
'(%Q IS NOT NULL)',
$field);
} else if ($can_page_if_nonnull && $value !== null) {
$parts[] = qsprintf(
$conn,
'(%Q IS NULL)',
$field);
}
}
if ($value !== null) {
$parts[] = qsprintf(
$conn,
'%Q %Q %Q',
$field,
$reverse ? qsprintf($conn, '>') : qsprintf($conn, '<'),
$value);
}
if ($parts) {
$clause[] = qsprintf($conn, '%LO', $parts);
}
if ($clause) {
$clauses[] = qsprintf($conn, '%LA', $clause);
}
if ($value === null) {
$accumulated[] = qsprintf(
$conn,
'%Q IS NULL',
$field);
} else {
$accumulated[] = qsprintf(
$conn,
'%Q = %Q',
$field,
$value);
}
}
if ($clauses) {
return qsprintf($conn, '%LO', $clauses);
}
return qsprintf($conn, '');
}
/* -( Result Ordering )---------------------------------------------------- */
/**
* Select a result ordering.
*
* This is a high-level method which selects an ordering from a predefined
* list of builtin orders, as provided by @{method:getBuiltinOrders}. These
* options are user-facing and not exhaustive, but are generally convenient
* and meaningful.
*
* You can also use @{method:setOrderVector} to specify a low-level ordering
* across individual orderable columns. This offers greater control but is
* also more involved.
*
* @param string Key of a builtin order supported by this query.
* @return this
* @task order
*/
public function setOrder($order) {
$aliases = $this->getBuiltinOrderAliasMap();
if (empty($aliases[$order])) {
throw new Exception(
pht(
'Query "%s" does not support a builtin order "%s". Supported orders '.
'are: %s.',
get_class($this),
$order,
implode(', ', array_keys($aliases))));
}
$this->builtinOrder = $aliases[$order];
$this->orderVector = null;
return $this;
}
/**
* Set a grouping order to apply before primary result ordering.
*
* This allows you to preface the query order vector with additional orders,
* so you can effect "group by" queries while still respecting "order by".
*
* This is a high-level method which works alongside @{method:setOrder}. For
* lower-level control over order vectors, use @{method:setOrderVector}.
*
* @param PhabricatorQueryOrderVector|list<string> List of order keys.
* @return this
* @task order
*/
public function setGroupVector($vector) {
$this->groupVector = $vector;
$this->orderVector = null;
return $this;
}
/**
* Get builtin orders for this class.
*
* In application UIs, we want to be able to present users with a small
* selection of meaningful order options (like "Order by Title") rather than
* an exhaustive set of column ordering options.
*
* Meaningful user-facing orders are often really orders across multiple
* columns: for example, a "title" ordering is usually implemented as a
* "title, id" ordering under the hood.
*
* Builtin orders provide a mapping from convenient, understandable
* user-facing orders to implementations.
*
* A builtin order should provide these keys:
*
* - `vector` (`list<string>`): The actual order vector to use.
* - `name` (`string`): Human-readable order name.
*
* @return map<string, wild> Map from builtin order keys to specification.
* @task order
*/
public function getBuiltinOrders() {
$orders = array(
'newest' => array(
'vector' => array('id'),
'name' => pht('Creation (Newest First)'),
'aliases' => array('created'),
),
'oldest' => array(
'vector' => array('-id'),
'name' => pht('Creation (Oldest First)'),
),
);
$object = $this->newResultObject();
if ($object instanceof PhabricatorCustomFieldInterface) {
$list = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
foreach ($list->getFields() as $field) {
$index = $field->buildOrderIndex();
if (!$index) {
continue;
}
$legacy_key = 'custom:'.$field->getFieldKey();
$modern_key = $field->getModernFieldKey();
$orders[$modern_key] = array(
'vector' => array($modern_key, 'id'),
'name' => $field->getFieldName(),
'aliases' => array($legacy_key),
);
$orders['-'.$modern_key] = array(
'vector' => array('-'.$modern_key, '-id'),
'name' => pht('%s (Reversed)', $field->getFieldName()),
);
}
}
if ($this->supportsFerretEngine()) {
$orders['relevance'] = array(
'vector' => array('rank', 'fulltext-modified', 'id'),
'name' => pht('Relevance'),
);
}
return $orders;
}
public function getBuiltinOrderAliasMap() {
$orders = $this->getBuiltinOrders();
$map = array();
foreach ($orders as $key => $order) {
$keys = array();
$keys[] = $key;
foreach (idx($order, 'aliases', array()) as $alias) {
$keys[] = $alias;
}
foreach ($keys as $alias) {
if (isset($map[$alias])) {
throw new Exception(
pht(
'Two builtin orders ("%s" and "%s") define the same key or '.
'alias ("%s"). Each order alias and key must be unique and '.
'identify a single order.',
$key,
$map[$alias],
$alias));
}
$map[$alias] = $key;
}
}
return $map;
}
/**
* Set a low-level column ordering.
*
* This is a low-level method which offers granular control over column
* ordering. In most cases, applications can more easily use
* @{method:setOrder} to choose a high-level builtin order.
*
* To set an order vector, specify a list of order keys as provided by
* @{method:getOrderableColumns}.
*
* @param PhabricatorQueryOrderVector|list<string> List of order keys.
* @return this
* @task order
*/
public function setOrderVector($vector) {
$vector = PhabricatorQueryOrderVector::newFromVector($vector);
$orderable = $this->getOrderableColumns();
// Make sure that all the components identify valid columns.
$unique = array();
foreach ($vector as $order) {
$key = $order->getOrderKey();
if (empty($orderable[$key])) {
$valid = implode(', ', array_keys($orderable));
throw new Exception(
pht(
'This query ("%s") does not support sorting by order key "%s". '.
'Supported orders are: %s.',
get_class($this),
$key,
$valid));
}
$unique[$key] = idx($orderable[$key], 'unique', false);
}
// Make sure that the last column is unique so that this is a strong
// ordering which can be used for paging.
$last = last($unique);
if ($last !== true) {
throw new Exception(
pht(
'Order vector "%s" is invalid: the last column in an order must '.
'be a column with unique values, but "%s" is not unique.',
$vector->getAsString(),
last_key($unique)));
}
// Make sure that other columns are not unique; an ordering like "id, name"
// does not make sense because only "id" can ever have an effect.
array_pop($unique);
foreach ($unique as $key => $is_unique) {
if ($is_unique) {
throw new Exception(
pht(
'Order vector "%s" is invalid: only the last column in an order '.
'may be unique, but "%s" is a unique column and not the last '.
'column in the order.',
$vector->getAsString(),
$key));
}
}
$this->orderVector = $vector;
return $this;
}
/**
* Get the effective order vector.
*
* @return PhabricatorQueryOrderVector Effective vector.
* @task order
*/
protected function getOrderVector() {
if (!$this->orderVector) {
if ($this->builtinOrder !== null) {
$builtin_order = idx($this->getBuiltinOrders(), $this->builtinOrder);
$vector = $builtin_order['vector'];
} else {
$vector = $this->getDefaultOrderVector();
}
if ($this->groupVector) {
$group = PhabricatorQueryOrderVector::newFromVector($this->groupVector);
$group->appendVector($vector);
$vector = $group;
}
$vector = PhabricatorQueryOrderVector::newFromVector($vector);
// We call setOrderVector() here to apply checks to the default vector.
// This catches any errors in the implementation.
$this->setOrderVector($vector);
}
return $this->orderVector;
}
/**
* @task order
*/
protected function getDefaultOrderVector() {
return array('id');
}
/**
* @task order
*/
public function getOrderableColumns() {
$cache = PhabricatorCaches::getRequestCache();
$class = get_class($this);
$cache_key = 'query.orderablecolumns.'.$class;
$columns = $cache->getKey($cache_key);
if ($columns !== null) {
return $columns;
}
$columns = array(
'id' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'id',
'reverse' => false,
'type' => 'int',
'unique' => true,
),
);
$object = $this->newResultObject();
if ($object instanceof PhabricatorCustomFieldInterface) {
$list = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
foreach ($list->getFields() as $field) {
$index = $field->buildOrderIndex();
if (!$index) {
continue;
}
$digest = $field->getFieldIndex();
$key = $field->getModernFieldKey();
$columns[$key] = array(
'table' => 'appsearch_order_'.$digest,
'column' => 'indexValue',
'type' => $index->getIndexValueType(),
'null' => 'tail',
'customfield' => true,
'customfield.index.table' => $index->getTableName(),
'customfield.index.key' => $digest,
);
}
}
if ($this->supportsFerretEngine()) {
$columns['rank'] = array(
'table' => null,
'column' => '_ft_rank',
'type' => 'int',
);
$columns['fulltext-created'] = array(
'table' => 'ft_doc',
'column' => 'epochCreated',
'type' => 'int',
);
$columns['fulltext-modified'] = array(
'table' => 'ft_doc',
'column' => 'epochModified',
'type' => 'int',
);
}
$cache->setKey($cache_key, $columns);
return $columns;
}
/**
* @task order
*/
final protected function buildOrderClause(
AphrontDatabaseConnection $conn,
$for_union = false) {
$orderable = $this->getOrderableColumns();
$vector = $this->getOrderVector();
$parts = array();
foreach ($vector as $order) {
$part = $orderable[$order->getOrderKey()];
if ($order->getIsReversed()) {
$part['reverse'] = !idx($part, 'reverse', false);
}
$parts[] = $part;
}
return $this->formatOrderClause($conn, $parts, $for_union);
}
/**
* @task order
*/
protected function formatOrderClause(
AphrontDatabaseConnection $conn,
array $parts,
$for_union = false) {
$is_query_reversed = false;
if ($this->getBeforeID()) {
$is_query_reversed = !$is_query_reversed;
}
$sql = array();
foreach ($parts as $key => $part) {
$is_column_reversed = !empty($part['reverse']);
$descending = true;
if ($is_query_reversed) {
$descending = !$descending;
}
if ($is_column_reversed) {
$descending = !$descending;
}
$table = idx($part, 'table');
// When we're building an ORDER BY clause for a sequence of UNION
// statements, we can't refer to tables from the subqueries.
if ($for_union) {
$table = null;
}
$column = $part['column'];
if ($table !== null) {
$field = qsprintf($conn, '%T.%T', $table, $column);
} else {
$field = qsprintf($conn, '%T', $column);
}
$null = idx($part, 'null');
if ($null) {
switch ($null) {
case 'head':
$null_field = qsprintf($conn, '(%Q IS NULL)', $field);
break;
case 'tail':
$null_field = qsprintf($conn, '(%Q IS NOT NULL)', $field);
break;
default:
throw new Exception(
pht(
'NULL value "%s" is invalid. Valid values are "head" and '.
'"tail".',
$null));
}
if ($descending) {
$sql[] = qsprintf($conn, '%Q DESC', $null_field);
} else {
$sql[] = qsprintf($conn, '%Q ASC', $null_field);
}
}
if ($descending) {
$sql[] = qsprintf($conn, '%Q DESC', $field);
} else {
$sql[] = qsprintf($conn, '%Q ASC', $field);
}
}
return qsprintf($conn, 'ORDER BY %LQ', $sql);
}
/* -( Application Search )------------------------------------------------- */
/**
* Constrain the query with an ApplicationSearch index, requiring field values
* contain at least one of the values in a set.
*
* This constraint can build the most common types of queries, like:
*
* - Find users with shirt sizes "X" or "XL".
* - Find shoes with size "13".
*
* @param PhabricatorCustomFieldIndexStorage Table where the index is stored.
* @param string|list<string> One or more values to filter by.
* @return this
* @task appsearch
*/
public function withApplicationSearchContainsConstraint(
PhabricatorCustomFieldIndexStorage $index,
$value) {
$values = (array)$value;
$data_values = array();
$constraint_values = array();
foreach ($values as $value) {
if ($value instanceof PhabricatorQueryConstraint) {
$constraint_values[] = $value;
} else {
$data_values[] = $value;
}
}
$alias = 'appsearch_'.count($this->applicationSearchConstraints);
$this->applicationSearchConstraints[] = array(
'type' => $index->getIndexValueType(),
'cond' => '=',
'table' => $index->getTableName(),
'index' => $index->getIndexKey(),
'alias' => $alias,
'value' => $values,
'data' => $data_values,
'constraints' => $constraint_values,
);
return $this;
}
/**
* Constrain the query with an ApplicationSearch index, requiring values
* exist in a given range.
*
* This constraint is useful for expressing date ranges:
*
* - Find events between July 1st and July 7th.
*
* The ends of the range are inclusive, so a `$min` of `3` and a `$max` of
* `5` will match fields with values `3`, `4`, or `5`. Providing `null` for
* either end of the range will leave that end of the constraint open.
*
* @param PhabricatorCustomFieldIndexStorage Table where the index is stored.
* @param int|null Minimum permissible value, inclusive.
* @param int|null Maximum permissible value, inclusive.
* @return this
* @task appsearch
*/
public function withApplicationSearchRangeConstraint(
PhabricatorCustomFieldIndexStorage $index,
$min,
$max) {
$index_type = $index->getIndexValueType();
if ($index_type != 'int') {
throw new Exception(
pht(
'Attempting to apply a range constraint to a field with index type '.
'"%s", expected type "%s".',
$index_type,
'int'));
}
$alias = 'appsearch_'.count($this->applicationSearchConstraints);
$this->applicationSearchConstraints[] = array(
'type' => $index->getIndexValueType(),
'cond' => 'range',
'table' => $index->getTableName(),
'index' => $index->getIndexKey(),
'alias' => $alias,
'value' => array($min, $max),
);
return $this;
}
/**
* Get the name of the query's primary object PHID column, for constructing
* JOIN clauses. Normally (and by default) this is just `"phid"`, but it may
* be something more exotic.
*
* See @{method:getPrimaryTableAlias} if the column needs to be qualified with
* a table alias.
*
* @param AphrontDatabaseConnection Connection executing queries.
* @return PhutilQueryString Column name.
* @task appsearch
*/
protected function getApplicationSearchObjectPHIDColumn(
AphrontDatabaseConnection $conn) {
if ($this->getPrimaryTableAlias()) {
return qsprintf($conn, '%T.phid', $this->getPrimaryTableAlias());
} else {
return qsprintf($conn, 'phid');
}
}
/**
* Determine if the JOINs built by ApplicationSearch might cause each primary
* object to return multiple result rows. Generally, this means the query
* needs an extra GROUP BY clause.
*
* @return bool True if the query may return multiple rows for each object.
* @task appsearch
*/
protected function getApplicationSearchMayJoinMultipleRows() {
foreach ($this->applicationSearchConstraints as $constraint) {
$type = $constraint['type'];
$value = $constraint['value'];
$cond = $constraint['cond'];
switch ($cond) {
case '=':
switch ($type) {
case 'string':
case 'int':
if (count($value) > 1) {
return true;
}
break;
default:
throw new Exception(pht('Unknown index type "%s"!', $type));
}
break;
case 'range':
// NOTE: It's possible to write a custom field where multiple rows
// match a range constraint, but we don't currently ship any in the
// upstream and I can't immediately come up with cases where this
// would make sense.
break;
default:
throw new Exception(pht('Unknown constraint condition "%s"!', $cond));
}
}
return false;
}
/**
* Construct a GROUP BY clause appropriate for ApplicationSearch constraints.
*
* @param AphrontDatabaseConnection Connection executing the query.
* @return string Group clause.
* @task appsearch
*/
protected function buildApplicationSearchGroupClause(
AphrontDatabaseConnection $conn) {
if ($this->getApplicationSearchMayJoinMultipleRows()) {
return qsprintf(
$conn,
'GROUP BY %Q',
$this->getApplicationSearchObjectPHIDColumn($conn));
} else {
return qsprintf($conn, '');
}
}
/**
* Construct a JOIN clause appropriate for applying ApplicationSearch
* constraints.
*
* @param AphrontDatabaseConnection Connection executing the query.
* @return string Join clause.
* @task appsearch
*/
protected function buildApplicationSearchJoinClause(
AphrontDatabaseConnection $conn) {
$joins = array();
foreach ($this->applicationSearchConstraints as $key => $constraint) {
$table = $constraint['table'];
$alias = $constraint['alias'];
$index = $constraint['index'];
$cond = $constraint['cond'];
$phid_column = $this->getApplicationSearchObjectPHIDColumn($conn);
switch ($cond) {
case '=':
// Figure out whether we need to do a LEFT JOIN or not. We need to
// LEFT JOIN if we're going to select "IS NULL" rows.
$join_type = qsprintf($conn, 'JOIN');
foreach ($constraint['constraints'] as $query_constraint) {
$op = $query_constraint->getOperator();
if ($op === PhabricatorQueryConstraint::OPERATOR_NULL) {
$join_type = qsprintf($conn, 'LEFT JOIN');
break;
}
}
$joins[] = qsprintf(
$conn,
'%Q %T %T ON %T.objectPHID = %Q
AND %T.indexKey = %s',
$join_type,
$table,
$alias,
$alias,
$phid_column,
$alias,
$index);
break;
case 'range':
list($min, $max) = $constraint['value'];
if (($min === null) && ($max === null)) {
// If there's no actual range constraint, just move on.
break;
}
if ($min === null) {
$constraint_clause = qsprintf(
$conn,
'%T.indexValue <= %d',
$alias,
$max);
} else if ($max === null) {
$constraint_clause = qsprintf(
$conn,
'%T.indexValue >= %d',
$alias,
$min);
} else {
$constraint_clause = qsprintf(
$conn,
'%T.indexValue BETWEEN %d AND %d',
$alias,
$min,
$max);
}
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON %T.objectPHID = %Q
AND %T.indexKey = %s
AND (%Q)',
$table,
$alias,
$alias,
$phid_column,
$alias,
$index,
$constraint_clause);
break;
default:
throw new Exception(pht('Unknown constraint condition "%s"!', $cond));
}
}
$phid_column = $this->getApplicationSearchObjectPHIDColumn($conn);
$orderable = $this->getOrderableColumns();
$vector = $this->getOrderVector();
foreach ($vector as $order) {
$spec = $orderable[$order->getOrderKey()];
if (empty($spec['customfield'])) {
continue;
}
$table = $spec['customfield.index.table'];
$alias = $spec['table'];
$key = $spec['customfield.index.key'];
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T %T ON %T.objectPHID = %Q
AND %T.indexKey = %s',
$table,
$alias,
$alias,
$phid_column,
$alias,
$key);
}
if ($joins) {
return qsprintf($conn, '%LJ', $joins);
} else {
return qsprintf($conn, '');
}
}
/**
* Construct a WHERE clause appropriate for applying ApplicationSearch
* constraints.
*
* @param AphrontDatabaseConnection Connection executing the query.
* @return list<string> Where clause parts.
* @task appsearch
*/
protected function buildApplicationSearchWhereClause(
AphrontDatabaseConnection $conn) {
$where = array();
foreach ($this->applicationSearchConstraints as $key => $constraint) {
$alias = $constraint['alias'];
$cond = $constraint['cond'];
$type = $constraint['type'];
$data_values = $constraint['data'];
$constraint_values = $constraint['constraints'];
$constraint_parts = array();
switch ($cond) {
case '=':
if ($data_values) {
switch ($type) {
case 'string':
$constraint_parts[] = qsprintf(
$conn,
'%T.indexValue IN (%Ls)',
$alias,
$data_values);
break;
case 'int':
$constraint_parts[] = qsprintf(
$conn,
'%T.indexValue IN (%Ld)',
$alias,
$data_values);
break;
default:
throw new Exception(pht('Unknown index type "%s"!', $type));
}
}
if ($constraint_values) {
foreach ($constraint_values as $value) {
$op = $value->getOperator();
switch ($op) {
case PhabricatorQueryConstraint::OPERATOR_NULL:
$constraint_parts[] = qsprintf(
$conn,
'%T.indexValue IS NULL',
$alias);
break;
case PhabricatorQueryConstraint::OPERATOR_ANY:
$constraint_parts[] = qsprintf(
$conn,
'%T.indexValue IS NOT NULL',
$alias);
break;
default:
throw new Exception(
pht(
'No support for applying operator "%s" against '.
'index of type "%s".',
$op,
$type));
}
}
}
if ($constraint_parts) {
$where[] = qsprintf($conn, '%LO', $constraint_parts);
}
break;
}
}
return $where;
}
/* -( Integration with CustomField )--------------------------------------- */
/**
* @task customfield
*/
protected function getPagingValueMapForCustomFields(
PhabricatorCustomFieldInterface $object) {
// We have to get the current field values on the cursor object.
$fields = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
$fields->setViewer($this->getViewer());
$fields->readFieldsFromStorage($object);
$map = array();
foreach ($fields->getFields() as $field) {
$map['custom:'.$field->getFieldKey()] = $field->getValueForStorage();
}
return $map;
}
/**
* @task customfield
*/
protected function isCustomFieldOrderKey($key) {
$prefix = 'custom:';
return !strncmp($key, $prefix, strlen($prefix));
}
/* -( Ferret )------------------------------------------------------------- */
public function supportsFerretEngine() {
$object = $this->newResultObject();
return ($object instanceof PhabricatorFerretInterface);
}
public function withFerretQuery(
PhabricatorFerretEngine $engine,
PhabricatorSavedQuery $query) {
if (!$this->supportsFerretEngine()) {
throw new Exception(
pht(
'Query ("%s") does not support the Ferret fulltext engine.',
get_class($this)));
}
$this->ferretEngine = $engine;
$this->ferretQuery = $query;
return $this;
}
public function getFerretTokens() {
if (!$this->supportsFerretEngine()) {
throw new Exception(
pht(
'Query ("%s") does not support the Ferret fulltext engine.',
get_class($this)));
}
return $this->ferretTokens;
}
public function withFerretConstraint(
PhabricatorFerretEngine $engine,
array $fulltext_tokens) {
if (!$this->supportsFerretEngine()) {
throw new Exception(
pht(
'Query ("%s") does not support the Ferret fulltext engine.',
get_class($this)));
}
if ($this->ferretEngine) {
throw new Exception(
pht(
'Query may not have multiple fulltext constraints.'));
}
if (!$fulltext_tokens) {
return $this;
}
$this->ferretEngine = $engine;
$this->ferretTokens = $fulltext_tokens;
$current_function = $engine->getDefaultFunctionKey();
$table_map = array();
$idx = 1;
foreach ($this->ferretTokens as $fulltext_token) {
$raw_token = $fulltext_token->getToken();
$function = $raw_token->getFunction();
if ($function === null) {
$function = $current_function;
}
$raw_field = $engine->getFieldForFunction($function);
if (!isset($table_map[$function])) {
$alias = 'ftfield_'.$idx++;
$table_map[$function] = array(
'alias' => $alias,
'key' => $raw_field,
);
}
$current_function = $function;
}
// Join the title field separately so we can rank results.
$table_map['rank'] = array(
'alias' => 'ft_rank',
'key' => PhabricatorSearchDocumentFieldType::FIELD_TITLE,
);
$this->ferretTables = $table_map;
return $this;
}
protected function buildFerretSelectClause(AphrontDatabaseConnection $conn) {
$select = array();
if (!$this->supportsFerretEngine()) {
return $select;
}
$vector = $this->getOrderVector();
if (!$vector->containsKey('rank')) {
// We only need to SELECT the virtual "_ft_rank" column if we're
// actually sorting the results by rank.
return $select;
}
if (!$this->ferretEngine) {
$select[] = qsprintf($conn, '0 _ft_rank');
return $select;
}
$engine = $this->ferretEngine;
$stemmer = $engine->newStemmer();
$op_sub = PhutilSearchQueryCompiler::OPERATOR_SUBSTRING;
$op_not = PhutilSearchQueryCompiler::OPERATOR_NOT;
$table_alias = 'ft_rank';
$parts = array();
foreach ($this->ferretTokens as $fulltext_token) {
$raw_token = $fulltext_token->getToken();
$value = $raw_token->getValue();
if ($raw_token->getOperator() == $op_not) {
// Ignore "not" terms when ranking, since they aren't useful.
continue;
}
if ($raw_token->getOperator() == $op_sub) {
$is_substring = true;
} else {
$is_substring = false;
}
if ($is_substring) {
$parts[] = qsprintf(
$conn,
'IF(%T.rawCorpus LIKE %~, 2, 0)',
$table_alias,
$value);
continue;
}
if ($raw_token->isQuoted()) {
$is_quoted = true;
$is_stemmed = false;
} else {
$is_quoted = false;
$is_stemmed = true;
}
$term_constraints = array();
$term_value = $engine->newTermsCorpus($value);
$parts[] = qsprintf(
$conn,
'IF(%T.termCorpus LIKE %~, 2, 0)',
$table_alias,
$term_value);
if ($is_stemmed) {
$stem_value = $stemmer->stemToken($value);
$stem_value = $engine->newTermsCorpus($stem_value);
$parts[] = qsprintf(
$conn,
'IF(%T.normalCorpus LIKE %~, 1, 0)',
$table_alias,
$stem_value);
}
}
$parts[] = qsprintf($conn, '%d', 0);
$sum = array_shift($parts);
foreach ($parts as $part) {
$sum = qsprintf(
$conn,
'%Q + %Q',
$sum,
$part);
}
$select[] = qsprintf(
$conn,
'%Q _ft_rank',
$sum);
return $select;
}
protected function buildFerretJoinClause(AphrontDatabaseConnection $conn) {
if (!$this->ferretEngine) {
return array();
}
$op_sub = PhutilSearchQueryCompiler::OPERATOR_SUBSTRING;
$op_not = PhutilSearchQueryCompiler::OPERATOR_NOT;
$engine = $this->ferretEngine;
$stemmer = $engine->newStemmer();
$ngram_table = $engine->getNgramsTableName();
$flat = array();
foreach ($this->ferretTokens as $fulltext_token) {
$raw_token = $fulltext_token->getToken();
// If this is a negated term like "-pomegranate", don't join the ngram
// table since we aren't looking for documents with this term. (We could
// LEFT JOIN the table and require a NULL row, but this is probably more
// trouble than it's worth.)
if ($raw_token->getOperator() == $op_not) {
continue;
}
$value = $raw_token->getValue();
$length = count(phutil_utf8v($value));
if ($raw_token->getOperator() == $op_sub) {
$is_substring = true;
} else {
$is_substring = false;
}
// If the user specified a substring query for a substring which is
// shorter than the ngram length, we can't use the ngram index, so
// don't do a join. We'll fall back to just doing LIKE on the full
// corpus.
if ($is_substring) {
if ($length < 3) {
continue;
}
}
if ($raw_token->isQuoted()) {
$is_stemmed = false;
} else {
$is_stemmed = true;
}
if ($is_substring) {
$ngrams = $engine->getSubstringNgramsFromString($value);
} else {
$terms_value = $engine->newTermsCorpus($value);
$ngrams = $engine->getTermNgramsFromString($terms_value);
// If this is a stemmed term, only look for ngrams present in both the
// unstemmed and stemmed variations.
if ($is_stemmed) {
// Trim the boundary space characters so the stemmer recognizes this
// is (or, at least, may be) a normal word and activates.
$terms_value = trim($terms_value, ' ');
$stem_value = $stemmer->stemToken($terms_value);
$stem_ngrams = $engine->getTermNgramsFromString($stem_value);
$ngrams = array_intersect($ngrams, $stem_ngrams);
}
}
foreach ($ngrams as $ngram) {
$flat[] = array(
'table' => $ngram_table,
'ngram' => $ngram,
);
}
}
// Remove common ngrams, like "the", which occur too frequently in
// documents to be useful in constraining the query. The best ngrams
// are obscure sequences which occur in very few documents.
if ($flat) {
$common_ngrams = queryfx_all(
$conn,
'SELECT ngram FROM %T WHERE ngram IN (%Ls)',
$engine->getCommonNgramsTableName(),
ipull($flat, 'ngram'));
$common_ngrams = ipull($common_ngrams, 'ngram', 'ngram');
foreach ($flat as $key => $spec) {
$ngram = $spec['ngram'];
if (isset($common_ngrams[$ngram])) {
unset($flat[$key]);
continue;
}
// NOTE: MySQL discards trailing whitespace in CHAR(X) columns.
$trim_ngram = rtrim($ngram, ' ');
if (isset($common_ngrams[$trim_ngram])) {
unset($flat[$key]);
continue;
}
}
}
// MySQL only allows us to join a maximum of 61 tables per query. Each
// ngram is going to cost us a join toward that limit, so if the user
// specified a very long query string, just pick 16 of the ngrams
// at random.
if (count($flat) > 16) {
shuffle($flat);
$flat = array_slice($flat, 0, 16);
}
$alias = $this->getPrimaryTableAlias();
if ($alias) {
$phid_column = qsprintf($conn, '%T.%T', $alias, 'phid');
} else {
$phid_column = qsprintf($conn, '%T', 'phid');
}
$document_table = $engine->getDocumentTableName();
$field_table = $engine->getFieldTableName();
$joins = array();
$joins[] = qsprintf(
$conn,
'JOIN %T ft_doc ON ft_doc.objectPHID = %Q',
$document_table,
$phid_column);
$idx = 1;
foreach ($flat as $spec) {
$table = $spec['table'];
$ngram = $spec['ngram'];
$alias = 'ftngram_'.$idx++;
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON %T.documentID = ft_doc.id AND %T.ngram = %s',
$table,
$alias,
$alias,
$alias,
$ngram);
}
foreach ($this->ferretTables as $table) {
$alias = $table['alias'];
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON ft_doc.id = %T.documentID
AND %T.fieldKey = %s',
$field_table,
$alias,
$alias,
$alias,
$table['key']);
}
return $joins;
}
protected function buildFerretWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->ferretEngine) {
return array();
}
$engine = $this->ferretEngine;
$stemmer = $engine->newStemmer();
$table_map = $this->ferretTables;
$op_sub = PhutilSearchQueryCompiler::OPERATOR_SUBSTRING;
$op_not = PhutilSearchQueryCompiler::OPERATOR_NOT;
$op_exact = PhutilSearchQueryCompiler::OPERATOR_EXACT;
$where = array();
$current_function = 'all';
foreach ($this->ferretTokens as $fulltext_token) {
$raw_token = $fulltext_token->getToken();
$value = $raw_token->getValue();
$function = $raw_token->getFunction();
if ($function === null) {
$function = $current_function;
}
$current_function = $function;
$table_alias = $table_map[$function]['alias'];
$is_not = ($raw_token->getOperator() == $op_not);
if ($raw_token->getOperator() == $op_sub) {
$is_substring = true;
} else {
$is_substring = false;
}
// If we're doing exact search, just test the raw corpus.
$is_exact = ($raw_token->getOperator() == $op_exact);
if ($is_exact) {
if ($is_not) {
$where[] = qsprintf(
$conn,
'(%T.rawCorpus != %s)',
$table_alias,
$value);
} else {
$where[] = qsprintf(
$conn,
'(%T.rawCorpus = %s)',
$table_alias,
$value);
}
continue;
}
// If we're doing substring search, we just match against the raw corpus
// and we're done.
if ($is_substring) {
if ($is_not) {
$where[] = qsprintf(
$conn,
'(%T.rawCorpus NOT LIKE %~)',
$table_alias,
$value);
} else {
$where[] = qsprintf(
$conn,
'(%T.rawCorpus LIKE %~)',
$table_alias,
$value);
}
continue;
}
// Otherwise, we need to match against the term corpus and the normal
// corpus, so that searching for "raw" does not find "strawberry".
if ($raw_token->isQuoted()) {
$is_quoted = true;
$is_stemmed = false;
} else {
$is_quoted = false;
$is_stemmed = true;
}
// Never stem negated queries, since this can exclude results users
// did not mean to exclude and generally confuse things.
if ($is_not) {
$is_stemmed = false;
}
$term_constraints = array();
$term_value = $engine->newTermsCorpus($value);
if ($is_not) {
$term_constraints[] = qsprintf(
$conn,
'(%T.termCorpus NOT LIKE %~)',
$table_alias,
$term_value);
} else {
$term_constraints[] = qsprintf(
$conn,
'(%T.termCorpus LIKE %~)',
$table_alias,
$term_value);
}
if ($is_stemmed) {
$stem_value = $stemmer->stemToken($value);
$stem_value = $engine->newTermsCorpus($stem_value);
$term_constraints[] = qsprintf(
$conn,
'(%T.normalCorpus LIKE %~)',
$table_alias,
$stem_value);
}
if ($is_not) {
$where[] = qsprintf(
$conn,
'%LA',
$term_constraints);
} else if ($is_quoted) {
$where[] = qsprintf(
$conn,
'(%T.rawCorpus LIKE %~ AND %LO)',
$table_alias,
$value,
$term_constraints);
} else {
$where[] = qsprintf(
$conn,
'%LO',
$term_constraints);
}
}
if ($this->ferretQuery) {
$query = $this->ferretQuery;
$author_phids = $query->getParameter('authorPHIDs');
if ($author_phids) {
$where[] = qsprintf(
$conn,
'ft_doc.authorPHID IN (%Ls)',
$author_phids);
}
$with_unowned = $query->getParameter('withUnowned');
$with_any = $query->getParameter('withAnyOwner');
if ($with_any && $with_unowned) {
throw new PhabricatorEmptyQueryException(
pht(
'This query matches only unowned documents owned by anyone, '.
'which is impossible.'));
}
$owner_phids = $query->getParameter('ownerPHIDs');
if ($owner_phids && !$with_any) {
if ($with_unowned) {
$where[] = qsprintf(
$conn,
'ft_doc.ownerPHID IN (%Ls) OR ft_doc.ownerPHID IS NULL',
$owner_phids);
} else {
$where[] = qsprintf(
$conn,
'ft_doc.ownerPHID IN (%Ls)',
$owner_phids);
}
} else if ($with_unowned) {
$where[] = qsprintf(
$conn,
'ft_doc.ownerPHID IS NULL');
}
if ($with_any) {
$where[] = qsprintf(
$conn,
'ft_doc.ownerPHID IS NOT NULL');
}
$rel_open = PhabricatorSearchRelationship::RELATIONSHIP_OPEN;
$statuses = $query->getParameter('statuses');
$is_closed = null;
if ($statuses) {
$statuses = array_fuse($statuses);
if (count($statuses) == 1) {
if (isset($statuses[$rel_open])) {
$is_closed = 0;
} else {
$is_closed = 1;
}
}
}
if ($is_closed !== null) {
$where[] = qsprintf(
$conn,
'ft_doc.isClosed = %d',
$is_closed);
}
}
return $where;
}
protected function shouldGroupFerretResultRows() {
return (bool)$this->ferretTokens;
}
/* -( Ngrams )------------------------------------------------------------- */
protected function withNgramsConstraint(
PhabricatorSearchNgrams $index,
$value) {
if (strlen($value)) {
$this->ngrams[] = array(
'index' => $index,
'value' => $value,
'length' => count(phutil_utf8v($value)),
);
}
return $this;
}
protected function buildNgramsJoinClause(AphrontDatabaseConnection $conn) {
$flat = array();
foreach ($this->ngrams as $spec) {
$index = $spec['index'];
$value = $spec['value'];
$length = $spec['length'];
if ($length >= 3) {
$ngrams = $index->getNgramsFromString($value, 'query');
$prefix = false;
} else if ($length == 2) {
$ngrams = $index->getNgramsFromString($value, 'prefix');
$prefix = false;
} else {
$ngrams = array(' '.$value);
$prefix = true;
}
foreach ($ngrams as $ngram) {
$flat[] = array(
'table' => $index->getTableName(),
'ngram' => $ngram,
'prefix' => $prefix,
);
}
}
// MySQL only allows us to join a maximum of 61 tables per query. Each
// ngram is going to cost us a join toward that limit, so if the user
// specified a very long query string, just pick 16 of the ngrams
// at random.
if (count($flat) > 16) {
shuffle($flat);
$flat = array_slice($flat, 0, 16);
}
$alias = $this->getPrimaryTableAlias();
if ($alias) {
$id_column = qsprintf($conn, '%T.%T', $alias, 'id');
} else {
$id_column = qsprintf($conn, '%T', 'id');
}
$idx = 1;
$joins = array();
foreach ($flat as $spec) {
$table = $spec['table'];
$ngram = $spec['ngram'];
$prefix = $spec['prefix'];
$alias = 'ngm'.$idx++;
if ($prefix) {
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON %T.objectID = %Q AND %T.ngram LIKE %>',
$table,
$alias,
$alias,
$id_column,
$alias,
$ngram);
} else {
$joins[] = qsprintf(
$conn,
'JOIN %T %T ON %T.objectID = %Q AND %T.ngram = %s',
$table,
$alias,
$alias,
$id_column,
$alias,
$ngram);
}
}
return $joins;
}
protected function buildNgramsWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
foreach ($this->ngrams as $ngram) {
$index = $ngram['index'];
$value = $ngram['value'];
$column = $index->getColumnName();
$alias = $this->getPrimaryTableAlias();
if ($alias) {
$column = qsprintf($conn, '%T.%T', $alias, $column);
} else {
$column = qsprintf($conn, '%T', $column);
}
$tokens = $index->tokenizeString($value);
foreach ($tokens as $token) {
$where[] = qsprintf(
$conn,
'%Q LIKE %~',
$column,
$token);
}
}
return $where;
}
protected function shouldGroupNgramResultRows() {
return (bool)$this->ngrams;
}
/* -( Edge Logic )--------------------------------------------------------- */
/**
* Convenience method for specifying edge logic constraints with a list of
* PHIDs.
*
* @param const Edge constant.
* @param const Constraint operator.
* @param list<phid> List of PHIDs.
* @return this
* @task edgelogic
*/
public function withEdgeLogicPHIDs($edge_type, $operator, array $phids) {
$constraints = array();
foreach ($phids as $phid) {
$constraints[] = new PhabricatorQueryConstraint($operator, $phid);
}
return $this->withEdgeLogicConstraints($edge_type, $constraints);
}
/**
* @return this
* @task edgelogic
*/
public function withEdgeLogicConstraints($edge_type, array $constraints) {
assert_instances_of($constraints, 'PhabricatorQueryConstraint');
$constraints = mgroup($constraints, 'getOperator');
foreach ($constraints as $operator => $list) {
foreach ($list as $item) {
$this->edgeLogicConstraints[$edge_type][$operator][] = $item;
}
}
$this->edgeLogicConstraintsAreValid = false;
return $this;
}
/**
* @task edgelogic
*/
public function buildEdgeLogicSelectClause(AphrontDatabaseConnection $conn) {
$select = array();
$this->validateEdgeLogicConstraints();
foreach ($this->edgeLogicConstraints as $type => $constraints) {
foreach ($constraints as $operator => $list) {
$alias = $this->getEdgeLogicTableAlias($operator, $type);
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_AND:
if (count($list) > 1) {
$select[] = qsprintf(
$conn,
'COUNT(DISTINCT(%T.dst)) %T',
$alias,
$this->buildEdgeLogicTableAliasCount($alias));
}
break;
case PhabricatorQueryConstraint::OPERATOR_ANCESTOR:
// This is tricky. We have a query which specifies multiple
// projects, each of which may have an arbitrarily large number
// of descendants.
// Suppose the projects are "Engineering" and "Operations", and
// "Engineering" has subprojects X, Y and Z.
// We first use `FIELD(dst, X, Y, Z)` to produce a 0 if a row
// is not part of Engineering at all, or some number other than
// 0 if it is.
// Then we use `IF(..., idx, NULL)` to convert the 0 to a NULL and
// any other value to an index (say, 1) for the ancestor.
// We build these up for every ancestor, then use `COALESCE(...)`
// to select the non-null one, giving us an ancestor which this
// row is a member of.
// From there, we use `COUNT(DISTINCT(...))` to make sure that
// each result row is a member of all ancestors.
if (count($list) > 1) {
$idx = 1;
$parts = array();
foreach ($list as $constraint) {
$parts[] = qsprintf(
$conn,
'IF(FIELD(%T.dst, %Ls) != 0, %d, NULL)',
$alias,
(array)$constraint->getValue(),
$idx++);
}
$parts = qsprintf($conn, '%LQ', $parts);
$select[] = qsprintf(
$conn,
'COUNT(DISTINCT(COALESCE(%Q))) %T',
$parts,
$this->buildEdgeLogicTableAliasAncestor($alias));
}
break;
default:
break;
}
}
}
return $select;
}
/**
* @task edgelogic
*/
public function buildEdgeLogicJoinClause(AphrontDatabaseConnection $conn) {
$edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE;
$phid_column = $this->getApplicationSearchObjectPHIDColumn($conn);
$joins = array();
foreach ($this->edgeLogicConstraints as $type => $constraints) {
$op_null = PhabricatorQueryConstraint::OPERATOR_NULL;
$has_null = isset($constraints[$op_null]);
// If we're going to process an only() operator, build a list of the
// acceptable set of PHIDs first. We'll only match results which have
// no edges to any other PHIDs.
$all_phids = array();
if (isset($constraints[PhabricatorQueryConstraint::OPERATOR_ONLY])) {
foreach ($constraints as $operator => $list) {
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_ANCESTOR:
case PhabricatorQueryConstraint::OPERATOR_AND:
case PhabricatorQueryConstraint::OPERATOR_OR:
foreach ($list as $constraint) {
$value = (array)$constraint->getValue();
foreach ($value as $v) {
$all_phids[$v] = $v;
}
}
break;
}
}
}
foreach ($constraints as $operator => $list) {
$alias = $this->getEdgeLogicTableAlias($operator, $type);
$phids = array();
foreach ($list as $constraint) {
$value = (array)$constraint->getValue();
foreach ($value as $v) {
$phids[$v] = $v;
}
}
$phids = array_keys($phids);
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_NOT:
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T %T ON %Q = %T.src AND %T.type = %d
AND %T.dst IN (%Ls)',
$edge_table,
$alias,
$phid_column,
$alias,
$alias,
$type,
$alias,
$phids);
break;
case PhabricatorQueryConstraint::OPERATOR_ANCESTOR:
case PhabricatorQueryConstraint::OPERATOR_AND:
case PhabricatorQueryConstraint::OPERATOR_OR:
// If we're including results with no matches, we have to degrade
// this to a LEFT join. We'll use WHERE to select matching rows
// later.
if ($has_null) {
$join_type = qsprintf($conn, 'LEFT');
} else {
$join_type = qsprintf($conn, '');
}
$joins[] = qsprintf(
$conn,
'%Q JOIN %T %T ON %Q = %T.src AND %T.type = %d
AND %T.dst IN (%Ls)',
$join_type,
$edge_table,
$alias,
$phid_column,
$alias,
$alias,
$type,
$alias,
$phids);
break;
case PhabricatorQueryConstraint::OPERATOR_NULL:
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T %T ON %Q = %T.src AND %T.type = %d',
$edge_table,
$alias,
$phid_column,
$alias,
$alias,
$type);
break;
case PhabricatorQueryConstraint::OPERATOR_ONLY:
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T %T ON %Q = %T.src AND %T.type = %d
AND %T.dst NOT IN (%Ls)',
$edge_table,
$alias,
$phid_column,
$alias,
$alias,
$type,
$alias,
$all_phids);
break;
}
}
}
return $joins;
}
/**
* @task edgelogic
*/
public function buildEdgeLogicWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
foreach ($this->edgeLogicConstraints as $type => $constraints) {
$full = array();
$null = array();
$op_null = PhabricatorQueryConstraint::OPERATOR_NULL;
$has_null = isset($constraints[$op_null]);
foreach ($constraints as $operator => $list) {
$alias = $this->getEdgeLogicTableAlias($operator, $type);
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_NOT:
case PhabricatorQueryConstraint::OPERATOR_ONLY:
$full[] = qsprintf(
$conn,
'%T.dst IS NULL',
$alias);
break;
case PhabricatorQueryConstraint::OPERATOR_AND:
case PhabricatorQueryConstraint::OPERATOR_OR:
if ($has_null) {
$full[] = qsprintf(
$conn,
'%T.dst IS NOT NULL',
$alias);
}
break;
case PhabricatorQueryConstraint::OPERATOR_NULL:
$null[] = qsprintf(
$conn,
'%T.dst IS NULL',
$alias);
break;
}
}
if ($full && $null) {
$where[] = qsprintf($conn, '(%LA OR %LA)', $full, $null);
} else if ($full) {
foreach ($full as $condition) {
$where[] = $condition;
}
} else if ($null) {
foreach ($null as $condition) {
$where[] = $condition;
}
}
}
return $where;
}
/**
* @task edgelogic
*/
public function buildEdgeLogicHavingClause(AphrontDatabaseConnection $conn) {
$having = array();
foreach ($this->edgeLogicConstraints as $type => $constraints) {
foreach ($constraints as $operator => $list) {
$alias = $this->getEdgeLogicTableAlias($operator, $type);
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_AND:
if (count($list) > 1) {
$having[] = qsprintf(
$conn,
'%T = %d',
$this->buildEdgeLogicTableAliasCount($alias),
count($list));
}
break;
case PhabricatorQueryConstraint::OPERATOR_ANCESTOR:
if (count($list) > 1) {
$having[] = qsprintf(
$conn,
'%T = %d',
$this->buildEdgeLogicTableAliasAncestor($alias),
count($list));
}
break;
}
}
}
return $having;
}
/**
* @task edgelogic
*/
public function shouldGroupEdgeLogicResultRows() {
foreach ($this->edgeLogicConstraints as $type => $constraints) {
foreach ($constraints as $operator => $list) {
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_NOT:
case PhabricatorQueryConstraint::OPERATOR_AND:
case PhabricatorQueryConstraint::OPERATOR_OR:
if (count($list) > 1) {
return true;
}
break;
case PhabricatorQueryConstraint::OPERATOR_ANCESTOR:
// NOTE: We must always group query results rows when using an
// "ANCESTOR" operator because a single task may be related to
// two different descendants of a particular ancestor. For
// discussion, see T12753.
return true;
case PhabricatorQueryConstraint::OPERATOR_NULL:
case PhabricatorQueryConstraint::OPERATOR_ONLY:
return true;
}
}
}
return false;
}
/**
* @task edgelogic
*/
private function getEdgeLogicTableAlias($operator, $type) {
return 'edgelogic_'.$operator.'_'.$type;
}
/**
* @task edgelogic
*/
private function buildEdgeLogicTableAliasCount($alias) {
return $alias.'_count';
}
/**
* @task edgelogic
*/
private function buildEdgeLogicTableAliasAncestor($alias) {
return $alias.'_ancestor';
}
/**
* Select certain edge logic constraint values.
*
* @task edgelogic
*/
protected function getEdgeLogicValues(
array $edge_types,
array $operators) {
$values = array();
$constraint_lists = $this->edgeLogicConstraints;
if ($edge_types) {
$constraint_lists = array_select_keys($constraint_lists, $edge_types);
}
foreach ($constraint_lists as $type => $constraints) {
if ($operators) {
$constraints = array_select_keys($constraints, $operators);
}
foreach ($constraints as $operator => $list) {
foreach ($list as $constraint) {
$value = (array)$constraint->getValue();
foreach ($value as $v) {
$values[] = $v;
}
}
}
}
return $values;
}
/**
* Validate edge logic constraints for the query.
*
* @return this
* @task edgelogic
*/
private function validateEdgeLogicConstraints() {
if ($this->edgeLogicConstraintsAreValid) {
return $this;
}
foreach ($this->edgeLogicConstraints as $type => $constraints) {
foreach ($constraints as $operator => $list) {
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_EMPTY:
throw new PhabricatorEmptyQueryException(
pht('This query specifies an empty constraint.'));
}
}
}
// This should probably be more modular, eventually, but we only do
// project-based edge logic today.
$project_phids = $this->getEdgeLogicValues(
array(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
),
array(
PhabricatorQueryConstraint::OPERATOR_AND,
PhabricatorQueryConstraint::OPERATOR_OR,
PhabricatorQueryConstraint::OPERATOR_NOT,
PhabricatorQueryConstraint::OPERATOR_ANCESTOR,
));
if ($project_phids) {
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
->withPHIDs($project_phids)
->execute();
$projects = mpull($projects, null, 'getPHID');
foreach ($project_phids as $phid) {
if (empty($projects[$phid])) {
throw new PhabricatorEmptyQueryException(
pht(
'This query is constrained by a project you do not have '.
'permission to see.'));
}
}
}
$op_and = PhabricatorQueryConstraint::OPERATOR_AND;
$op_or = PhabricatorQueryConstraint::OPERATOR_OR;
$op_ancestor = PhabricatorQueryConstraint::OPERATOR_ANCESTOR;
foreach ($this->edgeLogicConstraints as $type => $constraints) {
foreach ($constraints as $operator => $list) {
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_ONLY:
if (count($list) > 1) {
throw new PhabricatorEmptyQueryException(
pht(
'This query specifies only() more than once.'));
}
$have_and = idx($constraints, $op_and);
$have_or = idx($constraints, $op_or);
$have_ancestor = idx($constraints, $op_ancestor);
if (!$have_and && !$have_or && !$have_ancestor) {
throw new PhabricatorEmptyQueryException(
pht(
'This query specifies only(), but no other constraints '.
'which it can apply to.'));
}
break;
}
}
}
$this->edgeLogicConstraintsAreValid = true;
return $this;
}
/* -( Spaces )------------------------------------------------------------- */
/**
* Constrain the query to return results from only specific Spaces.
*
* Pass a list of Space PHIDs, or `null` to represent the default space. Only
* results in those Spaces will be returned.
*
* Queries are always constrained to include only results from spaces the
* viewer has access to.
*
* @param list<phid|null>
* @task spaces
*/
public function withSpacePHIDs(array $space_phids) {
$object = $this->newResultObject();
if (!$object) {
throw new Exception(
pht(
'This query (of class "%s") does not implement newResultObject(), '.
'but must implement this method to enable support for Spaces.',
get_class($this)));
}
if (!($object instanceof PhabricatorSpacesInterface)) {
throw new Exception(
pht(
'This query (of class "%s") returned an object of class "%s" from '.
'getNewResultObject(), but it does not implement the required '.
'interface ("%s"). Objects must implement this interface to enable '.
'Spaces support.',
get_class($this),
get_class($object),
'PhabricatorSpacesInterface'));
}
$this->spacePHIDs = $space_phids;
return $this;
}
public function withSpaceIsArchived($archived) {
$this->spaceIsArchived = $archived;
return $this;
}
/**
* Constrain the query to include only results in valid Spaces.
*
* This method builds part of a WHERE clause which considers the spaces the
* viewer has access to see with any explicit constraint on spaces added by
* @{method:withSpacePHIDs}.
*
* @param AphrontDatabaseConnection Database connection.
* @return string Part of a WHERE clause.
* @task spaces
*/
private function buildSpacesWhereClause(AphrontDatabaseConnection $conn) {
$object = $this->newResultObject();
if (!$object) {
return null;
}
if (!($object instanceof PhabricatorSpacesInterface)) {
return null;
}
$viewer = $this->getViewer();
// If we have an omnipotent viewer and no formal space constraints, don't
// emit a clause. This primarily enables older migrations to run cleanly,
// without fataling because they try to match a `spacePHID` column which
// does not exist yet. See T8743, T8746.
if ($viewer->isOmnipotent()) {
if ($this->spaceIsArchived === null && $this->spacePHIDs === null) {
return null;
}
}
+ // See T13240. If this query raises policy exceptions, don't filter objects
+ // in the MySQL layer. We want them to reach the application layer so we
+ // can reject them and raise an exception.
+ if ($this->shouldRaisePolicyExceptions()) {
+ return null;
+ }
+
$space_phids = array();
$include_null = false;
$all = PhabricatorSpacesNamespaceQuery::getAllSpaces();
if (!$all) {
// If there are no spaces at all, implicitly give the viewer access to
// the default space.
$include_null = true;
} else {
// Otherwise, give them access to the spaces they have permission to
// see.
$viewer_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces(
$viewer);
foreach ($viewer_spaces as $viewer_space) {
if ($this->spaceIsArchived !== null) {
if ($viewer_space->getIsArchived() != $this->spaceIsArchived) {
continue;
}
}
$phid = $viewer_space->getPHID();
$space_phids[$phid] = $phid;
if ($viewer_space->getIsDefaultNamespace()) {
$include_null = true;
}
}
}
// If we have additional explicit constraints, evaluate them now.
if ($this->spacePHIDs !== null) {
$explicit = array();
$explicit_null = false;
foreach ($this->spacePHIDs as $phid) {
if ($phid === null) {
$space = PhabricatorSpacesNamespaceQuery::getDefaultSpace();
} else {
$space = idx($all, $phid);
}
if ($space) {
$phid = $space->getPHID();
$explicit[$phid] = $phid;
if ($space->getIsDefaultNamespace()) {
$explicit_null = true;
}
}
}
// If the viewer can see the default space but it isn't on the explicit
// list of spaces to query, don't match it.
if ($include_null && !$explicit_null) {
$include_null = false;
}
// Include only the spaces common to the viewer and the constraints.
$space_phids = array_intersect_key($space_phids, $explicit);
}
if (!$space_phids && !$include_null) {
if ($this->spacePHIDs === null) {
throw new PhabricatorEmptyQueryException(
pht('You do not have access to any spaces.'));
} else {
throw new PhabricatorEmptyQueryException(
pht(
'You do not have access to any of the spaces this query '.
'is constrained to.'));
}
}
$alias = $this->getPrimaryTableAlias();
if ($alias) {
$col = qsprintf($conn, '%T.spacePHID', $alias);
} else {
$col = qsprintf($conn, 'spacePHID');
}
if ($space_phids && $include_null) {
return qsprintf(
$conn,
'(%Q IN (%Ls) OR %Q IS NULL)',
$col,
$space_phids,
$col);
} else if ($space_phids) {
return qsprintf(
$conn,
'%Q IN (%Ls)',
$col,
$space_phids);
} else {
return qsprintf(
$conn,
'%Q IS NULL',
$col);
}
}
}
diff --git a/src/infrastructure/sms/adapter/PhabricatorSMSImplementationAdapter.php b/src/infrastructure/sms/adapter/PhabricatorSMSImplementationAdapter.php
deleted file mode 100644
index 84325774f..000000000
--- a/src/infrastructure/sms/adapter/PhabricatorSMSImplementationAdapter.php
+++ /dev/null
@@ -1,88 +0,0 @@
-<?php
-
-abstract class PhabricatorSMSImplementationAdapter extends Phobject {
-
- private $fromNumber;
- private $toNumber;
- private $body;
-
- public function setFrom($number) {
- $this->fromNumber = $number;
- return $this;
- }
-
- public function getFrom() {
- return $this->fromNumber;
- }
-
- public function setTo($number) {
- $this->toNumber = $number;
- return $this;
- }
-
- public function getTo() {
- return $this->toNumber;
- }
-
- public function setBody($body) {
- $this->body = $body;
- return $this;
- }
-
- public function getBody() {
- return $this->body;
- }
-
- /**
- * 16 characters or less, to be used in database columns and exposed
- * to administrators during configuration directly.
- */
- abstract public function getProviderShortName();
-
- /**
- * Send the message. Generally, this means connecting to some service and
- * handing data to it. SMS APIs are generally asynchronous, so truly
- * determining success or failure is probably impossible synchronously.
- *
- * That said, if the adapter determines that the SMS will never be
- * deliverable, or there is some other known failure, it should throw
- * an exception.
- *
- * @return null
- */
- abstract public function send();
-
- /**
- * Most (all?) SMS APIs are asynchronous, but some do send back some
- * initial information. Use this hook to determine what the updated
- * sentStatus should be and what the provider is using for an SMS ID,
- * as well as throw exceptions if there are any failures.
- *
- * @return array Tuple of ($sms_id and $sent_status)
- */
- abstract public function getSMSDataFromResult($result);
-
- /**
- * Due to the asynchronous nature of sending SMS messages, it can be
- * necessary to poll the provider regarding the sent status of a given
- * sms.
- *
- * For now, this *MUST* be implemented and *MUST* work.
- */
- abstract public function pollSMSSentStatus(PhabricatorSMS $sms);
-
- /**
- * Convenience function to handle sending an SMS.
- */
- public static function sendSMS(array $to_numbers, $body) {
- PhabricatorWorker::scheduleTask(
- 'PhabricatorSMSDemultiplexWorker',
- array(
- 'toNumbers' => $to_numbers,
- 'body' => $body,
- ),
- array(
- 'priority' => PhabricatorWorker::PRIORITY_ALERTS,
- ));
- }
-}
diff --git a/src/infrastructure/sms/adapter/PhabricatorSMSImplementationTestBlackholeAdapter.php b/src/infrastructure/sms/adapter/PhabricatorSMSImplementationTestBlackholeAdapter.php
deleted file mode 100644
index 317ea7146..000000000
--- a/src/infrastructure/sms/adapter/PhabricatorSMSImplementationTestBlackholeAdapter.php
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-/**
- * This is useful for testing, but otherwise your SMS ends up in a blackhole.
- */
-final class PhabricatorSMSImplementationTestBlackholeAdapter
- extends PhabricatorSMSImplementationAdapter {
-
- public function getProviderShortName() {
- return 'testtesttest';
- }
-
- public function send() {
- // I guess this is what a blackhole looks like
- }
-
- public function getSMSDataFromResult($result) {
- return array(
- Filesystem::readRandomCharacters(40),
- PhabricatorSMS::STATUS_SENT,
- );
- }
-
- public function pollSMSSentStatus(PhabricatorSMS $sms) {
- if ($sms->getID()) {
- return PhabricatorSMS::STATUS_SENT;
- }
- return PhabricatorSMS::STATUS_SENT_UNCONFIRMED;
- }
-
-}
diff --git a/src/infrastructure/sms/adapter/PhabricatorSMSImplementationTwilioAdapter.php b/src/infrastructure/sms/adapter/PhabricatorSMSImplementationTwilioAdapter.php
deleted file mode 100644
index 56a91f7ca..000000000
--- a/src/infrastructure/sms/adapter/PhabricatorSMSImplementationTwilioAdapter.php
+++ /dev/null
@@ -1,99 +0,0 @@
-<?php
-
-final class PhabricatorSMSImplementationTwilioAdapter
- extends PhabricatorSMSImplementationAdapter {
-
- public function getProviderShortName() {
- return 'twilio';
- }
-
- /**
- * @phutil-external-symbol class Services_Twilio
- */
- private function buildClient() {
- $root = dirname(phutil_get_library_root('phabricator'));
- require_once $root.'/externals/twilio-php/Services/Twilio.php';
- $account_sid = PhabricatorEnv::getEnvConfig('twilio.account-sid');
- $auth_token = PhabricatorEnv::getEnvConfig('twilio.auth-token');
- return new Services_Twilio($account_sid, $auth_token);
- }
-
- /**
- * @phutil-external-symbol class Services_Twilio_RestException
- */
- public function send() {
- $client = $this->buildClient();
-
- try {
- $message = $client->account->sms_messages->create(
- $this->formatNumberForSMS($this->getFrom()),
- $this->formatNumberForSMS($this->getTo()),
- $this->getBody(),
- array());
- } catch (Services_Twilio_RestException $e) {
- $message = sprintf(
- 'HTTP Code %d: %s',
- $e->getStatus(),
- $e->getMessage());
-
- // Twilio tries to provide a link to more specific details if they can.
- if ($e->getInfo()) {
- $message .= sprintf(' For more information, see %s.', $e->getInfo());
- }
- throw new PhabricatorWorkerPermanentFailureException($message);
- }
- return $message;
- }
-
- public function getSMSDataFromResult($result) {
- return array($result->sid, $this->getSMSStatus($result->status));
- }
-
- public function pollSMSSentStatus(PhabricatorSMS $sms) {
- $client = $this->buildClient();
- $message = $client->account->messages->get($sms->getProviderSMSID());
-
- return $this->getSMSStatus($message->status);
- }
-
- /**
- * See https://www.twilio.com/docs/api/rest/sms#sms-status-values.
- */
- private function getSMSStatus($twilio_status) {
- switch ($twilio_status) {
- case 'failed':
- $status = PhabricatorSMS::STATUS_FAILED;
- break;
- case 'sent':
- $status = PhabricatorSMS::STATUS_SENT;
- break;
- case 'sending':
- case 'queued':
- default:
- $status = PhabricatorSMS::STATUS_SENT_UNCONFIRMED;
- break;
- }
- return $status;
- }
-
- /**
- * We expect numbers to be plainly entered - i.e. the preg_replace here
- * should do nothing - but try hard to format anyway.
- *
- * Twilio uses E164 format, e.g. +15551234567
- */
- private function formatNumberForSMS($number) {
- $number = preg_replace('/[^0-9]/', '', $number);
- $first_char = substr($number, 0, 1);
- switch ($first_char) {
- case '1':
- $prepend = '+';
- break;
- default:
- $prepend = '+1';
- break;
- }
- return $prepend.$number;
- }
-
-}
diff --git a/src/infrastructure/sms/management/PhabricatorSMSManagementListOutboundWorkflow.php b/src/infrastructure/sms/management/PhabricatorSMSManagementListOutboundWorkflow.php
deleted file mode 100644
index 576620166..000000000
--- a/src/infrastructure/sms/management/PhabricatorSMSManagementListOutboundWorkflow.php
+++ /dev/null
@@ -1,54 +0,0 @@
-<?php
-
-final class PhabricatorSMSManagementListOutboundWorkflow
- extends PhabricatorSMSManagementWorkflow {
-
- protected function didConstruct() {
- $this
- ->setName('list-outbound')
- ->setSynopsis(pht('List outbound SMS messages sent by Phabricator.'))
- ->setExamples('**list-outbound**')
- ->setArguments(
- array(
- array(
- 'name' => 'limit',
- 'param' => 'N',
- 'default' => 100,
- 'help' => pht(
- 'Show a specific number of SMS messages (default 100).'),
- ),
- ));
- }
-
- public function execute(PhutilArgumentParser $args) {
- $console = PhutilConsole::getConsole();
- $viewer = $this->getViewer();
-
- $sms_messages = id(new PhabricatorSMS())->loadAllWhere(
- '1 = 1 ORDER BY id DESC LIMIT %d',
- $args->getArg('limit'));
-
- if (!$sms_messages) {
- $console->writeErr("%s\n", pht('No sent SMS.'));
- return 0;
- }
-
- $table = id(new PhutilConsoleTable())
- ->setShowHeader(false)
- ->addColumn('id', array('title' => pht('ID')))
- ->addColumn('status', array('title' => pht('Status')))
- ->addColumn('recv', array('title' => pht('Recipient')));
-
- foreach (array_reverse($sms_messages) as $sms) {
- $table->addRow(array(
- 'id' => $sms->getID(),
- 'status' => $sms->getSendStatus(),
- 'recv' => $sms->getToNumber(),
- ));
- }
-
- $table->draw();
- return 0;
- }
-
-}
diff --git a/src/infrastructure/sms/management/PhabricatorSMSManagementSendTestWorkflow.php b/src/infrastructure/sms/management/PhabricatorSMSManagementSendTestWorkflow.php
deleted file mode 100644
index 7bb457309..000000000
--- a/src/infrastructure/sms/management/PhabricatorSMSManagementSendTestWorkflow.php
+++ /dev/null
@@ -1,47 +0,0 @@
-<?php
-
-final class PhabricatorSMSManagementSendTestWorkflow
- extends PhabricatorSMSManagementWorkflow {
-
- protected function didConstruct() {
- $this
- ->setName('send-test')
- ->setSynopsis(
- pht(
- 'Simulate sending an SMS. This may be useful to test your SMS '.
- 'configuration, or while developing new SMS adapters.'))
- ->setExamples("**send-test** --to 12345678 --body 'pizza time yet?'")
- ->setArguments(
- array(
- array(
- 'name' => 'to',
- 'param' => 'number',
- 'help' => pht('Send SMS "To:" the specified number.'),
- 'repeat' => true,
- ),
- array(
- 'name' => 'body',
- 'param' => 'text',
- 'help' => pht('Send SMS with the specified body.'),
- ),
- ));
- }
-
- public function execute(PhutilArgumentParser $args) {
- $console = PhutilConsole::getConsole();
- $viewer = $this->getViewer();
-
- $tos = $args->getArg('to');
- $body = $args->getArg('body');
-
- PhabricatorWorker::setRunAllTasksInProcess(true);
- PhabricatorSMSImplementationAdapter::sendSMS($tos, $body);
-
- $console->writeErr(
- "%s\n\n phabricator/ $ ./bin/sms list-outbound \n\n",
- pht(
- 'Send completed! You can view the list of SMS messages sent by '.
- 'running this command:'));
- }
-
-}
diff --git a/src/infrastructure/sms/management/PhabricatorSMSManagementShowOutboundWorkflow.php b/src/infrastructure/sms/management/PhabricatorSMSManagementShowOutboundWorkflow.php
deleted file mode 100644
index 19a306b6d..000000000
--- a/src/infrastructure/sms/management/PhabricatorSMSManagementShowOutboundWorkflow.php
+++ /dev/null
@@ -1,72 +0,0 @@
-<?php
-
-final class PhabricatorSMSManagementShowOutboundWorkflow
- extends PhabricatorSMSManagementWorkflow {
-
- protected function didConstruct() {
- $this
- ->setName('show-outbound')
- ->setSynopsis(pht('Show diagnostic details about outbound SMS.'))
- ->setExamples(
- '**show-outbound** --id 1 --id 2')
- ->setArguments(
- array(
- array(
- 'name' => 'id',
- 'param' => 'id',
- 'help' => pht('Show details about outbound SMS with given ID.'),
- 'repeat' => true,
- ),
- ));
- }
-
- public function execute(PhutilArgumentParser $args) {
- $console = PhutilConsole::getConsole();
-
- $ids = $args->getArg('id');
- if (!$ids) {
- throw new PhutilArgumentUsageException(
- pht(
- "Use the '%s' flag to specify one or more SMS messages to show.",
- '--id'));
- }
-
- $messages = id(new PhabricatorSMS())->loadAllWhere(
- 'id IN (%Ld)',
- $ids);
-
- if ($ids) {
- $ids = array_fuse($ids);
- $missing = array_diff_key($ids, $messages);
- if ($missing) {
- throw new PhutilArgumentUsageException(
- pht(
- 'Some specified SMS messages do not exist: %s',
- implode(', ', array_keys($missing))));
- }
- }
-
- $last_key = last_key($messages);
- foreach ($messages as $message_key => $message) {
- $info = array();
-
- $info[] = pht('PROPERTIES');
- $info[] = pht('ID: %d', $message->getID());
- $info[] = pht('Status: %s', $message->getSendStatus());
- $info[] = pht('To: %s', $message->getToNumber());
- $info[] = pht('From: %s', $message->getFromNumber());
-
- $info[] = null;
- $info[] = pht('BODY');
- $info[] = $message->getBody();
- $info[] = null;
-
- $console->writeOut('%s', implode("\n", $info));
-
- if ($message_key != $last_key) {
- $console->writeOut("\n%s\n\n", str_repeat('-', 80));
- }
- }
- }
-
-}
diff --git a/src/infrastructure/sms/management/PhabricatorSMSManagementWorkflow.php b/src/infrastructure/sms/management/PhabricatorSMSManagementWorkflow.php
deleted file mode 100644
index 64bc65e2e..000000000
--- a/src/infrastructure/sms/management/PhabricatorSMSManagementWorkflow.php
+++ /dev/null
@@ -1,4 +0,0 @@
-<?php
-
-abstract class PhabricatorSMSManagementWorkflow
- extends PhabricatorManagementWorkflow {}
diff --git a/src/infrastructure/sms/storage/PhabricatorSMS.php b/src/infrastructure/sms/storage/PhabricatorSMS.php
deleted file mode 100644
index 7b6c85f15..000000000
--- a/src/infrastructure/sms/storage/PhabricatorSMS.php
+++ /dev/null
@@ -1,75 +0,0 @@
-<?php
-
-final class PhabricatorSMS
- extends PhabricatorSMSDAO {
-
- const MAXIMUM_SEND_TRIES = 5;
-
- /**
- * Status constants should be 16 characters or less. See status entries
- * for details on what they indicate about the underlying SMS.
- */
-
- // in the beginning, all SMS are unsent
- const STATUS_UNSENT = 'unsent';
- // that nebulous time when we've sent it from Phabricator but haven't
- // heard anything from the external API
- const STATUS_SENT_UNCONFIRMED = 'sent-unconfirmed';
- // "success"
- const STATUS_SENT = 'sent';
- // "fail" but we'll try again
- const STATUS_FAILED = 'failed';
- // we're giving up on our external API partner
- const STATUS_FAILED_PERMANENTLY = 'permafailed';
-
- const SHORTNAME_PLACEHOLDER = 'phabricator';
-
- protected $providerShortName;
- protected $providerSMSID;
- // numbers can be up to 20 digits long
- protected $toNumber;
- protected $fromNumber;
- protected $body;
- protected $sendStatus;
-
- public static function initializeNewSMS($body) {
- // NOTE: these values will be updated to correct values when the
- // SMS is sent for the first time. In particular, the ProviderShortName
- // and ProviderSMSID are totally garbage data before a send it attempted.
- return id(new PhabricatorSMS())
- ->setBody($body)
- ->setSendStatus(self::STATUS_UNSENT)
- ->setProviderShortName(self::SHORTNAME_PLACEHOLDER)
- ->setProviderSMSID(Filesystem::readRandomCharacters(40));
- }
-
- protected function getConfiguration() {
- return array(
- self::CONFIG_COLUMN_SCHEMA => array(
- 'providerShortName' => 'text16',
- 'providerSMSID' => 'text40',
- 'toNumber' => 'text20',
- 'fromNumber' => 'text20?',
- 'body' => 'text',
- 'sendStatus' => 'text16?',
- ),
- self::CONFIG_KEY_SCHEMA => array(
- 'key_provider' => array(
- 'columns' => array('providerSMSID', 'providerShortName'),
- 'unique' => true,
- ),
- ),
- ) + parent::getConfiguration();
- }
-
- public function getTableName() {
- // Slightly non-standard, but otherwise this class needs "MetaMTA" in its
- // name. :/
- return 'sms';
- }
-
- public function hasBeenSentAtLeastOnce() {
- return ($this->getProviderShortName() !=
- self::SHORTNAME_PLACEHOLDER);
- }
-}
diff --git a/src/infrastructure/sms/storage/PhabricatorSMSDAO.php b/src/infrastructure/sms/storage/PhabricatorSMSDAO.php
deleted file mode 100644
index 90ed3dee1..000000000
--- a/src/infrastructure/sms/storage/PhabricatorSMSDAO.php
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php
-
-abstract class PhabricatorSMSDAO
- extends PhabricatorLiskDAO {
-
-
- public function getApplicationName() {
- return 'metamta';
- }
-
-}
diff --git a/src/infrastructure/sms/worker/PhabricatorSMSDemultiplexWorker.php b/src/infrastructure/sms/worker/PhabricatorSMSDemultiplexWorker.php
deleted file mode 100644
index 60baa0dbd..000000000
--- a/src/infrastructure/sms/worker/PhabricatorSMSDemultiplexWorker.php
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-
-final class PhabricatorSMSDemultiplexWorker extends PhabricatorSMSWorker {
-
- protected function doWork() {
- $viewer = PhabricatorUser::getOmnipotentUser();
-
- $task_data = $this->getTaskData();
-
- $to_numbers = idx($task_data, 'toNumbers');
- if (!$to_numbers) {
- // If we don't have any to numbers, don't send any sms.
- return;
- }
-
- foreach ($to_numbers as $number) {
- // NOTE: we will set the fromNumber and the proper provider data
- // in the `PhabricatorSMSSendWorker`.
- $sms = PhabricatorSMS::initializeNewSMS($task_data['body']);
- $sms->setToNumber($number);
- $sms->save();
- $this->queueTask(
- 'PhabricatorSMSSendWorker',
- array(
- 'smsID' => $sms->getID(),
- ));
- }
- }
-
-}
diff --git a/src/infrastructure/sms/worker/PhabricatorSMSSendWorker.php b/src/infrastructure/sms/worker/PhabricatorSMSSendWorker.php
deleted file mode 100644
index d7fefe25d..000000000
--- a/src/infrastructure/sms/worker/PhabricatorSMSSendWorker.php
+++ /dev/null
@@ -1,85 +0,0 @@
-<?php
-
-final class PhabricatorSMSSendWorker extends PhabricatorSMSWorker {
-
- public function getMaximumRetryCount() {
- return PhabricatorSMS::MAXIMUM_SEND_TRIES;
- }
-
- public function getWaitBeforeRetry(PhabricatorWorkerTask $task) {
- return phutil_units('1 minute in seconds');
- }
-
- protected function doWork() {
- $viewer = PhabricatorUser::getOmnipotentUser();
-
- $task_data = $this->getTaskData();
-
- $sms = id(new PhabricatorSMS())
- ->loadOneWhere('id = %d', $task_data['smsID']);
-
- if (!$sms) {
- throw new PhabricatorWorkerPermanentFailureException(
- pht('SMS object was not found.'));
- }
-
- // this has the potential to be updated asynchronously
- if ($sms->getSendStatus() == PhabricatorSMS::STATUS_SENT) {
- return;
- }
-
- $adapter = PhabricatorEnv::getEnvConfig('sms.default-adapter');
- $adapter = newv($adapter, array());
- if ($sms->hasBeenSentAtLeastOnce()) {
- $up_to_date_status = $adapter->pollSMSSentStatus($sms);
- if ($up_to_date_status) {
- $sms->setSendStatus($up_to_date_status);
- if ($up_to_date_status == PhabricatorSMS::STATUS_SENT) {
- $sms->save();
- return;
- }
- }
- // TODO - re-jigger this so we can try if appropos (e.g. rate limiting)
- return;
- }
-
- $from_number = PhabricatorEnv::getEnvConfig('sms.default-sender');
- // always set the from number if we get this far in case of configuration
- // changes.
- $sms->setFromNumber($from_number);
-
- $adapter->setTo($sms->getToNumber());
- $adapter->setFrom($sms->getFromNumber());
- $adapter->setBody($sms->getBody());
- // give the provider name the same treatment as phone number
- $sms->setProviderShortName($adapter->getProviderShortName());
-
- if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
- $sms->setSendStatus(PhabricatorSMS::STATUS_FAILED_PERMANENTLY);
- $sms->save();
- throw new PhabricatorWorkerPermanentFailureException(
- pht(
- 'Phabricator is running in silent mode. See `%s` '.
- 'in the configuration to change this setting.',
- 'phabricator.silent'));
- }
-
- try {
- $result = $adapter->send();
- list($sms_id, $sent_status) = $adapter->getSMSDataFromResult($result);
- } catch (PhabricatorWorkerPermanentFailureException $e) {
- $sms->setSendStatus(PhabricatorSMS::STATUS_FAILED_PERMANENTLY);
- $sms->save();
- throw $e;
- } catch (Exception $e) {
- $sms->setSendStatus(PhabricatorSMS::STATUS_FAILED_PERMANENTLY);
- $sms->save();
- throw new PhabricatorWorkerPermanentFailureException(
- $e->getMessage());
- }
- $sms->setProviderSMSID($sms_id);
- $sms->setSendStatus($sent_status);
- $sms->save();
- }
-
-}
diff --git a/src/infrastructure/sms/worker/PhabricatorSMSWorker.php b/src/infrastructure/sms/worker/PhabricatorSMSWorker.php
deleted file mode 100644
index 7b8f851ea..000000000
--- a/src/infrastructure/sms/worker/PhabricatorSMSWorker.php
+++ /dev/null
@@ -1,4 +0,0 @@
-<?php
-
-abstract class PhabricatorSMSWorker
- extends PhabricatorWorker {}
diff --git a/src/view/form/control/PHUIFormTimerControl.php b/src/view/form/control/PHUIFormTimerControl.php
new file mode 100644
index 000000000..7229d649e
--- /dev/null
+++ b/src/view/form/control/PHUIFormTimerControl.php
@@ -0,0 +1,40 @@
+<?php
+
+final class PHUIFormTimerControl extends AphrontFormControl {
+
+ private $icon;
+
+ public function setIcon(PHUIIconView $icon) {
+ $this->icon = $icon;
+ return $this;
+ }
+
+ public function getIcon() {
+ return $this->icon;
+ }
+
+ protected function getCustomControlClass() {
+ return 'phui-form-timer';
+ }
+
+ protected function renderInput() {
+ $icon_cell = phutil_tag(
+ 'td',
+ array(
+ 'class' => 'phui-form-timer-icon',
+ ),
+ $this->getIcon());
+
+ $content_cell = phutil_tag(
+ 'td',
+ array(
+ 'class' => 'phui-form-timer-content',
+ ),
+ $this->renderChildren());
+
+ $row = phutil_tag('tr', array(), array($icon_cell, $content_cell));
+
+ return phutil_tag('table', array(), $row);
+ }
+
+}
diff --git a/src/view/phui/PHUIInfoView.php b/src/view/phui/PHUIInfoView.php
index 69d054929..af984f583 100644
--- a/src/view/phui/PHUIInfoView.php
+++ b/src/view/phui/PHUIInfoView.php
@@ -1,200 +1,203 @@
<?php
final class PHUIInfoView extends AphrontTagView {
const SEVERITY_ERROR = 'error';
const SEVERITY_WARNING = 'warning';
const SEVERITY_NOTICE = 'notice';
const SEVERITY_NODATA = 'nodata';
const SEVERITY_SUCCESS = 'success';
const SEVERITY_PLAIN = 'plain';
+ const SEVERITY_MFA = 'mfa';
private $title;
private $errors = array();
private $severity = null;
private $id;
private $buttons = array();
private $isHidden;
private $flush;
private $icon;
public function setTitle($title) {
$this->title = $title;
return $this;
}
public function setSeverity($severity) {
$this->severity = $severity;
return $this;
}
private function getSeverity() {
$severity = $this->severity ? $this->severity : self::SEVERITY_ERROR;
return $severity;
}
public function setErrors(array $errors) {
$this->errors = $errors;
return $this;
}
public function setID($id) {
$this->id = $id;
return $this;
}
public function setIsHidden($bool) {
$this->isHidden = $bool;
return $this;
}
public function setFlush($flush) {
$this->flush = $flush;
return $this;
}
public function setIcon($icon) {
if ($icon instanceof PHUIIconView) {
$this->icon = $icon;
} else {
$icon = id(new PHUIIconView())
->setIcon($icon);
$this->icon = $icon;
}
return $this;
}
private function getIcon() {
if ($this->icon) {
return $this->icon;
}
switch ($this->getSeverity()) {
case self::SEVERITY_ERROR:
$icon = 'fa-exclamation-circle';
- break;
+ break;
case self::SEVERITY_WARNING:
$icon = 'fa-exclamation-triangle';
- break;
+ break;
case self::SEVERITY_NOTICE:
$icon = 'fa-info-circle';
- break;
+ break;
case self::SEVERITY_PLAIN:
case self::SEVERITY_NODATA:
return null;
- break;
case self::SEVERITY_SUCCESS:
$icon = 'fa-check-circle';
- break;
+ break;
+ case self::SEVERITY_MFA:
+ $icon = 'fa-lock';
+ break;
}
$icon = id(new PHUIIconView())
->setIcon($icon)
->addClass('phui-info-icon');
return $icon;
}
public function addButton(PHUIButtonView $button) {
$this->buttons[] = $button;
return $this;
}
protected function getTagAttributes() {
$classes = array();
$classes[] = 'phui-info-view';
$classes[] = 'phui-info-severity-'.$this->getSeverity();
$classes[] = 'grouped';
if ($this->flush) {
$classes[] = 'phui-info-view-flush';
}
if ($this->getIcon()) {
$classes[] = 'phui-info-has-icon';
}
return array(
'id' => $this->id,
'class' => implode(' ', $classes),
'style' => $this->isHidden ? 'display: none;' : null,
);
}
protected function getTagContent() {
require_celerity_resource('phui-info-view-css');
$errors = $this->errors;
if (count($errors) > 1) {
$list = array();
foreach ($errors as $error) {
$list[] = phutil_tag(
'li',
array(),
$error);
}
$list = phutil_tag(
'ul',
array(
'class' => 'phui-info-view-list',
),
$list);
} else if (count($errors) == 1) {
$list = head($this->errors);
} else {
$list = null;
}
$title = $this->title;
if (strlen($title)) {
$title = phutil_tag(
'h1',
array(
'class' => 'phui-info-view-head',
),
$title);
} else {
$title = null;
}
$children = $this->renderChildren();
if ($list) {
$children[] = $list;
}
$body = null;
if (!empty($children)) {
$body = phutil_tag(
'div',
array(
'class' => 'phui-info-view-body',
),
$children);
}
$buttons = null;
if (!empty($this->buttons)) {
$buttons = phutil_tag(
'div',
array(
'class' => 'phui-info-view-actions',
),
$this->buttons);
}
$icon = null;
if ($this->getIcon()) {
$icon = phutil_tag(
'div',
array(
'class' => 'phui-info-view-icon',
),
$this->getIcon());
}
return array(
$icon,
$buttons,
$title,
$body,
);
}
}
diff --git a/src/view/phui/PHUITimelineEventView.php b/src/view/phui/PHUITimelineEventView.php
index 1cf6400bb..86628058f 100644
--- a/src/view/phui/PHUITimelineEventView.php
+++ b/src/view/phui/PHUITimelineEventView.php
@@ -1,726 +1,726 @@
<?php
final class PHUITimelineEventView extends AphrontView {
const DELIMITER = " \xC2\xB7 ";
private $userHandle;
private $title;
private $icon;
private $color;
private $classes = array();
private $contentSource;
private $dateCreated;
private $anchor;
private $isEditable;
private $isEdited;
private $isRemovable;
private $transactionPHID;
private $isPreview;
private $eventGroup = array();
private $hideByDefault;
private $token;
private $tokenRemoved;
private $quoteTargetID;
private $isNormalComment;
private $quoteRef;
private $reallyMajorEvent;
private $hideCommentOptions = false;
private $authorPHID;
private $badges = array();
private $pinboardItems = array();
private $isSilent;
private $isMFA;
public function setAuthorPHID($author_phid) {
$this->authorPHID = $author_phid;
return $this;
}
public function getAuthorPHID() {
return $this->authorPHID;
}
public function setQuoteRef($quote_ref) {
$this->quoteRef = $quote_ref;
return $this;
}
public function getQuoteRef() {
return $this->quoteRef;
}
public function setQuoteTargetID($quote_target_id) {
$this->quoteTargetID = $quote_target_id;
return $this;
}
public function getQuoteTargetID() {
return $this->quoteTargetID;
}
public function setIsNormalComment($is_normal_comment) {
$this->isNormalComment = $is_normal_comment;
return $this;
}
public function getIsNormalComment() {
return $this->isNormalComment;
}
public function setHideByDefault($hide_by_default) {
$this->hideByDefault = $hide_by_default;
return $this;
}
public function getHideByDefault() {
return $this->hideByDefault;
}
public function setTransactionPHID($transaction_phid) {
$this->transactionPHID = $transaction_phid;
return $this;
}
public function getTransactionPHID() {
return $this->transactionPHID;
}
public function setIsEdited($is_edited) {
$this->isEdited = $is_edited;
return $this;
}
public function getIsEdited() {
return $this->isEdited;
}
public function setIsPreview($is_preview) {
$this->isPreview = $is_preview;
return $this;
}
public function getIsPreview() {
return $this->isPreview;
}
public function setIsEditable($is_editable) {
$this->isEditable = $is_editable;
return $this;
}
public function getIsEditable() {
return $this->isEditable;
}
public function setIsRemovable($is_removable) {
$this->isRemovable = $is_removable;
return $this;
}
public function getIsRemovable() {
return $this->isRemovable;
}
public function setDateCreated($date_created) {
$this->dateCreated = $date_created;
return $this;
}
public function getDateCreated() {
return $this->dateCreated;
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source;
return $this;
}
public function getContentSource() {
return $this->contentSource;
}
public function setUserHandle(PhabricatorObjectHandle $handle) {
$this->userHandle = $handle;
return $this;
}
public function setAnchor($anchor) {
$this->anchor = $anchor;
return $this;
}
public function getAnchor() {
return $this->anchor;
}
public function setTitle($title) {
$this->title = $title;
return $this;
}
public function addClass($class) {
$this->classes[] = $class;
return $this;
}
public function addBadge(PHUIBadgeMiniView $badge) {
$this->badges[] = $badge;
return $this;
}
public function setIcon($icon) {
$this->icon = $icon;
return $this;
}
public function setColor($color) {
$this->color = $color;
return $this;
}
public function setIsSilent($is_silent) {
$this->isSilent = $is_silent;
return $this;
}
public function getIsSilent() {
return $this->isSilent;
}
public function setIsMFA($is_mfa) {
$this->isMFA = $is_mfa;
return $this;
}
public function getIsMFA() {
return $this->isMFA;
}
public function setReallyMajorEvent($me) {
$this->reallyMajorEvent = $me;
return $this;
}
public function setHideCommentOptions($hide_comment_options) {
$this->hideCommentOptions = $hide_comment_options;
return $this;
}
public function getHideCommentOptions() {
return $this->hideCommentOptions;
}
public function addPinboardItem(PHUIPinboardItemView $item) {
$this->pinboardItems[] = $item;
return $this;
}
public function setToken($token, $removed = false) {
$this->token = $token;
$this->tokenRemoved = $removed;
return $this;
}
public function getEventGroup() {
return array_merge(array($this), $this->eventGroup);
}
public function addEventToGroup(PHUITimelineEventView $event) {
$this->eventGroup[] = $event;
return $this;
}
protected function shouldRenderEventTitle() {
if ($this->title === null) {
return false;
}
return true;
}
protected function renderEventTitle($force_icon, $has_menu, $extra) {
$title = $this->title;
$title_classes = array();
$title_classes[] = 'phui-timeline-title';
$icon = null;
if ($this->icon || $force_icon) {
$title_classes[] = 'phui-timeline-title-with-icon';
}
if ($has_menu) {
$title_classes[] = 'phui-timeline-title-with-menu';
}
if ($this->icon) {
$fill_classes = array();
$fill_classes[] = 'phui-timeline-icon-fill';
if ($this->color) {
$fill_classes[] = 'fill-has-color';
$fill_classes[] = 'phui-timeline-icon-fill-'.$this->color;
}
$icon = id(new PHUIIconView())
->setIcon($this->icon)
->addClass('phui-timeline-icon');
$icon = phutil_tag(
'span',
array(
'class' => implode(' ', $fill_classes),
),
$icon);
}
$token = null;
if ($this->token) {
$token = id(new PHUIIconView())
->addClass('phui-timeline-token')
->setSpriteSheet(PHUIIconView::SPRITE_TOKENS)
->setSpriteIcon($this->token);
if ($this->tokenRemoved) {
$token->addClass('strikethrough');
}
}
$title = phutil_tag(
'div',
array(
'class' => implode(' ', $title_classes),
),
array($icon, $token, $title, $extra));
return $title;
}
public function render() {
$events = $this->getEventGroup();
// Move events with icons first.
$icon_keys = array();
foreach ($this->getEventGroup() as $key => $event) {
if ($event->icon) {
$icon_keys[] = $key;
}
}
$events = array_select_keys($events, $icon_keys) + $events;
$force_icon = (bool)$icon_keys;
$menu = null;
$items = array();
if (!$this->getIsPreview() && !$this->getHideCommentOptions()) {
foreach ($this->getEventGroup() as $event) {
$items[] = $event->getMenuItems($this->anchor);
}
$items = array_mergev($items);
}
if ($items) {
$icon = id(new PHUIIconView())
->setIcon('fa-caret-down');
$aural = javelin_tag(
'span',
array(
'aural' => true,
),
pht('Comment Actions'));
if ($items) {
$sigil = 'phui-dropdown-menu';
Javelin::initBehavior('phui-dropdown-menu');
} else {
$sigil = null;
}
$action_list = id(new PhabricatorActionListView())
->setUser($this->getUser());
foreach ($items as $item) {
$action_list->addAction($item);
}
$menu = javelin_tag(
$items ? 'a' : 'span',
array(
'href' => '#',
'class' => 'phui-timeline-menu',
'sigil' => $sigil,
'aria-haspopup' => 'true',
'aria-expanded' => 'false',
'meta' => $action_list->getDropdownMenuMetadata(),
),
array(
$aural,
$icon,
));
$has_menu = true;
} else {
$has_menu = false;
}
// Render "extra" information (timestamp, etc).
$extra = $this->renderExtra($events);
$show_badges = false;
$group_titles = array();
$group_items = array();
$group_children = array();
foreach ($events as $event) {
if ($event->shouldRenderEventTitle()) {
// Render the group anchor here, outside the title box. If we render
// it inside the title box it ends up completely hidden and Chrome 55
// refuses to jump to it. See T11997 for discussion.
if ($extra && $this->anchor) {
$group_titles[] = id(new PhabricatorAnchorView())
->setAnchorName($this->anchor)
->render();
}
$group_titles[] = $event->renderEventTitle(
$force_icon,
$has_menu,
$extra);
// Don't render this information more than once.
$extra = null;
}
if ($event->hasChildren()) {
$group_children[] = $event->renderChildren();
$show_badges = true;
}
}
$image_uri = $this->userHandle->getImageURI();
$wedge = phutil_tag(
'div',
array(
'class' => 'phui-timeline-wedge',
'style' => (nonempty($image_uri)) ? '' : 'display: none;',
),
'');
$image = null;
$badges = null;
if ($image_uri) {
$image = phutil_tag(
($this->userHandle->getURI()) ? 'a' : 'div',
array(
'style' => 'background-image: url('.$image_uri.')',
'class' => 'phui-timeline-image visual-only',
'href' => $this->userHandle->getURI(),
),
'');
if ($this->badges && $show_badges) {
$flex = new PHUIBadgeBoxView();
$flex->addItems($this->badges);
$flex->setCollapsed(true);
$badges = phutil_tag(
'div',
array(
'class' => 'phui-timeline-badges',
),
$flex);
}
}
$content_classes = array();
$content_classes[] = 'phui-timeline-content';
$classes = array();
$classes[] = 'phui-timeline-event-view';
if ($group_children) {
$classes[] = 'phui-timeline-major-event';
$content = phutil_tag(
'div',
array(
'class' => 'phui-timeline-inner-content',
),
array(
$group_titles,
$menu,
phutil_tag(
'div',
array(
'class' => 'phui-timeline-core-content',
),
$group_children),
));
} else {
$classes[] = 'phui-timeline-minor-event';
$content = $group_titles;
}
$content = phutil_tag(
'div',
array(
'class' => 'phui-timeline-group',
),
$content);
// Image Events
$pinboard = null;
if ($this->pinboardItems) {
$pinboard = new PHUIPinboardView();
foreach ($this->pinboardItems as $item) {
$pinboard->addItem($item);
}
}
$content = phutil_tag(
'div',
array(
'class' => implode(' ', $content_classes),
),
array($image, $badges, $wedge, $content, $pinboard));
$outer_classes = $this->classes;
$outer_classes[] = 'phui-timeline-shell';
$color = null;
foreach ($this->getEventGroup() as $event) {
if ($event->color) {
$color = $event->color;
break;
}
}
if ($color) {
$outer_classes[] = 'phui-timeline-'.$color;
}
$sigil = null;
$meta = null;
if ($this->getTransactionPHID()) {
$sigil = 'transaction';
$meta = array(
'phid' => $this->getTransactionPHID(),
'anchor' => $this->anchor,
);
}
$major_event = null;
if ($this->reallyMajorEvent) {
$major_event = phutil_tag(
'div',
array(
'class' => 'phui-timeline-event-view '.
'phui-timeline-spacer '.
'phui-timeline-spacer-bold',
'',
));
}
return array(
javelin_tag(
'div',
array(
'class' => implode(' ', $outer_classes),
'id' => $this->anchor ? 'anchor-'.$this->anchor : null,
'sigil' => $sigil,
'meta' => $meta,
),
phutil_tag(
'div',
array(
'class' => implode(' ', $classes),
),
$content)),
$major_event,
);
}
private function renderExtra(array $events) {
$extra = array();
if ($this->getIsPreview()) {
$extra[] = pht('PREVIEW');
} else {
foreach ($events as $event) {
if ($event->getIsEdited()) {
$extra[] = pht('Edited');
break;
}
}
$source = $this->getContentSource();
$content_source = null;
if ($source) {
$content_source = id(new PhabricatorContentSourceView())
->setContentSource($source)
->setUser($this->getUser());
$content_source = pht('Via %s', $content_source->getSourceName());
}
$date_created = null;
foreach ($events as $event) {
if ($event->getDateCreated()) {
if ($date_created === null) {
$date_created = $event->getDateCreated();
} else {
$date_created = min($event->getDateCreated(), $date_created);
}
}
}
if ($date_created) {
$date = phabricator_datetime(
$date_created,
$this->getUser());
if ($this->anchor) {
Javelin::initBehavior('phabricator-watch-anchor');
Javelin::initBehavior('phabricator-tooltips');
$date = array(
javelin_tag(
'a',
array(
'href' => '#'.$this->anchor,
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $content_source,
),
),
$date),
);
}
$extra[] = $date;
}
// If this edit was applied silently, give user a hint that they should
// not expect to have received any mail or notifications.
if ($this->getIsSilent()) {
$extra[] = id(new PHUIIconView())
->setIcon('fa-bell-slash', 'red')
->setTooltip(pht('Silent Edit'));
}
// If this edit was applied while the actor was in high-security mode,
// provide a hint that it was extra authentic.
if ($this->getIsMFA()) {
$extra[] = id(new PHUIIconView())
- ->setIcon('fa-vcard', 'green')
+ ->setIcon('fa-vcard', 'pink')
->setTooltip(pht('MFA Authenticated'));
}
}
$extra = javelin_tag(
'span',
array(
'class' => 'phui-timeline-extra',
),
phutil_implode_html(
javelin_tag(
'span',
array(
'aural' => false,
),
self::DELIMITER),
$extra));
return $extra;
}
private function getMenuItems($anchor) {
$xaction_phid = $this->getTransactionPHID();
$items = array();
if ($this->getIsEditable()) {
$items[] = id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setHref('/transactions/edit/'.$xaction_phid.'/')
->setName(pht('Edit Comment'))
->addSigil('transaction-edit')
->setMetadata(
array(
'anchor' => $anchor,
));
}
if ($this->getQuoteTargetID()) {
$ref = null;
if ($this->getQuoteRef()) {
$ref = $this->getQuoteRef();
if ($anchor) {
$ref = $ref.'#'.$anchor;
}
}
$items[] = id(new PhabricatorActionView())
->setIcon('fa-quote-left')
->setName(pht('Quote Comment'))
->setHref('#')
->addSigil('transaction-quote')
->setMetadata(
array(
'targetID' => $this->getQuoteTargetID(),
'uri' => '/transactions/quote/'.$xaction_phid.'/',
'ref' => $ref,
));
}
if ($this->getIsNormalComment()) {
$items[] = id(new PhabricatorActionView())
->setIcon('fa-code')
->setHref('/transactions/raw/'.$xaction_phid.'/')
->setName(pht('View Remarkup'))
->addSigil('transaction-raw')
->setMetadata(
array(
'anchor' => $anchor,
));
$content_source = $this->getContentSource();
$source_email = PhabricatorEmailContentSource::SOURCECONST;
if ($content_source->getSource() == $source_email) {
$source_id = $content_source->getContentSourceParameter('id');
if ($source_id) {
$items[] = id(new PhabricatorActionView())
->setIcon('fa-envelope-o')
->setHref('/transactions/raw/'.$xaction_phid.'/?email')
->setName(pht('View Email Body'))
->addSigil('transaction-raw')
->setMetadata(
array(
'anchor' => $anchor,
));
}
}
}
if ($this->getIsEdited()) {
$items[] = id(new PhabricatorActionView())
->setIcon('fa-list')
->setHref('/transactions/history/'.$xaction_phid.'/')
->setName(pht('View Edit History'))
->setWorkflow(true);
}
if ($this->getIsRemovable()) {
$items[] = id(new PhabricatorActionView())
->setType(PhabricatorActionView::TYPE_DIVIDER);
$items[] = id(new PhabricatorActionView())
->setIcon('fa-trash-o')
->setHref('/transactions/remove/'.$xaction_phid.'/')
->setName(pht('Remove Comment'))
->setColor(PhabricatorActionView::RED)
->addSigil('transaction-remove')
->setMetadata(
array(
'anchor' => $anchor,
));
}
return $items;
}
}
diff --git a/src/view/phui/PHUITimelineView.php b/src/view/phui/PHUITimelineView.php
index 3353a2e2b..d0e942f46 100644
--- a/src/view/phui/PHUITimelineView.php
+++ b/src/view/phui/PHUITimelineView.php
@@ -1,279 +1,283 @@
<?php
final class PHUITimelineView extends AphrontView {
private $events = array();
private $id;
private $shouldTerminate = false;
private $shouldAddSpacers = true;
private $pager;
- private $renderData = array();
+ private $viewData = array();
private $quoteTargetID;
private $quoteRef;
public function setID($id) {
$this->id = $id;
return $this;
}
public function setShouldTerminate($term) {
$this->shouldTerminate = $term;
return $this;
}
public function setShouldAddSpacers($bool) {
$this->shouldAddSpacers = $bool;
return $this;
}
public function setPager(AphrontCursorPagerView $pager) {
$this->pager = $pager;
return $this;
}
public function getPager() {
return $this->pager;
}
public function addEvent(PHUITimelineEventView $event) {
$this->events[] = $event;
return $this;
}
- public function setRenderData(array $data) {
- $this->renderData = $data;
+ public function setViewData(array $data) {
+ $this->viewData = $data;
return $this;
}
+ public function getViewData() {
+ return $this->viewData;
+ }
+
public function setQuoteTargetID($quote_target_id) {
$this->quoteTargetID = $quote_target_id;
return $this;
}
public function getQuoteTargetID() {
return $this->quoteTargetID;
}
public function setQuoteRef($quote_ref) {
$this->quoteRef = $quote_ref;
return $this;
}
public function getQuoteRef() {
return $this->quoteRef;
}
public function render() {
if ($this->getPager()) {
if ($this->id === null) {
$this->id = celerity_generate_unique_node_id();
}
Javelin::initBehavior(
'phabricator-show-older-transactions',
array(
'timelineID' => $this->id,
- 'renderData' => $this->renderData,
+ 'viewData' => $this->getViewData(),
));
}
$events = $this->buildEvents();
return phutil_tag(
'div',
array(
'class' => 'phui-timeline-view',
'id' => $this->id,
),
array(
phutil_tag(
'h3',
array(
'class' => 'aural-only',
),
pht('Event Timeline')),
$events,
));
}
public function buildEvents() {
require_celerity_resource('phui-timeline-view-css');
$spacer = self::renderSpacer();
// Track why we're hiding older results.
$hide_reason = null;
$hide = array();
$show = array();
// Bucket timeline events into events we'll hide by default (because they
// predate your most recent interaction with the object) and events we'll
// show by default.
foreach ($this->events as $event) {
if ($event->getHideByDefault()) {
$hide[] = $event;
} else {
$show[] = $event;
}
}
// If you've never interacted with the object, all the events will be shown
// by default. We may still need to paginate if there are a large number
// of events.
$more = (bool)$hide;
if ($more) {
$hide_reason = 'comment';
}
if ($this->getPager()) {
if ($this->getPager()->getHasMoreResults()) {
if (!$more) {
$hide_reason = 'limit';
}
$more = true;
}
}
$events = array();
if ($more && $this->getPager()) {
switch ($hide_reason) {
case 'comment':
$hide_help = pht(
'Changes from before your most recent comment are hidden.');
break;
case 'limit':
default:
$hide_help = pht(
'There are a very large number of changes, so older changes are '.
'hidden.');
break;
}
$uri = $this->getPager()->getNextPageURI();
$uri->setQueryParam('quoteTargetID', $this->getQuoteTargetID());
$uri->setQueryParam('quoteRef', $this->getQuoteRef());
$events[] = javelin_tag(
'div',
array(
'sigil' => 'show-older-block',
'class' => 'phui-timeline-older-transactions-are-hidden',
),
array(
$hide_help,
' ',
javelin_tag(
'a',
array(
'href' => (string)$uri,
'mustcapture' => true,
'sigil' => 'show-older-link',
),
pht('Show Older Changes')),
));
if ($show) {
$events[] = $spacer;
}
}
if ($show) {
$this->prepareBadgeData($show);
$events[] = phutil_implode_html($spacer, $show);
}
if ($events) {
if ($this->shouldAddSpacers) {
$events = array($spacer, $events, $spacer);
}
} else {
$events = array($spacer);
}
if ($this->shouldTerminate) {
$events[] = self::renderEnder();
}
return $events;
}
public static function renderSpacer() {
return phutil_tag(
'div',
array(
'class' => 'phui-timeline-event-view '.
'phui-timeline-spacer',
),
'');
}
public static function renderEnder() {
return phutil_tag(
'div',
array(
'class' => 'phui-timeline-event-view '.
'the-worlds-end',
),
'');
}
private function prepareBadgeData(array $events) {
assert_instances_of($events, 'PHUITimelineEventView');
$viewer = $this->getUser();
$can_use_badges = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorBadgesApplication',
$viewer);
if (!$can_use_badges) {
return;
}
$user_phid_type = PhabricatorPeopleUserPHIDType::TYPECONST;
$user_phids = array();
foreach ($events as $key => $event) {
$author_phid = $event->getAuthorPHID();
if (!$author_phid) {
unset($events[$key]);
continue;
}
if (phid_get_type($author_phid) != $user_phid_type) {
// This is likely an application actor, like "Herald" or "Harbormaster".
// They can't have badges.
unset($events[$key]);
continue;
}
$user_phids[$author_phid] = $author_phid;
}
if (!$user_phids) {
return;
}
$users = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withPHIDs($user_phids)
->needBadgeAwards(true)
->execute();
$users = mpull($users, null, 'getPHID');
foreach ($events as $event) {
$user_phid = $event->getAuthorPHID();
if (!array_key_exists($user_phid, $users)) {
continue;
}
$badges = $users[$user_phid]->getRecentBadgeAwards();
foreach ($badges as $badge) {
$badge_view = id(new PHUIBadgeMiniView())
->setIcon($badge['icon'])
->setQuality($badge['quality'])
->setHeader($badge['name'])
->setTipDirection('E')
->setHref('/badges/view/'.$badge['id'].'/');
$event->addBadge($badge_view);
}
}
}
}
diff --git a/webroot/rsrc/css/application/auth/auth.css b/webroot/rsrc/css/application/auth/auth.css
index a5d326430..687aaf2bb 100644
--- a/webroot/rsrc/css/application/auth/auth.css
+++ b/webroot/rsrc/css/application/auth/auth.css
@@ -1,57 +1,66 @@
/**
* @provides auth-css
*/
.phabricator-login-buttons {
max-width: 508px;
margin: 16px auto;
}
.phabricator-login-buttons .phabricator-login-button .button {
width: 246px;
}
.device-desktop .phabricator-login-buttons .aphront-multi-column-column-last {
text-align: right;
}
.device .phabricator-login-buttons {
text-align: center;
}
.phabricator-link-button {
text-align: center;
}
.auth-account-view {
background-color: {$lightbluebackground};
border: 1px solid {$thinblueborder};
border-radius: 3px;
min-height: 50px;
position: relative;
padding: 4px 4px 4px 64px;
}
.auth-account-view-profile-image {
width: 50px;
height: 50px;
top: 6px;
left: 6px;
background-repeat: no-repeat;
background-size: 100%;
box-shadow: {$borderinset};
position: absolute;
}
.auth-account-view-name {
font-weight: bold;
}
.auth-account-view-provider-name {
color: {$lightgreytext};
}
.auth-account-view-account-uri {
word-break: break-word;
}
+
+.auth-custom-message {
+ margin: 32px auto 64px;
+ max-width: 548px;
+ background: #fff;
+ padding: 16px;
+ border: 1px solid {$lightblueborder};
+ border-radius: 4px;
+}
diff --git a/webroot/rsrc/css/application/phortune/phortune-invoice.css b/webroot/rsrc/css/application/phortune/phortune-invoice.css
index 34bceb4bb..59199f0c9 100644
--- a/webroot/rsrc/css/application/phortune/phortune-invoice.css
+++ b/webroot/rsrc/css/application/phortune/phortune-invoice.css
@@ -1,75 +1,75 @@
/**
* @provides phortune-invoice-css
*/
.phortune-invoice-view {
max-width: 800px;
margin: 16px auto;
background: #fff;
}
.phortune-invoice-view .phabricator-main-menu {
display: none;
}
.phortune-invoice-view .phabricator-standard-page-footer {
display: none;
}
.device-desktop .phortune-invoice-view .phui-property-list-key {
width: 16%;
}
.device-desktop .phortune-invoice-view .phui-property-list-value {
width: 80%;
}
.phortune-invoice-logo {
margin-bottom: 24px;
}
.phortune-invoice-logo img {
margin: 0 auto;
}
.phortune-invoice-contact {
margin-bottom: 32px;
}
.phortune-invoice-contact td {
padding: 4px 16px;
}
.phortune-invoice-to {
border-right: 1px solid {$lightblueborder};
}
.phortune-mini-header {
color: {$lightbluetext};
font-weight: bold;
text-transform: uppercase;
margin-bottom: 4px;
- letter-spacing: 0.3em;
+ letter-spacing: 0.25em;
}
.phortune-invoice-status {
margin-bottom: 24px;
}
.phortune-invoice-status .phui-info-view {
margin: 0;
}
.phortune-invoice-view .phui-box.phui-object-box {
margin-bottom: 24px;
}
.phortune-invoice-footer {
color: {$lightgreytext};
margin: 48px 0 64px;
text-align: center;
}
.phortune-invoice-footer strong {
color: #000;
}
diff --git a/webroot/rsrc/css/phui/object-item/phui-oi-big-ui.css b/webroot/rsrc/css/phui/object-item/phui-oi-big-ui.css
index 6f60560a2..a793c018c 100644
--- a/webroot/rsrc/css/phui/object-item/phui-oi-big-ui.css
+++ b/webroot/rsrc/css/phui/object-item/phui-oi-big-ui.css
@@ -1,74 +1,79 @@
/**
* @provides phui-oi-big-ui-css
* @requires phui-oi-list-view-css
*/
.phui-oi-list-big ul.phui-oi-list-view {
margin: 0;
padding: 20px;
}
.phui-oi-list-big .phui-oi-no-bar .phui-oi-frame {
border: 0;
}
.phui-oi-list-big .phui-oi-image-icon {
margin: 8px 2px 12px;
}
.phui-oi-list-big a.phui-oi-link {
color: {$blacktext};
font-size: {$biggestfontsize};
}
.phui-oi-list-big .phui-oi-name {
padding-top: 6px;
}
.phui-oi-list-big .phui-oi-launch-button a.button {
font-size: {$normalfontsize};
padding: 3px 12px 4px;
}
.device-desktop .phui-oi-list-big .phui-oi {
margin-bottom: 4px;
}
.phui-oi-list-big .phui-oi-col0 {
vertical-align: top;
padding: 0;
}
.phui-oi-list-big .phui-oi-status-icon {
padding: 5px;
}
.phui-oi-list-big .phui-oi-visited a.phui-oi-link {
color: {$violet};
}
.phui-box-white-config .phui-oi-list-big.phui-oi-list-view {
padding: 8px 8px 4px;
}
.phui-box-white-config .phui-oi-frame {
padding: 4px 8px 0;
}
.device-desktop .phui-box-white-config .phui-oi:hover .phui-oi-frame {
background-color: {$hoverblue};
border-radius: 3px;
}
.device-desktop .phui-oi-linked-container {
cursor: pointer;
}
.device-desktop .phui-oi-linked-container:hover {
background-color: {$hoverblue};
border-radius: 3px;
}
.device-desktop .phui-oi-linked-container a:hover {
text-decoration: none;
}
+
+/* Spacing for InfoView inside an object item list, like MFA setup. */
+.phui-oi .phui-info-view {
+ margin: 0 4px 4px;
+}
diff --git a/webroot/rsrc/css/phui/phui-form-view.css b/webroot/rsrc/css/phui/phui-form-view.css
index 539eaa2e7..3368bcaaf 100644
--- a/webroot/rsrc/css/phui/phui-form-view.css
+++ b/webroot/rsrc/css/phui/phui-form-view.css
@@ -1,558 +1,580 @@
/**
* @provides phui-form-view-css
*/
.phui-form-view {
padding: 16px;
}
.device-phone .phui-object-box .phui-form-view {
padding: 0;
}
.phui-form-view.phui-form-full-width {
padding: 0;
}
.phui-form-view label.aphront-form-label {
width: 19%;
height: 28px;
line-height: 28px;
float: left;
text-align: right;
font-weight: bold;
font-size: {$normalfontsize};
color: {$bluetext};
-webkit-font-smoothing: antialiased;
}
.device-phone .phui-form-view label.aphront-form-label,
.phui-form-full-width.phui-form-view label.aphront-form-label {
display: block;
float: none;
text-align: left;
width: 100%;
margin-bottom: 3px;
}
.aphront-form-input {
margin-left: 20%;
margin-right: 20%;
width: 60%;
}
.device-phone .aphront-form-input,
.device .aphront-form-input select,
.device .aphront-form-input pre,
.phui-form-full-width .aphront-form-input {
margin-left: 0%;
margin-right: 0%;
width: 100%;
}
.aphront-form-input *::-webkit-input-placeholder {
color:{$greytext} !important;
}
.aphront-form-input *::-moz-placeholder {
color:{$greytext} !important;
opacity: 1; /* Firefox nudges the opacity to 0.4 */
}
.aphront-form-input *:-ms-input-placeholder {
color:{$greytext} !important;
}
.aphront-form-error {
width: 18%;
float: right;
color: {$red};
font-weight: bold;
padding-top: 5px;
}
.aphront-form-label .aphront-form-error {
display: none;
}
.aphront-dialog-body .phui-form-view {
padding: 0;
}
.device-phone .aphront-form-error,
.phui-form-full-width .aphront-form-error {
display: none;
}
.device-phone .aphront-form-label .aphront-form-error,
.phui-form-full-width .aphront-form-label .aphront-form-error {
display: block;
float: right;
padding: 0;
width: auto;
}
.device-phone .aphront-form-drag-and-drop-upload {
display: none;
}
.aphront-form-required {
font-weight: normal;
color: {$lightgreytext};
font-size: {$smallestfontsize};
-webkit-font-smoothing: antialiased;
}
.aphront-form-input input[type="text"],
.aphront-form-input input[type="password"] {
width: 100%;
}
.aphront-form-cvc-input input {
width: 64px;
}
.aphront-form-input textarea {
display: block;
width: 100%;
box-sizing: border-box;
height: 12em;
}
.aphront-form-control {
padding: 4px;
}
.device-phone .aphront-form-control {
padding: 4px 8px 8px;
}
.phui-form-full-width .aphront-form-control {
padding: 4px 0;
}
.aphront-form-control-submit button,
.aphront-form-control-submit a.button,
.aphront-form-control-submit input[type="submit"] {
float: right;
margin: 4px 0 0 8px;
}
.aphront-form-control-textarea textarea.aphront-textarea-very-short {
height: 44px;
}
.aphront-form-control-textarea textarea.aphront-textarea-very-tall {
height: 24em;
}
.phui-form-view .aphront-form-caption {
font-size: {$smallerfontsize};
color: {$bluetext};
padding: 8px 0;
margin-right: 20%;
margin-left: 20%;
-webkit-font-smoothing: antialiased;
line-height: 16px;
}
.device-phone .phui-form-view .aphront-form-caption,
.phui-form-full-width .phui-form-view .aphront-form-caption {
margin: 0;
}
.aphront-form-instructions {
width: 60%;
margin-left: 20%;
padding: 12px 4px;
color: {$darkbluetext};
}
.device .aphront-form-instructions,
.phui-form-full-width .aphront-form-instructions {
width: auto;
margin: 0;
padding: 12px 8px 8px;
}
.aphront-form-important {
margin: .5em 0;
background: #ffffdd;
padding: .5em 1em;
}
.aphront-form-important code {
display: block;
padding: .25em;
margin: .5em 2em;
}
.aphront-form-control-markup .aphront-form-input {
font-size: {$normalfontsize};
padding: 3px 0;
}
.aphront-form-control-static .aphront-form-input {
line-height: 28px;
}
.aphront-form-control-togglebuttons .aphront-form-input {
padding: 2px 0 0 0;
}
table.aphront-form-control-radio-layout,
table.aphront-form-control-checkbox-layout {
margin-top: 4px !important;
font-size: {$normalfontsize};
}
table.aphront-form-control-radio-layout th {
padding-left: 8px;
padding-bottom: 8px;
font-weight: bold;
color: {$darkgreytext};
}
table.aphront-form-control-checkbox-layout th {
padding-top: 2px;
padding-left: 8px;
padding-bottom: 4px;
color: {$darkgreytext};
}
.aphront-form-control-radio-layout td input,
.aphront-form-control-checkbox-layout td input {
margin-top: 4px;
width: auto;
}
.aphront-form-control-radio-layout label.disabled,
.aphront-form-control-checkbox-layout label.disabled {
color: {$greytext};
}
.aphront-form-radio-caption {
margin-top: 4px;
font-size: {$smallerfontsize};
font-weight: normal;
color: {$bluetext};
}
.aphront-form-control-image span {
margin: 0 4px 0 2px;
}
.aphront-form-control-image .default-image {
display: inline;
width: 12px;
}
.aphront-form-input hr {
border: none;
background: #bbbbbb;
height: 1px;
position: relative;
}
.phui-form-inset {
margin: 12px 0;
padding: 8px;
background: #f7f9fd;
border: 1px solid {$lightblueborder};
border-radius: 3px;
}
.phui-form-inset h1 {
color: {$bluetext};
padding-bottom: 8px;
margin-bottom: 8px;
font-size: {$biggerfontsize};
border-bottom: 1px solid {$thinblueborder};
}
.aphront-form-drag-and-drop-file-list {
width: 400px;
}
.drag-and-drop-instructions {
color: {$darkgreytext};
font-size: {$smallestfontsize};
padding: 6px 8px;
}
.drag-and-drop-file-target {
border: 1px dashed #bfbfbf;
padding-top: 12px;
padding-bottom: 12px;
}
body .phui-form-view .remarkup-assist-textarea.aphront-textarea-drag-and-drop {
background: {$sh-greenbackground};
border: 1px solid {$sh-greenborder};
}
.aphront-form-crop .crop-box {
cursor: move;
overflow: hidden;
}
.aphront-form-crop .crop-box .crop-image {
position: relative;
top: 0px;
left: 0px;
}
.calendar-button {
display: inline;
padding: 8px 4px;
margin: 2px 8px 2px 2px;
position: relative;
}
.aphront-form-date-container {
position: relative;
display: inline;
}
.aphront-form-date-container select {
margin: 2px;
display: inline;
}
.aphront-form-date-container input.aphront-form-date-enabled-input {
width: auto;
display: inline;
margin-right: 8px;
font-size: 16px;
}
.aphront-form-date-container .aphront-form-time-input-container,
.aphront-form-date-container .aphront-form-date-input-container {
position: relative;
display: inline-block;
width: 7em;
}
.aphront-form-date-container input.aphront-form-time-input,
.aphront-form-date-container input.aphront-form-date-input {
width: 7em;
}
.aphront-form-time-input-container div.jx-typeahead-results a.jx-result {
border: none;
}
.phui-time-typeahead-value {
padding: 4px;
}
.fancy-datepicker {
position: absolute;
width: 240px;
}
.device .fancy-datepicker {
width: 100%;
}
.fancy-datepicker-core {
width: 240px;
margin: 0 auto;
padding: 1px;
font-size: {$smallerfontsize};
text-align: center;
}
.fancy-datepicker-core .month-table,
.fancy-datepicker-core .day-table {
margin: 0 auto;
border-collapse: separate;
border-spacing: 1px;
width: 100%;
}
.fancy-datepicker-core .month-table {
margin-bottom: 6px;
font-size: {$normalfontsize};
background-color: {$hoverblue};
border-radius: 2px;
}
.fancy-datepicker-core .month-table td.lrbutton {
width: 18%;
color: {$lightbluetext};
}
.fancy-datepicker-core .month-table td {
padding: 4px;
font-weight: bold;
color: {$bluetext};
}
.fancy-datepicker-core .month-table td.lrbutton:hover {
border-radius: 2px;
background: {$hoverselectedblue};
color: {$darkbluetext};
}
.fancy-datepicker-core .day-table td {
overflow: hidden;
vertical-align: center;
text-align: center;
border: 1px solid {$thinblueborder};
padding: 4px 0;
}
.fancy-datepicker .fancy-datepicker-core .day-table td.day:hover {
background-color: {$hoverblue};
border-color: {$lightblueborder};
}
.fancy-datepicker-core .day-table td.day-placeholder {
border-color: transparent;
background: transparent;
}
.fancy-datepicker-core .day-table td.weekend {
color: {$lightgreytext};
border-color: {$lightgreyborder};
background: {$lightgreybackground};
}
.fancy-datepicker-core .day-table td.day-name {
background: transparent;
border: 1px transparent;
vertical-align: bottom;
color: {$lightgreytext};
}
.fancy-datepicker-core .day-table td.today {
background: {$greybackground};
border-color: {$greyborder};
color: {$darkgreytext};
font-weight: bold;
}
.fancy-datepicker-core .day-table td.datepicker-selected {
background: {$lightgreen};
border-color: {$green};
color: {$green};
}
.fancy-datepicker-core td {
cursor: pointer;
}
.fancy-datepicker-core td.novalue {
cursor: inherit;
}
.picker-open .calendar-button .phui-icon-view {
color: {$sky};
}
.fancy-datepicker-core {
background-color: white;
border: 1px solid {$lightgreyborder};
box-shadow: {$dropshadow};
border-radius: 3px;
}
/* When the activation checkbox for the control is toggled off, visually
disable the individual controls. We don't actually use the "disabled" property
because we still want the values to submit. This is just a visual hint that
the controls won't be used. The controls themselves are still live, work
properly, and submit values. */
.datepicker-disabled select,
.datepicker-disabled .calendar-button,
.datepicker-disabled input[type="text"] {
opacity: 0.5;
}
.aphront-form-date-container.no-time .aphront-form-time-input{
display: none;
}
.login-to-comment {
margin: 12px;
}
.phui-form-divider hr {
height: 1px;
border: 0;
background: {$thinblueborder};
width: 85%;
margin: 15px auto;
}
.recaptcha_only_if_privacy {
display: none;
}
.phabricator-standard-custom-field-header {
font-size: 16px;
color: {$bluetext};
border-bottom: 1px solid {$lightbluetext};
padding: 16px 0 4px;
margin-bottom: 4px;
}
.device-desktop .text-with-submit-control-outer-bounds {
position: relative;
}
.device-desktop .text-with-submit-control-text-bounds {
position: absolute;
left: 0;
right: 184px;
}
.device-desktop .text-with-submit-control-submit-bounds {
text-align: right;
}
.device-desktop .text-with-submit-control-submit {
width: 180px;
}
.phui-form-iconset-table td {
vertical-align: middle;
padding: 4px 0;
}
.phui-form-iconset-table .phui-form-iconset-button-cell {
padding: 4px 8px;
}
.aphront-form-preview-hidden {
opacity: 0.5;
}
.aphront-form-error .phui-icon-view {
float: right;
color: {$lightgreyborder};
font-size: 20px;
}
.device-desktop .aphront-form-error .phui-icon-view:hover {
color: {$red};
}
.phui-form-static-action {
height: 28px;
line-height: 28px;
color: {$bluetext};
}
.phuix-form-checkbox-action {
padding: 4px;
color: {$bluetext};
}
.phuix-form-checkbox-action input[type=checkbox] {
margin: 4px 0;
}
.phuix-form-checkbox-label {
margin-left: 4px;
}
+
+.phui-form-timer-icon {
+ width: 28px;
+ height: 28px;
+ padding: 4px;
+ font-size: 18px;
+ background: {$greybackground};
+ border-radius: 4px;
+ text-align: center;
+ vertical-align: middle;
+ text-shadow: 1px 1px rgba(0, 0, 0, 0.05);
+}
+
+.phui-form-timer-content {
+ padding: 4px 8px;
+ color: {$darkgreytext};
+ vertical-align: middle;
+}
+
+.mfa-form-enroll-button {
+ text-align: center;
+}
diff --git a/webroot/rsrc/css/phui/phui-info-view.css b/webroot/rsrc/css/phui/phui-info-view.css
index 55400956e..b4fafc6e5 100644
--- a/webroot/rsrc/css/phui/phui-info-view.css
+++ b/webroot/rsrc/css/phui/phui-info-view.css
@@ -1,145 +1,154 @@
/**
* @provides phui-info-view-css
*/
.phui-info-view {
border-style: solid;
border-width: 1px;
background: {$page.content};
margin: 16px;
padding: 12px;
border-radius: 3px;
}
div.phui-info-view.phui-info-severity-plain {
background: {$lightgreybackground};
color: {$bluetext};
border: none;
padding: 8px 12px;
margin-bottom: 4px !important;
}
.phui-info-view.phui-info-view-flush {
margin: 0 0 20px 0;
}
.device .phui-info-view {
margin: 8px;
}
.phui-info-view .phui-form-view {
padding: 0;
}
.phui-info-view-icon {
width: 24px;
float: left;
}
.phui-info-view-body {
line-height: 1.6em;
color: {$blacktext};
}
.phui-info-view.phui-info-has-icon .phui-info-view-body {
margin-left: 24px;
}
.phui-info-view-body tt {
color: {$blacktext};
background: rgba({$alphablue},0.1);
padding: 1px 4px;
border-radius: 3px;
white-space: pre-wrap;
}
.phui-info-view-actions {
margin-top: -3px;
margin-bottom: -4px;
float: right;
}
.phui-info-view-actions .button {
margin-left: 4px;
}
.phui-info-view-head + .phui-info-view-body {
padding-top: 4px;
}
h1.phui-info-view-head {
font-weight: bold;
font-size: {$biggerfontsize};
line-height: 1.3em;
}
.phui-info-view-list {
margin: 0;
list-style: none;
line-height: 1.6em;
}
.phui-info-view .phui-info-icon {
padding-top: 1px;
font-size: 16px;
}
.phui-info-severity-error {
border-color: {$red};
border-left-width: 6px;
}
.phui-info-severity-error .phui-info-icon {
color: {$red};
}
+.phui-info-severity-mfa {
+ border-color: {$blue};
+ border-left-width: 6px;
+}
+
+.phui-info-severity-mfa .phui-info-icon {
+ color: {$blue};
+}
+
.phui-info-severity-warning {
border-color: {$yellow};
border-left-width: 6px;
}
.phui-info-severity-warning .phui-info-icon {
color: {$yellow};
}
.phui-info-severity-notice {
border-color: {$blue};
border-left-width: 6px;
}
.phui-info-severity-notice .phui-info-icon {
color: {$blue};
}
.phui-info-severity-nodata {
border-color: {$lightgreyborder};
}
.phui-info-severity-success {
border-color: {$green};
border-left-width: 6px;
}
.phui-info-severity-success .phui-info-icon {
color: {$green};
}
.aphront-dialog-body .phui-info-view {
margin: 0 0 8px 0;
}
.phui-crumbs-view + .phui-info-view {
margin-top: 0;
}
.phui-crumbs-view.phui-crumbs-border + .phui-info-view {
margin-top: 16px;
}
div.phui-object-box .phui-header-shell + .phui-info-view {
margin: 16px 0 8px;
}
div.phui-object-box.phui-box-white-config .phui-header-shell + .phui-info-view {
margin: 20px 16px 8px;
}
diff --git a/webroot/rsrc/js/application/maniphest/behavior-line-chart.js b/webroot/rsrc/js/application/maniphest/behavior-line-chart.js
index 2f63657c5..b2290620f 100644
--- a/webroot/rsrc/js/application/maniphest/behavior-line-chart.js
+++ b/webroot/rsrc/js/application/maniphest/behavior-line-chart.js
@@ -1,123 +1,126 @@
/**
* @provides javelin-behavior-line-chart
* @requires javelin-behavior
* javelin-dom
* javelin-vector
* phui-chart-css
*/
JX.behavior('line-chart', function(config) {
function fn(n) {
return n + '(' + JX.$A(arguments).slice(1).join(', ') + ')';
}
var h = JX.$(config.hardpoint);
var d = JX.Vector.getDim(h);
var padding = {
top: 24,
left: 48,
bottom: 48,
right: 32
};
var size = {
frameWidth: d.x,
frameHeight: d.y,
};
size.width = size.frameWidth - padding.left - padding.right;
size.height = size.frameHeight - padding.top - padding.bottom;
var x = d3.time.scale()
.range([0, size.width]);
var y = d3.scale.linear()
.range([size.height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient('bottom');
var yAxis = d3.svg.axis()
.scale(y)
.orient('left');
var svg = d3.select('#' + config.hardpoint).append('svg')
.attr('width', size.frameWidth)
.attr('height', size.frameHeight)
.attr('class', 'chart');
var g = svg.append('g')
.attr('transform', fn('translate', padding.left, padding.top));
g.append('rect')
.attr('class', 'inner')
.attr('width', size.width)
.attr('height', size.height);
var line = d3.svg.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.count); });
var data = [];
for (var ii = 0; ii < config.x[0].length; ii++) {
data.push(
{
date: new Date(config.x[0][ii] * 1000),
count: +config.y[0][ii]
});
}
x.domain(d3.extent(data, function(d) { return d.date; }));
var yex = d3.extent(data, function(d) { return d.count; });
yex[0] = 0;
yex[1] = yex[1] * 1.05;
y.domain(yex);
g.append('path')
.datum(data)
.attr('class', 'line')
.attr('d', line);
g.append('g')
.attr('class', 'x axis')
.attr('transform', fn('translate', 0, size.height))
.call(xAxis);
g.append('g')
.attr('class', 'y axis')
.attr('transform', fn('translate', 0, 0))
.call(yAxis);
var div = d3.select('body')
.append('div')
.attr('class', 'chart-tooltip')
.style('opacity', 0);
g.selectAll('dot')
.data(data)
.enter()
.append('circle')
.attr('class', 'point')
.attr('r', 3)
.attr('cx', function(d) { return x(d.date); })
.attr('cy', function(d) { return y(d.count); })
.on('mouseover', function(d) {
var d_y = d.date.getFullYear();
- var d_m = d.date.getMonth();
+
+ // NOTE: Javascript months are zero-based. See PHI1017.
+ var d_m = d.date.getMonth() + 1;
+
var d_d = d.date.getDate();
div
.html(d_y + '-' + d_m + '-' + d_d + ': ' + d.count)
.style('opacity', 0.9)
.style('left', (d3.event.pageX - 60) + 'px')
.style('top', (d3.event.pageY - 38) + 'px');
})
.on('mouseout', function() {
div.style('opacity', 0);
});
});
diff --git a/webroot/rsrc/js/application/transactions/behavior-comment-actions.js b/webroot/rsrc/js/application/transactions/behavior-comment-actions.js
index ab962592c..e5d7b27b2 100644
--- a/webroot/rsrc/js/application/transactions/behavior-comment-actions.js
+++ b/webroot/rsrc/js/application/transactions/behavior-comment-actions.js
@@ -1,287 +1,289 @@
/**
* @provides javelin-behavior-comment-actions
* @requires javelin-behavior
* javelin-stratcom
* javelin-workflow
* javelin-dom
* phuix-form-control-view
* phuix-icon-view
* javelin-behavior-phabricator-gesture
*/
JX.behavior('comment-actions', function(config) {
var action_map = config.actions;
var action_node = JX.$(config.actionID);
var form_node = JX.$(config.formID);
var input_node = JX.$(config.inputID);
var place_node = JX.$(config.placeID);
var rows = {};
JX.DOM.listen(action_node, 'change', null, function() {
var option = find_option(action_node.value);
action_node.value = '+';
if (option) {
add_row(option);
}
});
function find_option(key) {
var options = action_node.options;
var option;
for (var ii = 0; ii < options.length; ii++) {
option = options[ii];
if (option.value == key) {
return option;
}
}
return null;
}
function redraw() {
// If any of the stacked actions specify that they change the label for
// the "Submit" button, update the button text. Otherwise, return it to
// the default text.
var button_text = config.defaultButtonText;
for (var k in rows) {
var action = action_map[k];
if (action.buttonText) {
button_text = action.buttonText;
}
}
var button_node = JX.DOM.find(form_node, 'button', 'submit-transactions');
JX.DOM.setContent(button_node, button_text);
}
function remove_action(key) {
var row = rows[key];
if (row) {
JX.DOM.remove(row.node);
row.option.disabled = false;
delete rows[key];
}
redraw();
}
function serialize_actions() {
var data = [];
for (var k in rows) {
data.push({
type: k,
value: rows[k].control.getValue(),
initialValue: action_map[k].initialValue || null
});
}
return JX.JSON.stringify(data);
}
function get_data() {
var data = JX.DOM.convertFormToDictionary(form_node);
data.__preview__ = 1;
data[input_node.name] = serialize_actions();
+ data.viewData = JX.JSON.stringify(config.viewData);
+
return data;
}
function restore_draft_actions(drafts) {
var draft;
var option;
var control;
for (var ii = 0; ii < drafts.length; ii++) {
draft = drafts[ii];
option = find_option(draft);
if (!option) {
continue;
}
control = add_row(option);
}
redraw();
}
function onresponse(response) {
if (JX.Device.getDevice() != 'desktop') {
return;
}
var panel = JX.$(config.panelID);
if (!response.xactions.length) {
JX.DOM.hide(panel);
} else {
var preview_root = JX.$(config.timelineID);
JX.DOM.setContent(
preview_root,
[
JX.$H(response.header),
JX.$H(response.xactions.join('')),
JX.$H(response.previewContent)
]);
JX.DOM.show(panel);
// NOTE: Resonses are currently processed before associated behaviors are
// registered. We need to defer invoking this event so that any behaviors
// accompanying the response are registered.
var invoke_preview = function() {
JX.Stratcom.invoke(
'EditEngine.didCommentPreview',
null,
{
rootNode: preview_root
});
};
setTimeout(invoke_preview, 0);
}
}
function force_preview() {
if (!config.showPreview) {
return;
}
new JX.Request(config.actionURI, onresponse)
.setData(get_data())
.send();
}
function add_row(option) {
var action = action_map[option.value];
if (!action) {
return;
}
// Remove any conflicting actions. For example, "Accept Revision" conflicts
// with "Reject Revision".
var conflict_key = action.conflictKey || null;
if (conflict_key !== null) {
for (var k in action_map) {
if (k === action.key) {
continue;
}
if (action_map[k].conflictKey !== conflict_key) {
continue;
}
if (!(k in rows)) {
continue;
}
remove_action(k);
}
}
option.disabled = true;
var aural = JX.$N('span', {className: 'aural-only'}, action.auralLabel);
var icon = new JX.PHUIXIconView()
.setIcon('fa-times-circle');
var remove = JX.$N('a', {href: '#'}, [aural, icon.getNode()]);
var control = new JX.PHUIXFormControl()
.setLabel(action.label)
.setError(remove)
.setControl(action.type, action.spec)
.setClass('phui-comment-action');
var node = control.getNode();
JX.Stratcom.addSigil(node, 'touchable');
JX.DOM.listen(node, 'gesture.swipe.end', null, function(e) {
var data = e.getData();
if (data.direction != 'left') {
// Didn't swipe left.
return;
}
if (data.length <= (JX.Vector.getDim(node).x / 2)) {
// Didn't swipe far enough.
return;
}
remove_action(action.key);
});
rows[action.key] = {
control: control,
node: node,
option: option
};
JX.DOM.listen(remove, 'click', null, function(e) {
e.kill();
remove_action(action.key);
});
place_node.parentNode.insertBefore(node, place_node);
redraw();
force_preview();
return control;
}
JX.DOM.listen(form_node, ['submit', 'didSyntheticSubmit'], null, function() {
input_node.value = serialize_actions();
});
if (config.showPreview) {
var request = new JX.PhabricatorShapedRequest(
config.actionURI,
onresponse,
get_data);
var trigger = JX.bind(request, request.trigger);
JX.DOM.listen(form_node, 'keydown', null, trigger);
JX.DOM.listen(form_node, 'shouldRefresh', null, force_preview);
request.start();
var old_device = JX.Device.getDevice();
var ondevicechange = function() {
var new_device = JX.Device.getDevice();
var panel = JX.$(config.panelID);
if (new_device == 'desktop') {
request.setRateLimit(500);
// Force an immediate refresh if we switched from another device type
// to desktop.
if (old_device != new_device) {
force_preview();
}
} else {
// On mobile, don't show live previews and only save drafts every
// 10 seconds.
request.setRateLimit(10000);
JX.DOM.hide(panel);
}
old_device = new_device;
};
ondevicechange();
JX.Stratcom.listen('phabricator-device-change', null, ondevicechange);
}
restore_draft_actions(config.drafts || []);
});
diff --git a/webroot/rsrc/js/application/transactions/behavior-show-older-transactions.js b/webroot/rsrc/js/application/transactions/behavior-show-older-transactions.js
index 28754ee3a..74b17cd45 100644
--- a/webroot/rsrc/js/application/transactions/behavior-show-older-transactions.js
+++ b/webroot/rsrc/js/application/transactions/behavior-show-older-transactions.js
@@ -1,115 +1,119 @@
/**
* @provides javelin-behavior-phabricator-show-older-transactions
* @requires javelin-behavior
* javelin-stratcom
* javelin-dom
* phabricator-busy
*/
JX.behavior('phabricator-show-older-transactions', function(config) {
function get_hash() {
return window.location.hash.replace(/^#/, '');
}
function hash_is_hidden() {
var hash = get_hash();
if (!hash) {
return false;
}
// If the hash isn't purely numeric, ignore it. Comments always have
// numeric hashes. See PHI43 and T12970.
if (!hash.match(/^\d+$/)) {
return false;
}
var id = 'anchor-'+hash;
try {
JX.$(id);
} catch (not_found_exception) {
return true;
}
return false;
}
function check_hash() {
if (hash_is_hidden()) {
load_older(load_hidden_hash_callback);
}
}
function load_older(callback) {
var showOlderBlock = null;
try {
showOlderBlock = JX.DOM.find(
JX.$(config.timelineID),
'div',
'show-older-block');
} catch (not_found_exception) {
// we loaded everything...!
return;
}
var showOlderLink = JX.DOM.find(
showOlderBlock,
'a',
'show-older-link');
var workflow = fetch_older_workflow(
showOlderLink.href,
callback,
showOlderBlock);
var routable = workflow.getRoutable()
.setPriority(2000)
.setType('workflow');
JX.Router.getInstance().queue(routable);
}
var show_older = function(swap, r) {
JX.DOM.replace(swap, JX.$H(r.timeline).getFragment());
JX.Stratcom.invoke('resize');
};
var load_hidden_hash_callback = function(swap, r) {
show_older(swap, r);
// We aren't actually doing a scroll position because
// `behavior-watch-anchor` will handle that for us.
};
var load_all_older_callback = function(swap, r) {
show_older(swap, r);
load_older(load_all_older_callback);
};
var fetch_older_workflow = function(href, callback, swap) {
- return new JX.Workflow(href, config.renderData)
+ var params = {
+ viewData: JX.JSON.stringify(config.viewData)
+ };
+
+ return new JX.Workflow(href, params)
.setHandler(JX.bind(null, callback, swap));
};
JX.Stratcom.listen(
'click',
['show-older-block'],
function(e) {
e.kill();
var workflow = fetch_older_workflow(
JX.DOM.find(
e.getNode('show-older-block'),
'a',
'show-older-link').href,
show_older,
e.getNode('show-older-block'));
var routable = workflow.getRoutable()
.setPriority(2000)
.setType('workflow');
JX.Router.getInstance().queue(routable);
});
JX.Stratcom.listen('hashchange', null, check_hash);
check_hash();
new JX.KeyboardShortcut(['@'], 'Show all older changes in the timeline.')
.setHandler(JX.bind(null, load_older, load_all_older_callback))
.register();
});

Event Timeline