\d+)/$'
=> 'PhabricatorCountdownDeleteController'
),
+
+ '/feed/' => array(
+ '$' => 'PhabricatorFeedStreamController',
+ ),
);
}
diff --git a/src/applications/auth/controller/login/PhabricatorLoginController.php b/src/applications/auth/controller/login/PhabricatorLoginController.php
index 6736eb01f2..6980cac839 100644
--- a/src/applications/auth/controller/login/PhabricatorLoginController.php
+++ b/src/applications/auth/controller/login/PhabricatorLoginController.php
@@ -31,10 +31,17 @@ class PhabricatorLoginController extends PhabricatorAuthController {
}
$next_uri = $this->getRequest()->getPath();
- if ($next_uri == '/login/') {
- $next_uri = null;
+ $request->setCookie('next_uri', $next_uri);
+ if ($next_uri == '/login/' && !$request->isFormPost()) {
+ // The user went straight to /login/, so presumably they want to go
+ // to the dashboard upon logging in. Because, you know, that's logical.
+ // And people are logical. Sometimes... Fine, no they're not.
+ // We check for POST here because getPath() would get reset to /login/.
+ $request->setCookie('next_uri', '/');
}
+ // Always use $request->getCookie('next_uri', '/') after the above.
+
$password_auth = PhabricatorEnv::getEnvConfig('auth.password-auth-enabled');
$forms = array();
@@ -66,7 +73,7 @@ class PhabricatorLoginController extends PhabricatorAuthController {
$request->setCookie('phsid', $session_key);
return id(new AphrontRedirectResponse())
- ->setURI('/');
+ ->setURI($request->getCookie('next_uri', '/'));
} else {
$log = PhabricatorUserLog::newLog(
null,
@@ -93,7 +100,6 @@ class PhabricatorLoginController extends PhabricatorAuthController {
$form
->setUser($request->getUser())
->setAction('/login/')
- ->addHiddenInput('next', $next_uri)
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Username/Email')
@@ -115,8 +121,6 @@ class PhabricatorLoginController extends PhabricatorAuthController {
$forms['Phabricator Login'] = $form;
}
- $oauth_state = $next_uri;
-
$providers = array(
PhabricatorOAuthProvider::PROVIDER_FACEBOOK,
PhabricatorOAuthProvider::PROVIDER_GITHUB,
@@ -160,7 +164,6 @@ class PhabricatorLoginController extends PhabricatorAuthController {
->addHiddenInput('client_id', $client_id)
->addHiddenInput('redirect_uri', $redirect_uri)
->addHiddenInput('scope', $minimum_scope)
- ->addHiddenInput('state', $oauth_state)
->setUser($request->getUser())
->setMethod('GET')
->appendChild(
diff --git a/src/applications/auth/controller/oauth/PhabricatorOAuthLoginController.php b/src/applications/auth/controller/oauth/PhabricatorOAuthLoginController.php
index 6baf7fd6f4..bbcdad1538 100644
--- a/src/applications/auth/controller/oauth/PhabricatorOAuthLoginController.php
+++ b/src/applications/auth/controller/oauth/PhabricatorOAuthLoginController.php
@@ -135,12 +135,7 @@ class PhabricatorOAuthLoginController extends PhabricatorAuthController {
->setURI('/settings/page/'.$provider_key.'/');
}
- $next_uri = '/';
- if ($this->oauthState) {
- // Make sure a blind redirect to evil.com is impossible.
- $uri = new PhutilURI($this->oauthState);
- $next_uri = $uri->getPath();
- }
+ $next_uri = $request->getCookie('next_uri', '/');
// Login with known auth.
@@ -158,6 +153,7 @@ class PhabricatorOAuthLoginController extends PhabricatorAuthController {
$request->setCookie('phusr', $known_user->getUsername());
$request->setCookie('phsid', $session_key);
+ $request->clearCookie('next_uri');
return id(new AphrontRedirectResponse())
->setURI($next_uri);
}
diff --git a/src/applications/conduit/controller/api/PhabricatorConduitAPIController.php b/src/applications/conduit/controller/api/PhabricatorConduitAPIController.php
index 35a10d9d4b..cad7fa289c 100644
--- a/src/applications/conduit/controller/api/PhabricatorConduitAPIController.php
+++ b/src/applications/conduit/controller/api/PhabricatorConduitAPIController.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group conduit
+ */
class PhabricatorConduitAPIController
extends PhabricatorConduitController {
diff --git a/src/applications/conduit/controller/base/PhabricatorConduitController.php b/src/applications/conduit/controller/base/PhabricatorConduitController.php
index 2af8cdbd54..3bf54fa81f 100644
--- a/src/applications/conduit/controller/base/PhabricatorConduitController.php
+++ b/src/applications/conduit/controller/base/PhabricatorConduitController.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group conduit
+ */
abstract class PhabricatorConduitController extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
diff --git a/src/applications/conduit/controller/console/PhabricatorConduitConsoleController.php b/src/applications/conduit/controller/console/PhabricatorConduitConsoleController.php
index 4ffb6087fb..d0a2608b59 100644
--- a/src/applications/conduit/controller/console/PhabricatorConduitConsoleController.php
+++ b/src/applications/conduit/controller/console/PhabricatorConduitConsoleController.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group conduit
+ */
class PhabricatorConduitConsoleController
extends PhabricatorConduitController {
diff --git a/src/applications/conduit/controller/log/PhabricatorConduitLogController.php b/src/applications/conduit/controller/log/PhabricatorConduitLogController.php
index 0b9433cb11..74206aa9aa 100644
--- a/src/applications/conduit/controller/log/PhabricatorConduitLogController.php
+++ b/src/applications/conduit/controller/log/PhabricatorConduitLogController.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group conduit
+ */
class PhabricatorConduitLogController extends PhabricatorConduitController {
public function processRequest() {
diff --git a/src/applications/conduit/controller/token/PhabricatorConduitTokenController.php b/src/applications/conduit/controller/token/PhabricatorConduitTokenController.php
index 4fe6a55942..d340af76fd 100644
--- a/src/applications/conduit/controller/token/PhabricatorConduitTokenController.php
+++ b/src/applications/conduit/controller/token/PhabricatorConduitTokenController.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group conduit
+ */
class PhabricatorConduitTokenController extends PhabricatorConduitController {
public function processRequest() {
diff --git a/src/applications/conduit/method/base/ConduitAPIMethod.php b/src/applications/conduit/method/base/ConduitAPIMethod.php
index 5f38e4220f..4cf59a2148 100644
--- a/src/applications/conduit/method/base/ConduitAPIMethod.php
+++ b/src/applications/conduit/method/base/ConduitAPIMethod.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group conduit
+ */
abstract class ConduitAPIMethod {
abstract public function getMethodDescription();
@@ -63,4 +66,29 @@ abstract class ConduitAPIMethod {
return str_replace('_', '.', $method_fragment);
}
+ protected function validateHost($host) {
+ if (!$host) {
+ // If the client doesn't send a host key, don't complain. We should in
+ // the future, but this change isn't severe enough to bump the protocol
+ // version.
+
+ // TODO: Remove this once the protocol version gets bumped past 2 (i.e.,
+ // require the host key be present and valid).
+ return;
+ }
+
+ $host = new PhutilURI($host);
+ $host->setPath('/');
+ $host = (string)$host;
+
+ $self = PhabricatorEnv::getProductionURI('/');
+ if ($self !== $host) {
+ throw new Exception(
+ "Your client is connecting to this install as '{$host}', but it is ".
+ "configured as '{$self}'. The client and server must use the exact ".
+ "same URI to identify the install. Edit your .arcconfig or ".
+ "phabricator/conf so they agree on the URI for the install.");
+ }
+ }
+
}
diff --git a/src/applications/conduit/method/base/__init__.php b/src/applications/conduit/method/base/__init__.php
index f7268ea664..50c97a4d58 100644
--- a/src/applications/conduit/method/base/__init__.php
+++ b/src/applications/conduit/method/base/__init__.php
@@ -6,6 +6,9 @@
+phutil_require_module('phabricator', 'infrastructure/env');
+
+phutil_require_module('phutil', 'parser/uri');
phutil_require_module('phutil', 'utils');
diff --git a/src/applications/conduit/method/conduit/connect/ConduitAPI_conduit_connect_Method.php b/src/applications/conduit/method/conduit/connect/ConduitAPI_conduit_connect_Method.php
index b6723c7938..6e3e04d813 100644
--- a/src/applications/conduit/method/conduit/connect/ConduitAPI_conduit_connect_Method.php
+++ b/src/applications/conduit/method/conduit/connect/ConduitAPI_conduit_connect_Method.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group conduit
+ */
class ConduitAPI_conduit_connect_Method extends ConduitAPIMethod {
public function shouldRequireAuthentication() {
@@ -34,6 +37,7 @@ class ConduitAPI_conduit_connect_Method extends ConduitAPIMethod {
'user' => 'optional string',
'authToken' => 'optional int',
'authSignature' => 'optional string',
+ 'host' => 'required string',
);
}
@@ -67,6 +71,8 @@ class ConduitAPI_conduit_connect_Method extends ConduitAPIMethod {
protected function execute(ConduitAPIRequest $request) {
+ $this->validateHost($request->getValue('host'));
+
$client = $request->getValue('client');
$client_version = (int)$request->getValue('clientVersion');
$client_description = (string)$request->getValue('clientDescription');
diff --git a/src/applications/conduit/method/conduit/getcertificate/ConduitAPI_conduit_getcertificate_Method.php b/src/applications/conduit/method/conduit/getcertificate/ConduitAPI_conduit_getcertificate_Method.php
index 7c4101169e..4f24a08563 100644
--- a/src/applications/conduit/method/conduit/getcertificate/ConduitAPI_conduit_getcertificate_Method.php
+++ b/src/applications/conduit/method/conduit/getcertificate/ConduitAPI_conduit_getcertificate_Method.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group conduit
+ */
class ConduitAPI_conduit_getcertificate_Method extends ConduitAPIMethod {
public function shouldRequireAuthentication() {
@@ -29,6 +32,7 @@ class ConduitAPI_conduit_getcertificate_Method extends ConduitAPIMethod {
public function defineParamTypes() {
return array(
'token' => 'required string',
+ 'host' => 'required string',
);
}
@@ -46,6 +50,7 @@ class ConduitAPI_conduit_getcertificate_Method extends ConduitAPIMethod {
}
protected function execute(ConduitAPIRequest $request) {
+ $this->validateHost($request->getValue('host'));
$failed_attempts = PhabricatorUserLog::loadRecentEventsFromThisIP(
PhabricatorUserLog::ACTION_CONDUIT_CERTIFICATE_FAILURE,
diff --git a/src/applications/conduit/method/conduit/ping/ConduitAPI_conduit_ping_Method.php b/src/applications/conduit/method/conduit/ping/ConduitAPI_conduit_ping_Method.php
index 137348ea21..f3c33b6963 100644
--- a/src/applications/conduit/method/conduit/ping/ConduitAPI_conduit_ping_Method.php
+++ b/src/applications/conduit/method/conduit/ping/ConduitAPI_conduit_ping_Method.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group conduit
+ */
class ConduitAPI_conduit_ping_Method extends ConduitAPIMethod {
public function shouldRequireAuthentication() {
diff --git a/src/applications/conduit/method/daemon/launched/ConduitAPI_daemon_launched_Method.php b/src/applications/conduit/method/daemon/launched/ConduitAPI_daemon_launched_Method.php
index fb27edbfd4..6c7b3e02e2 100644
--- a/src/applications/conduit/method/daemon/launched/ConduitAPI_daemon_launched_Method.php
+++ b/src/applications/conduit/method/daemon/launched/ConduitAPI_daemon_launched_Method.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group conduit
+ */
class ConduitAPI_daemon_launched_Method extends ConduitAPIMethod {
public function shouldRequireAuthentication() {
diff --git a/src/applications/conduit/method/daemon/log/ConduitAPI_daemon_log_Method.php b/src/applications/conduit/method/daemon/log/ConduitAPI_daemon_log_Method.php
index 4847e4b260..f7ba004d09 100644
--- a/src/applications/conduit/method/daemon/log/ConduitAPI_daemon_log_Method.php
+++ b/src/applications/conduit/method/daemon/log/ConduitAPI_daemon_log_Method.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group conduit
+ */
class ConduitAPI_daemon_log_Method extends ConduitAPIMethod {
public function shouldRequireAuthentication() {
diff --git a/src/applications/conduit/method/differential/creatediff/ConduitAPI_differential_creatediff_Method.php b/src/applications/conduit/method/differential/creatediff/ConduitAPI_differential_creatediff_Method.php
index 324cb40d03..52342b534c 100644
--- a/src/applications/conduit/method/differential/creatediff/ConduitAPI_differential_creatediff_Method.php
+++ b/src/applications/conduit/method/differential/creatediff/ConduitAPI_differential_creatediff_Method.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group conduit
+ */
class ConduitAPI_differential_creatediff_Method extends ConduitAPIMethod {
public function getMethodDescription() {
diff --git a/src/applications/conduit/method/differential/createrevision/ConduitAPI_differential_createrevision_Method.php b/src/applications/conduit/method/differential/createrevision/ConduitAPI_differential_createrevision_Method.php
index f9f246448b..dd4ff2e4f2 100644
--- a/src/applications/conduit/method/differential/createrevision/ConduitAPI_differential_createrevision_Method.php
+++ b/src/applications/conduit/method/differential/createrevision/ConduitAPI_differential_createrevision_Method.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group conduit
+ */
class ConduitAPI_differential_createrevision_Method extends ConduitAPIMethod {
public function getMethodDescription() {
@@ -26,7 +29,6 @@ class ConduitAPI_differential_createrevision_Method extends ConduitAPIMethod {
return array(
'diffid' => 'required diffid',
'fields' => 'required dict',
- 'user' => 'required guid',
);
}
@@ -51,7 +53,7 @@ class ConduitAPI_differential_createrevision_Method extends ConduitAPIMethod {
$revision = DifferentialRevisionEditor::newRevisionFromConduitWithDiff(
$fields,
$diff,
- $request->getValue('user')); // TODO: Should be authoritative
+ $request->getUser()->getPHID());
return array(
'revisionid' => $revision->getID(),
diff --git a/src/applications/conduit/method/differential/find/ConduitAPI_differential_find_Method.php b/src/applications/conduit/method/differential/find/ConduitAPI_differential_find_Method.php
index 9983150496..a6084762c4 100644
--- a/src/applications/conduit/method/differential/find/ConduitAPI_differential_find_Method.php
+++ b/src/applications/conduit/method/differential/find/ConduitAPI_differential_find_Method.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group conduit
+ */
class ConduitAPI_differential_find_Method extends ConduitAPIMethod {
public function getMethodDescription() {
diff --git a/src/applications/conduit/method/differential/getalldiffs/ConduitAPI_differential_getalldiffs_Method.php b/src/applications/conduit/method/differential/getalldiffs/ConduitAPI_differential_getalldiffs_Method.php
index 93eb01d22b..527f0c146b 100644
--- a/src/applications/conduit/method/differential/getalldiffs/ConduitAPI_differential_getalldiffs_Method.php
+++ b/src/applications/conduit/method/differential/getalldiffs/ConduitAPI_differential_getalldiffs_Method.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group conduit
+ */
class ConduitAPI_differential_getalldiffs_Method extends ConduitAPIMethod {
public function getMethodDescription() {
diff --git a/src/applications/conduit/method/differential/getcommitmessage/ConduitAPI_differential_getcommitmessage_Method.php b/src/applications/conduit/method/differential/getcommitmessage/ConduitAPI_differential_getcommitmessage_Method.php
index 5818dfafe7..c24d901635 100644
--- a/src/applications/conduit/method/differential/getcommitmessage/ConduitAPI_differential_getcommitmessage_Method.php
+++ b/src/applications/conduit/method/differential/getcommitmessage/ConduitAPI_differential_getcommitmessage_Method.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group conduit
+ */
class ConduitAPI_differential_getcommitmessage_Method extends ConduitAPIMethod {
public function getMethodDescription() {
diff --git a/src/applications/conduit/method/differential/getcommitpaths/ConduitAPI_differential_getcommitpaths_Method.php b/src/applications/conduit/method/differential/getcommitpaths/ConduitAPI_differential_getcommitpaths_Method.php
index 8511b8fd54..cf39f4de71 100644
--- a/src/applications/conduit/method/differential/getcommitpaths/ConduitAPI_differential_getcommitpaths_Method.php
+++ b/src/applications/conduit/method/differential/getcommitpaths/ConduitAPI_differential_getcommitpaths_Method.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group conduit
+ */
class ConduitAPI_differential_getcommitpaths_Method extends ConduitAPIMethod {
public function getMethodDescription() {
diff --git a/src/applications/conduit/method/differential/getdiff/ConduitAPI_differential_getdiff_Method.php b/src/applications/conduit/method/differential/getdiff/ConduitAPI_differential_getdiff_Method.php
index 48428e18a2..f49ab8db04 100644
--- a/src/applications/conduit/method/differential/getdiff/ConduitAPI_differential_getdiff_Method.php
+++ b/src/applications/conduit/method/differential/getdiff/ConduitAPI_differential_getdiff_Method.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group conduit
+ */
class ConduitAPI_differential_getdiff_Method extends ConduitAPIMethod {
public function getMethodDescription() {
diff --git a/src/applications/conduit/method/differential/getrevision/ConduitAPI_differential_getrevision_Method.php b/src/applications/conduit/method/differential/getrevision/ConduitAPI_differential_getrevision_Method.php
index be5b6a016f..a633d3e6d1 100644
--- a/src/applications/conduit/method/differential/getrevision/ConduitAPI_differential_getrevision_Method.php
+++ b/src/applications/conduit/method/differential/getrevision/ConduitAPI_differential_getrevision_Method.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group conduit
+ */
class ConduitAPI_differential_getrevision_Method extends ConduitAPIMethod {
public function getMethodDescription() {
diff --git a/src/applications/conduit/method/differential/getrevisionfeedback/ConduitAPI_differential_getrevisionfeedback_Method.php b/src/applications/conduit/method/differential/getrevisionfeedback/ConduitAPI_differential_getrevisionfeedback_Method.php
index fcd351d309..253d1b8c90 100644
--- a/src/applications/conduit/method/differential/getrevisionfeedback/ConduitAPI_differential_getrevisionfeedback_Method.php
+++ b/src/applications/conduit/method/differential/getrevisionfeedback/ConduitAPI_differential_getrevisionfeedback_Method.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group conduit
+ */
class ConduitAPI_differential_getrevisionfeedback_Method
extends ConduitAPIMethod {
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 ccc959a173..d06253b663 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
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group conduit
+ */
class ConduitAPI_differential_markcommitted_Method extends ConduitAPIMethod {
public function getMethodDescription() {
diff --git a/src/applications/conduit/method/differential/parsecommitmessage/ConduitAPI_differential_parsecommitmessage_Method.php b/src/applications/conduit/method/differential/parsecommitmessage/ConduitAPI_differential_parsecommitmessage_Method.php
index 2e4058a4c6..b3feedabbb 100644
--- a/src/applications/conduit/method/differential/parsecommitmessage/ConduitAPI_differential_parsecommitmessage_Method.php
+++ b/src/applications/conduit/method/differential/parsecommitmessage/ConduitAPI_differential_parsecommitmessage_Method.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group conduit
+ */
class ConduitAPI_differential_parsecommitmessage_Method
extends ConduitAPIMethod {
diff --git a/src/applications/conduit/method/differential/setdiffproperty/ConduitAPI_differential_setdiffproperty_Method.php b/src/applications/conduit/method/differential/setdiffproperty/ConduitAPI_differential_setdiffproperty_Method.php
index 07afed39d6..44e1840227 100644
--- a/src/applications/conduit/method/differential/setdiffproperty/ConduitAPI_differential_setdiffproperty_Method.php
+++ b/src/applications/conduit/method/differential/setdiffproperty/ConduitAPI_differential_setdiffproperty_Method.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group conduit
+ */
class ConduitAPI_differential_setdiffproperty_Method extends ConduitAPIMethod {
public function getMethodDescription() {
diff --git a/src/applications/conduit/method/differential/updaterevision/ConduitAPI_differential_updaterevision_Method.php b/src/applications/conduit/method/differential/updaterevision/ConduitAPI_differential_updaterevision_Method.php
index 7acc5824c9..633761b4ed 100644
--- a/src/applications/conduit/method/differential/updaterevision/ConduitAPI_differential_updaterevision_Method.php
+++ b/src/applications/conduit/method/differential/updaterevision/ConduitAPI_differential_updaterevision_Method.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group conduit
+ */
class ConduitAPI_differential_updaterevision_Method extends ConduitAPIMethod {
public function getMethodDescription() {
diff --git a/src/applications/conduit/method/differential/updatetaskrevisionassoc/ConduitAPI_differential_updatetaskrevisionassoc_Method.php b/src/applications/conduit/method/differential/updatetaskrevisionassoc/ConduitAPI_differential_updatetaskrevisionassoc_Method.php
index 45a3a6829b..9bc2f54712 100644
--- a/src/applications/conduit/method/differential/updatetaskrevisionassoc/ConduitAPI_differential_updatetaskrevisionassoc_Method.php
+++ b/src/applications/conduit/method/differential/updatetaskrevisionassoc/ConduitAPI_differential_updatetaskrevisionassoc_Method.php
@@ -16,8 +16,12 @@
* limitations under the License.
*/
+/**
+ * @group conduit
+ */
class ConduitAPI_differential_updatetaskrevisionassoc_Method
extends ConduitAPIMethod {
+
public function getMethodDescription() {
return "Given a task together with its original and new associated ".
"revisions, update the revisions for their attached_tasks.";
diff --git a/src/applications/conduit/method/differential/updateunitresults/ConduitAPI_differential_updateunitresults_Method.php b/src/applications/conduit/method/differential/updateunitresults/ConduitAPI_differential_updateunitresults_Method.php
index 97eb8b3d84..5c2b498022 100644
--- a/src/applications/conduit/method/differential/updateunitresults/ConduitAPI_differential_updateunitresults_Method.php
+++ b/src/applications/conduit/method/differential/updateunitresults/ConduitAPI_differential_updateunitresults_Method.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group conduit
+ */
class ConduitAPI_differential_updateunitresults_Method
extends ConduitAPIMethod {
diff --git a/src/applications/conduit/method/diffusion/getcommits/ConduitAPI_diffusion_getcommits_Method.php b/src/applications/conduit/method/diffusion/getcommits/ConduitAPI_diffusion_getcommits_Method.php
index 29800e95de..e78db350eb 100644
--- a/src/applications/conduit/method/diffusion/getcommits/ConduitAPI_diffusion_getcommits_Method.php
+++ b/src/applications/conduit/method/diffusion/getcommits/ConduitAPI_diffusion_getcommits_Method.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group conduit
+ */
class ConduitAPI_diffusion_getcommits_Method extends ConduitAPIMethod {
public function getMethodDescription() {
diff --git a/src/applications/conduit/method/diffusion/getrecentcommitsbypath/ConduitAPI_diffusion_getrecentcommitsbypath_Method.php b/src/applications/conduit/method/diffusion/getrecentcommitsbypath/ConduitAPI_diffusion_getrecentcommitsbypath_Method.php
index 0bafc2aaa2..d5f9bb8378 100644
--- a/src/applications/conduit/method/diffusion/getrecentcommitsbypath/ConduitAPI_diffusion_getrecentcommitsbypath_Method.php
+++ b/src/applications/conduit/method/diffusion/getrecentcommitsbypath/ConduitAPI_diffusion_getrecentcommitsbypath_Method.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group conduit
+ */
class ConduitAPI_diffusion_getrecentcommitsbypath_Method
extends ConduitAPIMethod {
diff --git a/src/applications/conduit/method/feed/publish/ConduitAPI_feed_publish_Method.php b/src/applications/conduit/method/feed/publish/ConduitAPI_feed_publish_Method.php
new file mode 100644
index 0000000000..832350646e
--- /dev/null
+++ b/src/applications/conduit/method/feed/publish/ConduitAPI_feed_publish_Method.php
@@ -0,0 +1,65 @@
+ 'required string',
+ 'data' => 'required dict',
+ 'time' => 'optional int',
+ );
+ }
+
+ public function defineErrorTypes() {
+ return array(
+ );
+ }
+
+ public function defineReturnType() {
+ return 'nonempty phid';
+ }
+
+ protected function execute(ConduitAPIRequest $request) {
+ $type = $request->getValue('type');
+ $data = $request->getValue('data');
+ $time = $request->getValue('time');
+
+ $author_phid = $request->getUser()->getPHID();
+ $phids = array($author_phid);
+
+ $publisher = new PhabricatorFeedStoryPublisher();
+ $publisher->setStoryType($type);
+ $publisher->setStoryData($data);
+ $publisher->setStoryTime($time);
+ $publisher->setRelatedPHIDs($phids);
+ $publisher->setStoryAuthorPHID($author_phid);
+
+ $data = $publisher->publish();
+
+ return $data->getPHID();
+ }
+
+}
diff --git a/src/applications/conduit/method/feed/publish/__init__.php b/src/applications/conduit/method/feed/publish/__init__.php
new file mode 100644
index 0000000000..06f341680e
--- /dev/null
+++ b/src/applications/conduit/method/feed/publish/__init__.php
@@ -0,0 +1,13 @@
+events as $event) {
+
+ // Limit display log size. If a daemon gets stuck in an output loop this
+ // page can be like >100MB if we don't truncate stuff. Try to do cheap
+ // line-based truncation first, and fall back to expensive UTF-8 character
+ // truncation if that doesn't get things short enough.
+
+ $message = $event->getMessage();
+
+ $more_lines = null;
+ $more_chars = null;
+ $line_limit = 12;
+ if (substr_count($message, "\n") > $line_limit) {
+ $message = explode("\n", $message);
+ $more_lines = count($message) - $line_limit;
+ $message = array_slice($message, 0, $line_limit);
+ $message = implode("\n", $message);
+ }
+
+ $char_limit = 8192;
+ if (strlen($message) > $char_limit) {
+ $message = phutil_utf8v($message);
+ $more_chars = count($message) - $char_limit;
+ $message = array_slice($message, 0, $char_limit);
+ $message = implode('', $message);
+ }
+
+ $more = null;
+ if ($more_chars) {
+ $more = number_format($more_chars);
+ $more = "\n<... {$more} more characters ...>";
+ } else if ($more_lines) {
+ $more = number_format($more_lines);
+ $more = "\n<... {$more} more lines ...>";
+ }
+
$row = array(
phutil_escape_html($event->getLogType()),
phabricator_date($event->getEpoch(), $this->user),
phabricator_time($event->getEpoch(), $this->user),
- str_replace("\n", '
', phutil_escape_html($event->getMessage())),
+ str_replace("\n", '
', phutil_escape_html($message.$more)),
);
if ($this->combinedLog) {
diff --git a/src/applications/daemon/view/daemonlogevents/__init__.php b/src/applications/daemon/view/daemonlogevents/__init__.php
index dd4e977647..04be32163d 100644
--- a/src/applications/daemon/view/daemonlogevents/__init__.php
+++ b/src/applications/daemon/view/daemonlogevents/__init__.php
@@ -11,6 +11,7 @@ phutil_require_module('phabricator', 'view/control/table');
phutil_require_module('phabricator', 'view/utils');
phutil_require_module('phutil', 'markup');
+phutil_require_module('phutil', 'utils');
phutil_require_source('PhabricatorDaemonLogEventsView.php');
diff --git a/src/applications/differential/controller/revisionedit/DifferentialRevisionEditController.php b/src/applications/differential/controller/revisionedit/DifferentialRevisionEditController.php
index 3e1ce67956..7308e07a27 100644
--- a/src/applications/differential/controller/revisionedit/DifferentialRevisionEditController.php
+++ b/src/applications/differential/controller/revisionedit/DifferentialRevisionEditController.php
@@ -57,6 +57,7 @@ class DifferentialRevisionEditController extends DifferentialController {
$e_title = true;
$e_testplan = true;
+ $e_reviewers = null;
$errors = array();
$revision->loadRelationships();
@@ -71,17 +72,22 @@ class DifferentialRevisionEditController extends DifferentialController {
if (!strlen(trim($revision->getTitle()))) {
$errors[] = 'You must provide a title.';
$e_title = 'Required';
+ } else {
+ $e_title = null;
}
if (!strlen(trim($revision->getTestPlan()))) {
$errors[] = 'You must provide a test plan.';
$e_testplan = 'Required';
+ } else {
+ $e_testplan = null;
}
$user_phid = $request->getUser()->getPHID();
if (in_array($user_phid, $request->getArr('reviewers'))) {
$errors[] = 'You may not review your own revision.';
+ $e_reviewers = 'Invalid';
}
if (!$errors) {
@@ -172,6 +178,7 @@ class DifferentialRevisionEditController extends DifferentialController {
->setLabel('Reviewers')
->setName('reviewers')
->setDatasource('/typeahead/common/users/')
+ ->setError($e_reviewers)
->setValue($reviewer_map))
->appendChild(
id(new AphrontFormTokenizerControl())
diff --git a/src/applications/differential/mail/base/DifferentialMail.php b/src/applications/differential/mail/base/DifferentialMail.php
index 8ea77d456e..3b0b67c917 100644
--- a/src/applications/differential/mail/base/DifferentialMail.php
+++ b/src/applications/differential/mail/base/DifferentialMail.php
@@ -319,4 +319,12 @@ EOTEXT;
return $this->heraldTranscriptURI;
}
+ protected function renderHandleList(array $handles, array $phids) {
+ $names = array();
+ foreach ($phids as $phid) {
+ $names[] = $handles[$phid]->getName();
+ }
+ return implode(', ', $names);
+ }
+
}
diff --git a/src/applications/differential/mail/ccwelcome/DifferentialCCWelcomeMail.php b/src/applications/differential/mail/ccwelcome/DifferentialCCWelcomeMail.php
index 635f10c803..8d4b9ba00e 100644
--- a/src/applications/differential/mail/ccwelcome/DifferentialCCWelcomeMail.php
+++ b/src/applications/differential/mail/ccwelcome/DifferentialCCWelcomeMail.php
@@ -30,6 +30,7 @@ class DifferentialCCWelcomeMail extends DifferentialReviewRequestMail {
$body = array();
$body[] = "{$actor} added you to the CC list for the revision \"{$name}\".";
+ $body[] = $this->renderReviewersLine();
$body[] = null;
$body[] = $this->renderReviewRequestBody();
diff --git a/src/applications/differential/mail/comment/DifferentialCommentMail.php b/src/applications/differential/mail/comment/DifferentialCommentMail.php
index 060264b166..417f1e147b 100644
--- a/src/applications/differential/mail/comment/DifferentialCommentMail.php
+++ b/src/applications/differential/mail/comment/DifferentialCommentMail.php
@@ -71,6 +71,32 @@ class DifferentialCommentMail extends DifferentialMail {
$body = array();
$body[] = "{$actor} has {$verb} the revision \"{$name}\".";
+
+ // If the commented added reviewers or CCs, list them explicitly.
+ $meta = $comment->getMetadata();
+ $m_reviewers = idx(
+ $meta,
+ DifferentialComment::METADATA_ADDED_REVIEWERS,
+ array());
+ $m_cc = idx(
+ $meta,
+ DifferentialComment::METADATA_ADDED_CCS,
+ array());
+ $load = array_merge($m_reviewers, $m_cc);
+ if ($load) {
+ $handles = id(new PhabricatorObjectHandleData($load))->loadHandles();
+ if ($m_reviewers) {
+ $body[] = 'Added Reviewers: '.$this->renderHandleList(
+ $handles,
+ $m_reviewers);
+ }
+ if ($m_cc) {
+ $body[] = 'Added CCs: '.$this->renderHandleList(
+ $handles,
+ $m_cc);
+ }
+ }
+
$body[] = null;
$content = $comment->getContent();
diff --git a/src/applications/differential/mail/comment/__init__.php b/src/applications/differential/mail/comment/__init__.php
index f6f1281398..51b6e24688 100644
--- a/src/applications/differential/mail/comment/__init__.php
+++ b/src/applications/differential/mail/comment/__init__.php
@@ -9,6 +9,7 @@
phutil_require_module('phabricator', 'applications/differential/constants/action');
phutil_require_module('phabricator', 'applications/differential/constants/revisionstatus');
phutil_require_module('phabricator', 'applications/differential/mail/base');
+phutil_require_module('phabricator', 'applications/differential/storage/comment');
phutil_require_module('phabricator', 'applications/phid/handle/data');
phutil_require_module('phabricator', 'infrastructure/env');
diff --git a/src/applications/differential/mail/newdiff/DifferentialNewDiffMail.php b/src/applications/differential/mail/newdiff/DifferentialNewDiffMail.php
index 9b18f6fd67..6082be50fb 100644
--- a/src/applications/differential/mail/newdiff/DifferentialNewDiffMail.php
+++ b/src/applications/differential/mail/newdiff/DifferentialNewDiffMail.php
@@ -58,6 +58,7 @@ class DifferentialNewDiffMail extends DifferentialReviewRequestMail {
} else {
$body[] = "{$actor} updated the revision \"{$name}\".";
}
+ $body[] = $this->renderReviewersLine();
$body[] = null;
$body[] = $this->renderReviewRequestBody();
diff --git a/src/applications/differential/mail/reviewrequest/DifferentialReviewRequestMail.php b/src/applications/differential/mail/reviewrequest/DifferentialReviewRequestMail.php
index 0fc013363c..95ad604a5b 100644
--- a/src/applications/differential/mail/reviewrequest/DifferentialReviewRequestMail.php
+++ b/src/applications/differential/mail/reviewrequest/DifferentialReviewRequestMail.php
@@ -39,6 +39,12 @@ abstract class DifferentialReviewRequestMail extends DifferentialMail {
$this->setChangesets($changesets);
}
+ protected function renderReviewersLine() {
+ $reviewers = $this->getRevision()->getReviewers();
+ $handles = id(new PhabricatorObjectHandleData($reviewers))->loadHandles();
+ return 'Reviewers: '.$this->renderHandleList($handles, $reviewers);
+ }
+
protected function renderReviewRequestBody() {
$revision = $this->getRevision();
diff --git a/src/applications/differential/mail/reviewrequest/__init__.php b/src/applications/differential/mail/reviewrequest/__init__.php
index 2411be0156..2189b1648a 100644
--- a/src/applications/differential/mail/reviewrequest/__init__.php
+++ b/src/applications/differential/mail/reviewrequest/__init__.php
@@ -7,6 +7,9 @@
phutil_require_module('phabricator', 'applications/differential/mail/base');
+phutil_require_module('phabricator', 'applications/phid/handle/data');
+
+phutil_require_module('phutil', 'utils');
phutil_require_source('DifferentialReviewRequestMail.php');
diff --git a/src/applications/differential/parser/changeset/DifferentialChangesetParser.php b/src/applications/differential/parser/changeset/DifferentialChangesetParser.php
index fb6c5befa1..c43aaf9ec7 100644
--- a/src/applications/differential/parser/changeset/DifferentialChangesetParser.php
+++ b/src/applications/differential/parser/changeset/DifferentialChangesetParser.php
@@ -27,7 +27,6 @@ class DifferentialChangesetParser {
protected $parsedHunk = false;
protected $filename = null;
- protected $filetype = null;
protected $missingOld = array();
protected $missingNew = array();
@@ -161,10 +160,7 @@ class DifferentialChangesetParser {
public function setFilename($filename) {
$this->filename = $filename;
- if (strpos($filename, '.', 1) !== false) {
- $parts = explode('.', $filename);
- $this->filetype = end($parts);
- }
+ return $this;
}
public function setHandles(array $handles) {
@@ -341,6 +337,21 @@ class DifferentialChangesetParser {
switch ($this->whitespaceMode) {
case self::WHITESPACE_IGNORE_TRAILING:
if (rtrim($o_desc['text']) == rtrim($n_desc['text'])) {
+ if ($o_desc['type']) {
+ // If we're converting this into an unchanged line because of
+ // a trailing whitespace difference, mark it as a whitespace
+ // change so we can show "This file was modified only by
+ // adding or removing trailing whitespace." instead of
+ // "This file was not modified.".
+ $whitelines = true;
+ }
+ $similar = true;
+ }
+ break;
+ default:
+ // In this case, the lines are similar if there is no change type
+ // (that is, just trust the diff algorithm).
+ if (!$o_desc['type']) {
$similar = true;
}
break;
@@ -349,7 +360,6 @@ class DifferentialChangesetParser {
$o_desc['type'] = null;
$n_desc['type'] = null;
$skip_intra[count($old)] = true;
- $whitelines = true;
} else {
$changed = true;
}
@@ -700,7 +710,7 @@ class DifferentialChangesetParser {
foreach ($vector as $ii => $char) {
$result[] = $char;
if (isset($break_here[$ii])) {
- $result[] = "!
";
+ $result[] = "\xE2\xAC\x85
";
}
}
@@ -709,7 +719,7 @@ class DifferentialChangesetParser {
protected function getHighlightFuture($corpus) {
return $this->highlightEngine->getHighlightFuture(
- $this->filetype,
+ $this->highlightEngine->getLanguageFromFilename($this->filename),
$corpus);
}
@@ -744,24 +754,38 @@ class DifferentialChangesetParser {
$changeset->getFileType() == DifferentialChangeType::FILE_SYMLINK) {
if ($skip_cache || !$this->loadCache()) {
+ $ignore_all = ($this->whitespaceMode == self::WHITESPACE_IGNORE_ALL);
+
// The "ignore all whitespace" algorithm depends on rediffing the
// files, and we currently need complete representations of both
// files to do anything reasonable. If we only have parts of the files,
// don't use the "ignore all" algorithm.
- $can_use_ignore_all = true;
- $hunks = $changeset->getHunks();
- if (count($hunks) !== 1) {
- $can_use_ignore_all = false;
- } else {
- $first_hunk = reset($hunks);
- if ($first_hunk->getOldOffset() != 1 ||
- $first_hunk->getNewOffset() != 1) {
- $can_use_ignore_all = false;
+ if ($ignore_all) {
+ $hunks = $changeset->getHunks();
+ if (count($hunks) !== 1) {
+ $ignore_all = false;
+ } else {
+ $first_hunk = reset($hunks);
+ if ($first_hunk->getOldOffset() != 1 ||
+ $first_hunk->getNewOffset() != 1) {
+ $ignore_all = false;
+ }
}
}
- if ($this->whitespaceMode == self::WHITESPACE_IGNORE_ALL &&
- $can_use_ignore_all) {
+ if ($ignore_all) {
+ $old_file = $changeset->makeOldFile();
+ $new_file = $changeset->makeNewFile();
+ if ($old_file == $new_file) {
+ // If the old and new files are exactly identical, the synthetic
+ // diff below will give us nonsense and whitespace modes are
+ // irrelevant anyway. This occurs when you, e.g., copy a file onto
+ // itself in Subversion (see T271).
+ $ignore_all = false;
+ }
+ }
+
+ if ($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
@@ -773,8 +797,8 @@ class DifferentialChangesetParser {
$old_tmp = new TempFile();
$new_tmp = new TempFile();
- Filesystem::writeFile($old_tmp, $changeset->makeOldFile());
- Filesystem::writeFile($new_tmp, $changeset->makeNewFile());
+ Filesystem::writeFile($old_tmp, $old_file);
+ Filesystem::writeFile($new_tmp, $new_file);
list($err, $diff) = exec_manual(
'diff -bw -U65535 %s %s ',
$old_tmp,
@@ -849,10 +873,7 @@ EOSYNTHETIC;
$this->isTopLevel = (($range_start === null) && ($range_len === null));
- $this->highlightEngine = new PhutilDefaultSyntaxHighlighterEngine();
- $this->highlightEngine->setConfig(
- 'pygments.enabled',
- PhabricatorEnv::getEnvConfig('pygments.enabled'));
+ $this->highlightEngine = PhabricatorSyntaxHighlighter::newEngine();
$this->tryCacheStuff();
diff --git a/src/applications/differential/parser/changeset/__init__.php b/src/applications/differential/parser/changeset/__init__.php
index 34285ac975..7ca3982ed4 100644
--- a/src/applications/differential/parser/changeset/__init__.php
+++ b/src/applications/differential/parser/changeset/__init__.php
@@ -14,7 +14,7 @@ phutil_require_module('phabricator', 'applications/differential/storage/changese
phutil_require_module('phabricator', 'applications/differential/storage/diff');
phutil_require_module('phabricator', 'applications/differential/view/inlinecomment');
phutil_require_module('phabricator', 'applications/files/uri');
-phutil_require_module('phabricator', 'infrastructure/env');
+phutil_require_module('phabricator', 'applications/markup/syntax');
phutil_require_module('phabricator', 'infrastructure/javelin/markup');
phutil_require_module('phabricator', 'storage/queryfx');
@@ -24,7 +24,6 @@ phutil_require_module('phutil', 'filesystem/tempfile');
phutil_require_module('phutil', 'future');
phutil_require_module('phutil', 'future/exec');
phutil_require_module('phutil', 'markup');
-phutil_require_module('phutil', 'markup/syntax/engine/default');
phutil_require_module('phutil', 'utils');
diff --git a/src/applications/differential/replyhandler/DifferentialReplyHandler.php b/src/applications/differential/replyhandler/DifferentialReplyHandler.php
index ae7be54e1b..516ebd453c 100644
--- a/src/applications/differential/replyhandler/DifferentialReplyHandler.php
+++ b/src/applications/differential/replyhandler/DifferentialReplyHandler.php
@@ -31,6 +31,10 @@ class DifferentialReplyHandler extends PhabricatorMailReplyHandler {
return $this->getDefaultPrivateReplyHandlerEmailAddress($handle, 'D');
}
+ public function getPublicReplyHandlerEmailAddress() {
+ return $this->getDefaultPublicReplyHandlerEmailAddress('D');
+ }
+
public function getReplyHandlerDomain() {
return PhabricatorEnv::getEnvConfig(
'metamta.differential.reply-handler-domain');
diff --git a/src/applications/diffusion/controller/file/DiffusionBrowseFileController.php b/src/applications/diffusion/controller/file/DiffusionBrowseFileController.php
index c0b3402547..eac0ce27ff 100644
--- a/src/applications/diffusion/controller/file/DiffusionBrowseFileController.php
+++ b/src/applications/diffusion/controller/file/DiffusionBrowseFileController.php
@@ -179,13 +179,11 @@ class DiffusionBrowseFileController extends DiffusionController {
list($text_list, $rev_list, $blame_dict) = $file_query->getBlameData();
- $highlightEngine = new PhutilDefaultSyntaxHighlighterEngine();
- $highlightEngine->setConfig(
- 'pygments.enabled',
- PhabricatorEnv::getEnvConfig('pygments.enabled'));
-
- $text_list = explode("\n", $highlightEngine->highlightSource($path,
- implode("\n", $text_list)));
+ $text_list = implode("\n", $text_list);
+ $text_list = PhabricatorSyntaxHighlighter::highlightWithFilename(
+ $path,
+ $text_list);
+ $text_list = explode("\n", $text_list);
$rows = $this->buildDisplayRows($text_list, $rev_list, $blame_dict,
$needs_blame, $drequest, $file_query, $selected);
diff --git a/src/applications/diffusion/controller/file/__init__.php b/src/applications/diffusion/controller/file/__init__.php
index 8b61c73968..7bba4cebcd 100644
--- a/src/applications/diffusion/controller/file/__init__.php
+++ b/src/applications/diffusion/controller/file/__init__.php
@@ -8,13 +8,12 @@
phutil_require_module('phabricator', 'applications/diffusion/controller/base');
phutil_require_module('phabricator', 'applications/diffusion/query/filecontent/base');
+phutil_require_module('phabricator', 'applications/markup/syntax');
phutil_require_module('phabricator', 'infrastructure/celerity/api');
-phutil_require_module('phabricator', 'infrastructure/env');
phutil_require_module('phabricator', 'infrastructure/javelin/api');
phutil_require_module('phabricator', 'view/layout/panel');
phutil_require_module('phutil', 'markup');
-phutil_require_module('phutil', 'markup/syntax/engine/default');
phutil_require_module('phutil', 'utils');
diff --git a/src/applications/feed/controller/base/PhabricatorFeedController.php b/src/applications/feed/controller/base/PhabricatorFeedController.php
new file mode 100644
index 0000000000..3d81bbfd69
--- /dev/null
+++ b/src/applications/feed/controller/base/PhabricatorFeedController.php
@@ -0,0 +1,34 @@
+buildStandardPageView();
+
+ $page->setApplicationName('Feed');
+ $page->setBaseURI('/feed/');
+ $page->setTitle(idx($data, 'title'));
+ $page->setGlyph("\xE2\x88\x9E");
+ $page->appendChild($view);
+
+ $response = new AphrontWebpageResponse();
+ return $response->setContent($page->render());
+ }
+
+}
diff --git a/src/applications/feed/controller/base/__init__.php b/src/applications/feed/controller/base/__init__.php
new file mode 100644
index 0000000000..3ff17d1fb1
--- /dev/null
+++ b/src/applications/feed/controller/base/__init__.php
@@ -0,0 +1,15 @@
+execute();
+
+ $views = array();
+ foreach ($stories as $story) {
+ $views[] = $story->renderView();
+ }
+
+ return $this->buildStandardPageResponse(
+ $views,
+ array(
+ 'title' => 'Feed',
+ ));
+ }
+}
diff --git a/src/applications/feed/controller/stream/__init__.php b/src/applications/feed/controller/stream/__init__.php
new file mode 100644
index 0000000000..0005773ff0
--- /dev/null
+++ b/src/applications/feed/controller/stream/__init__.php
@@ -0,0 +1,13 @@
+relatedPHIDs = $phids;
+ return $this;
+ }
+
+ public function setStoryType($story_type) {
+ $this->storyType = $story_type;
+ return $this;
+ }
+
+ public function setStoryData(array $data) {
+ $this->storyData = $data;
+ return $this;
+ }
+
+ public function setStoryTime($time) {
+ $this->storyTime = $time;
+ return $this;
+ }
+
+ public function setStoryAuthorPHID($phid) {
+ $this->storyAuthorPHID = $phid;
+ return $this;
+ }
+
+ public function publish() {
+ if (!$this->relatedPHIDs) {
+ throw new Exception("There are no PHIDs related to this story!");
+ }
+
+ if (!$this->storyType) {
+ throw new Exception("Call setStoryType() before publishing!");
+ }
+
+ $chrono_key = $this->generateChronologicalKey();
+
+ $story = new PhabricatorFeedStoryData();
+ $story->setStoryType($this->storyType);
+ $story->setStoryData($this->storyData);
+ $story->setAuthorPHID($this->storyAuthorPHID);
+ $story->setChronologicalKey($chrono_key);
+ $story->save();
+
+ $ref = new PhabricatorFeedStoryReference();
+
+ $sql = array();
+ $conn = $ref->establishConnection('w');
+ foreach ($this->relatedPHIDs as $phid) {
+ $sql[] = qsprintf(
+ $conn,
+ '(%s, %s)',
+ $phid,
+ $chrono_key);
+ }
+
+ queryfx(
+ $conn,
+ 'INSERT INTO %T (objectPHID, chronologicalKey) VALUES %Q',
+ $ref->getTableName(),
+ implode(', ', $sql));
+
+ return $story;
+ }
+
+ /**
+ * We generate a unique chronological key for each story type because we want
+ * to be able to page through the stream with a cursor (i.e., select stories
+ * after ID = X) so we can efficiently perform filtering after selecting data,
+ * and multiple stories with the same ID make this cumbersome without putting
+ * a bunch of logic in the client. We could use the primary key, but that
+ * would prevent publishing stories which happened in the past. Since it's
+ * potentially useful to do that (e.g., if you're importing another data
+ * source) build a unique key for each story which has chronological ordering.
+ *
+ * @return string A unique, time-ordered key which identifies the story.
+ */
+ private function generateChronologicalKey() {
+ // Use the epoch timestamp for the upper 32 bits of the key. Default to
+ // the current time if the story doesn't have an explicit timestamp.
+ $time = nonempty($this->storyTime, time());
+
+ // Generate a random number for the lower 32 bits of the key.
+ $rand = head(unpack('L', Filesystem::readRandomBytes(4)));
+
+ return ($time << 32) + ($rand);
+ }
+}
diff --git a/src/applications/feed/publisher/__init__.php b/src/applications/feed/publisher/__init__.php
new file mode 100644
index 0000000000..ccdcf4029d
--- /dev/null
+++ b/src/applications/feed/publisher/__init__.php
@@ -0,0 +1,18 @@
+filterPHIDs = $phids;
+ return $this;
+ }
+
+ public function setLimit($limit) {
+ $this->limit = $limit;
+ return $this;
+ }
+
+ public function setAfter($after) {
+ $this->after = $after;
+ return $this;
+ }
+
+ public function execute() {
+
+ $ref_table = new PhabricatorFeedStoryReference();
+ $story_table = new PhabricatorFeedStoryData();
+
+ $conn = $story_table->establishConnection('r');
+
+ $where = array();
+ if ($this->filterPHIDs) {
+ $where[] = qsprintf(
+ $conn,
+ 'ref.objectPHID IN (%Ls)',
+ $this->filterPHIDs);
+ }
+
+ if ($where) {
+ $where = 'WHERE ('.implode(') AND (', $where).')';
+ } else {
+ $where = '';
+ }
+
+ $data = queryfx_all(
+ $conn,
+ 'SELECT story.* FROM %T ref
+ JOIN %T story ON ref.chronologicalKey = story.chronologicalKey
+ %Q
+ GROUP BY story.chronologicalKey
+ ORDER BY story.chronologicalKey DESC
+ LIMIT %d',
+ $ref_table->getTableName(),
+ $story_table->getTableName(),
+ $where,
+ $this->limit);
+ $data = $story_table->loadAllFromArray($data);
+
+ $stories = array();
+ foreach ($data as $story_data) {
+ $class = $story_data->getStoryType();
+
+ try {
+ if (!class_exists($class) ||
+ !is_subclass_of($class, 'PhabricatorFeedStory')) {
+ $class = 'PhabricatorFeedStoryUnknown';
+ }
+ } catch (PhutilMissingSymbolException $ex) {
+ // If the class can't be loaded, libphutil will throw an exception.
+ // Render the story using the unknown story view.
+ $class = 'PhabricatorFeedStoryUnknown';
+ }
+
+ $stories[] = newv($class, array($story_data));
+ }
+
+ return $stories;
+ }
+}
diff --git a/src/applications/feed/query/__init__.php b/src/applications/feed/query/__init__.php
new file mode 100644
index 0000000000..4d574245d8
--- /dev/null
+++ b/src/applications/feed/query/__init__.php
@@ -0,0 +1,17 @@
+ true,
+ self::CONFIG_SERIALIZATION => array(
+ 'storyData' => self::SERIALIZATION_JSON,
+ ),
+ ) + parent::getConfiguration();
+ }
+
+ public function generatePHID() {
+ return PhabricatorPHID::generateNewPHID(
+ PhabricatorPHIDConstants::PHID_TYPE_STRY);
+ }
+
+}
diff --git a/src/applications/feed/storage/story/__init__.php b/src/applications/feed/storage/story/__init__.php
new file mode 100644
index 0000000000..9da164a706
--- /dev/null
+++ b/src/applications/feed/storage/story/__init__.php
@@ -0,0 +1,14 @@
+ self::IDS_MANUAL,
+ self::CONFIG_TIMESTAMPS => false,
+ ) + parent::getConfiguration();
+ }
+
+}
diff --git a/src/applications/feed/storage/storyreference/__init__.php b/src/applications/feed/storage/storyreference/__init__.php
new file mode 100644
index 0000000000..a0921444f3
--- /dev/null
+++ b/src/applications/feed/storage/storyreference/__init__.php
@@ -0,0 +1,12 @@
+data = $data;
+ }
+
+ abstract public function renderView();
+
+}
diff --git a/src/applications/feed/story/base/__init__.php b/src/applications/feed/story/base/__init__.php
new file mode 100644
index 0000000000..96e61a9589
--- /dev/null
+++ b/src/applications/feed/story/base/__init__.php
@@ -0,0 +1,10 @@
+ 'border: 1px dashed black; '.
+ 'padding: 1em; margin: 1em; '.
+ 'background: #ffeedd;',
+ ),
+ 'This is a feed story!');
+ }
+
+}
diff --git a/src/applications/feed/view/story/__init__.php b/src/applications/feed/view/story/__init__.php
new file mode 100644
index 0000000000..df6094318e
--- /dev/null
+++ b/src/applications/feed/view/story/__init__.php
@@ -0,0 +1,14 @@
+setFieldName($condition[0]);
$obj->setFieldCondition($condition[1]);
@@ -149,6 +155,11 @@ class HeraldRuleController extends HeraldController {
$actions = array();
foreach ($data['actions'] as $action) {
+ if ($action === null) {
+ // Sparse on the client; removals can give us NULLs.
+ continue;
+ }
+
$obj = new HeraldAction();
$obj->setAction($action[0]);
diff --git a/src/applications/herald/controller/transcript/HeraldTranscriptController.php b/src/applications/herald/controller/transcript/HeraldTranscriptController.php
index ced383d98d..bf9f0fcd5d 100644
--- a/src/applications/herald/controller/transcript/HeraldTranscriptController.php
+++ b/src/applications/herald/controller/transcript/HeraldTranscriptController.php
@@ -42,40 +42,45 @@ class HeraldTranscriptController extends HeraldController {
throw new Exception('Uknown transcript!');
}
- $field_names = HeraldFieldConfig::getFieldMap();
- $condition_names = HeraldConditionConfig::getConditionMap();
- $action_names = HeraldActionConfig::getActionMap();
-
require_celerity_resource('herald-test-css');
- $filter = $this->getFilterPHIDs();
- $this->filterTranscript($xscript, $filter);
- $phids = array_merge($filter, $this->getTranscriptPHIDs($xscript));
- $phids = array_unique($phids);
- $phids = array_filter($phids);
-
- $handles = id(new PhabricatorObjectHandleData($phids))
- ->loadHandles();
- $this->handles = $handles;
-
- $object_xscript = $xscript->getObjectTranscript();
-
$nav = $this->buildSideNav();
- $apply_xscript_panel = $this->buildApplyTranscriptPanel(
- $xscript);
- $nav->appendChild($apply_xscript_panel);
+ $object_xscript = $xscript->getObjectTranscript();
+ if (!$object_xscript) {
+ $notice = id(new AphrontErrorView())
+ ->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
+ ->setTitle('Old Transcript')
+ ->appendChild(
+ 'Details of this transcript have been garbage collected.
');
+ $nav->appendChild($notice);
+ } else {
+ $filter = $this->getFilterPHIDs();
+ $this->filterTranscript($xscript, $filter);
+ $phids = array_merge($filter, $this->getTranscriptPHIDs($xscript));
+ $phids = array_unique($phids);
+ $phids = array_filter($phids);
- $action_xscript_panel = $this->buildActionTranscriptPanel(
- $xscript);
- $nav->appendChild($action_xscript_panel);
+ $handles = id(new PhabricatorObjectHandleData($phids))
+ ->loadHandles();
+ $this->handles = $handles;
- $object_xscript_panel = $this->buildObjectTranscriptPanel(
- $xscript);
- $nav->appendChild($object_xscript_panel);
+ $apply_xscript_panel = $this->buildApplyTranscriptPanel(
+ $xscript);
+ $nav->appendChild($apply_xscript_panel);
+
+ $action_xscript_panel = $this->buildActionTranscriptPanel(
+ $xscript);
+ $nav->appendChild($action_xscript_panel);
+
+ $object_xscript_panel = $this->buildObjectTranscriptPanel(
+ $xscript);
+ $nav->appendChild($object_xscript_panel);
+ }
/*
+ TODO
$notice = null;
if ($xscript->getDryRun()) {
@@ -84,30 +89,6 @@ class HeraldTranscriptController extends HeraldController {
This was a dry run to test Herald rules, no actions were executed.
;
}
-
- if (!$object_xscript) {
- $notice =
-
-
- Details of this transcript have been discarded. Full transcripts
- are retained for 30 days.
-
- {$notice}
- ;
- }
-
-
- return
-
-
- renderNavItems()}>
- {$notice}
- {$apply_xscript_markup}
- {$rule_table}
- {$object_xscript_table}
-
-
- ;
*/
return $this->buildStandardPageResponse(
@@ -264,7 +245,7 @@ class HeraldTranscriptController extends HeraldController {
foreach ($xscript->getApplyTranscripts() as $id => $apply_xscript) {
$rule_id = $apply_xscript->getRuleID();
if ($filter_owned) {
- if (!$rule_xscripts[$rule_id]) {
+ if (empty($rule_xscripts[$rule_id])) {
// No associated rule so you can't own this effect.
continue;
}
diff --git a/src/applications/herald/controller/transcript/__init__.php b/src/applications/herald/controller/transcript/__init__.php
index 81bfe3385f..3208b3b1e0 100644
--- a/src/applications/herald/controller/transcript/__init__.php
+++ b/src/applications/herald/controller/transcript/__init__.php
@@ -14,6 +14,7 @@ phutil_require_module('phabricator', 'applications/herald/storage/transcript/bas
phutil_require_module('phabricator', 'applications/phid/handle/data');
phutil_require_module('phabricator', 'infrastructure/celerity/api');
phutil_require_module('phabricator', 'view/control/table');
+phutil_require_module('phabricator', 'view/form/error');
phutil_require_module('phabricator', 'view/layout/panel');
phutil_require_module('phabricator', 'view/layout/sidenav');
diff --git a/src/applications/maniphest/constants/base/ManiphestConstants.php b/src/applications/maniphest/constants/base/ManiphestConstants.php
new file mode 100644
index 0000000000..7e12478621
--- /dev/null
+++ b/src/applications/maniphest/constants/base/ManiphestConstants.php
@@ -0,0 +1,24 @@
+getID() && $email_create) {
+ $email_hint = 'You can also create tasks by sending an email to: '.
+ ''.phutil_escape_html($email_create).'';
+ }
+
$form
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Description')
->setName('description')
+ ->setCaption($email_hint)
->setValue($task->getDescription()))
->appendChild(
id(new AphrontFormSubmitControl())
diff --git a/src/applications/maniphest/controller/taskedit/__init__.php b/src/applications/maniphest/controller/taskedit/__init__.php
index d524917ca9..a12c754262 100644
--- a/src/applications/maniphest/controller/taskedit/__init__.php
+++ b/src/applications/maniphest/controller/taskedit/__init__.php
@@ -19,6 +19,7 @@ phutil_require_module('phabricator', 'applications/maniphest/storage/transaction
phutil_require_module('phabricator', 'applications/phid/constants');
phutil_require_module('phabricator', 'applications/phid/handle/data');
phutil_require_module('phabricator', 'infrastructure/celerity/api');
+phutil_require_module('phabricator', 'infrastructure/env');
phutil_require_module('phabricator', 'infrastructure/javelin/api');
phutil_require_module('phabricator', 'infrastructure/javelin/markup');
phutil_require_module('phabricator', 'view/form/base');
diff --git a/src/applications/maniphest/controller/tasklist/ManiphestTaskListController.php b/src/applications/maniphest/controller/tasklist/ManiphestTaskListController.php
index 72250e2d1a..7303c65dd2 100644
--- a/src/applications/maniphest/controller/tasklist/ManiphestTaskListController.php
+++ b/src/applications/maniphest/controller/tasklist/ManiphestTaskListController.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group maniphest
+ */
class ManiphestTaskListController extends ManiphestController {
const DEFAULT_PAGE_SIZE = 1000;
@@ -33,23 +36,31 @@ class ManiphestTaskListController extends ManiphestController {
$uri = $request->getRequestURI();
if ($request->isFormPost()) {
- $phid_arr = $request->getArr('view_user');
- $view_target = head($phid_arr);
- return id(new AphrontRedirectResponse())
- ->setURI($request->getRequestURI()->alter('phid', $view_target));
- }
+ // Redirect to GET so URIs can be copy/pasted.
+ $user_phids = $request->getArr('set_users');
+ $proj_phids = $request->getArr('set_projects');
+ $user_phids = implode(',', $user_phids);
+ $proj_phids = implode(',', $proj_phids);
+ $user_phids = nonempty($user_phids, null);
+ $proj_phids = nonempty($proj_phids, null);
+
+ $uri = $request->getRequestURI()
+ ->alter('users', $user_phids)
+ ->alter('projects', $proj_phids);
+
+ return id(new AphrontRedirectResponse())->setURI($uri);
+ }
$views = array(
'User Tasks',
- 'action' => 'Assigned',
- 'created' => 'Created',
- 'triage' => 'Need Triage',
-// 'touched' => 'Touched',
+ 'action' => 'Assigned',
+ 'created' => 'Created',
+ 'subscribed' => 'Subscribed',
+ 'triage' => 'Need Triage',
'
',
'All Tasks',
'alltriage' => 'Need Triage',
- 'unassigned' => 'Unassigned',
'all' => 'All Tasks',
);
@@ -60,6 +71,7 @@ class ManiphestTaskListController extends ManiphestController {
$has_filter = array(
'action' => true,
'created' => true,
+ 'subscribed' => true,
'triage' => true,
);
@@ -77,7 +89,7 @@ class ManiphestTaskListController extends ManiphestController {
phutil_render_tag(
'a',
array(
- 'href' => $uri,
+ 'href' => $uri->alter('page', null),
'class' => ($this->view == $view)
? 'aphront-side-nav-selected'
: null,
@@ -90,13 +102,26 @@ class ManiphestTaskListController extends ManiphestController {
list($grouping, $group_links) = $this->renderGroupLinks();
list($order, $order_links) = $this->renderOrderLinks();
- $view_phid = nonempty($request->getStr('phid'), $user->getPHID());
+ $user_phids = $request->getStr('users');
+ if (strlen($user_phids)) {
+ $user_phids = explode(',', $user_phids);
+ } else {
+ $user_phids = array($user->getPHID());
+ }
+
+ $project_phids = $request->getStr('projects');
+ if (strlen($project_phids)) {
+ $project_phids = explode(',', $project_phids);
+ } else {
+ $project_phids = array();
+ }
$page = $request->getInt('page');
$page_size = self::DEFAULT_PAGE_SIZE;
list($tasks, $handles, $total_count) = $this->loadTasks(
- $view_phid,
+ $user_phids,
+ $project_phids,
array(
'status' => $status_map,
'group' => $grouping,
@@ -105,24 +130,34 @@ class ManiphestTaskListController extends ManiphestController {
'limit' => $page_size,
));
-
$form = id(new AphrontFormView())
- ->setUser($user);
+ ->setUser($user)
+ ->setAction($request->getRequestURI());
if (isset($has_filter[$this->view])) {
+ $tokens = array();
+ foreach ($user_phids as $phid) {
+ $tokens[$phid] = $handles[$phid]->getFullName();
+ }
$form->appendChild(
id(new AphrontFormTokenizerControl())
- ->setLimit(1)
->setDatasource('/typeahead/common/searchowner/')
- ->setName('view_user')
- ->setLabel('View User')
- ->setCaption('Use "upforgrabs" to find unassigned tasks.')
- ->setValue(
- array(
- $view_phid => $handles[$view_phid]->getFullName(),
- )));
+ ->setName('set_users')
+ ->setLabel('Users')
+ ->setValue($tokens));
}
+ $tokens = array();
+ foreach ($project_phids as $phid) {
+ $tokens[$phid] = $handles[$phid]->getFullName();
+ }
+ $form->appendChild(
+ id(new AphrontFormTokenizerControl())
+ ->setDatasource('/typeahead/common/projects/')
+ ->setName('set_projects')
+ ->setLabel('Projects')
+ ->setValue($tokens));
+
$form
->appendChild(
id(new AphrontFormToggleButtonsControl())
@@ -137,6 +172,10 @@ class ManiphestTaskListController extends ManiphestController {
->setLabel('Order')
->setValue($order_links));
+ $form->appendChild(
+ id(new AphrontFormSubmitControl())
+ ->setValue('Filter Tasks'));
+
$filter = new AphrontListFilterView();
$filter->addButton(
phutil_render_tag(
@@ -209,141 +248,74 @@ class ManiphestTaskListController extends ManiphestController {
));
}
- private function loadTasks($view_phid, array $dict) {
- $phids = array($view_phid);
+ private function loadTasks(
+ array $user_phids,
+ array $project_phids,
+ array $dict) {
- $include_upforgrabs = false;
- foreach ($phids as $key => $phid) {
- if ($phid == ManiphestTaskOwner::OWNER_UP_FOR_GRABS) {
- unset($phids[$key]);
- $include_upforgrabs = true;
- }
- }
-
- $task = new ManiphestTask();
-
- $argv = array();
+ $query = new ManiphestTaskQuery();
+ $query->withProjects($project_phids);
$status = $dict['status'];
if (!empty($status['open']) && !empty($status['closed'])) {
- $status_clause = '1 = 1';
+ $query->withStatus(ManiphestTaskQuery::STATUS_ANY);
} else if (!empty($status['open'])) {
- $status_clause = 'status = %d';
- $argv[] = 0;
+ $query->withStatus(ManiphestTaskQuery::STATUS_OPEN);
} else {
- $status_clause = 'status > %d';
- $argv[] = 0;
+ $query->withStatus(ManiphestTaskQuery::STATUS_CLOSED);
}
- $extra_clause = '1 = 1';
switch ($this->view) {
case 'action':
- $parts = array();
- if ($phids) {
- $parts[] = 'ownerPHID in (%Ls)';
- $argv[] = $phids;
- }
- if ($include_upforgrabs) {
- $parts[] = 'ownerPHID IS NULL';
- }
- $extra_clause = '('.implode(' OR ', $parts).')';
+ $query->withOwners($user_phids);
break;
case 'created':
- $parts = array();
- if ($phids) {
- $parts[] = 'authorPHID in (%Ls)';
- $argv[] = $phids;
- }
- if ($include_upforgrabs) {
- // This should be impossible since every task is supposed to have a
- // valid author, but we might as well run the query.
- $parts[] = 'authorPHID IS NULL';
- }
- $extra_clause = '('.implode(' OR ', $parts).')';
+ $query->withAuthors($user_phids);
+ break;
+ case 'subscribed':
+ $query->withSubscribers($user_phids);
break;
case 'triage':
- $parts = array();
- if ($phids) {
- $parts[] = 'ownerPHID in (%Ls)';
- $argv[] = $phids;
- }
- if ($include_upforgrabs) {
- $parts[] = 'ownerPHID IS NULL';
- }
- $extra_clause = '('.implode(' OR ', $parts).') AND priority = %d';
- $argv[] = ManiphestTaskPriority::PRIORITY_TRIAGE;
+ $query->withOwners($user_phids);
+ $query->withPriority(ManiphestTaskPriority::PRIORITY_TRIAGE);
break;
case 'alltriage':
- $extra_clause = 'priority = %d';
- $argv[] = ManiphestTaskPriority::PRIORITY_TRIAGE;
- break;
- case 'unassigned':
- $extra_clause = 'ownerPHID is NULL';
+ $query->withPriority(ManiphestTaskPriority::PRIORITY_TRIAGE);
break;
case 'all':
break;
}
- $order = array();
- switch ($dict['group']) {
- case 'priority':
- $order[] = 'priority';
- break;
- case 'owner':
- $order[] = 'ownerOrdering';
- break;
- case 'status':
- $order[] = 'status';
- break;
- }
+ $order_map = array(
+ 'priority' => ManiphestTaskQuery::ORDER_PRIORITY,
+ 'created' => ManiphestTaskQuery::ORDER_CREATED,
+ );
+ $query->setOrderBy(
+ idx(
+ $order_map,
+ $dict['order'],
+ ManiphestTaskQuery::ORDER_MODIFIED));
- switch ($dict['order']) {
- case 'priority':
- $order[] = 'priority';
- $order[] = 'dateModified';
- break;
- case 'created':
- $order[] = 'id';
- break;
- default:
- $order[] = 'dateModified';
- break;
- }
+ $group_map = array(
+ 'priority' => ManiphestTaskQuery::GROUP_PRIORITY,
+ 'owner' => ManiphestTaskQuery::GROUP_OWNER,
+ 'status' => ManiphestTaskQuery::GROUP_STATUS,
+ );
+ $query->setGroupBy(
+ idx(
+ $group_map,
+ $dict['group'],
+ ManiphestTaskQuery::GROUP_NONE));
- $order = array_unique($order);
+ $query->setCalculateRows(true);
+ $query->setLimit($dict['limit']);
+ $query->setOffset($dict['offset']);
- foreach ($order as $k => $column) {
- switch ($column) {
- case 'ownerOrdering':
- $order[$k] = "{$column} ASC";
- break;
- default:
- $order[$k] = "{$column} DESC";
- break;
- }
- }
-
- $order = implode(', ', $order);
-
- $offset = (int)idx($dict, 'offset', 0);
- $limit = (int)idx($dict, 'limit', self::DEFAULT_PAGE_SIZE);
-
- $sql = "SELECT SQL_CALC_FOUND_ROWS * FROM %T WHERE ".
- "({$status_clause}) AND ({$extra_clause}) ORDER BY {$order} ".
- "LIMIT {$offset}, {$limit}";
-
- array_unshift($argv, $task->getTableName());
-
- $conn = $task->establishConnection('r');
- $data = vqueryfx_all($conn, $sql, $argv);
-
- $total_row_count = queryfx_one($conn, 'SELECT FOUND_ROWS() N');
- $total_row_count = $total_row_count['N'];
-
- $data = $task->loadAllFromArray($data);
+ $data = $query->execute();
+ $total_row_count = $query->getRowCount();
$handle_phids = mpull($data, 'getOwnerPHID');
- $handle_phids[] = $view_phid;
+ $handle_phids = array_merge($handle_phids, $project_phids, $user_phids);
$handles = id(new PhabricatorObjectHandleData($handle_phids))
->loadHandles();
diff --git a/src/applications/maniphest/controller/tasklist/__init__.php b/src/applications/maniphest/controller/tasklist/__init__.php
index 16e6296085..28c7ec5a4c 100644
--- a/src/applications/maniphest/controller/tasklist/__init__.php
+++ b/src/applications/maniphest/controller/tasklist/__init__.php
@@ -7,17 +7,16 @@
phutil_require_module('phabricator', 'aphront/response/redirect');
-phutil_require_module('phabricator', 'applications/maniphest/constants/owner');
phutil_require_module('phabricator', 'applications/maniphest/constants/priority');
phutil_require_module('phabricator', 'applications/maniphest/constants/status');
phutil_require_module('phabricator', 'applications/maniphest/controller/base');
-phutil_require_module('phabricator', 'applications/maniphest/storage/task');
+phutil_require_module('phabricator', 'applications/maniphest/query');
phutil_require_module('phabricator', 'applications/maniphest/view/tasklist');
phutil_require_module('phabricator', 'applications/phid/handle/data');
phutil_require_module('phabricator', 'infrastructure/celerity/api');
-phutil_require_module('phabricator', 'storage/queryfx');
phutil_require_module('phabricator', 'view/control/pager');
phutil_require_module('phabricator', 'view/form/base');
+phutil_require_module('phabricator', 'view/form/control/submit');
phutil_require_module('phabricator', 'view/form/control/togglebuttons');
phutil_require_module('phabricator', 'view/form/control/tokenizer');
phutil_require_module('phabricator', 'view/layout/listfilter');
diff --git a/src/applications/maniphest/controller/transactionpreview/ManiphestTransactionPreviewController.php b/src/applications/maniphest/controller/transactionpreview/ManiphestTransactionPreviewController.php
index 6d63ef761e..a830d5f0cc 100644
--- a/src/applications/maniphest/controller/transactionpreview/ManiphestTransactionPreviewController.php
+++ b/src/applications/maniphest/controller/transactionpreview/ManiphestTransactionPreviewController.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group maniphest
+ */
class ManiphestTransactionPreviewController extends ManiphestController {
private $id;
diff --git a/src/applications/maniphest/controller/transactionsave/ManiphestTransactionSaveController.php b/src/applications/maniphest/controller/transactionsave/ManiphestTransactionSaveController.php
index 9284a8d027..99e95f6088 100644
--- a/src/applications/maniphest/controller/transactionsave/ManiphestTransactionSaveController.php
+++ b/src/applications/maniphest/controller/transactionsave/ManiphestTransactionSaveController.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group maniphest
+ */
class ManiphestTransactionSaveController extends ManiphestController {
public function processRequest() {
diff --git a/src/applications/maniphest/editor/transaction/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/transaction/ManiphestTransactionEditor.php
index 37acc5d26d..825d4936d5 100644
--- a/src/applications/maniphest/editor/transaction/ManiphestTransactionEditor.php
+++ b/src/applications/maniphest/editor/transaction/ManiphestTransactionEditor.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group maniphest
+ */
class ManiphestTransactionEditor {
private $parentMessageID;
diff --git a/src/applications/maniphest/query/ManiphestTaskQuery.php b/src/applications/maniphest/query/ManiphestTaskQuery.php
new file mode 100644
index 0000000000..4289613735
--- /dev/null
+++ b/src/applications/maniphest/query/ManiphestTaskQuery.php
@@ -0,0 +1,394 @@
+authorPHIDs = $authors;
+ return $this;
+ }
+
+ public function withOwners(array $owners) {
+ $this->includeUnowned = false;
+ foreach ($owners as $k => $phid) {
+ if ($phid == ManiphestTaskOwner::OWNER_UP_FOR_GRABS) {
+ $this->includeUnowned = true;
+ unset($owners[$k]);
+ break;
+ }
+ }
+ $this->ownerPHIDs = $owners;
+ return $this;
+ }
+
+ public function withProjects(array $projects) {
+ $this->projectPHIDs = $projects;
+ return $this;
+ }
+
+ public function withStatus($status) {
+ $this->status = $status;
+ return $this;
+ }
+
+ public function withPriority($priority) {
+ $this->priority = $priority;
+ return $this;
+ }
+
+ public function withSubscribers(array $subscribers) {
+ $this->subscriberPHIDs = $subscribers;
+ return $this;
+ }
+
+ public function setGroupBy($group) {
+ $this->groupBy = $group;
+ return $this;
+ }
+
+ public function setOrderBy($order) {
+ $this->orderBy = $order;
+ return $this;
+ }
+
+ public function setLimit($limit) {
+ $this->limit = $limit;
+ return $this;
+ }
+
+ public function setOffset($offset) {
+ $this->offset = $offset;
+ return $this;
+ }
+
+ public function setCalculateRows($calculate_rows) {
+ $this->calculateRows = $calculate_rows;
+ return $this;
+ }
+
+ public function getRowCount() {
+ if ($this->rowCount === null) {
+ throw new Exception(
+ "You must execute a query with setCalculateRows() before you can ".
+ "retrieve a row count.");
+ }
+ return $this->rowCount;
+ }
+
+ public function withAnyProject($any_project) {
+ $this->anyProject = $any_project;
+ return $this;
+ }
+
+ public function execute() {
+
+ $task_dao = new ManiphestTask();
+ $conn = $task_dao->establishConnection('r');
+
+ if ($this->calculateRows) {
+ $calc = 'SQL_CALC_FOUND_ROWS';
+ } else {
+ $calc = '';
+ }
+
+ $where = array();
+ $where[] = $this->buildStatusWhereClause($conn);
+ $where[] = $this->buildPriorityWhereClause($conn);
+ $where[] = $this->buildAuthorWhereClause($conn);
+ $where[] = $this->buildOwnerWhereClause($conn);
+ $where[] = $this->buildSubscriberWhereClause($conn);
+ $where[] = $this->buildProjectWhereClause($conn);
+
+ $where = array_filter($where);
+ if ($where) {
+ $where = 'WHERE ('.implode(') AND (', $where).')';
+ } else {
+ $where = '';
+ }
+
+ $join = array();
+ $join[] = $this->buildProjectJoinClause($conn);
+ $join[] = $this->buildSubscriberJoinClause($conn);
+
+ $join = array_filter($join);
+ if ($join) {
+ $join = implode(' ', $join);
+ } else {
+ $join = '';
+ }
+
+ $having = '';
+ $count = '';
+ $group = '';
+ if (count($this->projectPHIDs) > 1) {
+
+ // If we're searching for more than one project:
+ // - We'll get multiple rows for tasks when they join the project table
+ // multiple times. We use GROUP BY to make them distinct again.
+ // - We want to treat the query as an intersection query, not a union
+ // query. We sum the project count and require it be the same as the
+ // number of projects we're searching for. (If 'anyProject' is set,
+ // we do union instead.)
+
+ $group = 'GROUP BY task.id';
+
+ if (!$this->anyProject) {
+ $count = ', COUNT(1) projectCount';
+ $having = qsprintf(
+ $conn,
+ 'HAVING projectCount = %d',
+ count($this->projectPHIDs));
+ }
+ }
+
+ $order = $this->buildOrderClause($conn);
+
+ $offset = (int)nonempty($this->offset, 0);
+ $limit = (int)nonempty($this->limit, self::DEFAULT_PAGE_SIZE);
+
+ $data = queryfx_all(
+ $conn,
+ 'SELECT %Q * %Q FROM %T task %Q %Q %Q %Q %Q LIMIT %d, %d',
+ $calc,
+ $count,
+ $task_dao->getTableName(),
+ $join,
+ $where,
+ $group,
+ $having,
+ $order,
+ $offset,
+ $limit);
+
+ if ($this->calculateRows) {
+ $count = queryfx_one(
+ $conn,
+ 'SELECT FOUND_ROWS() N');
+ $this->rowCount = $count['N'];
+ } else {
+ $this->rowCount = null;
+ }
+
+ return $task_dao->loadAllFromArray($data);
+ }
+
+ private function buildStatusWhereClause($conn) {
+ switch ($this->status) {
+ case self::STATUS_ANY:
+ return null;
+ case self::STATUS_OPEN:
+ return 'status = 0';
+ case self::STATUS_CLOSED:
+ return 'status > 0';
+ default:
+ throw new Exception("Unknown status query '{$this->status}'!");
+ }
+ }
+
+ private function buildPriorityWhereClause($conn) {
+ if ($this->priority === null) {
+ return null;
+ }
+
+ return qsprintf(
+ $conn,
+ 'priority = %d',
+ $this->priority);
+ }
+
+ private function buildAuthorWhereClause($conn) {
+ if (!$this->authorPHIDs) {
+ return null;
+ }
+
+ return qsprintf(
+ $conn,
+ 'authorPHID in (%Ls)',
+ $this->authorPHIDs);
+ }
+
+ private function buildOwnerWhereClause($conn) {
+ if (!$this->ownerPHIDs) {
+ if ($this->includeUnowned === null) {
+ return null;
+ } else if ($this->includeUnowned) {
+ return qsprintf(
+ $conn,
+ 'ownerPHID IS NULL');
+ } else {
+ return qsprintf(
+ $conn,
+ 'ownerPHID IS NOT NULL');
+ }
+ }
+
+ if ($this->includeUnowned) {
+ return qsprintf(
+ $conn,
+ 'ownerPHID IN (%Ls) OR ownerPHID IS NULL',
+ $this->ownerPHIDs);
+ } else {
+ return qsprintf(
+ $conn,
+ 'ownerPHID IN (%Ls)',
+ $this->ownerPHIDs);
+ }
+ }
+
+ private function buildSubscriberWhereClause($conn) {
+ if (!$this->subscriberPHIDs) {
+ return null;
+ }
+
+ return qsprintf(
+ $conn,
+ 'subscriber.subscriberPHID IN (%Ls)',
+ $this->subscriberPHIDs);
+ }
+
+ private function buildProjectWhereClause($conn) {
+ if (!$this->projectPHIDs) {
+ return null;
+ }
+
+ return qsprintf(
+ $conn,
+ 'project.projectPHID IN (%Ls)',
+ $this->projectPHIDs);
+ }
+
+ private function buildProjectJoinClause($conn) {
+ if (!$this->projectPHIDs) {
+ return null;
+ }
+
+ $project_dao = new ManiphestTaskProject();
+ return qsprintf(
+ $conn,
+ 'JOIN %T project ON project.taskPHID = task.phid',
+ $project_dao->getTableName());
+ }
+
+ private function buildSubscriberJoinClause($conn) {
+ if (!$this->subscriberPHIDs) {
+ return null;
+ }
+
+ $subscriber_dao = new ManiphestTaskSubscriber();
+ return qsprintf(
+ $conn,
+ 'JOIN %T subscriber ON subscriber.taskPHID = task.phid',
+ $subscriber_dao->getTableName());
+ }
+
+ private function buildOrderClause($conn) {
+ $order = array();
+
+ switch ($this->groupBy) {
+ case self::GROUP_NONE:
+ break;
+ case self::GROUP_PRIORITY:
+ $order[] = 'priority';
+ break;
+ case self::GROUP_OWNER:
+ $order[] = 'ownerOrdering';
+ break;
+ case self::GROUP_STATUS:
+ $order[] = 'status';
+ break;
+ default:
+ throw new Exception("Unknown group query '{$this->groupBy}'!");
+ }
+
+ switch ($this->orderBy) {
+ case self::ORDER_PRIORITY:
+ $order[] = 'priority';
+ $order[] = 'dateModified';
+ break;
+ case self::ORDER_CREATED:
+ $order[] = 'id';
+ break;
+ case self::ORDER_MODIFIED:
+ $order[] = 'dateModified';
+ break;
+ default:
+ throw new Exception("Unknown order query '{$this->orderBy}'!");
+ }
+
+ $order = array_unique($order);
+
+ if (empty($order)) {
+ return null;
+ }
+
+ foreach ($order as $k => $column) {
+ switch ($column) {
+ case 'ownerOrdering':
+ $order[$k] = "task.{$column} ASC";
+ break;
+ default:
+ $order[$k] = "task.{$column} DESC";
+ break;
+ }
+ }
+
+ return 'ORDER BY '.implode(', ', $order);
+ }
+
+
+}
diff --git a/src/applications/maniphest/query/__init__.php b/src/applications/maniphest/query/__init__.php
new file mode 100644
index 0000000000..86a637a392
--- /dev/null
+++ b/src/applications/maniphest/query/__init__.php
@@ -0,0 +1,19 @@
+getDefaultPrivateReplyHandlerEmailAddress($handle, 'T');
}
+ public function getPublicReplyHandlerEmailAddress() {
+ return $this->getDefaultPublicReplyHandlerEmailAddress('T');
+ }
+
public function getReplyHandlerDomain() {
return PhabricatorEnv::getEnvConfig(
'metamta.maniphest.reply-handler-domain');
@@ -45,31 +52,85 @@ class ManiphestReplyHandler extends PhabricatorMailReplyHandler {
public function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
+ // NOTE: We'll drop in here on both the "reply to a task" and "create a
+ // new task" workflows! Make sure you test both if you make changes!
+
$task = $this->getMailReceiver();
+
+ $is_new_task = !$task->getID();
+
$user = $this->getActor();
$body = $mail->getCleanTextBody();
$body = trim($body);
- $lines = explode("\n", trim($body));
- $first_line = head($lines);
+ $xactions = array();
- $command = null;
- $matches = null;
- if (preg_match('/^!(\w+)/', $first_line, $matches)) {
- $lines = array_slice($lines, 1);
- $body = implode("\n", $lines);
- $body = trim($body);
+ $template = new ManiphestTransaction();
+ $template->setAuthorPHID($user->getPHID());
- $command = $matches[1];
+ if ($is_new_task) {
+ // If this is a new task, create a "User created this task." transaction
+ // and then set the title and description.
+ $xaction = clone $template;
+ $xaction->setTransactionType(ManiphestTransactionType::TYPE_STATUS);
+ $xaction->setNewValue(ManiphestTaskStatus::STATUS_OPEN);
+ $xactions[] = $xaction;
+
+ $task->setAuthorPHID($user->getPHID());
+ $task->setTitle(nonempty($mail->getSubject(), 'Untitled Task'));
+ $task->setDescription($body);
+
+ } else {
+ $lines = explode("\n", trim($body));
+ $first_line = head($lines);
+
+ $command = null;
+ $matches = null;
+ if (preg_match('/^!(\w+)/', $first_line, $matches)) {
+ $lines = array_slice($lines, 1);
+ $body = implode("\n", $lines);
+ $body = trim($body);
+
+ $command = $matches[1];
+ }
+
+ $ttype = ManiphestTransactionType::TYPE_NONE;
+ $new_value = null;
+ switch ($command) {
+ case 'close':
+ $ttype = ManiphestTransactionType::TYPE_STATUS;
+ $new_value = ManiphestTaskStatus::STATUS_CLOSED_RESOLVED;
+ break;
+ case 'claim':
+ $ttype = ManiphestTransactionType::TYPE_OWNER;
+ $new_value = $user->getPHID();
+ break;
+ case 'unsubscribe':
+ $ttype = ManiphestTransactionType::TYPE_CCS;
+ $ccs = $task->getCCPHIDs();
+ foreach ($ccs as $k => $phid) {
+ if ($phid == $user->getPHID()) {
+ unset($ccs[$k]);
+ }
+ }
+ $new_value = array_values($ccs);
+ break;
+ }
+
+ $xaction = clone $template;
+ $xaction->setTransactionType($ttype);
+ $xaction->setNewValue($new_value);
+ $xaction->setComments($body);
+
+ $xactions[] = $xaction;
}
- $xactions = array();
+ // TODO: We should look at CCs on the mail and add them as CCs.
$files = $mail->getAttachments();
if ($files) {
- $file_xaction = new ManiphestTransaction();
- $file_xaction->setAuthorPHID($user->getPHID());
+ $file_xaction = clone $template;
$file_xaction->setTransactionType(ManiphestTransactionType::TYPE_ATTACH);
$phid_type = PhabricatorPHIDConstants::PHID_TYPE_FILE;
@@ -82,37 +143,6 @@ class ManiphestReplyHandler extends PhabricatorMailReplyHandler {
$xactions[] = $file_xaction;
}
- $ttype = ManiphestTransactionType::TYPE_NONE;
- $new_value = null;
- switch ($command) {
- case 'close':
- $ttype = ManiphestTransactionType::TYPE_STATUS;
- $new_value = ManiphestTaskStatus::STATUS_CLOSED_RESOLVED;
- break;
- case 'claim':
- $ttype = ManiphestTransactionType::TYPE_OWNER;
- $new_value = $user->getPHID();
- break;
- case 'unsubscribe':
- $ttype = ManiphestTransactionType::TYPE_CCS;
- $ccs = $task->getCCPHIDs();
- foreach ($ccs as $k => $phid) {
- if ($phid == $user->getPHID()) {
- unset($ccs[$k]);
- }
- }
- $new_value = array_values($ccs);
- break;
- }
-
- $xaction = new ManiphestTransaction();
- $xaction->setAuthorPHID($user->getPHID());
- $xaction->setTransactionType($ttype);
- $xaction->setNewValue($new_value);
- $xaction->setComments($body);
-
- $xactions[] = $xaction;
-
$editor = new ManiphestTransactionEditor();
$editor->setParentMessageID($mail->getMessageID());
$editor->applyTransactions($task, $xactions);
diff --git a/src/applications/maniphest/storage/base/ManiphestDAO.php b/src/applications/maniphest/storage/base/ManiphestDAO.php
index 632fa9a0a7..c7df0c59b0 100644
--- a/src/applications/maniphest/storage/base/ManiphestDAO.php
+++ b/src/applications/maniphest/storage/base/ManiphestDAO.php
@@ -16,6 +16,9 @@
* limitations under the License.
*/
+/**
+ * @group maniphest
+ */
class ManiphestDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
diff --git a/src/applications/maniphest/storage/subscriber/ManiphestTaskSubscriber.php b/src/applications/maniphest/storage/subscriber/ManiphestTaskSubscriber.php
new file mode 100644
index 0000000000..a5b9222910
--- /dev/null
+++ b/src/applications/maniphest/storage/subscriber/ManiphestTaskSubscriber.php
@@ -0,0 +1,65 @@
+ self::IDS_MANUAL,
+ self::CONFIG_TIMESTAMPS => false,
+ );
+ }
+
+ public static function updateTaskSubscribers(ManiphestTask $task) {
+ $dao = new ManiphestTaskSubscriber();
+ $conn = $dao->establishConnection('w');
+
+ $sql = array();
+ $subscribers = $task->getCCPHIDs();
+ $subscribers[] = $task->getOwnerPHID();
+ $subscribers = array_unique($subscribers);
+
+ foreach ($subscribers as $subscriber_phid) {
+ $sql[] = qsprintf(
+ $conn,
+ '(%s, %s)',
+ $task->getPHID(),
+ $subscriber_phid);
+ }
+
+ queryfx(
+ $conn,
+ 'DELETE FROM %T WHERE taskPHID = %s',
+ $dao->getTableName(),
+ $task->getPHID());
+ if ($sql) {
+ queryfx(
+ $conn,
+ 'INSERT INTO %T (taskPHID, subscriberPHID) VALUES %Q',
+ $dao->getTableName(),
+ implode(', ', $sql));
+ }
+ }
+
+}
diff --git a/src/applications/maniphest/storage/subscriber/__init__.php b/src/applications/maniphest/storage/subscriber/__init__.php
new file mode 100644
index 0000000000..6fe3a8de86
--- /dev/null
+++ b/src/applications/maniphest/storage/subscriber/__init__.php
@@ -0,0 +1,14 @@
+ccPHIDs, array());
}
+ public function setProjectPHIDs(array $phids) {
+ $this->projectPHIDs = $phids;
+ $this->projectsNeedUpdate = true;
+ return $this;
+ }
+
+ public function setCCPHIDs(array $phids) {
+ $this->ccPHIDs = $phids;
+ $this->subscribersNeedUpdate = true;
+ return $this;
+ }
+
+ public function setOwnerPHID($phid) {
+ $this->ownerPHID = $phid;
+ $this->subscribersNeedUpdate = true;
+ return $this;
+ }
+
public function save() {
if (!$this->mailKey) {
$this->mailKey = sha1(Filesystem::readRandomBytes(20));
}
- return parent::save();
+
+ $result = parent::save();
+
+ if ($this->projectsNeedUpdate) {
+ // If we've changed the project PHIDs for this task, update the link
+ // table.
+ ManiphestTaskProject::updateTaskProjects($this);
+ $this->projectsNeedUpdate = false;
+ }
+
+ if ($this->subscribersNeedUpdate) {
+ // If we've changed the subscriber PHIDs for this task, update the link
+ // table.
+ ManiphestTaskSubscriber::updateTaskSubscribers($this);
+ $this->subscribersNeedUpdate = false;
+ }
+
+ return $result;
}
}
diff --git a/src/applications/maniphest/storage/task/__init__.php b/src/applications/maniphest/storage/task/__init__.php
index 22a5346f8c..42038f4e2d 100644
--- a/src/applications/maniphest/storage/task/__init__.php
+++ b/src/applications/maniphest/storage/task/__init__.php
@@ -7,6 +7,8 @@
phutil_require_module('phabricator', 'applications/maniphest/storage/base');
+phutil_require_module('phabricator', 'applications/maniphest/storage/subscriber');
+phutil_require_module('phabricator', 'applications/maniphest/storage/taskproject');
phutil_require_module('phabricator', 'applications/phid/constants');
phutil_require_module('phabricator', 'applications/phid/storage/phid');
diff --git a/src/applications/maniphest/storage/taskproject/ManiphestTaskProject.php b/src/applications/maniphest/storage/taskproject/ManiphestTaskProject.php
new file mode 100644
index 0000000000..687694934e
--- /dev/null
+++ b/src/applications/maniphest/storage/taskproject/ManiphestTaskProject.php
@@ -0,0 +1,67 @@
+ Project table, which denormalizes the
+ * relationship between tasks and projects into a link table so it can be
+ * efficiently queried. This table is not authoritative; the projectPHIDs field
+ * of ManiphestTask is. The rows in this table are regenerated when transactions
+ * are applied to tasks which affected their associated projects.
+ *
+ * @group maniphest
+ */
+final class ManiphestTaskProject extends ManiphestDAO {
+
+ protected $taskPHID;
+ protected $projectPHID;
+
+ public function getConfiguration() {
+ return array(
+ self::CONFIG_IDS => self::IDS_MANUAL,
+ self::CONFIG_TIMESTAMPS => false,
+ );
+ }
+
+ public static function updateTaskProjects(ManiphestTask $task) {
+ $dao = new ManiphestTaskProject();
+ $conn = $dao->establishConnection('w');
+
+ $sql = array();
+ foreach ($task->getProjectPHIDs() as $project_phid) {
+ $sql[] = qsprintf(
+ $conn,
+ '(%s, %s)',
+ $task->getPHID(),
+ $project_phid);
+ }
+
+ queryfx(
+ $conn,
+ 'DELETE FROM %T WHERE taskPHID = %s',
+ $dao->getTableName(),
+ $task->getPHID());
+ if ($sql) {
+ queryfx(
+ $conn,
+ 'INSERT INTO %T (taskPHID, projectPHID) VALUES %Q',
+ $dao->getTableName(),
+ implode(', ', $sql));
+ }
+ }
+
+}
diff --git a/src/applications/maniphest/storage/taskproject/__init__.php b/src/applications/maniphest/storage/taskproject/__init__.php
new file mode 100644
index 0000000000..8a3c5b26af
--- /dev/null
+++ b/src/applications/maniphest/storage/taskproject/__init__.php
@@ -0,0 +1,14 @@
+ PhabricatorEnv::getEnvConfig('pygments.enabled'),
+ 'filename.map' => PhabricatorEnv::getEnvConfig('syntax.filemap'),
+ );
+
+ foreach ($config as $key => $value) {
+ $engine->setConfig($key, $value);
+ }
+
+ return $engine;
+ }
+
+ public static function highlightWithFilename($filename, $source) {
+ $engine = self::newEngine();
+ $language = $engine->getLanguageFromFilename($filename);
+ return $engine->getHighlightFuture($language, $source)->resolve();
+ }
+
+ public static function highlightWithLanguage($language, $source) {
+ $engine = self::newEngine();
+ return $engine->getHighlightFuture($language, $source)->resolve();
+ }
+
+
+}
diff --git a/src/applications/markup/syntax/__init__.php b/src/applications/markup/syntax/__init__.php
new file mode 100644
index 0000000000..230b375ab6
--- /dev/null
+++ b/src/applications/markup/syntax/__init__.php
@@ -0,0 +1,14 @@
+getReplyHandlerDomain();
+ return (bool)$this->getReplyHandlerDomain() &&
+ !$this->supportsPublicReplies();
+ }
+
+ public function supportsPublicReplies() {
+ if (!PhabricatorEnv::getEnvConfig('metamta.public-replies')) {
+ return false;
+ }
+ return (bool)$this->getPublicReplyHandlerEmailAddress();
}
final public function supportsReplies() {
return $this->supportsPrivateReplies() ||
- (bool)$this->getPublicReplyHandlerEmailAddress();
+ $this->supportsPublicReplies();
}
public function getPublicReplyHandlerEmailAddress() {
@@ -145,6 +153,22 @@ abstract class PhabricatorMailReplyHandler {
return implode(', ', $list);
}
+ protected function getDefaultPublicReplyHandlerEmailAddress($prefix) {
+
+ $receiver = $this->getMailReceiver();
+ $receiver_id = $receiver->getID();
+ $domain = $this->getReplyHandlerDomain();
+
+ // We compute a hash using the object's own PHID to prevent an attacker
+ // from blindly interacting with objects that they haven't ever received
+ // mail about by just sending to D1@, D2@, etc...
+ $hash = PhabricatorMetaMTAReceivedMail::computeMailHash(
+ $receiver->getMailKey(),
+ $receiver->getPHID());
+
+ return "{$prefix}{$receiver_id}+public+{$hash}@{$domain}";
+ }
+
protected function getDefaultPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle,
$prefix) {
diff --git a/src/applications/metamta/replyhandler/base/__init__.php b/src/applications/metamta/replyhandler/base/__init__.php
index 5ff3e91560..145c5f6fd3 100644
--- a/src/applications/metamta/replyhandler/base/__init__.php
+++ b/src/applications/metamta/replyhandler/base/__init__.php
@@ -8,6 +8,7 @@
phutil_require_module('phabricator', 'applications/metamta/storage/receivedmail');
phutil_require_module('phabricator', 'applications/phid/constants');
+phutil_require_module('phabricator', 'infrastructure/env');
phutil_require_module('phutil', 'utils');
diff --git a/src/applications/metamta/storage/receivedmail/PhabricatorMetaMTAReceivedMail.php b/src/applications/metamta/storage/receivedmail/PhabricatorMetaMTAReceivedMail.php
index cdd978feef..bbeca66523 100644
--- a/src/applications/metamta/storage/receivedmail/PhabricatorMetaMTAReceivedMail.php
+++ b/src/applications/metamta/storage/receivedmail/PhabricatorMetaMTAReceivedMail.php
@@ -50,15 +50,50 @@ class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO {
return idx($this->headers, 'message-id');
}
+ public function getSubject() {
+ return idx($this->headers, 'subject');
+ }
+
public function processReceivedMail() {
$to = idx($this->headers, 'to');
+ $to = $this->getRawEmailAddress($to);
- // Accept a match either at the beginning of the address or after an open
- // angle bracket, as in:
- // "some display name"
+ $from = idx($this->headers, 'from');
+
+ $create_task = PhabricatorEnv::getEnvConfig(
+ 'metamta.maniphest.public-create-email');
+
+ if ($create_task && $to == $create_task) {
+ $user = $this->lookupPublicUser();
+ if (!$user) {
+ // TODO: We should probably bounce these since from the user's
+ // perspective their email vanishes into a black hole.
+ return $this->setMessage("Invalid public user '{$from}'.")->save();
+ }
+
+ $this->setAuthorPHID($user->getPHID());
+
+ $receiver = new ManiphestTask();
+ $receiver->setAuthorPHID($user->getPHID());
+ $receiver->setPriority(ManiphestTaskPriority::PRIORITY_TRIAGE);
+
+ $editor = new ManiphestTransactionEditor();
+ $handler = $editor->buildReplyHandler($receiver);
+
+ $handler->setActor($user);
+ $handler->receiveEmail($this);
+
+ $this->setRelatedPHID($receiver->getPHID());
+ $this->setMessage('OK');
+
+ return $this->save();
+ }
+
+ // We've already stripped this, so look for an object address which has
+ // a format like: D291+291+b0a41ca848d66dcc@example.com
$matches = null;
$ok = preg_match(
- '/(?:^|<)((?:D|T)\d+)\+(\d+)\+([a-f0-9]{16})@/U',
+ '/^((?:D|T)\d+)\+([\w]+)\+([a-f0-9]{16})@/U',
$to,
$matches);
@@ -70,9 +105,25 @@ class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO {
$user_id = $matches[2];
$hash = $matches[3];
- $user = id(new PhabricatorUser())->load($user_id);
- if (!$user) {
- return $this->setMessage("Invalid user '{$user_id}'")->save();
+ if ($user_id == 'public') {
+ if (!PhabricatorEnv::getEnvConfig('metamta.public-replies')) {
+ return $this->setMessage("Public replies not enabled.")->save();
+ }
+
+ $user = $this->lookupPublicUser();
+
+ if (!$user) {
+ return $this->setMessage("Invalid public user '{$from}'.")->save();
+ }
+
+ $use_user_hash = false;
+ } else {
+ $user = id(new PhabricatorUser())->load($user_id);
+ if (!$user) {
+ return $this->setMessage("Invalid private user '{$user_id}'.")->save();
+ }
+
+ $use_user_hash = true;
}
if ($user->getIsDisabled()) {
@@ -88,9 +139,17 @@ class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO {
$this->setRelatedPHID($receiver->getPHID());
- $expect_hash = self::computeMailHash(
- $receiver->getMailKey(),
- $user->getPHID());
+ if ($use_user_hash) {
+ // This is a private reply-to address, check that the user hash is
+ // correct.
+ $check_phid = $user->getPHID();
+ } else {
+ // This is a public reply-to address, check that the object hash is
+ // correct.
+ $check_phid = $receiver->getPHID();
+ }
+
+ $expect_hash = self::computeMailHash($receiver->getMailKey(), $check_phid);
if ($expect_hash != $hash) {
return $this->setMessage("Invalid mail hash!")->save();
}
@@ -147,5 +206,27 @@ class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO {
return substr($hash, 0, 16);
}
+ /**
+ * Strip an email address down to the actual user@domain.tld part if
+ * necessary, since sometimes it will have formatting like
+ * '"Abraham Lincoln" '.
+ */
+ private function getRawEmailAddress($address) {
+ $matches = null;
+ $ok = preg_match('/<(.*)>/', $address, $matches);
+ if ($ok) {
+ $address = $matches[1];
+ }
+ return $address;
+ }
+
+ private function lookupPublicUser() {
+ $from = idx($this->headers, 'from');
+ $from = $this->getRawEmailAddress($from);
+
+ return id(new PhabricatorUser())->loadOneWhere(
+ 'email = %s',
+ $from);
+ }
}
diff --git a/src/applications/metamta/storage/receivedmail/__init__.php b/src/applications/metamta/storage/receivedmail/__init__.php
index afd38aa144..6254939d3f 100644
--- a/src/applications/metamta/storage/receivedmail/__init__.php
+++ b/src/applications/metamta/storage/receivedmail/__init__.php
@@ -7,7 +7,9 @@
phutil_require_module('phabricator', 'applications/differential/mail/base');
+phutil_require_module('phabricator', 'applications/maniphest/constants/priority');
phutil_require_module('phabricator', 'applications/maniphest/editor/transaction');
+phutil_require_module('phabricator', 'applications/maniphest/storage/task');
phutil_require_module('phabricator', 'applications/metamta/parser');
phutil_require_module('phabricator', 'applications/metamta/storage/base');
phutil_require_module('phabricator', 'applications/people/storage/user');
diff --git a/src/applications/paste/controller/create/PhabricatorPasteCreateController.php b/src/applications/paste/controller/create/PhabricatorPasteCreateController.php
index 3f2f241d79..e56b400e05 100644
--- a/src/applications/paste/controller/create/PhabricatorPasteCreateController.php
+++ b/src/applications/paste/controller/create/PhabricatorPasteCreateController.php
@@ -31,6 +31,16 @@ class PhabricatorPasteCreateController extends PhabricatorPasteController {
if ($request->isFormPost()) {
$errors = array();
$title = $request->getStr('title');
+
+ $language = $request->getStr('language');
+ if ($language == 'infer') {
+ // If it's infer, store an empty string. Otherwise, store the
+ // language name. We do this so we can refer to 'infer' elsewhere
+ // in the code (such as default value) while retaining backwards
+ // compatibility with old posts with no language stored.
+ $language = '';
+ }
+
$text = $request->getStr('text');
if (!strlen($text)) {
@@ -41,6 +51,7 @@ class PhabricatorPasteCreateController extends PhabricatorPasteController {
}
$paste->setTitle($title);
+ $paste->setLanguage($language);
if (!$errors) {
$paste_file = PhabricatorFile::newFromFileData(
@@ -76,6 +87,26 @@ class PhabricatorPasteCreateController extends PhabricatorPasteController {
}
$form = new AphrontFormView();
+
+ // If we're coming back from an error and the language was already defined,
+ // use that. Otherwise, ask the config for the default.
+ if ($paste->getLanguage()) {
+ $language_default = $paste->getLanguage();
+ } else {
+ $language_default = PhabricatorEnv::getEnvConfig(
+ 'pygments.dropdown-default');
+ }
+
+ $available_languages = PhabricatorEnv::getEnvConfig(
+ 'pygments.dropdown-choices');
+ asort($available_languages);
+
+ $language_select = id(new AphrontFormSelectControl())
+ ->setLabel('Language')
+ ->setName('language')
+ ->setValue($language_default)
+ ->setOptions($available_languages);
+
$form
->setUser($user)
->setAction($request->getRequestURI()->getPath())
@@ -84,6 +115,7 @@ class PhabricatorPasteCreateController extends PhabricatorPasteController {
->setLabel('Title')
->setValue($paste->getTitle())
->setName('title'))
+ ->appendChild($language_select)
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Text')
diff --git a/src/applications/paste/controller/create/__init__.php b/src/applications/paste/controller/create/__init__.php
index f6151ac905..d0215f70f2 100644
--- a/src/applications/paste/controller/create/__init__.php
+++ b/src/applications/paste/controller/create/__init__.php
@@ -10,7 +10,9 @@ phutil_require_module('phabricator', 'aphront/response/redirect');
phutil_require_module('phabricator', 'applications/files/storage/file');
phutil_require_module('phabricator', 'applications/paste/controller/base');
phutil_require_module('phabricator', 'applications/paste/storage/paste');
+phutil_require_module('phabricator', 'infrastructure/env');
phutil_require_module('phabricator', 'view/form/base');
+phutil_require_module('phabricator', 'view/form/control/select');
phutil_require_module('phabricator', 'view/form/control/submit');
phutil_require_module('phabricator', 'view/form/control/text');
phutil_require_module('phabricator', 'view/form/control/textarea');
diff --git a/src/applications/paste/controller/view/PhabricatorPasteViewController.php b/src/applications/paste/controller/view/PhabricatorPasteViewController.php
index 850fb7842d..90816f9de7 100644
--- a/src/applications/paste/controller/view/PhabricatorPasteViewController.php
+++ b/src/applications/paste/controller/view/PhabricatorPasteViewController.php
@@ -89,15 +89,19 @@ class PhabricatorPasteViewController extends PhabricatorPasteController {
require_celerity_resource('diffusion-source-css');
require_celerity_resource('syntax-highlighting-css');
- $highlightEngine = new PhutilDefaultSyntaxHighlighterEngine();
- $highlightEngine->setConfig(
- 'pygments.enabled',
- PhabricatorEnv::getEnvConfig('pygments.enabled'));
-
- $text_list = explode(
- "\n", $highlightEngine->highlightSource(
+ $language = $paste->getLanguage();
+ $source = $file->loadFileData();
+ if (empty($language)) {
+ $source = PhabricatorSyntaxHighlighter::highlightWithFilename(
$paste->getTitle(),
- $file->loadFileData()));
+ $source);
+ } else {
+ $source = PhabricatorSyntaxHighlighter::highlightWithLanguage(
+ $language,
+ $source);
+ }
+
+ $text_list = explode("\n", $source);
$rows = $this->buildDisplayRows($text_list);
diff --git a/src/applications/paste/controller/view/__init__.php b/src/applications/paste/controller/view/__init__.php
index fc6f4b8546..527fc99e51 100644
--- a/src/applications/paste/controller/view/__init__.php
+++ b/src/applications/paste/controller/view/__init__.php
@@ -10,14 +10,13 @@ phutil_require_module('phabricator', 'aphront/response/400');
phutil_require_module('phabricator', 'aphront/response/404');
phutil_require_module('phabricator', 'applications/files/storage/file');
phutil_require_module('phabricator', 'applications/files/uri');
+phutil_require_module('phabricator', 'applications/markup/syntax');
phutil_require_module('phabricator', 'applications/paste/controller/base');
phutil_require_module('phabricator', 'applications/paste/storage/paste');
phutil_require_module('phabricator', 'infrastructure/celerity/api');
-phutil_require_module('phabricator', 'infrastructure/env');
phutil_require_module('phabricator', 'view/layout/panel');
phutil_require_module('phutil', 'markup');
-phutil_require_module('phutil', 'markup/syntax/engine/default');
phutil_require_module('phutil', 'utils');
diff --git a/src/applications/paste/storage/paste/PhabricatorPaste.php b/src/applications/paste/storage/paste/PhabricatorPaste.php
index 37c1564f83..4edeea81ea 100644
--- a/src/applications/paste/storage/paste/PhabricatorPaste.php
+++ b/src/applications/paste/storage/paste/PhabricatorPaste.php
@@ -22,6 +22,7 @@ class PhabricatorPaste extends PhabricatorPasteDAO {
protected $title;
protected $authorPHID;
protected $filePHID;
+ protected $language;
public function getConfiguration() {
return array(
diff --git a/src/applications/people/controller/list/PhabricatorPeopleListController.php b/src/applications/people/controller/list/PhabricatorPeopleListController.php
index e5f2d6b81e..991b5fff40 100644
--- a/src/applications/people/controller/list/PhabricatorPeopleListController.php
+++ b/src/applications/people/controller/list/PhabricatorPeopleListController.php
@@ -20,7 +20,8 @@ class PhabricatorPeopleListController extends PhabricatorPeopleController {
public function processRequest() {
$request = $this->getRequest();
- $is_admin = $request->getUser()->getIsAdmin();
+ $viewer = $request->getUser();
+ $is_admin = $viewer->getIsAdmin();
$user = new PhabricatorUser();
@@ -53,8 +54,8 @@ class PhabricatorPeopleListController extends PhabricatorPeopleController {
}
$rows[] = array(
- phabricator_date($user->getDateCreated(), $user),
- phabricator_time($user->getDateCreated(), $user),
+ phabricator_date($user->getDateCreated(), $viewer),
+ phabricator_time($user->getDateCreated(), $viewer),
phutil_render_tag(
'a',
array(
diff --git a/src/applications/phid/constants/PhabricatorPHIDConstants.php b/src/applications/phid/constants/PhabricatorPHIDConstants.php
index 9a4901e348..87703b4079 100644
--- a/src/applications/phid/constants/PhabricatorPHIDConstants.php
+++ b/src/applications/phid/constants/PhabricatorPHIDConstants.php
@@ -30,6 +30,7 @@ final class PhabricatorPHIDConstants {
const PHID_TYPE_CMIT = 'CMIT';
const PHID_TYPE_OPKG = 'OPKG';
const PHID_TYPE_PSTE = 'PSTE';
+ const PHID_TYPE_STRY = 'STRY';
public static function getTypes() {
return array(
@@ -44,6 +45,8 @@ final class PhabricatorPHIDConstants {
self::PHID_TYPE_REPO,
self::PHID_TYPE_CMIT,
self::PHID_TYPE_PSTE,
+ self::PHID_TYPE_OPKG,
+ self::PHID_TYPE_STRY,
);
}
}
diff --git a/src/applications/phid/handle/data/PhabricatorObjectHandleData.php b/src/applications/phid/handle/data/PhabricatorObjectHandleData.php
index 9e1f4c9c65..f4fb0059d2 100644
--- a/src/applications/phid/handle/data/PhabricatorObjectHandleData.php
+++ b/src/applications/phid/handle/data/PhabricatorObjectHandleData.php
@@ -202,7 +202,8 @@ class PhabricatorObjectHandleData {
$handle = new PhabricatorObjectHandle();
$handle->setPHID($phid);
$handle->setType($type);
- if (empty($commits[$phid])) {
+ if (empty($commits[$phid]) ||
+ !isset($callsigns[$repository_ids[$phid]])) {
$handle->setName('Unknown Commit');
} else {
$commit = $commits[$phid];
@@ -210,14 +211,22 @@ class PhabricatorObjectHandleData {
$repository = $repositories[$repository_ids[$phid]];
$commit_identifier = $commit->getCommitIdentifier();
- $vcs = $repository->getVersionControlSystem();
- if ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT) {
- $short_identifier = substr($commit_identifier, 0, 16);
+ // In case where the repository for the commit was deleted,
+ // we don't have have info about the repository anymore.
+ if ($repository) {
+ $vcs = $repository->getVersionControlSystem();
+ if ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT) {
+ $short_identifier = substr($commit_identifier, 0, 16);
+ } else {
+ $short_identifier = $commit_identifier;
+ }
+
+ $handle->setName('r'.$callsign.$short_identifier);
} else {
- $short_identifier = $commit_identifier;
+
+ $handle->setName('Commit '.'r'.$callsign.$commit_identifier);
}
- $handle->setName('r'.$callsign.$short_identifier);
$handle->setURI('/r'.$callsign.$commit_identifier);
$handle->setFullName('r'.$callsign.$commit_identifier);
$handle->setTimestamp($commit->getEpoch());
diff --git a/src/applications/project/constants/status/PhabricatorProjectStatus.php b/src/applications/project/constants/status/PhabricatorProjectStatus.php
index d933249636..1bce8fa0e5 100644
--- a/src/applications/project/constants/status/PhabricatorProjectStatus.php
+++ b/src/applications/project/constants/status/PhabricatorProjectStatus.php
@@ -30,7 +30,7 @@ final class PhabricatorProjectStatus {
public static function getNameForStatus($status) {
static $map = array(
- self::UNKNOWN => 'Who knows?',
+ self::UNKNOWN => '',
self::NOT_STARTED => 'Not started',
self::IN_PROGRESS => 'In progress',
self::ONGOING => 'Ongoing',
diff --git a/src/applications/project/controller/list/PhabricatorProjectListController.php b/src/applications/project/controller/list/PhabricatorProjectListController.php
index ba825511ee..05faacf307 100644
--- a/src/applications/project/controller/list/PhabricatorProjectListController.php
+++ b/src/applications/project/controller/list/PhabricatorProjectListController.php
@@ -43,38 +43,40 @@ class PhabricatorProjectListController
$handles = id(new PhabricatorObjectHandleData($author_phids))
->loadHandles();
+ $project_phids = mpull($projects, 'getPHID');
+
+ $query = id(new ManiphestTaskQuery())
+ ->withProjects($project_phids)
+ ->withAnyProject(true)
+ ->withStatus(ManiphestTaskQuery::STATUS_OPEN)
+ ->setLimit(PHP_INT_MAX);
+
+ $tasks = $query->execute();
+ $groups = array();
+ foreach ($tasks as $task) {
+ foreach ($task->getProjectPHIDs() as $phid) {
+ $groups[$phid][] = $task;
+ }
+ }
+
+
$rows = array();
foreach ($projects as $project) {
- $profile = $profiles[$project->getPHID()];
- $affiliations = $affil_groups[$project->getPHID()];
+ $phid = $project->getPHID();
- $documents = new PhabricatorProjectTransactionSearch($project->getPHID());
- // search all open documents by default
- $documents->setSearchOptions();
- $documents = $documents->executeSearch();
+ $profile = $profiles[$phid];
+ $affiliations = $affil_groups[$phid];
- $documents_types = igroup($documents, 'documentType');
- $tasks = idx(
- $documents_types,
- PhabricatorPHIDConstants::PHID_TYPE_TASK);
- $tasks_amount = count($tasks);
-
- // TODO: set up a relationship between the project and the arcanist's
- // project, to be able get the revisions.
- $revisions = idx(
- $documents_types,
- PhabricatorPHIDConstants::PHID_TYPE_DREV);
- $revisions_amount = count($revisions);
+ $group = idx($groups, $phid, array());
+ $task_count = count($group);
$population = count($affiliations);
$status = PhabricatorProjectStatus::getNameForStatus(
$project->getStatus());
- $blurb = nonempty(
- $profile->getBlurb(),
- 'Oops!, nothing is known about this elusive project.');
- $blurb = $this->textWrap($blurb, $columns = 100);
+ $blurb = $profile->getBlurb();
+ $blurb = phutil_utf8_shorten($blurb, $columns = 100);
$rows[] = array(
phutil_escape_html($project->getName()),
@@ -82,8 +84,12 @@ class PhabricatorProjectListController
$handles[$project->getAuthorPHID()]->renderLink(),
phutil_escape_html($population),
phutil_escape_html($status),
- phutil_escape_html($tasks_amount),
- // phutil_escape_html($revisions_amount),
+ phutil_render_tag(
+ 'a',
+ array(
+ 'href' => '/maniphest/view/all/?projects='.$phid,
+ ),
+ phutil_escape_html($task_count)),
phutil_render_tag(
'a',
array(
@@ -98,12 +104,11 @@ class PhabricatorProjectListController
$table->setHeaders(
array(
'Project',
- 'Blurb',
+ 'Description',
'Mastermind',
'Population',
'Status',
'Open Tasks',
- // 'Open Revisions',
'',
));
$table->setColumnClasses(
@@ -112,9 +117,8 @@ class PhabricatorProjectListController
'wide',
'',
'right',
- 'pri',
+ '',
'right',
- // 'right',
'action',
));
@@ -129,18 +133,4 @@ class PhabricatorProjectListController
'title' => 'Projects',
));
}
-
- private function textWrap($text, $length) {
- if (strlen($text) <= $length) {
- return $text;
- } else {
- // TODO: perhaps this could be improved, adding the ability to get the
- // last letter and suppress it, if it is one of [(,:; ,etc.
- // making "blurb" looks a little bit better. :)
- $wrapped = wordwrap($text, $length, '__#END#__');
- $end_position = strpos($wrapped, '__#END#__');
- $wrapped = substr($text, 0, $end_position).'...';
- return $wrapped;
- }
- }
}
diff --git a/src/applications/project/controller/list/__init__.php b/src/applications/project/controller/list/__init__.php
index c7ff3c35e3..c87483a57d 100644
--- a/src/applications/project/controller/list/__init__.php
+++ b/src/applications/project/controller/list/__init__.php
@@ -6,14 +6,13 @@
-phutil_require_module('phabricator', 'applications/phid/constants');
+phutil_require_module('phabricator', 'applications/maniphest/query');
phutil_require_module('phabricator', 'applications/phid/handle/data');
phutil_require_module('phabricator', 'applications/project/constants/status');
phutil_require_module('phabricator', 'applications/project/controller/base');
phutil_require_module('phabricator', 'applications/project/storage/affiliation');
phutil_require_module('phabricator', 'applications/project/storage/profile');
phutil_require_module('phabricator', 'applications/project/storage/project');
-phutil_require_module('phabricator', 'applications/project/transactions/search');
phutil_require_module('phabricator', 'view/control/table');
phutil_require_module('phabricator', 'view/layout/panel');
diff --git a/src/applications/project/transactions/search/PhabricatorProjectTransactionSearch.php b/src/applications/project/transactions/search/PhabricatorProjectTransactionSearch.php
deleted file mode 100644
index 02e569c81b..0000000000
--- a/src/applications/project/transactions/search/PhabricatorProjectTransactionSearch.php
+++ /dev/null
@@ -1,55 +0,0 @@
-projectPhids = $project_phids;
- } else {
- $this->projectPhids = array($project_phids);
- }
- return $this;
- }
-
- // search all open documents by default
- public function setSearchOptions($documents = '', $status = true) {
- $this->documents = $documents;
- $this->status = $status;
- return $this;
- }
-
- public function executeSearch() {
- $projects = $this->projectPhids;
- $on_documents = $this->documents;
- $with_status = $this->status;
-
- $query = new PhabricatorSearchQuery();
- $query->setQuery('');
- $query->setParameter('project', $projects);
- $query->setParameter('type', $on_documents);
- $query->setParameter('open', $with_status);
-
- $executor = new PhabricatorSearchMySQLExecutor();
- $results = $executor->executeSearch($query);
- return $results;
- }
-}
diff --git a/src/applications/project/transactions/search/__init__.php b/src/applications/project/transactions/search/__init__.php
deleted file mode 100644
index 722f521d91..0000000000
--- a/src/applications/project/transactions/search/__init__.php
+++ /dev/null
@@ -1,13 +0,0 @@
-## and #### references to these resources.
+
+These references point at ##/res/## URIs, which are handled by
+@{class:CelerityResourceController}. It responds to these requests and delivers
+the relevant resources and packages, managing cache lifetimes and handling any
+neessary preprocessing. It uses @{class:CelerityResourceMap} to locate resources
+and read packaging rules.
+
+The dependency and packaging maps are generated by
+##scripts/celerity_mapper.php##, which updates
+##src/__celerity_resource_map__.php##. This file is automatically included and
+just calls @{function:celerity_register_resource_map} with a large blob of
+static data to populate @{class:CelerityResourceMap}.
+
+@{class:CelerityStaticResourceResponse} also manages some Javelin information,
+and @{function:celerity_generate_unique_node_id} uses this metadata to provide
+a better uniqueness guarantee when generating unique node IDs.
diff --git a/src/docs/technical/conduit.diviner b/src/docs/technical/conduit.diviner
new file mode 100644
index 0000000000..a5c3523bb3
--- /dev/null
+++ b/src/docs/technical/conduit.diviner
@@ -0,0 +1,57 @@
+@title Conduit Technical Documentation
+@group conduit
+
+Technical overview of the Conduit API.
+
+= Overview =
+
+Conduit is an informal mechanism for transferring ad-hoc JSON blobs around on
+the internet.
+
+Theoretically, it provides an API to Phabricator so external scripts (including
+scripts written in other languages) can interface with the applications in the
+Phabricator suite. It technically does this, sort of, but it is unstable and
+incomplete so you should keep your expectations very low if you choose to build
+things on top of it.
+
+NOTE: Hopefully, this should improve over time, but making Conduit more robust
+isn't currently a major project priority because there isn't much demand for it
+outside of internal scripts. If you want to use Conduit to build things on top
+of Phabricator, let us know so we can adjust priorities.
+
+Conduit provides an authenticated HTTP API for Phabricator. It is informal and
+extremely simple: you post a JSON blob and you get a JSON blob back. You can
+access Conduit in PHP with @{class@libphutil:ConduitClient}, or in any language
+by executing ##arc call-conduit method## (see ##arc help call-conduit## for
+more information). You can see and test available methods at ##/conduit/## in
+the web interface.
+
+Arcanist is implemented using Conduit, and @{class:PhabricatorIRCBot} is
+intended as a practical example of how to write a program which interfaces with
+Phabricator over Conduit.
+
+= Class Relationships =
+
+The primary Conduit workflow is exposed at ##/api/##, which routes to
+@{class:PhabricatorConduitAPIController}. This controller builds a
+@{class:ConduitAPIRequest} representing authentication information and POST
+parameters, instantiates an appropriate subclass of @{class:ConduitAPIMethod},
+and passes the request to it. Subclasses of @{class:ConduitAPIMethod} implement
+the actual methods which Conduit exposes.
+
+Conduit calls which fail throw @{class:ConduitException}, which the controller
+handles.
+
+There is a web interface for viewing and testing Conduit called the "Conduit
+Console", implemented by @{class:PhabricatorConduitConsoleController} at
+##/conduit/##.
+
+A log of connections and calls is stored by
+@{class:PhabriatorConduitConnectionLog} and
+@{class:PhabricatorConduitMethodCallLog}, and can be accessed on the web via
+@{class:PhabricatorConduitLogController} at ##/conduit/log/##.
+
+Conduit provides a token-based handshake mechanism used by
+##arc install-certificate## at ##/conduit/token/##, implemented by
+@{class:PhabricatorConduitTokenController} which stores generated tokens using
+@{class:PhabricatorConduitCertificateToken}.
\ No newline at end of file
diff --git a/src/docs/userguide/remarkup.diviner b/src/docs/userguide/remarkup.diviner
index 7d338a8ef1..cc0ae5c59f 100644
--- a/src/docs/userguide/remarkup.diviner
+++ b/src/docs/userguide/remarkup.diviner
@@ -1,7 +1,7 @@
@title Remarkup Reference
@group userguide
-Explains how to make bold text, etc. This makes your words louder so you can win
+Explains how to make bold text; this makes your words louder so you can win
arguments.
= Overview =
diff --git a/src/infrastructure/celerity/api/CelerityAPI.php b/src/infrastructure/celerity/api/CelerityAPI.php
index bf9064f44b..16f7b640ba 100644
--- a/src/infrastructure/celerity/api/CelerityAPI.php
+++ b/src/infrastructure/celerity/api/CelerityAPI.php
@@ -16,6 +16,12 @@
* limitations under the License.
*/
+/**
+ * Indirection layer which provisions for a terrifying future where we need to
+ * build multiple resource responses per page.
+ *
+ * @group celerity
+ */
final class CelerityAPI {
private static $response;
@@ -27,18 +33,4 @@ final class CelerityAPI {
return self::$response;
}
-}
-
-function require_celerity_resource($symbol) {
- $response = CelerityAPI::getStaticResourceResponse();
- $response->requireResource($symbol);
-}
-
-function celerity_generate_unique_node_id() {
- static $uniq = 0;
- $response = CelerityAPI::getStaticResourceResponse();
- $block = $response->getMetadataBlock();
-
- return 'UQ'.$block.'_'.($uniq++);
-}
-
+}
\ No newline at end of file
diff --git a/src/infrastructure/celerity/api/__init__.php b/src/infrastructure/celerity/api/__init__.php
index 762576517c..144af866b1 100644
--- a/src/infrastructure/celerity/api/__init__.php
+++ b/src/infrastructure/celerity/api/__init__.php
@@ -10,3 +10,4 @@ phutil_require_module('phabricator', 'infrastructure/celerity/response');
phutil_require_source('CelerityAPI.php');
+phutil_require_source('utils.php');
diff --git a/src/infrastructure/celerity/api/utils.php b/src/infrastructure/celerity/api/utils.php
new file mode 100644
index 0000000000..ed82dbc9ce
--- /dev/null
+++ b/src/infrastructure/celerity/api/utils.php
@@ -0,0 +1,59 @@
+requireResource($symbol);
+}
+
+
+/**
+ * Generate a node ID which is guaranteed to be unique for the current page,
+ * even across Ajax requests. You should use this method to generate IDs for
+ * nodes which require a uniqueness guarantee.
+ *
+ * @return string A string appropriate for use as an 'id' attribute on a DOM
+ * node. It is guaranteed to be unique for the current page, even
+ * if the current request is a subsequent Ajax request.
+ *
+ * @group celerity
+ */
+function celerity_generate_unique_node_id() {
+ static $uniq = 0;
+ $response = CelerityAPI::getStaticResourceResponse();
+ $block = $response->getMetadataBlock();
+
+ return 'UQ'.$block.'_'.($uniq++);
+}
+
diff --git a/src/infrastructure/celerity/controller/CelerityResourceController.php b/src/infrastructure/celerity/controller/CelerityResourceController.php
index 2b09c7899e..0fe43c64ee 100644
--- a/src/infrastructure/celerity/controller/CelerityResourceController.php
+++ b/src/infrastructure/celerity/controller/CelerityResourceController.php
@@ -16,6 +16,13 @@
* limitations under the License.
*/
+/**
+ * Delivers CSS and JS resources to the browser. This controller handles all
+ * ##/res/## requests, and manages caching, package construction, and resource
+ * preprocessing.
+ *
+ * @group celerity
+ */
class CelerityResourceController extends AphrontController {
private $path;
diff --git a/src/infrastructure/celerity/map/CelerityResourceMap.php b/src/infrastructure/celerity/map/CelerityResourceMap.php
index 2dc0be4465..59fff93f34 100644
--- a/src/infrastructure/celerity/map/CelerityResourceMap.php
+++ b/src/infrastructure/celerity/map/CelerityResourceMap.php
@@ -16,6 +16,14 @@
* limitations under the License.
*/
+/**
+ * Interface to the static resource map, which is a graph of available
+ * resources, resource dependencies, and packaging information. You generally do
+ * not need to invoke it directly; instead, you call higher-level Celerity APIs
+ * and it uses the resource map to satisfy your requests.
+ *
+ * @group celerity
+ */
final class CelerityResourceMap {
private static $instance;
@@ -124,9 +132,3 @@ final class CelerityResourceMap {
}
}
-
-function celerity_register_resource_map(array $map, array $package_map) {
- $instance = CelerityResourceMap::getInstance();
- $instance->setResourceMap($map);
- $instance->setPackageMap($package_map);
-}
diff --git a/src/infrastructure/celerity/map/__init__.php b/src/infrastructure/celerity/map/__init__.php
index d34da6d1be..889b007a93 100644
--- a/src/infrastructure/celerity/map/__init__.php
+++ b/src/infrastructure/celerity/map/__init__.php
@@ -11,3 +11,4 @@ phutil_require_module('phutil', 'utils');
phutil_require_source('CelerityResourceMap.php');
+phutil_require_source('utils.php');
diff --git a/src/infrastructure/celerity/map/utils.php b/src/infrastructure/celerity/map/utils.php
new file mode 100644
index 0000000000..93c5e05653
--- /dev/null
+++ b/src/infrastructure/celerity/map/utils.php
@@ -0,0 +1,29 @@
+setResourceMap($map);
+ $instance->setPackageMap($package_map);
+}
diff --git a/src/infrastructure/celerity/response/CelerityStaticResourceResponse.php b/src/infrastructure/celerity/response/CelerityStaticResourceResponse.php
index 8ef6be945c..4331df11cc 100644
--- a/src/infrastructure/celerity/response/CelerityStaticResourceResponse.php
+++ b/src/infrastructure/celerity/response/CelerityStaticResourceResponse.php
@@ -16,6 +16,13 @@
* limitations under the License.
*/
+/**
+ * Tracks and resolves dependencies the page declares with
+ * @{function:require_celerity_resource}, and then builds appropriate HTML or
+ * Ajax responses.
+ *
+ * @group celerity
+ */
final class CelerityStaticResourceResponse {
private $symbols = array();
diff --git a/src/infrastructure/daemon/garbagecollector/PhabricatorGarbageCollectorDaemon.php b/src/infrastructure/daemon/garbagecollector/PhabricatorGarbageCollectorDaemon.php
new file mode 100644
index 0000000000..757d6084e9
--- /dev/null
+++ b/src/infrastructure/daemon/garbagecollector/PhabricatorGarbageCollectorDaemon.php
@@ -0,0 +1,139 @@
+ ($start + $run_for)) {
+ if ($just_ran) {
+ echo "Stopped garbage collector.\n";
+ $just_ran = false;
+ }
+ // The configuration says we can't collect garbage right now, so
+ // just sleep until we can.
+ $this->sleep(300);
+ continue;
+ }
+
+ if (!$just_ran) {
+ echo "Started garbage collector.\n";
+ $just_ran = true;
+ }
+
+ $n_herald = $this->collectHeraldTranscripts();
+ $n_daemon = $this->collectDaemonLogs();
+ $n_render = $this->collectRenderCaches();
+
+ $collected = array(
+ 'Herald Transcript' => $n_herald,
+ 'Daemon Log' => $n_daemon,
+ 'Render Cache' => $n_render,
+ );
+ $collected = array_filter($collected);
+
+ foreach ($collected as $thing => $count) {
+ $count = number_format($count);
+ echo "Garbage collected {$count} '{$thing}' objects.\n";
+ }
+
+ $total = array_sum($collected);
+ if ($total < 100) {
+ // We didn't max out any of the GCs so we're basically caught up. Ease
+ // off the GC loop so we don't keep doing table scans just to delete
+ // a handful of rows.
+ $this->sleep(300);
+ } else {
+ $this->stillWorking();
+ }
+ } while (true);
+
+ }
+
+ private function collectHeraldTranscripts() {
+ $ttl = PhabricatorEnv::getEnvConfig('gcdaemon.ttl.herald-transcripts');
+ if ($ttl <= 0) {
+ return 0;
+ }
+
+ $table = new HeraldTranscript();
+ $conn_w = $table->establishConnection('w');
+
+ queryfx(
+ $conn_w,
+ 'UPDATE %T SET
+ objectTranscript = "",
+ ruleTranscripts = "",
+ conditionTranscripts = "",
+ applyTranscripts = ""
+ WHERE `time` < %d AND objectTranscript != ""
+ LIMIT 100',
+ $table->getTableName(),
+ time() - $ttl);
+
+ return $conn_w->getAffectedRows();
+ }
+
+ private function collectDaemonLogs() {
+ $ttl = PhabricatorEnv::getEnvConfig('gcdaemon.ttl.daemon-logs');
+ if ($ttl <= 0) {
+ return 0;
+ }
+
+ $table = new PhabricatorDaemonLogEvent();
+ $conn_w = $table->establishConnection('w');
+
+ queryfx(
+ $conn_w,
+ 'DELETE FROM %T WHERE epoch < %d LIMIT 100',
+ $table->getTableName(),
+ time() - $ttl);
+
+ return $conn_w->getAffectedRows();
+ }
+
+ private function collectRenderCaches() {
+ // TODO: Implement this, no epoch column on the table right now.
+ return 0;
+ }
+
+}
diff --git a/src/infrastructure/daemon/garbagecollector/__init__.php b/src/infrastructure/daemon/garbagecollector/__init__.php
new file mode 100644
index 0000000000..f3de769a1d
--- /dev/null
+++ b/src/infrastructure/daemon/garbagecollector/__init__.php
@@ -0,0 +1,16 @@
+socket)) {
+ // This indicates the connection was terminated on the other side,
+ // just exit via exception and let the overseer restart us after a
+ // delay so we can reconnect.
+ throw new Exception("Remote host closed connection.");
+ }
do {
$data = fread($this->socket, 4096);
if ($data === false) {
diff --git a/src/infrastructure/daemon/irc/handler/objectname/PhabricatorIRCObjectNameHandler.php b/src/infrastructure/daemon/irc/handler/objectname/PhabricatorIRCObjectNameHandler.php
index 4b8e45bd95..c4126da658 100644
--- a/src/infrastructure/daemon/irc/handler/objectname/PhabricatorIRCObjectNameHandler.php
+++ b/src/infrastructure/daemon/irc/handler/objectname/PhabricatorIRCObjectNameHandler.php
@@ -122,7 +122,7 @@ class PhabricatorIRCObjectNameHandler extends PhabricatorIRCHandler {
// since we (ideally) want to keep the bot to Conduit calls...and
// not call to Phabricator-specific stuff (like actually loading
// the User object and fetching his/her username.)
- $output[$paste['phid']] = 'P'.$paste['id'].': '.$paste['uri'];
+ $output[$paste['phid']] = 'P'.$paste['id'].': '.$paste['uri'].' - '.$paste['title'];
}
}
diff --git a/src/storage/queryfx/queryfx.php b/src/storage/queryfx/queryfx.php
index 6195d9c88e..39fd5be254 100644
--- a/src/storage/queryfx/queryfx.php
+++ b/src/storage/queryfx/queryfx.php
@@ -57,6 +57,9 @@ function queryfx_one($conn, $sql/*, ... */) {
return null;
}
+/**
+ * @group storage
+ */
function vqueryfx_all($conn, $sql, array $argv) {
array_unshift($argv, $conn, $sql);
call_user_func_array('queryfx', $argv);
diff --git a/webroot/rsrc/js/application/core/behavior-keyboard-shortcuts.js b/webroot/rsrc/js/application/core/behavior-keyboard-shortcuts.js
index 98a79d88f1..f01a382933 100644
--- a/webroot/rsrc/js/application/core/behavior-keyboard-shortcuts.js
+++ b/webroot/rsrc/js/application/core/behavior-keyboard-shortcuts.js
@@ -20,7 +20,7 @@ JX.behavior('phabricator-keyboard-shortcuts', function(config) {
return;
}
var desc = manager.getShortcutDescriptions();
- var data = {keys : JX.JSON.serialize(desc)};
+ var data = {keys : JX.JSON.stringify(desc)};
workflow = new JX.Workflow(config.helpURI, data)
.setCloseHandler(function() {
workflow = null;
diff --git a/webroot/rsrc/js/application/herald/HeraldRuleEditor.js b/webroot/rsrc/js/application/herald/HeraldRuleEditor.js
index 5f14d0748e..c058352310 100644
--- a/webroot/rsrc/js/application/herald/HeraldRuleEditor.js
+++ b/webroot/rsrc/js/application/herald/HeraldRuleEditor.js
@@ -115,7 +115,7 @@ JX.install('HeraldRuleEditor', {
this._config.actions[k][1] = this._getActionTarget(k);
}
- rule.value = JX.JSON.serialize({
+ rule.value = JX.JSON.stringify({
conditions: this._config.conditions,
actions: this._config.actions
});