diff --git a/externals/phpmailer/class.phpmailer-lite.php b/externals/phpmailer/class.phpmailer-lite.php index f12a8d5844..ad0ea36edb 100755 --- a/externals/phpmailer/class.phpmailer-lite.php +++ b/externals/phpmailer/class.phpmailer-lite.php @@ -1,2086 +1,2086 @@ 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; + 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': $toArr = array(); foreach($this->to as $t) { $toArr[] = $this->AddrFormat($t); } $to = implode(', ', $toArr); return $this->customMailer->executeSend( "To: ".$to."\n". $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) { if(!@$mail = popen($sendmail, 'w')) { throw new phpmailerException($this->Lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } fputs($mail, "To: " . $val . "\n"); fputs($mail, $header); fputs($mail, $body); $result = pclose($mail); // implement call back function if it exists $isSent = ($result == 0) ? 1 : 0; $this->doCallback($isSent,$val,$this->cc,$this->bcc,$this->Subject,$body); if($result != 0) { throw new phpmailerException($this->Lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } } } else { if(!@$mail = popen($sendmail, 'w')) { throw new phpmailerException($this->Lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } fputs($mail, $header); fputs($mail, $body); $result = pclose($mail); // implement call back function if it exists $isSent = ($result == 0) ? 1 : 0; $this->doCallback($isSent,$this->to,$this->cc,$this->bcc,$this->Subject,$body); if($result != 0) { throw new phpmailerException($this->Lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } } 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; 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 $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; } /** * 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') { // There should not be any EOL in the string $encoded = preg_replace('/[\r\n]*/', '', $str); switch (strtolower($position)) { case 'phrase': $encoded = preg_replace("/([^A-Za-z0-9!*+\/ -])/e", "'='.sprintf('%02X', ord('\\1'))", $encoded); break; case 'comment': $encoded = preg_replace("/([\(\)\"])/e", "'='.sprintf('%02X', ord('\\1'))", $encoded); case 'text': default: // Replace every high ascii, control =, ? and _ characters //TODO using /e (equivalent to eval()) is probably not a good idea $encoded = preg_replace('/([\000-\011\013\014\016-\037\075\077\137\177-\377])/e', "'='.sprintf('%02X', ord('\\1'))", $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;$iDKIM_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 = '' . $this->getMessage() . "
\n"; return $errorMsg; } } ?> diff --git a/externals/xhprof/xhprof_lib.php b/externals/xhprof/xhprof_lib.php index fed9487039..8f8985ed26 100644 --- a/externals/xhprof/xhprof_lib.php +++ b/externals/xhprof/xhprof_lib.php @@ -1,866 +1,866 @@ array("Wall", "microsecs", "walltime" ), "ut" => array("User", "microsecs", "user cpu time" ), "st" => array("Sys", "microsecs", "system cpu time"), "cpu" => array("Cpu", "microsecs", "cpu time"), "mu" => array("MUse", "bytes", "memory usage"), "pmu" => array("PMUse", "bytes", "peak memory usage"), "samples" => array("Samples", "samples", "cpu time")); return $possible_metrics; } /* * Get the list of metrics present in $xhprof_data as an array. * * @author Kannan */ function xhprof_get_metrics($xhprof_data) { // get list of valid metrics $possible_metrics = xhprof_get_possible_metrics(); // return those that are present in the raw data. // We'll just look at the root of the subtree for this. $metrics = array(); foreach ($possible_metrics as $metric => $desc) { if (isset($xhprof_data["main()"][$metric])) { $metrics[] = $metric; } } return $metrics; } /** * Takes a parent/child function name encoded as * "a==>b" and returns array("a", "b"). * * @author Kannan */ function xhprof_parse_parent_child($parent_child) { $ret = explode("==>", $parent_child); // Return if both parent and child are set if (isset($ret[1])) { return $ret; } return array(null, $ret[0]); } /** * Given parent & child function name, composes the key * in the format present in the raw data. * * @author Kannan */ function xhprof_build_parent_child_key($parent, $child) { if ($parent) { return $parent . "==>" . $child; } else { return $child; } } /** * Checks if XHProf raw data appears to be valid and not corrupted. * * @param int $run_id Run id of run to be pruned. * [Used only for reporting errors.] * @param array $raw_data XHProf raw data to be pruned * & validated. * * @return bool true on success, false on failure * * @author Kannan */ function xhprof_valid_run($run_id, $raw_data) { $main_info = $raw_data["main()"]; if (empty($main_info)) { xhprof_error("XHProf: main() missing in raw data for Run ID: $run_id"); return false; } // raw data should contain either wall time or samples information... if (isset($main_info["wt"])) { $metric = "wt"; } else if (isset($main_info["samples"])) { $metric = "samples"; } else { xhprof_error("XHProf: Wall Time information missing from Run ID: $run_id"); return false; } foreach ($raw_data as $info) { $val = $info[$metric]; // basic sanity checks... if ($val < 0) { xhprof_error("XHProf: $metric should not be negative: Run ID $run_id" . serialize($info)); return false; } if ($val > (86400000000)) { xhprof_error("XHProf: $metric > 1 day found in Run ID: $run_id " . serialize($info)); return false; } } return true; } /** * Return a trimmed version of the XHProf raw data. Note that the raw * data contains one entry for each unique parent/child function * combination.The trimmed version of raw data will only contain * entries where either the parent or child function is in the list * of $functions_to_keep. * * Note: Function main() is also always kept so that overall totals * can still be obtained from the trimmed version. * * @param array XHProf raw data * @param array array of function names * * @return array Trimmed XHProf Report * * @author Kannan */ function xhprof_trim_run($raw_data, $functions_to_keep) { // convert list of functions to a hash with function as the key $function_map = array_fill_keys($functions_to_keep, 1); // always keep main() as well so that overall totals can still // be computed if need be. $function_map['main()'] = 1; $new_raw_data = array(); foreach ($raw_data as $parent_child => $info) { list($parent, $child) = xhprof_parse_parent_child($parent_child); if (isset($function_map[$parent]) || isset($function_map[$child])) { $new_raw_data[$parent_child] = $info; } } return $new_raw_data; } /** * Takes raw XHProf data that was aggregated over "$num_runs" number * of runs averages/nomalizes the data. Essentially the various metrics * collected are divided by $num_runs. * * @author Kannan */ function xhprof_normalize_metrics($raw_data, $num_runs) { if (empty($raw_data) || ($num_runs == 0)) { return $raw_data; } $raw_data_total = array(); if (isset($raw_data["==>main()"]) && isset($raw_data["main()"])) { xhprof_error("XHProf Error: both ==>main() and main() set in raw data..."); } foreach ($raw_data as $parent_child => $info) { foreach ($info as $metric => $value) { $raw_data_total[$parent_child][$metric] = ($value / $num_runs); } } return $raw_data_total; } /** * Get raw data corresponding to specified array of runs * aggregated by certain weightage. * * Suppose you have run:5 corresponding to page1.php, * run:6 corresponding to page2.php, * and run:7 corresponding to page3.php * * and you want to accumulate these runs in a 2:4:1 ratio. You * can do so by calling: * * xhprof_aggregate_runs(array(5, 6, 7), array(2, 4, 1)); * * The above will return raw data for the runs aggregated * in 2:4:1 ratio. * * @param object $xhprof_runs_impl An object that implements * the iXHProfRuns interface * @param array $runs run ids of the XHProf runs.. * @param array $wts integral (ideally) weights for $runs * @param string $source source to fetch raw data for run from * @param bool $use_script_name If true, a fake edge from main() to * to __script:: is introduced * in the raw data so that after aggregations * the script name is still preserved. * * @return array Return aggregated raw data * * @author Kannan */ function xhprof_aggregate_runs($xhprof_runs_impl, $runs, $wts, $source="phprof", $use_script_name=false) { $raw_data_total = null; $raw_data = null; $metrics = array(); $run_count = count($runs); $wts_count = count($wts); if (($run_count == 0) || (($wts_count > 0) && ($run_count != $wts_count))) { return array('description' => 'Invalid input..', 'raw' => null); } $bad_runs = array(); foreach($runs as $idx => $run_id) { - $raw_data = $xhprof_runs_impl->get_run($run_id, $source, $description); + $raw_data = $xhprof_runs_impl->get_run($run_id, $source, '?'); // use the first run to derive what metrics to aggregate on. if ($idx == 0) { foreach ($raw_data["main()"] as $metric => $val) { if ($metric != "pmu") { // for now, just to keep data size small, skip "peak" memory usage // data while aggregating. // The "regular" memory usage data will still be tracked. if (isset($val)) { $metrics[] = $metric; } } } } if (!xhprof_valid_run($run_id, $raw_data)) { $bad_runs[] = $run_id; continue; } if ($use_script_name) { - $page = $description; + $page = '?'; // create a fake function '__script::$page', and have and edge from // main() to '__script::$page'. We will also need edges to transfer // all edges originating from main() to now originate from // '__script::$page' to all function called from main(). // // We also weight main() ever so slightly higher so that // it shows up above the new entry in reports sorted by // inclusive metrics or call counts. if ($page) { foreach($raw_data["main()"] as $metric => $val) { $fake_edge[$metric] = $val; $new_main[$metric] = $val + 0.00001; } $raw_data["main()"] = $new_main; $raw_data[xhprof_build_parent_child_key("main()", "__script::$page")] = $fake_edge; } else { $use_script_name = false; } } // if no weights specified, use 1 as the default weightage.. $wt = ($wts_count == 0) ? 1 : $wts[$idx]; // aggregate $raw_data into $raw_data_total with appropriate weight ($wt) foreach ($raw_data as $parent_child => $info) { if ($use_script_name) { // if this is an old edge originating from main(), it now // needs to be from '__script::$page' if (substr($parent_child, 0, 9) == "main()==>") { $child =substr($parent_child, 9); // ignore the newly added edge from main() if (substr($child, 0, 10) != "__script::") { $parent_child = xhprof_build_parent_child_key("__script::$page", $child); } } } if (!isset($raw_data_total[$parent_child])) { foreach ($metrics as $metric) { $raw_data_total[$parent_child][$metric] = ($wt * $info[$metric]); } } else { foreach ($metrics as $metric) { $raw_data_total[$parent_child][$metric] += ($wt * $info[$metric]); } } } } $runs_string = implode(",", $runs); if (isset($wts)) { $wts_string = "in the ratio (" . implode(":", $wts) . ")"; $normalization_count = array_sum($wts); } else { $wts_string = ""; $normalization_count = $run_count; } $run_count = $run_count - count($bad_runs); $data['description'] = "Aggregated Report for $run_count runs: ". "$runs_string $wts_string\n"; $data['raw'] = xhprof_normalize_metrics($raw_data_total, $normalization_count); $data['bad_runs'] = $bad_runs; return $data; } /** * Analyze hierarchical raw data, and compute per-function (flat) * inclusive and exclusive metrics. * * Also, store overall totals in the 2nd argument. * * @param array $raw_data XHProf format raw profiler data. * @param array &$overall_totals OUT argument for returning * overall totals for various * metrics. * @return array Returns a map from function name to its * call count and inclusive & exclusive metrics * (such as wall time, etc.). * * @author Kannan Muthukkaruppan */ function xhprof_compute_flat_info($raw_data, &$overall_totals) { global $display_calls; $metrics = xhprof_get_metrics($raw_data); $overall_totals = array( "ct" => 0, "wt" => 0, "ut" => 0, "st" => 0, "cpu" => 0, "mu" => 0, "pmu" => 0, "samples" => 0 ); // compute inclusive times for each function $symbol_tab = xhprof_compute_inclusive_times($raw_data); /* total metric value is the metric value for "main()" */ foreach ($metrics as $metric) { $overall_totals[$metric] = $symbol_tab["main()"][$metric]; } /* * initialize exclusive (self) metric value to inclusive metric value * to start with. * In the same pass, also add up the total number of function calls. */ foreach ($symbol_tab as $symbol => $info) { foreach ($metrics as $metric) { $symbol_tab[$symbol]["excl_" . $metric] = $symbol_tab[$symbol][$metric]; } if ($display_calls) { /* keep track of total number of calls */ $overall_totals["ct"] += $info["ct"]; } } /* adjust exclusive times by deducting inclusive time of children */ foreach ($raw_data as $parent_child => $info) { list($parent, $child) = xhprof_parse_parent_child($parent_child); if ($parent) { foreach ($metrics as $metric) { // make sure the parent exists hasn't been pruned. if (isset($symbol_tab[$parent])) { $symbol_tab[$parent]["excl_" . $metric] -= $info[$metric]; } } } } return $symbol_tab; } /** * Hierarchical diff: * Compute and return difference of two call graphs: Run2 - Run1. * * @author Kannan */ function xhprof_compute_diff($xhprof_data1, $xhprof_data2) { global $display_calls; // use the second run to decide what metrics we will do the diff on $metrics = xhprof_get_metrics($xhprof_data2); $xhprof_delta = $xhprof_data2; foreach ($xhprof_data1 as $parent_child => $info) { if (!isset($xhprof_delta[$parent_child])) { // this pc combination was not present in run1; // initialize all values to zero. if ($display_calls) { $xhprof_delta[$parent_child] = array("ct" => 0); } else { $xhprof_delta[$parent_child] = array(); } foreach ($metrics as $metric) { $xhprof_delta[$parent_child][$metric] = 0; } } if ($display_calls) { $xhprof_delta[$parent_child]["ct"] -= $info["ct"]; } foreach ($metrics as $metric) { $xhprof_delta[$parent_child][$metric] -= $info[$metric]; } } return $xhprof_delta; } /** * Compute inclusive metrics for function. This code was factored out * of xhprof_compute_flat_info(). * * The raw data contains inclusive metrics of a function for each * unique parent function it is called from. The total inclusive metrics * for a function is therefore the sum of inclusive metrics for the * function across all parents. * * @return array Returns a map of function name to total (across all parents) * inclusive metrics for the function. * * @author Kannan */ function xhprof_compute_inclusive_times($raw_data) { global $display_calls; $metrics = xhprof_get_metrics($raw_data); $symbol_tab = array(); /* * First compute inclusive time for each function and total * call count for each function across all parents the * function is called from. */ foreach ($raw_data as $parent_child => $info) { list($parent, $child) = xhprof_parse_parent_child($parent_child); if ($parent == $child) { /* * XHProf PHP extension should never trigger this situation any more. * Recursion is handled in the XHProf PHP extension by giving nested * calls a unique recursion-depth appended name (for example, foo@1). */ xhprof_error("Error in Raw Data: parent & child are both: $parent"); return; } if (!isset($symbol_tab[$child])) { if ($display_calls) { $symbol_tab[$child] = array("ct" => $info["ct"]); } else { $symbol_tab[$child] = array(); } foreach ($metrics as $metric) { $symbol_tab[$child][$metric] = $info[$metric]; } } else { if ($display_calls) { /* increment call count for this child */ $symbol_tab[$child]["ct"] += $info["ct"]; } /* update inclusive times/metric for this child */ foreach ($metrics as $metric) { $symbol_tab[$child][$metric] += $info[$metric]; } } } return $symbol_tab; } /* * Prunes XHProf raw data: * * Any node whose inclusive walltime accounts for less than $prune_percent * of total walltime is pruned. [It is possible that a child function isn't * pruned, but one or more of its parents get pruned. In such cases, when * viewing the child function's hierarchical information, the cost due to * the pruned parent(s) will be attributed to a special function/symbol * "__pruned__()".] * * @param array $raw_data XHProf raw data to be pruned & validated. * @param double $prune_percent Any edges that account for less than * $prune_percent of time will be pruned * from the raw data. * * @return array Returns the pruned raw data. * * @author Kannan */ function xhprof_prune_run($raw_data, $prune_percent) { $main_info = $raw_data["main()"]; if (empty($main_info)) { xhprof_error("XHProf: main() missing in raw data"); return false; } // raw data should contain either wall time or samples information... if (isset($main_info["wt"])) { $prune_metric = "wt"; } else if (isset($main_info["samples"])) { $prune_metric = "samples"; } else { xhprof_error("XHProf: for main() we must have either wt " ."or samples attribute set"); return false; } // determine the metrics present in the raw data.. $metrics = array(); foreach ($main_info as $metric => $val) { if (isset($val)) { $metrics[] = $metric; } } $prune_threshold = (($main_info[$prune_metric] * $prune_percent) / 100.0); - init_metrics($raw_data, null, null, false); +// init_metrics($raw_data, null, null, false); $flat_info = xhprof_compute_inclusive_times($raw_data); foreach ($raw_data as $parent_child => $info) { list($parent, $child) = xhprof_parse_parent_child($parent_child); // is this child's overall total from all parents less than threshold? if ($flat_info[$child][$prune_metric] < $prune_threshold) { unset($raw_data[$parent_child]); // prune the edge } else if ($parent && ($parent != "__pruned__()") && ($flat_info[$parent][$prune_metric] < $prune_threshold)) { // Parent's overall inclusive metric is less than a threshold. // All edges to the parent node will get nuked, and this child will // be a dangling child. // So instead change its parent to be a special function __pruned__(). $pruned_edge = xhprof_build_parent_child_key("__pruned__()", $child); if (isset($raw_data[$pruned_edge])) { foreach ($metrics as $metric) { $raw_data[$pruned_edge][$metric]+=$raw_data[$parent_child][$metric]; } } else { $raw_data[$pruned_edge] = $raw_data[$parent_child]; } unset($raw_data[$parent_child]); // prune the edge } } return $raw_data; } /** * Set one key in an array and return the array * * @author Kannan */ function xhprof_array_set($arr, $k, $v) { $arr[$k] = $v; return $arr; } /** * Removes/unsets one key in an array and return the array * * @author Kannan */ function xhprof_array_unset($arr, $k) { unset($arr[$k]); return $arr; } /** * Type definitions for URL params */ define('XHPROF_STRING_PARAM', 1); define('XHPROF_UINT_PARAM', 2); define('XHPROF_FLOAT_PARAM', 3); define('XHPROF_BOOL_PARAM', 4); /** * Internal helper function used by various * xhprof_get_param* flavors for various * types of parameters. * * @param string name of the URL query string param * * @author Kannan */ function xhprof_get_param_helper($param) { $val = null; if (isset($_GET[$param])) $val = $_GET[$param]; else if (isset($_POST[$param])) { $val = $_POST[$param]; } return $val; } /** * Extracts value for string param $param from query * string. If param is not specified, return the * $default value. * * @author Kannan */ function xhprof_get_string_param($param, $default = '') { $val = xhprof_get_param_helper($param); if ($val === null) return $default; return $val; } /** * Extracts value for unsigned integer param $param from * query string. If param is not specified, return the * $default value. * * If value is not a valid unsigned integer, logs error * and returns null. * * @author Kannan */ function xhprof_get_uint_param($param, $default = 0) { $val = xhprof_get_param_helper($param); if ($val === null) $val = $default; // trim leading/trailing whitespace $val = trim($val); // if it only contains digits, then ok.. if (ctype_digit($val)) { return $val; } xhprof_error("$param is $val. It must be an unsigned integer."); return null; } /** * Extracts value for a float param $param from * query string. If param is not specified, return * the $default value. * * If value is not a valid unsigned integer, logs error * and returns null. * * @author Kannan */ function xhprof_get_float_param($param, $default = 0) { $val = xhprof_get_param_helper($param); if ($val === null) $val = $default; // trim leading/trailing whitespace $val = trim($val); // TBD: confirm the value is indeed a float. if (true) // for now.. return (float)$val; xhprof_error("$param is $val. It must be a float."); return null; } /** * Extracts value for a boolean param $param from * query string. If param is not specified, return * the $default value. * * If value is not a valid unsigned integer, logs error * and returns null. * * @author Kannan */ function xhprof_get_bool_param($param, $default = false) { $val = xhprof_get_param_helper($param); if ($val === null) $val = $default; // trim leading/trailing whitespace $val = trim($val); switch (strtolower($val)) { case '0': case '1': $val = (bool)$val; break; case 'true': case 'on': case 'yes': $val = true; break; case 'false': case 'off': case 'no': $val = false; break; default: xhprof_error("$param is $val. It must be a valid boolean string."); return null; } return $val; } /** * Initialize params from URL query string. The function * creates globals variables for each of the params * and if the URL query string doesn't specify a particular * param initializes them with the corresponding default * value specified in the input. * * @params array $params An array whose keys are the names * of URL params who value needs to * be retrieved from the URL query * string. PHP globals are created * with these names. The value is * itself an array with 2-elems (the * param type, and its default value). * If a param is not specified in the * query string the default value is * used. * @author Kannan */ function xhprof_param_init($params) { /* Create variables specified in $params keys, init defaults */ foreach ($params as $k => $v) { switch ($v[0]) { case XHPROF_STRING_PARAM: $p = xhprof_get_string_param($k, $v[1]); break; case XHPROF_UINT_PARAM: $p = xhprof_get_uint_param($k, $v[1]); break; case XHPROF_FLOAT_PARAM: $p = xhprof_get_float_param($k, $v[1]); break; case XHPROF_BOOL_PARAM: $p = xhprof_get_bool_param($k, $v[1]); break; default: xhprof_error("Invalid param type passed to xhprof_param_init: " . $v[0]); exit(); } // create a global variable using the parameter name. $GLOBALS[$k] = $p; } } /** * Given a partial query string $q return matching function names in * specified XHProf run. This is used for the type ahead function * selector. * * @author Kannan */ function xhprof_get_matching_functions($q, $xhprof_data) { $matches = array(); foreach ($xhprof_data as $parent_child => $info) { list($parent, $child) = xhprof_parse_parent_child($parent_child); if (stripos($parent, $q) !== false) { $matches[$parent] = 1; } if (stripos($child, $q) !== false) { $matches[$child] = 1; } } $res = array_keys($matches); // sort it so the answers are in some reliable order... asort($res); return ($res); } diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php index 0ef8fd2786..e42fd40b41 100644 --- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php @@ -1,293 +1,288 @@ getResourceURIMapRules() + array( '/' => array( '$' => 'PhabricatorDirectoryMainController', ), '/directory/' => array( 'item/$' => 'PhabricatorDirectoryItemListController', 'item/edit/(?:(?P\d+)/)?$' => 'PhabricatorDirectoryItemEditController', 'item/delete/(?P\d+)/' => 'PhabricatorDirectoryItemDeleteController', 'category/$' => 'PhabricatorDirectoryCategoryListController', 'category/edit/(?:(?P\d+)/)?$' => 'PhabricatorDirectoryCategoryEditController', 'category/delete/(?P\d+)/' => 'PhabricatorDirectoryCategoryDeleteController', ), '/file/' => array( '$' => 'PhabricatorFileListController', 'upload/$' => 'PhabricatorFileUploadController', '(?Pinfo)/(?P[^/]+)/' => 'PhabricatorFileViewController', '(?Pview)/(?P[^/]+)/' => 'PhabricatorFileViewController', '(?Pdownload)/(?P[^/]+)/' => 'PhabricatorFileViewController', ), '/phid/' => array( '$' => 'PhabricatorPHIDLookupController', 'list/$' => 'PhabricatorPHIDListController', 'type/$' => 'PhabricatorPHIDTypeListController', 'type/edit/(?:(?P\d+)/)?$' => 'PhabricatorPHIDTypeEditController', 'new/$' => 'PhabricatorPHIDAllocateController', ), '/people/' => array( '$' => 'PhabricatorPeopleListController', 'edit/(?:(?P\w+)/)?$' => 'PhabricatorPeopleEditController', ), '/p/(?P\w+)/$' => 'PhabricatorPeopleProfileController', '/profile/' => array( 'edit/$' => 'PhabricatorPeopleProfileEditController', ), '/conduit/' => array( '$' => 'PhabricatorConduitConsoleController', 'method/(?P[^/]+)$' => 'PhabricatorConduitConsoleController', 'log/$' => 'PhabricatorConduitLogController', ), '/api/(?P[^/]+)$' => 'PhabricatorConduitAPIController', '/D(?P\d+)' => 'DifferentialRevisionViewController', '/differential/' => array( '$' => 'DifferentialRevisionListController', 'filter/(?P\w+)/$' => 'DifferentialRevisionListController', 'diff/' => array( '(?P\d+)/$' => 'DifferentialDiffViewController', 'create/$' => 'DifferentialDiffCreateController', ), 'changeset/$' => 'DifferentialChangesetViewController', 'revision/edit/(?:(?P\d+)/)?$' => 'DifferentialRevisionEditController', 'comment/' => array( 'preview/(?P\d+)/$' => 'DifferentialCommentPreviewController', 'save/$' => 'DifferentialCommentSaveController', 'inline/' => array( 'preview/(?P\d+)/$' => 'DifferentialInlineCommentPreviewController', 'edit/(?P\d+)/$' => 'DifferentialInlineCommentEditController', ), ), 'attach/(?P\d+)/(?P\w+)/$' => 'DifferentialAttachController', 'subscribe/(?Padd|rem)/(?P\d+)/$' => 'DifferentialSubscribeController', ), '/typeahead/' => array( 'common/(?P\w+)/$' => 'PhabricatorTypeaheadCommonDatasourceController', ), '/mail/' => array( '$' => 'PhabricatorMetaMTAListController', 'send/$' => 'PhabricatorMetaMTASendController', 'view/(?P\d+)/$' => 'PhabricatorMetaMTAViewController', 'lists/$' => 'PhabricatorMetaMTAMailingListsController', 'lists/edit/(?:(?P\d+)/)?$' => 'PhabricatorMetaMTAMailingListEditController', ), '/login/' => array( '$' => 'PhabricatorLoginController', 'email/$' => 'PhabricatorEmailLoginController', 'etoken/(?P\w+)/$' => 'PhabricatorEmailTokenController', ), '/logout/$' => 'PhabricatorLogoutController', '/oauth/' => array( '(?Pgithub|facebook)/' => array( 'login/$' => 'PhabricatorOAuthLoginController', 'diagnose/$' => 'PhabricatorOAuthDiagnosticsController', 'unlink/$' => 'PhabricatorOAuthUnlinkController', ), ), '/xhprof/' => array( 'profile/(?P[^/]+)/$' => 'PhabricatorXHProfProfileController', ), '/~/' => 'DarkConsoleController', '/settings/' => array( '(?:page/(?P[^/]+)/)?$' => 'PhabricatorUserSettingsController', ), '/maniphest/' => array( '$' => 'ManiphestTaskListController', 'view/(?P\w+)/$' => 'ManiphestTaskListController', 'task/' => array( 'create/$' => 'ManiphestTaskEditController', 'edit/(?P\d+)/$' => 'ManiphestTaskEditController', ), 'transaction/' => array( 'save/' => 'ManiphestTransactionSaveController', ), 'select/search/$' => 'ManiphestTaskSelectorSearchController', ), '/T(?P\d+)$' => 'ManiphestTaskDetailController', '/github-post-receive/(?P\d+)/(?P[^/]+)/$' => 'PhabricatorRepositoryGitHubPostReceiveController', '/repository/' => array( '$' => 'PhabricatorRepositoryListController', 'create/$' => 'PhabricatorRepositoryCreateController', 'edit/(?P\d+)/$' => 'PhabricatorRepositoryEditController', 'delete/(?P\d+)/$' => 'PhabricatorRepositoryDeleteController', ), '/search/' => array( '$' => 'PhabricatorSearchController', '(?P\d+)/$' => 'PhabricatorSearchController', ), '/project/' => array( '$' => 'PhabricatorProjectListController', 'edit/(?:(?P\d+)/)?$' => 'PhabricatorProjectEditController', 'view/(?P\d+)/$' => 'PhabricatorProjectProfileController', 'affiliation/(?P\d+)/$' => 'PhabricatorProjectAffiliationEditController', ), ); } protected function getResourceURIMapRules() { return array( '/res/' => array( '(?Ppkg/)?(?P[a-f0-9]{8})/(?P.+\.(?:css|js))$' => 'CelerityResourceController', ), ); } public function buildRequest() { $request = new AphrontRequest($this->getHost(), $this->getPath()); $request->setRequestData($_GET + $_POST); $request->setApplicationConfiguration($this); return $request; } public function handleException(Exception $ex) { $class = phutil_escape_html(get_class($ex)); $message = phutil_escape_html($ex->getMessage()); $content = '
'. '

Unhandled Exception "'.$class.'": '.$message.'

'. ''.phutil_escape_html((string)$ex).''. '
'; - if ($this->getRequest()->isAjax()) { - $dialog = new AphrontDialogView(); - $dialog - ->setTitle('Exception!') - ->setClass('aphront-exception-dialog') - ->setUser($this->getRequest()->getUser()) - ->appendChild($content) - ->addCancelButton('/'); - - $response = new AphrontDialogResponse(); - $response->setDialog($dialog); - - return $response; + $user = $this->getRequest()->getUser(); + if (!$user) { + // If we hit an exception very early, we won't have a user. + $user = new PhabricatorUser(); } - $view = new PhabricatorStandardPageView(); - $view->setRequest($this->getRequest()); - $view->appendChild($content); + $dialog = new AphrontDialogView(); + $dialog + ->setTitle('Exception!') + ->setClass('aphront-exception-dialog') + ->setUser($user) + ->appendChild($content) + ->addCancelButton('/'); - $response = new AphrontWebpageResponse(); - $response->setContent($view->render()); + $response = new AphrontDialogResponse(); + $response->setDialog($dialog); return $response; } public function willSendResponse(AphrontResponse $response) { $request = $this->getRequest(); if ($response instanceof AphrontDialogResponse) { if (!$request->isAjax()) { $view = new PhabricatorStandardPageView(); $view->setRequest($request); $view->appendChild( '
'. $response->buildResponseString(). '
'); $response = new AphrontWebpageResponse(); $response->setContent($view->render()); return $response; } else { return id(new AphrontAjaxResponse()) ->setContent(array( 'dialog' => $response->buildResponseString(), )); } } else if ($response instanceof AphrontRedirectResponse) { if ($request->isAjax()) { return id(new AphrontAjaxResponse()) ->setContent( array( 'redirect' => $response->getURI(), )); } } else if ($response instanceof Aphront404Response) { $failure = new AphrontRequestFailureView(); $failure->setHeader('404 Not Found'); $failure->appendChild( '

The page you requested was not found.

'); $view = new PhabricatorStandardPageView(); $view->setTitle('404 Not Found'); $view->setRequest($this->getRequest()); $view->appendChild($failure); $response = new AphrontWebpageResponse(); $response->setContent($view->render()); $response->setHTTPResponseCode(404); return $response; } return $response; } public function build404Controller() { return array(new Phabricator404Controller($this->getRequest()), array()); } } diff --git a/src/aphront/default/configuration/__init__.php b/src/aphront/default/configuration/__init__.php index 62bdc33fb1..8fd005d1e7 100644 --- a/src/aphront/default/configuration/__init__.php +++ b/src/aphront/default/configuration/__init__.php @@ -1,23 +1,24 @@ provider = PhabricatorOAuthProvider::newProvider($data['provider']); } public function processRequest() { $current_user = $this->getRequest()->getUser(); $provider = $this->provider; if (!$provider->isProviderEnabled()) { return new Aphront400Response(); } $provider_name = $provider->getProviderName(); $provider_key = $provider->getProviderKey(); $request = $this->getRequest(); if ($request->getStr('error')) { $error_view = id(new PhabricatorOAuthFailureView()) ->setRequest($request); return $this->buildErrorResponse($error_view); } $token = $request->getStr('token'); if (!$token) { $client_id = $provider->getClientID(); $client_secret = $provider->getClientSecret(); $redirect_uri = $provider->getRedirectURI(); $auth_uri = $provider->getTokenURI(); $code = $request->getStr('code'); $query_data = array( 'client_id' => $client_id, 'client_secret' => $client_secret, 'redirect_uri' => $redirect_uri, 'code' => $code, ); + $post_data = http_build_query($query_data); + $post_length = strlen($post_data); + $stream_context = stream_context_create( array( 'http' => array( 'method' => 'POST', - 'header' => 'Content-type: application/x-www-form-urlencoded', - 'content' => http_build_query($query_data), + 'header' => + "Content-Type: application/x-www-form-urlencoded\r\n". + "Content-Length: {$post_length}\r\n", + 'content' => $post_data, ), )); $stream = fopen($auth_uri, 'r', false, $stream_context); - $meta = stream_get_meta_data($stream); - $response = stream_get_contents($stream); + $response = false; + $meta = null; + if ($stream) { + $meta = stream_get_meta_data($stream); + $response = stream_get_contents($stream); + fclose($stream); + } - fclose($stream); if ($response === false) { return $this->buildErrorResponse(new PhabricatorOAuthFailureView()); } $data = array(); parse_str($response, $data); $token = idx($data, 'access_token'); if (!$token) { return $this->buildErrorResponse(new PhabricatorOAuthFailureView()); } if (idx($data, 'expires')) { $this->tokenExpires = time() + $data['expires']; } } else { $this->tokenExpires = $request->getInt('expires'); } $userinfo_uri = new PhutilURI($provider->getUserInfoURI()); $userinfo_uri->setQueryParams( array( 'access_token' => $token, )); $user_json = @file_get_contents($userinfo_uri); $user_data = json_decode($user_json, true); $this->accessToken = $token; switch ($provider->getProviderKey()) { case PhabricatorOAuthProvider::PROVIDER_GITHUB: $user_data = $user_data['user']; break; } $this->userData = $user_data; $user_id = $this->retrieveUserID(); $known_oauth = id(new PhabricatorUserOAuthInfo())->loadOneWhere( 'oauthProvider = %s and oauthUID = %s', $provider->getProviderKey(), $user_id); if ($current_user->getPHID()) { - if ($known_oauth) { if ($known_oauth->getUserID() != $current_user->getID()) { $dialog = new AphrontDialogView(); $dialog->setUser($current_user); $dialog->setTitle('Already Linked to Another Account'); $dialog->appendChild( '

The '.$provider_name.' account you just authorized '. 'is already linked to another Phabricator account. Before you can '. 'associate your '.$provider_name.' account with this Phabriactor '. 'account, you must unlink it from the Phabricator account it is '. 'currently linked to.

'); $dialog->addCancelButton('/settings/page/'.$provider_key.'/'); return id(new AphrontDialogResponse())->setDialog($dialog); } else { return id(new AphrontRedirectResponse()) ->setURI('/settings/page/'.$provider_key.'/'); } } if (!$request->isDialogFormPost()) { $dialog = new AphrontDialogView(); $dialog->setUser($current_user); $dialog->setTitle('Link '.$provider_name.' Account'); $dialog->appendChild( '

Link your '.$provider_name.' account to your Phabricator '. 'account?

'); $dialog->addHiddenInput('token', $token); $dialog->addHiddenInput('expires', $this->tokenExpires); $dialog->addSubmitButton('Link Accounts'); $dialog->addCancelButton('/settings/page/'.$provider_key.'/'); return id(new AphrontDialogResponse())->setDialog($dialog); } $oauth_info = new PhabricatorUserOAuthInfo(); $oauth_info->setUserID($current_user->getID()); $this->configureOAuthInfo($oauth_info); $oauth_info->save(); return id(new AphrontRedirectResponse()) ->setURI('/settings/page/'.$provider_key.'/'); } // Login with known auth. if ($known_oauth) { $known_user = id(new PhabricatorUser())->load($known_oauth->getUserID()); $session_key = $known_user->establishSession('web'); $this->configureOAuthInfo($known_oauth); $known_oauth->save(); $request->setCookie('phusr', $known_user->getUsername()); $request->setCookie('phsid', $session_key); return id(new AphrontRedirectResponse()) ->setURI('/'); } // Merge accounts based on shared email. TODO: should probably get rid of // this. $oauth_email = $this->retrieveUserEmail(); if ($oauth_email) { $known_email = id(new PhabricatorUser()) ->loadOneWhere('email = %s', $oauth_email); if ($known_email) { $dialog = new AphrontDialogView(); $dialog->setUser($current_user); $dialog->setTitle('Already Linked to Another Account'); $dialog->appendChild( '

The '.$provider_name.' account you just authorized has an '. 'email address which is already in use by another Phabricator '. 'account. To link the accounts, log in to your Phabricator '. 'account and then go to Settings.

'); $dialog->addCancelButton('/login/'); return id(new AphrontDialogResponse())->setDialog($dialog); } } $errors = array(); $e_username = true; $e_email = true; $e_realname = true; $user = new PhabricatorUser(); $suggestion = $this->retrieveUsernameSuggestion(); $user->setUsername($suggestion); $oauth_realname = $this->retreiveRealNameSuggestion(); if ($request->isFormPost()) { $user->setUsername($request->getStr('username')); $username = $user->getUsername(); $matches = null; if (!strlen($user->getUsername())) { $e_username = 'Required'; $errors[] = 'Username is required.'; } else if (!preg_match('/^[a-zA-Z0-9]+$/', $username, $matches)) { $e_username = 'Invalid'; $errors[] = 'Username may only contain letters and numbers.'; } else { $e_username = null; } if ($oauth_email) { $user->setEmail($oauth_email); } else { $user->setEmail($request->getStr('email')); if (!strlen($user->getEmail())) { $e_email = 'Required'; $errors[] = 'Email is required.'; } else { $e_email = null; } } if ($oauth_realname) { $user->setRealName($oauth_realname); } else { $user->setRealName($request->getStr('realname')); if (!strlen($user->getStr('realname'))) { $e_realname = 'Required'; $errors[] = 'Real name is required.'; } else { $e_realname = null; } } if (!$errors) { $image = $this->retreiveProfileImageSuggestion(); if ($image) { $file = PhabricatorFile::newFromFileData( $image, array( 'name' => $provider->getProviderKey().'-profile.jpg' )); $user->setProfileImagePHID($file->getPHID()); } try { $user->save(); $oauth_info = new PhabricatorUserOAuthInfo(); $oauth_info->setUserID($user->getID()); $this->configureOAuthInfo($oauth_info); $oauth_info->save(); $session_key = $user->establishSession('web'); $request->setCookie('phusr', $user->getUsername()); $request->setCookie('phsid', $session_key); return id(new AphrontRedirectResponse())->setURI('/'); } catch (AphrontQueryDuplicateKeyException $exception) { - $key = $exception->getDuplicateKey(); - if ($key == 'userName') { + + $same_username = id(new PhabricatorUser())->loadOneWhere( + 'userName = %s', + $user->getUserName()); + + $same_email = id(new PhabricatorUser())->loadOneWhere( + 'email = %s', + $user->getEmail()); + + if ($same_username) { $e_username = 'Duplicate'; - $errors[] = 'That username is not unique.'; - } else if ($key == 'email') { + $errors[] = 'That username or email is not unique.'; + } else if ($same_email) { $e_email = 'Duplicate'; $errors[] = 'That email is not unique.'; } else { throw $exception; } } } } $error_view = null; if ($errors) { $error_view = new AphrontErrorView(); $error_view->setTitle('Registration Failed'); $error_view->setErrors($errors); } $form = new AphrontFormView(); $form ->addHiddenInput('token', $token) ->addHiddenInput('expires', $this->tokenExpires) ->setUser($request->getUser()) ->setAction($provider->getRedirectURI()) ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Username') ->setName('username') ->setValue($user->getUsername()) ->setError($e_username)); if (!$oauth_email) { $form->appendChild( id(new AphrontFormTextControl()) ->setLabel('Email') ->setName('email') ->setValue($request->getStr('email')) ->setError($e_email)); } if (!$oauth_realname) { $form->appendChild( id(new AphrontFormTextControl()) ->setLabel('Real Name') ->setName('realname') ->setValue($request->getStr('realname')) ->setError($e_realname)); } $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue('Create Account')); $panel = new AphrontPanelView(); $panel->setHeader('Create New Account'); $panel->setWidth(AphrontPanelView::WIDTH_FORM); $panel->appendChild($form); return $this->buildStandardPageResponse( array( $error_view, $panel, ), array( 'title' => 'Create New Account', )); } private function buildErrorResponse(PhabricatorOAuthFailureView $view) { $provider = $this->provider; $provider_name = $provider->getProviderName(); $view->setOAuthProvider($provider); return $this->buildStandardPageResponse( $view, array( 'title' => $provider_name.' Auth Failed', )); } private function retrieveUserID() { return $this->userData['id']; } private function retrieveUserEmail() { return $this->userData['email']; } private function retrieveUsernameSuggestion() { switch ($this->provider->getProviderKey()) { case PhabricatorOAuthProvider::PROVIDER_FACEBOOK: $matches = null; $link = $this->userData['link']; if (preg_match('@/([a-zA-Z0-9]+)$@', $link, $matches)) { return $matches[1]; } break; case PhabricatorOAuthProvider::PROVIDER_GITHUB: return $this->userData['login']; } return null; } private function retreiveProfileImageSuggestion() { switch ($this->provider->getProviderKey()) { case PhabricatorOAuthProvider::PROVIDER_FACEBOOK: $uri = 'https://graph.facebook.com/me/picture?access_token='; return @file_get_contents($uri.$this->accessToken); case PhabricatorOAuthProvider::PROVIDER_GITHUB: $id = $this->userData['gravatar_id']; if ($id) { $uri = 'http://www.gravatar.com/avatar/'.$id.'?s=50'; return @file_get_contents($uri); } } return null; } private function retrieveAccountURI() { switch ($this->provider->getProviderKey()) { case PhabricatorOAuthProvider::PROVIDER_FACEBOOK: return $this->userData['link']; case PhabricatorOAuthProvider::PROVIDER_GITHUB: $username = $this->retrieveUsernameSuggestion(); if ($username) { return 'https://github.com/'.$username; } return null; } return null; } private function retreiveRealNameSuggestion() { return $this->userData['name']; } private function configureOAuthInfo(PhabricatorUserOAuthInfo $oauth_info) { $provider = $this->provider; $oauth_info->setOAuthProvider($provider->getProviderKey()); $oauth_info->setOAuthUID($this->retrieveUserID()); $oauth_info->setAccountURI($this->retrieveAccountURI()); $oauth_info->setAccountName($this->retrieveUserNameSuggestion()); $oauth_info->setToken($this->accessToken); $oauth_info->setTokenStatus(PhabricatorUserOAuthInfo::TOKEN_STATUS_GOOD); // If we have out-of-date expiration info, just clear it out. Then replace // it with good info if the provider gave it to us. $expires = $oauth_info->getTokenExpires(); if ($expires <= time()) { $expires = null; } if ($this->tokenExpires) { $expires = $this->tokenExpires; } $oauth_info->setTokenExpires($expires); } } diff --git a/src/applications/conduit/method/differential/markcommitted/ConduitAPI_differential_markcommitted_Method.php b/src/applications/conduit/method/differential/markcommitted/ConduitAPI_differential_markcommitted_Method.php index ec8858c28a..ccc959a173 100644 --- a/src/applications/conduit/method/differential/markcommitted/ConduitAPI_differential_markcommitted_Method.php +++ b/src/applications/conduit/method/differential/markcommitted/ConduitAPI_differential_markcommitted_Method.php @@ -1,74 +1,73 @@ 'required revision_id', ); } public function defineReturnType() { return 'void'; } public function defineErrorTypes() { return array( 'ERR_NOT_FOUND' => 'Revision was not found.', ); } protected function execute(ConduitAPIRequest $request) { $id = $request->getValue('revision_id'); $revision = id(new DifferentialRevision())->load($id); if (!$revision) { throw new ConduitException('ERR_NOT_FOUND'); } if ($revision->getStatus() == DifferentialRevisionStatus::COMMITTED) { // This can occur if someone runs 'mark-committed' and hits a race, or // they have a remote hook installed but don't have the // 'remote_hook_installed' flag set, or similar. In any case, just treat // it as a no-op rather than adding another "X committed this revision" // message to the revision comments. return; } $revision->loadRelationships(); $editor = new DifferentialCommentEditor( $revision, $revision->getAuthorPHID(), - DifferentialAction::ACTION_COMMIT, - $inline_comments = array()); + DifferentialAction::ACTION_COMMIT); $editor->save(); $revision->setStatus(DifferentialRevisionStatus::COMMITTED); $revision->setDateCommitted(time()); $revision->save(); return; } } diff --git a/src/applications/differential/data/revisionlist/DifferentialRevisionListData.php b/src/applications/differential/data/revisionlist/DifferentialRevisionListData.php index d4dbb6eb31..c6c8f80540 100755 --- a/src/applications/differential/data/revisionlist/DifferentialRevisionListData.php +++ b/src/applications/differential/data/revisionlist/DifferentialRevisionListData.php @@ -1,292 +1,295 @@ filter = $filter; $this->ids = $ids; } public function getRevisions() { return $this->revisions; } public function setOrder($order) { $this->order = $order; return $this; } public function loadRevisions() { switch ($this->filter) { case self::QUERY_CC: $this->revisions = $this->loadAllOpenWithCCs($this->ids); break; case self::QUERY_ALL_OPEN: $this->revisions = $this->loadAllOpen(); break; case self::QUERY_OPEN_OWNED: $this->revisions = $this->loadAllWhere( 'revision.status in (%Ld) AND revision.authorPHID in (%Ls)', $this->getOpenStatuses(), $this->ids); break; case self::QUERY_COMMITTABLE: $this->revisions = $this->loadAllWhere( 'revision.status in (%Ld) AND revision.authorPHID in (%Ls)', array( DifferentialRevisionStatus::ACCEPTED, ), $this->ids); break; case self::QUERY_REVISION_IDS: $this->revisions = $this->loadAllWhere( 'id in (%Ld)', $this->ids); break; case self::QUERY_OPEN_REVIEWER: $this->revisions = $this->loadAllWhereJoinReview( 'revision.status in (%Ld) AND relationship.objectPHID in (%Ls)', $this->getOpenStatuses(), $this->ids); break; case self::QUERY_OWNED: $this->revisions = $this->loadAllWhere( 'revision.authorPHID in (%Ls)', $this->ids); break; case self::QUERY_OWNED_OR_REVIEWER: $this->revisions = $this->loadAllWhereJoinReview( 'revision.authorPHID in (%Ls) OR relationship.objectPHID in (%Ls)', $this->ids, $this->ids); break; case self::QUERY_NEED_ACTION_FROM_SELF: $rev = new DifferentialRevision(); $data = queryfx_all( $rev->establishConnection('r'), 'SELECT revision.* FROM %T revision WHERE revision.authorPHID in (%Ls) AND revision.status in (%Ld) UNION ALL SELECT revision.* FROM %T revision JOIN %T relationship ON relationship.revisionID = revision.id AND relationship.relation = %s WHERE relationship.objectPHID IN (%Ls) AND revision.status in (%Ld) %Q', $rev->getTableName(), $this->ids, array( DifferentialRevisionStatus::NEEDS_REVISION, DifferentialRevisionStatus::ACCEPTED, ), $rev->getTableName(), DifferentialRevision::RELATIONSHIP_TABLE, DifferentialRevision::RELATION_REVIEWER, $this->ids, array( DifferentialRevisionStatus::NEEDS_REVIEW, ), $this->getOrderClause()); $data = ipull($data, null, 'id'); $this->revisions = $rev->loadAllFromArray($data); break; case self::QUERY_NEED_ACTION_FROM_OTHERS: $rev = new DifferentialRevision(); $data = queryfx_all( $rev->establishConnection('r'), 'SELECT revision.* FROM %T revision WHERE revision.authorPHID in (%Ls) AND revision.status IN (%Ld) UNION ALL SELECT revision.* FROM %T revision JOIN %T relationship ON relationship.revisionID = revision.id AND relationship.relation = %s WHERE relationship.objectPHID IN (%Ls) AND revision.status in (%Ld) %Q', $rev->getTableName(), $this->ids, array( DifferentialRevisionStatus::NEEDS_REVIEW, ), $rev->getTableName(), DifferentialRevision::RELATIONSHIP_TABLE, DifferentialRevision::RELATION_REVIEWER, $this->ids, array( DifferentialRevisionStatus::NEEDS_REVISION, DifferentialRevisionStatus::ACCEPTED, ), $this->getOrderClause()); $data = ipull($data, null, 'id'); $this->revisions = $rev->loadAllFromArray($data); break; - case self::QUERY_BY_PHID: + case self::QUERY_PHIDS: $this->revisions = $this->loadAllWhere( 'revision.phid in (%Ls)', $this->ids); break; } return $this->revisions; } private function getOpenStatuses() { return array( DifferentialRevisionStatus::NEEDS_REVIEW, DifferentialRevisionStatus::NEEDS_REVISION, DifferentialRevisionStatus::ACCEPTED, ); } private function loadAllOpen() { return $this->loadAllWhere('status in (%Ld)', $this->getOpenStatuses()); } private function loadAllWhereJoinReview($pattern) { $reviewer = DifferentialRevision::RELATION_REVIEWER; $argv = func_get_args(); $rev = new DifferentialRevision(); $pattern = array_shift($argv); $pattern = 'SELECT revision.* FROM %T revision LEFT JOIN %T relationship ON revision.id = relationship.revisionID AND relationship.relation = %s WHERE '.$pattern.' GROUP BY revision.id '.$this->getOrderClause(); array_unshift( $argv, $rev->getTableName(), DifferentialRevision::RELATIONSHIP_TABLE, DifferentialRevision::RELATION_REVIEWER); $data = vqueryfx_all( $rev->establishConnection('r'), $pattern, $argv); return $rev->loadAllFromArray($data); } private function loadAllWhere($pattern) { $rev = new DifferentialRevision(); $argv = func_get_args(); array_shift($argv); array_unshift($argv, $rev->getTableName()); $data = vqueryfx_all( $rev->establishConnection('r'), 'SELECT * FROM %T revision WHERE '.$pattern.' '.$this->getOrderClause(), $argv); return $rev->loadAllFromArray($data); } private function loadAllOpenWithCCs(array $ccphids) { + $rev = new DifferentialRevision(); + $revision = new DifferentialRevision(); $data = queryfx_all( + $rev->establishConnection('r'), 'SELECT revision.* FROM %T revision JOIN %T relationship ON relationship.revisionID = revision.id AND relationship.relation = %s AND relationship.objectPHID in (%Ls) WHERE revision.status in (%Ld) %Q', $revision->getTableName(), DifferentialRevision::RELATIONSHIP_TABLE, DifferentialRevision::RELATION_SUBSCRIBED, $ccphids, $this->getOpenStatuses(), $this->getOrderClause()); return $revision->loadAllFromArray($data); } private function getOrderClause() { $reverse = false; $order = $this->order; if (strlen($order) && $order[0] == '-') { $reverse = true; $order = substr($order, 1); } $asc = $reverse ? 'DESC' : 'ASC'; switch ($order) { case 'ID': $clause = 'id'; break; case 'Revision': $clause = 'name'; break; case 'Status': $clause = 'status'; break; case 'Lines': $clause = 'lineCount'; break; case 'Created': $clause = 'dateCreated'; $asc = $reverse ? 'ASC' : 'DESC'; break; case '': case 'Modified': $clause = 'dateModified'; $asc = $reverse ? 'ASC' : 'DESC'; break; default: throw new Exception("Invalid order '{$order}'."); } return "ORDER BY {$clause} {$asc}"; } } diff --git a/src/applications/differential/parser/changeset/DifferentialChangesetParser.php b/src/applications/differential/parser/changeset/DifferentialChangesetParser.php index 4e693165d1..cd684cfef8 100644 --- a/src/applications/differential/parser/changeset/DifferentialChangesetParser.php +++ b/src/applications/differential/parser/changeset/DifferentialChangesetParser.php @@ -1,1342 +1,1342 @@ changeset = $changeset; $this->setFilename($changeset->getFilename()); $this->setChangesetID($changeset->getID()); return $this; } public function setWhitespaceMode($whitespace_mode) { $this->whitespaceMode = $whitespace_mode; return $this; } public function setOldChangesetID($old_changeset_id) { $this->oldChangesetID = $old_changeset_id; return $this; } public function setChangesetID($changeset_id) { $this->changesetID = $changeset_id; return $this; } public function getChangesetID() { return $this->changesetID; } public function setFilename($filename) { $this->filename = $filename; if (strpos($filename, '.', 1) !== false) { $parts = explode('.', $filename); $this->filetype = end($parts); } } public function setHandles(array $handles) { $this->handles = $handles; return $this; } public function setMarkupEngine(PhutilMarkupEngine $engine) { $this->markupEngine = $engine; return $this; } public function setUser(PhabricatorUser $user) { $this->user = $user; return $this; } public function parseHunk(DifferentialHunk $hunk) { $this->parsedHunk = true; $lines = $hunk->getChanges(); // Flatten UTF-8 into "\0". We don't support UTF-8 because the diffing // algorithms are byte-oriented (not character oriented) and everyone seems // to be in agreement that it's fairly reasonable not to allow UTF-8 in // source files. These bytes will later be replaced with a "?" glyph, but // in the meantime we replace them with "\0" since Pygments is happy to // deal with that. $lines = preg_replace('/[\x80-\xFF]/', "\0", $lines); $lines = str_replace( array("\t", "\r\n", "\r"), array(' ', "\n", "\n"), $lines); $lines = explode("\n", $lines); $types = array(); foreach ($lines as $line_index => $line) { $lines[$line_index] = $line; if (isset($line[0])) { $char = $line[0]; if ($char == ' ') { $types[$line_index] = null; } else if ($char == '\\' && $line_index > 0) { $types[$line_index] = $types[$line_index - 1]; } else { $types[$line_index] = $char; } } else { $types[$line_index] = null; } } $old_line = $hunk->getOldOffset(); $new_line = $hunk->getNewOffset(); $num_lines = count($lines); if ($old_line > 1) { $this->missingOld[$old_line] = true; } else if ($new_line > 1) { $this->missingNew[$new_line] = true; } for ($cursor = 0; $cursor < $num_lines; $cursor++) { $type = $types[$cursor]; $data = array( 'type' => $type, 'text' => (string)substr($lines[$cursor], 1), 'line' => $new_line, ); switch ($type) { case '+': $this->new[] = $data; ++$new_line; break; case '-': $data['line'] = $old_line; $this->old[] = $data; ++$old_line; break; default: $this->new[] = $data; $data['line'] = $old_line; $this->old[] = $data; ++$new_line; ++$old_line; break; } } } public function getDisplayLine($offset, $length) { $start = 1; for ($ii = $offset; $ii > 0; $ii--) { if ($this->new[$ii] && $this->new[$ii]['line']) { $start = $this->new[$ii]['line']; break; } } $end = $start; for ($ii = $offset + $length; $ii < count($this->new); $ii++) { if ($this->new[$ii] && $this->new[$ii]['line']) { $end = $this->new[$ii]['line']; break; } } return "{$start},{$end}"; } public function parseInlineComment(DifferentialInlineComment $comment) { $this->comments[] = $comment; return $this; } public function process() { $old = array(); $new = array(); $n = 0; $this->old = array_reverse($this->old); $this->new = array_reverse($this->new); $whitelines = false; $changed = false; $skip_intra = array(); while (count($this->old) || count($this->new)) { $o_desc = array_pop($this->old); $n_desc = array_pop($this->new); $oend = end($this->old); if ($oend) { $o_next = $oend['type']; } else { $o_next = null; } $nend = end($this->new); if ($nend) { $n_next = $nend['type']; } else { $n_next = null; } if ($o_desc) { $o_type = $o_desc['type']; } else { $o_type = null; } if ($n_desc) { $n_type = $n_desc['type']; } else { $n_type = null; } if (($o_type != null) && ($n_type == null)) { $old[] = $o_desc; $new[] = null; if ($n_desc) { array_push($this->new, $n_desc); } $changed = true; continue; } if (($n_type != null) && ($o_type == null)) { $old[] = null; $new[] = $n_desc; if ($o_desc) { array_push($this->old, $o_desc); } $changed = true; continue; } if ($this->whitespaceMode != self::WHITESPACE_SHOW_ALL) { $similar = false; switch ($this->whitespaceMode) { case self::WHITESPACE_IGNORE_TRAILING: if (rtrim($o_desc['text']) == rtrim($n_desc['text'])) { $similar = true; } break; } if ($similar) { $o_desc['type'] = null; $n_desc['type'] = null; $skip_intra[count($old)] = true; $whitelines = true; } else { $changed = true; } } else { $changed = true; } $old[] = $o_desc; $new[] = $n_desc; } $this->old = $old; $this->new = $new; if ($this->subparser && false) { // TODO: This is bugged // Use the subparser's side-by-side line information -- notably, the // change types -- but replace all the line text with ours. This lets us // render whitespace-only changes without marking them as different. $old = $this->subparser->old; $new = $this->subparser->new; $old_text = ipull($this->old, 'text', 'line'); $new_text = ipull($this->new, 'text', 'line'); foreach ($old as $k => $desc) { if (empty($desc)) { continue; } $old[$k]['text'] = idx($old_text, $desc['line']); } foreach ($new as $k => $desc) { if (empty($desc)) { continue; } $new[$k]['text'] = idx($new_text, $desc['line']); } $this->old = $old; $this->new = $new; } $min_length = min(count($this->old), count($this->new)); for ($ii = 0; $ii < $min_length; $ii++) { if ($this->old[$ii] || $this->new[$ii]) { if (isset($this->old[$ii]['text'])) { $otext = $this->old[$ii]['text']; } else { $otext = ''; } if (isset($this->new[$ii]['text'])) { $ntext = $this->new[$ii]['text']; } else { $ntext = ''; } if ($otext != $ntext && empty($skip_intra[$ii])) { $this->intra[$ii] = ArcanistDiffUtils::generateIntralineDiff( $otext, $ntext); } } } $lines_context = self::LINES_CONTEXT; $max_length = max(count($this->old), count($this->new)); $old = $this->old; $new = $this->new; $visible = false; $last = 0; for ($cursor = -$lines_context; $cursor < $max_length; $cursor++) { $offset = $cursor + $lines_context; if ((isset($old[$offset]) && $old[$offset]['type']) || (isset($new[$offset]) && $new[$offset]['type'])) { $visible = true; $last = $offset; } else if ($cursor > $last + $lines_context) { $visible = false; } if ($visible && $cursor > 0) { $this->visible[$cursor] = 1; } } $old_corpus = ipull($this->old, 'text'); $old_corpus_block = implode("\n", $old_corpus); $new_corpus = ipull($this->new, 'text'); $new_corpus_block = implode("\n", $new_corpus); if ($this->noHighlight) { $this->oldRender = explode("\n", phutil_escape_html($old_corpus_block)); $this->newRender = explode("\n", phutil_escape_html($new_corpus_block)); } else { $this->oldRender = $this->sourceHighlight($this->old, $old_corpus_block); $this->newRender = $this->sourceHighlight($this->new, $new_corpus_block); } $this->applyIntraline( $this->oldRender, ipull($this->intra, 0), $old_corpus); $this->applyIntraline( $this->newRender, ipull($this->intra, 1), $new_corpus); $this->tokenHighlight($this->oldRender); $this->tokenHighlight($this->newRender); $unchanged = false; if ($this->subparser && false) { $unchanged = $this->subparser->isUnchanged(); $whitelines = $this->subparser->isWhitespaceOnly(); } else if (!$changed) { $filetype = $this->changeset->getFileType(); if ($filetype == DifferentialChangeType::FILE_TEXT || $filetype == DifferentialChangeType::FILE_SYMLINK) { $unchanged = true; } } $generated = (strpos($new_corpus_block, '@'.'generated') !== false); $this->specialAttributes = array( self::ATTR_GENERATED => $generated, self::ATTR_UNCHANGED => $unchanged, self::ATTR_DELETED => array_filter($this->old) && !array_filter($this->new), self::ATTR_WHITELINES => $whitelines ); } public function loadCache() { if (!$this->changesetID) { return false; } $data = null; $changeset = new DifferentialChangeset(); $conn_r = $changeset->establishConnection('r'); $data = queryfx_one( $conn_r, 'SELECT * FROM %T WHERE id = %d', $changeset->getTableName().'_parse_cache', $this->changesetID); if (!$data) { return false; } $data = json_decode($data['cache'], true); if (!is_array($data) || !$data) { return false; } foreach (self::getCacheableProperties() as $cache_key) { if (!array_key_exists($cache_key, $data)) { // If we're missing a cache key, assume we're looking at an old cache // and ignore it. return false; } } if ($data['cacheVersion'] !== self::CACHE_VERSION) { return false; } unset($data['cacheVersion'], $data['cacheHost']); $cache_prop = array_select_keys($data, self::getCacheableProperties()); foreach ($cache_prop as $cache_key => $v) { $this->$cache_key = $v; } return true; } protected static function getCacheableProperties() { return array( 'visible', 'new', 'old', 'intra', 'newRender', 'oldRender', 'specialAttributes', 'missingOld', 'missingNew', 'cacheVersion', 'cacheHost', ); } public function saveCache() { if (!$this->changesetID) { return false; } $cache = array(); foreach (self::getCacheableProperties() as $cache_key) { switch ($cache_key) { case 'cacheVersion': $cache[$cache_key] = self::CACHE_VERSION; break; case 'cacheHost': $cache[$cache_key] = php_uname('n'); break; default: $cache[$cache_key] = $this->$cache_key; break; } } $cache = json_encode($cache); try { $changeset = new DifferentialChangeset(); $conn_w = $changeset->establishConnection('w'); queryfx( $conn_w, 'INSERT INTO %T (id, cache) VALUES (%d, %s) ON DUPLICATE KEY UPDATE cache = VALUES(cache)', $changeset->getTableName().'_parse_cache', $this->changesetID, $cache); - } catch (QueryException $ex) { + } catch (AphrontQueryException $ex) { // TODO: uhoh } } public function isGenerated() { return idx($this->specialAttributes, self::ATTR_GENERATED, false); } public function isDeleted() { return idx($this->specialAttributes, self::ATTR_DELETED, false); } public function isUnchanged() { return idx($this->specialAttributes, self::ATTR_UNCHANGED, false); } public function isWhitespaceOnly() { return idx($this->specialAttributes, self::ATTR_WHITELINES, false); } public function getLength() { return max(count($this->old), count($this->new)); } protected function applyIntraline(&$render, $intra, $corpus) { foreach ($render as $key => $text) { if (isset($intra[$key])) { $render[$key] = ArcanistDiffUtils::applyIntralineDiff( $text, $intra[$key]); } if (isset($corpus[$key]) && strlen($corpus[$key]) > 80) { $render[$key] = $this->lineWrap($render[$key]); } } } protected function lineWrap($l) { $c = 0; $len = strlen($l); $ins = array(); for ($ii = 0; $ii < $len; ++$ii) { if ($l[$ii] == '&') { do { ++$ii; } while ($l[$ii] != ';'); ++$c; } else if ($l[$ii] == '<') { do { ++$ii; } while ($l[$ii] != '>'); } else { ++$c; } if ($c == 80) { $ins[] = ($ii + 1); $c = 0; } } while (($pos = array_pop($ins))) { $l = substr_replace( $l, "\xE2\xAC\x85
", $pos, 0); } return $l; } protected function tokenHighlight(&$render) { foreach ($render as $key => $text) { $render[$key] = str_replace( "\0", ''."\xEF\xBF\xBD".'', $text); } } protected function sourceHighlight($data, $corpus) { $result = $this->highlightEngine->highlightSource( $this->filetype, $corpus); $result_lines = explode("\n", $result); foreach ($data as $key => $info) { if (!$info) { unset($result_lines[$key]); } } return $result_lines; } private function tryCacheStuff() { $whitespace_mode = $this->whitespaceMode; switch ($whitespace_mode) { case self::WHITESPACE_SHOW_ALL: case self::WHITESPACE_IGNORE_TRAILING: break; default: $whitespace_mode = self::WHITESPACE_IGNORE_ALL; break; } $skip_cache = ($whitespace_mode != self::WHITESPACE_IGNORE_ALL); $this->whitespaceMode = $whitespace_mode; $changeset = $this->changeset; if ($changeset->getFileType() == DifferentialChangeType::FILE_TEXT || $changeset->getFileType() == DifferentialChangeType::FILE_SYMLINK) { if ($skip_cache || !$this->loadCache()) { if ($this->whitespaceMode == self::WHITESPACE_IGNORE_ALL) { // Huge mess. Generate a "-bw" (ignore all whitespace changes) diff, // parse it out, and then play a shell game with the parsed format // in process() so we highlight only changed lines but render // whitespace differences. If we don't do this, we either fail to // render whitespace changes (which is incredibly confusing, // especially for python) or often produce a much larger set of // differences than necessary. $old_tmp = new TempFile(); $new_tmp = new TempFile(); Filesystem::writeFile($old_tmp, $changeset->makeOldFile()); Filesystem::writeFile($new_tmp, $changeset->makeNewFile()); list($err, $diff) = exec_manual( 'diff -bw -U65535 %s %s', $old_tmp, $new_tmp); if (!strlen($diff)) { // If there's no diff text, that means the files are identical // except for whitespace changes. Build a synthetic, changeless // diff. TODO: this is incredibly hacky. $entire_file = explode("\n", $changeset->makeOldFile()); foreach ($entire_file as $k => $line) { $entire_file[$k] = ' '.$line; } $len = count($entire_file); $entire_file = implode("\n", $entire_file); $diff = <<parseDiff($diff); $diff = DifferentialDiff::newFromRawChanges($changes); $changesets = $diff->getChangesets(); $alt_changeset = reset($changesets); $this->subparser = new DifferentialChangesetParser(); $this->subparser->setChangeset($alt_changeset); $this->subparser->setWhitespaceMode(self::WHITESPACE_IGNORE_TRAILING); } foreach ($changeset->getHunks() as $hunk) { $this->parseHunk($hunk); } $this->process(); if (!$skip_cache) { $this->saveCache(); } } } } public function render( $range_start = null, $range_len = null, $mask_force = array()) { $this->highlightEngine = new PhutilDefaultSyntaxHighlighterEngine(); $this->tryCacheStuff(); $changeset_id = $this->changesetID; $feedback_mask = array(); switch ($this->changeset->getFileType()) { case DifferentialChangeType::FILE_IMAGE: $old = null; $cur = null; $metadata = $this->changeset->getMetadata(); $data = idx($metadata, 'attachment-data'); $old_phid = idx($metadata, 'old:binary-phid'); $new_phid = idx($metadata, 'new:binary-phid'); if ($old_phid || $new_phid) { if ($old_phid) { $old_uri = PhabricatorFileURI::getViewURIForPHID($old_phid); $old = phutil_render_tag( 'img', array( 'src' => $old_uri, )); } if ($new_phid) { $new_uri = PhabricatorFileURI::getViewURIForPHID($new_phid); $cur = phutil_render_tag( 'img', array( 'src' => $new_uri, )); } } $output = $this->renderChangesetTable( $this->changeset, ''. ''. ''. '
'. $old. '
'. ''. ''. ''. '
'. $cur. '
'. ''. ''); return $output; case DifferentialChangeType::FILE_DIRECTORY: case DifferentialChangeType::FILE_BINARY: $output = $this->renderChangesetTable($this->changeset, null); return $output; } $shield = null; if ($range_start === null && $range_len === null) { if ($this->isGenerated()) { $shield = $this->renderShield( "This file contains generated code, which does not normally need ". "to be reviewed.", true); } else if ($this->isUnchanged()) { if ($this->isWhitespaceOnly()) { $shield = $this->renderShield( "This file was changed only by adding or removing trailing ". "whitespace.", false); } else { $shield = $this->renderShield( "The contents of this file were not changed.", false); } } else if ($this->isDeleted()) { $shield = $this->renderShield( "This file was completely deleted.", true); } else if ($this->changeset->getAffectedLineCount() > 2500) { $lines = number_format($this->changeset->getAffectedLineCount()); $shield = $this->renderShield( "This file has a very large number of changes ({$lines} lines).", true); } else if (preg_match('/\.sql3$/', $this->changeset->getFilename())) { $shield = $this->renderShield( ".sql3 files are hidden by default.", true); } } if ($shield) { return $this->renderChangesetTable($this->changeset, $shield); } $old_comments = array(); $new_comments = array(); $old_mask = array(); $new_mask = array(); $feedback_mask = array(); if ($this->comments) { foreach ($this->comments as $comment) { $start = max($comment->getLineNumber() - self::LINES_CONTEXT, 0); $end = $comment->getLineNumber() + $comment->getLineLength() + self::LINES_CONTEXT; $new = $this->isCommentInNewFile($comment); for ($ii = $start; $ii <= $end; $ii++) { if ($new) { $new_mask[$ii] = true; } else { $old_mask[$ii] = true; } } } foreach ($this->old as $ii => $old) { if (isset($old['line']) && isset($old_mask[$old['line']])) { $feedback_mask[$ii] = true; } } foreach ($this->new as $ii => $new) { if (isset($new['line']) && isset($new_mask[$new['line']])) { $feedback_mask[$ii] = true; } } $this->comments = msort($this->comments, 'getID'); foreach ($this->comments as $comment) { $final = $comment->getLineNumber() + $comment->getLineLength(); if ($this->isCommentInNewFile($comment)) { $new_comments[$final][] = $comment; } else { $old_comments[$final][] = $comment; } } } $html = $this->renderTextChange( $range_start, $range_len, $mask_force, $feedback_mask, $old_comments, $new_comments); return $this->renderChangesetTable($this->changeset, $html); } private function isCommentInNewFile(DifferentialInlineComment $comment) { if ($this->oldChangesetID) { return ($comment->getChangesetID() != $this->oldChangesetID); } else { return $comment->getIsNewFile(); } } protected function renderShield($message, $more) { $end = $this->getLength(); $changeset_id = $this->getChangesetID(); if ($more) { $more = ' '. javelin_render_tag( 'a', array( 'mustcapture' => true, 'sigil' => 'show-more', 'class' => 'complete', 'href' => '#', 'meta' => array( 'id' => $changeset_id, 'range' => "0-{$end}", ), ), 'Show File Contents'); } else { $more = null; } return javelin_render_tag( 'tr', array( 'sigil' => 'context-target', ), ''. phutil_escape_html($message). $more. ''); } protected function renderTextChange( $range_start, $range_len, $mask_force, $feedback_mask, array $old_comments, array $new_comments) { $context_not_available = null; if ($this->missingOld || $this->missingNew) { $context_not_available = javelin_render_tag( 'tr', array( 'sigil' => 'context-target', ), ''. 'Context not available.'. ''); $context_not_available = $context_not_available; } $html = array(); $rows = max( count($this->old), count($this->new)); if ($range_start === null) { $range_start = 0; } if ($range_len === null) { $range_len = $rows; } $range_len = min($range_len, $rows - $range_start); $gaps = array(); $gap_start = 0; $in_gap = false; $mask = $this->visible + $mask_force + $feedback_mask; $mask[$range_start + $range_len] = true; for ($ii = $range_start; $ii <= $range_start + $range_len; $ii++) { if (isset($mask[$ii])) { if ($in_gap) { $gap_length = $ii - $gap_start; if ($gap_length <= self::LINES_CONTEXT) { for ($jj = $gap_start; $jj <= $gap_start + $gap_length; $jj++) { $mask[$jj] = true; } } else { $gaps[] = array($gap_start, $gap_length); } $in_gap = false; } } else { if (!$in_gap) { $gap_start = $ii; $in_gap = true; } } } $gaps = array_reverse($gaps); $changeset = $this->changesetID; for ($ii = $range_start; $ii < $range_start + $range_len; $ii++) { if (empty($mask[$ii])) { $gap = array_pop($gaps); $top = $gap[0]; $len = $gap[1]; $end = $top + $len - 20; $contents = array(); if ($len > 40) { $contents[] = javelin_render_tag( 'a', array( 'href' => '#', 'mustcapture' => true, 'sigil' => 'show-more', 'meta' => array( 'id' => $changeset, 'range' => "{$top}-{$len}/{$top}-20", ), ), "\xE2\x96\xB2 Show 20 Lines"); } $contents[] = javelin_render_tag( 'a', array( 'href' => '#', 'mustcapture' => true, 'sigil' => 'show-more', 'meta' => array( 'id' => $changeset, 'range' => "{$top}-{$len}/{$top}-{$len}", ), ), 'Show All '.$len.' Lines'); if ($len > 40) { $contents[] = javelin_render_tag( 'a', array( 'href' => '#', 'mustcapture' => true, 'sigil' => 'show-more', 'meta' => array( 'id' => $changeset, 'range' => "{$top}-{$len}/{$end}-20", ), ), "\xE2\x96\xBC Show 20 Lines"); }; $container = javelin_render_tag( 'tr', array( 'sigil' => 'context-target', ), ''. implode(' • ', $contents). ''); $html[] = $container; $ii += ($len - 1); continue; } if (isset($this->old[$ii])) { $o_num = $this->old[$ii]['line']; $o_text = isset($this->oldRender[$ii]) ? $this->oldRender[$ii] : null; $o_attr = null; if ($this->old[$ii]['type']) { if (empty($this->new[$ii])) { $o_attr = ' class="old old-full"'; } else { $o_attr = ' class="old"'; } } } else { $o_num = null; $o_text = null; $o_attr = null; } if (isset($this->new[$ii])) { $n_num = $this->new[$ii]['line']; $n_text = isset($this->newRender[$ii]) ? $this->newRender[$ii] : null; $n_attr = null; if ($this->new[$ii]['type']) { if (empty($this->old[$ii])) { $n_attr = ' class="new new-full"'; } else { $n_attr = ' class="new"'; } } } else { $n_num = null; $n_text = null; $n_attr = null; } if (($o_num && !empty($this->missingOld[$o_num])) || ($n_num && !empty($this->missingNew[$n_num]))) { $html[] = $context_not_available; } if ($o_num) { $o_id = ' id="C'.$changeset.'OL'.$o_num.'"'; } else { $o_id = null; } if ($n_num) { $n_id = ' id="C'.$changeset.'NL'.$n_num.'"'; } else { $n_id = null; } // NOTE: The Javascript is sensitive to whitespace changes in this // block! $html[] = ''. ''.$o_num.''. ''.$o_text.''. ''.$n_num.''. ''.$n_text.''. ''; if ($context_not_available && ($ii == $rows - 1)) { $html[] = $context_not_available; } if ($o_num && isset($old_comments[$o_num])) { foreach ($old_comments[$o_num] as $comment) { $xhp = $this->renderInlineComment($comment); $html[] = ''. $xhp. ''; } } if ($n_num && isset($new_comments[$n_num])) { foreach ($new_comments[$n_num] as $comment) { $xhp = $this->renderInlineComment($comment); $html[] = ''. $xhp. ''; } } } return implode('', $html); } private function renderInlineComment(DifferentialInlineComment $comment) { $user = $this->user; $edit = $user && ($comment->getAuthorPHID() == $user->getPHID()) && (!$comment->getCommentID()); $on_right = $this->isCommentInNewFile($comment); return id(new DifferentialInlineCommentView()) ->setInlineComment($comment) ->setOnRight($on_right) ->setHandles($this->handles) ->setMarkupEngine($this->markupEngine) ->setEditable($edit) ->render(); } protected function renderPropertyChangeHeader($changeset) { $old = $changeset->getOldProperties(); $new = $changeset->getNewProperties(); if ($old === $new) { return null; } if ($changeset->getChangeType() == DifferentialChangeType::TYPE_ADD && $new == array('unix:filemode' => '100644')) { return null; } if ($changeset->getChangeType() == DifferentialChangeType::TYPE_DELETE && $old == array('unix:filemode' => '100644')) { return null; } return null; /* TODO $table = ; $table->appendChild( ); $keys = array_keys($old + $new); sort($keys); foreach ($keys as $key) { $oval = idx($old, $key); $nval = idx($new, $key); if ($oval !== $nval) { if ($oval === null) { $oval = null; } if ($nval === null) { $nval = null; } $table->appendChild( ); } } return $table; */ } protected function renderChangesetTable($changeset, $contents) { $props = $this->renderPropertyChangeHeader($this->changeset); $table = null; if ($contents) { $table = '
Property Changes Old Value New Value
{$key} {$oval} {$nval}
'. $contents. '
'; } if (!$table && !$props) { $notice = $this->renderChangeTypeHeader($this->changeset, true); } else { $notice = $this->renderChangeTypeHeader($this->changeset, false); } return implode( "\n", array( $notice, $props, $table, )); } protected function renderChangeTypeHeader($changeset, $force) { static $articles = array( DifferentialChangeType::FILE_IMAGE => 'an', ); static $files = array( DifferentialChangeType::FILE_TEXT => 'file', DifferentialChangeType::FILE_IMAGE => 'image', DifferentialChangeType::FILE_DIRECTORY => 'directory', DifferentialChangeType::FILE_BINARY => 'binary file', DifferentialChangeType::FILE_SYMLINK => 'symlink', ); static $changes = array( DifferentialChangeType::TYPE_ADD => 'added', DifferentialChangeType::TYPE_CHANGE => 'changed', DifferentialChangeType::TYPE_DELETE => 'deleted', DifferentialChangeType::TYPE_MOVE_HERE => 'moved from', DifferentialChangeType::TYPE_COPY_HERE => 'copied from', DifferentialChangeType::TYPE_MOVE_AWAY => 'moved to', DifferentialChangeType::TYPE_COPY_AWAY => 'copied to', DifferentialChangeType::TYPE_MULTICOPY => 'deleted after being copied to', ); $change = $changeset->getChangeType(); $file = $changeset->getFileType(); $message = null; if ($change == DifferentialChangeType::TYPE_CHANGE && $file == DifferentialChangeType::FILE_TEXT) { if ($force) { // We have to force something to render because there were no changes // of other kinds. $message = "This {$files[$file]} was not modified."; } else { // Default case of changes to a text file, no metadata. return null; } } else { $verb = idx($changes, $change, 'changed'); switch ($change) { default: $message = "This {$files[$file]} was {$verb}."; break; case DifferentialChangeType::TYPE_MOVE_HERE: case DifferentialChangeType::TYPE_COPY_HERE: $message = "This {$files[$file]} was {$verb} ". "{$changeset->getOldFile()}."; break; case DifferentialChangeType::TYPE_MOVE_AWAY: case DifferentialChangeType::TYPE_COPY_AWAY: case DifferentialChangeType::TYPE_MULTICOPY: $paths = $changeset->getAwayPaths(); if (count($paths) > 1) { $message = "This {$files[$file]} was {$verb}: ". "".implode(', ', $paths)."."; } else { $message = "This {$files[$file]} was {$verb} ". "".reset($paths)."."; } break; case DifferentialChangeType::TYPE_CHANGE: $message = "This is ".idx($articles, $file, 'a')." {$files[$file]}."; break; } } return '
'. $message. '
'; } public function renderForEmail() { $ret = ''; $min = min(count($this->old), count($this->new)); for ($i = 0; $i < $min; $i++) { $o = $this->old[$i]; $n = $this->new[$i]; if (!isset($this->visible[$i])) { continue; } if ($o['line'] && $n['line']) { // It is quite possible there are better ways to achieve this. For // example, "white-space: pre;" can do a better job, WERE IT NOT for // broken email clients like OWA which use newlines to do weird // wrapping. So dont give them newlines. if (isset($this->intra[$i])) { $ret .= sprintf( "- %s
", str_replace(" ", " ", phutil_escape_html($o['text'])) ); $ret .= sprintf( "+ %s
", str_replace(" ", " ", phutil_escape_html($n['text'])) ); } else { $ret .= sprintf("  %s
", str_replace(" ", " ", phutil_escape_html($n['text'])) ); } } else if ($o['line'] && !$n['line']) { $ret .= sprintf( "- %s
", str_replace(" ", " ", phutil_escape_html($o['text'])) ); } else { $ret .= sprintf( "+ %s
", str_replace(" ", " ", phutil_escape_html($n['text'])) ); } } return $ret; } } diff --git a/src/applications/maniphest/view/tasklist/ManiphestTaskListView.php b/src/applications/maniphest/view/tasklist/ManiphestTaskListView.php index ce630dc09e..8234ad3967 100644 --- a/src/applications/maniphest/view/tasklist/ManiphestTaskListView.php +++ b/src/applications/maniphest/view/tasklist/ManiphestTaskListView.php @@ -1,50 +1,50 @@ tasks = $tasks; return $this; } public function setHandles(array $handles) { $this->handles = $handles; return $this; } public function render() { $views = array(); foreach ($this->tasks as $task) { - $view = new ManiphestTaskSummaryView($task); + $view = new ManiphestTaskSummaryView(); $view->setTask($task); $view->setHandles($this->handles); $views[] = $view->render(); } return '
'. implode("\n", $views). '
'; } } diff --git a/src/applications/maniphest/view/transactionlist/ManiphestTransactionListView.php b/src/applications/maniphest/view/transactionlist/ManiphestTransactionListView.php index 8d010d83f1..4c71d090a9 100644 --- a/src/applications/maniphest/view/transactionlist/ManiphestTransactionListView.php +++ b/src/applications/maniphest/view/transactionlist/ManiphestTransactionListView.php @@ -1,88 +1,88 @@ transactions = $transactions; return $this; } public function setHandles(array $handles) { $this->handles = $handles; return $this; } public function setUser(PhabricatorUser $user) { $this->user = $user; return $this; } public function setMarkupEngine(PhutilMarkupEngine $engine) { $this->markupEngine = $engine; return $this; } public function render() { $views = array(); $last = null; $group = array(); $groups = array(); foreach ($this->transactions as $transaction) { if ($last === null) { $last = $transaction; $group[] = $transaction; continue; } else if ($last->canGroupWith($transaction)) { $group[] = $transaction; if ($transaction->hasComments()) { $last = $transaction; } } else { $groups[] = $group; $last = $transaction; $group = array($transaction); } } if ($group) { $groups[] = $group; } foreach ($groups as $group) { - $view = new ManiphestTransactionDetailView($transaction); + $view = new ManiphestTransactionDetailView(); $view->setTransactionGroup($group); $view->setHandles($this->handles); $view->setMarkupEngine($this->markupEngine); $views[] = $view->render(); } return '
'. implode("\n", $views). '
'; } } diff --git a/src/applications/repository/controller/create/PhabricatorRepositoryCreateController.php b/src/applications/repository/controller/create/PhabricatorRepositoryCreateController.php index ea29580d89..bbab3011cf 100644 --- a/src/applications/repository/controller/create/PhabricatorRepositoryCreateController.php +++ b/src/applications/repository/controller/create/PhabricatorRepositoryCreateController.php @@ -1,142 +1,138 @@ getRequest(); $user = $request->getUser(); $e_name = true; $e_callsign = true; $repository = new PhabricatorRepository(); $type_map = array( 'svn' => 'Subversion', 'git' => 'Git', ); $errors = array(); if ($request->isFormPost()) { $repository->setName($request->getStr('name')); $repository->setCallsign($request->getStr('callsign')); $repository->setVersionControlSystem($request->getStr('type')); if (!strlen($repository->getName())) { $e_name = 'Required'; $errors[] = 'Repository name is required.'; } else { $e_name = null; } if (!strlen($repository->getCallsign())) { $e_callsign = 'Required'; $errors[] = 'Callsign is required.'; } else if (!preg_match('/^[A-Z]+$/', $repository->getCallsign())) { $e_callsign = 'Invalid'; $errors[] = 'Callsign must be ALL UPPERCASE LETTERS.'; } else { $e_callsign = null; } if (empty($type_map[$repository->getVersionControlSystem()])) { $errors[] = 'Invalid version control system.'; } if (!$errors) { try { $repository->save(); return id(new AphrontRedirectResponse()) ->setURI('/repository/edit/'.$repository->getID().'/'); - } catch (PhabricatorQueryDuplicateKeyException $ex) { - if ($ex->getDuplicateKey() == 'callsign') { - $e_callsign = 'Duplicate'; - $errors[] = 'Callsign must be unique. Another repository already '. - 'uses that callsign.'; - } else { - throw $ex; - } + } catch (AphrontQueryDuplicateKeyException $ex) { + $e_callsign = 'Duplicate'; + $errors[] = 'Callsign must be unique. Another repository already '. + 'uses that callsign.'; } } } $error_view = null; if ($errors) { $error_view = new AphrontErrorView(); $error_view->setErrors($errors); $error_view->setTitle('Form Errors'); } $form = new AphrontFormView(); $form ->setUser($user) ->setAction('/repository/create/') ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Name') ->setName('name') ->setValue($repository->getName()) ->setError($e_name) ->setCaption('Human-readable repository name.')) ->appendChild( '

Select a "Callsign" — a '. 'short, uppercase string to identify revisions in this repository. If '. 'you choose "EX", revisions in this repository will be identified '. 'with the prefix "rEX".

') ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Callsign') ->setName('callsign') ->setValue($repository->getCallsign()) ->setError($e_callsign) ->setCaption( 'Short, UPPERCASE identifier. Once set, it can not be changed.')) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel('Type') ->setName('type') ->setOptions($type_map) ->setValue($repository->getVersionControlSystem())) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue('Create Repository') ->addCancelButton('/repository/')); $panel = new AphrontPanelView(); $panel->setHeader('Create Repository'); $panel->appendChild($form); $panel->setWidth(AphrontPanelView::WIDTH_FORM); return $this->buildStandardPageResponse( array( $error_view, $panel, ), array( 'title' => 'Create Repository', )); } } diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php index ff811248e4..c349913b5c 100644 --- a/src/infrastructure/env/PhabricatorEnv.php +++ b/src/infrastructure/env/PhabricatorEnv.php @@ -1,38 +1,38 @@ getTransactionKey(); if (!isset($levels[$key])) { $levels[$key] = array( 'read' => 0, 'write' => 0, ); } return $levels[$key]; } public function isReadLocking() { $levels = &$this->getLockLevels(); return ($levels['read'] > 0); } public function isWriteLocking() { $levels = &$this->getLockLevels(); return ($levels['write'] > 0); } public function startReadLocking() { $levels = &$this->getLockLevels(); ++$levels['read']; return $this; } public function startWriteLocking() { $levels = &$this->getLockLevels(); ++$levels['write']; return $this; } public function stopReadLocking() { $levels = &$this->getLockLevels(); if ($levels['read'] < 1) { throw new Exception('Unable to stop read locking: not read locking.'); } --$levels['read']; return $this; } public function stopWriteLocking() { $levels = &$this->getLockLevels(); if ($levels['write'] < 1) { throw new Exception('Unable to stop read locking: not write locking.'); } --$levels['write']; return $this; } protected function &getTransactionStack($key) { if (!self::$transactionShutdownRegistered) { self::$transactionShutdownRegistered = true; register_shutdown_function( array( 'AphrontDatabaseConnection', 'shutdownTransactionStacks', )); } if (!isset(self::$transactionStacks[$key])) { self::$transactionStacks[$key] = array(); } return self::$transactionStacks[$key]; } public static function shutdownTransactionStacks() { foreach (self::$transactionStacks as $stack) { if ($stack === false) { continue; } $count = count($stack); if ($count) { throw new Exception( 'Script exited with '.$count.' open transactions! The '. 'transactions will be implicitly rolled back. Calls to '. 'openTransaction() should always be paired with a call to '. 'saveTransaction() or killTransaction(); you have an unpaired '. 'call somewhere.', $count); } } } public function openTransaction() { $key = $this->getTransactionKey(); $stack = &$this->getTransactionStack($key); $new_transaction = !count($stack); // TODO: At least in development, push context information instead of // `true' so we can report (or, at least, guess) where unpaired // transaction calls happened. $stack[] = true; end($stack); $key = key($stack); if ($new_transaction) { $this->query('START TRANSACTION'); } else { $this->query('SAVEPOINT '.$this->getSavepointName($key)); } } public function isInsideTransaction() { $key = $this->getTransactionKey(); $stack = &$this->getTransactionStack($key); return (bool)count($stack); } public function saveTransaction() { $key = $this->getTransactionKey(); $stack = &$this->getTransactionStack($key); if (!count($stack)) { throw new Exception( "No open transaction! Unable to save transaction, since there ". "isn't one."); } array_pop($stack); if (!count($stack)) { $this->query('COMMIT'); } } public function saveTransactionUnless($cond) { if ($cond) { $this->killTransaction(); } else { $this->saveTransaction(); } } public function saveTransactionIf($cond) { $this->saveTransactionUnless(!$cond); } public function killTransaction() { $key = $this->getTransactionKey(); $stack = &$this->getTransactionStack($key); if (!count($stack)) { throw new Exception( "No open transaction! Unable to kill transaction, since there ". "isn't one."); } $count = count($stack); end($stack); $key = key($stack); array_pop($stack); if (!count($stack)) { $this->query('ROLLBACK'); } else { $this->query( 'ROLLBACK TO SAVEPOINT '.$this->getSavepointName($key) ); } } protected function getSavepointName($key) { return 'LiskSavepoint_'.$key; } } diff --git a/src/storage/connection/mysql/AphrontMySQLDatabaseConnection.php b/src/storage/connection/mysql/AphrontMySQLDatabaseConnection.php index 5af7dcb12f..3729954f5a 100644 --- a/src/storage/connection/mysql/AphrontMySQLDatabaseConnection.php +++ b/src/storage/connection/mysql/AphrontMySQLDatabaseConnection.php @@ -1,249 +1,248 @@ configuration = $configuration; } public function escapeString($string) { $this->requireConnection(); return mysql_real_escape_string($string, $this->connection); } public function escapeColumnName($name) { return '`'.str_replace('`', '\\`', $name).'`'; } public function escapeMultilineComment($comment) { // These can either terminate a comment, confuse the hell out of the parser, // make MySQL execute the comment as a query, or, in the case of semicolon, // are quasi-dangerous because the semicolon could turn a broken query into // a working query plus an ignored query. static $map = array( '--' => '(DOUBLEDASH)', '*/' => '(STARSLASH)', '//' => '(SLASHSLASH)', '#' => '(HASH)', '!' => '(BANG)', ';' => '(SEMICOLON)', ); $comment = str_replace( array_keys($map), array_values($map), $comment); // For good measure, kill anything else that isn't a nice printable // character. $comment = preg_replace('/[^\x20-\x7F]+/', ' ', $comment); return '/* '.$comment.' */'; } public function escapeStringForLikeClause($value) { $value = $this->escapeString($value); // Ideally the query shouldn't be modified after safely escaping it, // but we need to escape _ and % within LIKE terms. $value = str_replace( // Even though we've already escaped, we need to replace \ with \\ // because MYSQL unescapes twice inside a LIKE clause. See note // at mysql.com. However, if the \ is being used to escape a single // quote ('), then the \ should not be escaped. Thus, after all \ // are replaced with \\, we need to revert instances of \\' back to // \'. array('\\', '\\\\\'', '_', '%'), array('\\\\', '\\\'', '\_', '\%'), $value); return $value; } private function getConfiguration($key, $default = null) { return idx($this->configuration, $key, $default); } private function closeConnection() { if ($this->connection) { $this->connection = null; $key = $this->getConnectionCacheKey(); unset(self::$connectionCache[$key]); } } private function getConnectionCacheKey() { $user = $this->getConfiguration('user'); $host = $this->getConfiguration('host'); $database = $this->getConfiguration('database'); return "{$user}:{$host}:{$database}"; } private function establishConnection() { $this->closeConnection(); $user = $this->getConfiguration('user'); $host = $this->getConfiguration('host'); $database = $this->getConfiguration('database'); $key = $this->getConnectionCacheKey(); if (isset(self::$connectionCache[$key])) { $this->connection = self::$connectionCache[$key]; return; } $start = microtime(true); $conn = @mysql_connect( $host, $user, $this->getConfiguration('pass'), $new_link = true, $flags = 0); if (!$conn) { $errno = mysql_errno(); $error = mysql_error(); throw new AphrontQueryConnectionException( "Attempt to connect to {$user}@{$host} failed with error #{$errno}: ". "{$error}."); } $ret = @mysql_select_db($database, $conn); if (!$ret) { $this->throwQueryException($conn); } $end = microtime(true); DarkConsoleServicesPluginAPI::addEvent( array( 'event' => DarkConsoleServicesPluginAPI::EVENT_CONNECT, 'host' => $host, 'database' => $database, 'start' => $start, 'end' => $end, )); self::$connectionCache[$key] = $conn; $this->connection = $conn; } public function getInsertID() { return mysql_insert_id($this->requireConnection()); } public function getAffectedRows() { return mysql_affected_rows($this->requireConnection()); } public function getTransactionKey() { return (int)$this->requireConnection(); } private function requireConnection() { if (!$this->connection) { $this->establishConnection(); } return $this->connection; } public function selectAllResults() { $result = array(); $res = $this->lastResult; if ($res == null) { throw new Exception('No query result to fetch from!'); } while (($row = mysql_fetch_assoc($res)) !== false) { $result[] = $row; } return $result; } public function executeRawQuery($raw_query) { $this->lastResult = null; $retries = 3; while ($retries--) { try { $this->requireConnection(); $start = microtime(true); $result = @mysql_query($raw_query, $this->connection); $end = microtime(true); DarkConsoleServicesPluginAPI::addEvent( array( 'event' => DarkConsoleServicesPluginAPI::EVENT_QUERY, 'query' => $raw_query, 'start' => $start, 'end' => $end, )); if ($result) { $this->lastResult = $result; break; } $this->throwQueryException($this->connection); } catch (AphrontQueryConnectionLostException $ex) { if (!$retries) { throw $ex; } if ($this->isInsideTransaction()) { throw $ex; } $this->closeConnection(); } } } private function throwQueryException($connection) { $errno = mysql_errno($connection); $error = mysql_error($connection); switch ($errno) { case 2013: // Connection Dropped case 2006: // Gone Away throw new AphrontQueryConnectionLostException("#{$errno}: {$error}"); case 1213: // Deadlock case 1205: // Lock wait timeout exceeded throw new AphrontQueryRecoverableException("#{$errno}: {$error}"); case 1062: // Duplicate Key - $matches = null; - $key = null; - if (preg_match('/for key \'(.*)\'$/', $error, $matches)) { - $key = $matches[1]; - } - throw new AphrontQueryDuplicateKeyException($key, "{$errno}: {$error}"); + // NOTE: In some versions of MySQL we get a key name back here, but + // older versions just give us a key index ("key 2") so it's not + // portable to parse the key out of the error and attach it to the + // exception. + throw new AphrontQueryDuplicateKeyException("{$errno}: {$error}"); default: // TODO: 1064 is syntax error, and quite terrible in production. throw new AphrontQueryException("#{$errno}: {$error}"); } } } diff --git a/src/storage/exception/duplicatekey/AphrontQueryDuplicateKeyException.php b/src/storage/exception/duplicatekey/AphrontQueryDuplicateKeyException.php index 0c2a8be94e..496fec6fab 100644 --- a/src/storage/exception/duplicatekey/AphrontQueryDuplicateKeyException.php +++ b/src/storage/exception/duplicatekey/AphrontQueryDuplicateKeyException.php @@ -1,34 +1,24 @@ duplicateKey; - } - - public function __construct($duplicate_key, $message) { - $this->duplicateKey = $duplicate_key; - parent::__construct($message); - } } diff --git a/src/storage/queryfx/queryfx.php b/src/storage/queryfx/queryfx.php index 32fa9c6d5d..91cb5bb376 100644 --- a/src/storage/queryfx/queryfx.php +++ b/src/storage/queryfx/queryfx.php @@ -1,64 +1,70 @@ executeRawQuery($query); + $query = hphp_workaround_call_user_func_array('qsprintf', $argv); + $conn->executeRawQuery($query); } /** * @group storage */ function vqueryfx($conn, $sql, $argv) { array_unshift($argv, $conn, $sql); - return call_user_func_array('queryfx', $argv); + hphp_workaround_call_user_func_array('queryfx', $argv); } /** * @group storage */ function queryfx_all($conn, $sql/*, ... */) { $argv = func_get_args(); - $ret = call_user_func_array('queryfx', $argv); - return $conn->selectAllResults($ret); + hphp_workaround_call_user_func_array('queryfx', $argv); + return $conn->selectAllResults(); } /** * @group storage */ function queryfx_one($conn, $sql/*, ... */) { $argv = func_get_args(); - $ret = call_user_func_array('queryfx_all', $argv); + $ret = hphp_workaround_call_user_func_array('queryfx_all', $argv); if (count($ret) > 1) { throw new AphrontQueryCountException( 'Query returned more than one row.'); } else if (count($ret)) { return reset($ret); } return null; } function vqueryfx_all($conn, $sql, array $argv) { array_unshift($argv, $conn, $sql); - $ret = call_user_func_array('queryfx', $argv); - return $conn->selectAllResults($ret); + hphp_workaround_call_user_func_array('queryfx', $argv); + return $conn->selectAllResults(); } diff --git a/src/view/layout/headsup/actionlist/AphrontHeadsupActionListView.php b/src/view/layout/headsup/actionlist/AphrontHeadsupActionListView.php index 5f994b84b5..2eb411024e 100755 --- a/src/view/layout/headsup/actionlist/AphrontHeadsupActionListView.php +++ b/src/view/layout/headsup/actionlist/AphrontHeadsupActionListView.php @@ -1,43 +1,43 @@ actions = $actions; } public function render() { - + require_celerity_resource('aphront-headsup-action-list-view-css'); $actions = array(); foreach ($this->actions as $action_view) { $actions[] = $action_view->render(); } $actions = implode("\n", $actions); return '
'. $actions. '
'; } } diff --git a/src/view/layout/headsup/actionlist/__init__.php b/src/view/layout/headsup/actionlist/__init__.php index c5f5e313e5..7cf4e4b56d 100644 --- a/src/view/layout/headsup/actionlist/__init__.php +++ b/src/view/layout/headsup/actionlist/__init__.php @@ -1,12 +1,13 @@ request = $request; return $this; } public function getRequest() { return $this->request; } public function setApplicationName($application_name) { $this->applicationName = $application_name; return $this; } public function getApplicationName() { return $this->applicationName; } public function setBaseURI($base_uri) { $this->baseURI = $base_uri; return $this; } public function getBaseURI() { return $this->baseURI; } public function setTabs(array $tabs, $selected_tab) { $this->tabs = $tabs; $this->selectedTab = $selected_tab; return $this; } public function getTitle() { return $this->getGlyph().' '.parent::getTitle(); } protected function willRenderPage() { if (!$this->getRequest()) { throw new Exception( "You must set the Request to render a PhabricatorStandardPageView."); } $console = $this->getRequest()->getApplicationConfiguration()->getConsole(); require_celerity_resource('phabricator-core-css'); require_celerity_resource('phabricator-core-buttons-css'); require_celerity_resource('phabricator-standard-page-view'); require_celerity_resource('javelin-lib-dev'); require_celerity_resource('javelin-workflow-dev'); Javelin::initBehavior('workflow', array()); if ($console) { require_celerity_resource('aphront-dark-console-css'); Javelin::initBehavior( 'dark-console', array( 'uri' => '/~/', )); } $this->bodyContent = $this->renderChildren(); } protected function getHead() { $response = CelerityAPI::getStaticResourceResponse(); return ''. $response->renderResourcesOfType('css'). ''; } public function setGlyph($glyph) { $this->glyph = $glyph; return $this; } public function getGlyph() { return $this->glyph; } protected function willSendResponse($response) { $console = $this->getRequest()->getApplicationConfiguration()->getConsole(); if ($console) { $response = str_replace( '', $console->render($this->getRequest()), $response); } return $response; } protected function getBody() { $console = $this->getRequest()->getApplicationConfiguration()->getConsole(); $tabs = array(); foreach ($this->tabs as $name => $tab) { $tab_markup = phutil_render_tag( 'a', array( 'href' => idx($tab, 'href'), ), phutil_escape_html(idx($tab, 'name'))); $tab_markup = phutil_render_tag( 'td', array( 'class' => ($name == $this->selectedTab) ? 'phabricator-selected-tab' : null, ), $tab_markup); $tabs[] = $tab_markup; } $tabs = implode('', $tabs); $login_stuff = null; $request = $this->getRequest(); + $user = null; if ($request) { $user = $request->getUser(); - if ($user->getPHID()) { + // NOTE: user may not be set here if we caught an exception early + // in the execution workflow. + if ($user && $user->getPHID()) { $login_stuff = 'Logged in as '.phutil_render_tag( 'a', array( 'href' => '/p/'.$user->getUsername().'/', ), phutil_escape_html($user->getUsername())). ' · '. 'Settings'. ' · '. phabricator_render_form( $user, array( 'action' => '/search/', 'method' => 'post', 'style' => 'display: inline', ), ''. ''); } } $foot_links = array(); $version = PhabricatorEnv::getEnvConfig('phabricator.version'); $foot_links[] = phutil_escape_html('Phabricator '.$version); if (PhabricatorEnv::getEnvConfig('darkconsole.enabled') && !PhabricatorEnv::getEnvConfig('darkconsole.always-on')) { if ($console) { $link = javelin_render_tag( 'a', array( 'href' => '/~/', 'sigil' => 'workflow', ), 'Disable DarkConsole'); } else { $link = javelin_render_tag( 'a', array( 'href' => '/~/', 'sigil' => 'workflow', ), 'Enable DarkConsole'); } $foot_links[] = $link; } - // This ends up very early in tab order at the top of the page and there's - // a bunch of junk up there anyway, just shove it down here. - $foot_links[] = phabricator_render_form( - $user, - array( - 'action' => '/logout/', - 'method' => 'post', - 'style' => 'display: inline', - ), - ''); + + if ($user && $user->getPHID()) { + // This ends up very early in tab order at the top of the page and there's + // a bunch of junk up there anyway, just shove it down here. + $foot_links[] = phabricator_render_form( + $user, + array( + 'action' => '/logout/', + 'method' => 'post', + 'style' => 'display: inline', + ), + ''); + } $foot_links = implode(' · ', $foot_links); return ($console ? '' : null). '
'. '
'. ''. ''. ''. ''. ''. $tabs. ''. '
'. phutil_render_tag( 'a', array( 'href' => $this->getBaseURI(), 'class' => 'phabricator-head-appname', ), phutil_escape_html($this->getApplicationName())). '
'. '
'. $this->bodyContent. '
'. '
'. '
'. $foot_links. '
'; } protected function getTail() { $response = CelerityAPI::getStaticResourceResponse(); return $response->renderResourcesOfType('js'). $response->renderHTMLFooter(); } } diff --git a/webroot/index.php b/webroot/index.php index 6a8898304f..37e22d8f3f 100644 --- a/webroot/index.php +++ b/webroot/index.php @@ -1,174 +1,187 @@ ', where '' ". "is one of 'development', 'production', or a custom environment."); } if (!function_exists('mysql_connect')) { phabricator_fatal_config_error( "The PHP MySQL extension is not installed. This extension is required."); } if (!isset($_REQUEST['__path__'])) { phabricator_fatal_config_error( "__path__ is not set. Your rewrite rules are not configured correctly."); } require_once dirname(dirname(__FILE__)).'/conf/__init_conf__.php'; $conf = phabricator_read_config_file($env); $conf['phabricator.env'] = $env; setup_aphront_basics(); phutil_require_module('phabricator', 'infrastructure/env'); PhabricatorEnv::setEnvConfig($conf); phutil_require_module('phabricator', 'aphront/console/plugin/xhprof/api'); DarkConsoleXHProfPluginAPI::hookProfiler(); phutil_require_module('phabricator', 'aphront/console/plugin/errorlog/api'); set_error_handler(array('DarkConsoleErrorLogPluginAPI', 'handleError')); set_exception_handler(array('DarkConsoleErrorLogPluginAPI', 'handleException')); foreach (PhabricatorEnv::getEnvConfig('load-libraries') as $library) { phutil_load_library($library); } $host = $_SERVER['HTTP_HOST']; $path = $_REQUEST['__path__']; switch ($host) { default: $config_key = 'aphront.default-application-configuration-class'; $config_class = PhabricatorEnv::getEnvConfig($config_key); PhutilSymbolLoader::loadClass($config_class); $application = newv($config_class, array()); break; } $application->setHost($host); $application->setPath($path); $application->willBuildRequest(); $request = $application->buildRequest(); $application->setRequest($request); list($controller, $uri_data) = $application->buildController(); try { $controller->willBeginExecution(); $controller->willProcessRequest($uri_data); $response = $controller->processRequest(); } catch (AphrontRedirectException $ex) { $response = id(new AphrontRedirectResponse()) ->setURI($ex->getURI()); } catch (Exception $ex) { $response = $application->handleException($ex); } $response = $application->willSendResponse($response); $response->setRequest($request); $response_string = $response->buildResponseString(); $code = $response->getHTTPResponseCode(); if ($code != 200) { header("HTTP/1.0 {$code}"); } $headers = $response->getCacheHeaders(); $headers = array_merge($headers, $response->getHeaders()); foreach ($headers as $header) { list($header, $value) = $header; header("{$header}: {$value}"); } // TODO: This shouldn't be possible in a production-configured environment. if (isset($_REQUEST['__profile__']) && ($_REQUEST['__profile__'] == 'all')) { $profile = DarkConsoleXHProfPluginAPI::stopProfiler(); $profile = ''; if (strpos($response_string, '') !== false) { $response_string = str_replace( '', ''.$profile, $response_string); } else { echo $profile; } } echo $response_string; /** * @group aphront */ function setup_aphront_basics() { $aphront_root = dirname(dirname(__FILE__)); $libraries_root = dirname($aphront_root); $root = null; if (!empty($_SERVER['PHUTIL_LIBRARY_ROOT'])) { $root = $_SERVER['PHUTIL_LIBRARY_ROOT']; } ini_set('include_path', $libraries_root.':'.ini_get('include_path')); @include_once $root.'libphutil/src/__phutil_library_init__.php'; if (!@constant('__LIBPHUTIL__')) { echo "ERROR: Unable to load libphutil. Update your PHP 'include_path' to ". "include the parent directory of libphutil/.\n"; exit(1); } // Load Phabricator itself using the absolute path, so we never end up doing // anything surprising (loading index.php and libraries from different // directories). phutil_load_library($aphront_root.'/src'); phutil_load_library('arcanist/src'); } function __autoload($class_name) { PhutilSymbolLoader::loadClass($class_name); } function phabricator_fatal_config_error($msg) { header('Content-Type: text/plain', $replace = true, $http_error = 500); $error = "CONFIG ERROR: ".$msg."\n"; error_log($error); echo $error; die(); } +/** + * Workaround for HipHop bug, see Facebook Task #503624. + */ +function hphp_workaround_call_user_func_array($func, array $array) { + $f = new ReflectionFunction($func); + return $f->invokeArgs($array); +} +