Page MenuHomePhabricator

No OneTemporary

This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/.arcconfig b/.arcconfig
index 0b7f5a8a93..1e949e42ce 100644
--- a/.arcconfig
+++ b/.arcconfig
@@ -1,11 +1,10 @@
{
"project_id" : "phabricator",
"conduit_uri" : "https://secure.phabricator.com/api/",
"lint.engine" : "PhabricatorLintEngine",
"unit.engine" : "PhutilUnitTestEngine",
- "copyright_holder" : "Facebook, Inc.",
"phutil_libraries" : {
"phabricator" : "src/"
},
"lint.xhpast.naminghook" : "PhabricatorSymbolNameLinter"
}
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000..f433b1a53f
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,177 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000000..300edd6056
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,7 @@
+Phabricator
+Copyright 2012 Facebook, Inc.
+
+This product includes software developed at
+Facebook, Inc. (http://www.facebook.com/facebook).
+
+Libraries in externals/ have their own licenses and copyright holders.
diff --git a/README b/README
index 6dbcd68c85..eade5fd77f 100644
--- a/README
+++ b/README
@@ -1,13 +1,12 @@
Phabricator is a open source collection of web applications which make it easier
to write, review, and share source code. Phabricator was developed at Facebook.
This is an early release. It's pretty high-quality and usable, but under
active development so things may change quickly.
You can learn more about the project and find links to documentation and
resources at: http://phabricator.org/
LICENSE
Phabricator is released under the Apache 2.0 license except as otherwise noted.
-http://www.apache.org/licenses/LICENSE-2.0
\ No newline at end of file
diff --git a/conf/__init_conf__.php b/conf/__init_conf__.php
index 6cd7ecdd7e..d3b946da5f 100644
--- a/conf/__init_conf__.php
+++ b/conf/__init_conf__.php
@@ -1,62 +1,46 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
function phabricator_read_config_file($original_config) {
$root = dirname(dirname(__FILE__));
// Accept either "myconfig" (preferred) or "myconfig.conf.php".
$config = preg_replace('/\.conf\.php$/', '', $original_config);
$full_config_path = $root.'/conf/'.$config.'.conf.php';
// Make sure config file errors are reported.
$old_error_level = error_reporting(E_ALL | E_STRICT);
$old_display_errors = ini_get('display_errors');
ini_set('display_errors', 1);
ob_start();
$conf = include $full_config_path;
$errors = ob_get_clean();
error_reporting($old_error_level);
ini_set('display_errors', $old_display_errors);
if ($conf === false) {
if (!Filesystem::pathExists($full_config_path)) {
$files = id(new FileFinder($root.'/conf/'))
->withType('f')
->withSuffix('conf.php')
->withFollowSymlinks(true)
->find();
foreach ($files as $key => $file) {
$file = trim($file, './');
$files[$key] = preg_replace('/\.conf\.php$/', '', $file);
}
$files = " ".implode("\n ", $files);
throw new Exception(
"CONFIGURATION ERROR\n".
"Config file '{$original_config}' does not exist. Valid config files ".
"are:\n\n".$files);
}
throw new Exception("Failed to read config file '{$config}': {$errors}");
}
return $conf;
}
diff --git a/conf/default.conf.php b/conf/default.conf.php
index 0bf46fe894..dc45c6b7d5 100644
--- a/conf/default.conf.php
+++ b/conf/default.conf.php
@@ -1,1334 +1,1318 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
return array(
// The root URI which Phabricator is installed on.
// Example: "http://phabricator.example.com/"
'phabricator.base-uri' => null,
// If you have multiple environments, provide the production environment URI
// here so that emails, etc., generated in development/sandbox environments
// contain the right links.
'phabricator.production-uri' => null,
// Setting this to 'true' will invoke a special setup mode which helps guide
// you through setting up Phabricator.
'phabricator.setup' => false,
// -- IMPORTANT! Security! -------------------------------------------------- //
// IMPORTANT: By default, Phabricator serves files from the same domain the
// application lives on. This is convenient but not secure: it creates a large
// class of vulnerabilities which can not be generally mitigated.
//
// To avoid this, you should configure a second domain in the same way you
// have the primary domain configured (e.g., point it at the same machine and
// set up the same vhost rules) and provide it here. For instance, if your
// primary install is on "http://www.phabricator-example.com/", you could
// configure "http://www.phabricator-files.com/" and specify the entire
// domain (with protocol) here. This will enforce that files are
// served only from the alternate domain. Ideally, you should use a
// completely separate domain name rather than just a different subdomain.
//
// It is STRONGLY RECOMMENDED that you configure this. Your install is NOT
// SECURE unless you do so.
'security.alternate-file-domain' => null,
// Default key for HMAC digests where the key is not important (i.e., the
// hash itself is secret). You can change this if you want (to any other
// string), but doing so will break existing sessions and CSRF tokens.
'security.hmac-key' => '[D\t~Y7eNmnQGJ;rnH6aF;m2!vJ8@v8C=Cs:aQS\.Qw',
// If the web server responds to both HTTP and HTTPS requests but you want
// users to connect with only HTTPS, you can set this to true to make
// Phabricator redirect HTTP requests to HTTPS.
//
// Normally, you should just configure your server not to accept HTTP traffic,
// but this setting may be useful if you originally used HTTP and have now
// switched to HTTPS but don't want to break old links, or if your webserver
// sits behind a load balancer which terminates HTTPS connections and you
// can not reasonably configure more granular behavior there.
//
// NOTE: Phabricator determines if a request is HTTPS or not by examining the
// PHP $_SERVER['HTTPS'] variable. If you run Apache/mod_php this will
// probably be set correctly for you automatically, but if you run Phabricator
// as CGI/FCGI (e.g., through nginx or lighttpd), you need to configure your
// web server so that it passes the value correctly based on the connection
// type. Alternatively, you can add a PHP snippet to the top of this
// configuration file to directly set $_SERVER['HTTPS'] to the correct value.
'security.require-https' => false,
// -- Internationalization -------------------------------------------------- //
// This allows customizing texts used in Phabricator. The class must extend
// PhabricatorTranslation.
'translation.provider' => 'PhabricatorEnglishTranslation',
// You can use 'translation.override' if you don't want to create a full
// translation to give users an option for switching to it and you just want
// to override some strings in the default translation.
'translation.override' => array(),
// -- Access Policies ------------------------------------------------------- //
// Phabricator allows you to set the visibility of objects (like repositories
// and source code) to "Public", which means anyone on the internet can see
// them, even without being logged in. This is great for open source, but
// some installs may never want to make anything public, so this policy is
// disabled by default. You can enable it here, which will let you set the
// policy for objects to "Public". With this option disabled, the most open
// policy is "All Users", which means users must be logged in to view things.
'policy.allow-public' => false,
// -- Logging --------------------------------------------------------------- //
// To enable the Phabricator access log, specify a path here. The Phabricator
// access log can provide more detailed information about Phabricator access
// than normal HTTP access logs (for instance, it can show logged-in users,
// controllers, and other application data). If not set, no log will be
// written.
//
// Make sure the PHP process can write to the log!
'log.access.path' => null,
// Format for the access log. If not set, the default format will be used:
//
// "[%D]\t%h\t%u\t%M\t%C\t%m\t%U\t%c\t%T"
//
// Available variables are:
//
// - %c The HTTP response code.
// - %C The controller which handled the request.
// - %D The request date.
// - %e Epoch timestamp.
// - %h The webserver's host name.
// - %p The PID of the server process.
// - %R The HTTP referrer.
// - %r The remote IP.
// - %T The request duration, in microseconds.
// - %U The request path.
// - %u The logged-in user, if one is logged in.
// - %M The HTTP method.
// - %m For conduit, the Conduit method which was invoked.
//
// If a variable isn't available (for example, %m appears in the file format
// but the request is not a Conduit request), it will be rendered as "-".
//
// Note that the default format is subject to change in the future, so if you
// rely on the log's format, specify it explicitly.
'log.access.format' => null,
// -- DarkConsole ----------------------------------------------------------- //
// DarkConsole is a administrative debugging/profiling tool built into
// Phabricator. You can leave it disabled unless you're developing against
// Phabricator.
// Determines whether or not DarkConsole is available. DarkConsole exposes
// some data like queries and stack traces, so you should be careful about
// turning it on in production (although users can not normally see it, even
// if the deployment configuration enables it).
'darkconsole.enabled' => false,
// Always enable DarkConsole, even for logged out users. This potentially
// exposes sensitive information to users, so make sure untrusted users can
// not access an install running in this mode. You should definitely leave
// this off in production. It is only really useful for using DarkConsole
// utilities to debug or profile logged-out pages. You must set
// 'darkconsole.enabled' to use this option.
'darkconsole.always-on' => false,
// Allows you to mask certain configuration values from appearing in the
// "Config" tab of DarkConsole.
'darkconsole.config-mask' => array(
'mysql.pass',
'amazon-ses.secret-key',
'amazon-s3.secret-key',
'sendgrid.api-key',
'recaptcha.private-key',
'phabricator.csrf-key',
'facebook.application-secret',
'github.application-secret',
'google.application-secret',
'phabricator.application-secret',
'disqus.application-secret',
'phabricator.mail-key',
'security.hmac-key',
),
// -- MySQL --------------------------------------------------------------- //
// Class providing database configuration. It must implement
// DatabaseConfigurationProvider.
'mysql.configuration-provider' => 'DefaultDatabaseConfigurationProvider',
// The username to use when connecting to MySQL.
'mysql.user' => 'root',
// The password to use when connecting to MySQL.
'mysql.pass' => '',
// The MySQL server to connect to. If you want to connect to a different
// port than the default (which is 3306), specify it in the hostname
// (e.g., db.example.com:1234).
'mysql.host' => 'localhost',
// The number of times to try reconnecting to the MySQL database
'mysql.connection-retries' => 3,
// Phabricator supports PHP extensions MySQL and MySQLi. It is possible to
// implement also other access mechanism (e.g. PDO_MySQL). The class must
// extend AphrontMySQLDatabaseConnectionBase.
'mysql.implementation' => 'AphrontMySQLDatabaseConnection',
// -- Notifications --------------------------------------------------------- //
'notification.enabled' => false,
// Client port for the realtime server to listen on, and for realtime clients
// to connect to. Use "localhost" if you are running the notification server
// on the same host as the web server.
'notification.client-uri' => 'http://localhost:22280/',
// URI and port for the notification root server.
'notification.server-uri' => 'http://localhost:22281/',
// The server must be started as root so it can bind to privileged ports, but
// if you specify a user here it will drop permissions after binding.
'notification.user' => null,
// Location where the server should log to.
'notification.log' => '/var/log/aphlict.log',
// PID file to use.
'notification.pidfile' => '/var/run/aphlict.pid',
// Enable this option to get additional debug output in the browser.
'notification.debug' => false,
// -- Email ----------------------------------------------------------------- //
// Some Phabricator tools send email notifications, e.g. when Differential
// revisions are updated or Maniphest tasks are changed. These options allow
// you to configure how email is delivered.
// You can test your mail setup by going to "MetaMTA" in the web interface,
// clicking "Send New Message", and then composing a message.
// Default address to send mail "From".
'metamta.default-address' => 'noreply@example.com',
// Domain used to generate Message-IDs.
'metamta.domain' => 'example.com',
// When a message is sent to multiple recipients (for example, several
// reviewers on a code review), Phabricator can either deliver one email to
// everyone (e.g., "To: alincoln, usgrant, htaft") or separate emails to each
// user (e.g., "To: alincoln", "To: usgrant", "To: htaft"). The major
// advantages and disadvantages of each approach are:
//
// - One mail to everyone:
// - Recipients can see To/Cc at a glance.
// - If you use mailing lists, you won't get duplicate mail if you're
// a normal recipient and also Cc'd on a mailing list.
// - Getting threading to work properly is harder, and probably requires
// making mail less useful by turning off options.
// - Sometimes people will "Reply All" and everyone will get two mails,
// one from the user and one from Phabricator turning their mail into
// a comment.
// - Not supported with a private reply-to address.
// - Mails are sent in the server default translation.
// - One mail to each user:
// - Recipients need to look in the mail body to see To/Cc.
// - If you use mailing lists, recipients may sometimes get duplicate
// mail.
// - Getting threading to work properly is easier, and threading settings
// can be customzied by each user.
// - "Reply All" no longer spams all other users.
// - Required if private reply-to addresses are configured.
// - Mails are sent in the language of user preference.
//
// In the code, splitting one outbound email into one-per-recipient is
// sometimes referred to as "multiplexing".
'metamta.one-mail-per-recipient' => true,
// When sending a message that has no To recipient (i.e. all recipients
// are CC'd, for example when multiplexing mail), set the To field to the
// following value. If no value is set, messages with no To will have
// their CCs upgraded to To.
'metamta.placeholder-to-recipient' => null,
// When a user takes an action which generates an email notification (like
// commenting on a Differential revision), Phabricator can either send that
// mail "From" the user's email address (like "alincoln@logcabin.com") or
// "From" the 'metamta.default-address' address. The user experience is
// generally better if Phabricator uses the user's real address as the "From"
// since the messages are easier to organize when they appear in mail clients,
// but this will only work if the server is authorized to send email on behalf
// of the "From" domain. Practically, this means:
// - If you are doing an install for Example Corp and all the users will
// have corporate @corp.example.com addresses and any hosts Phabricator
// is running on are authorized to send email from corp.example.com,
// you can enable this to make the user experience a little better.
// - If you are doing an install for an open source project and your
// users will be registering via Facebook and using personal email
// addresses, you MUST NOT enable this or virtually all of your outgoing
// email will vanish into SFP blackholes.
// - If your install is anything else, you're much safer leaving this
// off since the risk in turning it on is that your outgoing mail will
// mostly never arrive.
'metamta.can-send-as-user' => false,
// Adapter class to use to transmit mail to the MTA. The default uses
// PHPMailerLite, which will invoke "sendmail". This is appropriate
// if sendmail actually works on your host, but if you haven't configured mail
// it may not be so great. You can also use Amazon SES, by changing this to
// 'PhabricatorMailImplementationAmazonSESAdapter', signing up for SES, and
// filling in your 'amazon-ses.access-key' and 'amazon-ses.secret-key' below.
'metamta.mail-adapter' =>
'PhabricatorMailImplementationPHPMailerLiteAdapter',
// When email is sent, try to hand it off to the MTA immediately instead of
// queueing it for delivery by the daemons. If you are running the Phabricator
// daemons with "phd start", you should disable this to provide a (sometimes
// substantial) performance boost. It's on by default to make setup and
// configuration a little easier.
'metamta.send-immediately' => true,
// When email is sent, what format should Phabricator use for user's
// email addresses? Valid values are:
// - 'short' - 'gwashington <gwashington@example.com>'
// - 'real' - 'George Washington <gwashington@example.com>'
// - 'full' - 'gwashington (George Washington) <gwashington@example.com>'
// The default is 'full'.
'metamta.user-address-format' => 'full',
// If you're using Amazon SES to send email, provide your AWS access key
// and AWS secret key here. To set up Amazon SES with Phabricator, you need
// to:
// - Make sure 'metamta.mail-adapter' is set to:
// "PhabricatorMailImplementationAmazonSESAdapter"
// - Make sure 'metamta.can-send-as-user' is false.
// - Make sure 'metamta.default-address' is configured to something sensible.
// - Make sure 'metamta.default-address' is a validated SES "From" address.
'amazon-ses.access-key' => null,
'amazon-ses.secret-key' => null,
// If you're using Sendgrid to send email, provide your access credentials
// here. This will use the REST API. You can also use Sendgrid as a normal
// SMTP service.
'sendgrid.api-user' => null,
'sendgrid.api-key' => null,
// You can configure a reply handler domain so that email sent from Maniphest
// will have a special "Reply To" address like "T123+82+af19f@example.com"
// that allows recipients to reply by email and interact with tasks. For
// instructions on configurating reply handlers, see the article
// "Configuring Inbound Email" in the Phabricator documentation. By default,
// this is set to 'null' and Phabricator will use a generic 'noreply@' address
// or the address of the acting user instead of a special reply handler
// address (see 'metamta.default-address'). If you set a domain here,
// Phabricator will begin generating private reply handler addresses. See
// also 'metamta.maniphest.reply-handler' to further configure behavior.
// This key should be set to the domain part after the @, like "example.com".
'metamta.maniphest.reply-handler-domain' => null,
// You can follow the instructions in "Configuring Inbound Email" in the
// Phabricator documentation and set 'metamta.maniphest.reply-handler-domain'
// to support updating Maniphest tasks by email. If you want more advanced
// customization than this provides, you can override the reply handler
// class with an implementation of your own. This will allow you to do things
// like have a single public reply handler or change how private reply
// handlers are generated and validated.
// This key should be set to a loadable subclass of
// PhabricatorMailReplyHandler (and possibly of ManiphestReplyHandler).
'metamta.maniphest.reply-handler' => 'ManiphestReplyHandler',
// If you don't want phabricator to take up an entire domain
// (or subdomain for that matter), you can use this and set a common
// prefix for mail sent by phabricator. It will make use of the fact that
// a mail-address such as phabricator+D123+1hjk213h@example.com will be
// delivered to the phabricator users mailbox.
// Set this to the left part of the email address and it well get
// prepended to all outgoing mail. If you want to use e.g.
// 'phabricator@example.com' this should be set to 'phabricator'.
'metamta.single-reply-handler-prefix' => null,
// Prefix prepended to mail sent by Maniphest. You can change this to
// distinguish between testing and development installs, for example.
'metamta.maniphest.subject-prefix' => '[Maniphest]',
// See 'metamta.maniphest.reply-handler-domain'. This does the same thing,
// but allows email replies via Differential.
'metamta.differential.reply-handler-domain' => null,
// See 'metamta.maniphest.reply-handler'. This does the same thing, but
// affects Differential.
'metamta.differential.reply-handler' => 'DifferentialReplyHandler',
// Prefix prepended to mail sent by Differential.
'metamta.differential.subject-prefix' => '[Differential]',
// Set this to true if you want patches to be attached to mail from
// Differential. This won't work if you are using SendGrid as your mail
// adapter.
'metamta.differential.attach-patches' => false,
// To include patches in email bodies, set this to a positive integer. Patches
// will be inlined if they are at most that many lines. For instance, a value
// of 100 means "inline patches if they are no longer than 100 lines". By
// default, patches are not inlined.
'metamta.differential.inline-patches' => 0,
// If you enable either of the options above, you can choose what format
// patches are sent in. Valid options are 'unified' (like diff -u) or 'git'.
'metamta.differential.patch-format' => 'unified',
// Enables a different format for comments in differential emails.
// Differential will create unified diffs around the comment, which
// will give enough context for people who are only viewing the
// reviews in email to understand what is going on. The context will
// be created based on the range of the comment.
'metamta.differential.unified-comment-context' => false,
// Prefix prepended to mail sent by Diffusion.
'metamta.diffusion.subject-prefix' => '[Diffusion]',
// See 'metamta.maniphest.reply-handler-domain'. This does the same thing,
// but allows email replies via Diffusion.
'metamta.diffusion.reply-handler-domain' => null,
// See 'metamta.maniphest.reply-handler'. This does the same thing, but
// affects Diffusion.
'metamta.diffusion.reply-handler' => 'PhabricatorAuditReplyHandler',
// Set this to true if you want patches to be attached to commit notifications
// from Diffusion. This won't work with SendGrid.
'metamta.diffusion.attach-patches' => false,
// To include patches in Diffusion email bodies, set this to a positive
// integer. Patches will be inlined if they are at most that many lines.
// By default, patches are not inlined.
'metamta.diffusion.inline-patches' => 0,
// If you've enabled attached patches or inline patches for commit emails, you
// can establish a hard byte limit on their size. You should generally set
// reasonable byte and time limits (defaults are 1MB and 60 seconds) to avoid
// sending ridiculously enormous email for changes like "importing an external
// library" or "accidentally committed this full-length movie as text".
'metamta.diffusion.byte-limit' => 1024 * 1024,
// If you've enabled attached patches or inline patches for commit emails, you
// can establish a hard time limit on generating them.
'metamta.diffusion.time-limit' => 60,
// Prefix prepended to mail sent by Package.
'metamta.package.subject-prefix' => '[Package]',
// See 'metamta.maniphest.reply-handler'. This does similar thing for package
// except that it only supports sending out mail and doesn't handle incoming
// email.
'metamta.package.reply-handler' => 'OwnersPackageReplyHandler',
// By default, Phabricator generates unique reply-to addresses and sends a
// separate email to each recipient when you enable reply handling. This is
// more secure than using "From" to establish user identity, but can mean
// users may receive multiple emails when they are on mailing lists. Instead,
// you can use a single, non-unique reply to address and authenticate users
// based on the "From" address by setting this to 'true'. This trades away
// a little bit of security for convenience, but it's reasonable in many
// installs. Object interactions are still protected using hashes in the
// single public email address, so objects can not be replied to blindly.
'metamta.public-replies' => false,
// You can configure an email address like "bugs@phabricator.example.com"
// which will automatically create Maniphest tasks when users send email
// to it. This relies on the "From" address to authenticate users, so it is
// is not completely secure. To set this up, enter a complete email
// address like "bugs@phabricator.example.com" and then configure mail to
// that address so it routed to Phabricator (if you've already configured
// reply handlers, you're probably already done). See "Configuring Inbound
// Email" in the documentation for more information.
'metamta.maniphest.public-create-email' => null,
// If you enable 'metamta.public-replies', Phabricator uses "From" to
// authenticate users. You can additionally enable this setting to try to
// authenticate with 'Reply-To'. Note that this is completely spoofable and
// insecure (any user can set any 'Reply-To' address) but depending on the
// nature of your install or other deliverability conditions this might be
// okay. Generally, you can't do much more by spoofing Reply-To than be
// annoying (you can write but not read content). But, you know, this is
// still **COMPLETELY INSECURE**.
'metamta.insecure-auth-with-reply-to' => false,
// If you enable 'metamta.maniphest.public-create-email' and create an
// email address like "bugs@phabricator.example.com", it will default to
// rejecting mail which doesn't come from a known user. However, you might
// want to let anyone send email to this address; to do so, set a default
// author here (a Phabricator username). A typical use of this might be to
// create a "System Agent" user called "bugs" and use that name here. If you
// specify a valid username, mail will always be accepted and used to create
// a task, even if the sender is not a system user. The original email
// address will be stored in an 'From Email' field on the task.
'metamta.maniphest.default-public-author' => null,
// You can disable the Herald hints in email if users prefer smaller messages.
// These are the links under the headers "MANAGE HERALD RULES" and
// "WHY DID I GET THIS EMAIL?". If you set this to true, they will not appear
// in any mail. Users can still navigate to the links via the web interface.
'metamta.herald.show-hints' => true,
// You can disable the hints under "REPLY HANDLER ACTIONS" if users prefer
// smaller messages. The actions themselves will still work properly.
'metamta.reply.show-hints' => true,
// You can disable the "To:" and "Cc:" footers in mail if users prefer
// smaller messages.
'metamta.recipients.show-hints' => true,
// If this option is enabled, Phabricator will add a "Precedence: bulk"
// header to transactional mail (e.g., Differential, Maniphest and Herald
// notifications). This may improve the behavior of some auto-responder
// software and prevent it from replying. However, it may also cause
// deliverability issues -- notably, you currently can not send this header
// via Amazon SES, and enabling this option with SES will prevent delivery
// of any affected mail.
'metamta.precedence-bulk' => false,
// Mail.app on OS X Lion won't respect threading headers unless the subject
// is prefixed with "Re:". If you enable this option, Phabricator will add
// "Re:" to the subject line of all mail which is expected to thread. If
// you've set 'metamta.one-mail-per-recipient', users can override this
// setting in their preferences.
'metamta.re-prefix' => false,
// If true, allow MetaMTA to change mail subjects to put text like
// '[Accepted]' and '[Commented]' in them. This makes subjects more useful,
// but might break threading on some clients. If you've set
// 'metamta.one-mail-per-recipient', users can override this setting in their
// preferences.
'metamta.vary-subjects' => true,
// -- Auth ------------------------------------------------------------------ //
// Can users login with a username/password, or by following the link from
// a password reset email? You can disable this and configure one or more
// OAuth providers instead.
'auth.password-auth-enabled' => true,
// Maximum number of simultaneous web sessions each user is permitted to have.
// Setting this to "1" will prevent a user from logging in on more than one
// browser at the same time.
'auth.sessions.web' => 5,
// Maximum number of simultaneous Conduit sessions each user is permitted
// to have.
'auth.sessions.conduit' => 5,
// Set this true to enable the Settings -> SSH Public Keys panel, which will
// allow users to associated SSH public keys with their accounts. This is only
// really useful if you're setting up services over SSH and want to use
// Phabricator for authentication; in most situations you can leave this
// disabled.
'auth.sshkeys.enabled' => false,
// If true, email addresses must be verified (by clicking a link in an
// email) before a user can login. By default, verification is optional
// unless 'auth.email-domains' is nonempty (see below).
'auth.require-email-verification' => false,
// You can restrict allowed email addresses to certain domains (like
// "yourcompany.com") by setting a list of allowed domains here. Users will
// only be allowed to register using email addresses at one of the domains,
// and will only be able to add new email addresses for these domains. If
// you configure this, it implies 'auth.require-email-verification'.
//
// To configure email domains, set a list of domains like this:
//
// array(
// 'yourcompany.com',
// 'yourcompany.co.uk',
// )
//
// You should omit the "@" from domains. Note that the domain must match
// exactly. If you allow "yourcompany.com", that permits "joe@yourcompany.com"
// but rejects "joe@mail.yourcompany.com".
'auth.email-domains' => array(),
// You can provide an arbitrary block of HTML here, which will appear on the
// login screen. Normally, you'd use this to provide login or registration
// instructions to users.
'auth.login-message' => null,
// -- Accounts -------------------------------------------------------------- //
// Is basic account information (email, real name, profile picture) editable?
// If you set up Phabricator to automatically synchronize account information
// from some other authoritative system, you can disable this to ensure
// information remains consistent across both systems.
'account.editable' => true,
// When users set or reset a password, it must have at least this many
// characters.
'account.minimum-password-length' => 8,
// -- Facebook OAuth -------------------------------------------------------- //
// Can users use Facebook credentials to login to Phabricator?
'facebook.auth-enabled' => false,
// Can users use Facebook credentials to create new Phabricator accounts?
'facebook.registration-enabled' => true,
// Are Facebook accounts permanently linked to Phabricator accounts, or can
// the user unlink them?
'facebook.auth-permanent' => false,
// The Facebook "Application ID" to use for Facebook API access.
'facebook.application-id' => null,
// The Facebook "Application Secret" to use for Facebook API access.
'facebook.application-secret' => null,
// Should Phabricator reject requests made by users with
// Secure Browsing disabled?
'facebook.require-https-auth' => false,
// -- GitHub OAuth ---------------------------------------------------------- //
// Can users use GitHub credentials to login to Phabricator?
'github.auth-enabled' => false,
// Can users use GitHub credentials to create new Phabricator accounts?
'github.registration-enabled' => true,
// Are GitHub accounts permanently linked to Phabricator accounts, or can
// the user unlink them?
'github.auth-permanent' => false,
// The GitHub "Client ID" to use for GitHub API access.
'github.application-id' => null,
// The GitHub "Secret" to use for GitHub API access.
'github.application-secret' => null,
// -- Google OAuth ---------------------------------------------------------- //
// Can users use Google credentials to login to Phabricator?
'google.auth-enabled' => false,
// Can users use Google credentials to create new Phabricator accounts?
'google.registration-enabled' => true,
// Are Google accounts permanently linked to Phabricator accounts, or can
// the user unlink them?
'google.auth-permanent' => false,
// The Google "Client ID" to use for Google API access.
'google.application-id' => null,
// The Google "Client Secret" to use for Google API access.
'google.application-secret' => null,
// -- LDAP Auth ----------------------------------------------------- //
// Enable ldap auth
'ldap.auth-enabled' => false,
// The LDAP server hostname
'ldap.hostname' => '',
// The LDAP server port
'ldap.port' => 389,
// The LDAP base domain name
'ldap.base_dn' => '',
// The attribute to be regarded as 'username'. Has to be unique
'ldap.search_attribute' => '',
// Perform a search to find a user
// Many LDAP installations do not have the username in the dn, if this is
// true for you set this to true and configure the username_attribute below
'ldap.search-first' => false,
// The attribute to search for if you have to search for a user
'ldap.username-attribute' => '',
// The attribute(s) to be regarded as 'real name'.
// If more then one attribute is supplied the values of the attributes in
// the array will be joined
'ldap.real_name_attributes' => array(),
// A domain name to use when authenticating against Active Directory
// (e.g. 'example.com')
'ldap.activedirectory_domain' => '',
// The LDAP version
'ldap.version' => 3,
// LDAP Referrals Option
// Whether referrals should be followed by the client
// Should be set to 0 if you use Windows 2003 AD
'ldap.referrals' => 1,
// -- Disqus OAuth ---------------------------------------------------------- //
// Can users use Disqus credentials to login to Phabricator?
'disqus.auth-enabled' => false,
// Can users use Disqus credentials to create new Phabricator accounts?
'disqus.registration-enabled' => true,
// Are Disqus accounts permanently linked to Phabricator accounts, or can
// the user unlink them?
'disqus.auth-permanent' => false,
// The Disqus "Client ID" to use for Disqus API access.
'disqus.application-id' => null,
// The Disqus "Client Secret" to use for Disqus API access.
'disqus.application-secret' => null,
// -- Phabricator OAuth ----------------------------------------------------- //
// Meta-town -- Phabricator is itself an OAuth Provider
// TODO -- T887 -- make this support multiple Phabricator instances!
// The URI of the Phabricator instance to use as an OAuth server.
'phabricator.oauth-uri' => null,
// Can users use Phabricator credentials to login to Phabricator?
'phabricator.auth-enabled' => false,
// Can users use Phabricator credentials to create new Phabricator accounts?
'phabricator.registration-enabled' => true,
// Are Phabricator accounts permanently linked to Phabricator accounts, or can
// the user unlink them?
'phabricator.auth-permanent' => false,
// The Phabricator "Client ID" to use for Phabricator API access.
'phabricator.application-id' => null,
// The Phabricator "Client Secret" to use for Phabricator API access.
'phabricator.application-secret' => null,
// -- Recaptcha ------------------------------------------------------------- //
// Is Recaptcha enabled? If disabled, captchas will not appear. You should
// enable Recaptcha if your install is public-facing, as it hinders
// brute-force attacks.
'recaptcha.enabled' => false,
// Your Recaptcha public key, obtained from Recaptcha.
'recaptcha.public-key' => null,
// Your Recaptcha private key, obtained from Recaptcha.
'recaptcha.private-key' => null,
// -- Misc ------------------------------------------------------------------ //
// This is hashed with other inputs to generate CSRF tokens. If you want, you
// can change it to some other string which is unique to your install. This
// will make your install more secure in a vague, mostly theoretical way. But
// it will take you like 3 seconds of mashing on your keyboard to set it up so
// you might as well.
'phabricator.csrf-key' => '0b7ec0592e0a2829d8b71df2fa269b2c6172eca3',
// This is hashed with other inputs to generate mail tokens. If you want, you
// can change it to some other string which is unique to your install. In
// particular, you will want to do this if you accidentally send a bunch of
// mail somewhere you shouldn't have, to invalidate all old reply-to
// addresses.
'phabricator.mail-key' => '5ce3e7e8787f6e40dfae861da315a5cdf1018f12',
// Version string displayed in the footer. You can generate this value from
// Git log or from the current date in the deploy with a script like this:
//
// git log -n1 --pretty=%h > version.txt
//
// You can then use this generated value like this:
//
// 'phabricator.version' =>
// file_get_contents(dirname(__FILE__).'/version.txt'),
'phabricator.version' => 'UNSTABLE',
// PHP requires that you set a timezone in your php.ini before using date
// functions, or it will emit a warning. If this isn't possible (for instance,
// because you are using HPHP) you can set some valid constant for
// date_default_timezone_set() here and Phabricator will set it on your
// behalf, silencing the warning.
'phabricator.timezone' => null,
// When unhandled exceptions occur, stack traces are hidden by default.
// You can enable traces for development to make it easier to debug problems.
'phabricator.show-stack-traces' => false,
// Shows an error callout if a page generated PHP errors, warnings or notices.
// This makes it harder to miss problems while developing Phabricator.
'phabricator.show-error-callout' => false,
// When users write comments which have URIs, they'll be automatically linked
// if the protocol appears in this set. This whitelist is primarily to prevent
// security issues like javascript:// URIs.
'uri.allowed-protocols' => array(
'http' => true,
'https' => true,
),
// Tokenizers are UI controls which let the user select other users, email
// addresses, project names, etc., by typing the first few letters and having
// the control autocomplete from a list. They can load their data in two ways:
// either in a big chunk up front, or as the user types. By default, the data
// is loaded in a big chunk. This is simpler and performs better for small
// datasets. However, if you have a very large number of users or projects,
// (in the ballpark of more than a thousand), loading all that data may become
// slow enough that it's worthwhile to query on demand instead. This makes
// the typeahead slightly less responsive but overall performance will be much
// better if you have a ton of stuff. You can figure out which setting is
// best for your install by changing this setting and then playing with a
// user tokenizer (like the user selectors in Maniphest or Differential) and
// seeing which setting loads faster and feels better.
'tokenizer.ondemand' => false,
// By default, Phabricator includes some silly nonsense in the UI, such as
// a submit button called "Clowncopterize" in Differential and a call to
// "Leap Into Action". If you'd prefer more traditional UI strings like
// "Submit", you can set this flag to disable most of the jokes and easter
// eggs.
'phabricator.serious-business' => false,
// -- Files ----------------------------------------------------------------- //
// Lists which uploaded file types may be viewed in the browser. If a file
// has a mime type which does not appear in this list, it will always be
// downloaded instead of displayed. This is mainly a usability
// consideration, since browsers tend to freak out when viewing enormous
// binary files.
//
// The keys in this array are viewable mime types; the values are the mime
// types they will be delivered as when they are viewed in the browser.
//
// IMPORTANT: Configure 'security.alternate-file-domain' above! Your install
// is NOT safe if it is left unconfigured.
'files.viewable-mime-types' => array(
'image/jpeg' => 'image/jpeg',
'image/jpg' => 'image/jpg',
'image/png' => 'image/png',
'image/gif' => 'image/gif',
'text/plain' => 'text/plain; charset=utf-8',
'text/x-diff' => 'text/plain; charset=utf-8',
// ".ico" favicon files, which have mime type diversity. See:
// http://en.wikipedia.org/wiki/ICO_(file_format)#MIME_type
'image/x-ico' => 'image/x-icon',
'image/x-icon' => 'image/x-icon',
'image/vnd.microsoft.icon' => 'image/x-icon',
),
// List of mime types which can be used as the source for an <img /> tag.
// This should be a subset of 'files.viewable-mime-types' and exclude files
// like text.
'files.image-mime-types' => array(
'image/jpeg' => true,
'image/jpg' => true,
'image/png' => true,
'image/gif' => true,
'image/x-ico' => true,
'image/x-icon' => true,
'image/vnd.microsoft.icon' => true,
),
// Phabricator can proxy images from other servers so you can paste the URI
// to a funny picture of a cat into the comment box and have it show up as an
// image. However, this means the webserver Phabricator is running on will
// make HTTP requests to arbitrary URIs. If the server has access to internal
// resources, this could be a security risk. You should only enable it if you
// are installed entirely a VPN and VPN access is required to access
// Phabricator, or if the webserver has no special access to anything. If
// unsure, it is safer to leave this disabled.
'files.enable-proxy' => false,
// -- Storage --------------------------------------------------------------- //
// Phabricator allows users to upload files, and can keep them in various
// storage engines. This section allows you to configure which engines
// Phabricator will use, and how it will use them.
// The largest filesize Phabricator will store in the MySQL BLOB storage
// engine, which just uses a database table to store files. While this isn't a
// best practice, it's really easy to set up. Set this to 0 to disable use of
// the MySQL blob engine.
'storage.mysql-engine.max-size' => 1000000,
// Phabricator provides a local disk storage engine, which just writes files
// to some directory on local disk. The webserver must have read/write
// permissions on this directory. This is straightforward and suitable for
// most installs, but will not scale past one web frontend unless the path
// is actually an NFS mount, since you'll end up with some of the files
// written to each web frontend and no way for them to share. To use the
// local disk storage engine, specify the path to a directory here. To
// disable it, specify null.
'storage.local-disk.path' => null,
// If you want to store files in Amazon S3, specify an AWS access and secret
// key here and a bucket name below.
'amazon-s3.access-key' => null,
'amazon-s3.secret-key' => null,
// Set this to a valid Amazon S3 bucket to store files there. You must also
// configure S3 access keys above.
'storage.s3.bucket' => null,
// Phabricator uses a storage engine selector to choose which storage engine
// to use when writing file data. If you add new storage engines or want to
// provide very custom rules (e.g., write images to one storage engine and
// other files to a different one), you can provide an alternate
// implementation here. The default engine will use choose MySQL, Local Disk,
// and S3, in that order, if they have valid configurations above and a file
// fits within configured limits.
'storage.engine-selector' => 'PhabricatorDefaultFileStorageEngineSelector',
// Set the size of the largest file a user may upload. This is used to render
// text like "Maximum file size: 10MB" on interfaces where users can upload
// files, and files larger than this size will be rejected.
//
// Specify this limit in bytes, or using a "K", "M", or "G" suffix.
//
// NOTE: Setting this to a large size is NOT sufficient to allow users to
// upload large files. You must also configure a number of other settings. To
// configure file upload limits, consult the article "Configuring File Upload
// Limits" in the documentation. Once you've configured some limit across all
// levels of the server, you can set this limit to an appropriate value and
// the UI will then reflect the actual configured limit.
'storage.upload-size-limit' => null,
// Phabricator puts databases in a namespace, which defualts to "phabricator"
// -- for instance, the Differential database is named
// "phabricator_differential" by default. You can change this namespace if you
// want. Normally, you should not do this unless you are developing
// Phabricator and using namespaces to separate multiple sandbox datasets.
'storage.default-namespace' => 'phabricator',
// -- Search ---------------------------------------------------------------- //
// Phabricator supports Elastic Search; to use it, specify a host like
// 'http://elastic.example.com:9200/' here.
'search.elastic.host' => null,
// Phabricator uses a search engine selector to choose which search engine
// to use when indexing and reconstructing documents, and when executing
// queries. You can override the engine selector to provide a new selector
// class which can select some custom engine you implement, if you want to
// store your documents in some search engine which does not have default
// support.
'search.engine-selector' => 'PhabricatorDefaultSearchEngineSelector',
// -- Differential ---------------------------------------------------------- //
'differential.revision-custom-detail-renderer' => null,
// Array for custom remarkup rules. The array should have a list of
// class names of classes that extend PhutilRemarkupRule
'differential.custom-remarkup-rules' => null,
// Array for custom remarkup block rules. The array should have a list of
// class names of classes that extend PhutilRemarkupEngineBlockRule
'differential.custom-remarkup-block-rules' => null,
// Set display word-wrap widths for Differential. Specify a dictionary of
// regular expressions mapping to column widths. The filename will be matched
// against each regexp in order until one matches. The default configuration
// uses a width of 100 for Java and 80 for other languages. Note that 80 is
// the greatest column width of all time. Changes here will not be immediately
// reflected in old revisions unless you purge the changeset render cache
// (with `./scripts/util/purge_cache.php --changesets`).
'differential.wordwrap' => array(
'/\.java$/' => 100,
'/.*/' => 80,
),
// List of file regexps where whitespace is meaningful and should not
// use 'ignore-all' by default
'differential.whitespace-matters' => array(
'/\.py$/',
'/\.l?hs$/',
),
'differential.field-selector' => 'DifferentialDefaultFieldSelector',
// Differential can show "Host" and "Path" fields on revisions, with
// information about the machine and working directory where the
// change came from. These fields are disabled by default because they may
// occasionally have sensitive information; you can set this to true to
// enable them.
'differential.show-host-field' => false,
// Differential has a required "Test Plan" field by default, which requires
// authors to fill out information about how they verified the correctness of
// their changes when sending code for review. If you'd prefer not to use
// this field, you can disable it here. You can also make it optional
// (instead of required) below.
'differential.show-test-plan-field' => true,
// Differential has a required "Test Plan" field by default. You can make it
// optional by setting this to false. You can also completely remove it above,
// if you prefer.
'differential.require-test-plan-field' => true,
// If you set this to true, users can "!accept" revisions via email (normally,
// they can take other actions but can not "!accept"). This action is disabled
// by default because email authentication can be configured to be very weak,
// and, socially, email "!accept" is kind of sketchy and implies revisions may
// not actually be receiving thorough review.
'differential.enable-email-accept' => false,
// If you set this to true, users won't need to login to view differential
// revisions. Anonymous users will have read-only access and won't be able to
// interact with the revisions.
'differential.anonymous-access' => false,
// List of file regexps that should be treated as if they are generated by
// an automatic process, and thus get hidden by default in differential.
'differential.generated-paths' => array(
// '/config\.h$/',
// '#/autobuilt/#',
),
// If you set this to true, users can accept their own revisions. This action
// is disabled by default because it's most likely not a behavior you want,
// but it proves useful if you are working alone on a project and want to make
// use of all of differential's features.
'differential.allow-self-accept' => false,
// If you set this to true, any user can close any revision so long as it has
// been accepted. This can be useful depending on your development model. For
// example, github-style pull requests where the reviewer is often the
// actual committer can benefit from turning this option to true. If false,
// only the submitter can close a revision.
'differential.always-allow-close' => false,
// Revisions newer than this number of days are marked as fresh in Action
// Required and Revisions Waiting on You views. Only work days (not weekends
// and holidays) are included. Set to 0 to disable this feature.
'differential.days-fresh' => 1,
// Similar to 'differential.days-fresh' but marks stale revisions. If the
// revision is even older than it is marked as old.
'differential.days-stale' => 3,
// -- Maniphest ------------------------------------------------------------- //
'maniphest.enabled' => true,
// Array of custom fields for Maniphest tasks. For details on adding custom
// fields to Maniphest, see "Maniphest User Guide: Adding Custom Fields".
'maniphest.custom-fields' => array(),
// Class which drives custom field construction. See "Maniphest User Guide:
// Adding Custom Fields" in the documentation for more information.
'maniphest.custom-task-extensions-class' => 'ManiphestDefaultTaskExtensions',
// What should the default task priority be in create flows?
// See the constants in @{class:ManiphestTaskPriority} for valid values.
// Defaults to "needs triage".
'maniphest.default-priority' => 90,
// -- Phriction ------------------------------------------------------------- //
'phriction.enabled' => true,
// -- Phame ----------------------------------------------------------------- //
// Should Phame users have Disqus comment widget, and if so what's the
// website shortname to use? For example, secure.phabricator.org uses
// "phabricator", which we registered with Disqus. If you aren't familiar
// with Disqus, see:
// Disqus quick start guide - http://docs.disqus.com/help/4/
// Information on shortnames - http://docs.disqus.com/help/68/
'disqus.shortname' => null,
// Directories to look for Phame skins inside of.
'phame.skins' => array(
'externals/skins/',
),
// -- Remarkup -------------------------------------------------------------- //
// If you enable this, linked YouTube videos will be embeded inline. This has
// mild security implications (you'll leak referrers to YouTube) and is pretty
// silly (but sort of awesome).
'remarkup.enable-embedded-youtube' => false,
// -- Garbage Collection ---------------------------------------------------- //
// Phabricator generates various logs and caches in the database which can
// be garbage collected after a while to make the total data size more
// manageable. To run garbage collection, launch a
// PhabricatorGarbageCollector daemon.
// Since the GC daemon can issue large writes and table scans, you may want to
// run it only during off hours or make sure it is scheduled so it doesn't
// overlap with backups. This determines when the daemon can start running
// each day.
'gcdaemon.run-at' => '12 AM',
// How many seconds after 'gcdaemon.run-at' the daemon may collect garbage
// for. By default it runs continuously, but you can set it to run for a
// limited period of time. For instance, if you do backups at 3 AM, you might
// run garbage collection for an hour beforehand. This is not a high-precision
// limit so you may want to leave some room for the GC to actually stop, and
// if you set it to something like 3 seconds you're on your own.
'gcdaemon.run-for' => 24 * 60 * 60,
// These 'ttl' keys configure how much old data the GC daemon keeps around.
// Objects older than the ttl will be collected. Set any value to 0 to store
// data indefinitely.
'gcdaemon.ttl.herald-transcripts' => 30 * (24 * 60 * 60),
'gcdaemon.ttl.daemon-logs' => 7 * (24 * 60 * 60),
'gcdaemon.ttl.differential-parse-cache' => 14 * (24 * 60 * 60),
'gcdaemon.ttl.markup-cache' => 30 * (24 * 60 * 60),
'gcdaemon.ttl.task-archive' => 14 * (24 * 60 * 60),
// -- Feed ------------------------------------------------------------------ //
// If you set this to true, you can embed Phabricator activity feeds in other
// pages using iframes. These feeds are completely public, and a login is not
// required to view them! This is intended for things like open source
// projects that want to expose an activity feed on the project homepage.
//
// NOTE: You must also set `policy.allow-public` to true for this setting
// to work properly.
'feed.public' => false,
// If you set this to a list of http URIs, when a feed story is published a
// task will be created for each uri that posts the story data to the uri.
// Daemons automagically retry failures 100 times, waiting $fail_count * 60s
// between each subsequent failure. Be sure to keep the daemon console
// (/daemon/) open while developing and testing your end points.
//
// NOTE: URIs are not validated, the URI must return http status 200 within
// 30 seconds, and no permission checks are performed.
'feed.http-hooks' => array(),
// -- Drydock --------------------------------------------------------------- //
// If you want to use Drydock's builtin EC2 Blueprints, configure your AWS
// EC2 credentials here.
'amazon-ec2.access-key' => null,
'amazon-ec2.secret-key' => null,
// -- Customization --------------------------------------------------------- //
// Paths to additional phutil libraries to load.
'load-libraries' => array(),
'aphront.default-application-configuration-class' =>
'AphrontDefaultApplicationConfiguration',
'controller.oauth-registration' =>
'PhabricatorOAuthDefaultRegistrationController',
// Directory that phd (the Phabricator daemon control script) should use to
// track running daemons.
'phd.pid-directory' => '/var/tmp/phd/pid',
// Directory that the Phabricator daemons should use to store the log file
'phd.log-directory' => '/var/tmp/phd/log',
// Number of "TaskMaster" daemons that "phd start" should start. You can
// raise this if you have a task backlog, or explicitly launch more with
// "phd launch <N> taskmaster".
'phd.start-taskmasters' => 4,
// Launch daemons in "verbose" mode by default. This creates a lot of output,
// but can help debug issues. Daemons launched in debug mode with "phd debug"
// are always launched in verbose mode. See also 'phd.trace'.
'phd.verbose' => false,
// Launch daemons in "trace" mode by default. This creates an ENORMOUS amount
// of output, but can help debug issues. Daemons launched in debug mode with
// "phd debug" are always launched in trace mdoe. See also 'phd.verbose'.
'phd.trace' => false,
// Path to custom celerity resource map relative to 'phabricator/src'.
// See also `scripts/celerity_mapper.php`.
'celerity.resource-path' => '__celerity_resource_map__.php',
// This value is an input to the hash function when building resource hashes.
// It has no security value, but if you accidentally poison user caches (by
// pushing a bad patch or having something go wrong with a CDN, e.g.) you can
// change this to something else and rebuild the Celerity map to break user
// caches. Unless you are doing Celerity development, it is exceptionally
// unlikely that you need to modify this.
'celerity.resource-hash' => 'd9455ea150622ee044f7931dabfa52aa',
// In a development environment, it is desirable to force static resources
// (CSS and JS) to be read from disk on every request, so that edits to them
// appear when you reload the page even if you haven't updated the resource
// maps. This setting ensures requests will be verified against the state on
// disk. Generally, you should leave this off in production (caching behavior
// and performance improve with it off) but turn it on in development. (These
// settings are the defaults.)
'celerity.force-disk-reads' => false,
// Minify static resources by removing whitespace and comments. You should
// enable this in production, but disable it in development.
'celerity.minify' => false,
// You can respond to various application events by installing listeners,
// which will receive callbacks when interesting things occur. Specify a list
// of classes which extend PhabricatorEventListener here.
'events.listeners' => array(),
// -- Syntax Highlighting --------------------------------------------------- //
// Phabricator can highlight PHP by default and use Pygments for other
// languages if enabled. You can provide a custom highlighter engine by
// extending class PhutilSyntaxHighlighterEngine.
'syntax-highlighter.engine' => 'PhutilDefaultSyntaxHighlighterEngine',
// If you want syntax highlighting for other languages than PHP then you can
// install the python package 'Pygments', make sure the 'pygmentize' script is
// available in the $PATH of the webserver, and then enable this.
'pygments.enabled' => false,
// In places that we display a dropdown to syntax-highlight code,
// this is where that list is defined.
// Syntax is 'lexer-name' => 'Display Name',
'pygments.dropdown-choices' => array(
'apacheconf' => 'Apache Configuration',
'bash' => 'Bash Scripting',
'brainfuck' => 'Brainf*ck',
'c' => 'C',
'cpp' => 'C++',
'css' => 'CSS',
'd' => 'D',
'diff' => 'Diff',
'django' => 'Django Templating',
'erb' => 'Embedded Ruby/ERB',
'erlang' => 'Erlang',
'haskell' => 'Haskell',
'html' => 'HTML',
'java' => 'Java',
'js' => 'Javascript',
'mysql' => 'MySQL',
'objc' => 'Objective-C',
'perl' => 'Perl',
'php' => 'PHP',
'rest' => 'reStructuredText',
'text' => 'Plain Text',
'python' => 'Python',
'rainbow' => 'Rainbow',
'remarkup' => 'Remarkup',
'ruby' => 'Ruby',
'xml' => 'XML',
),
// This is an override list of regular expressions which allows you to choose
// what language files are highlighted as. If your projects have certain rules
// about filenames or use unusual or ambiguous language extensions, you can
// create a mapping here. This is an ordered dictionary of regular expressions
// which will be tested against the filename. They should map to either an
// explicit language as a string value, or a numeric index into the captured
// groups as an integer.
'syntax.filemap' => array(
// Example: Treat all '*.xyz' files as PHP.
// '@\\.xyz$@' => 'php',
// Example: Treat 'httpd.conf' as 'apacheconf'.
// '@/httpd\\.conf$@' => 'apacheconf',
// Example: Treat all '*.x.bak' file as '.x'. NOTE: we map to capturing
// group 1 by specifying the mapping as "1".
// '@\\.([^.]+)\\.bak$@' => 1,
'@\.arcconfig$@' => 'js',
'@\.divinerconfig$@' => 'js',
),
// Set the default monospaced font style for users who haven't set a custom
// style.
'style.monospace' => '10px "Menlo", "Consolas", "Monaco", monospace',
// -- Debugging ------------------------------------------------------------- //
// Enable this to change HTTP redirects into normal pages with a link to the
// redirection target. For example, after you submit a form you'll get a page
// saying "normally, you'd be redirected...". This is useful to examine
// service or profiler information on write pathways, or debug redirects. It
// also makes the UX horrible for normal use, so you should enable it only
// when debugging.
//
// NOTE: This does not currently work for forms with Javascript "workflow",
// since the redirect happens in Javascript.
'debug.stop-on-redirect' => false,
// Set the rate for how often to do sampled profiling. On average, one
// request for every number of requests specified here will be sampled.
// Set this value to 0 to completely disable profiling. In a production
// environment, this value should either be set to 0 (to disable) or to
// a large number (to sample only a few requests).
'debug.profile-rate' => 0,
// -- Previews ------------------------------------------------------------- //
// Turn on to enable the "viewport" meta tag. This is a preview feature which
// will improve the usability of Phabricator on phones and tablets once it
// is ready.
'preview.viewport-meta-tag' => false,
// -- Environment ---------------------------------------------------------- //
// Phabricator occasionally shells out to other binaries on the server.
// An example of this is the "pygmentize" command, used to syntax-highlight
// code written in languages other than PHP. By default, it is assumed that
// these binaries are in the $PATH of the user running Phabricator (normally
// 'apache', 'httpd', or 'nobody'). Here you can add extra directories to
// the $PATH environment variable, for when these binaries are in non-standard
// locations.
'environment.append-paths' => array(),
);
diff --git a/conf/development.conf.php b/conf/development.conf.php
index 425afda13a..c18382bcf3 100644
--- a/conf/development.conf.php
+++ b/conf/development.conf.php
@@ -1,27 +1,11 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
return array(
'darkconsole.enabled' => true,
'celerity.force-disk-reads' => true,
'phabricator.show-stack-traces' => true,
'phabricator.show-error-callout' => true,
'celerity.minify' => false,
) + phabricator_read_config_file('default');
diff --git a/conf/production.conf.php b/conf/production.conf.php
index 7228521c76..e9a474ee11 100644
--- a/conf/production.conf.php
+++ b/conf/production.conf.php
@@ -1,22 +1,6 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
return array(
) + phabricator_read_config_file('default');
diff --git a/resources/sql/patches/059.engines.php b/resources/sql/patches/059.engines.php
index abcf2e170b..5fdb3ea2ec 100644
--- a/resources/sql/patches/059.engines.php
+++ b/resources/sql/patches/059.engines.php
@@ -1,47 +1,31 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$conn = $schema_conn;
$tables = queryfx_all(
$conn,
"SELECT TABLE_SCHEMA db, TABLE_NAME tbl
FROM information_schema.TABLES s
WHERE s.TABLE_SCHEMA LIKE %>
AND s.TABLE_NAME != 'search_documentfield'
AND s.ENGINE != 'InnoDB'",
'{$NAMESPACE}_');
if (!$tables) {
return;
}
echo "There are ".count($tables)." tables using the MyISAM engine. These will ".
"now be converted to InnoDB. This process may take a few minutes, please ".
"be patient.\n";
foreach ($tables as $table) {
$name = $table['db'].'.'.$table['tbl'];
echo "Converting {$name}...\n";
queryfx(
$conn,
"ALTER TABLE %T.%T ENGINE=InnoDB",
$table['db'],
$table['tbl']);
}
echo "Done!\n";
diff --git a/resources/sql/patches/079.nametokenindex.php b/resources/sql/patches/079.nametokenindex.php
index a6aca0189a..b545393693 100644
--- a/resources/sql/patches/079.nametokenindex.php
+++ b/resources/sql/patches/079.nametokenindex.php
@@ -1,30 +1,14 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$conn = $schema_conn;
echo "Indexing username tokens for typeaheads...\n";
$users = id(new PhabricatorUser())->loadAll();
echo count($users)." users to index";
foreach ($users as $user) {
$user->updateNameTokens();
echo ".";
}
echo "\nDone.\n";
diff --git a/resources/sql/patches/081.filekeys.php b/resources/sql/patches/081.filekeys.php
index 5feb300d3c..165b24bc61 100644
--- a/resources/sql/patches/081.filekeys.php
+++ b/resources/sql/patches/081.filekeys.php
@@ -1,31 +1,15 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
echo "Generating file keys...\n";
$files = id(new PhabricatorFile())->loadAllWhere('secretKey IS NULL');
echo count($files).' files to generate keys for';
foreach ($files as $file) {
queryfx(
$file->establishConnection('r'),
'UPDATE %T SET secretKey = %s WHERE id = %d',
$file->getTableName(),
$file->generateSecretKey(),
$file->getID());
echo '.';
}
echo "\nDone.\n";
diff --git a/resources/sql/patches/090.forceuniqueprojectnames.php b/resources/sql/patches/090.forceuniqueprojectnames.php
index ba62c65d76..7aa98d22d3 100644
--- a/resources/sql/patches/090.forceuniqueprojectnames.php
+++ b/resources/sql/patches/090.forceuniqueprojectnames.php
@@ -1,117 +1,101 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
echo "Ensuring project names are unique enough...\n";
$projects = id(new PhabricatorProject())->loadAll();
$slug_map = array();
foreach ($projects as $project) {
$project->setPhrictionSlug($project->getName());
$slug = $project->getPhrictionSlug();
if ($slug == '/') {
$project_id = $project->getID();
echo "Project #{$project_id} doesn't have a meaningful name...\n";
$project->setName(trim('Unnamed Project '.$project->getName()));
}
$slug_map[$slug][] = $project->getID();
}
foreach ($slug_map as $slug => $similar) {
if (count($similar) <= 1) {
continue;
}
echo "Too many projects are similar to '{$slug}'...\n";
foreach (array_slice($similar, 1, null, true) as $key => $project_id) {
$project = $projects[$project_id];
$old_name = $project->getName();
$new_name = rename_project($project, $projects);
echo "Renaming project #{$project_id} ".
"from '{$old_name}' to '{$new_name}'.\n";
$project->setName($new_name);
}
}
$update = $projects;
while ($update) {
$size = count($update);
foreach ($update as $key => $project) {
$id = $project->getID();
$name = $project->getName();
$project->setPhrictionSlug($name);
$slug = $project->getPhrictionSlug();
echo "Updating project #{$id} '{$name}' ({$slug})...";
try {
queryfx(
$project->establishConnection('w'),
'UPDATE %T SET name = %s, phrictionSlug = %s WHERE id = %d',
$project->getTableName(),
$name,
$slug,
$project->getID());
unset($update[$key]);
echo "okay.\n";
} catch (AphrontQueryDuplicateKeyException $ex) {
echo "failed, will retry.\n";
}
}
if (count($update) == $size) {
throw new Exception(
"Failed to make any progress while updating projects. Schema upgrade ".
"has failed. Go manually fix your project names to be unique (they are ".
"probably ridiculous?) and then try again.");
}
}
echo "Done.\n";
/**
* Rename the project so that it has a unique slug, by appending (2), (3), etc.
* to its name.
*/
function rename_project($project, $projects) {
$suffix = 2;
while (true) {
$new_name = $project->getName().' ('.$suffix.')';
$project->setPhrictionSlug($new_name);
$new_slug = $project->getPhrictionSlug();
$okay = true;
foreach ($projects as $other) {
if ($other->getID() == $project->getID()) {
continue;
}
if ($other->getPhrictionSlug() == $new_slug) {
$okay = false;
break;
}
}
if ($okay) {
break;
} else {
$suffix++;
}
}
return $new_name;
}
diff --git a/resources/sql/patches/093.gitremotes.php b/resources/sql/patches/093.gitremotes.php
index d483cdc972..667fe39d11 100644
--- a/resources/sql/patches/093.gitremotes.php
+++ b/resources/sql/patches/093.gitremotes.php
@@ -1,58 +1,42 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
echo "Stripping remotes from repository default branches...\n";
$table = new PhabricatorRepository();
$conn_w = $table->establishConnection('w');
$repos = queryfx_all(
$conn_w,
'SELECT id, name, details FROM %T WHERE versionControlSystem = %s',
$table->getTableName(),
'git');
foreach ($repos as $repo) {
$details = json_decode($repo['details'], true);
$old = idx($details, 'default-branch', '');
if (strpos($old, '/') === false) {
continue;
}
$parts = explode('/', $old);
$parts = array_filter($parts);
$new = end($parts);
$details['default-branch'] = $new;
$new_details = json_encode($details);
$id = $repo['id'];
$name = $repo['name'];
echo "Updating default branch for repository #{$id} '{$name}' from ".
"'{$old}' to '{$new}' to remove the explicit remote.\n";
queryfx(
$conn_w,
'UPDATE %T SET details = %s WHERE id = %d',
$table->getTableName(),
$new_details,
$id);
}
echo "Done.\n";
diff --git a/resources/sql/patches/098.heraldruletypemigration.php b/resources/sql/patches/098.heraldruletypemigration.php
index 1ce2bc1cb4..c30d761042 100644
--- a/resources/sql/patches/098.heraldruletypemigration.php
+++ b/resources/sql/patches/098.heraldruletypemigration.php
@@ -1,62 +1,46 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
echo "Checking for rules that can be converted to 'personal'. ";
$rules = id(new HeraldRule())->loadAll();
foreach ($rules as $rule) {
if ($rule->getRuleType() !== HeraldRuleTypeConfig::RULE_TYPE_PERSONAL) {
$actions = $rule->loadActions();
$can_be_personal = true;
foreach ($actions as $action) {
$target = $action->getTarget();
if (is_array($target)) {
if (count($target) > 1) {
$can_be_personal = false;
break;
} else {
$targetPHID = head($target);
if ($targetPHID !== $rule->getAuthorPHID()) {
$can_be_personal = false;
break;
}
}
} else if ($target) {
if ($target !== $rule->getAuthorPHID()) {
$can_be_personal = false;
break;
}
}
}
if ($can_be_personal) {
$rule->setRuleType(HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);
queryfx(
$rule->establishConnection('w'),
'UPDATE %T SET ruleType = %s WHERE id = %d',
$rule->getTableName(),
$rule->getRuleType(),
$rule->getID());
echo "Setting rule '" . $rule->getName() . "' to personal. ";
}
}
}
echo "Done. ";
diff --git a/resources/sql/patches/102.heraldcleanup.php b/resources/sql/patches/102.heraldcleanup.php
index 64b57a3e14..6589c7f0ef 100644
--- a/resources/sql/patches/102.heraldcleanup.php
+++ b/resources/sql/patches/102.heraldcleanup.php
@@ -1,51 +1,35 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
echo "Cleaning up old Herald rule applied rows...\n";
$rules = id(new HeraldRule())->loadAll();
foreach ($rules as $key => $rule) {
$first_policy = HeraldRepetitionPolicyConfig::toInt(
HeraldRepetitionPolicyConfig::FIRST);
if ($rule->getRepetitionPolicy() != $first_policy) {
unset($rules[$key]);
}
}
$conn_w = id(new HeraldRule())->establishConnection('w');
$clause = '';
if ($rules) {
$clause = qsprintf(
$conn_w,
'WHERE ruleID NOT IN (%Ld)',
mpull($rules, 'getID'));
}
echo "This may take a moment";
do {
queryfx(
$conn_w,
'DELETE FROM %T %Q LIMIT 1000',
HeraldRule::TABLE_RULE_APPLIED,
$clause);
echo ".";
} while ($conn_w->getAffectedRows());
echo "\n";
echo "Done.\n";
diff --git a/resources/sql/patches/111.commitauditmigration.php b/resources/sql/patches/111.commitauditmigration.php
index c89702f808..be734877c5 100644
--- a/resources/sql/patches/111.commitauditmigration.php
+++ b/resources/sql/patches/111.commitauditmigration.php
@@ -1,71 +1,55 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
echo "Updating old commit authors...\n";
$table = new PhabricatorRepositoryCommit();
$conn = $table->establishConnection('w');
$data = new PhabricatorRepositoryCommitData();
$commits = queryfx_all(
$conn,
'SELECT c.id id, c.authorPHID authorPHID, d.commitDetails details
FROM %T c JOIN %T d ON d.commitID = c.id
WHERE c.authorPHID IS NULL',
$table->getTableName(),
$data->getTableName());
foreach ($commits as $commit) {
$id = $commit['id'];
$details = json_decode($commit['details'], true);
$author_phid = idx($details, 'authorPHID');
if ($author_phid) {
queryfx(
$conn,
'UPDATE %T SET authorPHID = %s WHERE id = %d',
$table->getTableName(),
$author_phid,
$id);
echo "#{$id}\n";
}
}
echo "Done.\n";
echo "Updating old commit mailKeys...\n";
$table = new PhabricatorRepositoryCommit();
$conn = $table->establishConnection('w');
$commits = queryfx_all(
$conn,
'SELECT id FROM %T WHERE mailKey = %s',
$table->getTableName(),
'');
foreach ($commits as $commit) {
$id = $commit['id'];
queryfx(
$conn,
'UPDATE %T SET mailKey = %s WHERE id = %d',
$table->getTableName(),
Filesystem::readRandomCharacters(20),
$id);
echo "#{$id}\n";
}
echo "Done.\n";
diff --git a/resources/sql/patches/117.repositorydescription.php b/resources/sql/patches/117.repositorydescription.php
index 285eb0c0c2..18ee4a463f 100644
--- a/resources/sql/patches/117.repositorydescription.php
+++ b/resources/sql/patches/117.repositorydescription.php
@@ -1,22 +1,6 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$conn = id(new PhabricatorRepository())->establishConnection('w');
if (queryfx_one($conn, "SHOW COLUMNS FROM `repository` LIKE 'description'")) {
queryfx($conn, "ALTER TABLE `repository` DROP `description`");
}
diff --git a/resources/sql/patches/131.migraterevisionquery.php b/resources/sql/patches/131.migraterevisionquery.php
index 2876047552..5d155d10ba 100644
--- a/resources/sql/patches/131.migraterevisionquery.php
+++ b/resources/sql/patches/131.migraterevisionquery.php
@@ -1,47 +1,31 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$table = new DifferentialRevision();
$conn_w = $table->establishConnection('w');
echo "Migrating revisions";
do {
$revisions = id(new DifferentialRevision())
->loadAllWhere('branchName IS NULL LIMIT 1000');
foreach ($revisions as $revision) {
echo ".";
$diff = $revision->loadActiveDiff();
if (!$diff) {
continue;
}
$branch_name = $diff->getBranch();
$arc_project_phid = $diff->getArcanistProjectPHID();
queryfx(
$conn_w,
'UPDATE %T SET branchName = %s, arcanistProjectPHID = %s WHERE id = %d',
$table->getTableName(),
$branch_name,
$arc_project_phid,
$revision->getID());
}
} while (count($revisions) == 1000);
echo "\nDone.\n";
diff --git a/resources/sql/patches/emailtableport.php b/resources/sql/patches/emailtableport.php
index fb075b603c..742b9203be 100644
--- a/resources/sql/patches/emailtableport.php
+++ b/resources/sql/patches/emailtableport.php
@@ -1,49 +1,33 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
echo "Migrating user emails...\n";
$table = new PhabricatorUser();
$conn = $table->establishConnection('r');
$emails = queryfx_all(
$conn,
'SELECT phid, email FROM %T',
$table->getTableName());
$emails = ipull($emails, 'email', 'phid');
$etable = new PhabricatorUserEmail();
$econn = $etable->establishConnection('w');
foreach ($emails as $phid => $email) {
// NOTE: Grandfather all existing email in as primary / verified. We generate
// verification codes because they are used for password resets, etc.
echo "Migrating '{$phid}'...\n";
queryfx(
$econn,
'INSERT INTO %T (userPHID, address, verificationCode, isVerified, isPrimary)
VALUES (%s, %s, %s, 1, 1)',
$etable->getTableName(),
$phid,
$email,
Filesystem::readRandomCharacters(24));
}
echo "Done.\n";
diff --git a/resources/sql/patches/migrate-differential-dependencies.php b/resources/sql/patches/migrate-differential-dependencies.php
index de455172ca..16c7c0dbdb 100644
--- a/resources/sql/patches/migrate-differential-dependencies.php
+++ b/resources/sql/patches/migrate-differential-dependencies.php
@@ -1,42 +1,26 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
echo "Migrating differential dependencies to edges...\n";
foreach (new LiskMigrationIterator(new DifferentialRevision()) as $rev) {
$id = $rev->getID();
echo "Revision {$id}: ";
$deps = $rev->getAttachedPHIDs(PhabricatorPHIDConstants::PHID_TYPE_DREV);
if (!$deps) {
echo "-\n";
continue;
}
$editor = new PhabricatorEdgeEditor();
$editor->setSuppressEvents(true);
foreach ($deps as $dep) {
$editor->addEdge(
$rev->getPHID(),
PhabricatorEdgeConfig::TYPE_DREV_DEPENDS_ON_DREV,
$dep);
}
$editor->save();
echo "OKAY\n";
}
echo "Done.\n";
diff --git a/resources/sql/patches/migrate-maniphest-dependencies.php b/resources/sql/patches/migrate-maniphest-dependencies.php
index cfa93d7e5c..058a151a08 100644
--- a/resources/sql/patches/migrate-maniphest-dependencies.php
+++ b/resources/sql/patches/migrate-maniphest-dependencies.php
@@ -1,42 +1,26 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
echo "Migrating task dependencies to edges...\n";
foreach (new LiskMigrationIterator(new ManiphestTask()) as $task) {
$id = $task->getID();
echo "Task {$id}: ";
$deps = $task->getAttachedPHIDs(PhabricatorPHIDConstants::PHID_TYPE_TASK);
if (!$deps) {
echo "-\n";
continue;
}
$editor = new PhabricatorEdgeEditor();
$editor->setSuppressEvents(true);
foreach ($deps as $dep) {
$editor->addEdge(
$task->getPHID(),
PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK,
$dep);
}
$editor->save();
echo "OKAY\n";
}
echo "Done.\n";
diff --git a/resources/sql/patches/migrate-maniphest-revisions.php b/resources/sql/patches/migrate-maniphest-revisions.php
index 4b0fb049c1..19a5ec5590 100644
--- a/resources/sql/patches/migrate-maniphest-revisions.php
+++ b/resources/sql/patches/migrate-maniphest-revisions.php
@@ -1,42 +1,26 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
echo "Migrating task revisions to edges...\n";
foreach (new LiskMigrationIterator(new ManiphestTask()) as $task) {
$id = $task->getID();
echo "Task {$id}: ";
$revs = $task->getAttachedPHIDs(PhabricatorPHIDConstants::PHID_TYPE_DREV);
if (!$revs) {
echo "-\n";
continue;
}
$editor = new PhabricatorEdgeEditor();
$editor->setSuppressEvents(true);
foreach ($revs as $rev) {
$editor->addEdge(
$task->getPHID(),
PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV,
$rev);
}
$editor->save();
echo "OKAY\n";
}
echo "Done.\n";
diff --git a/resources/sql/patches/migrate-project-edges.php b/resources/sql/patches/migrate-project-edges.php
index 50a360a86a..d10da778eb 100644
--- a/resources/sql/patches/migrate-project-edges.php
+++ b/resources/sql/patches/migrate-project-edges.php
@@ -1,49 +1,33 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
echo "Migrating project members to edges...\n";
foreach (new LiskMigrationIterator(new PhabricatorProject()) as $proj) {
$id = $proj->getID();
echo "Project {$id}: ";
$members = queryfx_all(
$proj->establishConnection('r'),
'SELECT userPHID FROM %T WHERE projectPHID = %s',
'project_affiliation',
$proj->getPHID());
if (!$members) {
echo "-\n";
continue;
}
$members = ipull($members, 'userPHID');
$editor = new PhabricatorEdgeEditor();
$editor->setSuppressEvents(true);
foreach ($members as $user_phid) {
$editor->addEdge(
$proj->getPHID(),
PhabricatorEdgeConfig::TYPE_PROJ_MEMBER,
$user_phid);
}
$editor->save();
echo "OKAY\n";
}
echo "Done.\n";
diff --git a/resources/sql/patches/ponder-mailkey-populate.php b/resources/sql/patches/ponder-mailkey-populate.php
index 549ae093ae..70b36325b5 100644
--- a/resources/sql/patches/ponder-mailkey-populate.php
+++ b/resources/sql/patches/ponder-mailkey-populate.php
@@ -1,37 +1,21 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
echo "Populating Questions with mail keys...\n";
foreach (new LiskMigrationIterator(new PonderQuestion()) as $question) {
$id = $question->getID();
echo "Question {$id}: ";
if (!$question->getMailKey()) {
queryfx(
$question->establishConnection('w'),
'UPDATE %T SET mailKey = %s WHERE id = %d',
$question->getTableName(),
Filesystem::readRandomCharacters(20),
$id);
echo "Generated Key\n";
} else {
echo "-\n";
}
}
echo "Done.\n";
diff --git a/scripts/__init_script__.php b/scripts/__init_script__.php
index 24900a6b7b..4d70ec0dbd 100644
--- a/scripts/__init_script__.php
+++ b/scripts/__init_script__.php
@@ -1,88 +1,72 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
error_reporting(E_ALL | E_STRICT);
ini_set('display_errors', 1);
$include_path = ini_get('include_path');
ini_set(
'include_path',
$include_path.PATH_SEPARATOR.dirname(__FILE__).'/../../');
@include_once 'libphutil/scripts/__init_script__.php';
if (!@constant('__LIBPHUTIL__')) {
echo "ERROR: Unable to load libphutil. Update your PHP 'include_path' to ".
"include the parent directory of libphutil/.\n";
exit(1);
}
phutil_load_library(dirname(__FILE__).'/../src/');
// NOTE: This is dangerous in general, but we know we're in a script context and
// are not vulnerable to CSRF.
AphrontWriteGuard::allowDangerousUnguardedWrites(true);
require_once dirname(dirname(__FILE__)).'/conf/__init_conf__.php';
$env = isset($_SERVER['PHABRICATOR_ENV'])
? $_SERVER['PHABRICATOR_ENV']
: getenv('PHABRICATOR_ENV');
if (!$env) {
echo phutil_console_wrap(
phutil_console_format(
"**ERROR**: PHABRICATOR_ENV Not Set\n\n".
"Define the __PHABRICATOR_ENV__ environment variable before running ".
"this script. You can do it on the command line like this:\n\n".
" $ PHABRICATOR_ENV=__custom/myconfig__ %s ...\n\n".
"Replace __custom/myconfig__ with the path to your configuration file. ".
"For more information, see the 'Configuration Guide' in the ".
"Phabricator documentation.\n\n",
$argv[0]));
exit(1);
}
$conf = phabricator_read_config_file($env);
$conf['phabricator.env'] = $env;
PhabricatorEnv::setEnvConfig($conf);
phutil_load_library('arcanist/src');
foreach (PhabricatorEnv::getEnvConfig('load-libraries') as $library) {
phutil_load_library($library);
}
PhutilErrorHandler::initialize();
PhabricatorEventEngine::initialize();
$tz = PhabricatorEnv::getEnvConfig('phabricator.timezone');
if ($tz) {
date_default_timezone_set($tz);
}
$translation = PhabricatorEnv::newObjectFromConfig('translation.provider');
PhutilTranslator::getInstance()
->setLanguage($translation->getLanguage())
->addTranslations($translation->getTranslations());
// Append any paths to $PATH if we need to.
$paths = PhabricatorEnv::getEnvConfig('environment.append-paths');
if (!empty($paths)) {
$current_env_path = getenv('PATH');
$new_env_paths = implode(PATH_SEPARATOR, $paths);
putenv('PATH='.$current_env_path.PATH_SEPARATOR.$new_env_paths);
}
diff --git a/scripts/aphront/aphrontpath.php b/scripts/aphront/aphrontpath.php
index 70b3bda160..627ae064c1 100755
--- a/scripts/aphront/aphrontpath.php
+++ b/scripts/aphront/aphrontpath.php
@@ -1,42 +1,26 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
if ($argc !== 2 || $argv[1] === '--help') {
echo "Usage: aphrontpath.php <url>\n";
echo "Purpose: Print controller which will process passed <url>.\n";
exit(1);
}
$url = parse_url($argv[1]);
$path = '/'.(isset($url['path']) ? ltrim($url['path'], '/') : '');
$config_key = 'aphront.default-application-configuration-class';
$application = PhabricatorEnv::newObjectFromConfig($config_key);
$application->setRequest(new AphrontRequest('', $path));
list($controller) = $application->buildControllerForPath($path);
if (!$controller && substr($path, -1) !== '/') {
list($controller) = $application->buildControllerForPath($path.'/');
}
if ($controller) {
echo get_class($controller) . "\n";
}
diff --git a/scripts/calendar/import_us_holidays.php b/scripts/calendar/import_us_holidays.php
index d69367f6ac..71cfb56a70 100755
--- a/scripts/calendar/import_us_holidays.php
+++ b/scripts/calendar/import_us_holidays.php
@@ -1,78 +1,62 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
// http://www.opm.gov/operating_status_schedules/fedhol/
$holidays = array(
'2012-01-02' => "New Year's Day",
'2012-01-16' => "Birthday of Martin Luther King, Jr.",
'2012-02-20' => "Washington's Birthday",
'2012-05-28' => "Memorial Day",
'2012-07-04' => "Independence Day",
'2012-09-03' => "Labor Day",
'2012-10-08' => "Columbus Day",
'2012-11-12' => "Veterans Day",
'2012-11-22' => "Thanksgiving Day",
'2012-12-25' => "Christmas Day",
'2013-01-01' => "New Year's Day",
'2013-01-21' => "Birthday of Martin Luther King, Jr.",
'2013-02-18' => "Washington's Birthday",
'2013-05-27' => "Memorial Day",
'2013-07-04' => "Independence Day",
'2013-09-02' => "Labor Day",
'2013-10-14' => "Columbus Day",
'2013-11-11' => "Veterans Day",
'2013-11-28' => "Thanksgiving Day",
'2013-12-25' => "Christmas Day",
'2014-01-01' => "New Year's Day",
'2014-01-20' => "Birthday of Martin Luther King, Jr.",
'2014-02-17' => "Washington's Birthday",
'2014-05-26' => "Memorial Day",
'2014-07-04' => "Independence Day",
'2014-09-01' => "Labor Day",
'2014-10-13' => "Columbus Day",
'2014-11-11' => "Veterans Day",
'2014-11-27' => "Thanksgiving Day",
'2014-12-25' => "Christmas Day",
'2015-01-01' => "New Year's Day",
'2015-01-19' => "Birthday of Martin Luther King, Jr.",
'2015-02-16' => "Washington's Birthday",
'2015-05-25' => "Memorial Day",
'2015-07-03' => "Independence Day",
'2015-09-07' => "Labor Day",
'2015-10-12' => "Columbus Day",
'2015-11-11' => "Veterans Day",
'2015-11-26' => "Thanksgiving Day",
'2015-12-25' => "Christmas Day",
);
$table = new PhabricatorCalendarHoliday();
$conn_w = $table->establishConnection('w');
$table_name = $table->getTableName();
foreach ($holidays as $day => $name) {
queryfx(
$conn_w,
'INSERT IGNORE INTO %T (day, name) VALUES (%s, %s)',
$table_name,
$day,
$name);
}
diff --git a/scripts/celerity/generate_sprites.php b/scripts/celerity/generate_sprites.php
index f3ce81d7ae..53d21e3682 100755
--- a/scripts/celerity/generate_sprites.php
+++ b/scripts/celerity/generate_sprites.php
@@ -1,248 +1,232 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
require_once dirname(dirname(__FILE__)).'/__init_script__.php';
$args = new PhutilArgumentParser($argv);
$args->setTagline('regenerate CSS sprite sheets');
$args->setSynopsis(<<<EOHELP
**sprites**
Rebuild CSS sprite sheets.
EOHELP
);
$args->parseStandardArguments();
$args->parse(
array(
array(
'name' => 'source',
'param' => 'directory',
'help' => 'Directory with sprite sources.',
)
));
$srcroot = $args->getArg('source');
if (!$srcroot) {
throw new Exception(
"You must specify a source directory with '--source'.");
}
$webroot = dirname(phutil_get_library_root('phabricator')).'/webroot/rsrc';
$webroot = Filesystem::readablePath($webroot);
function glx($x) {
return (60 + (48 * $x));
}
function gly($y) {
return (110 + (48 * $y));
}
$sheet = new PhutilSpriteSheet();
$at = '@';
$sheet->setCSSHeader(<<<EOCSS
/**
* @provides autosprite-css
* {$at}generated
*/
.autosprite {
background-image: url(/rsrc/image/autosprite.png);
background-repeat: no-repeat;
}
EOCSS
);
$menu_normal_template = id(new PhutilSprite())
->setSourceFile($srcroot.'/menu_normal_1x.png')
->setSourceSize(26, 26);
$menu_hover_template = id(new PhutilSprite())
->setSourceFile($srcroot.'/menu_hover_1x.png')
->setSourceSize(26, 26);
$menu_selected_template = id(new PhutilSprite())
->setSourceFile($srcroot.'/menu_selected_1x.png')
->setSourceSize(26, 26);
$menu_map = array(
'' => $menu_normal_template,
'-selected' => $menu_selected_template,
':hover' => $menu_hover_template,
);
$icon_map = array(
'help' => array(4, 19),
'settings' => array(0, 28),
'logout' => array(3, 6),
'notifications' => array(5, 20),
'task' => array(1, 15),
);
foreach ($icon_map as $icon => $coords) {
list($x, $y) = $coords;
foreach ($menu_map as $suffix => $template) {
$sheet->addSprite(
id(clone $template)
->setSourcePosition(glx($x), gly($y))
->setTargetCSS('.main-menu-item-icon-'.$icon.$suffix));
}
}
$app_template_large = id(new PhutilSprite())
->setSourceFile($srcroot.'/application_large_1x.png')
->setSourceSize(60, 60);
$app_template_large_hover = id(new PhutilSprite())
->setSourceFile($srcroot.'/application_large_hover_1x.png')
->setSourceSize(60, 60);
$app_template_small = id(new PhutilSprite())
->setSourceFile($srcroot.'/menu_normal_1x.png')
->setSourceSize(30, 30);
$app_template_small_hover = id(new PhutilSprite())
->setSourceFile($srcroot.'/menu_hover_1x.png')
->setSourceSize(30, 30);
$app_template_small_selected = id(new PhutilSprite())
->setSourceFile($srcroot.'/menu_selected_1x.png')
->setSourceSize(30, 30);
$app_source_map = array(
'-large' => array($app_template_large, 2),
// For the application launch view, we only show hover state on the desktop
// because it looks glitchy on touch devices. We show the hover state when
// the surrounding <a> is hovered, not the icon itself.
'-large /* hover */' => array(
$app_template_large_hover,
2,
'.device-desktop .phabricator-application-launch-container:hover '),
'' => array($app_template_small, 1),
// Show hover state only for the desktop.
':hover' => array(
$app_template_small_hover,
1,
'.device-desktop ',
),
'-selected' => array($app_template_small_selected, 1),
);
$app_map = array(
'differential' => array(9, 1),
'fact' => array(2, 4),
'mail' => array(0, 1),
'diffusion' => array(7, 13),
'slowvote' => array(1, 4),
'phriction' => array(1, 7),
'maniphest' => array(3, 24),
'flags' => array(6, 26),
'settings' => array(9, 11),
'applications' => array(0, 34),
'default' => array(9, 9),
'people' => array(3, 0),
'ponder' => array(4, 35),
'calendar' => array(5, 4),
'files' => array(6, 3),
'projects' => array(7, 35),
'daemons' => array(7, 6),
'herald' => array(1, 5),
'countdown' => array(7, 5),
'conduit' => array(7, 30),
'feed' => array(3, 11),
'paste' => array(9, 2),
'audit' => array(8, 19),
'uiexample' => array(7, 28),
'phpast' => array(6, 31),
'owners' => array(5, 32),
'phid' => array(9, 25),
'diviner' => array(1, 35),
'repositories' => array(8, 13),
'phame' => array(8, 4),
'macro' => array(0, 31),
'releeph' => array(5, 18),
'drydock' => array(5, 25),
);
$xadj = -1;
foreach ($app_map as $icon => $coords) {
list($x, $y) = $coords;
foreach ($app_source_map as $suffix => $spec) {
list($template, $scale) = $spec;
if (isset($spec[2])) {
$prefix = $spec[2];
} else {
$prefix = '';
}
$sheet->addSprite(
id(clone $template)
->setSourcePosition(($xadj + glx($x)) * $scale, gly($y) * $scale)
->setTargetCSS($prefix.'.app-'.$icon.$suffix));
}
}
$action_template = id(new PhutilSprite())
->setSourcePosition(0, 0)
->setSourceSize(16, 16);
$action_icons = PhabricatorActionView::getAvailableIcons();
foreach ($action_icons as $icon) {
$action_map[$icon] = 'icon/'.$icon.'.png';
}
foreach ($action_map as $icon => $source) {
$sheet->addSprite(
id(clone $action_template)
->setSourceFile($srcroot.$source)
->setTargetCSS('.action-'.$icon));
}
$remarkup_template = id(new PhutilSprite())
->setSourcePosition(0, 0)
->setSourceSize(14, 14);
$remarkup_icons = array(
'b',
'code',
'i',
'image',
'ol',
'tag',
'tt',
'ul',
'help',
'table',
);
foreach ($remarkup_icons as $icon) {
$sheet->addSprite(
id(clone $remarkup_template)
->setSourceFile($srcroot.'remarkup/text_'.$icon.'.png')
->setTargetCSS('.remarkup-assist-'.$icon));
}
$sheet->generateImage($webroot.'/image/autosprite.png');
$sheet->generateCSS($webroot.'/css/autosprite.css');
echo "Done.\n";
diff --git a/scripts/celerity_mapper.php b/scripts/celerity_mapper.php
index e3c2fc7321..028010fee6 100755
--- a/scripts/celerity_mapper.php
+++ b/scripts/celerity_mapper.php
@@ -1,379 +1,363 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$package_spec = array(
'javelin.pkg.js' => array(
'javelin-util',
'javelin-install',
'javelin-event',
'javelin-stratcom',
'javelin-behavior',
'javelin-request',
'javelin-vector',
'javelin-dom',
'javelin-json',
'javelin-uri',
),
'typeahead.pkg.js' => array(
'javelin-typeahead',
'javelin-typeahead-normalizer',
'javelin-typeahead-source',
'javelin-typeahead-preloaded-source',
'javelin-typeahead-ondemand-source',
'javelin-tokenizer',
'javelin-behavior-aphront-basic-tokenizer',
),
'core.pkg.js' => array(
'javelin-mask',
'javelin-workflow',
'javelin-behavior-workflow',
'javelin-behavior-aphront-form-disable-on-submit',
'phabricator-keyboard-shortcut-manager',
'phabricator-keyboard-shortcut',
'javelin-behavior-phabricator-keyboard-shortcuts',
'javelin-behavior-refresh-csrf',
'javelin-behavior-phabricator-watch-anchor',
'javelin-behavior-phabricator-autofocus',
'phabricator-paste-file-upload',
'phabricator-menu-item',
'phabricator-dropdown-menu',
'javelin-behavior-phabricator-oncopy',
'phabricator-tooltip',
'javelin-behavior-phabricator-tooltips',
'phabricator-prefab',
),
'core.pkg.css' => array(
'phabricator-core-css',
'phabricator-core-buttons-css',
'phabricator-standard-page-view',
'aphront-dialog-view-css',
'aphront-form-view-css',
'aphront-panel-view-css',
'aphront-side-nav-view-css',
'aphront-table-view-css',
'aphront-crumbs-view-css',
'aphront-tokenizer-control-css',
'aphront-typeahead-control-css',
'aphront-list-filter-view-css',
'phabricator-directory-css',
'phabricator-jump-nav',
'phabricator-app-buttons-css',
'phabricator-remarkup-css',
'syntax-highlighting-css',
'aphront-pager-view-css',
'phabricator-transaction-view-css',
'aphront-tooltip-css',
'aphront-headsup-view-css',
'phabricator-flag-css',
'aphront-error-view-css',
),
'differential.pkg.css' => array(
'differential-core-view-css',
'differential-changeset-view-css',
'differential-results-table-css',
'differential-revision-history-css',
'differential-revision-list-css',
'differential-table-of-contents-css',
'differential-revision-comment-css',
'differential-revision-add-comment-css',
'differential-revision-comment-list-css',
'phabricator-object-selector-css',
'aphront-headsup-action-list-view-css',
'phabricator-content-source-view-css',
'differential-local-commits-view-css',
'inline-comment-summary-css',
),
'differential.pkg.js' => array(
'phabricator-drag-and-drop-file-upload',
'phabricator-shaped-request',
'javelin-behavior-differential-feedback-preview',
'javelin-behavior-differential-edit-inline-comments',
'javelin-behavior-differential-populate',
'javelin-behavior-differential-show-more',
'javelin-behavior-differential-diff-radios',
'javelin-behavior-differential-accept-with-errors',
'javelin-behavior-differential-comment-jump',
'javelin-behavior-differential-add-reviewers-and-ccs',
'javelin-behavior-differential-keyboard-navigation',
'javelin-behavior-aphront-drag-and-drop',
'javelin-behavior-aphront-drag-and-drop-textarea',
'javelin-behavior-phabricator-object-selector',
'javelin-behavior-repository-crossreference',
'differential-inline-comment-editor',
'javelin-behavior-differential-dropdown-menus',
),
'diffusion.pkg.css' => array(
'diffusion-commit-view-css',
'diffusion-icons-css',
),
'diffusion.pkg.js' => array(
'javelin-behavior-diffusion-pull-lastmodified',
'javelin-behavior-diffusion-commit-graph',
'javelin-behavior-audit-preview',
),
'maniphest.pkg.css' => array(
'maniphest-task-summary-css',
'maniphest-transaction-detail-css',
'aphront-attached-file-view-css',
'phabricator-project-tag-css',
),
'maniphest.pkg.js' => array(
'javelin-behavior-maniphest-batch-selector',
'javelin-behavior-maniphest-transaction-controls',
'javelin-behavior-maniphest-transaction-preview',
'javelin-behavior-maniphest-transaction-expand',
'javelin-behavior-maniphest-subpriority-editor',
),
);
require_once dirname(__FILE__).'/__init_script__.php';
$args = new PhutilArgumentParser($argv);
$args->setTagline('map static resources');
$args->setSynopsis(
"**celerity_mapper.php** [--output __path__] [--with-custom] <webroot>");
$args->parse(
array(
array(
'name' => 'output',
'param' => 'path',
'default' => '../src/__celerity_resource_map__.php',
'help' => "Set the path for resource map. It is usually useful for ".
"'celerity.resource-path' configuration.",
),
array(
'name' => 'with-custom',
'help' => 'Include resources in <webroot>/rsrc/custom/.',
),
array(
'name' => 'webroot',
'wildcard' => true,
),
));
$root = $args->getArg('webroot');
if (count($root) != 1 || !is_dir(reset($root))) {
$args->printHelpAndExit();
}
$root = Filesystem::resolvePath(reset($root));
$celerity_path = Filesystem::resolvePath($args->getArg('output'), $root);
$with_custom = $args->getArg('with-custom');
$resource_hash = PhabricatorEnv::getEnvConfig('celerity.resource-hash');
$runtime_map = array();
echo "Finding raw static resources...\n";
$finder = id(new FileFinder($root))
->withType('f')
->withSuffix('png')
->withSuffix('jpg')
->withSuffix('gif')
->withSuffix('swf')
->withFollowSymlinks(true)
->setGenerateChecksums(true);
if (!$with_custom) {
$finder->excludePath('./rsrc/custom');
}
$raw_files = $finder->find();
echo "Processing ".count($raw_files)." files";
foreach ($raw_files as $path => $hash) {
echo ".";
$path = '/'.Filesystem::readablePath($path, $root);
$type = CelerityResourceTransformer::getResourceType($path);
$hash = md5($hash.$path.$resource_hash);
$uri = '/res/'.substr($hash, 0, 8).$path;
$runtime_map[$path] = array(
'hash' => $hash,
'uri' => $uri,
'disk' => $path,
'type' => $type,
);
}
echo "\n";
$xformer = id(new CelerityResourceTransformer())
->setMinify(false)
->setRawResourceMap($runtime_map);
echo "Finding transformable static resources...\n";
$finder = id(new FileFinder($root))
->withType('f')
->withSuffix('js')
->withSuffix('css')
->withFollowSymlinks(true)
->setGenerateChecksums(true);
if (!$with_custom) {
$finder->excludePath('./rsrc/custom');
}
$files = $finder->find();
echo "Processing ".count($files)." files";
$file_map = array();
foreach ($files as $path => $raw_hash) {
echo ".";
$path = '/'.Filesystem::readablePath($path, $root);
$data = Filesystem::readFile($root.$path);
$data = $xformer->transformResource($path, $data);
$hash = md5($data);
$hash = md5($hash.$path.$resource_hash);
$file_map[$path] = array(
'hash' => $hash,
'disk' => $path,
);
}
echo "\n";
$resource_graph = array();
$hash_map = array();
$parser = new PhutilDocblockParser();
foreach ($file_map as $path => $info) {
$type = CelerityResourceTransformer::getResourceType($path);
$data = Filesystem::readFile($root.$info['disk']);
$matches = array();
$ok = preg_match('@/[*][*].*?[*]/@s', $data, $matches);
if (!$ok) {
throw new Exception(
"File {$path} does not have a header doc comment. Encode dependency ".
"data in a header docblock.");
}
list($description, $metadata) = $parser->parse($matches[0]);
$provides = preg_split('/\s+/', trim(idx($metadata, 'provides')));
$requires = preg_split('/\s+/', trim(idx($metadata, 'requires')));
$provides = array_filter($provides);
$requires = array_filter($requires);
if (!$provides) {
// Tests and documentation-only JS is permitted to @provide no targets.
continue;
}
if (count($provides) > 1) {
throw new Exception(
"File {$path} must @provide at most one Celerity target.");
}
$provides = reset($provides);
$uri = '/res/'.substr($info['hash'], 0, 8).$path;
$hash_map[$provides] = $info['hash'];
$resource_graph[$provides] = $requires;
$runtime_map[$provides] = array(
'uri' => $uri,
'type' => $type,
'requires' => $requires,
'disk' => $path,
);
}
$celerity_resource_graph = new CelerityResourceGraph();
$celerity_resource_graph->addNodes($resource_graph);
$celerity_resource_graph->setResourceGraph($resource_graph);
$celerity_resource_graph->loadGraph();
foreach ($resource_graph as $provides => $requires) {
$cycle = $celerity_resource_graph->detectCycles($provides);
if ($cycle) {
throw new Exception(
"Cycle detected in resource graph: ". implode($cycle, " => ")
);
}
}
$package_map = array();
foreach ($package_spec as $name => $package) {
$hashes = array();
$type = null;
foreach ($package as $symbol) {
if (empty($hash_map[$symbol])) {
throw new Exception(
"Package specification for '{$name}' includes '{$symbol}', but that ".
"symbol is not defined anywhere.");
}
if ($type === null) {
$type = $runtime_map[$symbol]['type'];
} else {
$ntype = $runtime_map[$symbol]['type'];
if ($type !== $ntype) {
throw new Exception(
"Package specification for '{$name}' mixes resources of type ".
"'{$type}' with resources of type '{$ntype}'. Each package may only ".
"contain one type of resource.");
}
}
$hashes[] = $symbol.':'.$hash_map[$symbol];
}
$key = substr(md5(implode("\n", $hashes)), 0, 8);
$package_map['packages'][$key] = array(
'name' => $name,
'symbols' => $package,
'uri' => '/res/pkg/'.$key.'/'.$name,
'type' => $type,
);
foreach ($package as $symbol) {
$package_map['reverse'][$symbol] = $key;
}
}
ksort($runtime_map);
$runtime_map = var_export($runtime_map, true);
$runtime_map = preg_replace('/\s+$/m', '', $runtime_map);
$runtime_map = preg_replace('/array \(/', 'array(', $runtime_map);
$package_map['packages'] = isort($package_map['packages'], 'name');
ksort($package_map['reverse']);
$package_map = var_export($package_map, true);
$package_map = preg_replace('/\s+$/m', '', $package_map);
$package_map = preg_replace('/array \(/', 'array(', $package_map);
$generated = '@'.'generated';
$resource_map = <<<EOFILE
<?php
/**
* This file is automatically generated. Use 'celerity_mapper.php' to rebuild
* it.
* {$generated}
*/
celerity_register_resource_map({$runtime_map}, {$package_map});
EOFILE;
echo "Writing map...\n";
Filesystem::writeFile($celerity_path, $resource_map);
echo "Done.\n";
diff --git a/scripts/conduit/api.php b/scripts/conduit/api.php
index e18cc880e4..583b1fdad9 100644
--- a/scripts/conduit/api.php
+++ b/scripts/conduit/api.php
@@ -1,95 +1,79 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$time_start = microtime(true);
if ($argc !== 3) {
echo "usage: api.php <user_phid> <method>\n";
exit(1);
}
$user = null;
$user_str = $argv[1];
try {
$user = id(new PhabricatorUser())
->loadOneWhere('phid = %s', $user_str);
} catch (Exception $e) {
// no op; we'll error in a line or two
}
if (empty($user)) {
echo "usage: api.php <user_phid> <method>\n" .
"user {$user_str} does not exist or failed to load\n";
exit(1);
}
$method = $argv[2];
$method_class_str = ConduitAPIMethod::getClassNameFromAPIMethodName($method);
try {
$method_class = newv($method_class_str, array());
} catch (Exception $e) {
echo "usage: api.php <user_phid> <method>\n" .
"method {$method_class_str} does not exist\n";
exit(1);
}
$log = new PhabricatorConduitMethodCallLog();
$log->setMethod($method);
$params = @file_get_contents('php://stdin');
$params = json_decode($params, true);
if (!is_array($params)) {
echo "provide method parameters on stdin as a JSON blob";
exit(1);
}
// build a quick ConduitAPIRequest from stdin PLUS the authenticated user
$conduit_request = new ConduitAPIRequest($params);
$conduit_request->setUser($user);
try {
$result = $method_class->executeMethod($conduit_request);
$error_code = null;
$error_info = null;
} catch (ConduitException $ex) {
$result = null;
$error_code = $ex->getMessage();
if ($ex->getErrorDescription()) {
$error_info = $ex->getErrorDescription();
} else {
$error_info = $method_handler->getErrorDescription($error_code);
}
}
$time_end = microtime(true);
$response = id(new ConduitAPIResponse())
->setResult($result)
->setErrorCode($error_code)
->setErrorInfo($error_info);
echo json_encode($response->toDictionary()), "\n";
// TODO -- how get $connection_id from SSH?
$connection_id = null;
$log->setConnectionID($connection_id);
$log->setError((string)$error_code);
$log->setDuration(1000000 * ($time_end - $time_start));
$log->save();
exit();
diff --git a/scripts/daemon/phabricator_daemon_launcher.php b/scripts/daemon/phabricator_daemon_launcher.php
index b7724290a1..2d2dc1ee3d 100755
--- a/scripts/daemon/phabricator_daemon_launcher.php
+++ b/scripts/daemon/phabricator_daemon_launcher.php
@@ -1,232 +1,216 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$control = new PhabricatorDaemonControl();
must_have_extension('pcntl');
must_have_extension('posix');
function must_have_extension($ext) {
if (!extension_loaded($ext)) {
echo "ERROR: The PHP extension '{$ext}' is not installed. You must ".
"install it to run daemons on this machine.\n";
exit(1);
}
}
$command = isset($argv[1]) ? $argv[1] : 'help';
switch ($command) {
case 'list':
$err = $control->executeListCommand();
exit($err);
case 'status':
$err = $control->executeStatusCommand();
exit($err);
case 'stop':
$pass_argv = array_slice($argv, 2);
$err = $control->executeStopCommand($pass_argv);
exit($err);
case 'restart':
$err = $control->executeStopCommand(array());
if ($err) {
exit($err);
}
/* Fall Through */
case 'start':
$running = $control->loadRunningDaemons();
// "running" might not mean actually running so much as was running at
// some point. ergo, do a quick grouping and only barf if daemons are
// *actually* running.
$running_dict = mgroup($running, 'isRunning');
if (!empty($running_dict[true])) {
echo phutil_console_wrap(
"phd start: Unable to start daemons because daemons are already ".
"running.\n".
"You can view running daemons with 'phd status'.\n".
"You can stop running daemons with 'phd stop'.\n".
"You can use 'phd restart' to stop all daemons before starting new ".
"daemons.\n");
exit(1);
}
$daemons = array(
array('PhabricatorRepositoryPullLocalDaemon', array()),
array('PhabricatorGarbageCollectorDaemon', array()),
);
$taskmasters = PhabricatorEnv::getEnvConfig('phd.start-taskmasters');
for ($ii = 0; $ii < $taskmasters; $ii++) {
$daemons[] = array('PhabricatorTaskmasterDaemon', array());
}
will_launch($control);
foreach ($daemons as $spec) {
list($name, $argv) = $spec;
echo "Launching '{$name}'...\n";
$control->launchDaemon($name, $argv);
}
echo "Done.\n";
break;
case 'repository-launch-readonly':
case 'repository-launch-master':
if ($command == 'repository-launch-readonly') {
$daemon_args = array(
'--',
'--no-discovery',
);
} else {
$daemon_args = array();
}
$need_launch = phd_load_tracked_repositories();
if (!$need_launch) {
echo "There are no repositories with tracking enabled.\n";
exit(1);
}
will_launch($control);
echo "Launching PullLocal daemon...\n";
$control->launchDaemon(
'PhabricatorRepositoryPullLocalDaemon',
$daemon_args);
echo "NOTE: '{$command}' is deprecated. Consult the documentation.\n";
echo "Done.\n";
break;
case 'launch':
case 'debug':
$is_debug = ($argv[1] == 'debug');
$daemon = idx($argv, 2);
if (!$daemon) {
throw new Exception("Daemon name required!");
}
$pass_argv = array_slice($argv, 3);
$n = 1;
if (!$is_debug) {
if (is_numeric($daemon)) {
$n = $daemon;
if ($n < 1) {
throw new Exception("Count must be at least 1!");
}
$daemon = idx($argv, 3);
if (!$daemon) {
throw new Exception("Daemon name required!");
}
$pass_argv = array_slice($argv, 4);
}
}
$loader = new PhutilSymbolLoader();
$symbols = $loader
->setAncestorClass('PhutilDaemon')
->selectSymbolsWithoutLoading();
$symbols = ipull($symbols, 'name');
$match = array();
foreach ($symbols as $symbol) {
if (stripos($symbol, $daemon) !== false) {
if (strtolower($symbol) == strtolower($daemon)) {
$match = array($symbol);
break;
} else {
$match[] = $symbol;
}
}
}
if (count($match) == 0) {
throw new Exception(
"No daemons match! Use 'phd list' for a list of daemons.");
} else if (count($match) > 1) {
throw new Exception(
"Which of these daemons did you mean?\n".
" ".implode("\n ", $match));
} else {
$daemon = reset($match);
}
$with_logs = true;
if ($is_debug) {
// In debug mode, we emit errors straight to stdout, so nothing useful
// will show up in the logs. Don't echo the message about stuff showing
// up in them, since it would be confusing.
$with_logs = false;
}
will_launch($control, $with_logs);
if ($is_debug) {
echo "Launching {$daemon} in debug mode (nondaemonized)...\n";
} else {
echo "Launching {$n} x {$daemon}";
}
for ($ii = 0; $ii < $n; $ii++) {
$control->launchDaemon($daemon, $pass_argv, $is_debug);
if (!$is_debug) {
echo ".";
}
}
echo "\n";
echo "Done.\n";
break;
case '--help':
case 'help':
default:
$err = $control->executeHelpCommand();
exit($err);
}
function phd_load_tracked_repositories() {
$repositories = id(new PhabricatorRepository())->loadAll();
foreach ($repositories as $key => $repository) {
if (!$repository->isTracked()) {
unset($repositories[$key]);
}
}
return $repositories;
}
function will_launch($control, $with_logs = true) {
echo "Staging launch...\n";
$control->pingConduit();
if ($with_logs) {
$log_dir = $control->getLogDirectory().'/daemons.log';
echo "NOTE: Logs will appear in '{$log_dir}'.\n\n";
}
}
diff --git a/scripts/differential/destroy_revision.php b/scripts/differential/destroy_revision.php
index 95fe0a0034..af97de3932 100755
--- a/scripts/differential/destroy_revision.php
+++ b/scripts/differential/destroy_revision.php
@@ -1,68 +1,52 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
$args->setTagline('permanently destroy a Differential Revision');
$args->setSynopsis(<<<EOHELP
**destroy_revision.php** __D123__
Permanently destroy the specified Differential Revision (for example,
because it contains secrets that the world is not ready to know).
Normally, you can just "Abandon" unwanted revisions, but in dire
circumstances this script can be used to completely destroy a
revision. Destroying a revision may cause some glitches in
linked objects.
The revision is utterly destroyed and can not be recovered unless you
have backups.
EOHELP
);
$args->parseStandardArguments();
$args->parse(
array(
array(
'name' => 'revision',
'wildcard' => true,
),
));
$revisions = $args->getArg('revision');
if (count($revisions) != 1) {
$args->printHelpAndExit();
}
$id = trim(strtolower(head($revisions)), 'd ');
$revision = id(new DifferentialRevision())->load($id);
if (!$revision) {
throw new Exception("No revision '{$id}' exists!");
}
$title = $revision->getTitle();
$ok = phutil_console_confirm("Really destroy 'D{$id}: {$title}' forever?");
if (!$ok) {
throw new Exception("User aborted workflow.");
}
$revision->delete();
echo "OK, destroyed revision.\n";
diff --git a/scripts/differential/detect_copied_code.php b/scripts/differential/detect_copied_code.php
index d3dfce46cf..1db5c0e76f 100755
--- a/scripts/differential/detect_copied_code.php
+++ b/scripts/differential/detect_copied_code.php
@@ -1,50 +1,34 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
if ($argc != 3 || !is_numeric($argv[1]) || !is_numeric($argv[2])) {
echo "Usage: {$argv[0]} <diff_id_from> <diff_id_to>\n";
exit(1);
}
list(, $from, $to) = $argv;
for ($diff_id = $from; $diff_id <= $to; $diff_id++) {
echo "Processing $diff_id";
$diff = id(new DifferentialDiff())->load($diff_id);
if ($diff) {
$diff->attachChangesets($diff->loadChangesets());
$orig_copy = array();
foreach ($diff->getChangesets() as $i => $changeset) {
$orig_copy[$i] = idx((array)$changeset->getMetadata(), 'copy:lines');
$changeset->attachHunks($changeset->loadHunks());
}
$diff->detectCopiedCode();
foreach ($diff->getChangesets() as $i => $changeset) {
if (idx($changeset->getMetadata(), 'copy:lines') || $orig_copy[$i]) {
echo ".";
$changeset->save();
}
}
}
echo "\n";
}
echo "Done.\n";
diff --git a/scripts/differential/remove_empty_revisions.php b/scripts/differential/remove_empty_revisions.php
index 1aa72b10fa..3588890171 100755
--- a/scripts/differential/remove_empty_revisions.php
+++ b/scripts/differential/remove_empty_revisions.php
@@ -1,51 +1,35 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$revision = new DifferentialRevision();
$empty_revisions = queryfx_all(
$revision->establishConnection('r'),
'select distinct r.id from differential_revision r left join '.
'differential_diff d on r.id=d.revisionID where d.revisionID is NULL');
$empty_revisions = ipull($empty_revisions, 'id');
if (!$empty_revisions) {
echo "No empty revisions found.\n";
exit(0);
}
echo phutil_console_wrap(
"The following revision don't contain any diff:\n".
implode(', ', $empty_revisions));
if (!phutil_console_confirm('Do you want to delete them?')) {
echo "Cancelled.\n";
exit(1);
}
foreach ($empty_revisions as $revision_id) {
$revision = id(new DifferentialRevision())->load($revision_id);
$revision->delete();
}
echo "Done.\n";
diff --git a/scripts/drydock/drydock_control.php b/scripts/drydock/drydock_control.php
index e9347a5be4..816c820312 100755
--- a/scripts/drydock/drydock_control.php
+++ b/scripts/drydock/drydock_control.php
@@ -1,39 +1,23 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
$args->setTagline('manage drydock software resources');
$args->setSynopsis(<<<EOSYNOPSIS
**drydock** __commmand__ [__options__]
Manage Drydock stuff. NEW AND EXPERIMENTAL.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = array(
new DrydockManagementWaitForLeaseWorkflow(),
new DrydockManagementLeaseWorkflow(),
new PhutilHelpArgumentWorkflow(),
);
$args->parseWorkflows($workflows);
diff --git a/scripts/fact/manage_facts.php b/scripts/fact/manage_facts.php
index d37be22ed5..13e3598651 100755
--- a/scripts/fact/manage_facts.php
+++ b/scripts/fact/manage_facts.php
@@ -1,43 +1,27 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
$args->setTagline('manage fact configuration');
$args->setSynopsis(<<<EOSYNOPSIS
**fact** __command__ [__options__]
Manage and debug Phabricator data extraction, storage and
configuration used to compute statistics.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = array(
new PhabricatorFactManagementDestroyWorkflow(),
new PhabricatorFactManagementAnalyzeWorkflow(),
new PhabricatorFactManagementStatusWorkflow(),
new PhabricatorFactManagementListWorkflow(),
new PhabricatorFactManagementCursorsWorkflow(),
new PhutilHelpArgumentWorkflow(),
);
$args->parseWorkflows($workflows);
diff --git a/scripts/files/manage_files.php b/scripts/files/manage_files.php
index 3a9ec7707e..02893a3e0f 100755
--- a/scripts/files/manage_files.php
+++ b/scripts/files/manage_files.php
@@ -1,39 +1,23 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
$args->setTagline('manage files');
$args->setSynopsis(<<<EOSYNOPSIS
**files** __command__ [__options__]
Manage Phabricator file storage.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = array(
new PhabricatorFilesManagementEnginesWorkflow(),
new PhabricatorFilesManagementMigrateWorkflow(),
new PhutilHelpArgumentWorkflow(),
);
$args->parseWorkflows($workflows);
diff --git a/scripts/fpm/warmup.php b/scripts/fpm/warmup.php
index f4744d1991..956a474131 100644
--- a/scripts/fpm/warmup.php
+++ b/scripts/fpm/warmup.php
@@ -1,54 +1,38 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* NOTE: This is an ADVANCED feature that improves performance but adds a lot
* of complexity! This is only suitable for production servers because workers
* won't pick up changes between when they spawn and when they handle a request.
*
* Phabricator spends a significant portion of its runtime loading classes
* and functions, even with APC enabled. Since we have very rigidly-defined
* rules about what can go in a module (specifically: no side effects), it
* is safe to load all the libraries *before* we receive a request.
*
* Normally, SAPIs don't provide a way to do this, but with a patched PHP-FPM
* SAPI you can provide a warmup file that it will execute before a request
* is received.
*
* We're limited in what we can do here, since request information won't
* exist yet, but we can load class and function definitions, which is what
* we're really interested in.
*
* Once this file exists, the FCGI process will drop into its normal accept loop
* and eventually process a request.
*/
function __warmup__() {
$root = dirname(dirname(dirname(dirname(__FILE__))));
require_once $root.'/libphutil/src/__phutil_library_init__.php';
require_once $root.'/arcanist/src/__phutil_library_init__.php';
require_once $root.'/phabricator/src/__phutil_library_init__.php';
// Load every symbol. We could possibly refine this -- we don't need to load
// every Controller, for instance.
$loader = new PhutilSymbolLoader();
$loader->selectAndLoadSymbols();
define('__WARMUP__', true);
}
__warmup__();
diff --git a/scripts/mail/create_worker_tasks.php b/scripts/mail/create_worker_tasks.php
index acd5f76049..1e0c0a3b99 100755
--- a/scripts/mail/create_worker_tasks.php
+++ b/scripts/mail/create_worker_tasks.php
@@ -1,45 +1,29 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/*
* After upgrading to/past D1723, the handling of messages queued for delivery
* is a bit different. Running this script will take any messages that are
* queued for delivery, but don't have a worker task created, and create that
* worker task. Without the worker task, the message will just sit at "queued
* for delivery" and nothing will happen to it.
*/
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$messages = id(new PhabricatorMetaMTAMail())->loadAllWhere(
'status = %s', PhabricatorMetaMTAMail::STATUS_QUEUE);
foreach ($messages as $message) {
if (!$message->getWorkerTaskID()) {
$mailer_task = PhabricatorWorker::scheduleTask(
'PhabricatorMetaMTAWorker',
$message->getID());
$message->setWorkerTaskID($mailer_task->getID());
$message->save();
$id = $message->getID();
echo "#$id\n";
}
}
diff --git a/scripts/mail/mail_handler.php b/scripts/mail/mail_handler.php
index d4cd47087a..19e315364d 100755
--- a/scripts/mail/mail_handler.php
+++ b/scripts/mail/mail_handler.php
@@ -1,83 +1,67 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
if ($argc > 1) {
$_SERVER['PHABRICATOR_ENV'] = $argv[1];
}
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
require_once $root.'/externals/mimemailparser/MimeMailParser.class.php';
$parser = new MimeMailParser();
$parser->setText(file_get_contents('php://stdin'));
$text_body = $parser->getMessageBody('text');
$text_body_headers = $parser->getMessageBodyHeaders('text');
$content_type = idx($text_body_headers, 'content-type');
if (
!phutil_is_utf8($text_body) &&
(preg_match('/charset="(.*?)"/', $content_type, $matches) ||
preg_match('/charset=(\S+)/', $content_type, $matches))
) {
$text_body = phutil_utf8_convert($text_body, "UTF-8", $matches[1]);
}
$headers = $parser->getHeaders();
$headers['subject'] = iconv_mime_decode($headers['subject'], 0, "UTF-8");
$headers['from'] = iconv_mime_decode($headers['from'], 0, "UTF-8");
$received = new PhabricatorMetaMTAReceivedMail();
$received->setHeaders($headers);
$received->setBodies(array(
'text' => $text_body,
'html' => $parser->getMessageBody('html'),
));
$attachments = array();
foreach ($parser->getAttachments() as $attachment) {
if (preg_match('@text/(plain|html)@', $attachment->getContentType()) &&
$attachment->getContentDisposition() == 'inline') {
// If this is an "inline" attachment with some sort of text content-type,
// do not treat it as a file for attachment. MimeMailParser already picked
// it up in the getMessageBody() call above. We still want to treat 'inline'
// attachments with other content types (e.g., images) as attachments.
continue;
}
$file = PhabricatorFile::newFromFileData(
$attachment->getContent(),
array(
'name' => $attachment->getFilename(),
));
$attachments[] = $file->getPHID();
}
try {
$received->setAttachments($attachments);
$received->save();
$received->processReceivedMail();
} catch (Exception $e) {
$received
->setMessage('EXCEPTION: '.$e->getMessage())
->save();
}
diff --git a/scripts/profile/rescale_all_user_pics.php b/scripts/profile/rescale_all_user_pics.php
index 2243f38760..3d019a3a93 100755
--- a/scripts/profile/rescale_all_user_pics.php
+++ b/scripts/profile/rescale_all_user_pics.php
@@ -1,57 +1,41 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
echo "Examining users.\n";
foreach (new LiskMigrationIterator(new PhabricatorUser()) as $user) {
$file = id(new PhabricatorFile())
->loadOneWhere('phid = %s', $user->getProfileImagePHID());
if (!$file) {
echo 'No pic for user ', $user->getUserName(), "\n";
continue;
}
$data = $file->loadFileData();
$img = imagecreatefromstring($data);
$sx = imagesx($img);
$sy = imagesy($img);
if ($sx != 50 || $sy != 50) {
echo 'Found one! User ', $user->getUserName(), "\n";
$xformer = new PhabricatorImageTransformer();
// Resize OAuth image to a reasonable size
$small_xformed = $xformer->executeProfileTransform(
$file,
$width = 50,
$min_height = 50,
$max_height = 50);
$user->setProfileImagePHID($small_xformed->getPHID());
$user->save();
break;
} else {
echo '.';
}
}
echo "\n";
echo "Done.\n";
diff --git a/scripts/repository/audit.php b/scripts/repository/audit.php
index 6e729d5352..2ebf249579 100755
--- a/scripts/repository/audit.php
+++ b/scripts/repository/audit.php
@@ -1,99 +1,83 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
$args->setTagline('manage open Audit requests');
$args->setSynopsis(<<<EOSYNOPSIS
**audit.php** __repository_callsign__
Close all open audit requests in a repository. This is intended to
reset the state of an imported repository which triggered a bunch of
spurious audit requests during import.
EOSYNOPSIS
);
$args->parseStandardArguments();
$args->parse(
array(
array(
'name' => 'more',
'wildcard' => true,
),
));
$more = $args->getArg('more');
if (count($more) !== 1) {
$args->printHelpAndExit();
}
$callsign = reset($more);
$repository = id(new PhabricatorRepository())->loadOneWhere(
'callsign = %s',
$callsign);
if (!$repository) {
throw new Exception("No repository exists with callsign '{$callsign}'!");
}
$ok = phutil_console_confirm(
'This will reset all open audit requests ("Audit Required" or "Concern '.
'Raised") for commits in this repository to "Audit Not Required". This '.
'operation destroys information and can not be undone! Are you sure '.
'you want to proceed?');
if (!$ok) {
echo "OK, aborting.\n";
die(1);
}
echo "Loading commits...\n";
$all_commits = id(new PhabricatorRepositoryCommit())->loadAllWhere(
'repositoryID = %d',
$repository->getID());
echo "Clearing audit requests...\n";
foreach ($all_commits as $commit) {
$query = id(new PhabricatorAuditQuery())
->withStatus(PhabricatorAuditQuery::STATUS_OPEN)
->withCommitPHIDs(array($commit->getPHID()));
$requests = $query->execute();
echo "Clearing ".$commit->getPHID()."... ";
if (!$requests) {
echo "nothing to do.\n";
continue;
} else {
echo count($requests)." requests to clear";
}
foreach ($requests as $request) {
$request->setAuditStatus(
PhabricatorAuditStatusConstants::AUDIT_NOT_REQUIRED);
$request->save();
echo ".";
}
$commit->setAuditStatus(PhabricatorAuditCommitStatusConstants::NONE);
$commit->save();
echo "\n";
}
echo "Done.\n";
diff --git a/scripts/repository/discover.php b/scripts/repository/discover.php
index 253a9db087..e5f2df166b 100755
--- a/scripts/repository/discover.php
+++ b/scripts/repository/discover.php
@@ -1,24 +1,8 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
echo
"This script has moved. All repository management is now performed through ".
"'bin/repository'. Use this command instead:\n\n".
" phabricator/ $ ./bin/repository discover ...\n";
exit(1);
diff --git a/scripts/repository/manage_repositories.php b/scripts/repository/manage_repositories.php
index b1e23f5ed8..694910a83d 100755
--- a/scripts/repository/manage_repositories.php
+++ b/scripts/repository/manage_repositories.php
@@ -1,42 +1,26 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
$args->setTagline('manage repositories');
$args->setSynopsis(<<<EOSYNOPSIS
**repository** __command__ [__options__]
Manage and debug Phabricator repository configuration, tracking,
discovery and import.
EOSYNOPSIS
);
$args->parseStandardArguments();
$workflows = array(
new PhabricatorRepositoryManagementPullWorkflow(),
new PhabricatorRepositoryManagementDiscoverWorkflow(),
new PhabricatorRepositoryManagementListWorkflow(),
new PhabricatorRepositoryManagementDeleteWorkflow(),
new PhutilHelpArgumentWorkflow(),
);
$args->parseWorkflows($workflows);
diff --git a/scripts/repository/parse_one_commit.php b/scripts/repository/parse_one_commit.php
index 855899b8bd..6b6d5f81b4 100755
--- a/scripts/repository/parse_one_commit.php
+++ b/scripts/repository/parse_one_commit.php
@@ -1,23 +1,7 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
echo "This script is obsolete. Instead, use:\n\n".
" $ reparse.php <commit_name> --message --change\n\n".
"See that script for more options.\n";
exit(1);
diff --git a/scripts/repository/pull.php b/scripts/repository/pull.php
index f4f62fc7b3..5f716e77d4 100755
--- a/scripts/repository/pull.php
+++ b/scripts/repository/pull.php
@@ -1,24 +1,8 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
echo
"This script has moved. All repository management is now performed through ".
"'bin/repository'. Use this command instead:\n\n".
" phabricator/ $ ./bin/repository pull ...\n";
exit(1);
diff --git a/scripts/repository/rebuild_summaries.php b/scripts/repository/rebuild_summaries.php
index d06f146c4d..d0bb215c3c 100755
--- a/scripts/repository/rebuild_summaries.php
+++ b/scripts/repository/rebuild_summaries.php
@@ -1,69 +1,53 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$commit = new PhabricatorRepositoryCommit();
$conn_w = id(new PhabricatorRepository())->establishConnection('w');
$sizes = queryfx_all(
$conn_w,
'SELECT repositoryID, count(*) N FROM %T GROUP BY repositoryID',
$commit->getTableName());
$sizes = ipull($sizes, 'N', 'repositoryID');
$maxes = queryfx_all(
$conn_w,
'SELECT repositoryID, max(epoch) maxEpoch FROM %T GROUP BY repositoryID',
$commit->getTableName());
$maxes = ipull($maxes, 'maxEpoch', 'repositoryID');
$repository_ids = array_keys($sizes + $maxes);
echo "Updating ".count($repository_ids)." repositories";
foreach ($repository_ids as $repository_id) {
$last_commit = queryfx_one(
$conn_w,
'SELECT id FROM %T WHERE repositoryID = %d AND epoch = %d LIMIT 1',
$commit->getTableName(),
$repository_id,
idx($maxes, $repository_id, 0));
if ($last_commit) {
$last_commit = $last_commit['id'];
} else {
$last_commit = 0;
}
queryfx(
$conn_w,
'INSERT INTO %T (repositoryID, lastCommitID, size, epoch)
VALUES (%d, %d, %d, %d) ON DUPLICATE KEY UPDATE
lastCommitID = VALUES(lastCommitID),
size = VALUES(size),
epoch = VALUES(epoch)',
PhabricatorRepository::TABLE_SUMMARY,
$repository_id,
$last_commit,
idx($sizes, $repository_id, 0),
idx($maxes, $repository_id, 0));
echo ".";
}
echo "\ndone.\n";
diff --git a/scripts/repository/reconcile.php b/scripts/repository/reconcile.php
index 224438c21b..1e0f99854b 100755
--- a/scripts/repository/reconcile.php
+++ b/scripts/repository/reconcile.php
@@ -1,168 +1,152 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
$args->setTagline('reconcile Phabricator state after repository changes');
$args->setSynopsis(<<<EOSYNOPSIS
**reconcile.php** __repository_callsign__
Reconcile the state of Phabricator's caches with the actual state
of the repository.
This is an administrative/maintenace operation and not generally
necessary, but if repository history has changed or been rewritten
(for example, if the repository was stored from a backup)
Phabricator may think commits which are no longer present in the
repository still exist.
This will delete all evidence of commits which Phabricator can't
find in the actual repository.
EOSYNOPSIS
);
$args->parseStandardArguments();
$args->parse(
array(
array(
'name' => 'more',
'wildcard' => true,
),
));
$more = $args->getArg('more');
if (count($more) !== 1) {
$args->printHelpAndExit();
}
$callsign = reset($more);
$repository = id(new PhabricatorRepository())->loadOneWhere(
'callsign = %s',
$callsign);
if (!$repository) {
throw new Exception("No repository exists with callsign '{$callsign}'!");
}
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
default:
throw new Exception("For now, you can only reconcile git repositories.");
}
echo "Loading commits...\n";
$all_commits = id(new PhabricatorRepositoryCommit())->loadAllWhere(
'repositoryID = %d',
$repository->getID());
echo "Updating repository..\n";
try {
// Sanity-check the repository working copy and make sure we're up to date.
$repository->execxLocalCommand('fetch --all');
} catch (Exception $ex) {
echo "Unable to `git fetch` the working copy to update it. Reconciliation ".
"requires an up-to-date working copy.\n";
throw $ex;
}
echo "Verifying commits (this may take some time if the repository is large)";
$futures = array();
foreach ($all_commits as $id => $commit) {
// NOTE: We use "cat-file -t", not "rev-parse --verify", because
// "rev-parse --verify" does not verify that the object actually exists, only
// that the name is properly formatted.
$futures[$id] = $repository->getLocalCommandFuture(
'cat-file -t %s',
$commit->getCommitIdentifier());
}
$bad = array();
foreach (Futures($futures)->limit(8) as $id => $future) {
list($err) = $future->resolve();
if ($err) {
$bad[$id] = $all_commits[$id];
echo "#";
} else {
echo ".";
}
}
echo "\nDone.\n";
if (!count($bad)) {
echo "No bad commits found!\n";
} else {
echo "Found ".count($bad)." bad commits:\n\n";
echo ' '.implode("\n ", mpull($bad, 'getCommitIdentifier'));
$ok = phutil_console_confirm("Do you want to delete these commits?");
if (!$ok) {
echo "OK, aborting.\n";
exit(1);
}
echo "Deleting commits";
foreach ($bad as $commit) {
echo ".";
$commit->delete();
}
echo "\nDone.\n";
}
//// Clean Up Links ////////////////////////////////////////////////////////
$table = new PhabricatorRepositoryCommit();
$valid_phids = queryfx_all(
$table->establishConnection('r'),
'SELECT phid FROM %T',
$table->getTableName());
$valid_phids = ipull($valid_phids, null, 'phid');
//////// Differential <-> Diffusion Links //////////////////////////////////
$dx_conn = id(new DifferentialRevision())->establishConnection('w');
$dx_table = DifferentialRevision::TABLE_COMMIT;
$dx_phids = queryfx_all(
$dx_conn,
'SELECT commitPHID FROM %T',
$dx_table);
$bad_phids = array();
foreach ($dx_phids as $dx_phid) {
if (empty($valid_phids[$dx_phid['commitPHID']])) {
$bad_phids[] = $dx_phid['commitPHID'];
}
}
if ($bad_phids) {
echo "Deleting ".count($bad_phids)." bad Diffusion links...\n";
queryfx(
$dx_conn,
'DELETE FROM %T WHERE commitPHID IN (%Ls)',
$dx_table,
$bad_phids);
echo "Done.\n";
} else {
echo "Diffusion links are clean.\n";
}
// TODO: There are some links in owners that we should probably clean up too.
diff --git a/scripts/repository/reparse.php b/scripts/repository/reparse.php
index 940e23f5aa..b983f7dfc5 100755
--- a/scripts/repository/reparse.php
+++ b/scripts/repository/reparse.php
@@ -1,247 +1,231 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
$args->setSynopsis(<<<EOHELP
**reparse.php** __what__ __which_parts__ [--trace] [--force]
Rerun the Diffusion parser on specific commits and repositories. Mostly
useful for debugging changes to Diffusion.
EOHELP
);
$args->parseStandardArguments();
$args->parse(
array(
// what
array(
'name' => 'revision',
'wildcard' => true,
),
array(
'name' => 'all',
'param' => 'callsign or phid',
'help' => 'Reparse all commits in the specified repository. This '.
'mode queues parsers into the task queue; you must run '.
'taskmasters to actually do the parses. Use with '.
'__--force-local__ to run the tasks locally instead of '.
'with taskmasters.',
),
array(
'name' => 'min-date',
'param' => 'date',
'help' => 'When used with __--all__, this will restrict to '.
'reparsing only the commits that are newer than __date__.',
),
// which parts
array(
'name' => 'message',
'help' => 'Reparse commit messages.',
),
array(
'name' => 'change',
'help' => 'Reparse changes.',
),
array(
'name' => 'herald',
'help' => 'Reevaluate Herald rules (may send huge amounts of email!)',
),
array(
'name' => 'owners',
'help' => 'Reevaluate related commits for owners packages (may '.
'delete existing relationship entries between your '.
'package and some old commits!)',
),
// misc options
array(
'name' => 'force',
'short' => 'f',
'help' => 'Act noninteractively, without prompting.',
),
array(
'name' => 'force-local',
'help' => 'Only used with __--all__, use this to run the tasks '.
'locally instead of deferring them to taskmaster daemons.',
),
));
$all_from_repo = $args->getArg('all');
$reparse_message = $args->getArg('message');
$reparse_change = $args->getArg('change');
$reparse_herald = $args->getArg('herald');
$reparse_owners = $args->getArg('owners');
$reparse_what = $args->getArg('revision');
$force = $args->getArg('force');
$force_local = $args->getArg('force-local');
$min_date = $args->getArg('min-date');
if (!$all_from_repo && !$reparse_what) {
usage("Specify a commit or repository to reparse.");
}
if (!$reparse_message && !$reparse_change && !$reparse_herald &&
!$reparse_owners) {
usage("Specify what information to reparse with --message, --change, ".
"--herald, and/or --owners");
}
if ($reparse_owners && !$force) {
echo phutil_console_wrap(
"You are about to recreate the relationship entries between the commits ".
"and the packages they touch. This might delete some existing ".
"relationship entries for some old commits.");
if (!phutil_console_confirm('Are you ready to continue?')) {
echo "Cancelled.\n";
exit(1);
}
}
$commits = array();
if ($all_from_repo) {
$repository = id(new PhabricatorRepository())->loadOneWhere(
'callsign = %s OR phid = %s',
$all_from_repo,
$all_from_repo);
if (!$repository) {
throw new Exception("Unknown repository {$all_from_repo}!");
}
$constraint = '';
if ($min_date) {
$table = new PhabricatorRepositoryCommit();
$conn_r = $table->establishConnection('r');
$constraint = qsprintf(
$conn_r,
'AND epoch > unix_timestamp(%s)',
$min_date);
}
$commits = id(new PhabricatorRepositoryCommit())->loadAllWhere(
'repositoryID = %d %Q',
$repository->getID(),
$constraint);
if (!$commits) {
throw new Exception("No commits have been discovered in that repository!");
}
$callsign = $repository->getCallsign();
} else {
$commits = array();
foreach ($reparse_what as $identifier) {
$matches = null;
if (!preg_match('/r([A-Z]+)([a-z0-9]+)/', $identifier, $matches)) {
throw new Exception("Can't parse commit identifier!");
}
$callsign = $matches[1];
$commit_identifier = $matches[2];
$repository = id(new PhabricatorRepository())->loadOneWhere(
'callsign = %s',
$callsign);
if (!$repository) {
throw new Exception("No repository with callsign '{$callsign}'!");
}
$commit = id(new PhabricatorRepositoryCommit())->loadOneWhere(
'repositoryID = %d AND commitIdentifier = %s',
$repository->getID(),
$commit_identifier);
if (!$commit) {
throw new Exception(
"No matching commit '{$commit_identifier}' in repository ".
"'{$callsign}'. (For git and mercurial repositories, you must specify ".
"the entire commit hash.)");
}
$commits[] = $commit;
}
}
if ($all_from_repo && !$force_local) {
echo phutil_console_format(
'**NOTE**: This script will queue tasks to reparse the data. Once the '.
'tasks have been queued, you need to run Taskmaster daemons to execute '.
'them.');
echo "\n\n";
echo "QUEUEING TASKS (".number_format(count($commits))." Commits):\n";
}
$tasks = array();
foreach ($commits as $commit) {
$classes = array();
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
if ($reparse_message) {
$classes[] = 'PhabricatorRepositoryGitCommitMessageParserWorker';
}
if ($reparse_change) {
$classes[] = 'PhabricatorRepositoryGitCommitChangeParserWorker';
}
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
if ($reparse_message) {
$classes[] = 'PhabricatorRepositoryMercurialCommitMessageParserWorker';
}
if ($reparse_change) {
$classes[] = 'PhabricatorRepositoryMercurialCommitChangeParserWorker';
}
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
if ($reparse_message) {
$classes[] = 'PhabricatorRepositorySvnCommitMessageParserWorker';
}
if ($reparse_change) {
$classes[] = 'PhabricatorRepositorySvnCommitChangeParserWorker';
}
break;
}
if ($reparse_herald) {
$classes[] = 'PhabricatorRepositoryCommitHeraldWorker';
}
if ($reparse_owners) {
$classes[] = 'PhabricatorRepositoryCommitOwnersWorker';
}
$spec = array(
'commitID' => $commit->getID(),
'only' => true,
);
if ($all_from_repo && !$force_local) {
foreach ($classes as $class) {
PhabricatorWorker::scheduleTask($class, $spec);
$commit_name = 'r'.$callsign.$commit->getCommitIdentifier();
echo " Queued '{$class}' for commit '{$commit_name}'.\n";
}
} else {
foreach ($classes as $class) {
$worker = newv($class, array($spec));
echo "Running '{$class}'...\n";
$worker->doWork();
}
}
}
echo "\nDone.\n";
function usage($message) {
echo phutil_console_format(
'**Usage Exception:** '.$message."\n");
exit(1);
}
diff --git a/scripts/repository/reparse_all_commit_messages.php b/scripts/repository/reparse_all_commit_messages.php
index 0b02192496..293aa6ff37 100755
--- a/scripts/repository/reparse_all_commit_messages.php
+++ b/scripts/repository/reparse_all_commit_messages.php
@@ -1,24 +1,8 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
echo "This script is obsolete. Instead, use:\n\n".
" $ reparse.php --all <repository_name> --message\n\n".
"See that script for more options.\n";
exit(1);
diff --git a/scripts/repository/test_connection.php b/scripts/repository/test_connection.php
index 4ccabbaed8..20c8486d3a 100755
--- a/scripts/repository/test_connection.php
+++ b/scripts/repository/test_connection.php
@@ -1,111 +1,95 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
if (empty($argv[1])) {
echo "usage: test_connection.php <repository_callsign>\n";
exit(1);
}
echo phutil_console_wrap(
phutil_console_format(
'This script will test that you have configured valid credentials for '.
'access to a repository, so the Phabricator daemons can pull from it. '.
'You should run this as the **same user you will run the daemons as**, '.
'from the **same machine they will run from**. Doing this will help '.
'detect various problems with your configuration, such as SSH issues.'));
list($whoami) = execx('whoami');
$whoami = trim($whoami);
$ok = phutil_console_confirm("Do you want to continue as '{$whoami}'?");
if (!$ok) {
die(1);
}
$callsign = $argv[1];
echo "Loading '{$callsign}' repository...\n";
$repository = id(new PhabricatorRepository())->loadOneWhere(
'callsign = %s',
$argv[1]);
if (!$repository) {
throw new Exception("No such repository exists!");
}
$vcs = $repository->getVersionControlSystem();
PhutilServiceProfiler::installEchoListener();
echo phutil_console_format(
"\n".
"**NOTE:** If you are prompted for an SSH password in the next step, the\n".
"daemon won't work because it doesn't have the password and can't respond\n".
"to an interactive prompt. Instead of typing the password, it will hang\n".
"forever when prompted. There are several ways to resolve this:\n\n".
" - Run the daemon inside an ssh-agent session where you have unlocked\n".
" the key (most secure, but most complicated).\n".
" - Generate a new, passwordless certificate for the daemon to use\n".
" (usually quite easy).\n".
" - Remove the passphrase from the key with `ssh-keygen -p`\n".
" (easy, but questionable).");
phutil_console_confirm('Did you read all that?', $default_no = false);
echo "Trying to connect to the remote...\n";
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$err = $repository->passthruRemoteCommand(
'--limit 1 log %s',
$repository->getRemoteURI());
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
// Do an ls-remote on a nonexistent ref, which we expect to just return
// nothing.
$err = $repository->passthruRemoteCommand(
'ls-remote %s %s',
$repository->getRemoteURI(),
'just-testing');
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
// TODO: 'hg id' doesn't support --insecure so we can't tell it not to
// spew. If 'hg id' eventually supports --insecure, consider using it.
echo "(It is safe to ignore any 'certificate with fingerprint ... not ".
"verified' warnings, although you may want to configure Mercurial ".
"to recognize the server's fingerprint/certificate.)\n";
$err = $repository->passthruRemoteCommand(
'id --rev tip %s',
$repository->getRemoteURI());
break;
default:
throw new Exception("Unsupported repository type.");
}
if ($err) {
echo phutil_console_format(
"<bg:red>** FAIL **</bg> Connection failed. The credentials for this ".
"repository appear to be incorrectly configured.\n");
exit(1);
} else {
echo phutil_console_format(
"<bg:green>** OKAY **</bg> Connection successful. The credentials for ".
"this repository appear to be correctly configured.\n");
}
diff --git a/scripts/repository/undo_commits.php b/scripts/repository/undo_commits.php
index a68f9dab98..1816205dc9 100755
--- a/scripts/repository/undo_commits.php
+++ b/scripts/repository/undo_commits.php
@@ -1,194 +1,178 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
$args->setTagline('reopen reviews accidentally closed by a bad push');
$args->setSynopsis(<<<EOSYNOPSIS
**undo_commits.php** --repository __callsign__ < __commits__
Reopen code reviews accidentally closed by a bad push. If someone
pushes a bunch of commits to a tracked branch that they shouldn't
have, you can pipe in all the commit hashes to this script to
"undo" the damage in Differential after you revert the commits.
To use this script:
1. Identify the commits you want to undo the effects of.
2. Put all their identifiers (commit hashes in git/hg, revision
numbers in svn) into a file, one per line.
3. Pipe that file into this script with relevant arguments.
4. Revisions marked "closed" by those commits will be
restored to their previous state.
EOSYNOPSIS
);
$args->parseStandardArguments();
$args->parse(
array(
array(
'name' => 'repository',
'param' => 'callsign',
'help' => 'Callsign for the repository these commits appear in.',
),
));
$callsign = $args->getArg('repository');
if (!$callsign) {
$args->printHelpAndExit();
}
$repository = id(new PhabricatorRepository())->loadOneWhere(
'callsign = %s',
$callsign);
if (!$repository) {
throw new Exception("No repository with callsign '{$callsign}'!");
}
echo "Reading commit identifiers from stdin...\n";
$identifiers = @file_get_contents('php://stdin');
$identifiers = trim($identifiers);
$identifiers = explode("\n", $identifiers);
echo "Read ".count($identifiers)." commit identifiers.\n";
if (!$identifiers) {
throw new Exception("You must provide commmit identifiers on stdin!");
}
echo "Looking up commits...\n";
$commits = id(new PhabricatorRepositoryCommit())->loadAllWhere(
'repositoryID = %d AND commitIdentifier IN (%Ls)',
$repository->getID(),
$identifiers);
echo "Found ".count($commits)." matching commits.\n";
if (!$commits) {
throw new Exception("None of the commits could be found!");
}
$commit_phids = mpull($commits, 'getPHID', 'getPHID');
echo "Looking up revisions marked 'closed' by these commits...\n";
$revision_ids = queryfx_all(
id(new DifferentialRevision())->establishConnection('r'),
'SELECT DISTINCT revisionID from %T WHERE commitPHID IN (%Ls)',
DifferentialRevision::TABLE_COMMIT,
$commit_phids);
$revision_ids = ipull($revision_ids, 'revisionID');
echo "Found ".count($revision_ids)." associated revisions.\n";
if (!$revision_ids) {
echo "Done -- nothing to do.\n";
return;
}
$status_closed = ArcanistDifferentialRevisionStatus::CLOSED;
$revisions = array();
$map = array();
if ($revision_ids) {
foreach ($revision_ids as $revision_id) {
echo "Assessing revision D{$revision_id}...\n";
$revision = id(new DifferentialRevision())->load($revision_id);
if ($revision->getStatus() != $status_closed) {
echo "Revision is not 'closed', skipping.\n";
}
$assoc_commits = queryfx_all(
$revision->establishConnection('r'),
'SELECT commitPHID FROM %T WHERE revisionID = %d',
DifferentialRevision::TABLE_COMMIT,
$revision_id);
$assoc_commits = ipull($assoc_commits, 'commitPHID', 'commitPHID');
if (array_diff_key($assoc_commits, $commit_phids)) {
echo "Revision is associated with other commits, skipping.\n";
}
$comments = id(new DifferentialComment())->loadAllWhere(
'revisionID = %d',
$revision_id);
$new_status = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW;
foreach ($comments as $comment) {
switch ($comment->getAction()) {
case DifferentialAction::ACTION_ACCEPT:
$new_status = ArcanistDifferentialRevisionStatus::ACCEPTED;
break;
case DifferentialAction::ACTION_REJECT:
case DifferentialAction::ACTION_RETHINK:
$new_status = ArcanistDifferentialRevisionStatus::NEEDS_REVISION;
break;
case DifferentialAction::ACTION_ABANDON:
$new_status = ArcanistDifferentialRevisionStatus::ABANDONED;
break;
case DifferentialAction::ACTION_RECLAIM:
case DifferentialAction::ACTION_UPDATE:
$new_status = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW;
break;
}
}
$revisions[$revision_id] = $revision;
$map[$revision_id] = $new_status;
}
}
if (!$revisions) {
echo "Done -- nothing to do.\n";
}
echo "Found ".count($revisions)." revisions to update:\n\n";
foreach ($revisions as $id => $revision) {
$old_status = ArcanistDifferentialRevisionStatus::getNameForRevisionStatus(
$revision->getStatus());
$new_status = ArcanistDifferentialRevisionStatus::getNameForRevisionStatus(
$map[$id]);
echo " - D{$id}: ".$revision->getTitle()."\n";
echo " Will update: {$old_status} -> {$new_status}\n\n";
}
$ok = phutil_console_confirm('Apply these changes?');
if (!$ok) {
echo "Aborted.\n";
exit(1);
}
echo "Saving changes...\n";
foreach ($revisions as $id => $revision) {
queryfx(
$revision->establishConnection('r'),
'UPDATE %T SET status = %d WHERE id = %d',
$revision->getTableName(),
$map[$id],
$id);
}
echo "Done.\n";
diff --git a/scripts/search/index_one_commit.php b/scripts/search/index_one_commit.php
index d75f8c3184..3429f675b3 100755
--- a/scripts/search/index_one_commit.php
+++ b/scripts/search/index_one_commit.php
@@ -1,52 +1,36 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
if (empty($argv[1])) {
echo "usage: index_one_commit.php <commit_name>\n";
die(1);
}
$commit = isset($argv[1]) ? $argv[1] : null;
if (!$commit) {
throw new Exception("Provide a commit to index!");
}
$matches = null;
if (!preg_match('/r([A-Z]+)([a-z0-9]+)/', $commit, $matches)) {
throw new Exception("Can't parse commit identifier!");
}
$repo = id(new PhabricatorRepository())->loadOneWhere(
'callsign = %s',
$matches[1]);
if (!$repo) {
throw new Exception("Unknown repository!");
}
$commit = id(new PhabricatorRepositoryCommit())->loadOneWhere(
'repositoryID = %d AND commitIdentifier = %s',
$repo->getID(),
$matches[2]);
if (!$commit) {
throw new Exception('Unknown commit.');
}
PhabricatorSearchCommitIndexer::indexCommit($commit);
echo "Done.\n";
diff --git a/scripts/search/reindex_all_users.php b/scripts/search/reindex_all_users.php
index b829d5843a..330102780f 100755
--- a/scripts/search/reindex_all_users.php
+++ b/scripts/search/reindex_all_users.php
@@ -1,31 +1,15 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$users = id(new PhabricatorUser())->loadAll();
echo "Indexing ".count($users)." users";
foreach ($users as $user) {
PhabricatorSearchUserIndexer::indexUser($user);
echo '.';
}
echo "\n";
echo "Done.\n";
diff --git a/scripts/search/reindex_everything.php b/scripts/search/reindex_everything.php
index 8081c58338..4639e28714 100755
--- a/scripts/search/reindex_everything.php
+++ b/scripts/search/reindex_everything.php
@@ -1,50 +1,34 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
// TODO: Get rid of this script eventually, once this stuff is better-formalized
// in Timeline consumers.
echo "Reindexing revisions...\n";
$revs = new LiskMigrationIterator(new DifferentialRevision());
foreach ($revs as $rev) {
PhabricatorSearchDifferentialIndexer::indexRevision($rev);
echo '.';
}
echo "\n";
echo "Reindexing commits...\n";
$commits = new LiskMigrationIterator(new PhabricatorRepositoryCommit());
foreach ($commits as $commit) {
PhabricatorSearchCommitIndexer::indexCommit($commit);
echo '.';
}
echo "\n";
echo "Reindexing tasks...\n";
$tasks = new LiskMigrationIterator(new ManiphestTask());
foreach ($tasks as $task) {
PhabricatorSearchManiphestIndexer::indexTask($task);
echo '.';
}
echo "\n";
include dirname(__FILE__).'/reindex_all_users.php';
diff --git a/scripts/search/reindex_maniphest.php b/scripts/search/reindex_maniphest.php
index 89235c6590..ed4670b690 100755
--- a/scripts/search/reindex_maniphest.php
+++ b/scripts/search/reindex_maniphest.php
@@ -1,32 +1,16 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
ini_set('memory_limit', -1);
$tasks = id(new ManiphestTask())->loadAll();
echo "Updating relationships for ".count($tasks)." tasks";
foreach ($tasks as $task) {
ManiphestTaskProject::updateTaskProjects($task);
ManiphestTaskSubscriber::updateTaskSubscribers($task);
echo '.';
}
echo "\nDone.\n";
diff --git a/scripts/setup/pcntl_available.php b/scripts/setup/pcntl_available.php
index 4fe250ceac..5664a6eb2c 100755
--- a/scripts/setup/pcntl_available.php
+++ b/scripts/setup/pcntl_available.php
@@ -1,24 +1,8 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
if (extension_loaded('pcntl')) {
echo "YES\n";
} else {
echo "NO\n";
}
diff --git a/scripts/sql/manage_storage.php b/scripts/sql/manage_storage.php
index 8ed3eb4e23..040046e75f 100755
--- a/scripts/sql/manage_storage.php
+++ b/scripts/sql/manage_storage.php
@@ -1,131 +1,115 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
$args->setTagline('manage Phabricator storage and schemata');
$args->setSynopsis(<<<EOHELP
**storage** __workflow__ [__options__]
Manage Phabricator database storage and schema versioning.
**storage** upgrade
Initialize or upgrade Phabricator storage.
**storage** upgrade --user __root__ --password __hunter2__
Use administrative credentials for schema changes.
EOHELP
);
$args->parseStandardArguments();
$conf = PhabricatorEnv::newObjectFromConfig(
'mysql.configuration-provider',
array($dao = null, 'w'));
$default_user = $conf->getUser();
$default_host = $conf->getHost();
$default_namespace = PhabricatorLiskDAO::getDefaultStorageNamespace();
try {
$args->parsePartial(
array(
array(
'name' => 'force',
'short' => 'f',
'help' => 'Do not prompt before performing dangerous operations.',
),
array(
'name' => 'user',
'short' => 'u',
'param' => 'username',
'default' => $default_user,
'help' => "Connect with __username__ instead of the configured ".
"default ('{$default_user}').",
),
array(
'name' => 'password',
'short' => 'p',
'param' => 'password',
'help' => 'Use __password__ instead of the configured default.',
),
array(
'name' => 'namespace',
'param' => 'name',
'default' => $default_namespace,
'help' => "Use namespace __namespace__ instead of the configured ".
"default ('{$default_namespace}'). This is an advanced ".
"feature used by unit tests; you should not normally ".
"use this flag.",
),
array(
'name' => 'dryrun',
'help' => 'Do not actually change anything, just show what would be '.
'changed.',
),
));
} catch (PhutilArgumentUsageException $ex) {
$args->printUsageException($ex);
exit(77);
}
if ($args->getArg('password') === null) {
// This is already a PhutilOpaqueEnvelope.
$password = $conf->getPassword();
} else {
// Put this in a PhutilOpaqueEnvelope.
$password = new PhutilOpaqueEnvelope($args->getArg('password'));
}
$api = new PhabricatorStorageManagementAPI();
$api->setUser($args->getArg('user'));
$api->setHost($default_host);
$api->setPassword($password);
$api->setNamespace($args->getArg('namespace'));
try {
queryfx(
$api->getConn('meta_data', $select_database = false),
'SELECT 1');
} catch (AphrontQueryException $ex) {
echo phutil_console_format(
"**%s**: %s\n",
'Unable To Connect',
$ex->getMessage());
exit(1);
}
$workflows = array(
new PhabricatorStorageManagementDatabasesWorkflow(),
new PhabricatorStorageManagementDestroyWorkflow(),
new PhabricatorStorageManagementDumpWorkflow(),
new PhabricatorStorageManagementStatusWorkflow(),
new PhabricatorStorageManagementUpgradeWorkflow(),
);
$patches = PhabricatorSQLPatchList::buildAllPatches();
foreach ($workflows as $workflow) {
$workflow->setAPI($api);
$workflow->setPatches($patches);
}
$workflows[] = new PhutilHelpArgumentWorkflow();
$args->parseWorkflows($workflows);
diff --git a/scripts/sql/probe.php b/scripts/sql/probe.php
index ac953e6dd1..4bfb78fb2d 100755
--- a/scripts/sql/probe.php
+++ b/scripts/sql/probe.php
@@ -1,69 +1,53 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$data = array();
$conn_r = id(new PhabricatorUser())->establishConnection('r');
$databases = queryfx_all($conn_r, 'SHOW DATABASES');
foreach ($databases as $database) {
$name = head($database);
queryfx($conn_r, 'USE %C', $name);
$tables = queryfx_all(
$conn_r,
'SHOW TABLE STATUS');
$tables = ipull($tables, null, 'Name');
$data[$name] = $tables;
}
$totals = array_fill_keys(array_keys($data), 0);
$overall = 0;
foreach ($data as $db => $tables) {
foreach ($tables as $table => $info) {
$table_size = $info['Data_length'] + $info['Index_length'];
$data[$db][$table]['_totalSize'] = $table_size;
$totals[$db] += $table_size;
$overall += $table_size;
}
}
echo "APPROXIMATE TABLE SIZES\n";
asort($totals);
foreach ($totals as $db => $size) {
printf("%-32.32s %18s\n", $db, fmt($totals[$db], $overall));
$data[$db] = isort($data[$db], '_totalSize');
foreach ($data[$db] as $table => $info) {
printf(" %-28.28s %18s\n", $table, fmt($info['_totalSize'], $overall));
}
}
printf("%-32.32s %18s\n", 'TOTAL', fmt($overall, $overall));
function fmt($n, $o) {
return sprintf(
'%8.8s MB %5.5s%%',
number_format($n / (1024 * 1024), 1),
sprintf('%3.1f', 100 * ($n / $o)));
}
diff --git a/scripts/sql/upgrade_schema.php b/scripts/sql/upgrade_schema.php
index 16383493f9..dc2d6b562b 100755
--- a/scripts/sql/upgrade_schema.php
+++ b/scripts/sql/upgrade_schema.php
@@ -1,26 +1,10 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
echo "This script has been replaced by 'phabricator/bin/storage'.\n\n".
"Run:\n\n".
" phabricator/bin $ ./storage help\n\n".
"...for help, or:\n\n".
" phabricator/bin $ ./storage upgrade\n\n".
"...to upgrade storage.\n\n";
exit(1);
diff --git a/scripts/symbols/clear_project_symbols.php b/scripts/symbols/clear_project_symbols.php
index cd1df72f2e..c27576dc29 100755
--- a/scripts/symbols/clear_project_symbols.php
+++ b/scripts/symbols/clear_project_symbols.php
@@ -1,48 +1,32 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$project = id(new PhabricatorRepositoryArcanistProject())->loadOneWhere(
'name = %s', $argv[1]);
if (!$project) {
throw new Exception('No such arcanist project.');
}
$input = file_get_contents('php://stdin');
$normalized = array();
foreach (explode("\n", trim($input)) as $path) {
// emulate the behavior of the symbol generation scripts
$normalized[] = '/'.ltrim($path, './');
}
$paths = PhabricatorRepositoryCommitChangeParserWorker::lookupOrCreatePaths(
$normalized);
$symbol = new PhabricatorRepositorySymbol();
$conn_w = $symbol->establishConnection('w');
foreach (array_chunk(array_values($paths), 128) as $chunk) {
queryfx(
$conn_w,
'DELETE FROM %T WHERE arcanistProjectID = %d AND pathID IN (%Ld)',
$symbol->getTableName(),
$project->getID(),
$chunk);
}
diff --git a/scripts/symbols/generate_ctags_symbols.php b/scripts/symbols/generate_ctags_symbols.php
index bce4f1bf98..15bc14b481 100755
--- a/scripts/symbols/generate_ctags_symbols.php
+++ b/scripts/symbols/generate_ctags_symbols.php
@@ -1,140 +1,124 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
if (ctags_check_executable() == false) {
echo phutil_console_format(
"Could not find Exuberant ctags. Make sure it is installed and\n".
"available in executable path.\n\n".
"Exuberant ctags project page: http://ctags.sourceforge.net/\n");
exit(1);
}
if ($argc !== 1 || posix_isatty(STDIN)) {
echo phutil_console_format(
"usage: find . -type f -name '*.py' | ./generate_ctags_symbols.php\n");
exit(1);
}
$input = file_get_contents('php://stdin');
$input = trim($input);
$input = explode("\n", $input);
$data = array();
$futures = array();
foreach ($input as $file) {
$file = Filesystem::readablePath($file);
$futures[$file] = ctags_get_parser_future($file);
}
foreach (Futures($futures)->limit(8) as $file => $future) {
$tags = $future->resolve();
$tags = explode("\n", $tags[1]);
foreach ($tags as $tag) {
$parts = explode(";", $tag);
// skip lines that we can not parse
if (count($parts) < 2) {
continue;
}
// split ctags information
$tag_info = explode("\t", $parts[0]);
// split exuberant ctags "extension fields" (additional information)
$parts[1] = trim($parts[1], "\t \"");
$extension_fields = explode("\t", $parts[1]);
// skip lines that we can not parse
if (count($tag_info) < 3 || count($extension_fields) < 2) {
continue;
}
// default $context to empty
$extension_fields[] = '';
list($token, $file_path, $line_num) = $tag_info;
list($type, $language, $context) = $extension_fields;
// skip lines with tokens containing a space
if (strpos($token, ' ') !== false) {
continue;
}
// strip "language:"
$language = substr($language, 9);
// To keep consistent with "Separate with commas, for example: php, py"
// in Arcanist Project edit form.
$language = str_ireplace("python", "py", $language);
// also, "normalize" c++ and c#
$language = str_ireplace("c++", "cpp", $language);
$language = str_ireplace("c#", "csharp", $language);
// Ruby has "singleton method", for example
$type = substr(str_replace(' ', '_', $type), 0, 12);
// class:foo, struct:foo, union:foo, enum:foo, ...
$context = last(explode(':', $context, 2));
$ignore = array(
'variable' => true,
);
if (empty($ignore[$type])) {
print_symbol($file_path, $line_num, $type, $token, $context, $language);
}
}
}
function ctags_get_parser_future($file_path) {
$future = new ExecFuture('ctags -n --fields=Kls -o - %s',
$file_path);
return $future;
}
function ctags_check_executable() {
$future = new ExecFuture('ctags --version');
$result = $future->resolve();
if (empty($result[1])) {
return false;
}
return true;
}
function print_symbol($file, $line_num, $type, $token, $context, $language) {
// get rid of relative path
$file = explode('/', $file);
if ($file[0] == '.' || $file[0] == "..") {
array_shift($file);
}
$file = '/' . implode('/', $file);
$parts = array(
$context,
$token,
$type,
strtolower($language),
$line_num,
$file,
);
echo implode(' ', $parts)."\n";
}
diff --git a/scripts/symbols/generate_php_symbols.php b/scripts/symbols/generate_php_symbols.php
index e4273ed9b4..65eecbb199 100755
--- a/scripts/symbols/generate_php_symbols.php
+++ b/scripts/symbols/generate_php_symbols.php
@@ -1,125 +1,109 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
if ($argc !== 1 || posix_isatty(STDIN)) {
echo phutil_console_format(
"usage: find . -type f -name '*.php' | ./generate_php_symbols.php\n");
exit(1);
}
$input = file_get_contents('php://stdin');
$input = trim($input);
$input = explode("\n", $input);
$data = array();
$futures = array();
foreach ($input as $file) {
$file = Filesystem::readablePath($file);
$data[$file] = Filesystem::readFile($file);
$futures[$file] = xhpast_get_parser_future($data[$file]);
}
foreach (Futures($futures)->limit(8) as $file => $future) {
$tree = XHPASTTree::newFromDataAndResolvedExecFuture(
$data[$file],
$future->resolve());
$root = $tree->getRootNode();
$scopes = array();
$functions = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');
foreach ($functions as $function) {
$name = $function->getChildByIndex(2);
print_symbol($file, 'function', $name);
}
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
foreach ($classes as $class) {
$class_name = $class->getChildByIndex(1);
print_symbol($file, 'class', $class_name);
$scopes[] = array($class, $class_name);
}
$interfaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION');
foreach ($interfaces as $interface) {
$interface_name = $interface->getChildByIndex(1);
// We don't differentiate classes and interfaces in highlighters.
print_symbol($file, 'class', $interface_name);
$scopes[] = array($interface, $interface_name);
}
$constants = $root->selectDescendantsOfType('n_CONSTANT_DECLARATION_LIST');
foreach ($constants as $constant_list) {
foreach ($constant_list->getChildren() as $constant) {
$constant_name = $constant->getChildByIndex(0);
print_symbol($file, 'constant', $constant_name);
}
}
foreach ($scopes as $scope) {
// this prints duplicate symbols in the case of nested classes
// luckily, PHP doesn't allow those
list($class, $class_name) = $scope;
$consts = $class->selectDescendantsOfType(
'n_CLASS_CONSTANT_DECLARATION_LIST');
foreach ($consts as $const_list) {
foreach ($const_list->getChildren() as $const) {
$const_name = $const->getChildByIndex(0);
print_symbol($file, 'class_const', $const_name, $class_name);
}
}
$members = $class->selectDescendantsOfType(
'n_CLASS_MEMBER_DECLARATION_LIST');
foreach ($members as $member_list) {
foreach ($member_list->getChildren() as $member) {
if ($member->getTypeName() == 'n_CLASS_MEMBER_MODIFIER_LIST') {
continue;
}
$member_name = $member->getChildByIndex(0);
print_symbol($file, 'member', $member_name, $class_name);
}
}
$methods = $class->selectDescendantsOfType('n_METHOD_DECLARATION');
foreach ($methods as $method) {
$method_name = $method->getChildByIndex(2);
print_symbol($file, 'method', $method_name, $class_name);
}
}
}
function print_symbol($file, $type, $token, $context=null) {
$parts = array(
$context ? $context->getConcreteString() : '',
// variable tokens are `$name`, not just `name`, so strip the $ off of
// class field names
ltrim($token->getConcreteString(), '$'),
$type,
'php',
$token->getLineNumber(),
'/'.ltrim($file, './'),
);
echo implode(' ', $parts)."\n";
}
diff --git a/scripts/symbols/import_project_symbols.php b/scripts/symbols/import_project_symbols.php
index 0892e27d61..6271dc7f75 100755
--- a/scripts/symbols/import_project_symbols.php
+++ b/scripts/symbols/import_project_symbols.php
@@ -1,192 +1,176 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
$args->setSynopsis(<<<EOSYNOPSIS
**import_project_symbols.php** [__options__] __project_name__ < symbols
Import project symbols (symbols are read from stdin).
EOSYNOPSIS
);
$args->parseStandardArguments();
$args->parse(
array(
array(
'name' => 'no-purge',
'help' => 'Do not clear all symbols for this project before '.
'uploading new symbols. Useful for incremental updating.',
),
array(
'name' => 'ignore-errors',
'help' => 'If a line can\'t be parsed, ignore that line and '.
'continue instead of exiting.',
),
array(
'name' => 'more',
'wildcard' => true,
),
));
$more = $args->getArg('more');
if (count($more) !== 1) {
$args->printHelpAndExit();
}
$project_name = head($more);
$project = id(new PhabricatorRepositoryArcanistProject())->loadOneWhere(
'name = %s',
$project_name);
if (!$project) {
// TODO: Provide a less silly way to do this explicitly, or just do it right
// here.
echo "Project '{$project_name}' is unknown. Upload a diff to implicitly ".
"create it.\n";
exit(1);
}
echo "Parsing input from stdin...\n";
$input = file_get_contents('php://stdin');
$input = trim($input);
$input = explode("\n", $input);
$symbols = array();
foreach ($input as $key => $line) {
try {
$line_no = $key + 1;
$matches = null;
$ok = preg_match(
'/^((?P<context>[^ ]+)? )?(?P<name>[^ ]+) (?P<type>[^ ]+) '.
'(?P<lang>[^ ]+) (?P<line>\d+) (?P<path>.*)$/',
$line,
$matches);
if (!$ok) {
throw new Exception(
"Line #{$line_no} of input is invalid. Expected five or six ".
"space-delimited fields: maybe symbol context, symbol name, symbol ".
"type, symbol language, line number, path. ".
"For example:\n\n".
"idx function php 13 /path/to/some/file.php\n\n".
"Actual line was:\n\n".
"{$line}");
}
if (empty($matches['context'])) {
$matches['context'] = '';
}
$context = $matches['context'];
$name = $matches['name'];
$type = $matches['type'];
$lang = $matches['lang'];
$line_number = $matches['line'];
$path = $matches['path'];
if (strlen($context) > 128) {
throw new Exception(
"Symbol context '{$context}' defined on line #{$line_no} is too long, ".
"maximum symbol context length is 128 characters.");
}
if (strlen($name) > 128) {
throw new Exception(
"Symbol name '{$name}' defined on line #{$line_no} is too long, ".
"maximum symbol name length is 128 characters.");
}
if (strlen($type) > 12) {
throw new Exception(
"Symbol type '{$type}' defined on line #{$line_no} is too long, ".
"maximum symbol type length is 12 characters.");
}
if (strlen($lang) > 32) {
throw new Exception(
"Symbol language '{$lang}' defined on line #{$line_no} is too long, ".
"maximum symbol language length is 32 characters.");
}
if (!strlen($path) || $path[0] != 0) {
throw new Exception(
"Path '{$path}' defined on line #{$line_no} is invalid. Paths should ".
"begin with '/' and specify a path from the root of the project, like ".
"'/src/utils/utils.php'.");
}
$symbols[] = array(
'ctxt' => $context,
'name' => $name,
'type' => $type,
'lang' => $lang,
'line' => $line_number,
'path' => $path,
);
} catch (Exception $e) {
if ($args->getArg('ignore-errors')) {
continue;
} else {
throw $e;
}
}
}
echo "Looking up path IDs...\n";
$path_map = PhabricatorRepositoryCommitChangeParserWorker::lookupOrCreatePaths(
ipull($symbols, 'path'));
$symbol = new PhabricatorRepositorySymbol();
$conn_w = $symbol->establishConnection('w');
echo "Preparing queries...\n";
$sql = array();
foreach ($symbols as $dict) {
$sql[] = qsprintf(
$conn_w,
'(%d, %s, %s, %s, %s, %d, %d)',
$project->getID(),
$dict['ctxt'],
$dict['name'],
$dict['type'],
$dict['lang'],
$dict['line'],
$path_map[$dict['path']]);
}
if (!$args->getArg('no-purge')) {
echo "Purging old symbols...\n";
queryfx(
$conn_w,
'DELETE FROM %T WHERE arcanistProjectID = %d',
$symbol->getTableName(),
$project->getID());
}
echo "Loading ".number_format(count($sql))." symbols...\n";
foreach (array_chunk($sql, 128) as $chunk) {
queryfx(
$conn_w,
'INSERT INTO %T
(arcanistProjectID, symbolContext, symbolName, symbolType,
symbolLanguage, lineNumber, pathID) VALUES %Q',
$symbol->getTableName(),
implode(', ', $chunk));
}
echo "Done.\n";
diff --git a/scripts/user/account_admin.php b/scripts/user/account_admin.php
index 2b7053777a..f27b4d86a2 100755
--- a/scripts/user/account_admin.php
+++ b/scripts/user/account_admin.php
@@ -1,216 +1,200 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
echo "Enter a username to create a new account or edit an existing account.";
$username = phutil_console_prompt("Enter a username:");
if (!strlen($username)) {
echo "Cancelled.\n";
exit(1);
}
if (!PhabricatorUser::validateUsername($username)) {
$valid = PhabricatorUser::describeValidUsername();
echo "The username '{$username}' is invalid. {$valid}\n";
exit(1);
}
$user = id(new PhabricatorUser())->loadOneWhere(
'username = %s',
$username);
if (!$user) {
$original = new PhabricatorUser();
echo "There is no existing user account '{$username}'.\n";
$ok = phutil_console_confirm(
"Do you want to create a new '{$username}' account?",
$default_no = false);
if (!$ok) {
echo "Cancelled.\n";
exit(1);
}
$user = new PhabricatorUser();
$user->setUsername($username);
$is_new = true;
} else {
$original = clone $user;
echo "There is an existing user account '{$username}'.\n";
$ok = phutil_console_confirm(
"Do you want to edit the existing '{$username}' account?",
$default_no = false);
if (!$ok) {
echo "Cancelled.\n";
exit(1);
}
$is_new = false;
}
$user_realname = $user->getRealName();
if (strlen($user_realname)) {
$realname_prompt = ' ['.$user_realname.']';
} else {
$realname_prompt = '';
}
$realname = nonempty(
phutil_console_prompt("Enter user real name{$realname_prompt}:"),
$user_realname);
$user->setRealName($realname);
// When creating a new user we prompt for an email address; when editing an
// existing user we just skip this because it would be quite involved to provide
// a reasonable CLI interface for editing multiple addresses and managing email
// verification and primary addresses.
$create_email = null;
if ($is_new) {
do {
$email = phutil_console_prompt("Enter user email address:");
$duplicate = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$email);
if ($duplicate) {
echo "ERROR: There is already a user with that email address. ".
"Each user must have a unique email address.\n";
} else {
break;
}
} while (true);
$create_email = $email;
}
$changed_pass = false;
// This disables local echo, so the user's password is not shown as they type
// it.
phutil_passthru('stty -echo');
$password = phutil_console_prompt(
"Enter a password for this user [blank to leave unchanged]:");
phutil_passthru('stty echo');
if (strlen($password)) {
$changed_pass = $password;
}
$is_system_agent = $user->getIsSystemAgent();
$set_system_agent = phutil_console_confirm(
'Should this user be a system agent?',
$default_no = !$is_system_agent);
$verify_email = null;
$set_verified = false;
// Allow administrators to verify primary email addresses at this time in edit
// scenarios. (Create will work just fine from here as we auto-verify email
// on create.)
if (!$is_new) {
$verify_email = $user->loadPrimaryEmail();
if (!$verify_email->getIsVerified()) {
$set_verified = phutil_console_confirm(
'Should the primary email address be verified?',
$default_no = true
);
} else {
// already verified so let's not make a fuss
$verify_email = null;
}
}
$is_admin = $user->getIsAdmin();
$set_admin = phutil_console_confirm(
'Should this user be an administrator?',
$default_no = !$is_admin);
echo "\n\nACCOUNT SUMMARY\n\n";
$tpl = "%12s %-30s %-30s\n";
printf($tpl, null, 'OLD VALUE', 'NEW VALUE');
printf($tpl, 'Username', $original->getUsername(), $user->getUsername());
printf($tpl, 'Real Name', $original->getRealName(), $user->getRealName());
if ($is_new) {
printf($tpl, 'Email', '', $create_email);
}
printf($tpl, 'Password', null,
($changed_pass !== false)
? 'Updated'
: 'Unchanged');
printf(
$tpl,
'System Agent',
$original->getIsSystemAgent() ? 'Y' : 'N',
$set_system_agent ? 'Y' : 'N');
if ($verify_email) {
printf(
$tpl,
'Verify Email',
$verify_email->getIsVerified() ? 'Y' : 'N',
$set_verified ? 'Y' : 'N');
}
printf(
$tpl,
'Admin',
$original->getIsAdmin() ? 'Y' : 'N',
$set_admin ? 'Y' : 'N');
echo "\n";
if (!phutil_console_confirm("Save these changes?", $default_no = false)) {
echo "Cancelled.\n";
exit(1);
}
$user->openTransaction();
$editor = new PhabricatorUserEditor();
// TODO: This is wrong, but we have a chicken-and-egg problem when you use
// this script to create the first user.
$editor->setActor($user);
if ($is_new) {
$email = id(new PhabricatorUserEmail())
->setAddress($create_email)
->setIsVerified(1);
$editor->createNewUser($user, $email);
} else {
if ($verify_email) {
$verify_email->setIsVerified($set_verified ? 1 : 0);
}
$editor->updateUser($user, $verify_email);
}
$editor->makeAdminUser($user, $set_admin);
$editor->makeSystemAgentUser($user, $set_system_agent);
if ($changed_pass !== false) {
$envelope = new PhutilOpaqueEnvelope($changed_pass);
$editor->changePassword($user, $envelope);
}
$user->saveTransaction();
echo "Saved changes.\n";
diff --git a/scripts/user/add_user.php b/scripts/user/add_user.php
index a8b22f6fe8..edb6a1a26b 100755
--- a/scripts/user/add_user.php
+++ b/scripts/user/add_user.php
@@ -1,72 +1,56 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
if ($argc !== 5) {
echo "usage: add_user.php <username> <email> <realname> <admin_user>\n";
exit(1);
}
$username = $argv[1];
$email = $argv[2];
$realname = $argv[3];
$admin = $argv[4];
$admin = id(new PhabricatorUser())->loadOneWhere(
'username = %s',
$argv[4]);
if (!$admin) {
throw new Exception(
"Admin user must be the username of a valid Phabricator account, used ".
"to send the new user a welcome email.");
}
$existing_user = id(new PhabricatorUser())->loadOneWhere(
'username = %s',
$username);
if ($existing_user) {
throw new Exception(
"There is already a user with the username '{$username}'!");
}
$existing_email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$email);
if ($existing_email) {
throw new Exception(
"There is already a user with the email '{$email}'!");
}
$user = new PhabricatorUser();
$user->setUsername($username);
$user->setRealname($realname);
$email_object = id(new PhabricatorUserEmail())
->setAddress($email)
->setIsVerified(1);
id(new PhabricatorUserEditor())
->setActor($admin)
->createNewUser($user, $email_object);
$user->sendWelcomeEmail($admin);
echo "Created user '{$username}' (realname='{$realname}', email='{$email}').\n";
diff --git a/scripts/util/add_macro.php b/scripts/util/add_macro.php
index cd35588bd5..e2b9ea5608 100755
--- a/scripts/util/add_macro.php
+++ b/scripts/util/add_macro.php
@@ -1,80 +1,64 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
$args->setTagline('load files as image macros');
$args->setSynopsis(<<<EOHELP
**add_macro.php** __image__ [--as __name__]
Add an image macro. This can be useful for importing a large number
of macros.
EOHELP
);
$args->parseStandardArguments();
$args->parse(
array(
array(
'name' => 'as',
'param' => 'name',
'help' => 'Use a specific name instead of the first part of the image '.
'name.',
),
array(
'name' => 'more',
'wildcard' => true,
),
));
$more = $args->getArg('more');
if (count($more) !== 1) {
$args->printHelpAndExit();
}
$path = head($more);
$data = Filesystem::readFile($path);
$name = $args->getArg('as');
if ($name === null) {
$name = head(explode('.', basename($path)));
}
$existing = id(new PhabricatorFileImageMacro())->loadOneWhere(
'name = %s',
$name);
if ($existing) {
throw new Exception("A macro already exists with the name '{$name}'!");
}
$file = PhabricatorFile::newFromFileData(
$data,
array(
'name' => basename($path),
));
$macro = id(new PhabricatorFileImageMacro())
->setFilePHID($file->getPHID())
->setName($name)
->save();
$id = $file->getID();
echo "Added macro '{$name}' (F{$id}).\n";
diff --git a/scripts/util/emit_test_event.php b/scripts/util/emit_test_event.php
index 35108e0d74..500e8390e4 100755
--- a/scripts/util/emit_test_event.php
+++ b/scripts/util/emit_test_event.php
@@ -1,57 +1,41 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
$args->setTagline('emit a test event');
$args->setSynopsis(<<<EOHELP
**emit_test_event.php** [--listen listener] ...
Emit a test event after installing any specified __listener__s.
EOHELP
);
$args->parseStandardArguments();
$args->parse(
array(
array(
'name' => 'listen',
'param' => 'listener',
'repeat' => true,
),
));
$console = PhutilConsole::getConsole();
foreach ($args->getArg('listen') as $listener) {
$console->writeOut("Installing '%s'...\n", $listener);
newv($listener, array())->register();
}
$console->writeOut("Emitting event...\n");
PhutilEventEngine::dispatchEvent(
new PhabricatorEvent(
PhabricatorEventType::TYPE_TEST_DIDRUNTEST,
array(
'time' => time(),
)));
$console->writeOut("Done.\n");
exit(0);
diff --git a/scripts/util/purge_cache.php b/scripts/util/purge_cache.php
index 0a273a72a2..8effd9d2ae 100755
--- a/scripts/util/purge_cache.php
+++ b/scripts/util/purge_cache.php
@@ -1,143 +1,127 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
$purge_changesets = false;
$purge_differential = false;
$args = array_slice($argv, 1);
if (!$args) {
usage("Specify which caches you want to purge.");
}
$changesets = array();
$len = count($args);
for ($ii = 0; $ii < $len; $ii++) {
switch ($args[$ii]) {
case '--all':
$purge_changesets = true;
$purge_differential = true;
break;
case '--changesets':
$purge_changesets = true;
while (isset($args[$ii + 1]) && (substr($args[$ii + 1], 0, 2) !== '--')) {
$changeset = $args[++$ii];
if (!is_numeric($changeset)) {
return usage("Changeset argument '{$changeset}' ".
"is not a positive integer.");
}
$changesets[] = intval($changeset);
}
break;
case '--differential':
$purge_differential = true;
break;
case '--help':
return help();
default:
return usage("Unrecognized argument '{$args[$ii]}'.");
}
}
if ($purge_changesets) {
$table = new DifferentialChangeset();
if ($changesets) {
echo "Purging changeset cache for changesets ".
implode($changesets, ",")."\n";
queryfx(
$table->establishConnection('w'),
'DELETE FROM %T WHERE id IN (%Ld)',
DifferentialChangeset::TABLE_CACHE,
$changesets);
} else {
echo "Purging changeset cache...\n";
queryfx(
$table->establishConnection('w'),
'TRUNCATE TABLE %T',
DifferentialChangeset::TABLE_CACHE);
}
echo "Done.\n";
}
if ($purge_differential) {
echo "Purging Differential comment cache...\n";
$table = new DifferentialComment();
queryfx(
$table->establishConnection('w'),
'UPDATE %T SET cache = NULL',
$table->getTableName());
echo "Purging Differential inline comment cache...\n";
$table = new DifferentialInlineComment();
queryfx(
$table->establishConnection('w'),
'UPDATE %T SET cache = NULL',
$table->getTableName());
echo "Done.\n";
}
echo "Ok, caches purged.\n";
function usage($message) {
echo "Usage Error: {$message}";
echo "\n\n";
echo "Run 'purge_cache.php --help' for detailed help.\n";
exit(1);
}
function help() {
$help = <<<EOHELP
**SUMMARY**
**purge_cache.php**
[--differential]
[--changesets [changeset_id ...]]
**purge_cache.php** --all
**purge_cache.php** --help
Purge various long-lived caches. Normally, Phabricator caches some data for
a long time or indefinitely, but certain configuration changes might
invalidate these caches. You can use this script to manually purge them.
For instance, if you change display widths in Differential or configure
syntax highlighting, you may want to purge the changeset cache (with
"--changesets") so your changes are reflected in older diffs.
If you change Remarkup rules, you may want to purge the Differential
comment caches ("--differential") so older comments pick up the new rules.
__--all__
Purge all long-lived caches.
__--changesets [changeset_id ...]__
Purge Differential changeset render cache. If changeset_ids are present,
the script will delete the cache for those changesets; otherwise it will
delete the cache for all the changesets.
__--differential__
Purge Differential comment formatting cache.
__--help__: show this help
EOHELP;
echo phutil_console_format($help);
exit(1);
}
diff --git a/src/__phutil_library_init__.php b/src/__phutil_library_init__.php
index a01941f7e1..f150c1f94f 100644
--- a/src/__phutil_library_init__.php
+++ b/src/__phutil_library_init__.php
@@ -1,19 +1,3 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
phutil_register_library('phabricator', __FILE__);
diff --git a/src/aphront/AphrontController.php b/src/aphront/AphrontController.php
index 8fc6a48d1d..06aecedb5c 100644
--- a/src/aphront/AphrontController.php
+++ b/src/aphront/AphrontController.php
@@ -1,69 +1,53 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group aphront
*/
abstract class AphrontController {
private $request;
private $currentApplication;
public function willBeginExecution() {
return;
}
public function willProcessRequest(array $uri_data) {
return;
}
public function didProcessRequest($response) {
return $response;
}
abstract public function processRequest();
final public function __construct(AphrontRequest $request) {
$this->request = $request;
}
final public function getRequest() {
return $this->request;
}
final public function delegateToController(AphrontController $controller) {
return $controller->processRequest();
}
final public function setCurrentApplication(
PhabricatorApplication $current_application) {
$this->currentApplication = $current_application;
return $this;
}
final public function getCurrentApplication() {
return $this->currentApplication;
}
public function __set($name, $value) {
phlog('Wrote to undeclared property '.get_class($this).'::$'.$name.'.');
$this->$name = $value;
}
}
diff --git a/src/aphront/AphrontRequest.php b/src/aphront/AphrontRequest.php
index 642e2bbf0f..d83bbb27c6 100644
--- a/src/aphront/AphrontRequest.php
+++ b/src/aphront/AphrontRequest.php
@@ -1,348 +1,332 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
*
* @task data Accessing Request Data
*
* @group aphront
*/
final class AphrontRequest {
// NOTE: These magic request-type parameters are automatically included in
// certain requests (e.g., by phabricator_render_form(), JX.Request,
// JX.Workflow, and ConduitClient) and help us figure out what sort of
// response the client expects.
const TYPE_AJAX = '__ajax__';
const TYPE_FORM = '__form__';
const TYPE_CONDUIT = '__conduit__';
const TYPE_WORKFLOW = '__wflow__';
private $host;
private $path;
private $requestData;
private $user;
private $applicationConfiguration;
final public function __construct($host, $path) {
$this->host = $host;
$this->path = $path;
}
final public function setApplicationConfiguration(
$application_configuration) {
$this->applicationConfiguration = $application_configuration;
return $this;
}
final public function getApplicationConfiguration() {
return $this->applicationConfiguration;
}
final public function setPath($path) {
$this->path = $path;
return $this;
}
final public function getPath() {
return $this->path;
}
final public function getHost() {
// The "Host" header may include a port number, or may be a malicious
// header in the form "realdomain.com:ignored@evil.com". Invoke the full
// parser to extract the real domain correctly. See here for coverage of
// a similar issue in Django:
//
// https://www.djangoproject.com/weblog/2012/oct/17/security/
$uri = new PhutilURI('http://'.$this->host);
return $uri->getDomain();
}
/* -( Accessing Request Data )--------------------------------------------- */
/**
* @task data
*/
final public function setRequestData(array $request_data) {
$this->requestData = $request_data;
return $this;
}
/**
* @task data
*/
final public function getRequestData() {
return $this->requestData;
}
/**
* @task data
*/
final public function getInt($name, $default = null) {
if (isset($this->requestData[$name])) {
return (int)$this->requestData[$name];
} else {
return $default;
}
}
/**
* @task data
*/
final public function getBool($name, $default = null) {
if (isset($this->requestData[$name])) {
if ($this->requestData[$name] === 'true') {
return true;
} else if ($this->requestData[$name] === 'false') {
return false;
} else {
return (bool)$this->requestData[$name];
}
} else {
return $default;
}
}
/**
* @task data
*/
final public function getStr($name, $default = null) {
if (isset($this->requestData[$name])) {
$str = (string)$this->requestData[$name];
// Normalize newline craziness.
$str = str_replace(
array("\r\n", "\r"),
array("\n", "\n"),
$str);
return $str;
} else {
return $default;
}
}
/**
* @task data
*/
final public function getArr($name, $default = array()) {
if (isset($this->requestData[$name]) &&
is_array($this->requestData[$name])) {
return $this->requestData[$name];
} else {
return $default;
}
}
/**
* @task data
*/
final public function getStrList($name, $default = array()) {
if (!isset($this->requestData[$name])) {
return $default;
}
$list = $this->getStr($name);
$list = preg_split('/[\s,]+/', $list, $limit = -1, PREG_SPLIT_NO_EMPTY);
return $list;
}
/**
* @task data
*/
final public function getExists($name) {
return array_key_exists($name, $this->requestData);
}
final public function isHTTPPost() {
return ($_SERVER['REQUEST_METHOD'] == 'POST');
}
final public function isAjax() {
return $this->getExists(self::TYPE_AJAX);
}
final public function isJavelinWorkflow() {
return $this->getExists(self::TYPE_WORKFLOW);
}
final public function isConduit() {
return $this->getExists(self::TYPE_CONDUIT);
}
public static function getCSRFTokenName() {
return '__csrf__';
}
public static function getCSRFHeaderName() {
return 'X-Phabricator-Csrf';
}
final public function validateCSRF() {
$token_name = self::getCSRFTokenName();
$token = $this->getStr($token_name);
// No token in the request, check the HTTP header which is added for Ajax
// requests.
if (empty($token)) {
// PHP mangles HTTP headers by uppercasing them and replacing hyphens with
// underscores, then prepending 'HTTP_'.
$php_index = self::getCSRFHeaderName();
$php_index = strtoupper($php_index);
$php_index = str_replace('-', '_', $php_index);
$php_index = 'HTTP_'.$php_index;
$token = idx($_SERVER, $php_index);
}
$valid = $this->getUser()->validateCSRFToken($token);
if (!$valid) {
// Add some diagnostic details so we can figure out if some CSRF issues
// are JS problems or people accessing Ajax URIs directly with their
// browsers.
if ($token) {
$token_info = "with an invalid CSRF token";
} else {
$token_info = "without a CSRF token";
}
if ($this->isAjax()) {
$more_info = "(This was an Ajax request, {$token_info}.)";
} else {
$more_info = "(This was a web request, {$token_info}.)";
}
// This should only be able to happen if you load a form, pull your
// internet for 6 hours, and then reconnect and immediately submit,
// but give the user some indication of what happened since the workflow
// is incredibly confusing otherwise.
throw new AphrontCSRFException(
"The form you just submitted did not include a valid CSRF token. ".
"This token is a technical security measure which prevents a ".
"certain type of login hijacking attack. However, the token can ".
"become invalid if you leave a page open for more than six hours ".
"without a connection to the internet. To fix this problem: reload ".
"the page, and then resubmit it. All data inserted to the form will ".
"be lost in some browsers so copy them somewhere before reloading.\n\n".
$more_info);
}
return true;
}
final public function isFormPost() {
$post = $this->getExists(self::TYPE_FORM) &&
$this->isHTTPPost();
if (!$post) {
return false;
}
return $this->validateCSRF();
}
final public function getCookie($name, $default = null) {
return idx($_COOKIE, $name, $default);
}
final public function clearCookie($name) {
$this->setCookie($name, '', time() - (60 * 60 * 24 * 30));
}
final public function setCookie($name, $value, $expire = null) {
// Ensure cookies are only set on the configured domain.
$base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
$base_uri = new PhutilURI($base_uri);
$base_domain = $base_uri->getDomain();
$base_protocol = $base_uri->getProtocol();
$host = $this->getHost();
if ($base_domain != $host) {
throw new Exception(
"This install of Phabricator is configured as '{$base_domain}' but ".
"you are accessing it via '{$host}'. Access Phabricator via ".
"the primary configured domain.");
}
if ($expire === null) {
$expire = time() + (60 * 60 * 24 * 365 * 5);
}
$is_secure = ($base_protocol == 'https');
setcookie(
$name,
$value,
$expire,
$path = '/',
$base_domain,
$is_secure,
$http_only = true);
return $this;
}
final public function setUser($user) {
$this->user = $user;
return $this;
}
final public function getUser() {
return $this->user;
}
final public function getRequestURI() {
$get = $_GET;
unset($get['__path__']);
$path = phutil_escape_uri($this->getPath());
return id(new PhutilURI($path))->setQueryParams($get);
}
final public function isDialogFormPost() {
return $this->isFormPost() && $this->getStr('__dialog__');
}
final public function getRemoteAddr() {
return $_SERVER['REMOTE_ADDR'];
}
public function isHTTPS() {
if (empty($_SERVER['HTTPS'])) {
return false;
}
if (!strcasecmp($_SERVER["HTTPS"], "off")) {
return false;
}
return true;
}
}
diff --git a/src/aphront/AphrontURIMapper.php b/src/aphront/AphrontURIMapper.php
index bdda98bdf6..20ae87db97 100644
--- a/src/aphront/AphrontURIMapper.php
+++ b/src/aphront/AphrontURIMapper.php
@@ -1,68 +1,52 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group aphront
*/
final class AphrontURIMapper {
private $map;
final public function __construct(array $map) {
$this->map = $map;
}
final public function mapPath($path) {
$map = $this->map;
foreach ($map as $rule => $value) {
list($controller, $data) = $this->tryRule($rule, $value, $path);
if ($controller) {
foreach ($data as $k => $v) {
if (is_numeric($k)) {
unset($data[$k]);
}
}
return array($controller, $data);
}
}
return array(null, null);
}
final private function tryRule($rule, $value, $path) {
$match = null;
$pattern = '#^'.$rule.(is_array($value) ? '' : '$').'#';
if (!preg_match($pattern, $path, $match)) {
return array(null, null);
}
if (!is_array($value)) {
return array($value, $match);
}
$path = substr($path, strlen($match[0]));
foreach ($value as $srule => $sval) {
list($controller, $data) = $this->tryRule($srule, $sval, $path);
if ($controller) {
return array($controller, $data + $match);
}
}
return array(null, null);
}
}
diff --git a/src/aphront/__tests__/AphrontRequestTestCase.php b/src/aphront/__tests__/AphrontRequestTestCase.php
index 75645ee53d..7001ba94d8 100644
--- a/src/aphront/__tests__/AphrontRequestTestCase.php
+++ b/src/aphront/__tests__/AphrontRequestTestCase.php
@@ -1,98 +1,82 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontRequestTestCase extends PhabricatorTestCase {
public function testRequestDataAccess() {
$r = new AphrontRequest('example.com', '/');
$r->setRequestData(
array(
'str_empty' => '',
'str' => 'derp',
'str_true' => 'true',
'str_false' => 'false',
'zero' => '0',
'one' => '1',
'arr_empty' => array(),
'arr_num' => array(1, 2, 3),
'comma' => ',',
'comma_1' => 'a, b',
'comma_2' => ' ,a ,, b ,,,, ,, ',
'comma_3' => '0',
'comma_4' => 'a, a, b, a',
'comma_5' => "a\nb, c\n\nd\n\n\n,\n",
));
$this->assertEqual(1, $r->getInt('one'));
$this->assertEqual(0, $r->getInt('zero'));
$this->assertEqual(null, $r->getInt('does-not-exist'));
$this->assertEqual(0, $r->getInt('str_empty'));
$this->assertEqual(true, $r->getBool('one'));
$this->assertEqual(false, $r->getBool('zero'));
$this->assertEqual(true, $r->getBool('str_true'));
$this->assertEqual(false, $r->getBool('str_false'));
$this->assertEqual(true, $r->getBool('str'));
$this->assertEqual(null, $r->getBool('does-not-exist'));
$this->assertEqual(false, $r->getBool('str_empty'));
$this->assertEqual('derp', $r->getStr('str'));
$this->assertEqual('', $r->getStr('str_empty'));
$this->assertEqual(null, $r->getStr('does-not-exist'));
$this->assertEqual(array(), $r->getArr('arr_empty'));
$this->assertEqual(array(1, 2, 3), $r->getArr('arr_num'));
$this->assertEqual(null, $r->getArr('str_empty', null));
$this->assertEqual(null, $r->getArr('str_true', null));
$this->assertEqual(null, $r->getArr('does-not-exist', null));
$this->assertEqual(array(), $r->getArr('does-not-exist'));
$this->assertEqual(array(), $r->getStrList('comma'));
$this->assertEqual(array('a', 'b'), $r->getStrList('comma_1'));
$this->assertEqual(array('a', 'b'), $r->getStrList('comma_2'));
$this->assertEqual(array('0'), $r->getStrList('comma_3'));
$this->assertEqual(array('a', 'a', 'b', 'a'), $r->getStrList('comma_4'));
$this->assertEqual(array('a', 'b', 'c', 'd'), $r->getStrList('comma_5'));
$this->assertEqual(array(), $r->getStrList('does-not-exist'));
$this->assertEqual(null, $r->getStrList('does-not-exist', null));
$this->assertEqual(true, $r->getExists('str'));
$this->assertEqual(false, $r->getExists('does-not-exist'));
}
public function testHostAttacks() {
static $tests = array(
'domain.com' => 'domain.com',
'domain.com:80' => 'domain.com',
'evil.com:evil.com@real.com' => 'real.com',
'evil.com:evil.com@real.com:80' => 'real.com',
);
foreach ($tests as $input => $expect) {
$r = new AphrontRequest($input, '/');
$this->assertEqual(
$expect,
$r->getHost(),
'Host: '.$input);
}
}
}
diff --git a/src/aphront/configuration/AphrontApplicationConfiguration.php b/src/aphront/configuration/AphrontApplicationConfiguration.php
index e99fad36bb..bdce0af0df 100644
--- a/src/aphront/configuration/AphrontApplicationConfiguration.php
+++ b/src/aphront/configuration/AphrontApplicationConfiguration.php
@@ -1,238 +1,222 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @task routing URI Routing
* @group aphront
*/
abstract class AphrontApplicationConfiguration {
private $request;
private $host;
private $path;
private $console;
abstract public function getApplicationName();
abstract public function getURIMap();
abstract public function buildRequest();
abstract public function build404Controller();
abstract public function buildRedirectController($uri);
final public function setRequest(AphrontRequest $request) {
$this->request = $request;
return $this;
}
final public function getRequest() {
return $this->request;
}
final public function getConsole() {
return $this->console;
}
final public function setConsole($console) {
$this->console = $console;
return $this;
}
final public function setHost($host) {
$this->host = $host;
return $this;
}
final public function getHost() {
return $this->host;
}
final public function setPath($path) {
$this->path = $path;
return $this;
}
final public function getPath() {
return $this->path;
}
public function willBuildRequest() {
}
/**
* Hook for synchronizing account information from OAuth workflows.
*
* @task hook
*/
public function willAuthenticateUserWithOAuth(
PhabricatorUser $user,
PhabricatorUserOAuthInfo $oauth_info,
PhabricatorOAuthProvider $provider) {
return;
}
/* -( URI Routing )-------------------------------------------------------- */
/**
* Using builtin and application routes, build the appropriate
* @{class:AphrontController} class for the request. To route a request, we
* first test if the HTTP_HOST is configured as a valid Phabricator URI. If
* it isn't, we do a special check to see if it's a custom domain for a blog
* in the Phame application and if that fails we error. Otherwise, we test
* the URI against all builtin routes from @{method:getURIMap}, then against
* all application routes from installed @{class:PhabricatorApplication}s.
*
* If we match a route, we construct the controller it points at, build it,
* and return it.
*
* If we fail to match a route, but the current path is missing a trailing
* "/", we try routing the same path with a trailing "/" and do a redirect
* if that has a valid route. The idea is to canoncalize URIs for consistency,
* but avoid breaking noncanonical URIs that we can easily salvage.
*
* NOTE: We only redirect on GET. On POST, we'd drop parameters and most
* likely mutate the request implicitly, and a bad POST usually indicates a
* programming error rather than a sloppy typist.
*
* If the failing path already has a trailing "/", or we can't route the
* version with a "/", we call @{method:build404Controller}, which build a
* fallback @{class:AphrontController}.
*
* @return pair<AphrontController,dict> Controller and dictionary of request
* parameters.
* @task routing
*/
final public function buildController() {
$request = $this->getRequest();
if (PhabricatorEnv::getEnvConfig('security.require-https')) {
if (!$request->isHTTPS()) {
$uri = $request->getRequestURI();
$uri->setDomain($request->getHost());
$uri->setProtocol('https');
return $this->buildRedirectController($uri);
}
}
$path = $request->getPath();
$host = $request->getHost();
$base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
$prod_uri = PhabricatorEnv::getEnvConfig('phabricator.production-uri');
$file_uri = PhabricatorEnv::getEnvConfig('security.alternate-file-domain');
if ($host != id(new PhutilURI($base_uri))->getDomain() &&
$host != id(new PhutilURI($prod_uri))->getDomain() &&
$host != id(new PhutilURI($file_uri))->getDomain()) {
try {
$blog = id(new PhameBlogQuery())
->setViewer(new PhabricatorUser())
->withDomain($host)
->executeOne();
} catch (PhabricatorPolicyException $ex) {
throw new Exception(
"This blog is not visible to logged out users, so it can not be ".
"visited from a custom domain.");
}
if (!$blog) {
if ($prod_uri && $prod_uri != $base_uri) {
$prod_str = ' or '.$prod_uri;
} else {
$prod_str = '';
}
throw new Exception(
'Specified domain '.$host.' is not configured for Phabricator '.
'requests. Please use '.$base_uri.$prod_str.' to visit this instance.'
);
}
// TODO: Make this more flexible and modular so any application can
// do crazy stuff here if it wants.
$path = '/phame/live/'.$blog->getID().'/'.$path;
}
list($controller, $uri_data) = $this->buildControllerForPath($path);
if (!$controller) {
if (!preg_match('@/$@', $path)) {
// If we failed to match anything but don't have a trailing slash, try
// to add a trailing slash and issue a redirect if that resolves.
list($controller, $uri_data) = $this->buildControllerForPath($path.'/');
// NOTE: For POST, just 404 instead of redirecting, since the redirect
// will be a GET without parameters.
if ($controller && !$request->isHTTPPost()) {
$uri = $request->getRequestURI()->setPath($path.'/');
return $this->buildRedirectController($uri);
}
}
return $this->build404Controller();
}
return array($controller, $uri_data);
}
/**
* Map a specific path to the corresponding controller. For a description
* of routing, see @{method:buildController}.
*
* @return pair<AphrontController,dict> Controller and dictionary of request
* parameters.
* @task routing
*/
final public function buildControllerForPath($path) {
$maps = array();
$maps[] = array(null, $this->getURIMap());
$applications = PhabricatorApplication::getAllInstalledApplications();
foreach ($applications as $application) {
$maps[] = array($application, $application->getRoutes());
}
$current_application = null;
$controller_class = null;
foreach ($maps as $map_info) {
list($application, $map) = $map_info;
$mapper = new AphrontURIMapper($map);
list($controller_class, $uri_data) = $mapper->mapPath($path);
if ($controller_class) {
if ($application) {
$current_application = $application;
}
break;
}
}
if (!$controller_class) {
return array(null, null);
}
$request = $this->getRequest();
$controller = newv($controller_class, array($request));
if ($current_application) {
$controller->setCurrentApplication($current_application);
}
return array($controller, $uri_data);
}
}
diff --git a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php
index 77905cb0ca..57ab9f84e3 100644
--- a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php
+++ b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php
@@ -1,411 +1,395 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group aphront
*/
class AphrontDefaultApplicationConfiguration
extends AphrontApplicationConfiguration {
public function __construct() {
}
public function getApplicationName() {
return 'aphront-default';
}
public function getURIMap() {
return $this->getResourceURIMapRules() + array(
'/(?:(?P<filter>(?:jump))/)?' =>
'PhabricatorDirectoryMainController',
'/(?:(?P<filter>feed)/)' => array(
'public/' => 'PhabricatorFeedPublicStreamController',
'(?:(?P<subfilter>[^/]+)/)?' =>
'PhabricatorDirectoryMainController',
),
'/typeahead/' => array(
'common/(?P<type>\w+)/'
=> 'PhabricatorTypeaheadCommonDatasourceController',
),
'/login/' => array(
'' => 'PhabricatorLoginController',
'email/' => 'PhabricatorEmailLoginController',
'etoken/(?P<token>\w+)/' => 'PhabricatorEmailTokenController',
'refresh/' => 'PhabricatorRefreshCSRFController',
'validate/' => 'PhabricatorLoginValidateController',
'mustverify/' => 'PhabricatorMustVerifyEmailController',
),
'/logout/' => 'PhabricatorLogoutController',
'/oauth/' => array(
'(?P<provider>\w+)/' => array(
'login/' => 'PhabricatorOAuthLoginController',
'diagnose/' => 'PhabricatorOAuthDiagnosticsController',
'unlink/' => 'PhabricatorOAuthUnlinkController',
),
),
'/ldap/' => array(
'login/' => 'PhabricatorLDAPLoginController',
'unlink/' => 'PhabricatorLDAPUnlinkController',
),
'/oauthserver/' => array(
'auth/' => 'PhabricatorOAuthServerAuthController',
'test/' => 'PhabricatorOAuthServerTestController',
'token/' => 'PhabricatorOAuthServerTokenController',
'clientauthorization/' => array(
'' => 'PhabricatorOAuthClientAuthorizationListController',
'delete/(?P<phid>[^/]+)/' =>
'PhabricatorOAuthClientAuthorizationDeleteController',
'edit/(?P<phid>[^/]+)/' =>
'PhabricatorOAuthClientAuthorizationEditController',
),
'client/' => array(
'' => 'PhabricatorOAuthClientListController',
'create/' => 'PhabricatorOAuthClientEditController',
'delete/(?P<phid>[^/]+)/' => 'PhabricatorOAuthClientDeleteController',
'edit/(?P<phid>[^/]+)/' => 'PhabricatorOAuthClientEditController',
'view/(?P<phid>[^/]+)/' => 'PhabricatorOAuthClientViewController',
),
),
'/xhprof/' => array(
'list/(?P<view>[^/]+)/' => 'PhabricatorXHProfSampleListController',
'profile/(?P<phid>[^/]+)/' => 'PhabricatorXHProfProfileController',
),
'/~/' => 'DarkConsoleController',
'/search/' => array(
'' => 'PhabricatorSearchController',
'(?P<key>[^/]+)/' => 'PhabricatorSearchController',
'attach/(?P<phid>[^/]+)/(?P<type>\w+)/(?:(?P<action>\w+)/)?'
=> 'PhabricatorSearchAttachController',
'select/(?P<type>\w+)/'
=> 'PhabricatorSearchSelectController',
'index/(?P<phid>[^/]+)/' => 'PhabricatorSearchIndexController',
),
'/status/' => 'PhabricatorStatusController',
'/help/' => array(
'keyboardshortcut/' => 'PhabricatorHelpKeyboardShortcutController',
),
'/chatlog/' => array(
'' =>
'PhabricatorChatLogChannelListController',
'channel/(?P<channel>[^/]+)/' =>
'PhabricatorChatLogChannelLogController',
),
'/notification/' => array(
'(?:(?P<filter>all|unread)/)?'
=> 'PhabricatorNotificationListController',
'panel/' => 'PhabricatorNotificationPanelController',
'individual/' => 'PhabricatorNotificationIndividualController',
'status/' => 'PhabricatorNotificationStatusController',
'clear/' => 'PhabricatorNotificationClearController',
),
'/phortune/' => array(
'stripe/' => array(
'testpaymentform/' => 'PhortuneStripeTestPaymentFormController',
),
),
);
}
protected function getResourceURIMapRules() {
return array(
'/res/' => array(
'(?P<package>pkg/)?'.
'(?P<hash>[a-f0-9]{8})/'.
'(?P<path>.+\.(?:css|js|jpg|png|swf|gif))'
=> 'CelerityPhabricatorResourceController',
),
);
}
public function buildRequest() {
$request = new AphrontRequest($this->getHost(), $this->getPath());
$request->setRequestData($_GET + $_POST);
$request->setApplicationConfiguration($this);
return $request;
}
public function handleException(Exception $ex) {
$request = $this->getRequest();
// For Conduit requests, return a Conduit response.
if ($request->isConduit()) {
$response = new ConduitAPIResponse();
$response->setErrorCode(get_class($ex));
$response->setErrorInfo($ex->getMessage());
return id(new AphrontJSONResponse())
->setContent($response->toDictionary());
}
// For non-workflow requests, return a Ajax response.
if ($request->isAjax() && !$request->isJavelinWorkflow()) {
$response = new AphrontAjaxResponse();
$response->setError(
array(
'code' => get_class($ex),
'info' => $ex->getMessage(),
));
return $response;
}
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
$user = $request->getUser();
if (!$user) {
// If we hit an exception very early, we won't have a user.
$user = new PhabricatorUser();
}
if ($ex instanceof PhabricatorPolicyException) {
if (!$user->isLoggedIn()) {
// If the user isn't logged in, just give them a login form. This is
// probably a generally more useful response than a policy dialog that
// they have to click through to get a login form.
//
// Possibly we should add a header here like "you need to login to see
// the thing you are trying to look at".
$login_controller = new PhabricatorLoginController($request);
return $login_controller->processRequest();
}
$content =
'<div class="aphront-policy-exception">'.
phutil_escape_html($ex->getMessage()).
'</div>';
$dialog = new AphrontDialogView();
$dialog
->setTitle(
$is_serious
? 'Access Denied'
: "You Shall Not Pass")
->setClass('aphront-access-dialog')
->setUser($user)
->appendChild($content);
if ($this->getRequest()->isAjax()) {
$dialog->addCancelButton('/', 'Close');
} else {
$dialog->addCancelButton('/', $is_serious ? 'OK' : 'Away With Thee');
}
$response = new AphrontDialogResponse();
$response->setDialog($dialog);
return $response;
}
if ($ex instanceof AphrontUsageException) {
$error = new AphrontErrorView();
$error->setTitle(phutil_escape_html($ex->getTitle()));
$error->appendChild(phutil_escape_html($ex->getMessage()));
$view = new PhabricatorStandardPageView();
$view->setRequest($this->getRequest());
$view->appendChild($error);
$response = new AphrontWebpageResponse();
$response->setContent($view->render());
return $response;
}
// Always log the unhandled exception.
phlog($ex);
$class = phutil_escape_html(get_class($ex));
$message = phutil_escape_html($ex->getMessage());
if ($ex instanceof AphrontQuerySchemaException) {
$message .=
"\n\n".
"NOTE: This usually indicates that the MySQL schema has not been ".
"properly upgraded. Run 'bin/storage upgrade' to ensure your ".
"schema is up to date.";
}
if (PhabricatorEnv::getEnvConfig('phabricator.show-stack-traces')) {
$trace = $this->renderStackTrace($ex->getTrace(), $user);
} else {
$trace = null;
}
$content =
'<div class="aphront-unhandled-exception">'.
'<div class="exception-message">'.$message.'</div>'.
$trace.
'</div>';
$dialog = new AphrontDialogView();
$dialog
->setTitle('Unhandled Exception ("'.$class.'")')
->setClass('aphront-exception-dialog')
->setUser($user)
->appendChild($content);
if ($this->getRequest()->isAjax()) {
$dialog->addCancelButton('/', 'Close');
}
$response = new AphrontDialogResponse();
$response->setDialog($dialog);
return $response;
}
public function willSendResponse(AphrontResponse $response) {
return $response;
}
public function build404Controller() {
return array(new Phabricator404Controller($this->getRequest()), array());
}
public function buildRedirectController($uri) {
return array(
new PhabricatorRedirectController($this->getRequest()),
array(
'uri' => $uri,
));
}
private function renderStackTrace($trace, PhabricatorUser $user) {
$libraries = PhutilBootloader::getInstance()->getAllLibraries();
$version = PhabricatorEnv::getEnvConfig('phabricator.version');
if (preg_match('/[^a-f0-9]/i', $version)) {
$version = '';
}
// TODO: Make this configurable?
$path = 'https://secure.phabricator.com/diffusion/%s/browse/master/src/';
$callsigns = array(
'arcanist' => 'ARC',
'phutil' => 'PHU',
'phabricator' => 'P',
);
$rows = array();
$depth = count($trace);
foreach ($trace as $part) {
$lib = null;
$file = idx($part, 'file');
$relative = $file;
foreach ($libraries as $library) {
$root = phutil_get_library_root($library);
if (Filesystem::isDescendant($file, $root)) {
$lib = $library;
$relative = Filesystem::readablePath($file, $root);
break;
}
}
$where = '';
if (isset($part['class'])) {
$where .= $part['class'].'::';
}
if (isset($part['function'])) {
$where .= $part['function'].'()';
}
if ($file) {
if (isset($callsigns[$lib])) {
$attrs = array('title' => $file);
try {
$attrs['href'] = $user->loadEditorLink(
'/src/'.$relative,
$part['line'],
$callsigns[$lib]);
} catch (Exception $ex) {
// The database can be inaccessible.
}
if (empty($attrs['href'])) {
$attrs['href'] = sprintf($path, $callsigns[$lib]).
str_replace(DIRECTORY_SEPARATOR, '/', $relative).
($version && $lib == 'phabricator' ? ';'.$version : '').
'$'.$part['line'];
$attrs['target'] = '_blank';
}
$file_name = phutil_render_tag(
'a',
$attrs,
phutil_escape_html($relative));
} else {
$file_name = phutil_render_tag(
'span',
array(
'title' => $file,
),
phutil_escape_html($relative));
}
$file_name = $file_name.' : '.(int)$part['line'];
} else {
$file_name = '<em>(Internal)</em>';
}
$rows[] = array(
$depth--,
phutil_escape_html($lib),
$file_name,
phutil_escape_html($where),
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Depth',
'Library',
'File',
'Where',
));
$table->setColumnClasses(
array(
'n',
'',
'',
'wide',
));
return
'<div class="exception-trace">'.
'<div class="exception-trace-header">Stack Trace</div>'.
$table->render().
'</div>';
}
}
diff --git a/src/aphront/console/DarkConsoleController.php b/src/aphront/console/DarkConsoleController.php
index 49e36c65b4..eb6c00f2f9 100644
--- a/src/aphront/console/DarkConsoleController.php
+++ b/src/aphront/console/DarkConsoleController.php
@@ -1,60 +1,44 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group console
*/
final class DarkConsoleController extends PhabricatorController {
protected $op;
protected $data;
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$visible = $request->getStr('visible');
if (strlen($visible)) {
$user->setConsoleVisible((int)$visible);
$user->save();
return id(new AphrontAjaxResponse())->setDisableConsole(true);
}
$tab = $request->getStr('tab');
if (strlen($tab)) {
$user->setConsoleTab($tab);
$user->save();
return id(new AphrontAjaxResponse())->setDisableConsole(true);
}
if (PhabricatorEnv::getEnvConfig('darkconsole.enabled')) {
$user->setConsoleEnabled(!$user->getConsoleEnabled());
if ($user->getConsoleEnabled()) {
$user->setConsoleVisible(true);
}
$user->save();
if ($request->isAjax()) {
return new AphrontRedirectResponse();
} else {
return id(new AphrontRedirectResponse())->setURI('/');
}
}
}
}
diff --git a/src/aphront/console/DarkConsoleCore.php b/src/aphront/console/DarkConsoleCore.php
index db08ec6074..455be1c1a0 100644
--- a/src/aphront/console/DarkConsoleCore.php
+++ b/src/aphront/console/DarkConsoleCore.php
@@ -1,215 +1,199 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group console
*/
final class DarkConsoleCore {
const PLUGIN_ERRORLOG = 'ErrorLog';
const PLUGIN_SERVICES = 'Services';
const PLUGIN_EVENT = 'Event';
const PLUGIN_XHPROF = 'XHProf';
const PLUGIN_REQUEST = 'Request';
const PLUGIN_CONFIG = 'Config';
public static function getPlugins() {
return array(
self::PLUGIN_ERRORLOG,
self::PLUGIN_REQUEST,
self::PLUGIN_SERVICES,
self::PLUGIN_EVENT,
self::PLUGIN_XHPROF,
self::PLUGIN_CONFIG,
);
}
private $plugins = array();
private $settings;
private $coredata;
public function getPlugin($plugin_name) {
return idx($this->plugins, $plugin_name);
}
public function __construct() {
foreach (self::getPlugins() as $plugin_name) {
$plugin = self::newPlugin($plugin_name);
if ($plugin->isPermanent() || !isset($disabled[$plugin_name])) {
if ($plugin->shouldStartup()) {
$plugin->didStartup();
$plugin->setConsoleCore($this);
$this->plugins[$plugin_name] = $plugin;
}
}
}
}
public static function newPlugin($plugin) {
$class = 'DarkConsole'.$plugin.'Plugin';
return newv($class, array());
}
public function getEnabledPlugins() {
return $this->plugins;
}
public function render(AphrontRequest $request) {
$user = $request->getUser();
$plugins = $this->getEnabledPlugins();
foreach ($plugins as $plugin) {
$plugin->setRequest($request);
$plugin->willShutdown();
}
foreach ($plugins as $plugin) {
$plugin->didShutdown();
}
foreach ($plugins as $plugin) {
$plugin->setData($plugin->generateData());
}
$selected = $user->getConsoleTab();
$visible = $user->getConsoleVisible();
if (!isset($plugins[$selected])) {
$selected = head_key($plugins);
}
$tabs = array();
foreach ($plugins as $key => $plugin) {
$tabs[$key] = array(
'name' => $plugin->getName(),
'panel' => $plugin->render(),
);
}
$tabs_markup = array();
$panel_markup = array();
foreach ($tabs as $key => $data) {
$is_selected = ($key == $selected);
if ($is_selected) {
$style = null;
$tabclass = 'dark-console-tab-selected';
} else {
$style = 'display: none;';
$tabclass = null;
}
$tabs_markup[] = javelin_render_tag(
'a',
array(
'class' => "dark-console-tab {$tabclass}",
'sigil' => 'dark-console-tab',
'id' => 'dark-console-tab-'.$key,
),
(string)$data['name']);
$panel_markup[] = javelin_render_tag(
'div',
array(
'class' => 'dark-console-panel dark-console-panel-'.$key,
'style' => $style,
'sigil' => 'dark-console-panel',
),
(string)$data['panel']);
}
$console = javelin_render_tag(
'table',
array(
'class' => 'dark-console',
'sigil' => 'dark-console',
'style' => $visible ? '' : 'display: none;',
),
'<tr>'.
'<th class="dark-console-tabs">'.
implode("\n", $tabs_markup).
'</th>'.
'<td>'.implode("\n", $panel_markup).'</td>'.
'</tr>');
if (!empty($_COOKIE['phsid'])) {
$console = str_replace(
$_COOKIE['phsid'],
phutil_escape_html('<session-key>'),
$console);
}
if ($request->isAjax()) {
// for ajax this HTML gets updated on the client
$request_history = null;
} else {
$request_table_header =
'<div class="dark-console-panel-request-log-separator"></div>';
$rows = array();
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Sequence',
'Type',
'URI',
));
$table->setColumnClasses(
array(
'',
'',
'wide',
));
$request_table = $request_table_header . $table->render();
$request_history = javelin_render_tag(
'table',
array(
'class' => 'dark-console dark-console-request-log',
'sigil' => 'dark-console-request-log',
'style' => $visible ? '' : 'display: none;',
),
'<tr>'.
'<th class="dark-console-tabs">'.
javelin_render_tag(
'a',
array(
'class' => 'dark-console-tab dark-console-tab-selected',
),
'Request Log').
'</th>'.
'<td>'.
javelin_render_tag(
'div',
array(
'class' => 'dark-console-panel dark-console-panel-RequestLog',
),
$request_table).
'</td>'.
'</tr>');
}
return "\n\n\n\n".$console.$request_history."\n\n\n\n";
}
}
diff --git a/src/aphront/console/plugin/DarkConsoleConfigPlugin.php b/src/aphront/console/plugin/DarkConsoleConfigPlugin.php
index ad5d89ab8d..96add3d32b 100644
--- a/src/aphront/console/plugin/DarkConsoleConfigPlugin.php
+++ b/src/aphront/console/plugin/DarkConsoleConfigPlugin.php
@@ -1,113 +1,97 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group console
*/
final class DarkConsoleConfigPlugin extends DarkConsolePlugin {
public function getName() {
return 'Config';
}
public function getDescription() {
return 'Information about Phabricator configuration';
}
public function generateData() {
$lib_data = array();
foreach (PhutilBootloader::getInstance()->getAllLibraries() as $lib) {
$lib_data[$lib] = phutil_get_library_root($lib);
}
return array(
'config' => PhabricatorEnv::getAllConfigKeys(),
'libraries' => $lib_data,
);
}
public function render() {
$data = $this->getData();
$lib_data = $data['libraries'];
$lib_rows = array();
foreach ($lib_data as $key => $value) {
$lib_rows[] = array(
phutil_escape_html($key),
phutil_escape_html($value),
);
}
$lib_table = new AphrontTableView($lib_rows);
$lib_table->setHeaders(
array(
'Library',
'Loaded From',
));
$lib_table->setColumnClasses(
array(
'header',
'wide wrap',
));
$config_data = $data['config'];
ksort($config_data);
$mask = PhabricatorEnv::getEnvConfig('darkconsole.config-mask');
$mask = array_fill_keys($mask, true);
foreach ($mask as $masked_key => $ignored) {
if (!PhabricatorEnv::envConfigExists($masked_key)) {
throw new Exception(
"Configuration 'darkconsole.config-mask' masks unknown ".
"configuration key '".$masked_key."'. If this key has been ".
"renamed, you might be accidentally exposing information which you ".
"don't intend to.");
}
}
$rows = array();
foreach ($config_data as $key => $value) {
if (empty($mask[$key])) {
$display_value = is_array($value) ? json_encode($value) : $value;
$display_value = phutil_escape_html($display_value);
} else {
$display_value = phutil_escape_html('<Masked>');
}
$rows[] = array(
phutil_escape_html($key),
$display_value,
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Key',
'Value',
));
$table->setColumnClasses(
array(
'header',
'wide wrap',
));
return $lib_table->render().$table->render();
}
}
diff --git a/src/aphront/console/plugin/DarkConsoleErrorLogPlugin.php b/src/aphront/console/plugin/DarkConsoleErrorLogPlugin.php
index 8e4b70a770..20123516f8 100644
--- a/src/aphront/console/plugin/DarkConsoleErrorLogPlugin.php
+++ b/src/aphront/console/plugin/DarkConsoleErrorLogPlugin.php
@@ -1,167 +1,151 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group console
*/
final class DarkConsoleErrorLogPlugin extends DarkConsolePlugin {
public function getName() {
$count = count($this->getData());
if ($count) {
return
'<span style="color: #ff0000;">&bull;</span> '.
"Error Log ({$count})";
}
return 'Error Log';
}
public function getDescription() {
return 'Shows errors and warnings.';
}
public function generateData() {
return DarkConsoleErrorLogPluginAPI::getErrors();
}
public function render() {
$data = $this->getData();
$rows = array();
$details = '';
foreach ($data as $index => $row) {
$file = $row['file'];
$line = $row['line'];
$tag = phutil_render_tag(
'a',
array(
'onclick' => jsprintf('show_details(%d)', $index),
),
phutil_escape_html($row['str'].' at ['.basename($file).':'.$line.']'));
$rows[] = array($tag);
$details .=
'<div class="dark-console-panel-error-details" id="row-details-'.
$index.'">'.
phutil_escape_html($row['details'])."\n".
'Stack trace:'."\n";
foreach ($row['trace'] as $key => $entry) {
$line = '';
if (isset($entry['class'])) {
$line .= $entry['class'].'::';
}
$line .= idx($entry, 'function', '');
$href = null;
if (isset($entry['file'])) {
$line .= ' called at ['.$entry['file'].':'.$entry['line'].']';
try {
$user = $this->getRequest()->getUser();
$href = $user->loadEditorLink($entry['file'], $entry['line'], '');
} catch (Exception $ex) {
// The database can be inaccessible.
}
}
$details .= phutil_render_tag(
'a',
array(
'href' => $href,
),
phutil_escape_html($line));
$details .= "\n";
}
$details .= '</div>';
}
$table = new AphrontTableView($rows);
$table->setClassName('error-log');
$table->setHeaders(array('Error'));
$table->setNoDataString('No errors.');
return '<div>'.
'<div>'.$table->render().'</div>'.
'<pre class="PhabricatorMonospaced">'.
$details.'</pre>'.
'</div>';
}
}
/*
$data = $this->getData();
if (!$data) {
return
<x:frag>
<div class="mu">No errors.</div>
</x:frag>;
}
$markup = <table class="LConsoleErrors" />;
$alt = false;
foreach ($data as $error) {
$row = <tr class={$alt ? 'alt' : null} />;
$text = $error['error'];
$text = preg_replace('/\(in .* on line \d+\)$/', '', trim($text));
$trace = $error['trace'];
$trace = explode("\n", $trace);
if (!$trace) {
$trace = array('unknown@0@unknown');
}
foreach ($trace as $idx => $traceline) {
list($file, $line, $where) = array_merge(
explode('@', $traceline),
array('?', '?', '?'));
if ($where == 'DarkConsole->addError' ||
$where == 'debug_rlog') {
unset($trace[$idx]);
}
}
$row->appendChild(<th rowspan={count($trace)}>{$text}</th>);
foreach ($trace as $traceline) {
list($file, $line, $where) = array_merge(
explode('@', $traceline),
array('?', '?', '?'));
$row->appendChild(<td>{$file}:{$line}</td>);
$row->appendChild(<td>{$where}()</td>);
$markup->appendChild($row);
$row = <tr class={$alt ? 'alt' : null} />;
}
$alt = !$alt;
}
return
<x:frag>
<h1>Errors</h1>
<div class="LConsoleErrors">{$markup}</div>
</x:frag>;
*/
diff --git a/src/aphront/console/plugin/DarkConsoleEventPlugin.php b/src/aphront/console/plugin/DarkConsoleEventPlugin.php
index eb5dbe5e43..d4bd7dcc3b 100644
--- a/src/aphront/console/plugin/DarkConsoleEventPlugin.php
+++ b/src/aphront/console/plugin/DarkConsoleEventPlugin.php
@@ -1,117 +1,101 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group console
*/
final class DarkConsoleEventPlugin extends DarkConsolePlugin {
public function getName() {
return 'Events';
}
public function getDescription() {
return 'Information about Phabricator events and event listeners.';
}
public function generateData() {
$listeners = PhutilEventEngine::getInstance()->getAllListeners();
foreach ($listeners as $key => $listener) {
$listeners[$key] = array(
'id' => $listener->getListenerID(),
'class' => get_class($listener),
);
}
$events = DarkConsoleEventPluginAPI::getEvents();
foreach ($events as $key => $event) {
$events[$key] = array(
'type' => $event->getType(),
'stopped' => $event->isStopped(),
);
}
return array(
'listeners' => $listeners,
'events' => $events,
);
}
public function render() {
$data = $this->getData();
$out = array();
$out[] =
'<div class="dark-console-panel-header">'.
'<h1>Registered Event Listeners</h1>'.
'</div>';
$rows = array();
foreach ($data['listeners'] as $listener) {
$rows[] = array(
phutil_escape_html($listener['id']),
phutil_escape_html($listener['class']),
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Internal ID',
'Listener Class',
));
$table->setColumnClasses(
array(
'',
'wide',
));
$out[] = $table->render();
$out[] =
'<div class="dark-console-panel-header">'.
'<h1>Event Log</h1>'.
'</div>';
$rows = array();
foreach ($data['events'] as $event) {
$rows[] = array(
phutil_escape_html($event['type']),
$event['stopped'] ? 'STOPPED' : null,
);
}
$table = new AphrontTableView($rows);
$table->setColumnClasses(
array(
'wide',
));
$table->setHeaders(
array(
'Event Type',
'Stopped',
));
$out[] = $table->render();
return implode("\n", $out);
}
}
diff --git a/src/aphront/console/plugin/DarkConsolePlugin.php b/src/aphront/console/plugin/DarkConsolePlugin.php
index 142d1aafab..2499cfbc7a 100644
--- a/src/aphront/console/plugin/DarkConsolePlugin.php
+++ b/src/aphront/console/plugin/DarkConsolePlugin.php
@@ -1,95 +1,79 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group console
*/
abstract class DarkConsolePlugin {
private $data;
private $request;
private $core;
abstract public function getName();
abstract public function getDescription();
abstract public function render();
public function __construct() {
}
public function setConsoleCore(DarkConsoleCore $core) {
$this->core = $core;
return $this;
}
public function getConsoleCore() {
return $this->core;
}
public function generateData() {
return null;
}
public function setData($data) {
$this->data = $data;
return $this;
}
public function getData() {
return $this->data;
}
public function setRequest($request) {
$this->request = $request;
return $this;
}
public function getRequest() {
return $this->request;
}
public function getRequestURI() {
return $this->getRequest()->getRequestURI();
}
public function isPermanent() {
return false;
}
public function shouldStartup() {
return true;
}
public function didStartup() {
return null;
}
public function willShutdown() {
return null;
}
public function didShutdown() {
return null;
}
public function processRequest() {
return null;
}
}
diff --git a/src/aphront/console/plugin/DarkConsoleRequestPlugin.php b/src/aphront/console/plugin/DarkConsoleRequestPlugin.php
index 7e4ee4a8fd..e252f41e8a 100644
--- a/src/aphront/console/plugin/DarkConsoleRequestPlugin.php
+++ b/src/aphront/console/plugin/DarkConsoleRequestPlugin.php
@@ -1,84 +1,68 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group console
*/
final class DarkConsoleRequestPlugin extends DarkConsolePlugin {
public function getName() {
return 'Request';
}
public function getDescription() {
return 'Information about $_REQUEST and $_SERVER.';
}
public function generateData() {
return array(
'Request' => $_REQUEST,
'Server' => $_SERVER,
);
}
public function render() {
$data = $this->getData();
$sections = array(
'Basics' => array(
'Machine' => php_uname('n'),
),
);
// NOTE: This may not be present for some SAPIs, like php-fpm.
if (!empty($data['Server']['SERVER_ADDR'])) {
$addr = $data['Server']['SERVER_ADDR'];
$sections['Basics']['Host'] = $addr;
$sections['Basics']['Hostname'] = @gethostbyaddr($addr);
}
$sections = array_merge($sections, $data);
$out = array();
foreach ($sections as $header => $map) {
$rows = array();
foreach ($map as $key => $value) {
$rows[] = array(
phutil_escape_html($key),
phutil_escape_html(is_array($value) ? json_encode($value) : $value),
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
$header,
null,
));
$table->setColumnClasses(
array(
'header',
'wide wrap',
));
$out[] = $table->render();
}
return implode("\n", $out);
}
}
diff --git a/src/aphront/console/plugin/DarkConsoleServicesPlugin.php b/src/aphront/console/plugin/DarkConsoleServicesPlugin.php
index a0db6bac0a..0cd69e578c 100644
--- a/src/aphront/console/plugin/DarkConsoleServicesPlugin.php
+++ b/src/aphront/console/plugin/DarkConsoleServicesPlugin.php
@@ -1,291 +1,275 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group console
*/
final class DarkConsoleServicesPlugin extends DarkConsolePlugin {
protected $observations;
public function getName() {
return 'Services';
}
public function getDescription() {
return 'Information about services.';
}
public function generateData() {
$log = PhutilServiceProfiler::getInstance()->getServiceCallLog();
foreach ($log as $key => $entry) {
$config = idx($entry, 'config', array());
unset($log[$key]['config']);
if (empty($_REQUEST['__analyze__'])) {
$log[$key]['explain'] = array(
'sev' => 7,
'size' => null,
'reason' => 'Disabled',
);
// Query analysis is disabled for this request, so don't do any of it.
continue;
}
if ($entry['type'] != 'query') {
continue;
}
// For each SELECT query, go issue an EXPLAIN on it so we can flag stuff
// causing table scans, etc.
if (preg_match('/^\s*SELECT\b/i', $entry['query'])) {
$conn = PhabricatorEnv::newObjectFromConfig(
'mysql.implementation',
array($entry['config']));
try {
$explain = queryfx_all(
$conn,
'EXPLAIN %Q',
$entry['query']);
$badness = 0;
$size = 1;
$reason = null;
foreach ($explain as $table) {
$size *= (int)$table['rows'];
switch ($table['type']) {
case 'index':
$cur_badness = 1;
$cur_reason = 'Index';
break;
case 'const':
$cur_badness = 1;
$cur_reason = 'Const';
break;
case 'eq_ref';
$cur_badness = 2;
$cur_reason = 'EqRef';
break;
case 'range':
$cur_badness = 3;
$cur_reason = 'Range';
break;
case 'ref':
$cur_badness = 3;
$cur_reason = 'Ref';
break;
case 'fulltext':
$cur_badness = 3;
$cur_reason = 'Fulltext';
break;
case 'ALL':
if (preg_match('/Using where/', $table['Extra'])) {
if ($table['rows'] < 256 && !empty($table['possible_keys'])) {
$cur_badness = 2;
$cur_reason = 'Small Table Scan';
} else {
$cur_badness = 6;
$cur_reason = 'TABLE SCAN!';
}
} else {
$cur_badness = 3;
$cur_reason = 'Whole Table';
}
break;
default:
if (preg_match('/No tables used/i', $table['Extra'])) {
$cur_badness = 1;
$cur_reason = 'No Tables';
} else if (preg_match('/Impossible/i', $table['Extra'])) {
$cur_badness = 1;
$cur_reason = 'Empty';
} else {
$cur_badness = 4;
$cur_reason = "Can't Analyze";
}
break;
}
if ($cur_badness > $badness) {
$badness = $cur_badness;
$reason = $cur_reason;
}
}
$log[$key]['explain'] = array(
'sev' => $badness,
'size' => $size,
'reason' => $reason,
);
} catch (Exception $ex) {
$log[$key]['explain'] = array(
'sev' => 5,
'size' => null,
'reason' => $ex->getMessage(),
);
}
}
}
return array(
'start' => $GLOBALS['__start__'],
'end' => microtime(true),
'log' => $log,
);
}
public function render() {
$data = $this->getData();
$log = $data['log'];
$results = array();
$results[] =
'<div class="dark-console-panel-header">'.
phutil_render_tag(
'a',
array(
'href' => $this->getRequestURI()->alter('__analyze__', true),
'class' => isset($_REQUEST['__analyze__'])
? 'disabled button'
: 'green button',
),
'Analyze Query Plans').
'<h1>Calls to External Services</h1>'.
'<div style="clear: both;"></div>'.
'</div>';
$page_total = $data['end'] - $data['start'];
$totals = array();
$counts = array();
foreach ($log as $row) {
$totals[$row['type']] = idx($totals, $row['type'], 0) + $row['duration'];
$counts[$row['type']] = idx($counts, $row['type'], 0) + 1;
}
$totals['All Services'] = array_sum($totals);
$counts['All Services'] = array_sum($counts);
$totals['Entire Page'] = $page_total;
$counts['Entire Page'] = 0;
$summary = array();
foreach ($totals as $type => $total) {
$summary[] = array(
$type,
number_format($counts[$type]),
number_format((int)(1000000 * $totals[$type])).' us',
sprintf('%.1f%%', 100 * $totals[$type] / $page_total),
);
}
$summary_table = new AphrontTableView($summary);
$summary_table->setColumnClasses(
array(
'',
'n',
'n',
'wide',
));
$summary_table->setHeaders(
array(
'Type',
'Count',
'Total Cost',
'Page Weight',
));
$results[] = $summary_table->render();
$rows = array();
foreach ($log as $row) {
$analysis = null;
switch ($row['type']) {
case 'query':
$info = $row['query'];
$info = wordwrap($info, 128, "\n", true);
if (!empty($row['explain'])) {
$analysis = phutil_escape_html($row['explain']['reason']);
$analysis = phutil_render_tag(
'span',
array(
'class' => 'explain-sev-'.$row['explain']['sev'],
),
$analysis);
}
$info = phutil_escape_html($info);
break;
case 'connect':
$info = $row['host'].':'.$row['database'];
$info = phutil_escape_html($info);
break;
case 'exec':
$info = $row['command'];
$info = phutil_escape_html($info);
break;
case 'conduit':
$info = $row['method'];
$info = phutil_escape_html($info);
break;
case 'http':
$info = $row['uri'];
$info = phutil_escape_html($info);
break;
default:
$info = '-';
break;
}
$rows[] = array(
phutil_escape_html($row['type']),
'+'.number_format(1000 * ($row['begin'] - $data['start'])).' ms',
number_format(1000000 * $row['duration']).' us',
$info,
$analysis,
);
}
$table = new AphrontTableView($rows);
$table->setColumnClasses(
array(
null,
'n',
'n',
'wide',
'',
));
$table->setHeaders(
array(
'Event',
'Start',
'Duration',
'Details',
'Analysis',
));
$results[] = $table->render();
return implode("\n", $results);
}
}
diff --git a/src/aphront/console/plugin/DarkConsoleXHProfPlugin.php b/src/aphront/console/plugin/DarkConsoleXHProfPlugin.php
index 0c38b60f99..299566e863 100644
--- a/src/aphront/console/plugin/DarkConsoleXHProfPlugin.php
+++ b/src/aphront/console/plugin/DarkConsoleXHProfPlugin.php
@@ -1,112 +1,96 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group console
*/
final class DarkConsoleXHProfPlugin extends DarkConsolePlugin {
protected $xhprofID;
public function getName() {
$run = $this->getData();
if ($run) {
return '<span style="color: #ff00ff;">&bull;</span> XHProf';
}
return 'XHProf';
}
public function getDescription() {
return 'Provides detailed PHP profiling information through XHProf.';
}
public function generateData() {
return $this->xhprofID;
}
public function getXHProfRunID() {
return $this->xhprofID;
}
public function render() {
if (!DarkConsoleXHProfPluginAPI::isProfilerAvailable()) {
$href = PhabricatorEnv::getDoclink('article/Installation_Guide.html');
$install_guide = phutil_render_tag(
'a',
array(
'href' => $href,
'class' => 'bright-link',
),
'Installation Guide');
return
'<div class="dark-console-no-content">'.
'The "xhprof" PHP extension is not available. Install xhprof '.
'to enable the XHProf console plugin. You can find instructions in '.
'the '.$install_guide.'.'.
'</div>';
}
$result = array();
$run = $this->getXHProfRunID();
$header =
'<div class="dark-console-panel-header">'.
phutil_render_tag(
'a',
array(
'href' => $this->getRequestURI()->alter('__profile__', 'page'),
'class' => $run
? 'disabled button'
: 'green button',
),
'Profile Page').
'<h1>XHProf Profiler</h1>'.
'</div>';
$result[] = $header;
if ($run) {
$result[] =
'<a href="/xhprof/profile/'.$run.'/" '.
'class="bright-link" '.
'style="float: right; margin: 1em 2em 0 0;'.
'font-weight: bold;" '.
'target="_blank">Profile Permalink</a>'.
'<iframe src="/xhprof/profile/'.$run.'/?frame=true"></iframe>';
} else {
$result[] =
'<div class="dark-console-no-content">'.
'Profiling was not enabled for this page. Use the button above '.
'to enable it.'.
'</div>';
}
return implode("\n", $result);
}
public function willShutdown() {
if (DarkConsoleXHProfPluginAPI::isProfilerRequested() &&
(DarkConsoleXHProfPluginAPI::isProfilerRequested() !== 'all')) {
$this->xhprofID = DarkConsoleXHProfPluginAPI::stopProfiler();
}
}
}
diff --git a/src/aphront/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php b/src/aphront/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php
index 142accdc84..fff59aab6d 100644
--- a/src/aphront/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php
+++ b/src/aphront/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php
@@ -1,86 +1,70 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group console
*/
final class DarkConsoleErrorLogPluginAPI {
private static $errors = array();
private static $discardMode = false;
public static function enableDiscardMode() {
self::$discardMode = true;
}
public static function disableDiscardMode() {
self::$discardMode = false;
}
public static function getErrors() {
return self::$errors;
}
public static function handleErrors($event, $value, $metadata) {
if (self::$discardMode) {
return;
}
switch ($event) {
case PhutilErrorHandler::EXCEPTION:
// $value is of type Exception
self::$errors[] = array(
'details' => $value->getMessage(),
'event' => $event,
'file' => $value->getFile(),
'line' => $value->getLine(),
'str' => $value->getMessage(),
'trace' => $metadata['trace'],
);
break;
case PhutilErrorHandler::ERROR:
// $value is a simple string
self::$errors[] = array(
'details' => $value,
'event' => $event,
'file' => $metadata['file'],
'line' => $metadata['line'],
'str' => $value,
'trace' => $metadata['trace'],
);
break;
case PhutilErrorHandler::PHLOG:
// $value can be anything
self::$errors[] = array(
'details' => PhutilReadableSerializer::printShallow($value, 3),
'event' => $event,
'file' => $metadata['file'],
'line' => $metadata['line'],
'str' => PhutilReadableSerializer::printShort($value),
'trace' => $metadata['trace'],
);
break;
default:
error_log('Unknown event : '.$event);
break;
}
}
}
diff --git a/src/aphront/console/plugin/event/DarkConsoleEventPluginAPI.php b/src/aphront/console/plugin/event/DarkConsoleEventPluginAPI.php
index 08bac90f65..81cb040154 100644
--- a/src/aphront/console/plugin/event/DarkConsoleEventPluginAPI.php
+++ b/src/aphront/console/plugin/event/DarkConsoleEventPluginAPI.php
@@ -1,47 +1,31 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group console
*/
final class DarkConsoleEventPluginAPI extends PhutilEventListener {
private static $events = array();
private static $discardMode = false;
public static function enableDiscardMode() {
self::$discardMode = true;
}
public static function getEvents() {
return self::$events;
}
public function register() {
$this->listen(PhabricatorEventType::TYPE_ALL);
}
public function handleEvent(PhutilEvent $event) {
if (self::$discardMode) {
return;
}
self::$events[] = $event;
}
}
diff --git a/src/aphront/console/plugin/xhprof/DarkConsoleXHProfPluginAPI.php b/src/aphront/console/plugin/xhprof/DarkConsoleXHProfPluginAPI.php
index fa56ead547..3452e675d0 100644
--- a/src/aphront/console/plugin/xhprof/DarkConsoleXHProfPluginAPI.php
+++ b/src/aphront/console/plugin/xhprof/DarkConsoleXHProfPluginAPI.php
@@ -1,134 +1,118 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group console
* @phutil-external-symbol function xhprof_enable
* @phutil-external-symbol function xhprof_disable
*/
final class DarkConsoleXHProfPluginAPI {
private static $profilerStarted;
public static function isProfilerAvailable() {
return extension_loaded('xhprof');
}
public static function isProfilerRequested() {
if (!empty($_REQUEST['__profile__'])) {
return $_REQUEST['__profile__'];
}
static $profilerRequested = null;
if (!isset($profilerRequested)) {
if (PhabricatorEnv::getEnvConfig('debug.profile-rate')) {
$rate = PhabricatorEnv::getEnvConfig('debug.profile-rate');
if (mt_rand(1, $rate) == 1) {
$profilerRequested = true;
} else {
$profilerRequested = false;
}
}
}
return $profilerRequested;
}
public static function includeXHProfLib() {
// TODO: this is incredibly stupid, but we may not have Phutil metamodule
// stuff loaded yet so we can't just phutil_get_library_root() our way
// to victory.
$root = __FILE__;
for ($ii = 0; $ii < 6; $ii++) {
$root = dirname($root);
}
require_once $root.'/externals/xhprof/xhprof_lib.php';
}
public static function hookProfiler() {
if (!self::isProfilerRequested()) {
return;
}
if (!self::isProfilerAvailable()) {
return;
}
if (self::$profilerStarted) {
return;
}
self::startProfiler();
self::$profilerStarted = true;
}
public static function startProfiler() {
self::includeXHProfLib();
xhprof_enable();
}
public static function stopProfiler() {
if (self::$profilerStarted) {
$data = xhprof_disable();
$data = serialize($data);
$file_class = 'PhabricatorFile';
// Since these happen on GET we can't do guarded writes. These also
// sometimes happen after we've disposed of the write guard; in this
// case we need to disable the whole mechanism.
$use_scope = AphrontWriteGuard::isGuardActive();
if ($use_scope) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
} else {
AphrontWriteGuard::allowDangerousUnguardedWrites(true);
}
$caught = null;
try {
$file = call_user_func(
array($file_class, 'newFromFileData'),
$data,
array(
'mime-type' => 'application/xhprof',
'name' => 'profile.xhprof',
));
} catch (Exception $ex) {
$caught = $ex;
}
if ($use_scope) {
unset($unguarded);
} else {
AphrontWriteGuard::allowDangerousUnguardedWrites(false);
}
if ($caught) {
throw $caught;
} else {
return $file->getPHID();
}
} else {
return null;
}
}
}
diff --git a/src/aphront/exception/AphrontCSRFException.php b/src/aphront/exception/AphrontCSRFException.php
index f84985f98d..58e00f0b61 100644
--- a/src/aphront/exception/AphrontCSRFException.php
+++ b/src/aphront/exception/AphrontCSRFException.php
@@ -1,21 +1,5 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontCSRFException extends AphrontException {
}
diff --git a/src/aphront/exception/AphrontException.php b/src/aphront/exception/AphrontException.php
index 6918acf552..62a80e98cc 100644
--- a/src/aphront/exception/AphrontException.php
+++ b/src/aphront/exception/AphrontException.php
@@ -1,21 +1,5 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class AphrontException extends Exception {
}
diff --git a/src/aphront/exception/AphrontRedirectException.php b/src/aphront/exception/AphrontRedirectException.php
index f144b52f3f..f7b2a5fd42 100644
--- a/src/aphront/exception/AphrontRedirectException.php
+++ b/src/aphront/exception/AphrontRedirectException.php
@@ -1,34 +1,18 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* TODO: Remove this entirely? We have no callsites.
*/
final class AphrontRedirectException extends AphrontException {
private $uri;
public function __construct($uri) {
$this->uri = $uri;
}
public function getURI() {
return $this->uri;
}
}
diff --git a/src/aphront/exception/AphrontUsageException.php b/src/aphront/exception/AphrontUsageException.php
index 3fe1366cb7..d242b56dea 100644
--- a/src/aphront/exception/AphrontUsageException.php
+++ b/src/aphront/exception/AphrontUsageException.php
@@ -1,37 +1,21 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* These exceptions represent user error, and are not logged.
*
* @concrete-extensible
*/
class AphrontUsageException extends AphrontException {
private $title;
public function __construct($title, $message) {
$this->title = $title;
parent::__construct($message);
}
public function getTitle() {
return $this->title;
}
}
diff --git a/src/aphront/response/Aphront304Response.php b/src/aphront/response/Aphront304Response.php
index 3055d57bf9..4a1d93c773 100644
--- a/src/aphront/response/Aphront304Response.php
+++ b/src/aphront/response/Aphront304Response.php
@@ -1,35 +1,19 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group aphront
*/
final class Aphront304Response extends AphrontResponse {
public function getHTTPResponseCode() {
return 304;
}
public function buildResponseString() {
// IMPORTANT! According to the HTTP/1.1 spec (RFC 2616) a 304 response
// "MUST NOT" have any content. Apache + Safari strongly agree, and
// completely flip out and you start getting 304s for no-cache pages.
return null;
}
}
diff --git a/src/aphront/response/Aphront400Response.php b/src/aphront/response/Aphront400Response.php
index 94c473ab55..92d7167d9d 100644
--- a/src/aphront/response/Aphront400Response.php
+++ b/src/aphront/response/Aphront400Response.php
@@ -1,32 +1,16 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group aphront
*/
final class Aphront400Response extends AphrontResponse {
public function getHTTPResponseCode() {
return 400;
}
public function buildResponseString() {
return '400 Bad Request';
}
}
diff --git a/src/aphront/response/Aphront403Response.php b/src/aphront/response/Aphront403Response.php
index 94fde842c5..f2eafd719d 100644
--- a/src/aphront/response/Aphront403Response.php
+++ b/src/aphront/response/Aphront403Response.php
@@ -1,55 +1,39 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group aphront
*/
final class Aphront403Response extends AphrontWebpageResponse {
private $forbiddenText;
public function setForbiddenText($text) {
$this->forbiddenText = $text;
return $this;
}
private function getForbiddenText() {
return $this->forbiddenText;
}
public function getHTTPResponseCode() {
return 403;
}
public function buildResponseString() {
$forbidden_text = $this->getForbiddenText();
if (!$forbidden_text) {
$forbidden_text =
'You do not have privileges to access the requested page.';
}
$failure = new AphrontRequestFailureView();
$failure->setHeader('403 Forbidden');
$failure->appendChild('<p>'.$forbidden_text.'</p>');
$view = new PhabricatorStandardPageView();
$view->setTitle('403 Forbidden');
$view->setRequest($this->getRequest());
$view->appendChild($failure);
return $view->render();
}
}
diff --git a/src/aphront/response/Aphront404Response.php b/src/aphront/response/Aphront404Response.php
index d887ee7e72..69b3603b3f 100644
--- a/src/aphront/response/Aphront404Response.php
+++ b/src/aphront/response/Aphront404Response.php
@@ -1,41 +1,25 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group aphront
*/
final class Aphront404Response extends AphrontWebpageResponse {
public function getHTTPResponseCode() {
return 404;
}
public function buildResponseString() {
$failure = new AphrontRequestFailureView();
$failure->setHeader('404 Not Found');
$failure->appendChild('<p>The page you requested was not found.</p>');
$view = new PhabricatorStandardPageView();
$view->setTitle('404 Not Found');
$view->setRequest($this->getRequest());
$view->appendChild($failure);
return $view->render();
}
}
diff --git a/src/aphront/response/AphrontAjaxResponse.php b/src/aphront/response/AphrontAjaxResponse.php
index 0aab0ffdf2..ff2dc2a9de 100644
--- a/src/aphront/response/AphrontAjaxResponse.php
+++ b/src/aphront/response/AphrontAjaxResponse.php
@@ -1,81 +1,65 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group aphront
*/
final class AphrontAjaxResponse extends AphrontResponse {
private $content;
private $error;
private $disableConsole;
public function setContent($content) {
$this->content = $content;
return $this;
}
public function setError($error) {
$this->error = $error;
return $this;
}
public function setDisableConsole($disable) {
$this->disableConsole = $disable;
return $this;
}
private function getConsole() {
if ($this->disableConsole) {
$console = null;
} else {
$request = $this->getRequest();
$console = $request->getApplicationConfiguration()->getConsole();
}
return $console;
}
public function buildResponseString() {
$console = $this->getConsole();
if ($console) {
Javelin::initBehavior(
'dark-console-ajax',
array(
'console' => $console->render($this->getRequest()),
'uri' => (string) $this->getRequest()->getRequestURI(),
));
}
$response = CelerityAPI::getStaticResourceResponse();
$object = $response->buildAjaxResponse(
$this->content,
$this->error);
$response_json = $this->encodeJSONForHTTPResponse($object);
return $this->addJSONShield($response_json);
}
public function getHeaders() {
$headers = array(
array('Content-Type', 'text/plain; charset=UTF-8'),
);
$headers = array_merge(parent::getHeaders(), $headers);
return $headers;
}
}
diff --git a/src/aphront/response/AphrontDialogResponse.php b/src/aphront/response/AphrontDialogResponse.php
index eab1201518..b19fca9cee 100644
--- a/src/aphront/response/AphrontDialogResponse.php
+++ b/src/aphront/response/AphrontDialogResponse.php
@@ -1,35 +1,19 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group aphront
*/
final class AphrontDialogResponse extends AphrontResponse {
private $dialog;
public function setDialog(AphrontDialogView $dialog) {
$this->dialog = $dialog;
return $this;
}
public function buildResponseString() {
return $this->dialog->render();
}
}
diff --git a/src/aphront/response/AphrontFileResponse.php b/src/aphront/response/AphrontFileResponse.php
index 62d8ab5871..1d9e0e3337 100644
--- a/src/aphront/response/AphrontFileResponse.php
+++ b/src/aphront/response/AphrontFileResponse.php
@@ -1,78 +1,62 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group aphront
*/
final class AphrontFileResponse extends AphrontResponse {
private $content;
private $mimeType;
private $download;
public function setDownload($download) {
$download = preg_replace('/[^A-Za-z0-9_.-]/', '_', $download);
if (!strlen($download)) {
$download = 'untitled_document.txt';
}
$this->download = $download;
return $this;
}
public function getDownload() {
return $this->download;
}
public function setMimeType($mime_type) {
$this->mimeType = $mime_type;
return $this;
}
public function getMimeType() {
return $this->mimeType;
}
public function setContent($content) {
$this->content = $content;
return $this;
}
public function buildResponseString() {
return $this->content;
}
public function getHeaders() {
$headers = array(
array('Content-Type', $this->getMimeType()),
);
if (strlen($this->getDownload())) {
$headers[] = array('X-Download-Options', 'noopen');
$filename = $this->getDownload();
$headers[] = array(
'Content-Disposition',
'attachment; filename='.$filename,
);
}
$headers = array_merge(parent::getHeaders(), $headers);
return $headers;
}
}
diff --git a/src/aphront/response/AphrontJSONResponse.php b/src/aphront/response/AphrontJSONResponse.php
index 5e820cc5ed..f5666db642 100644
--- a/src/aphront/response/AphrontJSONResponse.php
+++ b/src/aphront/response/AphrontJSONResponse.php
@@ -1,59 +1,43 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group aphront
*/
final class AphrontJSONResponse extends AphrontResponse {
private $content;
private $addJSONShield;
public function setContent($content) {
$this->content = $content;
return $this;
}
public function setAddJSONShield($should_add) {
$this->addJSONShield = $should_add;
return $this;
}
public function shouldAddJSONShield() {
if ($this->addJSONShield === null) {
return true;
}
return (bool) $this->addJSONShield;
}
public function buildResponseString() {
$response = $this->encodeJSONForHTTPResponse($this->content);
if ($this->shouldAddJSONShield()) {
$response = $this->addJSONShield($response);
}
return $response;
}
public function getHeaders() {
$headers = array(
array('Content-Type', 'application/json'),
);
$headers = array_merge(parent::getHeaders(), $headers);
return $headers;
}
}
diff --git a/src/aphront/response/AphrontPlainTextResponse.php b/src/aphront/response/AphrontPlainTextResponse.php
index a5906a318c..ac8fe3c236 100644
--- a/src/aphront/response/AphrontPlainTextResponse.php
+++ b/src/aphront/response/AphrontPlainTextResponse.php
@@ -1,41 +1,25 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group aphront
*/
final class AphrontPlainTextResponse extends AphrontResponse {
public function setContent($content) {
$this->content = $content;
return $this;
}
public function buildResponseString() {
return $this->content;
}
public function getHeaders() {
$headers = array(
array('Content-Type', 'text/plain'),
);
return array_merge(parent::getHeaders(), $headers);
}
}
diff --git a/src/aphront/response/AphrontProxyResponse.php b/src/aphront/response/AphrontProxyResponse.php
index afe97604e4..a644c45a9b 100644
--- a/src/aphront/response/AphrontProxyResponse.php
+++ b/src/aphront/response/AphrontProxyResponse.php
@@ -1,83 +1,67 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Base class for responses which augment other types of responses. For example,
* a response might be substantially an Ajax response, but add structure to the
* response content. It can do this by extending @{class:AphrontProxyResponse},
* instantiating an @{class:AphrontAjaxResponse} in @{method:buildProxy}, and
* then using the proxy to construct the response string in
* @{method:buildResponseString}.
*
* @group aphront
*/
abstract class AphrontProxyResponse extends AphrontResponse {
private $proxy;
protected function getProxy() {
if (!$this->proxy) {
$this->proxy = $this->buildProxy();
}
return $this->proxy;
}
public function setRequest($request) {
$this->getProxy()->setRequest($request);
return $this;
}
public function getRequest() {
return $this->getProxy()->getRequest();
}
public function getHeaders() {
return $this->getProxy()->getHeaders();
}
public function setCacheDurationInSeconds($duration) {
$this->getProxy()->setCacheDurationInSeconds($duration);
return $this;
}
public function setLastModified($epoch_timestamp) {
$this->getProxy()->setLastModified($epoch_timestamp);
return $this;
}
public function setHTTPResponseCode($code) {
$this->getProxy()->setHTTPResponseCode($code);
return $this;
}
public function getHTTPResponseCode() {
return $this->getProxy()->getHTTPResponseCode();
}
public function setFrameable($frameable) {
$this->getProxy()->setFrameable($frameable);
return $this;
}
public function getCacheHeaders() {
return $this->getProxy()->getCacheHeaders();
}
abstract protected function buildProxy();
}
diff --git a/src/aphront/response/AphrontRedirectResponse.php b/src/aphront/response/AphrontRedirectResponse.php
index 7d060bb16b..bb34a97d5b 100644
--- a/src/aphront/response/AphrontRedirectResponse.php
+++ b/src/aphront/response/AphrontRedirectResponse.php
@@ -1,81 +1,65 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* TODO: Should be final but isn't because of AphrontReloadResponse.
*
* @group aphront
*/
class AphrontRedirectResponse extends AphrontResponse {
private $uri;
public function setURI($uri) {
$this->uri = $uri;
return $this;
}
public function getURI() {
return (string)$this->uri;
}
public function shouldStopForDebugging() {
return PhabricatorEnv::getEnvConfig('debug.stop-on-redirect');
}
public function getHeaders() {
$headers = array();
if (!$this->shouldStopForDebugging()) {
$headers[] = array('Location', $this->uri);
}
$headers = array_merge(parent::getHeaders(), $headers);
return $headers;
}
public function buildResponseString() {
if ($this->shouldStopForDebugging()) {
$view = new PhabricatorStandardPageView();
$view->setRequest($this->getRequest());
$view->setApplicationName('Debug');
$view->setTitle('Stopped on Redirect');
$error = new AphrontErrorView();
$error->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
$error->setTitle('Stopped on Redirect');
$link = phutil_render_tag(
'a',
array(
'href' => $this->getURI(),
),
'Continue to: '.phutil_escape_html($this->getURI()));
$error->appendChild(
'<p>You were stopped here because <tt>debug.stop-on-redirect</tt> '.
'is set in your configuration.</p>'.
'<p>'.$link.'</p>');
$view->appendChild($error);
return $view->render();
}
return '';
}
}
diff --git a/src/aphront/response/AphrontReloadResponse.php b/src/aphront/response/AphrontReloadResponse.php
index d32151ee67..2b16512620 100644
--- a/src/aphront/response/AphrontReloadResponse.php
+++ b/src/aphront/response/AphrontReloadResponse.php
@@ -1,37 +1,21 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* When actions happen over a JX.Workflow, we may want to reload the page
* if the action is javascript-driven but redirect if it isn't. This preserves
* query parameters in the javascript case. A reload response behaves like
* a redirect response but causes a page reload when received via workflow.
*
* @group aphront
*/
final class AphrontReloadResponse extends AphrontRedirectResponse {
public function getURI() {
if ($this->getRequest()->isAjax()) {
return null;
} else {
return parent::getURI();
}
}
}
diff --git a/src/aphront/response/AphrontResponse.php b/src/aphront/response/AphrontResponse.php
index 824bba2efa..291c493c59 100644
--- a/src/aphront/response/AphrontResponse.php
+++ b/src/aphront/response/AphrontResponse.php
@@ -1,143 +1,127 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group aphront
*/
abstract class AphrontResponse {
private $request;
private $cacheable = false;
private $responseCode = 200;
private $lastModified = null;
protected $frameable;
public function setRequest($request) {
$this->request = $request;
return $this;
}
public function getRequest() {
return $this->request;
}
public function getHeaders() {
$headers = array();
if (!$this->frameable) {
$headers[] = array('X-Frame-Options', 'Deny');
}
return $headers;
}
public function setCacheDurationInSeconds($duration) {
$this->cacheable = $duration;
return $this;
}
public function setLastModified($epoch_timestamp) {
$this->lastModified = $epoch_timestamp;
return $this;
}
public function setHTTPResponseCode($code) {
$this->responseCode = $code;
return $this;
}
public function getHTTPResponseCode() {
return $this->responseCode;
}
public function setFrameable($frameable) {
$this->frameable = $frameable;
return $this;
}
protected function encodeJSONForHTTPResponse(array $object) {
$response = json_encode($object);
// Prevent content sniffing attacks by encoding "<" and ">", so browsers
// won't try to execute the document as HTML even if they ignore
// Content-Type and X-Content-Type-Options. See T865.
$response = str_replace(
array('<', '>'),
array('\u003c', '\u003e'),
$response);
return $response;
}
protected function addJSONShield($json_response) {
// Add a shield to prevent "JSON Hijacking" attacks where an attacker
// requests a JSON response using a normal <script /> tag and then uses
// Object.prototype.__defineSetter__() or similar to read response data.
// This header causes the browser to loop infinitely instead of handing over
// sensitive data.
$shield = 'for (;;);';
$response = $shield.$json_response;
return $response;
}
public function getCacheHeaders() {
$headers = array();
if ($this->cacheable) {
$headers[] = array(
'Expires',
$this->formatEpochTimestampForHTTPHeader(time() + $this->cacheable));
} else {
$headers[] = array(
'Cache-Control',
'private, no-cache, no-store, must-revalidate');
$headers[] = array(
'Pragma',
'no-cache');
$headers[] = array(
'Expires',
'Sat, 01 Jan 2000 00:00:00 GMT');
}
if ($this->lastModified) {
$headers[] = array(
'Last-Modified',
$this->formatEpochTimestampForHTTPHeader($this->lastModified));
}
// IE has a feature where it may override an explicit Content-Type
// declaration by inferring a content type. This can be a security risk
// and we always explicitly transmit the correct Content-Type header, so
// prevent IE from using inferred content types. This only offers protection
// on recent versions of IE; IE6/7 and Opera currently ignore this header.
$headers[] = array('X-Content-Type-Options', 'nosniff');
return $headers;
}
private function formatEpochTimestampForHTTPHeader($epoch_timestamp) {
return gmdate('D, d M Y H:i:s', $epoch_timestamp).' GMT';
}
abstract public function buildResponseString();
}
diff --git a/src/aphront/response/AphrontWebpageResponse.php b/src/aphront/response/AphrontWebpageResponse.php
index c94b400357..e800d1122f 100644
--- a/src/aphront/response/AphrontWebpageResponse.php
+++ b/src/aphront/response/AphrontWebpageResponse.php
@@ -1,45 +1,29 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* TODO: Should be final, but isn't because of Aphront403Response / 404Response.
*
* @group aphront
*/
class AphrontWebpageResponse extends AphrontResponse {
private $content;
public function setContent($content) {
$this->content = $content;
return $this;
}
public function buildResponseString() {
return $this->content;
}
public function getHeaders() {
$headers = array(
array('Content-Type', 'text/html; charset=UTF-8'),
);
$headers = array_merge(parent::getHeaders(), $headers);
return $headers;
}
}
diff --git a/src/aphront/sink/AphrontHTTPSink.php b/src/aphront/sink/AphrontHTTPSink.php
index 63493bdf09..88773cdff0 100644
--- a/src/aphront/sink/AphrontHTTPSink.php
+++ b/src/aphront/sink/AphrontHTTPSink.php
@@ -1,131 +1,115 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Abstract class which wraps some sort of output mechanism for HTTP responses.
* Normally this is just @{class:AphrontPHPHTTPSink}, which uses "echo" and
* "header()" to emit responses.
*
* Mostly, this class allows us to do install security or metrics hooks in the
* output pipeline.
*
* @task write Writing Response Components
* @task emit Emitting the Response
*
* @group aphront
*/
abstract class AphrontHTTPSink {
/* -( Writing Response Components )---------------------------------------- */
/**
* Write an HTTP status code to the output.
*
* @param int Numeric HTTP status code.
* @return void
*/
final public function writeHTTPStatus($code) {
if (!preg_match('/^\d{3}$/', $code)) {
throw new Exception("Malformed HTTP status code '{$code}'!");
}
$code = (int)$code;
$this->emitHTTPStatus($code);
}
/**
* Write HTTP headers to the output.
*
* @param list<pair> List of <name, value> pairs.
* @return void
*/
final public function writeHeaders(array $headers) {
foreach ($headers as $header) {
if (!is_array($header) || count($header) !== 2) {
throw new Exception('Malformed header.');
}
list($name, $value) = $header;
if (strpos($name, ':') !== false) {
throw new Exception(
"Declining to emit response with malformed HTTP header name: ".
$name);
}
// Attackers may perform an "HTTP response splitting" attack by making
// the application emit certain types of headers containing newlines:
//
// http://en.wikipedia.org/wiki/HTTP_response_splitting
//
// PHP has built-in protections against HTTP response-splitting, but they
// are of dubious trustworthiness:
//
// http://news.php.net/php.internals/57655
if (preg_match('/[\r\n\0]/', $name.$value)) {
throw new Exception(
"Declining to emit response with unsafe HTTP header: ".
"<'".$name."', '".$value."'>.");
}
}
foreach ($headers as $header) {
list($name, $value) = $header;
$this->emitHeader($name, $value);
}
}
/**
* Write HTTP body data to the output.
*
* @param string Body data.
* @return void
*/
final public function writeData($data) {
$this->emitData($data);
}
/**
* Write an entire @{class:AphrontResponse} to the output.
*
* @param AphrontResponse The response object to write.
* @return void
*/
final public function writeResponse(AphrontResponse $response) {
$all_headers = array_merge(
$response->getHeaders(),
$response->getCacheHeaders());
$this->writeHTTPStatus($response->getHTTPResponseCode());
$this->writeHeaders($all_headers);
$this->writeData($response->buildResponseString());
}
/* -( Emitting the Response )---------------------------------------------- */
abstract protected function emitHTTPStatus($code);
abstract protected function emitHeader($name, $value);
abstract protected function emitData($data);
}
diff --git a/src/aphront/sink/AphrontIsolatedHTTPSink.php b/src/aphront/sink/AphrontIsolatedHTTPSink.php
index 0f4b16df3a..ced5536553 100644
--- a/src/aphront/sink/AphrontIsolatedHTTPSink.php
+++ b/src/aphront/sink/AphrontIsolatedHTTPSink.php
@@ -1,54 +1,38 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Isolated HTTP sink for testing.
*
* @group aphront
*/
final class AphrontIsolatedHTTPSink extends AphrontHTTPSink {
private $status;
private $headers;
private $data;
protected function emitHTTPStatus($code) {
$this->status = $code;
}
protected function emitHeader($name, $value) {
$this->headers[] = array($name, $value);
}
protected function emitData($data) {
$this->data .= $data;
}
public function getEmittedHTTPStatus() {
return $this->status;
}
public function getEmittedHeaders() {
return $this->headers;
}
public function getEmittedData() {
return $this->data;
}
}
diff --git a/src/aphront/sink/AphrontPHPHTTPSink.php b/src/aphront/sink/AphrontPHPHTTPSink.php
index beabf17831..d94e2b6558 100644
--- a/src/aphront/sink/AphrontPHPHTTPSink.php
+++ b/src/aphront/sink/AphrontPHPHTTPSink.php
@@ -1,40 +1,24 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Concrete HTTP sink which uses "echo" and "header()" to emit data.
*
* @group aphront
*/
final class AphrontPHPHTTPSink extends AphrontHTTPSink {
protected function emitHTTPStatus($code) {
if ($code != 200) {
header("HTTP/1.0 {$code}");
}
}
protected function emitHeader($name, $value) {
header("{$name}: {$value}", $replace = false);
}
protected function emitData($data) {
echo $data;
}
}
diff --git a/src/aphront/sink/__tests__/AphrontHTTPSinkTestCase.php b/src/aphront/sink/__tests__/AphrontHTTPSinkTestCase.php
index 512ca2542f..4892de96c6 100644
--- a/src/aphront/sink/__tests__/AphrontHTTPSinkTestCase.php
+++ b/src/aphront/sink/__tests__/AphrontHTTPSinkTestCase.php
@@ -1,98 +1,82 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontHTTPSinkTestCase extends PhabricatorTestCase {
public function testHTTPSinkBasics() {
$sink = new AphrontIsolatedHTTPSink();
$sink->writeHTTPStatus(200);
$sink->writeHeaders(array(array('X-Test', 'test')));
$sink->writeData('test');
$this->assertEqual(200, $sink->getEmittedHTTPStatus());
$this->assertEqual(
array(array('X-Test', 'test')),
$sink->getEmittedHeaders());
$this->assertEqual('test', $sink->getEmittedData());
}
public function testHTTPSinkStatusCode() {
$input = $this->tryTestCaseMap(
array(
200 => true,
'201' => true,
1 => false,
1000 => false,
'apple' => false,
'' => false,
),
array($this, 'tryHTTPSinkStatusCode'));
}
protected function tryHTTPSinkStatusCode($input) {
$sink = new AphrontIsolatedHTTPSink();
$sink->writeHTTPStatus($input);
}
public function testHTTPSinkResponseSplitting() {
$input = $this->tryTestCaseMap(
array(
"test" => true,
"test\nx" => false,
"test\rx" => false,
"test\0x" => false,
),
array($this, 'tryHTTPSinkResponseSplitting'));
}
protected function tryHTTPSinkResponseSplitting($input) {
$sink = new AphrontIsolatedHTTPSink();
$sink->writeHeaders(array(array('X-Test', $input)));
}
public function testHTTPHeaderNames() {
$input = $this->tryTestCaseMap(
array(
'test' => true,
'test:' => false,
),
array($this, 'tryHTTPHeaderNames'));
}
protected function tryHTTPHeaderNames($input) {
$sink = new AphrontIsolatedHTTPSink();
$sink->writeHeaders(array(array($input, 'value')));
}
public function testJSONContentSniff() {
$response = id(new AphrontJSONResponse())
->setContent(
array(
'x' => '<iframe>',
));
$sink = new AphrontIsolatedHTTPSink();
$sink->writeResponse($response);
$this->assertEqual(
'for (;;);{"x":"\u003ciframe\u003e"}',
$sink->getEmittedData(),
"JSONResponse should prevent content-sniffing attacks.");
}
}
diff --git a/src/applications/audit/PhabricatorAuditReplyHandler.php b/src/applications/audit/PhabricatorAuditReplyHandler.php
index c9dd80ea3a..c7244d0c04 100644
--- a/src/applications/audit/PhabricatorAuditReplyHandler.php
+++ b/src/applications/audit/PhabricatorAuditReplyHandler.php
@@ -1,70 +1,54 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group audit
*/
final class PhabricatorAuditReplyHandler extends PhabricatorMailReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof PhabricatorRepositoryCommit)) {
throw new Exception("Mail receiver is not a commit!");
}
}
public function getPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle) {
return $this->getDefaultPrivateReplyHandlerEmailAddress($handle, 'C');
}
public function getPublicReplyHandlerEmailAddress() {
return $this->getDefaultPublicReplyHandlerEmailAddress('C');
}
public function getReplyHandlerDomain() {
return PhabricatorEnv::getEnvConfig(
'metamta.diffusion.reply-handler-domain');
}
public function getReplyHandlerInstructions() {
if ($this->supportsReplies()) {
return "Reply to comment.";
} else {
return null;
}
}
protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
$commit = $this->getMailReceiver();
$actor = $this->getActor();
// TODO: Support !raise, !accept, etc.
// TODO: Content sources.
$comment = id(new PhabricatorAuditComment())
->setAction(PhabricatorAuditActionConstants::COMMENT)
->setContent($mail->getCleanTextBody());
$editor = new PhabricatorAuditCommentEditor($commit);
$editor->setActor($actor);
$editor->setExcludeMailRecipientPHIDs(
$this->getExcludeMailRecipientPHIDs());
$editor->addComment($comment);
}
}
diff --git a/src/applications/audit/application/PhabricatorApplicationAudit.php b/src/applications/audit/application/PhabricatorApplicationAudit.php
index 5c089a42a6..20bf2ecdf3 100644
--- a/src/applications/audit/application/PhabricatorApplicationAudit.php
+++ b/src/applications/audit/application/PhabricatorApplicationAudit.php
@@ -1,96 +1,80 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationAudit extends PhabricatorApplication {
public function getShortDescription() {
return 'Audit Code';
}
public function getBaseURI() {
return '/audit/';
}
public function getAutospriteName() {
return 'audit';
}
public function getHelpURI() {
return PhabricatorEnv::getDoclink('article/Audit_User_Guide.html');
}
public function getRoutes() {
return array(
'/audit/' => array(
'' => 'PhabricatorAuditListController',
'view/(?P<filter>[^/]+)/(?:(?P<name>[^/]+)/)?'
=> 'PhabricatorAuditListController',
'addcomment/' => 'PhabricatorAuditAddCommentController',
'preview/(?P<id>[1-9]\d*)/' => 'PhabricatorAuditPreviewController',
),
);
}
public function getApplicationGroup() {
return self::GROUP_CORE;
}
public function getApplicationOrder() {
return 0.130;
}
public function loadStatus(PhabricatorUser $user) {
$status = array();
$phids = PhabricatorAuditCommentEditor::loadAuditPHIDsForUser($user);
$audits = id(new PhabricatorAuditQuery())
->withAuditorPHIDs($phids)
->withStatus(PhabricatorAuditQuery::STATUS_OPEN)
->withAwaitingUser($user)
->execute();
$count = count($audits);
$type = $count
? PhabricatorApplicationStatusView::TYPE_INFO
: PhabricatorApplicationStatusView::TYPE_EMPTY;
$status[] = id(new PhabricatorApplicationStatusView())
->setType($type)
->setText(pht('%d Commit(s) Awaiting Audit', $count))
->setCount($count);
$commits = id(new PhabricatorAuditCommitQuery())
->withAuthorPHIDs($phids)
->withStatus(PhabricatorAuditQuery::STATUS_OPEN)
->execute();
$count = count($commits);
$type = $count
? PhabricatorApplicationStatusView::TYPE_NEEDS_ATTENTION
: PhabricatorApplicationStatusView::TYPE_EMPTY;
$status[] = id(new PhabricatorApplicationStatusView())
->setType($type)
->setText(pht('%d Problem Commit(s)', $count))
->setCount($count);
return $status;
}
}
diff --git a/src/applications/audit/constants/PhabricatorAuditActionConstants.php b/src/applications/audit/constants/PhabricatorAuditActionConstants.php
index 5a64912ebe..c59a56374e 100644
--- a/src/applications/audit/constants/PhabricatorAuditActionConstants.php
+++ b/src/applications/audit/constants/PhabricatorAuditActionConstants.php
@@ -1,61 +1,45 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorAuditActionConstants {
const CONCERN = 'concern';
const ACCEPT = 'accept';
const COMMENT = 'comment';
const RESIGN = 'resign';
const CLOSE = 'close';
const ADD_CCS = 'add_ccs';
const ADD_AUDITORS = 'add_auditors';
public static function getActionNameMap() {
static $map = array(
self::COMMENT => 'Comment',
self::CONCERN => "Raise Concern \xE2\x9C\x98",
self::ACCEPT => "Accept Commit \xE2\x9C\x94",
self::RESIGN => 'Resign from Audit',
self::CLOSE => 'Close Audit',
self::ADD_CCS => 'Add CCs',
self::ADD_AUDITORS => 'Add Auditors',
);
return $map;
}
public static function getActionName($constant) {
$map = self::getActionNameMap();
return idx($map, $constant, 'Unknown');
}
public static function getActionPastTenseVerb($action) {
static $map = array(
self::COMMENT => 'commented on',
self::CONCERN => 'raised a concern with',
self::ACCEPT => 'accepted',
self::RESIGN => 'resigned from',
self::CLOSE => 'closed',
self::ADD_CCS => 'added CCs to',
self::ADD_AUDITORS => 'added auditors to',
);
return idx($map, $action, 'updated');
}
}
diff --git a/src/applications/audit/constants/PhabricatorAuditCommitStatusConstants.php b/src/applications/audit/constants/PhabricatorAuditCommitStatusConstants.php
index 27c31177a4..c052a6271d 100644
--- a/src/applications/audit/constants/PhabricatorAuditCommitStatusConstants.php
+++ b/src/applications/audit/constants/PhabricatorAuditCommitStatusConstants.php
@@ -1,43 +1,27 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorAuditCommitStatusConstants {
const NONE = 0;
const NEEDS_AUDIT = 1;
const CONCERN_RAISED = 2;
const PARTIALLY_AUDITED = 3;
const FULLY_AUDITED = 4;
public static function getStatusNameMap() {
static $map = array(
self::NONE => 'None',
self::NEEDS_AUDIT => 'Audit Required',
self::CONCERN_RAISED => 'Concern Raised',
self::PARTIALLY_AUDITED => 'Partially Audited',
self::FULLY_AUDITED => 'Audited',
);
return $map;
}
public static function getStatusName($code) {
return idx(self::getStatusNameMap(), $code, 'Unknown');
}
}
diff --git a/src/applications/audit/constants/PhabricatorAuditStatusConstants.php b/src/applications/audit/constants/PhabricatorAuditStatusConstants.php
index b90c0542c1..152d257dd0 100644
--- a/src/applications/audit/constants/PhabricatorAuditStatusConstants.php
+++ b/src/applications/audit/constants/PhabricatorAuditStatusConstants.php
@@ -1,63 +1,47 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorAuditStatusConstants {
const NONE = '';
const AUDIT_NOT_REQUIRED = 'audit-not-required';
const AUDIT_REQUIRED = 'audit-required';
const CONCERNED = 'concerned';
const ACCEPTED = 'accepted';
const AUDIT_REQUESTED = 'requested';
const RESIGNED = 'resigned';
const CLOSED = 'closed';
const CC = 'cc';
public static function getStatusNameMap() {
static $map = array(
self::NONE => 'Not Applicable',
self::AUDIT_NOT_REQUIRED => 'Audit Not Required',
self::AUDIT_REQUIRED => 'Audit Required',
self::CONCERNED => 'Concern Raised',
self::ACCEPTED => 'Accepted',
self::AUDIT_REQUESTED => 'Audit Requested',
self::RESIGNED => 'Resigned',
self::CLOSED => 'Closed',
self::CC => "Was CC'd",
);
return $map;
}
public static function getStatusName($code) {
return idx(self::getStatusNameMap(), $code, 'Unknown');
}
public static function getOpenStatusConstants() {
return array(
self::AUDIT_REQUIRED,
self::AUDIT_REQUESTED,
self::CONCERNED,
);
}
public static function isOpenStatus($status) {
return in_array($status, self::getOpenStatusConstants());
}
}
diff --git a/src/applications/audit/controller/PhabricatorAuditAddCommentController.php b/src/applications/audit/controller/PhabricatorAuditAddCommentController.php
index 3fdb9f4b94..e5c47ee8c6 100644
--- a/src/applications/audit/controller/PhabricatorAuditAddCommentController.php
+++ b/src/applications/audit/controller/PhabricatorAuditAddCommentController.php
@@ -1,84 +1,68 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorAuditAddCommentController
extends PhabricatorAuditController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
if (!$request->isFormPost()) {
return new Aphront403Response();
}
$commit_phid = $request->getStr('commit');
$commit = id(new PhabricatorRepositoryCommit())->loadOneWhere(
'phid = %s',
$commit_phid);
if (!$commit) {
return new Aphront404Response();
}
$phids = array($commit_phid);
$action = $request->getStr('action');
$comment = id(new PhabricatorAuditComment())
->setAction($action)
->setContent($request->getStr('content'));
// make sure we only add auditors or ccs if the action matches
switch ($action) {
case 'add_auditors':
$auditors = $request->getArr('auditors');
$ccs = array();
break;
case 'add_ccs':
$auditors = array();
$ccs = $request->getArr('ccs');
break;
default:
$auditors = array();
$ccs = array();
break;
}
id(new PhabricatorAuditCommentEditor($commit))
->setActor($user)
->setAttachInlineComments(true)
->addAuditors($auditors)
->addCCs($ccs)
->addComment($comment);
$handles = $this->loadViewerHandles($phids);
$uri = $handles[$commit_phid]->getURI();
$draft = id(new PhabricatorDraft())->loadOneWhere(
'authorPHID = %s AND draftKey = %s',
$user->getPHID(),
'diffusion-audit-'.$commit->getID());
if ($draft) {
$draft->delete();
}
return id(new AphrontRedirectResponse())->setURI($uri);
}
}
diff --git a/src/applications/audit/controller/PhabricatorAuditController.php b/src/applications/audit/controller/PhabricatorAuditController.php
index c6503b3096..52bcdb7e26 100644
--- a/src/applications/audit/controller/PhabricatorAuditController.php
+++ b/src/applications/audit/controller/PhabricatorAuditController.php
@@ -1,35 +1,19 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorAuditController extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('Audit');
$page->setBaseURI('/audit/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph("\xE2\x9C\x8D");
$page->appendChild($view);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
}
diff --git a/src/applications/audit/controller/PhabricatorAuditListController.php b/src/applications/audit/controller/PhabricatorAuditListController.php
index 49ce933013..bb0e740574 100644
--- a/src/applications/audit/controller/PhabricatorAuditListController.php
+++ b/src/applications/audit/controller/PhabricatorAuditListController.php
@@ -1,503 +1,487 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorAuditListController extends PhabricatorAuditController {
private $filter;
private $name;
private $filterStatus;
public function willProcessRequest(array $data) {
$this->filter = idx($data, 'filter');
$this->name = idx($data, 'name');
}
public function processRequest() {
$request = $this->getRequest();
$nav = $this->buildNavAndSelectFilter();
if ($request->isFormPost()) {
// If the list filter is POST'ed, redirect to GET so the page can be
// bookmarked.
$uri = $request->getRequestURI();
$phid = head($request->getArr('set_phid'));
$user = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$phid);
$uri = $request->getRequestURI();
if ($user) {
$username = phutil_escape_uri($user->getUsername());
$uri = '/audit/view/'.$this->filter.'/'.$username.'/';
} else if ($phid) {
$uri = $request->getRequestURI();
$uri = $uri->alter('phid', $phid);
}
return id(new AphrontRedirectResponse())->setURI($uri);
}
$this->filterStatus = $request->getStr('status', 'all');
$handle = $this->loadHandle();
$nav->appendChild($this->buildListFilters($handle));
$title = null;
$message = null;
if (!$handle) {
switch ($this->filter) {
case 'project':
$title = 'Choose A Project';
$message = 'Choose a project to view audits for.';
break;
case 'package':
case 'packagecommits':
$title = 'Choose a Package';
$message = 'Choose a package to view audits for.';
break;
}
}
if (!$message) {
$nav->appendChild($this->buildViews($handle));
} else {
$panel = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_NODATA)
->setTitle($title)
->appendChild($message);
$nav->appendChild($panel);
}
return $this->buildStandardPageResponse(
$nav,
array(
'title' => 'Audits',
));
}
private function buildNavAndSelectFilter() {
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI('/audit/view/'));
$nav->addLabel('Active');
$nav->addFilter('active', 'Need Attention');
$nav->addLabel('Audits');
$nav->addFilter('audits', 'All');
$nav->addFilter('user', 'By User');
$nav->addFilter('project', 'By Project');
$nav->addFilter('package', 'By Package');
$nav->addLabel('Commits');
$nav->addFilter('commits', 'All');
$nav->addFilter('author', 'By Author');
$nav->addFilter('packagecommits', 'By Package');
$this->filter = $nav->selectFilter($this->filter, 'active');
return $nav;
}
private function buildListFilters(PhabricatorObjectHandle $handle = null) {
$request = $this->getRequest();
$user = $request->getUser();
$form = new AphrontFormView();
$form->setUser($user);
$show_status = false;
$show_user = false;
$show_project = false;
$show_package = false;
switch ($this->filter) {
case 'audits':
case 'commits':
$show_status = true;
break;
case 'active':
$show_user = true;
break;
case 'author':
case 'user':
$show_user = true;
$show_status = true;
break;
case 'project':
$show_project = true;
$show_status = true;
break;
case 'package':
case 'packagecommits':
$show_package = true;
$show_status = true;
break;
}
if ($show_user || $show_project || $show_package) {
if ($show_user) {
$uri = '/typeahead/common/users/';
$label = 'User';
} else if ($show_project) {
$uri = '/typeahead/common/projects/';
$label = 'Project';
} else if ($show_package) {
$uri = '/typeahead/common/packages/';
$label = 'Package';
}
$tok_value = null;
if ($handle) {
$tok_value = array(
$handle->getPHID() => $handle->getFullName(),
);
}
$form->appendChild(
id(new AphrontFormTokenizerControl())
->setName('set_phid')
->setLabel($label)
->setLimit(1)
->setDatasource($uri)
->setValue($tok_value));
}
if ($show_status) {
$form->appendChild(
id(new AphrontFormToggleButtonsControl())
->setName('status')
->setLabel('Status')
->setBaseURI($request->getRequestURI(), 'status')
->setValue($this->filterStatus)
->setButtons(
array(
'all' => 'All',
'open' => 'Open',
)));
}
$form->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Filter Audits'));
$view = new AphrontListFilterView();
$view->appendChild($form);
return $view;
}
private function loadHandle() {
$request = $this->getRequest();
$default = null;
switch ($this->filter) {
case 'user':
case 'active':
case 'author':
$default = $request->getUser()->getPHID();
if ($this->name) {
$user = id(new PhabricatorUser())->loadOneWhere(
'username = %s',
$this->name);
if ($user) {
$default = $user->getPHID();
}
}
break;
}
$phid = $request->getStr('phid', $default);
if (!$phid) {
return null;
}
$phids = array($phid);
$handles = $this->loadViewerHandles($phids);
$handle = $handles[$phid];
$this->validateHandle($handle);
return $handle;
}
private function validateHandle(PhabricatorObjectHandle $handle) {
switch ($this->filter) {
case 'active':
case 'user':
case 'author':
if ($handle->getType() !== PhabricatorPHIDConstants::PHID_TYPE_USER) {
throw new Exception("PHID must be a user PHID!");
}
break;
case 'package':
case 'packagecommits':
if ($handle->getType() !== PhabricatorPHIDConstants::PHID_TYPE_OPKG) {
throw new Exception("PHID must be a package PHID!");
}
break;
case 'project':
if ($handle->getType() !== PhabricatorPHIDConstants::PHID_TYPE_PROJ) {
throw new Exception("PHID must be a project PHID!");
}
break;
case 'audits':
case 'commits':
break;
default:
throw new Exception("Unknown filter '{$this->filter}'!");
}
}
private function buildViews(PhabricatorObjectHandle $handle = null) {
$views = array();
switch ($this->filter) {
case 'active':
$views[] = $this->buildAuditView($handle);
$views[] = $this->buildCommitView($handle);
break;
case 'audits':
case 'user':
case 'package':
case 'project':
$views[] = $this->buildAuditView($handle);
break;
case 'commits':
case 'packagecommits':
case 'author':
$views[] = $this->buildCommitView($handle);
break;
}
return $views;
}
private function buildAuditView(PhabricatorObjectHandle $handle = null) {
$request = $this->getRequest();
$query = new PhabricatorAuditQuery();
$use_pager = ($this->filter != 'active');
if ($use_pager) {
$pager = new AphrontPagerView();
$pager->setURI($request->getRequestURI(), 'offset');
$pager->setOffset($request->getInt('offset'));
$query->setOffset($pager->getOffset());
$query->setLimit($pager->getPageSize() + 1);
}
$awaiting = null;
$phids = null;
switch ($this->filter) {
case 'user':
case 'active':
$obj = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$handle->getPHID());
if (!$obj) {
throw new Exception("Invalid user!");
}
$phids = PhabricatorAuditCommentEditor::loadAuditPHIDsForUser($obj);
$awaiting = $obj;
break;
case 'project':
case 'package':
$phids = array($handle->getPHID());
break;
case 'audits';
break;
default:
throw new Exception("Unknown filter!");
}
if ($phids) {
$query->withAuditorPHIDs($phids);
}
if ($awaiting) {
$query->withAwaitingUser($awaiting);
}
switch ($this->filter) {
case 'audits':
case 'user':
case 'project':
case 'package':
switch ($this->filterStatus) {
case 'open':
$query->withStatus(PhabricatorAuditQuery::STATUS_OPEN);
break;
}
break;
case 'active':
$query->withStatus(PhabricatorAuditQuery::STATUS_OPEN);
break;
}
if ($handle) {
$handle_name = phutil_escape_html($handle->getName());
} else {
$handle_name = null;
}
switch ($this->filter) {
case 'active':
$header = 'Required Audits';
$nodata = 'No commits require your audit.';
break;
case 'user':
$header = "Audits for {$handle_name}";
$nodata = "No matching audits by {$handle_name}.";
break;
case 'audits':
$header = "Audits";
$nodata = "No matching audits.";
break;
case 'project':
$header = "Audits in Project '{$handle_name}'";
$nodata = "No matching audits in project '{$handle_name}'.";
break;
case 'package':
$header = "Audits for Package '{$handle_name}'";
$nodata = "No matching audits in package '{$handle_name}'.";
break;
}
$query->needCommitData(true);
$audits = $query->execute();
if ($use_pager) {
$audits = $pager->sliceResults($audits);
}
$view = new PhabricatorAuditListView();
$view->setAudits($audits);
$view->setCommits($query->getCommits());
$view->setUser($request->getUser());
$view->setNoDataString($nodata);
$phids = $view->getRequiredHandlePHIDs();
$handles = $this->loadViewerHandles($phids);
$view->setHandles($handles);
$panel = new AphrontPanelView();
$panel->setHeader($header);
$panel->appendChild($view);
if ($use_pager) {
$panel->appendChild($pager);
}
return $panel;
}
private function buildCommitView(PhabricatorObjectHandle $handle = null) {
$request = $this->getRequest();
$query = new PhabricatorAuditCommitQuery();
$query->needCommitData(true);
$query->needAudits(true);
$use_pager = ($this->filter != 'active');
if ($use_pager) {
$pager = new AphrontPagerView();
$pager->setURI($request->getRequestURI(), 'offset');
$pager->setOffset($request->getInt('offset'));
$query->setOffset($pager->getOffset());
$query->setLimit($pager->getPageSize() + 1);
}
switch ($this->filter) {
case 'active':
case 'author':
$query->withAuthorPHIDs(array($handle->getPHID()));
break;
case 'packagecommits':
$query->withPackagePHIDs(array($handle->getPHID()));
break;
}
switch ($this->filter) {
case 'active':
$query->withStatus(PhabricatorAuditQuery::STATUS_OPEN);
break;
case 'author':
case 'packagecommits':
switch ($this->filterStatus) {
case 'open':
$query->withStatus(PhabricatorAuditQuery::STATUS_OPEN);
break;
}
break;
}
if ($handle) {
$handle_name = phutil_escape_html($handle->getName());
} else {
$handle_name = null;
}
switch ($this->filter) {
case 'active':
$header = 'Problem Commits';
$nodata = 'None of your commits have open concerns.';
break;
case 'author':
$header = "Commits by {$handle_name}";
$nodata = "No matching commits by {$handle_name}.";
break;
case 'commits':
$header = "Commits";
$nodata = "No matching commits.";
break;
case 'packagecommits':
$header = "Commits in Package '{$handle_name}'";
$nodata = "No matching commits in package '{$handle_name}'.";
break;
}
$commits = $query->execute();
if ($use_pager) {
$commits = $pager->sliceResults($commits);
}
$view = new PhabricatorAuditCommitListView();
$view->setUser($request->getUser());
$view->setCommits($commits);
$view->setNoDataString($nodata);
$phids = $view->getRequiredHandlePHIDs();
$handles = $this->loadViewerHandles($phids);
$view->setHandles($handles);
$panel = new AphrontPanelView();
$panel->setHeader($header);
$panel->appendChild($view);
if ($use_pager) {
$panel->appendChild($pager);
}
return $panel;
}
}
diff --git a/src/applications/audit/controller/PhabricatorAuditPreviewController.php b/src/applications/audit/controller/PhabricatorAuditPreviewController.php
index 98c853d688..e2d25e7c1f 100644
--- a/src/applications/audit/controller/PhabricatorAuditPreviewController.php
+++ b/src/applications/audit/controller/PhabricatorAuditPreviewController.php
@@ -1,92 +1,76 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorAuditPreviewController
extends PhabricatorAuditController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$commit = id(new PhabricatorRepositoryCommit())->load($this->id);
if (!$commit) {
return new Aphront404Response();
}
$action = $request->getStr('action');
$comment = id(new PhabricatorAuditComment())
->setActorPHID($user->getPHID())
->setTargetPHID($commit->getPHID())
->setAction($action)
->setContent($request->getStr('content'));
$phids = array(
$user->getPHID(),
$commit->getPHID(),
);
$auditors = $request->getStrList('auditors');
if ($action == PhabricatorAuditActionConstants::ADD_AUDITORS && $auditors) {
$comment->setMetadata(array(
PhabricatorAuditComment::METADATA_ADDED_AUDITORS => $auditors));
$phids = array_merge($phids, $auditors);
}
$ccs = $request->getStrList('ccs');
if ($action == PhabricatorAuditActionConstants::ADD_CCS && $ccs) {
$comment->setMetadata(array(
PhabricatorAuditComment::METADATA_ADDED_CCS => $ccs));
$phids = array_merge($phids, $ccs);
}
$engine = new PhabricatorMarkupEngine();
$engine->setViewer($user);
$engine->addObject(
$comment,
PhabricatorAuditComment::MARKUP_FIELD_BODY);
$engine->process();
$view = id(new DiffusionCommentView())
->setMarkupEngine($engine)
->setUser($user)
->setComment($comment)
->setIsPreview(true);
$phids = array_merge($phids, $view->getRequiredHandlePHIDs());
$handles = $this->loadViewerHandles($phids);
$view->setHandles($handles);
id(new PhabricatorDraft())
->setAuthorPHID($comment->getActorPHID())
->setDraftKey('diffusion-audit-'.$this->id)
->setDraft($comment->getContent())
->replaceOrDelete();
return id(new AphrontAjaxResponse())
->setContent($view->render());
}
}
diff --git a/src/applications/audit/editor/PhabricatorAuditCommentEditor.php b/src/applications/audit/editor/PhabricatorAuditCommentEditor.php
index 2cb45c1523..3f2845977e 100644
--- a/src/applications/audit/editor/PhabricatorAuditCommentEditor.php
+++ b/src/applications/audit/editor/PhabricatorAuditCommentEditor.php
@@ -1,527 +1,511 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorAuditCommentEditor extends PhabricatorEditor {
private $commit;
private $attachInlineComments;
private $auditors = array();
private $ccs = array();
public function __construct(PhabricatorRepositoryCommit $commit) {
$this->commit = $commit;
return $this;
}
public function addAuditors(array $auditor_phids) {
$this->auditors = array_merge($this->auditors, $auditor_phids);
return $this;
}
public function addCCs(array $cc_phids) {
$this->ccs = array_merge($this->ccs, $cc_phids);
return $this;
}
public function setAttachInlineComments($attach_inline_comments) {
$this->attachInlineComments = $attach_inline_comments;
return $this;
}
public function addComment(PhabricatorAuditComment $comment) {
$commit = $this->commit;
$actor = $this->getActor();
$other_comments = id(new PhabricatorAuditComment())->loadAllWhere(
'targetPHID = %s',
$commit->getPHID());
$inline_comments = array();
if ($this->attachInlineComments) {
$inline_comments = id(new PhabricatorAuditInlineComment())->loadAllWhere(
'authorPHID = %s AND commitPHID = %s
AND auditCommentID IS NULL',
$actor->getPHID(),
$commit->getPHID());
}
$comment
->setActorPHID($actor->getPHID())
->setTargetPHID($commit->getPHID())
->save();
$content_blocks = array($comment->getContent());
if ($inline_comments) {
foreach ($inline_comments as $inline) {
$inline->setAuditCommentID($comment->getID());
$inline->save();
$content_blocks[] = $inline->getContent();
}
}
$ccs = $this->ccs;
$auditors = $this->auditors;
$metadata = $comment->getMetadata();
$metacc = array();
// Find any "@mentions" in the content blocks.
$mention_ccs = PhabricatorMarkupEngine::extractPHIDsFromMentions(
$content_blocks);
if ($mention_ccs) {
$metacc = idx(
$metadata,
PhabricatorAuditComment::METADATA_ADDED_CCS,
array());
foreach ($mention_ccs as $cc_phid) {
$metacc[] = $cc_phid;
}
}
if ($metacc) {
$ccs = array_merge($ccs, $metacc);
}
// When an actor submits an audit comment, we update all the audit requests
// they have authority over to reflect the most recent status. The general
// idea here is that if audit has triggered for, e.g., several packages, but
// a user owns all of them, they can clear the audit requirement in one go
// without auditing the commit for each trigger.
$audit_phids = self::loadAuditPHIDsForUser($actor);
$audit_phids = array_fill_keys($audit_phids, true);
$requests = id(new PhabricatorRepositoryAuditRequest())
->loadAllWhere(
'commitPHID = %s',
$commit->getPHID());
$action = $comment->getAction();
// TODO: We should validate the action, currently we allow anyone to, e.g.,
// close an audit if they muck with form parameters. I'll followup with this
// and handle the no-effect cases (e.g., closing and already-closed audit).
$actor_is_author = ($actor->getPHID() == $commit->getAuthorPHID());
if ($action == PhabricatorAuditActionConstants::CLOSE) {
// "Close" means wipe out all the concerns.
$concerned_status = PhabricatorAuditStatusConstants::CONCERNED;
foreach ($requests as $request) {
if ($request->getAuditStatus() == $concerned_status) {
$request->setAuditStatus(PhabricatorAuditStatusConstants::CLOSED);
$request->save();
}
}
} else if ($action == PhabricatorAuditActionConstants::RESIGN) {
// "Resign" has unusual rules for writing user rows, only affects the
// user row (never package/project rows), and always affects the user
// row (other actions don't, if they were able to affect a package/project
// row).
$actor_request = null;
foreach ($requests as $request) {
if ($request->getAuditorPHID() == $actor->getPHID()) {
$actor_request = $request;
break;
}
}
if (!$actor_request) {
$actor_request = id(new PhabricatorRepositoryAuditRequest())
->setCommitPHID($commit->getPHID())
->setAuditorPHID($actor->getPHID())
->setAuditReasons(array("Resigned"));
}
$actor_request
->setAuditStatus(PhabricatorAuditStatusConstants::RESIGNED)
->save();
$requests[] = $actor_request;
} else {
$have_any_requests = false;
foreach ($requests as $request) {
if (empty($audit_phids[$request->getAuditorPHID()])) {
continue;
}
$request_is_for_actor =
($request->getAuditorPHID() == $actor->getPHID());
$have_any_requests = true;
$new_status = null;
switch ($action) {
case PhabricatorAuditActionConstants::COMMENT:
case PhabricatorAuditActionConstants::ADD_CCS:
case PhabricatorAuditActionConstants::ADD_AUDITORS:
// Commenting or adding cc's/auditors doesn't change status.
break;
case PhabricatorAuditActionConstants::ACCEPT:
if (!$actor_is_author || $request_is_for_actor) {
// When modifying your own commits, you act only on behalf of
// yourself, not your packages/projects -- the idea being that
// you can't accept your own commits.
$new_status = PhabricatorAuditStatusConstants::ACCEPTED;
}
break;
case PhabricatorAuditActionConstants::CONCERN:
if (!$actor_is_author || $request_is_for_actor) {
// See above.
$new_status = PhabricatorAuditStatusConstants::CONCERNED;
}
break;
default:
throw new Exception("Unknown action '{$action}'!");
}
if ($new_status !== null) {
$request->setAuditStatus($new_status);
$request->save();
}
}
// If the actor has no current authority over any audit trigger, make a
// new one to represent their audit state.
if (!$have_any_requests) {
$new_status = null;
switch ($action) {
case PhabricatorAuditActionConstants::COMMENT:
case PhabricatorAuditActionConstants::ADD_CCS:
case PhabricatorAuditActionConstants::ADD_AUDITORS:
$new_status = PhabricatorAuditStatusConstants::AUDIT_NOT_REQUIRED;
break;
case PhabricatorAuditActionConstants::ACCEPT:
$new_status = PhabricatorAuditStatusConstants::ACCEPTED;
break;
case PhabricatorAuditActionConstants::CONCERN:
$new_status = PhabricatorAuditStatusConstants::CONCERNED;
break;
case PhabricatorAuditActionConstants::CLOSE:
// Impossible to reach this block with 'close'.
default:
throw new Exception("Unknown or invalid action '{$action}'!");
}
$request = id(new PhabricatorRepositoryAuditRequest())
->setCommitPHID($commit->getPHID())
->setAuditorPHID($actor->getPHID())
->setAuditStatus($new_status)
->setAuditReasons(array("Voluntary Participant"))
->save();
$requests[] = $request;
}
}
$requests_by_auditor = mpull($requests, null, 'getAuditorPHID');
$requests_phids = array_keys($requests_by_auditor);
$ccs = array_diff($ccs, $requests_phids);
$auditors = array_diff($auditors, $requests_phids);
if ($action == PhabricatorAuditActionConstants::ADD_CCS) {
if ($ccs) {
$metadata[PhabricatorAuditComment::METADATA_ADDED_CCS] = $ccs;
$comment->setMetaData($metadata);
} else {
$comment->setAction(PhabricatorAuditActionConstants::COMMENT);
}
}
if ($action == PhabricatorAuditActionConstants::ADD_AUDITORS) {
if ($auditors) {
$metadata[PhabricatorAuditComment::METADATA_ADDED_AUDITORS]
= $auditors;
$comment->setMetaData($metadata);
} else {
$comment->setAction(PhabricatorAuditActionConstants::COMMENT);
}
}
$comment->save();
if ($auditors) {
foreach ($auditors as $auditor_phid) {
$audit_requested = PhabricatorAuditStatusConstants::AUDIT_REQUESTED;
$requests[] = id (new PhabricatorRepositoryAuditRequest())
->setCommitPHID($commit->getPHID())
->setAuditorPHID($auditor_phid)
->setAuditStatus($audit_requested)
->setAuditReasons(
array('Added by ' . $actor->getUsername()))
->save();
}
}
if ($ccs) {
foreach ($ccs as $cc_phid) {
$audit_cc = PhabricatorAuditStatusConstants::CC;
$requests[] = id (new PhabricatorRepositoryAuditRequest())
->setCommitPHID($commit->getPHID())
->setAuditorPHID($cc_phid)
->setAuditStatus($audit_cc)
->setAuditReasons(
array('Added by ' . $actor->getUsername()))
->save();
}
}
$commit->updateAuditStatus($requests);
$commit->save();
$this->publishFeedStory($comment, array_keys($audit_phids));
PhabricatorSearchCommitIndexer::indexCommit($commit);
$this->sendMail($comment, $other_comments, $inline_comments, $requests);
}
/**
* Load the PHIDs for all objects the user has the authority to act as an
* audit for. This includes themselves, and any packages they are an owner
* of.
*/
public static function loadAuditPHIDsForUser(PhabricatorUser $user) {
$phids = array();
// TODO: This method doesn't really use the right viewer, but in practice we
// never issue this query of this type on behalf of another user and are
// unlikely to do so in the future. This entire method should be refactored
// into a Query class, however, and then we should use a proper viewer.
// The user can audit on their own behalf.
$phids[$user->getPHID()] = true;
$owned_packages = id(new PhabricatorOwnersPackageQuery())
->setViewer($user)
->withOwnerPHIDs(array($user->getPHID()))
->execute();
foreach ($owned_packages as $package) {
$phids[$package->getPHID()] = true;
}
// The user can audit on behalf of all projects they are a member of.
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withMemberPHIDs(array($user->getPHID()))
->execute();
foreach ($projects as $project) {
$phids[$project->getPHID()] = true;
}
return array_keys($phids);
}
private function publishFeedStory(
PhabricatorAuditComment $comment,
array $more_phids) {
$commit = $this->commit;
$actor = $this->getActor();
$related_phids = array_merge(
array(
$actor->getPHID(),
$commit->getPHID(),
),
$more_phids);
id(new PhabricatorFeedStoryPublisher())
->setRelatedPHIDs($related_phids)
->setStoryAuthorPHID($actor->getPHID())
->setStoryTime(time())
->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_AUDIT)
->setStoryData(
array(
'commitPHID' => $commit->getPHID(),
'action' => $comment->getAction(),
'content' => $comment->getContent(),
))
->publish();
}
private function sendMail(
PhabricatorAuditComment $comment,
array $other_comments,
array $inline_comments,
array $requests) {
assert_instances_of($other_comments, 'PhabricatorAuditComment');
assert_instances_of($inline_comments, 'PhabricatorInlineCommentInterface');
$commit = $this->commit;
$data = $commit->loadCommitData();
$summary = $data->getSummary();
$commit_phid = $commit->getPHID();
$phids = array($commit_phid);
$handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
$handle = $handles[$commit_phid];
$name = $handle->getName();
$map = array(
PhabricatorAuditActionConstants::CONCERN => 'Raised Concern',
PhabricatorAuditActionConstants::ACCEPT => 'Accepted',
PhabricatorAuditActionConstants::RESIGN => 'Resigned',
PhabricatorAuditActionConstants::CLOSE => 'Closed',
PhabricatorAuditActionConstants::ADD_CCS => 'Added CCs',
PhabricatorAuditActionConstants::ADD_AUDITORS => 'Added Auditors',
);
$verb = idx($map, $comment->getAction(), 'Commented On');
$reply_handler = self::newReplyHandlerForCommit($commit);
$prefix = PhabricatorEnv::getEnvConfig('metamta.diffusion.subject-prefix');
$repository = id(new PhabricatorRepository())
->load($commit->getRepositoryID());
$threading = self::getMailThreading($repository, $commit);
list($thread_id, $thread_topic) = $threading;
$body = $this->renderMailBody(
$comment,
"{$name}: {$summary}",
$handle,
$reply_handler,
$inline_comments);
$email_to = array();
$email_cc = array();
$author_phid = $data->getCommitDetail('authorPHID');
if ($author_phid) {
$email_to[] = $author_phid;
}
$email_cc = array();
foreach ($other_comments as $other_comment) {
$email_cc[] = $other_comment->getActorPHID();
}
foreach ($requests as $request) {
if ($request->getAuditStatus() == PhabricatorAuditStatusConstants::CC) {
$email_cc[] = $request->getAuditorPHID();
}
}
$phids = array_merge($email_to, $email_cc);
$handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
// NOTE: Always set $is_new to false, because the "first" mail in the
// thread is the Herald notification of the commit.
$is_new = false;
$template = id(new PhabricatorMetaMTAMail())
->setSubject("{$name}: {$summary}")
->setSubjectPrefix($prefix)
->setVarySubjectPrefix("[{$verb}]")
->setFrom($comment->getActorPHID())
->setThreadID($thread_id, $is_new)
->addHeader('Thread-Topic', $thread_topic)
->setRelatedPHID($commit->getPHID())
->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs())
->setIsBulk(true)
->setBody($body);
$mails = $reply_handler->multiplexMail(
$template,
array_select_keys($handles, $email_to),
array_select_keys($handles, $email_cc));
foreach ($mails as $mail) {
$mail->saveAndSend();
}
}
public static function getMailThreading(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
return array(
'diffusion-audit-'.$commit->getPHID(),
'Commit r'.$repository->getCallsign().$commit->getCommitIdentifier(),
);
}
public static function newReplyHandlerForCommit($commit) {
$reply_handler = PhabricatorEnv::newObjectFromConfig(
'metamta.diffusion.reply-handler');
$reply_handler->setMailReceiver($commit);
return $reply_handler;
}
private function renderMailBody(
PhabricatorAuditComment $comment,
$cname,
PhabricatorObjectHandle $handle,
PhabricatorMailReplyHandler $reply_handler,
array $inline_comments) {
assert_instances_of($inline_comments, 'PhabricatorInlineCommentInterface');
$commit = $this->commit;
$actor = $this->getActor();
$name = $actor->getUsername();
$verb = PhabricatorAuditActionConstants::getActionPastTenseVerb(
$comment->getAction());
$body = new PhabricatorMetaMTAMailBody();
$body->addRawSection("{$name} {$verb} commit {$cname}.");
$body->addRawSection($comment->getContent());
if ($inline_comments) {
$block = array();
$path_map = id(new DiffusionPathQuery())
->withPathIDs(mpull($inline_comments, 'getPathID'))
->execute();
$path_map = ipull($path_map, 'path', 'id');
foreach ($inline_comments as $inline) {
$path = idx($path_map, $inline->getPathID());
if ($path === null) {
continue;
}
$start = $inline->getLineNumber();
$len = $inline->getLineLength();
if ($len) {
$range = $start.'-'.($start + $len);
} else {
$range = $start;
}
$content = $inline->getContent();
$block[] = "{$path}:{$range} {$content}";
}
$body->addTextSection(pht('INLINE COMMENTS'), implode("\n", $block));
}
$body->addTextSection(
pht('COMMIT'),
PhabricatorEnv::getProductionURI($handle->getURI()));
$body->addReplySection($reply_handler->getReplyHandlerInstructions());
return $body->render();
}
}
diff --git a/src/applications/audit/query/PhabricatorAuditCommitQuery.php b/src/applications/audit/query/PhabricatorAuditCommitQuery.php
index 2038dcf98b..4be00e0303 100644
--- a/src/applications/audit/query/PhabricatorAuditCommitQuery.php
+++ b/src/applications/audit/query/PhabricatorAuditCommitQuery.php
@@ -1,229 +1,213 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorAuditCommitQuery {
private $offset;
private $limit;
private $commitPHIDs;
private $authorPHIDs;
private $packagePHIDs;
private $identifiers = array();
private $needCommitData;
private $needAudits;
private $status = 'status-any';
const STATUS_ANY = 'status-any';
const STATUS_OPEN = 'status-open';
public function withAuthorPHIDs(array $author_phids) {
$this->authorPHIDs = $author_phids;
return $this;
}
public function withPackagePHIDs(array $phids) {
$this->packagePHIDs = $phids;
return $this;
}
public function withCommitPHIDs(array $phids) {
$this->commitPHIDs = $phids;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withIdentifiers($repository_id, array $identifiers) {
$this->identifiers[] = array($repository_id, $identifiers);
return $this;
}
public function needCommitData($need) {
$this->needCommitData = $need;
return $this;
}
public function needAudits($need) {
$this->needAudits = $need;
return $this;
}
public function setOffset($offset) {
$this->offset = $offset;
return $this;
}
public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
public function execute() {
$table = new PhabricatorRepositoryCommit();
$conn_r = $table->establishConnection('r');
$join = $this->buildJoinClause($conn_r);
$where = $this->buildWhereClause($conn_r);
$order = $this->buildOrderClause($conn_r);
$limit = $this->buildLimitClause($conn_r);
$data = queryfx_all(
$conn_r,
'SELECT c.* FROM %T c %Q %Q %Q %Q',
$table->getTableName(),
$join,
$where,
$order,
$limit);
$commits = $table->loadAllFromArray($data);
if ($this->needCommitData && $commits) {
$data = id(new PhabricatorRepositoryCommitData())->loadAllWhere(
'commitID in (%Ld)',
mpull($commits, 'getID'));
$data = mpull($data, null, 'getCommitID');
foreach ($commits as $commit) {
if (idx($data, $commit->getID())) {
$commit->attachCommitData($data[$commit->getID()]);
} else {
$commit->attachCommitData(new PhabricatorRepositoryCommitData());
}
}
}
if ($this->needAudits && $commits) {
$audits = id(new PhabricatorAuditComment())->loadAllWhere(
'targetPHID in (%Ls)',
mpull($commits, 'getPHID'));
$audits = mgroup($audits, 'getTargetPHID');
foreach ($commits as $commit) {
$commit->attachAudits(idx($audits, $commit->getPHID(), array()));
}
}
return $commits;
}
private function buildOrderClause($conn_r) {
return 'ORDER BY c.epoch DESC';
}
private function buildJoinClause($conn_r) {
$join = array();
if ($this->packagePHIDs) {
$join[] = qsprintf(
$conn_r,
'JOIN %T req ON c.phid = req.commitPHID',
id(new PhabricatorRepositoryAuditRequest())->getTableName());
}
if ($join) {
$join = implode(' ', $join);
} else {
$join = '';
}
return $join;
}
private function buildWhereClause($conn_r) {
$where = array();
if ($this->commitPHIDs) {
$where[] = qsprintf(
$conn_r,
'c.phid IN (%Ls)',
$this->commitPHIDs);
}
if ($this->authorPHIDs) {
$where[] = qsprintf(
$conn_r,
'c.authorPHID IN (%Ls)',
$this->authorPHIDs);
}
if ($this->packagePHIDs) {
$where[] = qsprintf(
$conn_r,
'req.auditorPHID in (%Ls)',
$this->packagePHIDs);
}
if ($this->identifiers) {
$clauses = array();
foreach ($this->identifiers as $spec) {
list($repository_id, $identifiers) = $spec;
if ($identifiers) {
$clauses[] = qsprintf(
$conn_r,
'c.repositoryID = %d AND c.commitIdentifier IN (%Ls)',
$repository_id,
$identifiers);
}
}
if ($clauses) {
$where[] = '('.implode(') OR (', $clauses).')';
}
}
$status = $this->status;
switch ($status) {
case self::STATUS_OPEN:
$where[] = qsprintf(
$conn_r,
'c.auditStatus = %s',
PhabricatorAuditCommitStatusConstants::CONCERN_RAISED);
break;
case self::STATUS_ANY:
break;
default:
throw new Exception("Unknown status '{$status}'!");
}
if ($where) {
$where = 'WHERE ('.implode(') AND (', $where).')';
} else {
$where = '';
}
return $where;
}
private function buildLimitClause($conn_r) {
if ($this->limit && $this->offset) {
return qsprintf($conn_r, 'LIMIT %d, %d', $this->offset, $this->limit);
} else if ($this->limit) {
return qsprintf($conn_r, 'LIMIT %d', $this->limit);
} else if ($this->offset) {
return qsprintf($conn_r, 'LIMIT %d, %d', $this->offset, PHP_INT_MAX);
} else {
return '';
}
}
}
diff --git a/src/applications/audit/query/PhabricatorAuditQuery.php b/src/applications/audit/query/PhabricatorAuditQuery.php
index b54bae8648..e2e221672b 100644
--- a/src/applications/audit/query/PhabricatorAuditQuery.php
+++ b/src/applications/audit/query/PhabricatorAuditQuery.php
@@ -1,227 +1,211 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorAuditQuery {
private $offset;
private $limit;
private $auditorPHIDs;
private $commitPHIDs;
private $needCommits;
private $needCommitData;
private $awaitingUser;
private $status = 'status-any';
const STATUS_ANY = 'status-any';
const STATUS_OPEN = 'status-open';
private $commits;
public function withCommitPHIDs(array $commit_phids) {
$this->commitPHIDs = $commit_phids;
return $this;
}
public function withAuditorPHIDs(array $auditor_phids) {
$this->auditorPHIDs = $auditor_phids;
return $this;
}
public function withAwaitingUser(PhabricatorUser $user) {
$this->awaitingUser = $user;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function setOffset($offset) {
$this->offset = $offset;
return $this;
}
public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
public function needCommits($need) {
$this->needCommits = $need;
return $this;
}
public function needCommitData($need) {
$this->needCommitData = $need;
return $this;
}
public function execute() {
$table = new PhabricatorRepositoryAuditRequest();
$conn_r = $table->establishConnection('r');
$joins = $this->buildJoinClause($conn_r);
$where = $this->buildWhereClause($conn_r);
$order = $this->buildOrderClause($conn_r);
$limit = $this->buildLimitClause($conn_r);
$data = queryfx_all(
$conn_r,
'SELECT req.* FROM %T req %Q %Q %Q %Q',
$table->getTableName(),
$joins,
$where,
$order,
$limit);
$audits = $table->loadAllFromArray($data);
if ($this->needCommits || $this->needCommitData) {
$phids = mpull($audits, 'getCommitPHID', 'getCommitPHID');
if ($phids) {
$cquery = new PhabricatorAuditCommitQuery();
$cquery->needCommitData($this->needCommitData);
$cquery->withCommitPHIDs(array_keys($phids));
$commits = $cquery->execute();
} else {
$commits = array();
}
$this->commits = $commits;
}
return $audits;
}
public function getCommits() {
if ($this->commits === null) {
throw new Exception(
"Call needCommits() or needCommitData() and then execute() the query ".
"before calling getCommits()!");
}
return $this->commits;
}
private function buildJoinClause($conn_r) {
$joins = array();
if ($this->awaitingUser) {
// Join the request table on the awaiting user's requests, so we can
// filter out package and project requests which the user has resigned
// from.
$joins[] = qsprintf(
$conn_r,
'LEFT JOIN %T awaiting ON req.commitPHID = awaiting.commitPHID AND
awaiting.auditorPHID = %s',
id(new PhabricatorRepositoryAuditRequest())->getTableName(),
$this->awaitingUser->getPHID());
// Join the commit table so we can get the commit author into the result
// row and filter by it later.
$joins[] = qsprintf(
$conn_r,
'JOIN %T commit ON req.commitPHID = commit.phid',
id(new PhabricatorRepositoryCommit())->getTableName());
}
if ($joins) {
return implode(' ', $joins);
} else {
return '';
}
}
private function buildWhereClause($conn_r) {
$where = array();
if ($this->commitPHIDs) {
$where[] = qsprintf(
$conn_r,
'req.commitPHID IN (%Ls)',
$this->commitPHIDs);
}
if ($this->auditorPHIDs) {
$where[] = qsprintf(
$conn_r,
'req.auditorPHID IN (%Ls)',
$this->auditorPHIDs);
}
if ($this->awaitingUser) {
// Exclude package and project audits associated with commits where
// the user is the author.
$where[] = qsprintf(
$conn_r,
'(commit.authorPHID IS NULL OR commit.authorPHID != %s)
OR (req.auditorPHID = %s)',
$this->awaitingUser->getPHID(),
$this->awaitingUser->getPHID());
}
$status = $this->status;
switch ($status) {
case self::STATUS_OPEN:
$where[] = qsprintf(
$conn_r,
'req.auditStatus in (%Ls)',
PhabricatorAuditStatusConstants::getOpenStatusConstants());
if ($this->awaitingUser) {
$where[] = qsprintf(
$conn_r,
'awaiting.auditStatus IS NULL OR awaiting.auditStatus != %s',
PhabricatorAuditStatusConstants::RESIGNED);
}
break;
case self::STATUS_ANY:
break;
default:
throw new Exception("Unknown status '{$status}'!");
}
if ($where) {
$where = 'WHERE ('.implode(') AND (', $where).')';
} else {
$where = '';
}
return $where;
}
private function buildLimitClause($conn_r) {
if ($this->limit && $this->offset) {
return qsprintf($conn_r, 'LIMIT %d, %d', $this->offset, $this->limit);
} else if ($this->limit) {
return qsprintf($conn_r, 'LIMIT %d', $this->limit);
} else if ($this->offset) {
return qsprintf($conn_r, 'LIMIT %d, %d', $this->offset, PHP_INT_MAX);
} else {
return '';
}
}
private function buildOrderClause($conn_r) {
return 'ORDER BY req.id DESC';
}
}
diff --git a/src/applications/audit/storage/PhabricatorAuditComment.php b/src/applications/audit/storage/PhabricatorAuditComment.php
index 9ad70ecd1a..fa02ebfd9b 100644
--- a/src/applications/audit/storage/PhabricatorAuditComment.php
+++ b/src/applications/audit/storage/PhabricatorAuditComment.php
@@ -1,71 +1,55 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorAuditComment extends PhabricatorAuditDAO
implements PhabricatorMarkupInterface {
const METADATA_ADDED_AUDITORS = 'added-auditors';
const METADATA_ADDED_CCS = 'added-ccs';
const MARKUP_FIELD_BODY = 'markup:body';
protected $phid;
protected $actorPHID;
protected $targetPHID;
protected $action;
protected $content;
protected $metadata = array();
public function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'metadata' => self::SERIALIZATION_JSON,
),
self::CONFIG_AUX_PHID => true,
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID('ACMT');
}
/* -( PhabricatorMarkupInterface Implementation )-------------------------- */
public function getMarkupFieldKey($field) {
return 'AC:'.$this->getID();
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newDiffusionMarkupEngine();
}
public function getMarkupText($field) {
return $this->getContent();
}
public function didMarkupText($field, $output, PhutilMarkupEngine $engine) {
return $output;
}
public function shouldUseMarkupCache($field) {
return (bool)$this->getID();
}
}
diff --git a/src/applications/audit/storage/PhabricatorAuditDAO.php b/src/applications/audit/storage/PhabricatorAuditDAO.php
index 65aaa5954c..3f523aadda 100644
--- a/src/applications/audit/storage/PhabricatorAuditDAO.php
+++ b/src/applications/audit/storage/PhabricatorAuditDAO.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorAuditDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'audit';
}
}
diff --git a/src/applications/audit/storage/PhabricatorAuditInlineComment.php b/src/applications/audit/storage/PhabricatorAuditInlineComment.php
index ff7a413adb..010b903088 100644
--- a/src/applications/audit/storage/PhabricatorAuditInlineComment.php
+++ b/src/applications/audit/storage/PhabricatorAuditInlineComment.php
@@ -1,147 +1,131 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorAuditInlineComment
extends PhabricatorAuditDAO
implements PhabricatorInlineCommentInterface {
protected $commitPHID;
protected $pathID;
protected $auditCommentID;
protected $authorPHID;
protected $isNewFile;
protected $lineNumber;
protected $lineLength;
protected $content;
protected $cache;
private $syntheticAuthor;
public function setSyntheticAuthor($synthetic_author) {
$this->syntheticAuthor = $synthetic_author;
return $this;
}
public function getSyntheticAuthor() {
return $this->syntheticAuthor;
}
public function isCompatible(PhabricatorInlineCommentInterface $comment) {
return
($this->getAuthorPHID() === $comment->getAuthorPHID()) &&
($this->getSyntheticAuthor() === $comment->getSyntheticAuthor()) &&
($this->getContent() === $comment->getContent());
}
public function setContent($content) {
$this->setCache(null);
$this->writeField('content', $content);
return $this;
}
public function getContent() {
return $this->readField('content');
}
public function isDraft() {
return !$this->getAuditCommentID();
}
public function setChangesetID($id) {
return $this->setPathID($id);
}
public function getChangesetID() {
return $this->getPathID();
}
// NOTE: We need to provide implementations so we conform to the shared
// interface; these are all trivial and just explicit versions of the Lisk
// defaults.
public function setIsNewFile($is_new) {
$this->writeField('isNewFile', $is_new);
return $this;
}
public function getIsNewFile() {
return $this->readField('isNewFile');
}
public function setLineNumber($number) {
$this->writeField('lineNumber', $number);
return $this;
}
public function getLineNumber() {
return $this->readField('lineNumber');
}
public function setLineLength($length) {
$this->writeField('lineLength', $length);
return $this;
}
public function getLineLength() {
return $this->readField('lineLength');
}
public function setCache($cache) {
$this->writeField('cache', $cache);
return $this;
}
public function getCache() {
return $this->readField('cache');
}
public function setAuthorPHID($phid) {
$this->writeField('authorPHID', $phid);
return $this;
}
public function getAuthorPHID() {
return $this->readField('authorPHID');
}
/* -( PhabricatorMarkupInterface Implementation )-------------------------- */
public function getMarkupFieldKey($field) {
return 'AI:'.$this->getID();
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newDifferentialMarkupEngine();
}
public function getMarkupText($field) {
return $this->getContent();
}
public function didMarkupText($field, $output, PhutilMarkupEngine $engine) {
return $output;
}
public function shouldUseMarkupCache($field) {
// Only cache submitted comments.
return ($this->getID() && $this->getAuditCommentID());
}
}
diff --git a/src/applications/audit/view/PhabricatorAuditCommitListView.php b/src/applications/audit/view/PhabricatorAuditCommitListView.php
index 1cc4788a65..f9e992076b 100644
--- a/src/applications/audit/view/PhabricatorAuditCommitListView.php
+++ b/src/applications/audit/view/PhabricatorAuditCommitListView.php
@@ -1,142 +1,126 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorAuditCommitListView extends AphrontView {
private $user;
private $commits;
private $handles;
private $noDataString;
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setNoDataString($no_data_string) {
$this->noDataString = $no_data_string;
return $this;
}
public function setCommits(array $commits) {
assert_instances_of($commits, 'PhabricatorRepositoryCommit');
$this->commits = $commits;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setAuthorityPHIDs(array $phids) {
$this->authorityPHIDs = $phids;
return $this;
}
public function getRequiredHandlePHIDs() {
$phids = array();
foreach ($this->commits as $commit) {
if ($commit->getAuthorPHID()) {
$phids[$commit->getAuthorPHID()] = true;
}
$phids[$commit->getPHID()] = true;
if ($commit->getAudits()) {
foreach ($commit->getAudits() as $audit) {
$phids[$audit->getActorPHID()] = true;
}
}
}
return array_keys($phids);
}
private function getHandle($phid) {
$handle = idx($this->handles, $phid);
if (!$handle) {
throw new Exception("No handle for '{$phid}'!");
}
return $handle;
}
public function render() {
$rows = array();
foreach ($this->commits as $commit) {
$commit_name = $this->getHandle($commit->getPHID())->renderLink();
$author_name = null;
if ($commit->getAuthorPHID()) {
$author_name = $this->getHandle($commit->getAuthorPHID())->renderLink();
}
$auditors = array();
if ($commit->getAudits()) {
foreach ($commit->getAudits() as $audit) {
$actor_phid = $audit->getActorPHID();
$auditors[$actor_phid] = $this->getHandle($actor_phid)->renderLink();
}
}
$rows[] = array(
$commit_name,
$author_name,
phutil_escape_html($commit->getCommitData()->getSummary()),
PhabricatorAuditCommitStatusConstants::getStatusName(
$commit->getAuditStatus()),
implode(', ', $auditors),
phabricator_datetime($commit->getEpoch(), $this->user),
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Commit',
'Author',
'Summary',
'Audit Status',
'Auditors',
'Date',
));
$table->setColumnClasses(
array(
'n',
'',
'wide',
'',
'',
'',
));
if ($this->commits && reset($this->commits)->getAudits() === null) {
$table->setColumnVisibility(
array(
true,
true,
true,
true,
false,
true,
));
}
if ($this->noDataString) {
$table->setNoDataString($this->noDataString);
}
return $table->render();
}
}
diff --git a/src/applications/audit/view/PhabricatorAuditListView.php b/src/applications/audit/view/PhabricatorAuditListView.php
index 94f6ef7f1e..0fad4b55a4 100644
--- a/src/applications/audit/view/PhabricatorAuditListView.php
+++ b/src/applications/audit/view/PhabricatorAuditListView.php
@@ -1,216 +1,200 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorAuditListView extends AphrontView {
private $audits;
private $handles;
private $authorityPHIDs = array();
private $noDataString;
private $commits;
private $user;
private $showDescriptions = true;
private $highlightedAudits;
public function setAudits(array $audits) {
assert_instances_of($audits, 'PhabricatorRepositoryAuditRequest');
$this->audits = $audits;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setAuthorityPHIDs(array $phids) {
$this->authorityPHIDs = $phids;
return $this;
}
public function setNoDataString($no_data_string) {
$this->noDataString = $no_data_string;
return $this;
}
public function getNoDataString() {
return $this->noDataString;
}
public function setCommits(array $commits) {
assert_instances_of($commits, 'PhabricatorRepositoryCommit');
$this->commits = mpull($commits, null, 'getPHID');
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setShowDescriptions($show_descriptions) {
$this->showDescriptions = $show_descriptions;
return $this;
}
public function getRequiredHandlePHIDs() {
$phids = array();
foreach ($this->audits as $audit) {
$phids[$audit->getCommitPHID()] = true;
$phids[$audit->getAuditorPHID()] = true;
}
return array_keys($phids);
}
private function getHandle($phid) {
$handle = idx($this->handles, $phid);
if (!$handle) {
throw new Exception("No handle for '{$phid}'!");
}
return $handle;
}
private function getCommitDescription($phid) {
if ($this->commits === null) {
return null;
}
$commit = idx($this->commits, $phid);
if (!$commit) {
return null;
}
return $commit->getCommitData()->getSummary();
}
public function getHighlightedAudits() {
if ($this->highlightedAudits === null) {
$this->highlightedAudits = array();
$user = $this->user;
$authority = array_fill_keys($this->authorityPHIDs, true);
foreach ($this->audits as $audit) {
$has_authority = !empty($authority[$audit->getAuditorPHID()]);
if ($has_authority) {
$commit_phid = $audit->getCommitPHID();
$commit_author = $this->commits[$commit_phid]->getAuthorPHID();
// You don't have authority over package and project audits on your
// own commits.
$auditor_is_user = ($audit->getAuditorPHID() == $user->getPHID());
$user_is_author = ($commit_author == $user->getPHID());
if ($auditor_is_user || !$user_is_author) {
$this->highlightedAudits[$audit->getID()] = $audit;
}
}
}
}
return $this->highlightedAudits;
}
public function render() {
$rowc = array();
$last = null;
$rows = array();
foreach ($this->audits as $audit) {
$commit_phid = $audit->getCommitPHID();
$committed = null;
if ($last == $commit_phid) {
$commit_name = null;
$commit_desc = null;
} else {
$commit_name = $this->getHandle($commit_phid)->renderLink();
$commit_desc = $this->getCommitDescription($commit_phid);
$commit = idx($this->commits, $commit_phid);
if ($commit && $this->user) {
$committed = phabricator_datetime($commit->getEpoch(), $this->user);
}
$last = $commit_phid;
}
$reasons = $audit->getAuditReasons();
foreach ($reasons as $key => $reason) {
$reasons[$key] = phutil_escape_html($reason);
}
$reasons = implode('<br />', $reasons);
$status_code = $audit->getAuditStatus();
$status = PhabricatorAuditStatusConstants::getStatusName($status_code);
$auditor_handle = $this->getHandle($audit->getAuditorPHID());
$rows[] = array(
$commit_name,
phutil_escape_html($commit_desc),
$committed,
$auditor_handle->renderLink(),
phutil_escape_html($status),
$reasons,
);
$row_class = null;
if (array_key_exists($audit->getID(), $this->getHighlightedAudits())) {
$row_class = 'highlighted';
}
$rowc[] = $row_class;
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Commit',
'Description',
'Committed',
'Auditor',
'Status',
'Details',
));
$table->setColumnClasses(
array(
'pri',
($this->showDescriptions ? 'wide' : ''),
'',
'',
'',
($this->showDescriptions ? '' : 'wide'),
));
$table->setRowClasses($rowc);
$table->setColumnVisibility(
array(
$this->showDescriptions,
$this->showDescriptions,
$this->showDescriptions,
true,
true,
true,
));
if ($this->noDataString) {
$table->setNoDataString($this->noDataString);
}
return $table->render();
}
}
diff --git a/src/applications/auth/application/PhabricatorApplicationAuth.php b/src/applications/auth/application/PhabricatorApplicationAuth.php
index a66a21ba55..1fce91912b 100644
--- a/src/applications/auth/application/PhabricatorApplicationAuth.php
+++ b/src/applications/auth/application/PhabricatorApplicationAuth.php
@@ -1,50 +1,34 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationAuth extends PhabricatorApplication {
public function shouldAppearInLaunchView() {
return false;
}
public function buildMainMenuItems(
PhabricatorUser $user,
PhabricatorController $controller = null) {
$items = array();
if ($controller instanceof PhabricatorLogoutController) {
$class = 'main-menu-item-icon-logout-selected';
} else {
$class = 'main-menu-item-icon-logout';
}
if ($user->isLoggedIn()) {
$item = new PhabricatorMainMenuIconView();
$item->setName(pht('Log Out'));
$item->addClass('autosprite main-menu-item-icon '.$class);
$item->setWorkflow(true);
$item->setHref('/logout/');
$item->setSortOrder(1.0);
$items[] = $item;
}
return $items;
}
}
diff --git a/src/applications/auth/controller/PhabricatorAuthController.php b/src/applications/auth/controller/PhabricatorAuthController.php
index 58f0780aeb..60dc5bcbc9 100644
--- a/src/applications/auth/controller/PhabricatorAuthController.php
+++ b/src/applications/auth/controller/PhabricatorAuthController.php
@@ -1,33 +1,17 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorAuthController extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('Login');
$page->setBaseURI('/login/');
$page->setTitle(idx($data, 'title'));
$page->appendChild($view);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
}
diff --git a/src/applications/auth/controller/PhabricatorDisabledUserController.php b/src/applications/auth/controller/PhabricatorDisabledUserController.php
index 02008b7b28..08647187b5 100644
--- a/src/applications/auth/controller/PhabricatorDisabledUserController.php
+++ b/src/applications/auth/controller/PhabricatorDisabledUserController.php
@@ -1,44 +1,28 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorDisabledUserController
extends PhabricatorAuthController {
public function shouldRequireEnabledUser() {
return false;
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
if (!$user->getIsDisabled()) {
return new Aphront404Response();
}
$failure_view = new AphrontRequestFailureView();
$failure_view->setHeader('Account Disabled');
$failure_view->appendChild('<p>Your account has been disabled.</p>');
return $this->buildStandardPageResponse(
$failure_view,
array(
'title' => 'Account Disabled',
));
}
}
diff --git a/src/applications/auth/controller/PhabricatorEmailLoginController.php b/src/applications/auth/controller/PhabricatorEmailLoginController.php
index af4c0aa212..bf142ea23e 100644
--- a/src/applications/auth/controller/PhabricatorEmailLoginController.php
+++ b/src/applications/auth/controller/PhabricatorEmailLoginController.php
@@ -1,169 +1,153 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorEmailLoginController
extends PhabricatorAuthController {
public function shouldRequireLogin() {
return false;
}
public function processRequest() {
$request = $this->getRequest();
if (!PhabricatorEnv::getEnvConfig('auth.password-auth-enabled')) {
return new Aphront400Response();
}
$e_email = true;
$e_captcha = true;
$errors = array();
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
if ($request->isFormPost()) {
$e_email = null;
$e_captcha = 'Again';
$captcha_ok = AphrontFormRecaptchaControl::processCaptcha($request);
if (!$captcha_ok) {
$errors[] = "Captcha response is incorrect, try again.";
$e_captcha = 'Invalid';
}
$email = $request->getStr('email');
if (!strlen($email)) {
$errors[] = "You must provide an email address.";
$e_email = 'Required';
}
if (!$errors) {
// NOTE: Don't validate the email unless the captcha is good; this makes
// it expensive to fish for valid email addresses while giving the user
// a better error if they goof their email.
$target_email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$email);
$target_user = null;
if ($target_email) {
$target_user = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$target_email->getUserPHID());
}
if (!$target_user) {
$errors[] = "There is no account associated with that email address.";
$e_email = "Invalid";
}
if (!$errors) {
$uri = $target_user->getEmailLoginURI($target_email);
if ($is_serious) {
$body = <<<EOBODY
You can use this link to reset your Phabricator password:
{$uri}
EOBODY;
} else {
$body = <<<EOBODY
Condolences on forgetting your password. You can use this link to reset it:
{$uri}
After you set a new password, consider writing it down on a sticky note and
attaching it to your monitor so you don't forget again! Choosing a very short,
easy-to-remember password like "cat" or "1234" might also help.
Best Wishes,
Phabricator
EOBODY;
}
// NOTE: Don't set the user as 'from', or they may not receive the
// mail if they have the "don't send me email about my own actions"
// preference set.
$mail = new PhabricatorMetaMTAMail();
$mail->setSubject('[Phabricator] Password Reset');
$mail->addTos(
array(
$target_user->getPHID(),
));
$mail->setBody($body);
$mail->saveAndSend();
$view = new AphrontRequestFailureView();
$view->setHeader('Check Your Email');
$view->appendChild(
'<p>An email has been sent with a link you can use to login.</p>');
return $this->buildStandardPageResponse(
$view,
array(
'title' => 'Email Sent',
));
}
}
}
$email_auth = new AphrontFormView();
$email_auth
->setAction('/login/email/')
->setUser($request->getUser())
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Email')
->setName('email')
->setValue($request->getStr('email'))
->setError($e_email))
->appendChild(
id(new AphrontFormRecaptchaControl())
->setLabel('Captcha')
->setError($e_captcha))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Send Email'));
$error_view = null;
if ($errors) {
$error_view = new AphrontErrorView();
$error_view->setTitle('Login Error');
$error_view->setErrors($errors);
}
$panel = new AphrontPanelView();
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->appendChild('<h1>Forgot Password / Email Login</h1>');
$panel->appendChild($email_auth);
return $this->buildStandardPageResponse(
array(
$error_view,
$panel,
),
array(
'title' => 'Create New Account',
));
}
}
diff --git a/src/applications/auth/controller/PhabricatorEmailTokenController.php b/src/applications/auth/controller/PhabricatorEmailTokenController.php
index bd4dfe5cfc..0f2187ee31 100644
--- a/src/applications/auth/controller/PhabricatorEmailTokenController.php
+++ b/src/applications/auth/controller/PhabricatorEmailTokenController.php
@@ -1,120 +1,104 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorEmailTokenController
extends PhabricatorAuthController {
private $token;
public function shouldRequireLogin() {
return false;
}
public function willProcessRequest(array $data) {
$this->token = $data['token'];
}
public function processRequest() {
$request = $this->getRequest();
if (!PhabricatorEnv::getEnvConfig('auth.password-auth-enabled')) {
return new Aphront400Response();
}
$token = $this->token;
$email = $request->getStr('email');
// NOTE: We need to bind verification to **addresses**, not **users**,
// because we verify addresses when they're used to login this way, and if
// we have a user-based verification you can:
//
// - Add some address you do not own;
// - request a password reset;
// - change the URI in the email to the address you don't own;
// - login via the email link; and
// - get a "verified" address you don't control.
$target_email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$email);
$target_user = null;
if ($target_email) {
$target_user = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$target_email->getUserPHID());
}
if (!$target_email ||
!$target_user ||
!$target_user->validateEmailToken($target_email, $token)) {
$view = new AphrontRequestFailureView();
$view->setHeader('Unable to Login');
$view->appendChild(
'<p>The authentication information in the link you clicked is '.
'invalid or out of date. Make sure you are copy-and-pasting the '.
'entire link into your browser. You can try again, or request '.
'a new email.</p>');
$view->appendChild(
'<div class="aphront-failure-continue">'.
'<a class="button" href="/login/email/">Send Another Email</a>'.
'</div>');
return $this->buildStandardPageResponse(
$view,
array(
'title' => 'Login Failure',
));
}
// Verify email so that clicking the link in the "Welcome" email is good
// enough, without requiring users to go through a second round of email
// verification.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$target_email->setIsVerified(1);
$target_email->save();
$session_key = $target_user->establishSession('web');
unset($unguarded);
$request->setCookie('phusr', $target_user->getUsername());
$request->setCookie('phsid', $session_key);
if (PhabricatorEnv::getEnvConfig('account.editable')) {
$next = (string)id(new PhutilURI('/settings/panel/password/'))
->setQueryParams(
array(
'token' => $token,
'email' => $email,
));
} else {
$next = '/';
}
$uri = new PhutilURI('/login/validate/');
$uri->setQueryParams(
array(
'phusr' => $target_user->getUsername(),
'next' => $next,
));
return id(new AphrontRedirectResponse())
->setURI((string)$uri);
}
}
diff --git a/src/applications/auth/controller/PhabricatorLDAPLoginController.php b/src/applications/auth/controller/PhabricatorLDAPLoginController.php
index 27cf982fe9..60595be4eb 100644
--- a/src/applications/auth/controller/PhabricatorLDAPLoginController.php
+++ b/src/applications/auth/controller/PhabricatorLDAPLoginController.php
@@ -1,187 +1,171 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorLDAPLoginController extends PhabricatorAuthController {
private $provider;
public function shouldRequireLogin() {
return false;
}
public function willProcessRequest(array $data) {
$this->provider = new PhabricatorLDAPProvider();
}
public function processRequest() {
if (!$this->provider->isProviderEnabled()) {
return new Aphront400Response();
}
$current_user = $this->getRequest()->getUser();
$request = $this->getRequest();
$ldap_username = $request->getCookie('phusr');
if ($request->isFormPost()) {
$ldap_username = $request->getStr('username');
try {
$envelope = new PhutilOpaqueEnvelope($request->getStr('password'));
$this->provider->auth($ldap_username, $envelope);
} catch (Exception $e) {
$errors[] = $e->getMessage();
}
if (empty($errors)) {
$ldap_info = $this->retrieveLDAPInfo($this->provider);
if ($current_user->getPHID()) {
if ($ldap_info->getID()) {
$existing_ldap = id(new PhabricatorUserLDAPInfo())->loadOneWhere(
'userID = %d',
$current_user->getID());
if ($ldap_info->getUserID() != $current_user->getID() ||
$existing_ldap) {
$dialog = new AphrontDialogView();
$dialog->setUser($current_user);
$dialog->setTitle('Already Linked to Another Account');
$dialog->appendChild(
'<p>The LDAP account you just authorized is already linked to '.
'another Phabricator account. Before you can link it to a '.
'different LDAP account, you must unlink the old account.</p>'
);
$dialog->addCancelButton('/settings/panel/ldap/');
return id(new AphrontDialogResponse())->setDialog($dialog);
} else {
return id(new AphrontRedirectResponse())
->setURI('/settings/panel/ldap/');
}
}
if (!$request->isDialogFormPost()) {
$dialog = new AphrontDialogView();
$dialog->setUser($current_user);
$dialog->setTitle('Link LDAP Account');
$dialog->appendChild(
'<p>Link your LDAP account to your Phabricator account?</p>');
$dialog->addHiddenInput('username', $request->getStr('username'));
$dialog->addHiddenInput('password', $request->getStr('password'));
$dialog->addSubmitButton('Link Accounts');
$dialog->addCancelButton('/settings/panel/ldap/');
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$ldap_info->setUserID($current_user->getID());
$this->saveLDAPInfo($ldap_info);
return id(new AphrontRedirectResponse())
->setURI('/settings/panel/ldap/');
}
if ($ldap_info->getID()) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$known_user = id(new PhabricatorUser())->load(
$ldap_info->getUserID());
$session_key = $known_user->establishSession('web');
$this->saveLDAPInfo($ldap_info);
$request->setCookie('phusr', $known_user->getUsername());
$request->setCookie('phsid', $session_key);
$uri = new PhutilURI('/login/validate/');
$uri->setQueryParams(
array(
'phusr' => $known_user->getUsername(),
));
return id(new AphrontRedirectResponse())->setURI((string)$uri);
}
$controller = newv('PhabricatorLDAPRegistrationController',
array($this->getRequest()));
$controller->setLDAPProvider($this->provider);
$controller->setLDAPInfo($ldap_info);
return $this->delegateToController($controller);
}
}
$ldap_form = new AphrontFormView();
$ldap_form
->setUser($request->getUser())
->setAction('/ldap/login/')
->appendChild(
id(new AphrontFormTextControl())
->setLabel('LDAP username')
->setName('username')
->setValue($ldap_username))
->appendChild(
id(new AphrontFormPasswordControl())
->setLabel('Password')
->setName('password'));
$ldap_form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Login'));
$panel = new AphrontPanelView();
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->appendChild('<h1>LDAP login</h1>');
$panel->appendChild($ldap_form);
if (isset($errors) && count($errors) > 0) {
$error_view = new AphrontErrorView();
$error_view->setTitle('Login Failed');
$error_view->setErrors($errors);
}
return $this->buildStandardPageResponse(
array(
isset($error_view) ? $error_view : null,
$panel,
),
array(
'title' => 'Login',
));
}
private function retrieveLDAPInfo(PhabricatorLDAPProvider $provider) {
$ldap_info = id(new PhabricatorUserLDAPInfo())->loadOneWhere(
'ldapUsername = %s',
$provider->retrieveUsername());
if (!$ldap_info) {
$ldap_info = new PhabricatorUserLDAPInfo();
$ldap_info->setLDAPUsername($provider->retrieveUsername());
}
return $ldap_info;
}
private function saveLDAPInfo(PhabricatorUserLDAPInfo $info) {
// UNGUARDED WRITES: Logging-in users don't have their CSRF set up yet.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$info->save();
}
}
diff --git a/src/applications/auth/controller/PhabricatorLDAPRegistrationController.php b/src/applications/auth/controller/PhabricatorLDAPRegistrationController.php
index 73b9958625..1fde2ef450 100644
--- a/src/applications/auth/controller/PhabricatorLDAPRegistrationController.php
+++ b/src/applications/auth/controller/PhabricatorLDAPRegistrationController.php
@@ -1,236 +1,220 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorLDAPRegistrationController
extends PhabricatorAuthController {
private $ldapProvider;
private $ldapInfo;
public function setLDAPProvider($provider) {
$this->ldapProvider = $provider;
return $this;
}
public function getLDAProvider() {
return $this->ldapProvider;
}
public function setLDAPInfo($info) {
$this->ldapInfo = $info;
return $this;
}
public function getLDAPInfo() {
return $this->ldapInfo;
}
public function processRequest() {
$provider = $this->getLDAProvider();
$ldap_info = $this->getLDAPInfo();
$request = $this->getRequest();
$errors = array();
$e_username = true;
$e_email = true;
$e_realname = true;
$user = new PhabricatorUser();
$user->setUsername($provider->retrieveUsername());
$user->setRealname($provider->retrieveUserRealName());
$new_email = $provider->retrieveUserEmail();
if ($new_email) {
// If the user's LDAP provider account has an email address but the
// email address domain is not allowed by the Phabricator configuration,
// we just pretend the provider did not supply an address.
//
// For instance, if the user uses LDAP Auth and their email address
// is "joe@personal.com" but Phabricator is configured to require users
// use "@company.com" addresses, we show a prompt below and tell the user
// to provide their "@company.com" address. They can still use the LDAP
// account to login, they just need to associate their account with an
// allowed address.
//
// If the email address is fine, we just use it and don't prompt the user.
if (!PhabricatorUserEmail::isAllowedAddress($new_email)) {
$new_email = null;
}
}
$show_email_input = ($new_email === null);
if ($request->isFormPost()) {
$user->setUsername($request->getStr('username'));
$username = $user->getUsername();
if (!strlen($user->getUsername())) {
$e_username = 'Required';
$errors[] = 'Username is required.';
} else if (!PhabricatorUser::validateUsername($username)) {
$e_username = 'Invalid';
$errors[] = PhabricatorUser::describeValidUsername();
} else {
$e_username = null;
}
if (!$new_email) {
$new_email = trim($request->getStr('email'));
if (!$new_email) {
$e_email = 'Required';
$errors[] = 'Email is required.';
} else {
$e_email = null;
}
}
if ($new_email) {
if (!PhabricatorUserEmail::isAllowedAddress($new_email)) {
$e_email = 'Invalid';
$errors[] = PhabricatorUserEmail::describeAllowedAddresses();
}
}
if (!strlen($user->getRealName())) {
$user->setRealName($request->getStr('realname'));
if (!strlen($user->getRealName())) {
$e_realname = 'Required';
$errors[] = 'Real name is required.';
} else {
$e_realname = null;
}
}
if (!$errors) {
try {
// NOTE: We don't verify LDAP email addresses by default because
// LDAP providers might associate email addresses with accounts that
// haven't actually verified they own them. We could selectively
// auto-verify some providers that we trust here, but the stakes for
// verifying an email address are high because having a corporate
// address at a company is sometimes the key to the castle.
$email_obj = id(new PhabricatorUserEmail())
->setAddress($new_email)
->setIsVerified(0);
id(new PhabricatorUserEditor())
->setActor($user)
->createNewUser($user, $email_obj);
$ldap_info->setUserID($user->getID());
$ldap_info->save();
$session_key = $user->establishSession('web');
$request->setCookie('phusr', $user->getUsername());
$request->setCookie('phsid', $session_key);
$email_obj->sendVerificationEmail($user);
return id(new AphrontRedirectResponse())->setURI('/');
} catch (AphrontQueryDuplicateKeyException $exception) {
$same_username = id(new PhabricatorUser())->loadOneWhere(
'userName = %s',
$user->getUserName());
$same_email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$new_email);
if ($same_username) {
$e_username = 'Duplicate';
$errors[] = 'That username or email is not unique.';
} else if ($same_email) {
$e_email = 'Duplicate';
$errors[] = 'That email is not unique.';
} else {
throw $exception;
}
}
}
}
$error_view = null;
if ($errors) {
$error_view = new AphrontErrorView();
$error_view->setTitle('Registration Failed');
$error_view->setErrors($errors);
}
// Strip the URI down to the path, because otherwise we'll trigger
// external CSRF protection (by having a protocol in the form "action")
// and generate a form with no CSRF token.
$action_uri = new PhutilURI('/ldap/login/');
$action_path = $action_uri->getPath();
$form = new AphrontFormView();
$form
->setUser($request->getUser())
->setAction($action_path)
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Username')
->setName('username')
->setValue($user->getUsername())
->setError($e_username));
$form->appendChild(
id(new AphrontFormPasswordControl())
->setLabel('Password')
->setName('password'));
if ($show_email_input) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel('Email')
->setName('email')
->setValue($request->getStr('email'))
->setError($e_email));
}
if ($provider->retrieveUserRealName() === null) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel('Real Name')
->setName('realname')
->setValue($request->getStr('realname'))
->setError($e_realname));
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Create Account'));
$panel = new AphrontPanelView();
$panel->setHeader('Create New Account');
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->appendChild($form);
return $this->buildStandardPageResponse(
array(
$error_view,
$panel,
),
array(
'title' => 'Create New Account',
));
}
}
diff --git a/src/applications/auth/controller/PhabricatorLDAPUnlinkController.php b/src/applications/auth/controller/PhabricatorLDAPUnlinkController.php
index 4db9b2772c..180c20ce73 100644
--- a/src/applications/auth/controller/PhabricatorLDAPUnlinkController.php
+++ b/src/applications/auth/controller/PhabricatorLDAPUnlinkController.php
@@ -1,52 +1,36 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorLDAPUnlinkController extends PhabricatorAuthController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$ldap_info = id(new PhabricatorUserLDAPInfo())->loadOneWhere(
'userID = %d',
$user->getID());
if (!$ldap_info) {
return new Aphront400Response();
}
if (!$request->isDialogFormPost()) {
$dialog = new AphrontDialogView();
$dialog->setUser($user);
$dialog->setTitle('Really unlink account?');
$dialog->appendChild(
'<p><strong>You will not be able to login</strong> using this account '.
'once you unlink it. Continue?</p>');
$dialog->addSubmitButton('Unlink Account');
$dialog->addCancelButton('/settings/panel/ldap/');
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$ldap_info->delete();
return id(new AphrontRedirectResponse())
->setURI('/settings/panel/ldap/');
}
}
diff --git a/src/applications/auth/controller/PhabricatorLoginController.php b/src/applications/auth/controller/PhabricatorLoginController.php
index 2d71fef199..9f4fa1f8ec 100644
--- a/src/applications/auth/controller/PhabricatorLoginController.php
+++ b/src/applications/auth/controller/PhabricatorLoginController.php
@@ -1,320 +1,304 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorLoginController
extends PhabricatorAuthController {
public function shouldRequireLogin() {
return false;
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
if ($user->isLoggedIn()) {
// Kick the user out if they're already logged in.
return id(new AphrontRedirectResponse())->setURI('/');
}
if ($request->isAjax()) {
// We end up here if the user clicks a workflow link that they need to
// login to use. We give them a dialog saying "You need to login..".
if ($request->isDialogFormPost()) {
return id(new AphrontRedirectResponse())->setURI(
$request->getRequestURI());
}
$dialog = new AphrontDialogView();
$dialog->setUser($user);
$dialog->setTitle('Login Required');
$dialog->appendChild('<p>You must login to continue.</p>');
$dialog->addSubmitButton('Login');
$dialog->addCancelButton('/', 'Cancel');
return id(new AphrontDialogResponse())->setDialog($dialog);
}
if ($request->isConduit()) {
// A common source of errors in Conduit client configuration is getting
// the request path wrong. The client will end up here, so make some
// effort to give them a comprehensible error message.
$request_path = $this->getRequest()->getPath();
$conduit_path = '/api/<method>';
$example_path = '/api/conduit.ping';
$message =
"ERROR: You are making a Conduit API request to '{$request_path}', ".
"but the correct HTTP request path to use in order to access a ".
"Conduit method is '{$conduit_path}' (for example, ".
"'{$example_path}'). Check your configuration.";
return id(new AphrontPlainTextResponse())->setContent($message);
}
$error_view = null;
if ($request->getCookie('phusr') && $request->getCookie('phsid')) {
// The session cookie is invalid, so clear it.
$request->clearCookie('phusr');
$request->clearCookie('phsid');
$error_view = new AphrontErrorView();
$error_view->setTitle('Invalid Session');
$error_view->setErrors(array(
"Your login session is invalid. Try logging in again. If that ".
"doesn't work, clear your browser cookies."
));
}
$next_uri_path = $this->getRequest()->getPath();
if ($next_uri_path == '/login/') {
$next_uri = '/';
} else {
$next_uri = $this->getRequest()->getRequestURI();
}
if (!$request->isFormPost()) {
$request->setCookie('next_uri', $next_uri);
}
$password_auth = PhabricatorEnv::getEnvConfig('auth.password-auth-enabled');
$username_or_email = $request->getCookie('phusr');
$forms = array();
$errors = array();
if ($password_auth) {
$require_captcha = false;
$e_captcha = true;
if ($request->isFormPost()) {
if (AphrontFormRecaptchaControl::isRecaptchaEnabled()) {
$failed_attempts = PhabricatorUserLog::loadRecentEventsFromThisIP(
PhabricatorUserLog::ACTION_LOGIN_FAILURE,
60 * 15);
if (count($failed_attempts) > 5) {
$require_captcha = true;
if (!AphrontFormRecaptchaControl::processCaptcha($request)) {
if (AphrontFormRecaptchaControl::hasCaptchaResponse($request)) {
$e_captcha = 'Invalid';
$errors[] = 'CAPTCHA was not entered correctly.';
} else {
$e_captcha = 'Required';
$errors[] = 'Too many login failures recently. You must '.
'submit a CAPTCHA with your login request.';
}
}
}
}
$username_or_email = $request->getStr('username_or_email');
$user = id(new PhabricatorUser())->loadOneWhere(
'username = %s',
$username_or_email);
if (!$user) {
$user = PhabricatorUser::loadOneWithEmailAddress($username_or_email);
}
if (!$errors) {
// Perform username/password tests only if we didn't get rate limited
// by the CAPTCHA.
$envelope = new PhutilOpaqueEnvelope($request->getStr('password'));
if (!$user || !$user->comparePassword($envelope)) {
$errors[] = 'Bad username/password.';
}
}
if (!$errors) {
$session_key = $user->establishSession('web');
$request->setCookie('phusr', $user->getUsername());
$request->setCookie('phsid', $session_key);
$uri = new PhutilURI('/login/validate/');
$uri->setQueryParams(
array(
'phusr' => $user->getUsername(),
));
return id(new AphrontRedirectResponse())
->setURI((string)$uri);
} else {
$log = PhabricatorUserLog::newLog(
null,
$user,
PhabricatorUserLog::ACTION_LOGIN_FAILURE);
$log->save();
$request->clearCookie('phusr');
$request->clearCookie('phsid');
}
}
if ($errors) {
$error_view = new AphrontErrorView();
$error_view->setTitle('Login Failed');
$error_view->setErrors($errors);
}
$form = new AphrontFormView();
$form
->setUser($request->getUser())
->setAction('/login/')
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Username/Email')
->setName('username_or_email')
->setValue($username_or_email))
->appendChild(
id(new AphrontFormPasswordControl())
->setLabel('Password')
->setName('password')
->setCaption(
'<a href="/login/email/">'.
'Forgot your password? / Email Login</a>'));
if ($require_captcha) {
$form->appendChild(
id(new AphrontFormRecaptchaControl())
->setError($e_captcha));
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Login'));
// $panel->setCreateButton('Register New Account', '/login/register/');
$forms['Phabricator Login'] = $form;
}
$ldap_provider = new PhabricatorLDAPProvider();
if ($ldap_provider->isProviderEnabled()) {
$ldap_form = new AphrontFormView();
$ldap_form
->setUser($request->getUser())
->setAction('/ldap/login/')
->appendChild(
id(new AphrontFormTextControl())
->setLabel('LDAP username')
->setName('username')
->setValue($username_or_email))
->appendChild(
id(new AphrontFormPasswordControl())
->setLabel('Password')
->setName('password'));
$ldap_form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Login'));
$forms['LDAP Login'] = $ldap_form;
}
$providers = PhabricatorOAuthProvider::getAllProviders();
foreach ($providers as $provider) {
$enabled = $provider->isProviderEnabled();
if (!$enabled) {
continue;
}
$auth_uri = $provider->getAuthURI();
$redirect_uri = $provider->getRedirectURI();
$client_id = $provider->getClientID();
$provider_name = $provider->getProviderName();
$minimum_scope = $provider->getMinimumScope();
$extra_auth = $provider->getExtraAuthParameters();
// TODO: In theory we should use 'state' to prevent CSRF, but the total
// effect of the CSRF attack is that an attacker can cause a user to login
// to Phabricator if they're already logged into some OAuth provider. This
// does not seem like the most severe threat in the world, and generating
// CSRF for logged-out users is vaugely tricky.
if ($provider->isProviderRegistrationEnabled()) {
$title = "Login or Register with {$provider_name}";
$body = 'Login or register for Phabricator using your '.
phutil_escape_html($provider_name).' account.';
$button = "Login or Register with {$provider_name}";
} else {
$title = "Login with {$provider_name}";
$body = 'Login to your existing Phabricator account using your '.
phutil_escape_html($provider_name).' account.<br /><br />'.
'<strong>You can not use '.
phutil_escape_html($provider_name).' to register a new '.
'account.</strong>';
$button = "Login with {$provider_name}";
}
$auth_form = new AphrontFormView();
$auth_form
->setAction($auth_uri)
->addHiddenInput('client_id', $client_id)
->addHiddenInput('redirect_uri', $redirect_uri)
->addHiddenInput('scope', $minimum_scope);
foreach ($extra_auth as $key => $value) {
$auth_form->addHiddenInput($key, $value);
}
$auth_form
->setUser($request->getUser())
->setMethod('GET')
->appendChild(
'<p class="aphront-form-instructions">'.$body.'</p>')
->appendChild(
id(new AphrontFormSubmitControl())
->setValue("{$button} \xC2\xBB"));
$forms[$title] = $auth_form;
}
$panel = new AphrontPanelView();
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
foreach ($forms as $name => $form) {
$panel->appendChild('<h1>'.$name.'</h1>');
$panel->appendChild($form);
$panel->appendChild('<br />');
}
$login_message = PhabricatorEnv::getEnvConfig('auth.login-message');
return $this->buildStandardPageResponse(
array(
$error_view,
$login_message,
$panel,
),
array(
'title' => 'Login',
));
}
}
diff --git a/src/applications/auth/controller/PhabricatorLoginValidateController.php b/src/applications/auth/controller/PhabricatorLoginValidateController.php
index 492b47cf5f..7ddc737011 100644
--- a/src/applications/auth/controller/PhabricatorLoginValidateController.php
+++ b/src/applications/auth/controller/PhabricatorLoginValidateController.php
@@ -1,92 +1,76 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorLoginValidateController
extends PhabricatorAuthController {
public function shouldRequireLogin() {
return false;
}
public function processRequest() {
$request = $this->getRequest();
$failures = array();
if (!strlen($request->getStr('phusr'))) {
throw new Exception(
"Login validation is missing expected parameters!");
}
$expect_phusr = $request->getStr('phusr');
$actual_phusr = $request->getCookie('phusr');
if ($actual_phusr != $expect_phusr) {
if ($actual_phusr) {
$cookie_info = "sent back a cookie with the value '{$actual_phusr}'.";
} else {
$cookie_info = "did not accept the cookie.";
}
$failures[] =
"Attempted to set 'phusr' cookie to '{$expect_phusr}', but your ".
"browser {$cookie_info}";
}
if (!$failures) {
if (!$request->getUser()->getPHID()) {
$failures[] = "Cookies were set correctly, but your session ".
"isn't valid.";
}
}
if ($failures) {
$list = array();
foreach ($failures as $failure) {
$list[] = '<li>'.phutil_escape_html($failure).'</li>';
}
$list = '<ul>'.implode("\n", $list).'</ul>';
$view = new AphrontRequestFailureView();
$view->setHeader('Login Failed');
$view->appendChild(
'<p>Login failed:</p>'.
$list.
'<p><strong>Clear your cookies</strong> and try again.</p>');
$view->appendChild(
'<div class="aphront-failure-continue">'.
'<a class="button" href="/login/">Try Again</a>'.
'</div>');
return $this->buildStandardPageResponse(
$view,
array(
'title' => 'Login Failed',
));
}
$next = nonempty($request->getStr('next'), $request->getCookie('next_uri'));
$request->clearCookie('next_uri');
if (!PhabricatorEnv::isValidLocalWebResource($next)) {
$next = '/';
}
return id(new AphrontRedirectResponse())->setURI($next);
}
}
diff --git a/src/applications/auth/controller/PhabricatorLogoutController.php b/src/applications/auth/controller/PhabricatorLogoutController.php
index d6362dafce..fc3e9c8dc5 100644
--- a/src/applications/auth/controller/PhabricatorLogoutController.php
+++ b/src/applications/auth/controller/PhabricatorLogoutController.php
@@ -1,75 +1,59 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorLogoutController
extends PhabricatorAuthController {
public function shouldRequireLogin() {
return true;
}
public function shouldRequireEmailVerification() {
// Allow unverified users to logout.
return false;
}
public function shouldRequireEnabledUser() {
// Allow disabled users to logout.
return false;
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
if ($request->isFormPost()) {
$log = PhabricatorUserLog::newLog(
$user,
$user,
PhabricatorUserLog::ACTION_LOGOUT);
$log->save();
// Destroy the user's session in the database so logout works even if
// their cookies have some issues. We'll detect cookie issues when they
// try to login again and tell them to clear any junk.
$phsid = $request->getCookie('phsid');
if ($phsid) {
$user->destroySession($phsid);
}
$request->clearCookie('phsid');
return id(new AphrontRedirectResponse())
->setURI('/login/');
}
if ($user->getPHID()) {
$dialog = id(new AphrontDialogView())
->setUser($user)
->setTitle('Log out of Phabricator?')
->appendChild('<p>Are you sure you want to log out?</p>')
->addSubmitButton('Log Out')
->addCancelButton('/');
return id(new AphrontDialogResponse())->setDialog($dialog);
}
return id(new AphrontRedirectResponse())->setURI('/');
}
}
diff --git a/src/applications/auth/controller/PhabricatorMustVerifyEmailController.php b/src/applications/auth/controller/PhabricatorMustVerifyEmailController.php
index cac618cbed..319163112d 100644
--- a/src/applications/auth/controller/PhabricatorMustVerifyEmailController.php
+++ b/src/applications/auth/controller/PhabricatorMustVerifyEmailController.php
@@ -1,90 +1,74 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorMustVerifyEmailController
extends PhabricatorAuthController {
public function shouldRequireLogin() {
return false;
}
public function shouldRequireEmailVerification() {
// NOTE: We don't technically need this since PhabricatorController forces
// us here in either case, but it's more consistent with intent.
return false;
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$email = $user->loadPrimaryEmail();
if ($email->getIsVerified()) {
return id(new AphrontRedirectResponse())->setURI('/');
}
$email_address = $email->getAddress();
$sent = null;
if ($request->isFormPost()) {
$email->sendVerificationEmail($user);
$sent = new AphrontErrorView();
$sent->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
$sent->setTitle('Email Sent');
$sent->appendChild(
'<p>Another verification email was sent to <strong>'.
phutil_escape_html($email_address).'</strong>.</p>');
}
$error_view = new AphrontRequestFailureView();
$error_view->setHeader('Check Your Email');
$error_view->appendChild(
'<p>You must verify your email address to login. You should have a new '.
'email message from Phabricator with verification instructions in your '.
'inbox (<strong>'.phutil_escape_html($email_address).'</strong>).</p>');
$error_view->appendChild(
'<p>If you did not receive an email, you can click the button below '.
'to try sending another one.</p>');
$error_view->appendChild(
'<div class="aphront-failure-continue">'.
phabricator_render_form(
$user,
array(
'action' => '/login/mustverify/',
'method' => 'POST',
),
phutil_render_tag(
'button',
array(
),
'Send Another Email')).
'</div>');
return $this->buildStandardPageResponse(
array(
$sent,
$error_view,
),
array(
'title' => 'Must Verify Email',
));
}
}
diff --git a/src/applications/auth/controller/PhabricatorOAuthDiagnosticsController.php b/src/applications/auth/controller/PhabricatorOAuthDiagnosticsController.php
index e4bf3835a4..94ef4b23aa 100644
--- a/src/applications/auth/controller/PhabricatorOAuthDiagnosticsController.php
+++ b/src/applications/auth/controller/PhabricatorOAuthDiagnosticsController.php
@@ -1,219 +1,203 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorOAuthDiagnosticsController
extends PhabricatorAuthController {
private $provider;
public function shouldRequireLogin() {
return false;
}
public function willProcessRequest(array $data) {
$this->provider = PhabricatorOAuthProvider::newProvider($data['provider']);
}
public function processRequest() {
$provider = $this->provider;
$auth_enabled = $provider->isProviderEnabled();
$client_id = $provider->getClientID();
$client_secret = $provider->getClientSecret();
$key = $provider->getProviderKey();
$name = phutil_escape_html($provider->getProviderName());
$res_ok = '<strong style="color: #00aa00;">OK</strong>';
$res_no = '<strong style="color: #aa0000;">NO</strong>';
$res_na = '<strong style="color: #999999;">N/A</strong>';
$results = array();
$auth_key = $key . '.auth-enabled';
if (!$auth_enabled) {
$results[$auth_key] = array(
$res_no,
'false',
$name . ' authentication is disabled in the configuration. Edit the '.
'Phabricator configuration to enable "'.$auth_key.'".');
} else {
$results[$auth_key] = array(
$res_ok,
'true',
$name.' authentication is enabled.');
}
$client_id_key = $key. '.application-id';
if (!$client_id) {
$results[$client_id_key] = array(
$res_no,
null,
'No '.$name.' Application ID is configured. Edit the Phabricator '.
'configuration to specify an application ID in '.
'"'.$client_id_key.'". '.$provider->renderGetClientIDHelp());
} else {
$results[$client_id_key] = array(
$res_ok,
$client_id,
'Application ID is set.');
}
$client_secret_key = $key.'.application-secret';
if (!$client_secret) {
$results[$client_secret_key] = array(
$res_no,
null,
'No '.$name.' Application secret is configured. Edit the '.
'Phabricator configuration to specify an Application Secret, in '.
'"'.$client_secret_key.'". '.$provider->renderGetClientSecretHelp());
} else {
$results[$client_secret_key] = array(
$res_ok,
"It's a secret!",
'Application secret is set.');
}
$timeout = 5;
$internet = HTTPSFuture::loadContent("http://google.com/", $timeout);
if ($internet === false) {
$results['internet'] = array(
$res_no,
null,
'Unable to make an HTTP request to Google. Check your outbound '.
'internet connection and firewall/filtering settings.');
} else {
$results['internet'] = array(
$res_ok,
null,
'Internet seems OK.');
}
$test_uris = $provider->getTestURIs();
foreach ($test_uris as $uri) {
$success = HTTPSFuture::loadContent($uri, $timeout);
if ($success === false) {
$results[$uri] = array(
$res_no,
null,
"Unable to make an HTTP request to {$uri}. {$name} may be ".
'down or inaccessible.');
} else {
$results[$uri] = array(
$res_ok,
null,
'Made a request to '.$uri.'.');
}
}
if ($provider->shouldDiagnoseAppLogin()) {
$test_uri = new PhutilURI($provider->getTokenURI());
$test_uri->setQueryParams(
array(
'client_id' => $client_id,
'client_secret' => $client_secret,
'grant_type' => 'client_credentials',
));
$future = new HTTPSFuture($test_uri);
$future->setTimeout($timeout);
try {
list($body) = $future->resolvex();
$results['App Login'] = array(
$res_ok,
'(A Valid Token)',
"Raw application login to {$name} works.");
} catch (Exception $ex) {
if ($ex instanceof HTTPFutureResponseStatusCURL) {
$results['App Login'] = array(
$res_no,
null,
"Unable to perform an application login with your Application ID ".
"and Application Secret. You may have mistyped or misconfigured ".
"them; {$name} may have revoked your authorization; or {$name} ".
"may be having technical problems.");
} else {
$data = json_decode($token_value, true);
if (!is_array($data)) {
$results['App Login'] = array(
$res_no,
$token_value,
"Application Login failed but the provider did not respond ".
"with valid JSON error information. {$name} may be experiencing ".
"technical problems.");
} else {
$results['App Login'] = array(
$res_no,
null,
"Application Login failed with error: ".$token_value);
}
}
}
}
return $this->renderResults($results);
}
private function renderResults($results) {
$provider = $this->provider;
$rows = array();
foreach ($results as $key => $result) {
$rows[] = array(
phutil_escape_html($key),
$result[0],
phutil_escape_html($result[1]),
phutil_escape_html($result[2]),
);
}
$table_view = new AphrontTableView($rows);
$table_view->setHeaders(
array(
'Test',
'Result',
'Value',
'Details',
));
$table_view->setColumnClasses(
array(
null,
null,
null,
'wide',
));
$title = $provider->getProviderName() . ' Auth Diagnostics';
$panel_view = new AphrontPanelView();
$panel_view->setHeader($title);
$panel_view->appendChild(
'<p class="aphront-panel-instructions">These tests may be able to '.
'help diagnose the root cause of problems you experience with '.
$provider->getProviderName() .
' Authentication. Reload the page to run the tests again.</p>');
$panel_view->appendChild($table_view);
return $this->buildStandardPageResponse(
$panel_view,
array(
'title' => $title,
));
}
}
diff --git a/src/applications/auth/controller/PhabricatorOAuthLoginController.php b/src/applications/auth/controller/PhabricatorOAuthLoginController.php
index b351aa87f0..09be7d076e 100644
--- a/src/applications/auth/controller/PhabricatorOAuthLoginController.php
+++ b/src/applications/auth/controller/PhabricatorOAuthLoginController.php
@@ -1,355 +1,339 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorOAuthLoginController
extends PhabricatorAuthController {
private $provider;
private $userID;
private $accessToken;
private $tokenExpires;
private $oauthState;
public function shouldRequireLogin() {
return false;
}
public function willProcessRequest(array $data) {
$this->provider = PhabricatorOAuthProvider::newProvider($data['provider']);
}
public function processRequest() {
$current_user = $this->getRequest()->getUser();
$provider = $this->provider;
if (!$provider->isProviderEnabled()) {
return new Aphront400Response();
}
$provider_name = $provider->getProviderName();
$provider_key = $provider->getProviderKey();
$request = $this->getRequest();
if ($request->getStr('error')) {
$error_view = id(new PhabricatorOAuthFailureView())
->setRequest($request);
return $this->buildErrorResponse($error_view);
}
$error_response = $this->retrieveAccessToken($provider);
if ($error_response) {
return $error_response;
}
$userinfo_uri = new PhutilURI($provider->getUserInfoURI());
$userinfo_uri->setQueryParam('access_token', $this->accessToken);
$userinfo_uri = (string)$userinfo_uri;
try {
$user_data = HTTPSFuture::loadContent($userinfo_uri);
if ($user_data === false) {
throw new PhabricatorOAuthProviderException(
"Request to '{$userinfo_uri}' failed!");
}
$provider->setUserData($user_data);
} catch (PhabricatorOAuthProviderException $e) {
return $this->buildErrorResponse(new PhabricatorOAuthFailureView(), $e);
}
$provider->setAccessToken($this->accessToken);
$user_id = $provider->retrieveUserID();
$provider_key = $provider->getProviderKey();
$oauth_info = $this->retrieveOAuthInfo($provider);
if ($current_user->getPHID()) {
if ($oauth_info->getID()) {
if ($oauth_info->getUserID() != $current_user->getID()) {
$dialog = new AphrontDialogView();
$dialog->setUser($current_user);
$dialog->setTitle('Already Linked to Another Account');
$dialog->appendChild(
hsprintf(
'<p>The %s account you just authorized is already linked to '.
'another Phabricator account. Before you can associate your %s '.
'account with this Phabriactor account, you must unlink it from '.
'the Phabricator account it is currently linked to.</p>',
$provider_name,
$provider_name));
$dialog->addCancelButton($provider->getSettingsPanelURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
} else {
$this->saveOAuthInfo($oauth_info); // Refresh token.
return id(new AphrontRedirectResponse())
->setURI($provider->getSettingsPanelURI());
}
}
$existing_oauth = id(new PhabricatorUserOAuthInfo())->loadOneWhere(
'userID = %d AND oauthProvider = %s',
$current_user->getID(),
$provider_key);
if ($existing_oauth) {
$dialog = new AphrontDialogView();
$dialog->setUser($current_user);
$dialog->setTitle('Already Linked to an Account From This Provider');
$dialog->appendChild(
hsprintf(
'<p>The account you are logged in with is already linked to a %s '.
'account. Before you can link it to a different %s account, you '.
'must unlink the old account.</p>',
$provider_name,
$provider_name));
$dialog->addCancelButton($provider->getSettingsPanelURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
if (!$request->isDialogFormPost()) {
$dialog = new AphrontDialogView();
$dialog->setUser($current_user);
$dialog->setTitle('Link '.$provider_name.' Account');
$dialog->appendChild(
hsprintf(
'<p>Link your %s account to your Phabricator account?</p>',
$provider_name));
$dialog->addHiddenInput('confirm_token', $provider->getAccessToken());
$dialog->addHiddenInput('expires', $oauth_info->getTokenExpires());
$dialog->addHiddenInput('state', $this->oauthState);
$dialog->addHiddenInput('scope', $oauth_info->getTokenScope());
$dialog->addSubmitButton('Link Accounts');
$dialog->addCancelButton($provider->getSettingsPanelURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$oauth_info->setUserID($current_user->getID());
$this->saveOAuthInfo($oauth_info);
return id(new AphrontRedirectResponse())
->setURI($provider->getSettingsPanelURI());
}
// Login with known auth.
if ($oauth_info->getID()) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$known_user = id(new PhabricatorUser())->load($oauth_info->getUserID());
$request->getApplicationConfiguration()->willAuthenticateUserWithOAuth(
$known_user,
$oauth_info,
$provider);
$session_key = $known_user->establishSession('web');
$this->saveOAuthInfo($oauth_info);
$request->setCookie('phusr', $known_user->getUsername());
$request->setCookie('phsid', $session_key);
$uri = new PhutilURI('/login/validate/');
$uri->setQueryParams(
array(
'phusr' => $known_user->getUsername(),
));
return id(new AphrontRedirectResponse())->setURI((string)$uri);
}
$oauth_email = $provider->retrieveUserEmail();
if ($oauth_email) {
$known_email = id(new PhabricatorUserEmail())
->loadOneWhere('address = %s', $oauth_email);
if ($known_email) {
$dialog = new AphrontDialogView();
$dialog->setUser($current_user);
$dialog->setTitle('Already Linked to Another Account');
$dialog->appendChild(
hsprintf(
'<p>The %s account you just authorized has an email address which '.
'is already in use by another Phabricator account. To link the '.
'accounts, log in to your Phabricator account and then go to '.
'Settings.</p>',
$provider_name));
$user = id(new PhabricatorUser())
->loadOneWhere('phid = %s', $known_email->getUserPHID());
$oauth_infos = id(new PhabricatorUserOAuthInfo())
->loadAllWhere('userID = %d', $user->getID());
if ($oauth_infos) {
$providers = array();
foreach ($oauth_infos as $info) {
$provider = $info->getOAuthProvider();
$providers[] = PhabricatorOAuthProvider::newProvider($provider)
->getProviderName();
}
$dialog->appendChild(
hsprintf(
'<p>The account is associated with: %s.</p>',
implode(', ', $providers)));
}
$dialog->addCancelButton('/login/');
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
if (!$provider->isProviderRegistrationEnabled()) {
$dialog = new AphrontDialogView();
$dialog->setUser($current_user);
$dialog->setTitle('No Account Registration With '.$provider_name);
$dialog->appendChild(
hsprintf(
'<p>You can not register a new account using %s; you can only use '.
'your %s account to log into an existing Phabricator account which '.
'you have registered through other means.</p>',
$provider_name,
$provider_name));
$dialog->addCancelButton('/login/');
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$controller = PhabricatorEnv::newObjectFromConfig(
'controller.oauth-registration',
array($this->getRequest()));
$controller->setOAuthProvider($provider);
$controller->setOAuthInfo($oauth_info);
$controller->setOAuthState($this->oauthState);
return $this->delegateToController($controller);
}
private function buildErrorResponse(PhabricatorOAuthFailureView $view,
Exception $e = null) {
$provider = $this->provider;
$provider_name = $provider->getProviderName();
$view->setOAuthProvider($provider);
if ($e) {
$view->setException($e);
}
return $this->buildStandardPageResponse(
$view,
array(
'title' => $provider_name.' Auth Failed',
));
}
private function retrieveAccessToken(PhabricatorOAuthProvider $provider) {
$request = $this->getRequest();
$token = $request->getStr('confirm_token');
if ($token) {
$this->tokenExpires = $request->getInt('expires');
$this->accessToken = $token;
$this->oauthState = $request->getStr('state');
return null;
}
$client_id = $provider->getClientID();
$client_secret = $provider->getClientSecret();
$redirect_uri = $provider->getRedirectURI();
$auth_uri = $provider->getTokenURI();
$code = $request->getStr('code');
$query_data = array(
'client_id' => $client_id,
'client_secret' => $client_secret,
'redirect_uri' => $redirect_uri,
'code' => $code,
) + $provider->getExtraTokenParameters();
$future = new HTTPSFuture($auth_uri, $query_data);
$future->setMethod('POST');
try {
list($response) = $future->resolvex();
} catch (Exception $ex) {
return $this->buildErrorResponse(new PhabricatorOAuthFailureView());
}
$data = $provider->decodeTokenResponse($response);
$token = idx($data, 'access_token');
if (!$token) {
return $this->buildErrorResponse(new PhabricatorOAuthFailureView());
}
$this->tokenExpires = $provider->getTokenExpiryFromArray($data);
$this->accessToken = $token;
$this->oauthState = $request->getStr('state');
return null;
}
private function retrieveOAuthInfo(PhabricatorOAuthProvider $provider) {
$oauth_info = id(new PhabricatorUserOAuthInfo())->loadOneWhere(
'oauthProvider = %s and oauthUID = %s',
$provider->getProviderKey(),
$provider->retrieveUserID());
$scope = $this->getRequest()->getStr('scope');
if (!$oauth_info) {
$oauth_info = new PhabricatorUserOAuthInfo();
$oauth_info->setOAuthProvider($provider->getProviderKey());
$oauth_info->setOAuthUID($provider->retrieveUserID());
// some providers don't tell you what scope you got, so default
// to the minimum Phabricator requires rather than assuming no scope
if (!$scope) {
$scope = $provider->getMinimumScope();
}
}
$oauth_info->setAccountURI($provider->retrieveUserAccountURI());
$oauth_info->setAccountName($provider->retrieveUserAccountName());
$oauth_info->setToken($provider->getAccessToken());
$oauth_info->setTokenStatus(PhabricatorUserOAuthInfo::TOKEN_STATUS_GOOD);
$oauth_info->setTokenScope($scope);
// If we have out-of-date expiration info, just clear it out. Then replace
// it with good info if the provider gave it to us.
$expires = $oauth_info->getTokenExpires();
if ($expires <= time()) {
$expires = null;
}
if ($this->tokenExpires) {
$expires = $this->tokenExpires;
}
$oauth_info->setTokenExpires($expires);
return $oauth_info;
}
private function saveOAuthInfo(PhabricatorUserOAuthInfo $info) {
// UNGUARDED WRITES: Logging-in users don't have their CSRF set up yet.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$info->save();
}
}
diff --git a/src/applications/auth/controller/PhabricatorOAuthUnlinkController.php b/src/applications/auth/controller/PhabricatorOAuthUnlinkController.php
index c156c8d55f..816dae4bba 100644
--- a/src/applications/auth/controller/PhabricatorOAuthUnlinkController.php
+++ b/src/applications/auth/controller/PhabricatorOAuthUnlinkController.php
@@ -1,68 +1,52 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorOAuthUnlinkController extends PhabricatorAuthController {
private $provider;
public function willProcessRequest(array $data) {
$this->provider = PhabricatorOAuthProvider::newProvider($data['provider']);
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$provider = $this->provider;
if ($provider->isProviderLinkPermanent()) {
throw new Exception(
"You may not unlink accounts from this OAuth provider.");
}
$provider_key = $provider->getProviderKey();
$oauth_info = id(new PhabricatorUserOAuthInfo())->loadOneWhere(
'userID = %d AND oauthProvider = %s',
$user->getID(),
$provider_key);
if (!$oauth_info) {
return new Aphront400Response();
}
if (!$request->isDialogFormPost()) {
$dialog = new AphrontDialogView();
$dialog->setUser($user);
$dialog->setTitle('Really unlink account?');
$dialog->appendChild(
'<p><strong>You will not be able to login</strong> using this account '.
'once you unlink it. Continue?</p>');
$dialog->addSubmitButton('Unlink Account');
$dialog->addCancelButton($provider->getSettingsPanelURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$oauth_info->delete();
return id(new AphrontRedirectResponse())
->setURI($provider->getSettingsPanelURI());
}
}
diff --git a/src/applications/auth/controller/PhabricatorRefreshCSRFController.php b/src/applications/auth/controller/PhabricatorRefreshCSRFController.php
index abd10ac037..19d7aa7eb1 100644
--- a/src/applications/auth/controller/PhabricatorRefreshCSRFController.php
+++ b/src/applications/auth/controller/PhabricatorRefreshCSRFController.php
@@ -1,32 +1,16 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorRefreshCSRFController extends PhabricatorAuthController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
return id(new AphrontAjaxResponse())
->setContent(
array(
'token' => $user->getCSRFToken(),
));
}
}
diff --git a/src/applications/auth/controller/oauthregistration/PhabricatorOAuthDefaultRegistrationController.php b/src/applications/auth/controller/oauthregistration/PhabricatorOAuthDefaultRegistrationController.php
index 3f3c114d9d..a88c27fd91 100644
--- a/src/applications/auth/controller/oauthregistration/PhabricatorOAuthDefaultRegistrationController.php
+++ b/src/applications/auth/controller/oauthregistration/PhabricatorOAuthDefaultRegistrationController.php
@@ -1,239 +1,223 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorOAuthDefaultRegistrationController
extends PhabricatorOAuthRegistrationController {
public function processRequest() {
$provider = $this->getOAuthProvider();
$oauth_info = $this->getOAuthInfo();
$request = $this->getRequest();
$errors = array();
$e_username = true;
$e_email = true;
$e_realname = true;
$user = new PhabricatorUser();
$user->setUsername($provider->retrieveUserAccountName());
$user->setRealName($provider->retrieveUserRealName());
$new_email = $provider->retrieveUserEmail();
if ($new_email) {
// If the user's OAuth provider account has an email address but the
// email address domain is not allowed by the Phabricator configuration,
// we just pretend the provider did not supply an address.
//
// For instance, if the user uses Google OAuth and their Google address
// is "joe@personal.com" but Phabricator is configured to require users
// use "@company.com" addresses, we show a prompt below and tell the user
// to provide their "@company.com" address. They can still use the OAuth
// account to login, they just need to associate their account with an
// allowed address.
//
// If the OAuth address is fine, we just use it and don't prompt the user.
if (!PhabricatorUserEmail::isAllowedAddress($new_email)) {
$new_email = null;
}
}
$show_email_input = ($new_email === null);
if ($request->isFormPost()) {
$user->setUsername($request->getStr('username'));
$username = $user->getUsername();
if (!strlen($user->getUsername())) {
$e_username = 'Required';
$errors[] = 'Username is required.';
} else if (!PhabricatorUser::validateUsername($username)) {
$e_username = 'Invalid';
$errors[] = PhabricatorUser::describeValidUsername();
} else {
$e_username = null;
}
if (!$new_email) {
$new_email = trim($request->getStr('email'));
if (!$new_email) {
$e_email = 'Required';
$errors[] = 'Email is required.';
} else {
$e_email = null;
}
}
if ($new_email) {
$email_ok = PhabricatorUserEmail::isAllowedAddress($new_email);
if (!$email_ok) {
$e_email = 'Invalid';
$errors[] = PhabricatorUserEmail::describeAllowedAddresses();
}
}
if (!strlen($user->getRealName())) {
$user->setRealName($request->getStr('realname'));
if (!strlen($user->getRealName())) {
$e_realname = 'Required';
$errors[] = 'Real name is required.';
} else {
$e_realname = null;
}
}
if (!$errors) {
$image = $provider->retrieveUserProfileImage();
if ($image) {
$file = PhabricatorFile::newFromFileData(
$image,
array(
'name' => $provider->getProviderKey().'-profile.jpg',
'authorPHID' => $user->getPHID(),
));
$xformer = new PhabricatorImageTransformer();
// Resize OAuth image to a reasonable size
$small_xformed = $xformer->executeProfileTransform(
$file,
$width = 50,
$min_height = 50,
$max_height = 50);
$user->setProfileImagePHID($small_xformed->getPHID());
}
try {
// NOTE: We don't verify OAuth email addresses by default because
// OAuth providers might associate email addresses with accounts that
// haven't actually verified they own them. We could selectively
// auto-verify some providers that we trust here, but the stakes for
// verifying an email address are high because having a corporate
// address at a company is sometimes the key to the castle.
$email_obj = id(new PhabricatorUserEmail())
->setAddress($new_email)
->setIsVerified(0);
id(new PhabricatorUserEditor())
->setActor($user)
->createNewUser($user, $email_obj);
$oauth_info->setUserID($user->getID());
$oauth_info->save();
$session_key = $user->establishSession('web');
$request->setCookie('phusr', $user->getUsername());
$request->setCookie('phsid', $session_key);
$email_obj->sendVerificationEmail($user);
return id(new AphrontRedirectResponse())->setURI('/');
} catch (AphrontQueryDuplicateKeyException $exception) {
$same_username = id(new PhabricatorUser())->loadOneWhere(
'userName = %s',
$user->getUserName());
$same_email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$new_email);
if ($same_username) {
$e_username = 'Duplicate';
$errors[] = 'That username or email is not unique.';
} else if ($same_email) {
$e_email = 'Duplicate';
$errors[] = 'That email is not unique.';
} else {
throw $exception;
}
}
}
}
$error_view = null;
if ($errors) {
$error_view = new AphrontErrorView();
$error_view->setTitle('Registration Failed');
$error_view->setErrors($errors);
}
// Strip the URI down to the path, because otherwise we'll trigger
// external CSRF protection (by having a protocol in the form "action")
// and generate a form with no CSRF token.
$action_uri = new PhutilURI($provider->getRedirectURI());
$action_path = $action_uri->getPath();
$form = new AphrontFormView();
$form
->addHiddenInput('confirm_token', $provider->getAccessToken())
->addHiddenInput('expires', $oauth_info->getTokenExpires())
->addHiddenInput('state', $this->getOAuthState())
->setUser($request->getUser())
->setAction($action_path)
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Username')
->setName('username')
->setValue($user->getUsername())
->setError($e_username));
if ($show_email_input) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel('Email')
->setName('email')
->setValue($request->getStr('email'))
->setCaption(PhabricatorUserEmail::describeAllowedAddresses())
->setError($e_email));
}
if ($provider->retrieveUserRealName() === null) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel('Real Name')
->setName('realname')
->setValue($request->getStr('realname'))
->setError($e_realname));
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Create Account'));
$panel = new AphrontPanelView();
$panel->setHeader('Create New Account');
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->appendChild($form);
return $this->buildStandardPageResponse(
array(
$error_view,
$panel,
),
array(
'title' => 'Create New Account',
));
}
}
diff --git a/src/applications/auth/controller/oauthregistration/PhabricatorOAuthRegistrationController.php b/src/applications/auth/controller/oauthregistration/PhabricatorOAuthRegistrationController.php
index ec90b91ed0..75a85ec598 100644
--- a/src/applications/auth/controller/oauthregistration/PhabricatorOAuthRegistrationController.php
+++ b/src/applications/auth/controller/oauthregistration/PhabricatorOAuthRegistrationController.php
@@ -1,53 +1,37 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorOAuthRegistrationController
extends PhabricatorAuthController {
private $oauthProvider;
private $oauthInfo;
private $oauthState;
final public function setOAuthInfo($info) {
$this->oauthInfo = $info;
return $this;
}
final public function getOAuthInfo() {
return $this->oauthInfo;
}
final public function setOAuthProvider($provider) {
$this->oauthProvider = $provider;
return $this;
}
final public function getOAuthProvider() {
return $this->oauthProvider;
}
final public function setOAuthState($state) {
$this->oauthState = $state;
return $this;
}
final public function getOAuthState() {
return $this->oauthState;
}
}
diff --git a/src/applications/auth/ldap/PhabricatorLDAPProvider.php b/src/applications/auth/ldap/PhabricatorLDAPProvider.php
index d458acc35d..97856adec3 100644
--- a/src/applications/auth/ldap/PhabricatorLDAPProvider.php
+++ b/src/applications/auth/ldap/PhabricatorLDAPProvider.php
@@ -1,261 +1,245 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorLDAPProvider {
// http://www.php.net/manual/en/function.ldap-errno.php#20665 states
// that the number could be 31 or 49, in testing it has always been 49
const LDAP_INVALID_CREDENTIALS = 49;
private $userData;
private $connection;
public function __construct() {
}
public function __destruct() {
if (isset($this->connection)) {
ldap_unbind($this->connection);
}
}
public function isProviderEnabled() {
return PhabricatorEnv::getEnvConfig('ldap.auth-enabled');
}
public function getHostname() {
return PhabricatorEnv::getEnvConfig('ldap.hostname');
}
public function getPort() {
return PhabricatorEnv::getEnvConfig('ldap.port');
}
public function getBaseDN() {
return PhabricatorEnv::getEnvConfig('ldap.base_dn');
}
public function getSearchAttribute() {
return PhabricatorEnv::getEnvConfig('ldap.search_attribute');
}
public function getUsernameAttribute() {
return PhabricatorEnv::getEnvConfig('ldap.username-attribute');
}
public function getLDAPVersion() {
return PhabricatorEnv::getEnvConfig('ldap.version');
}
public function getLDAPReferrals() {
return PhabricatorEnv::getEnvConfig('ldap.referrals');
}
public function retrieveUserEmail() {
return $this->userData['mail'][0];
}
public function retrieveUserRealName() {
return $this->retrieveUserRealNameFromData($this->userData);
}
public function retrieveUserRealNameFromData($data) {
$name_attributes = PhabricatorEnv::getEnvConfig(
'ldap.real_name_attributes');
$real_name = '';
if (is_array($name_attributes)) {
foreach ($name_attributes AS $attribute) {
if (isset($data[$attribute][0])) {
$real_name .= $data[$attribute][0].' ';
}
}
trim($real_name);
} else if (isset($data[$name_attributes][0])) {
$real_name = $data[$name_attributes][0];
}
if ($real_name == '') {
return null;
}
return $real_name;
}
public function retrieveUsername() {
$key = nonempty(
$this->getUsernameAttribute(),
$this->getSearchAttribute());
return $this->userData[$key][0];
}
public function getConnection() {
if (!isset($this->connection)) {
$this->connection = ldap_connect($this->getHostname(), $this->getPort());
if (!$this->connection) {
throw new Exception('Could not connect to LDAP host at '.
$this->getHostname().':'.$this->getPort());
}
ldap_set_option($this->connection, LDAP_OPT_PROTOCOL_VERSION,
$this->getLDAPVersion());
ldap_set_option($this->connection, LDAP_OPT_REFERRALS,
$this->getLDAPReferrals());
}
return $this->connection;
}
public function getUserData() {
return $this->userData;
}
private function invalidLDAPUserErrorMessage($errno, $errmsg) {
return "LDAP Error #".$errno.": ".$errmsg;
}
public function auth($username, PhutilOpaqueEnvelope $password) {
if (strlen(trim($username)) == 0) {
throw new Exception('Username can not be empty');
}
if (PhabricatorEnv::getEnvConfig('ldap.search-first')) {
// To protect against people phishing for accounts we catch the
// exception and present the default exception that would be presented
// in the case of a failed bind.
try {
$user = $this->getUser($this->getUsernameAttribute(), $username);
$username = $user[$this->getSearchAttribute()][0];
} catch (PhabricatorLDAPUnknownUserException $e) {
throw new Exception(
$this->invalidLDAPUserErrorMessage(
self::LDAP_INVALID_CREDENTIALS,
ldap_err2str(self::LDAP_INVALID_CREDENTIALS)));
}
}
$conn = $this->getConnection();
$activeDirectoryDomain =
PhabricatorEnv::getEnvConfig('ldap.activedirectory_domain');
if ($activeDirectoryDomain) {
$dn = $username.'@'.$activeDirectoryDomain;
} else {
$dn = ldap_sprintf(
'%Q=%s,%Q',
$this->getSearchAttribute(),
$username,
$this->getBaseDN());
}
// NOTE: It is very important we suppress any messages that occur here,
// because it logs passwords if it reaches an error log of any sort.
DarkConsoleErrorLogPluginAPI::enableDiscardMode();
$result = @ldap_bind($conn, $dn, $password->openEnvelope());
DarkConsoleErrorLogPluginAPI::disableDiscardMode();
if (!$result) {
throw new Exception(
$this->invalidLDAPUserErrorMessage(
ldap_errno($conn),
ldap_error($conn)));
}
$this->userData = $this->getUser($this->getSearchAttribute(), $username);
return $this->userData;
}
private function getUser($attribute, $username) {
$conn = $this->getConnection();
$query = ldap_sprintf(
'%Q=%S',
$attribute,
$username);
$result = ldap_search($conn, $this->getBaseDN(), $query);
if (!$result) {
throw new Exception('Search failed. Please check your LDAP and HTTP '.
'logs for more information.');
}
$entries = ldap_get_entries($conn, $result);
if ($entries === false) {
throw new Exception('Could not get entries');
}
if ($entries['count'] > 1) {
throw new Exception('Found more then one user with this '.
$attribute);
}
if ($entries['count'] == 0) {
throw new PhabricatorLDAPUnknownUserException('Could not find user');
}
return $entries[0];
}
public function search($query) {
$result = ldap_search($this->getConnection(), $this->getBaseDN(),
$query);
if (!$result) {
throw new Exception('Search failed. Please check your LDAP and HTTP '.
'logs for more information.');
}
$entries = ldap_get_entries($this->getConnection(), $result);
if ($entries === false) {
throw new Exception('Could not get entries');
}
if ($entries['count'] == 0) {
throw new Exception('No results found');
}
$rows = array();
for($i = 0; $i < $entries['count']; $i++) {
$row = array();
$entry = $entries[$i];
// Get username, email and realname
$username = $entry[$this->getSearchAttribute()][0];
if(empty($username)) {
continue;
}
$row[] = $username;
$row[] = $entry['mail'][0];
$row[] = $this->retrieveUserRealNameFromData($entry);
$rows[] = $row;
}
return $rows;
}
}
diff --git a/src/applications/auth/ldap/PhabricatorLDAPUnknownUserException.php b/src/applications/auth/ldap/PhabricatorLDAPUnknownUserException.php
index 6994025938..7013267a84 100644
--- a/src/applications/auth/ldap/PhabricatorLDAPUnknownUserException.php
+++ b/src/applications/auth/ldap/PhabricatorLDAPUnknownUserException.php
@@ -1,20 +1,4 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorLDAPUnknownUserException extends Exception{
}
diff --git a/src/applications/auth/oauth/provider/PhabricatorOAuthProvider.php b/src/applications/auth/oauth/provider/PhabricatorOAuthProvider.php
index 9f491b0dfd..c8cbf1274c 100644
--- a/src/applications/auth/oauth/provider/PhabricatorOAuthProvider.php
+++ b/src/applications/auth/oauth/provider/PhabricatorOAuthProvider.php
@@ -1,182 +1,166 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorOAuthProvider {
const PROVIDER_FACEBOOK = 'facebook';
const PROVIDER_GITHUB = 'github';
const PROVIDER_GOOGLE = 'google';
const PROVIDER_PHABRICATOR = 'phabricator';
const PROVIDER_DISQUS = 'disqus';
private $accessToken;
abstract public function getProviderKey();
abstract public function getProviderName();
abstract public function isProviderEnabled();
abstract public function isProviderLinkPermanent();
abstract public function isProviderRegistrationEnabled();
abstract public function getClientID();
abstract public function renderGetClientIDHelp();
abstract public function getClientSecret();
abstract public function renderGetClientSecretHelp();
abstract public function getAuthURI();
abstract public function getTestURIs();
public function getSettingsPanelURI() {
$panel = new PhabricatorSettingsPanelOAuth();
$panel->setOAuthProvider($this);
return $panel->getPanelURI();
}
/**
* If the provider needs extra stuff in the auth request, return it here.
* For example, Google needs a response_type parameter.
*/
public function getExtraAuthParameters() {
return array();
}
/**
* If the provider supports application login, the diagnostics page can try
* to test it. Most providers do not support this (Facebook does).
*/
public function shouldDiagnoseAppLogin() {
return false;
}
abstract public function getTokenURI();
/**
* Access tokens expire based on an implementation-specific key.
*/
abstract protected function getTokenExpiryKey();
public function getTokenExpiryFromArray(array $data) {
$key = $this->getTokenExpiryKey();
if ($key) {
$expiry_value = idx($data, $key, 0);
if ($expiry_value) {
return time() + $expiry_value;
}
}
return 0;
}
/**
* If the provider needs extra stuff in the token request, return it here.
* For example, Google needs a grant_type parameter.
*/
public function getExtraTokenParameters() {
return array();
}
abstract public function getUserInfoURI();
abstract public function getMinimumScope();
abstract public function setUserData($data);
abstract public function retrieveUserID();
abstract public function retrieveUserEmail();
abstract public function retrieveUserAccountName();
abstract public function retrieveUserProfileImage();
abstract public function retrieveUserAccountURI();
abstract public function retrieveUserRealName();
/**
* Override this if the provider returns the token response as, e.g., JSON
* or XML.
*/
public function decodeTokenResponse($response) {
$data = null;
parse_str($response, $data);
return $data;
}
public function __construct() {
}
/**
* This is where the OAuth provider will redirect the user after the user
* grants Phabricator access.
*/
final public function getRedirectURI() {
$key = $this->getProviderKey();
return PhabricatorEnv::getURI('/oauth/'.$key.'/login/');
}
final public function setAccessToken($access_token) {
$this->accessToken = $access_token;
return $this;
}
final public function getAccessToken() {
return $this->accessToken;
}
/**
* Often used within setUserData to make sure $data is not completely
* junk. More granular validations of data might be necessary depending on
* the provider and are generally encouraged.
*/
final protected function validateUserData($data) {
if (empty($data) || !is_array($data)) {
throw new PhabricatorOAuthProviderException();
}
return true;
}
public static function newProvider($which) {
switch ($which) {
case self::PROVIDER_FACEBOOK:
$class = 'PhabricatorOAuthProviderFacebook';
break;
case self::PROVIDER_GITHUB:
$class = 'PhabricatorOAuthProviderGitHub';
break;
case self::PROVIDER_GOOGLE:
$class = 'PhabricatorOAuthProviderGoogle';
break;
case self::PROVIDER_PHABRICATOR:
$class = 'PhabricatorOAuthProviderPhabricator';
break;
case self::PROVIDER_DISQUS:
$class = 'PhabricatorOAuthProviderDisqus';
break;
default:
throw new Exception('Unknown OAuth provider.');
}
return newv($class, array());
}
public static function getAllProviders() {
$all = array(
self::PROVIDER_FACEBOOK,
self::PROVIDER_GITHUB,
self::PROVIDER_GOOGLE,
self::PROVIDER_PHABRICATOR,
self::PROVIDER_DISQUS,
);
$providers = array();
foreach ($all as $provider) {
$providers[$provider] = self::newProvider($provider);
}
return $providers;
}
}
diff --git a/src/applications/auth/oauth/provider/PhabricatorOAuthProviderDisqus.php b/src/applications/auth/oauth/provider/PhabricatorOAuthProviderDisqus.php
index 126d463c43..939bf5999f 100644
--- a/src/applications/auth/oauth/provider/PhabricatorOAuthProviderDisqus.php
+++ b/src/applications/auth/oauth/provider/PhabricatorOAuthProviderDisqus.php
@@ -1,144 +1,128 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorOAuthProviderDisqus extends PhabricatorOAuthProvider {
private $userData;
public function getProviderKey() {
return self::PROVIDER_DISQUS;
}
public function getProviderName() {
return 'Disqus';
}
public function isProviderEnabled() {
return PhabricatorEnv::getEnvConfig('disqus.auth-enabled');
}
public function isProviderLinkPermanent() {
return PhabricatorEnv::getEnvConfig('disqus.auth-permanent');
}
public function isProviderRegistrationEnabled() {
return PhabricatorEnv::getEnvConfig('disqus.registration-enabled');
}
public function getClientID() {
return PhabricatorEnv::getEnvConfig('disqus.application-id');
}
public function renderGetClientIDHelp() {
return null;
}
public function getClientSecret() {
return PhabricatorEnv::getEnvConfig('disqus.application-secret');
}
public function renderGetClientSecretHelp() {
return null;
}
public function getAuthURI() {
return 'https://disqus.com/api/oauth/2.0/authorize/';
}
public function getTokenURI() {
return 'https://disqus.com/api/oauth/2.0/access_token/';
}
protected function getTokenExpiryKey() {
return 'expires_in';
}
public function getExtraAuthParameters() {
return array(
'response_type' => 'code',
);
}
public function getExtraTokenParameters() {
return array(
'grant_type' => 'authorization_code',
);
}
public function decodeTokenResponse($response) {
return json_decode($response, true);
}
public function getTestURIs() {
return array(
'http://disqus.com',
$this->getUserInfoURI(),
);
}
public function getUserInfoURI() {
return 'https://disqus.com/api/3.0/users/details.json?'.
'api_key='.$this->getClientID();
}
public function getMinimumScope() {
return 'read';
}
public function setUserData($data) {
$data = idx(json_decode($data, true), 'response');
$this->validateUserData($data);
$this->userData = $data;
return $this;
}
public function retrieveUserID() {
return $this->userData['id'];
}
public function retrieveUserEmail() {
return idx($this->userData, 'email');
}
public function retrieveUserAccountName() {
return $this->userData['username'];
}
public function retrieveUserProfileImage() {
$avatar = idx($this->userData, 'avatar');
if ($avatar) {
$uri = idx($avatar, 'permalink');
if ($uri) {
return HTTPSFuture::loadContent($uri);
}
}
return null;
}
public function retrieveUserAccountURI() {
return idx($this->userData, 'profileUrl');
}
public function retrieveUserRealName() {
return idx($this->userData, 'name');
}
public function shouldDiagnoseAppLogin() {
return true;
}
}
diff --git a/src/applications/auth/oauth/provider/PhabricatorOAuthProviderException.php b/src/applications/auth/oauth/provider/PhabricatorOAuthProviderException.php
index f3d75c4fb5..75edb1b4bc 100644
--- a/src/applications/auth/oauth/provider/PhabricatorOAuthProviderException.php
+++ b/src/applications/auth/oauth/provider/PhabricatorOAuthProviderException.php
@@ -1,20 +1,4 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorOAuthProviderException extends Exception {
}
diff --git a/src/applications/auth/oauth/provider/PhabricatorOAuthProviderFacebook.php b/src/applications/auth/oauth/provider/PhabricatorOAuthProviderFacebook.php
index 32bc418320..1a660a9447 100644
--- a/src/applications/auth/oauth/provider/PhabricatorOAuthProviderFacebook.php
+++ b/src/applications/auth/oauth/provider/PhabricatorOAuthProviderFacebook.php
@@ -1,142 +1,126 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorOAuthProviderFacebook extends PhabricatorOAuthProvider {
private $userData;
public function getProviderKey() {
return self::PROVIDER_FACEBOOK;
}
public function getProviderName() {
return 'Facebook';
}
public function isProviderEnabled() {
return PhabricatorEnv::getEnvConfig('facebook.auth-enabled');
}
public function isProviderLinkPermanent() {
return PhabricatorEnv::getEnvConfig('facebook.auth-permanent');
}
public function isProviderRegistrationEnabled() {
return PhabricatorEnv::getEnvConfig('facebook.registration-enabled');
}
public function getClientID() {
return PhabricatorEnv::getEnvConfig('facebook.application-id');
}
public function renderGetClientIDHelp() {
return 'To generate an ID, sign into Facebook, install the "Developer"'.
' application, and use it to create a new Facebook application.';
}
public function getClientSecret() {
return PhabricatorEnv::getEnvConfig('facebook.application-secret');
}
public function renderGetClientSecretHelp() {
return 'You can find the application secret in the Facebook'.
' "Developer" application on Facebook.';
}
public function getAuthURI() {
return 'https://www.facebook.com/dialog/oauth';
}
public function getTestURIs() {
return array(
'http://facebook.com',
'https://graph.facebook.com/me'
);
}
public function getTokenURI() {
return 'https://graph.facebook.com/oauth/access_token';
}
protected function getTokenExpiryKey() {
return 'expires';
}
public function getUserInfoURI() {
$fields = array('id', 'name', 'email', 'link', 'security_settings');
return 'https://graph.facebook.com/me?fields='.
implode(',', $fields);
}
public function getMinimumScope() {
return 'email';
}
public function setUserData($data) {
$data = json_decode($data, true);
$this->validateUserData($data);
if (PhabricatorEnv::getEnvConfig('facebook.require-https-auth')) {
if (!$data['security_settings']['secure_browsing']['enabled']) {
throw new PhabricatorOAuthProviderException(
'You must enable Secure Browsing on your Facebook account in'.
' order to log in to Phabricator. For more information, check'.
' out http://www.facebook.com/help/?faq=215897678434749'
);
}
}
$this->userData = $data;
return $this;
}
public function retrieveUserID() {
return $this->userData['id'];
}
public function retrieveUserEmail() {
return $this->userData['email'];
}
public function retrieveUserAccountName() {
$matches = null;
$link = $this->userData['link'];
if (preg_match('@/([a-zA-Z0-9]+)$@', $link, $matches)) {
return $matches[1];
}
return null;
}
public function retrieveUserProfileImage() {
$uri = 'https://graph.facebook.com/me/picture?access_token=';
return HTTPSFuture::loadContent($uri.$this->getAccessToken());
}
public function retrieveUserAccountURI() {
return $this->userData['link'];
}
public function retrieveUserRealName() {
return $this->userData['name'];
}
public function shouldDiagnoseAppLogin() {
return true;
}
}
diff --git a/src/applications/auth/oauth/provider/PhabricatorOAuthProviderGitHub.php b/src/applications/auth/oauth/provider/PhabricatorOAuthProviderGitHub.php
index 5313efbe9a..65ce274b4a 100644
--- a/src/applications/auth/oauth/provider/PhabricatorOAuthProviderGitHub.php
+++ b/src/applications/auth/oauth/provider/PhabricatorOAuthProviderGitHub.php
@@ -1,129 +1,113 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorOAuthProviderGitHub extends PhabricatorOAuthProvider {
private $userData;
public function getProviderKey() {
return self::PROVIDER_GITHUB;
}
public function getProviderName() {
return 'GitHub';
}
public function isProviderEnabled() {
return PhabricatorEnv::getEnvConfig('github.auth-enabled');
}
public function isProviderLinkPermanent() {
return PhabricatorEnv::getEnvConfig('github.auth-permanent');
}
public function isProviderRegistrationEnabled() {
return PhabricatorEnv::getEnvConfig('github.registration-enabled');
}
public function getClientID() {
return PhabricatorEnv::getEnvConfig('github.application-id');
}
public function renderGetClientIDHelp() {
return null;
}
public function getClientSecret() {
return PhabricatorEnv::getEnvConfig('github.application-secret');
}
public function renderGetClientSecretHelp() {
return null;
}
public function getAuthURI() {
return 'https://github.com/login/oauth/authorize';
}
public function getTokenURI() {
return 'https://github.com/login/oauth/access_token';
}
protected function getTokenExpiryKey() {
// github access tokens do not have time-based expiry
return null;
}
public function getTestURIs() {
return array(
'http://api.github.com',
);
}
public function getUserInfoURI() {
return 'https://api.github.com/user';
}
public function getMinimumScope() {
return null;
}
public function setUserData($data) {
$data = json_decode($data, true);
$this->validateUserData($data);
$this->userData = $data;
return $this;
}
public function retrieveUserID() {
return $this->userData['id'];
}
public function retrieveUserEmail() {
return idx($this->userData, 'email');
}
public function retrieveUserAccountName() {
return $this->userData['login'];
}
public function retrieveUserProfileImage() {
$uri = idx($this->userData, 'avatar_url');
if ($uri) {
return HTTPSFuture::loadContent($uri);
}
return null;
}
public function retrieveUserAccountURI() {
$username = $this->retrieveUserAccountName();
if (strlen($username)) {
return 'https://github.com/'.$username;
}
return null;
}
public function retrieveUserRealName() {
return idx($this->userData, 'name');
}
public function shouldDiagnoseAppLogin() {
return true;
}
}
diff --git a/src/applications/auth/oauth/provider/PhabricatorOAuthProviderGoogle.php b/src/applications/auth/oauth/provider/PhabricatorOAuthProviderGoogle.php
index 00cf66a030..6e4cbaced8 100644
--- a/src/applications/auth/oauth/provider/PhabricatorOAuthProviderGoogle.php
+++ b/src/applications/auth/oauth/provider/PhabricatorOAuthProviderGoogle.php
@@ -1,144 +1,128 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorOAuthProviderGoogle extends PhabricatorOAuthProvider {
private $userData;
public function getProviderKey() {
return self::PROVIDER_GOOGLE;
}
public function getProviderName() {
return 'Google';
}
public function isProviderEnabled() {
return PhabricatorEnv::getEnvConfig('google.auth-enabled');
}
public function isProviderLinkPermanent() {
return PhabricatorEnv::getEnvConfig('google.auth-permanent');
}
public function isProviderRegistrationEnabled() {
return PhabricatorEnv::getEnvConfig('google.registration-enabled');
}
public function getClientID() {
return PhabricatorEnv::getEnvConfig('google.application-id');
}
public function renderGetClientIDHelp() {
return null;
}
public function getClientSecret() {
return PhabricatorEnv::getEnvConfig('google.application-secret');
}
public function renderGetClientSecretHelp() {
return null;
}
public function getAuthURI() {
return 'https://accounts.google.com/o/oauth2/auth';
}
public function getTestURIs() {
return array(
'http://www.google.com'
);
}
public function getTokenURI() {
return 'https://accounts.google.com/o/oauth2/token';
}
protected function getTokenExpiryKey() {
return 'expires_in';
}
public function getUserInfoURI() {
return 'https://www.googleapis.com/oauth2/v1/userinfo';
}
public function getMinimumScope() {
$scopes = array(
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
);
return implode(' ', $scopes);
}
public function setUserData($data) {
$data = json_decode($data, true);
$this->validateUserData($data);
// Guess account name from email address, this is just a hint anyway.
$data['account'] = head(explode('@', $data['email']));
$this->userData = $data;
return $this;
}
public function retrieveUserID() {
return $this->userData['email'];
}
public function retrieveUserEmail() {
return $this->userData['email'];
}
public function retrieveUserAccountName() {
return $this->userData['account'];
}
public function retrieveUserProfileImage() {
// No apparent API access to Plus yet.
return null;
}
public function retrieveUserAccountURI() {
// No apparent API access to Plus yet.
return null;
}
public function retrieveUserRealName() {
return $this->userData['name'];
}
public function getExtraAuthParameters() {
return array(
'response_type' => 'code',
);
}
public function getExtraTokenParameters() {
return array(
'grant_type' => 'authorization_code',
);
}
public function decodeTokenResponse($response) {
return json_decode($response, true);
}
}
diff --git a/src/applications/auth/oauth/provider/PhabricatorOAuthProviderPhabricator.php b/src/applications/auth/oauth/provider/PhabricatorOAuthProviderPhabricator.php
index e4eafd62dd..fde311a552 100644
--- a/src/applications/auth/oauth/provider/PhabricatorOAuthProviderPhabricator.php
+++ b/src/applications/auth/oauth/provider/PhabricatorOAuthProviderPhabricator.php
@@ -1,147 +1,131 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorOAuthProviderPhabricator
extends PhabricatorOAuthProvider {
private $userData;
public function getExtraAuthParameters() {
return array(
'response_type' => 'code',
);
}
public function getExtraTokenParameters() {
return array(
'grant_type' => 'authorization_code',
);
}
public function decodeTokenResponse($response) {
$decoded = json_decode($response, true);
if (!is_array($decoded)) {
throw new Exception('Invalid token response.');
}
return $decoded;
}
public function getProviderKey() {
return self::PROVIDER_PHABRICATOR;
}
public function getProviderName() {
return 'Phabricator';
}
public function isProviderEnabled() {
return PhabricatorEnv::getEnvConfig('phabricator.auth-enabled');
}
public function isProviderLinkPermanent() {
return PhabricatorEnv::getEnvConfig('phabricator.auth-permanent');
}
public function isProviderRegistrationEnabled() {
return PhabricatorEnv::getEnvConfig('phabricator.registration-enabled');
}
public function getClientID() {
return PhabricatorEnv::getEnvConfig('phabricator.application-id');
}
public function renderGetClientIDHelp() {
return null;
}
public function getClientSecret() {
return PhabricatorEnv::getEnvConfig('phabricator.application-secret');
}
public function renderGetClientSecretHelp() {
return null;
}
public function getAuthURI() {
return $this->getURI('/oauthserver/auth/');
}
public function getTestURIs() {
return array(
$this->getURI('/'),
$this->getURI('/api/user.whoami/')
);
}
public function getTokenURI() {
return $this->getURI('/oauthserver/token/');
}
protected function getTokenExpiryKey() {
return 'expires_in';
}
public function getUserInfoURI() {
return $this->getURI('/api/user.whoami');
}
public function getMinimumScope() {
return 'whoami';
}
public function setUserData($data) {
// legacy conditionally strip shield. see D3265 for discussion.
if (strncmp($data, 'for(;;);', 8) === 0) {
$data = substr($data, 8);
}
$data = idx(json_decode($data, true), 'result');
$this->validateUserData($data);
$this->userData = $data;
return $this;
}
public function retrieveUserID() {
return $this->userData['phid'];
}
public function retrieveUserEmail() {
return $this->userData['email'];
}
public function retrieveUserAccountName() {
return $this->userData['userName'];
}
public function retrieveUserProfileImage() {
$uri = $this->userData['image'];
return HTTPSFuture::loadContent($uri);
}
public function retrieveUserAccountURI() {
return $this->userData['uri'];
}
public function retrieveUserRealName() {
return $this->userData['realName'];
}
private function getURI($path) {
return
rtrim(PhabricatorEnv::getEnvConfig('phabricator.oauth-uri'), '/') .
$path;
}
}
diff --git a/src/applications/auth/view/PhabricatorOAuthFailureView.php b/src/applications/auth/view/PhabricatorOAuthFailureView.php
index 1af0db2925..4853410dbd 100644
--- a/src/applications/auth/view/PhabricatorOAuthFailureView.php
+++ b/src/applications/auth/view/PhabricatorOAuthFailureView.php
@@ -1,104 +1,88 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorOAuthFailureView extends AphrontView {
private $request;
private $provider;
private $exception;
public function setRequest(AphrontRequest $request) {
$this->request = $request;
return $this;
}
public function setOAuthProvider($provider) {
$this->provider = $provider;
return $this;
}
public function setException(Exception $e) {
$this->exception = $e;
return $this;
}
public function render() {
$request = $this->request;
$provider = $this->provider;
$provider_name = $provider->getProviderName();
$diagnose = null;
$view = new AphrontRequestFailureView();
$view->setHeader($provider_name.' Auth Failed');
if ($this->request) {
$view->appendChild(
hsprintf(
'<p><strong>Description:</strong> %s</p>',
$request->getStr('error_description')));
$view->appendChild(
hsprintf(
'<p><strong>Error:</strong> %s</p>',
$request->getStr('error')));
$view->appendChild(
hsprintf(
'<p><strong>Error Reason:</strong> %s</p>',
$request->getStr('error_reason')));
} else if ($this->exception) {
$view->appendChild(
hsprintf(
'<p><strong>Error Details:</strong> %s</p>',
$this->exception->getMessage()));
} else {
// TODO: We can probably refine this.
$view->appendChild(
hsprintf(
'<p>Unable to authenticate with %s. '.
'There are several reasons this might happen:</p>'.
'<ul>'.
'<li>Phabricator may be configured with the wrong Application '.
'Secret; or</li>'.
'<li>the %s OAuth access token may have expired; or</li>'.
'<li>%s may have revoked authorization for the Application; '.
'or</li>'.
'<li>%s may be having technical problems.</li>'.
'</ul>'.
'<p>You can try again, or login using another method.</p>',
$provider_name,
$provider_name,
$provider_name,
$provider_name));
$provider_key = $provider->getProviderKey();
$diagnose = hsprintf(
'<a href="/oauth/'.$provider_key.'/diagnose/" class="button green">'.
'Diagnose %s OAuth Problems'.
'</a>',
$provider_name);
}
$view->appendChild(
'<div class="aphront-failure-continue">'.
$diagnose.
'<a href="/login/" class="button">Continue</a>'.
'</div>');
return $view->render();
}
}
diff --git a/src/applications/base/PhabricatorApplication.php b/src/applications/base/PhabricatorApplication.php
index 657739ef54..54a998a90c 100644
--- a/src/applications/base/PhabricatorApplication.php
+++ b/src/applications/base/PhabricatorApplication.php
@@ -1,205 +1,189 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @task info Application Information
* @task ui UI Integration
* @task uri URI Routing
* @task fact Fact Integration
* @task meta Application Management
* @group apps
*/
abstract class PhabricatorApplication {
const GROUP_CORE = 'core';
const GROUP_COMMUNICATION = 'communication';
const GROUP_ORGANIZATION = 'organization';
const GROUP_UTILITIES = 'util';
const GROUP_ADMIN = 'admin';
const GROUP_DEVELOPER = 'developer';
const GROUP_MISC = 'misc';
public static function getApplicationGroups() {
return array(
self::GROUP_CORE => pht('Core Applications'),
self::GROUP_COMMUNICATION => pht('Communication'),
self::GROUP_ORGANIZATION => pht('Organization'),
self::GROUP_UTILITIES => pht('Utilities'),
self::GROUP_ADMIN => pht('Administration'),
self::GROUP_DEVELOPER => pht('Developer Tools'),
self::GROUP_MISC => pht('Miscellaneous Applications'),
);
}
/* -( Application Information )-------------------------------------------- */
public function getName() {
return substr(get_class($this), strlen('PhabricatorApplication'));
}
public function getShortDescription() {
return $this->getName().' Application';
}
public function isEnabled() {
return true;
}
public function getPHID() {
return 'PHID-APPS-'.get_class($this);
}
public function getTypeaheadURI() {
return $this->getBaseURI();
}
public function getBaseURI() {
return null;
}
public function getIconURI() {
return null;
}
public function getAutospriteName() {
return 'default';
}
public function shouldAppearInLaunchView() {
return true;
}
public function getApplicationOrder() {
return PHP_INT_MAX;
}
public function getApplicationGroup() {
return self::GROUP_MISC;
}
public function getTitleGlyph() {
return null;
}
public function getHelpURI() {
// TODO: When these applications get created, link to their docs:
//
// - Drydock
// - OAuth Server
return null;
}
public function getEventListeners() {
return array();
}
/* -( URI Routing )-------------------------------------------------------- */
public function getRoutes() {
return array();
}
/* -( Fact Integration )--------------------------------------------------- */
public function getFactObjectsForAnalysis() {
return array();
}
/* -( UI Integration )----------------------------------------------------- */
/**
* Render status elements (like "3 Waiting Reviews") for application list
* views. These provide a way to alert users to new or pending action items
* in applications.
*
* @param PhabricatorUser Viewing user.
* @return list<PhabricatorApplicationStatusView> Application status elements.
* @task ui
*/
public function loadStatus(PhabricatorUser $user) {
return array();
}
/**
* You can provide an optional piece of flavor text for the application. This
* is currently rendered in application launch views if the application has no
* status elements.
*
* @return string|null Flavor text.
* @task ui
*/
public function getFlavorText() {
return null;
}
/**
* Build items for the main menu.
*
* @param PhabricatorUser The viewing user.
* @param AphrontController The current controller. May be null for special
* pages like 404, exception handlers, etc.
* @return list<PhabricatorMainMenuIconView> List of menu items.
* @task ui
*/
public function buildMainMenuItems(
PhabricatorUser $user,
PhabricatorController $controller = null) {
return array();
}
/* -( Application Management )--------------------------------------------- */
public static function getAllInstalledApplications() {
static $applications;
if (empty($applications)) {
$classes = id(new PhutilSymbolLoader())
->setAncestorClass(__CLASS__)
->setConcreteOnly(true)
->selectAndLoadSymbols();
$apps = array();
foreach ($classes as $class) {
$app = newv($class['name'], array());
if (!$app->isEnabled()) {
continue;
}
$apps[] = $app;
}
$applications = $apps;
}
return $applications;
}
}
diff --git a/src/applications/base/controller/Phabricator404Controller.php b/src/applications/base/controller/Phabricator404Controller.php
index 643ffdcfd6..1aa785a040 100644
--- a/src/applications/base/controller/Phabricator404Controller.php
+++ b/src/applications/base/controller/Phabricator404Controller.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class Phabricator404Controller extends PhabricatorController {
public function processRequest() {
return new Aphront404Response();
}
}
diff --git a/src/applications/base/controller/PhabricatorController.php b/src/applications/base/controller/PhabricatorController.php
index 1538246ecf..e7ae1d0f8d 100644
--- a/src/applications/base/controller/PhabricatorController.php
+++ b/src/applications/base/controller/PhabricatorController.php
@@ -1,258 +1,242 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorController extends AphrontController {
private $handles;
public function shouldRequireLogin() {
// If this install is configured to allow public resources and the
// controller works in public mode, allow the request through.
$is_public_allowed = PhabricatorEnv::getEnvConfig('policy.allow-public');
if ($is_public_allowed && $this->shouldAllowPublic()) {
return false;
}
return true;
}
public function shouldRequireAdmin() {
return false;
}
public function shouldRequireEnabledUser() {
return true;
}
public function shouldAllowPublic() {
return false;
}
public function shouldRequireEmailVerification() {
$need_verify = PhabricatorUserEmail::isEmailVerificationRequired();
$need_login = $this->shouldRequireLogin();
return ($need_login && $need_verify);
}
final public function willBeginExecution() {
$request = $this->getRequest();
$user = new PhabricatorUser();
$phusr = $request->getCookie('phusr');
$phsid = $request->getCookie('phsid');
if (strlen($phusr) && $phsid) {
$info = queryfx_one(
$user->establishConnection('r'),
'SELECT u.* FROM %T u JOIN %T s ON u.phid = s.userPHID
AND s.type LIKE %> AND s.sessionKey = %s',
$user->getTableName(),
'phabricator_session',
'web-',
$phsid);
if ($info) {
$user->loadFromArray($info);
}
}
$translation = $user->getTranslation();
if ($translation &&
$translation != PhabricatorEnv::getEnvConfig('translation.provider')) {
$translation = newv($translation, array());
PhutilTranslator::getInstance()
->setLanguage($translation->getLanguage())
->addTranslations($translation->getTranslations());
}
$request->setUser($user);
if ($user->getIsDisabled() && $this->shouldRequireEnabledUser()) {
$disabled_user_controller = new PhabricatorDisabledUserController(
$request);
return $this->delegateToController($disabled_user_controller);
}
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_CONTROLLER_CHECKREQUEST,
array(
'request' => $request,
'controller' => get_class($this),
));
$event->setUser($user);
PhutilEventEngine::dispatchEvent($event);
$checker_controller = $event->getValue('controller');
if ($checker_controller != get_class($this)) {
return $this->delegateToController($checker_controller);
}
if (PhabricatorEnv::getEnvConfig('darkconsole.enabled')) {
if ($user->getConsoleEnabled() ||
PhabricatorEnv::getEnvConfig('darkconsole.always-on')) {
$console = new DarkConsoleCore();
$request->getApplicationConfiguration()->setConsole($console);
}
}
if ($this->shouldRequireLogin() && !$user->getPHID()) {
$login_controller = new PhabricatorLoginController($request);
return $this->delegateToController($login_controller);
}
if ($this->shouldRequireEmailVerification()) {
$email = $user->loadPrimaryEmail();
if (!$email) {
throw new Exception(
"No primary email address associated with this account!");
}
if (!$email->getIsVerified()) {
$verify_controller = new PhabricatorMustVerifyEmailController($request);
return $this->delegateToController($verify_controller);
}
}
if ($this->shouldRequireAdmin() && !$user->getIsAdmin()) {
return new Aphront403Response();
}
}
public function buildStandardPageView() {
$view = new PhabricatorStandardPageView();
$view->setRequest($this->getRequest());
$view->setController($this);
return $view;
}
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->appendChild($view);
$response = new AphrontWebpageResponse();
$response->setContent($page->render());
return $response;
}
public function getApplicationURI($path = '') {
if (!$this->getCurrentApplication()) {
throw new Exception("No application!");
}
return $this->getCurrentApplication()->getBaseURI().ltrim($path, '/');
}
public function buildApplicationPage($view, array $options) {
$page = $this->buildStandardPageView();
$application = $this->getCurrentApplication();
if ($application) {
$page->setApplicationName($application->getName());
$page->setTitle(idx($options, 'title'));
if ($application->getTitleGlyph()) {
$page->setGlyph($application->getTitleGlyph());
}
}
if (!($view instanceof AphrontSideNavFilterView)) {
$nav = new AphrontSideNavFilterView();
$nav->appendChild($view);
$view = $nav;
}
if ($application) {
$view->setCurrentApplication($application);
}
$view->setUser($this->getRequest()->getUser());
$view->setFlexNav(true);
$view->setShowApplicationMenu(true);
$page->appendChild($view);
if (idx($options, 'device')) {
$page->setDeviceReady(true);
$view->appendChild($page->renderFooter());
}
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
public function didProcessRequest($response) {
$request = $this->getRequest();
$response->setRequest($request);
if ($response instanceof AphrontDialogResponse) {
if (!$request->isAjax()) {
$view = new PhabricatorStandardPageView();
$view->setRequest($request);
$view->setController($this);
$view->appendChild(
'<div style="padding: 2em 0;">'.
$response->buildResponseString().
'</div>');
$response = new AphrontWebpageResponse();
$response->setContent($view->render());
return $response;
} else {
return id(new AphrontAjaxResponse())
->setContent(array(
'dialog' => $response->buildResponseString(),
));
}
} else if ($response instanceof AphrontRedirectResponse) {
if ($request->isAjax()) {
return id(new AphrontAjaxResponse())
->setContent(
array(
'redirect' => $response->getURI(),
));
}
}
return $response;
}
protected function getHandle($phid) {
if (empty($this->handles[$phid])) {
throw new Exception(
"Attempting to access handle which wasn't loaded: {$phid}");
}
return $this->handles[$phid];
}
protected function loadHandles(array $phids) {
$phids = array_filter($phids);
$this->handles = $this->loadViewerHandles($phids);
return $this;
}
protected function loadViewerHandles(array $phids) {
return id(new PhabricatorObjectHandleData($phids))
->setViewer($this->getRequest()->getUser())
->loadHandles();
}
protected function renderHandlesForPHIDs(array $phids) {
$items = array();
foreach ($phids as $phid) {
$items[] = $this->getHandle($phid)->renderLink();
}
return implode('<br />', $items);
}
}
diff --git a/src/applications/base/controller/PhabricatorRedirectController.php b/src/applications/base/controller/PhabricatorRedirectController.php
index a841554879..19a2460e9a 100644
--- a/src/applications/base/controller/PhabricatorRedirectController.php
+++ b/src/applications/base/controller/PhabricatorRedirectController.php
@@ -1,39 +1,23 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorRedirectController extends PhabricatorController {
private $uri;
public function shouldRequireLogin() {
return false;
}
public function shouldRequireEnabledUser() {
return false;
}
public function willProcessRequest(array $data) {
$this->uri = $data['uri'];
}
public function processRequest() {
return id(new AphrontRedirectResponse())->setURI($this->uri);
}
}
diff --git a/src/applications/cache/storage/PhabricatorCacheDAO.php b/src/applications/cache/storage/PhabricatorCacheDAO.php
index 4d4dc4a72f..eff57d77f7 100644
--- a/src/applications/cache/storage/PhabricatorCacheDAO.php
+++ b/src/applications/cache/storage/PhabricatorCacheDAO.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorCacheDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'cache';
}
}
diff --git a/src/applications/cache/storage/PhabricatorMarkupCache.php b/src/applications/cache/storage/PhabricatorMarkupCache.php
index 64d5c6f22e..ba0b42ca8b 100644
--- a/src/applications/cache/storage/PhabricatorMarkupCache.php
+++ b/src/applications/cache/storage/PhabricatorMarkupCache.php
@@ -1,34 +1,18 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorMarkupCache extends PhabricatorCacheDAO {
protected $cacheKey;
protected $cacheData;
protected $metadata;
public function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'cacheData' => self::SERIALIZATION_PHP,
'metadata' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
}
diff --git a/src/applications/calendar/application/PhabricatorApplicationCalendar.php b/src/applications/calendar/application/PhabricatorApplicationCalendar.php
index a819d84af6..94161557ba 100644
--- a/src/applications/calendar/application/PhabricatorApplicationCalendar.php
+++ b/src/applications/calendar/application/PhabricatorApplicationCalendar.php
@@ -1,58 +1,42 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationCalendar extends PhabricatorApplication {
public function getShortDescription() {
return pht('Dates and Stuff');
}
public function getFlavorText() {
return pht('Never miss an episode ever again.');
}
public function getBaseURI() {
return '/calendar/';
}
public function getTitleGlyph() {
// Unicode has a calendar character but it's in some distant code plane,
// use "keyboard" since it looks vaguely similar.
return "\xE2\x8C\xA8";
}
public function getRoutes() {
return array(
'/calendar/' => array(
'' => 'PhabricatorCalendarBrowseController',
'status/' => array(
'' => 'PhabricatorCalendarViewStatusController',
'create/' =>
'PhabricatorCalendarEditStatusController',
'delete/(?P<id>[1-9]\d*)/' =>
'PhabricatorCalendarDeleteStatusController',
'edit/(?P<id>[1-9]\d*)/' =>
'PhabricatorCalendarEditStatusController',
'view/(?P<phid>[^/]+)/' =>
'PhabricatorCalendarViewStatusController',
),
),
);
}
}
diff --git a/src/applications/calendar/controller/PhabricatorCalendarBrowseController.php b/src/applications/calendar/controller/PhabricatorCalendarBrowseController.php
index e74f021bda..017f9fe2c4 100644
--- a/src/applications/calendar/controller/PhabricatorCalendarBrowseController.php
+++ b/src/applications/calendar/controller/PhabricatorCalendarBrowseController.php
@@ -1,107 +1,91 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorCalendarBrowseController
extends PhabricatorCalendarController {
public function processRequest() {
$now = time();
$request = $this->getRequest();
$user = $request->getUser();
$year_d = phabricator_format_local_time($now, $user, 'Y');
$year = $request->getInt('year', $year_d);
$month_d = phabricator_format_local_time($now, $user, 'm');
$month = $request->getInt('month', $month_d);
$holidays = id(new PhabricatorCalendarHoliday())->loadAllWhere(
'day BETWEEN %s AND %s',
"{$year}-{$month}-01",
"{$year}-{$month}-31");
$statuses = id(new PhabricatorUserStatus())
->loadAllWhere(
'dateTo >= %d AND dateFrom <= %d',
strtotime("{$year}-{$month}-01"),
strtotime("{$year}-{$month}-01 next month"));
$month_view = new AphrontCalendarMonthView($month, $year);
$month_view->setBrowseURI($request->getRequestURI());
$month_view->setUser($user);
$month_view->setHolidays($holidays);
$phids = mpull($statuses, 'getUserPHID');
$handles = $this->loadViewerHandles($phids);
foreach ($statuses as $status) {
$event = new AphrontCalendarEventView();
$event->setEpochRange($status->getDateFrom(), $status->getDateTo());
$name_text = $handles[$status->getUserPHID()]->getName();
$status_text = $status->getTextStatus();
$event->setUserPHID($status->getUserPHID());
$event->setName("{$name_text} ({$status_text})");
$details = '';
if ($status->getDescription()) {
$details = "\n\n".rtrim(phutil_escape_html($status->getDescription()));
}
$event->setDescription(
$status->getTerseSummary($user).$details
);
$month_view->addEvent($event);
}
$nav = $this->buildSideNavView();
$nav->selectFilter('edit');
$nav->appendChild(
array(
$this->getNoticeView(),
'<div style="padding: 2em;">',
$month_view,
'</div>',
));
return $this->buildApplicationPage(
$nav,
array(
'title' => 'Calendar',
'device' => true,
));
}
private function getNoticeView() {
$request = $this->getRequest();
$view = null;
if ($request->getExists('created')) {
$view = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
->setTitle(pht('Successfully created your status.'));
} else if ($request->getExists('updated')) {
$view = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
->setTitle(pht('Successfully updated your status.'));
} else if ($request->getExists('deleted')) {
$view = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
->setTitle(pht('Successfully deleted your status.'));
}
return $view;
}
}
diff --git a/src/applications/calendar/controller/PhabricatorCalendarController.php b/src/applications/calendar/controller/PhabricatorCalendarController.php
index d2ff7ec9bf..67582998be 100644
--- a/src/applications/calendar/controller/PhabricatorCalendarController.php
+++ b/src/applications/calendar/controller/PhabricatorCalendarController.php
@@ -1,43 +1,27 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorCalendarController extends PhabricatorController {
protected function buildSideNavView(PhabricatorUserStatus $status = null) {
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
$nav->addFilter('', pht('Calendar'), $this->getApplicationURI());
$nav->addSpacer();
$nav->addLabel(pht('Create Events'));
$nav->addFilter('status/create/', pht('New Status'));
$nav->addSpacer();
$nav->addLabel(pht('Your Events'));
if ($status && $status->getID()) {
$nav->addFilter('status/edit/'.$status->getID().'/', pht('Edit Status'));
}
$nav->addFilter('status/', pht('Upcoming Statuses'));
return $nav;
}
}
diff --git a/src/applications/calendar/controller/PhabricatorCalendarDeleteStatusController.php b/src/applications/calendar/controller/PhabricatorCalendarDeleteStatusController.php
index 673a2cd734..133b300321 100644
--- a/src/applications/calendar/controller/PhabricatorCalendarDeleteStatusController.php
+++ b/src/applications/calendar/controller/PhabricatorCalendarDeleteStatusController.php
@@ -1,70 +1,54 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorCalendarDeleteStatusController
extends PhabricatorCalendarController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$status = id(new PhabricatorUserStatus())
->loadOneWhere('id = %d', $this->id);
if (!$status) {
return new Aphront404Response();
}
if ($status->getUserPHID() != $user->getPHID()) {
return new Aphront403Response();
}
if ($request->isFormPost()) {
$status->delete();
$uri = new PhutilURI($this->getApplicationURI());
$uri->setQueryParams(
array(
'deleted' => true,
)
);
return id(new AphrontRedirectResponse())
->setURI($uri);
}
$dialog = new AphrontDialogView();
$dialog->setUser($user);
$dialog->setTitle(pht('Really delete status?'));
$dialog->appendChild(phutil_render_tag(
'p',
array(),
pht('Permanently delete this status? This action can not be undone.')
));
$dialog->addSubmitButton(pht('Delete'));
$dialog->addCancelButton(
$this->getApplicationURI('status/edit/'.$status->getID().'/')
);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/calendar/controller/PhabricatorCalendarEditStatusController.php b/src/applications/calendar/controller/PhabricatorCalendarEditStatusController.php
index ec00701c05..5acdd260fb 100644
--- a/src/applications/calendar/controller/PhabricatorCalendarEditStatusController.php
+++ b/src/applications/calendar/controller/PhabricatorCalendarEditStatusController.php
@@ -1,168 +1,152 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorCalendarEditStatusController
extends PhabricatorCalendarController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function isCreate() {
return !$this->id;
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$start_time = id(new AphrontFormDateControl())
->setUser($user)
->setName('start')
->setLabel(pht('Start'))
->setInitialTime(AphrontFormDateControl::TIME_START_OF_DAY);
$end_time = id(new AphrontFormDateControl())
->setUser($user)
->setName('end')
->setLabel(pht('End'))
->setInitialTime(AphrontFormDateControl::TIME_END_OF_DAY);
if ($this->isCreate()) {
$status = new PhabricatorUserStatus();
$end_value = $end_time->readValueFromRequest($request);
$start_value = $start_time->readValueFromRequest($request);
$submit_label = pht('Create');
$filter = 'status/create/';
$page_title = pht('Create Status');
$redirect = 'created';
} else {
$status = id(new PhabricatorUserStatus())
->loadOneWhere('id = %d', $this->id);
$end_time->setValue($status->getDateTo());
$start_time->setValue($status->getDateFrom());
$submit_label = pht('Update');
$filter = 'status/edit/'.$status->getID().'/';
$page_title = pht('Update Status');
$redirect = 'updated';
if ($status->getUserPHID() != $user->getPHID()) {
return new Aphront403Response();
}
}
$errors = array();
if ($request->isFormPost()) {
$type = $request->getInt('status');
$start_value = $start_time->readValueFromRequest($request);
$end_value = $end_time->readValueFromRequest($request);
$description = $request->getStr('description');
try {
$status
->setUserPHID($user->getPHID())
->setStatus($type)
->setDateFrom($start_value)
->setDateTo($end_value)
->setDescription($description)
->save();
} catch (PhabricatorUserStatusInvalidEpochException $e) {
$errors[] = 'Start must be before end.';
} catch (PhabricatorUserStatusOverlapException $e) {
$errors[] = 'There is already a status within the specified '.
'timeframe. Edit or delete this existing status.';
}
if (!$errors) {
$uri = new PhutilURI($this->getApplicationURI());
$uri->setQueryParams(
array(
'month' => phabricator_format_local_time($status->getDateFrom(),
$user,
'm'),
'year' => phabricator_format_local_time($status->getDateFrom(),
$user,
'Y'),
$redirect => true,
)
);
return id(new AphrontRedirectResponse())
->setURI($uri);
}
}
$error_view = null;
if ($errors) {
$error_view = id(new AphrontErrorView())
->setTitle('Status can not be set!')
->setErrors($errors);
}
$status_select = id(new AphrontFormSelectControl())
->setLabel(pht('Status'))
->setName('status')
->setOptions($status->getStatusOptions());
$description = id(new AphrontFormTextAreaControl())
->setLabel(pht('Description'))
->setName('description')
->setValue($status->getDescription());
$form = id(new AphrontFormView())
->setUser($user)
->setFlexible(true)
->appendChild($status_select)
->appendChild($start_time)
->appendChild($end_time)
->appendChild($description);
$submit = id(new AphrontFormSubmitControl())
->setValue($submit_label);
if ($this->isCreate()) {
$submit->addCancelButton($this->getApplicationURI());
} else {
$submit->addCancelButton(
$this->getApplicationURI('status/delete/'.$status->getID().'/'),
'Delete Status'
);
}
$form->appendChild($submit);
$nav = $this->buildSideNavView($status);
$nav->selectFilter($filter);
$nav->appendChild(
array(
id(new PhabricatorHeaderView())->setHeader($page_title),
$error_view,
$form,
)
);
return $this->buildApplicationPage(
$nav,
array(
'title' => $page_title,
'device' => true
)
);
}
}
diff --git a/src/applications/calendar/controller/PhabricatorCalendarViewStatusController.php b/src/applications/calendar/controller/PhabricatorCalendarViewStatusController.php
index 099147bcff..79943839de 100644
--- a/src/applications/calendar/controller/PhabricatorCalendarViewStatusController.php
+++ b/src/applications/calendar/controller/PhabricatorCalendarViewStatusController.php
@@ -1,141 +1,125 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorCalendarViewStatusController
extends PhabricatorCalendarController {
private $phid;
public function willProcessRequest(array $data) {
$user = $this->getRequest()->getUser();
$this->phid = idx($data, 'phid', $user->getPHID());
$this->loadHandles(array($this->phid));
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$handle = $this->getHandle($this->phid);
$statuses = id(new PhabricatorUserStatus())
->loadAllWhere('userPHID = %s AND dateTo > UNIX_TIMESTAMP()',
$this->phid);
$nav = $this->buildSideNavView();
$nav->selectFilter($this->getFilter());
$page_title = $this->getPageTitle();
$status_list = $this->buildStatusList($statuses);
$status_list->setNoDataString($this->getNoDataString());
$nav->appendChild(
array(
id(new PhabricatorHeaderView())->setHeader($page_title),
$status_list,
)
);
return $this->buildApplicationPage(
$nav,
array(
'title' => $page_title,
'device' => true
)
);
}
private function buildStatusList(array $statuses) {
assert_instances_of($statuses, 'PhabricatorUserStatus');
$user = $this->getRequest()->getUser();
$list = new PhabricatorObjectItemListView();
foreach ($statuses as $status) {
if ($status->getUserPHID() == $user->getPHID()) {
$href = $this->getApplicationURI('/status/edit/'.$status->getID().'/');
} else {
$from = $status->getDateFrom();
$month = phabricator_format_local_time($from, $user, 'm');
$year = phabricator_format_local_time($from, $user, 'Y');
$uri = new PhutilURI($this->getApplicationURI());
$uri->setQueryParams(
array(
'month' => $month,
'year' => $year,
)
);
$href = (string) $uri;
}
$from = phabricator_datetime($status->getDateFrom(), $user);
$to = phabricator_datetime($status->getDateTo(), $user);
$item = id(new PhabricatorObjectItemView())
->setHeader($status->getTerseSummary($user))
->setHref($href)
->addDetail(
pht('Description'),
$status->getDescription())
->addAttribute(pht('From %s', $from))
->addAttribute(pht('To %s', $to));
$list->addItem($item);
}
return $list;
}
private function getNoDataString() {
if ($this->isUserRequest()) {
$no_data =
pht('You do not have any upcoming status events.');
} else {
$no_data =
pht('%s does not have any upcoming status events.',
phutil_escape_html($this->getHandle($this->phid)->getName()));
}
return $no_data;
}
private function getFilter() {
if ($this->isUserRequest()) {
$filter = 'status/';
} else {
$filter = 'status/view/'.$this->phid.'/';
}
return $filter;
}
private function getPageTitle() {
if ($this->isUserRequest()) {
$page_title = pht('Upcoming Statuses');
} else {
$page_title = pht(
'Upcoming Statuses for %s',
phutil_escape_html($this->getHandle($this->phid)->getName())
);
}
return $page_title;
}
private function isUserRequest() {
$user = $this->getRequest()->getUser();
return $this->phid == $user->getPHID();
}
}
diff --git a/src/applications/calendar/storage/PhabricatorCalendarDAO.php b/src/applications/calendar/storage/PhabricatorCalendarDAO.php
index 800ff0ccbe..8aa18a427c 100644
--- a/src/applications/calendar/storage/PhabricatorCalendarDAO.php
+++ b/src/applications/calendar/storage/PhabricatorCalendarDAO.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorCalendarDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'calendar';
}
}
diff --git a/src/applications/calendar/storage/PhabricatorCalendarHoliday.php b/src/applications/calendar/storage/PhabricatorCalendarHoliday.php
index 242fff57f9..2de0a63cd5 100644
--- a/src/applications/calendar/storage/PhabricatorCalendarHoliday.php
+++ b/src/applications/calendar/storage/PhabricatorCalendarHoliday.php
@@ -1,48 +1,32 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorCalendarHoliday extends PhabricatorCalendarDAO {
protected $day;
protected $name;
public function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
) + parent::getConfiguration();
}
public static function getNthBusinessDay($epoch, $n) {
// Sadly, there are not many holidays. So we can load all of them.
$holidays = id(new PhabricatorCalendarHoliday())->loadAll();
$holidays = mpull($holidays, null, 'getDay');
$interval = ($n > 0 ? 1 : -1) * 24 * 60 * 60;
$return = $epoch;
for ($i = abs($n); $i > 0; ) {
$return += $interval;
$weekday = date('w', $return);
if ($weekday != 0 && $weekday != 6 && // Sunday and Saturday
!isset($holidays[date('Y-m-d', $return)])) {
$i--;
}
}
return $return;
}
}
diff --git a/src/applications/calendar/storage/__tests__/PhabricatorCalendarHolidayTestCase.php b/src/applications/calendar/storage/__tests__/PhabricatorCalendarHolidayTestCase.php
index be0a51b71a..40ec7f0aac 100644
--- a/src/applications/calendar/storage/__tests__/PhabricatorCalendarHolidayTestCase.php
+++ b/src/applications/calendar/storage/__tests__/PhabricatorCalendarHolidayTestCase.php
@@ -1,55 +1,39 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorCalendarHolidayTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
protected function willRunTests() {
parent::willRunTests();
id(new PhabricatorCalendarHoliday())
->setDay('2012-01-02')
->setName('International Testing Day')
->save();
}
public function testNthBusinessDay() {
$map = array(
array('2011-12-30', 1, '2012-01-03'),
array('2012-01-01', 1, '2012-01-03'),
array('2012-01-01', 0, '2012-01-01'),
array('2012-01-01', -1, '2011-12-30'),
array('2012-01-04', -1, '2012-01-03'),
);
foreach ($map as $val) {
list($date, $n, $expect) = $val;
$actual = PhabricatorCalendarHoliday::getNthBusinessDay(
strtotime($date),
$n);
$this->assertEqual(
$expect,
date('Y-m-d', $actual),
"{$n} business days since '{$date}'");
}
}
}
diff --git a/src/applications/calendar/view/AphrontCalendarEventView.php b/src/applications/calendar/view/AphrontCalendarEventView.php
index dd079ac7db..60c7fb0f5c 100644
--- a/src/applications/calendar/view/AphrontCalendarEventView.php
+++ b/src/applications/calendar/view/AphrontCalendarEventView.php
@@ -1,72 +1,56 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontCalendarEventView extends AphrontView {
private $userPHID;
private $name;
private $epochStart;
private $epochEnd;
private $description;
public function setUserPHID($user_phid) {
$this->userPHID = $user_phid;
return $this;
}
public function getUserPHID() {
return $this->userPHID;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function setEpochRange($start, $end) {
$this->epochStart = $start;
$this->epochEnd = $end;
return $this;
}
public function getEpochStart() {
return $this->epochStart;
}
public function getEpochEnd() {
return $this->epochEnd;
}
public function getName() {
return $this->name;
}
public function setDescription($description) {
$this->description = $description;
return $this;
}
public function getDescription() {
return $this->description;
}
public function render() {
throw new Exception("Events are only rendered indirectly.");
}
}
diff --git a/src/applications/calendar/view/AphrontCalendarMonthView.php b/src/applications/calendar/view/AphrontCalendarMonthView.php
index 65c310dd14..bf5bfd8e25 100644
--- a/src/applications/calendar/view/AphrontCalendarMonthView.php
+++ b/src/applications/calendar/view/AphrontCalendarMonthView.php
@@ -1,328 +1,312 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontCalendarMonthView extends AphrontView {
private $user;
private $month;
private $year;
private $holidays = array();
private $events = array();
private $browseURI;
public function setBrowseURI($browse_uri) {
$this->browseURI = $browse_uri;
return $this;
}
private function getBrowseURI() {
return $this->browseURI;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function addEvent(AphrontCalendarEventView $event) {
$this->events[] = $event;
return $this;
}
public function setHolidays(array $holidays) {
assert_instances_of($holidays, 'PhabricatorCalendarHoliday');
$this->holidays = mpull($holidays, null, 'getDay');
return $this;
}
public function __construct($month, $year) {
$this->month = $month;
$this->year = $year;
}
public function render() {
if (empty($this->user)) {
throw new Exception("Call setUser() before render()!");
}
$events = msort($this->events, 'getEpochStart');
$days = $this->getDatesInMonth();
require_celerity_resource('aphront-calendar-view-css');
$first = reset($days);
$empty = $first->format('w');
$markup = array();
$empty_box =
'<div class="aphront-calendar-day aphront-calendar-empty">'.
'</div>';
for ($ii = 0; $ii < $empty; $ii++) {
$markup[] = $empty_box;
}
$show_events = array();
foreach ($days as $day) {
$day_number = $day->format('j');
$holiday = idx($this->holidays, $day->format('Y-m-d'));
$class = 'aphront-calendar-day';
$weekday = $day->format('w');
if ($holiday || $weekday == 0 || $weekday == 6) {
$class .= ' aphront-calendar-not-work-day';
}
$day->setTime(0, 0, 0);
$epoch_start = $day->format('U');
$day->modify('+1 day');
$epoch_end = $day->format('U');
if ($weekday == 0) {
$show_events = array();
} else {
$show_events = array_fill_keys(
array_keys($show_events),
'<div class="aphront-calendar-event aphront-calendar-event-empty">'.
'&nbsp;'.
'</div>');
}
foreach ($events as $event) {
if ($event->getEpochStart() >= $epoch_end) {
// This list is sorted, so we can stop looking.
break;
}
if ($event->getEpochStart() < $epoch_end &&
$event->getEpochEnd() > $epoch_start) {
$show_events[$event->getUserPHID()] = $this->renderEvent(
$event,
$epoch_start,
$epoch_end);
}
}
$holiday_markup = null;
if ($holiday) {
$name = phutil_escape_html($holiday->getName());
$holiday_markup =
'<div class="aphront-calendar-holiday" title="'.$name.'">'.
$name.
'</div>';
}
$markup[] =
'<div class="'.$class.'">'.
'<div class="aphront-calendar-date-number">'.
$day_number.
'</div>'.
$holiday_markup.
implode("\n", $show_events).
'</div>';
}
$table = array();
$rows = array_chunk($markup, 7);
foreach ($rows as $row) {
$table[] = '<tr>';
while (count($row) < 7) {
$row[] = $empty_box;
}
foreach ($row as $cell) {
$table[] = '<td>'.$cell.'</td>';
}
$table[] = '</tr>';
}
$table =
'<table class="aphront-calendar-view">'.
$this->renderCalendarHeader($first).
'<tr class="aphront-calendar-day-of-week-header">'.
'<th>Sun</th>'.
'<th>Mon</th>'.
'<th>Tue</th>'.
'<th>Wed</th>'.
'<th>Thu</th>'.
'<th>Fri</th>'.
'<th>Sat</th>'.
'</tr>'.
implode("\n", $table).
'</table>';
return $table;
}
private function renderCalendarHeader(DateTime $date) {
$colspan = 7;
$left_th = '';
$right_th = '';
// check for a browseURI, which means we need "fancy" prev / next UI
$uri = $this->getBrowseURI();
if ($uri) {
$colspan = 5;
$uri = new PhutilURI($uri);
list($prev_year, $prev_month) = $this->getPrevYearAndMonth();
$query = array('year' => $prev_year, 'month' => $prev_month);
$prev_link = phutil_render_tag(
'a',
array('href' => (string) $uri->setQueryParams($query)),
'&larr;'
);
list($next_year, $next_month) = $this->getNextYearAndMonth();
$query = array('year' => $next_year, 'month' => $next_month);
$next_link = phutil_render_tag(
'a',
array('href' => (string) $uri->setQueryParams($query)),
'&rarr;'
);
$left_th = '<th>'.$prev_link.'</th>';
$right_th = '<th>'.$next_link.'</th>';
}
return
'<tr class="aphront-calendar-month-year-header">'.
$left_th.
'<th colspan="'.$colspan.'">'.$date->format('F Y').'</th>'.
$right_th.
'</tr>';
}
private function getNextYearAndMonth() {
$month = $this->month;
$year = $this->year;
$next_year = $year;
$next_month = $month + 1;
if ($next_month == 13) {
$next_year = $year + 1;
$next_month = 1;
}
return array($next_year, $next_month);
}
private function getPrevYearAndMonth() {
$month = $this->month;
$year = $this->year;
$prev_year = $year;
$prev_month = $month - 1;
if ($prev_month == 0) {
$prev_year = $year - 1;
$prev_month = 12;
}
return array($prev_year, $prev_month);
}
/**
* Return a DateTime object representing the first moment in each day in the
* month, according to the user's locale.
*
* @return list List of DateTimes, one for each day.
*/
private function getDatesInMonth() {
$user = $this->user;
$timezone = new DateTimeZone($user->getTimezoneIdentifier());
$month = $this->month;
$year = $this->year;
// Get the year and month numbers of the following month, so we can
// determine when this month ends.
list($next_year, $next_month) = $this->getNextYearAndMonth();
$end_date = new DateTime("{$next_year}-{$next_month}-01", $timezone);
$end_epoch = $end_date->format('U');
$days = array();
for ($day = 1; $day <= 31; $day++) {
$day_date = new DateTime("{$year}-{$month}-{$day}", $timezone);
$day_epoch = $day_date->format('U');
if ($day_epoch >= $end_epoch) {
break;
} else {
$days[] = $day_date;
}
}
return $days;
}
private function renderEvent(
AphrontCalendarEventView $event,
$epoch_start,
$epoch_end) {
$user = $this->user;
$event_start = $event->getEpochStart();
$event_end = $event->getEpochEnd();
$classes = array();
$when = array();
$classes[] = 'aphront-calendar-event';
if ($event_start < $epoch_start) {
$classes[] = 'aphront-calendar-event-continues-before';
$when[] = 'Started '.phabricator_datetime($event_start, $user);
} else {
$when[] = 'Starts at '.phabricator_time($event_start, $user);
}
if ($event_end > $epoch_end) {
$classes[] = 'aphront-calendar-event-continues-after';
$when[] = 'Ends '.phabricator_datetime($event_end, $user);
} else {
$when[] = 'Ends at '.phabricator_time($event_end, $user);
}
Javelin::initBehavior('phabricator-tooltips');
$info = $event->getName();
if ($event->getDescription()) {
$info .= "\n\n".$event->getDescription();
}
$text_div = javelin_render_tag(
'div',
array(
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $info."\n\n".implode("\n", $when),
'size' => 240,
),
'class' => 'aphront-calendar-event-text',
),
phutil_escape_html(phutil_utf8_shorten($event->getName(), 32)));
return javelin_render_tag(
'div',
array(
'class' => implode(' ', $classes),
),
$text_div);
}
}
diff --git a/src/applications/chatlog/PhabricatorChatLogQuery.php b/src/applications/chatlog/PhabricatorChatLogQuery.php
index 06e1f1ee5b..93674edec8 100644
--- a/src/applications/chatlog/PhabricatorChatLogQuery.php
+++ b/src/applications/chatlog/PhabricatorChatLogQuery.php
@@ -1,73 +1,57 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorChatLogQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $channels;
private $maximumEpoch;
public function withChannels(array $channels) {
$this->channels = $channels;
return $this;
}
public function withMaximumEpoch($epoch) {
$this->maximumEpoch = $epoch;
return $this;
}
public function loadPage() {
$table = new PhabricatorChatLogEvent();
$conn_r = $table->establishConnection('r');
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T e %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
$logs = $table->loadAllFromArray($data);
return $logs;
}
private function buildWhereClause($conn_r) {
$where = array();
$where[] = $this->buildPagingClause($conn_r);
if ($this->maximumEpoch) {
$where[] = qsprintf(
$conn_r,
'epoch <= %d',
$this->maximumEpoch);
}
if ($this->channels) {
$where[] = qsprintf(
$conn_r,
'channel IN (%Ls)',
$this->channels);
}
return $this->formatWhereClause($where);
}
}
diff --git a/src/applications/chatlog/constants/PhabricatorChatLogConstants.php b/src/applications/chatlog/constants/PhabricatorChatLogConstants.php
index 7e90ecd10c..f579f5f181 100644
--- a/src/applications/chatlog/constants/PhabricatorChatLogConstants.php
+++ b/src/applications/chatlog/constants/PhabricatorChatLogConstants.php
@@ -1,21 +1,5 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorChatLogConstants {
}
diff --git a/src/applications/chatlog/constants/PhabricatorChatLogEventType.php b/src/applications/chatlog/constants/PhabricatorChatLogEventType.php
index a2a58f3351..1782ef5852 100644
--- a/src/applications/chatlog/constants/PhabricatorChatLogEventType.php
+++ b/src/applications/chatlog/constants/PhabricatorChatLogEventType.php
@@ -1,24 +1,8 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorChatLogEventType
extends PhabricatorChatLogConstants {
const TYPE_MESSAGE = 'mesg';
}
diff --git a/src/applications/chatlog/controller/PhabricatorChatLogChannelListController.php b/src/applications/chatlog/controller/PhabricatorChatLogChannelListController.php
index f78a0d1868..9f2a03dfd6 100644
--- a/src/applications/chatlog/controller/PhabricatorChatLogChannelListController.php
+++ b/src/applications/chatlog/controller/PhabricatorChatLogChannelListController.php
@@ -1,62 +1,46 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorChatLogChannelListController
extends PhabricatorChatLogController {
public function processRequest() {
$table = new PhabricatorChatLogEvent();
$channels = queryfx_all(
$table->establishConnection('r'),
'SELECT DISTINCT channel FROM %T',
$table->getTableName());
$rows = array();
foreach ($channels as $channel) {
$name = $channel['channel'];
$rows[] = array(
phutil_render_tag(
'a',
array(
'href' => '/chatlog/channel/'.phutil_escape_uri($name).'/',
),
phutil_escape_html($name)));
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Channel',
));
$table->setColumnClasses(
array(
'pri wide',
));
$panel = new AphrontPanelView();
$panel->appendChild($table);
return $this->buildStandardPageResponse(
$panel,
array(
'title' => 'Channel List',
));
}
}
diff --git a/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php b/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php
index d4c741e989..76f98c7eed 100644
--- a/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php
+++ b/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php
@@ -1,242 +1,226 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorChatLogChannelLogController
extends PhabricatorChatLogController {
private $channel;
public function willProcessRequest(array $data) {
$this->channel = $data['channel'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$uri = clone $request->getRequestURI();
$uri->setQueryParams(array());
$pager = new AphrontCursorPagerView();
$pager->setURI($uri);
$pager->setPageSize(250);
$query = id(new PhabricatorChatLogQuery())
->setViewer($user)
->withChannels(array($this->channel));
list($after, $before, $map) = $this->getPagingParameters($request, $query);
$pager->setAfterID($after);
$pager->setBeforeID($before);
$logs = $query->executeWithCursorPager($pager);
// Show chat logs oldest-first.
$logs = array_reverse($logs);
// Divide all the logs into blocks, where a block is the same author saying
// several things in a row. A block ends when another user speaks, or when
// two minutes pass without the author speaking.
$blocks = array();
$block = null;
$last_author = null;
$last_epoch = null;
foreach ($logs as $log) {
$this_author = $log->getAuthor();
$this_epoch = $log->getEpoch();
// Decide whether we should start a new block or not.
$new_block = ($this_author !== $last_author) ||
($this_epoch - (60 * 2) > $last_epoch);
if ($new_block) {
if ($block) {
$blocks[] = $block;
}
$block = array(
'id' => $log->getID(),
'epoch' => $this_epoch,
'author' => $this_author,
'logs' => array($log),
);
} else {
$block['logs'][] = $log;
}
$last_author = $this_author;
$last_epoch = $this_epoch;
}
if ($block) {
$blocks[] = $block;
}
// Figure out CSS classes for the blocks. We alternate colors between
// lines, and highlight the entire block which contains the target ID or
// date, if applicable.
foreach ($blocks as $key => $block) {
$classes = array();
if ($key % 2) {
$classes[] = 'alternate';
}
$ids = mpull($block['logs'], 'getID', 'getID');
if (array_intersect_key($ids, $map)) {
$classes[] = 'highlight';
}
$blocks[$key]['class'] = $classes ? implode(' ', $classes) : null;
}
require_celerity_resource('phabricator-chatlog-css');
$out = array();
$out[] = '<table class="phabricator-chat-log">';
foreach ($blocks as $block) {
$author = $block['author'];
$author = phutil_utf8_shorten($author, 18);
$author = phutil_escape_html($author);
$author = phutil_render_tag('td', array('class' => 'author'), $author);
$message = mpull($block['logs'], 'getMessage');
$message = implode("\n", $message);
$message = phutil_escape_html($message);
$message = phutil_render_tag('td', array('class' => 'message'), $message);
$href = $uri->alter('at', $block['id']);
$timestamp = $block['epoch'];
$timestamp = phabricator_datetime($timestamp, $user);
$timestamp = phutil_render_tag('a', array('href' => $href), $timestamp);
$timestamp = phutil_render_tag(
'td',
array(
'class' => 'timestamp',
),
$timestamp);
$out[] = phutil_render_tag(
'tr',
array(
'class' => $block['class'],
),
$author.$message.$timestamp);
}
$out[] = '</table>';
$form = id(new AphrontFormView())
->setUser($user)
->setMethod('GET')
->setAction($uri)
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Date')
->setName('date')
->setValue($request->getStr('date')))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Jump'));
return $this->buildStandardPageResponse(
array(
'<div class="phabricator-chat-log-panel">',
$form,
'<br />',
implode("\n", $out),
$pager,
'</div>',
),
array(
'title' => 'Channel Log',
));
}
/**
* From request parameters, figure out where we should jump to in the log.
* We jump to either a date or log ID, but load a few lines of context before
* it so the user can see the nearby conversation.
*/
private function getPagingParameters(
AphrontRequest $request,
PhabricatorChatLogQuery $query) {
$user = $request->getUser();
$at_id = $request->getInt('at');
$at_date = $request->getStr('date');
$context_log = null;
$map = array();
$query = clone $query;
$query->setLimit(8);
if ($at_id) {
// Jump to the log in question, and load a few lines of context before
// it.
$context_logs = $query
->setAfterID($at_id)
->execute();
$context_log = last($context_logs);
$map = array(
$at_id => true,
);
} else if ($at_date) {
$timezone = new DateTimeZone($user->getTimezoneIdentifier());
try {
$date = new DateTime($at_date, $timezone);
$timestamp = $date->format('U');
} catch (Exception $e) {
$timestamp = null;
}
if ($timestamp) {
$context_logs = $query
->withMaximumEpoch($timestamp)
->execute();
$context_log = last($context_logs);
$target_log = head($context_logs);
if ($target_log) {
$map = array(
$target_log->getID() => true,
);
}
}
}
if ($context_log) {
$after = null;
$before = $context_log->getID() - 1;
} else {
$after = $request->getInt('after');
$before = $request->getInt('before');
}
return array($after, $before, $map);
}
}
diff --git a/src/applications/chatlog/controller/PhabricatorChatLogController.php b/src/applications/chatlog/controller/PhabricatorChatLogController.php
index d0dfe02edf..ef9a195fbf 100644
--- a/src/applications/chatlog/controller/PhabricatorChatLogController.php
+++ b/src/applications/chatlog/controller/PhabricatorChatLogController.php
@@ -1,34 +1,18 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorChatLogController extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('Chat Log');
$page->setBaseURI('/chatlog/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph('#');
$page->appendChild($view);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
}
diff --git a/src/applications/chatlog/storage/PhabricatorChatLogDAO.php b/src/applications/chatlog/storage/PhabricatorChatLogDAO.php
index 672b7bdc89..8014168814 100644
--- a/src/applications/chatlog/storage/PhabricatorChatLogDAO.php
+++ b/src/applications/chatlog/storage/PhabricatorChatLogDAO.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorChatLogDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'chatlog';
}
}
diff --git a/src/applications/chatlog/storage/PhabricatorChatLogEvent.php b/src/applications/chatlog/storage/PhabricatorChatLogEvent.php
index 5b44aff2f6..0c29886922 100644
--- a/src/applications/chatlog/storage/PhabricatorChatLogEvent.php
+++ b/src/applications/chatlog/storage/PhabricatorChatLogEvent.php
@@ -1,53 +1,37 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorChatLogEvent
extends PhabricatorChatLogDAO
implements PhabricatorPolicyInterface {
protected $channel;
protected $epoch;
protected $author;
protected $type;
protected $message;
protected $loggedByPHID;
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
// TODO: This is sort of silly and mostly just so that we can use
// CursorPagedPolicyAwareQuery; once we implement Channel objects we should
// just delegate policy to them.
return PhabricatorPolicies::POLICY_PUBLIC;
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
) + parent::getConfiguration();
}
}
diff --git a/src/applications/conduit/application/PhabricatorApplicationConduit.php b/src/applications/conduit/application/PhabricatorApplicationConduit.php
index 3c393fdcd2..2d9c6e22b1 100644
--- a/src/applications/conduit/application/PhabricatorApplicationConduit.php
+++ b/src/applications/conduit/application/PhabricatorApplicationConduit.php
@@ -1,63 +1,47 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationConduit extends PhabricatorApplication {
public function getBaseURI() {
return '/conduit/';
}
public function getAutospriteName() {
return 'conduit';
}
public function getHelpURI() {
return PhabricatorEnv::getDoclink(
'article/Conduit_Technical_Documentation.html');
}
public function getShortDescription() {
return 'Conduit API Console';
}
public function getTitleGlyph() {
return "\xE2\x87\xB5";
}
public function getApplicationGroup() {
return self::GROUP_DEVELOPER;
}
public function getApplicationOrder() {
return 0.100;
}
public function getRoutes() {
return array(
'/conduit/' => array(
'' => 'PhabricatorConduitListController',
'method/(?P<method>[^/]+)/' => 'PhabricatorConduitConsoleController',
'log/' => 'PhabricatorConduitLogController',
'log/view/(?P<view>[^/]+)/' => 'PhabricatorConduitLogController',
'token/' => 'PhabricatorConduitTokenController',
),
'/api/(?P<method>[^/]+)' => 'PhabricatorConduitAPIController',
);
}
}
diff --git a/src/applications/conduit/call/ConduitCall.php b/src/applications/conduit/call/ConduitCall.php
index b824fc78ee..738be10831 100644
--- a/src/applications/conduit/call/ConduitCall.php
+++ b/src/applications/conduit/call/ConduitCall.php
@@ -1,104 +1,88 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Run a conduit method in-process, without requiring HTTP requests. Usage:
*
* $call = new ConduitCall('method.name', array('param' => 'value'));
* $call->setUser($user);
* $result = $call->execute();
*
*/
final class ConduitCall {
private $method;
private $params;
private $request;
private $user;
public function __construct($method, array $params) {
$this->method = $method;
$this->params = $params;
$this->handler = $this->buildMethodHandler($method);
$this->request = new ConduitAPIRequest($params);
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function getUser() {
return $this->user;
}
public function shouldRequireAuthentication() {
return $this->handler->shouldRequireAuthentication();
}
public function shouldAllowUnguardedWrites() {
return $this->handler->shouldAllowUnguardedWrites();
}
public function getRequiredScope() {
return $this->handler->getRequiredScope();
}
public function getErrorDescription($code) {
return $this->handler->getErrorDescription($code);
}
public function execute() {
if (!$this->getUser()) {
if ($this->shouldRequireAuthentication()) {
throw new ConduitException("ERR-INVALID-AUTH");
}
} else {
$this->request->setUser($this->getUser());
}
return $this->handler->executeMethod($this->request);
}
protected function buildMethodHandler($method) {
$method_class = ConduitAPIMethod::getClassNameFromAPIMethodName($method);
// Test if the method exists.
$ok = false;
try {
$ok = class_exists($method_class);
} catch (Exception $ex) {
// Discard, we provide a more specific exception below.
}
if (!$ok) {
throw new Exception(
"Conduit method '{$method}' does not exist.");
}
$class_info = new ReflectionClass($method_class);
if ($class_info->isAbstract()) {
throw new Exception(
"Method '{$method}' is not valid; the implementation is an abstract ".
"base class.");
}
return newv($method_class, array());
}
}
diff --git a/src/applications/conduit/call/__tests__/ConduitCallTestCase.php b/src/applications/conduit/call/__tests__/ConduitCallTestCase.php
index 53278f0511..d5e0c6f3ed 100644
--- a/src/applications/conduit/call/__tests__/ConduitCallTestCase.php
+++ b/src/applications/conduit/call/__tests__/ConduitCallTestCase.php
@@ -1,43 +1,27 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class ConduitCallTestCase extends PhabricatorTestCase {
public function testConduitPing() {
$call = new ConduitCall('conduit.ping', array());
$result = $call->execute();
$this->assertEqual(false, empty($result));
}
public function testConduitAuth() {
$call = new ConduitCall('user.whoami', array());
$caught = null;
try {
$result = $call->execute();
} catch (ConduitException $ex) {
$caught = $ex;
}
$this->assertEqual(
true,
($caught instanceof ConduitException),
"user.whoami should require authentication");
}
}
diff --git a/src/applications/conduit/controller/PhabricatorConduitAPIController.php b/src/applications/conduit/controller/PhabricatorConduitAPIController.php
index 673e073be1..21cca29a0b 100644
--- a/src/applications/conduit/controller/PhabricatorConduitAPIController.php
+++ b/src/applications/conduit/controller/PhabricatorConduitAPIController.php
@@ -1,483 +1,467 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class PhabricatorConduitAPIController
extends PhabricatorConduitController {
public function shouldRequireLogin() {
return false;
}
private $method;
public function willProcessRequest(array $data) {
$this->method = $data['method'];
return $this;
}
public function processRequest() {
$time_start = microtime(true);
$request = $this->getRequest();
$method = $this->method;
$api_request = null;
$log = new PhabricatorConduitMethodCallLog();
$log->setMethod($method);
$metadata = array();
try {
$params = $this->decodeConduitParams($request, $method);
$metadata = idx($params, '__conduit__', array());
unset($params['__conduit__']);
$call = new ConduitCall($method, $params);
$result = null;
// TODO: Straighten out the auth pathway here. We shouldn't be creating
// a ConduitAPIRequest at this level, but some of the auth code expects
// it. Landing a halfway version of this to unblock T945.
$api_request = new ConduitAPIRequest($params);
$allow_unguarded_writes = false;
$auth_error = null;
$conduit_username = '-';
if ($call->shouldRequireAuthentication()) {
$metadata['scope'] = $call->getRequiredScope();
$auth_error = $this->authenticateUser($api_request, $metadata);
// If we've explicitly authenticated the user here and either done
// CSRF validation or are using a non-web authentication mechanism.
$allow_unguarded_writes = true;
if (isset($metadata['actAsUser'])) {
$this->actAsUser($api_request, $metadata['actAsUser']);
}
if ($auth_error === null) {
$conduit_user = $api_request->getUser();
if ($conduit_user && $conduit_user->getPHID()) {
$conduit_username = $conduit_user->getUsername();
}
$call->setUser($api_request->getUser());
}
}
$access_log = PhabricatorAccessLog::getLog();
if ($access_log) {
$access_log->setData(
array(
'u' => $conduit_username,
'm' => $method,
));
}
if ($call->shouldAllowUnguardedWrites()) {
$allow_unguarded_writes = true;
}
if ($auth_error === null) {
if ($allow_unguarded_writes) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
}
try {
$result = $call->execute();
$error_code = null;
$error_info = null;
} catch (ConduitException $ex) {
$result = null;
$error_code = $ex->getMessage();
if ($ex->getErrorDescription()) {
$error_info = $ex->getErrorDescription();
} else {
$error_info = $call->getErrorDescription($error_code);
}
}
if ($allow_unguarded_writes) {
unset($unguarded);
}
} else {
list($error_code, $error_info) = $auth_error;
}
} catch (Exception $ex) {
phlog($ex);
$result = null;
$error_code = 'ERR-CONDUIT-CORE';
$error_info = $ex->getMessage();
}
$time_end = microtime(true);
$connection_id = null;
if (idx($metadata, 'connectionID')) {
$connection_id = $metadata['connectionID'];
} else if (($method == 'conduit.connect') && $result) {
$connection_id = idx($result, 'connectionID');
}
$log->setConnectionID($connection_id);
$log->setError((string)$error_code);
$log->setDuration(1000000 * ($time_end - $time_start));
// TODO: This is a hack, but the insert is comparatively expensive and
// we only really care about having these logs for real CLI clients, if
// even that.
if (empty($metadata['authToken'])) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$log->save();
unset($unguarded);
}
$response = id(new ConduitAPIResponse())
->setResult($result)
->setErrorCode($error_code)
->setErrorInfo($error_info);
switch ($request->getStr('output')) {
case 'human':
return $this->buildHumanReadableResponse(
$method,
$api_request,
$response->toDictionary());
case 'json':
default:
return id(new AphrontJSONResponse())
->setAddJSONShield(false)
->setContent($response->toDictionary());
}
}
/**
* Change the api request user to the user that we want to act as.
* Only admins can use actAsUser
*
* @param ConduitAPIRequest Request being executed.
* @param string The username of the user we want to act as
*/
private function actAsUser(
ConduitAPIRequest $api_request,
$user_name) {
if (!$api_request->getUser()->getIsAdmin()) {
throw new Exception("Only administrators can use actAsUser");
}
$user = id(new PhabricatorUser())->loadOneWhere(
'userName = %s',
$user_name);
if (!$user) {
throw new Exception(
"The actAsUser username '{$user_name}' is not a valid user."
);
}
$api_request->setUser($user);
}
/**
* Authenticate the client making the request to a Phabricator user account.
*
* @param ConduitAPIRequest Request being executed.
* @param dict Request metadata.
* @return null|pair Null to indicate successful authentication, or
* an error code and error message pair.
*/
private function authenticateUser(
ConduitAPIRequest $api_request,
array $metadata) {
$request = $this->getRequest();
if ($request->getUser()->getPHID()) {
$request->validateCSRF();
return $this->validateAuthenticatedUser(
$api_request,
$request->getUser());
}
// handle oauth
$access_token = $request->getStr('access_token');
$method_scope = $metadata['scope'];
if ($access_token &&
$method_scope != PhabricatorOAuthServerScope::SCOPE_NOT_ACCESSIBLE) {
$token = id(new PhabricatorOAuthServerAccessToken())
->loadOneWhere('token = %s',
$access_token);
if (!$token) {
return array(
'ERR-INVALID-AUTH',
'Access token does not exist.',
);
}
$oauth_server = new PhabricatorOAuthServer();
$valid = $oauth_server->validateAccessToken($token,
$method_scope);
if (!$valid) {
return array(
'ERR-INVALID-AUTH',
'Access token is invalid.',
);
}
// valid token, so let's log in the user!
$user_phid = $token->getUserPHID();
$user = id(new PhabricatorUser())
->loadOneWhere('phid = %s',
$user_phid);
if (!$user) {
return array(
'ERR-INVALID-AUTH',
'Access token is for invalid user.',
);
}
return $this->validateAuthenticatedUser(
$api_request,
$user);
}
// Handle sessionless auth. TOOD: This is super messy.
if (isset($metadata['authUser'])) {
$user = id(new PhabricatorUser())->loadOneWhere(
'userName = %s',
$metadata['authUser']);
if (!$user) {
return array(
'ERR-INVALID-AUTH',
'Authentication is invalid.',
);
}
$token = idx($metadata, 'authToken');
$signature = idx($metadata, 'authSignature');
$certificate = $user->getConduitCertificate();
if (sha1($token.$certificate) !== $signature) {
return array(
'ERR-INVALID-AUTH',
'Authentication is invalid.',
);
}
return $this->validateAuthenticatedUser(
$api_request,
$user);
}
$session_key = idx($metadata, 'sessionKey');
if (!$session_key) {
return array(
'ERR-INVALID-SESSION',
'Session key is not present.'
);
}
$session = queryfx_one(
id(new PhabricatorUser())->establishConnection('r'),
'SELECT * FROM %T WHERE sessionKey = %s',
PhabricatorUser::SESSION_TABLE,
$session_key);
if (!$session) {
return array(
'ERR-INVALID-SESSION',
'Session key is invalid.',
);
}
// TODO: Make sessions timeout.
// TODO: When we pull a session, read connectionID from the session table.
$user = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$session['userPHID']);
if (!$user) {
return array(
'ERR-INVALID-SESSION',
'Session is for nonexistent user.',
);
}
return $this->validateAuthenticatedUser(
$api_request,
$user);
}
private function validateAuthenticatedUser(
ConduitAPIRequest $request,
PhabricatorUser $user) {
if ($user->getIsDisabled()) {
return array(
'ERR-USER-DISABLED',
'User is disabled.');
}
if (PhabricatorUserEmail::isEmailVerificationRequired()) {
$email = $user->loadPrimaryEmail();
if (!$email) {
return array(
'ERR-USER-NOEMAIL',
'User has no primary email address.');
}
if (!$email->getIsVerified()) {
return array(
'ERR-USER-UNVERIFIED',
'User has unverified email address.');
}
}
$request->setUser($user);
return null;
}
private function buildHumanReadableResponse(
$method,
ConduitAPIRequest $request = null,
$result = null) {
$param_rows = array();
$param_rows[] = array('Method', $this->renderAPIValue($method));
if ($request) {
foreach ($request->getAllParameters() as $key => $value) {
$param_rows[] = array(
phutil_escape_html($key),
$this->renderAPIValue($value),
);
}
}
$param_table = new AphrontTableView($param_rows);
$param_table->setColumnClasses(
array(
'header',
'wide',
));
$result_rows = array();
foreach ($result as $key => $value) {
$result_rows[] = array(
phutil_escape_html($key),
$this->renderAPIValue($value),
);
}
$result_table = new AphrontTableView($result_rows);
$result_table->setColumnClasses(
array(
'header',
'wide',
));
$param_panel = new AphrontPanelView();
$param_panel->setHeader('Method Parameters');
$param_panel->appendChild($param_table);
$result_panel = new AphrontPanelView();
$result_panel->setHeader('Method Result');
$result_panel->appendChild($result_table);
return $this->buildStandardPageResponse(
array(
$param_panel,
$result_panel,
),
array(
'title' => 'Method Call Result',
));
}
private function renderAPIValue($value) {
$json = new PhutilJSON();
if (is_array($value)) {
$value = $json->encodeFormatted($value);
$value = phutil_escape_html($value);
} else {
$value = phutil_escape_html($value);
}
$value = '<pre style="white-space: pre-wrap;">'.$value.'</pre>';
return $value;
}
private function decodeConduitParams(
AphrontRequest $request,
$method) {
// Look for parameters from the Conduit API Console, which are encoded
// as HTTP POST parameters in an array, e.g.:
//
// params[name]=value&params[name2]=value2
//
// The fields are individually JSON encoded, since we require users to
// enter JSON so that we avoid type ambiguity.
$params = $request->getArr('params', null);
if ($params !== null) {
foreach ($params as $key => $value) {
if ($value == '') {
// Interpret empty string null (e.g., the user didn't type anything
// into the box).
$value = 'null';
}
$decoded_value = json_decode($value, true);
if ($decoded_value === null && strtolower($value) != 'null') {
// When json_decode() fails, it returns null. This almost certainly
// indicates that a user was using the web UI and didn't put quotes
// around a string value. We can either do what we think they meant
// (treat it as a string) or fail. For now, err on the side of
// caution and fail. In the future, if we make the Conduit API
// actually do type checking, it might be reasonable to treat it as
// a string if the parameter type is string.
throw new Exception(
"The value for parameter '{$key}' is not valid JSON. All ".
"parameters must be encoded as JSON values, including strings ".
"(which means you need to surround them in double quotes). ".
"Check your syntax. Value was: {$value}");
}
$params[$key] = $decoded_value;
}
return $params;
}
// Otherwise, look for a single parameter called 'params' which has the
// entire param dictionary JSON encoded. This is the usual case for remote
// requests.
$params_json = $request->getStr('params');
if (!strlen($params_json)) {
$params = array();
} else {
$params = json_decode($params_json, true);
if (!is_array($params)) {
throw new Exception(
"Invalid parameter information was passed to method ".
"'{$method}', could not decode JSON serialization. Data: ".
$params_json);
}
}
return $params;
}
}
diff --git a/src/applications/conduit/controller/PhabricatorConduitConsoleController.php b/src/applications/conduit/controller/PhabricatorConduitConsoleController.php
index 9007feda8f..e8bcc28592 100644
--- a/src/applications/conduit/controller/PhabricatorConduitConsoleController.php
+++ b/src/applications/conduit/controller/PhabricatorConduitConsoleController.php
@@ -1,157 +1,141 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class PhabricatorConduitConsoleController
extends PhabricatorConduitController {
private $method;
public function willProcessRequest(array $data) {
$this->method = $data['method'];
}
public function processRequest() {
$request = $this->getRequest();
$methods = $this->getAllMethods();
if (empty($methods[$this->method])) {
return new Aphront404Response();
}
$this->setFilter('method/'.$this->method);
$method_class = $methods[$this->method];
$method_object = newv($method_class, array());
$status = $method_object->getMethodStatus();
$reason = $method_object->getMethodStatusDescription();
$status_view = null;
if ($status != ConduitAPIMethod::METHOD_STATUS_STABLE) {
$status_view = new AphrontErrorView();
switch ($status) {
case ConduitAPIMethod::METHOD_STATUS_DEPRECATED:
$status_view->setTitle('Deprecated Method');
$status_view->appendChild(
phutil_escape_html(
nonempty(
$reason,
"This method is deprecated.")));
break;
case ConduitAPIMethod::METHOD_STATUS_UNSTABLE:
$status_view->setSeverity(AphrontErrorView::SEVERITY_WARNING);
$status_view->setTitle('Unstable Method');
$status_view->appendChild(
phutil_escape_html(
nonempty(
$reason,
"This method is new and unstable. Its interface is subject ".
"to change.")));
break;
}
}
$error_description = array();
$error_types = $method_object->defineErrorTypes();
if ($error_types) {
$error_description[] = '<ul>';
foreach ($error_types as $error => $meaning) {
$error_description[] =
'<li>'.
'<strong>'.phutil_escape_html($error).':</strong> '.
phutil_escape_html($meaning).
'</li>';
}
$error_description[] = '</ul>';
$error_description = implode("\n", $error_description);
} else {
$error_description = "This method does not raise any specific errors.";
}
$form = new AphrontFormView();
$form
->setUser($request->getUser())
->setAction('/api/'.$this->method)
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Description')
->setValue($method_object->getMethodDescription()))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Returns')
->setValue($method_object->defineReturnType()))
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel('Errors')
->setValue($error_description))
->appendChild(
'<p class="aphront-form-instructions">Enter parameters using '.
'<strong>JSON</strong>. For instance, to enter a list, type: '.
'<tt>["apple", "banana", "cherry"]</tt>');
$params = $method_object->defineParamTypes();
foreach ($params as $param => $desc) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel($param)
->setName("params[{$param}]")
->setCaption(phutil_escape_html($desc)));
}
$form
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Output Format')
->setName('output')
->setOptions(
array(
'human' => 'Human Readable',
'json' => 'JSON',
)))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Call Method'));
$panel = new AphrontPanelView();
$panel->setHeader('Conduit API: '.phutil_escape_html($this->method));
$panel->appendChild($form);
$panel->setWidth(AphrontPanelView::WIDTH_FULL);
return $this->buildStandardPageResponse(
array(
$status_view,
$panel,
),
array(
'title' => 'Conduit Console - '.$this->method,
));
}
private function getAllMethods() {
$classes = $this->getAllMethodImplementationClasses();
$methods = array();
foreach ($classes as $class) {
$name = ConduitAPIMethod::getAPIMethodNameFromClassName($class);
$methods[$name] = $class;
}
return $methods;
}
}
diff --git a/src/applications/conduit/controller/PhabricatorConduitController.php b/src/applications/conduit/controller/PhabricatorConduitController.php
index f3f7ccdd7f..00c8064fe1 100644
--- a/src/applications/conduit/controller/PhabricatorConduitController.php
+++ b/src/applications/conduit/controller/PhabricatorConduitController.php
@@ -1,137 +1,121 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
abstract class PhabricatorConduitController extends PhabricatorController {
private $filter;
protected $showSideNav;
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('Conduit');
$page->setBaseURI('/conduit/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph("\xE2\x87\xB5");
if ($this->showSideNav()) {
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI('/conduit/'));
$method_filters = $this->getMethodFilters();
foreach ($method_filters as $group => $methods) {
$nav->addLabel($group);
foreach ($methods as $method) {
$method_name = $method['full_name'];
$display_name = $method_name;
switch ($method['status']) {
case ConduitAPIMethod::METHOD_STATUS_DEPRECATED:
$display_name = '('.$display_name.')';
break;
}
$nav->addFilter('method/'.$method_name,
$display_name);
}
$nav->addSpacer();
}
$nav->selectFilter($this->getFilter());
$nav->appendChild($view);
$body = $nav;
} else {
$body = $view;
}
$page->appendChild($body);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
private function getFilter() {
return $this->filter;
}
protected function setFilter($filter) {
$this->filter = $filter;
return $this;
}
private function showSideNav() {
return $this->showSideNav !== false;
}
protected function setShowSideNav($show_side_nav) {
$this->showSideNav = $show_side_nav;
return $this;
}
protected function getAllMethodImplementationClasses() {
$classes = id(new PhutilSymbolLoader())
->setAncestorClass('ConduitAPIMethod')
->setType('class')
->setConcreteOnly(true)
->selectSymbolsWithoutLoading();
return array_values(ipull($classes, 'name'));
}
protected function getMethodFilters() {
$classes = $this->getAllMethodImplementationClasses();
$method_names = array();
foreach ($classes as $method_class) {
$method_name = ConduitAPIMethod::getAPIMethodNameFromClassName(
$method_class);
$group_name = head(explode('.', $method_name));
$method_object = newv($method_class, array());
$status = $method_object->getMethodStatus();
$key = sprintf(
'%02d %s %s',
$this->getOrderForMethodStatus($status),
$group_name,
$method_name);
$method_names[$key] = array(
'full_name' => $method_name,
'group_name' => $group_name,
'status' => $status,
'description' => $method_object->getMethodDescription(),
);
}
ksort($method_names);
$method_names = igroup($method_names, 'group_name');
ksort($method_names);
return $method_names;
}
private function getOrderForMethodStatus($status) {
$map = array(
ConduitAPIMethod::METHOD_STATUS_STABLE => 0,
ConduitAPIMethod::METHOD_STATUS_UNSTABLE => 1,
ConduitAPIMethod::METHOD_STATUS_DEPRECATED => 2,
);
return idx($map, $status, 0);
}
}
diff --git a/src/applications/conduit/controller/PhabricatorConduitListController.php b/src/applications/conduit/controller/PhabricatorConduitListController.php
index 713a443107..7c16896516 100644
--- a/src/applications/conduit/controller/PhabricatorConduitListController.php
+++ b/src/applications/conduit/controller/PhabricatorConduitListController.php
@@ -1,97 +1,81 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class PhabricatorConduitListController
extends PhabricatorConduitController {
public function processRequest() {
$method_groups = $this->getMethodFilters();
$rows = array();
foreach ($method_groups as $group => $methods) {
foreach ($methods as $info) {
switch ($info['status']) {
case ConduitAPIMethod::METHOD_STATUS_DEPRECATED:
$status = 'Deprecated';
break;
case ConduitAPIMethod::METHOD_STATUS_UNSTABLE:
$status = 'Unstable';
break;
default:
$status = null;
break;
}
$rows[] = array(
$group,
phutil_render_tag(
'a',
array(
'href' => '/conduit/method/'.$info['full_name'],
),
phutil_escape_html($info['full_name'])),
$info['description'],
$status,
);
$group = null;
}
}
$table = new AphrontTableView($rows);
$table->setHeaders(array(
'Group',
'Name',
'Description',
'Status',
));
$table->setColumnClasses(array(
'pri',
'pri',
'wide',
null,
));
$panel = new AphrontPanelView();
$panel->setHeader('Conduit Methods');
$panel->appendChild($table);
$panel->setWidth(AphrontPanelView::WIDTH_FULL);
$utils = new AphrontPanelView();
$utils->setHeader('Utilities');
$utils->appendChild(
'<ul>'.
'<li><a href="/conduit/log/">Log</a> - Conduit Method Calls</li>'.
'<li><a href="/conduit/token/">Token</a> - Certificate Install</li>'.
'</ul>');
$utils->setWidth(AphrontPanelView::WIDTH_FULL);
$this->setShowSideNav(false);
return $this->buildStandardPageResponse(
array(
$panel,
$utils,
),
array(
'title' => 'Conduit Console',
));
}
}
diff --git a/src/applications/conduit/controller/PhabricatorConduitLogController.php b/src/applications/conduit/controller/PhabricatorConduitLogController.php
index 4951a3f79e..a6bcbf74d7 100644
--- a/src/applications/conduit/controller/PhabricatorConduitLogController.php
+++ b/src/applications/conduit/controller/PhabricatorConduitLogController.php
@@ -1,113 +1,97 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class PhabricatorConduitLogController
extends PhabricatorConduitController {
public function processRequest() {
$request = $this->getRequest();
$conn_table = new PhabricatorConduitConnectionLog();
$call_table = new PhabricatorConduitMethodCallLog();
$conn_r = $call_table->establishConnection('r');
$pager = new AphrontPagerView();
$pager->setOffset($request->getInt('page'));
$calls = $call_table->loadAllWhere(
'1 = 1 ORDER BY id DESC LIMIT %d, %d',
$pager->getOffset(),
$pager->getPageSize() + 1);
$calls = $pager->sliceResults($calls);
$pager->setURI(new PhutilURI('/conduit/log/'), 'page');
$pager->setEnableKeyboardShortcuts(true);
$min = $pager->getOffset() + 1;
$max = ($min + count($calls) - 1);
$conn_ids = array_filter(mpull($calls, 'getConnectionID'));
$conns = array();
if ($conn_ids) {
$conns = $conn_table->loadAllWhere(
'id IN (%Ld)',
$conn_ids);
}
$table = $this->renderCallTable($calls, $conns);
$panel = new AphrontPanelView();
$panel->setHeader('Conduit Method Calls ('.$min.'-'.$max.')');
$panel->appendChild($table);
$panel->appendChild($pager);
$this->setShowSideNav(false);
return $this->buildStandardPageResponse(
$panel,
array(
'title' => 'Conduit Logs',
));
}
private function renderCallTable(array $calls, array $conns) {
assert_instances_of($calls, 'PhabricatorConduitMethodCallLog');
assert_instances_of($conns, 'PhabricatorConduitConnectionLog');
$user = $this->getRequest()->getUser();
$rows = array();
foreach ($calls as $call) {
$conn = idx($conns, $call->getConnectionID());
if (!$conn) {
// If there's no connection, use an empty object.
$conn = new PhabricatorConduitConnectionLog();
}
$rows[] = array(
$call->getConnectionID(),
phutil_escape_html($conn->getUserName()),
phutil_escape_html($call->getMethod()),
phutil_escape_html($call->getError()),
number_format($call->getDuration()).' us',
phabricator_datetime($call->getDateCreated(), $user),
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Connection',
'User',
'Method',
'Error',
'Duration',
'Date',
));
$table->setColumnClasses(
array(
'',
'',
'wide',
'',
'n',
'right',
));
return $table;
}
}
diff --git a/src/applications/conduit/controller/PhabricatorConduitTokenController.php b/src/applications/conduit/controller/PhabricatorConduitTokenController.php
index 60b19d12c0..54273ad477 100644
--- a/src/applications/conduit/controller/PhabricatorConduitTokenController.php
+++ b/src/applications/conduit/controller/PhabricatorConduitTokenController.php
@@ -1,68 +1,52 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class PhabricatorConduitTokenController
extends PhabricatorConduitController {
public function processRequest() {
$user = $this->getRequest()->getUser();
// Ideally we'd like to verify this, but it's fine to leave it unguarded
// for now and verifying it would need some Ajax junk or for the user to
// click a button or similar.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$old_token = id(new PhabricatorConduitCertificateToken())
->loadOneWhere(
'userPHID = %s',
$user->getPHID());
if ($old_token) {
$old_token->delete();
}
$token = id(new PhabricatorConduitCertificateToken())
->setUserPHID($user->getPHID())
->setToken(Filesystem::readRandomCharacters(40))
->save();
$panel = new AphrontPanelView();
$panel->setHeader('Certificate Install Token');
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->appendChild(
'<p class="aphront-form-instructions">Copy and paste this token into '.
'the prompt given to you by "arc install-certificate":</p>'.
'<p style="padding: 0 0 1em 4em;">'.
'<strong>'.phutil_escape_html($token->getToken()).'</strong>'.
'</p>'.
'<p class="aphront-form-instructions">arc will then complete the '.
'install process for you.</p>');
$this->setShowSideNav(false);
return $this->buildStandardPageResponse(
$panel,
array(
'title' => 'Certificate Install Token',
));
}
}
diff --git a/src/applications/conduit/method/ConduitAPIMethod.php b/src/applications/conduit/method/ConduitAPIMethod.php
index 9d30f71073..96ccd22fe4 100644
--- a/src/applications/conduit/method/ConduitAPIMethod.php
+++ b/src/applications/conduit/method/ConduitAPIMethod.php
@@ -1,137 +1,121 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
*
* @task status Method Status
* @group conduit
*/
abstract class ConduitAPIMethod {
const METHOD_STATUS_STABLE = 'stable';
const METHOD_STATUS_UNSTABLE = 'unstable';
const METHOD_STATUS_DEPRECATED = 'deprecated';
abstract public function getMethodDescription();
abstract public function defineParamTypes();
abstract public function defineReturnType();
abstract public function defineErrorTypes();
abstract protected function execute(ConduitAPIRequest $request);
public function __construct() {
}
/**
* Get the status for this method (e.g., stable, unstable or deprecated).
* Should return a METHOD_STATUS_* constant. By default, methods are
* "stable".
*
* @return const METHOD_STATUS_* constant.
* @task status
*/
public function getMethodStatus() {
return self::METHOD_STATUS_STABLE;
}
/**
* Optional description to supplement the method status. In particular, if
* a method is deprecated, you can return a string here describing the reason
* for deprecation and stable alternatives.
*
* @return string|null Description of the method status, if available.
* @task status
*/
public function getMethodStatusDescription() {
return null;
}
public function getErrorDescription($error_code) {
return idx($this->defineErrorTypes(), $error_code, 'Unknown Error');
}
public function getRequiredScope() {
// by default, conduit methods are not accessible via OAuth
return PhabricatorOAuthServerScope::SCOPE_NOT_ACCESSIBLE;
}
public function executeMethod(ConduitAPIRequest $request) {
return $this->execute($request);
}
public function getAPIMethodName() {
return self::getAPIMethodNameFromClassName(get_class($this));
}
public static function getClassNameFromAPIMethodName($method_name) {
$method_fragment = str_replace('.', '_', $method_name);
return 'ConduitAPI_'.$method_fragment.'_Method';
}
public function shouldRequireAuthentication() {
return true;
}
public function shouldAllowUnguardedWrites() {
return false;
}
public static function getAPIMethodNameFromClassName($class_name) {
$match = null;
$is_valid = preg_match(
'/^ConduitAPI_(.*)_Method$/',
$class_name,
$match);
if (!$is_valid) {
throw new Exception(
"Parameter '{$class_name}' is not a valid Conduit API method class.");
}
$method_fragment = $match[1];
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;
}
// NOTE: Compare domains only so we aren't sensitive to port specification
// or omission.
$host = new PhutilURI($host);
$host = $host->getDomain();
$self = new PhutilURI(PhabricatorEnv::getURI('/'));
$self = $self->getDomain();
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/arcanist/ConduitAPI_arcanist_Method.php b/src/applications/conduit/method/arcanist/ConduitAPI_arcanist_Method.php
index 84ba3e2837..82391e1669 100644
--- a/src/applications/conduit/method/arcanist/ConduitAPI_arcanist_Method.php
+++ b/src/applications/conduit/method/arcanist/ConduitAPI_arcanist_Method.php
@@ -1,24 +1,8 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
abstract class ConduitAPI_arcanist_Method extends ConduitAPIMethod {
}
diff --git a/src/applications/conduit/method/arcanist/ConduitAPI_arcanist_projectinfo_Method.php b/src/applications/conduit/method/arcanist/ConduitAPI_arcanist_projectinfo_Method.php
index 1e9646f047..b5d9556719 100644
--- a/src/applications/conduit/method/arcanist/ConduitAPI_arcanist_projectinfo_Method.php
+++ b/src/applications/conduit/method/arcanist/ConduitAPI_arcanist_projectinfo_Method.php
@@ -1,76 +1,60 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_arcanist_projectinfo_Method
extends ConduitAPI_arcanist_Method {
public function getMethodDescription() {
return "Get information about Arcanist projects.";
}
public function defineParamTypes() {
return array(
'name' => 'required string',
);
}
public function defineReturnType() {
return 'nonempty dict';
}
public function defineErrorTypes() {
return array(
'ERR-BAD-ARCANIST-PROJECT' => 'No such project exists.',
);
}
protected function execute(ConduitAPIRequest $request) {
$name = $request->getValue('name');
$project = id(new PhabricatorRepositoryArcanistProject())->loadOneWhere(
'name = %s',
$name);
if (!$project) {
throw new ConduitException('ERR-BAD-ARCANIST-PROJECT');
}
$repository = $project->loadRepository();
$repository_phid = null;
$tracked = false;
$encoding = null;
if ($repository) {
$repository_phid = $repository->getPHID();
$tracked = $repository->isTracked();
$encoding = $repository->getDetail('encoding');
}
return array(
'name' => $project->getName(),
'phid' => $project->getPHID(),
'repositoryPHID' => $repository_phid,
'tracked' => $tracked,
'encoding' => $encoding,
);
}
}
diff --git a/src/applications/conduit/method/audit/ConduitAPI_audit_Method.php b/src/applications/conduit/method/audit/ConduitAPI_audit_Method.php
index b42dc5edd5..8faccd83b5 100644
--- a/src/applications/conduit/method/audit/ConduitAPI_audit_Method.php
+++ b/src/applications/conduit/method/audit/ConduitAPI_audit_Method.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
abstract class ConduitAPI_audit_Method extends ConduitAPIMethod {
}
diff --git a/src/applications/conduit/method/audit/ConduitAPI_audit_query_Method.php b/src/applications/conduit/method/audit/ConduitAPI_audit_query_Method.php
index c7e300cf9b..7149b56903 100644
--- a/src/applications/conduit/method/audit/ConduitAPI_audit_query_Method.php
+++ b/src/applications/conduit/method/audit/ConduitAPI_audit_query_Method.php
@@ -1,85 +1,69 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_audit_query_Method extends ConduitAPI_audit_Method {
public function getMethodDescription() {
return "Query audit requests.";
}
public function defineParamTypes() {
return array(
'auditorPHIDs' => 'optional list<phid>',
'commitPHIDs' => 'optional list<phid>',
'status' => 'optional enum<"status-any", "status-open"> '.
'(default = "status-any")',
'offset' => 'optional int',
'limit' => 'optional int (default = 100)',
);
}
public function defineReturnType() {
return 'list<dict>';
}
public function defineErrorTypes() {
return array(
);
}
protected function execute(ConduitAPIRequest $request) {
$query = new PhabricatorAuditQuery();
$auditor_phids = $request->getValue('auditorPHIDs', array());
if ($auditor_phids) {
$query->withAuditorPHIDs($auditor_phids);
}
$commit_phids = $request->getValue('commitPHIDs', array());
if ($commit_phids) {
$query->withCommitPHIDs($commit_phids);
}
$status = $request->getValue('status', PhabricatorAuditQuery::STATUS_ANY);
$query->withStatus($status);
$query->setOffset($request->getValue('offset', 0));
$query->setLimit($request->getValue('limit', 100));
$requests = $query->execute();
$results = array();
foreach ($requests as $request) {
$results[] = array(
'id' => $request->getID(),
'commitPHID' => $request->getCommitPHID(),
'auditorPHID' => $request->getAuditorPHID(),
'reasons' => $request->getAuditReasons(),
'status' => $request->getAuditStatus(),
);
}
return $results;
}
}
diff --git a/src/applications/conduit/method/chatlog/ConduitAPI_chatlog_Method.php b/src/applications/conduit/method/chatlog/ConduitAPI_chatlog_Method.php
index a8cbae8109..c50a4aba26 100644
--- a/src/applications/conduit/method/chatlog/ConduitAPI_chatlog_Method.php
+++ b/src/applications/conduit/method/chatlog/ConduitAPI_chatlog_Method.php
@@ -1,24 +1,8 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
abstract class ConduitAPI_chatlog_Method extends ConduitAPIMethod {
}
diff --git a/src/applications/conduit/method/chatlog/ConduitAPI_chatlog_query_Method.php b/src/applications/conduit/method/chatlog/ConduitAPI_chatlog_query_Method.php
index 8bca00e9d0..41faf9fd93 100644
--- a/src/applications/conduit/method/chatlog/ConduitAPI_chatlog_query_Method.php
+++ b/src/applications/conduit/method/chatlog/ConduitAPI_chatlog_query_Method.php
@@ -1,80 +1,64 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_chatlog_query_Method
extends ConduitAPI_chatlog_Method {
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
return "Retrieve chatter.";
}
public function defineParamTypes() {
return array(
'channels' => 'optional list<string>',
'limit' => 'optional int (default = 100)',
);
}
public function defineReturnType() {
return 'nonempty list<dict>';
}
public function defineErrorTypes() {
return array();
}
protected function execute(ConduitAPIRequest $request) {
$query = new PhabricatorChatLogQuery();
$channels = $request->getValue('channels');
if ($channels) {
$query->withChannels($channels);
}
$limit = $request->getValue('limit');
if (!$limit) {
$limit = 100;
}
$query->setLimit($limit);
$logs = $query->execute();
$results = array();
foreach ($logs as $log) {
$results[] = array(
'channel' => $log->getChannel(),
'epoch' => $log->getEpoch(),
'author' => $log->getAuthor(),
'type' => $log->getType(),
'message' => $log->getMessage(),
'loggedByPHID' => $log->getLoggedByPHID(),
);
}
return $results;
}
}
diff --git a/src/applications/conduit/method/chatlog/ConduitAPI_chatlog_record_Method.php b/src/applications/conduit/method/chatlog/ConduitAPI_chatlog_record_Method.php
index 687dee8fb8..3bdd5c9181 100644
--- a/src/applications/conduit/method/chatlog/ConduitAPI_chatlog_record_Method.php
+++ b/src/applications/conduit/method/chatlog/ConduitAPI_chatlog_record_Method.php
@@ -1,73 +1,57 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_chatlog_record_Method
extends ConduitAPI_chatlog_Method {
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
return "Record chatter.";
}
public function defineParamTypes() {
return array(
'logs' => 'required list<dict>',
);
}
public function defineReturnType() {
return 'list<id>';
}
public function defineErrorTypes() {
return array(
);
}
protected function execute(ConduitAPIRequest $request) {
$logs = $request->getValue('logs');
if (!is_array($logs)) {
$logs = array();
}
$template = new PhabricatorChatLogEvent();
$template->setLoggedByPHID($request->getUser()->getPHID());
$objs = array();
foreach ($logs as $log) {
$obj = clone $template;
$obj->setChannel(idx($log, 'channel'));
$obj->setType(idx($log, 'type'));
$obj->setAuthor(idx($log, 'author'));
$obj->setEpoch(idx($log, 'epoch'));
$obj->setMessage(idx($log, 'message'));
$obj->save();
$objs[] = $obj;
}
return array_values(mpull($objs, 'getID'));
}
}
diff --git a/src/applications/conduit/method/conduit/ConduitAPI_conduit_connect_Method.php b/src/applications/conduit/method/conduit/ConduitAPI_conduit_connect_Method.php
index 05aa9155ca..5350ccc45d 100644
--- a/src/applications/conduit/method/conduit/ConduitAPI_conduit_connect_Method.php
+++ b/src/applications/conduit/method/conduit/ConduitAPI_conduit_connect_Method.php
@@ -1,155 +1,139 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_conduit_connect_Method extends ConduitAPIMethod {
public function shouldRequireAuthentication() {
return false;
}
public function shouldAllowUnguardedWrites() {
return true;
}
public function getMethodDescription() {
return "Connect a session-based client.";
}
public function defineParamTypes() {
return array(
'client' => 'required string',
'clientVersion' => 'required int',
'clientDescription' => 'optional string',
'user' => 'optional string',
'authToken' => 'optional int',
'authSignature' => 'optional string',
'host' => 'required string',
);
}
public function defineReturnType() {
return 'dict<string, any>';
}
public function defineErrorTypes() {
return array(
"ERR-BAD-VERSION" =>
"Client/server version mismatch. Upgrade your server or downgrade ".
"your client.",
"NEW-ARC-VERSION" =>
"Client/server version mismatch. Upgrade your client.",
"ERR-UNKNOWN-CLIENT" =>
"Client is unknown.",
"ERR-INVALID-USER" =>
"The username you are attempting to authenticate with is not valid.",
"ERR-INVALID-CERTIFICATE" =>
"Your authentication certificate for this server is invalid.",
"ERR-INVALID-TOKEN" =>
"The challenge token you are authenticating with is outside of the ".
"allowed time range. Either your system clock is out of whack or ".
"you're executing a replay attack.",
"ERR-NO-CERTIFICATE" => "This server requires authentication.",
);
}
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');
$username = (string)$request->getValue('user');
// Log the connection, regardless of the outcome of checks below.
$connection = new PhabricatorConduitConnectionLog();
$connection->setClient($client);
$connection->setClientVersion($client_version);
$connection->setClientDescription($client_description);
$connection->setUsername($username);
$connection->save();
switch ($client) {
case 'arc':
$server_version = 5;
$supported_versions = array(
$server_version => true,
// NOTE: Version 5 of the server can support either version 4 or
// version 5 of the client; the breaking change was the introduction
// of a "user.query" call in client version 5.
4 => true,
);
if (empty($supported_versions[$client_version])) {
if ($server_version < $client_version) {
$ex = new ConduitException('ERR-BAD-VERSION');
$ex->setErrorDescription(
"Your 'arc' client version is '{$client_version}', which ".
"is newer than the server version, '{$server_version}'. ".
"Upgrade your Phabricator install.");
} else {
$ex = new ConduitException('NEW-ARC-VERSION');
$ex->setErrorDescription(
"A new version of arc is available! You need to upgrade ".
"to connect to this server (you are running version ".
"{$client_version}, the server is running version ".
"{$server_version}).");
}
throw $ex;
}
break;
default:
// Allow new clients by default.
break;
}
$token = $request->getValue('authToken');
$signature = $request->getValue('authSignature');
$user = id(new PhabricatorUser())->loadOneWhere(
'username = %s',
$username);
if (!$user) {
throw new ConduitException('ERR-INVALID-USER');
}
$session_key = null;
if ($token && $signature) {
if (abs($token - time()) > 60 * 15) {
throw new ConduitException('ERR-INVALID-TOKEN');
}
$valid = sha1($token.$user->getConduitCertificate());
if ($valid != $signature) {
throw new ConduitException('ERR-INVALID-CERTIFICATE');
}
$session_key = $user->establishSession('conduit');
} else {
throw new ConduitException('ERR-NO-CERTIFICATE');
}
return array(
'connectionID' => $connection->getID(),
'sessionKey' => $session_key,
'userPHID' => $user->getPHID(),
);
}
}
diff --git a/src/applications/conduit/method/conduit/ConduitAPI_conduit_getcertificate_Method.php b/src/applications/conduit/method/conduit/ConduitAPI_conduit_getcertificate_Method.php
index 580e2046c7..af31ac40c0 100644
--- a/src/applications/conduit/method/conduit/ConduitAPI_conduit_getcertificate_Method.php
+++ b/src/applications/conduit/method/conduit/ConduitAPI_conduit_getcertificate_Method.php
@@ -1,106 +1,90 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_conduit_getcertificate_Method extends ConduitAPIMethod {
public function shouldRequireAuthentication() {
return false;
}
public function shouldAllowUnguardedWrites() {
// This method performs logging and is on the authentication pathway.
return true;
}
public function getMethodDescription() {
return "Retrieve certificate information for a user.";
}
public function defineParamTypes() {
return array(
'token' => 'required string',
'host' => 'required string',
);
}
public function defineReturnType() {
return 'dict<string, any>';
}
public function defineErrorTypes() {
return array(
"ERR-BAD-TOKEN" => "Token does not exist or has expired.",
"ERR-RATE-LIMIT" =>
"You have made too many invalid token requests recently. Wait before ".
"making more.",
);
}
protected function execute(ConduitAPIRequest $request) {
$this->validateHost($request->getValue('host'));
$failed_attempts = PhabricatorUserLog::loadRecentEventsFromThisIP(
PhabricatorUserLog::ACTION_CONDUIT_CERTIFICATE_FAILURE,
60 * 5);
if (count($failed_attempts) > 5) {
$this->logFailure();
throw new ConduitException('ERR-RATE-LIMIT');
}
$token = $request->getValue('token');
$info = id(new PhabricatorConduitCertificateToken())->loadOneWhere(
'token = %s',
trim($token));
if (!$info || $info->getDateCreated() < time() - (60 * 15)) {
$this->logFailure();
throw new ConduitException('ERR-BAD-TOKEN');
} else {
$log = id(new PhabricatorUserLog())
->setActorPHID($info->getUserPHID())
->setUserPHID($info->getUserPHID())
->setAction(PhabricatorUserLog::ACTION_CONDUIT_CERTIFICATE)
->save();
}
$user = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$info->getUserPHID());
if (!$user) {
throw new Exception("Certificate token points to an invalid user!");
}
return array(
'username' => $user->getUserName(),
'certificate' => $user->getConduitCertificate(),
);
}
private function logFailure() {
$log = id(new PhabricatorUserLog())
->setUserPHID('-')
->setAction(PhabricatorUserLog::ACTION_CONDUIT_CERTIFICATE_FAILURE)
->save();
}
}
diff --git a/src/applications/conduit/method/conduit/ConduitAPI_conduit_ping_Method.php b/src/applications/conduit/method/conduit/ConduitAPI_conduit_ping_Method.php
index f21f678ccf..35c95ada30 100644
--- a/src/applications/conduit/method/conduit/ConduitAPI_conduit_ping_Method.php
+++ b/src/applications/conduit/method/conduit/ConduitAPI_conduit_ping_Method.php
@@ -1,47 +1,31 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_conduit_ping_Method extends ConduitAPIMethod {
public function shouldRequireAuthentication() {
return false;
}
public function getMethodDescription() {
return "Basic ping for monitoring or a health-check.";
}
public function defineParamTypes() {
return array();
}
public function defineReturnType() {
return 'string';
}
public function defineErrorTypes() {
return array();
}
protected function execute(ConduitAPIRequest $request) {
return php_uname('n');
}
}
diff --git a/src/applications/conduit/method/daemon/ConduitAPI_daemon_launched_Method.php b/src/applications/conduit/method/daemon/ConduitAPI_daemon_launched_Method.php
index f0fafd17d5..f18cda2905 100644
--- a/src/applications/conduit/method/daemon/ConduitAPI_daemon_launched_Method.php
+++ b/src/applications/conduit/method/daemon/ConduitAPI_daemon_launched_Method.php
@@ -1,69 +1,53 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_daemon_launched_Method extends ConduitAPIMethod {
public function shouldRequireAuthentication() {
// TODO: Lock this down once we build phantoms.
return false;
}
public function shouldAllowUnguardedWrites() {
return true;
}
public function getMethodDescription() {
return "Used by daemons to log run status.";
}
public function defineParamTypes() {
return array(
'daemon' => 'required string',
'host' => 'required string',
'pid' => 'required int',
'argv' => 'required string',
);
}
public function defineReturnType() {
return 'string';
}
public function defineErrorTypes() {
return array(
);
}
protected function execute(ConduitAPIRequest $request) {
$daemon_log = new PhabricatorDaemonLog();
$daemon_log->setDaemon($request->getValue('daemon'));
$daemon_log->setHost($request->getValue('host'));
$daemon_log->setPID($request->getValue('pid'));
$daemon_log->setStatus(PhabricatorDaemonLog::STATUS_RUNNING);
$daemon_log->setArgv(json_decode($request->getValue('argv')));
$daemon_log->save();
return $daemon_log->getID();
}
}
diff --git a/src/applications/conduit/method/daemon/ConduitAPI_daemon_log_Method.php b/src/applications/conduit/method/daemon/ConduitAPI_daemon_log_Method.php
index 928534df03..ed316b0f27 100644
--- a/src/applications/conduit/method/daemon/ConduitAPI_daemon_log_Method.php
+++ b/src/applications/conduit/method/daemon/ConduitAPI_daemon_log_Method.php
@@ -1,67 +1,51 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_daemon_log_Method extends ConduitAPIMethod {
public function shouldRequireAuthentication() {
// TODO: Lock this down once we build phantoms.
return false;
}
public function shouldAllowUnguardedWrites() {
return true;
}
public function getMethodDescription() {
return "Used by daemons to log events.";
}
public function defineParamTypes() {
return array(
'daemonLogID' => 'required int',
'type' => 'required string',
'message' => 'optional string',
);
}
public function defineReturnType() {
return 'void';
}
public function defineErrorTypes() {
return array(
);
}
protected function execute(ConduitAPIRequest $request) {
$daemon_event = new PhabricatorDaemonLogEvent();
$daemon_event->setLogID($request->getValue('daemonLogID'));
$daemon_event->setLogType($request->getValue('type'));
$daemon_event->setMessage((string)$request->getValue('message'));
$daemon_event->setEpoch(time());
$daemon_event->save();
return;
}
}
diff --git a/src/applications/conduit/method/daemon/ConduitAPI_daemon_setstatus_Method.php b/src/applications/conduit/method/daemon/ConduitAPI_daemon_setstatus_Method.php
index 6800518baa..7b034b87de 100644
--- a/src/applications/conduit/method/daemon/ConduitAPI_daemon_setstatus_Method.php
+++ b/src/applications/conduit/method/daemon/ConduitAPI_daemon_setstatus_Method.php
@@ -1,66 +1,50 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_daemon_setstatus_Method extends ConduitAPIMethod {
public function shouldRequireAuthentication() {
// TODO: Lock this down once we build phantoms.
return false;
}
public function shouldAllowUnguardedWrites() {
return true;
}
public function getMethodDescription() {
return "Used by daemons to update their status.";
}
public function defineParamTypes() {
return array(
'daemonLogID' => 'required string',
'status' => 'required enum<unknown, run, timeout, dead, exit>',
);
}
public function defineReturnType() {
return 'void';
}
public function defineErrorTypes() {
return array(
'ERR-INVALID-ID' => 'An invalid daemonLogID was provided.',
);
}
protected function execute(ConduitAPIRequest $request) {
$daemon_log = id(new PhabricatorDaemonLog())
->load($request->getValue('daemonLogID'));
if (!$daemon_log) {
throw new ConduitException('ERR-INVALID-ID');
}
$daemon_log->setStatus($request->getValue('status'));
$daemon_log->save();
}
}
diff --git a/src/applications/conduit/method/differential/ConduitAPI_differential_Method.php b/src/applications/conduit/method/differential/ConduitAPI_differential_Method.php
index 51c933ca81..a75b80b3f7 100644
--- a/src/applications/conduit/method/differential/ConduitAPI_differential_Method.php
+++ b/src/applications/conduit/method/differential/ConduitAPI_differential_Method.php
@@ -1,62 +1,46 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
abstract class ConduitAPI_differential_Method extends ConduitAPIMethod {
protected function buildDiffInfoDictionary(DifferentialDiff $diff) {
$uri = '/differential/diff/'.$diff->getID().'/';
$uri = PhabricatorEnv::getProductionURI($uri);
return array(
'id' => $diff->getID(),
'uri' => $uri,
);
}
protected function buildInlineInfoDictionary(
DifferentialInlineComment $inline,
DifferentialChangeset $changeset = null) {
$file_path = null;
$diff_id = null;
if ($changeset) {
$file_path = $inline->getIsNewFile()
? $changeset->getFilename()
: $changeset->getOldFile();
$diff_id = $changeset->getDiffID();
}
return array(
'id' => $inline->getID(),
'authorPHID' => $inline->getAuthorPHID(),
'filePath' => $file_path,
'isNewFile' => $inline->getIsNewFile(),
'lineNumber' => $inline->getLineNumber(),
'lineLength' => $inline->getLineLength(),
'diffID' => $diff_id,
'content' => $inline->getContent(),
);
}
}
diff --git a/src/applications/conduit/method/differential/ConduitAPI_differential_close_Method.php b/src/applications/conduit/method/differential/ConduitAPI_differential_close_Method.php
index 894347ed45..688f5b5f4c 100644
--- a/src/applications/conduit/method/differential/ConduitAPI_differential_close_Method.php
+++ b/src/applications/conduit/method/differential/ConduitAPI_differential_close_Method.php
@@ -1,77 +1,61 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_differential_close_Method
extends ConduitAPIMethod {
public function getMethodDescription() {
return "Close a Differential revision.";
}
public function defineParamTypes() {
return array(
'revisionID' => 'required int',
);
}
public function defineReturnType() {
return 'void';
}
public function defineErrorTypes() {
return array(
'ERR_NOT_FOUND' => 'Revision was not found.',
);
}
protected function execute(ConduitAPIRequest $request) {
$id = $request->getValue('revisionID');
$revision = id(new DifferentialRevision())->load($id);
if (!$revision) {
throw new ConduitException('ERR_NOT_FOUND');
}
if ($revision->getStatus() == ArcanistDifferentialRevisionStatus::CLOSED) {
// This can occur if someone runs 'close-revision' and hits a race, or
// they have a remote hook installed but don't have the
// 'remote_hook_installed' flag set, or similar. In any case, just treat
// it as a no-op rather than adding another "X closed this revision"
// message to the revision comments.
return;
}
$revision->loadRelationships();
$editor = new DifferentialCommentEditor(
$revision,
DifferentialAction::ACTION_CLOSE);
$editor->setActor($request->getUser());
$editor->save();
$revision->setStatus(ArcanistDifferentialRevisionStatus::CLOSED);
$revision->setDateCommitted(time());
$revision->save();
return;
}
}
diff --git a/src/applications/conduit/method/differential/ConduitAPI_differential_createcomment_Method.php b/src/applications/conduit/method/differential/ConduitAPI_differential_createcomment_Method.php
index 93b7f9a0c2..7be3951872 100644
--- a/src/applications/conduit/method/differential/ConduitAPI_differential_createcomment_Method.php
+++ b/src/applications/conduit/method/differential/ConduitAPI_differential_createcomment_Method.php
@@ -1,79 +1,63 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_differential_createcomment_Method
extends ConduitAPIMethod {
public function getMethodDescription() {
return "Add a comment to a Differential revision.";
}
public function defineParamTypes() {
return array(
'revision_id' => 'required revisionid',
'message' => 'optional string',
'action' => 'optional string',
'silent' => 'optional bool',
);
}
public function defineReturnType() {
return 'nonempty dict';
}
public function defineErrorTypes() {
return array(
'ERR_BAD_REVISION' => 'Bad revision ID.',
);
}
protected function execute(ConduitAPIRequest $request) {
$revision = id(new DifferentialRevision())->load(
$request->getValue('revision_id'));
if (!$revision) {
throw new ConduitException('ERR_BAD_REVISION');
}
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_CONDUIT,
array());
$action = $request->getValue('action');
if (!$action) {
$action = 'none';
}
$editor = new DifferentialCommentEditor(
$revision,
$action);
$editor->setActor($request->getUser());
$editor->setContentSource($content_source);
$editor->setMessage($request->getValue('message'));
$editor->setNoEmail($request->getValue('silent'));
$editor->save();
return array(
'revisionid' => $revision->getID(),
'uri' => PhabricatorEnv::getURI('/D'.$revision->getID()),
);
}
}
diff --git a/src/applications/conduit/method/differential/ConduitAPI_differential_creatediff_Method.php b/src/applications/conduit/method/differential/ConduitAPI_differential_creatediff_Method.php
index de70a1027d..a62cca98e2 100644
--- a/src/applications/conduit/method/differential/ConduitAPI_differential_creatediff_Method.php
+++ b/src/applications/conduit/method/differential/ConduitAPI_differential_creatediff_Method.php
@@ -1,166 +1,150 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_differential_creatediff_Method extends ConduitAPIMethod {
public function getMethodDescription() {
return "Create a new Differential diff.";
}
public function defineParamTypes() {
return array(
'changes' => 'required list<dict>',
'sourceMachine' => 'required string',
'sourcePath' => 'required string',
'branch' => 'required string',
'bookmark' => 'optional string',
'sourceControlSystem' => 'required enum<svn, git>',
'sourceControlPath' => 'required string',
'sourceControlBaseRevision' => 'required string',
'parentRevisionID' => 'optional revisionid',
'creationMethod' => 'optional string',
'authorPHID' => 'optional phid',
'arcanistProject' => 'optional string',
'repositoryUUID' => 'optional string',
'lintStatus' =>
'required enum<none, skip, okay, warn, fail, postponed>',
'unitStatus' =>
'required enum<none, skip, okay, warn, fail, postponed>',
);
}
public function defineReturnType() {
return 'nonempty dict';
}
public function defineErrorTypes() {
return array(
);
}
protected function execute(ConduitAPIRequest $request) {
$change_data = $request->getValue('changes');
$changes = array();
foreach ($change_data as $dict) {
$changes[] = ArcanistDiffChange::newFromDictionary($dict);
}
$diff = DifferentialDiff::newFromRawChanges($changes);
$diff->setSourcePath($request->getValue('sourcePath'));
$diff->setSourceMachine($request->getValue('sourceMachine'));
$diff->setBranch($request->getValue('branch'));
$diff->setCreationMethod($request->getValue('creationMethod'));
$diff->setAuthorPHID($request->getValue('authorPHID'));
$diff->setBookmark($request->getValue('bookmark'));
$parent_id = $request->getValue('parentRevisionID');
if ($parent_id) {
$parent_rev = id(new DifferentialRevision())->load($parent_id);
if ($parent_rev) {
if ($parent_rev->getStatus() !=
ArcanistDifferentialRevisionStatus::CLOSED) {
$diff->setParentRevisionID($parent_id);
}
}
}
$system = $request->getValue('sourceControlSystem');
$diff->setSourceControlSystem($system);
$diff->setSourceControlPath($request->getValue('sourceControlPath'));
$diff->setSourceControlBaseRevision(
$request->getValue('sourceControlBaseRevision'));
$project_name = $request->getValue('arcanistProject');
$project_phid = null;
if ($project_name) {
$arcanist_project = id(new PhabricatorRepositoryArcanistProject())
->loadOneWhere(
'name = %s',
$project_name);
if (!$arcanist_project) {
$arcanist_project = new PhabricatorRepositoryArcanistProject();
$arcanist_project->setName($project_name);
$arcanist_project->save();
}
$project_phid = $arcanist_project->getPHID();
}
$diff->setArcanistProjectPHID($project_phid);
$diff->setRepositoryUUID($request->getValue('repositoryUUID'));
switch ($request->getValue('lintStatus')) {
case 'skip':
$diff->setLintStatus(DifferentialLintStatus::LINT_SKIP);
break;
case 'okay':
$diff->setLintStatus(DifferentialLintStatus::LINT_OKAY);
break;
case 'warn':
$diff->setLintStatus(DifferentialLintStatus::LINT_WARN);
break;
case 'fail':
$diff->setLintStatus(DifferentialLintStatus::LINT_FAIL);
break;
case 'postponed':
$diff->setLintStatus(DifferentialLintStatus::LINT_POSTPONED);
break;
case 'none':
default:
$diff->setLintStatus(DifferentialLintStatus::LINT_NONE);
break;
}
switch ($request->getValue('unitStatus')) {
case 'skip':
$diff->setUnitStatus(DifferentialUnitStatus::UNIT_SKIP);
break;
case 'okay':
$diff->setUnitStatus(DifferentialUnitStatus::UNIT_OKAY);
break;
case 'warn':
$diff->setUnitStatus(DifferentialUnitStatus::UNIT_WARN);
break;
case 'fail':
$diff->setUnitStatus(DifferentialUnitStatus::UNIT_FAIL);
break;
case 'postponed':
$diff->setUnitStatus(DifferentialUnitStatus::UNIT_POSTPONED);
break;
case 'none':
default:
$diff->setUnitStatus(DifferentialUnitStatus::UNIT_NONE);
break;
}
$diff->save();
$path = '/differential/diff/'.$diff->getID().'/';
$uri = PhabricatorEnv::getURI($path);
return array(
'diffid' => $diff->getID(),
'uri' => $uri,
);
}
}
diff --git a/src/applications/conduit/method/differential/ConduitAPI_differential_createinline_Method.php b/src/applications/conduit/method/differential/ConduitAPI_differential_createinline_Method.php
index a9dfa7d743..b2359a82cc 100644
--- a/src/applications/conduit/method/differential/ConduitAPI_differential_createinline_Method.php
+++ b/src/applications/conduit/method/differential/ConduitAPI_differential_createinline_Method.php
@@ -1,121 +1,105 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_differential_createinline_Method
extends ConduitAPI_differential_Method {
public function getMethodDescription() {
return "Add an inline comment to a Differential revision.";
}
public function defineParamTypes() {
return array(
'revisionID' => 'optional revisionid',
'diffID' => 'optional diffid',
'filePath' => 'required string',
'isNewFile' => 'required bool',
'lineNumber' => 'required int',
'lineLength' => 'optional int',
'content' => 'required string',
);
}
public function defineReturnType() {
return 'nonempty dict';
}
public function defineErrorTypes() {
return array(
'ERR-BAD-REVISION' => 'Bad revision ID.',
'ERR-BAD-DIFF' => 'Bad diff ID, or diff does not belong to revision.',
'ERR-NEED-DIFF' => 'Neither revision ID nor diff ID was provided.',
'ERR-NEED-FILE' => 'A file path was not provided.',
'ERR-BAD-FILE' => "Requested file doesn't exist in this revision."
);
}
protected function execute(ConduitAPIRequest $request) {
$rid = $request->getValue('revisionID');
$did = $request->getValue('diffID');
if ($rid) {
// Given both a revision and a diff, check that they match.
// Given only a revision, find the active diff.
$revision = id(new DifferentialRevision())->load($rid);
if (!$revision) {
throw new ConduitException('ERR-BAD-REVISION');
}
if (!$did) { // did not!
$diff = $revision->loadActiveDiff();
$did = $diff->getID();
} else { // did too!
$diff = id(new DifferentialDiff())->load($did);
if (!$diff || $diff->getRevisionID() != $rid) {
throw new ConduitException('ERR-BAD-DIFF');
}
}
} else if ($did) {
// Given only a diff, find the parent revision.
$diff = id(new DifferentialDiff())->load($did);
if (!$diff) {
throw new ConduitException('ERR-BAD-DIFF');
}
$rid = $diff->getRevisionID();
} else {
// Given neither, bail.
throw new ConduitException('ERR-NEED-DIFF');
}
$file = $request->getValue('filePath');
if (!$file) {
throw new ConduitException('ERR-NEED-FILE');
}
$changes = id(new DifferentialChangeset())->loadAllWhere(
'diffID = %d',
$did);
$cid = null;
foreach ($changes as $id => $change) {
if ($file == $change->getFilename()) {
$cid = $id;
}
}
if ($cid == null) {
throw new ConduitException('ERR-BAD-FILE');
}
$inline = id(new DifferentialInlineComment())
->setRevisionID($rid)
->setChangesetID($cid)
->setAuthorPHID($request->getUser()->getPHID())
->setContent($request->getValue('content'))
->setIsNewFile($request->getValue('isNewFile'))
->setLineNumber($request->getValue('lineNumber'))
->setLineLength($request->getValue('lineLength', 0))
->save();
// Load everything again, just to be safe.
$changeset = id(new DifferentialChangeset())
->load($inline->getChangesetID());
return $this->buildInlineInfoDictionary($inline, $changeset);
}
}
diff --git a/src/applications/conduit/method/differential/ConduitAPI_differential_createrawdiff_Method.php b/src/applications/conduit/method/differential/ConduitAPI_differential_createrawdiff_Method.php
index 9792bc5756..648d38ebbd 100644
--- a/src/applications/conduit/method/differential/ConduitAPI_differential_createrawdiff_Method.php
+++ b/src/applications/conduit/method/differential/ConduitAPI_differential_createrawdiff_Method.php
@@ -1,61 +1,45 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_differential_createrawdiff_Method
extends ConduitAPI_differential_Method {
public function getMethodDescription() {
return "Create a new Differential diff from a raw diff source.";
}
public function defineParamTypes() {
return array(
'diff' => 'required string',
);
}
public function defineReturnType() {
return 'nonempty dict';
}
public function defineErrorTypes() {
return array(
);
}
protected function execute(ConduitAPIRequest $request) {
$raw_diff = $request->getValue('diff');
$parser = new ArcanistDiffParser();
$changes = $parser->parseDiff($raw_diff);
$diff = DifferentialDiff::newFromRawChanges($changes);
$diff->setLintStatus(DifferentialLintStatus::LINT_SKIP);
$diff->setUnitStatus(DifferentialUnitStatus::UNIT_SKIP);
$diff->setAuthorPHID($request->getUser()->getPHID());
$diff->setCreationMethod('web');
$diff->save();
return $this->buildDiffInfoDictionary($diff);
}
}
diff --git a/src/applications/conduit/method/differential/ConduitAPI_differential_createrevision_Method.php b/src/applications/conduit/method/differential/ConduitAPI_differential_createrevision_Method.php
index dd3321f0c0..3a5e972e46 100644
--- a/src/applications/conduit/method/differential/ConduitAPI_differential_createrevision_Method.php
+++ b/src/applications/conduit/method/differential/ConduitAPI_differential_createrevision_Method.php
@@ -1,65 +1,49 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_differential_createrevision_Method
extends ConduitAPIMethod {
public function getMethodDescription() {
return "Create a new Differential revision.";
}
public function defineParamTypes() {
return array(
'diffid' => 'required diffid',
'fields' => 'required dict',
);
}
public function defineReturnType() {
return 'nonempty dict';
}
public function defineErrorTypes() {
return array(
'ERR_BAD_DIFF' => 'Bad diff ID.',
);
}
protected function execute(ConduitAPIRequest $request) {
$fields = $request->getValue('fields');
$diff = id(new DifferentialDiff())->load($request->getValue('diffid'));
if (!$diff) {
throw new ConduitException('ERR_BAD_DIFF');
}
$revision = DifferentialRevisionEditor::newRevisionFromConduitWithDiff(
$fields,
$diff,
$request->getUser());
return array(
'revisionid' => $revision->getID(),
'uri' => PhabricatorEnv::getURI('/D'.$revision->getID()),
);
}
}
diff --git a/src/applications/conduit/method/differential/ConduitAPI_differential_find_Method.php b/src/applications/conduit/method/differential/ConduitAPI_differential_find_Method.php
index aac289101d..fab6abd861 100644
--- a/src/applications/conduit/method/differential/ConduitAPI_differential_find_Method.php
+++ b/src/applications/conduit/method/differential/ConduitAPI_differential_find_Method.php
@@ -1,98 +1,82 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_differential_find_Method extends ConduitAPIMethod {
public function getMethodStatus() {
return self::METHOD_STATUS_DEPRECATED;
}
public function getMethodStatusDescription() {
return "Replaced by 'differential.query'.";
}
public function getMethodDescription() {
return "Query Differential revisions which match certain criteria.";
}
public function defineParamTypes() {
$types = array(
DifferentialRevisionListData::QUERY_OPEN_OWNED,
DifferentialRevisionListData::QUERY_COMMITTABLE,
DifferentialRevisionListData::QUERY_REVISION_IDS,
DifferentialRevisionListData::QUERY_PHIDS,
);
$types = implode(', ', $types);
return array(
'query' => 'required enum<'.$types.'>',
'guids' => 'required nonempty list<guids>',
);
}
public function defineReturnType() {
return 'nonempty list<dict>';
}
public function defineErrorTypes() {
return array(
);
}
protected function execute(ConduitAPIRequest $request) {
$query = $request->getValue('query');
$guids = $request->getValue('guids');
$results = array();
if (!$guids) {
return $results;
}
$revisions = id(new DifferentialRevisionListData(
$query,
(array)$guids))
->loadRevisions();
foreach ($revisions as $revision) {
$diff = $revision->loadActiveDiff();
if (!$diff) {
continue;
}
$id = $revision->getID();
$results[] = array(
'id' => $id,
'phid' => $revision->getPHID(),
'name' => $revision->getTitle(),
'uri' => PhabricatorEnv::getProductionURI('/D'.$id),
'dateCreated' => $revision->getDateCreated(),
'authorPHID' => $revision->getAuthorPHID(),
'statusName' =>
ArcanistDifferentialRevisionStatus::getNameForRevisionStatus(
$revision->getStatus()),
'sourcePath' => $diff->getSourcePath(),
);
}
return $results;
}
}
diff --git a/src/applications/conduit/method/differential/ConduitAPI_differential_finishpostponedlinters_Method.php b/src/applications/conduit/method/differential/ConduitAPI_differential_finishpostponedlinters_Method.php
index ec1b638071..cc56418aea 100644
--- a/src/applications/conduit/method/differential/ConduitAPI_differential_finishpostponedlinters_Method.php
+++ b/src/applications/conduit/method/differential/ConduitAPI_differential_finishpostponedlinters_Method.php
@@ -1,131 +1,115 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_differential_finishpostponedlinters_Method
extends ConduitAPIMethod {
public function getMethodDescription() {
return "Update diff with new lint messages and mark postponed ".
"linters as finished.";
}
public function defineParamTypes() {
return array(
'diffID' => 'required diffID',
'linters' => 'required dict',
);
}
public function defineReturnType() {
return 'void';
}
public function defineErrorTypes() {
return array(
'ERR-BAD-DIFF' => 'Bad diff ID.',
'ERR-BAD-LINTER' => 'No postponed linter by the given name',
'ERR-NO-LINT' => 'No postponed lint field available in diff',
);
}
protected function execute(ConduitAPIRequest $request) {
$diff_id = $request->getValue('diffID');
$linter_map = $request->getValue('linters');
$diff = id(new DifferentialDiff())->load($diff_id);
if (!$diff) {
throw new ConduitException('ERR-BAD-DIFF');
}
// Extract the finished linters and messages from the linter map.
$finished_linters = array_keys($linter_map);
$new_messages = array();
foreach ($linter_map as $linter => $messages) {
$new_messages = array_merge($new_messages, $messages);
}
// Load the postponed linters attached to this diff.
$postponed_linters_property = id(
new DifferentialDiffProperty())->loadOneWhere(
'diffID = %d AND name = %s',
$diff_id,
'arc:lint-postponed');
if ($postponed_linters_property) {
$postponed_linters = $postponed_linters_property->getData();
} else {
$postponed_linters = array();
}
foreach ($finished_linters as $linter) {
if (!in_array($linter, $postponed_linters)) {
throw new ConduitException('ERR-BAD-LINTER');
}
}
foreach ($postponed_linters as $idx => $linter) {
if (in_array($linter, $finished_linters)) {
unset($postponed_linters[$idx]);
}
}
// Load the lint messages currenty attached to the diff. If this
// diff property doesn't exist, create it.
$messages_property = id(new DifferentialDiffProperty())->loadOneWhere(
'diffID = %d AND name = %s',
$diff_id,
'arc:lint'
);
if ($messages_property) {
$messages = $messages_property->getData();
} else {
$messages = array();
}
// Add new lint messages, removing duplicates.
foreach ($new_messages as $new_message) {
if (!in_array($new_message, $messages)) {
$messages[] = $new_message;
}
}
// Use setdiffproperty to update the postponed linters and messages,
// as these will also update the lint status correctly.
$call = new ConduitCall(
'differential.setdiffproperty',
array(
'diff_id' => $diff_id,
'name' => 'arc:lint',
'data' => json_encode($messages)));
$call->setUser($request->getUser());
$call->execute();
$call = new ConduitCall(
'differential.setdiffproperty',
array(
'diff_id' => $diff_id,
'name' => 'arc:lint-postponed',
'data' => json_encode($postponed_linters)));
$call->setUser($request->getUser());
$call->execute();
}
}
diff --git a/src/applications/conduit/method/differential/ConduitAPI_differential_getalldiffs_Method.php b/src/applications/conduit/method/differential/ConduitAPI_differential_getalldiffs_Method.php
index 22b487df12..117a30d270 100644
--- a/src/applications/conduit/method/differential/ConduitAPI_differential_getalldiffs_Method.php
+++ b/src/applications/conduit/method/differential/ConduitAPI_differential_getalldiffs_Method.php
@@ -1,64 +1,48 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_differential_getalldiffs_Method
extends ConduitAPIMethod {
public function getMethodDescription() {
return "Load all diffs for given revisions from Differential.";
}
public function defineParamTypes() {
return array(
'revision_ids' => 'required list<int>',
);
}
public function defineReturnType() {
return 'dict';
}
public function defineErrorTypes() {
return array();
}
protected function execute(ConduitAPIRequest $request) {
$results = array();
$revision_ids = $request->getValue('revision_ids');
if (!$revision_ids) {
return $results;
}
$diffs = id(new DifferentialDiff())->loadAllWhere(
'revisionID IN (%Ld)',
$revision_ids);
foreach ($diffs as $diff) {
$results[] = array(
'revision_id' => $diff->getRevisionID(),
'diff_id' => $diff->getID(),
);
}
return $results;
}
}
diff --git a/src/applications/conduit/method/differential/ConduitAPI_differential_getcommitmessage_Method.php b/src/applications/conduit/method/differential/ConduitAPI_differential_getcommitmessage_Method.php
index 0004884d02..30e4c2126e 100644
--- a/src/applications/conduit/method/differential/ConduitAPI_differential_getcommitmessage_Method.php
+++ b/src/applications/conduit/method/differential/ConduitAPI_differential_getcommitmessage_Method.php
@@ -1,145 +1,129 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_differential_getcommitmessage_Method
extends ConduitAPIMethod {
public function getMethodDescription() {
return "Retrieve Differential commit messages or message templates.";
}
public function defineParamTypes() {
return array(
'revision_id' => 'optional revision_id',
'fields' => 'optional dict<string, wild>',
'edit' => 'optional enum<"edit", "create">',
);
}
public function defineReturnType() {
return 'nonempty string';
}
public function defineErrorTypes() {
return array(
'ERR_NOT_FOUND' => 'Revision was not found.',
);
}
protected function execute(ConduitAPIRequest $request) {
$id = $request->getValue('revision_id');
if ($id) {
$revision = id(new DifferentialRevision())->load($id);
if (!$revision) {
throw new ConduitException('ERR_NOT_FOUND');
}
} else {
$revision = new DifferentialRevision();
}
$revision->loadRelationships();
$is_edit = $request->getValue('edit');
$is_create = ($is_edit == 'create');
$aux_fields = DifferentialFieldSelector::newSelector()
->getFieldSpecifications();
foreach ($aux_fields as $key => $aux_field) {
$aux_field->setRevision($revision);
if (!$aux_field->shouldAppearOnCommitMessage()) {
unset($aux_fields[$key]);
}
}
$aux_fields = DifferentialAuxiliaryField::loadFromStorage(
$revision,
$aux_fields);
$aux_fields = mpull($aux_fields, null, 'getCommitMessageKey');
if ($is_edit) {
$fields = $request->getValue('fields');
if (!is_array($fields)) {
$fields = array();
}
foreach ($fields as $field => $value) {
$aux_field = idx($aux_fields, $field);
if (!$aux_field) {
throw new Exception(
"Commit message includes field '{$field}' which does not ".
"correspond to any configured field.");
}
if ($is_create ||
$aux_field->shouldOverwriteWhenCommitMessageIsEdited()) {
$aux_field->setValueFromParsedCommitMessage($value);
}
}
}
$aux_phids = array();
foreach ($aux_fields as $field_key => $field) {
$aux_phids[$field_key] = $field->getRequiredHandlePHIDsForCommitMessage();
}
$phids = array_unique(array_mergev($aux_phids));
$handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
foreach ($aux_fields as $field_key => $field) {
$field->setHandles(array_select_keys($handles, $aux_phids[$field_key]));
}
$commit_message = array();
foreach ($aux_fields as $field_key => $field) {
$value = $field->renderValueForCommitMessage($is_edit);
$label = $field->renderLabelForCommitMessage();
if (!strlen($value)) {
if ($field_key === 'title') {
$commit_message[] = '<<Enter Revision Title>>';
} else {
if ($field->shouldAppearOnCommitMessageTemplate() && $is_edit) {
$commit_message[] = $label.': ';
}
}
} else {
if ($field_key === 'title') {
$commit_message[] = $value;
} else {
$value = str_replace(
array("\r\n", "\r"),
array("\n", "\n"),
$value);
if (strpos($value, "\n") !== false || substr($value, 0, 2) === ' ') {
$commit_message[] = "{$label}:\n{$value}";
} else {
$commit_message[] = "{$label}: {$value}";
}
}
}
}
$commit_message = implode("\n\n", $commit_message);
return $commit_message;
}
}
diff --git a/src/applications/conduit/method/differential/ConduitAPI_differential_getcommitpaths_Method.php b/src/applications/conduit/method/differential/ConduitAPI_differential_getcommitpaths_Method.php
index aea470403e..2cae4c63a0 100644
--- a/src/applications/conduit/method/differential/ConduitAPI_differential_getcommitpaths_Method.php
+++ b/src/applications/conduit/method/differential/ConduitAPI_differential_getcommitpaths_Method.php
@@ -1,68 +1,52 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_differential_getcommitpaths_Method
extends ConduitAPIMethod {
public function getMethodDescription() {
return "Query which paths should be included when committing a ".
"Differential revision.";
}
public function defineParamTypes() {
return array(
'revision_id' => 'required int',
);
}
public function defineReturnType() {
return 'nonempty list<string>';
}
public function defineErrorTypes() {
return array(
'ERR_NOT_FOUND' => 'No such revision exists.',
);
}
protected function execute(ConduitAPIRequest $request) {
$id = $request->getValue('revision_id');
$revision = id(new DifferentialRevision())->load($id);
if (!$revision) {
throw new ConduitException('ERR_NOT_FOUND');
}
$paths = array();
$diff = id(new DifferentialDiff())->loadOneWhere(
'revisionID = %d ORDER BY id DESC limit 1',
$revision->getID());
$diff->attachChangesets($diff->loadChangesets());
foreach ($diff->getChangesets() as $changeset) {
$paths[] = $changeset->getFilename();
}
return $paths;
}
}
diff --git a/src/applications/conduit/method/differential/ConduitAPI_differential_getdiff_Method.php b/src/applications/conduit/method/differential/ConduitAPI_differential_getdiff_Method.php
index 9f8920951a..85b59c72b6 100644
--- a/src/applications/conduit/method/differential/ConduitAPI_differential_getdiff_Method.php
+++ b/src/applications/conduit/method/differential/ConduitAPI_differential_getdiff_Method.php
@@ -1,95 +1,79 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_differential_getdiff_Method extends ConduitAPIMethod {
public function getMethodDescription() {
return "Load the content of a diff from Differential.";
}
public function defineParamTypes() {
return array(
'revision_id' => 'optional id',
'diff_id' => 'optional id',
);
}
public function defineReturnType() {
return 'nonempty dict';
}
public function defineErrorTypes() {
return array(
'ERR_BAD_REVISION' => 'No such revision exists.',
'ERR_BAD_DIFF' => 'No such diff exists.',
);
}
public function shouldRequireAuthentication() {
return !PhabricatorEnv::getEnvConfig('differential.anonymous-access');
}
protected function execute(ConduitAPIRequest $request) {
$diff = null;
$revision_id = $request->getValue('revision_id');
if ($revision_id) {
$revision = id(new DifferentialRevision())->load($revision_id);
if (!$revision) {
throw new ConduitException('ERR_BAD_REVISION');
}
$diff = id(new DifferentialDiff())->loadOneWhere(
'revisionID = %d ORDER BY id DESC LIMIT 1',
$revision->getID());
} else {
$diff_id = $request->getValue('diff_id');
if ($diff_id) {
$diff = id(new DifferentialDiff())->load($diff_id);
}
}
if (!$diff) {
throw new ConduitException('ERR_BAD_DIFF');
}
$diff->attachChangesets(
$diff->loadRelatives(new DifferentialChangeset(), 'diffID'));
foreach ($diff->getChangesets() as $changeset) {
$changeset->attachHunks(
$changeset->loadRelatives(new DifferentialHunk(), 'changesetID'));
}
$basic_dict = $diff->getDiffDict();
// for conduit calls, the basic dict is not enough
// we also need to include the arcanist project
$project = $diff->loadArcanistProject();
if ($project) {
$project_name = $project->getName();
} else {
$project_name = null;
}
$basic_dict['projectName'] = $project_name;
return $basic_dict;
}
}
diff --git a/src/applications/conduit/method/differential/ConduitAPI_differential_getrevision_Method.php b/src/applications/conduit/method/differential/ConduitAPI_differential_getrevision_Method.php
index 5e50142db5..57568c7bbf 100644
--- a/src/applications/conduit/method/differential/ConduitAPI_differential_getrevision_Method.php
+++ b/src/applications/conduit/method/differential/ConduitAPI_differential_getrevision_Method.php
@@ -1,129 +1,113 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_differential_getrevision_Method
extends ConduitAPIMethod {
public function getMethodStatus() {
return self::METHOD_STATUS_DEPRECATED;
}
public function getMethodStatusDescription() {
return "Replaced by 'differential.query'.";
}
public function getMethodDescription() {
return "Load the content of a revision from Differential.";
}
public function defineParamTypes() {
return array(
'revision_id' => 'required id',
);
}
public function defineReturnType() {
return 'nonempty dict';
}
public function defineErrorTypes() {
return array(
'ERR_BAD_REVISION' => 'No such revision exists.',
);
}
protected function execute(ConduitAPIRequest $request) {
$diff = null;
$revision_id = $request->getValue('revision_id');
$revision = id(new DifferentialRevision())->load($revision_id);
if (!$revision) {
throw new ConduitException('ERR_BAD_REVISION');
}
$revision->loadRelationships();
$reviewer_phids = array_values($revision->getReviewers());
$diffs = $revision->loadDiffs();
$diff_dicts = array();
foreach ($diffs as $diff) {
$diff->attachChangesets($diff->loadChangesets());
// TODO: We could batch this to improve performance.
foreach ($diff->getChangesets() as $changeset) {
$changeset->attachHunks($changeset->loadHunks());
}
$diff_dicts[] = $diff->getDiffDict();
}
$commit_dicts = array();
$commit_phids = $revision->loadCommitPHIDs();
$handles = id(new PhabricatorObjectHandleData($commit_phids))
->loadHandles();
foreach ($commit_phids as $commit_phid) {
$commit_dicts[] = array(
'fullname' => $handles[$commit_phid]->getFullName(),
'dateCommitted' => $handles[$commit_phid]->getTimestamp(),
);
}
$auxiliary_fields = $this->loadAuxiliaryFields($revision);
$dict = array(
'id' => $revision->getID(),
'phid' => $revision->getPHID(),
'authorPHID' => $revision->getAuthorPHID(),
'uri' => PhabricatorEnv::getURI('/D'.$revision->getID()),
'title' => $revision->getTitle(),
'status' => $revision->getStatus(),
'statusName' =>
ArcanistDifferentialRevisionStatus::getNameForRevisionStatus(
$revision->getStatus()),
'summary' => $revision->getSummary(),
'testPlan' => $revision->getTestPlan(),
'lineCount' => $revision->getLineCount(),
'reviewerPHIDs' => $reviewer_phids,
'diffs' => $diff_dicts,
'commits' => $commit_dicts,
'auxiliary' => $auxiliary_fields,
);
return $dict;
}
private function loadAuxiliaryFields(DifferentialRevision $revision) {
$aux_fields = DifferentialFieldSelector::newSelector()
->getFieldSpecifications();
foreach ($aux_fields as $key => $aux_field) {
if (!$aux_field->shouldAppearOnConduitView()) {
unset($aux_fields[$key]);
}
}
$aux_fields = DifferentialAuxiliaryField::loadFromStorage(
$revision,
$aux_fields);
return mpull($aux_fields, 'getValueForConduit', 'getKeyForConduit');
}
}
diff --git a/src/applications/conduit/method/differential/ConduitAPI_differential_getrevisioncomments_Method.php b/src/applications/conduit/method/differential/ConduitAPI_differential_getrevisioncomments_Method.php
index 8d71da0149..f3b98110ad 100644
--- a/src/applications/conduit/method/differential/ConduitAPI_differential_getrevisioncomments_Method.php
+++ b/src/applications/conduit/method/differential/ConduitAPI_differential_getrevisioncomments_Method.php
@@ -1,97 +1,81 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_differential_getrevisioncomments_Method
extends ConduitAPI_differential_Method {
public function getMethodDescription() {
return "Retrieve Differential Revision Comments.";
}
public function defineParamTypes() {
return array(
'ids' => 'required list<int>',
'inlines' => 'optional bool',
);
}
public function defineReturnType() {
return 'nonempty list<dict<string, wild>>';
}
public function defineErrorTypes() {
return array(
);
}
protected function execute(ConduitAPIRequest $request) {
$results = array();
$revision_ids = $request->getValue('ids');
if (!$revision_ids) {
return $results;
}
$comments = id(new DifferentialComment())->loadAllWhere(
'revisionID IN (%Ld)',
$revision_ids);
$with_inlines = $request->getValue('inlines');
if ($with_inlines) {
$inlines = id(new DifferentialInlineComment())->loadAllWhere(
'revisionID IN (%Ld)',
$revision_ids);
$changesets = array();
if ($inlines) {
$changesets = id(new DifferentialChangeset())->loadAllWhere(
'id IN (%Ld)',
array_unique(mpull($inlines, 'getChangesetID')));
$inlines = mgroup($inlines, 'getCommentID');
}
}
foreach ($comments as $comment) {
$revision_id = $comment->getRevisionID();
$result = array(
'revisionID' => $revision_id,
'action' => $comment->getAction(),
'authorPHID' => $comment->getAuthorPHID(),
'dateCreated' => $comment->getDateCreated(),
'content' => $comment->getContent(),
);
if ($with_inlines) {
$result['inlines'] = array();
foreach (idx($inlines, $comment->getID(), array()) as $inline) {
$changeset = idx($changesets, $inline->getChangesetID());
$result['inlines'][] = $this->buildInlineInfoDictionary(
$inline,
$changeset);
}
// TODO: Put synthetic inlines without an attached comment somewhere.
}
$results[$revision_id][] = $result;
}
return $results;
}
}
diff --git a/src/applications/conduit/method/differential/ConduitAPI_differential_getrevisionfeedback_Method.php b/src/applications/conduit/method/differential/ConduitAPI_differential_getrevisionfeedback_Method.php
index 70954ae50c..fb78f43a5d 100644
--- a/src/applications/conduit/method/differential/ConduitAPI_differential_getrevisionfeedback_Method.php
+++ b/src/applications/conduit/method/differential/ConduitAPI_differential_getrevisionfeedback_Method.php
@@ -1,83 +1,67 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_differential_getrevisionfeedback_Method
extends ConduitAPIMethod {
public function getMethodStatus() {
return self::METHOD_STATUS_DEPRECATED;
}
public function getMethodStatusDescription() {
return "Replaced by 'differential.getrevisioncomments'.";
}
public function getMethodDescription() {
return "Retrieve Differential Revision Feedback.";
}
public function defineParamTypes() {
return array(
'ids' => 'required list<int>',
);
}
public function defineReturnType() {
return 'nonempty list<dict<string, wild>>';
}
public function defineErrorTypes() {
return array(
);
}
protected function execute(ConduitAPIRequest $request) {
$results = array();
$revision_ids = $request->getValue('ids');
if (!$revision_ids) {
return $results;
}
$comments = id(new DifferentialComment())->loadAllWhere(
'revisionID IN (%Ld)',
$revision_ids);
// Helper dictionary to keep track of where the id/action pair is
// stored in results array.
$indexes = array();
foreach ($comments as $comment) {
$action = $comment->getAction();
$revision_id = $comment->getRevisionID();
if (isset($indexes[$action.$revision_id])) {
$results[$indexes[$action.$revision_id]]['count']++;
} else {
$indexes[$action.$revision_id] = count($results);
$results[] = array('id' => $revision_id,
'action' => $action,
'count' => 1);
}
}
return $results;
}
}
diff --git a/src/applications/conduit/method/differential/ConduitAPI_differential_markcommitted_Method.php b/src/applications/conduit/method/differential/ConduitAPI_differential_markcommitted_Method.php
index 8c836421e1..e0328b66a6 100644
--- a/src/applications/conduit/method/differential/ConduitAPI_differential_markcommitted_Method.php
+++ b/src/applications/conduit/method/differential/ConduitAPI_differential_markcommitted_Method.php
@@ -1,75 +1,59 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
* @deprecated
*/
final class ConduitAPI_differential_markcommitted_Method
extends ConduitAPIMethod {
public function getMethodStatus() {
return self::METHOD_STATUS_DEPRECATED;
}
public function getMethodStatusDescription() {
return "Replaced by 'differential.close'.";
}
public function getMethodDescription() {
return "Mark a revision closed.";
}
public function defineParamTypes() {
return array(
'revision_id' => 'required revision_id',
);
}
public function defineReturnType() {
return 'void';
}
public function defineErrorTypes() {
return array(
'ERR_NOT_FOUND' => 'Revision was not found.',
);
}
protected function execute(ConduitAPIRequest $request) {
$id = $request->getValue('revision_id');
$revision = id(new DifferentialRevision())->load($id);
if (!$revision) {
throw new ConduitException('ERR_NOT_FOUND');
}
if ($revision->getStatus() == ArcanistDifferentialRevisionStatus::CLOSED) {
return;
}
$revision->loadRelationships();
$editor = new DifferentialCommentEditor(
$revision,
DifferentialAction::ACTION_CLOSE);
$editor->setActor($request->getUser());
$editor->save();
}
}
diff --git a/src/applications/conduit/method/differential/ConduitAPI_differential_parsecommitmessage_Method.php b/src/applications/conduit/method/differential/ConduitAPI_differential_parsecommitmessage_Method.php
index bdccc8c793..0766c2b27b 100644
--- a/src/applications/conduit/method/differential/ConduitAPI_differential_parsecommitmessage_Method.php
+++ b/src/applications/conduit/method/differential/ConduitAPI_differential_parsecommitmessage_Method.php
@@ -1,192 +1,176 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_differential_parsecommitmessage_Method
extends ConduitAPIMethod {
private $errors;
public function getMethodDescription() {
return "Parse commit messages for Differential fields.";
}
public function defineParamTypes() {
return array(
'corpus' => 'required string',
'partial' => 'optional bool',
);
}
public function defineReturnType() {
return 'nonempty dict';
}
public function defineErrorTypes() {
return array(
);
}
protected function execute(ConduitAPIRequest $request) {
$corpus = $request->getValue('corpus');
$is_partial = $request->getValue('partial');
$aux_fields = DifferentialFieldSelector::newSelector()
->getFieldSpecifications();
foreach ($aux_fields as $key => $aux_field) {
if (!$aux_field->shouldAppearOnCommitMessage()) {
unset($aux_fields[$key]);
}
$aux_field->setUser($request->getUser());
}
$aux_fields = mpull($aux_fields, null, 'getCommitMessageKey');
$this->errors = array();
// Build a map from labels (like "Test Plan") to field keys
// (like "testPlan").
$label_map = $this->buildLabelMap($aux_fields);
$field_map = $this->parseCommitMessage($corpus, $label_map);
$fields = array();
foreach ($field_map as $field_key => $field_value) {
$field = $aux_fields[$field_key];
try {
$fields[$field_key] = $field->parseValueFromCommitMessage($field_value);
$field->setValueFromParsedCommitMessage($fields[$field_key]);
} catch (DifferentialFieldParseException $ex) {
$field_label = $field->renderLabelForCommitMessage();
$this->errors[] =
"Error parsing field '{$field_label}': ".$ex->getMessage();
}
}
if (!$is_partial) {
foreach ($aux_fields as $field_key => $aux_field) {
try {
$aux_field->validateField();
} catch (DifferentialFieldValidationException $ex) {
$field_label = $aux_field->renderLabelForCommitMessage();
$this->errors[] =
"Invalid or missing field '{$field_label}': ".
$ex->getMessage();
}
}
}
return array(
'errors' => $this->errors,
'fields' => $fields,
);
}
private function buildLabelMap(array $aux_fields) {
assert_instances_of($aux_fields, 'DifferentialFieldSpecification');
$label_map = array();
foreach ($aux_fields as $key => $aux_field) {
$labels = $aux_field->getSupportedCommitMessageLabels();
foreach ($labels as $label) {
$normal_label = strtolower($label);
if (!empty($label_map[$normal_label])) {
$previous = $label_map[$normal_label];
throw new Exception(
"Field label '{$label}' is parsed by two fields: '{$key}' and ".
"'{$previous}'. Each label must be parsed by only one field.");
}
$label_map[$normal_label] = $key;
}
}
return $label_map;
}
private function buildLabelRegexp(array $label_map) {
$field_labels = array_keys($label_map);
foreach ($field_labels as $key => $label) {
$field_labels[$key] = preg_quote($label, '/');
}
$field_labels = implode('|', $field_labels);
$field_pattern = '/^(?P<field>'.$field_labels.'):(?P<text>.*)$/i';
return $field_pattern;
}
private function parseCommitMessage($corpus, array $label_map) {
$label_regexp = $this->buildLabelRegexp($label_map);
// Note, deliberately not populating $seen with 'title' because it is
// optional to include the 'Title:' label. We're doing a little special
// casing to consume the first line as the title regardless of whether you
// label it as such or not.
$field = 'title';
$seen = array();
$lines = explode("\n", trim($corpus));
$field_map = array();
foreach ($lines as $key => $line) {
$match = null;
if (preg_match($label_regexp, $line, $match)) {
$lines[$key] = trim($match['text']);
$field = $label_map[strtolower($match['field'])];
if (!empty($seen[$field])) {
$this->errors[] = "Field '{$field}' occurs twice in commit message!";
}
$seen[$field] = true;
}
$field_map[$key] = $field;
}
$fields = array();
foreach ($lines as $key => $line) {
$fields[$field_map[$key]][] = $line;
}
// This is a piece of special-cased magic which allows you to omit the
// field labels for "title" and "summary". If the user enters a large block
// of text at the beginning of the commit message with an empty line in it,
// treat everything before the blank line as "title" and everything after
// as "summary".
if (isset($fields['title']) && empty($fields['summary'])) {
$lines = $fields['title'];
for ($ii = 0; $ii < count($lines); $ii++) {
if (strlen(trim($lines[$ii])) == 0) {
break;
}
}
if ($ii != count($lines)) {
$fields['title'] = array_slice($lines, 0, $ii);
$fields['summary'] = array_slice($lines, $ii);
}
}
// Implode all the lines back into chunks of text.
foreach ($fields as $name => $lines) {
$data = rtrim(implode("\n", $lines));
$data = ltrim($data, "\n");
$fields[$name] = $data;
}
return $fields;
}
}
diff --git a/src/applications/conduit/method/differential/ConduitAPI_differential_query_Method.php b/src/applications/conduit/method/differential/ConduitAPI_differential_query_Method.php
index 83097017be..a6a37ed302 100644
--- a/src/applications/conduit/method/differential/ConduitAPI_differential_query_Method.php
+++ b/src/applications/conduit/method/differential/ConduitAPI_differential_query_Method.php
@@ -1,249 +1,233 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_differential_query_Method
extends ConduitAPIMethod {
public function getMethodDescription() {
return "Query Differential revisions which match certain criteria.";
}
public function defineParamTypes() {
$hash_types = ArcanistDifferentialRevisionHash::getTypes();
$hash_types = implode(', ', $hash_types);
$status_types = array(
DifferentialRevisionQuery::STATUS_ANY,
DifferentialRevisionQuery::STATUS_OPEN,
DifferentialRevisionQuery::STATUS_ACCEPTED,
DifferentialRevisionQuery::STATUS_CLOSED,
);
$status_types = implode(', ', $status_types);
$order_types = array(
DifferentialRevisionQuery::ORDER_MODIFIED,
DifferentialRevisionQuery::ORDER_CREATED,
);
$order_types = implode(', ', $order_types);
return array(
'authors' => 'optional list<phid>',
'ccs' => 'optional list<phid>',
'reviewers' => 'optional list<phid>',
'paths' => 'optional list<pair<callsign, path>>',
'commitHashes' => 'optional list<pair<enum<'.
$hash_types.'>, string>>',
'status' => 'optional enum<'.$status_types.'>',
'order' => 'optional enum<'.$order_types.'>',
'limit' => 'optional uint',
'offset' => 'optional uint',
'ids' => 'optional list<uint>',
'phids' => 'optional list<phid>',
'subscribers' => 'optional list<phid>',
'responsibleUsers' => 'optional list<phid>',
'branches' => 'optional list<string>',
'arcanistProjects' => 'optional list<string>',
);
}
public function defineReturnType() {
return 'list<dict>';
}
public function defineErrorTypes() {
return array(
'ERR-INVALID-PARAMETER' => 'Missing or malformed parameter.',
);
}
protected function execute(ConduitAPIRequest $request) {
$authors = $request->getValue('authors');
$ccs = $request->getValue('ccs');
$reviewers = $request->getValue('reviewers');
$status = $request->getValue('status');
$order = $request->getValue('order');
$path_pairs = $request->getValue('paths');
$commit_hashes = $request->getValue('commitHashes');
$limit = $request->getValue('limit');
$offset = $request->getValue('offset');
$ids = $request->getValue('ids');
$phids = $request->getValue('phids');
$subscribers = $request->getValue('subscribers');
$responsible_users = $request->getValue('responsibleUsers');
$branches = $request->getValue('branches');
$arc_projects = $request->getValue('arcanistProjects');
$query = new DifferentialRevisionQuery();
if ($authors) {
$query->withAuthors($authors);
}
if ($ccs) {
$query->withCCs($ccs);
}
if ($reviewers) {
$query->withReviewers($reviewers);
}
if ($path_pairs) {
$paths = array();
foreach ($path_pairs as $pair) {
list($callsign, $path) = $pair;
$paths[] = $path;
}
$path_map = id(new DiffusionPathIDQuery($paths))->loadPathIDs();
if (count($path_map) != count($paths)) {
$unknown_paths = array();
foreach ($paths as $p) {
if (!idx($path_map, $p)) {
$unknown_paths[] = $p;
}
}
throw id(new ConduitException('ERR-INVALID-PARAMETER'))
->setErrorDescription(
'Unknown paths: '.implode(', ', $unknown_paths));
}
$repos = array();
foreach ($path_pairs as $pair) {
list($callsign, $path) = $pair;
if (!idx($repos, $callsign)) {
$repos[$callsign] = id(new PhabricatorRepository())->loadOneWhere(
'callsign = %s',
$callsign);
if (!$repos[$callsign]) {
throw id(new ConduitException('ERR-INVALID-PARAMETER'))
->setErrorDescription(
'Unknown repo callsign: '.$callsign);
}
}
$repo = $repos[$callsign];
$query->withPath($repo->getID(), idx($path_map, $path));
}
}
if ($commit_hashes) {
$hash_types = ArcanistDifferentialRevisionHash::getTypes();
foreach ($commit_hashes as $info) {
list($type, $hash) = $info;
if (empty($type) ||
!in_array($type, $hash_types) ||
empty($hash)) {
throw new ConduitException('ERR-INVALID-PARAMETER');
}
}
$query->withCommitHashes($commit_hashes);
}
if ($status) {
$query->withStatus($status);
}
if ($order) {
$query->setOrder($order);
}
if ($limit) {
$query->setLimit($limit);
}
if ($offset) {
$query->setOffset($offset);
}
if ($ids) {
$query->withIDs($ids);
}
if ($phids) {
$query->withPHIDs($phids);
}
if ($responsible_users) {
$query->withResponsibleUsers($responsible_users);
}
if ($subscribers) {
$query->withSubscribers($subscribers);
}
if ($branches) {
$query->withBranches($branches);
}
if ($arc_projects) {
// This is sort of special-cased, but don't make arc do an extra round
// trip.
$projects = id(new PhabricatorRepositoryArcanistProject())
->loadAllWhere(
'name in (%Ls)',
$arc_projects);
if (!$projects) {
return array();
}
$query->withArcanistProjectPHIDs(mpull($projects, 'getPHID'));
}
$query->needRelationships(true);
$query->needCommitPHIDs(true);
$query->needDiffIDs(true);
$query->needActiveDiffs(true);
$query->needHashes(true);
$revisions = $query->execute();
$results = array();
foreach ($revisions as $revision) {
$diff = $revision->getActiveDiff();
if (!$diff) {
continue;
}
$id = $revision->getID();
$result = array(
'id' => $id,
'phid' => $revision->getPHID(),
'title' => $revision->getTitle(),
'uri' => PhabricatorEnv::getProductionURI('/D'.$id),
'dateCreated' => $revision->getDateCreated(),
'dateModified' => $revision->getDateModified(),
'authorPHID' => $revision->getAuthorPHID(),
'status' => $revision->getStatus(),
'statusName' =>
ArcanistDifferentialRevisionStatus::getNameForRevisionStatus(
$revision->getStatus()),
'branch' => $diff->getBranch(),
'summary' => $revision->getSummary(),
'testPlan' => $revision->getTestPlan(),
'lineCount' => $revision->getLineCount(),
'diffs' => $revision->getDiffIDs(),
'commits' => $revision->getCommitPHIDs(),
'reviewers' => array_values($revision->getReviewers()),
'ccs' => array_values($revision->getCCPHIDs()),
'hashes' => $revision->getHashes(),
);
// TODO: This is a hacky way to put permissions on this field until we
// have first-class support, see T838.
if ($revision->getAuthorPHID() == $request->getUser()->getPHID()) {
$result['sourcePath'] = $diff->getSourcePath();
}
$results[] = $result;
}
return $results;
}
}
diff --git a/src/applications/conduit/method/differential/ConduitAPI_differential_setdiffproperty_Method.php b/src/applications/conduit/method/differential/ConduitAPI_differential_setdiffproperty_Method.php
index 703534ca45..8613195f7d 100644
--- a/src/applications/conduit/method/differential/ConduitAPI_differential_setdiffproperty_Method.php
+++ b/src/applications/conduit/method/differential/ConduitAPI_differential_setdiffproperty_Method.php
@@ -1,131 +1,115 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_differential_setdiffproperty_Method
extends ConduitAPIMethod {
public function getMethodDescription() {
return "Attach properties to Differential diffs.";
}
public function defineParamTypes() {
return array(
'diff_id' => 'required diff_id',
'name' => 'required string',
'data' => 'required string',
);
}
public function defineReturnType() {
return 'void';
}
public function defineErrorTypes() {
return array(
'ERR_NOT_FOUND' => 'Diff was not found.',
);
}
private static function updateLintStatus($diff_id) {
$diff = id(new DifferentialDiff())->load($diff_id);
if (!$diff) {
throw new ConduitException('ERR_NOT_FOUND');
}
// Load the postponed linters attached to this diff.
$postponed_linters_property = id(
new DifferentialDiffProperty())->loadOneWhere(
'diffID = %d AND name = %s',
$diff_id,
'arc:lint-postponed');
if ($postponed_linters_property) {
$postponed_linters = $postponed_linters_property->getData();
} else {
$postponed_linters = array();
}
// Load the lint messages currenty attached to the diff
$messages_property = id(new DifferentialDiffProperty())->loadOneWhere(
'diffID = %d AND name = %s',
$diff_id,
'arc:lint'
);
if ($messages_property) {
$results = $messages_property->getData();
} else {
$results = array();
}
$has_error = false;
$has_warning = false;
foreach ($results as $result) {
if ($result['severity'] === ArcanistLintSeverity::SEVERITY_ERROR) {
$has_error = true;
break;
} else if ($result['severity'] ===
ArcanistLintSeverity::SEVERITY_WARNING) {
$has_warning = true;
}
}
if ($has_error) {
$diff->setLintStatus(DifferentialLintStatus::LINT_FAIL);
} else if ($has_warning) {
$diff->setLintStatus(DifferentialLintStatus::LINT_WARN);
} else if (!empty($postponed_linters)) {
$diff->setLintStatus(DifferentialLintStatus::LINT_POSTPONED);
} else if ($results &&
$diff->getLintStatus() === DifferentialLintStatus::LINT_NONE) {
$diff->setLintStatus(DifferentialLintStatus::LINT_OKAY);
}
$diff->save();
}
protected function execute(ConduitAPIRequest $request) {
$diff_id = $request->getValue('diff_id');
$name = $request->getValue('name');
$data = json_decode($request->getValue('data'), true);
self::updateDiffProperty($diff_id, $name, $data);
if ($name === 'arc:lint' || $name == 'arc:lint-postponed') {
self::updateLintStatus($diff_id);
}
return;
}
private static function updateDiffProperty($diff_id, $name, $data) {
$property = id(new DifferentialDiffProperty())->loadOneWhere(
'diffID = %d AND name = %s',
$diff_id,
$name);
if (!$property) {
$property = new DifferentialDiffProperty();
$property->setDiffID($diff_id);
$property->setName($name);
}
$property->setData($data);
$property->save();
return $property;
}
}
diff --git a/src/applications/conduit/method/differential/ConduitAPI_differential_updaterevision_Method.php b/src/applications/conduit/method/differential/ConduitAPI_differential_updaterevision_Method.php
index 1a94a74063..a18a60bb64 100644
--- a/src/applications/conduit/method/differential/ConduitAPI_differential_updaterevision_Method.php
+++ b/src/applications/conduit/method/differential/ConduitAPI_differential_updaterevision_Method.php
@@ -1,90 +1,74 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_differential_updaterevision_Method
extends ConduitAPIMethod {
public function getMethodDescription() {
return "Update a Differential revision.";
}
public function defineParamTypes() {
return array(
'id' => 'required revisionid',
'diffid' => 'required diffid',
'fields' => 'required dict',
'message' => 'required string',
);
}
public function defineReturnType() {
return 'nonempty dict';
}
public function defineErrorTypes() {
return array(
'ERR_BAD_DIFF' => 'Bad diff ID.',
'ERR_BAD_REVISION' => 'Bad revision ID.',
'ERR_WRONG_USER' => 'You are not the author of this revision.',
'ERR_CLOSED' => 'This revision has already been closed.',
);
}
protected function execute(ConduitAPIRequest $request) {
$diff = id(new DifferentialDiff())->load($request->getValue('diffid'));
if (!$diff) {
throw new ConduitException('ERR_BAD_DIFF');
}
$revision = id(new DifferentialRevision())->load($request->getValue('id'));
if (!$revision) {
throw new ConduitException('ERR_BAD_REVISION');
}
if ($request->getUser()->getPHID() !== $revision->getAuthorPHID()) {
throw new ConduitException('ERR_WRONG_USER');
}
if ($revision->getStatus() == ArcanistDifferentialRevisionStatus::CLOSED) {
throw new ConduitException('ERR_CLOSED');
}
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_CONDUIT,
array());
$editor = new DifferentialRevisionEditor(
$revision);
$editor->setActor($request->getUser());
$editor->setContentSource($content_source);
$fields = $request->getValue('fields');
$editor->copyFieldsFromConduit($fields);
$editor->addDiff($diff, $request->getValue('message'));
$editor->save();
return array(
'revisionid' => $revision->getID(),
'uri' => PhabricatorEnv::getURI('/D'.$revision->getID()),
);
}
}
diff --git a/src/applications/conduit/method/differential/ConduitAPI_differential_updatetaskrevisionassoc_Method.php b/src/applications/conduit/method/differential/ConduitAPI_differential_updatetaskrevisionassoc_Method.php
index 9598159e99..4e4b4db4d2 100644
--- a/src/applications/conduit/method/differential/ConduitAPI_differential_updatetaskrevisionassoc_Method.php
+++ b/src/applications/conduit/method/differential/ConduitAPI_differential_updatetaskrevisionassoc_Method.php
@@ -1,81 +1,65 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_differential_updatetaskrevisionassoc_Method
extends ConduitAPIMethod {
public function getMethodStatus() {
return self::METHOD_STATUS_DEPRECATED;
}
public function getMethodStatusDescription() {
return "This method should not really exist. Pretend it doesn't.";
}
public function getMethodDescription() {
return "Given a task together with its original and new associated ".
"revisions, update the revisions for their attached_tasks.";
}
public function defineParamTypes() {
return array(
'task_phid' => 'required nonempty string',
'orig_rev_phids' => 'required list<string>',
'new_rev_phids' => 'required list<string>',
);
}
public function defineReturnType() {
return 'void';
}
public function defineErrorTypes() {
return array(
'ERR_NO_TASKATTACHER_DEFINED' => 'No task attacher defined.',
);
}
protected function execute(ConduitAPIRequest $request) {
$task_phid = $request->getValue('task_phid');
$orig_rev_phids = $request->getValue('orig_rev_phids');
if (empty($orig_rev_phids)) {
$orig_rev_phids = array();
}
$new_rev_phids = $request->getValue('new_rev_phids');
if (empty($new_rev_phids)) {
$new_rev_phids = array();
}
try {
$task_attacher = PhabricatorEnv::newObjectFromConfig(
'differential.attach-task-class');
$task_attacher->updateTaskRevisionAssoc(
$task_phid,
$orig_rev_phids,
$new_rev_phids);
} catch (ReflectionException $ex) {
throw new ConduitException('ERR_NO_TASKATTACHER_DEFINED');
}
}
}
diff --git a/src/applications/conduit/method/differential/ConduitAPI_differential_updateunitresults_Method.php b/src/applications/conduit/method/differential/ConduitAPI_differential_updateunitresults_Method.php
index b2540787cf..3ff34aef78 100644
--- a/src/applications/conduit/method/differential/ConduitAPI_differential_updateunitresults_Method.php
+++ b/src/applications/conduit/method/differential/ConduitAPI_differential_updateunitresults_Method.php
@@ -1,164 +1,148 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_differential_updateunitresults_Method
extends ConduitAPIMethod {
public function getMethodDescription() {
return "Update arc unit results for a postponed test.";
}
public function defineParamTypes() {
return array(
'diff_id' => 'required diff_id',
'file' => 'required string',
'name' => 'required string',
'link' => 'optional string',
'result' => 'required string',
'message' => 'required string',
'coverage' => 'optional map<string, string>',
);
}
public function defineReturnType() {
return 'void';
}
public function defineErrorTypes() {
return array(
'ERR_BAD_DIFF' => 'Bad diff ID.',
'ERR_NO_RESULTS' => 'Could not find the postponed test',
);
}
protected function execute(ConduitAPIRequest $request) {
$diff_id = $request->getValue('diff_id');
if (!$diff_id) {
throw new ConduitException('ERR_BAD_DIFF');
}
$file = $request->getValue('file');
$name = $request->getValue('name');
$link = $request->getValue('link');
$message = $request->getValue('message');
$result = $request->getValue('result');
$coverage = $request->getValue('coverage', array());
$diff_property = id(new DifferentialDiffProperty())->loadOneWhere(
'diffID = %d AND name = %s',
$diff_id,
'arc:unit'
);
if (!$diff_property) {
throw new ConduitException('ERR_NO_RESULTS');
}
$diff = id(new DifferentialDiff())->load($diff_id);
$unit_results = $diff_property->getData();
$postponed_count = 0;
$unit_status = null;
// If the test result already exists, then update it with
// the new info.
foreach ($unit_results as &$unit_result) {
if ($unit_result['name'] === $name ||
$unit_result['name'] === $file ||
$unit_result['name'] === $diff->getSourcePath().$file) {
$unit_result['name'] = $name;
$unit_result['link'] = $link;
$unit_result['file'] = $file;
$unit_result['result'] = $result;
$unit_result['userdata'] = $message;
$unit_result['coverage'] = $coverage;
$unit_status = $result;
break;
}
}
unset($unit_result);
// If the test result doesn't exist, just add it.
if (!$unit_status) {
$unit_result = array();
$unit_result['file'] = $file;
$unit_result['name'] = $name;
$unit_result['link'] = $link;
$unit_result['result'] = $result;
$unit_result['userdata'] = $message;
$unit_result['coverage'] = $coverage;
$unit_status = $result;
$unit_results[] = $unit_result;
}
unset($unit_result);
$diff_property->setData($unit_results);
$diff_property->save();
// Map external unit test status to internal overall diff status
$status_codes =
array(
DifferentialUnitTestResult::RESULT_PASS =>
DifferentialUnitStatus::UNIT_OKAY,
DifferentialUnitTestResult::RESULT_UNSOUND =>
DifferentialUnitStatus::UNIT_WARN,
DifferentialUnitTestResult::RESULT_FAIL =>
DifferentialUnitStatus::UNIT_FAIL,
DifferentialUnitTestResult::RESULT_BROKEN =>
DifferentialUnitStatus::UNIT_FAIL,
DifferentialUnitTestResult::RESULT_SKIP =>
DifferentialUnitStatus::UNIT_OKAY,
DifferentialUnitTestResult::RESULT_POSTPONED =>
DifferentialUnitStatus::UNIT_POSTPONED);
// These are the relative priorities for the unit test results
$status_codes_priority =
array(
DifferentialUnitStatus::UNIT_OKAY => 1,
DifferentialUnitStatus::UNIT_WARN => 2,
DifferentialUnitStatus::UNIT_POSTPONED => 3,
DifferentialUnitStatus::UNIT_FAIL => 4);
// Walk the now-current list of status codes to find the overall diff
// status
$final_diff_status = DifferentialUnitStatus::UNIT_NONE;
foreach ($unit_results as $unit_result) {
// Convert the text result into a diff unit status value
$status_code = idx($status_codes,
$unit_result['result'],
DifferentialUnitStatus::UNIT_NONE);
// Convert the unit status into a relative value
$diff_status_priority = idx($status_codes_priority, $status_code, 0);
// If the relative value of this result is "more bad" than previous
// results, use it as the new final diff status
if ($diff_status_priority > idx($status_codes_priority,
$final_diff_status, 0)) {
$final_diff_status = $status_code;
}
}
// Update our unit test result status with the final value
$diff->setUnitStatus($final_diff_status);
$diff->save();
}
}
diff --git a/src/applications/conduit/method/diffusion/ConduitAPI_diffusion_findsymbols_Method.php b/src/applications/conduit/method/diffusion/ConduitAPI_diffusion_findsymbols_Method.php
index eebf2dce39..6a3b3e9cab 100644
--- a/src/applications/conduit/method/diffusion/ConduitAPI_diffusion_findsymbols_Method.php
+++ b/src/applications/conduit/method/diffusion/ConduitAPI_diffusion_findsymbols_Method.php
@@ -1,100 +1,84 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_diffusion_findsymbols_Method
extends ConduitAPIMethod {
public function getMethodDescription() {
return "Retrieve Diffusion symbol information.";
}
public function defineParamTypes() {
return array(
'name' => 'optional string',
'namePrefix' => 'optional string',
'context' => 'optional string',
'language' => 'optional string',
'type' => 'optional string',
);
}
public function defineReturnType() {
return 'nonempty list<dict>';
}
public function defineErrorTypes() {
return array(
);
}
protected function execute(ConduitAPIRequest $request) {
$name = $request->getValue('name');
$name_prefix = $request->getValue('namePrefix');
$context = $request->getValue('context');
$language = $request->getValue('language');
$type = $request->getValue('type');
$query = new DiffusionSymbolQuery();
if ($name !== null) {
$query->setName($name);
}
if ($name_prefix !== null) {
$query->setNamePrefix($name_prefix);
}
if ($context !== null) {
$query->setContext($context);
}
if ($language !== null) {
$query->setLanguage($language);
}
if ($type !== null) {
$query->setType($type);
}
$query->needPaths(true);
$query->needArcanistProjects(true);
$query->needRepositories(true);
$results = $query->execute();
$response = array();
foreach ($results as $result) {
$uri = $result->getURI();
if ($uri) {
$uri = PhabricatorEnv::getProductionURI($uri);
}
$response[] = array(
'name' => $result->getSymbolName(),
'context' => $result->getSymbolContext(),
'type' => $result->getSymbolType(),
'language' => $result->getSymbolLanguage(),
'path' => $result->getPath(),
'line' => $result->getLineNumber(),
'uri' => $uri,
);
}
return $response;
}
}
diff --git a/src/applications/conduit/method/diffusion/ConduitAPI_diffusion_getcommits_Method.php b/src/applications/conduit/method/diffusion/ConduitAPI_diffusion_getcommits_Method.php
index 3147edf992..be6b2c1904 100644
--- a/src/applications/conduit/method/diffusion/ConduitAPI_diffusion_getcommits_Method.php
+++ b/src/applications/conduit/method/diffusion/ConduitAPI_diffusion_getcommits_Method.php
@@ -1,275 +1,259 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_diffusion_getcommits_Method
extends ConduitAPIMethod {
public function getMethodDescription() {
return "Retrieve Diffusion commit information.";
}
public function defineParamTypes() {
return array(
'commits' => 'required list<string>',
);
}
public function defineReturnType() {
return 'nonempty list<dict<string, wild>>';
}
public function defineErrorTypes() {
return array(
);
}
protected function execute(ConduitAPIRequest $request) {
$results = array();
$commits = $request->getValue('commits');
$commits = array_fill_keys($commits, array());
foreach ($commits as $name => $info) {
$matches = null;
if (!preg_match('/^r([A-Z]+)([0-9a-f]+)$/', $name, $matches)) {
$results[$name] = array(
'error' => 'ERR-UNPARSEABLE',
);
unset($commits[$name]);
continue;
}
$commits[$name] = array(
'callsign' => $matches[1],
'commitIdentifier' => $matches[2],
);
}
if (!$commits) {
return $results;
}
$callsigns = ipull($commits, 'callsign');
$callsigns = array_unique($callsigns);
$repos = id(new PhabricatorRepository())->loadAllWhere(
'callsign IN (%Ls)',
$callsigns);
$repos = mpull($repos, null, 'getCallsign');
foreach ($commits as $name => $info) {
$repo = idx($repos, $info['callsign']);
if (!$repo) {
$results[$name] = $info + array(
'error' => 'ERR-UNKNOWN-REPOSITORY',
);
unset($commits[$name]);
continue;
}
$commits[$name] += array(
'repositoryPHID' => $repo->getPHID(),
'repositoryID' => $repo->getID(),
);
}
if (!$commits) {
return $results;
}
// Execute a complicated query to figure out the primary commit information
// for each referenced commit.
$cdata = $this->queryCommitInformation($commits, $repos);
// We've built the queries so that each row also has the identifier we used
// to select it, which might be a git prefix rather than a full identifier.
$ref_map = ipull($cdata, 'commitIdentifier', 'commitRef');
$cobjs = id(new PhabricatorRepositoryCommit())->loadAllFromArray($cdata);
$cobjs = mgroup($cobjs, 'getRepositoryID', 'getCommitIdentifier');
foreach ($commits as $name => $commit) {
// Expand short git names into full identifiers. For SVN this map is just
// the identity.
$full_identifier = idx($ref_map, $commit['commitIdentifier']);
$repo_id = $commit['repositoryID'];
unset($commits[$name]['repositoryID']);
if (empty($full_identifier) ||
empty($cobjs[$commit['repositoryID']][$full_identifier])) {
$results[$name] = $commit + array(
'error' => 'ERR-UNKNOWN-COMMIT',
);
unset($commits[$name]);
continue;
}
$cobj_arr = $cobjs[$commit['repositoryID']][$full_identifier];
$cobj = head($cobj_arr);
$commits[$name] += array(
'epoch' => $cobj->getEpoch(),
'commitPHID' => $cobj->getPHID(),
'commitID' => $cobj->getID(),
);
// Upgrade git short references into full commit identifiers.
$identifier = $cobj->getCommitIdentifier();
$commits[$name]['commitIdentifier'] = $identifier;
$callsign = $commits[$name]['callsign'];
$uri = "/r{$callsign}{$identifier}";
$commits[$name]['uri'] = PhabricatorEnv::getProductionURI($uri);
}
if (!$commits) {
return $results;
}
$commits = $this->addRepositoryCommitDataInformation($commits);
$commits = $this->addDifferentialInformation($commits);
foreach ($commits as $name => $commit) {
$results[$name] = $commit;
}
return $results;
}
/**
* Retrieve primary commit information for all referenced commits.
*/
private function queryCommitInformation(array $commits, array $repos) {
assert_instances_of($repos, 'PhabricatorRepository');
$conn_r = id(new PhabricatorRepositoryCommit())->establishConnection('r');
$repos = mpull($repos, null, 'getID');
$groups = array();
foreach ($commits as $name => $commit) {
$groups[$commit['repositoryID']][] = $commit['commitIdentifier'];
}
// NOTE: MySQL goes crazy and does a massive table scan if we build a more
// sensible version of this query. Make sure the query plan is OK if you
// attempt to reduce the craziness here. METANOTE: The addition of prefix
// selection for Git further complicates matters.
$query = array();
$commit_table = id(new PhabricatorRepositoryCommit())->getTableName();
foreach ($groups as $repository_id => $identifiers) {
$vcs = $repos[$repository_id]->getVersionControlSystem();
$is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
if ($is_git) {
foreach ($identifiers as $identifier) {
if (strlen($identifier) < 7) {
// Don't bother with silly stuff like 'rX2', which will select
// 1/16th of all commits. Note that with length 7 we'll still get
// collisions in repositories at the tens-of-thousands-of-commits
// scale.
continue;
}
$query[] = qsprintf(
$conn_r,
'SELECT %T.*, %s commitRef
FROM %T WHERE repositoryID = %d
AND commitIdentifier LIKE %>',
$commit_table,
$identifier,
$commit_table,
$repository_id,
$identifier);
}
} else {
$query[] = qsprintf(
$conn_r,
'SELECT %T.*, commitIdentifier commitRef
FROM %T WHERE repositoryID = %d
AND commitIdentifier IN (%Ls)',
$commit_table,
$commit_table,
$repository_id,
$identifiers);
}
}
return queryfx_all(
$conn_r,
'%Q',
implode(' UNION ALL ', $query));
}
/**
* Enhance the commit list with RepositoryCommitData information.
*/
private function addRepositoryCommitDataInformation(array $commits) {
$commit_ids = ipull($commits, 'commitID');
$data = id(new PhabricatorRepositoryCommitData())->loadAllWhere(
'commitID in (%Ld)',
$commit_ids);
$data = mpull($data, null, 'getCommitID');
foreach ($commits as $name => $commit) {
if (isset($data[$commit['commitID']])) {
$dobj = $data[$commit['commitID']];
$commits[$name] += array(
'commitMessage' => $dobj->getCommitMessage(),
'commitDetails' => $dobj->getCommitDetails(),
);
}
// Remove this information so we don't expose it via the API since
// external services shouldn't be storing internal Commit IDs.
unset($commits[$name]['commitID']);
}
return $commits;
}
/**
* Enhance the commit list with Differential information.
*/
private function addDifferentialInformation(array $commits) {
$commit_phids = ipull($commits, 'commitPHID');
$rev_conn_r = id(new DifferentialRevision())->establishConnection('r');
$revs = queryfx_all(
$rev_conn_r,
'SELECT r.id id, r.phid phid, c.commitPHID commitPHID FROM %T r JOIN %T c
ON r.id = c.revisionID
WHERE c.commitPHID in (%Ls)',
id(new DifferentialRevision())->getTableName(),
DifferentialRevision::TABLE_COMMIT,
$commit_phids);
$revs = ipull($revs, null, 'commitPHID');
foreach ($commits as $name => $commit) {
if (isset($revs[$commit['commitPHID']])) {
$rev = $revs[$commit['commitPHID']];
$commits[$name] += array(
'differentialRevisionID' => 'D'.$rev['id'],
'differentialRevisionPHID' => $rev['phid'],
);
}
}
return $commits;
}
}
diff --git a/src/applications/conduit/method/diffusion/ConduitAPI_diffusion_getrecentcommitsbypath_Method.php b/src/applications/conduit/method/diffusion/ConduitAPI_diffusion_getrecentcommitsbypath_Method.php
index a5d9014333..89ff076aef 100644
--- a/src/applications/conduit/method/diffusion/ConduitAPI_diffusion_getrecentcommitsbypath_Method.php
+++ b/src/applications/conduit/method/diffusion/ConduitAPI_diffusion_getrecentcommitsbypath_Method.php
@@ -1,73 +1,57 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_diffusion_getrecentcommitsbypath_Method
extends ConduitAPIMethod {
const DEFAULT_LIMIT = 10;
public function getMethodDescription() {
return "Get commit identifiers for recent commits affecting a given path.";
}
public function defineParamTypes() {
return array(
'callsign' => 'required string',
'path' => 'required string',
'limit' => 'optional int',
);
}
public function defineReturnType() {
return 'nonempty list<string>';
}
public function defineErrorTypes() {
return array(
);
}
protected function execute(ConduitAPIRequest $request) {
$drequest = DiffusionRequest::newFromDictionary(
array(
'callsign' => $request->getValue('callsign'),
'path' => $request->getValue('path'),
));
$limit = nonempty(
$request->getValue('limit'),
self::DEFAULT_LIMIT
);
$history = DiffusionHistoryQuery::newFromDiffusionRequest($drequest)
->setLimit($limit)
->needDirectChanges(true)
->needChildChanges(true)
->loadHistory();
$raw_commit_identifiers = mpull($history, 'getCommitIdentifier');
$result = array();
foreach ($raw_commit_identifiers as $id) {
$result[] = 'r'.$request->getValue('callsign').$id;
}
return $result;
}
}
diff --git a/src/applications/conduit/method/feed/ConduitAPI_feed_publish_Method.php b/src/applications/conduit/method/feed/ConduitAPI_feed_publish_Method.php
index 05752a3994..33d0b672c6 100644
--- a/src/applications/conduit/method/feed/ConduitAPI_feed_publish_Method.php
+++ b/src/applications/conduit/method/feed/ConduitAPI_feed_publish_Method.php
@@ -1,70 +1,54 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_feed_publish_Method
extends ConduitAPIMethod {
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
return "Publish a story to the feed.";
}
public function defineParamTypes() {
return array(
'type' => '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/ConduitAPI_feed_query_Method.php b/src/applications/conduit/method/feed/ConduitAPI_feed_query_Method.php
index a119935488..a65c9b3f62 100644
--- a/src/applications/conduit/method/feed/ConduitAPI_feed_query_Method.php
+++ b/src/applications/conduit/method/feed/ConduitAPI_feed_query_Method.php
@@ -1,140 +1,124 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_feed_query_Method extends ConduitAPIMethod {
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
return "Query the feed for stories";
}
private function getDefaultLimit() {
return 100;
}
public function defineParamTypes() {
return array(
'filterPHIDs' => 'optional list <phid>',
'limit' => 'optional int (default '.$this->getDefaultLimit().')',
'after' => 'optional int',
'view' => 'optional string (data, html, html-summary)',
);
}
private function getSupportedViewTypes() {
return array(
'html' => 'Full HTML presentation of story',
'data' => 'Dictionary with various data of the story',
'html-summary' => 'Story contains only the title of the story',
);
}
public function defineErrorTypes() {
$view_types = array_keys($this->getSupportedViewTypes());
$view_types = implode(', ', $view_types);
return array(
'ERR-UNKNOWN-TYPE' =>
'Unsupported view type, possibles are: ' . $view_types
);
}
public function defineReturnType() {
return 'nonempty dict';
}
protected function execute(ConduitAPIRequest $request) {
$results = array();
$user = $request->getUser();
$view_type = $request->getValue('view');
if (!$view_type) {
$view_type = 'data';
}
$limit = $request->getValue('limit');
if (!$limit) {
$limit = $this->getDefaultLimit();
}
$filter_phids = $request->getValue('filterPHIDs');
if (!$filter_phids) {
$filter_phids = array();
}
$after = $request->getValue('after');
$query = id(new PhabricatorFeedQuery())
->setLimit($limit)
->setFilterPHIDs($filter_phids)
->setViewer($user)
->setAfterID($after);
$stories = $query->execute();
if ($stories) {
$handle_phids = array_mergev(mpull($stories, 'getRequiredHandlePHIDs'));
$handles = id(new PhabricatorObjectHandleData($handle_phids))
->loadHandles();
foreach ($stories as $story) {
$story->setHandles($handles);
$story_data = $story->getStoryData();
$data = null;
$view = $story->renderView();
$view->setEpoch($story->getEpoch());
$view->setViewer($user);
switch ($view_type) {
case 'html':
$data = $view->render();
break;
case 'html-summary':
$view->setOneLineStory(true);
$data = $view->render();
break;
case 'data':
$data = array(
'class' => $story_data->getStoryType(),
'epoch' => $story_data->getEpoch(),
'authorPHID' => $story_data->getAuthorPHID(),
'chronologicalKey' => $story_data->getChronologicalKey(),
'data' => $story_data->getStoryData(),
);
break;
default:
throw new ConduitException('ERR-UNKNOWN-TYPE');
}
$results[$story_data->getPHID()] = $data;
}
}
return $results;
}
}
diff --git a/src/applications/conduit/method/file/ConduitAPI_file_download_Method.php b/src/applications/conduit/method/file/ConduitAPI_file_download_Method.php
index d9cb2c08b6..9a6b161b4a 100644
--- a/src/applications/conduit/method/file/ConduitAPI_file_download_Method.php
+++ b/src/applications/conduit/method/file/ConduitAPI_file_download_Method.php
@@ -1,58 +1,42 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_file_download_Method
extends ConduitAPIMethod {
public function getMethodDescription() {
return "Download a file from the server.";
}
public function defineParamTypes() {
return array(
'phid' => 'required phid',
);
}
public function defineReturnType() {
return 'nonempty base64-bytes';
}
public function defineErrorTypes() {
return array(
'ERR-BAD-PHID' => 'No such file exists.',
);
}
protected function execute(ConduitAPIRequest $request) {
$phid = $request->getValue('phid');
$file = id(new PhabricatorFile())->loadOneWhere(
'phid = %s',
$phid);
if (!$file) {
throw new ConduitException('ERR-BAD-PHID');
}
return base64_encode($file->loadFileData());
}
}
diff --git a/src/applications/conduit/method/file/ConduitAPI_file_info_Method.php b/src/applications/conduit/method/file/ConduitAPI_file_info_Method.php
index 1e2fbdfac6..541caf76ee 100644
--- a/src/applications/conduit/method/file/ConduitAPI_file_info_Method.php
+++ b/src/applications/conduit/method/file/ConduitAPI_file_info_Method.php
@@ -1,77 +1,61 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_file_info_Method extends ConduitAPIMethod {
public function getMethodDescription() {
return "Get information about a file.";
}
public function defineParamTypes() {
return array(
'phid' => 'optional phid',
'id' => 'optional id',
);
}
public function defineReturnType() {
return 'nonempty dict';
}
public function defineErrorTypes() {
return array(
'ERR-NOT-FOUND' => 'No such file exists.',
);
}
protected function execute(ConduitAPIRequest $request) {
$phid = $request->getValue('phid');
$id = $request->getValue('id');
if ($id) {
$file = id(new PhabricatorFile())->load($id);
} else {
$file = id(new PhabricatorFile())->loadOneWhere(
'phid = %s',
$phid);
}
if (!$file) {
throw new ConduitException('ERR-NOT-FOUND');
}
$uri = $file->getBestURI();
return array(
'id' => $file->getID(),
'phid' => $file->getPHID(),
'objectName' => 'F'.$file->getID(),
'name' => $file->getName(),
'mimeType' => $file->getMimeType(),
'byteSize' => $file->getByteSize(),
'authorPHID' => $file->getAuthorPHID(),
'dateCreated' => $file->getDateCreated(),
'dateModified' => $file->getDateModified(),
'uri' => PhabricatorEnv::getProductionURI($uri),
);
}
}
diff --git a/src/applications/conduit/method/file/ConduitAPI_file_upload_Method.php b/src/applications/conduit/method/file/ConduitAPI_file_upload_Method.php
index 8509b25dcb..a2a87ee94d 100644
--- a/src/applications/conduit/method/file/ConduitAPI_file_upload_Method.php
+++ b/src/applications/conduit/method/file/ConduitAPI_file_upload_Method.php
@@ -1,59 +1,43 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_file_upload_Method extends ConduitAPIMethod {
public function getMethodDescription() {
return "Upload a file to the server.";
}
public function defineParamTypes() {
return array(
'data_base64' => 'required nonempty base64-bytes',
'name' => 'optional string',
);
}
public function defineReturnType() {
return 'nonempty guid';
}
public function defineErrorTypes() {
return array(
);
}
protected function execute(ConduitAPIRequest $request) {
$data = $request->getValue('data_base64');
$name = $request->getValue('name');
$data = base64_decode($data, $strict = true);
$user = $request->getUser();
$file = PhabricatorFile::newFromFileData(
$data,
array(
'name' => $name,
'authorPHID' => $user->getPHID(),
));
return $file->getPHID();
}
}
diff --git a/src/applications/conduit/method/flag/ConduitAPI_flag_Method.php b/src/applications/conduit/method/flag/ConduitAPI_flag_Method.php
index fffefc289c..83f7ffc2fb 100644
--- a/src/applications/conduit/method/flag/ConduitAPI_flag_Method.php
+++ b/src/applications/conduit/method/flag/ConduitAPI_flag_Method.php
@@ -1,53 +1,37 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
abstract class ConduitAPI_flag_Method extends ConduitAPIMethod {
protected function attachHandleToFlag($flag) {
$flag->attachHandle(
PhabricatorObjectHandleData::loadOneHandle($flag->getObjectPHID())
);
}
protected function buildFlagInfoDictionary($flag) {
$color = $flag->getColor();
$uri = PhabricatorEnv::getProductionURI($flag->getHandle()->getURI());
return array(
'id' => $flag->getID(),
'ownerPHID' => $flag->getOwnerPHID(),
'type' => $flag->getType(),
'objectPHID' => $flag->getObjectPHID(),
'reasonPHID' => $flag->getReasonPHID(),
'color' => $color,
'colorName' => PhabricatorFlagColor::getColorName($color),
'note' => $flag->getNote(),
'handle' => array(
'uri' => $uri,
'name' => $flag->getHandle()->getName(),
'fullname' => $flag->getHandle()->getFullName(),
),
'dateCreated' => $flag->getDateCreated(),
'dateModified' => $flag->getDateModified(),
);
}
}
diff --git a/src/applications/conduit/method/flag/ConduitAPI_flag_delete_Method.php b/src/applications/conduit/method/flag/ConduitAPI_flag_delete_Method.php
index 68ba0a9d40..b434c57b79 100644
--- a/src/applications/conduit/method/flag/ConduitAPI_flag_delete_Method.php
+++ b/src/applications/conduit/method/flag/ConduitAPI_flag_delete_Method.php
@@ -1,75 +1,59 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_flag_delete_Method extends ConduitAPI_flag_Method {
public function getMethodDescription() {
return "Clear a flag.";
}
public function defineParamTypes() {
return array(
'id' => 'optional id',
'objectPHID' => 'optional phid',
);
}
public function defineReturnType() {
return 'dict | null';
}
public function defineErrorTypes() {
return array(
'ERR_NOT_FOUND' => 'Bad flag ID.',
'ERR_WRONG_USER' => 'You are not the creator of this flag.',
'ERR_NEED_PARAM' => 'Must pass an id or an objectPHID.',
);
}
protected function execute(ConduitAPIRequest $request) {
$id = $request->getValue('id');
$object = $request->getValue('objectPHID');
if ($id) {
$flag = id(new PhabricatorFlag())->load($id);
if (!$flag) {
throw new ConduitException('ERR_NOT_FOUND');
}
if ($flag->getOwnerPHID() != $request->getUser()->getPHID()) {
throw new ConduitException('ERR_WRONG_USER');
}
} elseif ($object) {
$flag = id(new PhabricatorFlag())->loadOneWhere(
'objectPHID = %s AND ownerPHID = %s',
$object,
$request->getUser()->getPHID());
if (!$flag) {
return null;
}
} else {
throw new ConduitException('ERR_NEED_PARAM');
}
$this->attachHandleToFlag($flag);
$ret = $this->buildFlagInfoDictionary($flag);
$flag->delete();
return $ret;
}
}
diff --git a/src/applications/conduit/method/flag/ConduitAPI_flag_edit_Method.php b/src/applications/conduit/method/flag/ConduitAPI_flag_edit_Method.php
index 5dff3ed098..98e665d057 100644
--- a/src/applications/conduit/method/flag/ConduitAPI_flag_edit_Method.php
+++ b/src/applications/conduit/method/flag/ConduitAPI_flag_edit_Method.php
@@ -1,80 +1,64 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_flag_edit_Method extends ConduitAPI_flag_Method {
public function getMethodDescription() {
return "Create or modify a flag.";
}
public function defineParamTypes() {
return array(
'objectPHID' => 'required phid',
'color' => 'optional int',
'note' => 'optional string',
);
}
public function defineReturnType() {
return 'dict';
}
public function defineErrorTypes() {
return array(
);
}
protected function execute(ConduitAPIRequest $request) {
$user = $request->getUser()->getPHID();
$phid = $request->getValue('objectPHID');
$new = false;
$flag = id(new PhabricatorFlag())->loadOneWhere(
'objectPHID = %s AND ownerPHID = %s',
$phid,
$user);
if ($flag) {
$params = $request->getAllParameters();
if (isset($params['color'])) {
$flag->setColor($params['color']);
}
if (isset($params['note'])) {
$flag->setNote($params['note']);
}
} else {
$default_color = PhabricatorFlagColor::COLOR_BLUE;
$flag = id(new PhabricatorFlag())
->setOwnerPHID($user)
->setType(phid_get_type($phid))
->setObjectPHID($phid)
->setReasonPHID($user)
->setColor($request->getValue('color', $default_color))
->setNote($request->getValue('note', ''));
$new = true;
}
$this->attachHandleToFlag($flag);
$flag->save();
$ret = $this->buildFlagInfoDictionary($flag);
$ret['new'] = $new;
return $ret;
}
}
diff --git a/src/applications/conduit/method/flag/ConduitAPI_flag_query_Method.php b/src/applications/conduit/method/flag/ConduitAPI_flag_query_Method.php
index 04d4f4e5b4..56655c4b61 100644
--- a/src/applications/conduit/method/flag/ConduitAPI_flag_query_Method.php
+++ b/src/applications/conduit/method/flag/ConduitAPI_flag_query_Method.php
@@ -1,83 +1,67 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_flag_query_Method extends ConduitAPI_flag_Method {
public function getMethodDescription() {
return "Query flag markers.";
}
public function defineParamTypes() {
return array(
'ownerPHIDs' => 'optional list<phid>',
'types' => 'optional list<type>',
'objectPHIDs' => 'optional list<phid>',
'offset' => 'optional int',
'limit' => 'optional int (default = 100)',
);
}
public function defineReturnType() {
return 'list<dict>';
}
public function defineErrorTypes() {
return array(
);
}
protected function execute(ConduitAPIRequest $request) {
$query = new PhabricatorFlagQuery();
$query->setViewer($request->getUser());
$owner_phids = $request->getValue('ownerPHIDs', array());
if ($owner_phids) {
$query->withOwnerPHIDs($owner_phids);
}
$object_phids = $request->getValue('objectPHIDs', array());
if ($object_phids) {
$query->withObjectPHIDs($object_phids);
}
$types = $request->getValue('types', array());
if ($types) {
$query->withTypes($types);
}
$query->needHandles(true);
$query->setOffset($request->getValue('offset', 0));
$query->setLimit($request->getValue('limit', 100));
$flags = $query->execute();
$results = array();
foreach ($flags as $flag) {
$results[] = $this->buildFlagInfoDictionary($flag);
}
return $results;
}
}
diff --git a/src/applications/conduit/method/macro/ConduitAPI_macro_Method.php b/src/applications/conduit/method/macro/ConduitAPI_macro_Method.php
index 0c98cbf6f7..c79d05c755 100644
--- a/src/applications/conduit/method/macro/ConduitAPI_macro_Method.php
+++ b/src/applications/conduit/method/macro/ConduitAPI_macro_Method.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
abstract class ConduitAPI_macro_Method extends ConduitAPIMethod {
}
diff --git a/src/applications/conduit/method/macro/ConduitAPI_macro_query_Method.php b/src/applications/conduit/method/macro/ConduitAPI_macro_query_Method.php
index 09117bcfc1..5a35420dc6 100644
--- a/src/applications/conduit/method/macro/ConduitAPI_macro_query_Method.php
+++ b/src/applications/conduit/method/macro/ConduitAPI_macro_query_Method.php
@@ -1,67 +1,51 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_macro_query_Method extends ConduitAPI_macro_Method {
public function getMethodDescription() {
return "Retrieve image macro information.";
}
public function defineParamTypes() {
return array(
);
}
public function defineReturnType() {
return 'list<dict>';
}
public function defineErrorTypes() {
return array(
);
}
protected function execute(ConduitAPIRequest $request) {
$macros = id(new PhabricatorFileImageMacro())->loadAll();
$files = array();
if ($macros) {
$files = id(new PhabricatorFile())->loadAllWhere(
'phid IN (%Ls)',
mpull($macros, 'getFilePHID'));
$files = mpull($files, null, 'getPHID');
}
$results = array();
foreach ($macros as $macro) {
if (empty($files[$macro->getFilePHID()])) {
continue;
}
$results[$macro->getName()] = array(
'uri' => $files[$macro->getFilePHID()]->getBestURI(),
);
}
return $results;
}
}
diff --git a/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_Method.php b/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_Method.php
index be9e18c127..f307c13106 100644
--- a/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_Method.php
+++ b/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_Method.php
@@ -1,275 +1,259 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
abstract class ConduitAPI_maniphest_Method extends ConduitAPIMethod {
public function defineErrorTypes() {
return array(
'ERR-INVALID-PARAMETER' => 'Missing or malformed parameter.'
);
}
protected function buildTaskInfoDictionary(ManiphestTask $task) {
$results = $this->buildTaskInfoDictionaries(array($task));
return idx($results, $task->getPHID());
}
protected function getTaskFields($is_new) {
$fields = array();
if (!$is_new) {
$fields += array(
'id' => 'optional int',
'phid' => 'optional int',
);
}
$fields += array(
'title' => $is_new ? 'required string' : 'optional string',
'description' => 'optional string',
'ownerPHID' => 'optional phid',
'ccPHIDs' => 'optional list<phid>',
'priority' => 'optional int',
'projectPHIDs' => 'optional list<phid>',
'filePHIDs' => 'optional list<phid>',
'auxiliary' => 'optional dict',
);
if (!$is_new) {
$fields += array(
'status' => 'optional int',
'comments' => 'optional string',
);
}
return $fields;
}
protected function applyRequest(
ManiphestTask $task,
ConduitAPIRequest $request,
$is_new) {
$changes = array();
if ($is_new) {
$task->setTitle((string)$request->getValue('title'));
$task->setDescription((string)$request->getValue('description'));
$changes[ManiphestTransactionType::TYPE_STATUS] =
ManiphestTaskStatus::STATUS_OPEN;
} else {
$comments = $request->getValue('comments');
if (!$is_new && $comments !== null) {
$changes[ManiphestTransactionType::TYPE_NONE] = null;
}
$title = $request->getValue('title');
if ($title !== null) {
$changes[ManiphestTransactionType::TYPE_TITLE] = $title;
}
$desc = $request->getValue('description');
if ($desc !== null) {
$changes[ManiphestTransactionType::TYPE_DESCRIPTION] = $desc;
}
$status = $request->getValue('status');
if ($status !== null) {
$valid_statuses = ManiphestTaskStatus::getTaskStatusMap();
if (!isset($valid_statuses[$status])) {
throw id(new ConduitException('ERR-INVALID-PARAMETER'))
->setErrorDescription('Status set to invalid value.');
}
$changes[ManiphestTransactionType::TYPE_STATUS] = $status;
}
}
$priority = $request->getValue('priority');
if ($priority !== null) {
$valid_priorities = ManiphestTaskPriority::getTaskPriorityMap();
if (!isset($valid_priorities[$priority])) {
throw id(new ConduitException('ERR-INVALID-PARAMETER'))
->setErrorDescription('Priority set to invalid value.');
}
$changes[ManiphestTransactionType::TYPE_PRIORITY] = $priority;
}
$owner_phid = $request->getValue('ownerPHID');
if ($owner_phid !== null) {
$this->validatePHIDList(array($owner_phid),
PhabricatorPHIDConstants::PHID_TYPE_USER,
'ownerPHID');
$changes[ManiphestTransactionType::TYPE_OWNER] = $owner_phid;
}
$ccs = $request->getValue('ccPHIDs');
if ($ccs !== null) {
$this->validatePHIDList($ccs,
PhabricatorPHIDConstants::PHID_TYPE_USER,
'ccPHIDS');
$changes[ManiphestTransactionType::TYPE_CCS] = $ccs;
}
$project_phids = $request->getValue('projectPHIDs');
if ($project_phids !== null) {
$this->validatePHIDList($project_phids,
PhabricatorPHIDConstants::PHID_TYPE_PROJ,
'projectPHIDS');
$changes[ManiphestTransactionType::TYPE_PROJECTS] = $project_phids;
}
$file_phids = $request->getValue('filePHIDs');
if ($file_phids !== null) {
$this->validatePHIDList($file_phids,
PhabricatorPHIDConstants::PHID_TYPE_FILE,
'filePHIDS');
$file_map = array_fill_keys($file_phids, true);
$attached = $task->getAttached();
$attached[PhabricatorPHIDConstants::PHID_TYPE_FILE] = $file_map;
$changes[ManiphestTransactionType::TYPE_ATTACH] = $attached;
}
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_CONDUIT,
array());
$template = new ManiphestTransaction();
$template->setContentSource($content_source);
$template->setAuthorPHID($request->getUser()->getPHID());
$transactions = array();
foreach ($changes as $type => $value) {
$transaction = clone $template;
$transaction->setTransactionType($type);
$transaction->setNewValue($value);
if ($type == ManiphestTransactionType::TYPE_NONE) {
$transaction->setComments($comments);
}
$transactions[] = $transaction;
}
$auxiliary = $request->getValue('auxiliary');
if ($auxiliary) {
$task->loadAndAttachAuxiliaryAttributes();
foreach ($auxiliary as $aux_key => $aux_value) {
$transaction = clone $template;
$transaction->setTransactionType(
ManiphestTransactionType::TYPE_AUXILIARY);
$transaction->setMetadataValue('aux:key', $aux_key);
$transaction->setNewValue($aux_value);
$transactions[] = $transaction;
}
}
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK,
array(
'task' => $task,
'new' => $is_new,
'transactions' => $transactions,
));
$event->setUser($request->getUser());
$event->setConduitRequest($request);
PhutilEventEngine::dispatchEvent($event);
$task = $event->getValue('task');
$transactions = $event->getValue('transactions');
$editor = new ManiphestTransactionEditor();
$editor->setActor($request->getUser());
$editor->applyTransactions($task, $transactions);
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK,
array(
'task' => $task,
'new' => $is_new,
'transactions' => $transactions,
));
$event->setUser($request->getUser());
$event->setConduitRequest($request);
PhutilEventEngine::dispatchEvent($event);
}
protected function buildTaskInfoDictionaries(array $tasks) {
assert_instances_of($tasks, 'ManiphestTask');
if (!$tasks) {
return array();
}
$all_aux = id(new ManiphestTaskAuxiliaryStorage())->loadAllWhere(
'taskPHID in (%Ls)',
mpull($tasks, 'getPHID'));
$all_aux = mgroup($all_aux, 'getTaskPHID');
$result = array();
foreach ($tasks as $task) {
$auxiliary = idx($all_aux, $task->getPHID(), array());
$auxiliary = mpull($auxiliary, 'getValue', 'getName');
$result[$task->getPHID()] = array(
'id' => $task->getID(),
'phid' => $task->getPHID(),
'authorPHID' => $task->getAuthorPHID(),
'ownerPHID' => $task->getOwnerPHID(),
'ccPHIDs' => $task->getCCPHIDs(),
'status' => $task->getStatus(),
'priority' => ManiphestTaskPriority::getTaskPriorityName(
$task->getPriority()),
'title' => $task->getTitle(),
'description' => $task->getDescription(),
'projectPHIDs' => $task->getProjectPHIDs(),
'uri' => PhabricatorEnv::getProductionURI('/T'.$task->getID()),
'auxiliary' => $auxiliary,
'objectName' => 'T'.$task->getID(),
'dateCreated' => $task->getDateCreated(),
'dateModified' => $task->getDateModified(),
);
}
return $result;
}
/**
* Note this is a temporary stop gap since its easy to make malformed Tasks.
* Long-term, the values set in @{method:defineParamTypes} will be used to
* validate data implicitly within the larger Conduit application.
*
* TODO -- remove this in favor of generalized Conduit hotness
*/
private function validatePHIDList(array $phid_list, $phid_type, $field) {
$phid_groups = phid_group_by_type($phid_list);
unset($phid_groups[$phid_type]);
if (!empty($phid_groups)) {
throw id(new ConduitException('ERR-INVALID-PARAMETER'))
->setErrorDescription(
'One or more PHIDs were invalid for '.$field.'.'
);
}
return true;
}
}
diff --git a/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_createtask_Method.php b/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_createtask_Method.php
index 5ef413fde2..f9c6ec45f6 100644
--- a/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_createtask_Method.php
+++ b/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_createtask_Method.php
@@ -1,53 +1,37 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_maniphest_createtask_Method
extends ConduitAPI_maniphest_Method {
public function getMethodDescription() {
return "Create a new Maniphest task.";
}
public function defineParamTypes() {
return $this->getTaskFields($is_new = true);
}
public function defineReturnType() {
return 'nonempty dict';
}
public function defineErrorTypes() {
return array(
'ERR-INVALID-PARAMETER' => 'Missing or malformed parameter.'
);
}
protected function execute(ConduitAPIRequest $request) {
$task = new ManiphestTask();
$task->setPriority(ManiphestTaskPriority::getDefaultPriority());
$task->setAuthorPHID($request->getUser()->getPHID());
$this->applyRequest($task, $request, $is_new = true);
return $this->buildTaskInfoDictionary($task);
}
}
diff --git a/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_find_Method.php b/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_find_Method.php
index 320d5e99b3..76e2588aea 100644
--- a/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_find_Method.php
+++ b/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_find_Method.php
@@ -1,39 +1,23 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*
* @concrete-extensible
*/
final class ConduitAPI_maniphest_find_Method
extends ConduitAPI_maniphest_query_Method {
public function getMethodStatus() {
return self::METHOD_STATUS_DEPRECATED;
}
public function getMethodStatusDescription() {
return "Renamed to 'maniphest.query'.";
}
public function getMethodDescription() {
return "Deprecated alias of maniphest.query";
}
}
diff --git a/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_gettasktransactions_Method.php b/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_gettasktransactions_Method.php
index 8edb65aa57..9f11ac097a 100644
--- a/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_gettasktransactions_Method.php
+++ b/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_gettasktransactions_Method.php
@@ -1,74 +1,58 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ConduitAPI_maniphest_gettasktransactions_Method
extends ConduitAPI_maniphest_Method {
public function getMethodDescription() {
return "Retrieve Maniphest Task Transactions.";
}
public function defineParamTypes() {
return array(
'ids' => 'required list<int>',
);
}
public function defineReturnType() {
return 'nonempty list<dict<string, wild>>';
}
public function defineErrorTypes() {
return array(
);
}
protected function execute(ConduitAPIRequest $request) {
$results = array();
$task_ids = $request->getValue('ids');
if (!$task_ids) {
return $results;
}
$transactions = id(new ManiphestTransaction())->loadAllWhere(
'taskID IN (%Ld) ORDER BY id ASC',
$task_ids);
foreach ($transactions as $transaction) {
$task_id = $transaction->getTaskID();
if (!array_key_exists($task_id, $results)) {
$results[$task_id] = array();
}
$results[$task_id][] = array(
'taskID' => $task_id,
'transactionType' => $transaction->getTransactionType(),
'oldValue' => $transaction->getOldValue(),
'newValue' => $transaction->getNewValue(),
'comments' => $transaction->getComments(),
'authorPHID' => $transaction->getAuthorPHID(),
'dateCreated' => $transaction->getDateCreated(),
);
}
return $results;
}
}
diff --git a/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_info_Method.php b/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_info_Method.php
index 00a1fc3542..59c0ab8e2f 100644
--- a/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_info_Method.php
+++ b/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_info_Method.php
@@ -1,57 +1,41 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_maniphest_info_Method
extends ConduitAPI_maniphest_Method {
public function getMethodDescription() {
return "Retrieve information about a Maniphest task, given its id.";
}
public function defineParamTypes() {
return array(
'task_id' => 'required id',
);
}
public function defineReturnType() {
return 'nonempty dict';
}
public function defineErrorTypes() {
return array(
'ERR_BAD_TASK' => 'No such maniphest task exists',
);
}
protected function execute(ConduitAPIRequest $request) {
$task_id = $request->getValue('task_id');
$task = id(new ManiphestTask())->load($task_id);
if (!$task) {
throw new ConduitException('ERR_BAD_TASK');
}
return $this->buildTaskInfoDictionary($task);
}
}
diff --git a/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_query_Method.php b/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_query_Method.php
index 8d7c535e88..346944f2e3 100644
--- a/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_query_Method.php
+++ b/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_query_Method.php
@@ -1,143 +1,127 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*
* TODO: Remove maniphest.find, then make this final.
*
* @concrete-extensible
*/
class ConduitAPI_maniphest_query_Method
extends ConduitAPI_maniphest_Method {
public function getMethodDescription() {
return "Execute complex searches for Maniphest tasks.";
}
public function defineParamTypes() {
$statuses = array(
ManiphestTaskQuery::STATUS_ANY,
ManiphestTaskQuery::STATUS_OPEN,
ManiphestTaskQuery::STATUS_CLOSED,
ManiphestTaskQuery::STATUS_RESOLVED,
ManiphestTaskQuery::STATUS_WONTFIX,
ManiphestTaskQuery::STATUS_INVALID,
ManiphestTaskQuery::STATUS_SPITE,
ManiphestTaskQuery::STATUS_DUPLICATE,
);
$statuses = implode(', ', $statuses);
$orders = array(
ManiphestTaskQuery::ORDER_PRIORITY,
ManiphestTaskQuery::ORDER_CREATED,
ManiphestTaskQuery::ORDER_MODIFIED,
);
$orders = implode(', ', $orders);
return array(
'ids' => 'optional list<uint>',
'phids' => 'optional list<phid>',
'ownerPHIDs' => 'optional list<phid>',
'authorPHIDs' => 'optional list<phid>',
'projectPHIDs' => 'optional list<phid>',
'ccPHIDs' => 'optional list<phid>',
'fullText' => 'optional string',
'status' => 'optional enum<'.$statuses.'>',
'order' => 'optional enum<'.$orders.'>',
'limit' => 'optional int',
'offset' => 'optional int',
);
}
public function defineReturnType() {
return 'list';
}
public function defineErrorTypes() {
return array(
);
}
protected function execute(ConduitAPIRequest $request) {
$query = new ManiphestTaskQuery();
$task_ids = $request->getValue('ids');
if ($task_ids) {
$query->withTaskIDs($task_ids);
}
$task_phids = $request->getValue('phids');
if ($task_phids) {
$query->withTaskPHIDs($task_phids);
}
$owners = $request->getValue('ownerPHIDs');
if ($owners) {
$query->withOwners($owners);
}
$authors = $request->getValue('authorPHIDs');
if ($authors) {
$query->withAuthors($authors);
}
$projects = $request->getValue('projectPHIDs');
if ($projects) {
$query->withAllProjects($projects);
}
$ccs = $request->getValue('ccPHIDs');
if ($ccs) {
$query->withSubscribers($ccs);
}
$full_text = $request->getValue('fullText');
if ($full_text) {
$query->withFullTextSearch($full_text);
}
$status = $request->getValue('status');
if ($status) {
$query->withStatus($status);
}
$order = $request->getValue('order');
if ($order) {
$query->setOrderBy($order);
}
$limit = $request->getValue('limit');
if ($limit) {
$query->setLimit($limit);
}
$offset = $request->getValue('offset');
if ($offset) {
$query->setOffset($offset);
}
$results = $query->execute();
return $this->buildTaskInfoDictionaries($results);
}
}
diff --git a/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_update_Method.php b/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_update_Method.php
index 4d4adf1039..4d324b4320 100644
--- a/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_update_Method.php
+++ b/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_update_Method.php
@@ -1,69 +1,53 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_maniphest_update_Method
extends ConduitAPI_maniphest_Method {
public function getMethodDescription() {
return "Update an existing Maniphest task.";
}
public function defineErrorTypes() {
return array(
'ERR-BAD-TASK' => 'No such maniphest task exists.',
'ERR-INVALID-PARAMETER' => 'Missing or malformed parameter.'
);
}
public function defineParamTypes() {
return $this->getTaskFields($is_new = false);
}
public function defineReturnType() {
return 'nonempty dict';
}
protected function execute(ConduitAPIRequest $request) {
$id = $request->getValue('id');
$phid = $request->getValue('phid');
if (($id && $phid) || (!$id && !$phid)) {
throw new Exception("Specify exactly one of 'id' and 'phid'.");
}
if ($id) {
$task = id(new ManiphestTask())->load($id);
} else {
$task = id(new ManiphestTask())->loadOneWhere(
'phid = %s',
$phid);
}
if (!$task) {
throw new ConduitException('ERR-BAD-TASK');
}
$this->applyRequest($task, $request, $is_new = false);
return $this->buildTaskInfoDictionary($task);
}
}
diff --git a/src/applications/conduit/method/owners/ConduitAPI_owners_query_Method.php b/src/applications/conduit/method/owners/ConduitAPI_owners_query_Method.php
index 1174c77710..8e01e9abf9 100644
--- a/src/applications/conduit/method/owners/ConduitAPI_owners_query_Method.php
+++ b/src/applications/conduit/method/owners/ConduitAPI_owners_query_Method.php
@@ -1,165 +1,149 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_owners_query_Method
extends ConduitAPIMethod {
public function getMethodDescription() {
return 'Query for packages by one of the following: repository/path, ' .
'packages with a given user or project owner, or packages affiliated ' .
'with a user (owned by either the user or a project they are a member ' .
'of.) You should only provide at most one search query.';
}
public function defineParamTypes() {
return array(
'userOwner' => 'optional string',
'projectOwner' => 'optional string',
'userAffiliated' => 'optional string',
'repositoryCallsign' => 'optional string',
'path' => 'optional string',
);
}
public function defineReturnType() {
return 'dict<phid -> dict of package info>';
}
public function defineErrorTypes() {
return array(
'ERR-INVALID-USAGE' =>
'Provide one of a single owner phid (user/project), a single ' .
'affiliated user phid (user), or a repository/path.',
'ERR-INVALID-PARAMETER' => 'parameter should be a phid',
'ERR_REP_NOT_FOUND' => 'The repository callsign is not recognized',
);
}
protected static function queryAll() {
return id(new PhabricatorOwnersPackage())->loadAll();
}
protected static function queryByOwner($owner) {
$is_valid_phid =
phid_get_type($owner) == PhabricatorPHIDConstants::PHID_TYPE_USER ||
phid_get_type($owner) == PhabricatorPHIDConstants::PHID_TYPE_PROJ;
if (!$is_valid_phid) {
throw id(new ConduitException('ERR-INVALID-PARAMETER'))
->setErrorDescription(
'Expected user/project PHID for owner, got '.$owner);
}
$owners = id(new PhabricatorOwnersOwner())->loadAllWhere(
'userPHID = %s',
$owner);
$package_ids = mpull($owners, 'getPackageID');
$packages = array();
foreach ($package_ids as $id) {
$packages[] = id(new PhabricatorOwnersPackage())->load($id);
}
return $packages;
}
private static function queryByPath($repo_callsign, $path) {
$repository = id(new PhabricatorRepository())->loadOneWhere('callsign = %s',
$repo_callsign);
if (empty($repository)) {
throw id(new ConduitException('ERR_REP_NOT_FOUND'))
->setErrorDescription(
'Repository callsign '.$repo_callsign.' not recognized');
}
return PhabricatorOwnersPackage::loadOwningPackages(
$repository, $path);
}
public static function buildPackageInformationDictionaries($packages) {
assert_instances_of($packages, 'PhabricatorOwnersPackage');
$result = array();
foreach ($packages as $package) {
$p_owners = $package->loadOwners();
$p_paths = $package->loadPaths();
$owners = array_values(mpull($p_owners, 'getUserPHID'));
$paths = array();
foreach ($p_paths as $p) {
$paths[] = array($p->getRepositoryPHID(), $p->getPath());
}
$result[$package->getPHID()] = array(
'phid' => $package->getPHID(),
'name' => $package->getName(),
'description' => $package->getDescription(),
'primaryOwner' => $package->getPrimaryOwnerPHID(),
'owners' => $owners,
'paths' => $paths
);
}
return $result;
}
protected function execute(ConduitAPIRequest $request) {
$is_owner_query =
($request->getValue('userOwner') ||
$request->getValue('projectOwner')) ?
1 : 0;
$is_affiliated_query = $request->getValue('userAffiliated') ?
1 : 0;
$repo = $request->getValue('repositoryCallsign');
$path = $request->getValue('path');
$is_path_query = ($repo && $path) ? 1 : 0;
if ($is_owner_query + $is_path_query + $is_affiliated_query === 0) {
// if no search terms are provided, return everything
$packages = self::queryAll();
} else if ($is_owner_query + $is_path_query + $is_affiliated_query > 1) {
// otherwise, exactly one of these should be provided
throw new ConduitException('ERR-INVALID-USAGE');
}
if ($is_affiliated_query) {
$query = id(new PhabricatorOwnersPackageQuery())
->setViewer($request->getUser());
$query->withOwnerPHIDs(array($request->getValue('userAffiliated')));
$packages = $query->execute();
} else if ($is_owner_query) {
$owner = nonempty(
$request->getValue('userOwner'),
$request->getValue('projectOwner'));
$packages = self::queryByOwner($owner);
} else if ($is_path_query) {
$packages = self::queryByPath($repo, $path);
}
return self::buildPackageInformationDictionaries($packages);
}
}
diff --git a/src/applications/conduit/method/phid/ConduitAPI_phid_Method.php b/src/applications/conduit/method/phid/ConduitAPI_phid_Method.php
index a9a0ab925d..fc32bebf16 100644
--- a/src/applications/conduit/method/phid/ConduitAPI_phid_Method.php
+++ b/src/applications/conduit/method/phid/ConduitAPI_phid_Method.php
@@ -1,41 +1,25 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
abstract class ConduitAPI_phid_Method extends ConduitAPIMethod {
protected function buildHandleInformationDictionary(
PhabricatorObjectHandle $handle) {
return array(
'phid' => $handle->getPHID(),
'uri' => PhabricatorEnv::getProductionURI($handle->getURI()),
'typeName' => $handle->getTypeName(),
'type' => $handle->getType(),
'name' => $handle->getName(),
'fullName' => $handle->getFullName(),
'status' => $handle->getStatus(),
);
}
}
diff --git a/src/applications/conduit/method/phid/ConduitAPI_phid_info_Method.php b/src/applications/conduit/method/phid/ConduitAPI_phid_info_Method.php
index e908df409d..0cc657ef5e 100644
--- a/src/applications/conduit/method/phid/ConduitAPI_phid_info_Method.php
+++ b/src/applications/conduit/method/phid/ConduitAPI_phid_info_Method.php
@@ -1,68 +1,52 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_phid_info_Method
extends ConduitAPI_phid_Method {
public function getMethodStatus() {
return self::METHOD_STATUS_DEPRECATED;
}
public function getMethodStatusDescription() {
return "Replaced by 'phid.query'.";
}
public function getMethodDescription() {
return "Retrieve information about an arbitrary PHID.";
}
public function defineParamTypes() {
return array(
'phid' => 'required phid',
);
}
public function defineReturnType() {
return 'nonempty dict<string, wild>';
}
public function defineErrorTypes() {
return array(
'ERR-BAD-PHID' => 'No such object exists.',
);
}
protected function execute(ConduitAPIRequest $request) {
$phid = $request->getValue('phid');
$handles = id(new PhabricatorObjectHandleData(array($phid)))
->loadHandles();
$handle = $handles[$phid];
if (!$handle->isComplete()) {
throw new ConduitException('ERR-BAD-PHID');
}
return $this->buildHandleInformationDictionary($handle);
}
}
diff --git a/src/applications/conduit/method/phid/ConduitAPI_phid_lookup_Method.php b/src/applications/conduit/method/phid/ConduitAPI_phid_lookup_Method.php
index 9dd02ee47d..7f4d2d85e4 100644
--- a/src/applications/conduit/method/phid/ConduitAPI_phid_lookup_Method.php
+++ b/src/applications/conduit/method/phid/ConduitAPI_phid_lookup_Method.php
@@ -1,66 +1,50 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_phid_lookup_Method
extends ConduitAPI_phid_Method {
public function getMethodDescription() {
return "Look up objects by name.";
}
public function defineParamTypes() {
return array(
'names' => 'required list<string>',
);
}
public function defineReturnType() {
return 'nonempty dict<string, wild>';
}
public function defineErrorTypes() {
return array();
}
protected function execute(ConduitAPIRequest $request) {
$names = $request->getValue('names');
$phids = array();
foreach ($names as $name) {
$phid = PhabricatorPHID::fromObjectName($name);
if ($phid) {
$phids[$name] = $phid;
}
}
$handles = id(new PhabricatorObjectHandleData($phids))
->loadHandles();
$result = array();
foreach ($phids as $name => $phid) {
if (isset($handles[$phid]) && $handles[$phid]->isComplete()) {
$result[$name] = $this->buildHandleInformationDictionary(
$handles[$phid]);
}
}
return $result;
}
}
diff --git a/src/applications/conduit/method/phid/ConduitAPI_phid_query_Method.php b/src/applications/conduit/method/phid/ConduitAPI_phid_query_Method.php
index 76dbcc9348..f19feddbf1 100644
--- a/src/applications/conduit/method/phid/ConduitAPI_phid_query_Method.php
+++ b/src/applications/conduit/method/phid/ConduitAPI_phid_query_Method.php
@@ -1,60 +1,44 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_phid_query_Method
extends ConduitAPI_phid_Method {
public function getMethodDescription() {
return "Retrieve information about arbitrary PHIDs.";
}
public function defineParamTypes() {
return array(
'phids' => 'required list<phid>',
);
}
public function defineReturnType() {
return 'nonempty dict<string, wild>';
}
public function defineErrorTypes() {
return array();
}
protected function execute(ConduitAPIRequest $request) {
$phids = $request->getValue('phids');
$handles = id(new PhabricatorObjectHandleData($phids))
->loadHandles();
$result = array();
foreach ($handles as $phid => $handle) {
if ($handle->isComplete()) {
$result[$phid] = $this->buildHandleInformationDictionary($handle);
}
}
return $result;
}
}
diff --git a/src/applications/conduit/method/phpast/ConduitAPI_phpast_getast_Method.php b/src/applications/conduit/method/phpast/ConduitAPI_phpast_getast_Method.php
index abaa6385e5..698c574313 100644
--- a/src/applications/conduit/method/phpast/ConduitAPI_phpast_getast_Method.php
+++ b/src/applications/conduit/method/phpast/ConduitAPI_phpast_getast_Method.php
@@ -1,52 +1,36 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_phpast_getast_Method extends ConduitAPIMethod {
public function getMethodDescription() {
return "Parse a piece of PHP code.";
}
public function defineParamTypes() {
return array(
'code' => 'required string',
);
}
public function defineReturnType() {
return 'nonempty dict';
}
public function defineErrorTypes() {
return array(
'ERR-XHPAST-LEY' => 'xhpast got Rickrolled',
);
}
protected function execute(ConduitAPIRequest $request) {
$source = $request->getValue('code');
$future = xhpast_get_parser_future($source);
list($stdout) = $future->resolvex();
return json_decode($stdout, true);
}
}
diff --git a/src/applications/conduit/method/phpast/ConduitAPI_phpast_version_Method.php b/src/applications/conduit/method/phpast/ConduitAPI_phpast_version_Method.php
index 3ca3a976c9..62b86f2b02 100644
--- a/src/applications/conduit/method/phpast/ConduitAPI_phpast_version_Method.php
+++ b/src/applications/conduit/method/phpast/ConduitAPI_phpast_version_Method.php
@@ -1,55 +1,39 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_phpast_version_Method extends ConduitAPIMethod {
public function getMethodDescription() {
return "Get server xhpast version.";
}
public function defineParamTypes() {
return array();
}
public function defineReturnType() {
return 'string';
}
public function defineErrorTypes() {
return array(
'ERR-NOT-FOUND' => 'xhpast was not found on the server',
'ERR-COMMAND-FAILED' => 'xhpast died with a nonzero exit code',
);
}
protected function execute(ConduitAPIRequest $request) {
$path = xhpast_get_binary_path();
if (!Filesystem::pathExists($path)) {
throw new ConduitException('ERR-NOT-FOUND');
}
list($err, $stdout) = exec_manual('%s --version', $path);
if ($err) {
throw new ConduitException('ERR-COMMAND-FAILED');
}
return trim($stdout);
}
}
diff --git a/src/applications/conduit/method/phriction/ConduitAPI_phriction_Method.php b/src/applications/conduit/method/phriction/ConduitAPI_phriction_Method.php
index a616d4353c..1b8cf5cccc 100644
--- a/src/applications/conduit/method/phriction/ConduitAPI_phriction_Method.php
+++ b/src/applications/conduit/method/phriction/ConduitAPI_phriction_Method.php
@@ -1,52 +1,36 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
abstract class ConduitAPI_phriction_Method extends ConduitAPIMethod {
final protected function buildDocumentInfoDictionary(PhrictionDocument $doc) {
$content = $doc->getContent();
return $this->buildDocumentContentDictionary($doc, $content);
}
final protected function buildDocumentContentDictionary(
PhrictionDocument $doc,
PhrictionContent $content) {
$uri = PhrictionDocument::getSlugURI($content->getSlug());
$uri = PhabricatorEnv::getProductionURI($uri);
$doc_status = $doc->getStatus();
return array(
'phid' => $doc->getPHID(),
'uri' => $uri,
'slug' => $content->getSlug(),
'version' => $content->getVersion(),
'authorPHID' => $content->getAuthorPHID(),
'title' => $content->getTitle(),
'content' => $content->getContent(),
'status' => PhrictionDocumentStatus::getConduitConstant($doc_status),
'description' => $content->getDescription(),
'dateCreated' => $content->getDateCreated(),
);
}
}
diff --git a/src/applications/conduit/method/phriction/ConduitAPI_phriction_edit_Method.php b/src/applications/conduit/method/phriction/ConduitAPI_phriction_edit_Method.php
index 8d22a2f9bb..bd84e9f5c1 100644
--- a/src/applications/conduit/method/phriction/ConduitAPI_phriction_edit_Method.php
+++ b/src/applications/conduit/method/phriction/ConduitAPI_phriction_edit_Method.php
@@ -1,60 +1,44 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_phriction_edit_Method
extends ConduitAPI_phriction_Method {
public function getMethodDescription() {
return "Update a Phriction document.";
}
public function defineParamTypes() {
return array(
'slug' => 'required string',
'title' => 'optional string',
'content' => 'optional string',
'description' => 'optional string',
);
}
public function defineReturnType() {
return 'nonempty dict';
}
public function defineErrorTypes() {
return array(
);
}
protected function execute(ConduitAPIRequest $request) {
$slug = $request->getValue('slug');
$editor = id(PhrictionDocumentEditor::newForSlug($slug))
->setActor($request->getUser())
->setTitle($request->getValue('title'))
->setContent($request->getValue('content'))
->setDescription($request->getvalue('description'))
->save();
return $this->buildDocumentInfoDictionary($editor->getDocument());
}
}
diff --git a/src/applications/conduit/method/phriction/ConduitAPI_phriction_history_Method.php b/src/applications/conduit/method/phriction/ConduitAPI_phriction_history_Method.php
index eb26de8a2d..5dc467a23c 100644
--- a/src/applications/conduit/method/phriction/ConduitAPI_phriction_history_Method.php
+++ b/src/applications/conduit/method/phriction/ConduitAPI_phriction_history_Method.php
@@ -1,68 +1,52 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_phriction_history_Method
extends ConduitAPI_phriction_Method {
public function getMethodDescription() {
return "Retrieve history about a Phriction docuemnt.";
}
public function defineParamTypes() {
return array(
'slug' => 'required string',
);
}
public function defineReturnType() {
return 'nonempty list';
}
public function defineErrorTypes() {
return array(
'ERR-BAD-DOCUMENT' => 'No such document exists.',
);
}
protected function execute(ConduitAPIRequest $request) {
$slug = $request->getValue('slug');
$doc = id(new PhrictionDocument())->loadOneWhere(
'slug = %s',
PhabricatorSlug::normalize($slug));
if (!$doc) {
throw new ConduitException('ERR-BAD-DOCUMENT');
}
$content = id(new PhrictionContent())->loadAllWhere(
'documentID = %d ORDER BY version DESC',
$doc->getID());
$results = array();
foreach ($content as $version) {
$results[] = $this->buildDocumentContentDictionary(
$doc,
$version);
}
return $results;
}
}
diff --git a/src/applications/conduit/method/phriction/ConduitAPI_phriction_info_Method.php b/src/applications/conduit/method/phriction/ConduitAPI_phriction_info_Method.php
index c37ff4dc24..32947283e8 100644
--- a/src/applications/conduit/method/phriction/ConduitAPI_phriction_info_Method.php
+++ b/src/applications/conduit/method/phriction/ConduitAPI_phriction_info_Method.php
@@ -1,62 +1,46 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_phriction_info_Method
extends ConduitAPI_phriction_Method {
public function getMethodDescription() {
return "Retrieve information about a Phriction document.";
}
public function defineParamTypes() {
return array(
'slug' => 'required string',
);
}
public function defineReturnType() {
return 'nonempty dict';
}
public function defineErrorTypes() {
return array(
'ERR-BAD-DOCUMENT' => 'No such document exists.',
);
}
protected function execute(ConduitAPIRequest $request) {
$slug = $request->getValue('slug');
$doc = id(new PhrictionDocument())->loadOneWhere(
'slug = %s',
PhabricatorSlug::normalize($slug));
if (!$doc) {
throw new ConduitException('ERR-BAD-DOCUMENT');
}
$content = id(new PhrictionContent())->load($doc->getContentID());
$doc->attachContent($content);
return $this->buildDocumentInfoDictionary($doc);
}
}
diff --git a/src/applications/conduit/method/project/ConduitAPI_project_Method.php b/src/applications/conduit/method/project/ConduitAPI_project_Method.php
index 96bdc7f921..44f75069a6 100644
--- a/src/applications/conduit/method/project/ConduitAPI_project_Method.php
+++ b/src/applications/conduit/method/project/ConduitAPI_project_Method.php
@@ -1,54 +1,38 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
abstract class ConduitAPI_project_Method extends ConduitAPIMethod {
protected function buildProjectInfoDictionary(PhabricatorProject $project) {
$results = $this->buildProjectInfoDictionaries(array($project));
return idx($results, $project->getPHID());
}
protected function buildProjectInfoDictionaries(array $projects) {
assert_instances_of($projects, 'PhabricatorProject');
if (!$projects) {
return array();
}
$result = array();
foreach ($projects as $project) {
$member_phids = $project->getMemberPHIDs();
$member_phids = array_values($member_phids);
$result[$project->getPHID()] = array(
'id' => $project->getID(),
'phid' => $project->getPHID(),
'name' => $project->getName(),
'members' => $member_phids,
'dateCreated' => $project->getDateCreated(),
'dateModified' => $project->getDateModified(),
);
}
return $result;
}
}
diff --git a/src/applications/conduit/method/project/ConduitAPI_project_query_Method.php b/src/applications/conduit/method/project/ConduitAPI_project_query_Method.php
index 00645fbf03..f0d35bd410 100644
--- a/src/applications/conduit/method/project/ConduitAPI_project_query_Method.php
+++ b/src/applications/conduit/method/project/ConduitAPI_project_query_Method.php
@@ -1,98 +1,82 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_project_query_Method extends ConduitAPI_project_Method {
public function getMethodDescription() {
return "Execute searches for Projects.";
}
public function defineParamTypes() {
$statuses = array(
PhabricatorProjectQuery::STATUS_ANY,
PhabricatorProjectQuery::STATUS_OPEN,
PhabricatorProjectQuery::STATUS_CLOSED,
PhabricatorProjectQuery::STATUS_ACTIVE,
PhabricatorProjectQuery::STATUS_ARCHIVED,
);
return array(
'ids' => 'optional list<int>',
'phids' => 'optional list<phid>',
'status' => 'optional enum<'.implode(', ', $statuses).'>',
'members' => 'optional list<phid>',
'limit' => 'optional int',
'offset' => 'optional int',
);
}
public function defineReturnType() {
return 'list';
}
public function defineErrorTypes() {
return array(
);
}
protected function execute(ConduitAPIRequest $request) {
$query = new PhabricatorProjectQuery();
$query->setViewer($request->getUser());
$query->needMembers(true);
$ids = $request->getValue('ids');
if ($ids) {
$query->withIDs($ids);
}
$status = $request->getValue('status');
if ($status) {
$query->withStatus($status);
}
$phids = $request->getValue('phids');
if ($phids) {
$query->withPHIDs($phids);
}
$members = $request->getValue('members');
if ($members) {
$query->withMemberPHIDs($members);
}
$limit = $request->getValue('limit');
if ($limit) {
$query->setLimit($limit);
}
$offset = $request->getValue('offset');
if ($offset) {
$query->setOffset($offset);
}
$results = $query->execute();
return $this->buildProjectInfoDictionaries($results);
}
}
diff --git a/src/applications/conduit/method/remarkup/ConduitAPI_remarkup_process_Method.php b/src/applications/conduit/method/remarkup/ConduitAPI_remarkup_process_Method.php
index 892776ad51..6986488342 100644
--- a/src/applications/conduit/method/remarkup/ConduitAPI_remarkup_process_Method.php
+++ b/src/applications/conduit/method/remarkup/ConduitAPI_remarkup_process_Method.php
@@ -1,76 +1,60 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class ConduitAPI_remarkup_process_Method extends ConduitAPIMethod {
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
return 'Process text through remarkup in phabricator context.';
}
public function defineReturnType() {
return 'nonempty dict';
}
public function defineErrorTypes() {
return array(
'ERR-NO-CONTENT' => 'Content may not be empty.',
'ERR-INVALID-ENGINE' => 'Invalid markup engine.',
);
}
public function defineParamTypes() {
$available_contexts = array_keys($this->getEngineContexts());
$available_contexts = implode(', ', $available_contexts);
return array(
'context' => 'required enum<'.$available_contexts.'>',
'content' => 'required string',
);
}
protected function execute(ConduitAPIRequest $request) {
$content = $request->getValue('content');
$context = $request->getValue('context');
$engine_class = idx($this->getEngineContexts(), $context);
if (!$engine_class) {
throw new ConduitException('ERR-INVALID_ENGINE');
}
$engine = PhabricatorMarkupEngine::$engine_class();
$engine->setConfig('viewer', $request->getUser());
$result = array(
'content' => $engine->markupText($content),
);
return $result;
}
private function getEngineContexts() {
return array(
'phriction' => 'newPhrictionMarkupEngine',
'maniphest' => 'newManiphestMarkupEngine',
'differential' => 'newDifferentialMarkupEngine',
);
}
}
diff --git a/src/applications/conduit/method/repository/ConduitAPI_repository_Method.php b/src/applications/conduit/method/repository/ConduitAPI_repository_Method.php
index 1e1950e3f6..f2e073418f 100644
--- a/src/applications/conduit/method/repository/ConduitAPI_repository_Method.php
+++ b/src/applications/conduit/method/repository/ConduitAPI_repository_Method.php
@@ -1,37 +1,21 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
abstract class ConduitAPI_repository_Method extends ConduitAPIMethod {
protected function buildDictForRepository(PhabricatorRepository $repository) {
return array(
'name' => $repository->getName(),
'phid' => $repository->getPHID(),
'callsign' => $repository->getCallsign(),
'vcs' => $repository->getVersionControlSystem(),
'uri' => PhabricatorEnv::getProductionURI($repository->getURI()),
'remoteURI' => (string)$repository->getPublicRemoteURI(),
'tracking' => $repository->getDetail('tracking-enabled'),
'description' => $repository->getDetail('description'),
);
}
}
diff --git a/src/applications/conduit/method/repository/ConduitAPI_repository_create_Method.php b/src/applications/conduit/method/repository/ConduitAPI_repository_create_Method.php
index ec0f87f7d0..97d939a8a9 100644
--- a/src/applications/conduit/method/repository/ConduitAPI_repository_create_Method.php
+++ b/src/applications/conduit/method/repository/ConduitAPI_repository_create_Method.php
@@ -1,146 +1,130 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_repository_create_Method
extends ConduitAPI_repository_Method {
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodStatusDescription() {
return "Repository methods are new and subject to change.";
}
public function getMethodDescription() {
return "Create a new repository (Admin Only).";
}
public function defineParamTypes() {
return array(
'name' => 'required string',
'vcs' => 'required enum<git, hg, svn>',
'callsign' => 'required string',
'description' => 'optional string',
'encoding' => 'optional string',
'tracking' => 'optional bool',
'uri' => 'optional string',
'sshUser' => 'optional string',
'sshKey' => 'optional string',
'sshKeyFile' => 'optional string',
'httpUser' => 'optional string',
'httpPassword' => 'optional string',
'localPath' => 'optional string',
'svnSubpath' => 'optional string',
'branchFilter' => 'optional list<string>',
'closeCommitsFilter' => 'optional list<string>',
'pullFrequency' => 'optional int',
'defaultBranch' => 'optional string',
'heraldEnabled' => 'optional bool, default = true',
'autocloseEnabled' => 'optional bool, default = true',
'svnUUID' => 'optional string',
);
}
public function defineReturnType() {
return 'nonempty dict';
}
public function defineErrorTypes() {
return array(
'ERR-PERMISSIONS' =>
'You do not have the authority to call this method.',
'ERR-DUPLICATE' =>
'Duplicate repository callsign.',
'ERR-BAD-CALLSIGN' =>
'Callsign is required and must be ALL UPPERCASE LETTERS.',
'ERR-UNKNOWN-REPOSITORY-VCS' =>
'Unknown repository VCS type.',
);
}
protected function execute(ConduitAPIRequest $request) {
if (!$request->getUser()->getIsAdmin()) {
throw new ConduitException('ERR-PERMISSIONS');
}
// TODO: This has some duplication with (and lacks some of the validation
// of) the web workflow; refactor things so they can share more code as this
// stabilizes.
$repository = new PhabricatorRepository();
$repository->setName($request->getValue('name'));
$callsign = $request->getValue('callsign');
if (!preg_match('/[A-Z]+$/', $callsign)) {
throw new ConduitException('ERR-BAD-CALLSIGN');
}
$repository->setCallsign($callsign);
$vcs = $request->getValue('vcs');
$map = array(
'git' => PhabricatorRepositoryType::REPOSITORY_TYPE_GIT,
'hg' => PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL,
'svn' => PhabricatorRepositoryType::REPOSITORY_TYPE_SVN,
);
if (empty($map[$vcs])) {
throw new ConduitException('ERR-UNKNOWN-REPOSITORY-VCS');
}
$repository->setVersionControlSystem($map[$vcs]);
$details = array(
'encoding' => $request->getValue('encoding'),
'description' => $request->getValue('description'),
'tracking-enabled' => (bool)$request->getValue('tracking', true),
'remote-uri' => $request->getValue('uri'),
'local-path' => $request->getValue('localPath'),
'branch-filter' => array_fill_keys(
$request->getValue('branchFilter', array()),
true),
'close-commits-filter' => array_fill_keys(
$request->getValue('closeCommitsFilter', array()),
true),
'pull-frequency' => $request->getValue('pullFrequency'),
'default-branch' => $request->getValue('defaultBranch'),
'ssh-login' => $request->getValue('sshUser'),
'ssh-key' => $request->getValue('sshKey'),
'ssh-keyfile' => $request->getValue('sshKeyFile'),
'herald-disabled' => !$request->getValue('heraldEnabled', true),
'svn-subpath' => $request->getValue('svnSubpath'),
'disable-autoclose' => !$request->getValue('autocloseEnabled', true),
);
foreach ($details as $key => $value) {
$repository->setDetail($key, $value);
}
try {
$repository->save();
} catch (AphrontQueryDuplicateKeyException $ex) {
throw new ConduitException('ERR-DUPLICATE');
}
return $this->buildDictForRepository($repository);
}
}
diff --git a/src/applications/conduit/method/repository/ConduitAPI_repository_query_Method.php b/src/applications/conduit/method/repository/ConduitAPI_repository_query_Method.php
index d078a0dc35..0a474afd5c 100644
--- a/src/applications/conduit/method/repository/ConduitAPI_repository_query_Method.php
+++ b/src/applications/conduit/method/repository/ConduitAPI_repository_query_Method.php
@@ -1,61 +1,45 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_repository_query_Method
extends ConduitAPI_repository_Method {
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodStatusDescription() {
return "Repository methods are new and subject to change.";
}
public function getMethodDescription() {
return "Query repositories.";
}
public function defineParamTypes() {
return array(
);
}
public function defineReturnType() {
return 'list<dict>';
}
public function defineErrorTypes() {
return array(
);
}
protected function execute(ConduitAPIRequest $request) {
$repositories = id(new PhabricatorRepository())->loadAll();
$results = array();
foreach ($repositories as $repository) {
$results[] = $this->buildDictForRepository($repository);
}
return $results;
}
}
diff --git a/src/applications/conduit/method/slowvote/ConduitAPI_slowvote_info_Method.php b/src/applications/conduit/method/slowvote/ConduitAPI_slowvote_info_Method.php
index 7f7744d306..0fa3f9fc71 100644
--- a/src/applications/conduit/method/slowvote/ConduitAPI_slowvote_info_Method.php
+++ b/src/applications/conduit/method/slowvote/ConduitAPI_slowvote_info_Method.php
@@ -1,62 +1,46 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_slowvote_info_Method extends ConduitAPIMethod {
public function getMethodDescription() {
return "Retrieve an array of information about a poll.";
}
public function defineParamTypes() {
return array(
'poll_id' => 'required id',
);
}
public function defineReturnType() {
return 'nonempty dict';
}
public function defineErrorTypes() {
return array(
'ERR_BAD_POLL' => 'No such poll exists',
);
}
protected function execute(ConduitAPIRequest $request) {
$poll_id = $request->getValue('poll_id');
$poll = id(new PhabricatorSlowvotePoll())->load($poll_id);
if (!$poll) {
throw new ConduitException('ERR_BAD_POLL');
}
$result = array(
'id' => $poll->getID(),
'phid' => $poll->getPHID(),
'authorPHID' => $poll->getAuthorPHID(),
'question' => $poll->getQuestion(),
'uri' => PhabricatorEnv::getProductionURI('/V'.$poll->getID()),
);
return $result;
}
}
diff --git a/src/applications/conduit/method/user/ConduitAPI_user_Method.php b/src/applications/conduit/method/user/ConduitAPI_user_Method.php
index 7a1080b14c..a073726645 100644
--- a/src/applications/conduit/method/user/ConduitAPI_user_Method.php
+++ b/src/applications/conduit/method/user/ConduitAPI_user_Method.php
@@ -1,63 +1,47 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
abstract class ConduitAPI_user_Method extends ConduitAPIMethod {
protected function buildUserInformationDictionary(
PhabricatorUser $user,
PhabricatorUserStatus $current_status = null) {
$roles = array();
if ($user->getIsDisabled()) {
$roles[] = 'disabled';
}
if ($user->getIsSystemAgent()) {
$roles[] = 'agent';
}
if ($user->getIsAdmin()) {
$roles[] = 'admin';
}
$primary = $user->loadPrimaryEmail();
if ($primary && $primary->getIsVerified()) {
$roles[] = 'verified';
} else {
$roles[] = 'unverified';
}
$return = array(
'phid' => $user->getPHID(),
'userName' => $user->getUserName(),
'realName' => $user->getRealName(),
'image' => $user->loadProfileImageURI(),
'uri' => PhabricatorEnv::getURI('/p/'.$user->getUsername().'/'),
'roles' => $roles,
);
if ($current_status) {
$return['currentStatus'] = $current_status->getTextStatus();
$return['currentStatusUntil'] = $current_status->getDateTo();
}
return $return;
}
}
diff --git a/src/applications/conduit/method/user/ConduitAPI_user_addstatus_Method.php b/src/applications/conduit/method/user/ConduitAPI_user_addstatus_Method.php
index 782f01a6b6..8a2d856640 100644
--- a/src/applications/conduit/method/user/ConduitAPI_user_addstatus_Method.php
+++ b/src/applications/conduit/method/user/ConduitAPI_user_addstatus_Method.php
@@ -1,75 +1,59 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_user_addstatus_Method extends ConduitAPI_user_Method {
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
return "Add status information to the logged-in user.";
}
public function defineParamTypes() {
return array(
'fromEpoch' => 'required int',
'toEpoch' => 'required int',
'status' => 'required enum<away, sporadic>',
'description' => 'optional string',
);
}
public function defineReturnType() {
return 'void';
}
public function defineErrorTypes() {
return array(
'ERR-BAD-EPOCH' => "'toEpoch' must be bigger than 'fromEpoch'.",
'ERR-OVERLAP' =>
'There must be no status in any part of the specified epoch.',
);
}
protected function execute(ConduitAPIRequest $request) {
$user_phid = $request->getUser()->getPHID();
$from = $request->getValue('fromEpoch');
$to = $request->getValue('toEpoch');
$status = ucfirst($request->getValue('status'));
$description = $request->getValue('description');
try {
id(new PhabricatorUserStatus())
->setUserPHID($user_phid)
->setDateFrom($from)
->setDateTo($to)
->setTextStatus($status)
->setDescription($description)
->save();
} catch (PhabricatorUserStatusInvalidEpochException $e) {
throw new ConduitException('ERR-BAD-EPOCH');
} catch (PhabricatorUserStatusOverlapException $e) {
throw new ConduitException('ERR-OVERLAP');
}
}
}
diff --git a/src/applications/conduit/method/user/ConduitAPI_user_disable_Method.php b/src/applications/conduit/method/user/ConduitAPI_user_disable_Method.php
index 7aa5503ccb..6de78dcbac 100644
--- a/src/applications/conduit/method/user/ConduitAPI_user_disable_Method.php
+++ b/src/applications/conduit/method/user/ConduitAPI_user_disable_Method.php
@@ -1,69 +1,53 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_user_disable_Method
extends ConduitAPI_user_Method {
public function getMethodDescription() {
return "Permanently disable specified users (admin only).";
}
public function defineParamTypes() {
return array(
'phids' => 'required list<phid>',
);
}
public function defineReturnType() {
return 'void';
}
public function defineErrorTypes() {
return array(
'ERR-PERMISSIONS' => 'Only admins can call this method.',
'ERR-BAD-PHID' => 'Non existent user PHID.',
);
}
protected function execute(ConduitAPIRequest $request) {
$actor = $request->getUser();
if (!$actor->getIsAdmin()) {
throw new ConduitException('ERR-PERMISSIONS');
}
$phids = $request->getValue('phids');
$users = id(new PhabricatorUser())->loadAllWhere(
'phid IN (%Ls)',
$phids);
if (count($phids) != count($users)) {
throw new ConduitException('ERR-BAD-PHID');
}
foreach ($users as $user) {
id(new PhabricatorUserEditor())
->setActor($actor)
->disableUser($user, true);
}
}
}
diff --git a/src/applications/conduit/method/user/ConduitAPI_user_enable_Method.php b/src/applications/conduit/method/user/ConduitAPI_user_enable_Method.php
index 1ed9a80677..0c601cb0fe 100644
--- a/src/applications/conduit/method/user/ConduitAPI_user_enable_Method.php
+++ b/src/applications/conduit/method/user/ConduitAPI_user_enable_Method.php
@@ -1,69 +1,53 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_user_enable_Method
extends ConduitAPI_user_Method {
public function getMethodDescription() {
return "Re-enable specified users (admin only).";
}
public function defineParamTypes() {
return array(
'phids' => 'required list<phid>',
);
}
public function defineReturnType() {
return 'void';
}
public function defineErrorTypes() {
return array(
'ERR-PERMISSIONS' => 'Only admins can call this method.',
'ERR-BAD-PHID' => 'Non existent user PHID.',
);
}
protected function execute(ConduitAPIRequest $request) {
$actor = $request->getUser();
if (!$actor->getIsAdmin()) {
throw new ConduitException('ERR-PERMISSIONS');
}
$phids = $request->getValue('phids');
$users = id(new PhabricatorUser())->loadAllWhere(
'phid IN (%Ls)',
$phids);
if (count($phids) != count($users)) {
throw new ConduitException('ERR-BAD-PHID');
}
foreach ($users as $user) {
id(new PhabricatorUserEditor())
->setActor($actor)
->disableUser($user, false);
}
}
}
diff --git a/src/applications/conduit/method/user/ConduitAPI_user_find_Method.php b/src/applications/conduit/method/user/ConduitAPI_user_find_Method.php
index a1f1ee5388..ab2dadf102 100644
--- a/src/applications/conduit/method/user/ConduitAPI_user_find_Method.php
+++ b/src/applications/conduit/method/user/ConduitAPI_user_find_Method.php
@@ -1,53 +1,37 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_user_find_Method
extends ConduitAPI_user_Method {
public function getMethodDescription() {
return "Find user PHIDs which correspond to provided user aliases. ".
"Returns NULL for aliases which do have any corresponding PHIDs.";
}
public function defineParamTypes() {
return array(
'aliases' => 'required nonempty list<string>'
);
}
public function defineReturnType() {
return 'nonempty dict<string, phid>';
}
public function defineErrorTypes() {
return array(
);
}
protected function execute(ConduitAPIRequest $request) {
$users = id(new PhabricatorUser())->loadAllWhere(
'username in (%Ls)',
$request->getValue('aliases'));
return mpull($users, 'getPHID', 'getUsername');
}
}
diff --git a/src/applications/conduit/method/user/ConduitAPI_user_info_Method.php b/src/applications/conduit/method/user/ConduitAPI_user_info_Method.php
index 323183867f..9397162b1d 100644
--- a/src/applications/conduit/method/user/ConduitAPI_user_info_Method.php
+++ b/src/applications/conduit/method/user/ConduitAPI_user_info_Method.php
@@ -1,66 +1,50 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_user_info_Method
extends ConduitAPI_user_Method {
public function getMethodStatus() {
return self::METHOD_STATUS_DEPRECATED;
}
public function getMethodStatusDescription() {
return "Replaced by 'user.query'.";
}
public function getMethodDescription() {
return "Retrieve information about a user by PHID.";
}
public function defineParamTypes() {
return array(
'phid' => 'required phid',
);
}
public function defineReturnType() {
return 'nonempty dict<string, wild>';
}
public function defineErrorTypes() {
return array(
'ERR-BAD-USER' => 'No such user exists.',
);
}
protected function execute(ConduitAPIRequest $request) {
$user = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$request->getValue('phid'));
if (!$user) {
throw new ConduitException('ERR-BAD-USER');
}
return $this->buildUserInformationDictionary($user);
}
}
diff --git a/src/applications/conduit/method/user/ConduitAPI_user_query_Method.php b/src/applications/conduit/method/user/ConduitAPI_user_query_Method.php
index f62ef4a8c6..35b6fc94fc 100644
--- a/src/applications/conduit/method/user/ConduitAPI_user_query_Method.php
+++ b/src/applications/conduit/method/user/ConduitAPI_user_query_Method.php
@@ -1,97 +1,81 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_user_query_Method
extends ConduitAPI_user_Method {
public function getMethodDescription() {
return "Query users.";
}
public function defineParamTypes() {
return array(
'usernames' => 'optional list<string>',
'emails' => 'optional list<string>',
'realnames' => 'optional list<string>',
'phids' => 'optional list<phid>',
'ids' => 'optional list<uint>',
'offset' => 'optional int',
'limit' => 'optional int (default = 100)',
);
}
public function defineReturnType() {
return 'list<dict>';
}
public function defineErrorTypes() {
return array(
'ERR-INVALID-PARAMETER' => 'Missing or malformed parameter.',
);
}
protected function execute(ConduitAPIRequest $request) {
$usernames = $request->getValue('usernames', array());
$emails = $request->getValue('emails', array());
$realnames = $request->getValue('realnames', array());
$phids = $request->getValue('phids', array());
$ids = $request->getValue('ids', array());
$offset = $request->getValue('offset', 0);
$limit = $request->getValue('limit', 100);
$query = new PhabricatorPeopleQuery();
if ($usernames) {
$query->withUsernames($usernames);
}
if ($emails) {
$query->withEmails($emails);
}
if ($realnames) {
$query->withRealnames($realnames);
}
if ($phids) {
$query->withPHIDs($phids);
}
if ($ids) {
$query->withIDs($ids);
}
if ($limit) {
$query->setLimit($limit);
}
if ($offset) {
$query->setOffset($offset);
}
$users = $query->execute();
$statuses = id(new PhabricatorUserStatus())->loadCurrentStatuses(
mpull($users, 'getPHID'));
$results = array();
foreach ($users as $user) {
$results[] = $this->buildUserInformationDictionary(
$user,
idx($statuses, $user->getPHID()));
}
return $results;
}
}
diff --git a/src/applications/conduit/method/user/ConduitAPI_user_removestatus_Method.php b/src/applications/conduit/method/user/ConduitAPI_user_removestatus_Method.php
index be58bdac78..93bac9c656 100644
--- a/src/applications/conduit/method/user/ConduitAPI_user_removestatus_Method.php
+++ b/src/applications/conduit/method/user/ConduitAPI_user_removestatus_Method.php
@@ -1,94 +1,78 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_user_removestatus_Method extends ConduitAPI_user_Method {
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodDescription() {
return "Delete status information of the logged-in user.";
}
public function defineParamTypes() {
return array(
'fromEpoch' => 'required int',
'toEpoch' => 'required int',
);
}
public function defineReturnType() {
return 'int';
}
public function defineErrorTypes() {
return array(
'ERR-BAD-EPOCH' => "'toEpoch' must be bigger than 'fromEpoch'.",
);
}
protected function execute(ConduitAPIRequest $request) {
$user_phid = $request->getUser()->getPHID();
$from = $request->getValue('fromEpoch');
$to = $request->getValue('toEpoch');
if ($to <= $from) {
throw new ConduitException('ERR-BAD-EPOCH');
}
$table = new PhabricatorUserStatus();
$table->openTransaction();
$table->beginReadLocking();
$overlap = $table->loadAllWhere(
'userPHID = %s AND dateFrom < %d AND dateTo > %d',
$user_phid,
$to,
$from);
foreach ($overlap as $status) {
if ($status->getDateFrom() < $from) {
if ($status->getDateTo() > $to) {
// Split the interval.
id(new PhabricatorUserStatus())
->setUserPHID($user_phid)
->setDateFrom($to)
->setDateTo($status->getDateTo())
->setStatus($status->getStatus())
->setDescription($status->getDescription())
->save();
}
$status->setDateTo($from);
$status->save();
} else if ($status->getDateTo() > $to) {
$status->setDateFrom($to);
$status->save();
} else {
$status->delete();
}
}
$table->endReadLocking();
$table->saveTransaction();
return count($overlap);
}
}
diff --git a/src/applications/conduit/method/user/ConduitAPI_user_whoami_Method.php b/src/applications/conduit/method/user/ConduitAPI_user_whoami_Method.php
index 1be4109b05..dba0dbecb0 100644
--- a/src/applications/conduit/method/user/ConduitAPI_user_whoami_Method.php
+++ b/src/applications/conduit/method/user/ConduitAPI_user_whoami_Method.php
@@ -1,51 +1,35 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_user_whoami_Method
extends ConduitAPI_user_Method {
public function getMethodDescription() {
return "Retrieve information about the logged-in user.";
}
public function defineParamTypes() {
return array(
);
}
public function defineReturnType() {
return 'nonempty dict<string, wild>';
}
public function defineErrorTypes() {
return array(
);
}
public function getRequiredScope() {
return PhabricatorOAuthServerScope::SCOPE_WHOAMI;
}
protected function execute(ConduitAPIRequest $request) {
return $this->buildUserInformationDictionary($request->getUser());
}
}
diff --git a/src/applications/conduit/protocol/ConduitAPIRequest.php b/src/applications/conduit/protocol/ConduitAPIRequest.php
index 68d73d31dc..64752a85bc 100644
--- a/src/applications/conduit/protocol/ConduitAPIRequest.php
+++ b/src/applications/conduit/protocol/ConduitAPIRequest.php
@@ -1,64 +1,48 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPIRequest {
protected $params;
private $user;
public function __construct(array $params) {
$this->params = $params;
}
public function getValue($key, $default = null) {
return coalesce(idx($this->params, $key), $default);
}
public function getAllParameters() {
return $this->params;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
/**
* Retrieve the authentic identity of the user making the request. If a
* method requires authentication (the default) the user object will always
* be available. If a method does not require authentication (i.e., overrides
* shouldRequireAuthentication() to return false) the user object will NEVER
* be available.
*
* @return PhabricatorUser Authentic user, available ONLY if the method
* requires authentication.
*/
public function getUser() {
if (!$this->user) {
throw new Exception(
"You can not access the user inside the implementation of a Conduit ".
"method which does not require authentication (as per ".
"shouldRequireAuthentication()).");
}
return $this->user;
}
}
diff --git a/src/applications/conduit/protocol/ConduitAPIResponse.php b/src/applications/conduit/protocol/ConduitAPIResponse.php
index db24324a29..0e604019dd 100644
--- a/src/applications/conduit/protocol/ConduitAPIResponse.php
+++ b/src/applications/conduit/protocol/ConduitAPIResponse.php
@@ -1,59 +1,43 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPIResponse {
private $result;
private $errorCode;
private $errorInfo;
public function setResult($result) {
$this->result = $result;
return $this;
}
public function getResult() {
return $this->result;
}
public function setErrorCode($error_code) {
$this->errorCode = $error_code;
return $this;
}
public function getErrorCode() {
return $this->errorCode;
}
public function setErrorInfo($error_info) {
$this->errorInfo = $error_info;
return $this;
}
public function getErrorInfo() {
return $this->errorInfo;
}
public function toDictionary() {
return array(
'result' => $this->getResult(),
'error_code' => $this->getErrorCode(),
'error_info' => $this->getErrorInfo(),
);
}
}
diff --git a/src/applications/conduit/protocol/ConduitException.php b/src/applications/conduit/protocol/ConduitException.php
index 4893fd1eac..c916216df0 100644
--- a/src/applications/conduit/protocol/ConduitException.php
+++ b/src/applications/conduit/protocol/ConduitException.php
@@ -1,48 +1,32 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitException extends Exception {
private $errorDescription;
/**
* Set a detailed error description. If omitted, the generic error description
* will be used instead. This is useful to provide specific information about
* an exception (e.g., which values were wrong in an invalid request).
*
* @param string Detailed error description.
* @return this
*/
public function setErrorDescription($error_description) {
$this->errorDescription = $error_description;
return $this;
}
/**
* Get a detailed error description, if available.
*
* @return string|null Error description, if one is available.
*/
public function getErrorDescription() {
return $this->errorDescription;
}
}
diff --git a/src/applications/conduit/storage/PhabricatorConduitCertificateToken.php b/src/applications/conduit/storage/PhabricatorConduitCertificateToken.php
index 3e86c0d6ba..f480dcc1b8 100644
--- a/src/applications/conduit/storage/PhabricatorConduitCertificateToken.php
+++ b/src/applications/conduit/storage/PhabricatorConduitCertificateToken.php
@@ -1,27 +1,11 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class PhabricatorConduitCertificateToken extends PhabricatorConduitDAO {
protected $userPHID;
protected $token;
}
diff --git a/src/applications/conduit/storage/PhabricatorConduitConnectionLog.php b/src/applications/conduit/storage/PhabricatorConduitConnectionLog.php
index cc1838d363..f58ea28ad2 100644
--- a/src/applications/conduit/storage/PhabricatorConduitConnectionLog.php
+++ b/src/applications/conduit/storage/PhabricatorConduitConnectionLog.php
@@ -1,29 +1,13 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class PhabricatorConduitConnectionLog extends PhabricatorConduitDAO {
protected $client;
protected $clientVersion;
protected $clientDescription;
protected $username;
}
diff --git a/src/applications/conduit/storage/PhabricatorConduitDAO.php b/src/applications/conduit/storage/PhabricatorConduitDAO.php
index 117059f8ae..70e87d921f 100644
--- a/src/applications/conduit/storage/PhabricatorConduitDAO.php
+++ b/src/applications/conduit/storage/PhabricatorConduitDAO.php
@@ -1,28 +1,12 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
abstract class PhabricatorConduitDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'conduit';
}
}
diff --git a/src/applications/conduit/storage/PhabricatorConduitMethodCallLog.php b/src/applications/conduit/storage/PhabricatorConduitMethodCallLog.php
index 3a8d6d64ca..e796e3bf58 100644
--- a/src/applications/conduit/storage/PhabricatorConduitMethodCallLog.php
+++ b/src/applications/conduit/storage/PhabricatorConduitMethodCallLog.php
@@ -1,29 +1,13 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class PhabricatorConduitMethodCallLog extends PhabricatorConduitDAO {
protected $connectionID;
protected $method;
protected $error;
protected $duration;
}
diff --git a/src/applications/countdown/application/PhabricatorApplicationCountdown.php b/src/applications/countdown/application/PhabricatorApplicationCountdown.php
index 39434f5a34..be92e9a981 100644
--- a/src/applications/countdown/application/PhabricatorApplicationCountdown.php
+++ b/src/applications/countdown/application/PhabricatorApplicationCountdown.php
@@ -1,60 +1,44 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationCountdown extends PhabricatorApplication {
public function getBaseURI() {
return '/countdown/';
}
public function getAutospriteName() {
return 'countdown';
}
public function getShortDescription() {
return 'Countdown Timers';
}
public function getTitleGlyph() {
return "\xE2\x9A\xB2";
}
public function getFlavorText() {
return pht('Utilize the full capabilities of your ALU.');
}
public function getApplicationGroup() {
return self::GROUP_UTILITIES;
}
public function getRoutes() {
return array(
'/countdown/' => array(
''
=> 'PhabricatorCountdownListController',
'(?P<id>[1-9]\d*)/'
=> 'PhabricatorCountdownViewController',
'edit/(?:(?P<id>[1-9]\d*)/)?'
=> 'PhabricatorCountdownEditController',
'delete/(?P<id>[1-9]\d*)/'
=> 'PhabricatorCountdownDeleteController'
),
);
}
}
diff --git a/src/applications/countdown/controller/PhabricatorCountdownController.php b/src/applications/countdown/controller/PhabricatorCountdownController.php
index 23d7e88995..03f8495233 100644
--- a/src/applications/countdown/controller/PhabricatorCountdownController.php
+++ b/src/applications/countdown/controller/PhabricatorCountdownController.php
@@ -1,37 +1,21 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorCountdownController extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('Countdown');
$page->setBaseURI('/countdown/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph("\xE2\x9A\xB2");
$page->setShowChrome(idx($data, 'chrome', true));
$page->appendChild($view);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
}
diff --git a/src/applications/countdown/controller/PhabricatorCountdownDeleteController.php b/src/applications/countdown/controller/PhabricatorCountdownDeleteController.php
index 7981ac7973..d544723cd2 100644
--- a/src/applications/countdown/controller/PhabricatorCountdownDeleteController.php
+++ b/src/applications/countdown/controller/PhabricatorCountdownDeleteController.php
@@ -1,62 +1,46 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorCountdownDeleteController
extends PhabricatorCountdownController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$timer = id(new PhabricatorTimer())->load($this->id);
if (!$timer) {
return new Aphront404Response();
}
if (($timer->getAuthorPHID() !== $user->getPHID())
&& $user->getIsAdmin() === false) {
return new Aphront403Response();
}
if ($request->isFormPost()) {
$timer->delete();
return id(new AphrontRedirectResponse())
->setURI('/countdown/');
}
$dialog = new AphrontDialogView();
$dialog->setUser($request->getUser());
$dialog->setTitle('Really delete this countdown?');
$dialog->appendChild(
'<p>Are you sure you want to delete the countdown "'.
phutil_escape_html($timer->getTitle()).'"?</p>');
$dialog->addSubmitButton('Delete');
$dialog->addCancelButton('/countdown/');
$dialog->setSubmitURI($request->getPath());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/countdown/controller/PhabricatorCountdownEditController.php b/src/applications/countdown/controller/PhabricatorCountdownEditController.php
index 5853b912be..d4bab9ad35 100644
--- a/src/applications/countdown/controller/PhabricatorCountdownEditController.php
+++ b/src/applications/countdown/controller/PhabricatorCountdownEditController.php
@@ -1,140 +1,124 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorCountdownEditController
extends PhabricatorCountdownController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$action_label = 'Create Timer';
if ($this->id) {
$timer = id(new PhabricatorTimer())->load($this->id);
// If no timer is found
if (!$timer) {
return new Aphront404Response();
}
if (($timer->getAuthorPHID() != $user->getPHID())
&& $user->getIsAdmin() == false) {
return new Aphront403Response();
}
$action_label = 'Update Timer';
} else {
$timer = new PhabricatorTimer();
$timer->setDatePoint(time());
}
$error_view = null;
$e_text = null;
if ($request->isFormPost()) {
$errors = array();
$title = $request->getStr('title');
$datepoint = $request->getStr('datepoint');
$e_text = null;
if (!strlen($title)) {
$e_text = 'Required';
$errors[] = 'You must give it a name.';
}
// If the user types something like "5 PM", convert it to a timestamp
// using their local time, not the server time.
$timezone = new DateTimeZone($user->getTimezoneIdentifier());
try {
$date = new DateTime($datepoint, $timezone);
$timestamp = $date->format('U');
} catch (Exception $e) {
$errors[] = 'You entered an incorrect date. You can enter date like'.
' \'2011-06-26 13:33:37\' to create an event at'.
' 13:33:37 on the 26th of June 2011.';
$timestamp = null;
}
$timer->setTitle($title);
$timer->setDatePoint($timestamp);
if (!count($errors)) {
$timer->setAuthorPHID($user->getPHID());
$timer->save();
return id(new AphrontRedirectResponse())
->setURI('/countdown/'.$timer->getID().'/');
} else {
$error_view = id(new AphrontErrorView())
->setErrors($errors)
->setTitle('It\'s not The Final Countdown (du nu nuuu nun)' .
' until you fix these problem');
}
}
if ($timer->getDatePoint()) {
$display_datepoint = phabricator_datetime(
$timer->getDatePoint(),
$user);
} else {
$display_datepoint = $request->getStr('datepoint');
}
$form = id(new AphrontFormView())
->setUser($user)
->setAction($request->getRequestURI()->getPath())
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Title')
->setValue($timer->getTitle())
->setName('title'))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('End date')
->setValue($display_datepoint)
->setName('datepoint')
->setCaption(
'Examples: '.
'<tt>2011-12-25</tt> or '.
'<tt>3 hours</tt> or '.
'<tt>June 8 2011, 5 PM</tt>.'))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton('/countdown/')
->setValue($action_label));
$panel = id(new AphrontPanelView())
->setWidth(AphrontPanelView::WIDTH_FORM)
->setHeader($action_label)
->appendChild($form);
return $this->buildStandardPageResponse(
array(
$error_view,
$panel,
),
array(
'title' => 'Edit Countdown',
));
}
}
diff --git a/src/applications/countdown/controller/PhabricatorCountdownListController.php b/src/applications/countdown/controller/PhabricatorCountdownListController.php
index 1e22979182..1f8190254d 100644
--- a/src/applications/countdown/controller/PhabricatorCountdownListController.php
+++ b/src/applications/countdown/controller/PhabricatorCountdownListController.php
@@ -1,111 +1,95 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorCountdownListController
extends PhabricatorCountdownController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$pager = new AphrontPagerView();
$pager->setOffset($request->getInt('page'));
$pager->setURI($request->getRequestURI(), 'page');
$timers = id(new PhabricatorTimer())->loadAllWhere(
'1 = 1 ORDER BY id DESC LIMIT %d, %d',
$pager->getOffset(),
$pager->getPageSize() + 1);
$timers = $pager->sliceResults($timers);
$phids = mpull($timers, 'getAuthorPHID');
$handles = $this->loadViewerHandles($phids);
$rows = array();
foreach ($timers as $timer) {
$edit_button = null;
$delete_button = null;
if ($user->getIsAdmin() ||
($user->getPHID() == $timer->getAuthorPHID())) {
$edit_button = phutil_render_tag(
'a',
array(
'class' => 'small button grey',
'href' => '/countdown/edit/'.$timer->getID().'/'
),
'Edit');
$delete_button = javelin_render_tag(
'a',
array(
'class' => 'small button grey',
'href' => '/countdown/delete/'.$timer->getID().'/',
'sigil' => 'workflow'
),
'Delete');
}
$rows[] = array(
phutil_escape_html($timer->getID()),
$handles[$timer->getAuthorPHID()]->renderLink(),
phutil_render_tag(
'a',
array(
'href' => '/countdown/'.$timer->getID().'/',
),
phutil_escape_html($timer->getTitle())),
phabricator_datetime($timer->getDatepoint(), $user),
$edit_button,
$delete_button,
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'ID',
'Author',
'Title',
'End Date',
'',
''
));
$table->setColumnClasses(
array(
null,
null,
'wide pri',
null,
'action',
'action',
));
$panel = id(new AphrontPanelView())
->appendChild($table)
->setHeader('Timers')
->setCreateButton('Create Timer', '/countdown/edit/')
->appendChild($pager);
return $this->buildStandardPageResponse($panel,
array(
'title' => 'Countdown',
));
}
}
diff --git a/src/applications/countdown/controller/PhabricatorCountdownViewController.php b/src/applications/countdown/controller/PhabricatorCountdownViewController.php
index 90cf629d15..48a51ed1c0 100644
--- a/src/applications/countdown/controller/PhabricatorCountdownViewController.php
+++ b/src/applications/countdown/controller/PhabricatorCountdownViewController.php
@@ -1,95 +1,79 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorCountdownViewController
extends PhabricatorCountdownController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$timer = id(new PhabricatorTimer())->load($this->id);
if (!$timer) {
return new Aphront404Response();
}
require_celerity_resource('phabricator-countdown-css');
$chrome_visible = $request->getBool('chrome', true);
$chrome_new = $chrome_visible ? false : null;
$chrome_link = phutil_render_tag(
'a',
array(
'href' => $request->getRequestURI()->alter('chrome', $chrome_new),
'class' => 'phabricator-timer-chrome-link',
),
$chrome_visible ? 'Disable Chrome' : 'Enable Chrome');
$container = celerity_generate_unique_node_id();
$content =
'<div class="phabricator-timer" id="'.$container.'">
<h1 class="phabricator-timer-header">'.
phutil_escape_html($timer->getTitle()).' &middot; '.
phabricator_datetime($timer->getDatePoint(), $user).
'</h1>
<div class="phabricator-timer-pane">
<table class="phabricator-timer-table">
<tr>
<th>Days</th>
<th>Hours</th>
<th>Minutes</th>
<th>Seconds</th>
</tr>
<tr>'.
javelin_render_tag('td',
array('sigil' => 'phabricator-timer-days'), '').
javelin_render_tag('td',
array('sigil' => 'phabricator-timer-hours'), '').
javelin_render_tag('td',
array('sigil' => 'phabricator-timer-minutes'), '').
javelin_render_tag('td',
array('sigil' => 'phabricator-timer-seconds'), '').
'</tr>
</table>
</div>'.
$chrome_link.
'</div>';
Javelin::initBehavior('countdown-timer', array(
'timestamp' => $timer->getDatepoint(),
'container' => $container,
));
$panel = $content;
return $this->buildStandardPageResponse(
$panel,
array(
'title' => 'Countdown: '.$timer->getTitle(),
'chrome' => $chrome_visible
));
}
}
diff --git a/src/applications/countdown/storage/PhabricatorCountdownDAO.php b/src/applications/countdown/storage/PhabricatorCountdownDAO.php
index 191d0cdd55..50d05d149c 100644
--- a/src/applications/countdown/storage/PhabricatorCountdownDAO.php
+++ b/src/applications/countdown/storage/PhabricatorCountdownDAO.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorCountdownDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'countdown';
}
}
diff --git a/src/applications/countdown/storage/PhabricatorTimer.php b/src/applications/countdown/storage/PhabricatorTimer.php
index 508cdf3f67..138f558dac 100644
--- a/src/applications/countdown/storage/PhabricatorTimer.php
+++ b/src/applications/countdown/storage/PhabricatorTimer.php
@@ -1,26 +1,10 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorTimer extends PhabricatorCountdownDAO {
protected $id;
protected $title;
protected $authorPHID;
protected $datepoint;
}
diff --git a/src/applications/daemon/application/PhabricatorApplicationDaemons.php b/src/applications/daemon/application/PhabricatorApplicationDaemons.php
index 0421546a5f..3464a72013 100644
--- a/src/applications/daemon/application/PhabricatorApplicationDaemons.php
+++ b/src/applications/daemon/application/PhabricatorApplicationDaemons.php
@@ -1,64 +1,48 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationDaemons extends PhabricatorApplication {
public function getName() {
return 'Daemon Console';
}
public function getShortDescription() {
return 'Manage Daemons';
}
public function getBaseURI() {
return '/daemon/';
}
public function getTitleGlyph() {
return "\xE2\x98\xAF";
}
public function getAutospriteName() {
return 'daemons';
}
public function getApplicationGroup() {
return self::GROUP_ADMIN;
}
public function getRoutes() {
return array(
'/daemon/' => array(
'task/(?P<id>[1-9]\d*)/' => 'PhabricatorWorkerTaskDetailController',
'task/(?P<id>[1-9]\d*)/(?P<action>[^/]+)/'
=> 'PhabricatorWorkerTaskUpdateController',
'log/' => array(
'(?P<running>running/)?' => 'PhabricatorDaemonLogListController',
'combined/' => 'PhabricatorDaemonCombinedLogController',
'(?P<id>[1-9]\d*)/' => 'PhabricatorDaemonLogViewController',
),
'timeline/' => 'PhabricatorDaemonTimelineConsoleController',
'timeline/(?P<id>[1-9]\d*)/'
=> 'PhabricatorDaemonTimelineEventController',
'' => 'PhabricatorDaemonConsoleController',
),
);
}
}
diff --git a/src/applications/daemon/controller/PhabricatorDaemonCombinedLogController.php b/src/applications/daemon/controller/PhabricatorDaemonCombinedLogController.php
index 716cfeea69..35a9a54743 100644
--- a/src/applications/daemon/controller/PhabricatorDaemonCombinedLogController.php
+++ b/src/applications/daemon/controller/PhabricatorDaemonCombinedLogController.php
@@ -1,59 +1,43 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorDaemonCombinedLogController
extends PhabricatorDaemonController {
public function processRequest() {
$request = $this->getRequest();
$pager = new AphrontPagerView();
$pager->setOffset($request->getInt('page'));
$pager->setPageSize(1000);
$events = id(new PhabricatorDaemonLogEvent())->loadAllWhere(
'1 = 1 ORDER BY id DESC LIMIT %d, %d',
$pager->getOffset(),
$pager->getPageSize() + 1);
$events = $pager->sliceResults($events);
$pager->setURI($request->getRequestURI(), 'page');
$event_view = new PhabricatorDaemonLogEventsView();
$event_view->setEvents($events);
$event_view->setUser($request->getUser());
$event_view->setCombinedLog(true);
$log_panel = new AphrontPanelView();
$log_panel->setHeader('Combined Daemon Logs');
$log_panel->appendChild($event_view);
$log_panel->appendChild($pager);
$nav = $this->buildSideNavView();
$nav->selectFilter('log/combined');
$nav->appendChild($log_panel);
return $this->buildApplicationPage(
$nav,
array(
'title' => 'Combined Daemon Log',
));
}
}
diff --git a/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php b/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php
index e51bca5a0d..c40b1c4e68 100644
--- a/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php
+++ b/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php
@@ -1,160 +1,144 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorDaemonConsoleController
extends PhabricatorDaemonController {
public function processRequest() {
$logs = id(new PhabricatorDaemonLog())->loadAllWhere(
'`status` != %s ORDER BY id DESC LIMIT 15', 'exit');
$request = $this->getRequest();
$user = $request->getUser();
$daemon_table = new PhabricatorDaemonLogListView();
$daemon_table->setUser($user);
$daemon_table->setDaemonLogs($logs);
$daemon_panel = new AphrontPanelView();
$daemon_panel->setHeader('Recently Launched Daemons');
$daemon_panel->appendChild($daemon_table);
$tasks = id(new PhabricatorWorkerActiveTask())->loadAllWhere(
'leaseOwner IS NOT NULL');
$rows = array();
foreach ($tasks as $task) {
$rows[] = array(
$task->getID(),
$task->getTaskClass(),
$task->getLeaseOwner(),
$task->getLeaseExpires() - time(),
$task->getFailureCount(),
phutil_render_tag(
'a',
array(
'href' => '/daemon/task/'.$task->getID().'/',
'class' => 'button small grey',
),
'View Task'),
);
}
$leased_table = new AphrontTableView($rows);
$leased_table->setHeaders(
array(
'ID',
'Class',
'Owner',
'Expires',
'Failures',
'',
));
$leased_table->setColumnClasses(
array(
'n',
'wide',
'',
'',
'n',
'action',
));
$leased_table->setNoDataString('No tasks are leased by workers.');
$leased_panel = new AphrontPanelView();
$leased_panel->setHeader('Leased Tasks');
$leased_panel->appendChild($leased_table);
$task_table = new PhabricatorWorkerActiveTask();
$queued = queryfx_all(
$task_table->establishConnection('r'),
'SELECT taskClass, count(*) N FROM %T GROUP BY taskClass
ORDER BY N DESC',
$task_table->getTableName());
$rows = array();
foreach ($queued as $row) {
$rows[] = array(
phutil_escape_html($row['taskClass']),
number_format($row['N']),
);
}
$queued_table = new AphrontTableView($rows);
$queued_table->setHeaders(
array(
'Class',
'Count',
));
$queued_table->setColumnClasses(
array(
'wide',
'n',
));
$queued_table->setNoDataString('Task queue is empty.');
$queued_panel = new AphrontPanelView();
$queued_panel->setHeader('Queued Tasks');
$queued_panel->appendChild($queued_table);
$cursors = id(new PhabricatorTimelineCursor())
->loadAll();
$rows = array();
foreach ($cursors as $cursor) {
$rows[] = array(
phutil_escape_html($cursor->getName()),
number_format($cursor->getPosition()),
);
}
$cursor_table = new AphrontTableView($rows);
$cursor_table->setHeaders(
array(
'Name',
'Position',
));
$cursor_table->setColumnClasses(
array(
'wide',
'n',
));
$cursor_table->setNoDataString('No timeline cursors exist.');
$cursor_panel = new AphrontPanelView();
$cursor_panel->setHeader('Timeline Cursors');
$cursor_panel->appendChild($cursor_table);
$nav = $this->buildSideNavView();
$nav->selectFilter('');
$nav->appendChild(
array(
$daemon_panel,
$cursor_panel,
$queued_panel,
$leased_panel,
));
return $this->buildApplicationPage(
$nav,
array(
'title' => 'Console',
));
}
}
diff --git a/src/applications/daemon/controller/PhabricatorDaemonController.php b/src/applications/daemon/controller/PhabricatorDaemonController.php
index 9101d3914b..75c0f248f9 100644
--- a/src/applications/daemon/controller/PhabricatorDaemonController.php
+++ b/src/applications/daemon/controller/PhabricatorDaemonController.php
@@ -1,38 +1,22 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorDaemonController extends PhabricatorController {
protected function buildSideNavView() {
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
$nav->addLabel('Daemons');
$nav->addFilter('', 'Console', $this->getApplicationURI());
$nav->addFilter('log/running', 'Running Daemons');
$nav->addFilter('log', 'All Daemons');
$nav->addFilter('log/combined', 'Combined Log');
$nav->addSpacer();
$nav->addLabel('Event Timeline');
$nav->addFilter('timeline', 'Timeline');
return $nav;
}
}
diff --git a/src/applications/daemon/controller/PhabricatorDaemonLogListController.php b/src/applications/daemon/controller/PhabricatorDaemonLogListController.php
index ba1506bf7d..9802eef317 100644
--- a/src/applications/daemon/controller/PhabricatorDaemonLogListController.php
+++ b/src/applications/daemon/controller/PhabricatorDaemonLogListController.php
@@ -1,68 +1,52 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorDaemonLogListController
extends PhabricatorDaemonController {
private $running;
public function willProcessRequest(array $data) {
$this->running = !empty($data['running']);
}
public function processRequest() {
$request = $this->getRequest();
$pager = new AphrontPagerView();
$pager->setOffset($request->getInt('page'));
$clause = '1 = 1';
if ($this->running) {
$clause = "`status` != 'exit'";
}
$logs = id(new PhabricatorDaemonLog())->loadAllWhere(
'%Q ORDER BY id DESC LIMIT %d, %d',
$clause,
$pager->getOffset(),
$pager->getPageSize() + 1);
$logs = $pager->sliceResults($logs);
$pager->setURI($request->getRequestURI(), 'page');
$daemon_table = new PhabricatorDaemonLogListView();
$daemon_table->setUser($request->getUser());
$daemon_table->setDaemonLogs($logs);
$daemon_panel = new AphrontPanelView();
$daemon_panel->setHeader('Launched Daemons');
$daemon_panel->appendChild($daemon_table);
$daemon_panel->appendChild($pager);
$nav = $this->buildSideNavView();
$nav->selectFilter($this->running ? 'log/running' : 'log');
$nav->appendChild($daemon_panel);
return $this->buildApplicationPage(
$nav,
array(
'title' => $this->running ? 'Running Daemons' : 'All Daemons',
));
}
}
diff --git a/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php b/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php
index aceb52ec8a..bc7e1c974a 100644
--- a/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php
+++ b/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php
@@ -1,100 +1,84 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorDaemonLogViewController
extends PhabricatorDaemonController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$log = id(new PhabricatorDaemonLog())->load($this->id);
if (!$log) {
return new Aphront404Response();
}
$events = id(new PhabricatorDaemonLogEvent())->loadAllWhere(
'logID = %d ORDER BY id DESC LIMIT 1000',
$log->getID());
$content = array();
$argv = $log->getArgv();
if (is_array($argv)) {
$argv = implode("\n", $argv);
}
$form = id(new AphrontFormView())
->setUser($user)
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Daemon')
->setValue($log->getDaemon()))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Host')
->setValue($log->getHost()))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('PID')
->setValue($log->getPID()))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Started')
->setValue(
phabricator_datetime($log->getDateCreated(), $user)))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Argv')
->setValue($argv));
$panel = new AphrontPanelView();
$panel->setHeader('Daemon Details');
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->appendChild($form);
$content[] = $panel;
$event_view = new PhabricatorDaemonLogEventsView();
$event_view->setUser($user);
$event_view->setEvents($events);
$log_panel = new AphrontPanelView();
$log_panel->setHeader('Daemon Logs');
$log_panel->appendChild($event_view);
$content[] = $log_panel;
$nav = $this->buildSideNavView();
$nav->selectFilter('log');
$nav->appendChild($content);
return $this->buildApplicationPage(
$nav,
array(
'title' => 'Daemon Log',
));
}
}
diff --git a/src/applications/daemon/controller/PhabricatorDaemonTimelineConsoleController.php b/src/applications/daemon/controller/PhabricatorDaemonTimelineConsoleController.php
index efe4c78c48..0c5f711194 100644
--- a/src/applications/daemon/controller/PhabricatorDaemonTimelineConsoleController.php
+++ b/src/applications/daemon/controller/PhabricatorDaemonTimelineConsoleController.php
@@ -1,71 +1,55 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorDaemonTimelineConsoleController
extends PhabricatorDaemonController {
public function processRequest() {
$timeline_table = new PhabricatorTimelineEvent('NULL');
$events = queryfx_all(
$timeline_table->establishConnection('r'),
'SELECT id, type FROM %T ORDER BY id DESC LIMIT 100',
$timeline_table->getTableName());
$rows = array();
foreach ($events as $event) {
$rows[] = array(
phutil_render_tag(
'a',
array(
'href' => '/daemon/timeline/'.$event['id'].'/',
),
$event['id']),
phutil_escape_html($event['type']),
);
}
$event_table = new AphrontTableView($rows);
$event_table->setHeaders(
array(
'ID',
'Type',
));
$event_table->setColumnClasses(
array(
null,
'wide',
));
$event_panel = new AphrontPanelView();
$event_panel->setHeader('Timeline Events');
$event_panel->appendChild($event_table);
$nav = $this->buildSideNavView();
$nav->selectFilter('timeline');
$nav->appendChild($event_panel);
return $this->buildApplicationPage(
$nav,
array(
'title' => 'Timeline',
));
}
}
diff --git a/src/applications/daemon/controller/PhabricatorDaemonTimelineEventController.php b/src/applications/daemon/controller/PhabricatorDaemonTimelineEventController.php
index e93c530d81..25c72de011 100644
--- a/src/applications/daemon/controller/PhabricatorDaemonTimelineEventController.php
+++ b/src/applications/daemon/controller/PhabricatorDaemonTimelineEventController.php
@@ -1,84 +1,68 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorDaemonTimelineEventController
extends PhabricatorDaemonController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$event = id(new PhabricatorTimelineEvent('NULL'))->load($this->id);
if (!$event) {
return new Aphront404Response();
}
$request = $this->getRequest();
$user = $request->getUser();
if ($event->getDataID()) {
$data = id(new PhabricatorTimelineEventData())->load(
$event->getDataID());
}
if ($data) {
$data = json_encode($data->getEventData());
} else {
$data = 'null';
}
$form = new AphrontFormView();
$form
->setUser($user)
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('ID')
->setValue($event->getID()))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Type')
->setValue($event->getType()))
->appendChild(
id(new AphrontFormTextAreaControl())
->setDisabled(true)
->setLabel('Data')
->setValue($data))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton('/daemon/timeline/'));
$panel = new AphrontPanelView();
$panel->setHeader('Event');
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->appendChild($form);
$nav = $this->buildSideNavView();
$nav->selectFilter('timeline');
$nav->appendChild($panel);
return $this->buildApplicationPage(
$nav,
array(
'title' => 'Timeline Event',
));
}
}
diff --git a/src/applications/daemon/controller/PhabricatorWorkerTaskDetailController.php b/src/applications/daemon/controller/PhabricatorWorkerTaskDetailController.php
index a514e13b52..04e2b40032 100644
--- a/src/applications/daemon/controller/PhabricatorWorkerTaskDetailController.php
+++ b/src/applications/daemon/controller/PhabricatorWorkerTaskDetailController.php
@@ -1,193 +1,177 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorWorkerTaskDetailController
extends PhabricatorDaemonController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$task = id(new PhabricatorWorkerActiveTask())->load($this->id);
if (!$task) {
$task = id(new PhabricatorWorkerArchiveTask())->load($this->id);
}
if (!$task) {
$title = pht('Task Does Not Exist');
$error_view = new AphrontErrorView();
$error_view->setTitle('No Such Task');
$error_view->appendChild(
'<p>This task may have recently been garbage collected.</p>');
$error_view->setSeverity(AphrontErrorView::SEVERITY_NODATA);
$content = $error_view;
} else {
$title = 'Task '.$task->getID();
$header = id(new PhabricatorHeaderView())
->setHeader('Task '.$task->getID().' ('.$task->getTaskClass().')');
$actions = $this->buildActionListView($task);
$properties = $this->buildPropertyListView($task);
$content = array(
$header,
$actions,
$properties,
);
}
$nav = $this->buildSideNavView();
$nav->selectFilter('');
$nav->appendChild($content);
return $this->buildApplicationPage(
$nav,
array(
'title' => $title,
));
}
private function buildActionListView(PhabricatorWorkerTask $task) {
$user = $this->getRequest()->getUser();
$view = new PhabricatorActionListView();
$view->setUser($user);
$id = $task->getID();
if ($task->isArchived()) {
$result_success = PhabricatorWorkerArchiveTask::RESULT_SUCCESS;
$can_retry = ($task->getResult() != $result_success);
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Retry Task'))
->setHref($this->getApplicationURI('/task/'.$id.'/retry/'))
->setIcon('undo')
->setWorkflow(true)
->setDisabled(!$can_retry));
} else {
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Cancel Task'))
->setHref($this->getApplicationURI('/task/'.$id.'/cancel/'))
->setIcon('delete')
->setWorkflow(true));
}
$can_release = (!$task->isArchived()) &&
($task->getLeaseOwner());
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Free Lease'))
->setHref($this->getApplicationURI('/task/'.$id.'/release/'))
->setIcon('unlock')
->setWorkflow(true)
->setDisabled(!$can_release));
return $view;
}
private function buildPropertyListView(PhabricatorWorkerTask $task) {
$view = new PhabricatorPropertyListView();
if ($task->isArchived()) {
switch ($task->getResult()) {
case PhabricatorWorkerArchiveTask::RESULT_SUCCESS:
$status = pht('Complete');
break;
case PhabricatorWorkerArchiveTask::RESULT_FAILURE:
$status = pht('Failed');
break;
case PhabricatorWorkerArchiveTask::RESULT_CANCELLED:
$status = pht('Cancelled');
break;
default:
throw new Exception("Unknown task status!");
}
} else {
$status = pht('Queued');
}
$view->addProperty(
pht('Task Status'),
$status);
$view->addProperty(
pht('Task Class'),
phutil_escape_html($task->getTaskClass()));
if ($task->getLeaseExpires()) {
if ($task->getLeaseExpires() > time()) {
$lease_status = pht('Leased');
} else {
$lease_status = pht('Lease Expired');
}
} else {
$lease_status = '<em>'.pht('Not Leased').'</em>';
}
$view->addProperty(
pht('Lease Status'),
$lease_status);
$view->addProperty(
pht('Lease Owner'),
$task->getLeaseOwner()
? phutil_escape_html($task->getLeaseOwner())
: '<em>'.pht('None').'</em>');
if ($task->getLeaseExpires() && $task->getLeaseOwner()) {
$expires = ($task->getLeaseExpires() - time());
$expires = phabricator_format_relative_time_detailed($expires);
} else {
$expires = '<em>'.pht('None').'</em>';
}
$view->addProperty(
pht('Lease Expires'),
$expires);
$view->addProperty(
pht('Failure Count'),
phutil_escape_html($task->getFailureCount()));
if ($task->isArchived()) {
$duration = phutil_escape_html(number_format($task->getDuration()).' us');
} else {
$duration = '<em>'.pht('Not Completed').'</em>';
}
$view->addProperty(
pht('Duration'),
$duration);
return $view;
}
}
diff --git a/src/applications/daemon/controller/PhabricatorWorkerTaskUpdateController.php b/src/applications/daemon/controller/PhabricatorWorkerTaskUpdateController.php
index 0356473760..72f9e256aa 100644
--- a/src/applications/daemon/controller/PhabricatorWorkerTaskUpdateController.php
+++ b/src/applications/daemon/controller/PhabricatorWorkerTaskUpdateController.php
@@ -1,137 +1,121 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorWorkerTaskUpdateController
extends PhabricatorDaemonController {
private $id;
private $action;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
$this->action = $data['action'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$task = id(new PhabricatorWorkerActiveTask())->load($this->id);
if (!$task) {
$task = id(new PhabricatorWorkerArchiveTask())->load($this->id);
}
if (!$task) {
return new Aphront404Response();
}
$result_success = PhabricatorWorkerArchiveTask::RESULT_SUCCESS;
$can_retry = ($task->isArchived()) &&
($task->getResult() != $result_success);
$can_cancel = !$task->isArchived();
$can_release = (!$task->isArchived()) &&
($task->getLeaseOwner());
$next_uri = $this->getApplicationURI('/task/'.$task->getID().'/');
if ($request->isFormPost()) {
switch ($this->action) {
case 'retry':
if ($can_retry) {
$task->unarchiveTask();
}
break;
case 'cancel':
if ($can_cancel) {
// Forcibly break the lease if one exists, so we can archive the
// task.
$task->setLeaseOwner(null);
$task->setLeaseExpires(time());
$task->archiveTask(
PhabricatorWorkerArchiveTask::RESULT_CANCELLED,
0);
}
break;
case 'release':
if ($can_release) {
$task->setLeaseOwner(null);
$task->setLeaseExpires(time());
$task->save();
}
break;
}
return id(new AphrontRedirectResponse())
->setURI($next_uri);
}
$dialog = new AphrontDialogView();
$dialog->setUser($user);
switch ($this->action) {
case 'retry':
if ($can_retry) {
$dialog->setTitle('Really retry task?');
$dialog->appendChild(
'<p>The task will be put back in the queue and executed '.
'again.</p>');
$dialog->addSubmitButton('Retry Task');
} else {
$dialog->setTitle('Can Not Retry');
$dialog->appendChild(
'<p>Only archived, unsuccessful tasks can be retried.</p>');
}
break;
case 'cancel':
if ($can_cancel) {
$dialog->setTitle('Really cancel task?');
$dialog->appendChild(
'<p>The work this task represents will never be performed if you '.
'cancel it. Are you sure you want to cancel it?</p>');
$dialog->addSubmitButton('Cancel Task');
} else {
$dialog->setTitle('Can Not Cancel');
$dialog->appendChild(
'<p>Only active tasks can be cancelled.</p>');
}
break;
case 'release':
if ($can_release) {
$dialog->setTitle('Really free task lease?');
$dialog->appendChild(
'<p>If the process which owns the task lease is still doing work '.
'on it, the work may be performed twice. Are you sure you '.
'want to free the lease?</p>');
$dialog->addSubmitButton('Free Lease');
} else {
$dialog->setTitle('Can Not Free Lease');
$dialog->appendChild(
'<p>Only active, leased tasks may have their leases freed.</p>');
}
break;
default:
return new Aphront404Response();
}
$dialog->addCancelButton($next_uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/daemon/view/PhabricatorDaemonLogEventsView.php b/src/applications/daemon/view/PhabricatorDaemonLogEventsView.php
index ba55442742..bb50fc67a4 100644
--- a/src/applications/daemon/view/PhabricatorDaemonLogEventsView.php
+++ b/src/applications/daemon/view/PhabricatorDaemonLogEventsView.php
@@ -1,131 +1,115 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorDaemonLogEventsView extends AphrontView {
private $events;
private $combinedLog;
private $user;
public function setEvents(array $events) {
assert_instances_of($events, 'PhabricatorDaemonLogEvent');
$this->events = $events;
return $this;
}
public function setCombinedLog($is_combined) {
$this->combinedLog = $is_combined;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function render() {
$rows = array();
if (!$this->user) {
throw new Exception("Call setUser() before rendering!");
}
foreach ($this->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", '<br />', phutil_escape_html($message.$more)),
);
if ($this->combinedLog) {
array_unshift(
$row,
phutil_render_tag(
'a',
array(
'href' => '/daemon/log/'.$event->getLogID().'/',
),
phutil_escape_html('Daemon '.$event->getLogID())));
}
$rows[] = $row;
}
$classes = array(
'',
'',
'right',
'wide wrap',
);
$headers = array(
'Type',
'Date',
'Time',
'Message',
);
if ($this->combinedLog) {
array_unshift($classes, 'pri');
array_unshift($headers, 'Daemon');
}
$log_table = new AphrontTableView($rows);
$log_table->setHeaders($headers);
$log_table->setColumnClasses($classes);
return $log_table->render();
}
}
diff --git a/src/applications/daemon/view/PhabricatorDaemonLogListView.php b/src/applications/daemon/view/PhabricatorDaemonLogListView.php
index 03a04de53e..4202908189 100644
--- a/src/applications/daemon/view/PhabricatorDaemonLogListView.php
+++ b/src/applications/daemon/view/PhabricatorDaemonLogListView.php
@@ -1,141 +1,125 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorDaemonLogListView extends AphrontView {
private $daemonLogs;
private $user;
public function setDaemonLogs(array $daemon_logs) {
assert_instances_of($daemon_logs, 'PhabricatorDaemonLog');
$this->daemonLogs = $daemon_logs;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function render() {
$rows = array();
if (!$this->user) {
throw new Exception("Call setUser() before rendering!");
}
foreach ($this->daemonLogs as $log) {
$epoch = $log->getDateCreated();
$status = $log->getStatus();
if ($log->getHost() == php_uname('n') &&
$status != PhabricatorDaemonLog::STATUS_EXITED &&
$status != PhabricatorDaemonLog::STATUS_DEAD) {
$pid = $log->getPID();
$is_running = PhabricatorDaemonReference::isProcessRunning($pid);
if (!$is_running) {
$guard = AphrontWriteGuard::beginScopedUnguardedWrites();
$log->setStatus(PhabricatorDaemonLog::STATUS_DEAD);
$log->save();
unset($guard);
$status = PhabricatorDaemonLog::STATUS_DEAD;
}
}
$heartbeat_timeout =
$log->getDateModified() + 3 * PhutilDaemonOverseer::HEARTBEAT_WAIT;
if ($status == PhabricatorDaemonLog::STATUS_RUNNING &&
$heartbeat_timeout < time()) {
$status = PhabricatorDaemonLog::STATUS_UNKNOWN;
}
switch ($status) {
case PhabricatorDaemonLog::STATUS_RUNNING:
$style = 'color: #00cc00';
$title = 'Running';
$symbol = '&bull;';
break;
case PhabricatorDaemonLog::STATUS_DEAD:
$style = 'color: #cc0000';
$title = 'Died';
$symbol = '&bull;';
break;
case PhabricatorDaemonLog::STATUS_EXITED:
$style = 'color: #000000';
$title = 'Exited';
$symbol = '&bull;';
break;
case PhabricatorDaemonLog::STATUS_UNKNOWN:
default: // fallthrough
$style = 'color: #888888';
$title = 'Unknown';
$symbol = '?';
}
$running = phutil_render_tag(
'span',
array(
'style' => $style,
'title' => $title,
),
$symbol);
$rows[] = array(
$running,
phutil_escape_html($log->getDaemon()),
phutil_escape_html($log->getHost()),
$log->getPID(),
phabricator_date($epoch, $this->user),
phabricator_time($epoch, $this->user),
phutil_render_tag(
'a',
array(
'href' => '/daemon/log/'.$log->getID().'/',
'class' => 'button small grey',
),
'View Log'),
);
}
$daemon_table = new AphrontTableView($rows);
$daemon_table->setHeaders(
array(
'',
'Daemon',
'Host',
'PID',
'Date',
'Time',
'View',
));
$daemon_table->setColumnClasses(
array(
'',
'wide wrap',
'',
'',
'',
'right',
'action',
));
return $daemon_table->render();
}
}
diff --git a/src/applications/differential/DifferentialReplyHandler.php b/src/applications/differential/DifferentialReplyHandler.php
index 50b2c4099d..d0b8379ea7 100644
--- a/src/applications/differential/DifferentialReplyHandler.php
+++ b/src/applications/differential/DifferentialReplyHandler.php
@@ -1,190 +1,174 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
class DifferentialReplyHandler extends PhabricatorMailReplyHandler {
private $receivedMail;
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof DifferentialRevision)) {
throw new Exception("Receiver is not a DifferentialRevision!");
}
}
public function getPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle) {
return $this->getDefaultPrivateReplyHandlerEmailAddress($handle, 'D');
}
public function getPublicReplyHandlerEmailAddress() {
return $this->getDefaultPublicReplyHandlerEmailAddress('D');
}
public function getReplyHandlerDomain() {
return PhabricatorEnv::getEnvConfig(
'metamta.differential.reply-handler-domain');
}
/*
* Generate text like the following from the supported commands.
* "
*
* ACTIONS
* Reply to comment, or !accept, !reject, !abandon, !resign, !reclaim.
*
* "
*/
public function getReplyHandlerInstructions() {
if (!$this->supportsReplies()) {
return null;
}
$supported_commands = $this->getSupportedCommands();
$text = '';
if (empty($supported_commands)) {
return $text;
}
$comment_command_printed = false;
if (in_array(DifferentialAction::ACTION_COMMENT, $supported_commands)) {
$text .= 'Reply to comment';
$comment_command_printed = true;
$supported_commands = array_diff(
$supported_commands, array(DifferentialAction::ACTION_COMMENT));
}
if (!empty($supported_commands)) {
if ($comment_command_printed) {
$text .= ', or ';
}
$modified_commands = array();
foreach ($supported_commands as $command) {
$modified_commands[] = '!'.$command;
}
$text .= implode(', ', $modified_commands);
}
$text .= ".";
return $text;
}
public function getSupportedCommands() {
$actions = array(
DifferentialAction::ACTION_COMMENT,
DifferentialAction::ACTION_REJECT,
DifferentialAction::ACTION_ABANDON,
DifferentialAction::ACTION_RECLAIM,
DifferentialAction::ACTION_RESIGN,
DifferentialAction::ACTION_RETHINK,
'unsubscribe',
);
if (PhabricatorEnv::getEnvConfig('differential.enable-email-accept')) {
$actions[] = DifferentialAction::ACTION_ACCEPT;
}
return $actions;
}
protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
$this->receivedMail = $mail;
$this->handleAction($mail->getCleanTextBody(), $mail->getAttachments());
}
public function handleAction($body, array $attachments) {
// all commands start with a bang and separated from the body by a newline
// to make sure that actual feedback text couldn't trigger an action.
// unrecognized commands will be parsed as part of the comment.
$command = DifferentialAction::ACTION_COMMENT;
$supported_commands = $this->getSupportedCommands();
$regex = "/\A\n*!(" . implode('|', $supported_commands) . ")\n*/";
$matches = array();
if (preg_match($regex, $body, $matches)) {
$command = $matches[1];
$body = trim(str_replace('!' . $command, '', $body));
}
$actor = $this->getActor();
if (!$actor) {
throw new Exception('No actor is set for the reply action.');
}
switch ($command) {
case 'unsubscribe':
$this->unsubscribeUser($this->getMailReceiver(), $actor);
// TODO: Send the user a confirmation email?
return null;
}
$body = $this->enhanceBodyWithAttachments($body, $attachments);
try {
$editor = new DifferentialCommentEditor(
$this->getMailReceiver(),
$command);
$editor->setActor($actor);
$editor->setExcludeMailRecipientPHIDs(
$this->getExcludeMailRecipientPHIDs());
// NOTE: We have to be careful about this because Facebook's
// implementation jumps straight into handleAction() and will not have
// a PhabricatorMetaMTAReceivedMail object.
if ($this->receivedMail) {
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_EMAIL,
array(
'id' => $this->receivedMail->getID(),
));
$editor->setContentSource($content_source);
$editor->setParentMessageID($this->receivedMail->getMessageID());
}
$editor->setMessage($body);
$comment = $editor->save();
return $comment->getID();
} catch (Exception $ex) {
$exception_mail = new DifferentialExceptionMail(
$this->getMailReceiver(),
$ex,
$this->receivedMail->getRawTextBody());
$exception_mail->setToPHIDs(array($this->getActor()->getPHID()));
$exception_mail->send();
throw $ex;
}
}
private function unsubscribeUser(
DifferentialRevision $revision,
PhabricatorUser $user) {
$revision->loadRelationships();
DifferentialRevisionEditor::removeCCAndUpdateRevision(
$revision,
$user->getPHID(),
$user->getPHID());
}
}
diff --git a/src/applications/differential/DifferentialTasksAttacher.php b/src/applications/differential/DifferentialTasksAttacher.php
index ef3a34b40f..c17e246727 100644
--- a/src/applications/differential/DifferentialTasksAttacher.php
+++ b/src/applications/differential/DifferentialTasksAttacher.php
@@ -1,40 +1,24 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DifferentialTasksAttacher {
/**
* Implementation of this function should attach given tasks to
* the given revision. The function is called when 'arc' has task
* ids defined in the commit message.
*/
abstract public function attachTasksToRevision(
$user_phid,
DifferentialRevision $revision,
array $task_ids);
/**
* This method will be called with a task and its original and new
* associated revisions. Implementation of this method should update
* the affected revisions to maintain the new associations.
*/
abstract public function updateTaskRevisionAssoc(
$task_phid,
array $orig_rev_phids,
array $new_rev_phids);
}
diff --git a/src/applications/differential/application/PhabricatorApplicationDifferential.php b/src/applications/differential/application/PhabricatorApplicationDifferential.php
index 8c3883719c..d5156570ff 100644
--- a/src/applications/differential/application/PhabricatorApplicationDifferential.php
+++ b/src/applications/differential/application/PhabricatorApplicationDifferential.php
@@ -1,115 +1,99 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationDifferential extends PhabricatorApplication {
public function getBaseURI() {
return '/differential/';
}
public function getShortDescription() {
return 'Review Code';
}
public function getAutospriteName() {
return 'differential';
}
public function getHelpURI() {
return PhabricatorEnv::getDoclink('article/Differential_User_Guide.html');
}
public function getFactObjectsForAnalysis() {
return array(
new DifferentialRevision(),
);
}
public function getRoutes() {
return array(
'/D(?P<id>[1-9]\d*)' => 'DifferentialRevisionViewController',
'/differential/' => array(
'' => 'DifferentialRevisionListController',
'filter/(?P<filter>\w+)/(?:(?P<username>[\w\.-_]+)/)?' =>
'DifferentialRevisionListController',
'stats/(?P<filter>\w+)/' => 'DifferentialRevisionStatsController',
'diff/' => array(
'(?P<id>[1-9]\d*)/' => 'DifferentialDiffViewController',
'create/' => 'DifferentialDiffCreateController',
),
'changeset/' => 'DifferentialChangesetViewController',
'revision/edit/(?:(?P<id>[1-9]\d*)/)?'
=> 'DifferentialRevisionEditController',
'comment/' => array(
'preview/(?P<id>[1-9]\d*)/' => 'DifferentialCommentPreviewController',
'save/' => 'DifferentialCommentSaveController',
'inline/' => array(
'preview/(?P<id>[1-9]\d*)/'
=> 'DifferentialInlineCommentPreviewController',
'edit/(?P<id>[1-9]\d*)/'
=> 'DifferentialInlineCommentEditController',
),
),
'subscribe/(?P<action>add|rem)/(?P<id>[1-9]\d*)/'
=> 'DifferentialSubscribeController',
),
);
}
public function getApplicationGroup() {
return self::GROUP_CORE;
}
public function getApplicationOrder() {
return 0.100;
}
public function loadStatus(PhabricatorUser $user) {
$revisions = id(new DifferentialRevisionQuery())
->withResponsibleUsers(array($user->getPHID()))
->withStatus(DifferentialRevisionQuery::STATUS_OPEN)
->execute();
list($active, $waiting) = DifferentialRevisionQuery::splitResponsible(
$revisions,
$user->getPHID());
$status = array();
$active = count($active);
$type = $active
? PhabricatorApplicationStatusView::TYPE_NEEDS_ATTENTION
: PhabricatorApplicationStatusView::TYPE_EMPTY;
$status[] = id(new PhabricatorApplicationStatusView())
->setType($type)
->setText(pht('%d Review(s) Need Attention', $active))
->setCount($active);
$waiting = count($waiting);
$type = $waiting
? PhabricatorApplicationStatusView::TYPE_INFO
: PhabricatorApplicationStatusView::TYPE_EMPTY;
$status[] = id(new PhabricatorApplicationStatusView())
->setType($type)
->setText(pht('%d Review(s) Waiting on Others', $waiting));
return $status;
}
}
diff --git a/src/applications/differential/constants/DifferentialAction.php b/src/applications/differential/constants/DifferentialAction.php
index ddca9f26a7..404a551d8c 100644
--- a/src/applications/differential/constants/DifferentialAction.php
+++ b/src/applications/differential/constants/DifferentialAction.php
@@ -1,96 +1,80 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialAction {
const ACTION_CLOSE = 'commit';
const ACTION_COMMENT = 'none';
const ACTION_ACCEPT = 'accept';
const ACTION_REJECT = 'reject';
const ACTION_RETHINK = 'rethink';
const ACTION_ABANDON = 'abandon';
const ACTION_REQUEST = 'request_review';
const ACTION_RECLAIM = 'reclaim';
const ACTION_UPDATE = 'update';
const ACTION_RESIGN = 'resign';
const ACTION_SUMMARIZE = 'summarize';
const ACTION_TESTPLAN = 'testplan';
const ACTION_CREATE = 'create';
const ACTION_ADDREVIEWERS = 'add_reviewers';
const ACTION_ADDCCS = 'add_ccs';
const ACTION_CLAIM = 'claim';
public static function getActionPastTenseVerb($action) {
$verbs = array(
self::ACTION_COMMENT => 'commented on',
self::ACTION_ACCEPT => 'accepted',
self::ACTION_REJECT => 'requested changes to',
self::ACTION_RETHINK => 'planned changes to',
self::ACTION_ABANDON => 'abandoned',
self::ACTION_CLOSE => pht('closed'),
self::ACTION_REQUEST => 'requested a review of',
self::ACTION_RECLAIM => 'reclaimed',
self::ACTION_UPDATE => 'updated',
self::ACTION_RESIGN => 'resigned from',
self::ACTION_SUMMARIZE => 'summarized',
self::ACTION_TESTPLAN => 'explained the test plan for',
self::ACTION_CREATE => 'created',
self::ACTION_ADDREVIEWERS => 'added reviewers to',
self::ACTION_ADDCCS => 'added CCs to',
self::ACTION_CLAIM => 'commandeered',
);
if (!empty($verbs[$action])) {
return $verbs[$action];
} else {
return 'brazenly "'.$action.'ed"';
}
}
public static function getActionVerb($action) {
static $verbs = array(
self::ACTION_COMMENT => 'Comment',
self::ACTION_ACCEPT => "Accept Revision \xE2\x9C\x94",
self::ACTION_REJECT => "Request Changes \xE2\x9C\x98",
self::ACTION_RETHINK => "Plan Changes \xE2\x9C\x98",
self::ACTION_ABANDON => 'Abandon Revision',
self::ACTION_REQUEST => 'Request Review',
self::ACTION_RECLAIM => 'Reclaim Revision',
self::ACTION_RESIGN => 'Resign as Reviewer',
self::ACTION_ADDREVIEWERS => 'Add Reviewers',
self::ACTION_ADDCCS => 'Add CCs',
self::ACTION_CLOSE => 'Close Revision',
self::ACTION_CLAIM => 'Commandeer Revision',
);
if (!empty($verbs[$action])) {
return $verbs[$action];
} else {
return 'brazenly '.$action;
}
}
public static function allowReviewers($action) {
if ($action == DifferentialAction::ACTION_ADDREVIEWERS ||
$action == DifferentialAction::ACTION_REQUEST) {
return true;
}
return false;
}
}
diff --git a/src/applications/differential/constants/DifferentialChangeType.php b/src/applications/differential/constants/DifferentialChangeType.php
index e9951b88c4..078b9bd3a9 100644
--- a/src/applications/differential/constants/DifferentialChangeType.php
+++ b/src/applications/differential/constants/DifferentialChangeType.php
@@ -1,127 +1,111 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialChangeType {
const TYPE_ADD = 1;
const TYPE_CHANGE = 2;
const TYPE_DELETE = 3;
const TYPE_MOVE_AWAY = 4;
const TYPE_COPY_AWAY = 5;
const TYPE_MOVE_HERE = 6;
const TYPE_COPY_HERE = 7;
const TYPE_MULTICOPY = 8;
const TYPE_MESSAGE = 9;
const TYPE_CHILD = 10;
const FILE_TEXT = 1;
const FILE_IMAGE = 2;
const FILE_BINARY = 3;
const FILE_DIRECTORY = 4;
const FILE_SYMLINK = 5;
const FILE_DELETED = 6;
const FILE_NORMAL = 7;
const FILE_SUBMODULE = 8;
public static function getSummaryCharacterForChangeType($type) {
static $types = array(
self::TYPE_ADD => 'A',
self::TYPE_CHANGE => 'M',
self::TYPE_DELETE => 'D',
self::TYPE_MOVE_AWAY => 'V',
self::TYPE_COPY_AWAY => 'P',
self::TYPE_MOVE_HERE => 'V',
self::TYPE_COPY_HERE => 'P',
self::TYPE_MULTICOPY => 'P',
self::TYPE_MESSAGE => 'Q',
self::TYPE_CHILD => '@',
);
return idx($types, coalesce($type, '?'), '~');
}
public static function getShortNameForFileType($type) {
static $names = array(
self::FILE_TEXT => null,
self::FILE_DIRECTORY => 'dir',
self::FILE_IMAGE => 'img',
self::FILE_BINARY => 'bin',
self::FILE_SYMLINK => 'sym',
self::FILE_SUBMODULE => 'sub',
);
return idx($names, coalesce($type, '?'), '???');
}
public static function isOldLocationChangeType($type) {
static $types = array(
DifferentialChangeType::TYPE_MOVE_AWAY => true,
DifferentialChangeType::TYPE_COPY_AWAY => true,
DifferentialChangeType::TYPE_MULTICOPY => true,
);
return isset($types[$type]);
}
public static function isNewLocationChangeType($type) {
static $types = array(
DifferentialChangeType::TYPE_MOVE_HERE => true,
DifferentialChangeType::TYPE_COPY_HERE => true,
);
return isset($types[$type]);
}
public static function isDeleteChangeType($type) {
static $types = array(
DifferentialChangeType::TYPE_DELETE => true,
DifferentialChangeType::TYPE_MOVE_AWAY => true,
DifferentialChangeType::TYPE_MULTICOPY => true,
);
return isset($types[$type]);
}
public static function isCreateChangeType($type) {
static $types = array(
DifferentialChangeType::TYPE_ADD => true,
DifferentialChangeType::TYPE_COPY_HERE => true,
DifferentialChangeType::TYPE_MOVE_HERE => true,
);
return isset($types[$type]);
}
public static function isModifyChangeType($type) {
static $types = array(
DifferentialChangeType::TYPE_CHANGE => true,
);
return isset($types[$type]);
}
public static function getFullNameForChangeType($type) {
static $types = array(
self::TYPE_ADD => 'Added',
self::TYPE_CHANGE => 'Modified',
self::TYPE_DELETE => 'Deleted',
self::TYPE_MOVE_AWAY => 'Moved Away',
self::TYPE_COPY_AWAY => 'Copied Away',
self::TYPE_MOVE_HERE => 'Moved Here',
self::TYPE_COPY_HERE => 'Copied Here',
self::TYPE_MULTICOPY => 'Deleted After Multiple Copy',
self::TYPE_MESSAGE => 'Commit Message',
self::TYPE_CHILD => 'Contents Modified',
);
return idx($types, coalesce($type, '?'), 'Unknown');
}
}
diff --git a/src/applications/differential/constants/DifferentialLintStatus.php b/src/applications/differential/constants/DifferentialLintStatus.php
index 01ca2d03fd..2a519adc6c 100644
--- a/src/applications/differential/constants/DifferentialLintStatus.php
+++ b/src/applications/differential/constants/DifferentialLintStatus.php
@@ -1,28 +1,12 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialLintStatus {
const LINT_NONE = 0;
const LINT_OKAY = 1;
const LINT_WARN = 2;
const LINT_FAIL = 3;
const LINT_SKIP = 4;
const LINT_POSTPONED = 5;
}
diff --git a/src/applications/differential/constants/DifferentialMailPhase.php b/src/applications/differential/constants/DifferentialMailPhase.php
index b8c80b00b9..dbf370e0b3 100644
--- a/src/applications/differential/constants/DifferentialMailPhase.php
+++ b/src/applications/differential/constants/DifferentialMailPhase.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialMailPhase {
const WELCOME = 1;
const UPDATE = 2;
const COMMENT = 3;
}
diff --git a/src/applications/differential/constants/DifferentialRevisionControlSystem.php b/src/applications/differential/constants/DifferentialRevisionControlSystem.php
index c4b68160a7..6e819ec3eb 100644
--- a/src/applications/differential/constants/DifferentialRevisionControlSystem.php
+++ b/src/applications/differential/constants/DifferentialRevisionControlSystem.php
@@ -1,26 +1,10 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
// TODO: Unify with similar Repository constants
final class DifferentialRevisionControlSystem {
const SVN = 'svn';
const GIT = 'git';
const MERCURIAL = 'hg';
}
diff --git a/src/applications/differential/constants/DifferentialUnitStatus.php b/src/applications/differential/constants/DifferentialUnitStatus.php
index ced0dc0498..6732d71205 100644
--- a/src/applications/differential/constants/DifferentialUnitStatus.php
+++ b/src/applications/differential/constants/DifferentialUnitStatus.php
@@ -1,28 +1,12 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialUnitStatus {
const UNIT_NONE = 0;
const UNIT_OKAY = 1;
const UNIT_WARN = 2;
const UNIT_FAIL = 3;
const UNIT_SKIP = 4;
const UNIT_POSTPONED = 5;
}
diff --git a/src/applications/differential/constants/DifferentialUnitTestResult.php b/src/applications/differential/constants/DifferentialUnitTestResult.php
index dddb4ec5bf..70ff89cca7 100644
--- a/src/applications/differential/constants/DifferentialUnitTestResult.php
+++ b/src/applications/differential/constants/DifferentialUnitTestResult.php
@@ -1,28 +1,12 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialUnitTestResult {
const RESULT_PASS = 'pass';
const RESULT_FAIL = 'fail';
const RESULT_SKIP = 'skip';
const RESULT_BROKEN = 'broken';
const RESULT_UNSOUND = 'unsound';
const RESULT_POSTPONED = 'postponed';
}
diff --git a/src/applications/differential/controller/DifferentialChangesetViewController.php b/src/applications/differential/controller/DifferentialChangesetViewController.php
index 39d49d6d6c..ce83be9cbc 100644
--- a/src/applications/differential/controller/DifferentialChangesetViewController.php
+++ b/src/applications/differential/controller/DifferentialChangesetViewController.php
@@ -1,364 +1,348 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialChangesetViewController extends DifferentialController {
public function shouldRequireLogin() {
return !$this->allowsAnonymousAccess();
}
public function processRequest() {
$request = $this->getRequest();
$author_phid = $request->getUser()->getPHID();
$rendering_reference = $request->getStr('ref');
$parts = explode('/', $rendering_reference);
if (count($parts) == 2) {
list($id, $vs) = $parts;
} else {
$id = $parts[0];
$vs = 0;
}
$id = (int)$id;
$vs = (int)$vs;
$changeset = id(new DifferentialChangeset())->load($id);
if (!$changeset) {
return new Aphront404Response();
}
$view = $request->getStr('view');
if ($view) {
$changeset->attachHunks($changeset->loadHunks());
$phid = idx($changeset->getMetadata(), "$view:binary-phid");
if ($phid) {
return id(new AphrontRedirectResponse())->setURI("/file/info/$phid/");
}
switch ($view) {
case 'new':
return $this->buildRawFileResponse($changeset, $is_new = true);
case 'old':
if ($vs && ($vs != -1)) {
$vs_changeset = id(new DifferentialChangeset())->load($vs);
if ($vs_changeset) {
$vs_changeset->attachHunks($vs_changeset->loadHunks());
return $this->buildRawFileResponse($vs_changeset, $is_new = true);
}
}
return $this->buildRawFileResponse($changeset, $is_new = false);
default:
return new Aphront400Response();
}
}
if ($vs && ($vs != -1)) {
$vs_changeset = id(new DifferentialChangeset())->load($vs);
if (!$vs_changeset) {
return new Aphront404Response();
}
}
if (!$vs) {
$right = $changeset;
$left = null;
$right_source = $right->getID();
$right_new = true;
$left_source = $right->getID();
$left_new = false;
$render_cache_key = $right->getID();
} else if ($vs == -1) {
$right = null;
$left = $changeset;
$right_source = $left->getID();
$right_new = false;
$left_source = $left->getID();
$left_new = true;
$render_cache_key = null;
} else {
$right = $changeset;
$left = $vs_changeset;
$right_source = $right->getID();
$right_new = true;
$left_source = $left->getID();
$left_new = true;
$render_cache_key = null;
}
if ($left) {
$left->attachHunks($left->loadHunks());
}
if ($right) {
$right->attachHunks($right->loadHunks());
}
if ($left) {
$left_data = $left->makeNewFile();
if ($right) {
$right_data = $right->makeNewFile();
} else {
$right_data = $left->makeOldFile();
}
$engine = new PhabricatorDifferenceEngine();
$synthetic = $engine->generateChangesetFromFileContent(
$left_data,
$right_data);
$choice = clone nonempty($left, $right);
$choice->attachHunks($synthetic->getHunks());
$changeset = $choice;
}
$coverage = null;
if ($right && $right->getDiffID()) {
$unit = id(new DifferentialDiffProperty())->loadOneWhere(
'diffID = %d AND name = %s',
$right->getDiffID(),
'arc:unit');
if ($unit) {
$coverage = array();
foreach ($unit->getData() as $result) {
$result_coverage = idx($result, 'coverage');
if (!$result_coverage) {
continue;
}
$file_coverage = idx($result_coverage, $right->getFileName());
if (!$file_coverage) {
continue;
}
$coverage[] = $file_coverage;
}
$coverage = ArcanistUnitTestResult::mergeCoverage($coverage);
}
}
$spec = $request->getStr('range');
list($range_s, $range_e, $mask) =
DifferentialChangesetParser::parseRangeSpecification($spec);
$parser = new DifferentialChangesetParser();
$parser->setCoverage($coverage);
$parser->setChangeset($changeset);
$parser->setRenderingReference($rendering_reference);
$parser->setRenderCacheKey($render_cache_key);
$parser->setRightSideCommentMapping($right_source, $right_new);
$parser->setLeftSideCommentMapping($left_source, $left_new);
$parser->setWhitespaceMode($request->getStr('whitespace'));
if ($left && $right) {
$parser->setOriginals($left, $right);
}
// Load both left-side and right-side inline comments.
$inlines = $this->loadInlineComments(
array($left_source, $right_source),
$author_phid);
if ($left_new) {
$inlines = array_merge(
$inlines,
$this->buildLintInlineComments($left));
}
if ($right_new) {
$inlines = array_merge(
$inlines,
$this->buildLintInlineComments($right));
}
$phids = array();
foreach ($inlines as $inline) {
$parser->parseInlineComment($inline);
if ($inline->getAuthorPHID()) {
$phids[$inline->getAuthorPHID()] = true;
}
}
$phids = array_keys($phids);
$handles = $this->loadViewerHandles($phids);
$parser->setHandles($handles);
$engine = new PhabricatorMarkupEngine();
$engine->setViewer($request->getUser());
foreach ($inlines as $inline) {
$engine->addObject(
$inline,
PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY);
}
$engine->process();
$parser->setMarkupEngine($engine);
if ($request->isAjax()) {
// TODO: This is sort of lazy, the effect is just to not render "Edit"
// and "Reply" links on the "standalone view".
$parser->setUser($request->getUser());
}
$output = $parser->render($range_s, $range_e, $mask);
$mcov = $parser->renderModifiedCoverage();
if ($request->isAjax()) {
$coverage = array(
'differential-mcoverage-'.md5($changeset->getFilename()) => $mcov,
);
return id(new PhabricatorChangesetResponse())
->setRenderedChangeset($output)
->setCoverage($coverage);
}
Javelin::initBehavior('differential-show-more', array(
'uri' => '/differential/changeset/',
'whitespace' => $request->getStr('whitespace'),
));
Javelin::initBehavior('differential-comment-jump', array());
$detail = new DifferentialChangesetDetailView();
$detail->setChangeset($changeset);
$detail->appendChild($output);
$detail->setVsChangesetID($left_source);
$panel = id(new DifferentialPrimaryPaneView())
->setLineWidthFromChangesets(array($changeset));
$panel->appendChild(phutil_render_tag('div',
array(
'class' => 'differential-review-stage',
'id' => 'differential-review-stage',
'style' => "max-width: {$panel->calculateSideBySideWidth()}px;"
), $detail->render())
);
return $this->buildStandardPageResponse(
array(
$panel
),
array(
'title' => 'Changeset View',
));
}
private function loadInlineComments(array $changeset_ids, $author_phid) {
$changeset_ids = array_unique(array_filter($changeset_ids));
if (!$changeset_ids) {
return;
}
return id(new DifferentialInlineComment())->loadAllWhere(
'changesetID IN (%Ld) AND (commentID IS NOT NULL OR authorPHID = %s)',
$changeset_ids,
$author_phid);
}
private function buildRawFileResponse(
DifferentialChangeset $changeset,
$is_new) {
if ($is_new) {
$key = 'raw:new:phid';
} else {
$key = 'raw:old:phid';
}
$metadata = $changeset->getMetadata();
$file = null;
$phid = idx($metadata, $key);
if ($phid) {
$file = id(new PhabricatorFile())->loadOneWhere(
'phid = %s',
$phid);
}
if (!$file) {
// This is just building a cache of the changeset content in the file
// tool, and is safe to run on a read pathway.
$unguard = AphrontWriteGuard::beginScopedUnguardedWrites();
if ($is_new) {
$data = $changeset->makeNewFile();
} else {
$data = $changeset->makeOldFile();
}
$file = PhabricatorFile::newFromFileData(
$data,
array(
'name' => $changeset->getFilename(),
'mime-type' => 'text/plain',
));
$metadata[$key] = $file->getPHID();
$changeset->setMetadata($metadata);
$changeset->save();
unset($unguard);
}
return id(new AphrontRedirectResponse())
->setURI($file->getBestURI());
}
private function buildLintInlineComments($changeset) {
$lint = id(new DifferentialDiffProperty())->loadOneWhere(
'diffID = %d AND name = %s',
$changeset->getDiffID(),
'arc:lint');
if (!$lint) {
return array();
}
$lint = $lint->getData();
$inlines = array();
foreach ($lint as $msg) {
if ($msg['path'] != $changeset->getFilename()) {
continue;
}
$inline = new DifferentialInlineComment();
$inline->setChangesetID($changeset->getID());
$inline->setIsNewFile(true);
$inline->setSyntheticAuthor('Lint: '.$msg['name']);
$inline->setLineNumber($msg['line']);
$inline->setLineLength(0);
$inline->setContent('%%%'.$msg['description'].'%%%');
$inlines[] = $inline;
}
return $inlines;
}
}
diff --git a/src/applications/differential/controller/DifferentialCommentPreviewController.php b/src/applications/differential/controller/DifferentialCommentPreviewController.php
index b8e0dc02a5..cce6b36fcb 100644
--- a/src/applications/differential/controller/DifferentialCommentPreviewController.php
+++ b/src/applications/differential/controller/DifferentialCommentPreviewController.php
@@ -1,92 +1,76 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialCommentPreviewController
extends DifferentialController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$author_phid = $request->getUser()->getPHID();
$action = $request->getStr('action');
$comment = new DifferentialComment();
$comment->setContent($request->getStr('content'));
$comment->setAction($action);
$comment->setAuthorPHID($author_phid);
$handles = array($author_phid);
$reviewers = $request->getStrList('reviewers');
if (DifferentialAction::allowReviewers($action) && $reviewers) {
$comment->setMetadata(array(
DifferentialComment::METADATA_ADDED_REVIEWERS => $reviewers));
$handles = array_merge($handles, $reviewers);
}
$ccs = $request->getStrList('ccs');
if ($action == DifferentialAction::ACTION_ADDCCS && $ccs) {
$comment->setMetadata(array(
DifferentialComment::METADATA_ADDED_CCS => $ccs));
$handles = array_merge($handles, $ccs);
}
$handles = $this->loadViewerHandles($handles);
$engine = new PhabricatorMarkupEngine();
$engine->setViewer($request->getUser());
$engine->addObject($comment, DifferentialComment::MARKUP_FIELD_BODY);
$engine->process();
$view = new DifferentialRevisionCommentView();
$view->setUser($request->getUser());
$view->setComment($comment);
$view->setHandles($handles);
$view->setMarkupEngine($engine);
$view->setPreview(true);
$view->setTargetDiff(null);
$metadata = array(
'reviewers' => $reviewers,
'ccs' => $ccs,
);
if ($action != DifferentialAction::ACTION_COMMENT) {
$metadata['action'] = $action;
}
id(new PhabricatorDraft())
->setAuthorPHID($author_phid)
->setDraftKey('differential-comment-'.$this->id)
->setDraft($comment->getContent())
->setMetadata($metadata)
->replaceOrDelete();
return id(new AphrontAjaxResponse())
->setContent($view->render());
}
}
diff --git a/src/applications/differential/controller/DifferentialCommentSaveController.php b/src/applications/differential/controller/DifferentialCommentSaveController.php
index f5af856df9..ff92e55b65 100644
--- a/src/applications/differential/controller/DifferentialCommentSaveController.php
+++ b/src/applications/differential/controller/DifferentialCommentSaveController.php
@@ -1,102 +1,86 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialCommentSaveController extends DifferentialController {
public function processRequest() {
$request = $this->getRequest();
if (!$request->isFormPost()) {
return new Aphront400Response();
}
$revision_id = $request->getInt('revision_id');
$revision = id(new DifferentialRevision())->load($revision_id);
if (!$revision) {
return new Aphront400Response();
}
$comment = $request->getStr('comment');
$action = $request->getStr('action');
$reviewers = $request->getArr('reviewers');
$ccs = $request->getArr('ccs');
$editor = new DifferentialCommentEditor(
$revision,
$action);
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_WEB,
array(
'ip' => $request->getRemoteAddr(),
));
try {
$editor
->setActor($request->getUser())
->setMessage($comment)
->setContentSource($content_source)
->setAttachInlineComments(true)
->setAddedReviewers($reviewers)
->setAddedCCs($ccs)
->save();
} catch (DifferentialActionHasNoEffectException $no_effect) {
$has_inlines = id(new DifferentialInlineComment())->loadAllWhere(
'authorPHID = %s AND revisionID = %d AND commentID IS NULL',
$request->getUser()->getPHID(),
$revision->getID());
$dialog = new AphrontDialogView();
$dialog->setUser($request->getUser());
$dialog->addCancelButton('/D'.$revision_id);
$dialog->addHiddenInput('revision_id', $revision_id);
$dialog->addHiddenInput('action', 'none');
$dialog->addHiddenInput('reviewers', $reviewers);
$dialog->addHiddenInput('ccs', $ccs);
$dialog->addHiddenInput('comment', $comment);
$dialog->setTitle('Action Has No Effect');
$dialog->appendChild(
'<p>'.phutil_escape_html($no_effect->getMessage()).'</p>');
if (strlen($comment) || $has_inlines) {
$dialog->addSubmitButton('Post as Comment');
$dialog->appendChild('<br />');
$dialog->appendChild(
'<p>Do you want to post your feedback anyway, as a normal '.
'comment?</p>');
}
return id(new AphrontDialogResponse())->setDialog($dialog);
}
// TODO: Diff change detection?
$draft = id(new PhabricatorDraft())->loadOneWhere(
'authorPHID = %s AND draftKey = %s',
$request->getUser()->getPHID(),
'differential-comment-'.$revision->getID());
if ($draft) {
$draft->delete();
}
return id(new AphrontRedirectResponse())
->setURI('/D'.$revision->getID());
}
}
diff --git a/src/applications/differential/controller/DifferentialController.php b/src/applications/differential/controller/DifferentialController.php
index e77c750d05..27f6c447af 100644
--- a/src/applications/differential/controller/DifferentialController.php
+++ b/src/applications/differential/controller/DifferentialController.php
@@ -1,43 +1,27 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DifferentialController extends PhabricatorController {
protected function allowsAnonymousAccess() {
return PhabricatorEnv::getEnvConfig('differential.anonymous-access');
}
public function buildStandardPageResponse($view, array $data) {
require_celerity_resource('differential-core-view-css');
$viewer_is_anonymous = !$this->getRequest()->getUser()->isLoggedIn();
$page = $this->buildStandardPageView();
$page->setApplicationName('Differential');
$page->setBaseURI('/differential/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph("\xE2\x9A\x99");
$page->appendChild($view);
$page->setSearchDefaultScope(PhabricatorSearchScope::SCOPE_OPEN_REVISIONS);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
}
diff --git a/src/applications/differential/controller/DifferentialDiffCreateController.php b/src/applications/differential/controller/DifferentialDiffCreateController.php
index eeda03b398..3450f20203 100644
--- a/src/applications/differential/controller/DifferentialDiffCreateController.php
+++ b/src/applications/differential/controller/DifferentialDiffCreateController.php
@@ -1,90 +1,74 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialDiffCreateController extends DifferentialController {
public function processRequest() {
$request = $this->getRequest();
if ($request->isFormPost()) {
$diff = null;
try {
$diff = PhabricatorFile::readUploadedFileData($_FILES['diff-file']);
} catch (Exception $ex) {
$diff = $request->getStr('diff');
}
$call = new ConduitCall(
'differential.createrawdiff',
array(
'diff' => $diff,
));
$call->setUser($request->getUser());
$result = $call->execute();
$path = id(new PhutilURI($result['uri']))->getPath();
return id(new AphrontRedirectResponse())->setURI($path);
}
$form = new AphrontFormView();
$arcanist_href = PhabricatorEnv::getDoclink(
'article/Arcanist_User_Guide.html');
$arcanist_link = phutil_render_tag(
'a',
array(
'href' => $arcanist_href,
'target' => '_blank',
),
'Arcanist');
$form
->setAction('/differential/diff/create/')
->setEncType('multipart/form-data')
->setUser($request->getUser())
->appendChild(
'<p class="aphront-form-instructions">The best way to create a '.
"Differential diff is by using $arcanist_link, but you ".
'can also just paste a diff (e.g., from <tt>svn diff</tt> or '.
'<tt>git diff</tt>) into this box or upload it as a file if you '.
'really want.</p>')
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Raw Diff')
->setName('diff')
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL))
->appendChild(
id(new AphrontFormFileControl())
->setLabel('Raw Diff from file')
->setName('diff-file'))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue("Create Diff \xC2\xBB"));
$panel = new AphrontPanelView();
$panel->setHeader('Create New Diff');
$panel->appendChild($form);
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
return $this->buildStandardPageResponse(
$panel,
array(
'title' => 'Create Diff',
));
}
}
diff --git a/src/applications/differential/controller/DifferentialDiffViewController.php b/src/applications/differential/controller/DifferentialDiffViewController.php
index a729b5c093..c359e57e75 100644
--- a/src/applications/differential/controller/DifferentialDiffViewController.php
+++ b/src/applications/differential/controller/DifferentialDiffViewController.php
@@ -1,138 +1,122 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialDiffViewController extends DifferentialController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$diff = id(new DifferentialDiff())->load($this->id);
if (!$diff) {
return new Aphront404Response();
}
if ($diff->getRevisionID()) {
$top_panel = new AphrontPanelView();
$top_panel->setWidth(AphrontPanelView::WIDTH_WIDE);
$link = phutil_render_tag(
'a',
array(
'href' => PhabricatorEnv::getURI('/D'.$diff->getRevisionID()),
),
phutil_escape_html('D'.$diff->getRevisionID()));
$top_panel->appendChild("<h1>This diff belongs to revision {$link}</h1>");
} else {
$action_panel = new AphrontPanelView();
$action_panel->setHeader('Preview Diff');
$action_panel->setWidth(AphrontPanelView::WIDTH_WIDE);
$action_panel->appendChild(
'<p class="aphront-panel-instructions">Review the diff for '.
'correctness. When you are satisfied, either <strong>create a new '.
'revision</strong> or <strong>update an existing revision</strong>.');
// TODO: implmenent optgroup support in AphrontFormSelectControl?
$select = array();
$select[] = '<optgroup label="Create New Revision">';
$select[] = '<option value="">Create a new Revision...</option>';
$select[] = '</optgroup>';
$revision_data = new DifferentialRevisionListData(
DifferentialRevisionListData::QUERY_OPEN_OWNED,
array($request->getUser()->getPHID()));
$revisions = $revision_data->loadRevisions();
if ($revisions) {
$select[] = '<optgroup label="Update Existing Revision">';
foreach ($revisions as $revision) {
$select[] = phutil_render_tag(
'option',
array(
'value' => $revision->getID(),
),
phutil_escape_html($revision->getTitle()));
}
$select[] = '</optgroup>';
}
$select =
'<select name="revisionID">'.
implode("\n", $select).
'</select>';
$action_form = new AphrontFormView();
$action_form
->setUser($request->getUser())
->setAction('/differential/revision/edit/')
->addHiddenInput('diffID', $diff->getID())
->addHiddenInput('viaDiffView', 1)
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel('Attach To')
->setValue($select))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Continue'));
$action_panel->appendChild($action_form);
$top_panel = $action_panel;
}
$changesets = $diff->loadChangesets();
$changesets = msort($changesets, 'getSortKey');
$table_of_contents = id(new DifferentialDiffTableOfContentsView())
->setChangesets($changesets)
->setVisibleChangesets($changesets);
$refs = array();
foreach ($changesets as $changeset) {
$refs[$changeset->getID()] = $changeset->getID();
}
$details = id(new DifferentialChangesetListView())
->setChangesets($changesets)
->setVisibleChangesets($changesets)
->setLineWidthFromChangesets($changesets)
->setRenderingReferences($refs)
->setStandaloneURI('/differential/changeset/')
->setUser($request->getUser());
return $this->buildStandardPageResponse(
id(new DifferentialPrimaryPaneView())
->setLineWidthFromChangesets($changesets)
->appendChild(
array(
$top_panel->render(),
$table_of_contents->render(),
$details->render(),
)),
array(
'title' => 'Diff View',
));
}
}
diff --git a/src/applications/differential/controller/DifferentialInlineCommentEditController.php b/src/applications/differential/controller/DifferentialInlineCommentEditController.php
index bc44d1510b..4e1987e917 100644
--- a/src/applications/differential/controller/DifferentialInlineCommentEditController.php
+++ b/src/applications/differential/controller/DifferentialInlineCommentEditController.php
@@ -1,84 +1,68 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialInlineCommentEditController
extends PhabricatorInlineCommentController {
private $revisionID;
public function willProcessRequest(array $data) {
$this->revisionID = $data['id'];
}
protected function createComment() {
// Verify revision and changeset correspond to actual objects.
$revision_id = $this->revisionID;
$changeset_id = $this->getChangesetID();
if (!id(new DifferentialRevision())->load($revision_id)) {
throw new Exception("Invalid revision ID!");
}
if (!id(new DifferentialChangeset())->load($changeset_id)) {
throw new Exception("Invalid changeset ID!");
}
return id(new DifferentialInlineComment())
->setRevisionID($revision_id)
->setChangesetID($changeset_id);
}
protected function loadComment($id) {
return id(new DifferentialInlineComment())->load($id);
}
protected function loadCommentForEdit($id) {
$request = $this->getRequest();
$user = $request->getUser();
$inline = $this->loadComment($id);
if (!$this->canEditInlineComment($user, $inline)) {
throw new Exception("That comment is not editable!");
}
return $inline;
}
private function canEditInlineComment(
PhabricatorUser $user,
DifferentialInlineComment $inline) {
// Only the author may edit a comment.
if ($inline->getAuthorPHID() != $user->getPHID()) {
return false;
}
// Saved comments may not be edited.
if ($inline->getCommentID()) {
return false;
}
// Inline must be attached to the active revision.
if ($inline->getRevisionID() != $this->revisionID) {
return false;
}
return true;
}
}
diff --git a/src/applications/differential/controller/DifferentialInlineCommentPreviewController.php b/src/applications/differential/controller/DifferentialInlineCommentPreviewController.php
index 8ffc741202..e5fea8c72f 100644
--- a/src/applications/differential/controller/DifferentialInlineCommentPreviewController.php
+++ b/src/applications/differential/controller/DifferentialInlineCommentPreviewController.php
@@ -1,39 +1,23 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialInlineCommentPreviewController
extends PhabricatorInlineCommentPreviewController {
private $revisionID;
public function willProcessRequest(array $data) {
$this->revisionID = $data['id'];
}
protected function loadInlineComments() {
$user = $this->getRequest()->getUser();
$inlines = id(new DifferentialInlineComment())->loadAllWhere(
'authorPHID = %s AND revisionID = %d AND commentID IS NULL',
$user->getPHID(),
$this->revisionID);
return $inlines;
}
}
diff --git a/src/applications/differential/controller/DifferentialRevisionDetailRenderer.php b/src/applications/differential/controller/DifferentialRevisionDetailRenderer.php
index 9dc1e739e9..670c9a78df 100644
--- a/src/applications/differential/controller/DifferentialRevisionDetailRenderer.php
+++ b/src/applications/differential/controller/DifferentialRevisionDetailRenderer.php
@@ -1,52 +1,36 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DifferentialRevisionDetailRenderer {
private $diff;
private $vsDiff;
final public function setDiff(DifferentialDiff $diff) {
$this->diff = $diff;
return $this;
}
final protected function getDiff() {
return $this->diff;
}
final public function setVSDiff(DifferentialDiff $diff) {
$this->vsDiff = $diff;
return $this;
}
final protected function getVSDiff() {
return $this->vsDiff;
}
/**
* This function must return an array of action links that will be
* added to the end of action links on the differential revision
* page. Each element in the array must be an array which must
* contain 'name' and 'href' fields. 'name' will be the name of the
* link and 'href' will be the address where the link points
* to. 'class' is optional and can be used for specifying a CSS
* class.
*/
abstract public function generateActionLinks(DifferentialRevision $revision,
DifferentialDiff $diff);
}
diff --git a/src/applications/differential/controller/DifferentialRevisionEditController.php b/src/applications/differential/controller/DifferentialRevisionEditController.php
index c71c3f5082..e94ddb866a 100644
--- a/src/applications/differential/controller/DifferentialRevisionEditController.php
+++ b/src/applications/differential/controller/DifferentialRevisionEditController.php
@@ -1,191 +1,175 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialRevisionEditController extends DifferentialController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
if (!$this->id) {
$this->id = $request->getInt('revisionID');
}
if ($this->id) {
$revision = id(new DifferentialRevision())->load($this->id);
if (!$revision) {
return new Aphront404Response();
}
} else {
$revision = new DifferentialRevision();
}
$revision->loadRelationships();
$aux_fields = $this->loadAuxiliaryFields($revision);
$diff_id = $request->getInt('diffID');
if ($diff_id) {
$diff = id(new DifferentialDiff())->load($diff_id);
if (!$diff) {
return new Aphront404Response();
}
if ($diff->getRevisionID()) {
// TODO: Redirect?
throw new Exception("This diff is already attached to a revision!");
}
} else {
$diff = null;
}
$errors = array();
if ($request->isFormPost() && !$request->getStr('viaDiffView')) {
foreach ($aux_fields as $aux_field) {
$aux_field->setValueFromRequest($request);
try {
$aux_field->validateField();
} catch (DifferentialFieldValidationException $ex) {
$errors[] = $ex->getMessage();
}
}
if (!$errors) {
$editor = new DifferentialRevisionEditor($revision);
$editor->setActor($request->getUser());
if ($diff) {
$editor->addDiff($diff, $request->getStr('comments'));
}
$editor->setAuxiliaryFields($aux_fields);
$editor->save();
return id(new AphrontRedirectResponse())
->setURI('/D'.$revision->getID());
}
}
$aux_phids = array();
foreach ($aux_fields as $key => $aux_field) {
$aux_phids[$key] = $aux_field->getRequiredHandlePHIDsForRevisionEdit();
}
$phids = array_mergev($aux_phids);
$phids = array_unique($phids);
$handles = $this->loadViewerHandles($phids);
foreach ($aux_fields as $key => $aux_field) {
$aux_field->setHandles(array_select_keys($handles, $aux_phids[$key]));
}
$form = new AphrontFormView();
$form->setUser($request->getUser());
if ($diff) {
$form->addHiddenInput('diffID', $diff->getID());
}
if ($revision->getID()) {
$form->setAction('/differential/revision/edit/'.$revision->getID().'/');
} else {
$form->setAction('/differential/revision/edit/');
}
$error_view = null;
if ($errors) {
$error_view = id(new AphrontErrorView())
->setTitle('Form Errors')
->setErrors($errors);
}
if ($diff && $revision->getID()) {
$form
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Comments')
->setName('comments')
->setCaption("Explain what's new in this diff.")
->setValue($request->getStr('comments')))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Save'))
->appendChild(
id(new AphrontFormDividerControl()));
}
foreach ($aux_fields as $aux_field) {
$control = $aux_field->renderEditControl();
if ($control) {
$form->appendChild($control);
}
}
$submit = id(new AphrontFormSubmitControl())
->setValue('Save');
if ($diff) {
$submit->addCancelButton('/differential/diff/'.$diff->getID().'/');
} else {
$submit->addCancelButton('/D'.$revision->getID());
}
$form->appendChild($submit);
$panel = new AphrontPanelView();
if ($revision->getID()) {
if ($diff) {
$panel->setHeader('Update Differential Revision');
} else {
$panel->setHeader('Edit Differential Revision');
}
} else {
$panel->setHeader('Create New Differential Revision');
}
$panel->appendChild($form);
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
return $this->buildStandardPageResponse(
array($error_view, $panel),
array(
'title' => 'Edit Differential Revision',
));
}
private function loadAuxiliaryFields(DifferentialRevision $revision) {
$user = $this->getRequest()->getUser();
$aux_fields = DifferentialFieldSelector::newSelector()
->getFieldSpecifications();
foreach ($aux_fields as $key => $aux_field) {
$aux_field->setRevision($revision);
if (!$aux_field->shouldAppearOnEdit()) {
unset($aux_fields[$key]);
} else {
$aux_field->setUser($user);
}
}
return DifferentialAuxiliaryField::loadFromStorage(
$revision,
$aux_fields);
}
}
diff --git a/src/applications/differential/controller/DifferentialRevisionListController.php b/src/applications/differential/controller/DifferentialRevisionListController.php
index 7ad4b5356e..f02cd9138b 100644
--- a/src/applications/differential/controller/DifferentialRevisionListController.php
+++ b/src/applications/differential/controller/DifferentialRevisionListController.php
@@ -1,506 +1,490 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialRevisionListController extends DifferentialController {
private $filter;
private $username;
public function shouldRequireLogin() {
return !$this->allowsAnonymousAccess();
}
public function willProcessRequest(array $data) {
$this->filter = idx($data, 'filter');
$this->username = idx($data, 'username');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$viewer_is_anonymous = !$user->isLoggedIn();
$params = array_filter(
array(
'status' => $request->getStr('status'),
'order' => $request->getStr('order'),
));
$default_filter = ($viewer_is_anonymous ? 'all' : 'active');
$filters = $this->getFilters();
$this->filter = $this->selectFilter(
$filters,
$this->filter,
$default_filter);
// Redirect from search to canonical URL.
$phid_arr = $request->getArr('view_user');
if ($phid_arr) {
$view_user = id(new PhabricatorUser())
->loadOneWhere('phid = %s', head($phid_arr));
$base_uri = '/differential/filter/'.$this->filter.'/';
if ($view_user) {
// This is a user, so generate a pretty URI.
$uri = $base_uri.phutil_escape_uri($view_user->getUserName()).'/';
} else {
// We're assuming this is a mailing list, generate an ugly URI.
$uri = $base_uri;
$params['phid'] = head($phid_arr);
}
$uri = new PhutilURI($uri);
$uri->setQueryParams($params);
return id(new AphrontRedirectResponse())->setURI($uri);
}
$uri = new PhutilURI('/differential/filter/'.$this->filter.'/');
$uri->setQueryParams($params);
$username = '';
if ($this->username) {
$view_user = id(new PhabricatorUser())
->loadOneWhere('userName = %s', $this->username);
if (!$view_user) {
return new Aphront404Response();
}
$username = phutil_escape_uri($this->username).'/';
$uri->setPath('/differential/filter/'.$this->filter.'/'.$username);
$params['phid'] = $view_user->getPHID();
} else {
$phid = $request->getStr('phid');
if (strlen($phid)) {
$params['phid'] = $phid;
}
}
// Fill in the defaults we'll actually use for calculations if any
// parameters are missing.
$params += array(
'phid' => $user->getPHID(),
'status' => 'all',
'order' => 'modified',
);
$side_nav = new AphrontSideNavView();
foreach ($filters as $filter) {
list($filter_name, $display_name) = $filter;
if ($filter_name) {
$href = clone $uri;
$href->setPath('/differential/filter/'.$filter_name.'/'.$username);
if ($filter_name == $this->filter) {
$class = 'aphront-side-nav-selected';
} else {
$class = null;
}
$item = phutil_render_tag(
'a',
array(
'href' => (string)$href,
'class' => $class,
),
phutil_escape_html($display_name));
} else {
$item = phutil_render_tag(
'span',
array(),
phutil_escape_html($display_name));
}
$side_nav->addNavItem($item);
}
$panels = array();
$handles = array();
$controls = $this->getFilterControls($this->filter);
if ($this->getFilterRequiresUser($this->filter) && !$params['phid']) {
// In the anonymous case, we still want to let you see some user's
// list, but we don't have a default PHID to provide (normally, we use
// the viewing user's). Show a warning instead.
$warning = new AphrontErrorView();
$warning->setSeverity(AphrontErrorView::SEVERITY_WARNING);
$warning->setTitle('User Required');
$warning->appendChild(
'This filter requires that a user be specified above.');
$panels[] = $warning;
} else {
$query = $this->buildQuery($this->filter, $params['phid']);
$pager = null;
if ($this->getFilterAllowsPaging($this->filter)) {
$pager = new AphrontPagerView();
$pager->setOffset($request->getInt('page'));
$pager->setPageSize(1000);
$pager->setURI($uri, 'page');
$query->setOffset($pager->getOffset());
$query->setLimit($pager->getPageSize() + 1);
}
foreach ($controls as $control) {
$this->applyControlToQuery($control, $query, $params);
}
$revisions = $query->execute();
if ($pager) {
$revisions = $pager->sliceResults($revisions);
}
$views = $this->buildViews($this->filter, $params['phid'], $revisions);
$view_objects = array();
foreach ($views as $view) {
if (empty($view['special'])) {
$view_objects[] = $view['view'];
}
}
$phids = array_mergev(mpull($view_objects, 'getRequiredHandlePHIDs'));
$phids[] = $params['phid'];
$handles = $this->loadViewerHandles($phids);
foreach ($views as $view) {
if (empty($view['special'])) {
$view['view']->setHandles($handles);
}
$panel = new AphrontPanelView();
$panel->setHeader($view['title']);
$panel->appendChild($view['view']);
if ($pager) {
$panel->appendChild($pager);
}
$panels[] = $panel;
}
}
$filter_form = id(new AphrontFormView())
->setMethod('GET')
->setAction('/differential/filter/'.$this->filter.'/')
->setUser($user);
foreach ($controls as $control) {
$control_view = $this->renderControl($control, $handles, $uri, $params);
$filter_form->appendChild($control_view);
}
$filter_form
->addHiddenInput('status', $params['status'])
->addHiddenInput('order', $params['order'])
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Filter Revisions'));
$filter_view = new AphrontListFilterView();
$filter_view->appendChild($filter_form);
if (!$viewer_is_anonymous) {
$create_uri = new PhutilURI('/differential/diff/create/');
$filter_view->addButton(
phutil_render_tag(
'a',
array(
'href' => (string)$create_uri,
'class' => 'green button',
),
'Create Revision'));
}
$side_nav->appendChild($filter_view);
foreach ($panels as $panel) {
$side_nav->appendChild($panel);
}
return $this->buildStandardPageResponse(
$side_nav,
array(
'title' => 'Differential Home',
));
}
private function getFilters() {
return array(
array(null, 'User Revisions'),
array('active', 'Active'),
array('revisions', 'Revisions'),
array('reviews', 'Reviews'),
array('subscribed', 'Subscribed'),
array('drafts', 'Draft Reviews'),
array(null, 'All Revisions'),
array('all', 'All'),
);
}
private function selectFilter(
array $filters,
$requested_filter,
$default_filter) {
// If the user requested a filter, make sure it actually exists.
if ($requested_filter) {
foreach ($filters as $filter) {
if ($filter[0] === $requested_filter) {
return $requested_filter;
}
}
}
// If not, return the default filter.
return $default_filter;
}
private function getFilterRequiresUser($filter) {
static $requires = array(
'active' => true,
'revisions' => true,
'reviews' => true,
'subscribed' => true,
'drafts' => true,
'all' => false,
);
if (!isset($requires[$filter])) {
throw new Exception("Unknown filter '{$filter}'!");
}
return $requires[$filter];
}
private function getFilterAllowsPaging($filter) {
static $allows = array(
'active' => false,
'revisions' => true,
'reviews' => true,
'subscribed' => true,
'drafts' => true,
'all' => true,
);
if (!isset($allows[$filter])) {
throw new Exception("Unknown filter '{$filter}'!");
}
return $allows[$filter];
}
private function getFilterControls($filter) {
static $controls = array(
'active' => array('phid'),
'revisions' => array('phid', 'status', 'order'),
'reviews' => array('phid', 'status', 'order'),
'subscribed' => array('subscriber', 'status', 'order'),
'drafts' => array('phid', 'status', 'order'),
'all' => array('status', 'order'),
);
if (!isset($controls[$filter])) {
throw new Exception("Unknown filter '{$filter}'!");
}
return $controls[$filter];
}
private function buildQuery($filter, $user_phid) {
$query = new DifferentialRevisionQuery();
$query->needRelationships(true);
switch ($filter) {
case 'active':
$query->withResponsibleUsers(array($user_phid));
$query->withStatus(DifferentialRevisionQuery::STATUS_OPEN);
$query->setLimit(null);
break;
case 'revisions':
$query->withAuthors(array($user_phid));
break;
case 'reviews':
$query->withReviewers(array($user_phid));
break;
case 'subscribed':
$query->withSubscribers(array($user_phid));
break;
case 'drafts':
$query->withDraftRepliesByAuthors(array($user_phid));
break;
case 'all':
break;
default:
throw new Exception("Unknown filter '{$filter}'!");
}
return $query;
}
private function renderControl(
$control,
array $handles,
PhutilURI $uri,
array $params) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
switch ($control) {
case 'subscriber':
case 'phid':
$view_phid = $params['phid'];
$value = array();
if ($view_phid) {
$value = array(
$view_phid => $handles[$view_phid]->getFullName(),
);
}
if ($control == 'subscriber') {
$source = '/typeahead/common/allmailable/';
$label = 'View Subscriber';
} else {
$source = '/typeahead/common/accounts/';
$label = 'View User';
}
return id(new AphrontFormTokenizerControl())
->setDatasource($source)
->setLabel($label)
->setName('view_user')
->setValue($value)
->setLimit(1);
case 'status':
return id(new AphrontFormToggleButtonsControl())
->setLabel('Status')
->setValue($params['status'])
->setBaseURI($uri, 'status')
->setButtons(
array(
'all' => 'All',
'open' => 'Open',
'closed' => pht('Closed'),
'abandoned' => 'Abandoned',
));
case 'order':
return id(new AphrontFormToggleButtonsControl())
->setLabel('Order')
->setValue($params['order'])
->setBaseURI($uri, 'order')
->setButtons(
array(
'modified' => 'Updated',
'created' => 'Created',
));
default:
throw new Exception("Unknown control '{$control}'!");
}
}
private function applyControlToQuery($control, $query, array $params) {
switch ($control) {
case 'phid':
case 'subscriber':
// Already applied by query construction.
break;
case 'status':
if ($params['status'] == 'open') {
$query->withStatus(DifferentialRevisionQuery::STATUS_OPEN);
} else if ($params['status'] == 'closed') {
$query->withStatus(DifferentialRevisionQuery::STATUS_CLOSED);
} else if ($params['status'] == 'abandoned') {
$query->withStatus(DifferentialRevisionQuery::STATUS_ABANDONED);
}
break;
case 'order':
if ($params['order'] == 'created') {
$query->setOrder(DifferentialRevisionQuery::ORDER_CREATED);
}
break;
default:
throw new Exception("Unknown control '{$control}'!");
}
}
private function buildViews($filter, $user_phid, array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$user = $this->getRequest()->getUser();
$template = id(new DifferentialRevisionListView())
->setUser($user)
->setFields(DifferentialRevisionListView::getDefaultFields());
$views = array();
switch ($filter) {
case 'active':
list($active, $waiting) = DifferentialRevisionQuery::splitResponsible(
$revisions,
$user_phid);
$view = id(clone $template)
->setHighlightAge(true)
->setRevisions($active)
->loadAssets();
$views[] = array(
'title' => 'Action Required',
'view' => $view,
);
// Flags are sort of private, so only show the flag panel if you're
// looking at your own requests.
if ($user_phid == $user->getPHID()) {
$flags = id(new PhabricatorFlagQuery())
->withOwnerPHIDs(array($user_phid))
->withTypes(array(PhabricatorPHIDConstants::PHID_TYPE_DREV))
->needHandles(true)
->execute();
if ($flags) {
$view = id(new PhabricatorFlagListView())
->setFlags($flags)
->setUser($user);
$views[] = array(
'title' => 'Flagged Revisions',
'view' => $view,
'special' => true,
);
}
}
$view = id(clone $template)
->setRevisions($waiting)
->loadAssets();
$views[] = array(
'title' => 'Waiting On Others',
'view' => $view,
);
break;
case 'revisions':
case 'reviews':
case 'subscribed':
case 'drafts':
case 'all':
$titles = array(
'revisions' => 'Revisions by Author',
'reviews' => 'Revisions by Reviewer',
'subscribed' => 'Revisions by Subscriber',
'all' => 'Revisions',
);
$view = id(clone $template)
->setRevisions($revisions)
->loadAssets();
$views[] = array(
'title' => idx($titles, $filter),
'view' => $view,
);
break;
default:
throw new Exception("Unknown filter '{$filter}'!");
}
return $views;
}
}
diff --git a/src/applications/differential/controller/DifferentialRevisionStatsController.php b/src/applications/differential/controller/DifferentialRevisionStatsController.php
index 9c8252a381..cb2a470a2c 100644
--- a/src/applications/differential/controller/DifferentialRevisionStatsController.php
+++ b/src/applications/differential/controller/DifferentialRevisionStatsController.php
@@ -1,182 +1,166 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialRevisionStatsController extends DifferentialController {
private $filter;
private function loadRevisions($phid) {
$table = new DifferentialRevision();
$conn_r = $table->establishConnection('r');
$rows = queryfx_all(
$conn_r,
'SELECT revisions.* FROM %T revisions ' .
'JOIN %T comments ON comments.revisionID = revisions.id ' .
'JOIN (' .
' SELECT revisionID FROM %T WHERE objectPHID = %s ' .
' UNION ALL ' .
' SELECT id from differential_revision WHERE authorPHID = %s) rel ' .
'ON (comments.revisionID = rel.revisionID)' .
'WHERE comments.action = %s' .
'AND comments.authorPHID = %s',
$table->getTableName(),
id(new DifferentialComment())->getTableName(),
DifferentialRevision::RELATIONSHIP_TABLE,
$phid,
$phid,
$this->filter,
$phid
);
return $table->loadAllFromArray($rows);
}
private function loadComments($phid) {
$table = new DifferentialComment();
$conn_r = $table->establishConnection('r');
$rows = queryfx_all(
$conn_r,
'SELECT comments.* FROM %T comments ' .
'JOIN (' .
' SELECT revisionID FROM %T WHERE objectPHID = %s ' .
' UNION ALL ' .
' SELECT id from differential_revision WHERE authorPHID = %s) rel ' .
'ON (comments.revisionID = rel.revisionID)' .
'WHERE comments.action = %s' .
'AND comments.authorPHID = %s',
$table->getTableName(),
DifferentialRevision::RELATIONSHIP_TABLE,
$phid,
$phid,
$this->filter,
$phid
);
return $table->loadAllFromArray($rows);
}
private function loadDiffs(array $revisions) {
if (!$revisions) {
return array();
}
$diff_teml = new DifferentialDiff();
$diffs = $diff_teml->loadAllWhere(
'revisionID in (%Ld)',
array_keys($revisions)
);
return $diffs;
}
public function willProcessRequest(array $data) {
$this->filter = idx($data, 'filter');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
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));
}
$params = array_filter(
array(
'phid' => $request->getStr('phid'),
));
// Fill in the defaults we'll actually use for calculations if any
// parameters are missing.
$params += array(
'phid' => $user->getPHID(),
);
$side_nav = new AphrontSideNavFilterView();
$side_nav->setBaseURI(id(new PhutilURI('/differential/stats/'))
->alter('phid', $params['phid']));
foreach (array(
DifferentialAction::ACTION_CLOSE,
DifferentialAction::ACTION_ACCEPT,
DifferentialAction::ACTION_REJECT,
DifferentialAction::ACTION_UPDATE,
DifferentialAction::ACTION_COMMENT,
) as $action) {
$verb = ucfirst(DifferentialAction::getActionPastTenseVerb($action));
$side_nav->addFilter($action, $verb);
}
$this->filter =
$side_nav->selectFilter($this->filter,
DifferentialAction::ACTION_CLOSE);
$panels = array();
$handles = $this->loadViewerHandles(array($params['phid']));
$filter_form = id(new AphrontFormView())
->setAction('/differential/stats/'.$this->filter.'/')
->setUser($user);
$filter_form->appendChild(
$this->renderControl($params['phid'], $handles));
$filter_form->appendChild(id(new AphrontFormSubmitControl())
->setValue('Filter Revisions'));
$side_nav->appendChild($filter_form);
$comments = $this->loadComments($params['phid']);
$revisions = $this->loadRevisions($params['phid']);
$diffs = $this->loadDiffs($revisions);
$panel = new AphrontPanelView();
$panel->setHeader('Differential rate analysis');
$panel->appendChild(
id(new DifferentialRevisionStatsView())
->setComments($comments)
->setFilter($this->filter)
->setRevisions($revisions)
->setDiffs($diffs)
->setUser($user));
$panels[] = $panel;
foreach ($panels as $panel) {
$side_nav->appendChild($panel);
}
return $this->buildStandardPageResponse(
$side_nav,
array(
'title' => 'Differential statistics',
));
}
private function renderControl($view_phid, $handles) {
$value = array();
if ($view_phid) {
$value = array(
$view_phid => $handles[$view_phid]->getFullName(),
);
}
return id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/users/')
->setLabel('View User')
->setName('view_user')
->setValue($value)
->setLimit(1);
}
}
diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php
index c7a4a245d3..0c9b7c662e 100644
--- a/src/applications/differential/controller/DifferentialRevisionViewController.php
+++ b/src/applications/differential/controller/DifferentialRevisionViewController.php
@@ -1,1080 +1,1064 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialRevisionViewController extends DifferentialController {
private $revisionID;
public function shouldRequireLogin() {
return !$this->allowsAnonymousAccess();
}
public function willProcessRequest(array $data) {
$this->revisionID = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$viewer_is_anonymous = !$user->isLoggedIn();
$revision = id(new DifferentialRevision())->load($this->revisionID);
if (!$revision) {
return new Aphront404Response();
}
$revision->loadRelationships();
$diffs = $revision->loadDiffs();
if (!$diffs) {
throw new Exception(
"This revision has no diffs. Something has gone quite wrong.");
}
$diff_vs = $request->getInt('vs');
$target_id = $request->getInt('id');
$target = idx($diffs, $target_id, end($diffs));
$target_manual = $target;
if (!$target_id) {
foreach ($diffs as $diff) {
if ($diff->getCreationMethod() != 'commit') {
$target_manual = $diff;
}
}
}
if (empty($diffs[$diff_vs])) {
$diff_vs = null;
}
$arc_project = $target->loadArcanistProject();
$repository = ($arc_project ? $arc_project->loadRepository() : null);
list($changesets, $vs_map, $vs_changesets, $rendering_references) =
$this->loadChangesetsAndVsMap(
$target,
idx($diffs, $diff_vs),
$repository);
if ($request->getExists('download')) {
return $this->buildRawDiffResponse($changesets,
$vs_changesets,
$vs_map,
$repository);
}
$props = id(new DifferentialDiffProperty())->loadAllWhere(
'diffID = %d',
$target_manual->getID());
$props = mpull($props, 'getData', 'getName');
$aux_fields = $this->loadAuxiliaryFields($revision);
$comments = $revision->loadComments();
$comments = array_merge(
$this->getImplicitComments($revision, reset($diffs)),
$comments);
$all_changesets = $changesets;
$inlines = $this->loadInlineComments($comments, $all_changesets);
$object_phids = array_merge(
$revision->getReviewers(),
$revision->getCCPHIDs(),
$revision->loadCommitPHIDs(),
array(
$revision->getAuthorPHID(),
$user->getPHID(),
),
mpull($comments, 'getAuthorPHID'));
foreach ($comments as $comment) {
$metadata = $comment->getMetadata();
$added_reviewers = idx(
$metadata,
DifferentialComment::METADATA_ADDED_REVIEWERS);
if ($added_reviewers) {
foreach ($added_reviewers as $phid) {
$object_phids[] = $phid;
}
}
$added_ccs = idx(
$metadata,
DifferentialComment::METADATA_ADDED_CCS);
if ($added_ccs) {
foreach ($added_ccs as $phid) {
$object_phids[] = $phid;
}
}
}
foreach ($revision->getAttached() as $type => $phids) {
foreach ($phids as $phid => $info) {
$object_phids[] = $phid;
}
}
$aux_phids = array();
foreach ($aux_fields as $key => $aux_field) {
$aux_field->setDiff($target);
$aux_field->setManualDiff($target_manual);
$aux_field->setDiffProperties($props);
$aux_phids[$key] = $aux_field->getRequiredHandlePHIDsForRevisionView();
}
$object_phids = array_merge($object_phids, array_mergev($aux_phids));
$object_phids = array_unique($object_phids);
$handles = $this->loadViewerHandles($object_phids);
foreach ($aux_fields as $key => $aux_field) {
// Make sure each field only has access to handles it specifically
// requested, not all handles. Otherwise you can get a field which works
// only in the presence of other fields.
$aux_field->setHandles(array_select_keys($handles, $aux_phids[$key]));
}
$reviewer_warning = null;
$has_live_reviewer = false;
foreach ($revision->getReviewers() as $reviewer) {
if (!$handles[$reviewer]->isDisabled()) {
$has_live_reviewer = true;
}
}
if (!$has_live_reviewer) {
$reviewer_warning = new AphrontErrorView();
$reviewer_warning->setSeverity(AphrontErrorView::SEVERITY_WARNING);
$reviewer_warning->setTitle('No Active Reviewers');
if ($revision->getReviewers()) {
$reviewer_warning->appendChild(
'<p>All specified reviewers are disabled. You may want to add '.
'some new reviewers.</p>');
} else {
$reviewer_warning->appendChild(
'<p>This revision has no specified reviewers. You may want to '.
'add some.</p>');
}
}
$request_uri = $request->getRequestURI();
$limit = 100;
$large = $request->getStr('large');
if (count($changesets) > $limit && !$large) {
$count = number_format(count($changesets));
$warning = new AphrontErrorView();
$warning->setTitle('Very Large Diff');
$warning->setSeverity(AphrontErrorView::SEVERITY_WARNING);
$warning->appendChild(
"<p>This diff is very large and affects {$count} files. Load ".
"each file individually. ".
"<strong>".
phutil_render_tag(
'a',
array(
'href' => $request_uri
->alter('large', 'true')
->setFragment('toc'),
),
'Show All Files Inline').
"</strong>");
$warning = $warning->render();
$my_inlines = id(new DifferentialInlineComment())->loadAllWhere(
'revisionID = %d AND commentID IS NULL AND authorPHID = %s AND '.
'changesetID IN (%Ld)',
$this->revisionID,
$user->getPHID(),
mpull($changesets, 'getID'));
$visible_changesets = array();
foreach ($inlines + $my_inlines as $inline) {
$changeset_id = $inline->getChangesetID();
if (isset($changesets[$changeset_id])) {
$visible_changesets[$changeset_id] = $changesets[$changeset_id];
}
}
if (!empty($props['arc:lint'])) {
$changeset_paths = mpull($changesets, null, 'getFilename');
foreach ($props['arc:lint'] as $lint) {
$changeset = idx($changeset_paths, $lint['path']);
if ($changeset) {
$visible_changesets[$changeset->getID()] = $changeset;
}
}
}
} else {
$warning = null;
$visible_changesets = $changesets;
}
$revision_detail = new DifferentialRevisionDetailView();
$revision_detail->setRevision($revision);
$revision_detail->setAuxiliaryFields($aux_fields);
$actions = $this->getRevisionActions($revision);
$custom_renderer_class = PhabricatorEnv::getEnvConfig(
'differential.revision-custom-detail-renderer');
if ($custom_renderer_class) {
// TODO: build a better version of the action links and deprecate the
// whole DifferentialRevisionDetailRenderer class.
$custom_renderer = newv($custom_renderer_class, array());
$custom_renderer->setDiff($target);
if ($diff_vs) {
$custom_renderer->setVSDiff($diffs[$diff_vs]);
}
$actions = array_merge(
$actions,
$custom_renderer->generateActionLinks($revision, $target_manual));
}
$whitespace = $request->getStr(
'whitespace',
DifferentialChangesetParser::WHITESPACE_IGNORE_ALL);
if ($arc_project) {
list($symbol_indexes, $project_phids) = $this->buildSymbolIndexes(
$arc_project,
$visible_changesets);
} else {
$symbol_indexes = array();
$project_phids = null;
}
$revision_detail->setActions($actions);
$revision_detail->setUser($user);
$comment_view = new DifferentialRevisionCommentListView();
$comment_view->setComments($comments);
$comment_view->setHandles($handles);
$comment_view->setInlineComments($inlines);
$comment_view->setChangesets($all_changesets);
$comment_view->setUser($user);
$comment_view->setTargetDiff($target);
$comment_view->setVersusDiffID($diff_vs);
if ($arc_project) {
Javelin::initBehavior(
'repository-crossreference',
array(
'section' => $comment_view->getID(),
'projects' => $project_phids,
));
}
$changeset_view = new DifferentialChangesetListView();
$changeset_view->setLineWidthFromChangesets($changesets);
$changeset_view->setChangesets($changesets);
$changeset_view->setVisibleChangesets($visible_changesets);
if (!$viewer_is_anonymous) {
$changeset_view->setInlineCommentControllerURI(
'/differential/comment/inline/edit/'.$revision->getID().'/');
}
$changeset_view->setStandaloneURI('/differential/changeset/');
$changeset_view->setRawFileURIs(
'/differential/changeset/?view=old',
'/differential/changeset/?view=new');
$changeset_view->setUser($user);
$changeset_view->setDiff($target);
$changeset_view->setRenderingReferences($rendering_references);
$changeset_view->setVsMap($vs_map);
$changeset_view->setWhitespace($whitespace);
if ($repository) {
$changeset_view->setRepository($repository);
}
$changeset_view->setSymbolIndexes($symbol_indexes);
$diff_history = new DifferentialRevisionUpdateHistoryView();
$diff_history->setDiffs($diffs);
$diff_history->setSelectedVersusDiffID($diff_vs);
$diff_history->setSelectedDiffID($target->getID());
$diff_history->setSelectedWhitespace($whitespace);
$diff_history->setUser($user);
$local_view = new DifferentialLocalCommitsView();
$local_view->setUser($user);
$local_view->setLocalCommits(idx($props, 'local:commits'));
if ($repository) {
$other_revisions = $this->loadOtherRevisions(
$changesets,
$target,
$repository);
} else {
$other_revisions = array();
}
$other_view = null;
if ($other_revisions) {
$other_view = $this->renderOtherRevisions($other_revisions);
}
$toc_view = new DifferentialDiffTableOfContentsView();
$toc_view->setChangesets($changesets);
$toc_view->setVisibleChangesets($visible_changesets);
$toc_view->setRenderingReferences($rendering_references);
$toc_view->setUnitTestData(idx($props, 'arc:unit', array()));
if ($repository) {
$toc_view->setRepository($repository);
}
$toc_view->setDiff($target);
$toc_view->setUser($user);
$toc_view->setRevisionID($revision->getID());
$toc_view->setWhitespace($whitespace);
$comment_form = null;
if (!$viewer_is_anonymous) {
$draft = id(new PhabricatorDraft())->loadOneWhere(
'authorPHID = %s AND draftKey = %s',
$user->getPHID(),
'differential-comment-'.$revision->getID());
$reviewers = array();
$ccs = array();
if ($draft) {
$reviewers = idx($draft->getMetadata(), 'reviewers', array());
$ccs = idx($draft->getMetadata(), 'ccs', array());
if ($reviewers || $ccs) {
$handles = $this->loadViewerHandles(array_merge($reviewers, $ccs));
$reviewers = array_select_keys($handles, $reviewers);
$ccs = array_select_keys($handles, $ccs);
}
}
$comment_form = new DifferentialAddCommentView();
$comment_form->setRevision($revision);
$comment_form->setAuxFields($aux_fields);
$comment_form->setActions($this->getRevisionCommentActions($revision));
$comment_form->setActionURI('/differential/comment/save/');
$comment_form->setUser($user);
$comment_form->setDraft($draft);
$comment_form->setReviewers(mpull($reviewers, 'getFullName', 'getPHID'));
$comment_form->setCCs(mpull($ccs, 'getFullName', 'getPHID'));
}
$pane_id = celerity_generate_unique_node_id();
Javelin::initBehavior(
'differential-keyboard-navigation',
array(
'haunt' => $pane_id,
));
Javelin::initBehavior('differential-user-select');
$page_pane = id(new DifferentialPrimaryPaneView())
->setLineWidthFromChangesets($changesets)
->setID($pane_id)
->appendChild(
$comment_view->render().
$diff_history->render().
$warning.
$local_view->render().
$toc_view->render().
$other_view.
$changeset_view->render());
if ($comment_form) {
$page_pane->appendChild($comment_form->render());
}
PhabricatorFeedStoryNotification::updateObjectNotificationViews(
$user, $revision->getPHID());
$top_anchor = id(new PhabricatorAnchorView())
->setAnchorName('top')
->setNavigationMarker(true);
$nav = $this->buildSideNavView($revision, $changesets);
$nav->selectFilter('');
$nav->appendChild(
array(
$reviewer_warning,
$top_anchor,
$revision_detail,
$page_pane,
));
return $this->buildApplicationPage(
$nav,
array(
'title' => 'D'.$revision->getID().' '.$revision->getTitle(),
));
}
private function getImplicitComments(
DifferentialRevision $revision,
DifferentialDiff $diff) {
$author_phid = nonempty(
$diff->getAuthorPHID(),
$revision->getAuthorPHID());
$template = new DifferentialComment();
$template->setAuthorPHID($author_phid);
$template->setRevisionID($revision->getID());
$template->setDateCreated($revision->getDateCreated());
$comments = array();
if (strlen($revision->getSummary())) {
$summary_comment = clone $template;
$summary_comment->setContent($revision->getSummary());
$summary_comment->setAction(DifferentialAction::ACTION_SUMMARIZE);
$comments[] = $summary_comment;
}
if (strlen($revision->getTestPlan())) {
$testplan_comment = clone $template;
$testplan_comment->setContent($revision->getTestPlan());
$testplan_comment->setAction(DifferentialAction::ACTION_TESTPLAN);
$comments[] = $testplan_comment;
}
return $comments;
}
private function getRevisionActions(DifferentialRevision $revision) {
$user = $this->getRequest()->getUser();
$viewer_phid = $user->getPHID();
$viewer_is_owner = ($revision->getAuthorPHID() == $viewer_phid);
$viewer_is_reviewer = in_array($viewer_phid, $revision->getReviewers());
$viewer_is_cc = in_array($viewer_phid, $revision->getCCPHIDs());
$viewer_is_anonymous = !$this->getRequest()->getUser()->isLoggedIn();
$status = $revision->getStatus();
$revision_id = $revision->getID();
$revision_phid = $revision->getPHID();
$links = array();
if ($viewer_is_owner) {
$links[] = array(
'class' => 'revision-edit',
'href' => "/differential/revision/edit/{$revision_id}/",
'name' => 'Edit Revision',
);
}
if (!$viewer_is_anonymous) {
require_celerity_resource('phabricator-flag-css');
$flag = PhabricatorFlagQuery::loadUserFlag($user, $revision_phid);
if ($flag) {
$class = PhabricatorFlagColor::getCSSClass($flag->getColor());
$color = PhabricatorFlagColor::getColorName($flag->getColor());
$links[] = array(
'class' => 'flag-clear '.$class,
'href' => '/flag/delete/'.$flag->getID().'/',
'name' => phutil_escape_html('Remove '.$color.' Flag'),
'sigil' => 'workflow',
);
} else {
$links[] = array(
'class' => 'flag-add phabricator-flag-ghost',
'href' => '/flag/edit/'.$revision_phid.'/',
'name' => 'Flag Revision',
'sigil' => 'workflow',
);
}
if (!$viewer_is_owner && !$viewer_is_reviewer) {
$action = $viewer_is_cc ? 'rem' : 'add';
$links[] = array(
'class' => $viewer_is_cc ? 'subscribe-rem' : 'subscribe-add',
'href' => "/differential/subscribe/{$action}/{$revision_id}/",
'name' => $viewer_is_cc ? 'Unsubscribe' : 'Subscribe',
'instant' => true,
);
} else {
$links[] = array(
'class' => 'subscribe-rem unavailable',
'name' => 'Automatically Subscribed',
);
}
require_celerity_resource('phabricator-object-selector-css');
require_celerity_resource('javelin-behavior-phabricator-object-selector');
$links[] = array(
'class' => 'action-dependencies',
'name' => 'Edit Dependencies',
'href' => "/search/attach/{$revision_phid}/DREV/dependencies/",
'sigil' => 'workflow',
);
if (PhabricatorEnv::getEnvConfig('maniphest.enabled')) {
$links[] = array(
'class' => 'attach-maniphest',
'name' => 'Edit Maniphest Tasks',
'href' => "/search/attach/{$revision_phid}/TASK/",
'sigil' => 'workflow',
);
}
if ($user->getIsAdmin()) {
$links[] = array(
'class' => 'transcripts-metamta',
'name' => 'MetaMTA Transcripts',
'href' => "/mail/?phid={$revision_phid}",
);
}
$links[] = array(
'class' => 'transcripts-herald',
'name' => 'Herald Transcripts',
'href' => "/herald/transcript/?phid={$revision_phid}",
);
}
$request_uri = $this->getRequest()->getRequestURI();
$links[] = array(
'class' => 'action-download',
'name' => 'Download Raw Diff',
'href' => $request_uri->alter('download', 'true')
);
return $links;
}
private function getRevisionCommentActions(DifferentialRevision $revision) {
$actions = array(
DifferentialAction::ACTION_COMMENT => true,
);
$viewer = $this->getRequest()->getUser();
$viewer_phid = $viewer->getPHID();
$viewer_is_owner = ($viewer_phid == $revision->getAuthorPHID());
$viewer_is_reviewer = in_array($viewer_phid, $revision->getReviewers());
$viewer_did_accept = ($viewer_phid === $revision->loadReviewedBy());
$status = $revision->getStatus();
$allow_self_accept = PhabricatorEnv::getEnvConfig(
'differential.allow-self-accept', false);
$always_allow_close = PhabricatorEnv::getEnvConfig(
'differential.always-allow-close', false);
if ($viewer_is_owner) {
switch ($status) {
case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
$actions[DifferentialAction::ACTION_ACCEPT] = $allow_self_accept;
$actions[DifferentialAction::ACTION_ABANDON] = true;
$actions[DifferentialAction::ACTION_RETHINK] = true;
break;
case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
$actions[DifferentialAction::ACTION_ACCEPT] = $allow_self_accept;
$actions[DifferentialAction::ACTION_ABANDON] = true;
$actions[DifferentialAction::ACTION_REQUEST] = true;
break;
case ArcanistDifferentialRevisionStatus::ACCEPTED:
$actions[DifferentialAction::ACTION_ABANDON] = true;
$actions[DifferentialAction::ACTION_REQUEST] = true;
$actions[DifferentialAction::ACTION_RETHINK] = true;
$actions[DifferentialAction::ACTION_CLOSE] = true;
break;
case ArcanistDifferentialRevisionStatus::CLOSED:
break;
case ArcanistDifferentialRevisionStatus::ABANDONED:
$actions[DifferentialAction::ACTION_RECLAIM] = true;
break;
}
} else {
switch ($status) {
case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
$actions[DifferentialAction::ACTION_ACCEPT] = true;
$actions[DifferentialAction::ACTION_REJECT] = true;
$actions[DifferentialAction::ACTION_RESIGN] = $viewer_is_reviewer;
break;
case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
$actions[DifferentialAction::ACTION_ACCEPT] = true;
$actions[DifferentialAction::ACTION_RESIGN] = $viewer_is_reviewer;
break;
case ArcanistDifferentialRevisionStatus::ACCEPTED:
$actions[DifferentialAction::ACTION_REJECT] = true;
$actions[DifferentialAction::ACTION_RESIGN] =
$viewer_is_reviewer && !$viewer_did_accept;
break;
case ArcanistDifferentialRevisionStatus::CLOSED:
case ArcanistDifferentialRevisionStatus::ABANDONED:
break;
}
if ($status != ArcanistDifferentialRevisionStatus::CLOSED) {
$actions[DifferentialAction::ACTION_CLAIM] = true;
$actions[DifferentialAction::ACTION_CLOSE] = $always_allow_close;
}
}
$actions[DifferentialAction::ACTION_ADDREVIEWERS] = true;
$actions[DifferentialAction::ACTION_ADDCCS] = true;
$actions = array_keys(array_filter($actions));
$actions_dict = array();
foreach ($actions as $action) {
$actions_dict[$action] = DifferentialAction::getActionVerb($action);
}
return $actions_dict;
}
private function loadInlineComments(array $comments, array &$changesets) {
assert_instances_of($comments, 'DifferentialComment');
assert_instances_of($changesets, 'DifferentialChangeset');
$inline_comments = array();
$comment_ids = array_filter(mpull($comments, 'getID'));
if (!$comment_ids) {
return $inline_comments;
}
$inline_comments = id(new DifferentialInlineComment())
->loadAllWhere(
'commentID in (%Ld)',
$comment_ids);
$load_changesets = array();
foreach ($inline_comments as $inline) {
$changeset_id = $inline->getChangesetID();
if (isset($changesets[$changeset_id])) {
continue;
}
$load_changesets[$changeset_id] = true;
}
$more_changesets = array();
if ($load_changesets) {
$changeset_ids = array_keys($load_changesets);
$more_changesets += id(new DifferentialChangeset())
->loadAllWhere(
'id IN (%Ld)',
$changeset_ids);
}
if ($more_changesets) {
$changesets += $more_changesets;
$changesets = msort($changesets, 'getSortKey');
}
return $inline_comments;
}
private function loadChangesetsAndVsMap(
DifferentialDiff $target,
DifferentialDiff $diff_vs = null,
PhabricatorRepository $repository = null) {
$load_ids = array();
if ($diff_vs) {
$load_ids[] = $diff_vs->getID();
}
$load_ids[] = $target->getID();
$raw_changesets = id(new DifferentialChangeset())
->loadAllWhere(
'diffID IN (%Ld)',
$load_ids);
$changeset_groups = mgroup($raw_changesets, 'getDiffID');
$changesets = idx($changeset_groups, $target->getID(), array());
$changesets = mpull($changesets, null, 'getID');
$refs = array();
$vs_map = array();
$vs_changesets = array();
if ($diff_vs) {
$vs_id = $diff_vs->getID();
$vs_changesets_path_map = array();
foreach (idx($changeset_groups, $vs_id, array()) as $changeset) {
$path = $changeset->getAbsoluteRepositoryPath($repository, $diff_vs);
$vs_changesets_path_map[$path] = $changeset;
$vs_changesets[$changeset->getID()] = $changeset;
}
foreach ($changesets as $key => $changeset) {
$path = $changeset->getAbsoluteRepositoryPath($repository, $target);
if (isset($vs_changesets_path_map[$path])) {
$vs_map[$changeset->getID()] =
$vs_changesets_path_map[$path]->getID();
$refs[$changeset->getID()] =
$changeset->getID().'/'.$vs_changesets_path_map[$path]->getID();
unset($vs_changesets_path_map[$path]);
} else {
$refs[$changeset->getID()] = $changeset->getID();
}
}
foreach ($vs_changesets_path_map as $path => $changeset) {
$changesets[$changeset->getID()] = $changeset;
$vs_map[$changeset->getID()] = -1;
$refs[$changeset->getID()] = $changeset->getID().'/-1';
}
} else {
foreach ($changesets as $changeset) {
$refs[$changeset->getID()] = $changeset->getID();
}
}
$changesets = msort($changesets, 'getSortKey');
return array($changesets, $vs_map, $vs_changesets, $refs);
}
private function loadAuxiliaryFields(DifferentialRevision $revision) {
$aux_fields = DifferentialFieldSelector::newSelector()
->getFieldSpecifications();
foreach ($aux_fields as $key => $aux_field) {
if (!$aux_field->shouldAppearOnRevisionView()) {
unset($aux_fields[$key]);
} else {
$aux_field->setUser($this->getRequest()->getUser());
}
}
$aux_fields = DifferentialAuxiliaryField::loadFromStorage(
$revision,
$aux_fields);
return $aux_fields;
}
private function buildSymbolIndexes(
PhabricatorRepositoryArcanistProject $arc_project,
array $visible_changesets) {
assert_instances_of($visible_changesets, 'DifferentialChangeset');
$engine = PhabricatorSyntaxHighlighter::newEngine();
$langs = $arc_project->getSymbolIndexLanguages();
if (!$langs) {
return array(array(), array());
}
$symbol_indexes = array();
$project_phids = array_merge(
array($arc_project->getPHID()),
nonempty($arc_project->getSymbolIndexProjects(), array()));
$indexed_langs = array_fill_keys($langs, true);
foreach ($visible_changesets as $key => $changeset) {
$lang = $engine->getLanguageFromFilename($changeset->getFilename());
if (isset($indexed_langs[$lang])) {
$symbol_indexes[$key] = array(
'lang' => $lang,
'projects' => $project_phids,
);
}
}
return array($symbol_indexes, $project_phids);
}
private function loadOtherRevisions(
array $changesets,
DifferentialDiff $target,
PhabricatorRepository $repository) {
assert_instances_of($changesets, 'DifferentialChangeset');
$paths = array();
foreach ($changesets as $changeset) {
$paths[] = $changeset->getAbsoluteRepositoryPath(
$repository,
$target);
}
if (!$paths) {
return array();
}
$path_map = id(new DiffusionPathIDQuery($paths))->loadPathIDs();
if (!$path_map) {
return array();
}
$query = id(new DifferentialRevisionQuery())
->withStatus(DifferentialRevisionQuery::STATUS_OPEN)
->setOrder(DifferentialRevisionQuery::ORDER_PATH_MODIFIED)
->setLimit(10)
->needRelationships(true);
foreach ($path_map as $path => $path_id) {
$query->withPath($repository->getID(), $path_id);
}
$results = $query->execute();
// Strip out *this* revision.
foreach ($results as $key => $result) {
if ($result->getID() == $this->revisionID) {
unset($results[$key]);
}
}
return $results;
}
private function renderOtherRevisions(array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$view = id(new DifferentialRevisionListView())
->setRevisions($revisions)
->setFields(DifferentialRevisionListView::getDefaultFields())
->setUser($this->getRequest()->getUser())
->loadAssets();
$phids = $view->getRequiredHandlePHIDs();
$handles = $this->loadViewerHandles($phids);
$view->setHandles($handles);
return
'<div class="differential-panel">'.
'<h1>Open Revisions Affecting These Files</h1>'.
$view->render().
'</div>';
}
/**
* Straight copy of the loadFileByPhid method in
* @{class:DifferentialReviewRequestMail}.
*
* This is because of the code similarity between the buildPatch method in
* @{class:DifferentialReviewRequestMail} and @{method:buildRawDiffResponse}
* in this class. Both of these methods end up using call_user_func and this
* piece of code is the lucky function.
*
* @return mixed (@{class:PhabricatorFile} if found, null if not)
*/
public function loadFileByPHID($phid) {
$file = id(new PhabricatorFile())->loadOneWhere(
'phid = %s',
$phid);
if (!$file) {
return null;
}
return $file->loadFileData();
}
/**
* Note this code is somewhat similar to the buildPatch method in
* @{class:DifferentialReviewRequestMail}.
*
* @return @{class:AphrontRedirectResponse}
*/
private function buildRawDiffResponse(
array $changesets,
array $vs_changesets,
array $vs_map,
PhabricatorRepository $repository = null) {
assert_instances_of($changesets, 'DifferentialChangeset');
assert_instances_of($vs_changesets, 'DifferentialChangeset');
$engine = new PhabricatorDifferenceEngine();
$generated_changesets = array();
foreach ($changesets as $changeset) {
$changeset->attachHunks($changeset->loadHunks());
$right = $changeset->makeNewFile();
$choice = $changeset;
$vs = idx($vs_map, $changeset->getID());
if ($vs == -1) {
$left = $right;
$right = $changeset->makeOldFile();
} else if ($vs) {
$choice = $vs_changeset = $vs_changesets[$vs];
$vs_changeset->attachHunks($vs_changeset->loadHunks());
$left = $vs_changeset->makeNewFile();
} else {
$left = $changeset->makeOldFile();
}
$synthetic = $engine->generateChangesetFromFileContent(
$left,
$right);
if (!$synthetic->getAffectedLineCount()) {
$filetype = $choice->getFileType();
if ($filetype == DifferentialChangeType::FILE_TEXT ||
$filetype == DifferentialChangeType::FILE_SYMLINK) {
continue;
}
}
$choice->attachHunks($synthetic->getHunks());
$generated_changesets[] = $choice;
}
$diff = new DifferentialDiff();
$diff->attachChangesets($generated_changesets);
$diff_dict = $diff->getDiffDict();
$changes = array();
foreach ($diff_dict['changes'] as $changedict) {
$changes[] = ArcanistDiffChange::newFromDictionary($changedict);
}
$bundle = ArcanistBundle::newFromChanges($changes);
$bundle->setLoadFileDataCallback(array($this, 'loadFileByPHID'));
$vcs = $repository ? $repository->getVersionControlSystem() : null;
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$raw_diff = $bundle->toGitPatch();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
default:
$raw_diff = $bundle->toUnifiedDiff();
break;
}
$request_uri = $this->getRequest()->getRequestURI();
// this ends up being something like
// D123.diff
// or the verbose
// D123.vs123.id123.whitespaceignore-all.diff
// lame but nice to include these options
$file_name = ltrim($request_uri->getPath(), '/').'.';
foreach ($request_uri->getQueryParams() as $key => $value) {
if ($key == 'download') {
continue;
}
$file_name .= $key.$value.'.';
}
$file_name .= 'diff';
$file = PhabricatorFile::buildFromFileDataOrHash(
$raw_diff,
array(
'name' => $file_name,
));
return id(new AphrontRedirectResponse())->setURI($file->getBestURI());
}
private function buildSideNavView(
DifferentialRevision $revision,
array $changesets) {
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI('/D'.$revision->getID()));
$nav->setFlexible(true);
$nav->addFilter('top', 'D'.$revision->getID(), '#top',
$relative = false,
'phabricator-active-nav-focus');
$tree = new PhutilFileTree();
foreach ($changesets as $changeset) {
try {
$tree->addPath($changeset->getFilename(), $changeset);
} catch (Exception $ex) {
// TODO: See T1702. When viewing the versus diff of diffs, we may
// have files with the same filename. For example, if you have a setup
// like this in SVN:
//
// a/
// README
// b/
// README
//
// ...and you run "arc diff" once from a/, and again from b/, you'll
// get two diffs with path README. However, in the versus diff view we
// will compute their absolute repository paths and detect that they
// aren't really the same file. This is correct, but causes us to
// throw when inserting them.
//
// We should probably compute the smallest unique path for each file
// and show these as "a/README" and "b/README" when diffed against
// one another. However, we get this wrong in a lot of places (the
// other TOC shows two "README" files, and we generate the same anchor
// hash for both) so I'm just stopping the bleeding until we can get
// a proper fix in place.
}
}
require_celerity_resource('phabricator-filetree-view-css');
$filetree = array();
$path = $tree;
while (($path = $path->getNextNode())) {
$data = $path->getData();
$name = $path->getName();
$style = 'padding-left: '.(2 + (3 * $path->getDepth())).'px';
$href = null;
if ($data) {
$href = '#'.$data->getAnchorName();
$title = $name;
$icon = 'phabricator-filetree-icon-file';
} else {
$name .= '/';
$title = $path->getFullPath().'/';
$icon = 'phabricator-filetree-icon-dir';
}
$icon = phutil_render_tag(
'span',
array(
'class' => 'phabricator-filetree-icon '.$icon,
),
'');
$name_element = phutil_render_tag(
'span',
array(
'class' => 'phabricator-filetree-name',
),
phutil_escape_html($name));
$filetree[] = javelin_render_tag(
$href ? 'a' : 'span',
array(
'href' => $href,
'style' => $style,
'title' => $title,
'class' => 'phabricator-filetree-item',
),
$icon.$name_element);
}
$tree->destroy();
$filetree =
'<div class="phabricator-filetree">'.
implode("\n", $filetree).
'</div>';
$nav->addFilter('toc', 'Table of Contents', '#toc');
$nav->addCustomBlock($filetree);
$nav->addFilter('comment', 'Add Comment', '#comment');
$nav->setActive(true);
return $nav;
}
}
diff --git a/src/applications/differential/controller/DifferentialSubscribeController.php b/src/applications/differential/controller/DifferentialSubscribeController.php
index de72ff86fc..32ce5cecef 100644
--- a/src/applications/differential/controller/DifferentialSubscribeController.php
+++ b/src/applications/differential/controller/DifferentialSubscribeController.php
@@ -1,92 +1,76 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialSubscribeController extends DifferentialController {
private $id;
private $action;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
$this->action = $data['action'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$revision = id(new DifferentialRevision())->load($this->id);
if (!$revision) {
return new Aphront404Response();
}
if (!$request->isFormPost()) {
$dialog = new AphrontDialogView();
switch ($this->action) {
case 'add':
$button = 'Subscribe';
$title = 'Subscribe to Revision';
$prompt = 'Really subscribe to this revision?';
break;
case 'rem':
$button = 'Unsubscribe';
$title = 'Unsubscribe from Revision';
$prompt = 'Really unsubscribe from this revision? Herald will '.
'not resubscribe you to a revision you unsubscribe '.
'from.';
break;
default:
return new Aphront400Response();
}
$dialog
->setUser($user)
->setTitle($title)
->appendChild('<p>'.$prompt.'</p>')
->setSubmitURI($request->getRequestURI())
->addSubmitButton($button)
->addCancelButton('/D'.$revision->getID());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$revision->loadRelationships();
$phid = $user->getPHID();
switch ($this->action) {
case 'add':
DifferentialRevisionEditor::addCCAndUpdateRevision(
$revision,
$phid,
$phid);
break;
case 'rem':
DifferentialRevisionEditor::removeCCAndUpdateRevision(
$revision,
$phid,
$phid);
break;
default:
return new Aphront400Response();
}
return id(new AphrontRedirectResponse())->setURI('/D'.$revision->getID());
}
}
diff --git a/src/applications/differential/data/DifferentialRevisionListData.php b/src/applications/differential/data/DifferentialRevisionListData.php
index 2e567e28a9..9b21327108 100644
--- a/src/applications/differential/data/DifferentialRevisionListData.php
+++ b/src/applications/differential/data/DifferentialRevisionListData.php
@@ -1,318 +1,302 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialRevisionListData {
const QUERY_OPEN_OWNED = 'open';
const QUERY_OPEN_REVIEWER = 'reviewer';
const QUERY_OWNED = 'owned';
const QUERY_OWNED_OR_REVIEWER = 'related';
const QUERY_NEED_ACTION_FROM_OTHERS = 'need-other-action';
const QUERY_NEED_ACTION_FROM_SELF = 'need-self-action';
const QUERY_COMMITTABLE = 'committable';
const QUERY_REVISION_IDS = 'revision-ids';
const QUERY_PHIDS = 'phids';
const QUERY_CC = 'cc';
const QUERY_ALL_OPEN = 'all-open';
private $ids;
private $filter;
private $handles;
private $revisions;
private $order;
public function __construct($filter, array $ids) {
$this->filter = $filter;
$this->ids = $ids;
}
public function getRevisions() {
return $this->revisions;
}
public function setOrder($order) {
$this->order = $order;
return $this;
}
public function loadRevisions() {
switch ($this->filter) {
case self::QUERY_CC:
$this->revisions = $this->loadAllOpenWithCCs($this->ids);
break;
case self::QUERY_ALL_OPEN:
$this->revisions = $this->loadAllOpen();
break;
case self::QUERY_OPEN_OWNED:
$this->revisions = $this->loadAllWhere(
'revision.status in (%Ld) AND revision.authorPHID in (%Ls)',
$this->getOpenStatuses(),
$this->ids);
break;
case self::QUERY_COMMITTABLE:
$this->revisions = $this->loadAllWhere(
'revision.status in (%Ld) AND revision.authorPHID in (%Ls)',
array(
ArcanistDifferentialRevisionStatus::ACCEPTED,
),
$this->ids);
break;
case self::QUERY_REVISION_IDS:
$this->revisions = $this->loadAllWhere(
'id in (%Ld)',
$this->ids);
break;
case self::QUERY_OPEN_REVIEWER:
$this->revisions = $this->loadAllWhereJoinReview(
'revision.status in (%Ld) AND relationship.objectPHID in (%Ls)',
$this->getOpenStatuses(),
$this->ids);
break;
case self::QUERY_OWNED:
$this->revisions = $this->loadAllWhere(
'revision.authorPHID in (%Ls)',
$this->ids);
break;
case self::QUERY_OWNED_OR_REVIEWER:
$rev = new DifferentialRevision();
$data = queryfx_all(
$rev->establishConnection('r'),
'SELECT revs.* FROM (
(
SELECT revision.*
FROM %T revision
WHERE revision.authorPHID in (%Ls)
)
UNION
(
SELECT revision.*
FROM %T revision, %T rel
WHERE rel.revisionId = revision.Id
AND rel.relation = %s
AND rel.objectPHID in (%Ls)
)
) as revs
%Q',
$rev->getTableName(),
$this->ids,
$rev->getTableName(),
DifferentialRevision::RELATIONSHIP_TABLE,
DifferentialRevision::RELATION_REVIEWER,
$this->ids,
$this->getOrderClause());
$this->revisions = $rev->loadAllFromArray($data);
break;
case self::QUERY_NEED_ACTION_FROM_SELF:
$rev = new DifferentialRevision();
$data = queryfx_all(
$rev->establishConnection('r'),
'SELECT revision.* FROM %T revision
WHERE revision.authorPHID in (%Ls)
AND revision.status in (%Ld)
UNION ALL
SELECT revision.* FROM %T revision JOIN %T relationship
ON relationship.revisionID = revision.id
AND relationship.relation = %s
WHERE relationship.objectPHID IN (%Ls)
AND revision.status in (%Ld)
%Q',
$rev->getTableName(),
$this->ids,
array(
ArcanistDifferentialRevisionStatus::NEEDS_REVISION,
ArcanistDifferentialRevisionStatus::ACCEPTED,
),
$rev->getTableName(),
DifferentialRevision::RELATIONSHIP_TABLE,
DifferentialRevision::RELATION_REVIEWER,
$this->ids,
array(
ArcanistDifferentialRevisionStatus::NEEDS_REVIEW,
),
$this->getOrderClause());
$data = ipull($data, null, 'id');
$this->revisions = $rev->loadAllFromArray($data);
break;
case self::QUERY_NEED_ACTION_FROM_OTHERS:
$rev = new DifferentialRevision();
$data = queryfx_all(
$rev->establishConnection('r'),
'SELECT revision.* FROM %T revision
WHERE revision.authorPHID in (%Ls)
AND revision.status IN (%Ld)
UNION ALL
SELECT revision.* FROM %T revision JOIN %T relationship
ON relationship.revisionID = revision.id
AND relationship.relation = %s
WHERE relationship.objectPHID IN (%Ls)
AND revision.status in (%Ld)
%Q',
$rev->getTableName(),
$this->ids,
array(
ArcanistDifferentialRevisionStatus::NEEDS_REVIEW,
),
$rev->getTableName(),
DifferentialRevision::RELATIONSHIP_TABLE,
DifferentialRevision::RELATION_REVIEWER,
$this->ids,
array(
ArcanistDifferentialRevisionStatus::NEEDS_REVISION,
ArcanistDifferentialRevisionStatus::ACCEPTED,
),
$this->getOrderClause());
$data = ipull($data, null, 'id');
$this->revisions = $rev->loadAllFromArray($data);
break;
case self::QUERY_PHIDS:
$this->revisions = $this->loadAllWhere(
'revision.phid in (%Ls)',
$this->ids);
break;
}
return $this->revisions;
}
private function getOpenStatuses() {
return array(
ArcanistDifferentialRevisionStatus::NEEDS_REVIEW,
ArcanistDifferentialRevisionStatus::NEEDS_REVISION,
ArcanistDifferentialRevisionStatus::ACCEPTED,
);
}
private function loadAllOpen() {
return $this->loadAllWhere('status in (%Ld)', $this->getOpenStatuses());
}
private function loadAllWhereJoinReview($pattern) {
$reviewer = DifferentialRevision::RELATION_REVIEWER;
$argv = func_get_args();
$rev = new DifferentialRevision();
$pattern = array_shift($argv);
$pattern =
'SELECT revision.*
FROM %T revision LEFT JOIN %T relationship
ON revision.id = relationship.revisionID
AND relationship.relation = %s
WHERE '.$pattern.'
GROUP BY revision.id '.$this->getOrderClause();
array_unshift(
$argv,
$rev->getTableName(),
DifferentialRevision::RELATIONSHIP_TABLE,
DifferentialRevision::RELATION_REVIEWER);
$data = vqueryfx_all(
$rev->establishConnection('r'),
$pattern,
$argv);
return $rev->loadAllFromArray($data);
}
private function loadAllWhere($pattern) {
$rev = new DifferentialRevision();
$argv = func_get_args();
array_shift($argv);
array_unshift($argv, $rev->getTableName());
$data = vqueryfx_all(
$rev->establishConnection('r'),
'SELECT * FROM %T revision WHERE '.$pattern.' '.$this->getOrderClause(),
$argv);
return $rev->loadAllFromArray($data);
}
private function loadAllOpenWithCCs(array $ccphids) {
$rev = new DifferentialRevision();
$revision = new DifferentialRevision();
$data = queryfx_all(
$rev->establishConnection('r'),
'SELECT revision.* FROM %T revision
JOIN %T relationship ON relationship.revisionID = revision.id
AND relationship.relation = %s
AND relationship.objectPHID in (%Ls)
WHERE revision.status in (%Ld) %Q',
$revision->getTableName(),
DifferentialRevision::RELATIONSHIP_TABLE,
DifferentialRevision::RELATION_SUBSCRIBED,
$ccphids,
$this->getOpenStatuses(),
$this->getOrderClause());
return $revision->loadAllFromArray($data);
}
private function getOrderClause() {
$reverse = false;
$order = $this->order;
if (strlen($order) && $order[0] == '-') {
$reverse = true;
$order = substr($order, 1);
}
$asc = $reverse ? 'DESC' : 'ASC';
switch ($order) {
case 'ID':
$clause = 'id';
break;
case 'Revision':
$clause = 'name';
break;
case 'Status':
$clause = 'status';
break;
case 'Lines':
$clause = 'lineCount';
break;
case 'Created':
$clause = 'dateCreated';
$asc = $reverse ? 'ASC' : 'DESC';
break;
case '':
case 'Modified':
$clause = 'dateModified';
$asc = $reverse ? 'ASC' : 'DESC';
break;
default:
throw new Exception("Invalid order '{$order}'.");
}
return "ORDER BY {$clause} {$asc}";
}
}
diff --git a/src/applications/differential/editor/DifferentialCommentEditor.php b/src/applications/differential/editor/DifferentialCommentEditor.php
index 207edf3cfa..3826c29397 100644
--- a/src/applications/differential/editor/DifferentialCommentEditor.php
+++ b/src/applications/differential/editor/DifferentialCommentEditor.php
@@ -1,687 +1,671 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialCommentEditor extends PhabricatorEditor {
protected $revision;
protected $action;
protected $attachInlineComments;
protected $message;
protected $changedByCommit;
protected $addedReviewers = array();
protected $removedReviewers = array();
private $addedCCs = array();
private $parentMessageID;
private $contentSource;
private $noEmail;
private $isDaemonWorkflow;
public function __construct(
DifferentialRevision $revision,
$action) {
$this->revision = $revision;
$this->action = $action;
}
public function setParentMessageID($parent_message_id) {
$this->parentMessageID = $parent_message_id;
return $this;
}
public function setMessage($message) {
$this->message = $message;
return $this;
}
public function setAttachInlineComments($attach) {
$this->attachInlineComments = $attach;
return $this;
}
public function setChangedByCommit($changed_by_commit) {
$this->changedByCommit = $changed_by_commit;
return $this;
}
public function getChangedByCommit() {
return $this->changedByCommit;
}
public function setAddedReviewers(array $added_reviewers) {
$this->addedReviewers = $added_reviewers;
return $this;
}
public function getAddedReviewers() {
return $this->addedReviewers;
}
public function setRemovedReviewers(array $removeded_reviewers) {
$this->removedReviewers = $removeded_reviewers;
return $this;
}
public function getRemovedReviewers() {
return $this->removedReviewers;
}
public function setAddedCCs($added_ccs) {
$this->addedCCs = $added_ccs;
return $this;
}
public function getAddedCCs() {
return $this->addedCCs;
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source;
return $this;
}
public function setIsDaemonWorkflow($is_daemon) {
$this->isDaemonWorkflow = $is_daemon;
return $this;
}
public function setNoEmail($no_email) {
$this->noEmail = $no_email;
return $this;
}
public function save() {
$actor = $this->requireActor();
$revision = $this->revision;
$action = $this->action;
$actor_phid = $actor->getPHID();
$actor_is_author = ($actor_phid == $revision->getAuthorPHID());
$allow_self_accept = PhabricatorEnv::getEnvConfig(
'differential.allow-self-accept', false);
$always_allow_close = PhabricatorEnv::getEnvConfig(
'differential.always-allow-close', false);
$revision_status = $revision->getStatus();
$revision->loadRelationships();
$reviewer_phids = $revision->getReviewers();
if ($reviewer_phids) {
$reviewer_phids = array_combine($reviewer_phids, $reviewer_phids);
}
$metadata = array();
$inline_comments = array();
if ($this->attachInlineComments) {
$inline_comments = id(new DifferentialInlineComment())->loadAllWhere(
'authorPHID = %s AND revisionID = %d AND commentID IS NULL',
$actor_phid,
$revision->getID());
}
switch ($action) {
case DifferentialAction::ACTION_COMMENT:
if (!$this->message && !$inline_comments) {
throw new DifferentialActionHasNoEffectException(
"You are submitting an empty comment with no action: ".
"you must act on the revision or post a comment.");
}
break;
case DifferentialAction::ACTION_RESIGN:
if ($actor_is_author) {
throw new Exception('You can not resign from your own revision!');
}
if (empty($reviewer_phids[$actor_phid])) {
throw new DifferentialActionHasNoEffectException(
"You can not resign from this revision because you are not ".
"a reviewer.");
}
DifferentialRevisionEditor::alterReviewers(
$revision,
$reviewer_phids,
$rem = array($actor_phid),
$add = array(),
$actor_phid);
break;
case DifferentialAction::ACTION_ABANDON:
if (!$actor_is_author) {
throw new Exception('You can only abandon your own revisions.');
}
if ($revision_status == ArcanistDifferentialRevisionStatus::CLOSED) {
throw new DifferentialActionHasNoEffectException(
"You can not abandon this revision because it has already ".
"been closed.");
}
if ($revision_status == ArcanistDifferentialRevisionStatus::ABANDONED) {
throw new DifferentialActionHasNoEffectException(
"You can not abandon this revision because it has already ".
"been abandoned.");
}
$revision->setStatus(ArcanistDifferentialRevisionStatus::ABANDONED);
break;
case DifferentialAction::ACTION_ACCEPT:
if ($actor_is_author && !$allow_self_accept) {
throw new Exception('You can not accept your own revision.');
}
if (($revision_status !=
ArcanistDifferentialRevisionStatus::NEEDS_REVIEW) &&
($revision_status !=
ArcanistDifferentialRevisionStatus::NEEDS_REVISION)) {
switch ($revision_status) {
case ArcanistDifferentialRevisionStatus::ACCEPTED:
throw new DifferentialActionHasNoEffectException(
"You can not accept this revision because someone else ".
"already accepted it.");
case ArcanistDifferentialRevisionStatus::ABANDONED:
throw new DifferentialActionHasNoEffectException(
"You can not accept this revision because it has been ".
"abandoned.");
case ArcanistDifferentialRevisionStatus::CLOSED:
throw new DifferentialActionHasNoEffectException(
"You can not accept this revision because it has already ".
"been closed.");
default:
throw new Exception(
"Unexpected revision state '{$revision_status}'!");
}
}
$revision
->setStatus(ArcanistDifferentialRevisionStatus::ACCEPTED);
if (!isset($reviewer_phids[$actor_phid])) {
DifferentialRevisionEditor::alterReviewers(
$revision,
$reviewer_phids,
$rem = array(),
$add = array($actor_phid),
$actor_phid);
}
break;
case DifferentialAction::ACTION_REQUEST:
if (!$actor_is_author) {
throw new Exception('You must own a revision to request review.');
}
switch ($revision_status) {
case ArcanistDifferentialRevisionStatus::ACCEPTED:
case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
$revision->setStatus(
ArcanistDifferentialRevisionStatus::NEEDS_REVIEW);
break;
case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
throw new DifferentialActionHasNoEffectException(
"You can not request review of this revision because it has ".
"been abandoned.");
case ArcanistDifferentialRevisionStatus::ABANDONED:
throw new DifferentialActionHasNoEffectException(
"You can not request review of this revision because it has ".
"been abandoned.");
case ArcanistDifferentialRevisionStatus::CLOSED:
throw new DifferentialActionHasNoEffectException(
"You can not request review of this revision because it has ".
"already been closed.");
default:
throw new Exception(
"Unexpected revision state '{$revision_status}'!");
}
list($added_reviewers, $ignored) = $this->alterReviewers();
if ($added_reviewers) {
$key = DifferentialComment::METADATA_ADDED_REVIEWERS;
$metadata[$key] = $added_reviewers;
}
break;
case DifferentialAction::ACTION_REJECT:
if ($actor_is_author) {
throw new Exception(
'You can not request changes to your own revision.');
}
switch ($revision_status) {
case ArcanistDifferentialRevisionStatus::ACCEPTED:
case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
// NOTE: We allow you to reject an already-rejected revision
// because it doesn't create any ambiguity and avoids a rather
// needless dialog.
break;
case ArcanistDifferentialRevisionStatus::ABANDONED:
throw new DifferentialActionHasNoEffectException(
"You can not request changes to this revision because it has ".
"been abandoned.");
case ArcanistDifferentialRevisionStatus::CLOSED:
throw new DifferentialActionHasNoEffectException(
"You can not request changes to this revision because it has ".
"already been closed.");
default:
throw new Exception(
"Unexpected revision state '{$revision_status}'!");
}
if (!isset($reviewer_phids[$actor_phid])) {
DifferentialRevisionEditor::alterReviewers(
$revision,
$reviewer_phids,
$rem = array(),
$add = array($actor_phid),
$actor_phid);
}
$revision
->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVISION);
break;
case DifferentialAction::ACTION_RETHINK:
if (!$actor_is_author) {
throw new Exception(
"You can not plan changes to somebody else's revision");
}
switch ($revision_status) {
case ArcanistDifferentialRevisionStatus::ACCEPTED:
case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
break;
case ArcanistDifferentialRevisionStatus::ABANDONED:
throw new DifferentialActionHasNoEffectException(
"You can not plan changes to this revision because it has ".
"been abandoned.");
case ArcanistDifferentialRevisionStatus::CLOSED:
throw new DifferentialActionHasNoEffectException(
"You can not plan changes to this revision because it has ".
"already been closed.");
default:
throw new Exception(
"Unexpected revision state '{$revision_status}'!");
}
$revision
->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVISION);
break;
case DifferentialAction::ACTION_RECLAIM:
if (!$actor_is_author) {
throw new Exception('You can not reclaim a revision you do not own.');
}
if ($revision_status != ArcanistDifferentialRevisionStatus::ABANDONED) {
throw new DifferentialActionHasNoEffectException(
"You can not reclaim this revision because it is not abandoned.");
}
$revision
->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVIEW);
break;
case DifferentialAction::ACTION_CLOSE:
// NOTE: The daemons can mark things closed from any state. We treat
// them as completely authoritative.
if (!$this->isDaemonWorkflow) {
if (!$actor_is_author && !$always_allow_close) {
throw new Exception(
"You can not mark a revision you don't own as closed.");
}
$status_closed = ArcanistDifferentialRevisionStatus::CLOSED;
$status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED;
if ($revision_status == $status_closed) {
throw new DifferentialActionHasNoEffectException(
"You can not mark this revision as closed because it has ".
"already been marked as closed.");
}
if ($revision_status != $status_accepted) {
throw new DifferentialActionHasNoEffectException(
"You can not mark this revision as closed because it is ".
"has not been accepted.");
}
}
if (!$revision->getDateCommitted()) {
$revision->setDateCommitted(time());
}
$revision->setStatus(ArcanistDifferentialRevisionStatus::CLOSED);
break;
case DifferentialAction::ACTION_ADDREVIEWERS:
list($added_reviewers, $ignored) = $this->alterReviewers();
if ($added_reviewers) {
$key = DifferentialComment::METADATA_ADDED_REVIEWERS;
$metadata[$key] = $added_reviewers;
} else {
$user_tried_to_add = count($this->getAddedReviewers());
if ($user_tried_to_add == 0) {
throw new DifferentialActionHasNoEffectException(
"You can not add reviewers, because you did not specify any ".
"reviewers.");
} else if ($user_tried_to_add == 1) {
throw new DifferentialActionHasNoEffectException(
"You can not add that reviewer, because they are already an ".
"author or reviewer.");
} else {
throw new DifferentialActionHasNoEffectException(
"You can not add those reviewers, because they are all already ".
"authors or reviewers.");
}
}
break;
case DifferentialAction::ACTION_ADDCCS:
$added_ccs = $this->getAddedCCs();
$user_tried_to_add = count($added_ccs);
$added_ccs = $this->filterAddedCCs($added_ccs);
if ($added_ccs) {
foreach ($added_ccs as $cc) {
DifferentialRevisionEditor::addCC(
$revision,
$cc,
$actor_phid);
}
$key = DifferentialComment::METADATA_ADDED_CCS;
$metadata[$key] = $added_ccs;
} else {
if ($user_tried_to_add == 0) {
throw new DifferentialActionHasNoEffectException(
"You can not add CCs, because you did not specify any ".
"CCs.");
} else if ($user_tried_to_add == 1) {
throw new DifferentialActionHasNoEffectException(
"You can not add that CC, because they are already an ".
"author, reviewer or CC.");
} else {
throw new DifferentialActionHasNoEffectException(
"You can not add those CCs, because they are all already ".
"authors, reviewers or CCs.");
}
}
break;
case DifferentialAction::ACTION_CLAIM:
if ($actor_is_author) {
throw new Exception("You can not commandeer your own revision.");
}
switch ($revision_status) {
case ArcanistDifferentialRevisionStatus::CLOSED:
throw new DifferentialActionHasNoEffectException(
"You can not commandeer this revision because it has ".
"already been closed.");
break;
}
$this->setAddedReviewers(array($revision->getAuthorPHID()));
$this->setRemovedReviewers(array($actor_phid));
// NOTE: Set the new author PHID before calling addReviewers(), since it
// doesn't permit the author to become a reviewer.
$revision->setAuthorPHID($actor_phid);
list($added_reviewers, $removed_reviewers) = $this->alterReviewers();
if ($added_reviewers) {
$key = DifferentialComment::METADATA_ADDED_REVIEWERS;
$metadata[$key] = $added_reviewers;
}
if ($removed_reviewers) {
$key = DifferentialComment::METADATA_REMOVED_REVIEWERS;
$metadata[$key] = $removed_reviewers;
}
break;
default:
throw new Exception('Unsupported action.');
}
// Update information about reviewer in charge.
if ($action == DifferentialAction::ACTION_ACCEPT ||
$action == DifferentialAction::ACTION_REJECT) {
$revision->setLastReviewerPHID($actor_phid);
}
// TODO: Call beginReadLocking() prior to loading the revision.
$revision->openTransaction();
// Always save the revision (even if we didn't actually change any of its
// properties) so that it jumps to the top of the revision list when sorted
// by "updated". Notably, this allows "ping" comments to push it to the
// top of the action list.
$revision->save();
if ($action != DifferentialAction::ACTION_RESIGN) {
DifferentialRevisionEditor::addCC(
$revision,
$actor_phid,
$actor_phid);
}
$comment = id(new DifferentialComment())
->setAuthorPHID($actor_phid)
->setRevisionID($revision->getID())
->setAction($action)
->setContent((string)$this->message)
->setMetadata($metadata);
if ($this->contentSource) {
$comment->setContentSource($this->contentSource);
}
$comment->save();
$changesets = array();
if ($inline_comments) {
$load_ids = mpull($inline_comments, 'getChangesetID');
if ($load_ids) {
$load_ids = array_unique($load_ids);
$changesets = id(new DifferentialChangeset())->loadAllWhere(
'id in (%Ld)',
$load_ids);
}
foreach ($inline_comments as $inline) {
$inline->setCommentID($comment->getID());
$inline->save();
}
}
// Find any "@mentions" in the comment blocks.
$content_blocks = array($comment->getContent());
foreach ($inline_comments as $inline) {
$content_blocks[] = $inline->getContent();
}
$mention_ccs = PhabricatorMarkupEngine::extractPHIDsFromMentions(
$content_blocks);
if ($mention_ccs) {
$mention_ccs = $this->filterAddedCCs($mention_ccs);
if ($mention_ccs) {
$metadata = $comment->getMetadata();
$metacc = idx(
$metadata,
DifferentialComment::METADATA_ADDED_CCS,
array());
foreach ($mention_ccs as $cc_phid) {
DifferentialRevisionEditor::addCC(
$revision,
$cc_phid,
$actor_phid);
$metacc[] = $cc_phid;
}
$metadata[DifferentialComment::METADATA_ADDED_CCS] = $metacc;
$comment->setMetadata($metadata);
$comment->save();
}
}
$revision->saveTransaction();
$phids = array($actor_phid);
$handles = id(new PhabricatorObjectHandleData($phids))
->loadHandles();
$actor_handle = $handles[$actor_phid];
$xherald_header = HeraldTranscript::loadXHeraldRulesHeader(
$revision->getPHID());
$mailed_phids = array();
if (!$this->noEmail) {
$mail = id(new DifferentialCommentMail(
$revision,
$actor_handle,
$comment,
$changesets,
$inline_comments))
->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs())
->setToPHIDs(
array_merge(
$revision->getReviewers(),
array($revision->getAuthorPHID())))
->setCCPHIDs($revision->getCCPHIDs())
->setChangedByCommit($this->getChangedByCommit())
->setXHeraldRulesHeader($xherald_header)
->setParentMessageID($this->parentMessageID)
->send();
$mailed_phids = $mail->getRawMail()->buildRecipientList();
}
$event_data = array(
'revision_id' => $revision->getID(),
'revision_phid' => $revision->getPHID(),
'revision_name' => $revision->getTitle(),
'revision_author_phid' => $revision->getAuthorPHID(),
'action' => $comment->getAction(),
'feedback_content' => $comment->getContent(),
'actor_phid' => $actor_phid,
);
// TODO: Get rid of this
id(new PhabricatorTimelineEvent('difx', $event_data))
->recordEvent();
id(new PhabricatorFeedStoryPublisher())
->setStoryType('PhabricatorFeedStoryDifferential')
->setStoryData($event_data)
->setStoryTime(time())
->setStoryAuthorPHID($actor_phid)
->setRelatedPHIDs(
array(
$revision->getPHID(),
$actor_phid,
$revision->getAuthorPHID(),
))
->setPrimaryObjectPHID($revision->getPHID())
->setSubscribedPHIDs(
array_merge(
array($revision->getAuthorPHID()),
$revision->getReviewers(),
$revision->getCCPHIDs()))
->setMailRecipientPHIDs($mailed_phids)
->publish();
// TODO: Move to workers
PhabricatorSearchDifferentialIndexer::indexRevision($revision);
return $comment;
}
private function filterAddedCCs(array $ccs) {
$revision = $this->revision;
$current_ccs = $revision->getCCPHIDs();
$current_ccs = array_fill_keys($current_ccs, true);
$reviewer_phids = $revision->getReviewers();
$reviewer_phids = array_fill_keys($reviewer_phids, true);
foreach ($ccs as $key => $cc) {
if (isset($current_ccs[$cc])) {
unset($ccs[$key]);
}
if (isset($reviewer_phids[$cc])) {
unset($ccs[$key]);
}
if ($cc == $revision->getAuthorPHID()) {
unset($ccs[$key]);
}
}
return $ccs;
}
private function alterReviewers() {
$actor_phid = $this->getActor()->getPHID();
$revision = $this->revision;
$added_reviewers = $this->getAddedReviewers();
$removed_reviewers = $this->getRemovedReviewers();
$reviewer_phids = $revision->getReviewers();
$allow_self_accept = PhabricatorEnv::getEnvConfig(
'differential.allow-self-accept', false);
$reviewer_phids_map = array_fill_keys($reviewer_phids, true);
foreach ($added_reviewers as $k => $user_phid) {
if (!$allow_self_accept && $user_phid == $revision->getAuthorPHID()) {
unset($added_reviewers[$k]);
}
if (isset($reviewer_phids_map[$user_phid])) {
unset($added_reviewers[$k]);
}
}
foreach ($removed_reviewers as $k => $user_phid) {
if (!isset($reviewer_phids_map[$user_phid])) {
unset($removed_reviewers[$k]);
}
}
$added_reviewers = array_unique($added_reviewers);
$removed_reviewers = array_unique($removed_reviewers);
if ($added_reviewers) {
DifferentialRevisionEditor::alterReviewers(
$revision,
$reviewer_phids,
$removed_reviewers,
$added_reviewers,
$actor_phid);
}
return array($added_reviewers, $removed_reviewers);
}
}
diff --git a/src/applications/differential/editor/DifferentialRevisionEditor.php b/src/applications/differential/editor/DifferentialRevisionEditor.php
index fc4a4b04e0..37f7b29119 100644
--- a/src/applications/differential/editor/DifferentialRevisionEditor.php
+++ b/src/applications/differential/editor/DifferentialRevisionEditor.php
@@ -1,965 +1,949 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Handle major edit operations to DifferentialRevision -- adding and removing
* reviewers, diffs, and CCs. Unlike simple edits, these changes trigger
* complicated email workflows.
*/
final class DifferentialRevisionEditor extends PhabricatorEditor {
protected $revision;
protected $cc = null;
protected $reviewers = null;
protected $diff;
protected $comments;
protected $silentUpdate;
private $auxiliaryFields = array();
private $contentSource;
public function __construct(DifferentialRevision $revision) {
$this->revision = $revision;
}
public static function newRevisionFromConduitWithDiff(
array $fields,
DifferentialDiff $diff,
PhabricatorUser $actor) {
$revision = new DifferentialRevision();
$revision->setPHID($revision->generatePHID());
$revision->setAuthorPHID($actor->getPHID());
$revision->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVIEW);
$editor = new DifferentialRevisionEditor($revision);
$editor->setActor($actor);
$editor->copyFieldsFromConduit($fields);
$editor->addDiff($diff, null);
$editor->save();
return $revision;
}
public function copyFieldsFromConduit(array $fields) {
$actor = $this->getActor();
$revision = $this->revision;
$revision->loadRelationships();
$aux_fields = DifferentialFieldSelector::newSelector()
->getFieldSpecifications();
foreach ($aux_fields as $key => $aux_field) {
$aux_field->setRevision($revision);
$aux_field->setUser($actor);
if (!$aux_field->shouldAppearOnCommitMessage()) {
unset($aux_fields[$key]);
}
}
$aux_fields = mpull($aux_fields, null, 'getCommitMessageKey');
foreach ($fields as $field => $value) {
if (empty($aux_fields[$field])) {
throw new Exception(
"Parsed commit message contains unrecognized field '{$field}'.");
}
$aux_fields[$field]->setValueFromParsedCommitMessage($value);
}
foreach ($aux_fields as $aux_field) {
$aux_field->validateField();
}
$aux_fields = array_values($aux_fields);
$this->setAuxiliaryFields($aux_fields);
}
public function setAuxiliaryFields(array $auxiliary_fields) {
assert_instances_of($auxiliary_fields, 'DifferentialFieldSpecification');
$this->auxiliaryFields = $auxiliary_fields;
return $this;
}
public function getRevision() {
return $this->revision;
}
public function setReviewers(array $reviewers) {
$this->reviewers = $reviewers;
return $this;
}
public function setCCPHIDs(array $cc) {
$this->cc = $cc;
return $this;
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source;
return $this;
}
public function addDiff(DifferentialDiff $diff, $comments) {
if ($diff->getRevisionID() &&
$diff->getRevisionID() != $this->getRevision()->getID()) {
$diff_id = (int)$diff->getID();
$targ_id = (int)$this->getRevision()->getID();
$real_id = (int)$diff->getRevisionID();
throw new Exception(
"Can not attach diff #{$diff_id} to Revision D{$targ_id}, it is ".
"already attached to D{$real_id}.");
}
$this->diff = $diff;
$this->comments = $comments;
return $this;
}
protected function getDiff() {
return $this->diff;
}
protected function getComments() {
return $this->comments;
}
protected function getActorPHID() {
return $this->getActor()->getPHID();
}
public function isNewRevision() {
return !$this->getRevision()->getID();
}
/**
* A silent update does not trigger Herald rules or send emails. This is used
* for auto-amends at commit time.
*/
public function setSilentUpdate($silent) {
$this->silentUpdate = $silent;
return $this;
}
public function save() {
$revision = $this->getRevision();
$is_new = $this->isNewRevision();
if ($is_new) {
$this->initializeNewRevision($revision);
}
$revision->loadRelationships();
$this->willWriteRevision();
if ($this->reviewers === null) {
$this->reviewers = $revision->getReviewers();
}
if ($this->cc === null) {
$this->cc = $revision->getCCPHIDs();
}
$diff = $this->getDiff();
if ($diff) {
$revision->setLineCount($diff->getLineCount());
}
// Save the revision, to generate its ID and PHID if it is new. We need
// the ID/PHID in order to record them in Herald transcripts, but don't
// want to hold a transaction open while running Herald because it is
// potentially somewhat slow. The downside is that we may end up with a
// saved revision/diff pair without appropriate CCs. We could be better
// about this -- for example:
//
// - Herald can't affect reviewers, so we could compute them before
// opening the transaction and then save them in the transaction.
// - Herald doesn't *really* need PHIDs to compute its effects, we could
// run it before saving these objects and then hand over the PHIDs later.
//
// But this should address the problem of orphaned revisions, which is
// currently the only problem we experience in practice.
$revision->openTransaction();
if ($diff) {
$revision->setBranchName($diff->getBranch());
$revision->setArcanistProjectPHID($diff->getArcanistProjectPHID());
}
$revision->save();
if ($diff) {
$diff->setRevisionID($revision->getID());
$diff->save();
}
$revision->saveTransaction();
// We're going to build up three dictionaries: $add, $rem, and $stable. The
// $add dictionary has added reviewers/CCs. The $rem dictionary has
// reviewers/CCs who have been removed, and the $stable array is
// reviewers/CCs who haven't changed. We're going to send new reviewers/CCs
// a different ("welcome") email than we send stable reviewers/CCs.
$old = array(
'rev' => array_fill_keys($revision->getReviewers(), true),
'ccs' => array_fill_keys($revision->getCCPHIDs(), true),
);
$xscript_header = null;
$xscript_uri = null;
$new = array(
'rev' => array_fill_keys($this->reviewers, true),
'ccs' => array_fill_keys($this->cc, true),
);
$rem_ccs = array();
$xscript_phid = null;
if ($diff) {
$adapter = new HeraldDifferentialRevisionAdapter(
$revision,
$diff);
$adapter->setExplicitCCs($new['ccs']);
$adapter->setExplicitReviewers($new['rev']);
$adapter->setForbiddenCCs($revision->getUnsubscribedPHIDs());
$xscript = HeraldEngine::loadAndApplyRules($adapter);
$xscript_uri = '/herald/transcript/'.$xscript->getID().'/';
$xscript_phid = $xscript->getPHID();
$xscript_header = $xscript->getXHeraldRulesHeader();
$xscript_header = HeraldTranscript::saveXHeraldRulesHeader(
$revision->getPHID(),
$xscript_header);
$sub = array(
'rev' => array(),
'ccs' => $adapter->getCCsAddedByHerald(),
);
$rem_ccs = $adapter->getCCsRemovedByHerald();
} else {
$sub = array(
'rev' => array(),
'ccs' => array(),
);
}
// Remove any CCs which are prevented by Herald rules.
$sub['ccs'] = array_diff_key($sub['ccs'], $rem_ccs);
$new['ccs'] = array_diff_key($new['ccs'], $rem_ccs);
$add = array();
$rem = array();
$stable = array();
foreach (array('rev', 'ccs') as $key) {
$add[$key] = array();
if ($new[$key] !== null) {
$add[$key] += array_diff_key($new[$key], $old[$key]);
}
$add[$key] += array_diff_key($sub[$key], $old[$key]);
$combined = $sub[$key];
if ($new[$key] !== null) {
$combined += $new[$key];
}
$rem[$key] = array_diff_key($old[$key], $combined);
$stable[$key] = array_diff_key($old[$key], $add[$key] + $rem[$key]);
}
self::alterReviewers(
$revision,
$this->reviewers,
array_keys($rem['rev']),
array_keys($add['rev']),
$this->getActorPHID());
// We want to attribute new CCs to a "reasonPHID", representing the reason
// they were added. This is either a user (if some user explicitly CCs
// them, or uses "Add CCs...") or a Herald transcript PHID, indicating that
// they were added by a Herald rule.
if ($add['ccs'] || $rem['ccs']) {
$reasons = array();
foreach ($add['ccs'] as $phid => $ignored) {
if (empty($new['ccs'][$phid])) {
$reasons[$phid] = $xscript_phid;
} else {
$reasons[$phid] = $this->getActorPHID();
}
}
foreach ($rem['ccs'] as $phid => $ignored) {
if (empty($new['ccs'][$phid])) {
$reasons[$phid] = $this->getActorPHID();
} else {
$reasons[$phid] = $xscript_phid;
}
}
} else {
$reasons = $this->getActorPHID();
}
self::alterCCs(
$revision,
$this->cc,
array_keys($rem['ccs']),
array_keys($add['ccs']),
$reasons);
$this->updateAuxiliaryFields();
// Add the author and users included from Herald rules to the relevant set
// of users so they get a copy of the email.
if (!$this->silentUpdate) {
if ($is_new) {
$add['rev'][$this->getActorPHID()] = true;
if ($diff) {
$add['rev'] += $adapter->getEmailPHIDsAddedByHerald();
}
} else {
$stable['rev'][$this->getActorPHID()] = true;
if ($diff) {
$stable['rev'] += $adapter->getEmailPHIDsAddedByHerald();
}
}
}
$mail = array();
$phids = array($this->getActorPHID());
$handles = id(new PhabricatorObjectHandleData($phids))
->loadHandles();
$actor_handle = $handles[$this->getActorPHID()];
$changesets = null;
$comment = null;
if ($diff) {
$changesets = $diff->loadChangesets();
// TODO: This should probably be in DifferentialFeedbackEditor?
if (!$is_new) {
$comment = $this->createComment();
}
if ($comment) {
$mail[] = id(new DifferentialNewDiffMail(
$revision,
$actor_handle,
$changesets))
->setIsFirstMailAboutRevision($is_new)
->setIsFirstMailToRecipients($is_new)
->setComments($this->getComments())
->setToPHIDs(array_keys($stable['rev']))
->setCCPHIDs(array_keys($stable['ccs']));
}
// Save the changes we made above.
$diff->setDescription(preg_replace('/\n.*/s', '', $this->getComments()));
$diff->save();
$this->updateAffectedPathTable($revision, $diff, $changesets);
$this->updateRevisionHashTable($revision, $diff);
// An updated diff should require review, as long as it's not closed
// or accepted. The "accepted" status is "sticky" to encourage courtesy
// re-diffs after someone accepts with minor changes/suggestions.
$status = $revision->getStatus();
if ($status != ArcanistDifferentialRevisionStatus::CLOSED &&
$status != ArcanistDifferentialRevisionStatus::ACCEPTED) {
$revision->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVIEW);
}
} else {
$diff = $revision->loadActiveDiff();
if ($diff) {
$changesets = $diff->loadChangesets();
} else {
$changesets = array();
}
}
$revision->save();
$this->didWriteRevision();
$event_data = array(
'revision_id' => $revision->getID(),
'revision_phid' => $revision->getPHID(),
'revision_name' => $revision->getTitle(),
'revision_author_phid' => $revision->getAuthorPHID(),
'action' => $is_new
? DifferentialAction::ACTION_CREATE
: DifferentialAction::ACTION_UPDATE,
'feedback_content' => $is_new
? phutil_utf8_shorten($revision->getSummary(), 140)
: $this->getComments(),
'actor_phid' => $revision->getAuthorPHID(),
);
id(new PhabricatorTimelineEvent('difx', $event_data))
->recordEvent();
$mailed_phids = array();
if (!$this->silentUpdate) {
$revision->loadRelationships();
if ($add['rev']) {
$message = id(new DifferentialNewDiffMail(
$revision,
$actor_handle,
$changesets))
->setIsFirstMailAboutRevision($is_new)
->setIsFirstMailToRecipients(true)
->setToPHIDs(array_keys($add['rev']));
if ($is_new) {
// The first time we send an email about a revision, put the CCs in
// the "CC:" field of the same "Review Requested" email that reviewers
// get, so you don't get two initial emails if you're on a list that
// is CC'd.
$message->setCCPHIDs(array_keys($add['ccs']));
}
$mail[] = $message;
}
// If we added CCs, we want to send them an email, but only if they were
// not already a reviewer and were not added as one (in these cases, they
// got a "NewDiff" mail, either in the past or just a moment ago). You can
// still get two emails, but only if a revision is updated and you are
// added as a reviewer at the same time a list you are on is added as a
// CC, which is rare and reasonable.
$implied_ccs = self::getImpliedCCs($revision);
$implied_ccs = array_fill_keys($implied_ccs, true);
$add['ccs'] = array_diff_key($add['ccs'], $implied_ccs);
if (!$is_new && $add['ccs']) {
$mail[] = id(new DifferentialCCWelcomeMail(
$revision,
$actor_handle,
$changesets))
->setIsFirstMailToRecipients(true)
->setToPHIDs(array_keys($add['ccs']));
}
foreach ($mail as $message) {
$message->setHeraldTranscriptURI($xscript_uri);
$message->setXHeraldRulesHeader($xscript_header);
$message->send();
$mailed_phids[] = $message->getRawMail()->buildRecipientList();
}
$mailed_phids = array_mergev($mailed_phids);
}
id(new PhabricatorFeedStoryPublisher())
->setStoryType('PhabricatorFeedStoryDifferential')
->setStoryData($event_data)
->setStoryTime(time())
->setStoryAuthorPHID($revision->getAuthorPHID())
->setRelatedPHIDs(
array(
$revision->getPHID(),
$revision->getAuthorPHID(),
))
->setPrimaryObjectPHID($revision->getPHID())
->setSubscribedPHIDs(
array_merge(
array($revision->getAuthorPHID()),
$revision->getReviewers(),
$revision->getCCPHIDs()))
->setMailRecipientPHIDs($mailed_phids)
->publish();
// TODO: Move this into a worker task thing.
PhabricatorSearchDifferentialIndexer::indexRevision($revision);
}
public static function addCCAndUpdateRevision(
$revision,
$phid,
$reason) {
self::addCC($revision, $phid, $reason);
$unsubscribed = $revision->getUnsubscribed();
if (isset($unsubscribed[$phid])) {
unset($unsubscribed[$phid]);
$revision->setUnsubscribed($unsubscribed);
$revision->save();
}
}
public static function removeCCAndUpdateRevision(
$revision,
$phid,
$reason) {
self::removeCC($revision, $phid, $reason);
$unsubscribed = $revision->getUnsubscribed();
if (empty($unsubscribed[$phid])) {
$unsubscribed[$phid] = true;
$revision->setUnsubscribed($unsubscribed);
$revision->save();
}
}
public static function addCC(
DifferentialRevision $revision,
$phid,
$reason) {
return self::alterCCs(
$revision,
$revision->getCCPHIDs(),
$rem = array(),
$add = array($phid),
$reason);
}
public static function removeCC(
DifferentialRevision $revision,
$phid,
$reason) {
return self::alterCCs(
$revision,
$revision->getCCPHIDs(),
$rem = array($phid),
$add = array(),
$reason);
}
protected static function alterCCs(
DifferentialRevision $revision,
array $stable_phids,
array $rem_phids,
array $add_phids,
$reason_phid) {
$dont_add = self::getImpliedCCs($revision);
$add_phids = array_diff($add_phids, $dont_add);
return self::alterRelationships(
$revision,
$stable_phids,
$rem_phids,
$add_phids,
$reason_phid,
DifferentialRevision::RELATION_SUBSCRIBED);
}
private static function getImpliedCCs(DifferentialRevision $revision) {
return array_merge(
$revision->getReviewers(),
array($revision->getAuthorPHID()));
}
public static function alterReviewers(
DifferentialRevision $revision,
array $stable_phids,
array $rem_phids,
array $add_phids,
$reason_phid) {
return self::alterRelationships(
$revision,
$stable_phids,
$rem_phids,
$add_phids,
$reason_phid,
DifferentialRevision::RELATION_REVIEWER);
}
private static function alterRelationships(
DifferentialRevision $revision,
array $stable_phids,
array $rem_phids,
array $add_phids,
$reason_phid,
$relation_type) {
$rem_map = array_fill_keys($rem_phids, true);
$add_map = array_fill_keys($add_phids, true);
$seq_map = array_values($stable_phids);
$seq_map = array_flip($seq_map);
foreach ($rem_map as $phid => $ignored) {
if (!isset($seq_map[$phid])) {
$seq_map[$phid] = count($seq_map);
}
}
foreach ($add_map as $phid => $ignored) {
if (!isset($seq_map[$phid])) {
$seq_map[$phid] = count($seq_map);
}
}
$raw = $revision->getRawRelations($relation_type);
$raw = ipull($raw, null, 'objectPHID');
$sequence = count($seq_map);
foreach ($raw as $phid => $ignored) {
if (isset($seq_map[$phid])) {
$raw[$phid]['sequence'] = $seq_map[$phid];
} else {
$raw[$phid]['sequence'] = $sequence++;
}
}
$raw = isort($raw, 'sequence');
foreach ($raw as $phid => $ignored) {
if (isset($rem_map[$phid])) {
unset($raw[$phid]);
}
}
foreach ($add_phids as $add) {
$reason = is_array($reason_phid)
? idx($reason_phid, $add)
: $reason_phid;
$raw[$add] = array(
'objectPHID' => $add,
'sequence' => idx($seq_map, $add, $sequence++),
'reasonPHID' => $reason,
);
}
$conn_w = $revision->establishConnection('w');
$sql = array();
foreach ($raw as $relation) {
$sql[] = qsprintf(
$conn_w,
'(%d, %s, %s, %d, %s)',
$revision->getID(),
$relation_type,
$relation['objectPHID'],
$relation['sequence'],
$relation['reasonPHID']);
}
$conn_w->openTransaction();
queryfx(
$conn_w,
'DELETE FROM %T WHERE revisionID = %d AND relation = %s',
DifferentialRevision::RELATIONSHIP_TABLE,
$revision->getID(),
$relation_type);
if ($sql) {
queryfx(
$conn_w,
'INSERT INTO %T
(revisionID, relation, objectPHID, sequence, reasonPHID)
VALUES %Q',
DifferentialRevision::RELATIONSHIP_TABLE,
implode(', ', $sql));
}
$conn_w->saveTransaction();
$revision->loadRelationships();
}
private function createComment() {
$revision_id = $this->revision->getID();
$comment = id(new DifferentialComment())
->setAuthorPHID($this->getActorPHID())
->setRevisionID($revision_id)
->setContent($this->getComments())
->setAction(DifferentialAction::ACTION_UPDATE)
->setMetadata(
array(
DifferentialComment::METADATA_DIFF_ID => $this->getDiff()->getID(),
));
if ($this->contentSource) {
$comment->setContentSource($this->contentSource);
}
$comment->save();
return $comment;
}
private function updateAuxiliaryFields() {
$aux_map = array();
foreach ($this->auxiliaryFields as $aux_field) {
$key = $aux_field->getStorageKey();
if ($key !== null) {
$val = $aux_field->getValueForStorage();
$aux_map[$key] = $val;
}
}
if (!$aux_map) {
return;
}
$revision = $this->revision;
$fields = id(new DifferentialAuxiliaryField())->loadAllWhere(
'revisionPHID = %s AND name IN (%Ls)',
$revision->getPHID(),
array_keys($aux_map));
$fields = mpull($fields, null, 'getName');
foreach ($aux_map as $key => $val) {
$obj = idx($fields, $key);
if (!strlen($val)) {
// If the new value is empty, just delete the old row if one exists and
// don't add a new row if it doesn't.
if ($obj) {
$obj->delete();
}
} else {
if (!$obj) {
$obj = new DifferentialAuxiliaryField();
$obj->setRevisionPHID($revision->getPHID());
$obj->setName($key);
}
if ($obj->getValue() !== $val) {
$obj->setValue($val);
$obj->save();
}
}
}
}
private function willWriteRevision() {
foreach ($this->auxiliaryFields as $aux_field) {
$aux_field->willWriteRevision($this);
}
}
private function didWriteRevision() {
foreach ($this->auxiliaryFields as $aux_field) {
$aux_field->didWriteRevision($this);
}
}
/**
* Update the table which links Differential revisions to paths they affect,
* so Diffusion can efficiently find pending revisions for a given file.
*/
private function updateAffectedPathTable(
DifferentialRevision $revision,
DifferentialDiff $diff,
array $changesets) {
assert_instances_of($changesets, 'DifferentialChangeset');
$project = $diff->loadArcanistProject();
if (!$project) {
// Probably an old revision from before projects.
return;
}
$repository = $project->loadRepository();
if (!$repository) {
// Probably no project <-> repository link, or the repository where the
// project lives is untracked.
return;
}
$path_prefix = null;
$local_root = $diff->getSourceControlPath();
if ($local_root) {
// We're in a working copy which supports subdirectory checkouts (e.g.,
// SVN) so we need to figure out what prefix we should add to each path
// (e.g., trunk/projects/example/) to get the absolute path from the
// root of the repository. DVCS systems like Git and Mercurial are not
// affected.
// Normalize both paths and check if the repository root is a prefix of
// the local root. If so, throw it away. Note that this correctly handles
// the case where the remote path is "/".
$local_root = id(new PhutilURI($local_root))->getPath();
$local_root = rtrim($local_root, '/');
$repo_root = id(new PhutilURI($repository->getRemoteURI()))->getPath();
$repo_root = rtrim($repo_root, '/');
if (!strncmp($repo_root, $local_root, strlen($repo_root))) {
$path_prefix = substr($local_root, strlen($repo_root));
}
}
$paths = array();
foreach ($changesets as $changeset) {
$paths[] = $path_prefix.'/'.$changeset->getFilename();
}
// Mark this as also touching all parent paths, so you can see all pending
// changes to any file within a directory.
$all_paths = array();
foreach ($paths as $local) {
foreach (DiffusionPathIDQuery::expandPathToRoot($local) as $path) {
$all_paths[$path] = true;
}
}
$all_paths = array_keys($all_paths);
$path_map = id(new DiffusionPathIDQuery($all_paths))->loadPathIDs();
$table = new DifferentialAffectedPath();
$conn_w = $table->establishConnection('w');
$sql = array();
foreach ($all_paths as $path) {
$path_id = idx($path_map, $path);
if (!$path_id) {
// Don't bother creating these, it probably means we're either adding
// a file (in which case having this row is irrelevant since Diffusion
// won't be querying for it) or something is misconfigured (in which
// case we'd just be writing garbage).
continue;
}
$sql[] = qsprintf(
$conn_w,
'(%d, %d, %d, %d)',
$repository->getID(),
$path_id,
time(),
$revision->getID());
}
queryfx(
$conn_w,
'DELETE FROM %T WHERE revisionID = %d',
$table->getTableName(),
$revision->getID());
foreach (array_chunk($sql, 256) as $chunk) {
queryfx(
$conn_w,
'INSERT INTO %T (repositoryID, pathID, epoch, revisionID) VALUES %Q',
$table->getTableName(),
implode(', ', $chunk));
}
}
/**
* Update the table connecting revisions to DVCS local hashes, so we can
* identify revisions by commit/tree hashes.
*/
private function updateRevisionHashTable(
DifferentialRevision $revision,
DifferentialDiff $diff) {
$vcs = $diff->getSourceControlSystem();
if ($vcs == DifferentialRevisionControlSystem::SVN) {
// Subversion has no local commit or tree hash information, so we don't
// have to do anything.
return;
}
$property = id(new DifferentialDiffProperty())->loadOneWhere(
'diffID = %d AND name = %s',
$diff->getID(),
'local:commits');
if (!$property) {
return;
}
$hashes = array();
$data = $property->getData();
switch ($vcs) {
case DifferentialRevisionControlSystem::GIT:
foreach ($data as $commit) {
$hashes[] = array(
ArcanistDifferentialRevisionHash::HASH_GIT_COMMIT,
$commit['commit'],
);
$hashes[] = array(
ArcanistDifferentialRevisionHash::HASH_GIT_TREE,
$commit['tree'],
);
}
break;
case DifferentialRevisionControlSystem::MERCURIAL:
foreach ($data as $commit) {
$hashes[] = array(
ArcanistDifferentialRevisionHash::HASH_MERCURIAL_COMMIT,
$commit['rev'],
);
}
break;
}
$conn_w = $revision->establishConnection('w');
$sql = array();
foreach ($hashes as $info) {
list($type, $hash) = $info;
$sql[] = qsprintf(
$conn_w,
'(%d, %s, %s)',
$revision->getID(),
$type,
$hash);
}
queryfx(
$conn_w,
'DELETE FROM %T WHERE revisionID = %d',
ArcanistDifferentialRevisionHash::TABLE_NAME,
$revision->getID());
if ($sql) {
queryfx(
$conn_w,
'INSERT INTO %T (revisionID, type, hash) VALUES %Q',
ArcanistDifferentialRevisionHash::TABLE_NAME,
implode(', ', $sql));
}
}
private function initializeNewRevision(DifferentialRevision $revision) {
// These fields aren't nullable; set them to sensible defaults if they
// haven't been configured. We're just doing this so we can generate an
// ID for the revision if we don't have one already.
$revision->setLineCount(0);
if ($revision->getStatus() === null) {
$revision->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVIEW);
}
if ($revision->getTitle() === null) {
$revision->setTitle('Untitled Revision');
}
if ($revision->getAuthorPHID() === null) {
$revision->setAuthorPHID($this->getActorPHID());
}
if ($revision->getSummary() === null) {
$revision->setSummary('');
}
if ($revision->getTestPlan() === null) {
$revision->setTestPlan('');
}
}
}
diff --git a/src/applications/differential/exception/DifferentialActionHasNoEffectException.php b/src/applications/differential/exception/DifferentialActionHasNoEffectException.php
index 6f516f44d3..cf81970fc4 100644
--- a/src/applications/differential/exception/DifferentialActionHasNoEffectException.php
+++ b/src/applications/differential/exception/DifferentialActionHasNoEffectException.php
@@ -1,24 +1,8 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialActionHasNoEffectException
extends DifferentialException {
}
diff --git a/src/applications/differential/exception/DifferentialException.php b/src/applications/differential/exception/DifferentialException.php
index 794efd7937..6ac5a8d218 100644
--- a/src/applications/differential/exception/DifferentialException.php
+++ b/src/applications/differential/exception/DifferentialException.php
@@ -1,21 +1,5 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DifferentialException extends Exception {
}
diff --git a/src/applications/differential/field/exception/DifferentialFieldDataNotAvailableException.php b/src/applications/differential/field/exception/DifferentialFieldDataNotAvailableException.php
index c5cba09267..f3e5ea66dd 100644
--- a/src/applications/differential/field/exception/DifferentialFieldDataNotAvailableException.php
+++ b/src/applications/differential/field/exception/DifferentialFieldDataNotAvailableException.php
@@ -1,30 +1,14 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialFieldDataNotAvailableException extends Exception {
public function __construct(DifferentialFieldSpecification $spec) {
$key = $spec->getStorageKey();
$class = get_class($spec);
parent::__construct(
"Differential field specification for '{$key}' (of class '{$class}') is ".
"attempting to access data which is not available in this context.");
}
}
diff --git a/src/applications/differential/field/exception/DifferentialFieldParseException.php b/src/applications/differential/field/exception/DifferentialFieldParseException.php
index 33254675a1..45ce9e9dcb 100644
--- a/src/applications/differential/field/exception/DifferentialFieldParseException.php
+++ b/src/applications/differential/field/exception/DifferentialFieldParseException.php
@@ -1,21 +1,5 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialFieldParseException extends Exception {
}
diff --git a/src/applications/differential/field/exception/DifferentialFieldSpecificationIncompleteException.php b/src/applications/differential/field/exception/DifferentialFieldSpecificationIncompleteException.php
index 2d49f77d5f..7b12db90a0 100644
--- a/src/applications/differential/field/exception/DifferentialFieldSpecificationIncompleteException.php
+++ b/src/applications/differential/field/exception/DifferentialFieldSpecificationIncompleteException.php
@@ -1,32 +1,16 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialFieldSpecificationIncompleteException
extends Exception {
public function __construct(DifferentialFieldSpecification $spec) {
$key = $spec->getStorageKey();
$class = get_class($spec);
parent::__construct(
"Differential field specification for '{$key}' (of class '{$class}') is ".
"incompletely implemented: it claims it should appear in a context but ".
"does not implement all the required methods for that context.");
}
}
diff --git a/src/applications/differential/field/exception/DifferentialFieldValidationException.php b/src/applications/differential/field/exception/DifferentialFieldValidationException.php
index 8de10f1a29..6711465f84 100644
--- a/src/applications/differential/field/exception/DifferentialFieldValidationException.php
+++ b/src/applications/differential/field/exception/DifferentialFieldValidationException.php
@@ -1,21 +1,5 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialFieldValidationException extends Exception {
}
diff --git a/src/applications/differential/field/selector/DifferentialDefaultFieldSelector.php b/src/applications/differential/field/selector/DifferentialDefaultFieldSelector.php
index c0117a17c4..d2dc3fa7d4 100644
--- a/src/applications/differential/field/selector/DifferentialDefaultFieldSelector.php
+++ b/src/applications/differential/field/selector/DifferentialDefaultFieldSelector.php
@@ -1,101 +1,85 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialDefaultFieldSelector
extends DifferentialFieldSelector {
public function getFieldSpecifications() {
$fields = array(
new DifferentialTitleFieldSpecification(),
new DifferentialSummaryFieldSpecification(),
new DifferentialTestPlanFieldSpecification(),
new DifferentialRevisionStatusFieldSpecification(),
new DifferentialAuthorFieldSpecification(),
new DifferentialReviewersFieldSpecification(),
new DifferentialReviewedByFieldSpecification(),
new DifferentialCCsFieldSpecification(),
new DifferentialLintFieldSpecification(),
new DifferentialUnitFieldSpecification(),
new DifferentialCommitsFieldSpecification(),
new DifferentialDependsOnFieldSpecification(),
new DifferentialDependenciesFieldSpecification(),
new DifferentialManiphestTasksFieldSpecification(),
new DifferentialHostFieldSpecification(),
new DifferentialPathFieldSpecification(),
new DifferentialBranchFieldSpecification(),
new DifferentialArcanistProjectFieldSpecification(),
new DifferentialApplyPatchFieldSpecification(),
new DifferentialRevisionIDFieldSpecification(),
new DifferentialGitSVNIDFieldSpecification(),
new DifferentialDateModifiedFieldSpecification(),
new DifferentialDateCreatedFieldSpecification(),
new DifferentialAuditorsFieldSpecification(),
);
return $fields;
}
public function sortFieldsForRevisionList(array $fields) {
assert_instances_of($fields, 'DifferentialFieldSpecification');
$map = array();
foreach ($fields as $field) {
$map[get_class($field)] = $field;
}
$map = array_select_keys(
$map,
array(
'DifferentialRevisionIDFieldSpecification',
'DifferentialTitleFieldSpecification',
'DifferentialRevisionStatusFieldSpecification',
'DifferentialAuthorFieldSpecification',
'DifferentialReviewersFieldSpecification',
'DifferentialDateModifiedFieldSpecification',
'DifferentialDateCreatedFieldSpecification',
)) + $map;
return array_values($map);
}
public function sortFieldsForMail(array $fields) {
assert_instances_of($fields, 'DifferentialFieldSpecification');
$map = array();
foreach ($fields as $field) {
$map[get_class($field)] = $field;
}
$map = array_select_keys(
$map,
array(
'DifferentialReviewersFieldSpecification',
'DifferentialSummaryFieldSpecification',
'DifferentialTestPlanFieldSpecification',
'DifferentialRevisionIDFieldSpecification',
'DifferentialManiphestTasksFieldSpecification',
'DifferentialBranchFieldSpecification',
'DifferentialArcanistProjectFieldSpecification',
'DifferentialCommitsFieldSpecification',
)) + $map;
return array_values($map);
}
}
diff --git a/src/applications/differential/field/selector/DifferentialFieldSelector.php b/src/applications/differential/field/selector/DifferentialFieldSelector.php
index 523b5e829e..dee585d79c 100644
--- a/src/applications/differential/field/selector/DifferentialFieldSelector.php
+++ b/src/applications/differential/field/selector/DifferentialFieldSelector.php
@@ -1,41 +1,25 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DifferentialFieldSelector {
final public function __construct() {
// <empty>
}
final public static function newSelector() {
return PhabricatorEnv::newObjectFromConfig('differential.field-selector');
}
abstract public function getFieldSpecifications();
public function sortFieldsForRevisionList(array $fields) {
assert_instances_of($fields, 'DifferentialFieldSpecification');
return $fields;
}
public function sortFieldsForMail(array $fields) {
assert_instances_of($fields, 'DifferentialFieldSpecification');
return $fields;
}
}
diff --git a/src/applications/differential/field/specification/DifferentialApplyPatchFieldSpecification.php b/src/applications/differential/field/specification/DifferentialApplyPatchFieldSpecification.php
index d3dff09fff..3a8c0efd6f 100644
--- a/src/applications/differential/field/specification/DifferentialApplyPatchFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialApplyPatchFieldSpecification.php
@@ -1,35 +1,19 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialApplyPatchFieldSpecification
extends DifferentialFieldSpecification {
public function shouldAppearOnRevisionView() {
return true;
}
public function renderLabelForRevisionView() {
return 'Apply Patch:';
}
public function renderValueForRevisionView() {
$revision = $this->getRevision();
return '<tt>arc patch D'.$revision->getID().'</tt>';
}
}
diff --git a/src/applications/differential/field/specification/DifferentialArcanistProjectFieldSpecification.php b/src/applications/differential/field/specification/DifferentialArcanistProjectFieldSpecification.php
index a405747656..c64839eb9b 100644
--- a/src/applications/differential/field/specification/DifferentialArcanistProjectFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialArcanistProjectFieldSpecification.php
@@ -1,72 +1,56 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialArcanistProjectFieldSpecification
extends DifferentialFieldSpecification {
public function shouldAppearOnRevisionView() {
return true;
}
public function getRequiredHandlePHIDs() {
$arcanist_phid = $this->getArcanistProjectPHID();
if (!$arcanist_phid) {
return array();
}
return array($arcanist_phid);
}
public function renderLabelForRevisionView() {
return 'Arcanist Project:';
}
public function renderValueForRevisionView() {
$arcanist_phid = $this->getArcanistProjectPHID();
if (!$arcanist_phid) {
return null;
}
$handle = $this->getHandle($arcanist_phid);
return phutil_escape_html($handle->getName());
}
private function getArcanistProjectPHID() {
$diff = $this->getDiff();
return $diff->getArcanistProjectPHID();
}
public function renderValueForMail($phase) {
$status = $this->getRevision()->getStatus();
if ($status != ArcanistDifferentialRevisionStatus::NEEDS_REVISION &&
$status != ArcanistDifferentialRevisionStatus::ACCEPTED) {
return null;
}
$diff = $this->getRevision()->loadActiveDiff();
if ($diff) {
$phid = $diff->getArcanistProjectPHID();
if ($phid) {
$handle = PhabricatorObjectHandleData::loadOneHandle($phid);
return "ARCANIST PROJECT\n ".$handle->getName();
}
}
}
}
diff --git a/src/applications/differential/field/specification/DifferentialAuditorsFieldSpecification.php b/src/applications/differential/field/specification/DifferentialAuditorsFieldSpecification.php
index f0e4cad4d8..54d32fc8e5 100644
--- a/src/applications/differential/field/specification/DifferentialAuditorsFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialAuditorsFieldSpecification.php
@@ -1,83 +1,67 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialAuditorsFieldSpecification
extends DifferentialFieldSpecification {
private $auditors = array();
public function shouldAppearOnCommitMessage() {
return true;
}
public function shouldAppearOnCommitMessageTemplate() {
return false;
}
public function getCommitMessageKey() {
return 'auditorPHIDs';
}
public function setValueFromParsedCommitMessage($value) {
$this->auditors = nonempty($value, array());
return $this;
}
public function renderLabelForCommitMessage() {
return 'Auditors';
}
public function getRequiredHandlePHIDsForCommitMessage() {
return $this->auditors;
}
public function renderValueForCommitMessage($is_edit) {
if (!$this->auditors) {
return null;
}
$names = array();
foreach ($this->auditors as $phid) {
$names[] = $this->getHandle($phid)->getName();
}
return implode(', ', $names);
}
public function parseValueFromCommitMessage($value) {
return $this->parseCommitMessageUserList($value);
}
public function getStorageKey() {
return 'phabricator:auditors';
}
public function getValueForStorage() {
return json_encode($this->auditors);
}
public function setValueFromStorage($value) {
$auditors = json_decode($value, true);
if (!is_array($auditors)) {
$auditors = array();
}
$this->auditors = $auditors;
return $this;
}
}
diff --git a/src/applications/differential/field/specification/DifferentialAuthorFieldSpecification.php b/src/applications/differential/field/specification/DifferentialAuthorFieldSpecification.php
index 1e0a666575..d060b66ce7 100644
--- a/src/applications/differential/field/specification/DifferentialAuthorFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialAuthorFieldSpecification.php
@@ -1,60 +1,44 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialAuthorFieldSpecification
extends DifferentialFieldSpecification {
public function shouldAppearOnRevisionView() {
return true;
}
public function getRequiredHandlePHIDsForRevisionView() {
return array($this->getAuthorPHID());
}
public function renderLabelForRevisionView() {
return 'Author:';
}
public function renderValueForRevisionView() {
return $this->renderUserList(array($this->getAuthorPHID()));
}
private function getAuthorPHID() {
$revision = $this->getRevision();
return $revision->getAuthorPHID();
}
public function shouldAppearOnRevisionList() {
return true;
}
public function renderHeaderForRevisionList() {
return 'Author';
}
public function renderValueForRevisionList(DifferentialRevision $revision) {
return $this->getHandle($revision->getAuthorPHID())->renderLink();
}
public function getRequiredHandlePHIDsForRevisionList(
DifferentialRevision $revision) {
return array($revision->getAuthorPHID());
}
}
diff --git a/src/applications/differential/field/specification/DifferentialBlameRevisionFieldSpecification.php b/src/applications/differential/field/specification/DifferentialBlameRevisionFieldSpecification.php
index 2821ede35b..27573da141 100644
--- a/src/applications/differential/field/specification/DifferentialBlameRevisionFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialBlameRevisionFieldSpecification.php
@@ -1,114 +1,98 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialBlameRevisionFieldSpecification
extends DifferentialFieldSpecification {
private $value;
public function getStorageKey() {
return 'phabricator:blame-revision';
}
public function getValueForStorage() {
return $this->value;
}
public function setValueFromStorage($value) {
$this->value = $value;
return $this;
}
public function shouldAppearOnEdit() {
return true;
}
public function setValueFromRequest(AphrontRequest $request) {
$this->value = $request->getStr($this->getStorageKey());
return $this;
}
public function renderEditControl() {
return id(new AphrontFormTextControl())
->setLabel('Blame Revision')
->setCaption('Revision which broke the stuff which this change fixes.')
->setName($this->getStorageKey())
->setValue($this->value);
}
public function shouldAppearOnRevisionView() {
return true;
}
public function renderLabelForRevisionView() {
return 'Blame Revision:';
}
public function renderValueForRevisionView() {
if (!$this->value) {
return null;
}
$engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine();
return $engine->markupText($this->value);
}
public function shouldAppearOnConduitView() {
return true;
}
public function getValueForConduit() {
return $this->value;
}
public function shouldAppearOnCommitMessage() {
return true;
}
public function getCommitMessageKey() {
return 'blameRevision';
}
public function setValueFromParsedCommitMessage($value) {
$this->value = $value;
return $this;
}
public function shouldOverwriteWhenCommitMessageIsEdited() {
return true;
}
public function renderLabelForCommitMessage() {
return 'Blame Revision';
}
public function renderValueForCommitMessage($is_edit) {
return $this->value;
}
public function getSupportedCommitMessageLabels() {
return array(
'Blame Revision',
'Blame Rev',
);
}
public function parseValueFromCommitMessage($value) {
return $value;
}
}
diff --git a/src/applications/differential/field/specification/DifferentialBranchFieldSpecification.php b/src/applications/differential/field/specification/DifferentialBranchFieldSpecification.php
index 213d7b3021..2119d5699e 100644
--- a/src/applications/differential/field/specification/DifferentialBranchFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialBranchFieldSpecification.php
@@ -1,65 +1,49 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialBranchFieldSpecification
extends DifferentialFieldSpecification {
public function shouldAppearOnRevisionView() {
return true;
}
public function renderLabelForRevisionView() {
return 'Branch:';
}
public function renderValueForRevisionView() {
$diff = $this->getManualDiff();
$branch = $diff->getBranch();
$bookmark = $diff->getBookmark();
$has_branch = ($branch != '');
$has_bookmark = ($bookmark != '');
if ($has_branch && $has_bookmark) {
$branch = "{$bookmark} bookmark on {$branch} branch";
} else if ($has_bookmark) {
$branch = "{$bookmark} bookmark";
} else if (!$has_branch) {
return null;
}
return phutil_escape_html($branch);
}
public function renderValueForMail($phase) {
$status = $this->getRevision()->getStatus();
if ($status != ArcanistDifferentialRevisionStatus::NEEDS_REVISION &&
$status != ArcanistDifferentialRevisionStatus::ACCEPTED) {
return null;
}
$diff = $this->getRevision()->loadActiveDiff();
if ($diff) {
$branch = $diff->getBranch();
if ($branch) {
return "BRANCH\n $branch";
}
}
}
}
diff --git a/src/applications/differential/field/specification/DifferentialCCsFieldSpecification.php b/src/applications/differential/field/specification/DifferentialCCsFieldSpecification.php
index 41dabae4cb..1af5fedd59 100644
--- a/src/applications/differential/field/specification/DifferentialCCsFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialCCsFieldSpecification.php
@@ -1,126 +1,110 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialCCsFieldSpecification
extends DifferentialFieldSpecification {
private $ccs = array();
public function shouldAppearOnRevisionView() {
return true;
}
public function getRequiredHandlePHIDsForRevisionView() {
return $this->getCCPHIDs();
}
public function renderLabelForRevisionView() {
return 'CCs:';
}
public function renderValueForRevisionView() {
return $this->renderUserList($this->getCCPHIDs());
}
private function getCCPHIDs() {
$revision = $this->getRevision();
return $revision->getCCPHIDs();
}
public function shouldAppearOnEdit() {
return true;
}
protected function didSetRevision() {
$this->ccs = $this->getCCPHIDs();
}
public function getRequiredHandlePHIDsForRevisionEdit() {
return $this->ccs;
}
public function getRequiredHandlePHIDsForCommitMessage() {
return $this->ccs;
}
public function setValueFromRequest(AphrontRequest $request) {
$this->ccs = $request->getArr('cc');
return $this;
}
public function renderEditControl() {
$cc_map = array();
foreach ($this->ccs as $phid) {
$cc_map[$phid] = $this->getHandle($phid)->getFullName();
}
return id(new AphrontFormTokenizerControl())
->setLabel('CC')
->setName('cc')
->setUser($this->getUser())
->setDatasource('/typeahead/common/mailable/')
->setValue($cc_map);
}
public function willWriteRevision(DifferentialRevisionEditor $editor) {
$editor->setCCPHIDs($this->ccs);
}
public function shouldAppearOnCommitMessage() {
return true;
}
public function getCommitMessageKey() {
return 'ccPHIDs';
}
public function setValueFromParsedCommitMessage($value) {
$this->ccs = nonempty($value, array());
return $this;
}
public function renderLabelForCommitMessage() {
return 'CC';
}
public function renderValueForCommitMessage($is_edit) {
if (!$this->ccs) {
return null;
}
$names = array();
foreach ($this->ccs as $phid) {
$handle = $this->getHandle($phid);
if ($handle->isComplete()) {
$names[] = $handle->getName();
}
}
return implode(', ', $names);
}
public function getSupportedCommitMessageLabels() {
return array(
'CC',
'CCs',
);
}
public function parseValueFromCommitMessage($value) {
return $this->parseCommitMessageMailableList($value);
}
}
diff --git a/src/applications/differential/field/specification/DifferentialCommitsFieldSpecification.php b/src/applications/differential/field/specification/DifferentialCommitsFieldSpecification.php
index 530debc173..671e3d5f9f 100644
--- a/src/applications/differential/field/specification/DifferentialCommitsFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialCommitsFieldSpecification.php
@@ -1,76 +1,60 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialCommitsFieldSpecification
extends DifferentialFieldSpecification {
public function shouldAppearOnRevisionView() {
return true;
}
public function getRequiredHandlePHIDsForRevisionView() {
return $this->getCommitPHIDs();
}
public function renderLabelForRevisionView() {
return 'Commits:';
}
public function renderValueForRevisionView() {
$commit_phids = $this->getCommitPHIDs();
if (!$commit_phids) {
return null;
}
$links = array();
foreach ($commit_phids as $commit_phid) {
$links[] = $this->getHandle($commit_phid)->renderLink();
}
return implode('<br />', $links);
}
private function getCommitPHIDs() {
$revision = $this->getRevision();
return $revision->getCommitPHIDs();
}
public function renderValueForMail($phase) {
$revision = $this->getRevision();
if ($revision->getStatus() != ArcanistDifferentialRevisionStatus::CLOSED) {
return null;
}
$phids = $revision->loadCommitPHIDs();
if (!$phids) {
return null;
}
$body = array();
$handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
$body[] = pht('COMMIT(S)', count($handles));
foreach ($handles as $handle) {
$body[] = ' '.PhabricatorEnv::getProductionURI($handle->getURI());
}
return implode("\n", $body);
}
}
diff --git a/src/applications/differential/field/specification/DifferentialDateCreatedFieldSpecification.php b/src/applications/differential/field/specification/DifferentialDateCreatedFieldSpecification.php
index b0b21a5bf5..8c74841a58 100644
--- a/src/applications/differential/field/specification/DifferentialDateCreatedFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialDateCreatedFieldSpecification.php
@@ -1,38 +1,22 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialDateCreatedFieldSpecification
extends DifferentialFieldSpecification {
public function shouldAppearOnRevisionList() {
return true;
}
public function renderHeaderForRevisionList() {
return 'Created';
}
public function getColumnClassForRevisionList() {
return 'right';
}
public function renderValueForRevisionList(DifferentialRevision $revision) {
return phabricator_date($revision->getDateCreated(), $this->getUser());
}
}
diff --git a/src/applications/differential/field/specification/DifferentialDateModifiedFieldSpecification.php b/src/applications/differential/field/specification/DifferentialDateModifiedFieldSpecification.php
index 2d8eecbdc5..f3ea757d01 100644
--- a/src/applications/differential/field/specification/DifferentialDateModifiedFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialDateModifiedFieldSpecification.php
@@ -1,38 +1,22 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialDateModifiedFieldSpecification
extends DifferentialFieldSpecification {
public function shouldAppearOnRevisionList() {
return true;
}
public function renderHeaderForRevisionList() {
return 'Updated';
}
public function getColumnClassForRevisionList() {
return 'right';
}
public function renderValueForRevisionList(DifferentialRevision $revision) {
return phabricator_datetime($revision->getDateModified(), $this->getUser());
}
}
diff --git a/src/applications/differential/field/specification/DifferentialDependenciesFieldSpecification.php b/src/applications/differential/field/specification/DifferentialDependenciesFieldSpecification.php
index 731d6c3ac1..6d03fe7ecb 100644
--- a/src/applications/differential/field/specification/DifferentialDependenciesFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialDependenciesFieldSpecification.php
@@ -1,54 +1,38 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialDependenciesFieldSpecification
extends DifferentialFieldSpecification {
public function shouldAppearOnRevisionView() {
return true;
}
public function getRequiredHandlePHIDsForRevisionView() {
return $this->getDependentRevisionPHIDs();
}
public function renderLabelForRevisionView() {
return 'Dependents:';
}
public function renderValueForRevisionView() {
$revision_phids = $this->getDependentRevisionPHIDs();
if (!$revision_phids) {
return null;
}
$links = array();
foreach ($revision_phids as $revision_phids) {
$links[] = $this->getHandle($revision_phids)->renderLink();
}
return implode('<br />', $links);
}
private function getDependentRevisionPHIDs() {
return PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getRevision()->getPHID(),
PhabricatorEdgeConfig::TYPE_DREV_DEPENDED_ON_BY_DREV);
}
}
diff --git a/src/applications/differential/field/specification/DifferentialDependsOnFieldSpecification.php b/src/applications/differential/field/specification/DifferentialDependsOnFieldSpecification.php
index 1f0b7299b1..24096ccabd 100644
--- a/src/applications/differential/field/specification/DifferentialDependsOnFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialDependsOnFieldSpecification.php
@@ -1,54 +1,38 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialDependsOnFieldSpecification
extends DifferentialFieldSpecification {
public function shouldAppearOnRevisionView() {
return true;
}
public function getRequiredHandlePHIDsForRevisionView() {
return $this->getDependentRevisionPHIDs();
}
public function renderLabelForRevisionView() {
return 'Depends On:';
}
public function renderValueForRevisionView() {
$revision_phids = $this->getDependentRevisionPHIDs();
if (!$revision_phids) {
return null;
}
$links = array();
foreach ($revision_phids as $revision_phids) {
$links[] = $this->getHandle($revision_phids)->renderLink();
}
return implode('<br />', $links);
}
private function getDependentRevisionPHIDs() {
return PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getRevision()->getPHID(),
PhabricatorEdgeConfig::TYPE_DREV_DEPENDS_ON_DREV);
}
}
diff --git a/src/applications/differential/field/specification/DifferentialExportPatchFieldSpecification.php b/src/applications/differential/field/specification/DifferentialExportPatchFieldSpecification.php
index 7499a5ce1a..00c16b4547 100644
--- a/src/applications/differential/field/specification/DifferentialExportPatchFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialExportPatchFieldSpecification.php
@@ -1,35 +1,19 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialExportPatchFieldSpecification
extends DifferentialFieldSpecification {
public function shouldAppearOnRevisionView() {
return true;
}
public function renderLabelForRevisionView() {
return 'Export Patch:';
}
public function renderValueForRevisionView() {
$revision = $this->getRevision();
return '<tt>arc export --revision '.$revision->getID().'</tt>';
}
}
diff --git a/src/applications/differential/field/specification/DifferentialFieldSpecification.php b/src/applications/differential/field/specification/DifferentialFieldSpecification.php
index 046704d139..d5310530f2 100644
--- a/src/applications/differential/field/specification/DifferentialFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialFieldSpecification.php
@@ -1,935 +1,919 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Describes and implements the behavior for a custom field on Differential
* revisions. Along with other configuration, you can extend this class to add
* custom fields to Differential revisions and commit messages.
*
* Generally, you should implement all methods from the storage task and then
* the methods from one or more interface tasks.
*
* @task storage Field Storage
* @task edit Extending the Revision Edit Interface
* @task view Extending the Revision View Interface
* @task list Extending the Revision List Interface
* @task mail Extending the E-mail Interface
* @task conduit Extending the Conduit View Interface
* @task commit Extending Commit Messages
* @task load Loading Additional Data
* @task context Contextual Data
*/
abstract class DifferentialFieldSpecification {
private $revision;
private $diff;
private $manualDiff;
private $handles;
private $diffProperties;
private $user;
/* -( Storage )------------------------------------------------------------ */
/**
* Return a unique string used to key storage of this field's value, like
* "mycompany.fieldname" or similar. You can return null (the default) to
* indicate that this field does not use any storage. This is appropriate for
* display fields, like @{class:DifferentialLinesFieldSpecification}. If you
* implement this, you must also implement @{method:getValueForStorage} and
* @{method:setValueFromStorage}.
*
* @return string|null Unique key which identifies this field in auxiliary
* field storage. Maximum length is 32. Alternatively,
* null (default) to indicate that this field does not
* use auxiliary field storage.
* @task storage
*/
public function getStorageKey() {
return null;
}
/**
* Return a serialized representation of the field value, appropriate for
* storing in auxiliary field storage. You must implement this method if
* you implement @{method:getStorageKey}.
*
* @return string Serialized field value.
* @task storage
*/
public function getValueForStorage() {
throw new DifferentialFieldSpecificationIncompleteException($this);
}
/**
* Set the field's value given a serialized storage value. This is called
* when the field is loaded; if no data is available, the value will be
* null. You must implement this method if you implement
* @{method:getStorageKey}.
*
* @param string|null Serialized field representation (from
* @{method:getValueForStorage}) or null if no value has
* ever been stored.
* @return this
* @task storage
*/
public function setValueFromStorage($value) {
throw new DifferentialFieldSpecificationIncompleteException($this);
}
/* -( Extending the Revision Edit Interface )------------------------------ */
/**
* Determine if this field should appear on the "Edit Revision" interface. If
* you return true from this method, you must implement
* @{method:setValueFromRequest}, @{method:renderEditControl} and
* @{method:validateField}.
*
* For a concrete example of a field which implements an edit interface, see
* @{class:DifferentialRevertPlanFieldSpecification}.
*
* @return bool True to indicate that this field implements an edit interface.
* @task edit
*/
public function shouldAppearOnEdit() {
return false;
}
/**
* Set the field's value from an HTTP request. Generally, you should read
* the value of some field name you emitted in @{method:renderEditControl}
* and save it into the object, e.g.:
*
* $this->value = $request->getStr('my-custom-field');
*
* If you have some particularly complicated field, you may need to read
* more data; this is why you have access to the entire request.
*
* You must implement this if you implement @{method:shouldAppearOnEdit}.
*
* You should not perform field validation here; instead, you should implement
* @{method:validateField}.
*
* @param AphrontRequest HTTP request representing a user submitting a form
* with this field in it.
* @return this
* @task edit
*/
public function setValueFromRequest(AphrontRequest $request) {
throw new DifferentialFieldSpecificationIncompleteException($this);
}
/**
* Build a renderable object (generally, some @{class:AphrontFormControl})
* which can be appended to a @{class:AphrontFormView} and represents the
* interface the user sees on the "Edit Revision" screen when interacting
* with this field.
*
* For example:
*
* return id(new AphrontFormTextControl())
* ->setLabel('Custom Field')
* ->setName('my-custom-key')
* ->setValue($this->value);
*
* You must implement this if you implement @{method:shouldAppearOnEdit}.
*
* @return AphrontView|string Something renderable.
* @task edit
*/
public function renderEditControl() {
throw new DifferentialFieldSpecificationIncompleteException($this);
}
/**
* This method will be called after @{method:setValueFromRequest} but before
* the field is saved. It gives you an opportunity to inspect the field value
* and throw a @{class:DifferentialFieldValidationException} if there is a
* problem with the value the user has provided (for example, the value the
* user entered is not correctly formatted). This method is also called after
* @{method:setValueFromParsedCommitMessage} before the revision is saved.
*
* By default, fields are not validated.
*
* @return void
* @task edit
*/
public function validateField() {
return;
}
/**
* Hook for applying revision changes via the editor. Normally, you should
* not implement this, but a number of builtin fields use the revision object
* itself as storage. If you need to do something similar for whatever reason,
* this method gives you an opportunity to interact with the editor or
* revision before changes are saved (for example, you can write the field's
* value into some property of the revision).
*
* @param DifferentialRevisionEditor Active editor which is applying changes
* to the revision.
* @return void
* @task edit
*/
public function willWriteRevision(DifferentialRevisionEditor $editor) {
return;
}
/**
* Hook after an edit operation has completed. This allows you to update
* link tables or do other write operations which should happen after the
* revision is saved. Normally you don't need to implement this.
*
*
* @param DifferentialRevisionEditor Active editor which has just applied
* changes to the revision.
* @return void
* @task edit
*/
public function didWriteRevision(DifferentialRevisionEditor $editor) {
return;
}
/* -( Extending the Revision View Interface )------------------------------ */
/**
* Determine if this field should appear on the revision detail view
* interface. One use of this interface is to add purely informational
* fields to the revision view, without any sort of backing storage.
*
* If you return true from this method, you must implement the methods
* @{method:renderLabelForRevisionView} and
* @{method:renderValueForRevisionView}.
*
* @return bool True if this field should appear when viewing a revision.
* @task view
*/
public function shouldAppearOnRevisionView() {
return false;
}
/**
* Return a string field label which will appear in the revision detail
* table.
*
* You must implement this method if you return true from
* @{method:shouldAppearOnRevisionView}.
*
* @return string Label for field in revision detail view.
* @task view
*/
public function renderLabelForRevisionView() {
throw new DifferentialFieldSpecificationIncompleteException($this);
}
/**
* Return a markup block representing the field for the revision detail
* view. Note that you can return null to suppress display (for instance,
* if the field shows related objects of some type and the revision doesn't
* have any related objects).
*
* You must implement this method if you return true from
* @{method:shouldAppearOnRevisionView}.
*
* @return string|null Display markup for field value, or null to suppress
* field rendering.
* @task view
*/
public function renderValueForRevisionView() {
throw new DifferentialFieldSpecificationIncompleteException($this);
}
/**
* Load users, their current statuses and return a markup with links to the
* user profiles and information about their current status.
*
* @return string Display markup.
* @task view
*/
public function renderUserList(array $user_phids) {
if (!$user_phids) {
return '<em>None</em>';
}
$links = array();
foreach ($user_phids as $user_phid) {
$handle = $this->getHandle($user_phid);
$links[] = $handle->renderLink();
}
return implode(', ', $links);
}
/**
* Return a markup block representing a warning to display with the comment
* box when preparing to accept a diff. A return value of null indicates no
* warning box should be displayed for this field.
*
* @return string|null Display markup for warning box, or null for no warning
*/
public function renderWarningBoxForRevisionAccept() {
return null;
}
/* -( Extending the Revision List Interface )------------------------------ */
/**
* Determine if this field should appear in the table on the revision list
* interface.
*
* @return bool True if this field should appear in the table.
*
* @task list
*/
public function shouldAppearOnRevisionList() {
return false;
}
/**
* Return a column header for revision list tables.
*
* @return string Column header.
*
* @task list
*/
public function renderHeaderForRevisionList() {
throw new DifferentialFieldSpecificationIncompleteException($this);
}
/**
* Optionally, return a column class for revision list tables.
*
* @return string CSS class for table cells.
*
* @task list
*/
public function getColumnClassForRevisionList() {
return null;
}
/**
* Return a table cell value for revision list tables.
*
* @param DifferentialRevision The revision to render a value for.
* @return string Table cell value.
*
* @task list
*/
public function renderValueForRevisionList(DifferentialRevision $revision) {
throw new DifferentialFieldSpecificationIncompleteException($this);
}
/* -( Extending the E-mail Interface )------------------------------------- */
/**
* Return plain text to render in e-mail messages. The text may span
* multiple lines.
*
* @return int One of DifferentialMailPhase constants.
* @return string|null Plain text, or null for no message.
*
* @task mail
*/
public function renderValueForMail($phase) {
return null;
}
/* -( Extending the Conduit Interface )------------------------------------ */
/**
* @task conduit
*/
public function shouldAppearOnConduitView() {
return false;
}
/**
* @task conduit
*/
public function getValueForConduit() {
throw new DifferentialFieldSpecificationIncompleteException($this);
}
/**
* @task conduit
*/
public function getKeyForConduit() {
$key = $this->getStorageKey();
if ($key === null) {
throw new DifferentialFieldSpecificationIncompleteException($this);
}
return $key;
}
/* -( Extending Commit Messages )------------------------------------------ */
/**
* Determine if this field should appear in commit messages. You should return
* true if this field participates in any part of the commit message workflow,
* even if it is not rendered by default.
*
* If you implement this method, you must implement
* @{method:getCommitMessageKey} and
* @{method:setValueFromParsedCommitMessage}.
*
* @return bool True if this field appears in commit messages in any capacity.
* @task commit
*/
public function shouldAppearOnCommitMessage() {
return false;
}
/**
* Key which identifies this field in parsed commit messages. Commit messages
* exist in two forms: raw textual commit messages and parsed dictionaries of
* fields. This method must return a unique string which identifies this field
* in dictionaries. Principally, this dictionary is shipped to and from arc
* over Conduit. Keys should be appropriate property names, like "testPlan"
* (not "Test Plan") and must be globally unique.
*
* You must implement this method if you return true from
* @{method:shouldAppearOnCommitMessage}.
*
* @return string Key which identifies the field in dictionaries.
* @task commit
*/
public function getCommitMessageKey() {
throw new DifferentialFieldSpecificationIncompleteException($this);
}
/**
* Set this field's value from a value in a parsed commit message dictionary.
* Afterward, this field will go through the normal write workflows and the
* change will be permanently stored via either the storage mechanisms (if
* your field implements them), revision write hooks (if your field implements
* them) or discarded (if your field implements neither, e.g. is just a
* display field).
*
* The value you receive will either be null or something you originally
* returned from @{method:parseValueFromCommitMessage}.
*
* You must implement this method if you return true from
* @{method:shouldAppearOnCommitMessage}.
*
* @param mixed Field value from a parsed commit message dictionary.
* @return this
* @task commit
*/
public function setValueFromParsedCommitMessage($value) {
throw new DifferentialFieldSpecificationIncompleteException($this);
}
/**
* In revision control systems which read revision information from the
* working copy, the user may edit the commit message outside of invoking
* "arc diff --edit". When they do this, only some fields (those fields which
* can not be edited by other users) are safe to overwrite. For instance, it
* is fine to overwrite "Summary" because no one else can edit it, but not
* to overwrite "Reviewers" because reviewers may have been added or removed
* via the web interface.
*
* If a field is safe to overwrite when edited in a working copy commit
* message, return true. If the authoritative value should always be used,
* return false. By default, fields can not be overwritten.
*
* arc will only attempt to overwrite field values if run with "--verbatim".
*
* @return bool True to indicate the field is save to overwrite.
* @task commit
*/
public function shouldOverwriteWhenCommitMessageIsEdited() {
return false;
}
/**
* Return true if this field should be suggested to the user during
* "arc diff --edit". Basicially, return true if the field is something the
* user might want to fill out (like "Summary"), and false if it's a
* system/display/readonly field (like "Differential Revision"). If this
* method returns true, the field will be rendered even if it has no value
* during edit and update operations.
*
* @return bool True to indicate the field should appear in the edit template.
* @task commit
*/
public function shouldAppearOnCommitMessageTemplate() {
return true;
}
/**
* Render a human-readable label for this field, like "Summary" or
* "Test Plan". This is distinct from the commit message key, but generally
* they should be similar.
*
* @return string Human-readable field label for commit messages.
* @task commit
*/
public function renderLabelForCommitMessage() {
throw new DifferentialFieldSpecificationIncompleteException($this);
}
/**
* Render a human-readable value for this field when it appears in commit
* messages (for instance, lists of users should be rendered as user names).
*
* The ##$is_edit## parameter allows you to distinguish between commit
* messages being rendered for editing and those being rendered for amending
* or commit. Some fields may decline to render a value in one mode (for
* example, "Reviewed By" appears only when doing commit/amend, not while
* editing).
*
* @param bool True if the message is being edited.
* @return string Human-readable field value.
* @task commit
*/
public function renderValueForCommitMessage($is_edit) {
throw new DifferentialFieldSpecificationIncompleteException($this);
}
/**
* Return one or more labels which this field parses in commit messages. For
* example, you might parse all of "Task", "Tasks" and "Task Numbers" or
* similar. This is just to make it easier to get commit messages to parse
* when users are typing in the fields manually as opposed to using a
* template, by accepting alternate spellings / pluralizations / etc. By
* default, only the label returned from @{method:renderLabelForCommitMessage}
* is parsed.
*
* @return list List of supported labels that this field can parse from commit
* messages.
* @task commit
*/
public function getSupportedCommitMessageLabels() {
return array($this->renderLabelForCommitMessage());
}
/**
* Parse a raw text block from a commit message into a canonical
* representation of the field value. For example, the "CC" field accepts a
* comma-delimited list of usernames and emails and parses them into valid
* PHIDs, emitting a PHID list.
*
* If you encounter errors (like a nonexistent username) while parsing,
* you should throw a @{class:DifferentialFieldParseException}.
*
* Generally, this method should accept whatever you return from
* @{method:renderValueForCommitMessage} and parse it back into a sensible
* representation.
*
* You must implement this method if you return true from
* @{method:shouldAppearOnCommitMessage}.
*
* @param string
* @return mixed The canonical representation of the field value. For example,
* you should lookup usernames and object references.
* @task commit
*/
public function parseValueFromCommitMessage($value) {
throw new DifferentialFieldSpecificationIncompleteException($this);
}
/**
* This method allows you to take action when a commit appears in a tracked
* branch (for example, by closing tasks associated with the commit).
*
* @param PhabricatorRepository The repository the commit appeared in.
* @param PhabricatorRepositoryCommit The commit itself.
* @param PhabricatorRepostioryCommitData Commit data.
* @return void
*
* @task commit
*/
public function didParseCommit(
PhabricatorRepository $repo,
PhabricatorRepositoryCommit $commit,
PhabricatorRepositoryCommitData $data) {
return;
}
/* -( Loading Additional Data )-------------------------------------------- */
/**
* Specify which @{class:PhabricatorObjectHandle}s need to be loaded for your
* field to render correctly.
*
* This is a convenience method which makes the handles available on all
* interfaces where the field appears. If your field needs handles on only
* some interfaces (or needs different handles on different interfaces) you
* can overload the more specific methods to customize which interfaces you
* retrieve handles for. Requesting only the handles you need will improve
* the performance of your field.
*
* You can later retrieve these handles by calling @{method:getHandle}.
*
* @return list List of PHIDs to load handles for.
* @task load
*/
protected function getRequiredHandlePHIDs() {
return array();
}
/**
* Specify which @{class:PhabricatorObjectHandle}s need to be loaded for your
* field to render correctly on the view interface.
*
* This is a more specific version of @{method:getRequiredHandlePHIDs} which
* can be overridden to improve field performance by loading only data you
* need.
*
* @return list List of PHIDs to load handles for.
* @task load
*/
public function getRequiredHandlePHIDsForRevisionView() {
return $this->getRequiredHandlePHIDs();
}
/**
* Specify which @{class:PhabricatorObjectHandle}s need to be loaded for your
* field to render correctly on the list interface.
*
* This is a more specific version of @{method:getRequiredHandlePHIDs} which
* can be overridden to improve field performance by loading only data you
* need.
*
* @param DifferentialRevision The revision to pull PHIDs for.
* @return list List of PHIDs to load handles for.
* @task load
*/
public function getRequiredHandlePHIDsForRevisionList(
DifferentialRevision $revision) {
return array();
}
/**
* Specify which @{class:PhabricatorObjectHandle}s need to be loaded for your
* field to render correctly on the edit interface.
*
* This is a more specific version of @{method:getRequiredHandlePHIDs} which
* can be overridden to improve field performance by loading only data you
* need.
*
* @return list List of PHIDs to load handles for.
* @task load
*/
public function getRequiredHandlePHIDsForRevisionEdit() {
return $this->getRequiredHandlePHIDs();
}
/**
* Specify which @{class:PhabricatorObjectHandle}s need to be loaded for your
* field to render correctly on the commit message interface.
*
* This is a more specific version of @{method:getRequiredHandlePHIDs} which
* can be overridden to improve field performance by loading only data you
* need.
*
* @return list List of PHIDs to load handles for.
* @task load
*/
public function getRequiredHandlePHIDsForCommitMessage() {
return $this->getRequiredHandlePHIDs();
}
/**
* Parse a list of users into a canonical PHID list.
*
* @param string Raw list of comma-separated user names.
* @return list List of corresponding PHIDs.
* @task load
*/
protected function parseCommitMessageUserList($value) {
return $this->parseCommitMessageObjectList($value, $mailables = false);
}
/**
* Parse a list of mailable objects into a canonical PHID list.
*
* @param string Raw list of comma-separated mailable names.
* @return list List of corresponding PHIDs.
* @task load
*/
protected function parseCommitMessageMailableList($value) {
return $this->parseCommitMessageObjectList($value, $mailables = true);
}
/**
* Parse and lookup a list of object names, converting them to PHIDs.
*
* @param string Raw list of comma-separated object names.
* @param bool True to include mailing lists.
* @param bool True to make a best effort. By default, an exception is
* thrown if any item is invalid.
* @return list List of corresponding PHIDs.
* @task load
*/
public static function parseCommitMessageObjectList(
$value,
$include_mailables,
$allow_partial = false) {
$value = array_unique(array_filter(preg_split('/[\s,]+/', $value)));
if (!$value) {
return array();
}
$object_map = array();
$users = id(new PhabricatorUser())->loadAllWhere(
'(username IN (%Ls))',
$value);
$user_map = mpull($users, 'getPHID', 'getUsername');
foreach ($user_map as $username => $phid) {
// Usernames may have uppercase letters in them. Put both names in the
// map so we can try the original case first, so that username *always*
// works in weird edge cases where some other mailable object collides.
$object_map[$username] = $phid;
$object_map[strtolower($username)] = $phid;
}
if ($include_mailables) {
$mailables = id(new PhabricatorMetaMTAMailingList())->loadAllWhere(
'(email IN (%Ls)) OR (name IN (%Ls))',
$value,
$value);
$object_map += mpull($mailables, 'getPHID', 'getName');
$object_map += mpull($mailables, 'getPHID', 'getEmail');
}
$invalid = array();
$results = array();
foreach ($value as $name) {
if (empty($object_map[$name])) {
if (empty($object_map[strtolower($name)])) {
$invalid[] = $name;
} else {
$results[] = $object_map[strtolower($name)];
}
} else {
$results[] = $object_map[$name];
}
}
if ($invalid && !$allow_partial) {
$invalid = implode(', ', $invalid);
$what = $include_mailables
? "users and mailing lists"
: "users";
throw new DifferentialFieldParseException(
"Commit message references nonexistent {$what}: {$invalid}.");
}
return array_unique($results);
}
/* -( Contextual Data )---------------------------------------------------- */
/**
* @task context
*/
final public function setRevision(DifferentialRevision $revision) {
$this->revision = $revision;
$this->didSetRevision();
return $this;
}
/**
* @task context
*/
protected function didSetRevision() {
return;
}
/**
* @task context
*/
final public function setDiff(DifferentialDiff $diff) {
$this->diff = $diff;
return $this;
}
/**
* @task context
*/
final public function setManualDiff(DifferentialDiff $diff) {
$this->manualDiff = $diff;
return $this;
}
/**
* @task context
*/
final public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
/**
* @task context
*/
final public function setDiffProperties(array $diff_properties) {
$this->diffProperties = $diff_properties;
return $this;
}
/**
* @task context
*/
final public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
/**
* @task context
*/
final protected function getRevision() {
if (empty($this->revision)) {
throw new DifferentialFieldDataNotAvailableException($this);
}
return $this->revision;
}
/**
* Determine if revision context is currently available.
*
* @task context
*/
final protected function hasRevision() {
return (bool)$this->revision;
}
/**
* @task context
*/
final protected function getDiff() {
if (empty($this->diff)) {
throw new DifferentialFieldDataNotAvailableException($this);
}
return $this->diff;
}
/**
* @task context
*/
final protected function getManualDiff() {
if (!$this->manualDiff) {
return $this->getDiff();
}
return $this->manualDiff;
}
/**
* @task context
*/
final protected function getUser() {
if (empty($this->user)) {
throw new DifferentialFieldDataNotAvailableException($this);
}
return $this->user;
}
/**
* Get the handle for an object PHID. You must overload
* @{method:getRequiredHandlePHIDs} (or a more specific version thereof)
* and include the PHID you want in the list for it to be available here.
*
* @return PhabricatorObjectHandle Handle to the object.
* @task context
*/
final protected function getHandle($phid) {
if ($this->handles === null) {
throw new DifferentialFieldDataNotAvailableException($this);
}
if (empty($this->handles[$phid])) {
$class = get_class($this);
throw new Exception(
"A differential field (of class '{$class}') is attempting to retrieve ".
"a handle ('{$phid}') which it did not request. Return all handle ".
"PHIDs you need from getRequiredHandlePHIDs().");
}
return $this->handles[$phid];
}
/**
* Get the list of properties for a diff set by @{method:setManualDiff}.
*
* @return array Array of all Diff properties.
* @task context
*/
final public function getDiffProperties() {
if ($this->diffProperties === null) {
// This will be set to some (possibly empty) array if we've loaded
// properties, so null means diff properties aren't available in this
// context.
throw new DifferentialFieldDataNotAvailableException($this);
}
return $this->diffProperties;
}
/**
* Get a property of a diff set by @{method:setManualDiff}.
*
* @param string Diff property key.
* @return mixed|null Diff property, or null if the property does not have
* a value.
* @task context
*/
final public function getDiffProperty($key) {
return idx($this->getDiffProperties(), $key);
}
}
diff --git a/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php b/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php
index 6b34a9e5db..8b02f69306 100644
--- a/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php
@@ -1,138 +1,122 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DifferentialFreeformFieldSpecification
extends DifferentialFieldSpecification {
public function didParseCommit(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit,
PhabricatorRepositoryCommitData $data) {
$user = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$data->getCommitDetail('authorPHID'));
if (!$user) {
return;
}
$prefixes = array(
'resolves' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
'fixes' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
'wontfix' => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX,
'wontfixes' => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX,
'spite' => ManiphestTaskStatus::STATUS_CLOSED_SPITE,
'spites' => ManiphestTaskStatus::STATUS_CLOSED_SPITE,
'invalidate' => ManiphestTaskStatus::STATUS_CLOSED_INVALID,
'invaldiates' => ManiphestTaskStatus::STATUS_CLOSED_INVALID,
'close' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
'closes' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
'ref' => null,
'refs' => null,
'references' => null,
'cf.' => null,
);
$suffixes = array(
'as resolved' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
'as fixed' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
'as wontfix' => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX,
'as spite' => ManiphestTaskStatus::STATUS_CLOSED_SPITE,
'out of spite' => ManiphestTaskStatus::STATUS_CLOSED_SPITE,
'as invalid' => ManiphestTaskStatus::STATUS_CLOSED_INVALID,
'' => null,
);
$prefix_regex = array();
foreach ($prefixes as $prefix => $resolution) {
$prefix_regex[] = preg_quote($prefix, '/');
}
$prefix_regex = implode('|', $prefix_regex);
$suffix_regex = array();
foreach ($suffixes as $suffix => $resolution) {
$suffix_regex[] = preg_quote($suffix, '/');
}
$suffix_regex = implode('|', $suffix_regex);
$matches = null;
$ok = preg_match_all(
"/({$prefix_regex})\s+T(\d+)\s*({$suffix_regex})/i",
$this->renderValueForCommitMessage($is_edit = false),
$matches,
PREG_SET_ORDER);
if (!$ok) {
return;
}
foreach ($matches as $set) {
$prefix = strtolower($set[1]);
$task_id = (int)$set[2];
$suffix = strtolower($set[3]);
$status = idx($suffixes, $suffix);
if (!$status) {
$status = idx($prefixes, $prefix);
}
$tasks = id(new ManiphestTaskQuery())
->withTaskIDs(array($task_id))
->execute();
$task = idx($tasks, $task_id);
if (!$task) {
// Task doesn't exist, or the user can't see it.
continue;
}
id(new PhabricatorEdgeEditor())
->setActor($user)
->addEdge(
$task->getPHID(),
PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT,
$commit->getPHID())
->save();
if (!$status) {
// Text like "Ref T123", don't change the task status.
continue;
}
if ($task->getStatus() != ManiphestTaskStatus::STATUS_OPEN) {
// Task is already closed.
continue;
}
$commit_name = $repository->formatCommitName(
$commit->getCommitIdentifier());
$call = new ConduitCall(
'maniphest.update',
array(
'id' => $task->getID(),
'status' => $status,
'comments' => "Closed by commit {$commit_name}.",
));
$call->setUser($user);
$call->execute();
}
}
}
diff --git a/src/applications/differential/field/specification/DifferentialGitSVNIDFieldSpecification.php b/src/applications/differential/field/specification/DifferentialGitSVNIDFieldSpecification.php
index 0b684aeab8..c07519ca93 100644
--- a/src/applications/differential/field/specification/DifferentialGitSVNIDFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialGitSVNIDFieldSpecification.php
@@ -1,53 +1,37 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialGitSVNIDFieldSpecification
extends DifferentialFieldSpecification {
private $gitSVNID;
public function shouldAppearOnCommitMessage() {
return true;
}
public function shouldAppearOnCommitMessageTemplate() {
return false;
}
public function getCommitMessageKey() {
return 'gitSVNID';
}
public function setValueFromParsedCommitMessage($value) {
$this->gitSVNID = $value;
return $this;
}
public function renderLabelForCommitMessage() {
return 'git-svn-id';
}
public function renderValueForCommitMessage($is_edit) {
return null;
}
public function parseValueFromCommitMessage($value) {
return $value;
}
}
diff --git a/src/applications/differential/field/specification/DifferentialHostFieldSpecification.php b/src/applications/differential/field/specification/DifferentialHostFieldSpecification.php
index 2d2f1a6bc9..1f9ed6b700 100644
--- a/src/applications/differential/field/specification/DifferentialHostFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialHostFieldSpecification.php
@@ -1,39 +1,23 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialHostFieldSpecification
extends DifferentialFieldSpecification {
public function shouldAppearOnRevisionView() {
return PhabricatorEnv::getEnvConfig('differential.show-host-field');
}
public function renderLabelForRevisionView() {
return 'Host:';
}
public function renderValueForRevisionView() {
$diff = $this->getManualDiff();
$host = $diff->getSourceMachine();
if (!$host) {
return null;
}
return phutil_escape_html($host);
}
}
diff --git a/src/applications/differential/field/specification/DifferentialLinesFieldSpecification.php b/src/applications/differential/field/specification/DifferentialLinesFieldSpecification.php
index a0e897202b..01ac9ec1f7 100644
--- a/src/applications/differential/field/specification/DifferentialLinesFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialLinesFieldSpecification.php
@@ -1,51 +1,35 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialLinesFieldSpecification
extends DifferentialFieldSpecification {
public function shouldAppearOnRevisionView() {
return true;
}
public function renderLabelForRevisionView() {
return 'Lines:';
}
public function renderValueForRevisionView() {
$diff = $this->getDiff();
return phutil_escape_html(number_format($diff->getLineCount()));
}
public function shouldAppearOnRevisionList() {
return true;
}
public function renderHeaderForRevisionList() {
return 'Lines';
}
public function getColumnClassForRevisionList() {
return 'n';
}
public function renderValueForRevisionList(DifferentialRevision $revision) {
return number_format($revision->getLineCount());
}
}
diff --git a/src/applications/differential/field/specification/DifferentialLintFieldSpecification.php b/src/applications/differential/field/specification/DifferentialLintFieldSpecification.php
index c29fd9b6e3..200d7ef0db 100644
--- a/src/applications/differential/field/specification/DifferentialLintFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialLintFieldSpecification.php
@@ -1,252 +1,236 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialLintFieldSpecification
extends DifferentialFieldSpecification {
public function shouldAppearOnRevisionView() {
return true;
}
public function renderLabelForRevisionView() {
return 'Lint:';
}
private function getLintExcuse() {
return $this->getDiffProperty('arc:lint-excuse');
}
private function getPostponedLinters() {
return $this->getDiffProperty('arc:lint-postponed');
}
public function renderValueForRevisionView() {
$diff = $this->getManualDiff();
$path_changesets = mpull($diff->loadChangesets(), 'getID', 'getFilename');
$lstar = DifferentialRevisionUpdateHistoryView::renderDiffLintStar($diff);
$lmsg = DifferentialRevisionUpdateHistoryView::getDiffLintMessage($diff);
$ldata = $this->getDiffProperty('arc:lint');
$ltail = null;
$rows = array();
$rows[] = array(
'style' => 'star',
'name' => $lstar,
'value' => $lmsg,
'show' => true,
);
$excuse = $this->getLintExcuse();
if ($excuse) {
$rows[] = array(
'style' => 'excuse',
'name' => 'Excuse',
'value' => nl2br(phutil_escape_html($excuse)),
'show' => true,
);
}
$show_limit = 10;
$hidden = array();
if ($ldata) {
$ldata = igroup($ldata, 'path');
foreach ($ldata as $path => $messages) {
$rows[] = array(
'style' => 'section',
'name' => phutil_escape_html($path),
'show' => $show_limit,
);
foreach ($messages as $message) {
$path = idx($message, 'path');
$line = idx($message, 'line');
$code = idx($message, 'code');
$severity = idx($message, 'severity');
$name = idx($message, 'name');
$description = idx($message, 'description');
$line_link = 'line '.intval($line);
if (isset($path_changesets[$path])) {
$href = '#C'.$path_changesets[$path].'NL'.max(1, $line);
if ($diff->getID() != $this->getDiff()->getID()) {
$href = '/D'.$diff->getRevisionID().'?id='.$diff->getID().$href;
}
$line_link = phutil_render_tag(
'a',
array(
'href' => $href,
),
$line_link);
}
if ($show_limit) {
--$show_limit;
$show = true;
} else {
$show = false;
if (empty($hidden[$severity])) {
$hidden[$severity] = 0;
}
$hidden[$severity]++;
}
$rows[] = array(
'style' => $this->getSeverityStyle($severity),
'name' => phutil_escape_html(ucwords($severity)),
'value' => hsprintf(
"(%s) %s at {$line_link}",
$code,
$name),
'show' => $show,
);
if (strlen($description)) {
$rows[] = array(
'style' => 'details',
'value' => nl2br(phutil_escape_html($description)),
'show' => false,
);
if (empty($hidden['details'])) {
$hidden['details'] = 0;
}
$hidden['details']++;
}
}
}
}
$postponed = $this->getPostponedLinters();
if ($postponed) {
foreach ($postponed as $linter) {
$rows[] = array(
'style' => $this->getPostponedStyle(),
'name' => 'Postponed',
'value' => phutil_escape_html($linter),
'show' => false,
);
if (empty($hidden['postponed'])) {
$hidden['postponed'] = 0;
}
$hidden['postponed']++;
}
}
$show_string = $this->renderShowString($hidden);
$view = new DifferentialResultsTableView();
$view->setRows($rows);
$view->setShowMoreString($show_string);
return $view->render();
}
private function getSeverityStyle($severity) {
$map = array(
ArcanistLintSeverity::SEVERITY_ERROR => 'red',
ArcanistLintSeverity::SEVERITY_WARNING => 'yellow',
ArcanistLintSeverity::SEVERITY_AUTOFIX => 'yellow',
ArcanistLintSeverity::SEVERITY_ADVICE => 'yellow',
);
return idx($map, $severity);
}
private function getPostponedStyle() {
return 'blue';
}
private function renderShowString(array $hidden) {
if (!$hidden) {
return null;
}
// Reorder hidden things by severity.
$hidden = array_select_keys(
$hidden,
array(
ArcanistLintSeverity::SEVERITY_ERROR,
ArcanistLintSeverity::SEVERITY_WARNING,
ArcanistLintSeverity::SEVERITY_AUTOFIX,
ArcanistLintSeverity::SEVERITY_ADVICE,
'details',
'postponed',
)) + $hidden;
$show = array();
foreach ($hidden as $key => $value) {
switch ($key) {
case ArcanistLintSeverity::SEVERITY_ERROR:
$show[] = pht('%d Error(s)', $value);
break;
case ArcanistLintSeverity::SEVERITY_WARNING:
$show[] = pht('%d Warning(s)', $value);
break;
case ArcanistLintSeverity::SEVERITY_AUTOFIX:
$show[] = pht('%d Auto-Fix(es)', $value);
break;
case ArcanistLintSeverity::SEVERITY_ADVICE:
$show[] = pht('%d Advice(s)', $value);
break;
case 'details':
$show[] = pht('%d Detail(s)', $value);
break;
case 'postponed':
$show[] = pht('%d Postponed', $value);
break;
default:
$show[] = $value;
break;
}
}
return "Show Full Lint Results (".implode(', ', $show).")";
}
public function renderWarningBoxForRevisionAccept() {
$diff = $this->getDiff();
$lint_warning = null;
if ($diff->getLintStatus() >= DifferentialLintStatus::LINT_WARN) {
$titles =
array(
DifferentialLintStatus::LINT_WARN => 'Lint Warning',
DifferentialLintStatus::LINT_FAIL => 'Lint Failure',
DifferentialLintStatus::LINT_SKIP => 'Lint Skipped'
);
if ($diff->getLintStatus() == DifferentialLintStatus::LINT_SKIP) {
$content =
"<p>This diff was created without running lint. Make sure you are ".
"OK with that before you accept this diff.</p>";
} else {
$content =
"<p>This diff has Lint Problems. Make sure you are OK with them ".
"before you accept this diff.</p>";
}
$lint_warning = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_ERROR)
->appendChild($content)
->setTitle(idx($titles, $diff->getLintStatus(), 'Warning'));
}
return $lint_warning;
}
}
diff --git a/src/applications/differential/field/specification/DifferentialManiphestTasksFieldSpecification.php b/src/applications/differential/field/specification/DifferentialManiphestTasksFieldSpecification.php
index b436b057e2..fe04ceacf5 100644
--- a/src/applications/differential/field/specification/DifferentialManiphestTasksFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialManiphestTasksFieldSpecification.php
@@ -1,191 +1,175 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialManiphestTasksFieldSpecification
extends DifferentialFieldSpecification {
private $maniphestTasks = array();
public function shouldAppearOnRevisionView() {
return PhabricatorEnv::getEnvConfig('maniphest.enabled');
}
public function getRequiredHandlePHIDsForRevisionView() {
return $this->getManiphestTaskPHIDs();
}
public function renderLabelForRevisionView() {
return 'Maniphest Tasks:';
}
public function renderValueForRevisionView() {
$task_phids = $this->getManiphestTaskPHIDs();
if (!$task_phids) {
return null;
}
$links = array();
foreach ($task_phids as $task_phid) {
$links[] = $this->getHandle($task_phid)->renderLink();
}
return implode('<br />', $links);
}
private function getManiphestTaskPHIDs() {
$revision = $this->getRevision();
if (!$revision->getPHID()) {
return array();
}
return PhabricatorEdgeQuery::loadDestinationPHIDs(
$revision->getPHID(),
PhabricatorEdgeConfig::TYPE_DREV_HAS_RELATED_TASK);
}
/**
* Attach the revision to the task(s) and the task(s) to the revision.
*
* @return void
*/
public function didWriteRevision(DifferentialRevisionEditor $editor) {
$revision = $editor->getRevision();
$revision_phid = $revision->getPHID();
$edge_type = PhabricatorEdgeConfig::TYPE_DREV_HAS_RELATED_TASK;
$old_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$revision_phid,
$edge_type);
$add_phids = $this->maniphestTasks;
$rem_phids = array_diff($old_phids, $add_phids);
$edge_editor = id(new PhabricatorEdgeEditor())
->setActor($this->getUser());
foreach ($add_phids as $phid) {
$edge_editor->addEdge($revision_phid, $edge_type, $phid);
}
foreach ($rem_phids as $phid) {
$edge_editor->removeEdge($revision_phid, $edge_type, $phid);
}
$edge_editor->save();
}
protected function didSetRevision() {
$this->maniphestTasks = $this->getManiphestTaskPHIDs();
}
public function getRequiredHandlePHIDsForCommitMessage() {
return $this->maniphestTasks;
}
public function shouldAppearOnCommitMessageTemplate() {
return PhabricatorEnv::getEnvConfig('maniphest.enabled');
}
public function shouldAppearOnCommitMessage() {
return PhabricatorEnv::getEnvConfig('maniphest.enabled');
}
public function getCommitMessageKey() {
return 'maniphestTaskPHIDs';
}
public function setValueFromParsedCommitMessage($value) {
$this->maniphestTasks = nonempty($value, array());
return $this;
}
public function renderLabelForCommitMessage() {
return 'Maniphest Tasks';
}
public function getSupportedCommitMessageLabels() {
return array(
'Maniphest Task',
'Maniphest Tasks',
);
}
public function renderValueForCommitMessage($is_edit) {
if (!$this->maniphestTasks) {
return null;
}
$names = array();
foreach ($this->maniphestTasks as $phid) {
$handle = $this->getHandle($phid);
$names[] = 'T'.$handle->getAlternateID();
}
return implode(', ', $names);
}
public function parseValueFromCommitMessage($value) {
$matches = null;
preg_match_all('/T(\d+)/', $value, $matches);
if (empty($matches[0])) {
return array();
}
$task_ids = $matches[1];
$tasks = id(new ManiphestTask())
->loadAllWhere('id in (%Ld)', $task_ids);
$task_phids = array();
$invalid = array();
foreach ($task_ids as $task_id) {
$task = idx($tasks, $task_id);
if (empty($task)) {
$invalid[] = 'T'.$task_id;
} else {
$task_phids[] = $task->getPHID();
}
}
if ($invalid) {
$what = pht('Maniphest Task(s)', count($invalid));
$invalid = implode(', ', $invalid);
throw new DifferentialFieldParseException(
"Commit message references nonexistent {$what}: {$invalid}.");
}
return $task_phids;
}
public function renderValueForMail($phase) {
if ($phase == DifferentialMailPhase::COMMENT) {
return null;
}
if (!$this->maniphestTasks) {
return null;
}
$handles = id(new PhabricatorObjectHandleData($this->maniphestTasks))
->loadHandles();
$body = array();
$body[] = 'MANIPHEST TASKS';
foreach ($handles as $handle) {
$body[] = ' '.PhabricatorEnv::getProductionURI($handle->getURI());
}
return implode("\n", $body);
}
}
diff --git a/src/applications/differential/field/specification/DifferentialPathFieldSpecification.php b/src/applications/differential/field/specification/DifferentialPathFieldSpecification.php
index 60eecd365f..7a85823833 100644
--- a/src/applications/differential/field/specification/DifferentialPathFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialPathFieldSpecification.php
@@ -1,41 +1,25 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialPathFieldSpecification
extends DifferentialFieldSpecification {
public function shouldAppearOnRevisionView() {
return PhabricatorEnv::getEnvConfig('differential.show-host-field');
}
public function renderLabelForRevisionView() {
return 'Path:';
}
public function renderValueForRevisionView() {
$diff = $this->getManualDiff();
$path = $diff->getSourcePath();
if (!$path) {
return null;
}
return phutil_escape_html($path);
}
}
diff --git a/src/applications/differential/field/specification/DifferentialRevertPlanFieldSpecification.php b/src/applications/differential/field/specification/DifferentialRevertPlanFieldSpecification.php
index 9a7cacaf76..4e84e3f492 100644
--- a/src/applications/differential/field/specification/DifferentialRevertPlanFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialRevertPlanFieldSpecification.php
@@ -1,114 +1,98 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialRevertPlanFieldSpecification
extends DifferentialFieldSpecification {
private $value;
public function getStorageKey() {
return 'phabricator:revert-plan';
}
public function getValueForStorage() {
return $this->value;
}
public function setValueFromStorage($value) {
$this->value = $value;
return $this;
}
public function shouldAppearOnEdit() {
return true;
}
public function setValueFromRequest(AphrontRequest $request) {
$this->value = $request->getStr($this->getStorageKey());
return $this;
}
public function renderEditControl() {
return id(new AphrontFormTextAreaControl())
->setLabel('Revert Plan')
->setName($this->getStorageKey())
->setCaption('Special steps required to safely revert this change.')
->setValue($this->value);
}
public function shouldAppearOnRevisionView() {
return true;
}
public function renderLabelForRevisionView() {
return 'Revert Plan:';
}
public function renderValueForRevisionView() {
if (!$this->value) {
return null;
}
return phutil_escape_html($this->value);
}
public function shouldAppearOnConduitView() {
return true;
}
public function getValueForConduit() {
return $this->value;
}
public function shouldAppearOnCommitMessage() {
return true;
}
public function getCommitMessageKey() {
return 'revertPlan';
}
public function setValueFromParsedCommitMessage($value) {
$this->value = $value;
return $this;
}
public function shouldOverwriteWhenCommitMessageIsEdited() {
return true;
}
public function renderLabelForCommitMessage() {
return 'Revert Plan';
}
public function renderValueForCommitMessage($is_edit) {
return $this->value;
}
public function getSupportedCommitMessageLabels() {
return array(
'Revert Plan',
'Revert',
);
}
public function parseValueFromCommitMessage($value) {
return $value;
}
}
diff --git a/src/applications/differential/field/specification/DifferentialReviewedByFieldSpecification.php b/src/applications/differential/field/specification/DifferentialReviewedByFieldSpecification.php
index 8ce201756c..f3a50ce209 100644
--- a/src/applications/differential/field/specification/DifferentialReviewedByFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialReviewedByFieldSpecification.php
@@ -1,80 +1,64 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialReviewedByFieldSpecification
extends DifferentialFieldSpecification {
private $reviewedBy;
protected function didSetRevision() {
$this->reviewedBy = array();
$revision = $this->getRevision();
$reviewer = $revision->loadReviewedBy();
if ($reviewer) {
$this->reviewedBy = array($reviewer);
}
}
public function shouldAppearOnCommitMessage() {
return true;
}
public function getCommitMessageKey() {
return 'reviewedByPHIDs';
}
public function setValueFromParsedCommitMessage($value) {
$this->reviewedBy = $value;
return $this;
}
public function shouldAppearOnCommitMessageTemplate() {
return false;
}
public function renderLabelForCommitMessage() {
return 'Reviewed By';
}
public function getRequiredHandlePHIDsForCommitMessage() {
return $this->reviewedBy;
}
public function renderValueForCommitMessage($is_edit) {
if ($is_edit) {
return null;
}
if (!$this->reviewedBy) {
return null;
}
$names = array();
foreach ($this->reviewedBy as $phid) {
$names[] = $this->getHandle($phid)->getName();
}
return implode(', ', $names);
}
public function parseValueFromCommitMessage($value) {
return $this->parseCommitMessageUserList($value);
}
}
diff --git a/src/applications/differential/field/specification/DifferentialReviewersFieldSpecification.php b/src/applications/differential/field/specification/DifferentialReviewersFieldSpecification.php
index c6afb6e93d..98ec21feb3 100644
--- a/src/applications/differential/field/specification/DifferentialReviewersFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialReviewersFieldSpecification.php
@@ -1,207 +1,191 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialReviewersFieldSpecification
extends DifferentialFieldSpecification {
private $reviewers = array();
private $error;
public function shouldAppearOnRevisionView() {
return true;
}
public function getRequiredHandlePHIDsForRevisionView() {
return $this->getReviewerPHIDs();
}
public function renderLabelForRevisionView() {
return 'Reviewers:';
}
public function renderValueForRevisionView() {
return $this->renderUserList($this->getReviewerPHIDs());
}
private function getReviewerPHIDs() {
$revision = $this->getRevision();
return $revision->getReviewers();
}
public function shouldAppearOnEdit() {
return true;
}
protected function didSetRevision() {
$this->reviewers = $this->getReviewerPHIDs();
}
public function getRequiredHandlePHIDsForRevisionEdit() {
return $this->reviewers;
}
public function setValueFromRequest(AphrontRequest $request) {
$this->reviewers = $request->getArr('reviewers');
return $this;
}
public function validateField() {
if (!$this->hasRevision()) {
return;
}
$self = PhabricatorEnv::getEnvConfig('differential.allow-self-accept');
if ($self) {
return;
}
$author_phid = $this->getRevision()->getAuthorPHID();
if (!in_array($author_phid, $this->reviewers)) {
return;
}
$this->error = 'Invalid';
throw new DifferentialFieldValidationException(
"The owner of a revision may not be a reviewer.");
}
public function renderEditControl() {
$reviewer_map = array();
foreach ($this->reviewers as $phid) {
$reviewer_map[$phid] = $this->getHandle($phid)->getFullName();
}
return id(new AphrontFormTokenizerControl())
->setLabel('Reviewers')
->setName('reviewers')
->setUser($this->getUser())
->setDatasource('/typeahead/common/users/')
->setValue($reviewer_map)
->setError($this->error);
}
public function willWriteRevision(DifferentialRevisionEditor $editor) {
$editor->setReviewers($this->reviewers);
}
public function shouldAppearOnCommitMessage() {
return true;
}
public function getCommitMessageKey() {
return 'reviewerPHIDs';
}
public function setValueFromParsedCommitMessage($value) {
$this->reviewers = nonempty($value, array());
return $this;
}
public function renderLabelForCommitMessage() {
return 'Reviewers';
}
public function getRequiredHandlePHIDsForCommitMessage() {
return $this->reviewers;
}
public function renderValueForCommitMessage($is_edit) {
if (!$this->reviewers) {
return null;
}
$names = array();
foreach ($this->reviewers as $phid) {
$names[] = $this->getHandle($phid)->getName();
}
return implode(', ', $names);
}
public function getSupportedCommitMessageLabels() {
return array(
'Reviewer',
'Reviewers',
);
}
public function parseValueFromCommitMessage($value) {
return $this->parseCommitMessageUserList($value);
}
public function shouldAppearOnRevisionList() {
return true;
}
public function renderHeaderForRevisionList() {
return 'Reviewers';
}
public function renderValueForRevisionList(DifferentialRevision $revision) {
$primary_reviewer = $revision->getPrimaryReviewer();
if ($primary_reviewer) {
$other_reviewers = array_flip($revision->getReviewers());
unset($other_reviewers[$primary_reviewer]);
if ($other_reviewers) {
$names = array();
foreach ($other_reviewers as $reviewer => $_) {
$names[] = phutil_escape_html(
$this->getHandle($reviewer)->getLinkName());
}
$suffix = ' '.javelin_render_tag(
'abbr',
array(
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => implode(', ', $names),
'align' => 'E',
),
),
'(+'.(count($names)).')');
} else {
$suffix = null;
}
return $this->getHandle($primary_reviewer)->renderLink().$suffix;
} else {
return '<em>None</em>';
}
}
public function getRequiredHandlePHIDsForRevisionList(
DifferentialRevision $revision) {
return $revision->getReviewers();
}
public function renderValueForMail($phase) {
if ($phase == DifferentialMailPhase::COMMENT) {
return null;
}
if (!$this->reviewers) {
return null;
}
$handles = id(new PhabricatorObjectHandleData($this->reviewers))
->loadHandles();
$handles = array_select_keys(
$handles,
array($this->getRevision()->getPrimaryReviewer())) + $handles;
$names = mpull($handles, 'getName');
return 'Reviewers: '.implode(', ', $names);
}
}
diff --git a/src/applications/differential/field/specification/DifferentialRevisionIDFieldSpecification.php b/src/applications/differential/field/specification/DifferentialRevisionIDFieldSpecification.php
index 57807b2ca7..0684a0fe39 100644
--- a/src/applications/differential/field/specification/DifferentialRevisionIDFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialRevisionIDFieldSpecification.php
@@ -1,128 +1,112 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialRevisionIDFieldSpecification
extends DifferentialFieldSpecification {
private $id;
protected function didSetRevision() {
$this->id = $this->getRevision()->getID();
}
public function shouldAppearOnCommitMessage() {
return true;
}
public function shouldAppearOnCommitMessageTemplate() {
return false;
}
public function getCommitMessageKey() {
return 'revisionID';
}
public function setValueFromParsedCommitMessage($value) {
$this->id = $value;
return $this;
}
public function renderLabelForCommitMessage() {
return 'Differential Revision';
}
public function renderValueForCommitMessage($is_edit) {
if (!$this->id) {
return null;
}
return PhabricatorEnv::getProductionURI('/D'.$this->id);
}
public function parseValueFromCommitMessage($value) {
$rev = trim($value);
if (!strlen($rev)) {
return null;
}
if (is_numeric($rev)) {
// TODO: Eventually, remove support for bare revision numbers.
return (int)$rev;
}
$rev = self::parseRevisionIDFromURI($rev);
if ($rev !== null) {
return $rev;
}
$example_uri = PhabricatorEnv::getProductionURI('/D123');
throw new DifferentialFieldParseException(
"Commit references invalid 'Differential Revision'. Expected a ".
"Phabricator URI like '{$example_uri}', got '{$value}'.");
}
public static function parseRevisionIDFromURI($uri) {
$path = id(new PhutilURI($uri))->getPath();
$matches = null;
if (preg_match('#^/D(\d+)$#', $path, $matches)) {
$id = (int)$matches[1];
// Make sure the URI is the same as our URI. Basically, we want to ignore
// commits from other Phabricator installs.
if ($uri == PhabricatorEnv::getProductionURI('/D'.$id)) {
return $id;
}
}
return null;
}
public function shouldAppearOnRevisionList() {
return true;
}
public function renderHeaderForRevisionList() {
return 'ID';
}
public function renderValueForRevisionList(DifferentialRevision $revision) {
return 'D'.$revision->getID();
}
public function renderValueForMail($phase) {
$body = array();
$body[] = 'REVISION DETAIL';
$body[] = ' '.PhabricatorEnv::getProductionURI('/D'.$this->id);
if ($phase == DifferentialMailPhase::UPDATE) {
$diffs = id(new DifferentialDiff())->loadAllWhere(
'revisionID = %d ORDER BY id DESC LIMIT 2',
$this->id);
if (count($diffs) == 2) {
list($new, $old) = array_values(mpull($diffs, 'getID'));
$body[] = null;
$body[] = 'CHANGE SINCE LAST DIFF';
$body[] = ' '.PhabricatorEnv::getProductionURI(
"/D{$this->id}?vs={$old}&id={$new}#toc");
}
}
return implode("\n", $body);
}
}
diff --git a/src/applications/differential/field/specification/DifferentialRevisionStatusFieldSpecification.php b/src/applications/differential/field/specification/DifferentialRevisionStatusFieldSpecification.php
index 0f63f2ecef..22788e1fc1 100644
--- a/src/applications/differential/field/specification/DifferentialRevisionStatusFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialRevisionStatusFieldSpecification.php
@@ -1,100 +1,84 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialRevisionStatusFieldSpecification
extends DifferentialFieldSpecification {
public function shouldAppearOnRevisionView() {
return true;
}
public function renderLabelForRevisionView() {
return 'Revision Status:';
}
public function renderValueForRevisionView() {
$revision = $this->getRevision();
$diff = $this->getDiff();
$status = $revision->getStatus();
$info = null;
$local_vcs = $diff->getSourceControlSystem();
$backing_vcs = $diff->getBackingVersionControlSystem();
if (!$backing_vcs) {
$backing_vcs = $local_vcs;
}
if ($status == ArcanistDifferentialRevisionStatus::ACCEPTED) {
$next_step = null;
if ($local_vcs == $backing_vcs) {
switch ($local_vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$next_step = '<tt>hg push</tt>';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$next_step = '<tt>arc land</tt>';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$next_step = '<tt>arc commit</tt>';
break;
}
} else {
$next_step = '<tt>arc amend</tt>';
}
if ($next_step) {
$info = ' &middot; Next step: '.$next_step;
}
} else if ($status == ArcanistDifferentialRevisionStatus::CLOSED) {
$committed = $revision->getDateCommitted();
if ($committed) {
$verb = null;
switch ($backing_vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$verb = 'Pushed';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$verb = 'Committed';
break;
}
if ($verb) {
$when = phabricator_datetime($committed, $this->getUser());
$info = " ({$verb} {$when})";
}
}
}
$status =
ArcanistDifferentialRevisionStatus::getNameForRevisionStatus($status);
return '<strong>'.$status.'</strong>'.$info;
}
public function shouldAppearOnRevisionList() {
return true;
}
public function renderHeaderForRevisionList() {
return 'Status';
}
public function renderValueForRevisionList(DifferentialRevision $revision) {
return ArcanistDifferentialRevisionStatus::getNameForRevisionStatus(
$revision->getStatus());
}
}
diff --git a/src/applications/differential/field/specification/DifferentialSummaryFieldSpecification.php b/src/applications/differential/field/specification/DifferentialSummaryFieldSpecification.php
index fb56aa301f..e47966cc02 100644
--- a/src/applications/differential/field/specification/DifferentialSummaryFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialSummaryFieldSpecification.php
@@ -1,89 +1,73 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialSummaryFieldSpecification
extends DifferentialFreeformFieldSpecification {
private $summary = '';
public function shouldAppearOnEdit() {
return true;
}
protected function didSetRevision() {
$this->summary = (string)$this->getRevision()->getSummary();
}
public function setValueFromRequest(AphrontRequest $request) {
$this->summary = $request->getStr('summary');
return $this;
}
public function renderEditControl() {
return id(new AphrontFormTextAreaControl())
->setLabel('Summary')
->setName('summary')
->setValue($this->summary);
}
public function willWriteRevision(DifferentialRevisionEditor $editor) {
$this->getRevision()->setSummary($this->summary);
}
public function shouldAppearOnCommitMessage() {
return true;
}
public function getCommitMessageKey() {
return 'summary';
}
public function setValueFromParsedCommitMessage($value) {
$this->summary = (string)$value;
return $this;
}
public function shouldOverwriteWhenCommitMessageIsEdited() {
return true;
}
public function renderLabelForCommitMessage() {
return 'Summary';
}
public function renderValueForCommitMessage($is_edit) {
return $this->summary;
}
public function parseValueFromCommitMessage($value) {
return (string)$value;
}
public function renderValueForMail($phase) {
if ($phase != DifferentialMailPhase::WELCOME) {
return null;
}
if ($this->summary == '') {
return null;
}
return $this->summary;
}
}
diff --git a/src/applications/differential/field/specification/DifferentialTestPlanFieldSpecification.php b/src/applications/differential/field/specification/DifferentialTestPlanFieldSpecification.php
index 1a4f943e35..30238271e2 100644
--- a/src/applications/differential/field/specification/DifferentialTestPlanFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialTestPlanFieldSpecification.php
@@ -1,126 +1,110 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialTestPlanFieldSpecification
extends DifferentialFieldSpecification {
private $plan = '';
// NOTE: This means "uninitialized".
private $error = false;
public function shouldAppearOnEdit() {
return PhabricatorEnv::getEnvConfig('differential.show-test-plan-field');
}
protected function didSetRevision() {
$this->plan = (string)$this->getRevision()->getTestPlan();
}
public function setValueFromRequest(AphrontRequest $request) {
$this->plan = $request->getStr('testplan');
$this->error = null;
return $this;
}
public function renderEditControl() {
if ($this->error === false) {
if ($this->isRequired()) {
$this->error = true;
} else {
$this->error = null;
}
}
return id(new AphrontFormTextAreaControl())
->setLabel('Test Plan')
->setName('testplan')
->setValue($this->plan)
->setError($this->error);
}
public function willWriteRevision(DifferentialRevisionEditor $editor) {
$this->getRevision()->setTestPlan($this->plan);
}
public function validateField() {
if ($this->isRequired()) {
if (!strlen($this->plan)) {
$this->error = 'Required';
throw new DifferentialFieldValidationException(
"You must provide a test plan.");
}
}
}
public function shouldAppearOnCommitMessage() {
return PhabricatorEnv::getEnvConfig('differential.show-test-plan-field');
}
public function getCommitMessageKey() {
return 'testPlan';
}
public function setValueFromParsedCommitMessage($value) {
$this->plan = (string)$value;
return $this;
}
public function shouldOverwriteWhenCommitMessageIsEdited() {
return true;
}
public function renderLabelForCommitMessage() {
return 'Test Plan';
}
public function getSupportedCommitMessageLabels() {
return array(
'Test Plan',
'Testplan',
'Tested',
'Tests',
);
}
public function renderValueForCommitMessage($is_edit) {
return $this->plan;
}
public function parseValueFromCommitMessage($value) {
return $value;
}
public function renderValueForMail($phase) {
if ($phase != DifferentialMailPhase::WELCOME) {
return null;
}
if ($this->plan == '') {
return null;
}
return "TEST PLAN\n".preg_replace('/^/m', ' ', $this->plan);
}
private function isRequired() {
return PhabricatorEnv::getEnvConfig('differential.require-test-plan-field');
}
}
diff --git a/src/applications/differential/field/specification/DifferentialTitleFieldSpecification.php b/src/applications/differential/field/specification/DifferentialTitleFieldSpecification.php
index 2fb4567679..9a785d07ab 100644
--- a/src/applications/differential/field/specification/DifferentialTitleFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialTitleFieldSpecification.php
@@ -1,110 +1,94 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialTitleFieldSpecification
extends DifferentialFreeformFieldSpecification {
private $title;
private $error = true;
public function shouldAppearOnEdit() {
return true;
}
protected function didSetRevision() {
$this->title = $this->getRevision()->getTitle();
}
public function setValueFromRequest(AphrontRequest $request) {
$this->title = $request->getStr('title');
$this->error = null;
return $this;
}
public function renderEditControl() {
return id(new AphrontFormTextAreaControl())
->setLabel('Title')
->setName('title')
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT)
->setError($this->error)
->setValue($this->title);
}
public function willWriteRevision(DifferentialRevisionEditor $editor) {
$this->getRevision()->setTitle($this->title);
}
public function validateField() {
if (!strlen($this->title)) {
$this->error = 'Required';
throw new DifferentialFieldValidationException(
"You must provide a title.");
}
}
public function shouldAppearOnCommitMessage() {
return true;
}
public function getCommitMessageKey() {
return 'title';
}
public function setValueFromParsedCommitMessage($value) {
$this->title = $value;
return $this;
}
public function shouldOverwriteWhenCommitMessageIsEdited() {
return true;
}
public function renderLabelForCommitMessage() {
return 'Title';
}
public function renderValueForCommitMessage($is_edit) {
return $this->title;
}
public function parseValueFromCommitMessage($value) {
return preg_replace('/\s*\n\s*/', ' ', $value);
}
public function shouldAppearOnRevisionList() {
return true;
}
public function renderHeaderForRevisionList() {
return 'Revision';
}
public function getColumnClassForRevisionList() {
return 'wide pri';
}
public function renderValueForRevisionList(DifferentialRevision $revision) {
return phutil_render_tag(
'a',
array(
'href' => '/D'.$revision->getID(),
),
phutil_escape_html($revision->getTitle()));
}
}
diff --git a/src/applications/differential/field/specification/DifferentialUnitFieldSpecification.php b/src/applications/differential/field/specification/DifferentialUnitFieldSpecification.php
index 556fc6e521..b235fe78df 100644
--- a/src/applications/differential/field/specification/DifferentialUnitFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialUnitFieldSpecification.php
@@ -1,227 +1,211 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialUnitFieldSpecification
extends DifferentialFieldSpecification {
public function shouldAppearOnRevisionView() {
return true;
}
public function renderLabelForRevisionView() {
return 'Unit:';
}
private function getUnitExcuse() {
return $this->getDiffProperty('arc:unit-excuse');
}
public function renderValueForRevisionView() {
$diff = $this->getManualDiff();
$ustar = DifferentialRevisionUpdateHistoryView::renderDiffUnitStar($diff);
$umsg = DifferentialRevisionUpdateHistoryView::getDiffUnitMessage($diff);
$rows = array();
$rows[] = array(
'style' => 'star',
'name' => $ustar,
'value' => $umsg,
'show' => true,
);
$excuse = $this->getUnitExcuse();
if ($excuse) {
$rows[] = array(
'style' => 'excuse',
'name' => 'Excuse',
'value' => nl2br(phutil_escape_html($excuse)),
'show' => true,
);
}
$show_limit = 10;
$hidden = array();
$udata = $this->getDiffProperty('arc:unit');
if ($udata) {
$sort_map = array(
ArcanistUnitTestResult::RESULT_BROKEN => 0,
ArcanistUnitTestResult::RESULT_FAIL => 1,
ArcanistUnitTestResult::RESULT_UNSOUND => 2,
ArcanistUnitTestResult::RESULT_SKIP => 3,
ArcanistUnitTestResult::RESULT_POSTPONED => 4,
ArcanistUnitTestResult::RESULT_PASS => 5,
);
foreach ($udata as $key => $test) {
$udata[$key]['sort'] = idx($sort_map, idx($test, 'result'));
}
$udata = isort($udata, 'sort');
foreach ($udata as $test) {
$result = idx($test, 'result');
$default_hide = false;
switch ($result) {
case ArcanistUnitTestResult::RESULT_POSTPONED:
case ArcanistUnitTestResult::RESULT_PASS:
$default_hide = true;
break;
}
if ($show_limit && !$default_hide) {
--$show_limit;
$show = true;
} else {
$show = false;
if (empty($hidden[$result])) {
$hidden[$result] = 0;
}
$hidden[$result]++;
}
$value = phutil_escape_html(idx($test, 'name'));
if (!empty($test['link'])) {
$value = phutil_render_tag(
'a',
array(
'href' => $test['link'],
'target' => '_blank',
),
$value);
}
$rows[] = array(
'style' => $this->getResultStyle($result),
'name' => phutil_escape_html(ucwords($result)),
'value' => $value,
'show' => $show,
);
$userdata = idx($test, 'userdata');
if ($userdata) {
$engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine();
$userdata = $engine->markupText($userdata);
$rows[] = array(
'style' => 'details',
'value' => $userdata,
'show' => false,
);
if (empty($hidden['details'])) {
$hidden['details'] = 0;
}
$hidden['details']++;
}
}
}
$show_string = $this->renderShowString($hidden);
$view = new DifferentialResultsTableView();
$view->setRows($rows);
$view->setShowMoreString($show_string);
return $view->render();
}
private function getResultStyle($result) {
$map = array(
ArcanistUnitTestResult::RESULT_PASS => 'green',
ArcanistUnitTestResult::RESULT_FAIL => 'red',
ArcanistUnitTestResult::RESULT_SKIP => 'blue',
ArcanistUnitTestResult::RESULT_BROKEN => 'red',
ArcanistUnitTestResult::RESULT_UNSOUND => 'yellow',
ArcanistUnitTestResult::RESULT_POSTPONED => 'blue',
);
return idx($map, $result);
}
private function renderShowString(array $hidden) {
if (!$hidden) {
return null;
}
// Reorder hidden things by severity.
$hidden = array_select_keys(
$hidden,
array(
ArcanistUnitTestResult::RESULT_BROKEN,
ArcanistUnitTestResult::RESULT_FAIL,
ArcanistUnitTestResult::RESULT_UNSOUND,
ArcanistUnitTestResult::RESULT_SKIP,
ArcanistUnitTestResult::RESULT_POSTPONED,
ArcanistUnitTestResult::RESULT_PASS,
'details',
)) + $hidden;
$noun = array(
ArcanistUnitTestResult::RESULT_BROKEN => 'Broken',
ArcanistUnitTestResult::RESULT_FAIL => 'Failed',
ArcanistUnitTestResult::RESULT_UNSOUND => 'Unsound',
ArcanistUnitTestResult::RESULT_SKIP => 'Skipped',
ArcanistUnitTestResult::RESULT_POSTPONED => 'Postponed',
ArcanistUnitTestResult::RESULT_PASS => 'Passed',
);
$show = array();
foreach ($hidden as $key => $value) {
if ($key == 'details') {
$show[] = pht('%d Detail(s)', $value);
} else {
$show[] = $value.' '.idx($noun, $key);
}
}
return "Show Full Unit Results (".implode(', ', $show).")";
}
public function renderWarningBoxForRevisionAccept() {
$diff = $this->getDiff();
$unit_warning = null;
if ($diff->getUnitStatus() >= DifferentialUnitStatus::UNIT_WARN) {
$titles =
array(
DifferentialUnitStatus::UNIT_WARN => 'Unit Tests Warning',
DifferentialUnitStatus::UNIT_FAIL => 'Unit Tests Failure',
DifferentialUnitStatus::UNIT_SKIP => 'Unit Tests Skipped',
DifferentialUnitStatus::UNIT_POSTPONED => 'Unit Tests Postponed'
);
if ($diff->getUnitStatus() == DifferentialUnitStatus::UNIT_POSTPONED) {
$content =
"<p>This diff has postponed unit tests. The results should be ".
"coming in soon. You should probably wait for them before accepting ".
"this diff.</p>";
} else if ($diff->getUnitStatus() == DifferentialUnitStatus::UNIT_SKIP) {
$content =
"<p>Unit tests were skipped when this diff was created. Make sure ".
"you are OK with that before you accept this diff.</p>";
} else {
$content =
"<p>This diff has Unit Test Problems. Make sure you are OK with ".
"them before you accept this diff.</p>";
}
$unit_warning = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_ERROR)
->appendChild($content)
->setTitle(idx($titles, $diff->getUnitStatus(), 'Warning'));
}
return $unit_warning;
}
}
diff --git a/src/applications/differential/field/specification/__tests__/DifferentialRevisionIDFieldParserTestCase.php b/src/applications/differential/field/specification/__tests__/DifferentialRevisionIDFieldParserTestCase.php
index f2bb9e3d32..2f47b91aff 100644
--- a/src/applications/differential/field/specification/__tests__/DifferentialRevisionIDFieldParserTestCase.php
+++ b/src/applications/differential/field/specification/__tests__/DifferentialRevisionIDFieldParserTestCase.php
@@ -1,48 +1,32 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialRevisionIDFieldParserTestCase
extends PhabricatorTestCase {
public function testFieldParser() {
$this->assertEqual(
null,
$this->parse('123'));
$this->assertEqual(
null,
$this->parse('D123'));
// NOTE: We expect foreign, validly-formatted URIs to be ignored.
$this->assertEqual(
null,
$this->parse('http://phabricator.example.com/D123'));
$this->assertEqual(
123,
$this->parse(PhabricatorEnv::getProductionURI('/D123')));
}
private function parse($value) {
return DifferentialRevisionIDFieldSpecification::parseRevisionIDFromURI(
$value);
}
}
diff --git a/src/applications/differential/mail/DifferentialCCWelcomeMail.php b/src/applications/differential/mail/DifferentialCCWelcomeMail.php
index 33a58f0124..8d96e47ec9 100644
--- a/src/applications/differential/mail/DifferentialCCWelcomeMail.php
+++ b/src/applications/differential/mail/DifferentialCCWelcomeMail.php
@@ -1,38 +1,22 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialCCWelcomeMail extends DifferentialReviewRequestMail {
protected function renderVaryPrefix() {
return '[Added to CC]';
}
protected function renderBody() {
$actor = $this->getActorName();
$name = $this->getRevision()->getTitle();
$body = array();
$body[] = "{$actor} added you to the CC list for the revision \"{$name}\".";
$body[] = null;
$body[] = $this->renderReviewRequestBody();
return implode("\n", $body);
}
}
diff --git a/src/applications/differential/mail/DifferentialCommentMail.php b/src/applications/differential/mail/DifferentialCommentMail.php
index 5ee13d3278..adf6f3f271 100644
--- a/src/applications/differential/mail/DifferentialCommentMail.php
+++ b/src/applications/differential/mail/DifferentialCommentMail.php
@@ -1,213 +1,197 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialCommentMail extends DifferentialMail {
protected $changedByCommit;
private $addedReviewers;
private $addedCCs;
public function setChangedByCommit($changed_by_commit) {
$this->changedByCommit = $changed_by_commit;
return $this;
}
public function getChangedByCommit() {
return $this->changedByCommit;
}
public function __construct(
DifferentialRevision $revision,
PhabricatorObjectHandle $actor,
DifferentialComment $comment,
array $changesets,
array $inline_comments) {
assert_instances_of($changesets, 'DifferentialChangeset');
assert_instances_of($inline_comments, 'PhabricatorInlineCommentInterface');
$this->setRevision($revision);
$this->setActorHandle($actor);
$this->setComment($comment);
$this->setChangesets($changesets);
$this->setInlineComments($inline_comments);
}
protected function getMailTags() {
$tags = array();
$comment = $this->getComment();
$action = $comment->getAction();
switch ($action) {
case DifferentialAction::ACTION_ADDCCS:
$tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_CC;
break;
case DifferentialAction::ACTION_CLOSE:
$tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_CLOSED;
break;
case DifferentialAction::ACTION_ADDREVIEWERS:
$tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_REVIEWERS;
break;
case DifferentialAction::ACTION_UPDATE:
$tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_UPDATED;
break;
case DifferentialAction::ACTION_REQUEST:
$tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_REVIEW_REQUEST;
break;
case DifferentialAction::ACTION_COMMENT:
// this is a comment which we will check separately below for content
break;
default:
$tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_OTHER;
break;
}
if (strlen(trim($comment->getContent()))) {
switch ($action) {
case DifferentialAction::ACTION_CLOSE:
// Commit comments are auto-generated and not especially interesting,
// so don't tag them as having a comment.
break;
default:
$tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_COMMENT;
break;
}
}
return $tags;
}
protected function renderVaryPrefix() {
$verb = ucwords($this->getVerb());
return "[{$verb}]";
}
protected function getVerb() {
$comment = $this->getComment();
$action = $comment->getAction();
$verb = DifferentialAction::getActionPastTenseVerb($action);
return $verb;
}
protected function prepareBody() {
parent::prepareBody();
// If the commented added reviewers or CCs, list them explicitly.
$meta = $this->getComment()->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) {
$this->addedReviewers = $this->renderHandleList($handles, $m_reviewers);
}
if ($m_cc) {
$this->addedCCs = $this->renderHandleList($handles, $m_cc);
}
}
}
protected function renderBody() {
$comment = $this->getComment();
$actor = $this->getActorName();
$name = $this->getRevision()->getTitle();
$verb = $this->getVerb();
$body = array();
$body[] = "{$actor} has {$verb} the revision \"{$name}\".";
if ($this->addedReviewers) {
$body[] = 'Added Reviewers: '.$this->addedReviewers;
}
if ($this->addedCCs) {
$body[] = 'Added CCs: '.$this->addedCCs;
}
$body[] = null;
$content = $comment->getContent();
if (strlen($content)) {
$body[] = $this->formatText($content);
$body[] = null;
}
if ($this->getChangedByCommit()) {
$body[] = 'CHANGED PRIOR TO COMMIT';
$body[] = ' '.$this->getChangedByCommit();
$body[] = null;
}
$inlines = $this->getInlineComments();
if ($inlines) {
$body[] = 'INLINE COMMENTS';
$changesets = $this->getChangesets();
if (PhabricatorEnv::getEnvConfig(
'metamta.differential.unified-comment-context', false)) {
foreach ($changesets as $changeset) {
$changeset->attachHunks($changeset->loadHunks());
}
}
foreach ($inlines as $inline) {
$changeset = $changesets[$inline->getChangesetID()];
if (!$changeset) {
throw new Exception('Changeset missing!');
}
$file = $changeset->getFilename();
$start = $inline->getLineNumber();
$len = $inline->getLineLength();
if ($len) {
$range = $start.'-'.($start + $len);
} else {
$range = $start;
}
$inline_content = $inline->getContent();
if (!PhabricatorEnv::getEnvConfig(
'metamta.differential.unified-comment-context', false)) {
$body[] = $this->formatText("{$file}:{$range} {$inline_content}");
} else {
$body[] = "================";
$body[] = "Comment at: " . $file . ":" . $range;
$body[] = $changeset->makeContextDiff($inline, 1);
$body[] = "----------------";
$body[] = $inline_content;
$body[] = null;
}
}
$body[] = null;
}
$body[] = $this->renderAuxFields(DifferentialMailPhase::COMMENT);
return implode("\n", $body);
}
}
diff --git a/src/applications/differential/mail/DifferentialDiffContentMail.php b/src/applications/differential/mail/DifferentialDiffContentMail.php
index 37b079c342..17822d8675 100644
--- a/src/applications/differential/mail/DifferentialDiffContentMail.php
+++ b/src/applications/differential/mail/DifferentialDiffContentMail.php
@@ -1,35 +1,19 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialDiffContentMail extends DifferentialMail {
protected $content;
public function __construct(DifferentialRevision $revision, $content) {
$this->setRevision($revision);
$this->content = $content;
}
protected function renderVaryPrefix() {
return '[Content]';
}
protected function renderBody() {
return $this->content;
}
}
diff --git a/src/applications/differential/mail/DifferentialExceptionMail.php b/src/applications/differential/mail/DifferentialExceptionMail.php
index 90ceed2e8e..efe90d0c04 100644
--- a/src/applications/differential/mail/DifferentialExceptionMail.php
+++ b/src/applications/differential/mail/DifferentialExceptionMail.php
@@ -1,63 +1,47 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialExceptionMail extends DifferentialMail {
public function __construct(
DifferentialRevision $revision,
Exception $exception,
$original_body) {
$this->revision = $revision;
$this->exception = $exception;
$this->originalBody = $original_body;
}
protected function renderBody() {
// Never called since buildBody() is overridden.
}
protected function renderSubject() {
return "Exception: unable to process your mail request";
}
protected function renderVaryPrefix() {
return '';
}
protected function buildBody() {
$exception = $this->exception;
$original_body = $this->originalBody;
$message = $exception->getMessage();
return <<<EOBODY
Your request failed because an exception was encoutered while processing it:
EXCEPTION: {$message}
-- Original Body -------------------------------------------------------------
{$original_body}
EOBODY;
}
}
diff --git a/src/applications/differential/mail/DifferentialMail.php b/src/applications/differential/mail/DifferentialMail.php
index a5cc25c1e2..37e329a1e7 100644
--- a/src/applications/differential/mail/DifferentialMail.php
+++ b/src/applications/differential/mail/DifferentialMail.php
@@ -1,466 +1,450 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DifferentialMail {
protected $to = array();
protected $cc = array();
protected $excludePHIDs = array();
protected $actorHandle;
protected $revision;
protected $comment;
protected $changesets;
protected $inlineComments;
protected $isFirstMailAboutRevision;
protected $isFirstMailToRecipients;
protected $heraldTranscriptURI;
protected $heraldRulesHeader;
protected $replyHandler;
protected $parentMessageID;
private $rawMail;
public function getRawMail() {
if (!$this->rawMail) {
throw new Exception("Call send() before getRawMail()!");
}
return $this->rawMail;
}
protected function renderSubject() {
$revision = $this->getRevision();
$title = $revision->getTitle();
$id = $revision->getID();
return "D{$id}: {$title}";
}
abstract protected function renderVaryPrefix();
abstract protected function renderBody();
public function setActorHandle($actor_handle) {
$this->actorHandle = $actor_handle;
return $this;
}
public function getActorHandle() {
return $this->actorHandle;
}
protected function getActorName() {
$handle = $this->getActorHandle();
if ($handle) {
return $handle->getName();
}
return '???';
}
public function setParentMessageID($parent_message_id) {
$this->parentMessageID = $parent_message_id;
return $this;
}
public function setXHeraldRulesHeader($header) {
$this->heraldRulesHeader = $header;
return $this;
}
public function send() {
$to_phids = $this->getToPHIDs();
if (!$to_phids) {
throw new Exception('No "To:" users provided!');
}
$cc_phids = $this->getCCPHIDs();
$attachments = $this->buildAttachments();
$template = new PhabricatorMetaMTAMail();
$actor_handle = $this->getActorHandle();
$reply_handler = $this->getReplyHandler();
if ($actor_handle) {
$template->setFrom($actor_handle->getPHID());
}
$template
->setIsHTML($this->shouldMarkMailAsHTML())
->setParentMessageID($this->parentMessageID)
->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs())
->addHeader('Thread-Topic', $this->getThreadTopic());
$template->setAttachments($attachments);
$template->setThreadID(
$this->getThreadID(),
$this->isFirstMailAboutRevision());
if ($this->heraldRulesHeader) {
$template->addHeader('X-Herald-Rules', $this->heraldRulesHeader);
}
$revision = $this->revision;
if ($revision) {
if ($revision->getAuthorPHID()) {
$template->addHeader(
'X-Differential-Author',
'<'.$revision->getAuthorPHID().'>');
}
$reviewer_phids = $revision->getReviewers();
if ($reviewer_phids) {
// Add several headers to support e-mail clients which are not able to
// create rules using regular expressions or wildcards (namely Outlook).
$template->addPHIDHeaders('X-Differential-Reviewer', $reviewer_phids);
// Add it also as a list to allow matching of the first reviewer and
// also for backwards compatibility.
$template->addHeader(
'X-Differential-Reviewers',
'<'.implode('>, <', $reviewer_phids).'>');
}
if ($cc_phids) {
$template->addPHIDHeaders('X-Differential-CC', $cc_phids);
$template->addHeader(
'X-Differential-CCs',
'<'.implode('>, <', $cc_phids).'>');
// Determine explicit CCs (those added by humans) and put them in a
// header so users can differentiate between Herald CCs and human CCs.
$relation_subscribed = DifferentialRevision::RELATION_SUBSCRIBED;
$raw = $revision->getRawRelations($relation_subscribed);
$reason_phids = ipull($raw, 'reasonPHID');
$reason_handles = id(new PhabricatorObjectHandleData($reason_phids))
->loadHandles();
$explicit_cc = array();
foreach ($raw as $relation) {
if (!$relation['reasonPHID']) {
continue;
}
$type = $reason_handles[$relation['reasonPHID']]->getType();
if ($type == PhabricatorPHIDConstants::PHID_TYPE_USER) {
$explicit_cc[] = $relation['objectPHID'];
}
}
if ($explicit_cc) {
$template->addPHIDHeaders('X-Differential-Explicit-CC', $explicit_cc);
$template->addHeader(
'X-Differential-Explicit-CCs',
'<'.implode('>, <', $explicit_cc).'>');
}
}
}
$template->setIsBulk(true);
$template->setRelatedPHID($this->getRevision()->getPHID());
$mailtags = $this->getMailTags();
if ($mailtags) {
$template->setMailTags($mailtags);
}
$phids = array();
foreach ($to_phids as $phid) {
$phids[$phid] = true;
}
foreach ($cc_phids as $phid) {
$phids[$phid] = true;
}
$phids = array_keys($phids);
$handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
$objects = id(new PhabricatorObjectHandleData($phids))->loadObjects();
$to_handles = array_select_keys($handles, $to_phids);
$cc_handles = array_select_keys($handles, $cc_phids);
$this->prepareBody();
$this->rawMail = clone $template;
$this->rawMail->addTos($to_phids);
$this->rawMail->addCCs($cc_phids);
$mails = $reply_handler->multiplexMail($template, $to_handles, $cc_handles);
$original_translator = PhutilTranslator::getInstance();
if (!PhabricatorMetaMTAMail::shouldMultiplexAllMail()) {
$translation = PhabricatorEnv::newObjectFromConfig(
'translation.provider');
$translator = id(new PhutilTranslator())
->setLanguage($translation->getLanguage())
->addTranslations($translation->getTranslations());
}
try {
foreach ($mails as $mail) {
if (PhabricatorMetaMTAMail::shouldMultiplexAllMail()) {
$translation = newv($mail->getTranslation($objects), array());
$translator = id(new PhutilTranslator())
->setLanguage($translation->getLanguage())
->addTranslations($translation->getTranslations());
PhutilTranslator::setInstance($translator);
}
$body =
$this->buildBody()."\n".
$reply_handler->getRecipientsSummary($to_handles, $cc_handles);
$mail
->setSubject($this->renderSubject())
->setSubjectPrefix($this->getSubjectPrefix())
->setVarySubjectPrefix($this->renderVaryPrefix())
->setBody($body);
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_DIFFERENTIAL_WILLSENDMAIL,
array(
'mail' => $mail,
)
);
PhutilEventEngine::dispatchEvent($event);
$mail = $event->getValue('mail');
$mail->saveAndSend();
}
} catch (Exception $ex) {
PhutilTranslator::setInstance($original_translator);
throw $ex;
}
PhutilTranslator::setInstance($original_translator);
return $this;
}
protected function getMailTags() {
return array();
}
protected function getSubjectPrefix() {
return PhabricatorEnv::getEnvConfig('metamta.differential.subject-prefix');
}
protected function shouldMarkMailAsHTML() {
return false;
}
/**
* @{method:buildBody} is called once for each e-mail recipient to allow
* translating text to his language. This method can be used to load data that
* don't need translation and use them later in @{method:buildBody}.
*
* @param
* @return
*/
protected function prepareBody() {
}
protected function buildBody() {
$main_body = $this->renderBody();
$body = new PhabricatorMetaMTAMailBody();
$body->addRawSection($main_body);
$reply_handler = $this->getReplyHandler();
$body->addReplySection($reply_handler->getReplyHandlerInstructions());
if ($this->getHeraldTranscriptURI() && $this->isFirstMailToRecipients()) {
$manage_uri = '/herald/view/differential/';
$xscript_uri = $this->getHeraldTranscriptURI();
$body->addHeraldSection($manage_uri, $xscript_uri);
}
return $body->render();
}
/**
* You can override this method in a subclass and return array of attachments
* to be sent with the email. Each attachment is an instance of
* PhabricatorMetaMTAAttachment.
*/
protected function buildAttachments() {
return array();
}
public function getReplyHandler() {
if (!$this->replyHandler) {
$this->replyHandler =
self::newReplyHandlerForRevision($this->getRevision());
}
return $this->replyHandler;
}
public static function newReplyHandlerForRevision(
DifferentialRevision $revision) {
$reply_handler = PhabricatorEnv::newObjectFromConfig(
'metamta.differential.reply-handler');
$reply_handler->setMailReceiver($revision);
return $reply_handler;
}
protected function formatText($text) {
$text = explode("\n", rtrim($text));
foreach ($text as &$line) {
$line = rtrim(' '.$line);
}
unset($line);
return implode("\n", $text);
}
public function setExcludeMailRecipientPHIDs(array $exclude) {
$this->excludePHIDs = $exclude;
return $this;
}
public function getExcludeMailRecipientPHIDs() {
return $this->excludePHIDs;
}
public function setToPHIDs(array $to) {
$this->to = $to;
return $this;
}
public function setCCPHIDs(array $cc) {
$this->cc = $cc;
return $this;
}
protected function getToPHIDs() {
return $this->to;
}
protected function getCCPHIDs() {
return $this->cc;
}
public function setRevision($revision) {
$this->revision = $revision;
return $this;
}
public function getRevision() {
return $this->revision;
}
protected function getThreadID() {
$phid = $this->getRevision()->getPHID();
return "differential-rev-{$phid}-req";
}
protected function getThreadTopic() {
$id = $this->getRevision()->getID();
$title = $this->getRevision()->getOriginalTitle();
return "D{$id}: {$title}";
}
public function setComment($comment) {
$this->comment = $comment;
return $this;
}
public function getComment() {
return $this->comment;
}
public function setChangesets($changesets) {
$this->changesets = $changesets;
return $this;
}
public function getChangesets() {
return $this->changesets;
}
public function setInlineComments(array $inline_comments) {
assert_instances_of($inline_comments, 'PhabricatorInlineCommentInterface');
$this->inlineComments = $inline_comments;
return $this;
}
public function getInlineComments() {
return $this->inlineComments;
}
protected function renderAuxFields($phase) {
$selector = DifferentialFieldSelector::newSelector();
$aux_fields = $selector->sortFieldsForMail(
$selector->getFieldSpecifications());
$body = array();
foreach ($aux_fields as $field) {
$field->setRevision($this->getRevision());
// TODO: Introduce and use getRequiredHandlePHIDsForMail() and load all
// handles in prepareBody().
$text = $field->renderValueForMail($phase);
if ($text !== null) {
$body[] = $text;
$body[] = null;
}
}
return implode("\n", $body);
}
public function setIsFirstMailToRecipients($first) {
$this->isFirstMailToRecipients = $first;
return $this;
}
public function isFirstMailToRecipients() {
return $this->isFirstMailToRecipients;
}
public function setIsFirstMailAboutRevision($first) {
$this->isFirstMailAboutRevision = $first;
return $this;
}
public function isFirstMailAboutRevision() {
return $this->isFirstMailAboutRevision;
}
public function setHeraldTranscriptURI($herald_transcript_uri) {
$this->heraldTranscriptURI = $herald_transcript_uri;
return $this;
}
public function getHeraldTranscriptURI() {
return $this->heraldTranscriptURI;
}
protected function renderHandleList(array $handles, array $phids) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$names = array();
foreach ($phids as $phid) {
$names[] = $handles[$phid]->getName();
}
return implode(', ', $names);
}
}
diff --git a/src/applications/differential/mail/DifferentialNewDiffMail.php b/src/applications/differential/mail/DifferentialNewDiffMail.php
index 04c8886f10..1779c71352 100644
--- a/src/applications/differential/mail/DifferentialNewDiffMail.php
+++ b/src/applications/differential/mail/DifferentialNewDiffMail.php
@@ -1,53 +1,37 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialNewDiffMail extends DifferentialReviewRequestMail {
protected function renderVaryPrefix() {
$revision = $this->getRevision();
$line_count = $revision->getLineCount();
$lines = pht('%d line(s)', $line_count);
if ($this->isFirstMailToRecipients()) {
$verb = 'Request';
} else {
$verb = 'Updated';
}
return "[{$verb}, {$lines}]";
}
protected function renderBody() {
$actor = $this->getActorName();
$name = $this->getRevision()->getTitle();
$body = array();
if ($this->isFirstMailToRecipients()) {
$body[] = "{$actor} requested code review of \"{$name}\".";
} else {
$body[] = "{$actor} updated the revision \"{$name}\".";
}
$body[] = null;
$body[] = $this->renderReviewRequestBody();
return implode("\n", $body);
}
}
diff --git a/src/applications/differential/mail/DifferentialReviewRequestMail.php b/src/applications/differential/mail/DifferentialReviewRequestMail.php
index 446579468f..8b50ab2184 100644
--- a/src/applications/differential/mail/DifferentialReviewRequestMail.php
+++ b/src/applications/differential/mail/DifferentialReviewRequestMail.php
@@ -1,152 +1,136 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DifferentialReviewRequestMail extends DifferentialMail {
protected $comments;
private $patch;
public function setComments($comments) {
$this->comments = $comments;
return $this;
}
public function getComments() {
return $this->comments;
}
public function __construct(
DifferentialRevision $revision,
PhabricatorObjectHandle $actor,
array $changesets) {
assert_instances_of($changesets, 'DifferentialChangeset');
$this->setRevision($revision);
$this->setActorHandle($actor);
$this->setChangesets($changesets);
}
protected function prepareBody() {
parent::prepareBody();
$inline_max_length = PhabricatorEnv::getEnvConfig(
'metamta.differential.inline-patches');
if ($inline_max_length) {
$patch = $this->buildPatch();
if (count(explode("\n", $patch)) <= $inline_max_length) {
$this->patch = $patch;
}
}
}
protected function renderReviewRequestBody() {
$revision = $this->getRevision();
$body = array();
if (!$this->isFirstMailToRecipients()) {
if (strlen($this->getComments())) {
$body[] = $this->formatText($this->getComments());
$body[] = null;
}
}
$phase = ($this->isFirstMailToRecipients() ?
DifferentialMailPhase::WELCOME :
DifferentialMailPhase::UPDATE);
$body[] = $this->renderAuxFields($phase);
$changesets = $this->getChangesets();
if ($changesets) {
$body[] = 'AFFECTED FILES';
foreach ($changesets as $changeset) {
$body[] = ' '.$changeset->getFilename();
}
$body[] = null;
}
if ($this->patch) {
$body[] = 'CHANGE DETAILS';
$body[] = $this->patch;
}
return implode("\n", $body);
}
protected function buildAttachments() {
$attachments = array();
if (PhabricatorEnv::getEnvConfig('metamta.differential.attach-patches')) {
$revision = $this->getRevision();
$revision_id = $revision->getID();
$diffs = $revision->loadDiffs();
$diff_number = count($diffs);
$attachments[] = new PhabricatorMetaMTAAttachment(
$this->buildPatch(),
"D{$revision_id}.{$diff_number}.patch",
'text/x-patch; charset=utf-8'
);
}
return $attachments;
}
public function loadFileByPHID($phid) {
$file = id(new PhabricatorFile())->loadOneWhere(
'phid = %s',
$phid);
if (!$file) {
return null;
}
return $file->loadFileData();
}
private function buildPatch() {
$diff = new DifferentialDiff();
$diff->attachChangesets($this->getChangesets());
// TODO: We could batch this to improve performance.
foreach ($diff->getChangesets() as $changeset) {
$changeset->attachHunks($changeset->loadHunks());
}
$diff_dict = $diff->getDiffDict();
$changes = array();
foreach ($diff_dict['changes'] as $changedict) {
$changes[] = ArcanistDiffChange::newFromDictionary($changedict);
}
$bundle = ArcanistBundle::newFromChanges($changes);
$bundle->setLoadFileDataCallback(array($this, 'loadFileByPHID'));
$format = PhabricatorEnv::getEnvConfig('metamta.differential.patch-format');
switch ($format) {
case 'git':
return $bundle->toGitPatch();
break;
case 'unified':
default:
return $bundle->toUnifiedDiff();
break;
}
}
}
diff --git a/src/applications/differential/parser/DifferentialChangesetParser.php b/src/applications/differential/parser/DifferentialChangesetParser.php
index 477306fa07..4ad2c02f31 100644
--- a/src/applications/differential/parser/DifferentialChangesetParser.php
+++ b/src/applications/differential/parser/DifferentialChangesetParser.php
@@ -1,2181 +1,2165 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialChangesetParser {
protected $visible = array();
protected $new = array();
protected $old = array();
protected $intra = array();
protected $newRender = null;
protected $oldRender = null;
protected $filename = null;
protected $missingOld = array();
protected $missingNew = array();
protected $comments = array();
protected $specialAttributes = array();
protected $changeset;
protected $whitespaceMode = null;
protected $subparser;
protected $renderCacheKey = null;
private $handles;
private $user;
private $leftSideChangesetID;
private $leftSideAttachesToNewFile;
private $rightSideChangesetID;
private $rightSideAttachesToNewFile;
private $originalLeft;
private $originalRight;
private $renderingReference;
private $isSubparser;
private $lineWidth = 80;
private $isTopLevel;
private $coverage;
private $markupEngine;
private $highlightErrors;
const CACHE_VERSION = 8;
const CACHE_MAX_SIZE = 8e6;
const ATTR_GENERATED = 'attr:generated';
const ATTR_DELETED = 'attr:deleted';
const ATTR_UNCHANGED = 'attr:unchanged';
const ATTR_WHITELINES = 'attr:white';
const LINES_CONTEXT = 8;
const WHITESPACE_SHOW_ALL = 'show-all';
const WHITESPACE_IGNORE_TRAILING = 'ignore-trailing';
// TODO: This is now "Ignore Most" in the UI.
const WHITESPACE_IGNORE_ALL = 'ignore-all';
const WHITESPACE_IGNORE_FORCE = 'ignore-force';
/**
* Configure which Changeset comments added to the right side of the visible
* diff will be attached to. The ID must be the ID of a real Differential
* Changeset.
*
* The complexity here is that we may show an arbitrary side of an arbitrary
* changeset as either the left or right part of a diff. This method allows
* the left and right halves of the displayed diff to be correctly mapped to
* storage changesets.
*
* @param id The Differential Changeset ID that comments added to the right
* side of the visible diff should be attached to.
* @param bool If true, attach new comments to the right side of the storage
* changeset. Note that this may be false, if the left side of
* some storage changeset is being shown as the right side of
* a display diff.
* @return this
*/
public function setRightSideCommentMapping($id, $is_new) {
$this->rightSideChangesetID = $id;
$this->rightSideAttachesToNewFile = $is_new;
return $this;
}
/**
* See setRightSideCommentMapping(), but this sets information for the left
* side of the display diff.
*/
public function setLeftSideCommentMapping($id, $is_new) {
$this->leftSideChangesetID = $id;
$this->leftSideAttachesToNewFile = $is_new;
return $this;
}
public function setOriginals(
DifferentialChangeset $left,
DifferentialChangeset $right) {
$this->originalLeft = $left;
$this->originalRight = $right;
}
public function diffOriginals() {
$engine = new PhabricatorDifferenceEngine();
$changeset = $engine->generateChangesetFromFileContent(
implode('', mpull($this->originalLeft->getHunks(), 'getChanges')),
implode('', mpull($this->originalRight->getHunks(), 'getChanges')));
// Put changes side by side.
$olds = array();
$news = array();
foreach ($changeset->getHunks() as $hunk) {
$n_old = $hunk->getOldOffset();
$n_new = $hunk->getNewOffset();
$changes = phutil_split_lines($hunk->getChanges());
foreach ($changes as $line) {
$diff_type = $line[0]; // Change type in diff of diffs.
$orig_type = $line[1]; // Change type in the original diff.
if ($diff_type == ' ') {
// Use the same key for lines that are next to each other.
$key = max(last_key($olds), last_key($news)) + 1;
$olds[$key] = null;
$news[$key] = null;
} else if ($diff_type == '-') {
$olds[] = array($n_old, $orig_type);
} else if ($diff_type == '+') {
$news[] = array($n_new, $orig_type);
}
if (($diff_type == '-' || $diff_type == ' ') && $orig_type != '-') {
$n_old++;
}
if (($diff_type == '+' || $diff_type == ' ') && $orig_type != '-') {
$n_new++;
}
}
}
$offsets_old = $this->originalLeft->computeOffsets();
$offsets_new = $this->originalRight->computeOffsets();
// Highlight lines that were added on each side or removed on the other
// side.
$highlight_old = array();
$highlight_new = array();
$last = max(last_key($olds), last_key($news));
for ($i = 0; $i <= $last; $i++) {
if (isset($olds[$i])) {
list($n, $type) = $olds[$i];
if ($type == '+' ||
($type == ' ' && isset($news[$i]) && $news[$i][1] != ' ')) {
$highlight_old[] = $offsets_old[$n];
}
}
if (isset($news[$i])) {
list($n, $type) = $news[$i];
if ($type == '+' ||
($type == ' ' && isset($olds[$i]) && $olds[$i][1] != ' ')) {
$highlight_new[] = $offsets_new[$n];
}
}
}
return array($highlight_old, $highlight_new);
}
/**
* Set a key for identifying this changeset in the render cache. If set, the
* parser will attempt to use the changeset render cache, which can improve
* performance for frequently-viewed changesets.
*
* By default, there is no render cache key and parsers do not use the cache.
* This is appropriate for rarely-viewed changesets.
*
* NOTE: Currently, this key must be a valid Differential Changeset ID.
*
* @param string Key for identifying this changeset in the render cache.
* @return this
*/
public function setRenderCacheKey($key) {
$this->renderCacheKey = $key;
return $this;
}
/**
* Set the character width at which lines will be wrapped. Defaults to 80.
*
* @param int Hard-wrap line-width for diff display.
* @return this
*/
public function setLineWidth($width) {
$this->lineWidth = $width;
return $this;
}
private function getRenderCacheKey() {
return $this->renderCacheKey;
}
public function setChangeset($changeset) {
$this->changeset = $changeset;
$this->setFilename($changeset->getFilename());
$this->setLineWidth($changeset->getWordWrapWidth());
return $this;
}
public function setWhitespaceMode($whitespace_mode) {
$this->whitespaceMode = $whitespace_mode;
return $this;
}
public function setRenderingReference($ref) {
$this->renderingReference = $ref;
return $this;
}
public function getChangeset() {
return $this->changeset;
}
public function setFilename($filename) {
$this->filename = $filename;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setMarkupEngine(PhabricatorMarkupEngine $engine) {
$this->markupEngine = $engine;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setCoverage($coverage) {
$this->coverage = $coverage;
return $this;
}
public function parseHunk(DifferentialHunk $hunk) {
$lines = $hunk->getChanges();
$lines = phutil_split_lines($lines);
$types = array();
foreach ($lines as $line_index => $line) {
if (isset($line[0])) {
$char = $line[0];
if ($char == ' ') {
$types[$line_index] = null;
} else {
$types[$line_index] = $char;
}
} else {
$types[$line_index] = null;
}
}
$old_line = $hunk->getOldOffset();
$new_line = $hunk->getNewOffset();
$num_lines = count($lines);
if ($old_line > 1) {
$this->missingOld[$old_line] = true;
} else if ($new_line > 1) {
$this->missingNew[$new_line] = true;
}
for ($cursor = 0; $cursor < $num_lines; $cursor++) {
$type = $types[$cursor];
$data = array(
'type' => $type,
'text' => (string)substr($lines[$cursor], 1),
'line' => $new_line,
);
if ($type == '\\') {
$type = $types[$cursor - 1];
$data['text'] = ltrim($data['text']);
}
switch ($type) {
case '+':
$this->new[] = $data;
++$new_line;
break;
case '-':
$data['line'] = $old_line;
$this->old[] = $data;
++$old_line;
break;
default:
$this->new[] = $data;
$data['line'] = $old_line;
$this->old[] = $data;
++$new_line;
++$old_line;
break;
}
}
}
public function parseInlineComment(
PhabricatorInlineCommentInterface $comment) {
// Parse only comments which are actually visible.
if ($this->isCommentVisibleOnRenderedDiff($comment)) {
$this->comments[] = $comment;
}
return $this;
}
public function process() {
$old = array();
$new = array();
$this->old = array_reverse($this->old);
$this->new = array_reverse($this->new);
$whitelines = false;
$changed = false;
$skip_intra = array();
while (count($this->old) || count($this->new)) {
$o_desc = array_pop($this->old);
$n_desc = array_pop($this->new);
if ($o_desc) {
$o_type = $o_desc['type'];
} else {
$o_type = null;
}
if ($n_desc) {
$n_type = $n_desc['type'];
} else {
$n_type = null;
}
if (($o_type != null) && ($n_type == null)) {
$old[] = $o_desc;
$new[] = null;
if ($n_desc) {
array_push($this->new, $n_desc);
}
$changed = true;
continue;
}
if (($n_type != null) && ($o_type == null)) {
$old[] = null;
$new[] = $n_desc;
if ($o_desc) {
array_push($this->old, $o_desc);
}
$changed = true;
continue;
}
if ($this->whitespaceMode != self::WHITESPACE_SHOW_ALL) {
$similar = false;
switch ($this->whitespaceMode) {
case self::WHITESPACE_IGNORE_TRAILING:
if (rtrim($o_desc['text']) == rtrim($n_desc['text'])) {
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;
}
if ($similar) {
if ($o_desc['type'] == '\\') {
// These are similar because they're "No newline at end of file"
// comments.
} else {
$o_desc['type'] = null;
$n_desc['type'] = null;
$skip_intra[count($old)] = true;
}
} else {
$changed = true;
}
} else {
$changed = true;
}
$old[] = $o_desc;
$new[] = $n_desc;
}
$this->old = $old;
$this->new = $new;
$unchanged = false;
if ($this->subparser) {
$unchanged = $this->subparser->isUnchanged();
$whitelines = $this->subparser->isWhitespaceOnly();
} else if (!$changed) {
$filetype = $this->changeset->getFileType();
if ($filetype == DifferentialChangeType::FILE_TEXT ||
$filetype == DifferentialChangeType::FILE_SYMLINK) {
$unchanged = true;
}
}
$changetype = $this->changeset->getChangeType();
if ($changetype == DifferentialChangeType::TYPE_MOVE_AWAY) {
// sometimes we show moved files as unchanged, sometimes deleted,
// and sometimes inconsistent with what actually happened at the
// destination of the move. Rather than make a false claim,
// omit the 'not changed' notice if this is the source of a move
$unchanged = false;
}
$this->specialAttributes = array(
self::ATTR_UNCHANGED => $unchanged,
self::ATTR_DELETED => array_filter($this->old) &&
!array_filter($this->new),
self::ATTR_WHITELINES => $whitelines
);
if ($this->isSubparser) {
// The rest of this function deals with formatting the diff for display;
// we can exit early if we're a subparser and avoid doing extra work.
return;
}
if ($this->subparser) {
// Use this parser's side-by-side line information -- notably, the
// change types -- but replace all the line text with the subparser's.
// This lets us render whitespace-only changes without marking them as
// different.
$old = $this->old;
$new = $this->new;
$old_text = ipull($this->subparser->old, 'text', 'line');
$new_text = ipull($this->subparser->new, 'text', 'line');
foreach ($old as $k => $desc) {
if (empty($desc)) {
continue;
}
$old[$k]['text'] = idx($old_text, $desc['line']);
}
foreach ($new as $k => $desc) {
if (empty($desc)) {
continue;
}
$new[$k]['text'] = idx($new_text, $desc['line']);
if ($this->whitespaceMode == self::WHITESPACE_IGNORE_FORCE) {
// Under forced ignore mode, ignore even internal whitespace
// changes.
continue;
}
// If there's a corresponding "old" text and the line is marked as
// unchanged, test if there are internal whitespace changes between
// non-whitespace characters, e.g. spaces added to a string or spaces
// added around operators. If we find internal spaces, mark the line
// as changed.
//
// We only need to do this for "new" lines because any line that is
// missing either "old" or "new" text certainly can not have internal
// whitespace changes without also having non-whitespace changes,
// because characters had to be either added or removed to create the
// possibility of internal whitespace.
if (isset($old[$k]['text']) && empty($new[$k]['type'])) {
if (trim($old[$k]['text']) != trim($new[$k]['text'])) {
// The strings aren't the same when trimmed, so there are internal
// whitespace changes. Mark this line changed.
$old[$k]['type'] = '-';
$new[$k]['type'] = '+';
// Re-mark this line for intraline diffing.
unset($skip_intra[$k]);
}
}
}
$this->old = $old;
$this->new = $new;
}
$min_length = min(count($this->old), count($this->new));
for ($ii = 0; $ii < $min_length; $ii++) {
if ($this->old[$ii] || $this->new[$ii]) {
if (isset($this->old[$ii]['text'])) {
$otext = $this->old[$ii]['text'];
} else {
$otext = '';
}
if (isset($this->new[$ii]['text'])) {
$ntext = $this->new[$ii]['text'];
} else {
$ntext = '';
}
if ($otext != $ntext && empty($skip_intra[$ii])) {
$this->intra[$ii] = ArcanistDiffUtils::generateIntralineDiff(
$otext,
$ntext);
}
}
}
$lines_context = self::LINES_CONTEXT;
$max_length = max(count($this->old), count($this->new));
$old = $this->old;
$new = $this->new;
$visible = false;
$last = 0;
for ($cursor = -$lines_context; $cursor < $max_length; $cursor++) {
$offset = $cursor + $lines_context;
if ((isset($old[$offset]) && $old[$offset]['type']) ||
(isset($new[$offset]) && $new[$offset]['type'])) {
$visible = true;
$last = $offset;
} else if ($cursor > $last + $lines_context) {
$visible = false;
}
if ($visible && $cursor > 0) {
$this->visible[$cursor] = 1;
}
}
$old_corpus = array();
foreach ($this->old as $o) {
if ($o['type'] != '\\') {
if ($o['text'] === null) {
// There's no text on this side of the diff, but insert a placeholder
// newline so the highlighted line numbers match up.
$old_corpus[] = "\n";
} else {
$old_corpus[] = $o['text'];
}
}
}
$old_corpus_block = implode('', $old_corpus);
$new_corpus = array();
foreach ($this->new as $n) {
if ($n['type'] != '\\') {
if ($n['text'] === null) {
$new_corpus[] = "\n";
} else {
$new_corpus[] = $n['text'];
}
}
}
$new_corpus_block = implode('', $new_corpus);
$this->markGenerated($new_corpus_block);
if ($this->isTopLevel && !$this->comments &&
($this->isGenerated() || $this->isUnchanged() || $this->isDeleted())) {
return;
}
$old_future = $this->getHighlightFuture($old_corpus_block);
$new_future = $this->getHighlightFuture($new_corpus_block);
$futures = array(
'old' => $old_future,
'new' => $new_future,
);
$corpus_blocks = array(
'old' => $old_corpus_block,
'new' => $new_corpus_block,
);
$this->highlightErrors = false;
foreach (Futures($futures) as $key => $future) {
try {
try {
$highlighted = $future->resolve();
} catch (PhutilSyntaxHighlighterException $ex) {
$this->highlightErrors = true;
$highlighted = id(new PhutilDefaultSyntaxHighlighter())
->getHighlightFuture($corpus_blocks[$key])
->resolve();
}
switch ($key) {
case 'old':
$this->oldRender = $this->processHighlightedSource(
$this->old,
$highlighted);
break;
case 'new':
$this->newRender = $this->processHighlightedSource(
$this->new,
$highlighted);
break;
}
} catch (Exception $ex) {
phlog($ex);
throw $ex;
}
}
$this->applyIntraline(
$this->oldRender,
ipull($this->intra, 0),
$old_corpus);
$this->applyIntraline(
$this->newRender,
ipull($this->intra, 1),
$new_corpus);
}
public function loadCache() {
$render_cache_key = $this->getRenderCacheKey();
if (!$render_cache_key) {
return false;
}
$data = null;
$changeset = new DifferentialChangeset();
$conn_r = $changeset->establishConnection('r');
$data = queryfx_one(
$conn_r,
'SELECT * FROM %T WHERE id = %d',
$changeset->getTableName().'_parse_cache',
$render_cache_key);
if (!$data) {
return false;
}
$data = json_decode($data['cache'], true);
if (!is_array($data) || !$data) {
return false;
}
foreach (self::getCacheableProperties() as $cache_key) {
if (!array_key_exists($cache_key, $data)) {
// If we're missing a cache key, assume we're looking at an old cache
// and ignore it.
return false;
}
}
if ($data['cacheVersion'] !== self::CACHE_VERSION) {
return false;
}
// Someone displays contents of a partially cached shielded file.
if (!isset($data['newRender']) && (!$this->isTopLevel || $this->comments)) {
return false;
}
unset($data['cacheVersion'], $data['cacheHost']);
$cache_prop = array_select_keys($data, self::getCacheableProperties());
foreach ($cache_prop as $cache_key => $v) {
$this->$cache_key = $v;
}
return true;
}
protected static function getCacheableProperties() {
return array(
'visible',
'new',
'old',
'intra',
'newRender',
'oldRender',
'specialAttributes',
'missingOld',
'missingNew',
'cacheVersion',
'cacheHost',
);
}
public function saveCache() {
if ($this->highlightErrors) {
return false;
}
$render_cache_key = $this->getRenderCacheKey();
if (!$render_cache_key) {
return false;
}
$cache = array();
foreach (self::getCacheableProperties() as $cache_key) {
switch ($cache_key) {
case 'cacheVersion':
$cache[$cache_key] = self::CACHE_VERSION;
break;
case 'cacheHost':
$cache[$cache_key] = php_uname('n');
break;
default:
$cache[$cache_key] = $this->$cache_key;
break;
}
}
$cache = json_encode($cache);
// We don't want to waste too much space by a single changeset.
if (strlen($cache) > self::CACHE_MAX_SIZE) {
return;
}
try {
$changeset = new DifferentialChangeset();
$conn_w = $changeset->establishConnection('w');
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
queryfx(
$conn_w,
'INSERT INTO %T (id, cache, dateCreated) VALUES (%d, %s, %d)
ON DUPLICATE KEY UPDATE cache = VALUES(cache)',
DifferentialChangeset::TABLE_CACHE,
$render_cache_key,
$cache,
time());
} catch (AphrontQueryException $ex) {
// TODO: uhoh
}
}
private function markGenerated($new_corpus_block = '') {
$generated_guess = (strpos($new_corpus_block, '@'.'generated') !== false);
if (!$generated_guess) {
$config_key = 'differential.generated-paths';
$generated_path_regexps = PhabricatorEnv::getEnvConfig($config_key);
foreach ($generated_path_regexps as $regexp) {
if (preg_match($regexp, $this->changeset->getFilename())) {
$generated_guess = true;
break;
}
}
}
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_DIFFERENTIAL_WILLMARKGENERATED,
array(
'corpus' => $new_corpus_block,
'is_generated' => $generated_guess,
)
);
PhutilEventEngine::dispatchEvent($event);
$generated = $event->getValue('is_generated');
$this->specialAttributes[self::ATTR_GENERATED] = $generated;
}
public function isGenerated() {
return idx($this->specialAttributes, self::ATTR_GENERATED, false);
}
public function isDeleted() {
return idx($this->specialAttributes, self::ATTR_DELETED, false);
}
public function isUnchanged() {
return idx($this->specialAttributes, self::ATTR_UNCHANGED, false);
}
public function isWhitespaceOnly() {
return idx($this->specialAttributes, self::ATTR_WHITELINES, false);
}
public function getLength() {
return max(count($this->old), count($this->new));
}
protected function applyIntraline(&$render, $intra, $corpus) {
$line_break = "<span class=\"over-the-line\">\xE2\xAC\x85</span><br />";
foreach ($render as $key => $text) {
if (isset($intra[$key])) {
$render[$key] = ArcanistDiffUtils::applyIntralineDiff(
$text,
$intra[$key]);
}
if (isset($corpus[$key]) &&
strlen($corpus[$key]) > $this->lineWidth &&
strlen(rtrim($corpus[$key], "\r\n")) > $this->lineWidth) {
$lines = phutil_utf8_hard_wrap_html($render[$key], $this->lineWidth);
$render[$key] = implode($line_break, $lines);
}
}
}
protected function getHighlightFuture($corpus) {
if (preg_match('/\r(?!\n)/', $corpus)) {
// TODO: Pygments converts "\r" newlines into "\n" newlines, so we can't
// use it on files with "\r" newlines. If we have "\r" not followed by
// "\n" in the file, skip highlighting.
$result = phutil_escape_html($corpus);
return new ImmediateFuture($result);
}
return $this->highlightEngine->getHighlightFuture(
$this->highlightEngine->getLanguageFromFilename($this->filename),
$corpus);
}
protected function processHighlightedSource($data, $result) {
$result_lines = phutil_split_lines($result);
foreach ($data as $key => $info) {
if (!$info) {
unset($result_lines[$key]);
}
}
return $result_lines;
}
private function tryCacheStuff() {
$whitespace_mode = $this->whitespaceMode;
switch ($whitespace_mode) {
case self::WHITESPACE_SHOW_ALL:
case self::WHITESPACE_IGNORE_TRAILING:
case self::WHITESPACE_IGNORE_FORCE:
break;
default:
$whitespace_mode = self::WHITESPACE_IGNORE_ALL;
break;
}
$skip_cache = ($whitespace_mode != self::WHITESPACE_IGNORE_ALL);
$this->whitespaceMode = $whitespace_mode;
$changeset = $this->changeset;
if ($changeset->getFileType() != DifferentialChangeType::FILE_TEXT &&
$changeset->getFileType() != DifferentialChangeType::FILE_SYMLINK) {
$this->markGenerated();
} else {
if ($skip_cache || !$this->loadCache()) {
$ignore_all = (($whitespace_mode == self::WHITESPACE_IGNORE_ALL) ||
($whitespace_mode == self::WHITESPACE_IGNORE_FORCE));
$force_ignore = ($whitespace_mode == self::WHITESPACE_IGNORE_FORCE);
if (!$force_ignore) {
if ($ignore_all && $changeset->getWhitespaceMatters()) {
$ignore_all = false;
}
}
// 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.
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 ($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
// in process() so we highlight only changed lines but render
// whitespace differences. If we don't do this, we either fail to
// render whitespace changes (which is incredibly confusing,
// especially for python) or often produce a much larger set of
// differences than necessary.
$engine = new PhabricatorDifferenceEngine();
$engine->setIgnoreWhitespace(true);
$no_whitespace_changeset = $engine->generateChangesetFromFileContent(
$old_file,
$new_file);
// subparser takes over the current non-whitespace-ignoring changeset
$subparser = new DifferentialChangesetParser();
$subparser->isSubparser = true;
$subparser->setChangeset($changeset);
foreach ($changeset->getHunks() as $hunk) {
$subparser->parseHunk($hunk);
}
// We need to call process() so that the subparser's values for
// metadata (like 'unchanged') is correct.
$subparser->process();
$this->subparser = $subparser;
// While we aren't updating $this->changeset (since it has a bunch
// of metadata we need to preserve, so that headers like "this file
// was moved" render correctly), we're overwriting the local
// $changeset so that the block below will choose the synthetic
// hunks we've built instead of the original hunks.
$changeset = $no_whitespace_changeset;
}
// This either uses the real hunks, or synthetic hunks we built above.
foreach ($changeset->getHunks() as $hunk) {
$this->parseHunk($hunk);
}
$this->process();
if (!$skip_cache) {
$this->saveCache();
}
}
}
}
public function render(
$range_start = null,
$range_len = null,
$mask_force = array()) {
// "Top level" renders are initial requests for the whole file, versus
// requests for a specific range generated by clicking "show more". We
// generate property changes and "shield" UI elements only for toplevel
// requests.
$this->isTopLevel = (($range_start === null) && ($range_len === null));
$this->highlightEngine = PhabricatorSyntaxHighlighter::newEngine();
$this->tryCacheStuff();
$shield = null;
if ($this->isTopLevel && !$this->comments) {
if ($this->isGenerated()) {
$shield = $this->renderShield(
"This file contains generated code, which does not normally need ".
"to be reviewed.",
true);
} else if ($this->isUnchanged()) {
if ($this->isWhitespaceOnly()) {
$shield = $this->renderShield(
"This file was changed only by adding or removing trailing ".
"whitespace.",
false);
} else {
$shield = $this->renderShield(
"The contents of this file were not changed.",
false);
}
} else if ($this->isDeleted()) {
$shield = $this->renderShield(
"This file was completely deleted.",
true);
} else if ($this->changeset->getAffectedLineCount() > 2500) {
$lines = number_format($this->changeset->getAffectedLineCount());
$shield = $this->renderShield(
"This file has a very large number of changes ({$lines} lines).",
true);
}
}
if ($shield) {
return $this->renderChangesetTable($this->changeset, $shield);
}
$feedback_mask = array();
switch ($this->changeset->getFileType()) {
case DifferentialChangeType::FILE_IMAGE:
$old = null;
$cur = null;
// TODO: Improve the architectural issue as discussed in D955
// https://secure.phabricator.com/D955
$reference = $this->renderingReference;
$parts = explode('/', $reference);
if (count($parts) == 2) {
list($id, $vs) = $parts;
} else {
$id = $parts[0];
$vs = 0;
}
$id = (int)$id;
$vs = (int)$vs;
if (!$vs) {
$metadata = $this->changeset->getMetadata();
$data = idx($metadata, 'attachment-data');
$old_phid = idx($metadata, 'old:binary-phid');
$new_phid = idx($metadata, 'new:binary-phid');
} else {
$vs_changeset = id(new DifferentialChangeset())->load($vs);
$vs_metadata = $vs_changeset->getMetadata();
$old_phid = idx($vs_metadata, 'new:binary-phid');
$changeset = id(new DifferentialChangeset())->load($id);
$metadata = $changeset->getMetadata();
$new_phid = idx($metadata, 'new:binary-phid');
}
if ($old_phid || $new_phid) {
// grab the files, (micro) optimization for 1 query not 2
$file_phids = array();
if ($old_phid) {
$file_phids[] = $old_phid;
}
if ($new_phid) {
$file_phids[] = $new_phid;
}
$files = id(new PhabricatorFile())->loadAllWhere(
'phid IN (%Ls)',
$file_phids);
foreach ($files as $file) {
if (empty($file)) {
continue;
}
if ($file->getPHID() == $old_phid) {
$old = phutil_render_tag(
'img',
array(
'src' => $file->getBestURI(),
));
} else {
$cur = phutil_render_tag(
'img',
array(
'src' => $file->getBestURI(),
));
}
}
}
$this->comments = msort($this->comments, 'getID');
$old_comments = array();
$new_comments = array();
foreach ($this->comments as $comment) {
if ($this->isCommentOnRightSideWhenDisplayed($comment)) {
$new_comments[] = $comment;
} else {
$old_comments[] = $comment;
}
}
$html_old = array();
$html_new = array();
foreach ($old_comments as $comment) {
$xhp = $this->renderInlineComment($comment);
$html_old[] =
'<tr class="inline"><th /><td>'.
$xhp.
'</td><th /><td colspan="2" /></tr>';
}
foreach ($new_comments as $comment) {
$xhp = $this->renderInlineComment($comment);
$html_new[] =
'<tr class="inline"><th /><td /><th /><td colspan="2">'.
$xhp.
'</td></tr>';
}
if (!$old) {
$th_old = '<th></th>';
}
else {
$th_old = '<th id="C'.$vs.'OL1">1</th>';
}
if (!$cur) {
$th_new = '<th></th>';
}
else {
$th_new = '<th id="C'.$id.'NL1">1</th>';
}
$output = $this->renderChangesetTable(
$this->changeset,
'<tr class="differential-image-diff">'.
$th_old.
'<td class="differential-old-image">'.
'<div class="differential-image-stage">'.
$old.
'</div>'.
'</td>'.
$th_new.
'<td class="copy differential-new-image"></td>'.
'<td class="differential-new-image">'.
'<div class="differential-image-stage">'.
$cur.
'</div>'.
'</td>'.
'</tr>'.
implode('', $html_old).
implode('', $html_new));
return $output;
case DifferentialChangeType::FILE_DIRECTORY:
case DifferentialChangeType::FILE_BINARY:
$output = $this->renderChangesetTable($this->changeset, null);
return $output;
}
$old_comments = array();
$new_comments = array();
$old_mask = array();
$new_mask = array();
$feedback_mask = array();
if ($this->comments) {
foreach ($this->comments as $comment) {
$start = max($comment->getLineNumber() - self::LINES_CONTEXT, 0);
$end = $comment->getLineNumber() +
$comment->getLineLength() +
self::LINES_CONTEXT;
$new = $this->isCommentOnRightSideWhenDisplayed($comment);
for ($ii = $start; $ii <= $end; $ii++) {
if ($new) {
$new_mask[$ii] = true;
} else {
$old_mask[$ii] = true;
}
}
}
foreach ($this->old as $ii => $old) {
if (isset($old['line']) && isset($old_mask[$old['line']])) {
$feedback_mask[$ii] = true;
}
}
foreach ($this->new as $ii => $new) {
if (isset($new['line']) && isset($new_mask[$new['line']])) {
$feedback_mask[$ii] = true;
}
}
$this->comments = msort($this->comments, 'getID');
foreach ($this->comments as $comment) {
$final = $comment->getLineNumber() +
$comment->getLineLength();
$final = max(1, $final);
if ($this->isCommentOnRightSideWhenDisplayed($comment)) {
$new_comments[$final][] = $comment;
} else {
$old_comments[$final][] = $comment;
}
}
}
$html = $this->renderTextChange(
$range_start,
$range_len,
$mask_force,
$feedback_mask,
$old_comments,
$new_comments);
return $this->renderChangesetTable($this->changeset, $html);
}
/**
* Determine if an inline comment will appear on the rendered diff,
* taking into consideration which halves of which changesets will actually
* be shown.
*
* @param PhabricatorInlineCommentInterface Comment to test for visibility.
* @return bool True if the comment is visible on the rendered diff.
*/
private function isCommentVisibleOnRenderedDiff(
PhabricatorInlineCommentInterface $comment) {
$changeset_id = $comment->getChangesetID();
$is_new = $comment->getIsNewFile();
if ($changeset_id == $this->rightSideChangesetID &&
$is_new == $this->rightSideAttachesToNewFile) {
return true;
}
if ($changeset_id == $this->leftSideChangesetID &&
$is_new == $this->leftSideAttachesToNewFile) {
return true;
}
return false;
}
/**
* Determine if a comment will appear on the right side of the display diff.
* Note that the comment must appear somewhere on the rendered changeset, as
* per isCommentVisibleOnRenderedDiff().
*
* @param PhabricatorInlineCommentInterface Comment to test for display
* location.
* @return bool True for right, false for left.
*/
private function isCommentOnRightSideWhenDisplayed(
PhabricatorInlineCommentInterface $comment) {
if (!$this->isCommentVisibleOnRenderedDiff($comment)) {
throw new Exception("Comment is not visible on changeset!");
}
$changeset_id = $comment->getChangesetID();
$is_new = $comment->getIsNewFile();
if ($changeset_id == $this->rightSideChangesetID &&
$is_new == $this->rightSideAttachesToNewFile) {
return true;
}
return false;
}
protected function renderShield($message, $more) {
if ($more) {
$end = $this->getLength();
$reference = $this->renderingReference;
$more =
' '.
javelin_render_tag(
'a',
array(
'mustcapture' => true,
'sigil' => 'show-more',
'class' => 'complete',
'href' => '#',
'meta' => array(
'ref' => $reference,
'range' => "0-{$end}",
),
),
'Show File Contents');
} else {
$more = null;
}
return javelin_render_tag(
'tr',
array(
'sigil' => 'context-target',
),
'<td class="differential-shield" colspan="6">'.
phutil_escape_html($message).
$more.
'</td>');
}
protected function renderTextChange(
$range_start,
$range_len,
$mask_force,
$feedback_mask,
array $old_comments,
array $new_comments) {
foreach (array_merge($old_comments, $new_comments) as $comments) {
assert_instances_of($comments, 'PhabricatorInlineCommentInterface');
}
$context_not_available = null;
if ($this->missingOld || $this->missingNew) {
$context_not_available = javelin_render_tag(
'tr',
array(
'sigil' => 'context-target',
),
'<td colspan="6" class="show-more">'.
'Context not available.'.
'</td>');
}
$html = array();
$rows = max(
count($this->old),
count($this->new));
if ($range_start === null) {
$range_start = 0;
}
if ($range_len === null) {
$range_len = $rows;
}
$range_len = min($range_len, $rows - $range_start);
// Gaps - compute gaps in the visible display diff, where we will render
// "Show more context" spacers. This builds an aggregate $mask of all the
// lines we must show (because they are near changed lines, near inline
// comments, or the request has explicitly asked for them, i.e. resulting
// from the user clicking "show more") and then finds all the gaps between
// visible lines. If a gap is smaller than the context size, we just
// display it. Otherwise, we record it into $gaps and will render a
// "show more context" element instead of diff text below.
$gaps = array();
$gap_start = 0;
$in_gap = false;
$mask = $this->visible + $mask_force + $feedback_mask;
$mask[$range_start + $range_len] = true;
for ($ii = $range_start; $ii <= $range_start + $range_len; $ii++) {
if (isset($mask[$ii])) {
if ($in_gap) {
$gap_length = $ii - $gap_start;
if ($gap_length <= self::LINES_CONTEXT) {
for ($jj = $gap_start; $jj <= $gap_start + $gap_length; $jj++) {
$mask[$jj] = true;
}
} else {
$gaps[] = array($gap_start, $gap_length);
}
$in_gap = false;
}
} else {
if (!$in_gap) {
$gap_start = $ii;
$in_gap = true;
}
}
}
$gaps = array_reverse($gaps);
$reference = $this->renderingReference;
$left_id = $this->leftSideChangesetID;
$right_id = $this->rightSideChangesetID;
// "N" stands for 'new' and means the comment should attach to the new file
// when stored, i.e. DifferentialInlineComment->setIsNewFile().
// "O" stands for 'old' and means the comment should attach to the old file.
$left_char = $this->leftSideAttachesToNewFile
? 'N'
: 'O';
$right_char = $this->rightSideAttachesToNewFile
? 'N'
: 'O';
$copy_lines = idx($this->changeset->getMetadata(), 'copy:lines', array());
if ($this->originalLeft && $this->originalRight) {
list($highlight_old, $highlight_new) = $this->diffOriginals();
$highlight_old = array_flip($highlight_old);
$highlight_new = array_flip($highlight_new);
}
// We need to go backwards to properly indent whitespace in this code:
//
// 0: class C {
// 1:
// 1: function f() {
// 2:
// 2: return;
//
$depths = array();
$last_depth = 0;
$range_end = $range_start + $range_len;
if (!isset($this->new[$range_end])) {
$range_end--;
}
for ($ii = $range_end; $ii >= $range_start; $ii--) {
// We need to expand tabs to process mixed indenting and to round
// correctly later.
$line = str_replace("\t", " ", $this->new[$ii]['text']);
$trimmed = ltrim($line);
if ($trimmed != '') {
// We round down to flatten "/**" and " *".
$last_depth = floor((strlen($line) - strlen($trimmed)) / 2);
}
$depths[$ii] = $last_depth;
}
for ($ii = $range_start; $ii < $range_start + $range_len; $ii++) {
if (empty($mask[$ii])) {
// If we aren't going to show this line, we've just entered a gap.
// Pop information about the next gap off the $gaps stack and render
// an appropriate "Show more context" element. This branch eventually
// increments $ii by the entire size of the gap and then continues
// the loop.
$gap = array_pop($gaps);
$top = $gap[0];
$len = $gap[1];
$end = $top + $len - 20;
$contents = array();
if ($len > 40) {
$is_first_block = false;
if ($ii == 0) {
$is_first_block = true;
}
$contents[] = javelin_render_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'sigil' => 'show-more',
'meta' => array(
'ref' => $reference,
'range' => "{$top}-{$len}/{$top}-20",
),
),
$is_first_block
? "Show First 20 Lines"
: "\xE2\x96\xB2 Show 20 Lines");
}
$contents[] = javelin_render_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'sigil' => 'show-more',
'meta' => array(
'type' => 'all',
'ref' => $reference,
'range' => "{$top}-{$len}/{$top}-{$len}",
),
),
'Show All '.$len.' Lines');
$is_last_block = false;
if ($ii + $len >= $rows) {
$is_last_block = true;
}
if ($len > 40) {
$contents[] = javelin_render_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'sigil' => 'show-more',
'meta' => array(
'ref' => $reference,
'range' => "{$top}-{$len}/{$end}-20",
),
),
$is_last_block
? "Show Last 20 Lines"
: "\xE2\x96\xBC Show 20 Lines");
}
$context = null;
$context_line = null;
if (!$is_last_block && $depths[$ii + $len]) {
for ($l = $ii + $len - 1; $l >= $ii; $l--) {
$line = $this->new[$l]['text'];
if ($depths[$l] < $depths[$ii + $len] && trim($line) != '') {
$context = $this->newRender[$l];
$context_line = $this->new[$l]['line'];
break;
}
}
}
$container = javelin_render_tag(
'tr',
array(
'sigil' => 'context-target',
),
'<td colspan="2" class="show-more">'.
implode(' &bull; ', $contents).
'</td>'.
'<th class="show-context-line">'.$context_line.'</td>'.
'<td colspan="3" class="show-context">'.$context.'</td>');
$html[] = $container;
$ii += ($len - 1);
continue;
}
if (isset($this->old[$ii])) {
$o_num = $this->old[$ii]['line'];
$o_text = isset($this->oldRender[$ii]) ? $this->oldRender[$ii] : null;
$o_attr = null;
if ($this->old[$ii]['type']) {
if ($this->old[$ii]['type'] == '\\') {
$o_text = $this->old[$ii]['text'];
$o_attr = ' class="comment"';
} else if ($this->originalLeft && !isset($highlight_old[$o_num])) {
$o_attr = ' class="old-rebase"';
} else if (empty($this->new[$ii])) {
$o_attr = ' class="old old-full"';
} else {
$o_attr = ' class="old"';
}
}
} else {
$o_num = null;
$o_text = null;
$o_attr = null;
}
$n_copy = '<td class="copy"></td>';
if (isset($this->new[$ii])) {
$n_num = $this->new[$ii]['line'];
$n_text = isset($this->newRender[$ii]) ? $this->newRender[$ii] : null;
$n_attr = null;
$cov_class = null;
if ($this->coverage !== null) {
if (empty($this->coverage[$n_num - 1])) {
$cov_class = 'N';
} else {
$cov_class = $this->coverage[$n_num - 1];
}
$cov_class = 'cov-'.$cov_class;
}
$n_cov = '<td class="cov '.$cov_class.'"></td>';
if ($this->new[$ii]['type']) {
if ($this->new[$ii]['type'] == '\\') {
$n_text = $this->new[$ii]['text'];
$n_class = 'comment';
} else if ($this->originalRight && !isset($highlight_new[$n_num])) {
$n_class = 'new-rebase';
} else if (empty($this->old[$ii])) {
$n_class = 'new new-full';
} else {
$n_class = 'new';
}
$n_attr = ' class="'.$n_class.'"';
if ($this->new[$ii]['type'] == '\\' || !isset($copy_lines[$n_num])) {
$n_copy = '<td class="copy '.$n_class.'"></td>';
} else {
list($orig_file, $orig_line, $orig_type) = $copy_lines[$n_num];
$title = ($orig_type == '-' ? 'Moved' : 'Copied').' from ';
if ($orig_file == '') {
$title .= "line {$orig_line}";
} else {
$title .=
basename($orig_file).
":{$orig_line} in dir ".
dirname('/'.$orig_file);
}
$class = ($orig_type == '-' ? 'new-move' : 'new-copy');
$n_copy = javelin_render_tag(
'td',
array(
'meta' => array(
'msg' => $title,
),
'class' => 'copy '.$class,
),
'');
}
}
} else {
$n_num = null;
$n_text = null;
$n_attr = null;
$n_cov = null;
}
if (($o_num && !empty($this->missingOld[$o_num])) ||
($n_num && !empty($this->missingNew[$n_num]))) {
$html[] = $context_not_available;
}
if ($o_num && $left_id) {
$o_id = ' id="C'.$left_id.$left_char.'L'.$o_num.'"';
} else {
$o_id = null;
}
if ($n_num && $right_id) {
$n_id = ' id="C'.$right_id.$right_char.'L'.$n_num.'"';
} else {
$n_id = null;
}
// NOTE: The Javascript is sensitive to whitespace changes in this
// block!
$html[] =
'<tr>'.
'<th'.$o_id.'>'.$o_num.'</th>'.
'<td'.$o_attr.'>'.$o_text.'</td>'.
'<th'.$n_id.'>'.$n_num.'</th>'.
$n_copy.
// NOTE: This is a unicode zero-width space, which we use as a hint
// when intercepting 'copy' events to make sure sensible text ends
// up on the clipboard. See the 'phabricator-oncopy' behavior.
'<td'.$n_attr.'>'."\xE2\x80\x8B".$n_text.'</td>'.
$n_cov.
'</tr>';
if ($context_not_available && ($ii == $rows - 1)) {
$html[] = $context_not_available;
}
if ($o_num && isset($old_comments[$o_num])) {
foreach ($old_comments[$o_num] as $comment) {
$xhp = $this->renderInlineComment($comment);
$new = '';
if ($n_num && isset($new_comments[$n_num])) {
foreach ($new_comments[$n_num] as $key => $new_comment) {
if ($comment->isCompatible($new_comment)) {
$new = $this->renderInlineComment($new_comment);
unset($new_comments[$n_num][$key]);
}
}
}
$html[] =
'<tr class="inline"><th /><td>'.
$xhp.
'</td><th /><td colspan="2">'.
$new.
'</td><td class="cov" /></tr>';
}
}
if ($n_num && isset($new_comments[$n_num])) {
foreach ($new_comments[$n_num] as $comment) {
$xhp = $this->renderInlineComment($comment);
$html[] =
'<tr class="inline"><th /><td /><th /><td colspan="2">'.
$xhp.
'</td><td class="cov" /></tr>';
}
}
}
return implode('', $html);
}
private function renderInlineComment(
PhabricatorInlineCommentInterface $comment) {
$user = $this->user;
$edit = $user &&
($comment->getAuthorPHID() == $user->getPHID()) &&
($comment->isDraft());
$allow_reply = (bool)$this->user;
$on_right = $this->isCommentOnRightSideWhenDisplayed($comment);
return id(new DifferentialInlineCommentView())
->setInlineComment($comment)
->setOnRight($on_right)
->setHandles($this->handles)
->setMarkupEngine($this->markupEngine)
->setEditable($edit)
->setAllowReply($allow_reply)
->render();
}
protected function renderPropertyChangeHeader($changeset) {
if (!$this->isTopLevel) {
// We render properties only at top level; otherwise we get multiple
// copies of them when a user clicks "Show More".
return null;
}
$old = $changeset->getOldProperties();
$new = $changeset->getNewProperties();
if ($old === $new) {
return null;
}
if ($changeset->getChangeType() == DifferentialChangeType::TYPE_ADD &&
$new == array('unix:filemode' => '100644')) {
return null;
}
if ($changeset->getChangeType() == DifferentialChangeType::TYPE_DELETE &&
$old == array('unix:filemode' => '100644')) {
return null;
}
$keys = array_keys($old + $new);
sort($keys);
$rows = array();
foreach ($keys as $key) {
$oval = idx($old, $key);
$nval = idx($new, $key);
if ($oval !== $nval) {
if ($oval === null) {
$oval = '<em>null</em>';
} else {
$oval = nl2br(phutil_escape_html($oval));
}
if ($nval === null) {
$nval = '<em>null</em>';
} else {
$nval = nl2br(phutil_escape_html($nval));
}
$rows[] =
'<tr>'.
'<th>'.phutil_escape_html($key).'</th>'.
'<td class="oval">'.$oval.'</td>'.
'<td class="nval">'.$nval.'</td>'.
'</tr>';
}
}
return
'<table class="differential-property-table">'.
'<tr class="property-table-header">'.
'<th>Property Changes</th>'.
'<td class="oval">Old Value</td>'.
'<td class="nval">New Value</td>'.
'</tr>'.
implode('', $rows).
'</table>';
}
protected function renderChangesetTable($changeset, $contents) {
$props = $this->renderPropertyChangeHeader($this->changeset);
$table = null;
if ($contents) {
$table = javelin_render_tag(
'table',
array(
'class' => 'differential-diff remarkup-code PhabricatorMonospaced',
'sigil' => 'differential-diff',
),
$contents);
}
if (!$table && !$props) {
$notice = $this->renderChangeTypeHeader($this->changeset, true);
} else {
$notice = $this->renderChangeTypeHeader($this->changeset, false);
}
$result = implode(
"\n",
array(
$notice,
$props,
$table,
));
// TODO: Let the user customize their tab width / display style.
$result = str_replace("\t", ' ', $result);
// TODO: We should possibly post-process "\r" as well.
return $result;
}
protected function renderChangeTypeHeader($changeset, $force) {
$change = $changeset->getChangeType();
$file = $changeset->getFileType();
$message = null;
if ($change == DifferentialChangeType::TYPE_CHANGE &&
$file == DifferentialChangeType::FILE_TEXT) {
if ($force) {
// We have to force something to render because there were no changes
// of other kinds.
$message = pht('This file was not modified.');
} else {
// Default case of changes to a text file, no metadata.
return null;
}
} else {
switch ($change) {
case DifferentialChangeType::TYPE_ADD:
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$message = pht('This file was <strong>added</strong>.');
break;
case DifferentialChangeType::FILE_IMAGE:
$message = pht('This image was <strong>added</strong>.');
break;
case DifferentialChangeType::FILE_DIRECTORY:
$message = pht('This directory was <strong>added</strong>.');
break;
case DifferentialChangeType::FILE_BINARY:
$message = pht('This binary file was <strong>added</strong>.');
break;
case DifferentialChangeType::FILE_SYMLINK:
$message = pht('This symlink was <strong>added</strong>.');
break;
case DifferentialChangeType::FILE_SUBMODULE:
$message = pht('This submodule was <strong>added</strong>.');
break;
}
break;
case DifferentialChangeType::TYPE_DELETE:
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$message = pht('This file was <strong>deleted</strong>.');
break;
case DifferentialChangeType::FILE_IMAGE:
$message = pht('This image was <strong>deleted</strong>.');
break;
case DifferentialChangeType::FILE_DIRECTORY:
$message = pht('This directory was <strong>deleted</strong>.');
break;
case DifferentialChangeType::FILE_BINARY:
$message = pht('This binary file was <strong>deleted</strong>.');
break;
case DifferentialChangeType::FILE_SYMLINK:
$message = pht('This symlink was <strong>deleted</strong>.');
break;
case DifferentialChangeType::FILE_SUBMODULE:
$message = pht('This submodule was <strong>deleted</strong>.');
break;
}
break;
case DifferentialChangeType::TYPE_MOVE_HERE:
$from =
"<strong>".
phutil_escape_html($changeset->getOldFile()).
"</strong>";
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$message = pht('This file was moved from %s.', $from);
break;
case DifferentialChangeType::FILE_IMAGE:
$message = pht('This image was moved from %s.', $from);
break;
case DifferentialChangeType::FILE_DIRECTORY:
$message = pht('This directory was moved from %s.', $from);
break;
case DifferentialChangeType::FILE_BINARY:
$message = pht('This binary file was moved from %s.', $from);
break;
case DifferentialChangeType::FILE_SYMLINK:
$message = pht('This symlink was moved from %s.', $from);
break;
case DifferentialChangeType::FILE_SUBMODULE:
$message = pht('This submodule was moved from %s.', $from);
break;
}
break;
case DifferentialChangeType::TYPE_COPY_HERE:
$from =
"<strong>".
phutil_escape_html($changeset->getOldFile()).
"</strong>";
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$message = pht('This file was copied from %s.', $from);
break;
case DifferentialChangeType::FILE_IMAGE:
$message = pht('This image was copied from %s.', $from);
break;
case DifferentialChangeType::FILE_DIRECTORY:
$message = pht('This directory was copied from %s.', $from);
break;
case DifferentialChangeType::FILE_BINARY:
$message = pht('This binary file was copied from %s.', $from);
break;
case DifferentialChangeType::FILE_SYMLINK:
$message = pht('This symlink was copied from %s.', $from);
break;
case DifferentialChangeType::FILE_SUBMODULE:
$message = pht('This submodule was copied from %s.', $from);
break;
}
break;
case DifferentialChangeType::TYPE_MOVE_AWAY:
$paths =
"<strong>".
phutil_escape_html(implode(', ', $changeset->getAwayPaths())).
"</strong>";
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$message = pht('This file was moved to %s.', $paths);
break;
case DifferentialChangeType::FILE_IMAGE:
$message = pht('This image was moved to %s.', $paths);
break;
case DifferentialChangeType::FILE_DIRECTORY:
$message = pht('This directory was moved to %s.', $paths);
break;
case DifferentialChangeType::FILE_BINARY:
$message = pht('This binary file was moved to %s.', $paths);
break;
case DifferentialChangeType::FILE_SYMLINK:
$message = pht('This symlink was moved to %s.', $paths);
break;
case DifferentialChangeType::FILE_SUBMODULE:
$message = pht('This submodule was moved to %s.', $paths);
break;
}
break;
case DifferentialChangeType::TYPE_COPY_AWAY:
$paths =
"<strong>".
phutil_escape_html(implode(', ', $changeset->getAwayPaths())).
"</strong>";
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$message = pht('This file was copied to %s.', $paths);
break;
case DifferentialChangeType::FILE_IMAGE:
$message = pht('This image was copied to %s.', $paths);
break;
case DifferentialChangeType::FILE_DIRECTORY:
$message = pht('This directory was copied to %s.', $paths);
break;
case DifferentialChangeType::FILE_BINARY:
$message = pht('This binary file was copied to %s.', $paths);
break;
case DifferentialChangeType::FILE_SYMLINK:
$message = pht('This symlink was copied to %s.', $paths);
break;
case DifferentialChangeType::FILE_SUBMODULE:
$message = pht('This submodule was copied to %s.', $paths);
break;
}
break;
case DifferentialChangeType::TYPE_MULTICOPY:
$paths =
"<strong>".
phutil_escape_html(implode(', ', $changeset->getAwayPaths())).
"</strong>";
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$message = pht(
'This file was deleted after being copied to %s.',
$paths);
break;
case DifferentialChangeType::FILE_IMAGE:
$message = pht(
'This image was deleted after being copied to %s.',
$paths);
break;
case DifferentialChangeType::FILE_DIRECTORY:
$message = pht(
'This directory was deleted after being copied to %s.',
$paths);
break;
case DifferentialChangeType::FILE_BINARY:
$message = pht(
'This binary file was deleted after being copied to %s.',
$paths);
break;
case DifferentialChangeType::FILE_SYMLINK:
$message = pht(
'This symlink was deleted after being copied to %s.',
$paths);
break;
case DifferentialChangeType::FILE_SUBMODULE:
$message = pht(
'This submodule was deleted after being copied to %s.',
$paths);
break;
}
break;
default:
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$message = pht('This is a file.');
break;
case DifferentialChangeType::FILE_IMAGE:
$message = pht('This is an image.');
break;
case DifferentialChangeType::FILE_DIRECTORY:
$message = pht('This is a directory.');
break;
case DifferentialChangeType::FILE_BINARY:
$message = pht('This is a binary file.');
break;
case DifferentialChangeType::FILE_SYMLINK:
$message = pht('This is a symlink.');
break;
case DifferentialChangeType::FILE_SUBMODULE:
$message = pht('This is a submodule.');
break;
}
break;
}
}
return
'<div class="differential-meta-notice">'.
$message.
'</div>';
}
public function renderForEmail() {
$ret = '';
$min = min(count($this->old), count($this->new));
for ($i = 0; $i < $min; $i++) {
$o = $this->old[$i];
$n = $this->new[$i];
if (!isset($this->visible[$i])) {
continue;
}
if ($o['line'] && $n['line']) {
// It is quite possible there are better ways to achieve this. For
// example, "white-space: pre;" can do a better job, WERE IT NOT for
// broken email clients like OWA which use newlines to do weird
// wrapping. So dont give them newlines.
if (isset($this->intra[$i])) {
$ret .= sprintf(
"<font color=\"red\">-&nbsp;%s</font><br/>",
str_replace(" ", "&nbsp;", phutil_escape_html($o['text']))
);
$ret .= sprintf(
"<font color=\"green\">+&nbsp;%s</font><br/>",
str_replace(" ", "&nbsp;", phutil_escape_html($n['text']))
);
} else {
$ret .= sprintf("&nbsp;&nbsp;%s<br/>",
str_replace(" ", "&nbsp;", phutil_escape_html($n['text']))
);
}
} else if ($o['line'] && !$n['line']) {
$ret .= sprintf(
"<font color=\"red\">-&nbsp;%s</font><br/>",
str_replace(" ", "&nbsp;", phutil_escape_html($o['text']))
);
} else {
$ret .= sprintf(
"<font color=\"green\">+&nbsp;%s</font><br/>",
str_replace(" ", "&nbsp;", phutil_escape_html($n['text']))
);
}
}
return $ret;
}
/**
* Parse the 'range' specification that this class and the client-side JS
* emit to indicate that a user clicked "Show more..." on a diff. Generally,
* use is something like this:
*
* $spec = $request->getStr('range');
* $parsed = DifferentialChangesetParser::parseRangeSpecification($spec);
* list($start, $end, $mask) = $parsed;
* $parser->render($start, $end, $mask);
*
* @param string Range specification, indicating the range of the diff that
* should be rendered.
* @return tuple List of <start, end, mask> suitable for passing to
* @{method:render}.
*/
public static function parseRangeSpecification($spec) {
$range_s = null;
$range_e = null;
$mask = array();
if ($spec) {
$match = null;
if (preg_match('@^(\d+)-(\d+)(?:/(\d+)-(\d+))?$@', $spec, $match)) {
$range_s = (int)$match[1];
$range_e = (int)$match[2];
if (count($match) > 3) {
$start = (int)$match[3];
$len = (int)$match[4];
for ($ii = $start; $ii < $start + $len; $ii++) {
$mask[$ii] = true;
}
}
}
}
return array($range_s, $range_e, $mask);
}
/**
* Render "modified coverage" information; test coverage on modified lines.
* This synthesizes diff information with unit test information into a useful
* indicator of how well tested a change is.
*/
public function renderModifiedCoverage() {
$na = '<em>-</em>';
if (!$this->coverage) {
return $na;
}
$covered = 0;
$not_covered = 0;
foreach ($this->new as $k => $new) {
if (!$new['line']) {
continue;
}
if (!$new['type']) {
continue;
}
if (empty($this->coverage[$new['line'] - 1])) {
continue;
}
switch ($this->coverage[$new['line'] - 1]) {
case 'C':
$covered++;
break;
case 'U':
$not_covered++;
break;
}
}
if (!$covered && !$not_covered) {
return $na;
}
return sprintf('%d%%', 100 * ($covered / ($covered + $not_covered)));
}
}
diff --git a/src/applications/differential/parser/__tests__/DifferentialChangesetParserTestCase.php b/src/applications/differential/parser/__tests__/DifferentialChangesetParserTestCase.php
index 5d66386d1d..ab062d361c 100644
--- a/src/applications/differential/parser/__tests__/DifferentialChangesetParserTestCase.php
+++ b/src/applications/differential/parser/__tests__/DifferentialChangesetParserTestCase.php
@@ -1,52 +1,36 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialChangesetParserTestCase extends ArcanistPhutilTestCase {
public function testDiffChangesets() {
$hunk = new DifferentialHunk();
$hunk->setChanges("+a\n b\n-c");
$hunk->setNewOffset(1);
$hunk->setNewLen(2);
$left = new DifferentialChangeset();
$left->attachHunks(array($hunk));
$tests = array(
"+a\n b\n-c" => array(array(), array()),
"+a\n x\n-c" => array(array(), array()),
"+aa\n b\n-c" => array(array(1), array(11)),
" b\n-c" => array(array(1), array()),
"+a\n b\n c" => array(array(), array(13)),
"+a\n x\n c" => array(array(), array(13)),
);
foreach ($tests as $changes => $expected) {
$hunk = new DifferentialHunk();
$hunk->setChanges($changes);
$hunk->setNewOffset(11);
$hunk->setNewLen(3);
$right = new DifferentialChangeset();
$right->attachHunks(array($hunk));
$parser = new DifferentialChangesetParser();
$parser->setOriginals($left, $right);
$this->assertEqual($expected, $parser->diffOriginals(), $changes);
}
}
}
diff --git a/src/applications/differential/query/DifferentialRevisionQuery.php b/src/applications/differential/query/DifferentialRevisionQuery.php
index 6045b72030..a9b687ce38 100644
--- a/src/applications/differential/query/DifferentialRevisionQuery.php
+++ b/src/applications/differential/query/DifferentialRevisionQuery.php
@@ -1,936 +1,920 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Flexible query API for Differential revisions. Example:
*
* // Load open revisions
* $revisions = id(new DifferentialRevisionQuery())
* ->withStatus(DifferentialRevisionQuery::STATUS_OPEN)
* ->execute();
*
* @task config Query Configuration
* @task exec Query Execution
* @task internal Internals
*/
final class DifferentialRevisionQuery {
// TODO: Replace DifferentialRevisionListData with this class.
private $pathIDs = array();
private $status = 'status-any';
const STATUS_ANY = 'status-any';
const STATUS_OPEN = 'status-open';
const STATUS_ACCEPTED = 'status-accepted';
const STATUS_NEEDS_REVIEW = 'status-needs-review';
const STATUS_CLOSED = 'status-closed'; // NOTE: Same as 'committed'.
const STATUS_COMMITTED = 'status-committed'; // TODO: Remove.
const STATUS_ABANDONED = 'status-abandoned';
private $authors = array();
private $draftAuthors = array();
private $ccs = array();
private $reviewers = array();
private $revIDs = array();
private $commitHashes = array();
private $phids = array();
private $subscribers = array();
private $responsibles = array();
private $branches = array();
private $arcanistProjectPHIDs = array();
private $draftRevisions = array();
private $order = 'order-modified';
const ORDER_MODIFIED = 'order-modified';
const ORDER_CREATED = 'order-created';
/**
* This is essentially a denormalized copy of the revision modified time that
* should perform better for path queries with a LIMIT. Critically, when you
* browse "/", every revision in that repository for all time will match so
* the query benefits from being able to stop before fully materializing the
* result set.
*/
const ORDER_PATH_MODIFIED = 'order-path-modified';
private $limit = 1000;
private $offset = 0;
private $needRelationships = false;
private $needActiveDiffs = false;
private $needDiffIDs = false;
private $needCommitPHIDs = false;
private $needHashes = false;
/* -( Query Configuration )------------------------------------------------ */
/**
* Filter results to revisions which affect a Diffusion path ID in a given
* repository. You can call this multiple times to select revisions for
* several paths.
*
* @param int Diffusion repository ID.
* @param int Diffusion path ID.
* @return this
* @task config
*/
public function withPath($repository_id, $path_id) {
$this->pathIDs[] = array(
'repositoryID' => $repository_id,
'pathID' => $path_id,
);
return $this;
}
/**
* Filter results to revisions authored by one of the given PHIDs. Calling
* this function will clear anything set by previous calls to
* @{method:withAuthors}.
*
* @param array List of PHIDs of authors
* @return this
* @task config
*/
public function withAuthors(array $author_phids) {
$this->authors = $author_phids;
return $this;
}
/**
* Filter results to revisions with comments authored bythe given PHIDs
*
* @param array List of PHIDs of authors
* @return this
* @task config
*/
public function withDraftRepliesByAuthors(array $author_phids) {
$this->draftAuthors = $author_phids;
return $this;
}
/**
* Filter results to revisions which CC one of the listed people. Calling this
* function will clear anything set by previous calls to @{method:withCCs}.
*
* @param array List of PHIDs of subscribers
* @return this
* @task config
*/
public function withCCs(array $cc_phids) {
$this->ccs = $cc_phids;
return $this;
}
/**
* Filter results to revisions that have one of the provided PHIDs as
* reviewers. Calling this function will clear anything set by previous calls
* to @{method:withReviewers}.
*
* @param array List of PHIDs of reviewers
* @return this
* @task config
*/
public function withReviewers(array $reviewer_phids) {
$this->reviewers = $reviewer_phids;
return $this;
}
/**
* Filter results to revisions that have one of the provided commit hashes.
* Calling this function will clear anything set by previous calls to
* @{method:withCommitHashes}.
*
* @param array List of pairs <Class
* ArcanistDifferentialRevisionHash::HASH_$type constant,
* hash>
* @return this
* @task config
*/
public function withCommitHashes(array $commit_hashes) {
$this->commitHashes = $commit_hashes;
return $this;
}
/**
* Filter results to revisions with a given status. Provide a class constant,
* such as ##DifferentialRevisionQuery::STATUS_OPEN##.
*
* @param const Class STATUS constant, like STATUS_OPEN.
* @return this
* @task config
*/
public function withStatus($status_constant) {
$this->status = $status_constant;
return $this;
}
/**
* Filter results to revisions on given branches.
*
* @param list List of branch names.
* @return this
* @task config
*/
public function withBranches(array $branches) {
$this->branches = $branches;
return $this;
}
/**
* Filter results to only return revisions whose ids are in the given set.
*
* @param array List of revision ids
* @return this
* @task config
*/
public function withIDs(array $ids) {
$this->revIDs = $ids;
return $this;
}
/**
* Filter results to only return revisions whose PHIDs are in the given set.
*
* @param array List of revision PHIDs
* @return this
* @task config
*/
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
/**
* Given a set of users, filter results to return only revisions they are
* responsible for (i.e., they are either authors or reviewers).
*
* @param array List of user PHIDs.
* @return this
* @task config
*/
public function withResponsibleUsers(array $responsible_phids) {
$this->responsibles = $responsible_phids;
return $this;
}
/**
* Filter results to only return revisions with a given set of subscribers
* (i.e., they are authors, reviewers or CC'd).
*
* @param array List of user PHIDs.
* @return this
* @task config
*/
public function withSubscribers(array $subscriber_phids) {
$this->subscribers = $subscriber_phids;
return $this;
}
/**
* Filter results to only return revisions with a given set of arcanist
* projects.
*
* @param array List of project PHIDs.
* @return this
* @task config
*/
public function withArcanistProjectPHIDs(array $arc_project_phids) {
$this->arcanistProjectPHIDs = $arc_project_phids;
return $this;
}
/**
* Set result ordering. Provide a class constant, such as
* ##DifferentialRevisionQuery::ORDER_CREATED##.
*
* @task config
*/
public function setOrder($order_constant) {
$this->order = $order_constant;
return $this;
}
/**
* Set result limit. If unspecified, defaults to 1000.
*
* @param int Result limit.
* @return this
* @task config
*/
public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
/**
* Set result offset. If unspecified, defaults to 0.
*
* @param int Result offset.
* @return this
* @task config
*/
public function setOffset($offset) {
$this->offset = $offset;
return $this;
}
/**
* Set whether or not the query will load and attach relationships.
*
* @param bool True to load and attach relationships.
* @return this
* @task config
*/
public function needRelationships($need_relationships) {
$this->needRelationships = $need_relationships;
return $this;
}
/**
* Set whether or not the query should load the active diff for each
* revision.
*
* @param bool True to load and attach diffs.
* @return this
* @task config
*/
public function needActiveDiffs($need_active_diffs) {
$this->needActiveDiffs = $need_active_diffs;
return $this;
}
/**
* Set whether or not the query should load the associated commit PHIDs for
* each revision.
*
* @param bool True to load and attach diffs.
* @return this
* @task config
*/
public function needCommitPHIDs($need_commit_phids) {
$this->needCommitPHIDs = $need_commit_phids;
return $this;
}
/**
* Set whether or not the query should load associated diff IDs for each
* revision.
*
* @param bool True to load and attach diff IDs.
* @return this
* @task config
*/
public function needDiffIDs($need_diff_ids) {
$this->needDiffIDs = $need_diff_ids;
return $this;
}
/**
* Set whether or not the query should load associated commit hashes for each
* revision.
*
* @param bool True to load and attach commit hashes.
* @return this
* @task config
*/
public function needHashes($need_hashes) {
$this->needHashes = $need_hashes;
return $this;
}
/* -( Query Execution )---------------------------------------------------- */
/**
* Execute the query as configured, returning matching
* @{class:DifferentialRevision} objects.
*
* @return list List of matching DifferentialRevision objects.
* @task exec
*/
public function execute() {
$table = new DifferentialRevision();
$conn_r = $table->establishConnection('r');
if ($this->shouldUseResponsibleFastPath()) {
$data = $this->loadDataUsingResponsibleFastPath();
} else {
$data = $this->loadData();
}
$revisions = $table->loadAllFromArray($data);
if ($revisions) {
if ($this->needRelationships) {
$this->loadRelationships($conn_r, $revisions);
}
if ($this->needCommitPHIDs) {
$this->loadCommitPHIDs($conn_r, $revisions);
}
$need_active = $this->needActiveDiffs;
$need_ids = $need_active ||
$this->needDiffIDs;
if ($need_ids) {
$this->loadDiffIDs($conn_r, $revisions);
}
if ($need_active) {
$this->loadActiveDiffs($conn_r, $revisions);
}
if ($this->needHashes) {
$this->loadHashes($conn_r, $revisions);
}
}
return $revisions;
}
/**
* Determine if we should execute an optimized, fast-path query to fetch
* open revisions for one responsible user. This is used by the Differential
* dashboard and much faster when executed as a UNION ALL than with JOIN
* and WHERE, which is why we special case it.
*/
private function shouldUseResponsibleFastPath() {
if ((count($this->responsibles) == 1) &&
($this->status == self::STATUS_OPEN) &&
($this->order == self::ORDER_MODIFIED) &&
!$this->offset &&
!$this->limit &&
!$this->subscribers &&
!$this->reviewers &&
!$this->ccs &&
!$this->authors &&
!$this->revIDs &&
!$this->commitHashes &&
!$this->phids &&
!$this->branches &&
!$this->arcanistProjectPHIDs) {
return true;
}
return false;
}
private function loadDataUsingResponsibleFastPath() {
$table = new DifferentialRevision();
$conn_r = $table->establishConnection('r');
$responsible_phid = reset($this->responsibles);
$open_statuses = array(
ArcanistDifferentialRevisionStatus::NEEDS_REVIEW,
ArcanistDifferentialRevisionStatus::NEEDS_REVISION,
ArcanistDifferentialRevisionStatus::ACCEPTED,
);
return queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE authorPHID = %s AND status IN (%Ld)
UNION ALL
SELECT r.* FROM %T r JOIN %T rel
ON rel.revisionID = r.id
AND rel.relation = %s
AND rel.objectPHID = %s
WHERE r.status IN (%Ld) ORDER BY dateModified DESC',
$table->getTableName(),
$responsible_phid,
$open_statuses,
$table->getTableName(),
DifferentialRevision::RELATIONSHIP_TABLE,
DifferentialRevision::RELATION_REVIEWER,
$responsible_phid,
$open_statuses);
}
private function loadData() {
$table = new DifferentialRevision();
$conn_r = $table->establishConnection('r');
if ($this->draftAuthors) {
$this->draftRevisions = array();
$draft_key = 'differential-comment-';
$drafts = id(new PhabricatorDraft())->loadAllWhere(
'authorPHID IN (%Ls) AND draftKey LIKE %> AND draft != %s',
$this->draftAuthors,
$draft_key,
'');
$len = strlen($draft_key);
foreach ($drafts as $draft) {
$this->draftRevisions[] = substr($draft->getDraftKey(), $len);
}
$inlines = id(new DifferentialInlineComment())->loadAllWhere(
'commentID IS NULL AND authorPHID IN (%Ls)',
$this->draftAuthors);
foreach ($inlines as $inline) {
$this->draftRevisions[] = $inline->getRevisionID();
}
if (!$this->draftRevisions) {
return array();
}
}
$select = qsprintf(
$conn_r,
'SELECT r.* FROM %T r',
$table->getTableName());
$joins = $this->buildJoinsClause($conn_r);
$where = $this->buildWhereClause($conn_r);
$group_by = $this->buildGroupByClause($conn_r);
$order_by = $this->buildOrderByClause($conn_r);
$limit = '';
if ($this->offset || $this->limit) {
$limit = qsprintf(
$conn_r,
'LIMIT %d, %d',
(int)$this->offset,
$this->limit);
}
return queryfx_all(
$conn_r,
'%Q %Q %Q %Q %Q %Q',
$select,
$joins,
$where,
$group_by,
$order_by,
$limit);
}
/* -( Internals )---------------------------------------------------------- */
/**
* @task internal
*/
private function buildJoinsClause($conn_r) {
$joins = array();
if ($this->pathIDs) {
$path_table = new DifferentialAffectedPath();
$joins[] = qsprintf(
$conn_r,
'JOIN %T p ON p.revisionID = r.id',
$path_table->getTableName());
}
if ($this->commitHashes) {
$joins[] = qsprintf(
$conn_r,
'JOIN %T hash_rel ON hash_rel.revisionID = r.id',
ArcanistDifferentialRevisionHash::TABLE_NAME);
}
if ($this->ccs) {
$joins[] = qsprintf(
$conn_r,
'JOIN %T cc_rel ON cc_rel.revisionID = r.id '.
'AND cc_rel.relation = %s '.
'AND cc_rel.objectPHID in (%Ls)',
DifferentialRevision::RELATIONSHIP_TABLE,
DifferentialRevision::RELATION_SUBSCRIBED,
$this->ccs);
}
if ($this->reviewers) {
$joins[] = qsprintf(
$conn_r,
'JOIN %T reviewer_rel ON reviewer_rel.revisionID = r.id '.
'AND reviewer_rel.relation = %s '.
'AND reviewer_rel.objectPHID in (%Ls)',
DifferentialRevision::RELATIONSHIP_TABLE,
DifferentialRevision::RELATION_REVIEWER,
$this->reviewers);
}
if ($this->subscribers) {
$joins[] = qsprintf(
$conn_r,
'JOIN %T sub_rel ON sub_rel.revisionID = r.id '.
'AND sub_rel.relation IN (%Ls) '.
'AND sub_rel.objectPHID in (%Ls)',
DifferentialRevision::RELATIONSHIP_TABLE,
array(
DifferentialRevision::RELATION_SUBSCRIBED,
DifferentialRevision::RELATION_REVIEWER,
),
$this->subscribers);
}
if ($this->responsibles) {
$joins[] = qsprintf(
$conn_r,
'LEFT JOIN %T responsibles_rel ON responsibles_rel.revisionID = r.id '.
'AND responsibles_rel.relation = %s '.
'AND responsibles_rel.objectPHID in (%Ls)',
DifferentialRevision::RELATIONSHIP_TABLE,
DifferentialRevision::RELATION_REVIEWER,
$this->responsibles);
}
$joins = implode(' ', $joins);
return $joins;
}
/**
* @task internal
*/
private function buildWhereClause($conn_r) {
$where = array();
if ($this->pathIDs) {
$path_clauses = array();
$repo_info = igroup($this->pathIDs, 'repositoryID');
foreach ($repo_info as $repository_id => $paths) {
$path_clauses[] = qsprintf(
$conn_r,
'(p.repositoryID = %d AND p.pathID IN (%Ld))',
$repository_id,
ipull($paths, 'pathID'));
}
$path_clauses = '('.implode(' OR ', $path_clauses).')';
$where[] = $path_clauses;
}
if ($this->authors) {
$where[] = qsprintf(
$conn_r,
'r.authorPHID IN (%Ls)',
$this->authors);
}
if ($this->draftRevisions) {
$where[] = qsprintf(
$conn_r,
'r.id IN (%Ld)',
$this->draftRevisions);
}
if ($this->revIDs) {
$where[] = qsprintf(
$conn_r,
'r.id IN (%Ld)',
$this->revIDs);
}
if ($this->commitHashes) {
$hash_clauses = array();
foreach ($this->commitHashes as $info) {
list($type, $hash) = $info;
$hash_clauses[] = qsprintf(
$conn_r,
'(hash_rel.type = %s AND hash_rel.hash = %s)',
$type,
$hash);
}
$hash_clauses = '('.implode(' OR ', $hash_clauses).')';
$where[] = $hash_clauses;
}
if ($this->phids) {
$where[] = qsprintf(
$conn_r,
'r.phid IN (%Ls)',
$this->phids);
}
if ($this->responsibles) {
$where[] = qsprintf(
$conn_r,
'(responsibles_rel.objectPHID IS NOT NULL OR r.authorPHID IN (%Ls))',
$this->responsibles);
}
if ($this->branches) {
$where[] = qsprintf(
$conn_r,
'r.branchName in (%Ls)',
$this->branches);
}
if ($this->arcanistProjectPHIDs) {
$where[] = qsprintf(
$conn_r,
'r.arcanistProjectPHID in (%Ls)',
$this->arcanistProjectPHIDs);
}
switch ($this->status) {
case self::STATUS_ANY:
break;
case self::STATUS_OPEN:
$where[] = qsprintf(
$conn_r,
'r.status IN (%Ld)',
array(
ArcanistDifferentialRevisionStatus::NEEDS_REVIEW,
ArcanistDifferentialRevisionStatus::NEEDS_REVISION,
ArcanistDifferentialRevisionStatus::ACCEPTED,
));
break;
case self::STATUS_NEEDS_REVIEW:
$where[] = qsprintf(
$conn_r,
'r.status IN (%Ld)',
array(
ArcanistDifferentialRevisionStatus::NEEDS_REVIEW,
));
break;
case self::STATUS_ACCEPTED:
$where[] = qsprintf(
$conn_r,
'r.status IN (%Ld)',
array(
ArcanistDifferentialRevisionStatus::ACCEPTED,
));
break;
case self::STATUS_COMMITTED:
phlog(
"WARNING: DifferentialRevisionQuery using deprecated ".
"STATUS_COMMITTED constant. This will be removed soon. ".
"Use STATUS_CLOSED.");
// fallthrough
case self::STATUS_CLOSED:
$where[] = qsprintf(
$conn_r,
'r.status IN (%Ld)',
array(
ArcanistDifferentialRevisionStatus::CLOSED,
));
break;
case self::STATUS_ABANDONED:
$where[] = qsprintf(
$conn_r,
'r.status IN (%Ld)',
array(
ArcanistDifferentialRevisionStatus::ABANDONED,
));
break;
default:
throw new Exception(
"Unknown revision status filter constant '{$this->status}'!");
}
if ($where) {
$where = 'WHERE '.implode(' AND ', $where);
} else {
$where = '';
}
return $where;
}
/**
* @task internal
*/
private function buildGroupByClause($conn_r) {
$join_triggers = array_merge(
$this->pathIDs,
$this->ccs,
$this->reviewers,
$this->subscribers,
$this->responsibles);
$needs_distinct = (count($join_triggers) > 1);
if ($needs_distinct) {
return 'GROUP BY r.id';
} else {
return '';
}
}
/**
* @task internal
*/
private function buildOrderByClause($conn_r) {
switch ($this->order) {
case self::ORDER_MODIFIED:
return 'ORDER BY r.dateModified DESC';
case self::ORDER_CREATED:
return 'ORDER BY r.dateCreated DESC';
case self::ORDER_PATH_MODIFIED:
if (!$this->pathIDs) {
throw new Exception(
"To use ORDER_PATH_MODIFIED, you must specify withPath().");
}
return 'ORDER BY p.epoch DESC';
default:
throw new Exception("Unknown query order constant '{$this->order}'.");
}
}
private function loadRelationships($conn_r, array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$relationships = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE revisionID in (%Ld) ORDER BY sequence',
DifferentialRevision::RELATIONSHIP_TABLE,
mpull($revisions, 'getID'));
$relationships = igroup($relationships, 'revisionID');
foreach ($revisions as $revision) {
$revision->attachRelationships(
idx(
$relationships,
$revision->getID(),
array()));
}
}
private function loadCommitPHIDs($conn_r, array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$commit_phids = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE revisionID IN (%Ld)',
DifferentialRevision::TABLE_COMMIT,
mpull($revisions, 'getID'));
$commit_phids = igroup($commit_phids, 'revisionID');
foreach ($revisions as $revision) {
$phids = idx($commit_phids, $revision->getID(), array());
$phids = ipull($phids, 'commitPHID');
$revision->attachCommitPHIDs($phids);
}
}
private function loadDiffIDs($conn_r, array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$diff_table = new DifferentialDiff();
$diff_ids = queryfx_all(
$conn_r,
'SELECT revisionID, id FROM %T WHERE revisionID IN (%Ld)
ORDER BY id DESC',
$diff_table->getTableName(),
mpull($revisions, 'getID'));
$diff_ids = igroup($diff_ids, 'revisionID');
foreach ($revisions as $revision) {
$ids = idx($diff_ids, $revision->getID(), array());
$ids = ipull($ids, 'id');
$revision->attachDiffIDs($ids);
}
}
private function loadActiveDiffs($conn_r, array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$diff_table = new DifferentialDiff();
$load_ids = array();
foreach ($revisions as $revision) {
$diffs = $revision->getDiffIDs();
if ($diffs) {
$load_ids[] = max($diffs);
}
}
$active_diffs = array();
if ($load_ids) {
$active_diffs = $diff_table->loadAllWhere(
'id IN (%Ld)',
$load_ids);
}
$active_diffs = mpull($active_diffs, null, 'getRevisionID');
foreach ($revisions as $revision) {
$revision->attachActiveDiff(idx($active_diffs, $revision->getID()));
}
}
private function loadHashes(
AphrontDatabaseConnection $conn_r,
array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE revisionID IN (%Ld)',
'differential_revisionhash',
mpull($revisions, 'getID'));
$data = igroup($data, 'revisionID');
foreach ($revisions as $revision) {
$hashes = idx($data, $revision->getID(), array());
$list = array();
foreach ($hashes as $hash) {
$list[] = array($hash['type'], $hash['hash']);
}
$revision->attachHashes($list);
}
}
public static function splitResponsible(array $revisions, $user_phid) {
$active = array();
$waiting = array();
$status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW;
// Bucket revisions into $active (revisions you need to do something
// about) and $waiting (revisions you're waiting on someone else to do
// something about).
foreach ($revisions as $revision) {
$needs_review = ($revision->getStatus() == $status_review);
$filter_is_author = ($revision->getAuthorPHID() == $user_phid);
// If exactly one of "needs review" and "the user is the author" is
// true, the user needs to act on it. Otherwise, they're waiting on
// it.
if ($needs_review ^ $filter_is_author) {
$active[] = $revision;
} else {
$waiting[] = $revision;
}
}
return array($active, $waiting);
}
}
diff --git a/src/applications/differential/stats/DifferentialReviewerStats.php b/src/applications/differential/stats/DifferentialReviewerStats.php
index 564cbbc907..90f7937c01 100644
--- a/src/applications/differential/stats/DifferentialReviewerStats.php
+++ b/src/applications/differential/stats/DifferentialReviewerStats.php
@@ -1,242 +1,226 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialReviewerStats {
private $since = 0;
private $until;
public function setSince($value) {
$this->since = $value;
return $this;
}
public function setUntil($value) {
$this->until = $value;
return $this;
}
/**
* @return array($reviewed, $not_reviewed)
*/
public function computeTimes(
DifferentialRevision $revision,
array $comments) {
assert_instances_of($comments, 'DifferentialComment');
$add_rev = DifferentialComment::METADATA_ADDED_REVIEWERS;
$rem_rev = DifferentialComment::METADATA_REMOVED_REVIEWERS;
$date = $revision->getDateCreated();
// Find out original reviewers.
$reviewers = array_fill_keys($revision->getReviewers(), $date);
foreach (array_reverse($comments) as $comment) {
$metadata = $comment->getMetadata();
foreach (idx($metadata, $add_rev, array()) as $phid) {
unset($reviewers[$phid]);
}
foreach (idx($metadata, $rem_rev, array()) as $phid) {
$reviewers[$phid] = $date;
}
}
$reviewed = array();
$not_reviewed = array();
$status = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW;
foreach ($comments as $comment) {
$date = $comment->getDateCreated();
$old_status = $status;
switch ($comment->getAction()) {
case DifferentialAction::ACTION_UPDATE:
if ($status != ArcanistDifferentialRevisionStatus::CLOSED &&
$status != ArcanistDifferentialRevisionStatus::ACCEPTED) {
$status = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW;
}
break;
case DifferentialAction::ACTION_REQUEST:
case DifferentialAction::ACTION_RECLAIM:
$status = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW;
break;
case DifferentialAction::ACTION_REJECT:
case DifferentialAction::ACTION_RETHINK:
$status = ArcanistDifferentialRevisionStatus::NEEDS_REVISION;
break;
case DifferentialAction::ACTION_ACCEPT:
$status = ArcanistDifferentialRevisionStatus::ACCEPTED;
break;
case DifferentialAction::ACTION_CLOSE:
$status = ArcanistDifferentialRevisionStatus::CLOSED;
break;
case DifferentialAction::ACTION_ABANDON:
$status = ArcanistDifferentialRevisionStatus::ABANDONED;
break;
}
// Update current reviewers.
$metadata = $comment->getMetadata();
foreach (idx($metadata, $add_rev, array()) as $phid) {
// If someone reviewed a revision without being its reviewer then give
// him zero response time.
$reviewers[$phid] = $date;
}
foreach (idx($metadata, $rem_rev, array()) as $phid) {
$start = idx($reviewers, $phid);
if ($start !== null) {
if ($date >= $this->since) {
$reviewed[$phid][] = $date - $start;
}
unset($reviewers[$phid]);
}
}
// TODO: Respect workdays and status away.
if ($old_status != $status) {
if ($status == ArcanistDifferentialRevisionStatus::NEEDS_REVIEW) {
$reviewers = array_fill_keys(array_keys($reviewers), $date);
} else if ($date >= $this->since) {
if ($old_status == ArcanistDifferentialRevisionStatus::NEEDS_REVIEW) {
foreach ($reviewers as $phid => $start) {
if ($phid == $comment->getAuthorPHID()) {
$reviewed[$phid][] = $date - $start;
} else {
$not_reviewed[$phid][] = $date - $start;
}
}
}
}
}
}
if ($status == ArcanistDifferentialRevisionStatus::NEEDS_REVIEW) {
$date = ($this->until !== null ? $this->until : time());
if ($date >= $this->since) {
foreach ($reviewers as $phid => $start) {
$not_reviewed[$phid][] = $date - $start;
}
}
}
return array($reviewed, $not_reviewed);
}
public function loadAvgs() {
$limit = 1000;
$conn_r = id(new DifferentialRevision())->establishConnection('r');
$sums = array();
$counts = array();
$all_not_reviewed = array();
$last_id = 0;
do {
$where = '';
if ($this->until !== null) {
$where .= qsprintf(
$conn_r,
' AND dateCreated < %d',
$this->until);
}
if ($this->since) {
$where .= qsprintf(
$conn_r,
' AND (dateModified > %d OR status = %s)',
$this->since,
ArcanistDifferentialRevisionStatus::NEEDS_REVIEW);
}
$revisions = id(new DifferentialRevision())->loadAllWhere(
'id > %d%Q ORDER BY id LIMIT %d',
$last_id,
$where,
$limit);
if (!$revisions) {
break;
}
$last_id = last_key($revisions);
$relations = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE revisionID IN (%Ld) AND relation = %s',
DifferentialRevision::RELATIONSHIP_TABLE,
array_keys($revisions),
DifferentialRevision::RELATION_REVIEWER);
$relations = igroup($relations, 'revisionID');
$where = '';
if ($this->until !== null) {
$where = qsprintf(
$conn_r,
' AND dateCreated < %d',
$this->until);
}
$all_comments = id(new DifferentialComment())->loadAllWhere(
'revisionID IN (%Ld)%Q ORDER BY revisionID, id',
array_keys($revisions),
$where);
$all_comments = mgroup($all_comments, 'getRevisionID');
foreach ($revisions as $id => $revision) {
$revision->attachRelationships(idx($relations, $id, array()));
$comments = idx($all_comments, $id, array());
list($reviewed, $not_reviewed) =
$this->computeTimes($revision, $comments);
foreach ($reviewed as $phid => $times) {
$sums[$phid] = idx($sums, $phid, 0) + array_sum($times);
$counts[$phid] = idx($counts, $phid, 0) + count($times);
}
foreach ($not_reviewed as $phid => $times) {
$all_not_reviewed[$phid][] = $times;
}
}
} while (count($revisions) >= $limit);
foreach ($all_not_reviewed as $phid => $not_reviewed) {
if (!array_key_exists($phid, $counts)) {
// If the person didn't make any reviews than take maximum time because
// he is at least that slow.
$sums[$phid] = max(array_map('max', $not_reviewed));
$counts[$phid] = 1;
continue;
}
$avg = $sums[$phid] / $counts[$phid];
foreach ($not_reviewed as $times) {
foreach ($times as $time) {
// Don't shorten the average time just because the reviewer was lucky
// to be in a group with someone faster.
if ($time > $avg) {
$sums[$phid] += $time;
$counts[$phid]++;
}
}
}
}
$avgs = array();
foreach ($sums as $phid => $sum) {
$avgs[$phid] = $sum / $counts[$phid];
}
return $avgs;
}
}
diff --git a/src/applications/differential/stats/__tests__/DifferentialReviewerStatsTestCase.php b/src/applications/differential/stats/__tests__/DifferentialReviewerStatsTestCase.php
index e8792aa2cb..9d75253cd0 100644
--- a/src/applications/differential/stats/__tests__/DifferentialReviewerStatsTestCase.php
+++ b/src/applications/differential/stats/__tests__/DifferentialReviewerStatsTestCase.php
@@ -1,116 +1,100 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialReviewerStatsTestCase extends PhabricatorTestCase {
public function testReviewerStats() {
$revision = new DifferentialRevision();
$revision->setDateCreated(1);
$revision->attachRelationships(array(
$this->newReviewer('R1'),
$this->newReviewer('R3'),
));
$comments = array(
$this->newComment(2, 'A', DifferentialAction::ACTION_COMMENT),
$this->newComment(4, 'A', DifferentialAction::ACTION_ADDREVIEWERS,
array(DifferentialComment::METADATA_ADDED_REVIEWERS => array('R3'))),
$this->newComment(8, 'R1', DifferentialAction::ACTION_REJECT),
$this->newComment(16, 'A', DifferentialAction::ACTION_COMMENT),
$this->newComment(32, 'A', DifferentialAction::ACTION_UPDATE),
$this->newComment(64, 'A', DifferentialAction::ACTION_UPDATE),
$this->newComment(128, 'A', DifferentialAction::ACTION_COMMENT),
$this->newComment(256, 'R2', DifferentialAction::ACTION_RESIGN,
array(DifferentialComment::METADATA_REMOVED_REVIEWERS => array('R2'))),
$this->newComment(512, 'R3', DifferentialAction::ACTION_ACCEPT),
$this->newComment(1024, 'A', DifferentialAction::ACTION_UPDATE),
// TODO: claim, abandon, reclaim
);
$stats = new DifferentialReviewerStats();
list($reviewed, $not_reviewed) = $stats->computeTimes($revision, $comments);
ksort($reviewed);
$this->assertEqual(
array(
'R1' => array(8 - 1),
'R2' => array(256 - 32),
'R3' => array(512 - 32),
),
$reviewed);
ksort($not_reviewed);
$this->assertEqual(
array(
'R1' => array(512 - 32),
'R2' => array(8 - 1),
'R3' => array(8 - 4),
),
$not_reviewed);
}
public function testReviewerStatsSince() {
$revision = new DifferentialRevision();
$revision->setDateCreated(1);
$revision->attachRelationships(array($this->newReviewer('R')));
$comments = array(
$this->newComment(2, 'R', DifferentialAction::ACTION_REJECT),
$this->newComment(4, 'A', DifferentialAction::ACTION_REQUEST),
$this->newComment(8, 'R', DifferentialAction::ACTION_ACCEPT),
);
$stats = new DifferentialReviewerStats();
$stats->setSince(4);
list($reviewed, $not_reviewed) = $stats->computeTimes($revision, $comments);
$this->assertEqual(array('R' => array(8 - 4)), $reviewed);
$this->assertEqual(array(), $not_reviewed);
}
public function testReviewerStatsRequiresReview() {
$revision = new DifferentialRevision();
$revision->setDateCreated(1);
$revision->attachRelationships(array($this->newReviewer('R')));
$comments = array();
$stats = new DifferentialReviewerStats();
$stats->setUntil(2);
list($reviewed, $not_reviewed) = $stats->computeTimes($revision, $comments);
$this->assertEqual(array(), $reviewed);
$this->assertEqual(array('R' => array(2 - 1)), $not_reviewed);
}
private function newReviewer($phid) {
return array(
'relation' => DifferentialRevision::RELATION_REVIEWER,
'objectPHID' => $phid,
);
}
private function newComment($date, $author, $action, $metadata = array()) {
return id(new DifferentialComment())
->setDateCreated($date)
->setAuthorPHID($author)
->setAction($action)
->setMetadata($metadata);
}
}
diff --git a/src/applications/differential/storage/DifferentialAffectedPath.php b/src/applications/differential/storage/DifferentialAffectedPath.php
index 6b4ddfcb82..83b35f5639 100644
--- a/src/applications/differential/storage/DifferentialAffectedPath.php
+++ b/src/applications/differential/storage/DifferentialAffectedPath.php
@@ -1,37 +1,21 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Denormalized index table which stores relationships between revisions in
* Differential and paths in Diffusion.
*
* @group differential
*/
final class DifferentialAffectedPath extends DifferentialDAO {
protected $repositoryID;
protected $pathID;
protected $epoch;
protected $revisionID;
public function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
) + parent::getConfiguration();
}
}
diff --git a/src/applications/differential/storage/DifferentialAuxiliaryField.php b/src/applications/differential/storage/DifferentialAuxiliaryField.php
index 49246fdcf6..0584956562 100644
--- a/src/applications/differential/storage/DifferentialAuxiliaryField.php
+++ b/src/applications/differential/storage/DifferentialAuxiliaryField.php
@@ -1,61 +1,45 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialAuxiliaryField extends DifferentialDAO {
protected $revisionPHID;
protected $name;
protected $value;
public function setName($name) {
if (strlen($name) > 32) {
throw new Exception(
"Tried to set name '{$name}' for a Differential auxiliary field; ".
"auxiliary field names must be no longer than 32 characters.");
}
$this->name = $name;
return $this;
}
public static function loadFromStorage(
DifferentialRevision $revision,
array $aux_fields) {
assert_instances_of($aux_fields, 'DifferentialFieldSpecification');
$storage_keys = array_filter(mpull($aux_fields, 'getStorageKey'));
$field_data = array();
if ($storage_keys) {
$field_data = id(new DifferentialAuxiliaryField())->loadAllWhere(
'revisionPHID = %s AND name IN (%Ls)',
$revision->getPHID(),
$storage_keys);
$field_data = mpull($field_data, 'getValue', 'getName');
}
foreach ($aux_fields as $aux_field) {
$aux_field->setRevision($revision);
$key = $aux_field->getStorageKey();
if ($key) {
$aux_field->setValueFromStorage(idx($field_data, $key));
}
}
return $aux_fields;
}
}
diff --git a/src/applications/differential/storage/DifferentialChangeset.php b/src/applications/differential/storage/DifferentialChangeset.php
index ef36c09891..abf3ba7f67 100644
--- a/src/applications/differential/storage/DifferentialChangeset.php
+++ b/src/applications/differential/storage/DifferentialChangeset.php
@@ -1,304 +1,288 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialChangeset extends DifferentialDAO {
protected $diffID;
protected $oldFile;
protected $filename;
protected $awayPaths;
protected $changeType;
protected $fileType;
protected $metadata;
protected $oldProperties;
protected $newProperties;
protected $addLines;
protected $delLines;
private $unsavedHunks = array();
private $hunks;
const TABLE_CACHE = 'differential_changeset_parse_cache';
protected function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'metadata' => self::SERIALIZATION_JSON,
'oldProperties' => self::SERIALIZATION_JSON,
'newProperties' => self::SERIALIZATION_JSON,
'awayPaths' => self::SERIALIZATION_JSON,
)) + parent::getConfiguration();
}
public function getAffectedLineCount() {
return $this->getAddLines() + $this->getDelLines();
}
public function attachHunks(array $hunks) {
assert_instances_of($hunks, 'DifferentialHunk');
$this->hunks = $hunks;
return $this;
}
public function getHunks() {
if ($this->hunks === null) {
throw new Exception("Must load and attach hunks first!");
}
return $this->hunks;
}
public function getDisplayFilename() {
$name = $this->getFilename();
if ($this->getFileType() == DifferentialChangeType::FILE_DIRECTORY) {
$name .= '/';
}
return $name;
}
public function addUnsavedHunk(DifferentialHunk $hunk) {
if ($this->hunks === null) {
$this->hunks = array();
}
$this->hunks[] = $hunk;
$this->unsavedHunks[] = $hunk;
return $this;
}
public function loadHunks() {
if (!$this->getID()) {
return array();
}
return id(new DifferentialHunk())->loadAllWhere(
'changesetID = %d',
$this->getID());
}
public function save() {
$this->openTransaction();
$ret = parent::save();
foreach ($this->unsavedHunks as $hunk) {
$hunk->setChangesetID($this->getID());
$hunk->save();
}
$this->saveTransaction();
return $ret;
}
public function delete() {
$this->openTransaction();
foreach ($this->loadHunks() as $hunk) {
$hunk->delete();
}
$this->_hunks = array();
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE id = %d',
self::TABLE_CACHE,
$this->getID());
$ret = parent::delete();
$this->saveTransaction();
return $ret;
}
public function getSortKey() {
$sort_key = $this->getFilename();
// Sort files with ".h" in them first, so headers (.h, .hpp) come before
// implementations (.c, .cpp, .cs).
$sort_key = str_replace('.h', '.!h', $sort_key);
return $sort_key;
}
public function makeNewFile() {
$file = mpull($this->getHunks(), 'makeNewFile');
return implode('', $file);
}
public function makeOldFile() {
$file = mpull($this->getHunks(), 'makeOldFile');
return implode('', $file);
}
public function computeOffsets() {
$offsets = array();
$n = 1;
foreach ($this->getHunks() as $hunk) {
for ($i = 0; $i < $hunk->getNewLen(); $i++) {
$offsets[$n] = $hunk->getNewOffset() + $i;
$n++;
}
}
return $offsets;
}
public function makeChangesWithContext($num_lines = 3) {
$with_context = array();
foreach ($this->getHunks() as $hunk) {
$context = array();
$changes = explode("\n", $hunk->getChanges());
foreach ($changes as $l => $line) {
if ($line[0] == '+' || $line[0] == '-') {
$context += array_fill($l - $num_lines, 2 * $num_lines + 1, true);
}
}
$with_context[] = array_intersect_key($changes, $context);
}
return array_mergev($with_context);
}
public function getAnchorName() {
return substr(md5($this->getFilename()), 0, 8);
}
public function getAbsoluteRepositoryPath(
PhabricatorRepository $repository = null,
DifferentialDiff $diff = null) {
$base = '/';
if ($diff && $diff->getSourceControlPath()) {
$base = id(new PhutilURI($diff->getSourceControlPath()))->getPath();
}
$path = $this->getFilename();
$path = rtrim($base, '/').'/'.ltrim($path, '/');
$svn = PhabricatorRepositoryType::REPOSITORY_TYPE_SVN;
if ($repository && $repository->getVersionControlSystem() == $svn) {
$prefix = $repository->getDetail('remote-uri');
$prefix = id(new PhutilURI($prefix))->getPath();
if (!strncmp($path, $prefix, strlen($prefix))) {
$path = substr($path, strlen($prefix));
}
$path = '/'.ltrim($path, '/');
}
return $path;
}
/**
* Retreive the configured wordwrap width for this changeset.
*/
public function getWordWrapWidth() {
$config = PhabricatorEnv::getEnvConfig('differential.wordwrap');
foreach ($config as $regexp => $width) {
if (preg_match($regexp, $this->getFilename())) {
return $width;
}
}
return 80;
}
public function getWhitespaceMatters() {
$config = PhabricatorEnv::getEnvConfig('differential.whitespace-matters');
foreach ($config as $regexp) {
if (preg_match($regexp, $this->getFilename())) {
return true;
}
}
return false;
}
public function makeContextDiff($inline, $add_context) {
$context = array();
$debug = false;
if ($debug) {
$context[] = 'Inline: '.$inline->getIsNewFile().' '.
$inline->getLineNumber().' '.$inline->getLineLength();
foreach ($this->getHunks() as $hunk) {
$context[] = 'hunk: '.$hunk->getOldOffset().'-'.
$hunk->getOldLen().'; '.$hunk->getNewOffset().'-'.$hunk->getNewLen();
$context[] = $hunk->getChanges();
}
}
if ($inline->getIsNewFile()) {
$prefix = '+';
} else {
$prefix = '-';
}
foreach ($this->getHunks() as $hunk) {
if ($inline->getIsNewFile()) {
$offset = $hunk->getNewOffset();
$length = $hunk->getNewLen();
} else {
$offset = $hunk->getOldOffset();
$length = $hunk->getOldLen();
}
$start = $inline->getLineNumber() - $offset;
$end = $start + $inline->getLineLength();
// We need to go in if $start == $length, because the last line
// might be a "\No newline at end of file" marker, which we want
// to show if the additional context is > 0.
if ($start <= $length && $end >= 0) {
$start = $start - $add_context;
$end = $end + $add_context;
$hunk_content = array();
$hunk_pos = array( "-" => 0, "+" => 0 );
$hunk_offset = array( "-" => NULL, "+" => NULL );
$hunk_last = array( "-" => NULL, "+" => NULL );
foreach (explode("\n", $hunk->getChanges()) as $line) {
$in_common = strncmp($line, " ", 1) === 0;
$in_old = strncmp($line, "-", 1) === 0 || $in_common;
$in_new = strncmp($line, "+", 1) === 0 || $in_common;
$in_selected = strncmp($line, $prefix, 1) === 0;
$skip = !$in_selected && !$in_common;
if ($hunk_pos[$prefix] <= $end) {
if ($start <= $hunk_pos[$prefix]) {
if (!$skip || ($hunk_pos[$prefix] != $start &&
$hunk_pos[$prefix] != $end)) {
if ($in_old) {
if ($hunk_offset["-"] === NULL) {
$hunk_offset["-"] = $hunk_pos["-"];
}
$hunk_last["-"] = $hunk_pos["-"];
}
if ($in_new) {
if ($hunk_offset["+"] === NULL) {
$hunk_offset["+"] = $hunk_pos["+"];
}
$hunk_last["+"] = $hunk_pos["+"];
}
$hunk_content[] = $line;
}
}
if ($in_old) { ++$hunk_pos["-"]; }
if ($in_new) { ++$hunk_pos["+"]; }
}
}
if ($hunk_offset["-"] !== NULL || $hunk_offset["+"] !== NULL) {
$header = "@@";
if ($hunk_offset["-"] !== NULL) {
$header .= " -" . ($hunk->getOldOffset() + $hunk_offset["-"]) .
"," . ($hunk_last["-"]-$hunk_offset["-"]+1);
}
if ($hunk_offset["+"] !== NULL) {
$header .= " +" . ($hunk->getNewOffset() + $hunk_offset["+"]) .
"," . ($hunk_last["+"]-$hunk_offset["+"]+1);
}
$header .= " @@";
$context[] = $header;
$context[] = implode("\n", $hunk_content);
}
}
}
return implode("\n", $context);
}
}
diff --git a/src/applications/differential/storage/DifferentialComment.php b/src/applications/differential/storage/DifferentialComment.php
index 50737a6d7c..401b291b51 100644
--- a/src/applications/differential/storage/DifferentialComment.php
+++ b/src/applications/differential/storage/DifferentialComment.php
@@ -1,103 +1,87 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialComment extends DifferentialDAO
implements PhabricatorMarkupInterface {
const METADATA_ADDED_REVIEWERS = 'added-reviewers';
const METADATA_REMOVED_REVIEWERS = 'removed-reviewers';
const METADATA_ADDED_CCS = 'added-ccs';
const METADATA_DIFF_ID = 'diff-id';
const MARKUP_FIELD_BODY = 'markup:body';
protected $authorPHID;
protected $revisionID;
protected $action;
protected $content;
protected $cache;
protected $metadata = array();
protected $contentSource;
private $arbitraryDiffForFacebook;
public function giveFacebookSomeArbitraryDiff(DifferentialDiff $diff) {
$this->arbitraryDiffForFacebook = $diff;
return $this;
}
public function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'metadata' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source->serialize();
return $this;
}
public function getContentSource() {
return PhabricatorContentSource::newFromSerialized($this->contentSource);
}
public function getMarkupFieldKey($field) {
if ($this->getID()) {
return 'DC:'.$this->getID();
}
// The summary and test plan render as comments, but do not have IDs.
// They are also mutable. Build keys using content hashes.
$hash = PhabricatorHash::digest($this->getMarkupText($field));
return 'DC:'.$hash;
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newDifferentialMarkupEngine(
array(
'differential.diff' => $this->arbitraryDiffForFacebook,
));
}
public function getMarkupText($field) {
return $this->getContent();
}
public function didMarkupText($field, $output, PhutilMarkupEngine $engine) {
return $output;
}
public function shouldUseMarkupCache($field) {
if ($this->getID()) {
return true;
}
$action = $this->getAction();
switch ($action) {
case DifferentialAction::ACTION_SUMMARIZE:
case DifferentialAction::ACTION_TESTPLAN:
return true;
}
return false;
}
}
diff --git a/src/applications/differential/storage/DifferentialDAO.php b/src/applications/differential/storage/DifferentialDAO.php
index 5729dbb444..56eed76ebf 100644
--- a/src/applications/differential/storage/DifferentialDAO.php
+++ b/src/applications/differential/storage/DifferentialDAO.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DifferentialDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'differential';
}
}
diff --git a/src/applications/differential/storage/DifferentialDiff.php b/src/applications/differential/storage/DifferentialDiff.php
index 4f43bc1f12..c8f09d0463 100644
--- a/src/applications/differential/storage/DifferentialDiff.php
+++ b/src/applications/differential/storage/DifferentialDiff.php
@@ -1,331 +1,315 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialDiff extends DifferentialDAO {
protected $revisionID;
protected $authorPHID;
protected $sourceMachine;
protected $sourcePath;
protected $sourceControlSystem;
protected $sourceControlBaseRevision;
protected $sourceControlPath;
protected $lintStatus;
protected $unitStatus;
protected $lineCount;
protected $branch;
protected $bookmark;
protected $parentRevisionID;
protected $arcanistProjectPHID;
protected $creationMethod;
protected $repositoryUUID;
protected $description;
private $unsavedChangesets = array();
private $changesets;
public function addUnsavedChangeset(DifferentialChangeset $changeset) {
if ($this->changesets === null) {
$this->changesets = array();
}
$this->unsavedChangesets[] = $changeset;
$this->changesets[] = $changeset;
return $this;
}
public function attachChangesets(array $changesets) {
assert_instances_of($changesets, 'DifferentialChangeset');
$this->changesets = $changesets;
return $this;
}
public function getChangesets() {
if ($this->changesets === null) {
throw new Exception("Must load and attach changesets first!");
}
return $this->changesets;
}
public function loadChangesets() {
if (!$this->getID()) {
return array();
}
return id(new DifferentialChangeset())->loadAllWhere(
'diffID = %d',
$this->getID());
}
public function loadArcanistProject() {
if (!$this->getArcanistProjectPHID()) {
return null;
}
return id(new PhabricatorRepositoryArcanistProject())->loadOneWhere(
'phid = %s',
$this->getArcanistProjectPHID());
}
public function getBackingVersionControlSystem() {
$arcanist_project = $this->loadArcanistProject();
if (!$arcanist_project) {
return null;
}
$repository = $arcanist_project->loadRepository();
if (!$repository) {
return null;
}
return $repository->getVersionControlSystem();
}
public function save() {
$this->openTransaction();
$ret = parent::save();
foreach ($this->unsavedChangesets as $changeset) {
$changeset->setDiffID($this->getID());
$changeset->save();
}
$this->saveTransaction();
return $ret;
}
public function delete() {
$this->openTransaction();
foreach ($this->loadChangesets() as $changeset) {
$changeset->delete();
}
$properties = id(new DifferentialDiffProperty())->loadAllWhere(
'diffID = %d',
$this->getID());
foreach ($properties as $prop) {
$prop->delete();
}
$ret = parent::delete();
$this->saveTransaction();
return $ret;
}
public static function newFromRawChanges(array $changes) {
assert_instances_of($changes, 'ArcanistDiffChange');
$diff = new DifferentialDiff();
$lines = 0;
foreach ($changes as $change) {
if ($change->getType() == ArcanistDiffChangeType::TYPE_MESSAGE) {
// If a user pastes a diff into Differential which includes a commit
// message (e.g., they ran `git show` to generate it), discard that
// change when constructing a DifferentialDiff.
continue;
}
$changeset = new DifferentialChangeset();
$add_lines = 0;
$del_lines = 0;
$first_line = PHP_INT_MAX;
$hunks = $change->getHunks();
if ($hunks) {
foreach ($hunks as $hunk) {
$dhunk = new DifferentialHunk();
$dhunk->setOldOffset($hunk->getOldOffset());
$dhunk->setOldLen($hunk->getOldLength());
$dhunk->setNewOffset($hunk->getNewOffset());
$dhunk->setNewLen($hunk->getNewLength());
$dhunk->setChanges($hunk->getCorpus());
$changeset->addUnsavedHunk($dhunk);
$add_lines += $hunk->getAddLines();
$del_lines += $hunk->getDelLines();
$added_lines = $hunk->getChangedLines('new');
if ($added_lines) {
$first_line = min($first_line, head_key($added_lines));
}
}
$lines += $add_lines + $del_lines;
} else {
// This happens when you add empty files.
$changeset->attachHunks(array());
}
$metadata = $change->getAllMetadata();
if ($first_line != PHP_INT_MAX) {
$metadata['line:first'] = $first_line;
}
$changeset->setOldFile($change->getOldPath());
$changeset->setFilename($change->getCurrentPath());
$changeset->setChangeType($change->getType());
$changeset->setFileType($change->getFileType());
$changeset->setMetadata($metadata);
$changeset->setOldProperties($change->getOldProperties());
$changeset->setNewProperties($change->getNewProperties());
$changeset->setAwayPaths($change->getAwayPaths());
$changeset->setAddLines($add_lines);
$changeset->setDelLines($del_lines);
$diff->addUnsavedChangeset($changeset);
}
$diff->setLineCount($lines);
$diff->detectCopiedCode();
return $diff;
}
public function detectCopiedCode($min_width = 30, $min_lines = 3) {
$map = array();
$files = array();
$types = array();
foreach ($this->changesets as $changeset) {
$file = $changeset->getFilename();
foreach ($changeset->getHunks() as $hunk) {
$line = $hunk->getOldOffset();
foreach (explode("\n", $hunk->getChanges()) as $code) {
$type = (isset($code[0]) ? $code[0] : '');
if ($type == '-' || $type == ' ') {
$code = trim(substr($code, 1));
$files[$file][$line] = $code;
$types[$file][$line] = $type;
if (strlen($code) >= $min_width) {
$map[$code][] = array($file, $line);
}
$line++;
}
}
}
}
foreach ($this->changesets as $changeset) {
$copies = array();
foreach ($changeset->getHunks() as $hunk) {
$added = array_map('trim', $hunk->getAddedLines());
for (reset($added); list($line, $code) = each($added); next($added)) {
if (isset($map[$code])) { // We found a long matching line.
$best_length = 0;
foreach ($map[$code] as $val) { // Explore all candidates.
list($file, $orig_line) = $val;
$length = 1;
// Search also backwards for short lines.
foreach (array(-1, 1) as $direction) {
$offset = $direction;
while (!isset($copies[$line + $offset]) &&
isset($added[$line + $offset]) &&
idx($files[$file], $orig_line + $offset) ===
$added[$line + $offset]) {
$length++;
$offset += $direction;
}
}
if ($length > $best_length ||
($length == $best_length && // Prefer moves.
idx($types[$file], $orig_line) == '-')) {
$best_length = $length;
// ($offset - 1) contains number of forward matching lines.
$best_offset = $offset - 1;
$best_file = $file;
$best_line = $orig_line;
}
}
$file = ($best_file == $changeset->getFilename() ? '' : $best_file);
for ($i = $best_length; $i--; ) {
$type = idx($types[$best_file], $best_line + $best_offset - $i);
$copies[$line + $best_offset - $i] = ($best_length < $min_lines
? array() // Ignore short blocks.
: array($file, $best_line + $best_offset - $i, $type));
}
for ($i = 0; $i < $best_offset; $i++) {
next($added);
}
}
}
}
$copies = array_filter($copies);
if ($copies) {
$metadata = $changeset->getMetadata();
$metadata['copy:lines'] = $copies;
$changeset->setMetadata($metadata);
}
}
}
public function getDiffDict() {
$dict = array(
'id' => $this->getID(),
'parent' => $this->getParentRevisionID(),
'revisionID' => $this->getRevisionID(),
'sourceControlBaseRevision' => $this->getSourceControlBaseRevision(),
'sourceControlPath' => $this->getSourceControlPath(),
'sourceControlSystem' => $this->getSourceControlSystem(),
'branch' => $this->getBranch(),
'bookmark' => $this->getBookmark(),
'creationMethod' => $this->getCreationMethod(),
'description' => $this->getDescription(),
'unitStatus' => $this->getUnitStatus(),
'lintStatus' => $this->getLintStatus(),
'changes' => array(),
'properties' => array(),
);
foreach ($this->getChangesets() as $changeset) {
$hunks = array();
foreach ($changeset->getHunks() as $hunk) {
$hunks[] = array(
'oldOffset' => $hunk->getOldOffset(),
'newOffset' => $hunk->getNewOffset(),
'oldLength' => $hunk->getOldLen(),
'newLength' => $hunk->getNewLen(),
'addLines' => null,
'delLines' => null,
'isMissingOldNewline' => null,
'isMissingNewNewline' => null,
'corpus' => $hunk->getChanges(),
);
}
$change = array(
'metadata' => $changeset->getMetadata(),
'oldPath' => $changeset->getOldFile(),
'currentPath' => $changeset->getFilename(),
'awayPaths' => $changeset->getAwayPaths(),
'oldProperties' => $changeset->getOldProperties(),
'newProperties' => $changeset->getNewProperties(),
'type' => $changeset->getChangeType(),
'fileType' => $changeset->getFileType(),
'commitHash' => null,
'addLines' => $changeset->getAddLines(),
'delLines' => $changeset->getDelLines(),
'hunks' => $hunks,
);
$dict['changes'][] = $change;
}
$properties = id(new DifferentialDiffProperty())->loadAllWhere(
'diffID = %d',
$this->getID());
foreach ($properties as $property) {
$dict['properties'][$property->getName()] = $property->getData();
}
return $dict;
}
}
diff --git a/src/applications/differential/storage/DifferentialDiffProperty.php b/src/applications/differential/storage/DifferentialDiffProperty.php
index 91365b5b22..11be887eb4 100644
--- a/src/applications/differential/storage/DifferentialDiffProperty.php
+++ b/src/applications/differential/storage/DifferentialDiffProperty.php
@@ -1,32 +1,16 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialDiffProperty extends DifferentialDAO {
protected $diffID;
protected $name;
protected $data;
protected function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'data' => self::SERIALIZATION_JSON,
)) + parent::getConfiguration();
}
}
diff --git a/src/applications/differential/storage/DifferentialHunk.php b/src/applications/differential/storage/DifferentialHunk.php
index f9f2567960..77484cdc01 100644
--- a/src/applications/differential/storage/DifferentialHunk.php
+++ b/src/applications/differential/storage/DifferentialHunk.php
@@ -1,92 +1,76 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialHunk extends DifferentialDAO {
protected $changesetID;
protected $changes;
protected $oldOffset;
protected $oldLen;
protected $newOffset;
protected $newLen;
public function getAddedLines() {
return $this->makeContent($include = '+');
}
public function makeNewFile() {
return implode('', $this->makeContent($include = ' +'));
}
public function makeOldFile() {
return implode('', $this->makeContent($include = ' -'));
}
public function makeChanges() {
return implode('', $this->makeContent($include = '-+'));
}
final private function makeContent($include) {
$results = array();
$lines = explode("\n", $this->changes);
// NOTE: To determine whether the recomposed file should have a trailing
// newline, we look for a "\ No newline at end of file" line which appears
// after a line which we don't exclude. For example, if we're constructing
// the "new" side of a diff (excluding "-"), we want to ignore this one:
//
// - x
// \ No newline at end of file
// + x
//
// ...since it's talking about the "old" side of the diff, but interpret
// this as meaning we should omit the newline:
//
// - x
// + x
// \ No newline at end of file
$n = (strpos($include, '+') !== false ?
$this->newOffset :
$this->oldOffset);
$use_next_newline = false;
foreach ($lines as $line) {
if (!isset($line[0])) {
continue;
}
if ($line[0] == '\\') {
if ($use_next_newline) {
$results[last_key($results)] = rtrim(end($results), "\n");
}
} else if (strpos($include, $line[0]) === false) {
$use_next_newline = false;
} else {
$use_next_newline = true;
$results[$n] = substr($line, 1)."\n";
}
if ($line[0] == ' ' || strpos($include, $line[0]) !== false) {
$n++;
}
}
return $results;
}
}
diff --git a/src/applications/differential/storage/DifferentialInlineComment.php b/src/applications/differential/storage/DifferentialInlineComment.php
index 3b064adb45..469bd3ea5d 100644
--- a/src/applications/differential/storage/DifferentialInlineComment.php
+++ b/src/applications/differential/storage/DifferentialInlineComment.php
@@ -1,150 +1,134 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialInlineComment
extends DifferentialDAO
implements PhabricatorInlineCommentInterface {
protected $revisionID;
protected $changesetID;
protected $commentID;
protected $authorPHID;
protected $isNewFile;
protected $lineNumber;
protected $lineLength;
protected $content;
protected $cache;
private $syntheticAuthor;
public function setSyntheticAuthor($synthetic_author) {
$this->syntheticAuthor = $synthetic_author;
return $this;
}
public function getSyntheticAuthor() {
return $this->syntheticAuthor;
}
public function isCompatible(PhabricatorInlineCommentInterface $comment) {
return
($this->getAuthorPHID() === $comment->getAuthorPHID()) &&
($this->getSyntheticAuthor() === $comment->getSyntheticAuthor()) &&
($this->getContent() === $comment->getContent());
}
public function setContent($content) {
$this->setCache(null);
$this->writeField('content', $content);
return $this;
}
public function getContent() {
return $this->readField('content');
}
public function isDraft() {
return !$this->getCommentID();
}
// NOTE: We need to provide implementations so we conform to the shared
// interface; these are all trivial and just explicit versions of the Lisk
// defaults.
public function setChangesetID($id) {
$this->writeField('changesetID', $id);
return $this;
}
public function getChangesetID() {
return $this->readField('changesetID');
}
public function setIsNewFile($is_new) {
$this->writeField('isNewFile', $is_new);
return $this;
}
public function getIsNewFile() {
return $this->readField('isNewFile');
}
public function setLineNumber($number) {
$this->writeField('lineNumber', $number);
return $this;
}
public function getLineNumber() {
return $this->readField('lineNumber');
}
public function setLineLength($length) {
$this->writeField('lineLength', $length);
return $this;
}
public function getLineLength() {
return $this->readField('lineLength');
}
public function setCache($cache) {
$this->writeField('cache', $cache);
return $this;
}
public function getCache() {
return $this->readField('cache');
}
public function setAuthorPHID($phid) {
$this->writeField('authorPHID', $phid);
return $this;
}
public function getAuthorPHID() {
return $this->readField('authorPHID');
}
/* -( PhabricatorMarkupInterface Implementation )-------------------------- */
public function getMarkupFieldKey($field) {
// We can't use ID because synthetic comments don't have it.
return 'DI:'.PhabricatorHash::digest($this->getContent());
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newDifferentialMarkupEngine();
}
public function getMarkupText($field) {
return $this->getContent();
}
public function didMarkupText($field, $output, PhutilMarkupEngine $engine) {
return $output;
}
public function shouldUseMarkupCache($field) {
// Only cache submitted comments.
return ($this->getID() && $this->getCommentID());
}
}
diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php
index e59f864ce1..b34e5d8afe 100644
--- a/src/applications/differential/storage/DifferentialRevision.php
+++ b/src/applications/differential/storage/DifferentialRevision.php
@@ -1,329 +1,313 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialRevision extends DifferentialDAO {
protected $title;
protected $originalTitle;
protected $status;
protected $summary;
protected $testPlan;
protected $phid;
protected $authorPHID;
protected $lastReviewerPHID;
protected $dateCommitted;
protected $lineCount;
protected $attached = array();
protected $unsubscribed = array();
protected $mailKey;
protected $branchName;
protected $arcanistProjectPHID;
private $relationships;
private $commits;
private $activeDiff = false;
private $diffIDs;
private $hashes;
const RELATIONSHIP_TABLE = 'differential_relationship';
const TABLE_COMMIT = 'differential_commit';
const RELATION_REVIEWER = 'revw';
const RELATION_SUBSCRIBED = 'subd';
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'attached' => self::SERIALIZATION_JSON,
'unsubscribed' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function setTitle($title) {
$this->title = $title;
if (!$this->getID()) {
$this->originalTitle = $title;
}
return $this;
}
public function loadIDsByCommitPHIDs($phids) {
if (!$phids) {
return array();
}
$revision_ids = queryfx_all(
$this->establishConnection('r'),
'SELECT * FROM %T WHERE commitPHID IN (%Ls)',
self::TABLE_COMMIT,
$phids);
return ipull($revision_ids, 'revisionID', 'commitPHID');
}
public function loadCommitPHIDs() {
if (!$this->getID()) {
return ($this->commits = array());
}
$commits = queryfx_all(
$this->establishConnection('r'),
'SELECT commitPHID FROM %T WHERE revisionID = %d',
self::TABLE_COMMIT,
$this->getID());
$commits = ipull($commits, 'commitPHID');
return ($this->commits = $commits);
}
public function getCommitPHIDs() {
if ($this->commits === null) {
throw new Exception("Must attach commits first!");
}
return $this->commits;
}
public function getActiveDiff() {
// TODO: Because it's currently technically possible to create a revision
// without an associated diff, we allow an attached-but-null active diff.
// It would be good to get rid of this once we make diff-attaching
// transactional.
if ($this->activeDiff === false) {
throw new Exception("Must attach active diff first!");
}
return $this->activeDiff;
}
public function attachActiveDiff($diff) {
$this->activeDiff = $diff;
return $this;
}
public function getDiffIDs() {
if ($this->diffIDs === null) {
throw new Exception("Must attach diff IDs first!");
}
return $this->diffIDs;
}
public function attachDiffIDs(array $ids) {
rsort($ids);
$this->diffIDs = array_values($ids);
return $this;
}
public function attachCommitPHIDs(array $phids) {
$this->commits = array_values($phids);
return $this;
}
public function getAttachedPHIDs($type) {
return array_keys(idx($this->attached, $type, array()));
}
public function setAttachedPHIDs($type, array $phids) {
$this->attached[$type] = array_fill_keys($phids, array());
return $this;
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_DREV);
}
public function loadDiffs() {
if (!$this->getID()) {
return array();
}
return id(new DifferentialDiff())->loadAllWhere(
'revisionID = %d',
$this->getID());
}
public function loadComments() {
if (!$this->getID()) {
return array();
}
return id(new DifferentialComment())->loadAllWhere(
'revisionID = %d',
$this->getID());
}
public function loadActiveDiff() {
return id(new DifferentialDiff())->loadOneWhere(
'revisionID = %d ORDER BY id DESC LIMIT 1',
$this->getID());
}
public function save() {
if (!$this->getMailKey()) {
$this->mailKey = Filesystem::readRandomCharacters(40);
}
return parent::save();
}
public function delete() {
$this->openTransaction();
$diffs = $this->loadDiffs();
foreach ($diffs as $diff) {
$diff->delete();
}
$conn_w = $this->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE revisionID = %d',
self::RELATIONSHIP_TABLE,
$this->getID());
queryfx(
$conn_w,
'DELETE FROM %T WHERE revisionID = %d',
self::TABLE_COMMIT,
$this->getID());
$comments = id(new DifferentialComment())->loadAllWhere(
'revisionID = %d',
$this->getID());
foreach ($comments as $comment) {
$comment->delete();
}
$inlines = id(new DifferentialInlineComment())->loadAllWhere(
'revisionID = %d',
$this->getID());
foreach ($inlines as $inline) {
$inline->delete();
}
$fields = id(new DifferentialAuxiliaryField())->loadAllWhere(
'revisionPHID = %s',
$this->getPHID());
foreach ($fields as $field) {
$field->delete();
}
// we have to do paths a little differentally as they do not have
// an id or phid column for delete() to act on
$dummy_path = new DifferentialAffectedPath();
queryfx(
$conn_w,
'DELETE FROM %T WHERE revisionID = %d',
$dummy_path->getTableName(),
$this->getID());
$result = parent::delete();
$this->saveTransaction();
return $result;
}
public function loadRelationships() {
if (!$this->getID()) {
$this->relationships = array();
return;
}
$data = queryfx_all(
$this->establishConnection('r'),
'SELECT * FROM %T WHERE revisionID = %d ORDER BY sequence',
self::RELATIONSHIP_TABLE,
$this->getID());
return $this->attachRelationships($data);
}
public function attachRelationships(array $relationships) {
$this->relationships = igroup($relationships, 'relation');
return $this;
}
public function getReviewers() {
return $this->getRelatedPHIDs(self::RELATION_REVIEWER);
}
public function getCCPHIDs() {
return $this->getRelatedPHIDs(self::RELATION_SUBSCRIBED);
}
private function getRelatedPHIDs($relation) {
if ($this->relationships === null) {
throw new Exception("Must load relationships!");
}
return ipull($this->getRawRelations($relation), 'objectPHID');
}
public function getRawRelations($relation) {
return idx($this->relationships, $relation, array());
}
public function getUnsubscribedPHIDs() {
return array_keys($this->getUnsubscribed());
}
public function getPrimaryReviewer() {
$reviewers = $this->getReviewers();
$last = $this->lastReviewerPHID;
if (!$last || !in_array($last, $reviewers)) {
return head($this->getReviewers());
}
return $last;
}
public function loadReviewedBy() {
$reviewer = null;
if ($this->status == ArcanistDifferentialRevisionStatus::ACCEPTED ||
$this->status == ArcanistDifferentialRevisionStatus::CLOSED) {
$comments = $this->loadComments();
foreach ($comments as $comment) {
$action = $comment->getAction();
if ($action == DifferentialAction::ACTION_ACCEPT) {
$reviewer = $comment->getAuthorPHID();
} else if ($action == DifferentialAction::ACTION_REJECT ||
$action == DifferentialAction::ACTION_ABANDON ||
$action == DifferentialAction::ACTION_RETHINK) {
$reviewer = null;
}
}
}
return $reviewer;
}
public function getHashes() {
if ($this->hashes === null) {
throw new Exception("Call attachHashes() before getHashes()!");
}
return $this->hashes;
}
public function attachHashes(array $hashes) {
$this->hashes = $hashes;
return $this;
}
}
diff --git a/src/applications/differential/storage/__tests__/DifferentialHunkTestCase.php b/src/applications/differential/storage/__tests__/DifferentialHunkTestCase.php
index 331c58a640..1e1e7e927d 100644
--- a/src/applications/differential/storage/__tests__/DifferentialHunkTestCase.php
+++ b/src/applications/differential/storage/__tests__/DifferentialHunkTestCase.php
@@ -1,53 +1,37 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialHunkTestCase extends ArcanistPhutilTestCase {
public function testMakeChanges() {
$root = dirname(__FILE__).'/hunk/';
$hunk = new DifferentialHunk();
$hunk->setChanges(Filesystem::readFile($root.'basic.diff'));
$hunk->setOldOffset(1);
$hunk->setNewOffset(11);
$old = Filesystem::readFile($root.'old.txt');
$this->assertEqual($old, $hunk->makeOldFile());
$new = Filesystem::readFile($root.'new.txt');
$this->assertEqual($new, $hunk->makeNewFile());
$added = array(
12 => "1 quack\n",
13 => "1 quack\n",
16 => "5 drake\n",
);
$this->assertEqual($added, $hunk->getAddedLines());
$hunk = new DifferentialHunk();
$hunk->setChanges(Filesystem::readFile($root.'newline.diff'));
$hunk->setOldOffset(1);
$hunk->setNewOffset(11);
$this->assertEqual("a\n", $hunk->makeOldFile());
$this->assertEqual("a", $hunk->makeNewFile());
$this->assertEqual(array(11 => "a"), $hunk->getAddedLines());
}
}
diff --git a/src/applications/differential/view/DifferentialAddCommentView.php b/src/applications/differential/view/DifferentialAddCommentView.php
index f5743a7d1e..0273432033 100644
--- a/src/applications/differential/view/DifferentialAddCommentView.php
+++ b/src/applications/differential/view/DifferentialAddCommentView.php
@@ -1,225 +1,209 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialAddCommentView extends AphrontView {
private $revision;
private $actions;
private $actionURI;
private $user;
private $draft;
private $auxFields;
private $reviewers = array();
private $ccs = array();
public function setRevision($revision) {
$this->revision = $revision;
return $this;
}
public function setAuxFields(array $aux_fields) {
assert_instances_of($aux_fields, 'DifferentialFieldSpecification');
$this->auxFields = $aux_fields;
return $this;
}
public function setActions(array $actions) {
$this->actions = $actions;
return $this;
}
public function setActionURI($uri) {
$this->actionURI = $uri;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setDraft(PhabricatorDraft $draft = null) {
$this->draft = $draft;
return $this;
}
public function setReviewers(array $names) {
$this->reviewers = $names;
return $this;
}
public function setCCs(array $names) {
$this->ccs = $names;
return $this;
}
private function generateWarningView(
$status,
array $titles,
$id,
$content) {
$warning = new AphrontErrorView();
$warning->setSeverity(AphrontErrorView::SEVERITY_ERROR);
$warning->setID($id);
$warning->appendChild($content);
$warning->setTitle(idx($titles, $status, 'Warning'));
return $warning;
}
public function render() {
require_celerity_resource('differential-revision-add-comment-css');
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
$revision = $this->revision;
$action = null;
if ($this->draft) {
$action = idx($this->draft->getMetadata(), 'action');
}
$enable_reviewers = DifferentialAction::allowReviewers($action);
$enable_ccs = ($action == DifferentialAction::ACTION_ADDCCS);
$form = new AphrontFormView();
$form
->setWorkflow(true)
->setFlexible(true)
->setUser($this->user)
->setAction($this->actionURI)
->addHiddenInput('revision_id', $revision->getID())
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Action')
->setName('action')
->setValue($action)
->setID('comment-action')
->setOptions($this->actions))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel('Add Reviewers')
->setName('reviewers')
->setControlID('add-reviewers')
->setControlStyle($enable_reviewers ? null : 'display: none')
->setID('add-reviewers-tokenizer')
->setDisableBehavior(true))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel('Add CCs')
->setName('ccs')
->setControlID('add-ccs')
->setControlStyle($enable_ccs ? null : 'display: none')
->setID('add-ccs-tokenizer')
->setDisableBehavior(true))
->appendChild(
id(new PhabricatorRemarkupControl())
->setName('comment')
->setID('comment-content')
->setLabel('Comment')
->setValue($this->draft ? $this->draft->getDraft() : null))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue($is_serious ? 'Submit' : 'Clowncopterize'));
Javelin::initBehavior(
'differential-add-reviewers-and-ccs',
array(
'dynamic' => array(
'add-reviewers-tokenizer' => array(
'actions' => array('request_review' => 1, 'add_reviewers' => 1),
'src' => '/typeahead/common/users/',
'value' => $this->reviewers,
'row' => 'add-reviewers',
'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'),
'placeholder' => 'Type a user name...',
),
'add-ccs-tokenizer' => array(
'actions' => array('add_ccs' => 1),
'src' => '/typeahead/common/mailable/',
'value' => $this->ccs,
'row' => 'add-ccs',
'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'),
'placeholder' => 'Type a user or mailing list...',
),
),
'select' => 'comment-action',
));
$diff = $revision->loadActiveDiff();
$warnings = mpull($this->auxFields, 'renderWarningBoxForRevisionAccept');
Javelin::initBehavior(
'differential-accept-with-errors',
array(
'select' => 'comment-action',
'warnings' => 'warnings',
));
$rev_id = $revision->getID();
Javelin::initBehavior(
'differential-feedback-preview',
array(
'uri' => '/differential/comment/preview/'.$rev_id.'/',
'preview' => 'comment-preview',
'action' => 'comment-action',
'content' => 'comment-content',
'previewTokenizers' => array(
'reviewers' => 'add-reviewers-tokenizer',
'ccs' => 'add-ccs-tokenizer',
),
'inlineuri' => '/differential/comment/inline/preview/'.$rev_id.'/',
'inline' => 'inline-comment-preview',
));
$warning_container = '<div id="warnings">';
foreach ($warnings as $warning) {
if ($warning) {
$warning_container .= $warning->render();
}
}
$warning_container .= '</div>';
$header = id(new PhabricatorHeaderView())
->setHeader($is_serious ? 'Add Comment' : 'Leap Into Action');
return
id(new PhabricatorAnchorView())
->setAnchorName('comment')
->setNavigationMarker(true)
->render().
'<div class="differential-add-comment-panel">'.
$header->render().
$form->render().
$warning_container.
'<div class="aphront-panel-preview aphront-panel-flush">'.
'<div id="comment-preview">'.
'<span class="aphront-panel-preview-loading-text">'.
'Loading comment preview...'.
'</span>'.
'</div>'.
'<div id="inline-comment-preview">'.
'</div>'.
'</div>'.
'</div>';
}
}
diff --git a/src/applications/differential/view/DifferentialChangesetDetailView.php b/src/applications/differential/view/DifferentialChangesetDetailView.php
index ccc622d38d..364fe35701 100644
--- a/src/applications/differential/view/DifferentialChangesetDetailView.php
+++ b/src/applications/differential/view/DifferentialChangesetDetailView.php
@@ -1,122 +1,106 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialChangesetDetailView extends AphrontView {
private $changeset;
private $buttons = array();
private $editable;
private $symbolIndex;
private $id;
private $vsChangesetID;
public function setChangeset($changeset) {
$this->changeset = $changeset;
return $this;
}
public function addButton($button) {
$this->buttons[] = $button;
return $this;
}
public function setEditable($editable) {
$this->editable = $editable;
return $this;
}
public function setSymbolIndex($symbol_index) {
$this->symbolIndex = $symbol_index;
return $this;
}
public function getID() {
if (!$this->id) {
$this->id = celerity_generate_unique_node_id();
}
return $this->id;
}
public function setVsChangesetID($vs_changeset_id) {
$this->vsChangesetID = $vs_changeset_id;
return $this;
}
public function getVsChangesetID() {
return $this->vsChangesetID;
}
public function render() {
require_celerity_resource('differential-changeset-view-css');
require_celerity_resource('syntax-highlighting-css');
Javelin::initBehavior('phabricator-oncopy', array());
$changeset = $this->changeset;
$class = 'differential-changeset';
if (!$this->editable) {
$class .= ' differential-changeset-immutable';
}
$buttons = null;
if ($this->buttons) {
$buttons =
'<div class="differential-changeset-buttons">'.
implode('', $this->buttons).
'</div>';
}
$id = $this->getID();
if ($this->symbolIndex) {
Javelin::initBehavior(
'repository-crossreference',
array(
'container' => $id,
) + $this->symbolIndex);
}
$display_filename = $changeset->getDisplayFilename();
$output = javelin_render_tag(
'div',
array(
'sigil' => 'differential-changeset',
'meta' => array(
'left' => nonempty(
$this->getVsChangesetID(),
$this->changeset->getID()),
'right' => $this->changeset->getID(),
),
'class' => $class,
'id' => $id,
),
id(new PhabricatorAnchorView())
->setAnchorName($changeset->getAnchorName())
->setNavigationMarker(true)
->render().
$buttons.
'<h1>'.phutil_escape_html($display_filename).'</h1>'.
'<div style="clear: both;"></div>'.
$this->renderChildren());
return $output;
}
}
diff --git a/src/applications/differential/view/DifferentialChangesetListView.php b/src/applications/differential/view/DifferentialChangesetListView.php
index 31ae0e9246..27effc6166 100644
--- a/src/applications/differential/view/DifferentialChangesetListView.php
+++ b/src/applications/differential/view/DifferentialChangesetListView.php
@@ -1,327 +1,311 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialChangesetListView
extends DifferentialCodeWidthSensitiveView {
private $changesets = array();
private $visibleChangesets = array();
private $references = array();
private $inlineURI;
private $renderURI = '/differential/changeset/';
private $whitespace;
private $standaloneURI;
private $leftRawFileURI;
private $rightRawFileURI;
private $user;
private $symbolIndexes = array();
private $repository;
private $branch;
private $diff;
private $vsMap = array();
public function setBranch($branch) {
$this->branch = $branch;
return $this;
}
private function getBranch() {
return $this->branch;
}
public function setChangesets($changesets) {
$this->changesets = $changesets;
return $this;
}
public function setVisibleChangesets($visible_changesets) {
$this->visibleChangesets = $visible_changesets;
return $this;
}
public function setInlineCommentControllerURI($uri) {
$this->inlineURI = $uri;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
public function setDiff(DifferentialDiff $diff) {
$this->diff = $diff;
return $this;
}
public function setRenderingReferences(array $references) {
$this->references = $references;
return $this;
}
public function setSymbolIndexes(array $indexes) {
$this->symbolIndexes = $indexes;
return $this;
}
public function setRenderURI($render_uri) {
$this->renderURI = $render_uri;
return $this;
}
public function setWhitespace($whitespace) {
$this->whitespace = $whitespace;
return $this;
}
public function setVsMap(array $vs_map) {
$this->vsMap = $vs_map;
return $this;
}
public function getVsMap() {
return $this->vsMap;
}
public function setStandaloneURI($uri) {
$this->standaloneURI = $uri;
return $this;
}
public function setRawFileURIs($l, $r) {
$this->leftRawFileURI = $l;
$this->rightRawFileURI = $r;
return $this;
}
public function render() {
require_celerity_resource('differential-changeset-view-css');
$changesets = $this->changesets;
Javelin::initBehavior('differential-toggle-files', array());
$output = array();
$mapping = array();
foreach ($changesets as $key => $changeset) {
$file = $changeset->getFilename();
$class = 'differential-changeset';
if (!$this->inlineURI) {
$class .= ' differential-changeset-noneditable';
}
$ref = $this->references[$key];
$detail = new DifferentialChangesetDetailView();
$view_options = $this->renderViewOptionsDropdown(
$detail,
$ref,
$changeset);
$prefs = $this->user->loadPreferences();
$pref_symbols = $prefs->getPreference(
PhabricatorUserPreferences::PREFERENCE_DIFFUSION_SYMBOLS);
$detail->setChangeset($changeset);
$detail->addButton($view_options);
if ($pref_symbols != 'disabled') {
$detail->setSymbolIndex(idx($this->symbolIndexes, $key));
}
$detail->setVsChangesetID(idx($this->vsMap, $changeset->getID()));
$detail->setEditable(true);
$uniq_id = 'diff-'.$changeset->getAnchorName();
if (isset($this->visibleChangesets[$key])) {
$load = 'Loading...';
$mapping[$uniq_id] = $ref;
} else {
$load = javelin_render_tag(
'a',
array(
'href' => '#'.$uniq_id,
'meta' => array(
'id' => $uniq_id,
'ref' => $ref,
'kill' => true,
),
'sigil' => 'differential-load',
'mustcapture' => true,
),
'Load');
}
$detail->appendChild(
phutil_render_tag(
'div',
array(
'id' => $uniq_id,
),
'<div class="differential-loading">'.$load.'</div>'));
$output[] = $detail->render();
}
require_celerity_resource('aphront-tooltip-css');
Javelin::initBehavior('differential-populate', array(
'registry' => $mapping,
'whitespace' => $this->whitespace,
'uri' => $this->renderURI,
));
Javelin::initBehavior('differential-show-more', array(
'uri' => $this->renderURI,
'whitespace' => $this->whitespace,
));
Javelin::initBehavior('differential-comment-jump', array());
if ($this->inlineURI) {
$undo_templates = $this->renderUndoTemplates();
Javelin::initBehavior('differential-edit-inline-comments', array(
'uri' => $this->inlineURI,
'undo_templates' => $undo_templates,
'stage' => 'differential-review-stage',
));
}
return phutil_render_tag(
'div',
array(
'class' => 'differential-review-stage',
'id' => 'differential-review-stage',
'style' => "max-width: {$this->calculateSideBySideWidth()}px; ",
),
implode("\n", $output));
}
/**
* Render the "Undo" markup for the inline comment undo feature.
*/
private function renderUndoTemplates() {
$link = javelin_render_tag(
'a',
array(
'href' => '#',
'sigil' => 'differential-inline-comment-undo',
),
'Undo');
$div = phutil_render_tag(
'div',
array(
'class' => 'differential-inline-undo',
),
'Changes discarded. '.$link);
$template =
'<table><tr>'.
'<th></th><td>%s</td>'.
'<th></th><td colspan="2">%s</td>'.
'</tr></table>';
return array(
'l' => sprintf($template, $div, ''),
'r' => sprintf($template, '', $div),
);
}
private function renderViewOptionsDropdown(
DifferentialChangesetDetailView $detail,
$ref,
DifferentialChangeset $changeset) {
$meta = array();
$qparams = array(
'ref' => $ref,
'whitespace' => $this->whitespace,
);
if ($this->standaloneURI) {
$uri = new PhutilURI($this->standaloneURI);
$uri->setQueryParams($uri->getQueryParams() + $qparams);
$meta['standaloneURI'] = (string)$uri;
}
$repository = $this->repository;
if ($repository) {
$meta['diffusionURI'] = (string)$repository->getDiffusionBrowseURIForPath(
$changeset->getAbsoluteRepositoryPath($repository, $this->diff),
idx($changeset->getMetadata(), 'line:first'),
$this->getBranch());
}
$change = $changeset->getChangeType();
if ($this->leftRawFileURI) {
if ($change != DifferentialChangeType::TYPE_ADD) {
$uri = new PhutilURI($this->leftRawFileURI);
$uri->setQueryParams($uri->getQueryParams() + $qparams);
$meta['leftURI'] = (string)$uri;
}
}
if ($this->rightRawFileURI) {
if ($change != DifferentialChangeType::TYPE_DELETE &&
$change != DifferentialChangeType::TYPE_MULTICOPY) {
$uri = new PhutilURI($this->rightRawFileURI);
$uri->setQueryParams($uri->getQueryParams() + $qparams);
$meta['rightURI'] = (string)$uri;
}
}
$user = $this->user;
if ($user && $repository) {
$path = ltrim(
$changeset->getAbsoluteRepositoryPath($repository, $this->diff),
'/');
$line = idx($changeset->getMetadata(), 'line:first', 1);
$callsign = $repository->getCallsign();
$editor_link = $user->loadEditorLink($path, $line, $callsign);
if ($editor_link) {
$meta['editor'] = $editor_link;
} else {
$meta['editorConfigure'] = '/settings/panel/display/';
}
}
$meta['containerID'] = $detail->getID();
Javelin::initBehavior(
'differential-dropdown-menus',
array());
return javelin_render_tag(
'a',
array(
'class' => 'button small grey',
'meta' => $meta,
'href' => idx($meta, 'detailURI', '#'),
'target' => '_blank',
'sigil' => 'differential-view-options',
),
"View Options \xE2\x96\xBC");
}
}
diff --git a/src/applications/differential/view/DifferentialCodeWidthSensitiveView.php b/src/applications/differential/view/DifferentialCodeWidthSensitiveView.php
index c46f424f3c..2b27718b07 100644
--- a/src/applications/differential/view/DifferentialCodeWidthSensitiveView.php
+++ b/src/applications/differential/view/DifferentialCodeWidthSensitiveView.php
@@ -1,55 +1,39 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DifferentialCodeWidthSensitiveView extends AphrontView {
private $lineWidth = 80;
private function setLineWidth($width) {
$this->lineWidth = $width;
return $this;
}
public function getLineWidth() {
return $this->lineWidth;
}
public function setLineWidthFromChangesets(array $changesets) {
assert_instances_of($changesets, 'DifferentialChangeset');
if (empty($changesets)) {
return $this;
}
$max = 1;
foreach ($changesets as $changeset) {
$max = max($max, $changeset->getWordWrapWidth());
}
$this->setLineWidth($max);
return $this;
}
public function calculateSideBySideWidth() {
// Width of the constant-width elements (like line numbers, padding,
// and borders).
$const = 148;
$width = ceil(((1162 - $const) / 80) * $this->getLineWidth()) + $const;
return max(1162, $width);
}
}
diff --git a/src/applications/differential/view/DifferentialDiffTableOfContentsView.php b/src/applications/differential/view/DifferentialDiffTableOfContentsView.php
index cbacf9df16..15002dc8d2 100644
--- a/src/applications/differential/view/DifferentialDiffTableOfContentsView.php
+++ b/src/applications/differential/view/DifferentialDiffTableOfContentsView.php
@@ -1,280 +1,264 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialDiffTableOfContentsView extends AphrontView {
private $changesets = array();
private $visibleChangesets = array();
private $references = array();
private $repository;
private $diff;
private $user;
private $renderURI = '/differential/changeset/';
private $revisionID;
private $whitespace;
private $unitTestData;
public function setChangesets($changesets) {
$this->changesets = $changesets;
return $this;
}
public function setVisibleChangesets($visible_changesets) {
$this->visibleChangesets = $visible_changesets;
return $this;
}
public function setRenderingReferences(array $references) {
$this->references = $references;
return $this;
}
public function setRepository(PhabricatorRepository $repository) {
$this->repository = $repository;
return $this;
}
public function setDiff(DifferentialDiff $diff) {
$this->diff = $diff;
return $this;
}
public function setUnitTestData($unit_test_data) {
$this->unitTestData = $unit_test_data;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setRevisionID($revision_id) {
$this->revisionID = $revision_id;
return $this;
}
public function setWhitespace($whitespace) {
$this->whitespace = $whitespace;
return $this;
}
public function render() {
require_celerity_resource('differential-core-view-css');
require_celerity_resource('differential-table-of-contents-css');
$rows = array();
$coverage = array();
if ($this->unitTestData) {
$coverage_by_file = array();
foreach ($this->unitTestData as $result) {
$test_coverage = idx($result, 'coverage');
if (!$test_coverage) {
continue;
}
foreach ($test_coverage as $file => $results) {
$coverage_by_file[$file][] = $results;
}
}
foreach ($coverage_by_file as $file => $coverages) {
$coverage[$file] = ArcanistUnitTestResult::mergeCoverage($coverages);
}
}
$changesets = $this->changesets;
$paths = array();
foreach ($changesets as $id => $changeset) {
$type = $changeset->getChangeType();
$ftype = $changeset->getFileType();
$ref = idx($this->references, $id);
$link = $this->renderChangesetLink($changeset, $ref);
if (DifferentialChangeType::isOldLocationChangeType($type)) {
$away = $changeset->getAwayPaths();
if (count($away) > 1) {
$meta = array();
if ($type == DifferentialChangeType::TYPE_MULTICOPY) {
$meta[] = 'Deleted after being copied to multiple locations:';
} else {
$meta[] = 'Copied to multiple locations:';
}
foreach ($away as $path) {
$meta[] = phutil_escape_html($path);
}
$meta = implode('<br />', $meta);
} else {
if ($type == DifferentialChangeType::TYPE_MOVE_AWAY) {
$meta = 'Moved to '.phutil_escape_html(reset($away));
} else {
$meta = 'Copied to '.phutil_escape_html(reset($away));
}
}
} else if ($type == DifferentialChangeType::TYPE_MOVE_HERE) {
$meta = 'Moved from '.phutil_escape_html($changeset->getOldFile());
} else if ($type == DifferentialChangeType::TYPE_COPY_HERE) {
$meta = 'Copied from '.phutil_escape_html($changeset->getOldFile());
} else {
$meta = null;
}
$line_count = $changeset->getAffectedLineCount();
if ($line_count == 0) {
$lines = null;
} else {
$lines = ' '.pht('(%d line(s))', $line_count);
}
$char = DifferentialChangeType::getSummaryCharacterForChangeType($type);
$chartitle = DifferentialChangeType::getFullNameForChangeType($type);
$desc = DifferentialChangeType::getShortNameForFileType($ftype);
if ($desc) {
$desc = '('.$desc.')';
}
$pchar =
($changeset->getOldProperties() === $changeset->getNewProperties())
? null
: '<span title="Properties Changed">M</span>';
$fname = $changeset->getFilename();
$cov = $this->renderCoverage($coverage, $fname);
if ($cov === null) {
$mcov = $cov = '<em>-</em>';
} else {
$mcov = phutil_render_tag(
'div',
array(
'id' => 'differential-mcoverage-'.md5($fname),
'class' => 'differential-mcoverage-loading',
),
(isset($this->visibleChangesets[$id]) ? 'Loading...' : '?'));
}
$rows[] =
'<tr>'.
phutil_render_tag(
'td',
array(
'class' => 'differential-toc-char',
'title' => $chartitle,
),
$char).
'<td class="differential-toc-prop">'.$pchar.'</td>'.
'<td class="differential-toc-ftype">'.$desc.'</td>'.
'<td class="differential-toc-file">'.$link.$lines.'</td>'.
'<td class="differential-toc-cov">'.$cov.'</td>'.
'<td class="differential-toc-mcov">'.$mcov.'</td>'.
'</tr>';
if ($meta) {
$rows[] =
'<tr>'.
'<td colspan="3"></td>'.
'<td class="differential-toc-meta">'.$meta.'</td>'.
'</tr>';
}
if ($this->diff && $this->repository) {
$paths[] =
$changeset->getAbsoluteRepositoryPath($this->repository, $this->diff);
}
}
$editor_link = null;
if ($paths && $this->user) {
$editor_link = $this->user->loadEditorLink(
implode(' ', $paths),
1, // line number
$this->repository->getCallsign());
if ($editor_link) {
$editor_link = phutil_render_tag(
'a',
array(
'href' => $editor_link,
'class' => 'button differential-toc-edit-all',
),
'Open All in Editor');
}
}
$reveal_link = javelin_render_tag(
'a',
array(
'sigil' => 'differential-reveal-all',
'mustcapture' => true,
'class' => 'button differential-toc-reveal-all',
),
'Show All Context'
);
return
id(new PhabricatorAnchorView())
->setAnchorName('toc')
->setNavigationMarker(true)
->render().
'<div class="differential-toc differential-panel">'.
$editor_link.
$reveal_link.
'<h1>Table of Contents</h1>'.
'<table>'.
'<tr>'.
'<th></th>'.
'<th></th>'.
'<th></th>'.
'<th>Path</th>'.
'<th class="differential-toc-cov">Coverage (All)</th>'.
'<th class="differential-toc-mcov">Coverage (Touched)</th>'.
'</tr>'.
implode("\n", $rows).
'</table>'.
'</div>';
}
private function renderCoverage(array $coverage, $file) {
$info = idx($coverage, $file);
if (!$info) {
return null;
}
$not_covered = substr_count($info, 'U');
$covered = substr_count($info, 'C');
if (!$not_covered && !$covered) {
return null;
}
return sprintf('%d%%', 100 * ($covered / ($covered + $not_covered)));
}
private function renderChangesetLink(DifferentialChangeset $changeset, $ref) {
$display_file = $changeset->getDisplayFilename();
return javelin_render_tag(
'a',
array(
'href' => '#'.$changeset->getAnchorName(),
'meta' => array(
'id' => 'diff-'.$changeset->getAnchorName(),
'ref' => $ref,
),
'sigil' => 'differential-load',
),
phutil_escape_html($display_file));
}
}
diff --git a/src/applications/differential/view/DifferentialInlineCommentEditView.php b/src/applications/differential/view/DifferentialInlineCommentEditView.php
index a89156eecb..fa48ba3cc7 100644
--- a/src/applications/differential/view/DifferentialInlineCommentEditView.php
+++ b/src/applications/differential/view/DifferentialInlineCommentEditView.php
@@ -1,156 +1,140 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialInlineCommentEditView extends AphrontView {
private $user;
private $inputs = array();
private $uri;
private $title;
private $onRight;
private $number;
private $length;
public function addHiddenInput($key, $value) {
$this->inputs[] = array($key, $value);
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setSubmitURI($uri) {
$this->uri = $uri;
return $this;
}
public function setTitle($title) {
$this->title = $title;
return $this;
}
public function setOnRight($on_right) {
$this->onRight = $on_right;
$this->addHiddenInput('on_right', $on_right);
return $this;
}
public function setNumber($number) {
$this->number = $number;
return $this;
}
public function setLength($length) {
$this->length = $length;
return $this;
}
public function render() {
if (!$this->uri) {
throw new Exception("Call setSubmitURI() before render()!");
}
if (!$this->user) {
throw new Exception("Call setUser() before render()!");
}
$content = phabricator_render_form(
$this->user,
array(
'action' => $this->uri,
'method' => 'POST',
'sigil' => 'inline-edit-form',
),
$this->renderInputs().
$this->renderBody());
if ($this->onRight) {
$core = '<th></th><td></td><th></th><td colspan="2">'.$content.'</td>';
} else {
$core = '<th></th><td>'.$content.'</td><th></th><td colspan="2"></td>';
}
return '<table><tr class="inline-comment-splint">'.$core.'</tr></table>';
}
private function renderInputs() {
$out = array();
foreach ($this->inputs as $input) {
list($name, $value) = $input;
$out[] = phutil_render_tag(
'input',
array(
'type' => 'hidden',
'name' => $name,
'value' => $value,
),
null);
}
return implode('', $out);
}
private function renderBody() {
$buttons = array();
$buttons[] = '<button>Ready</button>';
$buttons[] = javelin_render_tag(
'button',
array(
'sigil' => 'inline-edit-cancel',
'class' => 'grey',
),
'Cancel');
$buttons = implode('', $buttons);
$formatting = phutil_render_tag(
'a',
array(
'href' => PhabricatorEnv::getDoclink(
'article/Remarkup_Reference.html'),
'tabindex' => '-1',
'target' => '_blank',
),
'Formatting Reference');
return javelin_render_tag(
'div',
array(
'class' => 'differential-inline-comment-edit',
'sigil' => 'differential-inline-comment',
'meta' => array(
'on_right' => $this->onRight,
'number' => $this->number,
'length' => $this->length,
),
),
'<div class="differential-inline-comment-edit-title">'.
phutil_escape_html($this->title).
'</div>'.
'<div class="differential-inline-comment-edit-body">'.
$this->renderChildren().
'</div>'.
'<div class="differential-inline-comment-edit-buttons">'.
$formatting.
$buttons.
'<div style="clear: both;"></div>'.
'</div>');
}
}
diff --git a/src/applications/differential/view/DifferentialInlineCommentView.php b/src/applications/differential/view/DifferentialInlineCommentView.php
index 2ba429842f..26b123a40e 100644
--- a/src/applications/differential/view/DifferentialInlineCommentView.php
+++ b/src/applications/differential/view/DifferentialInlineCommentView.php
@@ -1,277 +1,261 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialInlineCommentView extends AphrontView {
private $inlineComment;
private $onRight;
private $buildScaffolding;
private $handles;
private $markupEngine;
private $editable;
private $preview;
private $allowReply;
public function setInlineComment(PhabricatorInlineCommentInterface $comment) {
$this->inlineComment = $comment;
return $this;
}
public function setOnRight($on_right) {
$this->onRight = $on_right;
return $this;
}
public function setBuildScaffolding($scaffold) {
$this->buildScaffolding = $scaffold;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setMarkupEngine(PhabricatorMarkupEngine $engine) {
$this->markupEngine = $engine;
return $this;
}
public function setEditable($editable) {
$this->editable = $editable;
return $this;
}
public function setPreview($preview) {
$this->preview = $preview;
return $this;
}
public function setAllowReply($allow_reply) {
$this->allowReply = $allow_reply;
return $this;
}
public function render() {
$inline = $this->inlineComment;
$start = $inline->getLineNumber();
$length = $inline->getLineLength();
if ($length) {
$end = $start + $length;
$line = 'Lines '.number_format($start).'-'.number_format($end);
} else {
$line = 'Line '.number_format($start);
}
$metadata = array(
'id' => $inline->getID(),
'number' => $inline->getLineNumber(),
'length' => $inline->getLineLength(),
'on_right' => $this->onRight,
'original' => $inline->getContent(),
);
$sigil = 'differential-inline-comment';
if ($this->preview) {
$sigil = $sigil . ' differential-inline-comment-preview';
}
$content = $inline->getContent();
$handles = $this->handles;
$links = array();
$is_synthetic = false;
if ($inline->getSyntheticAuthor()) {
$is_synthetic = true;
}
$is_draft = false;
if ($inline->isDraft() && !$is_synthetic) {
$links[] = 'Not Submitted Yet';
$is_draft = true;
}
if (!$this->preview) {
$links[] = javelin_render_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'sigil' => 'differential-inline-prev',
),
'Previous');
$links[] = javelin_render_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'sigil' => 'differential-inline-next',
),
'Next');
if ($this->allowReply) {
if (!$is_synthetic) {
// NOTE: No product reason why you can't reply to these, but the reply
// mechanism currently sends the inline comment ID to the server, not
// file/line information, and synthetic comments don't have an inline
// comment ID.
$links[] = javelin_render_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'sigil' => 'differential-inline-reply',
),
'Reply');
}
}
}
$anchor_name = 'inline-'.$inline->getID();
if ($this->editable && !$this->preview) {
$links[] = javelin_render_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'sigil' => 'differential-inline-edit',
),
'Edit');
$links[] = javelin_render_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'sigil' => 'differential-inline-delete',
),
'Delete');
} else if ($this->preview) {
$links[] = javelin_render_tag(
'a',
array(
'meta' => array(
'anchor' => $anchor_name,
),
'sigil' => 'differential-inline-preview-jump',
),
'Not Visible');
$links[] = javelin_render_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'sigil' => 'differential-inline-delete',
),
'Delete');
}
if ($links) {
$links =
'<span class="differential-inline-comment-links">'.
implode(' &middot; ', $links).
'</span>';
} else {
$links = null;
}
$content = $this->markupEngine->getOutput(
$inline,
PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY);
if ($this->preview) {
$anchor = null;
} else {
$anchor = phutil_render_tag(
'a',
array(
'name' => $anchor_name,
'id' => $anchor_name,
'class' => 'differential-inline-comment-anchor',
),
'');
}
$classes = array(
'differential-inline-comment',
);
if ($is_draft) {
$classes[] = 'differential-inline-comment-unsaved-draft';
}
if ($is_synthetic) {
$classes[] = 'differential-inline-comment-synthetic';
}
$classes = implode(' ', $classes);
if ($is_synthetic) {
$author = $inline->getSyntheticAuthor();
} else {
$author = $handles[$inline->getAuthorPHID()]->getName();
}
$markup = javelin_render_tag(
'div',
array(
'class' => $classes,
'sigil' => $sigil,
'meta' => $metadata,
),
'<div class="differential-inline-comment-head">'.
$anchor.
$links.
' <span class="differential-inline-comment-line">'.$line.'</span> '.
phutil_escape_html($author).
'</div>'.
'<div class="differential-inline-comment-content">'.
'<div class="phabricator-remarkup">'.
$content.
'</div>'.
'</div>');
return $this->scaffoldMarkup($markup);
}
private function scaffoldMarkup($markup) {
if (!$this->buildScaffolding) {
return $markup;
}
$left_markup = !$this->onRight ? $markup : '';
$right_markup = $this->onRight ? $markup : '';
return
'<table>'.
'<tr class="inline">'.
'<th></th>'.
'<td>'.$left_markup.'</td>'.
'<th></th>'.
'<td colspan="2">'.$right_markup.'</td>'.
'</tr>'.
'</table>';
}
}
diff --git a/src/applications/differential/view/DifferentialLocalCommitsView.php b/src/applications/differential/view/DifferentialLocalCommitsView.php
index 2b774e22d2..d88512ff7a 100644
--- a/src/applications/differential/view/DifferentialLocalCommitsView.php
+++ b/src/applications/differential/view/DifferentialLocalCommitsView.php
@@ -1,153 +1,137 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialLocalCommitsView extends AphrontView {
private $localCommits;
private $user;
public function setLocalCommits($local_commits) {
$this->localCommits = $local_commits;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function render() {
$user = $this->user;
if (!$user) {
throw new Exception("Call setUser() before render()-ing this view.");
}
$local = $this->localCommits;
if (!$local) {
return null;
}
require_celerity_resource('differential-local-commits-view-css');
$has_tree = false;
$has_local = false;
foreach ($local as $commit) {
if (idx($commit, 'tree')) {
$has_tree = true;
}
if (idx($commit, 'local')) {
$has_local = true;
}
}
$rows = array();
foreach ($local as $commit) {
$row = array();
if (idx($commit, 'commit')) {
$commit_hash = substr($commit['commit'], 0, 16);
} else if (isset($commit['rev'])) {
$commit_hash = substr($commit['rev'], 0, 16);
} else {
$commit_hash = null;
}
$row[] = '<td>'.phutil_escape_html($commit_hash).'</td>';
if ($has_tree) {
$tree = idx($commit, 'tree');
$tree = substr($tree, 0, 16);
$row[] = '<td>'.phutil_escape_html($tree).'</td>';
}
if ($has_local) {
$local_rev = idx($commit, 'local', null);
$row[] = '<td>'.phutil_escape_html($local_rev).'</td>';
}
$parents = idx($commit, 'parents', array());
foreach ($parents as $k => $parent) {
if (is_array($parent)) {
$parent = idx($parent, 'rev');
}
$parents[$k] = phutil_escape_html(substr($parent, 0, 16));
}
$parents = implode('<br />', $parents);
$row[] = '<td>'.$parents.'</td>';
$author = nonempty(
idx($commit, 'user'),
idx($commit, 'author'));
$row[] = '<td>'.phutil_escape_html($author).'</td>';
$message = idx($commit, 'message');
$summary = idx($commit, 'summary');
$summary = phutil_utf8_shorten($summary, 80);
$view = new AphrontMoreView();
$view->setSome(phutil_escape_html($summary));
if ($message && (trim($summary) != trim($message))) {
$view->setMore(nl2br(phutil_escape_html($message)));
}
$row[] = phutil_render_tag(
'td',
array(
'class' => 'summary',
),
$view->render());
$date = nonempty(
idx($commit, 'date'),
idx($commit, 'time'));
if ($date) {
$date = phabricator_datetime($date, $user);
}
$row[] = '<td>'.$date.'</td>';
$rows[] = '<tr>'.implode('', $row).'</tr>';
}
$headers = array();
$headers[] = '<th>Commit</th>';
if ($has_tree) {
$headers[] = '<th>Tree</th>';
}
if ($has_local) {
$headers[] = '<th>Local</th>';
}
$headers[] = '<th>Parents</th>';
$headers[] = '<th>Author</th>';
$headers[] = '<th>Summary</th>';
$headers[] = '<th>Date</th>';
$headers = '<tr>'.implode('', $headers).'</tr>';
return
'<div class="differential-panel">'.
'<h1>Local Commits</h1>'.
'<table class="differential-local-commits-table">'.
$headers.
implode("\n", $rows).
'</table>'.
'</div>';
}
}
diff --git a/src/applications/differential/view/DifferentialPrimaryPaneView.php b/src/applications/differential/view/DifferentialPrimaryPaneView.php
index 061fede6dc..a8cd6cdc41 100644
--- a/src/applications/differential/view/DifferentialPrimaryPaneView.php
+++ b/src/applications/differential/view/DifferentialPrimaryPaneView.php
@@ -1,55 +1,39 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialPrimaryPaneView
extends DifferentialCodeWidthSensitiveView {
private $id;
public function setID($id) {
$this->id = $id;
return $this;
}
public function render() {
// This is chosen somewhat arbitrarily so the math works out correctly
// for 80 columns and sets it to the preexisting width (1162px). It may
// need some tweaking, but when lineWidth = 80, the computed pixel width
// should be 1162px or something along those lines.
// Override the 'td' width rule with a more specific, inline style tag.
// TODO: move this to <head> somehow.
$td_width = ceil((88 / 80) * $this->getLineWidth());
$style_tag = phutil_render_tag(
'style',
array(
'type' => 'text/css',
),
".differential-diff td { width: {$td_width}ex; }");
return phutil_render_tag(
'div',
array(
'class' => 'differential-primary-pane',
'id' => $this->id,
),
$style_tag.$this->renderChildren());
}
}
diff --git a/src/applications/differential/view/DifferentialResultsTableView.php b/src/applications/differential/view/DifferentialResultsTableView.php
index 7b43473c0b..46c8595968 100644
--- a/src/applications/differential/view/DifferentialResultsTableView.php
+++ b/src/applications/differential/view/DifferentialResultsTableView.php
@@ -1,131 +1,115 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialResultsTableView extends AphrontView {
private $rows;
private $showMoreString;
public function setRows(array $rows) {
$this->rows = $rows;
return $this;
}
public function setShowMoreString($show_more_string) {
$this->showMoreString = $show_more_string;
return $this;
}
public function render() {
$rows = array();
$any_hidden = false;
foreach ($this->rows as $row) {
$style = idx($row, 'style');
switch ($style) {
case 'section':
$cells = phutil_render_tag(
'th',
array(
'colspan' => 2,
),
idx($row, 'name'));
break;
default:
$name = phutil_render_tag(
'th',
array(
),
idx($row, 'name'));
$value = phutil_render_tag(
'td',
array(
),
idx($row, 'value'));
$cells = $name.$value;
break;
}
$show = idx($row, 'show');
$rows[] = javelin_render_tag(
'tr',
array(
'style' => $show ? null : 'display: none',
'sigil' => $show ? null : 'differential-results-row-toggle',
'class' => 'differential-results-row-'.$style,
),
$cells);
if (!$show) {
$any_hidden = true;
}
}
if ($any_hidden) {
$show_more = javelin_render_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
),
$this->showMoreString);
$hide_more = javelin_render_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
),
'Hide');
$rows[] = javelin_render_tag(
'tr',
array(
'class' => 'differential-results-row-show',
'sigil' => 'differential-results-row-show',
),
'<th colspan="2">'.$show_more.'</td>');
$rows[] = javelin_render_tag(
'tr',
array(
'class' => 'differential-results-row-show',
'sigil' => 'differential-results-row-hide',
'style' => 'display: none',
),
'<th colspan="2">'.$hide_more.'</th>');
Javelin::initBehavior('differential-show-field-details');
}
require_celerity_resource('differential-results-table-css');
return javelin_render_tag(
'table',
array(
'class' => 'differential-results-table',
'sigil' => 'differential-results-table',
),
implode("\n", $rows));
}
}
diff --git a/src/applications/differential/view/DifferentialRevisionCommentListView.php b/src/applications/differential/view/DifferentialRevisionCommentListView.php
index 0974eca7d1..71175718fd 100644
--- a/src/applications/differential/view/DifferentialRevisionCommentListView.php
+++ b/src/applications/differential/view/DifferentialRevisionCommentListView.php
@@ -1,214 +1,198 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialRevisionCommentListView extends AphrontView {
private $comments;
private $handles;
private $inlines;
private $changesets;
private $user;
private $target;
private $versusDiffID;
private $id;
public function setComments(array $comments) {
assert_instances_of($comments, 'DifferentialComment');
$this->comments = $comments;
return $this;
}
public function setInlineComments(array $inline_comments) {
assert_instances_of($inline_comments, 'PhabricatorInlineCommentInterface');
$this->inlines = $inline_comments;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setChangesets(array $changesets) {
assert_instances_of($changesets, 'DifferentialChangeset');
$this->changesets = $changesets;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setTargetDiff(DifferentialDiff $target) {
$this->target = $target;
return $this;
}
public function setVersusDiffID($diff_vs) {
$this->versusDiffID = $diff_vs;
return $this;
}
public function getID() {
if (!$this->id) {
$this->id = celerity_generate_unique_node_id();
}
return $this->id;
}
public function render() {
require_celerity_resource('differential-revision-comment-list-css');
$engine = new PhabricatorMarkupEngine();
$engine->setViewer($this->user);
foreach ($this->comments as $comment) {
$comment->giveFacebookSomeArbitraryDiff($this->target);
$engine->addObject(
$comment,
DifferentialComment::MARKUP_FIELD_BODY);
}
foreach ($this->inlines as $inline) {
$engine->addObject(
$inline,
PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY);
}
$engine->process();
$inlines = mgroup($this->inlines, 'getCommentID');
$num = 1;
$html = array();
foreach ($this->comments as $comment) {
$view = new DifferentialRevisionCommentView();
$view->setComment($comment);
$view->setUser($this->user);
$view->setHandles($this->handles);
$view->setMarkupEngine($engine);
$view->setInlineComments(idx($inlines, $comment->getID(), array()));
$view->setChangesets($this->changesets);
$view->setTargetDiff($this->target);
$view->setVersusDiffID($this->versusDiffID);
if ($comment->getAction() == DifferentialAction::ACTION_SUMMARIZE) {
$view->setAnchorName('summary');
} else if ($comment->getAction() == DifferentialAction::ACTION_TESTPLAN) {
$view->setAnchorName('test-plan');
} else {
$view->setAnchorName('comment-'.$num);
$num++;
}
$html[] = $view->render();
}
$objs = array_reverse(array_values($this->comments));
$html = array_reverse(array_values($html));
$user = $this->user;
$last_comment = null;
// Find the most recent comment by the viewer.
foreach ($objs as $position => $comment) {
if ($user && ($comment->getAuthorPHID() == $user->getPHID())) {
if ($last_comment === null) {
$last_comment = $position;
} else if ($last_comment == $position - 1) {
// If the viewer made several comments in a row, show them all. This
// is a spaz rule for epriestley.
$last_comment = $position;
}
}
}
$header = array();
$hidden = array();
if ($last_comment !== null) {
foreach ($objs as $position => $comment) {
if (!$comment->getID()) {
// These are synthetic comments with summary/test plan information.
$header[] = $html[$position];
unset($html[$position]);
continue;
}
if ($position <= $last_comment) {
// Always show comments after the viewer's last comment.
continue;
}
if ($position < 3) {
// Always show the 3 most recent comments.
continue;
}
$hidden[] = $position;
}
}
if (count($hidden) <= 3) {
// Don't hide if there's not much to hide.
$hidden = array();
}
$header = array_reverse($header);
$hidden = array_select_keys($html, $hidden);
$visible = array_diff_key($html, $hidden);
$hidden = array_reverse($hidden);
$visible = array_reverse($visible);
if ($hidden) {
Javelin::initBehavior(
'differential-show-all-comments',
array(
'markup' => implode("\n", $hidden),
));
$hidden = javelin_render_tag(
'div',
array(
'sigil' => "differential-all-comments-container",
),
'<div class="differential-older-comments-are-hidden">'.
number_format(count($hidden)).' older comments are hidden. '.
javelin_render_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'sigil' => 'differential-show-all-comments',
),
'Show all comments.').
'</div>');
} else {
$hidden = null;
}
return javelin_render_tag(
'div',
array(
'class' => 'differential-comment-list',
'id' => $this->getID(),
),
implode("\n", $header).
$hidden.
implode("\n", $visible));
}
}
diff --git a/src/applications/differential/view/DifferentialRevisionCommentView.php b/src/applications/differential/view/DifferentialRevisionCommentView.php
index 2ba1fb39e2..7c10f59fe9 100644
--- a/src/applications/differential/view/DifferentialRevisionCommentView.php
+++ b/src/applications/differential/view/DifferentialRevisionCommentView.php
@@ -1,309 +1,293 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialRevisionCommentView extends AphrontView {
private $comment;
private $handles;
private $markupEngine;
private $preview;
private $inlines;
private $changesets;
private $target;
private $anchorName;
private $user;
private $versusDiffID;
public function setComment($comment) {
$this->comment = $comment;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setMarkupEngine(PhabricatorMarkupEngine $markup_engine) {
$this->markupEngine = $markup_engine;
return $this;
}
public function setPreview($preview) {
$this->preview = $preview;
return $this;
}
public function setInlineComments(array $inline_comments) {
assert_instances_of($inline_comments, 'PhabricatorInlineCommentInterface');
$this->inlines = $inline_comments;
return $this;
}
public function setChangesets(array $changesets) {
assert_instances_of($changesets, 'DifferentialChangeset');
// Ship these in sorted by getSortKey() and keyed by ID... or else!
$this->changesets = $changesets;
return $this;
}
public function setTargetDiff($target) {
$this->target = $target;
return $this;
}
public function setVersusDiffID($diff_vs) {
$this->versusDiffID = $diff_vs;
return $this;
}
public function setAnchorName($anchor_name) {
$this->anchorName = $anchor_name;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function render() {
if (!$this->user) {
throw new Exception("Call setUser() before rendering!");
}
require_celerity_resource('phabricator-remarkup-css');
require_celerity_resource('differential-revision-comment-css');
$comment = $this->comment;
$action = $comment->getAction();
$action_class = 'differential-comment-action-'.$action;
$info = array();
$content = $comment->getContent();
$hide_comments = true;
if (strlen(rtrim($content))) {
$hide_comments = false;
$content = $this->markupEngine->getOutput(
$comment,
PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY);
$content =
'<div class="phabricator-remarkup">'.
$content.
'</div>';
}
$inline_render = $this->renderInlineComments();
if ($inline_render) {
$hide_comments = false;
}
$author = $this->handles[$comment->getAuthorPHID()];
$author_link = $author->renderLink();
$metadata = $comment->getMetadata();
$added_reviewers = idx(
$metadata,
DifferentialComment::METADATA_ADDED_REVIEWERS,
array());
$removed_reviewers = idx(
$metadata,
DifferentialComment::METADATA_REMOVED_REVIEWERS,
array());
$added_ccs = idx(
$metadata,
DifferentialComment::METADATA_ADDED_CCS,
array());
$verb = DifferentialAction::getActionPastTenseVerb($comment->getAction());
$verb = phutil_escape_html($verb);
$actions = array();
switch ($comment->getAction()) {
case DifferentialAction::ACTION_ADDCCS:
$actions[] = "{$author_link} added CCs: ".
$this->renderHandleList($added_ccs).".";
$added_ccs = null;
break;
case DifferentialAction::ACTION_ADDREVIEWERS:
$actions[] = "{$author_link} added reviewers: ".
$this->renderHandleList($added_reviewers).".";
$added_reviewers = null;
break;
case DifferentialAction::ACTION_UPDATE:
$diff_id = idx($metadata, DifferentialComment::METADATA_DIFF_ID);
if ($diff_id) {
$diff_link = phutil_render_tag(
'a',
array(
'href' => '/D'.$comment->getRevisionID().'?id='.$diff_id,
),
'Diff #'.phutil_escape_html($diff_id));
$actions[] = "{$author_link} updated this revision to {$diff_link}.";
} else {
$actions[] = "{$author_link} {$verb} this revision.";
}
break;
default:
$actions[] = "{$author_link} {$verb} this revision.";
break;
}
if ($added_reviewers) {
$actions[] = "{$author_link} added reviewers: ".
$this->renderHandleList($added_reviewers).".";
}
if ($removed_reviewers) {
$actions[] = "{$author_link} removed reviewers: ".
$this->renderHandleList($removed_reviewers).".";
}
if ($added_ccs) {
$actions[] = "{$author_link} added CCs: ".
$this->renderHandleList($added_ccs).".";
}
foreach ($actions as $key => $action) {
$actions[$key] = '<div>'.$action.'</div>';
}
$xaction_view = id(new PhabricatorTransactionView())
->setUser($this->user)
->setImageURI($author->getImageURI())
->setContentSource($comment->getContentSource())
->addClass($action_class)
->setActions($actions);
if ($this->preview) {
$xaction_view->setIsPreview($this->preview);
} else {
$xaction_view->setEpoch($comment->getDateCreated());
if ($this->anchorName) {
$anchor_name = $this->anchorName;
$anchor_text = 'D'.$comment->getRevisionID().'#'.$anchor_name;
$xaction_view->setAnchor($anchor_name, $anchor_text);
}
}
if (!$hide_comments) {
$xaction_view->appendChild(
'<div class="differential-comment-core">'.
$content.
'</div>'.
$this->renderSingleView($inline_render));
}
return $xaction_view->render();
}
private function renderHandleList(array $phids) {
$result = array();
foreach ($phids as $phid) {
$result[] = $this->handles[$phid]->renderLink();
}
return implode(', ', $result);
}
private function renderInlineComments() {
if (!$this->inlines) {
return null;
}
$inlines = $this->inlines;
$changesets = $this->changesets;
$inlines_by_changeset = mgroup($inlines, 'getChangesetID');
$inlines_by_changeset = array_select_keys(
$inlines_by_changeset,
array_keys($this->changesets));
$view = new PhabricatorInlineSummaryView();
foreach ($inlines_by_changeset as $changeset_id => $inlines) {
$changeset = $changesets[$changeset_id];
$items = array();
foreach ($inlines as $inline) {
$on_target = ($this->target) &&
($this->target->getID() == $changeset->getDiffID());
$is_visible = false;
if ($inline->getIsNewFile()) {
// This comment is on the right side of the versus diff, and visible
// on the left side of the page.
if ($this->versusDiffID) {
if ($changeset->getDiffID() == $this->versusDiffID) {
$is_visible = true;
}
}
// This comment is on the right side of the target diff, and visible
// on the right side of the page.
if ($on_target) {
$is_visible = true;
}
} else {
// Ths comment is on the left side of the target diff, and visible
// on the left side of the page.
if (!$this->versusDiffID) {
if ($on_target) {
$is_visible = true;
}
}
// TODO: We still get one edge case wrong here, when we have a
// versus diff and the file didn't exist in the old version. The
// comment is visible because we show the left side of the target
// diff when there's no corresponding file in the versus diff, but
// we incorrectly link it off-page.
}
$item = array(
'id' => $inline->getID(),
'line' => $inline->getLineNumber(),
'length' => $inline->getLineLength(),
'content' => $this->markupEngine->getOutput(
$inline,
DifferentialInlineComment::MARKUP_FIELD_BODY),
);
if (!$is_visible) {
$diff_id = $changeset->getDiffID();
$item['where'] = '(On Diff #'.$diff_id.')';
$item['href'] =
'D'.$this->comment->getRevisionID().
'?id='.$diff_id.
'#inline-'.$inline->getID();
}
$items[] = $item;
}
$view->addCommentGroup($changeset->getFilename(), $items);
}
return $view;
}
}
diff --git a/src/applications/differential/view/DifferentialRevisionDetailView.php b/src/applications/differential/view/DifferentialRevisionDetailView.php
index 6b9d4cd366..886a3b8c34 100644
--- a/src/applications/differential/view/DifferentialRevisionDetailView.php
+++ b/src/applications/differential/view/DifferentialRevisionDetailView.php
@@ -1,87 +1,71 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialRevisionDetailView extends AphrontView {
private $revision;
private $actions;
private $user;
private $auxiliaryFields = array();
public function setRevision($revision) {
$this->revision = $revision;
return $this;
}
public function setActions(array $actions) {
$this->actions = $actions;
return $this;
}
public function setUser($user) {
$this->user = $user;
return $this;
}
public function setAuxiliaryFields(array $fields) {
assert_instances_of($fields, 'DifferentialFieldSpecification');
$this->auxiliaryFields = $fields;
return $this;
}
public function render() {
require_celerity_resource('differential-core-view-css');
$revision = $this->revision;
$dict = array();
foreach ($this->auxiliaryFields as $field) {
$value = $field->renderValueForRevisionView();
if (strlen($value)) {
$label = rtrim($field->renderLabelForRevisionView(), ':');
$dict[$label] = $value;
}
}
$actions = array();
foreach ($this->actions as $action) {
$obj = new AphrontHeadsupActionView();
$obj->setName($action['name']);
$obj->setURI(idx($action, 'href'));
$obj->setWorkflow(idx($action, 'sigil') == 'workflow');
$obj->setClass(idx($action, 'class'));
$obj->setInstant(idx($action, 'instant'));
$obj->setUser($this->user);
$actions[] = $obj;
}
$action_list = new AphrontHeadsupActionListView();
$action_list->setActions($actions);
$action_panel = new AphrontHeadsupView();
$action_panel->setActionList($action_list);
$action_panel->setHasKeyboardShortcuts(true);
$action_panel->setProperties($dict);
$action_panel->setObjectName('D'.$revision->getID());
$action_panel->setHeader($revision->getTitle());
return $action_panel->render();
}
}
diff --git a/src/applications/differential/view/DifferentialRevisionListView.php b/src/applications/differential/view/DifferentialRevisionListView.php
index 1e8335683c..30f9de2aef 100644
--- a/src/applications/differential/view/DifferentialRevisionListView.php
+++ b/src/applications/differential/view/DifferentialRevisionListView.php
@@ -1,228 +1,212 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Render a table of Differential revisions.
*/
final class DifferentialRevisionListView extends AphrontView {
private $revisions;
private $flags = array();
private $drafts = array();
private $handles;
private $user;
private $fields;
private $highlightAge;
const NO_DATA_STRING = 'No revisions found.';
public function setFields(array $fields) {
assert_instances_of($fields, 'DifferentialFieldSpecification');
$this->fields = $fields;
return $this;
}
public function setRevisions(array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$this->revisions = $revisions;
return $this;
}
public function setHighlightAge($bool) {
$this->highlightAge = $bool;
return $this;
}
public function getRequiredHandlePHIDs() {
$phids = array();
foreach ($this->fields as $field) {
foreach ($this->revisions as $revision) {
$phids[] = $field->getRequiredHandlePHIDsForRevisionList($revision);
}
}
return array_mergev($phids);
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function loadAssets() {
$user = $this->user;
if (!$user) {
throw new Exception("Call setUser() before loadAssets()!");
}
if ($this->revisions === null) {
throw new Exception("Call setRevisions() before loadAssets()!");
}
$this->flags = id(new PhabricatorFlagQuery())
->withOwnerPHIDs(array($user->getPHID()))
->withObjectPHIDs(mpull($this->revisions, 'getPHID'))
->execute();
$this->drafts = id(new DifferentialRevisionQuery())
->withIDs(mpull($this->revisions, 'getID'))
->withDraftRepliesByAuthors(array($user->getPHID()))
->execute();
return $this;
}
public function render() {
$user = $this->user;
if (!$user) {
throw new Exception("Call setUser() before render()!");
}
$fresh = null;
$stale = null;
if ($this->highlightAge) {
$fresh = PhabricatorEnv::getEnvConfig('differential.days-fresh');
if ($fresh) {
$fresh = PhabricatorCalendarHoliday::getNthBusinessDay(
time(),
-$fresh);
}
$stale = PhabricatorEnv::getEnvConfig('differential.days-stale');
if ($stale) {
$stale = PhabricatorCalendarHoliday::getNthBusinessDay(
time(),
-$stale);
}
}
Javelin::initBehavior('phabricator-tooltips', array());
require_celerity_resource('aphront-tooltip-css');
$flagged = mpull($this->flags, null, 'getObjectPHID');
foreach ($this->fields as $field) {
$field->setUser($this->user);
$field->setHandles($this->handles);
}
$cell_classes = array();
$rows = array();
foreach ($this->revisions as $revision) {
$phid = $revision->getPHID();
$flag = '';
if (isset($flagged[$phid])) {
$class = PhabricatorFlagColor::getCSSClass($flagged[$phid]->getColor());
$note = $flagged[$phid]->getNote();
$flag = javelin_render_tag(
'div',
$note ? array(
'class' => 'phabricator-flag-icon '.$class,
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $note,
'align' => 'N',
'size' => 240,
),
) : array(
'class' => 'phabricator-flag-icon '.$class,
),
'');
} else if (array_key_exists($revision->getID(), $this->drafts)) {
$src = '/rsrc/image/icon/fatcow/page_white_edit.png';
$flag =
'<a href="/D'.$revision->getID().'#comment-preview">'.
phutil_render_tag(
'img',
array(
'src' => celerity_get_resource_uri($src),
'width' => 16,
'height' => 16,
'alt' => 'Draft',
'title' => 'Draft Comment',
)).
'</a>';
}
$row = array($flag);
$modified = $revision->getDateModified();
foreach ($this->fields as $field) {
if (($fresh || $stale) &&
$field instanceof DifferentialDateModifiedFieldSpecification) {
if ($stale && $modified < $stale) {
$class = 'revision-age-old';
} else if ($fresh && $modified < $fresh) {
$class = 'revision-age-stale';
} else {
$class = 'revision-age-fresh';
}
$cell_classes[count($rows)][count($row)] = $class;
}
$row[] = $field->renderValueForRevisionList($revision);
}
$rows[] = $row;
}
$headers = array('');
$classes = array('');
foreach ($this->fields as $field) {
$headers[] = $field->renderHeaderForRevisionList();
$classes[] = $field->getColumnClassForRevisionList();
}
$table = new AphrontTableView($rows);
$table->setHeaders($headers);
$table->setColumnClasses($classes);
$table->setCellClasses($cell_classes);
$table->setNoDataString(DifferentialRevisionListView::NO_DATA_STRING);
require_celerity_resource('differential-revision-history-css');
return $table->render();
}
public static function getDefaultFields() {
$selector = DifferentialFieldSelector::newSelector();
$fields = $selector->getFieldSpecifications();
foreach ($fields as $key => $field) {
if (!$field->shouldAppearOnRevisionList()) {
unset($fields[$key]);
}
}
if (!$fields) {
throw new Exception(
"Phabricator configuration has no fields that appear on the list ".
"interface!");
}
return $selector->sortFieldsForRevisionList($fields);
}
}
diff --git a/src/applications/differential/view/DifferentialRevisionStatsView.php b/src/applications/differential/view/DifferentialRevisionStatsView.php
index a455a1aeae..b6676c9d4a 100644
--- a/src/applications/differential/view/DifferentialRevisionStatsView.php
+++ b/src/applications/differential/view/DifferentialRevisionStatsView.php
@@ -1,234 +1,218 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Render some distracting statistics on revisions
*/
final class DifferentialRevisionStatsView extends AphrontView {
private $comments;
private $revisions;
private $diffs;
private $user;
private $filter;
public function setRevisions(array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$this->revisions = $revisions;
return $this;
}
public function setComments(array $comments) {
assert_instances_of($comments, 'DifferentialComment');
$this->comments = $comments;
return $this;
}
public function setDiffs(array $diffs) {
assert_instances_of($diffs, 'DifferentialDiff');
$this->diffs = $diffs;
return $this;
}
public function setFilter($filter) {
$this->filter = $filter;
return $this;
}
public function setUser($user) {
$this->user = $user;
return $this;
}
public function render() {
$user = $this->user;
if (!$user) {
throw new Exception("Call setUser() before render()!");
}
$id_to_revision_map = array();
foreach ($this->revisions as $rev) {
$id_to_revision_map[$rev->getID()] = $rev;
}
$revisions_seen = array();
$dates = array();
$counts = array();
$lines = array();
$days_with_diffs = array();
$count_active = array();
$response_time = array();
$response_count = array();
$now = time();
$row_array = array();
foreach (array(
'1 week', '2 weeks', '3 weeks',
'1 month', '2 months', '3 months', '6 months', '9 months',
'1 year', '18 months',
'2 years', '3 years', '4 years', '5 years',
) as $age) {
$dates[$age] = strtotime($age . ' ago 23:59:59');
$counts[$age] = 0;
$lines[$age] = 0;
$count_active[$age] = 0;
$response_time[$age] = array();
}
$revision_diffs_map = mgroup($this->diffs, 'getRevisionID');
foreach ($revision_diffs_map as $revision_id => $diffs) {
$revision_diffs_map[$revision_id] = msort($diffs, 'getID');
}
foreach ($this->comments as $comment) {
$comment_date = $comment->getDateCreated();
$day = phabricator_date($comment_date, $user);
$old_daycount = idx($days_with_diffs, $day, 0);
$days_with_diffs[$day] = $old_daycount + 1;
$rev_id = $comment->getRevisionID();
if (idx($revisions_seen, $rev_id)) {
$revision_seen = true;
$rev = null;
} else {
$revision_seen = false;
$rev = $id_to_revision_map[$rev_id];
$revisions_seen[$rev_id] = true;
}
foreach ($dates as $age => $cutoff) {
if ($cutoff >= $comment_date) {
continue;
}
if (!$revision_seen) {
if ($rev) {
$lines[$age] += $rev->getLineCount();
}
$counts[$age]++;
if (!$old_daycount) {
$count_active[$age]++;
}
}
$diffs = $revision_diffs_map[$rev_id];
$target_diff = $this->findTargetDiff($diffs, $comment);
if ($target_diff) {
$response_time[$age][] =
$comment_date - $target_diff->getDateCreated();
}
}
}
$old_count = 0;
foreach (array_reverse($dates) as $age => $cutoff) {
$weeks = ceil(($now - $cutoff) / (60 * 60 * 24)) / 7;
if ($old_count == $counts[$age] && count($row_array) == 1) {
unset($dates[last_key($row_array)]);
$row_array = array();
}
$old_count = $counts[$age];
$row_array[$age] = array(
'Revisions per week' => number_format($counts[$age] / $weeks, 2),
'Lines per week' => number_format($lines[$age] / $weeks, 1),
'Active days per week' =>
number_format($count_active[$age] / $weeks, 1),
'Revisions' => number_format($counts[$age]),
'Lines' => number_format($lines[$age]),
'Lines per diff' => number_format($lines[$age] /
($counts[$age] + 0.0001)),
'Active days' => number_format($count_active[$age]),
);
switch ($this->filter) {
case DifferentialAction::ACTION_CLOSE:
case DifferentialAction::ACTION_UPDATE:
case DifferentialAction::ACTION_COMMENT:
break;
case DifferentialAction::ACTION_ACCEPT:
case DifferentialAction::ACTION_REJECT:
$count = count($response_time[$age]);
if ($count) {
rsort($response_time[$age]);
$median = $response_time[$age][round($count / 2) - 1];
$average = array_sum($response_time[$age]) / $count;
} else {
$median = 0;
$average = 0;
}
$row_array[$age]['Response hours (median|average)'] =
number_format($median / 3600, 1).
' | '.
number_format($average / 3600, 1);
break;
}
}
$rows = array();
$row_names = array_keys(head($row_array));
foreach ($row_names as $row_name) {
$rows[] = array($row_name);
}
foreach (array_keys($dates) as $age) {
$i = 0;
foreach ($row_names as $row_name) {
$rows[$i][] = idx(idx($row_array, $age), $row_name, '-');
++$i;
}
}
$table = new AphrontTableView($rows);
$table->setColumnClasses(
array(
'wide pri',
));
$table->setHeaders(
array_merge(
array(
'Metric',
),
array_keys($dates)));
return $table->render();
}
private function findTargetDiff(array $diffs,
DifferentialComment $comment) {
switch ($this->filter) {
case DifferentialAction::ACTION_CLOSE:
case DifferentialAction::ACTION_UPDATE:
case DifferentialAction::ACTION_COMMENT:
return null;
case DifferentialAction::ACTION_ACCEPT:
case DifferentialAction::ACTION_REJECT:
$result = head($diffs);
foreach ($diffs as $diff) {
if ($diff->getDateCreated() >= $comment->getDateCreated()) {
break;
}
$result = $diff;
}
return $result;
}
}
}
diff --git a/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php b/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php
index 462a33aaa2..ae96c1ad29 100644
--- a/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php
+++ b/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php
@@ -1,343 +1,327 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DifferentialRevisionUpdateHistoryView extends AphrontView {
private $diffs = array();
private $selectedVersusDiffID;
private $selectedDiffID;
private $selectedWhitespace;
private $user;
public function setDiffs(array $diffs) {
assert_instances_of($diffs, 'DifferentialDiff');
$this->diffs = $diffs;
return $this;
}
public function setSelectedVersusDiffID($id) {
$this->selectedVersusDiffID = $id;
return $this;
}
public function setSelectedDiffID($id) {
$this->selectedDiffID = $id;
return $this;
}
public function setSelectedWhitespace($whitespace) {
$this->selectedWhitespace = $whitespace;
return $this;
}
public function setUser($user) {
$this->user = $user;
return $this;
}
public function getUser() {
return $this->user;
}
public function render() {
require_celerity_resource('differential-core-view-css');
require_celerity_resource('differential-revision-history-css');
$data = array(
array(
'name' => 'Base',
'id' => null,
'desc' => 'Base',
'age' => null,
'obj' => null,
),
);
$seq = 0;
foreach ($this->diffs as $diff) {
$data[] = array(
'name' => 'Diff '.(++$seq),
'id' => $diff->getID(),
'desc' => $diff->getDescription(),
'age' => $diff->getDateCreated(),
'obj' => $diff,
);
}
$max_id = $diff->getID();
$idx = 0;
$rows = array();
$disable = false;
$radios = array();
$last_base = null;
foreach ($data as $row) {
$diff = $row['obj'];
$name = $row['name'];
$id = $row['id'];
$old_class = null;
$new_class = null;
if ($id) {
$new_checked = ($this->selectedDiffID == $id);
$new = javelin_render_tag(
'input',
array(
'type' => 'radio',
'name' => 'id',
'value' => $id,
'checked' => $new_checked ? 'checked' : null,
'sigil' => 'differential-new-radio',
));
if ($new_checked) {
$new_class = " revhistory-new-now";
$disable = true;
}
} else {
$new = null;
}
if ($max_id != $id) {
$uniq = celerity_generate_unique_node_id();
$old_checked = ($this->selectedVersusDiffID == $id);
$old = phutil_render_tag(
'input',
array(
'type' => 'radio',
'name' => 'vs',
'value' => $id,
'id' => $uniq,
'checked' => $old_checked ? 'checked' : null,
'disabled' => $disable ? 'disabled' : null,
));
$radios[] = $uniq;
if ($old_checked) {
$old_class = " revhistory-old-now";
}
} else {
$old = null;
}
$desc = $row['desc'];
if ($row['age']) {
$age = phabricator_datetime($row['age'], $this->getUser());
} else {
$age = null;
}
if (++$idx % 2) {
$class = ' class="alt"';
} else {
$class = null;
}
if ($diff) {
$lint = self::renderDiffLintStar($row['obj']);
$unit = self::renderDiffUnitStar($row['obj']);
$lint_message = self::getDiffLintMessage($diff);
$unit_message = self::getDiffUnitMessage($diff);
$lint_title = ' title="'.phutil_escape_html($lint_message).'"';
$unit_title = ' title="'.phutil_escape_html($unit_message).'"';
$base = $this->renderBaseRevision($diff);
} else {
$lint = null;
$unit = null;
$lint_title = null;
$unit_title = null;
$base = null;
}
if ($last_base !== null && $base !== $last_base) {
// TODO: Render some kind of notice about rebases.
}
$last_base = $base;
$rows[] =
'<tr'.$class.'>'.
'<td class="revhistory-name">'.phutil_escape_html($name).'</td>'.
'<td class="revhistory-id">'.phutil_escape_html($id).'</td>'.
'<td class="revhistory-base">'.phutil_escape_html($base).'</td>'.
'<td class="revhistory-desc">'.phutil_escape_html($desc).'</td>'.
'<td class="revhistory-age">'.$age.'</td>'.
'<td class="revhistory-star"'.$lint_title.'>'.$lint.'</td>'.
'<td class="revhistory-star"'.$unit_title.'>'.$unit.'</td>'.
'<td class="revhistory-old'.$old_class.'">'.$old.'</td>'.
'<td class="revhistory-new'.$new_class.'">'.$new.'</td>'.
'</tr>';
}
Javelin::initBehavior(
'differential-diff-radios',
array(
'radios' => $radios,
));
$options = array(
DifferentialChangesetParser::WHITESPACE_IGNORE_FORCE => 'Ignore All',
DifferentialChangesetParser::WHITESPACE_IGNORE_ALL => 'Ignore Most',
DifferentialChangesetParser::WHITESPACE_IGNORE_TRAILING =>
'Ignore Trailing',
DifferentialChangesetParser::WHITESPACE_SHOW_ALL => 'Show All',
);
$select = '<select name="whitespace">';
foreach ($options as $value => $label) {
$select .= phutil_render_tag(
'option',
array(
'value' => $value,
'selected' => ($value == $this->selectedWhitespace)
? 'selected'
: null,
),
phutil_escape_html($label));
}
$select .= '</select>';
return
'<div class="differential-revision-history differential-panel">'.
'<h1>Revision Update History</h1>'.
'<form action="#toc">'.
'<table class="differential-revision-history-table">'.
'<tr>'.
'<th>Diff</th>'.
'<th>ID</th>'.
'<th>Base</th>'.
'<th>Description</th>'.
'<th>Created</th>'.
'<th>Lint</th>'.
'<th>Unit</th>'.
'</tr>'.
implode("\n", $rows).
'<tr>'.
'<td colspan="8" class="diff-differ-submit">'.
'<label>Whitespace Changes: '.$select.'</label>'.
'<button>Show Diff</button>'.
'</td>'.
'</tr>'.
'</table>'.
'</form>'.
'</div>';
}
const STAR_NONE = 'none';
const STAR_OKAY = 'okay';
const STAR_WARN = 'warn';
const STAR_FAIL = 'fail';
const STAR_SKIP = 'skip';
public static function renderDiffLintStar(DifferentialDiff $diff) {
static $map = array(
DifferentialLintStatus::LINT_NONE => self::STAR_NONE,
DifferentialLintStatus::LINT_OKAY => self::STAR_OKAY,
DifferentialLintStatus::LINT_WARN => self::STAR_WARN,
DifferentialLintStatus::LINT_FAIL => self::STAR_FAIL,
DifferentialLintStatus::LINT_SKIP => self::STAR_SKIP,
DifferentialLintStatus::LINT_POSTPONED => self::STAR_SKIP
);
$star = idx($map, $diff->getLintStatus(), self::STAR_FAIL);
return self::renderDiffStar($star);
}
public static function renderDiffUnitStar(DifferentialDiff $diff) {
static $map = array(
DifferentialUnitStatus::UNIT_NONE => self::STAR_NONE,
DifferentialUnitStatus::UNIT_OKAY => self::STAR_OKAY,
DifferentialUnitStatus::UNIT_WARN => self::STAR_WARN,
DifferentialUnitStatus::UNIT_FAIL => self::STAR_FAIL,
DifferentialUnitStatus::UNIT_SKIP => self::STAR_SKIP,
DifferentialUnitStatus::UNIT_POSTPONED => self::STAR_SKIP,
);
$star = idx($map, $diff->getUnitStatus(), self::STAR_FAIL);
return self::renderDiffStar($star);
}
public static function getDiffLintMessage(DifferentialDiff $diff) {
switch ($diff->getLintStatus()) {
case DifferentialLintStatus::LINT_NONE:
return 'No Linters Available';
case DifferentialLintStatus::LINT_OKAY:
return 'Lint OK';
case DifferentialLintStatus::LINT_WARN:
return 'Lint Warnings';
case DifferentialLintStatus::LINT_FAIL:
return 'Lint Errors';
case DifferentialLintStatus::LINT_SKIP:
return 'Lint Skipped';
case DifferentialLintStatus::LINT_POSTPONED:
return 'Lint Postponed';
}
return '???';
}
public static function getDiffUnitMessage(DifferentialDiff $diff) {
switch ($diff->getUnitStatus()) {
case DifferentialUnitStatus::UNIT_NONE:
return 'No Unit Test Coverage';
case DifferentialUnitStatus::UNIT_OKAY:
return 'Unit Tests OK';
case DifferentialUnitStatus::UNIT_WARN:
return 'Unit Test Warnings';
case DifferentialUnitStatus::UNIT_FAIL:
return 'Unit Test Errors';
case DifferentialUnitStatus::UNIT_SKIP:
return 'Unit Tests Skipped';
case DifferentialUnitStatus::UNIT_POSTPONED:
return 'Unit Tests Postponed';
}
return '???';
}
private static function renderDiffStar($star) {
$class = 'diff-star-'.$star;
return
'<span class="'.$class.'">'.
"\xE2\x98\x85".
'</span>';
}
private function renderBaseRevision(DifferentialDiff $diff) {
switch ($diff->getSourceControlSystem()) {
case 'git':
$base = $diff->getSourceControlBaseRevision();
if (strpos($base, '@') === false) {
return substr($base, 0, 7);
} else {
// The diff is from git-svn
$base = explode('@', $base);
$base = last($base);
return $base;
}
case 'svn':
$base = $diff->getSourceControlBaseRevision();
$base = explode('@', $base);
$base = last($base);
return $base;
default:
return null;
}
}
}
diff --git a/src/applications/diffusion/DiffusionRepositoryTag.php b/src/applications/diffusion/DiffusionRepositoryTag.php
index b863a18a13..5846499565 100644
--- a/src/applications/diffusion/DiffusionRepositoryTag.php
+++ b/src/applications/diffusion/DiffusionRepositoryTag.php
@@ -1,82 +1,66 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionRepositoryTag {
private $author;
private $epoch;
private $commitIdentifier;
private $name;
private $description;
private $type;
public function setType($type) {
$this->type = $type;
return $this;
}
public function getType() {
return $this->type;
}
public function setDescription($description) {
$this->description = $description;
return $this;
}
public function getDescription() {
return $this->description;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function setCommitIdentifier($commit_identifier) {
$this->commitIdentifier = $commit_identifier;
return $this;
}
public function getCommitIdentifier() {
return $this->commitIdentifier;
}
public function setEpoch($epoch) {
$this->epoch = $epoch;
return $this;
}
public function getEpoch() {
return $this->epoch;
}
public function setAuthor($author) {
$this->author = $author;
return $this;
}
public function getAuthor() {
return $this->author;
}
}
diff --git a/src/applications/diffusion/application/PhabricatorApplicationDiffusion.php b/src/applications/diffusion/application/PhabricatorApplicationDiffusion.php
index d5ae8c24ee..44a6f2749e 100644
--- a/src/applications/diffusion/application/PhabricatorApplicationDiffusion.php
+++ b/src/applications/diffusion/application/PhabricatorApplicationDiffusion.php
@@ -1,94 +1,78 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationDiffusion extends PhabricatorApplication {
public function getShortDescription() {
return 'Repository Browser';
}
public function getBaseURI() {
return '/diffusion/';
}
public function getAutospriteName() {
return 'diffusion';
}
public function getHelpURI() {
return PhabricatorEnv::getDoclink('article/Diffusion_User_Guide.html');
}
public function getFactObjectsForAnalysis() {
return array(
new PhabricatorRepositoryCommit(),
);
}
public function getRoutes() {
return array(
'/r(?P<callsign>[A-Z]+)(?P<commit>[a-z0-9]+)'
=> 'DiffusionCommitController',
'/diffusion/' => array(
'' => 'DiffusionHomeController',
'(?P<callsign>[A-Z]+)/' => array(
'' => 'DiffusionRepositoryController',
'repository/(?P<dblob>.*)' => 'DiffusionRepositoryController',
'change/(?P<dblob>.*)' => 'DiffusionChangeController',
'history/(?P<dblob>.*)' => 'DiffusionHistoryController',
'browse/(?P<dblob>.*)' => 'DiffusionBrowseController',
'lastmodified/(?P<dblob>.*)' => 'DiffusionLastModifiedController',
'diff/' => 'DiffusionDiffController',
'tags/(?P<dblob>.*)' => 'DiffusionTagListController',
'branches/(?P<dblob>.*)' => 'DiffusionBranchTableController',
'commit/(?P<commit>[a-z0-9]+)/branches/'
=> 'DiffusionCommitBranchesController',
'commit/(?P<commit>[a-z0-9]+)/tags/'
=> 'DiffusionCommitTagsController',
'commit/(?P<commit>[a-z0-9]+)/edit/'
=> 'DiffusionCommitEditController',
),
'inline/' => array(
'edit/(?P<phid>[^/]+)/' => 'DiffusionInlineCommentController',
'preview/(?P<phid>[^/]+)/' =>
'DiffusionInlineCommentPreviewController',
),
'services/' => array(
'path/' => array(
'complete/' => 'DiffusionPathCompleteController',
'validate/' => 'DiffusionPathValidateController',
),
),
'symbol/(?P<name>[^/]+)/' => 'DiffusionSymbolController',
'external/' => 'DiffusionExternalController',
),
);
}
public function getApplicationGroup() {
return self::GROUP_CORE;
}
public function getApplicationOrder() {
return 0.120;
}
}
diff --git a/src/applications/diffusion/controller/DiffusionBranchTableController.php b/src/applications/diffusion/controller/DiffusionBranchTableController.php
index ee83f57e92..282b53cc9c 100644
--- a/src/applications/diffusion/controller/DiffusionBranchTableController.php
+++ b/src/applications/diffusion/controller/DiffusionBranchTableController.php
@@ -1,84 +1,68 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionBranchTableController extends DiffusionController {
public function processRequest() {
$drequest = $this->getDiffusionRequest();
$request = $this->getRequest();
$user = $request->getUser();
$repository = $drequest->getRepository();
$pager = new AphrontPagerView();
$pager->setURI($request->getRequestURI(), 'offset');
$pager->setOffset($request->getInt('offset'));
// TODO: Add support for branches that contain commit
$query = DiffusionBranchQuery::newFromDiffusionRequest($drequest);
$query->setOffset($pager->getOffset());
$query->setLimit($pager->getPageSize() + 1);
$branches = $query->loadBranches();
$branches = $pager->sliceResults($branches);
$content = null;
if (!$branches) {
$content = new AphrontErrorView();
$content->setTitle('No Branches');
$content->appendChild('This repository has no branches.');
$content->setSeverity(AphrontErrorView::SEVERITY_NODATA);
} else {
$commits = id(new PhabricatorAuditCommitQuery())
->withIdentifiers(
$drequest->getRepository()->getID(),
mpull($branches, 'getHeadCommitIdentifier'))
->needCommitData(true)
->execute();
$view = id(new DiffusionBranchTableView())
->setBranches($branches)
->setUser($user)
->setCommits($commits)
->setDiffusionRequest($drequest);
$panel = id(new AphrontPanelView())
->setHeader('Branches')
->appendChild($view)
->appendChild($pager);
$content = $panel;
}
return $this->buildStandardPageResponse(
array(
$this->buildCrumbs(
array(
'branches' => true,
)),
$content,
),
array(
'title' => array(
'Branches',
$repository->getCallsign().' Repository',
),
));
}
}
diff --git a/src/applications/diffusion/controller/DiffusionBrowseController.php b/src/applications/diffusion/controller/DiffusionBrowseController.php
index e92b46dd47..1387c03780 100644
--- a/src/applications/diffusion/controller/DiffusionBrowseController.php
+++ b/src/applications/diffusion/controller/DiffusionBrowseController.php
@@ -1,135 +1,119 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionBrowseController extends DiffusionController {
public function processRequest() {
$drequest = $this->diffusionRequest;
$browse_query = DiffusionBrowseQuery::newFromDiffusionRequest($drequest);
$results = $browse_query->loadPaths();
$content = array();
$content[] = $this->buildCrumbs(
array(
'branch' => true,
'path' => true,
'view' => 'browse',
));
if ($drequest->getTagContent()) {
$title = 'Tag: '.$drequest->getSymbolicCommit();
$tag_view = new AphrontPanelView();
$tag_view->setHeader(phutil_escape_html($title));
$tag_view->appendChild(
$this->markupText($drequest->getTagContent()));
$content[] = $tag_view;
}
if (!$results) {
if ($browse_query->getReasonForEmptyResultSet() ==
DiffusionBrowseQuery::REASON_IS_FILE) {
$controller = new DiffusionBrowseFileController($this->getRequest());
$controller->setDiffusionRequest($drequest);
return $this->delegateToController($controller);
}
$empty_result = new DiffusionEmptyResultView();
$empty_result->setDiffusionRequest($drequest);
$empty_result->setBrowseQuery($browse_query);
$empty_result->setView($this->getRequest()->getStr('view'));
$content[] = $empty_result;
} else {
$readme = null;
$phids = array();
foreach ($results as $result) {
$data = $result->getLastCommitData();
if ($data) {
if ($data->getCommitDetail('authorPHID')) {
$phids[$data->getCommitDetail('authorPHID')] = true;
}
}
$path = $result->getPath();
if (preg_match('/^readme(|\.txt|\.remarkup)$/i', $path)) {
$readme = $result;
}
}
$phids = array_keys($phids);
$handles = $this->loadViewerHandles($phids);
$browse_table = new DiffusionBrowseTableView();
$browse_table->setDiffusionRequest($drequest);
$browse_table->setHandles($handles);
$browse_table->setPaths($results);
$browse_table->setUser($this->getRequest()->getUser());
$browse_panel = new AphrontPanelView();
$browse_panel->appendChild($browse_table);
$content[] = $browse_panel;
}
$content[] = $this->buildOpenRevisions();
$readme_content = $browse_query->renderReadme($results);
if ($readme_content) {
$readme_panel = new AphrontPanelView();
$readme_panel->setHeader('README');
$readme_panel->appendChild($readme_content);
$content[] = $readme_panel;
}
$nav = $this->buildSideNav('browse', false);
$nav->appendChild($content);
return $this->buildStandardPageResponse(
$nav,
array(
'title' => array(
nonempty(basename($drequest->getPath()), '/'),
$drequest->getRepository()->getCallsign().' Repository',
),
));
}
private function markupText($text) {
$engine = PhabricatorMarkupEngine::newDiffusionMarkupEngine();
$text = $engine->markupText($text);
$text = phutil_render_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
$text);
return $text;
}
}
diff --git a/src/applications/diffusion/controller/DiffusionBrowseFileController.php b/src/applications/diffusion/controller/DiffusionBrowseFileController.php
index e563df2e57..b88d41bafc 100644
--- a/src/applications/diffusion/controller/DiffusionBrowseFileController.php
+++ b/src/applications/diffusion/controller/DiffusionBrowseFileController.php
@@ -1,905 +1,889 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionBrowseFileController extends DiffusionController {
private $corpusType = 'text';
public function processRequest() {
$request = $this->getRequest();
$drequest = $this->getDiffusionRequest();
$before = $request->getStr('before');
if ($before) {
return $this->buildBeforeResponse($before);
}
$path = $drequest->getPath();
$selected = $request->getStr('view');
$preferences = $request->getUser()->loadPreferences();
if (!$selected) {
$selected = $preferences->getPreference(
PhabricatorUserPreferences::PREFERENCE_DIFFUSION_VIEW,
'highlighted');
} else if ($request->isFormPost() && $selected != 'raw') {
$preferences->setPreference(
PhabricatorUserPreferences::PREFERENCE_DIFFUSION_VIEW,
$selected);
$preferences->save();
return id(new AphrontRedirectResponse())
->setURI($request->getRequestURI()->alter('view', $selected));
}
$needs_blame = ($selected == 'blame' || $selected == 'plainblame');
$file_query = DiffusionFileContentQuery::newFromDiffusionRequest(
$this->diffusionRequest);
$file_query->setViewer($request->getUser());
$file_query->setNeedsBlame($needs_blame);
$file_query->loadFileContent();
$data = $file_query->getRawData();
if ($selected === 'raw') {
return $this->buildRawResponse($path, $data);
}
// Build the content of the file.
$corpus = $this->buildCorpus(
$selected,
$file_query,
$needs_blame,
$drequest,
$path,
$data);
require_celerity_resource('diffusion-source-css');
if ($this->corpusType == 'text') {
$view_select_panel = $this->renderViewSelectPanel($selected);
} else {
$view_select_panel = null;
}
// Render the page.
$content = array();
$content[] = $this->buildCrumbs(
array(
'branch' => true,
'path' => true,
'view' => 'browse',
));
$follow = $request->getStr('follow');
if ($follow) {
$notice = new AphrontErrorView();
$notice->setSeverity(AphrontErrorView::SEVERITY_WARNING);
$notice->setTitle('Unable to Continue');
switch ($follow) {
case 'first':
$notice->appendChild(
"Unable to continue tracing the history of this file because ".
"this commit is the first commit in the repository.");
break;
case 'created':
$notice->appendChild(
"Unable to continue tracing the history of this file because ".
"this commit created the file.");
break;
}
$content[] = $notice;
}
$renamed = $request->getStr('renamed');
if ($renamed) {
$notice = new AphrontErrorView();
$notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
$notice->setTitle('File Renamed');
$notice->appendChild(
"File history passes through a rename from '".
phutil_escape_html($drequest->getPath())."' to '".
phutil_escape_html($renamed)."'.");
$content[] = $notice;
}
$content[] = $view_select_panel;
$content[] = $corpus;
$content[] = $this->buildOpenRevisions();
$nav = $this->buildSideNav('browse', true);
$nav->appendChild($content);
$basename = basename($this->getDiffusionRequest()->getPath());
return $this->buildStandardPageResponse(
$nav,
array(
'title' => $basename,
));
}
private function buildCorpus($selected,
DiffusionFileContentQuery $file_query,
$needs_blame,
DiffusionRequest $drequest,
$path,
$data) {
if (ArcanistDiffUtils::isHeuristicBinaryFile($data)) {
$file = $this->loadFileForData($path, $data);
$file_uri = $file->getBestURI();
if ($file->isViewableImage()) {
$this->corpusType = 'image';
return $this->buildImageCorpus($file_uri);
} else {
$this->corpusType = 'binary';
return $this->buildBinaryCorpus($file_uri, $data);
}
}
switch ($selected) {
case 'plain':
$style =
"margin: 1em 2em; width: 90%; height: 80em; font-family: monospace";
$corpus = phutil_render_tag(
'textarea',
array(
'style' => $style,
),
phutil_escape_html($file_query->getRawData()));
break;
case 'plainblame':
$style =
"margin: 1em 2em; width: 90%; height: 80em; font-family: monospace";
list($text_list, $rev_list, $blame_dict) =
$file_query->getBlameData();
$rows = array();
foreach ($text_list as $k => $line) {
$rev = $rev_list[$k];
if (isset($blame_dict[$rev]['handle'])) {
$author = $blame_dict[$rev]['handle']->getName();
} else {
$author = $blame_dict[$rev]['author'];
}
$rows[] =
sprintf("%-10s %-20s %s", substr($rev, 0, 7), $author, $line);
}
$corpus = phutil_render_tag(
'textarea',
array(
'style' => $style,
),
phutil_escape_html(implode("\n", $rows)));
break;
case 'highlighted':
case 'blame':
default:
require_celerity_resource('syntax-highlighting-css');
list($text_list, $rev_list, $blame_dict) = $file_query->getBlameData();
$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);
$id = celerity_generate_unique_node_id();
$projects = $drequest->loadArcanistProjects();
$langs = array();
foreach ($projects as $project) {
$ls = $project->getSymbolIndexLanguages();
if (!$ls) {
continue;
}
$dep_projects = $project->getSymbolIndexProjects();
$dep_projects[] = $project->getPHID();
foreach ($ls as $lang) {
if (!isset($langs[$lang])) {
$langs[$lang] = array();
}
$langs[$lang] += $dep_projects + array($project);
}
}
$lang = last(explode('.', $drequest->getPath()));
$prefs = $this->getRequest()->getUser()->loadPreferences();
$pref_symbols = $prefs->getPreference(
PhabricatorUserPreferences::PREFERENCE_DIFFUSION_SYMBOLS);
if (isset($langs[$lang]) && $pref_symbols != 'disabled') {
Javelin::initBehavior(
'repository-crossreference',
array(
'container' => $id,
'lang' => $lang,
'projects' => $langs[$lang],
));
}
$corpus_table = javelin_render_tag(
'table',
array(
'class' => "diffusion-source remarkup-code PhabricatorMonospaced",
'sigil' => 'diffusion-source',
),
implode("\n", $rows));
$corpus = phutil_render_tag(
'div',
array(
'style' => 'padding: 0 2em;',
'id' => $id,
),
$corpus_table);
break;
}
return $corpus;
}
private function renderViewSelectPanel($selected) {
$toggle_blame = array(
'highlighted' => 'blame',
'blame' => 'highlighted',
'plain' => 'plainblame',
'plainblame' => 'plain',
'raw' => 'raw', // not a real case.
);
$toggle_highlight = array(
'highlighted' => 'plain',
'blame' => 'plainblame',
'plain' => 'highlighted',
'plainblame' => 'blame',
'raw' => 'raw', // not a real case.
);
$user = $this->getRequest()->getUser();
$blame_on = ($selected == 'blame' || $selected == 'plainblame');
if ($blame_on) {
$blame_text = pht('Disable Blame');
} else {
$blame_text = pht('Enable Blame');
}
$blame_button = $this->createViewAction(
$blame_text,
$toggle_blame[$selected],
$user);
$highlight_on = ($selected == 'blame' || $selected == 'highlighted');
if ($highlight_on) {
$highlight_text = pht('Disable Highlighting');
} else {
$highlight_text = pht('Enable Highlighting');
}
$highlight_button = $this->createViewAction(
$highlight_text,
$toggle_highlight[$selected],
$user);
$raw_button = $this->createViewAction(
pht('View Raw File'),
'raw',
$user,
'file');
$edit_button = $this->createEditAction();
return id(new PhabricatorActionListView())
->setUser($user)
->addAction($blame_button)
->addAction($highlight_button)
->addAction($raw_button)
->addAction($edit_button);
}
private function createViewAction(
$localized_text,
$view_mode,
$user,
$icon = null) {
$base_uri = $this->getRequest()->getRequestURI();
return id(new PhabricatorActionView())
->setName($localized_text)
->setIcon($icon)
->setUser($user)
->setRenderAsForm(true)
->setHref($base_uri->alter('view', $view_mode));
}
private function createEditAction() {
$request = $this->getRequest();
$user = $request->getUser();
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$path = $drequest->getPath();
$line = nonempty((int)$drequest->getLine(), 1);
$callsign = $repository->getCallsign();
$editor_link = $user->loadEditorLink($path, $line, $callsign);
$action = id(new PhabricatorActionView())
->setName(pht('Open in Editor'))
->setIcon('edit');
$action->setHref($editor_link);
$action->setDisabled(!$editor_link);
return $action;
}
private function buildDisplayRows(
array $text_list,
array $rev_list,
array $blame_dict,
$needs_blame,
DiffusionRequest $drequest,
DiffusionFileContentQuery $file_query,
$selected) {
if ($blame_dict) {
$epoch_list = ipull(ifilter($blame_dict, 'epoch'), 'epoch');
$epoch_min = min($epoch_list);
$epoch_max = max($epoch_list);
$epoch_range = ($epoch_max - $epoch_min) + 1;
}
$line_arr = array();
$line_str = $drequest->getLine();
$ranges = explode(',', $line_str);
foreach ($ranges as $range) {
if (strpos($range, '-') !== false) {
list($min, $max) = explode('-', $range, 2);
$line_arr[] = array(
'min' => min($min, $max),
'max' => max($min, $max),
);
} else if (strlen($range)) {
$line_arr[] = array(
'min' => $range,
'max' => $range,
);
}
}
$display = array();
$line_number = 1;
$last_rev = null;
$color = null;
foreach ($text_list as $k => $line) {
$display_line = array(
'color' => null,
'epoch' => null,
'commit' => null,
'author' => null,
'target' => null,
'highlighted' => null,
'line' => $line_number,
'data' => $line,
);
if ($needs_blame) {
// If the line's rev is same as the line above, show empty content
// with same color; otherwise generate blame info. The newer a change
// is, the more saturated the color.
// TODO: SVN doesn't always give us blame for the last line, if empty?
// Bug with our stuff or with SVN?
$rev = idx($rev_list, $k, $last_rev);
if ($last_rev == $rev) {
$display_line['color'] = $color;
} else {
$blame = $blame_dict[$rev];
if (!isset($blame['epoch'])) {
$color = '#ffd'; // Render as warning.
} else {
$color_ratio = ($blame['epoch'] - $epoch_min) / $epoch_range;
$color_value = 0xF6 * (1.0 - $color_ratio);
$color = sprintf(
'#%02x%02x%02x',
$color_value,
0xF6,
$color_value);
}
$display_line['epoch'] = idx($blame, 'epoch');
$display_line['color'] = $color;
$display_line['commit'] = $rev;
if (isset($blame['handle'])) {
$author_link = $blame['handle']->renderLink();
} else {
$author_link = phutil_render_tag(
'span',
array(
),
phutil_escape_html($blame['author']));
}
$display_line['author'] = $author_link;
$last_rev = $rev;
}
}
if ($line_arr) {
if ($line_number == $line_arr[0]['min']) {
$display_line['target'] = true;
}
foreach ($line_arr as $range) {
if ($line_number >= $range['min'] &&
$line_number <= $range['max']) {
$display_line['highlighted'] = true;
}
}
}
$display[] = $display_line;
++$line_number;
}
$commits = array_filter(ipull($display, 'commit'));
if ($commits) {
$commits = id(new PhabricatorAuditCommitQuery())
->withIdentifiers($drequest->getRepository()->getID(), $commits)
->needCommitData(true)
->execute();
$commits = mpull($commits, null, 'getCommitIdentifier');
}
$revision_ids = id(new DifferentialRevision())
->loadIDsByCommitPHIDs(mpull($commits, 'getPHID'));
$revisions = array();
if ($revision_ids) {
$revisions = id(new DifferentialRevision())->loadAllWhere(
'id IN (%Ld)',
$revision_ids);
}
$request = $this->getRequest();
$user = $request->getUser();
Javelin::initBehavior('phabricator-oncopy', array());
$rows = array();
foreach ($display as $line) {
$line_href = $drequest->generateURI(
array(
'action' => 'browse',
'line' => $line['line'],
'stable' => true,
));
$blame = array();
if ($line['color']) {
$color = $line['color'];
$before_link = null;
$commit_link = null;
$revision_link = null;
if (idx($line, 'commit')) {
$commit = $line['commit'];
$summary = 'Unknown';
if (idx($commits, $commit)) {
$summary = $commits[$commit]->getCommitData()->getSummary();
}
$tooltip = phabricator_date(
$line['epoch'],
$user)." \xC2\xB7 ".$summary;
Javelin::initBehavior('phabricator-tooltips', array());
require_celerity_resource('aphront-tooltip-css');
$commit_link = javelin_render_tag(
'a',
array(
'href' => $drequest->generateURI(
array(
'action' => 'commit',
'commit' => $line['commit'],
)),
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $tooltip,
'align' => 'E',
'size' => 600,
),
),
phutil_escape_html(phutil_utf8_shorten($line['commit'], 9, '')));
$revision_id = null;
if (idx($commits, $commit)) {
$revision_id = idx($revision_ids, $commits[$commit]->getPHID());
}
if ($revision_id) {
$revision = idx($revisions, $revision_id);
if (!$revision) {
$tooltip = '(Invalid revision)';
} else {
$tooltip =
phabricator_date($revision->getDateModified(), $user).
" \xC2\xB7 ".
$revision->getTitle();
}
$revision_link = javelin_render_tag(
'a',
array(
'href' => '/D'.$revision_id,
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $tooltip,
'align' => 'E',
'size' => 600,
),
),
'D'.$revision_id);
}
$uri = $line_href->alter('before', $commit);
$before_link = javelin_render_tag(
'a',
array(
'href' => $uri->setQueryParam('view', 'blame'),
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => 'Skip Past This Commit',
'align' => 'E',
'size' => 300,
),
),
"\xC2\xAB");
}
$blame[] = phutil_render_tag(
'th',
array(
'class' => 'diffusion-blame-link',
'style' => 'background: '.$color,
),
$before_link);
$blame[] = phutil_render_tag(
'th',
array(
'class' => 'diffusion-rev-link',
'style' => 'background: '.$color,
),
$commit_link);
$blame[] = phutil_render_tag(
'th',
array(
'class' => 'diffusion-rev-link',
'style' => 'background: '.$color,
),
$revision_link);
$blame[] = phutil_render_tag(
'th',
array(
'class' => 'diffusion-author-link',
'style' => 'background: '.$color,
),
idx($line, 'author'));
}
$line_link = phutil_render_tag(
'a',
array(
'href' => $line_href,
),
phutil_escape_html($line['line']));
$blame[] = javelin_render_tag(
'th',
array(
'class' => 'diffusion-line-link',
'sigil' => 'diffusion-line-link',
'style' => isset($color) ? 'background: '.$color : null,
),
$line_link);
Javelin::initBehavior('diffusion-line-linker');
$blame = implode('', $blame);
if ($line['target']) {
Javelin::initBehavior(
'diffusion-jump-to',
array(
'target' => 'scroll_target',
));
$anchor_text = '<a id="scroll_target"></a>';
} else {
$anchor_text = null;
}
$line_text = phutil_render_tag(
'td',
array(
),
$anchor_text.
"\xE2\x80\x8B". // NOTE: See phabricator-oncopy behavior.
$line['data']);
$rows[] = phutil_render_tag(
'tr',
array(
'class' => ($line['highlighted'] ? 'highlighted' : null),
),
$blame.
$line_text);
}
return $rows;
}
private static function renderRevision(DiffusionRequest $drequest,
$revision) {
$callsign = $drequest->getCallsign();
$name = 'r'.$callsign.$revision;
return phutil_render_tag(
'a',
array(
'href' => '/'.$name,
),
$name
);
}
private static function renderBrowse(
DiffusionRequest $drequest,
$path,
$name = null,
$rev = null,
$line = null,
$view = null,
$title = null) {
$callsign = $drequest->getCallsign();
if ($name === null) {
$name = $path;
}
$at = null;
if ($rev) {
$at = ';'.$rev;
}
if ($view) {
$view = '?view='.$view;
}
if ($line) {
$line = '$'.$line;
}
return phutil_render_tag(
'a',
array(
'href' => "/diffusion/{$callsign}/browse/{$path}{$at}{$line}{$view}",
'title' => $title,
),
$name
);
}
private function loadFileForData($path, $data) {
return PhabricatorFile::buildFromFileDataOrHash(
$data,
array(
'name' => basename($path),
));
}
private function buildRawResponse($path, $data) {
$file = $this->loadFileForData($path, $data);
return id(new AphrontRedirectResponse())->setURI($file->getBestURI());
}
private function buildImageCorpus($file_uri) {
$properties = new PhabricatorPropertyListView();
$properties->addProperty(
pht('Image'),
phutil_render_tag(
'img',
array(
'src' => $file_uri,
)));
$actions = id(new PhabricatorActionListView())
->setUser($this->getRequest()->getUser())
->addAction($this->createEditAction());
return array($actions, $properties);
}
private function buildBinaryCorpus($file_uri, $data) {
$properties = new PhabricatorPropertyListView();
$properties->addTextContent(
pht('This is a binary file. It is %d bytes in length.',
number_format(strlen($data)))
);
$actions = id(new PhabricatorActionListView())
->setUser($this->getRequest()->getUser())
->addAction($this->createEditAction())
->addAction(id(new PhabricatorActionView())
->setName(pht('Download Binary File...'))
->setIcon('download')
->setHref($file_uri));
return array($actions, $properties);
}
private function buildBeforeResponse($before) {
$request = $this->getRequest();
$drequest = $this->getDiffusionRequest();
// NOTE: We need to get the grandparent so we can capture filename changes
// in the parent.
$parent = $this->loadParentRevisionOf($before);
$old_filename = null;
$was_created = false;
if ($parent) {
$grandparent = $this->loadParentRevisionOf(
$parent->getCommitIdentifier());
if ($grandparent) {
$rename_query = new DiffusionRenameHistoryQuery();
$rename_query->setRequest($drequest);
$rename_query->setOldCommit($grandparent->getCommitIdentifier());
$old_filename = $rename_query->loadOldFilename();
$was_created = $rename_query->getWasCreated();
}
}
$follow = null;
if ($was_created) {
// If the file was created in history, that means older commits won't
// have it. Since we know it existed at 'before', it must have been
// created then; jump there.
$target_commit = $before;
$follow = 'created';
} else if ($parent) {
// If we found a parent, jump to it. This is the normal case.
$target_commit = $parent->getCommitIdentifier();
} else {
// If there's no parent, this was probably created in the initial commit?
// And the "was_created" check will fail because we can't identify the
// grandparent. Keep the user at 'before'.
$target_commit = $before;
$follow = 'first';
}
$path = $drequest->getPath();
$renamed = null;
if ($old_filename !== null &&
$old_filename !== '/'.$path) {
$renamed = $path;
$path = $old_filename;
}
$line = null;
// If there's a follow error, drop the line so the user sees the message.
if (!$follow) {
$line = $this->getBeforeLineNumber($target_commit);
}
$before_uri = $drequest->generateURI(
array(
'action' => 'browse',
'commit' => $target_commit,
'line' => $line,
'path' => $path,
));
$before_uri->setQueryParams($request->getRequestURI()->getQueryParams());
$before_uri = $before_uri->alter('before', null);
$before_uri = $before_uri->alter('renamed', $renamed);
$before_uri = $before_uri->alter('follow', $follow);
return id(new AphrontRedirectResponse())->setURI($before_uri);
}
private function getBeforeLineNumber($target_commit) {
$drequest = $this->getDiffusionRequest();
$line = $drequest->getLine();
if (!$line) {
return null;
}
$diff_query = DiffusionRawDiffQuery::newFromDiffusionRequest($drequest);
$diff_query->setAgainstCommit($target_commit);
try {
$raw_diff = $diff_query->loadRawDiff();
$old_line = 0;
$new_line = 0;
foreach (explode("\n", $raw_diff) as $text) {
if ($text[0] == '-' || $text[0] == ' ') {
$old_line++;
}
if ($text[0] == '+' || $text[0] == ' ') {
$new_line++;
}
if ($new_line == $line) {
return $old_line;
}
}
// We didn't find the target line.
return $line;
} catch (Exception $ex) {
return $line;
}
}
private function loadParentRevisionOf($commit) {
$drequest = $this->getDiffusionRequest();
$before_req = DiffusionRequest::newFromDictionary(
array(
'repository' => $drequest->getRepository(),
'commit' => $commit,
));
$query = DiffusionCommitParentsQuery::newFromDiffusionRequest($before_req);
$parents = $query->loadParents();
return head($parents);
}
}
diff --git a/src/applications/diffusion/controller/DiffusionChangeController.php b/src/applications/diffusion/controller/DiffusionChangeController.php
index ff30e34deb..f09786769d 100644
--- a/src/applications/diffusion/controller/DiffusionChangeController.php
+++ b/src/applications/diffusion/controller/DiffusionChangeController.php
@@ -1,89 +1,73 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionChangeController extends DiffusionController {
public function processRequest() {
$drequest = $this->diffusionRequest;
$content = array();
$diff_query = DiffusionDiffQuery::newFromDiffusionRequest($drequest);
$changeset = $diff_query->loadChangeset();
if (!$changeset) {
// TODO: Refine this.
return new Aphront404Response();
}
$callsign = $drequest->getRepository()->getCallsign();
$changesets = array(
0 => $changeset,
);
$changeset_view = new DifferentialChangesetListView();
$changeset_view->setChangesets($changesets);
$changeset_view->setVisibleChangesets($changesets);
$changeset_view->setRenderingReferences(
array(
0 => $diff_query->getRenderingReference(),
));
$raw_params = array(
'action' => 'browse',
'params' => array(
'view' => 'raw',
),
);
$right_uri = $drequest->generateURI($raw_params);
$raw_params['params']['before'] = $drequest->getRawCommit();
$left_uri = $drequest->generateURI($raw_params);
$changeset_view->setRawFileURIs($left_uri, $right_uri);
$changeset_view->setRenderURI(
'/diffusion/'.$callsign.'/diff/');
$changeset_view->setWhitespace(
DifferentialChangesetParser::WHITESPACE_SHOW_ALL);
$changeset_view->setUser($this->getRequest()->getUser());
$content[] = $this->buildCrumbs(
array(
'branch' => true,
'path' => true,
'view' => 'change',
));
// TODO: This is pretty awkward, unify the CSS between Diffusion and
// Differential better.
require_celerity_resource('differential-core-view-css');
$content[] =
'<div class="differential-primary-pane">'.
$changeset_view->render().
'</div>';
$nav = $this->buildSideNav('change', true);
$nav->appendChild($content);
return $this->buildStandardPageResponse(
$nav,
array(
'title' => 'Change',
));
}
}
diff --git a/src/applications/diffusion/controller/DiffusionCommitBranchesController.php b/src/applications/diffusion/controller/DiffusionCommitBranchesController.php
index 28a3844749..c456d5e9af 100644
--- a/src/applications/diffusion/controller/DiffusionCommitBranchesController.php
+++ b/src/applications/diffusion/controller/DiffusionCommitBranchesController.php
@@ -1,48 +1,32 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionCommitBranchesController extends DiffusionController {
public function willProcessRequest(array $data) {
$this->diffusionRequest = DiffusionRequest::newFromDictionary($data);
}
public function processRequest() {
$request = $this->getDiffusionRequest();
$branch_query = DiffusionContainsQuery::newFromDiffusionRequest($request);
$branches = $branch_query->loadContainingBranches();
$branch_links = array();
foreach ($branches as $branch => $commit) {
$branch_links[] = phutil_render_tag(
'a',
array(
'href' => $request->generateURI(
array(
'action' => 'browse',
'branch' => $branch,
)),
),
phutil_escape_html($branch));
}
return id(new AphrontAjaxResponse())
->setContent($branch_links ? implode(', ', $branch_links) : 'None');
}
}
diff --git a/src/applications/diffusion/controller/DiffusionCommitController.php b/src/applications/diffusion/controller/DiffusionCommitController.php
index 4268650cad..d35eae1630 100644
--- a/src/applications/diffusion/controller/DiffusionCommitController.php
+++ b/src/applications/diffusion/controller/DiffusionCommitController.php
@@ -1,913 +1,897 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionCommitController extends DiffusionController {
const CHANGES_LIMIT = 100;
private $auditAuthorityPHIDs;
private $highlightedAudits;
public function willProcessRequest(array $data) {
// This controller doesn't use blob/path stuff, just pass the dictionary
// in directly instead of using the AphrontRequest parsing mechanism.
$drequest = DiffusionRequest::newFromDictionary($data);
$this->diffusionRequest = $drequest;
}
public function processRequest() {
$drequest = $this->getDiffusionRequest();
$request = $this->getRequest();
$user = $request->getUser();
if ($request->getStr('diff')) {
return $this->buildRawDiffResponse($drequest);
}
$callsign = $drequest->getRepository()->getCallsign();
$content = array();
$content[] = $this->buildCrumbs(array(
'commit' => true,
));
$repository = $drequest->getRepository();
$commit = $drequest->loadCommit();
if (!$commit) {
$query = DiffusionExistsQuery::newFromDiffusionRequest($drequest);
$exists = $query->loadExistentialData();
if (!$exists) {
return new Aphront404Response();
}
return $this->buildStandardPageResponse(
id(new AphrontErrorView())
->setTitle('Error displaying commit.')
->appendChild('Failed to load the commit because the commit has not '.
'been parsed yet.'),
array('title' => 'Commit Still Parsing')
);
}
$commit_data = $drequest->loadCommitData();
$commit->attachCommitData($commit_data);
$is_foreign = $commit_data->getCommitDetail('foreign-svn-stub');
if ($is_foreign) {
$subpath = $commit_data->getCommitDetail('svn-subpath');
$error_panel = new AphrontErrorView();
$error_panel->setTitle('Commit Not Tracked');
$error_panel->setSeverity(AphrontErrorView::SEVERITY_WARNING);
$error_panel->appendChild(
"This Diffusion repository is configured to track only one ".
"subdirectory of the entire Subversion repository, and this commit ".
"didn't affect the tracked subdirectory ('".
phutil_escape_html($subpath)."'), so no information is available.");
$content[] = $error_panel;
} else {
$engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine();
require_celerity_resource('diffusion-commit-view-css');
require_celerity_resource('phabricator-remarkup-css');
$parent_query = DiffusionCommitParentsQuery::newFromDiffusionRequest(
$drequest);
$headsup_panel = new AphrontHeadsupView();
$headsup_panel->setHeader('Commit Detail');
$headsup_panel->setActionList(
$this->renderHeadsupActionList($commit, $repository));
$headsup_panel->setProperties(
$this->getCommitProperties(
$commit,
$commit_data,
$parent_query->loadParents()));
$headsup_panel->appendChild(
'<div class="diffusion-commit-message phabricator-remarkup">'.
$engine->markupText($commit_data->getCommitMessage()).
'</div>');
$content[] = $headsup_panel;
}
$query = new PhabricatorAuditQuery();
$query->withCommitPHIDs(array($commit->getPHID()));
$audit_requests = $query->execute();
$this->auditAuthorityPHIDs =
PhabricatorAuditCommentEditor::loadAuditPHIDsForUser($user);
$content[] = $this->buildAuditTable($commit, $audit_requests);
$content[] = $this->buildComments($commit);
$change_query = DiffusionPathChangeQuery::newFromDiffusionRequest(
$drequest);
$changes = $change_query->loadChanges();
$content[] = $this->buildMergesTable($commit);
$owners_paths = array();
if ($this->highlightedAudits) {
$packages = id(new PhabricatorOwnersPackage())->loadAllWhere(
'phid IN (%Ls)',
mpull($this->highlightedAudits, 'getAuditorPHID'));
if ($packages) {
$owners_paths = id(new PhabricatorOwnersPath())->loadAllWhere(
'repositoryPHID = %s AND packageID IN (%Ld)',
$repository->getPHID(),
mpull($packages, 'getID'));
}
}
$change_table = new DiffusionCommitChangeTableView();
$change_table->setDiffusionRequest($drequest);
$change_table->setPathChanges($changes);
$change_table->setOwnersPaths($owners_paths);
$count = count($changes);
$bad_commit = null;
if ($count == 0) {
$bad_commit = queryfx_one(
id(new PhabricatorRepository())->establishConnection('r'),
'SELECT * FROM %T WHERE fullCommitName = %s',
PhabricatorRepository::TABLE_BADCOMMIT,
'r'.$callsign.$commit->getCommitIdentifier());
}
$pane_id = null;
if ($bad_commit) {
$error_panel = new AphrontErrorView();
$error_panel->setTitle('Bad Commit');
$error_panel->appendChild(
phutil_escape_html($bad_commit['description']));
$content[] = $error_panel;
} else if ($is_foreign) {
// Don't render anything else.
} else if (!count($changes)) {
$no_changes = new AphrontErrorView();
$no_changes->setSeverity(AphrontErrorView::SEVERITY_WARNING);
$no_changes->setTitle('Not Yet Parsed');
// TODO: This can also happen with weird SVN changes that don't do
// anything (or only alter properties?), although the real no-changes case
// is extremely rare and might be impossible to produce organically. We
// should probably write some kind of "Nothing Happened!" change into the
// DB once we parse these changes so we can distinguish between
// "not parsed yet" and "no changes".
$no_changes->appendChild(
"This commit hasn't been fully parsed yet (or doesn't affect any ".
"paths).");
$content[] = $no_changes;
} else {
$change_panel = new AphrontPanelView();
$change_panel->setHeader("Changes (".number_format($count).")");
$change_panel->setID('toc');
if ($count > self::CHANGES_LIMIT) {
$show_all_button = phutil_render_tag(
'a',
array(
'class' => 'button green',
'href' => '?show_all=true',
),
phutil_escape_html('Show All Changes'));
$warning_view = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_WARNING)
->setTitle('Very Large Commit')
->appendChild(
"<p>This commit is very large. Load each file individually.</p>");
$change_panel->appendChild($warning_view);
$change_panel->addButton($show_all_button);
}
$change_panel->appendChild($change_table);
$content[] = $change_panel;
$changesets = DiffusionPathChange::convertToDifferentialChangesets(
$changes);
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$vcs_supports_directory_changes = true;
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$vcs_supports_directory_changes = false;
break;
default:
throw new Exception("Unknown VCS.");
}
$references = array();
foreach ($changesets as $key => $changeset) {
$file_type = $changeset->getFileType();
if ($file_type == DifferentialChangeType::FILE_DIRECTORY) {
if (!$vcs_supports_directory_changes) {
unset($changesets[$key]);
continue;
}
}
$references[$key] = $drequest->generateURI(
array(
'action' => 'rendering-ref',
'path' => $changeset->getFilename(),
));
}
// TODO: Some parts of the views still rely on properties of the
// DifferentialChangeset. Make the objects ephemeral to make sure we don't
// accidentally save them, and then set their ID to the appropriate ID for
// this application (the path IDs).
$path_ids = array_flip(mpull($changes, 'getPath'));
foreach ($changesets as $changeset) {
$changeset->makeEphemeral();
$changeset->setID($path_ids[$changeset->getFilename()]);
}
if ($count <= self::CHANGES_LIMIT) {
$visible_changesets = $changesets;
} else {
$visible_changesets = array();
$inlines = id(new PhabricatorAuditInlineComment())->loadAllWhere(
'commitPHID = %s AND (auditCommentID IS NOT NULL OR authorPHID = %s)',
$commit->getPHID(),
$user->getPHID());
$path_ids = mpull($inlines, null, 'getPathID');
foreach ($changesets as $key => $changeset) {
if (array_key_exists($changeset->getID(), $path_ids)) {
$visible_changesets[$key] = $changeset;
}
}
}
$change_list = new DifferentialChangesetListView();
$change_list->setChangesets($changesets);
$change_list->setVisibleChangesets($visible_changesets);
$change_list->setRenderingReferences($references);
$change_list->setRenderURI('/diffusion/'.$callsign.'/diff/');
$change_list->setRepository($repository);
$change_list->setUser($user);
// pick the first branch for "Browse in Diffusion" View Option
$branches = $commit_data->getCommitDetail('seenOnBranches');
$first_branch = reset($branches);
$change_list->setBranch($first_branch);
$change_list->setStandaloneURI(
'/diffusion/'.$callsign.'/diff/');
$change_list->setRawFileURIs(
// TODO: Implement this, somewhat tricky if there's an octopus merge
// or whatever?
null,
'/diffusion/'.$callsign.'/diff/?view=r');
$change_list->setInlineCommentControllerURI(
'/diffusion/inline/edit/'.phutil_escape_uri($commit->getPHID()).'/');
$change_references = array();
foreach ($changesets as $key => $changeset) {
$change_references[$changeset->getID()] = $references[$key];
}
$change_table->setRenderingReferences($change_references);
// TODO: This is pretty awkward, unify the CSS between Diffusion and
// Differential better.
require_celerity_resource('differential-core-view-css');
$pane_id = celerity_generate_unique_node_id();
$add_comment_view = $this->renderAddCommentPanel($commit,
$audit_requests,
$pane_id);
$main_pane = phutil_render_tag(
'div',
array(
'class' => 'differential-primary-pane',
'id' => $pane_id
),
$change_list->render().
$add_comment_view);
$content[] = $main_pane;
}
return $this->buildStandardPageResponse(
$content,
array(
'title' => 'r'.$callsign.$commit->getCommitIdentifier(),
));
}
private function getCommitProperties(
PhabricatorRepositoryCommit $commit,
PhabricatorRepositoryCommitData $data,
array $parents) {
assert_instances_of($parents, 'PhabricatorRepositoryCommit');
$user = $this->getRequest()->getUser();
$commit_phid = $commit->getPHID();
$edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($commit_phid))
->withEdgeTypes(array(
PhabricatorEdgeConfig::TYPE_COMMIT_HAS_TASK,
PhabricatorEdgeConfig::TYPE_COMMIT_HAS_PROJECT
))
->execute();
$task_phids = array_keys(
$edges[$commit_phid][PhabricatorEdgeConfig::TYPE_COMMIT_HAS_TASK]
);
$proj_phids = array_keys(
$edges[$commit_phid][PhabricatorEdgeConfig::TYPE_COMMIT_HAS_PROJECT]
);
$phids = array_merge($task_phids, $proj_phids);
if ($data->getCommitDetail('authorPHID')) {
$phids[] = $data->getCommitDetail('authorPHID');
}
if ($data->getCommitDetail('reviewerPHID')) {
$phids[] = $data->getCommitDetail('reviewerPHID');
}
if ($data->getCommitDetail('committerPHID')) {
$phids[] = $data->getCommitDetail('committerPHID');
}
if ($data->getCommitDetail('differential.revisionPHID')) {
$phids[] = $data->getCommitDetail('differential.revisionPHID');
}
if ($parents) {
foreach ($parents as $parent) {
$phids[] = $parent->getPHID();
}
}
$handles = array();
if ($phids) {
$handles = $this->loadViewerHandles($phids);
}
$props = array();
if ($commit->getAuditStatus()) {
$status = PhabricatorAuditCommitStatusConstants::getStatusName(
$commit->getAuditStatus());
$props['Status'] = phutil_render_tag(
'strong',
array(),
phutil_escape_html($status));
}
$props['Committed'] = phabricator_datetime($commit->getEpoch(), $user);
$author_phid = $data->getCommitDetail('authorPHID');
if ($data->getCommitDetail('authorPHID')) {
$props['Author'] = $handles[$author_phid]->renderLink();
} else {
$props['Author'] = phutil_escape_html($data->getAuthorName());
}
$reviewer_phid = $data->getCommitDetail('reviewerPHID');
$reviewer_name = $data->getCommitDetail('reviewerName');
if ($reviewer_phid) {
$props['Reviewer'] = $handles[$reviewer_phid]->renderLink();
} else if ($reviewer_name) {
$props['Reviewer'] = phutil_escape_html($reviewer_name);
}
$committer = $data->getCommitDetail('committer');
if ($committer) {
$committer_phid = $data->getCommitDetail('committerPHID');
if ($data->getCommitDetail('committerPHID')) {
$props['Committer'] = $handles[$committer_phid]->renderLink();
} else {
$props['Committer'] = phutil_escape_html($committer);
}
}
$revision_phid = $data->getCommitDetail('differential.revisionPHID');
if ($revision_phid) {
$props['Differential Revision'] = $handles[$revision_phid]->renderLink();
}
if ($parents) {
$parent_links = array();
foreach ($parents as $parent) {
$parent_links[] = $handles[$parent->getPHID()]->renderLink();
}
$props['Parents'] = implode(' &middot; ', $parent_links);
}
$request = $this->getDiffusionRequest();
$props['Branches'] = '<span id="commit-branches">Unknown</span>';
$props['Tags'] = '<span id="commit-tags">Unknown</span>';
$callsign = $request->getRepository()->getCallsign();
$root = '/diffusion/'.$callsign.'/commit/'.$commit->getCommitIdentifier();
Javelin::initBehavior(
'diffusion-commit-branches',
array(
$root.'/branches/' => 'commit-branches',
$root.'/tags/' => 'commit-tags',
));
$refs = $this->buildRefs($request);
if ($refs) {
$props['References'] = $refs;
}
if ($task_phids) {
$task_list = array();
foreach ($task_phids as $phid) {
$task_list[] = $handles[$phid]->renderLink();
}
$task_list = implode('<br />', $task_list);
$props['Tasks'] = $task_list;
}
if ($proj_phids) {
$proj_list = array();
foreach ($proj_phids as $phid) {
$proj_list[] = $handles[$phid]->renderLink();
}
$proj_list = implode('<br />', $proj_list);
$props['Projects'] = $proj_list;
}
return $props;
}
private function buildAuditTable(
PhabricatorRepositoryCommit $commit,
array $audits) {
assert_instances_of($audits, 'PhabricatorRepositoryAuditRequest');
$user = $this->getRequest()->getUser();
$view = new PhabricatorAuditListView();
$view->setAudits($audits);
$view->setCommits(array($commit));
$view->setUser($user);
$view->setShowDescriptions(false);
$phids = $view->getRequiredHandlePHIDs();
$handles = $this->loadViewerHandles($phids);
$view->setHandles($handles);
$view->setAuthorityPHIDs($this->auditAuthorityPHIDs);
$this->highlightedAudits = $view->getHighlightedAudits();
$panel = new AphrontPanelView();
$panel->setHeader('Audits');
$panel->setCaption('Audits you are responsible for are highlighted.');
$panel->appendChild($view);
return $panel;
}
private function buildComments(PhabricatorRepositoryCommit $commit) {
$user = $this->getRequest()->getUser();
$comments = id(new PhabricatorAuditComment())->loadAllWhere(
'targetPHID = %s ORDER BY dateCreated ASC',
$commit->getPHID());
$inlines = id(new PhabricatorAuditInlineComment())->loadAllWhere(
'commitPHID = %s AND auditCommentID IS NOT NULL',
$commit->getPHID());
$path_ids = mpull($inlines, 'getPathID');
$path_map = array();
if ($path_ids) {
$path_map = id(new DiffusionPathQuery())
->withPathIDs($path_ids)
->execute();
$path_map = ipull($path_map, 'path', 'id');
}
$engine = new PhabricatorMarkupEngine();
$engine->setViewer($user);
foreach ($comments as $comment) {
$engine->addObject(
$comment,
PhabricatorAuditComment::MARKUP_FIELD_BODY);
}
foreach ($inlines as $inline) {
$engine->addObject(
$inline,
PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY);
}
$engine->process();
$view = new DiffusionCommentListView();
$view->setMarkupEngine($engine);
$view->setUser($user);
$view->setComments($comments);
$view->setInlineComments($inlines);
$view->setPathMap($path_map);
$phids = $view->getRequiredHandlePHIDs();
$handles = $this->loadViewerHandles($phids);
$view->setHandles($handles);
return $view;
}
private function renderAddCommentPanel(
PhabricatorRepositoryCommit $commit,
array $audit_requests,
$pane_id = null) {
assert_instances_of($audit_requests, 'PhabricatorRepositoryAuditRequest');
$user = $this->getRequest()->getUser();
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
Javelin::initBehavior(
'differential-keyboard-navigation',
array(
'haunt' => $pane_id,
));
$draft = id(new PhabricatorDraft())->loadOneWhere(
'authorPHID = %s AND draftKey = %s',
$user->getPHID(),
'diffusion-audit-'.$commit->getID());
if ($draft) {
$draft = $draft->getDraft();
} else {
$draft = null;
}
$actions = $this->getAuditActions($commit, $audit_requests);
$form = id(new AphrontFormView())
->setUser($user)
->setAction('/audit/addcomment/')
->addHiddenInput('commit', $commit->getPHID())
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Action')
->setName('action')
->setID('audit-action')
->setOptions($actions))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel('Add Auditors')
->setName('auditors')
->setControlID('add-auditors')
->setControlStyle('display: none')
->setID('add-auditors-tokenizer')
->setDisableBehavior(true))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel('Add CCs')
->setName('ccs')
->setControlID('add-ccs')
->setControlStyle('display: none')
->setID('add-ccs-tokenizer')
->setDisableBehavior(true))
->appendChild(
id(new PhabricatorRemarkupControl())
->setLabel('Comments')
->setName('content')
->setValue($draft)
->setID('audit-content'))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue($is_serious ? 'Submit' : 'Cook the Books'));
$panel = new AphrontPanelView();
$panel->setHeader($is_serious ? 'Audit Commit' : 'Creative Accounting');
$panel->appendChild($form);
$panel->addClass('aphront-panel-accent');
$panel->addClass('aphront-panel-flush');
require_celerity_resource('phabricator-transaction-view-css');
Javelin::initBehavior(
'differential-add-reviewers-and-ccs',
array(
'dynamic' => array(
'add-auditors-tokenizer' => array(
'actions' => array('add_auditors' => 1),
'src' => '/typeahead/common/users/',
'row' => 'add-auditors',
'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'),
'placeholder' => 'Type a user name...',
),
'add-ccs-tokenizer' => array(
'actions' => array('add_ccs' => 1),
'src' => '/typeahead/common/mailable/',
'row' => 'add-ccs',
'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'),
'placeholder' => 'Type a user or mailing list...',
),
),
'select' => 'audit-action',
));
Javelin::initBehavior('differential-feedback-preview', array(
'uri' => '/audit/preview/'.$commit->getID().'/',
'preview' => 'audit-preview',
'content' => 'audit-content',
'action' => 'audit-action',
'previewTokenizers' => array(
'auditors' => 'add-auditors-tokenizer',
'ccs' => 'add-ccs-tokenizer',
),
'inline' => 'inline-comment-preview',
'inlineuri' => '/diffusion/inline/preview/'.$commit->getPHID().'/',
));
$preview_panel =
'<div class="aphront-panel-preview aphront-panel-flush">
<div id="audit-preview">
<div class="aphront-panel-preview-loading-text">
Loading preview...
</div>
</div>
<div id="inline-comment-preview">
</div>
</div>';
return
phutil_render_tag(
'div',
array(
'class' => 'differential-add-comment-panel',
),
$panel->render().
$preview_panel);
}
/**
* Return a map of available audit actions for rendering into a <select />.
* This shows the user valid actions, and does not show nonsense/invalid
* actions (like closing an already-closed commit, or resigning from a commit
* you have no association with).
*/
private function getAuditActions(
PhabricatorRepositoryCommit $commit,
array $audit_requests) {
assert_instances_of($audit_requests, 'PhabricatorRepositoryAuditRequest');
$user = $this->getRequest()->getUser();
$user_is_author = ($commit->getAuthorPHID() == $user->getPHID());
$user_request = null;
foreach ($audit_requests as $audit_request) {
if ($audit_request->getAuditorPHID() == $user->getPHID()) {
$user_request = $audit_request;
break;
}
}
$actions = array();
$actions[PhabricatorAuditActionConstants::COMMENT] = true;
$actions[PhabricatorAuditActionConstants::ADD_CCS] = true;
$actions[PhabricatorAuditActionConstants::ADD_AUDITORS] = true;
// We allow you to accept your own commits. A use case here is that you
// notice an issue with your own commit and "Raise Concern" as an indicator
// to other auditors that you're on top of the issue, then later resolve it
// and "Accept". You can not accept on behalf of projects or packages,
// however.
$actions[PhabricatorAuditActionConstants::ACCEPT] = true;
$actions[PhabricatorAuditActionConstants::CONCERN] = true;
// To resign, a user must have authority on some request and not be the
// commit's author.
if (!$user_is_author) {
$may_resign = false;
$authority_map = array_fill_keys($this->auditAuthorityPHIDs, true);
foreach ($audit_requests as $request) {
if (empty($authority_map[$request->getAuditorPHID()])) {
continue;
}
$may_resign = true;
break;
}
// If the user has already resigned, don't show "Resign...".
$status_resigned = PhabricatorAuditStatusConstants::RESIGNED;
if ($user_request) {
if ($user_request->getAuditStatus() == $status_resigned) {
$may_resign = false;
}
}
if ($may_resign) {
$actions[PhabricatorAuditActionConstants::RESIGN] = true;
}
}
$status_concern = PhabricatorAuditCommitStatusConstants::CONCERN_RAISED;
$concern_raised = ($commit->getAuditStatus() == $status_concern);
if ($user_is_author && $concern_raised) {
$actions[PhabricatorAuditActionConstants::CLOSE] = true;
}
foreach ($actions as $constant => $ignored) {
$actions[$constant] =
PhabricatorAuditActionConstants::getActionName($constant);
}
return $actions;
}
private function buildMergesTable(PhabricatorRepositoryCommit $commit) {
$drequest = $this->getDiffusionRequest();
$limit = 50;
$merge_query = DiffusionMergedCommitsQuery::newFromDiffusionRequest(
$drequest);
$merge_query->setLimit($limit + 1);
$merges = $merge_query->loadMergedCommits();
if (!$merges) {
return null;
}
$caption = null;
if (count($merges) > $limit) {
$merges = array_slice($merges, 0, $limit);
$caption =
"This commit merges more than {$limit} changes. Only the first ".
"{$limit} are shown.";
}
$history_table = new DiffusionHistoryTableView();
$history_table->setDiffusionRequest($drequest);
$history_table->setHistory($merges);
$history_table->loadRevisions();
$phids = $history_table->getRequiredHandlePHIDs();
$handles = $this->loadViewerHandles($phids);
$history_table->setHandles($handles);
$panel = new AphrontPanelView();
$panel->setHeader('Merged Changes');
$panel->setCaption($caption);
$panel->appendChild($history_table);
return $panel;
}
private function renderHeadsupActionList(
PhabricatorRepositoryCommit $commit,
PhabricatorRepository $repository) {
$request = $this->getRequest();
$user = $request->getUser();
$actions = array();
// TODO -- integrate permissions into whether or not this action is shown
$uri = '/diffusion/'.$repository->getCallSign().'/commit/'.
$commit->getCommitIdentifier().'/edit/';
$action = new AphrontHeadsupActionView();
$action->setClass('action-edit');
$action->setURI($uri);
$action->setName('Edit Commit');
$action->setWorkflow(false);
$actions[] = $action;
require_celerity_resource('phabricator-flag-css');
$flag = PhabricatorFlagQuery::loadUserFlag($user, $commit->getPHID());
if ($flag) {
$class = PhabricatorFlagColor::getCSSClass($flag->getColor());
$color = PhabricatorFlagColor::getColorName($flag->getColor());
$action = new AphrontHeadsupActionView();
$action->setClass('flag-clear '.$class);
$action->setURI('/flag/delete/'.$flag->getID().'/');
$action->setName('Remove '.$color.' Flag');
$action->setWorkflow(true);
$actions[] = $action;
} else {
$action = new AphrontHeadsupActionView();
$action->setClass('phabricator-flag-ghost');
$action->setURI('/flag/edit/'.$commit->getPHID().'/');
$action->setName('Flag Commit');
$action->setWorkflow(true);
$actions[] = $action;
}
require_celerity_resource('phabricator-object-selector-css');
require_celerity_resource('javelin-behavior-phabricator-object-selector');
if (PhabricatorEnv::getEnvConfig('maniphest.enabled')) {
$action = new AphrontHeadsupActionView();
$action->setName('Edit Maniphest Tasks');
$action->setURI('/search/attach/'.$commit->getPHID().'/TASK/edge/');
$action->setWorkflow(true);
$action->setClass('attach-maniphest');
$actions[] = $action;
}
if ($user->getIsAdmin()) {
$action = new AphrontHeadsupActionView();
$action->setName('MetaMTA Transcripts');
$action->setURI('/mail/?phid='.$commit->getPHID());
$action->setClass('transcripts-metamta');
$actions[] = $action;
}
$action = new AphrontHeadsupActionView();
$action->setName('Herald Transcripts');
$action->setURI('/herald/transcript/?phid='.$commit->getPHID());
$action->setClass('transcripts-herald');
$actions[] = $action;
$action = new AphrontHeadsupActionView();
$action->setName('Download Raw Diff');
$action->setURI($request->getRequestURI()->alter('diff', true));
$action->setClass('action-download');
$actions[] = $action;
$action_list = new AphrontHeadsupActionListView();
$action_list->setActions($actions);
return $action_list;
}
private function buildRefs(DiffusionRequest $request) {
// Not turning this into a proper Query class since it's pretty simple,
// one-off, and Git-specific.
$type_git = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
$repository = $request->getRepository();
if ($repository->getVersionControlSystem() != $type_git) {
return null;
}
list($stdout) = $repository->execxLocalCommand(
'log --format=%s -n 1 %s --',
'%d',
$request->getCommit());
// %d, gives a weird output format
// similar to (remote/one, remote/two, remote/three)
$refs = trim($stdout, "() \n");
if (!$refs) {
return null;
}
$refs = explode(',', $refs);
$refs = array_map('trim', $refs);
$ref_links = array();
foreach ($refs as $ref) {
$ref_links[] = phutil_render_tag(
'a',
array(
'href' => $request->generateURI(
array(
'action' => 'browse',
'branch' => $ref,
)),
),
phutil_escape_html($ref));
}
$ref_links = implode(', ', $ref_links);
return $ref_links;
}
private function buildRawDiffResponse(DiffusionRequest $drequest) {
$raw_query = DiffusionRawDiffQuery::newFromDiffusionRequest($drequest);
$raw_diff = $raw_query->loadRawDiff();
$file = PhabricatorFile::buildFromFileDataOrHash(
$raw_diff,
array(
'name' => $drequest->getCommit().'.diff',
));
return id(new AphrontRedirectResponse())->setURI($file->getBestURI());
}
}
diff --git a/src/applications/diffusion/controller/DiffusionCommitEditController.php b/src/applications/diffusion/controller/DiffusionCommitEditController.php
index c927c79bf9..49aeaf9acc 100644
--- a/src/applications/diffusion/controller/DiffusionCommitEditController.php
+++ b/src/applications/diffusion/controller/DiffusionCommitEditController.php
@@ -1,111 +1,95 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionCommitEditController extends DiffusionController {
public function willProcessRequest(array $data) {
$this->diffusionRequest = DiffusionRequest::newFromDictionary($data);
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$drequest = $this->getDiffusionRequest();
$callsign = $drequest->getRepository()->getCallsign();
$repository = $drequest->getRepository();
$commit = $drequest->loadCommit();
$page_title = 'Edit Diffusion Commit';
if (!$commit) {
return new Aphront404Response();
}
$commit_phid = $commit->getPHID();
$edge_type = PhabricatorEdgeConfig::TYPE_COMMIT_HAS_PROJECT;
$current_proj_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$commit_phid,
$edge_type
);
$handles = $this->loadViewerHandles($current_proj_phids);
$proj_t_values = mpull($handles, 'getFullName', 'getPHID');
if ($request->isFormPost()) {
$proj_phids = $request->getArr('projects');
$new_proj_phids = array_values($proj_phids);
$rem_proj_phids = array_diff($current_proj_phids,
$new_proj_phids);
$editor = id(new PhabricatorEdgeEditor());
$editor->setActor($user);
foreach ($rem_proj_phids as $phid) {
$editor->removeEdge($commit_phid, $edge_type, $phid);
}
foreach ($new_proj_phids as $phid) {
$editor->addEdge($commit_phid, $edge_type, $phid);
}
$editor->save();
PhabricatorSearchCommitIndexer::indexCommit($commit);
return id(new AphrontRedirectResponse())
->setURI('/r'.$callsign.$commit->getCommitIdentifier());
}
$tokenizer_id = celerity_generate_unique_node_id();
$form = id(new AphrontFormView())
->setUser($user)
->setAction($request->getRequestURI()->getPath())
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel('Projects')
->setName('projects')
->setValue($proj_t_values)
->setID($tokenizer_id)
->setCaption(
javelin_render_tag(
'a',
array(
'href' => '/project/create/',
'mustcapture' => true,
'sigil' => 'project-create',
),
'Create New Project'))
->setDatasource('/typeahead/common/projects/'));;
Javelin::initBehavior('project-create', array(
'tokenizerID' => $tokenizer_id,
));
$submit = id(new AphrontFormSubmitControl())
->setValue('Save')
->addCancelButton('/r'.$callsign.$commit->getCommitIdentifier());
$form->appendChild($submit);
$panel = id(new AphrontPanelView())
->setHeader('Edit Diffusion Commit')
->appendChild($form)
->setWidth(AphrontPanelView::WIDTH_FORM);
return $this->buildStandardPageResponse(
$panel,
array(
'title' => $page_title,
));
}
}
diff --git a/src/applications/diffusion/controller/DiffusionCommitTagsController.php b/src/applications/diffusion/controller/DiffusionCommitTagsController.php
index 14747f927f..fea9d58716 100644
--- a/src/applications/diffusion/controller/DiffusionCommitTagsController.php
+++ b/src/applications/diffusion/controller/DiffusionCommitTagsController.php
@@ -1,65 +1,49 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionCommitTagsController extends DiffusionController {
public function willProcessRequest(array $data) {
$this->diffusionRequest = DiffusionRequest::newFromDictionary($data);
}
public function processRequest() {
$request = $this->getDiffusionRequest();
$tag_limit = 10;
$tag_query = DiffusionCommitTagsQuery::newFromDiffusionRequest($request);
$tag_query->setLimit($tag_limit + 1);
$tags = $tag_query->loadTags();
$has_more_tags = (count($tags) > $tag_limit);
$tags = array_slice($tags, 0, $tag_limit);
$tag_links = array();
foreach ($tags as $tag) {
$tag_links[] = phutil_render_tag(
'a',
array(
'href' => $request->generateURI(
array(
'action' => 'browse',
'commit' => $tag->getName(),
)),
),
phutil_escape_html($tag->getName()));
}
if ($has_more_tags) {
$tag_links[] = phutil_render_tag(
'a',
array(
'href' => $request->generateURI(
array(
'action' => 'tags',
)),
),
"More tags\xE2\x80\xA6");
}
return id(new AphrontAjaxResponse())
->setContent($tag_links ? implode(', ', $tag_links) : 'None');
}
}
diff --git a/src/applications/diffusion/controller/DiffusionController.php b/src/applications/diffusion/controller/DiffusionController.php
index ab9e7e1103..1f86170fff 100644
--- a/src/applications/diffusion/controller/DiffusionController.php
+++ b/src/applications/diffusion/controller/DiffusionController.php
@@ -1,334 +1,318 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DiffusionController extends PhabricatorController {
protected $diffusionRequest;
public function willProcessRequest(array $data) {
if (isset($data['callsign'])) {
$drequest = DiffusionRequest::newFromAphrontRequestDictionary($data);
$this->diffusionRequest = $drequest;
}
}
public function setDiffusionRequest(DiffusionRequest $request) {
$this->diffusionRequest = $request;
return $this;
}
protected function getDiffusionRequest() {
if (!$this->diffusionRequest) {
throw new Exception("No Diffusion request object!");
}
return $this->diffusionRequest;
}
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('Diffusion');
$page->setBaseURI('/diffusion/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph("\xE2\x89\x88");
$page->setSearchDefaultScope(PhabricatorSearchScope::SCOPE_COMMITS);
$page->appendChild($view);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
final protected function buildSideNav($selected, $has_change_view) {
$nav = new AphrontSideNavView();
$navs = array(
'history' => 'History View',
'browse' => 'Browse View',
'change' => 'Change View',
);
if (!$has_change_view) {
unset($navs['change']);
}
$drequest = $this->getDiffusionRequest();
foreach ($navs as $action => $name) {
$href = $drequest->generateURI(
array(
'action' => $action,
));
$nav->addNavItem(
phutil_render_tag(
'a',
array(
'href' => $href,
'class' =>
($action == $selected
? 'aphront-side-nav-selected'
: null),
),
$name));
}
// TODO: URI encoding might need to be sorted out for this link.
$nav->addNavItem(
phutil_render_tag(
'a',
array(
'href' => '/owners/view/search/'.
'?repository='.phutil_escape_uri($drequest->getCallsign()).
'&path='.phutil_escape_uri('/'.$drequest->getPath()),
),
'Search Owners'));
return $nav;
}
public function buildCrumbs(array $spec = array()) {
$crumbs = new AphrontCrumbsView();
$crumb_list = $this->buildCrumbList($spec);
$crumbs->setCrumbs($crumb_list);
return $crumbs;
}
protected function buildOpenRevisions() {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$path = $drequest->getPath();
$path_map = id(new DiffusionPathIDQuery(array($path)))->loadPathIDs();
$path_id = idx($path_map, $path);
if (!$path_id) {
return null;
}
$revisions = id(new DifferentialRevisionQuery())
->withPath($repository->getID(), $path_id)
->withStatus(DifferentialRevisionQuery::STATUS_OPEN)
->setOrder(DifferentialRevisionQuery::ORDER_PATH_MODIFIED)
->setLimit(10)
->needRelationships(true)
->execute();
if (!$revisions) {
return null;
}
$view = id(new DifferentialRevisionListView())
->setRevisions($revisions)
->setFields(DifferentialRevisionListView::getDefaultFields())
->setUser($this->getRequest()->getUser())
->loadAssets();
$phids = $view->getRequiredHandlePHIDs();
$handles = $this->loadViewerHandles($phids);
$view->setHandles($handles);
$panel = new AphrontPanelView();
$panel->setId('pending-differential-revisions');
$panel->setHeader('Pending Differential Revisions');
$panel->appendChild($view);
return $panel;
}
private function buildCrumbList(array $spec = array()) {
$spec = $spec + array(
'commit' => null,
'tags' => null,
'branches' => null,
'view' => null,
);
$crumb_list = array();
// On the home page, we don't have a DiffusionRequest.
if ($this->diffusionRequest) {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
} else {
$drequest = null;
$repository = null;
}
if ($repository) {
$crumb_list[] = phutil_render_tag(
'a',
array(
'href' => '/diffusion/',
),
'Diffusion');
} else {
$crumb_list[] = 'Diffusion';
return $crumb_list;
}
$callsign = $repository->getCallsign();
$repository_name = phutil_escape_html($repository->getName()).' Repository';
if (!$spec['commit'] && !$spec['tags'] && !$spec['branches']) {
$branch_name = $drequest->getBranch();
if ($branch_name) {
$repository_name .= ' ('.phutil_escape_html($branch_name).')';
}
}
if (!$spec['view'] && !$spec['commit']
&& !$spec['tags'] && !$spec['branches']) {
$crumb_list[] = $repository_name;
return $crumb_list;
}
$crumb_list[] = phutil_render_tag(
'a',
array(
'href' => "/diffusion/{$callsign}/",
),
$repository_name);
$raw_commit = $drequest->getRawCommit();
if ($spec['tags']) {
if ($spec['commit']) {
$crumb_list[] = "Tags for ".phutil_render_tag(
'a',
array(
'href' => $drequest->generateURI(
array(
'action' => 'commit',
'commit' => $raw_commit,
)),
),
phutil_escape_html("r{$callsign}{$raw_commit}"));
} else {
$crumb_list[] = 'Tags';
}
return $crumb_list;
}
if ($spec['branches']) {
$crumb_list[] = 'Branches';
return $crumb_list;
}
if ($spec['commit']) {
$crumb_list[] = "r{$callsign}{$raw_commit}";
return $crumb_list;
}
$view = $spec['view'];
$path = null;
if (isset($spec['path'])) {
$path = $drequest->getPath();
}
if ($raw_commit) {
$commit_link = DiffusionView::linkCommit(
$repository,
$raw_commit);
} else {
$commit_link = '';
}
switch ($view) {
case 'history':
$view_name = 'History';
break;
case 'browse':
$view_name = 'Browse';
break;
case 'change':
$view_name = 'Change';
$crumb_list[] = phutil_escape_html($path).' ('.$commit_link.')';
return $crumb_list;
}
$uri_params = array(
'action' => $view,
);
if (!strlen($path)) {
$crumb_list[] = $view_name;
} else {
$crumb_list[] = phutil_render_tag(
'a',
array(
'href' => $drequest->generateURI(
array(
'path' => '',
) + $uri_params),
),
$view_name);
$path_parts = explode('/', $path);
do {
$last = array_pop($path_parts);
} while ($last == '');
$path_sections = array();
$thus_far = '';
foreach ($path_parts as $path_part) {
$thus_far .= $path_part.'/';
$path_sections[] = phutil_render_tag(
'a',
array(
'href' => $drequest->generateURI(
array(
'path' => $thus_far,
) + $uri_params),
),
phutil_escape_html($path_part));
}
$path_sections[] = phutil_escape_html($last);
$path_sections = '/'.implode('/', $path_sections);
$crumb_list[] = $path_sections;
}
$last_crumb = array_pop($crumb_list);
if ($raw_commit) {
$jump_link = phutil_render_tag(
'a',
array(
'href' => $drequest->generateURI(
array(
'commit' => '',
) + $uri_params),
),
'Jump to HEAD');
$last_crumb .= " @ {$commit_link} ({$jump_link})";
} else {
$last_crumb .= " @ HEAD";
}
$crumb_list[] = $last_crumb;
return $crumb_list;
}
}
diff --git a/src/applications/diffusion/controller/DiffusionDiffController.php b/src/applications/diffusion/controller/DiffusionDiffController.php
index 41a548e0f7..fc8bc03ab9 100644
--- a/src/applications/diffusion/controller/DiffusionDiffController.php
+++ b/src/applications/diffusion/controller/DiffusionDiffController.php
@@ -1,121 +1,105 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionDiffController extends DiffusionController {
public function willProcessRequest(array $data) {
$data = $data + array(
'dblob' => $this->getRequest()->getStr('ref'),
);
$drequest = DiffusionRequest::newFromAphrontRequestDictionary($data);
$this->diffusionRequest = $drequest;
}
public function processRequest() {
$drequest = $this->getDiffusionRequest();
$request = $this->getRequest();
$user = $request->getUser();
if (!$request->isAjax()) {
// This request came out of the dropdown menu, either "View Standalone"
// or "View Raw File".
$view = $request->getStr('view');
if ($view == 'r') {
$uri = $drequest->generateURI(
array(
'action' => 'browse',
'params' => array(
'view' => 'raw',
),
));
} else {
$uri = $drequest->generateURI(
array(
'action' => 'change',
));
}
return id(new AphrontRedirectResponse())->setURI($uri);
}
$diff_query = DiffusionDiffQuery::newFromDiffusionRequest($drequest);
$changeset = $diff_query->loadChangeset();
if (!$changeset) {
return new Aphront404Response();
}
$parser = new DifferentialChangesetParser();
$parser->setUser($user);
$parser->setChangeset($changeset);
$parser->setRenderingReference($diff_query->getRenderingReference());
$pquery = new DiffusionPathIDQuery(array($changeset->getFilename()));
$ids = $pquery->loadPathIDs();
$path_id = $ids[$changeset->getFilename()];
$parser->setLeftSideCommentMapping($path_id, false);
$parser->setRightSideCommentMapping($path_id, true);
$parser->setWhitespaceMode(
DifferentialChangesetParser::WHITESPACE_SHOW_ALL);
$inlines = id(new PhabricatorAuditInlineComment())->loadAllWhere(
'commitPHID = %s AND pathID = %d AND
(authorPHID = %s OR auditCommentID IS NOT NULL)',
$drequest->loadCommit()->getPHID(),
$path_id,
$user->getPHID());
if ($inlines) {
foreach ($inlines as $inline) {
$parser->parseInlineComment($inline);
}
$phids = mpull($inlines, 'getAuthorPHID');
$handles = $this->loadViewerHandles($phids);
$parser->setHandles($handles);
}
$engine = new PhabricatorMarkupEngine();
$engine->setViewer($user);
foreach ($inlines as $inline) {
$engine->addObject(
$inline,
PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY);
}
$engine->process();
$parser->setMarkupEngine($engine);
$spec = $request->getStr('range');
list($range_s, $range_e, $mask) =
DifferentialChangesetParser::parseRangeSpecification($spec);
$output = $parser->render($range_s, $range_e, $mask);
return id(new PhabricatorChangesetResponse())
->setRenderedChangeset($output);
}
}
diff --git a/src/applications/diffusion/controller/DiffusionExternalController.php b/src/applications/diffusion/controller/DiffusionExternalController.php
index ea1bc24ae2..1f3fc6c6b2 100644
--- a/src/applications/diffusion/controller/DiffusionExternalController.php
+++ b/src/applications/diffusion/controller/DiffusionExternalController.php
@@ -1,151 +1,135 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionExternalController extends DiffusionController {
public function willProcessRequest(array $data) {
// Don't build a DiffusionRequest.
}
public function processRequest() {
$request = $this->getRequest();
$uri = $request->getStr('uri');
$id = $request->getStr('id');
$repositories = id(new PhabricatorRepository())->loadAll();
if ($uri) {
$uri_path = id(new PhutilURI($uri))->getPath();
$matches = array();
// Try to figure out which tracked repository this external lives in by
// comparing repository metadata. We look for an exact match, but accept
// a partial match.
foreach ($repositories as $key => $repository) {
$remote_uri = new PhutilURI($repository->getRemoteURI());
if ($remote_uri->getPath() == $uri_path) {
$matches[$key] = 1;
}
if ($repository->getPublicRemoteURI() == $uri) {
$matches[$key] = 2;
}
if ($repository->getRemoteURI() == $uri) {
$matches[$key] = 3;
}
}
arsort($matches);
$best_match = head_key($matches);
if ($best_match) {
$repository = $repositories[$best_match];
$redirect = DiffusionRequest::generateDiffusionURI(
array(
'action' => 'browse',
'callsign' => $repository->getCallsign(),
'branch' => $repository->getDefaultBranch(),
'commit' => $id,
));
return id(new AphrontRedirectResponse())->setURI($redirect);
}
}
// TODO: This is a rare query but does a table scan, add a key?
$commits = id(new PhabricatorRepositoryCommit())->loadAllWhere(
'commitIdentifier = %s',
$id);
if (empty($commits)) {
$desc = null;
if ($uri) {
$desc = phutil_escape_html($uri).', at ';
}
$desc .= phutil_escape_html($id);
$content = id(new AphrontErrorView())
->setTitle('Unknown External')
->setSeverity(AphrontErrorView::SEVERITY_WARNING)
->appendChild(
"<p>This external ({$desc}) does not appear in any tracked ".
"repository. It may exist in an untracked repository that ".
"Diffusion does not know about.</p>");
} else if (count($commits) == 1) {
$commit = head($commits);
$repo = $repositories[$commit->getRepositoryID()];
$redirect = DiffusionRequest::generateDiffusionURI(
array(
'action' => 'browse',
'callsign' => $repo->getCallsign(),
'branch' => $repo->getDefaultBranch(),
'commit' => $commit->getCommitIdentifier(),
));
return id(new AphrontRedirectResponse())->setURI($redirect);
} else {
$rows = array();
foreach ($commits as $commit) {
$repo = $repositories[$commit->getRepositoryID()];
$href = DiffusionRequest::generateDiffusionURI(
array(
'action' => 'browse',
'callsign' => $repo->getCallsign(),
'branch' => $repo->getDefaultBranch(),
'commit' => $commit->getCommitIdentifier(),
));
$rows[] = array(
phutil_render_tag(
'a',
array(
'href' => $href,
),
phutil_escape_html(
'r'.$repo->getCallsign().$commit->getCommitIdentifier())),
phutil_escape_html($commit->loadCommitData()->getSummary()),
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Commit',
'Description',
));
$table->setColumnClasses(
array(
'pri',
'wide',
));
$content = new AphrontPanelView();
$content->setHeader('Multiple Matching Commits');
$content->setCaption(
'This external reference matches multiple known commits.');
$content->appendChild($table);
}
return $this->buildStandardPageResponse(
$content,
array(
'title' => 'Unresolvable External',
));
}
}
diff --git a/src/applications/diffusion/controller/DiffusionHistoryController.php b/src/applications/diffusion/controller/DiffusionHistoryController.php
index f3df297894..ced077881a 100644
--- a/src/applications/diffusion/controller/DiffusionHistoryController.php
+++ b/src/applications/diffusion/controller/DiffusionHistoryController.php
@@ -1,116 +1,100 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionHistoryController extends DiffusionController {
public function processRequest() {
$drequest = $this->diffusionRequest;
$request = $this->getRequest();
$page_size = $request->getInt('pagesize', 100);
$offset = $request->getInt('page', 0);
$history_query = DiffusionHistoryQuery::newFromDiffusionRequest(
$drequest);
$history_query->setOffset($offset);
$history_query->setLimit($page_size + 1);
if (!$request->getBool('copies')) {
$history_query->needDirectChanges(true);
$history_query->needChildChanges(true);
}
$show_graph = !strlen($drequest->getPath());
if ($show_graph) {
$history_query->needParents(true);
}
$history = $history_query->loadHistory();
$pager = new AphrontPagerView();
$pager->setPageSize($page_size);
$pager->setOffset($offset);
if (count($history) == $page_size + 1) {
array_pop($history);
$pager->setHasMorePages(true);
} else {
$pager->setHasMorePages(false);
}
$pager->setURI($request->getRequestURI(), 'page');
$content = array();
$content[] = $this->buildCrumbs(
array(
'branch' => true,
'path' => true,
'view' => 'history',
));
if ($request->getBool('copies')) {
$button_title = 'Hide Copies/Branches';
$copies_new = null;
} else {
$button_title = 'Show Copies/Branches';
$copies_new = true;
}
$button = phutil_render_tag(
'a',
array(
'class' => 'button small grey',
'href' => $request->getRequestURI()->alter('copies', $copies_new),
),
phutil_escape_html($button_title));
$history_table = new DiffusionHistoryTableView();
$history_table->setDiffusionRequest($drequest);
$history_table->setHistory($history);
$history_table->loadRevisions();
$phids = $history_table->getRequiredHandlePHIDs();
$handles = $this->loadViewerHandles($phids);
$history_table->setHandles($handles);
if ($show_graph) {
$history_table->setParents($history_query->getParents());
$history_table->setIsHead($offset == 0);
}
$history_panel = new AphrontPanelView();
$history_panel->setHeader('History');
$history_panel->addButton($button);
$history_panel->appendChild($history_table);
$history_panel->appendChild($pager);
$content[] = $history_panel;
// TODO: Sometimes we do have a change view, we need to look at the most
// recent history entry to figure it out.
$nav = $this->buildSideNav('history', false);
$nav->appendChild($content);
return $this->buildStandardPageResponse(
$nav,
array(
'title' => 'history',
));
}
}
diff --git a/src/applications/diffusion/controller/DiffusionHomeController.php b/src/applications/diffusion/controller/DiffusionHomeController.php
index 0872a42831..b11eec881d 100644
--- a/src/applications/diffusion/controller/DiffusionHomeController.php
+++ b/src/applications/diffusion/controller/DiffusionHomeController.php
@@ -1,180 +1,164 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionHomeController extends DiffusionController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$shortcuts = id(new PhabricatorRepositoryShortcut())->loadAll();
if ($shortcuts) {
$shortcuts = msort($shortcuts, 'getSequence');
$rows = array();
foreach ($shortcuts as $shortcut) {
$rows[] = array(
phutil_render_tag(
'a',
array(
'href' => $shortcut->getHref(),
),
phutil_escape_html($shortcut->getName())),
phutil_escape_html($shortcut->getDescription()),
);
}
$shortcut_table = new AphrontTableView($rows);
$shortcut_table->setHeaders(
array(
'Link',
'',
));
$shortcut_table->setColumnClasses(
array(
'pri',
'wide',
));
$shortcut_panel = new AphrontPanelView();
$shortcut_panel->setHeader('Shortcuts');
$shortcut_panel->appendChild($shortcut_table);
} else {
$shortcut_panel = null;
}
$repository = new PhabricatorRepository();
$repositories = $repository->loadAll();
foreach ($repositories as $key => $repo) {
if (!$repo->isTracked()) {
unset($repositories[$key]);
}
}
$repository_ids = mpull($repositories, 'getID');
$summaries = array();
$commits = array();
if ($repository_ids) {
$summaries = queryfx_all(
$repository->establishConnection('r'),
'SELECT * FROM %T WHERE repositoryID IN (%Ld)',
PhabricatorRepository::TABLE_SUMMARY,
$repository_ids);
$summaries = ipull($summaries, null, 'repositoryID');
$commit_ids = array_filter(ipull($summaries, 'lastCommitID'));
if ($commit_ids) {
$commit = new PhabricatorRepositoryCommit();
$commits = $commit->loadAllWhere('id IN (%Ld)', $commit_ids);
$commits = mpull($commits, null, 'getRepositoryID');
}
}
$rows = array();
foreach ($repositories as $repository) {
$id = $repository->getID();
$commit = idx($commits, $id);
$size = idx(idx($summaries, $id, array()), 'size', 0);
$date = '-';
$time = '-';
if ($commit) {
$date = phabricator_date($commit->getEpoch(), $user);
$time = phabricator_time($commit->getEpoch(), $user);
}
$rows[] = array(
phutil_render_tag(
'a',
array(
'href' => '/diffusion/'.$repository->getCallsign().'/',
),
phutil_escape_html($repository->getName())),
phutil_escape_html($repository->getDetail('description')),
PhabricatorRepositoryType::getNameForRepositoryType(
$repository->getVersionControlSystem()),
$size ? number_format($size) : '-',
$commit
? DiffusionView::linkCommit(
$repository,
$commit->getCommitIdentifier())
: '-',
$date,
$time,
);
}
$repository_tool_uri = PhabricatorEnv::getProductionURI('/repository/');
$repository_tool = phutil_render_tag('a',
array(
'href' => $repository_tool_uri,
),
'repository tool');
$no_repositories_txt = 'This instance of Phabricator does not have any '.
'configured repositories. ';
if ($user->getIsAdmin()) {
$no_repositories_txt .= 'To setup one or more repositories, visit the '.
$repository_tool.'.';
} else {
$no_repositories_txt .= 'Ask an administrator to setup one or more '.
'repositories via the '.$repository_tool.'.';
}
$table = new AphrontTableView($rows);
$table->setNoDataString($no_repositories_txt);
$table->setHeaders(
array(
'Repository',
'Description',
'VCS',
'Commits',
'Last',
'Date',
'Time',
));
$table->setColumnClasses(
array(
'pri',
'wide',
'',
'n',
'n',
'',
'right',
));
$panel = new AphrontPanelView();
$panel->setHeader('Browse Repositories');
$panel->appendChild($table);
$crumbs = $this->buildCrumbs();
return $this->buildStandardPageResponse(
array(
$crumbs,
$shortcut_panel,
$panel,
),
array(
'title' => 'Diffusion',
));
}
}
diff --git a/src/applications/diffusion/controller/DiffusionInlineCommentController.php b/src/applications/diffusion/controller/DiffusionInlineCommentController.php
index 0c8ca71908..5cc0c384d5 100644
--- a/src/applications/diffusion/controller/DiffusionInlineCommentController.php
+++ b/src/applications/diffusion/controller/DiffusionInlineCommentController.php
@@ -1,95 +1,79 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionInlineCommentController
extends PhabricatorInlineCommentController {
private $commitPHID;
public function willProcessRequest(array $data) {
$this->commitPHID = $data['phid'];
}
protected function createComment() {
// Verify commit and path correspond to actual objects.
$commit_phid = $this->commitPHID;
$path_id = $this->getChangesetID();
$commit = id(new PhabricatorRepositoryCommit())->loadOneWhere(
'phid = %s',
$commit_phid);
if (!$commit) {
throw new Exception("Invalid commit ID!");
}
// TODO: Write a real PathQuery object?
$path = queryfx_one(
id(new PhabricatorRepository())->establishConnection('r'),
'SELECT path FROM %T WHERE id = %d',
PhabricatorRepository::TABLE_PATH,
$path_id);
if (!$path) {
throw new Exception("Invalid path ID!");
}
return id(new PhabricatorAuditInlineComment())
->setCommitPHID($commit_phid)
->setPathID($path_id);
}
protected function loadComment($id) {
return id(new PhabricatorAuditInlineComment())->load($id);
}
protected function loadCommentForEdit($id) {
$request = $this->getRequest();
$user = $request->getUser();
$inline = $this->loadComment($id);
if (!$this->canEditInlineComment($user, $inline)) {
throw new Exception("That comment is not editable!");
}
return $inline;
}
private function canEditInlineComment(
PhabricatorUser $user,
PhabricatorAuditInlineComment $inline) {
// Only the author may edit a comment.
if ($inline->getAuthorPHID() != $user->getPHID()) {
return false;
}
// Saved comments may not be edited.
if ($inline->getAuditCommentID()) {
return false;
}
// Inline must be attached to the active revision.
if ($inline->getCommitPHID() != $this->commitPHID) {
return false;
}
return true;
}
}
diff --git a/src/applications/diffusion/controller/DiffusionInlineCommentPreviewController.php b/src/applications/diffusion/controller/DiffusionInlineCommentPreviewController.php
index ed6ecdb2e6..23908d5743 100644
--- a/src/applications/diffusion/controller/DiffusionInlineCommentPreviewController.php
+++ b/src/applications/diffusion/controller/DiffusionInlineCommentPreviewController.php
@@ -1,38 +1,22 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionInlineCommentPreviewController
extends PhabricatorInlineCommentPreviewController {
private $commitPHID;
public function willProcessRequest(array $data) {
$this->commitPHID = $data['phid'];
}
protected function loadInlineComments() {
$user = $this->getRequest()->getUser();
$inlines = id(new PhabricatorAuditInlineComment())->loadAllWhere(
'authorPHID = %s AND commitPHID = %s AND auditCommentID IS NULL',
$user->getPHID(),
$this->commitPHID);
return $inlines;
}
}
diff --git a/src/applications/diffusion/controller/DiffusionLastModifiedController.php b/src/applications/diffusion/controller/DiffusionLastModifiedController.php
index 5d4d8d1b67..ceb92b2d3b 100644
--- a/src/applications/diffusion/controller/DiffusionLastModifiedController.php
+++ b/src/applications/diffusion/controller/DiffusionLastModifiedController.php
@@ -1,51 +1,35 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionLastModifiedController extends DiffusionController {
public function processRequest() {
$drequest = $this->getDiffusionRequest();
$request = $this->getRequest();
$modified_query = DiffusionLastModifiedQuery::newFromDiffusionRequest(
$drequest);
list($commit, $commit_data) = $modified_query->loadLastModification();
$phids = array();
if ($commit_data) {
if ($commit_data->getCommitDetail('authorPHID')) {
$phids[$commit_data->getCommitDetail('authorPHID')] = true;
}
if ($commit_data->getCommitDetail('committerPHID')) {
$phids[$commit_data->getCommitDetail('committerPHID')] = true;
}
}
$phids = array_keys($phids);
$handles = $this->loadViewerHandles($phids);
$output = DiffusionBrowseTableView::renderLastModifiedColumns(
$drequest->getRepository(),
$handles,
$commit,
$commit_data);
return id(new AphrontAjaxResponse())
->setContent($output);
}
}
diff --git a/src/applications/diffusion/controller/DiffusionPathCompleteController.php b/src/applications/diffusion/controller/DiffusionPathCompleteController.php
index 67108c72b3..ebacb43a2c 100644
--- a/src/applications/diffusion/controller/DiffusionPathCompleteController.php
+++ b/src/applications/diffusion/controller/DiffusionPathCompleteController.php
@@ -1,64 +1,48 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionPathCompleteController extends DiffusionController {
public function willProcessRequest(array $data) {
// Don't build a DiffusionRequest.
}
public function processRequest() {
$request = $this->getRequest();
$repository_phid = $request->getStr('repositoryPHID');
$repository = id(new PhabricatorRepository())->loadOneWhere(
'phid = %s',
$repository_phid);
if (!$repository) {
return new Aphront400Response();
}
$query_path = $request->getStr('q');
if (preg_match('@/$@', $query_path)) {
$query_dir = $query_path;
} else {
$query_dir = dirname($query_path).'/';
}
$query_dir = ltrim($query_dir, '/');
$drequest = DiffusionRequest::newFromDictionary(
array(
'repository' => $repository,
'path' => $query_dir,
));
$browse_query = DiffusionBrowseQuery::newFromDiffusionRequest($drequest);
$paths = $browse_query->loadPaths();
$output = array();
foreach ($paths as $path) {
$full_path = $query_dir.$path->getPath();
if ($path->getFileType() == DifferentialChangeType::FILE_DIRECTORY) {
$full_path .= '/';
}
$output[] = array('/'.$full_path, null, substr(md5($full_path), 0, 7));
}
return id(new AphrontAjaxResponse())->setContent($output);
}
}
diff --git a/src/applications/diffusion/controller/DiffusionPathValidateController.php b/src/applications/diffusion/controller/DiffusionPathValidateController.php
index 6a74f03055..80a6658b99 100644
--- a/src/applications/diffusion/controller/DiffusionPathValidateController.php
+++ b/src/applications/diffusion/controller/DiffusionPathValidateController.php
@@ -1,79 +1,63 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionPathValidateController extends DiffusionController {
public function willProcessRequest(array $data) {
// Don't build a DiffusionRequest.
}
public function processRequest() {
$request = $this->getRequest();
$repository_phid = $request->getStr('repositoryPHID');
$repository = id(new PhabricatorRepository())->loadOneWhere(
'phid = %s',
$repository_phid);
if (!$repository) {
return new Aphront400Response();
}
$path = $request->getStr('path');
$path = ltrim($path, '/');
$drequest = DiffusionRequest::newFromDictionary(
array(
'repository' => $repository,
'path' => $path,
));
$browse_query = DiffusionBrowseQuery::newFromDiffusionRequest($drequest);
$browse_query->needValidityOnly(true);
$valid = $browse_query->loadPaths();
if (!$valid) {
switch ($browse_query->getReasonForEmptyResultSet()) {
case DiffusionBrowseQuery::REASON_IS_FILE:
$valid = true;
break;
case DiffusionBrowseQuery::REASON_IS_EMPTY:
$valid = true;
break;
}
}
$output = array(
'valid' => (bool)$valid,
);
if (!$valid) {
$branch = $drequest->getBranch();
if ($branch) {
$message = 'Not found in '.$branch;
} else {
$message = 'Not found at HEAD';
}
} else {
$message = 'OK';
}
$output['message'] = $message;
return id(new AphrontAjaxResponse())->setContent($output);
}
}
diff --git a/src/applications/diffusion/controller/DiffusionRepositoryController.php b/src/applications/diffusion/controller/DiffusionRepositoryController.php
index 4a4f2fa7f6..94b0916a02 100644
--- a/src/applications/diffusion/controller/DiffusionRepositoryController.php
+++ b/src/applications/diffusion/controller/DiffusionRepositoryController.php
@@ -1,269 +1,253 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionRepositoryController extends DiffusionController {
public function processRequest() {
$drequest = $this->diffusionRequest;
$content = array();
$crumbs = $this->buildCrumbs();
$content[] = $crumbs;
$content[] = $this->buildPropertiesTable($drequest->getRepository());
$history_query = DiffusionHistoryQuery::newFromDiffusionRequest(
$drequest);
$history_query->setLimit(15);
$history_query->needParents(true);
$history = $history_query->loadHistory();
$browse_query = DiffusionBrowseQuery::newFromDiffusionRequest($drequest);
$browse_results = $browse_query->loadPaths();
$phids = array();
foreach ($history as $item) {
$data = $item->getCommitData();
if ($data) {
if ($data->getCommitDetail('authorPHID')) {
$phids[$data->getCommitDetail('authorPHID')] = true;
}
if ($data->getCommitDetail('committerPHID')) {
$phids[$data->getCommitDetail('committerPHID')] = true;
}
}
}
foreach ($browse_results as $item) {
$data = $item->getLastCommitData();
if ($data) {
if ($data->getCommitDetail('authorPHID')) {
$phids[$data->getCommitDetail('authorPHID')] = true;
}
if ($data->getCommitDetail('committerPHID')) {
$phids[$data->getCommitDetail('committerPHID')] = true;
}
}
}
$phids = array_keys($phids);
$handles = $this->loadViewerHandles($phids);
$history_table = new DiffusionHistoryTableView();
$history_table->setDiffusionRequest($drequest);
$history_table->setHandles($handles);
$history_table->setHistory($history);
$history_table->loadRevisions();
$history_table->setParents($history_query->getParents());
$history_table->setIsHead(true);
$callsign = $drequest->getRepository()->getCallsign();
$all = phutil_render_tag(
'a',
array(
'href' => "/diffusion/{$callsign}/history/",
),
'View Full Commit History');
$panel = new AphrontPanelView();
$panel->setHeader("Recent Commits &middot; {$all}");
$panel->appendChild($history_table);
$content[] = $panel;
$browse_table = new DiffusionBrowseTableView();
$browse_table->setDiffusionRequest($drequest);
$browse_table->setHandles($handles);
$browse_table->setPaths($browse_results);
$browse_table->setUser($this->getRequest()->getUser());
$browse_panel = new AphrontPanelView();
$browse_panel->setHeader('Browse Repository');
$browse_panel->appendChild($browse_table);
$content[] = $browse_panel;
$content[] = $this->buildTagListTable($drequest);
$content[] = $this->buildBranchListTable($drequest);
$readme = $browse_query->renderReadme($browse_results);
if ($readme) {
$panel = new AphrontPanelView();
$panel->setHeader('README');
$panel->appendChild($readme);
$content[] = $panel;
}
return $this->buildStandardPageResponse(
$content,
array(
'title' => $drequest->getRepository()->getName(),
));
}
private function buildPropertiesTable(PhabricatorRepository $repository) {
$properties = array();
$properties['Name'] = $repository->getName();
$properties['Callsign'] = $repository->getCallsign();
$properties['Description'] = $repository->getDetail('description');
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$properties['Clone URI'] = $repository->getPublicRemoteURI();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$properties['Repository Root'] = $repository->getPublicRemoteURI();
break;
}
$rows = array();
foreach ($properties as $key => $value) {
$rows[] = array(
phutil_escape_html($key),
phutil_escape_html($value));
}
$table = new AphrontTableView($rows);
$table->setColumnClasses(
array(
'header',
'wide',
));
$panel = new AphrontPanelView();
$panel->setHeader('Repository Properties');
$panel->appendChild($table);
return $panel;
}
private function buildBranchListTable(DiffusionRequest $drequest) {
if ($drequest->getBranch() !== null) {
$limit = 15;
$branch_query = DiffusionBranchQuery::newFromDiffusionRequest($drequest);
$branch_query->setLimit($limit + 1);
$branches = $branch_query->loadBranches();
if (!$branches) {
return null;
}
$more_branches = (count($branches) > $limit);
$branches = array_slice($branches, 0, $limit);
$commits = id(new PhabricatorAuditCommitQuery())
->withIdentifiers(
$drequest->getRepository()->getID(),
mpull($branches, 'getHeadCommitIdentifier'))
->needCommitData(true)
->execute();
$table = new DiffusionBranchTableView();
$table->setDiffusionRequest($drequest);
$table->setBranches($branches);
$table->setCommits($commits);
$table->setUser($this->getRequest()->getUser());
$panel = new AphrontPanelView();
$panel->setHeader('Branches');
if ($more_branches) {
$panel->setCaption('Showing ' . $limit . ' branches.');
}
$panel->addButton(
phutil_render_tag(
'a',
array(
'href' => $drequest->generateURI(
array(
'action' => 'branches',
)),
'class' => 'grey button',
),
"Show All Branches \xC2\xBB"));
$panel->appendChild($table);
return $panel;
}
return null;
}
private function buildTagListTable(DiffusionRequest $drequest) {
$tag_limit = 15;
$query = DiffusionTagListQuery::newFromDiffusionRequest($drequest);
$query->setLimit($tag_limit + 1);
$tags = $query->loadTags();
if (!$tags) {
return null;
}
$more_tags = (count($tags) > $tag_limit);
$tags = array_slice($tags, 0, $tag_limit);
$commits = id(new PhabricatorAuditCommitQuery())
->withIdentifiers(
$drequest->getRepository()->getID(),
mpull($tags, 'getCommitIdentifier'))
->needCommitData(true)
->execute();
$view = new DiffusionTagListView();
$view->setDiffusionRequest($drequest);
$view->setTags($tags);
$view->setUser($this->getRequest()->getUser());
$view->setCommits($commits);
$phids = $view->getRequiredHandlePHIDs();
$handles = $this->loadViewerHandles($phids);
$view->setHandles($handles);
$panel = new AphrontPanelView();
$panel->setHeader('Tags');
if ($more_tags) {
$panel->setCaption('Showing the '.$tag_limit.' most recent tags.');
}
$panel->addButton(
phutil_render_tag(
'a',
array(
'href' => $drequest->generateURI(
array(
'action' => 'tags',
)),
'class' => 'grey button',
),
"Show All Tags \xC2\xBB"));
$panel->appendChild($view);
return $panel;
}
}
diff --git a/src/applications/diffusion/controller/DiffusionSymbolController.php b/src/applications/diffusion/controller/DiffusionSymbolController.php
index 68ead4d06e..ab118c3948 100644
--- a/src/applications/diffusion/controller/DiffusionSymbolController.php
+++ b/src/applications/diffusion/controller/DiffusionSymbolController.php
@@ -1,170 +1,154 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionSymbolController extends DiffusionController {
private $name;
public function willProcessRequest(array $data) {
$this->name = $data['name'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$query = new DiffusionSymbolQuery();
$query->setName($this->name);
if ($request->getStr('context') !== null) {
$query->setContext($request->getStr('context'));
}
if ($request->getStr('type')) {
$query->setType($request->getStr('type'));
}
if ($request->getStr('lang')) {
$query->setLanguage($request->getStr('lang'));
}
if ($request->getStr('projects')) {
$phids = $request->getStr('projects');
$phids = explode(',', $phids);
$phids = array_filter($phids);
if ($phids) {
$projects = id(new PhabricatorRepositoryArcanistProject())
->loadAllWhere(
'phid IN (%Ls)',
$phids);
$projects = mpull($projects, 'getID');
if ($projects) {
$query->setProjectIDs($projects);
}
}
}
$query->needPaths(true);
$query->needArcanistProjects(true);
$query->needRepositories(true);
$symbols = $query->execute();
// For PHP builtins, jump to php.net documentation.
if ($request->getBool('jump') && count($symbols) == 0) {
if ($request->getStr('lang', 'php') == 'php') {
if ($request->getStr('type', 'function') == 'function') {
$functions = get_defined_functions();
if (in_array($this->name, $functions['internal'])) {
return id(new AphrontRedirectResponse())
->setURI('http://www.php.net/function.'.$this->name);
}
}
if ($request->getStr('type', 'class') == 'class') {
if (class_exists($this->name, false) ||
interface_exists($this->name, false)) {
if (id(new ReflectionClass($this->name))->isInternal()) {
return id(new AphrontRedirectResponse())
->setURI('http://www.php.net/class.'.$this->name);
}
}
}
}
}
$rows = array();
foreach ($symbols as $symbol) {
$project = $symbol->getArcanistProject();
if ($project) {
$project_name = $project->getName();
} else {
$project_name = '-';
}
$file = phutil_escape_html($symbol->getPath());
$line = phutil_escape_html($symbol->getLineNumber());
$repo = $symbol->getRepository();
if ($repo) {
$href = $symbol->getURI();
if ($request->getBool('jump') && count($symbols) == 1) {
// If this is a clickthrough from Differential, just jump them
// straight to the target if we got a single hit.
return id(new AphrontRedirectResponse())->setURI($href);
}
$location = phutil_render_tag(
'a',
array(
'href' => $href,
),
phutil_escape_html($file.':'.$line));
} else if ($file) {
$location = phutil_escape_html($file.':'.$line);
} else {
$location = '?';
}
$rows[] = array(
phutil_escape_html($symbol->getSymbolType()),
phutil_escape_html($symbol->getSymbolContext()),
phutil_escape_html($symbol->getSymbolName()),
phutil_escape_html($symbol->getSymbolLanguage()),
phutil_escape_html($project_name),
$location,
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Type',
'Context',
'Name',
'Language',
'Project',
'File',
));
$table->setColumnClasses(
array(
'',
'',
'pri',
'',
'',
'',
));
$table->setNoDataString(
"No matching symbol could be found in any indexed project.");
$panel = new AphrontPanelView();
$panel->setHeader('Similar Symbols');
$panel->appendChild($table);
return $this->buildStandardPageResponse(
array(
$panel,
),
array(
'title' => 'Find Symbol',
));
}
}
diff --git a/src/applications/diffusion/controller/DiffusionTagListController.php b/src/applications/diffusion/controller/DiffusionTagListController.php
index 1da231fecc..a2b05c5b80 100644
--- a/src/applications/diffusion/controller/DiffusionTagListController.php
+++ b/src/applications/diffusion/controller/DiffusionTagListController.php
@@ -1,103 +1,87 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionTagListController extends DiffusionController {
public function processRequest() {
$drequest = $this->getDiffusionRequest();
$request = $this->getRequest();
$user = $request->getUser();
$repository = $drequest->getRepository();
$pager = new AphrontPagerView();
$pager->setURI($request->getRequestURI(), 'offset');
$pager->setOffset($request->getInt('offset'));
if ($drequest->getRawCommit()) {
$is_commit = true;
$query = DiffusionCommitTagsQuery::newFromDiffusionRequest($drequest);
$query->setOffset($pager->getOffset());
$query->setLimit($pager->getPageSize() + 1);
$tags = $query->loadTags();
} else {
$is_commit = false;
$query = DiffusionTagListQuery::newFromDiffusionRequest($drequest);
$query->setOffset($pager->getOffset());
$query->setLimit($pager->getPageSize() + 1);
$tags = $query->loadTags();
}
$tags = $pager->sliceResults($tags);
$content = null;
if (!$tags) {
$content = new AphrontErrorView();
$content->setTitle('No Tags');
if ($is_commit) {
$content->appendChild('This commit has no tags.');
} else {
$content->appendChild('This repository has no tags.');
}
$content->setSeverity(AphrontErrorView::SEVERITY_NODATA);
} else {
$commits = id(new PhabricatorAuditCommitQuery())
->withIdentifiers(
$drequest->getRepository()->getID(),
mpull($tags, 'getCommitIdentifier'))
->needCommitData(true)
->execute();
$view = id(new DiffusionTagListView())
->setTags($tags)
->setUser($user)
->setCommits($commits)
->setDiffusionRequest($drequest);
$phids = $view->getRequiredHandlePHIDs();
$handles = $this->loadViewerHandles($phids);
$view->setHandles($handles);
$panel = id(new AphrontPanelView())
->setHeader('Tags')
->appendChild($view)
->appendChild($pager);
$content = $panel;
}
return $this->buildStandardPageResponse(
array(
$this->buildCrumbs(
array(
'tags' => true,
'commit' => $drequest->getRawCommit(),
)),
$content,
),
array(
'title' => array(
'Tags',
$repository->getCallsign().' Repository',
),
));
}
}
diff --git a/src/applications/diffusion/data/DiffusionBranchInformation.php b/src/applications/diffusion/data/DiffusionBranchInformation.php
index 4c8121f3f1..fa98b44d91 100644
--- a/src/applications/diffusion/data/DiffusionBranchInformation.php
+++ b/src/applications/diffusion/data/DiffusionBranchInformation.php
@@ -1,44 +1,28 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionBranchInformation {
const DEFAULT_GIT_REMOTE = 'origin';
private $name;
private $headCommitIdentifier;
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function setHeadCommitIdentifier($head_commit_identifier) {
$this->headCommitIdentifier = $head_commit_identifier;
return $this;
}
public function getHeadCommitIdentifier() {
return $this->headCommitIdentifier;
}
}
diff --git a/src/applications/diffusion/data/DiffusionFileContent.php b/src/applications/diffusion/data/DiffusionFileContent.php
index 0cdb0d0e6a..4eb1c95f5d 100644
--- a/src/applications/diffusion/data/DiffusionFileContent.php
+++ b/src/applications/diffusion/data/DiffusionFileContent.php
@@ -1,32 +1,16 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionFileContent {
private $corpus;
final public function setCorpus($corpus) {
$this->corpus = $corpus;
return $this;
}
final public function getCorpus() {
return $this->corpus;
}
}
diff --git a/src/applications/diffusion/data/DiffusionPathChange.php b/src/applications/diffusion/data/DiffusionPathChange.php
index ea1f31304e..10a69da283 100644
--- a/src/applications/diffusion/data/DiffusionPathChange.php
+++ b/src/applications/diffusion/data/DiffusionPathChange.php
@@ -1,168 +1,152 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionPathChange {
private $path;
private $commitIdentifier;
private $commit;
private $commitData;
private $changeType;
private $fileType;
private $targetPath;
private $targetCommitIdentifier;
private $awayPaths = array();
final public function setPath($path) {
$this->path = $path;
return $this;
}
final public function getPath() {
return $this->path;
}
public function setChangeType($change_type) {
$this->changeType = $change_type;
return $this;
}
public function getChangeType() {
return $this->changeType;
}
public function setFileType($file_type) {
$this->fileType = $file_type;
return $this;
}
public function getFileType() {
return $this->fileType;
}
public function setTargetPath($target_path) {
$this->targetPath = $target_path;
return $this;
}
public function getTargetPath() {
return $this->targetPath;
}
public function setAwayPaths(array $away_paths) {
$this->awayPaths = $away_paths;
return $this;
}
public function getAwayPaths() {
return $this->awayPaths;
}
final public function setCommitIdentifier($commit) {
$this->commitIdentifier = $commit;
return $this;
}
final public function getCommitIdentifier() {
return $this->commitIdentifier;
}
final public function setTargetCommitIdentifier($target_commit_identifier) {
$this->targetCommitIdentifier = $target_commit_identifier;
return $this;
}
final public function getTargetCommitIdentifier() {
return $this->targetCommitIdentifier;
}
final public function setCommit($commit) {
$this->commit = $commit;
return $this;
}
final public function getCommit() {
return $this->commit;
}
final public function setCommitData($commit_data) {
$this->commitData = $commit_data;
return $this;
}
final public function getCommitData() {
return $this->commitData;
}
final public function getEpoch() {
if ($this->getCommit()) {
return $this->getCommit()->getEpoch();
}
return null;
}
final public function getAuthorName() {
if ($this->getCommitData()) {
return $this->getCommitData()->getAuthorName();
}
return null;
}
final public function getSummary() {
if (!$this->getCommitData()) {
return null;
}
$message = $this->getCommitData()->getCommitMessage();
$first = idx(explode("\n", $message), 0);
return substr($first, 0, 80);
}
final public static function convertToArcanistChanges(array $changes) {
assert_instances_of($changes, 'DiffusionPathChange');
$direct = array();
$result = array();
foreach ($changes as $path) {
$change = new ArcanistDiffChange();
$change->setCurrentPath($path->getPath());
$direct[] = $path->getPath();
$change->setType($path->getChangeType());
$file_type = $path->getFileType();
if ($file_type == DifferentialChangeType::FILE_NORMAL) {
$file_type = DifferentialChangeType::FILE_TEXT;
}
$change->setFileType($file_type);
$change->setOldPath($path->getTargetPath());
foreach ($path->getAwayPaths() as $away_path) {
$change->addAwayPath($away_path);
}
$result[$path->getPath()] = $change;
}
return array_select_keys($result, $direct);
}
final public static function convertToDifferentialChangesets(array $changes) {
assert_instances_of($changes, 'DiffusionPathChange');
$arcanist_changes = self::convertToArcanistChanges($changes);
$diff = DifferentialDiff::newFromRawChanges($arcanist_changes);
return $diff->getChangesets();
}
}
diff --git a/src/applications/diffusion/data/DiffusionRepositoryPath.php b/src/applications/diffusion/data/DiffusionRepositoryPath.php
index 46a0799c6b..b050b75bd5 100644
--- a/src/applications/diffusion/data/DiffusionRepositoryPath.php
+++ b/src/applications/diffusion/data/DiffusionRepositoryPath.php
@@ -1,105 +1,89 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionRepositoryPath {
private $fullPath;
private $path;
private $hash;
private $fileType;
private $fileSize;
private $externalURI;
private $lastModifiedCommit;
private $lastCommitData;
public function setFullPath($full_path) {
$this->fullPath = $full_path;
return $this;
}
public function getFullPath() {
return $this->fullPath;
}
final public function setPath($path) {
$this->path = $path;
return $this;
}
final public function getPath() {
return $this->path;
}
final public function setHash($hash) {
$this->hash = $hash;
return $this;
}
final public function getHash() {
return $this->hash;
}
final public function setLastModifiedCommit(
PhabricatorRepositoryCommit $commit) {
$this->lastModifiedCommit = $commit;
return $this;
}
final public function getLastModifiedCommit() {
return $this->lastModifiedCommit;
}
final public function setLastCommitData(
PhabricatorRepositoryCommitData $last_commit_data) {
$this->lastCommitData = $last_commit_data;
return $this;
}
final public function getLastCommitData() {
return $this->lastCommitData;
}
final public function setFileType($file_type) {
$this->fileType = $file_type;
return $this;
}
final public function getFileType() {
return $this->fileType;
}
final public function setFileSize($file_size) {
$this->fileSize = $file_size;
return $this;
}
final public function getFileSize() {
return $this->fileSize;
}
final public function setExternalURI($external_uri) {
$this->externalURI = $external_uri;
return $this;
}
final public function getExternalURI() {
return $this->externalURI;
}
}
diff --git a/src/applications/diffusion/exception/DiffusionSetupException.php b/src/applications/diffusion/exception/DiffusionSetupException.php
index d63775d94c..eecf7a81fc 100755
--- a/src/applications/diffusion/exception/DiffusionSetupException.php
+++ b/src/applications/diffusion/exception/DiffusionSetupException.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionSetupException extends AphrontUsageException {
public function __construct($message) {
parent::__construct('Diffusion Setup Exception', $message);
}
}
diff --git a/src/applications/diffusion/query/DiffusionPathQuery.php b/src/applications/diffusion/query/DiffusionPathQuery.php
index f2af6264cd..33e7b57909 100644
--- a/src/applications/diffusion/query/DiffusionPathQuery.php
+++ b/src/applications/diffusion/query/DiffusionPathQuery.php
@@ -1,59 +1,43 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionPathQuery {
private $pathIDs;
public function withPathIDs(array $path_ids) {
$this->pathIDs = $path_ids;
return $this;
}
public function execute() {
$conn_r = id(new PhabricatorRepository())->establishConnection('r');
$where = $this->buildWhereClause($conn_r);
$results = queryfx_all(
$conn_r,
'SELECT * FROM %T %Q',
PhabricatorRepository::TABLE_PATH,
$where);
return ipull($results, null, 'id');
}
private function buildWhereClause($conn_r) {
$where = array();
if ($this->pathIDs) {
$where[] = qsprintf(
$conn_r,
'id IN (%Ld)',
$this->pathIDs);
}
if ($where) {
return 'WHERE ('.implode(') AND (', $where).')';
} else {
return '';
}
}
}
diff --git a/src/applications/diffusion/query/DiffusionQuery.php b/src/applications/diffusion/query/DiffusionQuery.php
index a27e4e406b..d902587de4 100644
--- a/src/applications/diffusion/query/DiffusionQuery.php
+++ b/src/applications/diffusion/query/DiffusionQuery.php
@@ -1,179 +1,163 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DiffusionQuery {
private $request;
final protected function __construct() {
// <protected>
}
protected static function newQueryObject(
$base_class,
DiffusionRequest $request) {
$repository = $request->getRepository();
$map = array(
PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'Git',
PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL => 'Mercurial',
PhabricatorRepositoryType::REPOSITORY_TYPE_SVN => 'Svn',
);
$name = idx($map, $repository->getVersionControlSystem());
if (!$name) {
throw new Exception("Unsupported VCS!");
}
$class = str_replace('Diffusion', 'Diffusion'.$name, $base_class);
$obj = new $class();
$obj->request = $request;
return $obj;
}
final protected function getRequest() {
return $this->request;
}
abstract protected function executeQuery();
/* -( Query Utilities )---------------------------------------------------- */
final protected function loadCommitsByIdentifiers(array $identifiers) {
if (!$identifiers) {
return array();
}
$commits = array();
$commit_data = array();
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
$commits = id(new PhabricatorRepositoryCommit())->loadAllWhere(
'repositoryID = %d AND commitIdentifier IN (%Ls)',
$repository->getID(),
$identifiers);
$commits = mpull($commits, null, 'getCommitIdentifier');
// Build empty commit objects for every commit, so we can show unparsed
// commits in history views as "unparsed" instead of not showing them. This
// makes the process of importing and parsing commits much clearer to the
// user.
$commit_list = array();
foreach ($identifiers as $identifier) {
$commit_obj = idx($commits, $identifier);
if (!$commit_obj) {
$commit_obj = new PhabricatorRepositoryCommit();
$commit_obj->setRepositoryID($repository->getID());
$commit_obj->setCommitIdentifier($identifier);
$commit_obj->setIsUnparsed(true);
$commit_obj->makeEphemeral();
}
$commit_list[$identifier] = $commit_obj;
}
$commits = $commit_list;
$commit_ids = array_filter(mpull($commits, 'getID'));
if ($commit_ids) {
$commit_data = id(new PhabricatorRepositoryCommitData())->loadAllWhere(
'commitID in (%Ld)',
$commit_ids);
$commit_data = mpull($commit_data, null, 'getCommitID');
}
foreach ($commits as $commit) {
if (!$commit->getID()) {
continue;
}
if (idx($commit_data, $commit->getID())) {
$commit->attachCommitData($commit_data[$commit->getID()]);
}
}
return $commits;
}
final protected function loadHistoryForCommitIdentifiers(array $identifiers) {
if (!$identifiers) {
return array();
}
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
$commits = self::loadCommitsByIdentifiers($identifiers);
if (!$commits) {
return array();
}
$path = $drequest->getPath();
$conn_r = $repository->establishConnection('r');
$path_normal = DiffusionPathIDQuery::normalizePath($path);
$paths = queryfx_all(
$conn_r,
'SELECT id, path FROM %T WHERE pathHash IN (%Ls)',
PhabricatorRepository::TABLE_PATH,
array(md5($path_normal)));
$paths = ipull($paths, 'id', 'path');
$path_id = idx($paths, $path_normal);
$commit_ids = array_filter(mpull($commits, 'getID'));
$path_changes = array();
if ($path_id && $commit_ids) {
$path_changes = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE commitID IN (%Ld) AND pathID = %d',
PhabricatorRepository::TABLE_PATHCHANGE,
$commit_ids,
$path_id);
$path_changes = ipull($path_changes, null, 'commitID');
}
$history = array();
foreach ($identifiers as $identifier) {
$item = new DiffusionPathChange();
$item->setCommitIdentifier($identifier);
$commit = idx($commits, $identifier);
if ($commit) {
$item->setCommit($commit);
try {
$item->setCommitData($commit->getCommitData());
} catch (Exception $ex) {
// Ignore, commit just doesn't have data.
}
$change = idx($path_changes, $commit->getID());
if ($change) {
$item->setChangeType($change['changeType']);
$item->setFileType($change['fileType']);
}
}
$history[] = $item;
}
return $history;
}
}
diff --git a/src/applications/diffusion/query/DiffusionRenameHistoryQuery.php b/src/applications/diffusion/query/DiffusionRenameHistoryQuery.php
index cb2db9eb7c..8145d73db8 100644
--- a/src/applications/diffusion/query/DiffusionRenameHistoryQuery.php
+++ b/src/applications/diffusion/query/DiffusionRenameHistoryQuery.php
@@ -1,112 +1,96 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionRenameHistoryQuery {
private $oldCommit;
private $wasCreated;
private $request;
public function getWasCreated() {
return $this->wasCreated;
}
public function setRequest(DiffusionRequest $request) {
$this->request = $request;
return $this;
}
public function setOldCommit($old_commit) {
$this->oldCommit = $old_commit;
return $this;
}
public function getOldCommit() {
return $this->oldCommit;
}
final public function loadOldFilename() {
$drequest = $this->request;
$repository_id = $drequest->getRepository()->getID();
$conn_r = id(new PhabricatorRepository())->establishConnection('r');
$commit_id = $this->loadCommitId($this->oldCommit);
$old_commit_sequence = $this->loadCommitSequence($commit_id);
$path = '/'.$drequest->getPath();
$commit_id = $this->loadCommitId($drequest->getCommit());
do {
$commit_sequence = $this->loadCommitSequence($commit_id);
$change = queryfx_one(
$conn_r,
'SELECT pc.changeType, pc.targetCommitID, tp.path
FROM %T p
JOIN %T pc ON p.id = pc.pathID
LEFT JOIN %T tp ON pc.targetPathID = tp.id
WHERE p.pathHash = %s
AND pc.repositoryID = %d
AND pc.changeType IN (%d, %d)
AND pc.commitSequence BETWEEN %d AND %d
ORDER BY pc.commitSequence DESC
LIMIT 1',
PhabricatorRepository::TABLE_PATH,
PhabricatorRepository::TABLE_PATHCHANGE,
PhabricatorRepository::TABLE_PATH,
md5($path),
$repository_id,
ArcanistDiffChangeType::TYPE_MOVE_HERE,
ArcanistDiffChangeType::TYPE_ADD,
$old_commit_sequence,
$commit_sequence);
if ($change) {
if ($change['changeType'] == ArcanistDiffChangeType::TYPE_ADD) {
$this->wasCreated = true;
return $path;
}
$commit_id = $change['targetCommitID'];
$path = $change['path'];
}
} while ($change && $path);
return $path;
}
private function loadCommitId($commit_identifier) {
$commit = id(new PhabricatorRepositoryCommit())->loadOneWhere(
'repositoryID = %d AND commitIdentifier = %s',
$this->request->getRepository()->getID(),
$commit_identifier);
return $commit->getID();
}
private function loadCommitSequence($commit_id) {
$conn_r = id(new PhabricatorRepository())->establishConnection('r');
$path_change = queryfx_one(
$conn_r,
'SELECT commitSequence
FROM %T
WHERE repositoryID = %d AND commitID = %d
LIMIT 1',
PhabricatorRepository::TABLE_PATHCHANGE,
$this->request->getRepository()->getID(),
$commit_id);
return reset($path_change);
}
}
diff --git a/src/applications/diffusion/query/DiffusionSymbolQuery.php b/src/applications/diffusion/query/DiffusionSymbolQuery.php
index b11e6c3135..462f52b02e 100644
--- a/src/applications/diffusion/query/DiffusionSymbolQuery.php
+++ b/src/applications/diffusion/query/DiffusionSymbolQuery.php
@@ -1,302 +1,286 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Query symbol information (class and function names and location), returning
* a list of matching @{class:PhabricatorRepositorySymbol} objects and possibly
* attached data.
*
* @task config Configuring the Query
* @task exec Executing the Query
* @task internal Internals
*
* @group diffusion
*/
final class DiffusionSymbolQuery extends PhabricatorOffsetPagedQuery {
private $context;
private $namePrefix;
private $name;
private $projectIDs;
private $language;
private $type;
private $needPaths;
private $needArcanistProject;
private $needRepositories;
/* -( Configuring the Query )---------------------------------------------- */
/**
* @task config
*/
public function setContext($context) {
$this->context = $context;
return $this;
}
/**
* @task config
*/
public function setName($name) {
$this->name = $name;
return $this;
}
/**
* @task config
*/
public function setNamePrefix($name_prefix) {
$this->namePrefix = $name_prefix;
return $this;
}
/**
* @task config
*/
public function setProjectIDs(array $project_ids) {
$this->projectIDs = $project_ids;
return $this;
}
/**
* @task config
*/
public function setLanguage($language) {
$this->language = $language;
return $this;
}
/**
* @task config
*/
public function setType($type) {
$this->type = $type;
return $this;
}
/**
* @task config
*/
public function needPaths($need_paths) {
$this->needPaths = $need_paths;
return $this;
}
/**
* @task config
*/
public function needArcanistProjects($need_arcanist_projects) {
$this->needArcanistProjects = $need_arcanist_projects;
return $this;
}
/**
* @task config
*/
public function needRepositories($need_repositories) {
$this->needRepositories = $need_repositories;
return $this;
}
/* -( Executing the Query )------------------------------------------------ */
/**
* @task exec
*/
public function execute() {
if ($this->name && $this->namePrefix) {
throw new Exception(
"You can not set both a name and a name prefix!");
} else if (!$this->name && !$this->namePrefix) {
throw new Exception(
"You must set a name or a name prefix!");
}
$symbol = new PhabricatorRepositorySymbol();
$conn_r = $symbol->establishConnection('r');
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T %Q %Q %Q',
$symbol->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
$symbols = $symbol->loadAllFromArray($data);
if ($symbols) {
if ($this->needPaths) {
$this->loadPaths($symbols);
}
if ($this->needArcanistProjects || $this->needRepositories) {
$this->loadArcanistProjects($symbols);
}
if ($this->needRepositories) {
$this->loadRepositories($symbols);
}
}
return $symbols;
}
/* -( Internals )---------------------------------------------------------- */
/**
* @task internal
*/
private function buildOrderClause($conn_r) {
return qsprintf(
$conn_r,
'ORDER BY symbolName ASC');
}
/**
* @task internal
*/
private function buildWhereClause($conn_r) {
$where = array();
if (isset($this->context)) {
$where[] = qsprintf(
$conn_r,
'symbolContext = %s',
$this->context);
}
if ($this->name) {
$where[] = qsprintf(
$conn_r,
'symbolName = %s',
$this->name);
}
if ($this->namePrefix) {
$where[] = qsprintf(
$conn_r,
'symbolName LIKE %>',
$this->namePrefix);
}
if ($this->projectIDs) {
$where[] = qsprintf(
$conn_r,
'arcanistProjectID IN (%Ld)',
$this->projectIDs);
}
if ($this->language) {
$where[] = qsprintf(
$conn_r,
'symbolLanguage = %s',
$this->language);
}
if ($this->type) {
$where[] = qsprintf(
$conn_r,
'symbolType = %s',
$this->type);
}
return $this->formatWhereClause($where);
}
/**
* @task internal
*/
private function loadPaths(array $symbols) {
assert_instances_of($symbols, 'PhabricatorRepositorySymbol');
$path_map = queryfx_all(
id(new PhabricatorRepository())->establishConnection('r'),
'SELECT * FROM %T WHERE id IN (%Ld)',
PhabricatorRepository::TABLE_PATH,
mpull($symbols, 'getPathID'));
$path_map = ipull($path_map, 'path', 'id');
foreach ($symbols as $symbol) {
$symbol->attachPath(idx($path_map, $symbol->getPathID()));
}
}
/**
* @task internal
*/
private function loadArcanistProjects(array $symbols) {
assert_instances_of($symbols, 'PhabricatorRepositorySymbol');
$projects = id(new PhabricatorRepositoryArcanistProject())->loadAllWhere(
'id IN (%Ld)',
mpull($symbols, 'getArcanistProjectID'));
foreach ($symbols as $symbol) {
$project = idx($projects, $symbol->getArcanistProjectID());
$symbol->attachArcanistProject($project);
}
}
/**
* @task internal
*/
private function loadRepositories(array $symbols) {
assert_instances_of($symbols, 'PhabricatorRepositorySymbol');
$projects = mpull($symbols, 'getArcanistProject');
$projects = array_filter($projects);
$repo_ids = mpull($projects, 'getRepositoryID');
$repo_ids = array_filter($repo_ids);
if ($repo_ids) {
$repos = id(new PhabricatorRepository())->loadAllWhere(
'id IN (%Ld)',
$repo_ids);
} else {
$repos = array();
}
foreach ($symbols as $symbol) {
$proj = $symbol->getArcanistProject();
if ($proj) {
$symbol->attachRepository(idx($repos, $proj->getRepositoryID()));
} else {
$symbol->attachRepository(null);
}
}
}
}
diff --git a/src/applications/diffusion/query/branch/DiffusionBranchQuery.php b/src/applications/diffusion/query/branch/DiffusionBranchQuery.php
index 7372a22b2e..fb042acb69 100644
--- a/src/applications/diffusion/query/branch/DiffusionBranchQuery.php
+++ b/src/applications/diffusion/query/branch/DiffusionBranchQuery.php
@@ -1,77 +1,61 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DiffusionBranchQuery {
private $request;
private $limit;
private $offset;
public function setOffset($offset) {
$this->offset = $offset;
return $this;
}
public function getOffset() {
return $this->offset;
}
public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
protected function getLimit() {
return $this->limit;
}
final private function __construct() {
// <private>
}
final public static function newFromDiffusionRequest(
DiffusionRequest $request) {
$repository = $request->getRepository();
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$query = new DiffusionGitBranchQuery();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$query = new DiffusionMercurialBranchQuery();
break;
default:
throw new Exception("Unsupported VCS!");
}
$query->request = $request;
return $query;
}
final protected function getRequest() {
return $this->request;
}
final public function loadBranches() {
return $this->executeQuery();
}
abstract protected function executeQuery();
}
diff --git a/src/applications/diffusion/query/branch/DiffusionGitBranchQuery.php b/src/applications/diffusion/query/branch/DiffusionGitBranchQuery.php
index 4be88bb28b..a46217e295 100644
--- a/src/applications/diffusion/query/branch/DiffusionGitBranchQuery.php
+++ b/src/applications/diffusion/query/branch/DiffusionGitBranchQuery.php
@@ -1,136 +1,120 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionGitBranchQuery extends DiffusionBranchQuery {
protected function executeQuery() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
// We need to add 1 in case we pick up HEAD.
$count = $this->getOffset() + $this->getLimit() + 1;
list($stdout) = $repository->execxLocalCommand(
'for-each-ref %C --sort=-creatordate --format=%s refs/remotes',
$count ? '--count='.(int)$count : null,
'%(refname:short) %(objectname)'
);
$branch_list = self::parseGitRemoteBranchOutput(
$stdout,
$only_this_remote = DiffusionBranchInformation::DEFAULT_GIT_REMOTE);
$branches = array();
foreach ($branch_list as $name => $head) {
if (!$repository->shouldTrackBranch($name)) {
continue;
}
$branch = new DiffusionBranchInformation();
$branch->setName($name);
$branch->setHeadCommitIdentifier($head);
$branches[] = $branch;
}
$offset = $this->getOffset();
if ($offset) {
$branches = array_slice($branches, $offset);
}
// We might have too many even after offset slicing, if there was no HEAD
// for some reason.
$limit = $this->getLimit();
if ($limit) {
$branches = array_slice($branches, 0, $limit);
}
return $branches;
}
/**
* Parse the output of 'git branch -r --verbose --no-abbrev' or similar into
* a map. For instance:
*
* array(
* 'origin/master' => '99a9c082f9a1b68c7264e26b9e552484a5ae5f25',
* );
*
* If you specify $only_this_remote, branches will be filtered to only those
* on the given remote, **and the remote name will be stripped**. For example:
*
* array(
* 'master' => '99a9c082f9a1b68c7264e26b9e552484a5ae5f25',
* );
*
* @param string stdout of git branch command.
* @param string Filter branches to those on a specific remote.
* @return map Map of 'branch' or 'remote/branch' to hash at HEAD.
*/
public static function parseGitRemoteBranchOutput(
$stdout,
$only_this_remote = null) {
$map = array();
$lines = array_filter(explode("\n", $stdout));
foreach ($lines as $line) {
$matches = null;
if (preg_match('/^ (\S+)\s+-> (\S+)$/', $line, $matches)) {
// This is a line like:
//
// origin/HEAD -> origin/master
//
// ...which we don't currently do anything interesting with, although
// in theory we could use it to automatically choose the default
// branch.
continue;
}
if (!preg_match('/^ *(\S+)\s+([a-z0-9]{40})/', $line, $matches)) {
throw new Exception("Failed to parse {$line}!");
}
$remote_branch = $matches[1];
$branch_head = $matches[2];
if (strpos($remote_branch, 'HEAD') !== false) {
// let's assume that no one will call their remote or branch HEAD
continue;
}
if ($only_this_remote) {
$matches = null;
if (!preg_match('#^([^/]+)/(.*)$#', $remote_branch, $matches)) {
throw new Exception(
"Failed to parse remote branch '{$remote_branch}'!");
}
$remote_name = $matches[1];
$branch_name = $matches[2];
if ($remote_name != $only_this_remote) {
continue;
}
$map[$branch_name] = $branch_head;
} else {
$map[$remote_branch] = $branch_head;
}
}
return $map;
}
}
diff --git a/src/applications/diffusion/query/branch/DiffusionMercurialBranchQuery.php b/src/applications/diffusion/query/branch/DiffusionMercurialBranchQuery.php
index 750c6a3e2a..b6a4f2f712 100644
--- a/src/applications/diffusion/query/branch/DiffusionMercurialBranchQuery.php
+++ b/src/applications/diffusion/query/branch/DiffusionMercurialBranchQuery.php
@@ -1,48 +1,32 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionMercurialBranchQuery extends DiffusionBranchQuery {
protected function executeQuery() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
list($stdout) = $repository->execxLocalCommand(
'--debug branches');
$branch_info = ArcanistMercurialParser::parseMercurialBranches($stdout);
$branches = array();
foreach ($branch_info as $name => $info) {
$branch = new DiffusionBranchInformation();
$branch->setName($name);
$branch->setHeadCommitIdentifier($info['rev']);
$branches[] = $branch;
}
if ($this->getOffset()) {
$branches = array_slice($branches, $this->getOffset());
}
if ($this->getLimit()) {
$branches = array_slice($branches, 0, $this->getLimit());
}
return $branches;
}
}
diff --git a/src/applications/diffusion/query/branch/__tests__/DiffusionGitBranchQueryTestCase.php b/src/applications/diffusion/query/branch/__tests__/DiffusionGitBranchQueryTestCase.php
index 8a2f864ff0..898605a33a 100644
--- a/src/applications/diffusion/query/branch/__tests__/DiffusionGitBranchQueryTestCase.php
+++ b/src/applications/diffusion/query/branch/__tests__/DiffusionGitBranchQueryTestCase.php
@@ -1,57 +1,41 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionGitBranchQueryTestCase
extends PhabricatorTestCase {
public function testRemoteBranchParser() {
$output = <<<EOTXT
origin/HEAD -> origin/master
origin/accent-folding bfaea2e72197506e028c604cd1a294b6e37aa17d Add...
origin/eventordering 185a90a3c1b0556015e5f318fb86ccf8f7a6f3e3 RFC: Order...
origin/master 713f1fc54f9cfc830acbf6bbdb46a2883f772896 Automat...
alternate/stuff 4444444444444444444444444444444444444444 Hmm...
origin/HEAD 713f1fc54f9cfc830acbf6bbdb46a2883f772896
origin/weekend-refactoring 6e947ab0498b82075ca6195ac168385a11326c4b
alternate/release-1.0.0 9ddd5d67962dd89fa167f9989954468b6c517b87
EOTXT;
$this->assertEqual(
array(
'origin/accent-folding' => 'bfaea2e72197506e028c604cd1a294b6e37aa17d',
'origin/eventordering' => '185a90a3c1b0556015e5f318fb86ccf8f7a6f3e3',
'origin/master' => '713f1fc54f9cfc830acbf6bbdb46a2883f772896',
'alternate/stuff' => '4444444444444444444444444444444444444444',
'origin/weekend-refactoring' => '6e947ab0498b82075ca6195ac168385a11326c4b',
'alternate/release-1.0.0' => '9ddd5d67962dd89fa167f9989954468b6c517b87',
),
DiffusionGitBranchQuery::parseGitRemoteBranchOutput($output));
$this->assertEqual(
array(
'accent-folding' => 'bfaea2e72197506e028c604cd1a294b6e37aa17d',
'eventordering' => '185a90a3c1b0556015e5f318fb86ccf8f7a6f3e3',
'master' => '713f1fc54f9cfc830acbf6bbdb46a2883f772896',
'weekend-refactoring' => '6e947ab0498b82075ca6195ac168385a11326c4b',
),
DiffusionGitBranchQuery::parseGitRemoteBranchOutput($output, 'origin'));
}
}
diff --git a/src/applications/diffusion/query/browse/DiffusionBrowseQuery.php b/src/applications/diffusion/query/browse/DiffusionBrowseQuery.php
index 5240d031a4..358815c600 100644
--- a/src/applications/diffusion/query/browse/DiffusionBrowseQuery.php
+++ b/src/applications/diffusion/query/browse/DiffusionBrowseQuery.php
@@ -1,162 +1,146 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DiffusionBrowseQuery {
private $request;
protected $reason;
protected $existedAtCommit;
protected $deletedAtCommit;
protected $validityOnly;
const REASON_IS_FILE = 'is-file';
const REASON_IS_DELETED = 'is-deleted';
const REASON_IS_NONEXISTENT = 'nonexistent';
const REASON_BAD_COMMIT = 'bad-commit';
const REASON_IS_EMPTY = 'empty';
const REASON_IS_UNTRACKED_PARENT = 'untracked-parent';
final private function __construct() {
// <private>
}
final public static function newFromDiffusionRequest(
DiffusionRequest $request) {
$repository = $request->getRepository();
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
// TODO: Verify local-path?
$query = new DiffusionGitBrowseQuery();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$query = new DiffusionMercurialBrowseQuery();
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$query = new DiffusionSvnBrowseQuery();
break;
default:
throw new Exception("Unsupported VCS!");
}
$query->request = $request;
return $query;
}
final protected function getRequest() {
return $this->request;
}
final public function getReasonForEmptyResultSet() {
return $this->reason;
}
final public function getExistedAtCommit() {
return $this->existedAtCommit;
}
final public function getDeletedAtCommit() {
return $this->deletedAtCommit;
}
final public function loadPaths() {
return $this->executeQuery();
}
final public function shouldOnlyTestValidity() {
return $this->validityOnly;
}
final public function needValidityOnly($need_validity_only) {
$this->validityOnly = $need_validity_only;
return $this;
}
final public function renderReadme(array $results) {
$drequest = $this->getRequest();
$readme = null;
foreach ($results as $result) {
$file_type = $result->getFileType();
if (($file_type != ArcanistDiffChangeType::FILE_NORMAL) &&
($file_type != ArcanistDiffChangeType::FILE_TEXT)) {
// Skip directories, etc.
continue;
}
$path = $result->getPath();
if (preg_match('/^readme(|\.txt|\.remarkup|\.rainbow)$/i', $path)) {
$readme = $result;
break;
}
}
if (!$readme) {
return null;
}
$readme_request = DiffusionRequest::newFromDictionary(
array(
'repository' => $drequest->getRepository(),
'commit' => $drequest->getStableCommitName(),
'path' => $readme->getFullPath(),
));
$content_query = DiffusionFileContentQuery::newFromDiffusionRequest(
$readme_request);
$content_query->loadFileContent();
$readme_content = $content_query->getRawData();
if (preg_match('/\\.txt$/', $readme->getPath())) {
$readme_content = phutil_escape_html($readme_content);
$readme_content = nl2br($readme_content);
$class = null;
} else if (preg_match('/\\.rainbow$/', $readme->getPath())) {
$highlighter = new PhutilRainbowSyntaxHighlighter();
$readme_content = $highlighter
->getHighlightFuture($readme_content)
->resolve();
$readme_content = nl2br($readme_content);
require_celerity_resource('syntax-highlighting-css');
$class = 'remarkup-code';
} else {
// Markup extensionless files as remarkup so we get links and such.
$engine = PhabricatorMarkupEngine::newDiffusionMarkupEngine();
$readme_content = $engine->markupText($readme_content);
$class = 'phabricator-remarkup';
}
$readme_content = phutil_render_tag(
'div',
array(
'class' => $class,
),
$readme_content);
return $readme_content;
}
abstract protected function executeQuery();
}
diff --git a/src/applications/diffusion/query/browse/DiffusionGitBrowseQuery.php b/src/applications/diffusion/query/browse/DiffusionGitBrowseQuery.php
index 348a12aaf1..1b81181b7e 100644
--- a/src/applications/diffusion/query/browse/DiffusionGitBrowseQuery.php
+++ b/src/applications/diffusion/query/browse/DiffusionGitBrowseQuery.php
@@ -1,159 +1,143 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionGitBrowseQuery extends DiffusionBrowseQuery {
protected function executeQuery() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
$path = $drequest->getPath();
$commit = $drequest->getCommit();
if ($path == '') {
// Fast path to improve the performance of the repository view; we know
// the root is always a tree at any commit and always exists.
$stdout = 'tree';
} else {
try {
list($stdout) = $repository->execxLocalCommand(
'cat-file -t %s:%s',
$commit,
$path);
} catch (CommandException $e) {
$stderr = $e->getStdErr();
if (preg_match('/^fatal: Not a valid object name/', $stderr)) {
// Grab two logs, since the first one is when the object was deleted.
list($stdout) = $repository->execxLocalCommand(
'log -n2 --format="%%H" %s -- %s',
$commit,
$path);
$stdout = trim($stdout);
if ($stdout) {
$commits = explode("\n", $stdout);
$this->reason = self::REASON_IS_DELETED;
$this->deletedAtCommit = idx($commits, 0);
$this->existedAtCommit = idx($commits, 1);
return array();
}
$this->reason = self::REASON_IS_NONEXISTENT;
return array();
} else {
throw $e;
}
}
}
if (trim($stdout) == 'blob') {
$this->reason = self::REASON_IS_FILE;
return array();
}
if ($this->shouldOnlyTestValidity()) {
return true;
}
list($stdout) = $repository->execxLocalCommand(
'ls-tree -z -l %s:%s',
$commit,
$path);
$submodules = array();
$results = array();
foreach (explode("\0", rtrim($stdout)) as $line) {
// NOTE: Limit to 5 components so we parse filenames with spaces in them
// correctly.
list($mode, $type, $hash, $size, $name) = preg_split('/\s+/', $line, 5);
$result = new DiffusionRepositoryPath();
if ($type == 'tree') {
$file_type = DifferentialChangeType::FILE_DIRECTORY;
} else if ($type == 'commit') {
$file_type = DifferentialChangeType::FILE_SUBMODULE;
$submodules[] = $result;
} else {
$mode = intval($mode, 8);
if (($mode & 0120000) == 0120000) {
$file_type = DifferentialChangeType::FILE_SYMLINK;
} else {
$file_type = DifferentialChangeType::FILE_NORMAL;
}
}
$result->setFullPath($path.$name);
$result->setPath($name);
$result->setHash($hash);
$result->setFileType($file_type);
$result->setFileSize($size);
$results[] = $result;
}
// If we identified submodules, lookup the module info at this commit to
// find their source URIs.
if ($submodules) {
// NOTE: We need to read the file out of git and write it to a temporary
// location because "git config -f" doesn't accept a "commit:path"-style
// argument.
// NOTE: This file may not exist, e.g. because the commit author removed
// it when they added the submodule. See T1448. If it's not present, just
// show the submodule without enriching it. If ".gitmodules" was removed
// it seems to partially break submodules, but the repository as a whole
// continues to work fine and we've seen at least two cases of this in
// the wild.
list($err, $contents) = $repository->execLocalCommand(
'cat-file blob %s:.gitmodules',
$commit);
if (!$err) {
$tmp = new TempFile();
Filesystem::writeFile($tmp, $contents);
list($module_info) = $repository->execxLocalCommand(
'config -l -f %s',
$tmp);
$dict = array();
$lines = explode("\n", trim($module_info));
foreach ($lines as $line) {
list($key, $value) = explode('=', $line, 2);
$parts = explode('.', $key);
$dict[$key] = $value;
}
foreach ($submodules as $path) {
$full_path = $path->getFullPath();
$key = 'submodule.'.$full_path.'.url';
if (isset($dict[$key])) {
$path->setExternalURI($dict[$key]);
}
}
}
}
return $results;
}
}
diff --git a/src/applications/diffusion/query/browse/DiffusionMercurialBrowseQuery.php b/src/applications/diffusion/query/browse/DiffusionMercurialBrowseQuery.php
index 92eb0c65ef..990169a3e7 100644
--- a/src/applications/diffusion/query/browse/DiffusionMercurialBrowseQuery.php
+++ b/src/applications/diffusion/query/browse/DiffusionMercurialBrowseQuery.php
@@ -1,96 +1,80 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionMercurialBrowseQuery extends DiffusionBrowseQuery {
protected function executeQuery() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
$path = $drequest->getPath();
$commit = $drequest->getStableCommitName();
// TODO: This is a really really awful mess but Mercurial doesn't offer
// an equivalent of "git ls-files -- directory". If it's any comfort, this
// is what "hgweb" does too, see:
//
// http://selenic.com/repo/hg/file/91dc8878f888/mercurial/hgweb/webcommands.py#l320
//
// derp derp derp derp
//
// Anyway, figure out what's in this path by applying massive amounts
// of brute force.
list($entire_manifest) = $repository->execxLocalCommand(
'manifest --rev %s',
$commit);
$entire_manifest = explode("\n", $entire_manifest);
$results = array();
$match_against = trim($path, '/');
$match_len = strlen($match_against);
// For the root, don't trim. For other paths, trim the "/" after we match.
// We need this because Mercurial's canonical paths have no leading "/",
// but ours do.
$trim_len = $match_len ? $match_len + 1 : 0;
foreach ($entire_manifest as $path) {
if (strncmp($path, $match_against, $match_len)) {
continue;
}
if (!strlen($path)) {
continue;
}
$remainder = substr($path, $trim_len);
if (!strlen($remainder)) {
// There is a file with this exact name in the manifest, so clearly
// it's a file.
$this->reason = self::REASON_IS_FILE;
return array();
}
$parts = explode('/', $remainder);
if (count($parts) == 1) {
$type = DifferentialChangeType::FILE_NORMAL;
} else {
$type = DifferentialChangeType::FILE_DIRECTORY;
}
$results[reset($parts)] = $type;
}
foreach ($results as $key => $type) {
$result = new DiffusionRepositoryPath();
$result->setPath($key);
$result->setFileType($type);
$result->setFullPath(ltrim($match_against.'/', '/').$key);
$results[$key] = $result;
}
if (empty($results)) {
// TODO: Detect "deleted" by issuing "hg log"?
$this->reason = self::REASON_IS_NONEXISTENT;
}
return $results;
}
}
diff --git a/src/applications/diffusion/query/browse/DiffusionSvnBrowseQuery.php b/src/applications/diffusion/query/browse/DiffusionSvnBrowseQuery.php
index d7699ff74a..4e1467e6f7 100644
--- a/src/applications/diffusion/query/browse/DiffusionSvnBrowseQuery.php
+++ b/src/applications/diffusion/query/browse/DiffusionSvnBrowseQuery.php
@@ -1,204 +1,188 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionSvnBrowseQuery extends DiffusionBrowseQuery {
protected function executeQuery() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
$path = $drequest->getPath();
$commit = $drequest->getCommit();
$subpath = $repository->getDetail('svn-subpath');
if ($subpath && strncmp($subpath, $path, strlen($subpath))) {
// If we have a subpath and the path isn't a child of it, it (almost
// certainly) won't exist since we don't track commits which affect
// it. (Even if it exists, return a consistent result.)
$this->reason = self::REASON_IS_UNTRACKED_PARENT;
return array();
}
$conn_r = $repository->establishConnection('r');
$parent_path = DiffusionPathIDQuery::getParentPath($path);
$path_query = new DiffusionPathIDQuery(
array(
$path,
$parent_path,
));
$path_map = $path_query->loadPathIDs();
$path_id = $path_map[$path];
$parent_path_id = $path_map[$parent_path];
if (empty($path_id)) {
$this->reason = self::REASON_IS_NONEXISTENT;
return array();
}
if ($commit) {
$slice_clause = 'AND svnCommit <= '.(int)$commit;
} else {
$slice_clause = '';
}
$index = queryfx_all(
$conn_r,
'SELECT pathID, max(svnCommit) maxCommit FROM %T WHERE
repositoryID = %d AND parentID = %d
%Q GROUP BY pathID',
PhabricatorRepository::TABLE_FILESYSTEM,
$repository->getID(),
$path_id,
$slice_clause);
if (!$index) {
if ($path == '/') {
$this->reason = self::REASON_IS_EMPTY;
} else {
// NOTE: The parent path ID is included so this query can take
// advantage of the table's primary key; it is uniquely determined by
// the pathID but if we don't do the lookup ourselves MySQL doesn't have
// the information it needs to avoid a table scan.
$reasons = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE repositoryID = %d
AND parentID = %d
AND pathID = %d
%Q ORDER BY svnCommit DESC LIMIT 2',
PhabricatorRepository::TABLE_FILESYSTEM,
$repository->getID(),
$parent_path_id,
$path_id,
$slice_clause);
$reason = reset($reasons);
if (!$reason) {
$this->reason = self::REASON_IS_NONEXISTENT;
} else {
$file_type = $reason['fileType'];
if (empty($reason['existed'])) {
$this->reason = self::REASON_IS_DELETED;
$this->deletedAtCommit = $reason['svnCommit'];
if (!empty($reasons[1])) {
$this->existedAtCommit = $reasons[1]['svnCommit'];
}
} else if ($file_type == DifferentialChangeType::FILE_DIRECTORY) {
$this->reason = self::REASON_IS_EMPTY;
} else {
$this->reason = self::REASON_IS_FILE;
}
}
}
return array();
}
if ($this->shouldOnlyTestValidity()) {
return true;
}
$sql = array();
foreach ($index as $row) {
$sql[] = '('.(int)$row['pathID'].', '.(int)$row['maxCommit'].')';
}
$browse = queryfx_all(
$conn_r,
'SELECT *, p.path pathName
FROM %T f JOIN %T p ON f.pathID = p.id
WHERE repositoryID = %d
AND parentID = %d
AND existed = 1
AND (pathID, svnCommit) in (%Q)
ORDER BY pathName',
PhabricatorRepository::TABLE_FILESYSTEM,
PhabricatorRepository::TABLE_PATH,
$repository->getID(),
$path_id,
implode(', ', $sql));
$loadable_commits = array();
foreach ($browse as $key => $file) {
// We need to strip out directories because we don't store last-modified
// in the filesystem table.
if ($file['fileType'] != DifferentialChangeType::FILE_DIRECTORY) {
$loadable_commits[] = $file['svnCommit'];
$browse[$key]['hasCommit'] = true;
}
}
$commits = array();
$commit_data = array();
if ($loadable_commits) {
// NOTE: Even though these are integers, use '%Ls' because MySQL doesn't
// use the second part of the key otherwise!
$commits = id(new PhabricatorRepositoryCommit())->loadAllWhere(
'repositoryID = %d AND commitIdentifier IN (%Ls)',
$repository->getID(),
$loadable_commits);
$commits = mpull($commits, null, 'getCommitIdentifier');
if ($commits) {
$commit_data = id(new PhabricatorRepositoryCommitData())->loadAllWhere(
'commitID in (%Ld)',
mpull($commits, 'getID'));
$commit_data = mpull($commit_data, null, 'getCommitID');
} else {
$commit_data = array();
}
}
$path_normal = DiffusionPathIDQuery::normalizePath($path);
$results = array();
foreach ($browse as $file) {
$full_path = $file['pathName'];
$file_path = ltrim(substr($full_path, strlen($path_normal)), '/');
$full_path = ltrim($full_path, '/');
$result = new DiffusionRepositoryPath();
$result->setPath($file_path);
$result->setFullPath($full_path);
// $result->setHash($hash);
$result->setFileType($file['fileType']);
// $result->setFileSize($size);
if (!empty($file['hasCommit'])) {
$commit = idx($commits, $file['svnCommit']);
if ($commit) {
$data = idx($commit_data, $commit->getID());
$result->setLastModifiedCommit($commit);
$result->setLastCommitData($data);
}
}
$results[] = $result;
}
if (empty($results)) {
$this->reason = self::REASON_IS_EMPTY;
}
return $results;
}
}
diff --git a/src/applications/diffusion/query/committags/DiffusionCommitTagsQuery.php b/src/applications/diffusion/query/committags/DiffusionCommitTagsQuery.php
index a919a6b710..5cacdd813f 100644
--- a/src/applications/diffusion/query/committags/DiffusionCommitTagsQuery.php
+++ b/src/applications/diffusion/query/committags/DiffusionCommitTagsQuery.php
@@ -1,51 +1,35 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DiffusionCommitTagsQuery extends DiffusionQuery {
private $limit;
private $offset;
public function setOffset($offset) {
$this->offset = $offset;
return $this;
}
public function getOffset() {
return $this->offset;
}
public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
protected function getLimit() {
return $this->limit;
}
final public static function newFromDiffusionRequest(
DiffusionRequest $request) {
return self::newQueryObject(__CLASS__, $request);
}
final public function loadTags() {
return $this->executeQuery();
}
}
diff --git a/src/applications/diffusion/query/committags/DiffusionGitCommitTagsQuery.php b/src/applications/diffusion/query/committags/DiffusionGitCommitTagsQuery.php
index 649f180f65..3a25b835d9 100644
--- a/src/applications/diffusion/query/committags/DiffusionGitCommitTagsQuery.php
+++ b/src/applications/diffusion/query/committags/DiffusionGitCommitTagsQuery.php
@@ -1,64 +1,48 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionGitCommitTagsQuery
extends DiffusionCommitTagsQuery {
protected function executeQuery() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
list($err, $stdout) = $repository->execLocalCommand(
'tag -l --contains %s',
$drequest->getCommit());
if ($err) {
// Git exits with an error code if the commit is bogus.
return array();
}
$stdout = trim($stdout);
if (!strlen($stdout)) {
return array();
}
$tag_names = explode("\n", $stdout);
$tag_names = array_fill_keys($tag_names, true);
$tag_query = DiffusionTagListQuery::newFromDiffusionRequest($drequest);
$tags = $tag_query->loadTags();
$result = array();
foreach ($tags as $tag) {
if (isset($tag_names[$tag->getName()])) {
$result[] = $tag;
}
}
if ($this->getOffset()) {
$result = array_slice($result, $this->getOffset());
}
if ($this->getLimit()) {
$result = array_slice($result, 0, $this->getLimit());
}
return $result;
}
}
diff --git a/src/applications/diffusion/query/committags/DiffusionMercurialCommitTagsQuery.php b/src/applications/diffusion/query/committags/DiffusionMercurialCommitTagsQuery.php
index 1395e681bd..dd89ae1180 100644
--- a/src/applications/diffusion/query/committags/DiffusionMercurialCommitTagsQuery.php
+++ b/src/applications/diffusion/query/committags/DiffusionMercurialCommitTagsQuery.php
@@ -1,27 +1,11 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionMercurialCommitTagsQuery
extends DiffusionCommitTagsQuery {
protected function executeQuery() {
// TODO: Implement this.
return array();
}
}
diff --git a/src/applications/diffusion/query/committags/DiffusionSvnCommitTagsQuery.php b/src/applications/diffusion/query/committags/DiffusionSvnCommitTagsQuery.php
index 03fe5181b1..55ab217142 100644
--- a/src/applications/diffusion/query/committags/DiffusionSvnCommitTagsQuery.php
+++ b/src/applications/diffusion/query/committags/DiffusionSvnCommitTagsQuery.php
@@ -1,27 +1,11 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionSvnCommitTagsQuery
extends DiffusionCommitTagsQuery {
protected function executeQuery() {
// No meaningful concept of tags in Subversion.
return array();
}
}
diff --git a/src/applications/diffusion/query/contains/DiffusionContainsQuery.php b/src/applications/diffusion/query/contains/DiffusionContainsQuery.php
index a933a192ef..03aaf0aac7 100644
--- a/src/applications/diffusion/query/contains/DiffusionContainsQuery.php
+++ b/src/applications/diffusion/query/contains/DiffusionContainsQuery.php
@@ -1,30 +1,14 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DiffusionContainsQuery extends DiffusionQuery {
final public static function newFromDiffusionRequest(
DiffusionRequest $request) {
return self::newQueryObject(__CLASS__, $request);
}
final public function loadContainingBranches() {
return $this->executeQuery();
}
}
diff --git a/src/applications/diffusion/query/contains/DiffusionGitContainsQuery.php b/src/applications/diffusion/query/contains/DiffusionGitContainsQuery.php
index 96f31f12e7..8a78e40e13 100644
--- a/src/applications/diffusion/query/contains/DiffusionGitContainsQuery.php
+++ b/src/applications/diffusion/query/contains/DiffusionGitContainsQuery.php
@@ -1,34 +1,18 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionGitContainsQuery extends DiffusionContainsQuery {
final public function executeQuery() {
$request = $this->getRequest();
$repository = $request->getRepository();
list($contains) = $repository->execxLocalCommand(
'branch -r --verbose --no-abbrev --contains %s',
$request->getCommit());
return DiffusionGitBranchQuery::parseGitRemoteBranchOutput(
$contains,
DiffusionBranchInformation::DEFAULT_GIT_REMOTE);
}
}
diff --git a/src/applications/diffusion/query/contains/DiffusionMercurialContainsQuery.php b/src/applications/diffusion/query/contains/DiffusionMercurialContainsQuery.php
index 98b8bae3df..d796525c55 100644
--- a/src/applications/diffusion/query/contains/DiffusionMercurialContainsQuery.php
+++ b/src/applications/diffusion/query/contains/DiffusionMercurialContainsQuery.php
@@ -1,28 +1,12 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionMercurialContainsQuery extends DiffusionContainsQuery {
protected function executeQuery() {
// TODO: Implement this.
return array();
}
}
diff --git a/src/applications/diffusion/query/contains/DiffusionSvnContainsQuery.php b/src/applications/diffusion/query/contains/DiffusionSvnContainsQuery.php
index 0926466e02..3f86c46931 100644
--- a/src/applications/diffusion/query/contains/DiffusionSvnContainsQuery.php
+++ b/src/applications/diffusion/query/contains/DiffusionSvnContainsQuery.php
@@ -1,26 +1,10 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionSvnContainsQuery extends DiffusionContainsQuery {
protected function executeQuery() {
// NOTE: Subversion doesn't have branches, so we always return empty.
return array();
}
}
diff --git a/src/applications/diffusion/query/diff/DiffusionDiffQuery.php b/src/applications/diffusion/query/diff/DiffusionDiffQuery.php
index c29590ff78..804d8d3e09 100644
--- a/src/applications/diffusion/query/diff/DiffusionDiffQuery.php
+++ b/src/applications/diffusion/query/diff/DiffusionDiffQuery.php
@@ -1,49 +1,33 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DiffusionDiffQuery extends DiffusionQuery {
protected $renderingReference;
final public static function newFromDiffusionRequest(
DiffusionRequest $request) {
return parent::newQueryObject(__CLASS__, $request);
}
final public function getRenderingReference() {
return $this->renderingReference;
}
final public function loadChangeset() {
return $this->executeQuery();
}
protected function getEffectiveCommit() {
$drequest = $this->getRequest();
$modified_query = DiffusionLastModifiedQuery::newFromDiffusionRequest(
$drequest);
list($commit) = $modified_query->loadLastModification();
if (!$commit) {
// TODO: Improve error messages here.
return null;
}
return $commit->getCommitIdentifier();
}
}
diff --git a/src/applications/diffusion/query/diff/DiffusionGitDiffQuery.php b/src/applications/diffusion/query/diff/DiffusionGitDiffQuery.php
index 73446cc958..51c5d19bc1 100644
--- a/src/applications/diffusion/query/diff/DiffusionGitDiffQuery.php
+++ b/src/applications/diffusion/query/diff/DiffusionGitDiffQuery.php
@@ -1,61 +1,45 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionGitDiffQuery extends DiffusionDiffQuery {
protected function executeQuery() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
$effective_commit = $this->getEffectiveCommit();
if (!$effective_commit) {
return null;
}
// TODO: This side effect is kind of sketchy.
$drequest->setCommit($effective_commit);
$raw_query = DiffusionRawDiffQuery::newFromDiffusionRequest($drequest);
$raw_diff = $raw_query->loadRawDiff();
if (!$raw_diff) {
return null;
}
$parser = new ArcanistDiffParser();
$try_encoding = $repository->getDetail('encoding');
if ($try_encoding) {
$parser->setTryEncoding($try_encoding);
}
$parser->setDetectBinaryFiles(true);
$changes = $parser->parseDiff($raw_diff);
$diff = DifferentialDiff::newFromRawChanges($changes);
$changesets = $diff->getChangesets();
$changeset = reset($changesets);
$this->renderingReference = $drequest->generateURI(
array(
'action' => 'rendering-ref',
));
return $changeset;
}
}
diff --git a/src/applications/diffusion/query/diff/DiffusionMercurialDiffQuery.php b/src/applications/diffusion/query/diff/DiffusionMercurialDiffQuery.php
index d6525e95fb..ba1e32863d 100644
--- a/src/applications/diffusion/query/diff/DiffusionMercurialDiffQuery.php
+++ b/src/applications/diffusion/query/diff/DiffusionMercurialDiffQuery.php
@@ -1,57 +1,41 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionMercurialDiffQuery extends DiffusionDiffQuery {
protected function executeQuery() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
$effective_commit = $this->getEffectiveCommit();
if (!$effective_commit) {
return null;
}
// TODO: This side effect is kind of skethcy.
$drequest->setCommit($effective_commit);
$query = DiffusionRawDiffQuery::newFromDiffusionRequest($drequest);
$raw_diff = $query->loadRawDiff();
$parser = new ArcanistDiffParser();
$try_encoding = $repository->getDetail('encoding');
if ($try_encoding) {
$parser->setTryEncoding($try_encoding);
}
$parser->setDetectBinaryFiles(true);
$changes = $parser->parseDiff($raw_diff);
$diff = DifferentialDiff::newFromRawChanges($changes);
$changesets = $diff->getChangesets();
$changeset = reset($changesets);
$this->renderingReference = $drequest->generateURI(
array(
'action' => 'rendering-ref',
));
return $changeset;
}
}
diff --git a/src/applications/diffusion/query/diff/DiffusionSvnDiffQuery.php b/src/applications/diffusion/query/diff/DiffusionSvnDiffQuery.php
index 3824c74df1..083d584907 100644
--- a/src/applications/diffusion/query/diff/DiffusionSvnDiffQuery.php
+++ b/src/applications/diffusion/query/diff/DiffusionSvnDiffQuery.php
@@ -1,163 +1,147 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionSvnDiffQuery extends DiffusionDiffQuery {
protected function executeQuery() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
$effective_commit = $this->getEffectiveCommit();
if (!$effective_commit) {
return null;
}
$drequest = clone $drequest;
$drequest->setCommit($effective_commit);
$path_change_query = DiffusionPathChangeQuery::newFromDiffusionRequest(
$drequest);
$path_changes = $path_change_query->loadChanges();
$path = null;
foreach ($path_changes as $change) {
if ($change->getPath() == $drequest->getPath()) {
$path = $change;
}
}
if (!$path) {
return null;
}
$change_type = $path->getChangeType();
switch ($change_type) {
case DifferentialChangeType::TYPE_MULTICOPY:
case DifferentialChangeType::TYPE_DELETE:
if ($path->getTargetPath()) {
$old = array(
$path->getTargetPath(),
$path->getTargetCommitIdentifier());
} else {
$old = array($path->getPath(), $path->getCommitIdentifier() - 1);
}
$old_name = $path->getPath();
$new_name = '';
$new = null;
break;
case DifferentialChangeType::TYPE_ADD:
$old = null;
$new = array($path->getPath(), $path->getCommitIdentifier());
$old_name = '';
$new_name = $path->getPath();
break;
case DifferentialChangeType::TYPE_MOVE_HERE:
case DifferentialChangeType::TYPE_COPY_HERE:
$old = array(
$path->getTargetPath(),
$path->getTargetCommitIdentifier());
$new = array($path->getPath(), $path->getCommitIdentifier());
$old_name = $path->getTargetPath();
$new_name = $path->getPath();
break;
case DifferentialChangeType::TYPE_MOVE_AWAY:
$old = array(
$path->getPath(),
$path->getCommitIdentifier() - 1);
$old_name = $path->getPath();
$new_name = null;
$new = null;
break;
default:
$old = array($path->getPath(), $path->getCommitIdentifier() - 1);
$new = array($path->getPath(), $path->getCommitIdentifier());
$old_name = $path->getPath();
$new_name = $path->getPath();
break;
}
$futures = array(
'old' => $this->buildContentFuture($old),
'new' => $this->buildContentFuture($new),
);
$futures = array_filter($futures);
foreach (Futures($futures) as $key => $future) {
$stdout = '';
try {
list($stdout) = $future->resolvex();
} catch (CommandException $e) {
if ($path->getFileType() != DifferentialChangeType::FILE_DIRECTORY) {
throw $e;
}
}
$futures[$key] = $stdout;
}
$old_data = idx($futures, 'old', '');
$new_data = idx($futures, 'new', '');
$engine = new PhabricatorDifferenceEngine();
$engine->setOldName($old_name);
$engine->setNewName($new_name);
$raw_diff = $engine->generateRawDiffFromFileContent($old_data, $new_data);
$parser = new ArcanistDiffParser();
$try_encoding = $repository->getDetail('encoding');
if ($try_encoding) {
$parser->setTryEncoding($try_encoding);
}
$parser->setDetectBinaryFiles(true);
$arcanist_changes = DiffusionPathChange::convertToArcanistChanges(
$path_changes);
$parser->setChanges($arcanist_changes);
$parser->forcePath($path->getPath());
$changes = $parser->parseDiff($raw_diff);
$change = $changes[$path->getPath()];
$diff = DifferentialDiff::newFromRawChanges(array($change));
$changesets = $diff->getChangesets();
$changeset = reset($changesets);
$this->renderingReference = $drequest->getPath().';'.$drequest->getCommit();
return $changeset;
}
private function buildContentFuture($spec) {
if (!$spec) {
return null;
}
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
list($ref, $rev) = $spec;
return $repository->getRemoteCommandFuture(
'cat %s%s@%d',
$repository->getRemoteURI(),
$ref,
$rev);
}
}
diff --git a/src/applications/diffusion/query/exists/DiffusionExistsQuery.php b/src/applications/diffusion/query/exists/DiffusionExistsQuery.php
index 1e67a72856..17a22970c4 100644
--- a/src/applications/diffusion/query/exists/DiffusionExistsQuery.php
+++ b/src/applications/diffusion/query/exists/DiffusionExistsQuery.php
@@ -1,29 +1,13 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DiffusionExistsQuery extends DiffusionQuery {
final public static function newFromDiffusionRequest(
DiffusionRequest $request) {
return self::newQueryObject(__CLASS__, $request);
}
final public function loadExistentialData() {
return $this->executeQuery();
}
}
diff --git a/src/applications/diffusion/query/exists/DiffusionGitExistsQuery.php b/src/applications/diffusion/query/exists/DiffusionGitExistsQuery.php
index 40cbf49e4b..45e0aa43cd 100644
--- a/src/applications/diffusion/query/exists/DiffusionGitExistsQuery.php
+++ b/src/applications/diffusion/query/exists/DiffusionGitExistsQuery.php
@@ -1,33 +1,17 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionGitExistsQuery extends DiffusionExistsQuery {
final protected function executeQuery() {
$request = $this->getRequest();
$repository = $request->getRepository();
list($err, $merge_base) = $repository->execLocalCommand(
'cat-file -t %s',
$request->getCommit()
);
return !$err;
}
}
diff --git a/src/applications/diffusion/query/exists/DiffusionMercurialExistsQuery.php b/src/applications/diffusion/query/exists/DiffusionMercurialExistsQuery.php
index ea28beb20a..7623c3412c 100644
--- a/src/applications/diffusion/query/exists/DiffusionMercurialExistsQuery.php
+++ b/src/applications/diffusion/query/exists/DiffusionMercurialExistsQuery.php
@@ -1,33 +1,17 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionMercurialExistsQuery extends DiffusionExistsQuery {
protected function executeQuery() {
$request = $this->getRequest();
$repository = $request->getRepository();
list($err, $stdout) = $repository->execLocalCommand(
'id --rev %s',
$request->getCommit()
);
return !$err;
}
}
diff --git a/src/applications/diffusion/query/exists/DiffusionSvnExistsQuery.php b/src/applications/diffusion/query/exists/DiffusionSvnExistsQuery.php
index 33da27facd..cfd6b6f2fd 100644
--- a/src/applications/diffusion/query/exists/DiffusionSvnExistsQuery.php
+++ b/src/applications/diffusion/query/exists/DiffusionSvnExistsQuery.php
@@ -1,40 +1,24 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionSvnExistsQuery extends DiffusionExistsQuery {
protected function executeQuery() {
$request = $this->getRequest();
$repository = $request->getRepository();
list($info) = $repository->execxRemoteCommand(
'info %s',
$repository->getRemoteURI()
);
$matches = null;
$exists = false;
if (preg_match('/^Revision: (\d+)$/m', $info, $matches)) {
$base_revision = $matches[1];
$exists = $base_revision >= $request->getCommit();
}
return $exists;
}
}
diff --git a/src/applications/diffusion/query/filecontent/DiffusionFileContentQuery.php b/src/applications/diffusion/query/filecontent/DiffusionFileContentQuery.php
index 0e28b1587e..0004dec335 100644
--- a/src/applications/diffusion/query/filecontent/DiffusionFileContentQuery.php
+++ b/src/applications/diffusion/query/filecontent/DiffusionFileContentQuery.php
@@ -1,141 +1,125 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DiffusionFileContentQuery extends DiffusionQuery {
private $needsBlame;
private $fileContent;
private $viewer;
final public static function newFromDiffusionRequest(
DiffusionRequest $request) {
return parent::newQueryObject(__CLASS__, $request);
}
final public function loadFileContent() {
$this->fileContent = $this->executeQuery();
$repository = $this->getRequest()->getRepository();
$try_encoding = $repository->getDetail('encoding');
if ($try_encoding) {
$this->fileContent->setCorpus(
phutil_utf8_convert(
$this->fileContent->getCorpus(), "UTF-8", $try_encoding));
}
return $this->fileContent;
}
final public function getRawData() {
return $this->fileContent->getCorpus();
}
final public function getBlameData() {
$raw_data = $this->getRawData();
$text_list = array();
$rev_list = array();
$blame_dict = array();
if (!$this->getNeedsBlame()) {
$text_list = explode("\n", rtrim($raw_data));
} else {
$lines = array();
foreach (explode("\n", rtrim($raw_data)) as $k => $line) {
$lines[$k] = $this->tokenizeLine($line);
list($rev_id, $author, $text) = $lines[$k];
$text_list[$k] = $text;
$rev_list[$k] = $rev_id;
}
$rev_list = $this->processRevList($rev_list);
foreach ($lines as $k => $line) {
list($rev_id, $author, $text) = $line;
$rev_id = $rev_list[$k];
if (!isset($blame_dict[$rev_id])) {
$blame_dict[$rev_id]['author'] = $author;
}
}
$repository = $this->getRequest()->getRepository();
$commits = id(new PhabricatorAuditCommitQuery())
->withIdentifiers(
$repository->getID(),
array_unique($rev_list))
->execute();
foreach ($commits as $commit) {
$blame_dict[$commit->getCommitIdentifier()]['epoch'] =
$commit->getEpoch();
}
$commits_data = array();
if ($commits) {
$commits_data = id(new PhabricatorRepositoryCommitData())->loadAllWhere(
'commitID IN (%Ls)',
mpull($commits, 'getID'));
}
$phids = array();
foreach ($commits_data as $data) {
$phids[] = $data->getCommitDetail('authorPHID');
}
$loader = new PhabricatorObjectHandleData(array_unique($phids));
if ($this->viewer) {
$loader->setViewer($this->viewer);
}
$handles = $loader->loadHandles();
foreach ($commits_data as $data) {
if ($data->getCommitDetail('authorPHID')) {
$commit_identifier =
$commits[$data->getCommitID()]->getCommitIdentifier();
$blame_dict[$commit_identifier]['handle'] =
$handles[$data->getCommitDetail('authorPHID')];
}
}
}
return array($text_list, $rev_list, $blame_dict);
}
abstract protected function tokenizeLine($line);
public function setNeedsBlame($needs_blame) {
$this->needsBlame = $needs_blame;
return $this;
}
public function getNeedsBlame() {
return $this->needsBlame;
}
public function setViewer(PhabricatorUser $user) {
$this->viewer = $user;
return $this;
}
protected function processRevList(array $rev_list) {
return $rev_list;
}
}
diff --git a/src/applications/diffusion/query/filecontent/DiffusionGitFileContentQuery.php b/src/applications/diffusion/query/filecontent/DiffusionGitFileContentQuery.php
index 0e427e4858..4703937371 100644
--- a/src/applications/diffusion/query/filecontent/DiffusionGitFileContentQuery.php
+++ b/src/applications/diffusion/query/filecontent/DiffusionGitFileContentQuery.php
@@ -1,65 +1,49 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionGitFileContentQuery extends DiffusionFileContentQuery {
protected function executeQuery() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
$path = $drequest->getPath();
$commit = $drequest->getCommit();
if ($this->getNeedsBlame()) {
list($corpus) = $repository->execxLocalCommand(
'--no-pager blame -c -l --date=short %s -- %s',
$commit,
$path);
} else {
list($corpus) = $repository->execxLocalCommand(
'cat-file blob %s:%s',
$commit,
$path);
}
$file_content = new DiffusionFileContent();
$file_content->setCorpus($corpus);
return $file_content;
}
protected function tokenizeLine($line) {
$m = array();
// sample lines:
//
// d1b4fcdd2a7c8c0f8cbdd01ca839d992135424dc
// ( hzhao 2009-05-01 202)function print();
//
// 8220d5d54f6d5d5552a636576cbe9c35f15b65b2
// (Andrew Gallagher 2010-12-03 324)
// // Add the lines for trailing context
preg_match('/^\s*?(\S+?)\s*\(\s*([^)]*)\s+\d{4}-\d{2}-\d{2}\s+\d+\)(.*)?$/',
$line, $m);
$rev_id = $m[1];
$author = $m[2];
$text = idx($m, 3);
return array($rev_id, $author, $text);
}
}
diff --git a/src/applications/diffusion/query/filecontent/DiffusionMercurialFileContentQuery.php b/src/applications/diffusion/query/filecontent/DiffusionMercurialFileContentQuery.php
index 070176ec03..512a427253 100644
--- a/src/applications/diffusion/query/filecontent/DiffusionMercurialFileContentQuery.php
+++ b/src/applications/diffusion/query/filecontent/DiffusionMercurialFileContentQuery.php
@@ -1,90 +1,74 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionMercurialFileContentQuery
extends DiffusionFileContentQuery {
protected function executeQuery() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
$path = $drequest->getPath();
$commit = $drequest->getCommit();
if ($this->getNeedsBlame()) {
// NOTE: We're using "--number" instead of "--changeset" because there is
// no way to get "--changeset" to show us the full commit hashes.
list($corpus) = $repository->execxLocalCommand(
'annotate --user --number --rev %s -- %s',
$commit,
$path);
} else {
list($corpus) = $repository->execxLocalCommand(
'cat --rev %s -- %s',
$commit,
$path);
}
$file_content = new DiffusionFileContent();
$file_content->setCorpus($corpus);
return $file_content;
}
protected function tokenizeLine($line) {
$matches = null;
preg_match(
'/^(.*?)\s+([0-9]+): (.*)$/',
$line,
$matches);
return array($matches[2], $matches[1], $matches[3]);
}
/**
* Convert local revision IDs into full commit identifier hashes.
*/
protected function processRevList(array $rev_list) {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
$revs = array_unique($rev_list);
foreach ($revs as $key => $rev) {
$revs[$key] = '--rev '.(int)$rev;
}
list($stdout) = $repository->execxLocalCommand(
'log --template=%s %C',
'{rev} {node}\\n',
implode(' ', $revs));
$map = array();
foreach (explode("\n", trim($stdout)) as $line) {
list($rev, $node) = explode(' ', $line);
$map[$rev] = $node;
}
foreach ($rev_list as $k => $rev) {
$rev_list[$k] = $map[$rev];
}
return $rev_list;
}
}
diff --git a/src/applications/diffusion/query/filecontent/DiffusionSvnFileContentQuery.php b/src/applications/diffusion/query/filecontent/DiffusionSvnFileContentQuery.php
index 29b9baccb7..e77c6851ce 100644
--- a/src/applications/diffusion/query/filecontent/DiffusionSvnFileContentQuery.php
+++ b/src/applications/diffusion/query/filecontent/DiffusionSvnFileContentQuery.php
@@ -1,71 +1,55 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionSvnFileContentQuery extends DiffusionFileContentQuery {
protected function executeQuery() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
$path = $drequest->getPath();
$commit = $drequest->getCommit();
$remote_uri = $repository->getRemoteURI();
try {
list($corpus) = $repository->execxRemoteCommand(
'%s %s%s@%s',
$this->getNeedsBlame() ? 'blame' : 'cat',
$remote_uri,
$path,
$commit);
} catch (CommandException $ex) {
$stderr = $ex->getStdErr();
if (preg_match('/path not found$/', trim($stderr))) {
// TODO: Improve user experience for this. One way to end up here
// is to have the parser behind and look at a file which was recently
// nuked; Diffusion will think it still exists and try to grab content
// at HEAD.
throw new Exception(
"Failed to retrieve file content from Subversion. The file may ".
"have been recently deleted, or the Diffusion cache may be out of ".
"date.");
} else {
throw $ex;
}
}
$file_content = new DiffusionFileContent();
$file_content->setCorpus($corpus);
return $file_content;
}
protected function tokenizeLine($line) {
// sample line:
// 347498 yliu function print();
$m = array();
preg_match('/^\s*(\d+)\s+(\S+)(?: (.*))?$/', $line, $m);
$rev_id = $m[1];
$author = $m[2];
$text = idx($m, 3);
return array($rev_id, $author, $text);
}
}
diff --git a/src/applications/diffusion/query/history/DiffusionGitHistoryQuery.php b/src/applications/diffusion/query/history/DiffusionGitHistoryQuery.php
index 7b61abd292..1d79412c0c 100644
--- a/src/applications/diffusion/query/history/DiffusionGitHistoryQuery.php
+++ b/src/applications/diffusion/query/history/DiffusionGitHistoryQuery.php
@@ -1,60 +1,44 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionGitHistoryQuery extends DiffusionHistoryQuery {
protected function executeQuery() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
$path = $drequest->getPath();
$commit_hash = $drequest->getCommit();
list($stdout) = $repository->execxLocalCommand(
'log '.
'--skip=%d '.
'-n %d '.
'--pretty=format:%s '.
'%s -- %C',
$this->getOffset(),
$this->getLimit(),
'%H:%P',
$commit_hash,
// Git omits merge commits if the path is provided, even if it is empty.
(strlen($path) ? csprintf('%s', $path) : ''));
$lines = explode("\n", trim($stdout));
$lines = array_filter($lines);
if (!$lines) {
return array();
}
$hash_list = array();
$parent_map = array();
foreach ($lines as $line) {
list($hash, $parents) = explode(":", $line);
$hash_list[] = $hash;
$parent_map[$hash] = preg_split('/\s+/', $parents);
}
$this->parents = $parent_map;
return $this->loadHistoryForCommitIdentifiers($hash_list);
}
}
diff --git a/src/applications/diffusion/query/history/DiffusionHistoryQuery.php b/src/applications/diffusion/query/history/DiffusionHistoryQuery.php
index 865cc3cffc..56c1a6020e 100644
--- a/src/applications/diffusion/query/history/DiffusionHistoryQuery.php
+++ b/src/applications/diffusion/query/history/DiffusionHistoryQuery.php
@@ -1,80 +1,64 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DiffusionHistoryQuery extends DiffusionQuery {
private $limit = 100;
private $offset = 0;
protected $needDirectChanges;
protected $needChildChanges;
protected $needParents;
protected $parents = array();
final public static function newFromDiffusionRequest(
DiffusionRequest $request) {
return parent::newQueryObject(__CLASS__, $request);
}
final public function needDirectChanges($direct) {
$this->needDirectChanges = $direct;
return $this;
}
final public function needChildChanges($child) {
$this->needChildChanges = $child;
return $this;
}
final public function needParents($parents) {
$this->needParents = $parents;
return $this;
}
final public function getParents() {
if (!$this->needParents) {
throw new Exception('Specify needParents() before calling getParents()!');
}
return $this->parents;
}
final public function loadHistory() {
return $this->executeQuery();
}
final public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
final public function getLimit() {
return $this->limit;
}
final public function setOffset($offset) {
$this->offset = $offset;
return $this;
}
final public function getOffset() {
return $this->offset;
}
}
diff --git a/src/applications/diffusion/query/history/DiffusionMercurialHistoryQuery.php b/src/applications/diffusion/query/history/DiffusionMercurialHistoryQuery.php
index 7b6add1adb..5377e820f9 100644
--- a/src/applications/diffusion/query/history/DiffusionMercurialHistoryQuery.php
+++ b/src/applications/diffusion/query/history/DiffusionMercurialHistoryQuery.php
@@ -1,105 +1,89 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionMercurialHistoryQuery extends DiffusionHistoryQuery {
protected function executeQuery() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
$path = $drequest->getPath();
$commit_hash = $drequest->getStableCommitName();
$path = DiffusionPathIDQuery::normalizePath($path);
$path = ltrim($path, '/');
// NOTE: Older versions of Mercurial give different results for these
// commands (see T1268):
//
// $ hg log -- ''
// $ hg log
//
// All versions of Mercurial give different results for these commands
// (merge commits are excluded with the "." version):
//
// $ hg log -- .
// $ hg log
//
// If we don't have a path component in the query, omit it from the command
// entirely to avoid these inconsistencies.
$path_arg = '';
if (strlen($path)) {
$path_arg = csprintf('-- %s', $path);
}
// NOTE: --branch used to be called --only-branch; use -b for compatibility.
list($stdout) = $repository->execxLocalCommand(
'log --debug --template %s --limit %d -b %s --rev %s:0 %C',
'{node};{parents}\\n',
($this->getOffset() + $this->getLimit()), // No '--skip' in Mercurial.
$drequest->getBranch(),
$commit_hash,
$path_arg);
$lines = explode("\n", trim($stdout));
$lines = array_slice($lines, $this->getOffset());
$hash_list = array();
$parent_map = array();
$last = null;
foreach (array_reverse($lines) as $line) {
list($hash, $parents) = explode(';', $line);
$parents = trim($parents);
if (!$parents) {
if ($last === null) {
$parent_map[$hash] = array('...');
} else {
$parent_map[$hash] = array($last);
}
} else {
$parents = preg_split('/\s+/', $parents);
foreach ($parents as $parent) {
list($plocal, $phash) = explode(':', $parent);
if (!preg_match('/^0+$/', $phash)) {
$parent_map[$hash][] = $phash;
}
}
// This may happen for the zeroth commit in repository, both hashes
// are "000000000...".
if (empty($parent_map[$hash])) {
$parent_map[$hash] = array('...');
}
}
// The rendering code expects the first commit to be "mainline", like
// Git. Flip the order so it does the right thing.
$parent_map[$hash] = array_reverse($parent_map[$hash]);
$hash_list[] = $hash;
$last = $hash;
}
$hash_list = array_reverse($hash_list);
$this->parents = $parent_map;
return $this->loadHistoryForCommitIdentifiers($hash_list);
}
}
diff --git a/src/applications/diffusion/query/history/DiffusionSvnHistoryQuery.php b/src/applications/diffusion/query/history/DiffusionSvnHistoryQuery.php
index ee2c7573c3..2588d52821 100644
--- a/src/applications/diffusion/query/history/DiffusionSvnHistoryQuery.php
+++ b/src/applications/diffusion/query/history/DiffusionSvnHistoryQuery.php
@@ -1,107 +1,91 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionSvnHistoryQuery extends DiffusionHistoryQuery {
protected function executeQuery() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
$path = $drequest->getPath();
$commit = $drequest->getCommit();
$conn_r = $repository->establishConnection('r');
$paths = queryfx_all(
$conn_r,
'SELECT id, path FROM %T WHERE pathHash IN (%Ls)',
PhabricatorRepository::TABLE_PATH,
array(md5('/'.trim($path, '/'))));
$paths = ipull($paths, 'id', 'path');
$path_id = idx($paths, '/'.trim($path, '/'));
if (!$path_id) {
return array();
}
$filter_query = '';
if ($this->needDirectChanges) {
if ($this->needChildChanges) {
$type = DifferentialChangeType::TYPE_CHILD;
$filter_query = 'AND (isDirect = 1 OR changeType = '.$type.')';
} else {
$filter_query = 'AND (isDirect = 1)';
}
}
$history_data = queryfx_all(
$conn_r,
'SELECT * FROM %T WHERE repositoryID = %d AND pathID = %d
AND commitSequence <= %d
%Q
ORDER BY commitSequence DESC
LIMIT %d, %d',
PhabricatorRepository::TABLE_PATHCHANGE,
$repository->getID(),
$path_id,
$commit ? $commit : 0x7FFFFFFF,
$filter_query,
$this->getOffset(),
$this->getLimit());
$commits = array();
$commit_data = array();
$commit_ids = ipull($history_data, 'commitID');
if ($commit_ids) {
$commits = id(new PhabricatorRepositoryCommit())->loadAllWhere(
'id IN (%Ld)',
$commit_ids);
if ($commits) {
$commit_data = id(new PhabricatorRepositoryCommitData())->loadAllWhere(
'commitID in (%Ld)',
$commit_ids);
$commit_data = mpull($commit_data, null, 'getCommitID');
}
}
$history = array();
foreach ($history_data as $row) {
$item = new DiffusionPathChange();
$commit = idx($commits, $row['commitID']);
if ($commit) {
$item->setCommit($commit);
$item->setCommitIdentifier($commit->getCommitIdentifier());
$data = idx($commit_data, $commit->getID());
if ($data) {
$item->setCommitData($data);
}
}
$item->setChangeType($row['changeType']);
$item->setFileType($row['fileType']);
$history[] = $item;
}
return $history;
}
}
diff --git a/src/applications/diffusion/query/lastmodified/DiffusionGitLastModifiedQuery.php b/src/applications/diffusion/query/lastmodified/DiffusionGitLastModifiedQuery.php
index ed40cb7364..70a87a8cfb 100644
--- a/src/applications/diffusion/query/lastmodified/DiffusionGitLastModifiedQuery.php
+++ b/src/applications/diffusion/query/lastmodified/DiffusionGitLastModifiedQuery.php
@@ -1,45 +1,29 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionGitLastModifiedQuery extends DiffusionLastModifiedQuery {
protected function executeQuery() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
list($hash) = $repository->execxLocalCommand(
'log -n1 --format=%%H %s -- %s',
$drequest->getCommit(),
$drequest->getPath());
$hash = trim($hash);
$commit = id(new PhabricatorRepositoryCommit())->loadOneWhere(
'repositoryID = %d AND commitIdentifier = %s',
$repository->getID(),
$hash);
if ($commit) {
$commit_data = $commit->loadCommitData();
} else {
$commit_data = null;
}
return array($commit, $commit_data);
}
}
diff --git a/src/applications/diffusion/query/lastmodified/DiffusionLastModifiedQuery.php b/src/applications/diffusion/query/lastmodified/DiffusionLastModifiedQuery.php
index 9359266952..dc649cdc58 100644
--- a/src/applications/diffusion/query/lastmodified/DiffusionLastModifiedQuery.php
+++ b/src/applications/diffusion/query/lastmodified/DiffusionLastModifiedQuery.php
@@ -1,30 +1,14 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DiffusionLastModifiedQuery extends DiffusionQuery {
final public static function newFromDiffusionRequest(
DiffusionRequest $request) {
return parent::newQueryObject(__CLASS__, $request);
}
final public function loadLastModification() {
return $this->executeQuery();
}
}
diff --git a/src/applications/diffusion/query/lastmodified/DiffusionMercurialLastModifiedQuery.php b/src/applications/diffusion/query/lastmodified/DiffusionMercurialLastModifiedQuery.php
index ee8ae64e8b..728814b0ab 100644
--- a/src/applications/diffusion/query/lastmodified/DiffusionMercurialLastModifiedQuery.php
+++ b/src/applications/diffusion/query/lastmodified/DiffusionMercurialLastModifiedQuery.php
@@ -1,50 +1,34 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionMercurialLastModifiedQuery
extends DiffusionLastModifiedQuery {
protected function executeQuery() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
$path = $drequest->getPath();
// TODO: Share some of this with History query.
list($hash) = $repository->execxLocalCommand(
'log --template %s --limit 1 -b %s --rev %s:0 -- %s',
'{node}',
$drequest->getBranch(),
$drequest->getCommit(),
nonempty(ltrim($path, '/'), '.'));
$commit = id(new PhabricatorRepositoryCommit())->loadOneWhere(
'repositoryID = %d AND commitIdentifier = %s',
$repository->getID(),
$hash);
if ($commit) {
$commit_data = $commit->loadCommitData();
} else {
$commit_data = null;
}
return array($commit, $commit_data);
}
}
diff --git a/src/applications/diffusion/query/lastmodified/DiffusionSvnLastModifiedQuery.php b/src/applications/diffusion/query/lastmodified/DiffusionSvnLastModifiedQuery.php
index c6c1e4b6f4..5140eb50f2 100644
--- a/src/applications/diffusion/query/lastmodified/DiffusionSvnLastModifiedQuery.php
+++ b/src/applications/diffusion/query/lastmodified/DiffusionSvnLastModifiedQuery.php
@@ -1,43 +1,27 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionSvnLastModifiedQuery extends DiffusionLastModifiedQuery {
protected function executeQuery() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
$path = $drequest->getPath();
$history_query = DiffusionHistoryQuery::newFromDiffusionRequest(
$drequest);
$history_query->setLimit(1);
$history_query->needChildChanges(true);
$history_query->needDirectChanges(true);
$history_array = $history_query->loadHistory();
if (!$history_array) {
return array(null, null);
}
$history = reset($history_array);
return array($history->getCommit(), $history->getCommitData());
}
}
diff --git a/src/applications/diffusion/query/mergedcommits/DiffusionGitMergedCommitsQuery.php b/src/applications/diffusion/query/mergedcommits/DiffusionGitMergedCommitsQuery.php
index 6c1d5e0b78..89685fad9e 100644
--- a/src/applications/diffusion/query/mergedcommits/DiffusionGitMergedCommitsQuery.php
+++ b/src/applications/diffusion/query/mergedcommits/DiffusionGitMergedCommitsQuery.php
@@ -1,55 +1,39 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionGitMergedCommitsQuery extends DiffusionMergedCommitsQuery {
protected function executeQuery() {
$request = $this->getRequest();
$repository = $request->getRepository();
list($parents) = $repository->execxLocalCommand(
'log -n 1 --format=%s %s',
'%P',
$request->getCommit());
$parents = preg_split('/\s+/', trim($parents));
if (count($parents) < 2) {
// This is not a merge commit, so it doesn't merge anything.
return array();
}
// Get all of the commits which are not reachable from the first parent.
// These are the commits this change merges.
$first_parent = head($parents);
list($logs) = $repository->execxLocalCommand(
'log -n %d --format=%s %s %s --',
// NOTE: "+ 1" accounts for the merge commit itself.
$this->getLimit() + 1,
'%H',
$request->getCommit(),
'^'.$first_parent);
$hashes = explode("\n", trim($logs));
// Remove the merge commit.
$hashes = array_diff($hashes, array($request->getCommit()));
return $this->loadHistoryForCommitIdentifiers($hashes);
}
}
diff --git a/src/applications/diffusion/query/mergedcommits/DiffusionMercurialMergedCommitsQuery.php b/src/applications/diffusion/query/mergedcommits/DiffusionMercurialMergedCommitsQuery.php
index 1528332323..33482ef90c 100644
--- a/src/applications/diffusion/query/mergedcommits/DiffusionMercurialMergedCommitsQuery.php
+++ b/src/applications/diffusion/query/mergedcommits/DiffusionMercurialMergedCommitsQuery.php
@@ -1,57 +1,41 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionMercurialMergedCommitsQuery
extends DiffusionMergedCommitsQuery {
protected function executeQuery() {
$request = $this->getRequest();
$repository = $request->getRepository();
list($parents) = $repository->execxLocalCommand(
'parents --template=%s --rev %s',
'{node}\\n',
$request->getCommit());
$parents = explode("\n", trim($parents));
if (count($parents) < 2) {
// Not a merge commit.
return array();
}
// NOTE: In Git, the first parent is the "mainline". In Mercurial, the
// second parent is the "mainline" (the way 'git merge' and 'hg merge'
// work is also reversed).
$last_parent = last($parents);
list($logs) = $repository->execxLocalCommand(
'log --template=%s --follow --limit %d --rev %s:0 --prune %s --',
'{node}\\n',
$this->getLimit() + 1,
$request->getCommit(),
$last_parent);
$hashes = explode("\n", trim($logs));
// Remove the merge commit.
$hashes = array_diff($hashes, array($request->getCommit()));
return $this->loadHistoryForCommitIdentifiers($hashes);
}
}
diff --git a/src/applications/diffusion/query/mergedcommits/DiffusionMergedCommitsQuery.php b/src/applications/diffusion/query/mergedcommits/DiffusionMergedCommitsQuery.php
index 5451fbd16b..a03f9d9070 100644
--- a/src/applications/diffusion/query/mergedcommits/DiffusionMergedCommitsQuery.php
+++ b/src/applications/diffusion/query/mergedcommits/DiffusionMergedCommitsQuery.php
@@ -1,42 +1,26 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DiffusionMergedCommitsQuery extends DiffusionQuery {
private $limit = PHP_INT_MAX;
final public static function newFromDiffusionRequest(
DiffusionRequest $request) {
return self::newQueryObject(__CLASS__, $request);
}
final public function loadMergedCommits() {
return $this->executeQuery();
}
final public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
final public function getLimit() {
return $this->limit;
}
}
diff --git a/src/applications/diffusion/query/mergedcommits/DiffusionSvnMergedCommitsQuery.php b/src/applications/diffusion/query/mergedcommits/DiffusionSvnMergedCommitsQuery.php
index 6b52f4dd3e..c9a6739160 100644
--- a/src/applications/diffusion/query/mergedcommits/DiffusionSvnMergedCommitsQuery.php
+++ b/src/applications/diffusion/query/mergedcommits/DiffusionSvnMergedCommitsQuery.php
@@ -1,27 +1,11 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionSvnMergedCommitsQuery extends DiffusionMergedCommitsQuery {
protected function executeQuery() {
// TODO: It might be possible to do something reasonable in recent versions
// of SVN.
return array();
}
}
diff --git a/src/applications/diffusion/query/parents/DiffusionCommitParentsQuery.php b/src/applications/diffusion/query/parents/DiffusionCommitParentsQuery.php
index fe2c11e0ec..a8a9e53af7 100644
--- a/src/applications/diffusion/query/parents/DiffusionCommitParentsQuery.php
+++ b/src/applications/diffusion/query/parents/DiffusionCommitParentsQuery.php
@@ -1,30 +1,14 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DiffusionCommitParentsQuery extends DiffusionQuery {
final public static function newFromDiffusionRequest(
DiffusionRequest $request) {
return self::newQueryObject(__CLASS__, $request);
}
final public function loadParents() {
return $this->executeQuery();
}
}
diff --git a/src/applications/diffusion/query/parents/DiffusionGitCommitParentsQuery.php b/src/applications/diffusion/query/parents/DiffusionGitCommitParentsQuery.php
index 058ace79e3..8dd68f6464 100644
--- a/src/applications/diffusion/query/parents/DiffusionGitCommitParentsQuery.php
+++ b/src/applications/diffusion/query/parents/DiffusionGitCommitParentsQuery.php
@@ -1,35 +1,19 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionGitCommitParentsQuery
extends DiffusionCommitParentsQuery {
protected function executeQuery() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
list($stdout) = $repository->execxLocalCommand(
'log -n 1 --format=%s %s',
'%P',
$drequest->getStableCommitName());
$hashes = preg_split('/\s+/', trim($stdout));
return self::loadCommitsByIdentifiers($hashes);
}
}
diff --git a/src/applications/diffusion/query/parents/DiffusionMercurialCommitParentsQuery.php b/src/applications/diffusion/query/parents/DiffusionMercurialCommitParentsQuery.php
index 9520291d60..7836e28bf9 100644
--- a/src/applications/diffusion/query/parents/DiffusionMercurialCommitParentsQuery.php
+++ b/src/applications/diffusion/query/parents/DiffusionMercurialCommitParentsQuery.php
@@ -1,46 +1,30 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionMercurialCommitParentsQuery
extends DiffusionCommitParentsQuery {
protected function executeQuery() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
list($stdout) = $repository->execxLocalCommand(
'log --debug --limit 1 --template={parents} --rev %s',
$drequest->getStableCommitName());
$hashes = preg_split('/\s+/', trim($stdout));
foreach ($hashes as $key => $value) {
// Mercurial parents look like "23:ad9f769d6f786fad9f76d9a" -- we want
// to strip out the local rev part.
list($local, $global) = explode(':', $value);
$hashes[$key] = $global;
// With --debug we get 40-character hashes but also get the "000000..."
// hash for missing parents; ignore it.
if (preg_match('/^0+$/', $global)) {
unset($hashes[$key]);
}
}
return self::loadCommitsByIdentifiers($hashes);
}
}
diff --git a/src/applications/diffusion/query/parents/DiffusionSvnCommitParentsQuery.php b/src/applications/diffusion/query/parents/DiffusionSvnCommitParentsQuery.php
index 864adea9b9..970119f321 100644
--- a/src/applications/diffusion/query/parents/DiffusionSvnCommitParentsQuery.php
+++ b/src/applications/diffusion/query/parents/DiffusionSvnCommitParentsQuery.php
@@ -1,38 +1,22 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionSvnCommitParentsQuery
extends DiffusionCommitParentsQuery {
protected function executeQuery() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
// TODO: With merge properties in recent versions of SVN, can we do
// a better job of this?
$n = $drequest->getStableCommitName();
if ($n > 1) {
$ids = array($n - 1);
} else {
$ids = array();
}
return self::loadCommitsByIdentifiers($ids);
}
}
diff --git a/src/applications/diffusion/query/pathchange/DiffusionPathChangeQuery.php b/src/applications/diffusion/query/pathchange/DiffusionPathChangeQuery.php
index 925057ae2f..c7b53980ed 100644
--- a/src/applications/diffusion/query/pathchange/DiffusionPathChangeQuery.php
+++ b/src/applications/diffusion/query/pathchange/DiffusionPathChangeQuery.php
@@ -1,104 +1,88 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionPathChangeQuery {
private $request;
final private function __construct() {
// <private>
}
final public static function newFromDiffusionRequest(
DiffusionRequest $request) {
$query = new DiffusionPathChangeQuery();
$query->request = $request;
return $query;
}
final protected function getRequest() {
return $this->request;
}
final public function loadChanges() {
return $this->executeQuery();
}
protected function executeQuery() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
$commit = $drequest->loadCommit();
$raw_changes = queryfx_all(
$repository->establishConnection('r'),
'SELECT c.*, p.path pathName, t.path targetPathName,
i.commitIdentifier targetCommitIdentifier
FROM %T c
LEFT JOIN %T p ON c.pathID = p.id
LEFT JOIN %T t ON c.targetPathID = t.id
LEFT JOIN %T i ON c.targetCommitID = i.id
WHERE c.commitID = %d AND isDirect = 1',
PhabricatorRepository::TABLE_PATHCHANGE,
PhabricatorRepository::TABLE_PATH,
PhabricatorRepository::TABLE_PATH,
$commit->getTableName(),
$commit->getID());
$changes = array();
$raw_changes = isort($raw_changes, 'pathName');
foreach ($raw_changes as $raw_change) {
$type = $raw_change['changeType'];
if ($type == DifferentialChangeType::TYPE_CHILD) {
continue;
}
$change = new DiffusionPathChange();
$change->setPath(ltrim($raw_change['pathName'], '/'));
$change->setChangeType($raw_change['changeType']);
$change->setFileType($raw_change['fileType']);
$change->setCommitIdentifier($commit->getCommitIdentifier());
$change->setTargetPath(ltrim($raw_change['targetPathName'], '/'));
$change->setTargetCommitIdentifier($raw_change['targetCommitIdentifier']);
$id = $raw_change['pathID'];
$changes[$id] = $change;
}
// Deduce the away paths by examining all the changes.
$away = array();
foreach ($changes as $change) {
if ($change->getTargetPath()) {
$away[$change->getTargetPath()][] = $change->getPath();
}
}
foreach ($changes as $change) {
if (isset($away[$change->getPath()])) {
$change->setAwayPaths($away[$change->getPath()]);
}
}
return $changes;
}
}
diff --git a/src/applications/diffusion/query/pathid/DiffusionPathIDQuery.php b/src/applications/diffusion/query/pathid/DiffusionPathIDQuery.php
index 285ff86061..bcb3c6f3f0 100644
--- a/src/applications/diffusion/query/pathid/DiffusionPathIDQuery.php
+++ b/src/applications/diffusion/query/pathid/DiffusionPathIDQuery.php
@@ -1,106 +1,90 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @task pathutil Path Utilities
*/
final class DiffusionPathIDQuery {
public function __construct(array $paths) {
$this->paths = $paths;
}
public function loadPathIDs() {
$repository = new PhabricatorRepository();
$path_normal_map = array();
foreach ($this->paths as $path) {
$normal = self::normalizePath($path);
$path_normal_map[$normal][] = $path;
}
$paths = queryfx_all(
$repository->establishConnection('r'),
'SELECT * FROM %T WHERE pathHash IN (%Ls)',
PhabricatorRepository::TABLE_PATH,
array_map('md5', array_keys($path_normal_map)));
$paths = ipull($paths, 'id', 'path');
$result = array();
foreach ($path_normal_map as $normal => $originals) {
foreach ($originals as $original) {
$result[$original] = idx($paths, $normal);
}
}
return $result;
}
/**
* Convert a path to the canonical, absolute representation used by Diffusion.
*
* @param string Some repository path.
* @return string Canonicalized Diffusion path.
* @task pathutil
*/
public static function normalizePath($path) {
// Normalize to single slashes, e.g. "///" => "/".
$path = preg_replace('@[/]{2,}@', '/', $path);
return '/'.trim($path, '/');
}
/**
* Return the canonical parent directory for a path. Note, returns "/" when
* passed "/".
*
* @param string Some repository path.
* @return string That path's canonical parent directory.
* @task pathutil
*/
public static function getParentPath($path) {
$path = self::normalizePath($path);
return dirname($path);
}
/**
* Generate a list of parents for a repository path. The path itself is
* included.
*
* @param string Some repository path.
* @return list List of canonical paths between the path and the root.
* @task pathutil
*/
public static function expandPathToRoot($path) {
$path = self::normalizePath($path);
$parents = array($path);
$parts = explode('/', trim($path, '/'));
while (count($parts) >= 1) {
if (array_pop($parts)) {
$parents[] = '/'.implode('/', $parts);
}
}
return $parents;
}
}
diff --git a/src/applications/diffusion/query/pathid/__tests__/DiffusionPathQueryTestCase.php b/src/applications/diffusion/query/pathid/__tests__/DiffusionPathQueryTestCase.php
index 760f4e0409..00602372a0 100644
--- a/src/applications/diffusion/query/pathid/__tests__/DiffusionPathQueryTestCase.php
+++ b/src/applications/diffusion/query/pathid/__tests__/DiffusionPathQueryTestCase.php
@@ -1,59 +1,43 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionPathQueryTestCase extends PhabricatorTestCase {
public function testParentEdgeCases() {
$this->assertEqual(
'/',
DiffusionPathIDQuery::getParentPath('/'),
'Parent of /');
$this->assertEqual(
'/',
DiffusionPathIDQuery::getParentPath('x.txt'),
'Parent of x.txt');
$this->assertEqual(
'/a',
DiffusionPathIDQuery::getParentPath('/a/b'),
'Parent of /a/b');
$this->assertEqual(
'/a',
DiffusionPathIDQuery::getParentPath('/a///b'),
'Parent of /a///b');
}
public function testExpandEdgeCases() {
$this->assertEqual(
array('/'),
DiffusionPathIDQuery::expandPathToRoot('/'));
$this->assertEqual(
array('/'),
DiffusionPathIDQuery::expandPathToRoot('//'));
$this->assertEqual(
array('/a/b', '/a', '/'),
DiffusionPathIDQuery::expandPathToRoot('/a/b'));
$this->assertEqual(
array('/a/b', '/a', '/'),
DiffusionPathIDQuery::expandPathToRoot('/a//b'));
$this->assertEqual(
array('/a/b', '/a', '/'),
DiffusionPathIDQuery::expandPathToRoot('a/b'));
}
}
diff --git a/src/applications/diffusion/query/rawdiff/DiffusionGitRawDiffQuery.php b/src/applications/diffusion/query/rawdiff/DiffusionGitRawDiffQuery.php
index c408044bb6..e589721e73 100644
--- a/src/applications/diffusion/query/rawdiff/DiffusionGitRawDiffQuery.php
+++ b/src/applications/diffusion/query/rawdiff/DiffusionGitRawDiffQuery.php
@@ -1,90 +1,74 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionGitRawDiffQuery extends DiffusionRawDiffQuery {
protected function executeQuery() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
$commit = $drequest->getCommit();
$options = array(
'-M',
'-C',
'--no-ext-diff',
'--no-color',
'--src-prefix=a/',
'--dst-prefix=b/',
'-U'.(int)$this->getLinesOfContext(),
);
$options = implode(' ', $options);
$against = $this->getAgainstCommit();
if ($against === null) {
$against = $commit.'^';
}
// If there's no path, get the entire raw diff.
$path = nonempty($drequest->getPath(), '.');
$future = $repository->getLocalCommandFuture(
"diff %C %s %s -- %s",
$options,
$against,
$commit,
$path);
if ($this->getTimeout()) {
$future->setTimeout($this->getTimeout());
}
try {
list($raw_diff) = $future->resolvex();
} catch (CommandException $ex) {
// Check if this is the root commit by seeing if it has parents.
list($parents) = $repository->execxLocalCommand(
'log --format=%s %s --',
'%P', // "parents"
$commit);
if (strlen(trim($parents))) {
throw $ex;
}
// No parents means we're looking at the root revision. Diff against
// the empty tree hash instead, since there is no parent so "^" does
// not work. See ArcanistGitAPI for more discussion.
$future = $repository->getLocalCommandFuture(
'diff %C %s %s -- %s',
$options,
ArcanistGitAPI::GIT_MAGIC_ROOT_COMMIT,
$commit,
$drequest->getPath());
if ($this->getTimeout()) {
$future->setTimeout($this->getTimeout());
}
list($raw_diff) = $future->resolvex();
}
return $raw_diff;
}
}
diff --git a/src/applications/diffusion/query/rawdiff/DiffusionMercurialRawDiffQuery.php b/src/applications/diffusion/query/rawdiff/DiffusionMercurialRawDiffQuery.php
index 23b352eea2..4a016703ac 100644
--- a/src/applications/diffusion/query/rawdiff/DiffusionMercurialRawDiffQuery.php
+++ b/src/applications/diffusion/query/rawdiff/DiffusionMercurialRawDiffQuery.php
@@ -1,72 +1,56 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionMercurialRawDiffQuery extends DiffusionRawDiffQuery {
protected function executeQuery() {
$raw_diff = $this->executeRawDiffCommand();
// the only legitimate case here is if we are looking at the first commit
// in the repository. no parents means first commit.
if (!$raw_diff) {
$drequest = $this->getRequest();
$parent_query =
DiffusionCommitParentsQuery::newFromDiffusionRequest($drequest);
$parents = $parent_query->loadParents();
if ($parents === array()) {
// mercurial likes the string null here
$this->setAgainstCommit('null');
$raw_diff = $this->executeRawDiffCommand();
}
}
return $raw_diff;
}
protected function executeRawDiffCommand() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
$commit = $drequest->getCommit();
// If there's no path, get the entire raw diff.
$path = nonempty($drequest->getPath(), '.');
$against = $this->getAgainstCommit();
if ($against === null) {
$against = $commit.'^';
}
$future = $repository->getLocalCommandFuture(
'diff -U %d --git --rev %s:%s -- %s',
$this->getLinesOfContext(),
$against,
$commit,
$path);
if ($this->getTimeout()) {
$future->setTimeout($this->getTimeout());
}
list($raw_diff) = $future->resolvex();
return $raw_diff;
}
}
diff --git a/src/applications/diffusion/query/rawdiff/DiffusionRawDiffQuery.php b/src/applications/diffusion/query/rawdiff/DiffusionRawDiffQuery.php
index eca89d6724..57c2ab9d07 100644
--- a/src/applications/diffusion/query/rawdiff/DiffusionRawDiffQuery.php
+++ b/src/applications/diffusion/query/rawdiff/DiffusionRawDiffQuery.php
@@ -1,62 +1,46 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DiffusionRawDiffQuery extends DiffusionQuery {
private $request;
private $timeout;
private $linesOfContext = 65535;
private $againstCommit;
final public static function newFromDiffusionRequest(
DiffusionRequest $request) {
return parent::newQueryObject(__CLASS__, $request);
}
final public function loadRawDiff() {
return $this->executeQuery();
}
final public function setTimeout($timeout) {
$this->timeout = $timeout;
return $this;
}
final public function getTimeout() {
return $this->timeout;
}
final public function setLinesOfContext($lines_of_context) {
$this->linesOfContext = $lines_of_context;
return $this;
}
final public function getLinesOfContext() {
return $this->linesOfContext;
}
final public function setAgainstCommit($value) {
$this->againstCommit = $value;
return $this;
}
final public function getAgainstCommit() {
return $this->againstCommit;
}
}
diff --git a/src/applications/diffusion/query/rawdiff/DiffusionSvnRawDiffQuery.php b/src/applications/diffusion/query/rawdiff/DiffusionSvnRawDiffQuery.php
index 8633ae3132..14bd664718 100644
--- a/src/applications/diffusion/query/rawdiff/DiffusionSvnRawDiffQuery.php
+++ b/src/applications/diffusion/query/rawdiff/DiffusionSvnRawDiffQuery.php
@@ -1,50 +1,34 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionSvnRawDiffQuery extends DiffusionRawDiffQuery {
protected function executeQuery() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
$commit = $drequest->getCommit();
$arc_root = phutil_get_library_root('arcanist');
$against = $this->getAgainstCommit();
if ($against === null) {
$against = $commit - 1;
}
$future = $repository->getRemoteCommandFuture(
'diff --diff-cmd %s -x -U%d -r %d:%d %s%s@',
$arc_root.'/../scripts/repository/binary_safe_diff.sh',
$this->getLinesOfContext(),
$against,
$commit,
$repository->getRemoteURI(),
$drequest->getPath());
if ($this->getTimeout()) {
$future->setTimeout($this->getTimeout());
}
list($raw_diff) = $future->resolvex();
return $raw_diff;
}
}
diff --git a/src/applications/diffusion/query/taglist/DiffusionGitTagListQuery.php b/src/applications/diffusion/query/taglist/DiffusionGitTagListQuery.php
index 50f1f8e65b..66b689b71f 100644
--- a/src/applications/diffusion/query/taglist/DiffusionGitTagListQuery.php
+++ b/src/applications/diffusion/query/taglist/DiffusionGitTagListQuery.php
@@ -1,79 +1,63 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionGitTagListQuery extends DiffusionTagListQuery {
protected function executeQuery() {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
$count = $this->getOffset() + $this->getLimit();
list($stdout) = $repository->execxLocalCommand(
'for-each-ref %C --sort=-creatordate --format=%s refs/tags',
$count ? '--count='.(int)$count : null,
'%(objectname) %(objecttype) %(refname) %(*objectname) %(*objecttype) '.
'%(subject)%01%(creator)'
);
$stdout = trim($stdout);
if (!strlen($stdout)) {
return array();
}
$tags = array();
foreach (explode("\n", $stdout) as $line) {
list($info, $creator) = explode("\1", $line);
list(
$objectname,
$objecttype,
$refname,
$refobjectname,
$refobjecttype,
$description) = explode(' ', $info, 6);
$matches = null;
if (!preg_match('/^(.*) ([0-9]+) ([0-9+-]+)$/', $creator, $matches)) {
// It's possible a tag doesn't have a creator (tagger)
$author = null;
$epoch = null;
} else {
$author = $matches[1];
$epoch = $matches[2];
}
$tag = new DiffusionRepositoryTag();
$tag->setAuthor($author);
$tag->setEpoch($epoch);
$tag->setCommitIdentifier(nonempty($refobjectname, $objectname));
$tag->setName(preg_replace('@^refs/tags/@', '', $refname));
$tag->setDescription($description);
$tag->setType('git/'.$objecttype);
$tags[] = $tag;
}
$offset = $this->getOffset();
if ($offset) {
$tags = array_slice($tags, $offset);
}
return $tags;
}
}
diff --git a/src/applications/diffusion/query/taglist/DiffusionMercurialTagListQuery.php b/src/applications/diffusion/query/taglist/DiffusionMercurialTagListQuery.php
index a69bc80bd3..0e37128a9b 100644
--- a/src/applications/diffusion/query/taglist/DiffusionMercurialTagListQuery.php
+++ b/src/applications/diffusion/query/taglist/DiffusionMercurialTagListQuery.php
@@ -1,26 +1,10 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionMercurialTagListQuery extends DiffusionTagListQuery {
protected function executeQuery() {
// TODO: Implement this for Mercurial.
return array();
}
}
diff --git a/src/applications/diffusion/query/taglist/DiffusionSvnTagListQuery.php b/src/applications/diffusion/query/taglist/DiffusionSvnTagListQuery.php
index b74b694b13..562ae02597 100644
--- a/src/applications/diffusion/query/taglist/DiffusionSvnTagListQuery.php
+++ b/src/applications/diffusion/query/taglist/DiffusionSvnTagListQuery.php
@@ -1,26 +1,10 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionSvnTagListQuery extends DiffusionTagListQuery {
protected function executeQuery() {
// Nothing meaningful to be done in Subversion.
return array();
}
}
diff --git a/src/applications/diffusion/query/taglist/DiffusionTagListQuery.php b/src/applications/diffusion/query/taglist/DiffusionTagListQuery.php
index f1f6960108..047b62141c 100644
--- a/src/applications/diffusion/query/taglist/DiffusionTagListQuery.php
+++ b/src/applications/diffusion/query/taglist/DiffusionTagListQuery.php
@@ -1,51 +1,35 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DiffusionTagListQuery extends DiffusionQuery {
private $limit;
private $offset;
public function setOffset($offset) {
$this->offset = $offset;
return $this;
}
public function getOffset() {
return $this->offset;
}
public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
protected function getLimit() {
return $this->limit;
}
final public static function newFromDiffusionRequest(
DiffusionRequest $request) {
return self::newQueryObject(__CLASS__, $request);
}
final public function loadTags() {
return $this->executeQuery();
}
}
diff --git a/src/applications/diffusion/request/DiffusionGitRequest.php b/src/applications/diffusion/request/DiffusionGitRequest.php
index d27a972c66..4dfc22be5d 100644
--- a/src/applications/diffusion/request/DiffusionGitRequest.php
+++ b/src/applications/diffusion/request/DiffusionGitRequest.php
@@ -1,115 +1,99 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group diffusion
*/
final class DiffusionGitRequest extends DiffusionRequest {
protected function getSupportsBranches() {
return true;
}
protected function didInitialize() {
$repository = $this->getRepository();
if (!Filesystem::pathExists($repository->getLocalPath())) {
$this->raiseCloneException();
}
if (!$this->commit) {
return;
}
// Expand short commit names and verify
$future = $repository->getLocalCommandFuture(
'cat-file --batch');
$future->write($this->commit);
list($stdout) = $future->resolvex();
list($hash, $type) = explode(' ', $stdout);
if ($type == 'missing') {
throw new Exception("Bad commit '{$this->commit}'.");
}
switch ($type) {
case 'tag':
$this->commitType = 'tag';
$matches = null;
$ok = preg_match(
'/^object ([a-f0-9]+)$.*?\n\n(.*)$/sm',
$stdout,
$matches);
if (!$ok) {
throw new Exception(
"Unparseable output from cat-file: {$stdout}");
}
$hash = $matches[1];
$this->tagContent = trim($matches[2]);
break;
case 'commit':
break;
default:
throw new AphrontUsageException(
"Invalid Object Name",
"The reference '{$this->commit}' does not name a valid ".
"commit or a tag in this repository.");
break;
}
$this->commit = $hash;
}
public function getBranch() {
if ($this->branch) {
return $this->branch;
}
if ($this->repository) {
return $this->repository->getDefaultBranch();
}
throw new Exception("Unable to determine branch!");
}
public function getCommit() {
if ($this->commit) {
return $this->commit;
}
$remote = DiffusionBranchInformation::DEFAULT_GIT_REMOTE;
return $remote.'/'.$this->getBranch();
}
public function getStableCommitName() {
if (!$this->stableCommitName) {
if ($this->commit) {
$this->stableCommitName = $this->commit;
} else {
$branch = $this->getBranch();
list($stdout) = $this->getRepository()->execxLocalCommand(
'rev-parse --verify %s/%s',
DiffusionBranchInformation::DEFAULT_GIT_REMOTE,
$branch);
$this->stableCommitName = trim($stdout);
}
}
return substr($this->stableCommitName, 0, 16);
}
}
diff --git a/src/applications/diffusion/request/DiffusionMercurialRequest.php b/src/applications/diffusion/request/DiffusionMercurialRequest.php
index b2ef171813..18c7d66b10 100644
--- a/src/applications/diffusion/request/DiffusionMercurialRequest.php
+++ b/src/applications/diffusion/request/DiffusionMercurialRequest.php
@@ -1,85 +1,69 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group diffusion
*/
final class DiffusionMercurialRequest extends DiffusionRequest {
protected function getSupportsBranches() {
return true;
}
protected function didInitialize() {
$repository = $this->getRepository();
if (!Filesystem::pathExists($repository->getLocalPath())) {
$this->raiseCloneException();
}
return;
}
public function getBranch() {
if ($this->branch) {
return $this->branch;
}
if ($this->repository) {
return $this->repository->getDefaultBranch();
}
throw new Exception("Unable to determine branch!");
}
public function getCommit() {
if ($this->commit) {
return $this->commit;
}
return $this->getBranch();
}
public function getStableCommitName() {
if (!$this->stableCommitName) {
if ($this->commit) {
$this->stableCommitName = $this->commit;
} else {
// NOTE: For branches with spaces in their name like "a b", this
// does not work properly:
//
// $ hg log --rev 'a b'
//
// We can use revsets instead:
//
// $ hg log --rev branch('a b')
//
// ...but they require a somewhat newer version of Mercurial. Instead,
// use "-b" flag with limit 1 for greatest compatibility across
// versions.
list($this->stableCommitName) = $this->repository->execxLocalCommand(
'log --template=%s -b %s --limit 1',
'{node}',
$this->getBranch());
}
}
return $this->stableCommitName;
}
}
diff --git a/src/applications/diffusion/request/DiffusionRequest.php b/src/applications/diffusion/request/DiffusionRequest.php
index a9c92cb457..8ec58bc079 100644
--- a/src/applications/diffusion/request/DiffusionRequest.php
+++ b/src/applications/diffusion/request/DiffusionRequest.php
@@ -1,539 +1,523 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Contains logic to parse Diffusion requests, which have a complicated URI
* structure.
*
*
* @task new Creating Requests
* @task uri Managing Diffusion URIs
*
* @group diffusion
*/
abstract class DiffusionRequest {
protected $callsign;
protected $path;
protected $line;
protected $symbolicCommit;
protected $commit;
protected $branch;
protected $commitType = 'commit';
protected $tagContent;
protected $repository;
protected $repositoryCommit;
protected $repositoryCommitData;
protected $stableCommitName;
protected $arcanistProjects;
abstract protected function getSupportsBranches();
abstract protected function didInitialize();
/* -( Creating Requests )-------------------------------------------------- */
/**
* Create a new synthetic request from a parameter dictionary. If you need
* a @{class:DiffusionRequest} object in order to issue a DiffusionQuery, you
* can use this method to build one.
*
* Parameters are:
*
* - `callsign` Repository callsign. Provide this or `repository`.
* - `repository` Repository object. Provide this or `callsign`.
* - `branch` Optional, branch name.
* - `path` Optional, file path.
* - `commit` Optional, commit identifier.
* - `line` Optional, line range.
*
* @param map See documentation.
* @return DiffusionRequest New request object.
* @task new
*/
final public static function newFromDictionary(array $data) {
if (isset($data['repository']) && isset($data['callsign'])) {
throw new Exception(
"Specify 'repository' or 'callsign', but not both.");
} else if (!isset($data['repository']) && !isset($data['callsign'])) {
throw new Exception(
"One of 'repository' and 'callsign' is required.");
}
if (isset($data['repository'])) {
$object = self::newFromRepository($data['repository']);
} else {
$object = self::newFromCallsign($data['callsign']);
}
$object->initializeFromDictionary($data);
return $object;
}
/**
* Create a new request from an Aphront request dictionary. This is an
* internal method that you generally should not call directly; instead,
* call @{method:newFromDictionary}.
*
* @param map Map of Aphront request data.
* @return DiffusionRequest New request object.
* @task new
*/
final public static function newFromAphrontRequestDictionary(array $data) {
$callsign = phutil_unescape_uri_path_component(idx($data, 'callsign'));
$object = self::newFromCallsign($callsign);
$use_branches = $object->getSupportsBranches();
$parsed = self::parseRequestBlob(idx($data, 'dblob'), $use_branches);
$object->initializeFromDictionary($parsed);
return $object;
}
/**
* Internal.
*
* @task new
*/
final private function __construct() {
// <private>
}
/**
* Internal. Use @{method:newFromDictionary}, not this method.
*
* @param string Repository callsign.
* @return DiffusionRequest New request object.
* @task new
*/
final private static function newFromCallsign($callsign) {
$repository = id(new PhabricatorRepository())->loadOneWhere(
'callsign = %s',
$callsign);
if (!$repository) {
throw new Exception("No such repository '{$callsign}'.");
}
return self::newFromRepository($repository);
}
/**
* Internal. Use @{method:newFromDictionary}, not this method.
*
* @param PhabricatorRepository Repository object.
* @return DiffusionRequest New request object.
* @task new
*/
final private static function newFromRepository(
PhabricatorRepository $repository) {
$map = array(
PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'DiffusionGitRequest',
PhabricatorRepositoryType::REPOSITORY_TYPE_SVN => 'DiffusionSvnRequest',
PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL =>
'DiffusionMercurialRequest',
);
$class = idx($map, $repository->getVersionControlSystem());
if (!$class) {
throw new Exception("Unknown version control system!");
}
$object = new $class();
$object->repository = $repository;
$object->callsign = $repository->getCallsign();
return $object;
}
/**
* Internal. Use @{method:newFromDictionary}, not this method.
*
* @param map Map of parsed data.
* @return void
* @task new
*/
final private function initializeFromDictionary(array $data) {
$this->path = idx($data, 'path');
$this->symbolicCommit = idx($data, 'commit');
$this->commit = idx($data, 'commit');
$this->line = idx($data, 'line');
if ($this->getSupportsBranches()) {
$this->branch = idx($data, 'branch');
}
$this->didInitialize();
}
public function getRepository() {
return $this->repository;
}
public function getCallsign() {
return $this->callsign;
}
public function getPath() {
return $this->path;
}
public function getLine() {
return $this->line;
}
public function getCommit() {
return $this->commit;
}
public function getSymbolicCommit() {
return $this->symbolicCommit;
}
public function getBranch() {
return $this->branch;
}
public function getTagContent() {
return $this->tagContent;
}
public function loadCommit() {
if (empty($this->repositoryCommit)) {
$repository = $this->getRepository();
$commit = id(new PhabricatorRepositoryCommit())->loadOneWhere(
'repositoryID = %d AND commitIdentifier = %s',
$repository->getID(),
$this->getCommit());
$this->repositoryCommit = $commit;
}
return $this->repositoryCommit;
}
public function loadArcanistProjects() {
if (empty($this->arcanistProjects)) {
$projects = id(new PhabricatorRepositoryArcanistProject())->loadAllWhere(
'repositoryID = %d',
$this->getRepository()->getID());
$this->arcanistProjects = $projects;
}
return $this->arcanistProjects;
}
public function loadCommitData() {
if (empty($this->repositoryCommitData)) {
$commit = $this->loadCommit();
$data = id(new PhabricatorRepositoryCommitData())->loadOneWhere(
'commitID = %d',
$commit->getID());
if (!$data) {
$data = new PhabricatorRepositoryCommitData();
$data->setCommitMessage(
'(This commit has not been fully parsed yet.)');
}
$this->repositoryCommitData = $data;
}
return $this->repositoryCommitData;
}
/**
* Retrieve a stable, permanent commit name. This returns a non-symbolic
* identifier for the current commit: e.g., a specific commit hash in git
* (NOT a symbolic name like "origin/master") or a specific revision number
* in SVN (NOT a symbolic name like "HEAD").
*
* @return string Stable commit name, like a git hash or SVN revision. Not
* a symbolic commit reference.
*/
public function getStableCommitName() {
return $this->stableCommitName;
}
final public function getRawCommit() {
return $this->commit;
}
public function setCommit($commit) {
$this->commit = $commit;
return $this;
}
/* -( Managing Diffusion URIs )-------------------------------------------- */
/**
* Generate a Diffusion URI using this request to provide defaults. See
* @{method:generateDiffusionURI} for details. This method is the same, but
* preserves the request parameters if they are not overridden.
*
* @param map See @{method:generateDiffusionURI}.
* @return PhutilURI Generated URI.
* @task uri
*/
public function generateURI(array $params) {
if (empty($params['stable'])) {
$default_commit = $this->getRawCommit();
} else {
$default_commit = $this->getStableCommitName();
}
$defaults = array(
'callsign' => $this->getCallsign(),
'path' => $this->getPath(),
'branch' => $this->getBranch(),
'commit' => $default_commit,
);
foreach ($defaults as $key => $val) {
if (!isset($params[$key])) { // Overwrite NULL.
$params[$key] = $val;
}
}
return self::generateDiffusionURI($params);
}
/**
* Generate a Diffusion URI from a parameter map. Applies the correct encoding
* and formatting to the URI. Parameters are:
*
* - `action` One of `history`, `browse`, `change`, `lastmodified`,
* `branch` or `revision-ref`. The action specified by the URI.
* - `callsign` Repository callsign.
* - `branch` Optional if action is not `branch`, branch name.
* - `path` Optional, path to file.
* - `commit` Optional, commit identifier.
* - `line` Optional, line range.
* - `params` Optional, query parameters.
*
* The function generates the specified URI and returns it.
*
* @param map See documentation.
* @return PhutilURI Generated URI.
* @task uri
*/
public static function generateDiffusionURI(array $params) {
$action = idx($params, 'action');
$callsign = idx($params, 'callsign');
$path = idx($params, 'path');
$branch = idx($params, 'branch');
$commit = idx($params, 'commit');
$line = idx($params, 'line');
if (strlen($callsign)) {
$callsign = phutil_escape_uri_path_component($callsign).'/';
}
if (strlen($branch)) {
$branch = phutil_escape_uri_path_component($branch).'/';
}
if (strlen($path)) {
$path = ltrim($path, '/');
$path = str_replace(array(';', '$'), array(';;', '$$'), $path);
$path = phutil_escape_uri($path);
}
$path = "{$branch}{$path}";
if (strlen($commit)) {
$commit = str_replace('$', '$$', $commit);
$commit = ';'.phutil_escape_uri($commit);
}
if (strlen($line)) {
$line = '$'.phutil_escape_uri($line);
}
$req_callsign = false;
$req_branch = false;
$req_commit = false;
switch ($action) {
case 'history':
case 'browse':
case 'change':
case 'lastmodified':
case 'tags':
case 'branches':
$req_callsign = true;
break;
case 'branch':
$req_callsign = true;
$req_branch = true;
break;
case 'commit':
$req_callsign = true;
$req_commit = true;
break;
}
if ($req_callsign && !strlen($callsign)) {
throw new Exception(
"Diffusion URI action '{$action}' requires callsign!");
}
if ($req_branch && !strlen($branch)) {
throw new Exception(
"Diffusion URI action '{$action}' requires branch!");
}
if ($req_commit && !strlen($commit)) {
throw new Exception(
"Diffusion URI action '{$action}' requires commit!");
}
switch ($action) {
case 'change':
case 'history':
case 'browse':
case 'lastmodified':
case 'tags':
case 'branches':
$uri = "/diffusion/{$callsign}{$action}/{$path}{$commit}{$line}";
break;
case 'branch':
$uri = "/diffusion/{$callsign}repository/{$path}";
break;
case 'external':
$commit = ltrim($commit, ';');
$uri = "/diffusion/external/{$commit}/";
break;
case 'rendering-ref':
// This isn't a real URI per se, it's passed as a query parameter to
// the ajax changeset stuff but then we parse it back out as though
// it came from a URI.
$uri = rawurldecode("{$path}{$commit}");
break;
case 'commit':
$commit = ltrim($commit, ';');
$callsign = rtrim($callsign, '/');
$uri = "/r{$callsign}{$commit}";
break;
default:
throw new Exception("Unknown Diffusion URI action '{$action}'!");
}
if ($action == 'rendering-ref') {
return $uri;
}
$uri = new PhutilURI($uri);
if (idx($params, 'params')) {
$uri->setQueryParams($params['params']);
}
return $uri;
}
/**
* Internal. Public only for unit tests.
*
* Parse the request URI into components.
*
* @param string URI blob.
* @param bool True if this VCS supports branches.
* @return map Parsed URI.
*
* @task uri
*/
public static function parseRequestBlob($blob, $supports_branches) {
$result = array(
'branch' => null,
'path' => null,
'commit' => null,
'line' => null,
);
$matches = null;
if ($supports_branches) {
// Consume the front part of the URI, up to the first "/". This is the
// path-component encoded branch name.
if (preg_match('@^([^/]+)/@', $blob, $matches)) {
$result['branch'] = phutil_unescape_uri_path_component($matches[1]);
$blob = substr($blob, strlen($matches[1]) + 1);
}
}
// Consume the back part of the URI, up to the first "$". Use a negative
// lookbehind to prevent matching '$$'. We double the '$' symbol when
// encoding so that files with names like "money/$100" will survive.
$pattern = '@(?:(?:^|[^$])(?:[$][$])*)[$]([\d-,]+)$@';
if (preg_match($pattern, $blob, $matches)) {
$result['line'] = $matches[1];
$blob = substr($blob, 0, -(strlen($matches[1]) + 1));
}
// We've consumed the line number if it exists, so unescape "$" in the
// rest of the string.
$blob = str_replace('$$', '$', $blob);
// Consume the commit name, stopping on ';;'. We allow any character to
// appear in commits names, as they can sometimes be symbolic names (like
// tag names or refs).
if (preg_match('@(?:(?:^|[^;])(?:;;)*);([^;].*)$@', $blob, $matches)) {
$result['commit'] = $matches[1];
$blob = substr($blob, 0, -(strlen($matches[1]) + 1));
}
// We've consumed the commit if it exists, so unescape ";" in the rest
// of the string.
$blob = str_replace(';;', ';', $blob);
if (strlen($blob)) {
$result['path'] = $blob;
}
$parts = explode('/', $result['path']);
foreach ($parts as $part) {
// Prevent any hyjinx since we're ultimately shipping this to the
// filesystem under a lot of workflows.
if ($part == '..') {
throw new Exception("Invalid path URI.");
}
}
return $result;
}
protected function raiseCloneException() {
$host = php_uname('n');
$callsign = $this->getRepository()->getCallsign();
throw new DiffusionSetupException(
"The working copy for this repository ('{$callsign}') hasn't been ".
"cloned yet on this machine ('{$host}'). Make sure you've started the ".
"Phabricator daemons. If this problem persists for longer than a clone ".
"should take, check the daemon logs (in the Daemon Console) to see if ".
"there were errors cloning the repository. Consult the 'Diffusion User ".
"Guide' in the documentation for help setting up repositories.");
}
}
diff --git a/src/applications/diffusion/request/DiffusionSvnRequest.php b/src/applications/diffusion/request/DiffusionSvnRequest.php
index 315c2f1a0b..b305b10fbe 100644
--- a/src/applications/diffusion/request/DiffusionSvnRequest.php
+++ b/src/applications/diffusion/request/DiffusionSvnRequest.php
@@ -1,67 +1,51 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group diffusion
*/
final class DiffusionSvnRequest extends DiffusionRequest {
protected function getSupportsBranches() {
return false;
}
protected function didInitialize() {
if ($this->path === null) {
$subpath = $this->repository->getDetail('svn-subpath');
if ($subpath) {
$this->path = $subpath;
}
}
}
public function getStableCommitName() {
if ($this->commit) {
return $this->commit;
}
if ($this->stableCommitName === null) {
$commit = id(new PhabricatorRepositoryCommit())
->loadOneWhere(
'repositoryID = %d ORDER BY epoch DESC LIMIT 1',
$this->getRepository()->getID());
if ($commit) {
$this->stableCommitName = $commit->getCommitIdentifier();
} else {
// For new repositories, we may not have parsed any commits yet. Call
// the stable commit "1" and avoid fataling.
$this->stableCommitName = 1;
}
}
return $this->stableCommitName;
}
public function getCommit() {
if ($this->commit) {
return $this->commit;
}
return $this->getStableCommitName();
}
}
diff --git a/src/applications/diffusion/request/__tests__/DiffusionURITestCase.php b/src/applications/diffusion/request/__tests__/DiffusionURITestCase.php
index 3a4e3cce32..9ffafd9360 100644
--- a/src/applications/diffusion/request/__tests__/DiffusionURITestCase.php
+++ b/src/applications/diffusion/request/__tests__/DiffusionURITestCase.php
@@ -1,166 +1,150 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionURITestCase extends ArcanistPhutilTestCase {
public function testBlobDecode() {
$map = array(
// This is a basic blob.
'branch/path.ext;abc$3' => array(
'branch' => 'branch',
'path' => 'path.ext',
'commit' => 'abc',
'line' => '3',
),
'branch/path.ext$3' => array(
'branch' => 'branch',
'path' => 'path.ext',
'line' => '3',
),
'branch/money;;/$$100' => array(
'branch' => 'branch',
'path' => 'money;/$100',
),
'a%252Fb/' => array(
'branch' => 'a/b',
),
'branch/path/;Version-1_0_0' => array(
'branch' => 'branch',
'path' => 'path/',
'commit' => 'Version-1_0_0',
),
'branch/path/;$$moneytag$$' => array(
'branch' => 'branch',
'path' => 'path/',
'commit' => '$moneytag$',
),
'branch/path/semicolon;;;;;$$;;semicolon;;$$$$$100' => array(
'branch' => 'branch',
'path' => 'path/semicolon;;',
'commit' => '$;;semicolon;;$$',
'line' => '100',
),
'branch/path.ext;abc$3-5,7-12,14' => array(
'branch' => 'branch',
'path' => 'path.ext',
'commit' => 'abc',
'line' => '3-5,7-12,14',
),
);
foreach ($map as $input => $expect) {
// Simulate decode effect of the webserver.
$input = rawurldecode($input);
$expect = $expect + array(
'branch' => null,
'path' => null,
'commit' => null,
'line' => null,
);
$expect = array_select_keys(
$expect,
array('branch', 'path', 'commit', 'line'));
$actual = $this->parseBlob($input);
$this->assertEqual(
$expect,
$actual,
"Parsing '{$input}'");
}
}
public function testBlobDecodeFail() {
$this->tryTestCaseMap(
array(
'branch/path/../../../secrets/secrets.key' => false,
),
array($this, 'parseBlob'));
}
public function parseBlob($blob) {
return DiffusionRequest::parseRequestBlob(
$blob,
$supports_branches = true);
}
public function testURIGeneration() {
$map = array(
'/diffusion/A/browse/branch/path.ext;abc$1' => array(
'action' => 'browse',
'callsign' => 'A',
'branch' => 'branch',
'path' => 'path.ext',
'commit' => 'abc',
'line' => '1',
),
'/diffusion/A/browse/a%252Fb/path.ext' => array(
'action' => 'browse',
'callsign' => 'A',
'branch' => 'a/b',
'path' => 'path.ext',
),
'/diffusion/A/browse/%2B/%20%21' => array(
'action' => 'browse',
'callsign' => 'A',
'path' => '+/ !',
),
'/diffusion/A/browse/money/%24%24100$2' => array(
'action' => 'browse',
'callsign' => 'A',
'path' => 'money/$100',
'line' => '2',
),
'/diffusion/A/browse/path/to/file.ext?view=things' => array(
'action' => 'browse',
'callsign' => 'A',
'path' => 'path/to/file.ext',
'params' => array(
'view' => 'things',
),
),
'/diffusion/A/repository/master/' => array(
'action' => 'branch',
'callsign' => 'A',
'branch' => 'master',
),
'path/to/file.ext;abc' => array(
'action' => 'rendering-ref',
'path' => 'path/to/file.ext',
'commit' => 'abc',
),
'/diffusion/A/browse/branch/path.ext$3-5%2C7-12%2C14' => array(
'action' => 'browse',
'callsign' => 'A',
'branch' => 'branch',
'path' => 'path.ext',
'line' => '3-5,7-12,14',
),
);
foreach ($map as $expect => $input) {
$actual = DiffusionRequest::generateDiffusionURI($input);
$this->assertEqual(
$expect,
(string)$actual);
}
}
}
diff --git a/src/applications/diffusion/view/DiffusionBranchTableView.php b/src/applications/diffusion/view/DiffusionBranchTableView.php
index 61076776fc..0ce10bffe9 100644
--- a/src/applications/diffusion/view/DiffusionBranchTableView.php
+++ b/src/applications/diffusion/view/DiffusionBranchTableView.php
@@ -1,118 +1,102 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionBranchTableView extends DiffusionView {
private $branches;
private $user;
private $commits = array();
public function setBranches(array $branches) {
assert_instances_of($branches, 'DiffusionBranchInformation');
$this->branches = $branches;
return $this;
}
public function setCommits(array $commits) {
$this->commits = mpull($commits, null, 'getCommitIdentifier');
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function render() {
$drequest = $this->getDiffusionRequest();
$current_branch = $drequest->getBranch();
$rows = array();
$rowc = array();
foreach ($this->branches as $branch) {
$commit = idx($this->commits, $branch->getHeadCommitIdentifier());
if ($commit) {
$details = $commit->getCommitData()->getCommitMessage();
$details = idx(explode("\n", $details), 0);
$details = substr($details, 0, 80);
$datetime = phabricator_datetime($commit->getEpoch(), $this->user);
} else {
$datetime = null;
$details = null;
}
$rows[] = array(
phutil_render_tag(
'a',
array(
'href' => $drequest->generateURI(
array(
'action' => 'history',
'branch' => $branch->getName(),
))
),
'History'
),
phutil_render_tag(
'a',
array(
'href' => $drequest->generateURI(
array(
'action' => 'browse',
'branch' => $branch->getName(),
)),
),
phutil_escape_html($branch->getName())),
self::linkCommit(
$drequest->getRepository(),
$branch->getHeadCommitIdentifier()),
$datetime,
AphrontTableView::renderSingleDisplayLine(
phutil_escape_html($details))
// TODO: etc etc
);
if ($branch->getName() == $current_branch) {
$rowc[] = 'highlighted';
} else {
$rowc[] = null;
}
}
$view = new AphrontTableView($rows);
$view->setHeaders(
array(
'History',
'Branch',
'Head',
'Modified',
'Details',
));
$view->setColumnClasses(
array(
'',
'pri',
'',
'',
'wide',
));
$view->setRowClasses($rowc);
return $view->render();
}
}
diff --git a/src/applications/diffusion/view/DiffusionBrowseTableView.php b/src/applications/diffusion/view/DiffusionBrowseTableView.php
index 2c0978d434..9a962bd900 100644
--- a/src/applications/diffusion/view/DiffusionBrowseTableView.php
+++ b/src/applications/diffusion/view/DiffusionBrowseTableView.php
@@ -1,261 +1,245 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionBrowseTableView extends DiffusionView {
private $paths;
private $handles = array();
private $user;
public function setPaths(array $paths) {
assert_instances_of($paths, 'DiffusionRepositoryPath');
$this->paths = $paths;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public static function renderLastModifiedColumns(
PhabricatorRepository $repository,
array $handles,
PhabricatorRepositoryCommit $commit = null,
PhabricatorRepositoryCommitData $data = null) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
if ($commit) {
$epoch = $commit->getEpoch();
$modified = DiffusionView::linkCommit(
$repository,
$commit->getCommitIdentifier());
$date = date('M j, Y', $epoch);
$time = date('g:i A', $epoch);
} else {
$modified = '';
$date = '';
$time = '';
}
if ($data) {
$author_phid = $data->getCommitDetail('authorPHID');
if ($author_phid && isset($handles[$author_phid])) {
$author = $handles[$author_phid]->renderLink();
} else {
$author = self::renderName($data->getAuthorName());
}
$committer = $data->getCommitDetail('committer');
if ($committer) {
$committer_phid = $data->getCommitDetail('committerPHID');
if ($committer_phid && isset($handles[$committer_phid])) {
$committer = $handles[$committer_phid]->renderLink();
} else {
$committer = self::renderName($committer);
}
if ($author != $committer) {
$author .= '/'.$committer;
}
}
$details = AphrontTableView::renderSingleDisplayLine(
phutil_escape_html($data->getSummary()));
} else {
$author = '';
$details = '';
}
return array(
'commit' => $modified,
'date' => $date,
'time' => $time,
'author' => $author,
'details' => $details,
);
}
public function render() {
$request = $this->getDiffusionRequest();
$repository = $request->getRepository();
$base_path = trim($request->getPath(), '/');
if ($base_path) {
$base_path = $base_path.'/';
}
$need_pull = array();
$rows = array();
$show_edit = false;
foreach ($this->paths as $path) {
$dir_slash = null;
$file_type = $path->getFileType();
if ($file_type == DifferentialChangeType::FILE_DIRECTORY) {
$browse_text = $path->getPath().'/';
$dir_slash = '/';
$browse_link = '<strong>'.$this->linkBrowse(
$base_path.$path->getPath().$dir_slash,
array(
'html' => $this->renderPathIcon(
'dir',
$browse_text),
)).'</strong>';
} else if ($file_type == DifferentialChangeType::FILE_SUBMODULE) {
$browse_text = $path->getPath().'/';
$browse_link =
'<strong>'.
$this->linkExternal(
$path->getHash(),
$path->getExternalURI(),
$this->renderPathIcon(
'ext',
$browse_text)).
'</strong>';
} else {
if ($file_type == DifferentialChangeType::FILE_SYMLINK) {
$type = 'link';
} else {
$type = 'file';
}
$browse_text = $path->getPath();
$browse_link = $this->linkBrowse(
$base_path.$path->getPath(),
array(
'html' => $this->renderPathIcon($type, $browse_text),
));
}
$commit = $path->getLastModifiedCommit();
if ($commit) {
$dict = self::renderLastModifiedColumns(
$repository,
$this->handles,
$commit,
$path->getLastCommitData());
} else {
$dict = array(
'commit' => celerity_generate_unique_node_id(),
'date' => celerity_generate_unique_node_id(),
'time' => celerity_generate_unique_node_id(),
'author' => celerity_generate_unique_node_id(),
'details' => celerity_generate_unique_node_id(),
);
$uri = (string)$request->generateURI(
array(
'action' => 'lastmodified',
'path' => $base_path.$path->getPath(),
));
$need_pull[$uri] = $dict;
foreach ($dict as $k => $uniq) {
$dict[$k] = '<span id="'.$uniq.'"></span>';
}
}
$editor_button = '';
if ($this->user) {
$editor_link = $this->user->loadEditorLink(
$base_path.$path->getPath(),
1,
$request->getRepository()->getCallsign());
if ($editor_link) {
$show_edit = true;
$editor_button = phutil_render_tag(
'a',
array(
'href' => $editor_link,
),
'Edit');
}
}
$rows[] = array(
$this->linkHistory($base_path.$path->getPath().$dir_slash),
$editor_button,
$browse_link,
$dict['commit'],
$dict['date'],
$dict['time'],
$dict['author'],
$dict['details'],
);
}
if ($need_pull) {
Javelin::initBehavior('diffusion-pull-lastmodified', $need_pull);
}
$view = new AphrontTableView($rows);
$view->setHeaders(
array(
'History',
'Edit',
'Path',
'Modified',
'Date',
'Time',
'Author/Committer',
'Details',
));
$view->setColumnClasses(
array(
'',
'',
'',
'',
'',
'right',
'',
'wide',
));
$view->setColumnVisibility(
array(
true,
$show_edit,
true,
true,
true,
true,
true,
true,
));
return $view->render();
}
private function renderPathIcon($type, $text) {
require_celerity_resource('diffusion-icons-css');
return phutil_render_tag(
'span',
array(
'class' => 'diffusion-path-icon diffusion-path-icon-'.$type,
),
phutil_escape_html($text));
}
}
diff --git a/src/applications/diffusion/view/DiffusionCommentListView.php b/src/applications/diffusion/view/DiffusionCommentListView.php
index 0f5b1e0a48..3c940b0a84 100644
--- a/src/applications/diffusion/view/DiffusionCommentListView.php
+++ b/src/applications/diffusion/view/DiffusionCommentListView.php
@@ -1,118 +1,102 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionCommentListView extends AphrontView {
private $user;
private $comments;
private $inlineComments = array();
private $pathMap = array();
private $handles = array();
private $markupEngine;
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setComments(array $comments) {
assert_instances_of($comments, 'PhabricatorAuditComment');
$this->comments = $comments;
return $this;
}
public function setInlineComments(array $inline_comments) {
assert_instances_of($inline_comments, 'PhabricatorInlineCommentInterface');
$this->inlineComments = $inline_comments;
return $this;
}
public function setPathMap(array $path_map) {
$this->pathMap = $path_map;
return $this;
}
public function setMarkupEngine(PhabricatorMarkupEngine $markup_engine) {
$this->markupEngine = $markup_engine;
return $this;
}
public function getMarkupEngine() {
return $this->markupEngine;
}
public function getRequiredHandlePHIDs() {
$phids = array();
foreach ($this->comments as $comment) {
$phids[$comment->getActorPHID()] = true;
$metadata = $comment->getMetaData();
$ccs_key = PhabricatorAuditComment::METADATA_ADDED_CCS;
$added_ccs = idx($metadata, $ccs_key, array());
foreach ($added_ccs as $cc) {
$phids[$cc] = true;
}
$auditors_key = PhabricatorAuditComment::METADATA_ADDED_AUDITORS;
$added_auditors = idx($metadata, $auditors_key, array());
foreach ($added_auditors as $auditor) {
$phids[$auditor] = true;
}
}
foreach ($this->inlineComments as $comment) {
$phids[$comment->getAuthorPHID()] = true;
}
return array_keys($phids);
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function render() {
$inline_comments = mgroup($this->inlineComments, 'getAuditCommentID');
$num = 1;
$comments = array();
foreach ($this->comments as $comment) {
$inlines = idx($inline_comments, $comment->getID(), array());
$view = id(new DiffusionCommentView())
->setMarkupEngine($this->getMarkupEngine())
->setComment($comment)
->setInlineComments($inlines)
->setCommentNumber($num)
->setHandles($this->handles)
->setPathMap($this->pathMap)
->setUser($this->user);
$comments[] = $view->render();
++$num;
}
return
'<div class="diffusion-comment-list">'.
$this->renderSingleView($comments).
'</div>';
}
}
diff --git a/src/applications/diffusion/view/DiffusionCommentView.php b/src/applications/diffusion/view/DiffusionCommentView.php
index 42ef8a11f0..c591f021bb 100644
--- a/src/applications/diffusion/view/DiffusionCommentView.php
+++ b/src/applications/diffusion/view/DiffusionCommentView.php
@@ -1,230 +1,214 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionCommentView extends AphrontView {
private $user;
private $comment;
private $commentNumber;
private $handles;
private $isPreview;
private $pathMap;
private $inlineComments;
private $markupEngine;
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setComment(PhabricatorAuditComment $comment) {
$this->comment = $comment;
return $this;
}
public function setCommentNumber($comment_number) {
$this->commentNumber = $comment_number;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setIsPreview($is_preview) {
$this->isPreview = $is_preview;
return $this;
}
public function setInlineComments(array $inline_comments) {
assert_instances_of($inline_comments, 'PhabricatorInlineCommentInterface');
$this->inlineComments = $inline_comments;
return $this;
}
public function setPathMap(array $path_map) {
$this->pathMap = $path_map;
return $this;
}
public function setMarkupEngine(PhabricatorMarkupEngine $markup_engine) {
$this->markupEngine = $markup_engine;
return $this;
}
public function getMarkupEngine() {
return $this->markupEngine;
}
public function getRequiredHandlePHIDs() {
return array($this->comment->getActorPHID());
}
private function getHandle($phid) {
if (empty($this->handles[$phid])) {
throw new Exception("Unloaded handle '{$phid}'!");
}
return $this->handles[$phid];
}
public function render() {
$comment = $this->comment;
$author = $this->getHandle($comment->getActorPHID());
$author_link = $author->renderLink();
$actions = $this->renderActions();
$content = $this->renderContent();
$classes = $this->renderClasses();
$xaction_view = id(new PhabricatorTransactionView())
->setUser($this->user)
->setImageURI($author->getImageURI())
->setActions($actions)
->appendChild($content);
if ($this->isPreview) {
$xaction_view->setIsPreview(true);
} else {
$xaction_view
->setAnchor('comment-'.$this->commentNumber, '#'.$this->commentNumber)
->setEpoch($comment->getDateCreated());
}
foreach ($classes as $class) {
$xaction_view->addClass($class);
}
return $xaction_view->render();
}
private function renderActions() {
$comment = $this->comment;
$author = $this->getHandle($comment->getActorPHID());
$author_link = $author->renderLink();
$action = $comment->getAction();
$verb = PhabricatorAuditActionConstants::getActionPastTenseVerb($action);
$metadata = $comment->getMetadata();
$added_auditors = idx(
$metadata,
PhabricatorAuditComment::METADATA_ADDED_AUDITORS,
array());
$added_ccs = idx(
$metadata,
PhabricatorAuditComment::METADATA_ADDED_CCS,
array());
$actions = array();
if ($action == PhabricatorAuditActionConstants::ADD_CCS) {
$rendered_ccs = $this->renderHandleList($added_ccs);
$actions[] = "{$author_link} added CCs: {$rendered_ccs}.";
} else if ($action == PhabricatorAuditActionConstants::ADD_AUDITORS) {
$rendered_auditors = $this->renderHandleList($added_auditors);
$actions[] = "{$author_link} added auditors: ".
"{$rendered_auditors}.";
} else {
$actions[] = "{$author_link} ".phutil_escape_html($verb)." this commit.";
}
foreach ($actions as $key => $action) {
$actions[$key] = '<div>'.$action.'</div>';
}
return $actions;
}
private function renderContent() {
$comment = $this->comment;
$engine = $this->getMarkupEngine();
if (!strlen($comment->getContent()) && empty($this->inlineComments)) {
return null;
} else {
return
'<div class="phabricator-remarkup">'.
$engine->getOutput(
$comment,
PhabricatorAuditComment::MARKUP_FIELD_BODY).
$this->renderSingleView($this->renderInlines()).
'</div>';
}
}
private function renderInlines() {
if (!$this->inlineComments) {
return null;
}
$engine = $this->getMarkupEngine();
$inlines_by_path = mgroup($this->inlineComments, 'getPathID');
$view = new PhabricatorInlineSummaryView();
foreach ($inlines_by_path as $path_id => $inlines) {
$path = idx($this->pathMap, $path_id);
if ($path === null) {
continue;
}
$items = array();
foreach ($inlines as $inline) {
$items[] = array(
'id' => $inline->getID(),
'line' => $inline->getLineNumber(),
'length' => $inline->getLineLength(),
'content' => $engine->getOutput(
$inline,
PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY),
);
}
$view->addCommentGroup($path, $items);
}
return $view;
}
private function renderHandleList(array $phids) {
$result = array();
foreach ($phids as $phid) {
$result[] = $this->handles[$phid]->renderLink();
}
return implode(', ', $result);
}
private function renderClasses() {
$comment = $this->comment;
$classes = array();
switch ($comment->getAction()) {
case PhabricatorAuditActionConstants::ACCEPT:
$classes[] = 'audit-accept';
break;
case PhabricatorAuditActionConstants::CONCERN:
$classes[] = 'audit-concern';
break;
}
return $classes;
}
}
diff --git a/src/applications/diffusion/view/DiffusionCommitChangeTableView.php b/src/applications/diffusion/view/DiffusionCommitChangeTableView.php
index 7deb75b6f0..1a1f66bc4c 100644
--- a/src/applications/diffusion/view/DiffusionCommitChangeTableView.php
+++ b/src/applications/diffusion/view/DiffusionCommitChangeTableView.php
@@ -1,115 +1,99 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionCommitChangeTableView extends DiffusionView {
private $pathChanges;
private $ownersPaths = array();
private $renderingReferences;
public function setPathChanges(array $path_changes) {
assert_instances_of($path_changes, 'DiffusionPathChange');
$this->pathChanges = $path_changes;
return $this;
}
public function setOwnersPaths(array $owners_paths) {
assert_instances_of($owners_paths, 'PhabricatorOwnersPath');
$this->ownersPaths = $owners_paths;
return $this;
}
public function setRenderingReferences(array $value) {
$this->renderingReferences = $value;
return $this;
}
public function render() {
$rows = array();
$rowc = array();
// TODO: Experiment with path stack rendering.
// TODO: Copy Away and Move Away are rendered junkily still.
foreach ($this->pathChanges as $id => $change) {
$path = $change->getPath();
$hash = substr(md5($path), 0, 8);
if ($change->getFileType() == DifferentialChangeType::FILE_DIRECTORY) {
$path .= '/';
}
if (isset($this->renderingReferences[$id])) {
$path_column = javelin_render_tag(
'a',
array(
'href' => '#'.$hash,
'meta' => array(
'id' => 'diff-'.$hash,
'ref' => $this->renderingReferences[$id],
),
'sigil' => 'differential-load',
),
phutil_escape_html($path));
} else {
$path_column = phutil_escape_html($path);
}
$rows[] = array(
$this->linkHistory($change->getPath()),
$this->linkBrowse($change->getPath()),
$this->linkChange(
$change->getChangeType(),
$change->getFileType(),
$change->getPath()),
$path_column,
);
$row_class = null;
foreach ($this->ownersPaths as $owners_path) {
$owners_path = $owners_path->getPath();
if (strncmp('/'.$path, $owners_path, strlen($owners_path)) == 0) {
$row_class = 'highlighted';
break;
}
}
$rowc[] = $row_class;
}
$view = new AphrontTableView($rows);
$view->setHeaders(
array(
'History',
'Browse',
'Change',
'Path',
));
$view->setColumnClasses(
array(
'',
'',
'',
'wide',
));
$view->setRowClasses($rowc);
$view->setNoDataString('This change has not been fully parsed yet.');
return $view->render();
}
}
diff --git a/src/applications/diffusion/view/DiffusionEmptyResultView.php b/src/applications/diffusion/view/DiffusionEmptyResultView.php
index a6a0144471..75997fafa0 100644
--- a/src/applications/diffusion/view/DiffusionEmptyResultView.php
+++ b/src/applications/diffusion/view/DiffusionEmptyResultView.php
@@ -1,102 +1,86 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionEmptyResultView extends DiffusionView {
private $browseQuery;
private $view;
public function setBrowseQuery($browse_query) {
$this->browseQuery = $browse_query;
return $this;
}
public function setView($view) {
$this->view = $view;
return $this;
}
public function render() {
$drequest = $this->getDiffusionRequest();
$commit = $drequest->getCommit();
$callsign = $drequest->getRepository()->getCallsign();
if ($commit) {
$commit = "r{$callsign}{$commit}";
} else {
$commit = 'HEAD';
}
switch ($this->browseQuery->getReasonForEmptyResultSet()) {
case DiffusionBrowseQuery::REASON_IS_NONEXISTENT:
$title = 'Path Does Not Exist';
// TODO: Under git, this error message should be more specific. It
// may exist on some other branch.
$body = "This path does not exist anywhere.";
$severity = AphrontErrorView::SEVERITY_ERROR;
break;
case DiffusionBrowseQuery::REASON_IS_EMPTY:
$title = 'Empty Directory';
$body = "This path was an empty directory at {$commit}.\n";
$severity = AphrontErrorView::SEVERITY_NOTICE;
break;
case DiffusionBrowseQuery::REASON_IS_DELETED:
$deleted = $this->browseQuery->getDeletedAtCommit();
$existed = $this->browseQuery->getExistedAtCommit();
$deleted = self::linkCommit($drequest->getRepository(), $deleted);
$browse = $this->linkBrowse(
$drequest->getPath(),
array(
'text' => 'existed',
'commit' => $existed,
'params' => array('view' => $this->view),
)
);
$existed = "r{$callsign}{$existed}";
$title = 'Path Was Deleted';
$body = "This path does not exist at {$commit}. It was deleted in ".
"{$deleted} and last {$browse} at {$existed}.";
$severity = AphrontErrorView::SEVERITY_WARNING;
break;
case DiffusionBrowseQuery::REASON_IS_UNTRACKED_PARENT:
$subdir = $drequest->getRepository()->getDetail('svn-subpath');
$title = 'Directory Not Tracked';
$body =
"This repository is configured to track only one subdirectory ".
"of the entire repository ('".phutil_escape_html($subdir)."'), ".
"but you aren't looking at something in that subdirectory, so no ".
"information is available.";
$severity = AphrontErrorView::SEVERITY_WARNING;
break;
default:
throw new Exception("Unknown failure reason!");
}
$error_view = new AphrontErrorView();
$error_view->setSeverity($severity);
$error_view->setTitle($title);
$error_view->appendChild('<p>'.$body.'</p>');
return $error_view->render();
}
}
diff --git a/src/applications/diffusion/view/DiffusionHistoryTableView.php b/src/applications/diffusion/view/DiffusionHistoryTableView.php
index fb48834fe3..6c577a586c 100644
--- a/src/applications/diffusion/view/DiffusionHistoryTableView.php
+++ b/src/applications/diffusion/view/DiffusionHistoryTableView.php
@@ -1,349 +1,333 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionHistoryTableView extends DiffusionView {
private $history;
private $revisions = array();
private $handles = array();
private $isHead;
private $parents;
public function setHistory(array $history) {
assert_instances_of($history, 'DiffusionPathChange');
$this->history = $history;
return $this;
}
public function loadRevisions() {
$commit_phids = array();
foreach ($this->history as $item) {
if ($item->getCommit()) {
$commit_phids[] = $item->getCommit()->getPHID();
}
}
$this->revisions = id(new DifferentialRevision())
->loadIDsByCommitPHIDs($commit_phids);
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function getRequiredHandlePHIDs() {
$phids = array();
foreach ($this->history as $item) {
$data = $item->getCommitData();
if ($data) {
if ($data->getCommitDetail('authorPHID')) {
$phids[$data->getCommitDetail('authorPHID')] = true;
}
if ($data->getCommitDetail('committerPHID')) {
$phids[$data->getCommitDetail('committerPHID')] = true;
}
}
}
return array_keys($phids);
}
public function setParents(array $parents) {
$this->parents = $parents;
return $this;
}
public function setIsHead($is_head) {
$this->isHead = $is_head;
return $this;
}
public function render() {
$drequest = $this->getDiffusionRequest();
$handles = $this->handles;
$graph = null;
if ($this->parents) {
$graph = $this->renderGraph();
}
$rows = array();
$ii = 0;
foreach ($this->history as $history) {
$epoch = $history->getEpoch();
if ($epoch) {
$date = date('M j, Y', $epoch);
$time = date('g:i A', $epoch);
} else {
$date = null;
$time = null;
}
$data = $history->getCommitData();
$author_phid = $committer = $committer_phid = null;
if ($data) {
$author_phid = $data->getCommitDetail('authorPHID');
$committer_phid = $data->getCommitDetail('committerPHID');
$committer = $data->getCommitDetail('committer');
}
if ($author_phid && isset($handles[$author_phid])) {
$author = $handles[$author_phid]->renderLink();
} else {
$author = self::renderName($history->getAuthorName());
}
$different_committer = false;
if ($committer_phid) {
$different_committer = ($committer_phid != $author_phid);
} else if ($committer != '') {
$different_committer = ($committer != $history->getAuthorName());
}
if ($different_committer) {
if ($committer_phid && isset($handles[$committer_phid])) {
$committer = $handles[$committer_phid]->renderLink();
} else {
$committer = self::renderName($committer);
}
$author .= '/'.$committer;
}
$commit = $history->getCommit();
if ($commit && !$commit->getIsUnparsed() && $data) {
$change = $this->linkChange(
$history->getChangeType(),
$history->getFileType(),
$path = null,
$history->getCommitIdentifier());
} else {
$change = "<em>Importing\xE2\x80\xA6</em>";
}
$rows[] = array(
$this->linkBrowse(
$drequest->getPath(),
array(
'commit' => $history->getCommitIdentifier(),
)),
$graph ? $graph[$ii++] : null,
self::linkCommit(
$drequest->getRepository(),
$history->getCommitIdentifier()),
($commit ?
self::linkRevision(idx($this->revisions, $commit->getPHID())) :
null),
$change,
$date,
$time,
$author,
AphrontTableView::renderSingleDisplayLine(
phutil_escape_html($history->getSummary())),
// TODO: etc etc
);
}
$view = new AphrontTableView($rows);
$view->setHeaders(
array(
'Browse',
'',
'Commit',
'Revision',
'Change',
'Date',
'Time',
'Author/Committer',
'Details',
));
$view->setColumnClasses(
array(
'',
'threads',
'n',
'n',
'',
'',
'right',
'',
'wide',
));
$view->setColumnVisibility(
array(
true,
$graph ? true : false,
));
return $view->render();
}
/**
* Draw a merge/branch graph from the parent revision data. We're basically
* building up a bunch of strings like this:
*
* ^
* |^
* o|
* |o
* o
*
* ...which form an ASCII representation of the graph we eventaully want to
* draw.
*
* NOTE: The actual implementation is black magic.
*/
private function renderGraph() {
// This keeps our accumulated information about each line of the
// merge/branch graph.
$graph = array();
// This holds the next commit we're looking for in each column of the
// graph.
$threads = array();
// This is the largest number of columns any row has, i.e. the width of
// the graph.
$count = 0;
foreach ($this->history as $key => $history) {
$joins = array();
$splits = array();
$parent_list = $this->parents[$history->getCommitIdentifier()];
// Look for some thread which has this commit as the next commit. If
// we find one, this commit goes on that thread. Otherwise, this commit
// goes on a new thread.
$line = '';
$found = false;
$pos = count($threads);
for ($n = 0; $n < $count; $n++) {
if (empty($threads[$n])) {
$line .= ' ';
continue;
}
if ($threads[$n] == $history->getCommitIdentifier()) {
if ($found) {
$line .= ' ';
$joins[] = $n;
unset($threads[$n]);
} else {
$line .= 'o';
$found = true;
$pos = $n;
}
} else {
// We render a "|" for any threads which have a commit that we haven't
// seen yet, this is later drawn as a vertical line.
$line .= '|';
}
}
// If we didn't find the thread this commit goes on, start a new thread.
// We use "o" to mark the commit for the rendering engine, or "^" to
// indicate that there's nothing after it so the line from the commit
// upward should not be drawn.
if (!$found) {
if ($this->isHead) {
$line .= '^';
} else {
$line .= 'o';
foreach ($graph as $k => $meta) {
// Go back across all the lines we've already drawn and add a
// "|" to the end, since this is connected to some future commit
// we don't know about.
for ($jj = strlen($meta['line']); $jj <= $count; $jj++) {
$graph[$k]['line'] .= '|';
}
}
}
}
// Update the next commit on this thread to the commit's first parent.
// This might have the effect of making a new thread.
$threads[$pos] = head($parent_list);
// If we made a new thread, increase the thread count.
$count = max($pos + 1, $count);
// Now, deal with splits (merges). I picked this terms opposite to the
// underlying repository term to confuse you.
foreach (array_slice($parent_list, 1) as $parent) {
$found = false;
// Try to find the other parent(s) in our existing threads. If we find
// them, split to that thread.
foreach ($threads as $idx => $thread_commit) {
if ($thread_commit == $parent) {
$found = true;
$splits[] = $idx;
}
}
// If we didn't find the parent, we don't know about it yet. Find the
// first free thread and add it as the "next" commit in that thread.
// This might create a new thread.
if (!$found) {
for ($n = 0; $n < $count; $n++) {
if (empty($threads[$n])) {
break;
}
}
$threads[$n] = $parent;
$splits[] = $n;
$count = max($n + 1, $count);
}
}
$graph[] = array(
'line' => $line,
'split' => $splits,
'join' => $joins,
);
}
// Render into tags for the behavior.
foreach ($graph as $k => $meta) {
$graph[$k] = javelin_render_tag(
'div',
array(
'sigil' => 'commit-graph',
'meta' => $meta,
),
'');
}
Javelin::initBehavior(
'diffusion-commit-graph',
array(
'count' => $count,
));
return $graph;
}
}
diff --git a/src/applications/diffusion/view/DiffusionTagListView.php b/src/applications/diffusion/view/DiffusionTagListView.php
index 57ece7fddc..7b286ff2b5 100644
--- a/src/applications/diffusion/view/DiffusionTagListView.php
+++ b/src/applications/diffusion/view/DiffusionTagListView.php
@@ -1,134 +1,118 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DiffusionTagListView extends DiffusionView {
private $tags;
private $user;
private $commits = array();
private $handles = array();
public function setUser($user) {
$this->user = $user;
return $this;
}
public function setTags($tags) {
$this->tags = $tags;
return $this;
}
public function setCommits(array $commits) {
$this->commits = mpull($commits, null, 'getCommitIdentifier');
return $this;
}
public function setHandles(array $handles) {
$this->handles = $handles;
return $this;
}
public function getRequiredHandlePHIDs() {
return array_filter(mpull($this->commits, 'getAuthorPHID'));
}
public function render() {
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$rows = array();
foreach ($this->tags as $tag) {
$commit = idx($this->commits, $tag->getCommitIdentifier());
$tag_link = phutil_render_tag(
'a',
array(
'href' => $drequest->generateURI(
array(
'action' => 'browse',
'commit' => $tag->getName(),
)),
),
phutil_escape_html($tag->getName()));
$commit_link = phutil_render_tag(
'a',
array(
'href' => $drequest->generateURI(
array(
'action' => 'commit',
'commit' => $tag->getCommitIdentifier(),
)),
),
phutil_escape_html(
$repository->formatCommitName(
$tag->getCommitIdentifier())));
$author = null;
if ($commit && $commit->getAuthorPHID()) {
$author = $this->handles[$commit->getAuthorPHID()]->renderLink();
} else if ($commit && $commit->getCommitData()) {
$author = self::renderName($commit->getCommitData()->getAuthorName());
} else {
$author = self::renderName($tag->getAuthor());
}
$description = null;
if ($tag->getType() == 'git/tag') {
// In Git, a tag may be a "real" tag, or just a reference to a commit.
// If it's a real tag, use the message on the tag, since this may be
// unique data which isn't otherwise available.
$description = $tag->getDescription();
} else {
if ($commit && $commit->getCommitData()) {
$description = $commit->getCommitData()->getSummary();
} else {
$description = $tag->getDescription();
}
}
$description = phutil_escape_html($description);
$rows[] = array(
$tag_link,
$commit_link,
$description,
$author,
phabricator_datetime($tag->getEpoch(), $this->user),
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Tag',
'Commit',
'Description',
'Author',
'Created',
));
$table->setColumnClasses(
array(
'pri',
'',
'wide',
));
return $table->render();
}
}
diff --git a/src/applications/diffusion/view/DiffusionView.php b/src/applications/diffusion/view/DiffusionView.php
index 767e4fb84a..c079d816f0 100644
--- a/src/applications/diffusion/view/DiffusionView.php
+++ b/src/applications/diffusion/view/DiffusionView.php
@@ -1,170 +1,154 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DiffusionView extends AphrontView {
private $diffusionRequest;
final public function setDiffusionRequest(DiffusionRequest $request) {
$this->diffusionRequest = $request;
return $this;
}
final public function getDiffusionRequest() {
return $this->diffusionRequest;
}
final public function linkChange($change_type, $file_type, $path = null,
$commit_identifier = null) {
$text = DifferentialChangeType::getFullNameForChangeType($change_type);
if ($change_type == DifferentialChangeType::TYPE_CHILD) {
// TODO: Don't link COPY_AWAY without a direct change.
return $text;
}
if ($file_type == DifferentialChangeType::FILE_DIRECTORY) {
return $text;
}
$href = $this->getDiffusionRequest()->generateURI(
array(
'action' => 'change',
'path' => $path,
'commit' => $commit_identifier,
));
return phutil_render_tag(
'a',
array(
'href' => $href,
),
$text);
}
final public function linkHistory($path) {
$href = $this->getDiffusionRequest()->generateURI(
array(
'action' => 'history',
'path' => $path,
));
return phutil_render_tag(
'a',
array(
'href' => $href,
),
'History');
}
final public function linkBrowse($path, array $details = array()) {
$href = $this->getDiffusionRequest()->generateURI(
$details + array(
'action' => 'browse',
'path' => $path,
));
if (isset($details['html'])) {
$text = $details['html'];
} else if (isset($details['text'])) {
$text = phutil_escape_html($details['text']);
} else {
$text = 'Browse';
}
return phutil_render_tag(
'a',
array(
'href' => $href,
),
$text);
}
final public function linkExternal($hash, $uri, $text) {
$href = id(new PhutilURI('/diffusion/external/'))
->setQueryParams(
array(
'uri' => $uri,
'id' => $hash,
));
return phutil_render_tag(
'a',
array(
'href' => $href,
),
$text);
}
final public static function linkCommit($repository, $commit) {
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$commit_name = substr($commit, 0, 12);
break;
default:
$commit_name = $commit;
break;
}
$callsign = $repository->getCallsign();
$commit_name = "r{$callsign}{$commit_name}";
return phutil_render_tag(
'a',
array(
'href' => "/r{$callsign}{$commit}",
),
$commit_name);
}
final public static function linkRevision($id) {
if (!$id) {
return null;
}
return phutil_render_tag(
'a',
array(
'href' => "/D{$id}",
),
"D{$id}");
}
final protected static function renderName($name) {
$email = new PhutilEmailAddress($name);
if ($email->getDisplayName() || $email->getDomainName()) {
Javelin::initBehavior('phabricator-tooltips', array());
require_celerity_resource('aphront-tooltip-css');
return javelin_render_tag(
'span',
array(
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $email->getAddress(),
'align' => 'E',
'size' => 'auto',
),
),
phutil_escape_html($email->getDisplayName()));
}
return phutil_escape_html($name);
}
}
diff --git a/src/applications/directory/controller/PhabricatorDirectoryController.php b/src/applications/directory/controller/PhabricatorDirectoryController.php
index 41524dc44a..cde8d109cd 100644
--- a/src/applications/directory/controller/PhabricatorDirectoryController.php
+++ b/src/applications/directory/controller/PhabricatorDirectoryController.php
@@ -1,50 +1,34 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorDirectoryController extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setBaseURI('/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph("\xE2\x9A\x92");
$page->appendChild($view);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
public function buildNav() {
$user = $this->getRequest()->getUser();
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI('/'));
$nav->addLabel('Phabricator');
$nav->addFilter('home', 'Tactical Command', '/');
$nav->addFilter('jump', 'Jump Nav');
$nav->addFilter('feed', 'Feed');
$nav->addSpacer();
$nav->addFilter('applications', 'More Stuff');
return $nav;
}
}
diff --git a/src/applications/directory/controller/PhabricatorDirectoryMainController.php b/src/applications/directory/controller/PhabricatorDirectoryMainController.php
index 76b8d5d198..4ec012006d 100644
--- a/src/applications/directory/controller/PhabricatorDirectoryMainController.php
+++ b/src/applications/directory/controller/PhabricatorDirectoryMainController.php
@@ -1,686 +1,670 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorDirectoryMainController
extends PhabricatorDirectoryController {
private $filter;
private $subfilter;
public function willProcessRequest(array $data) {
$this->filter = idx($data, 'filter');
$this->subfilter = idx($data, 'subfilter');
}
public function processRequest() {
$user = $this->getRequest()->getUser();
$nav = $this->buildNav();
$this->filter = $nav->selectFilter($this->filter, 'home');
switch ($this->filter) {
case 'jump':
break;
case 'home':
case 'feed':
$project_query = new PhabricatorProjectQuery();
$project_query->setViewer($user);
$project_query->withMemberPHIDs(array($user->getPHID()));
$projects = $project_query->execute();
break;
default:
throw new Exception("Unknown filter '{$this->filter}'!");
}
switch ($this->filter) {
case 'feed':
return $this->buildFeedResponse($nav, $projects);
case 'jump':
return $this->buildJumpResponse($nav);
default:
return $this->buildMainResponse($nav, $projects);
}
}
private function buildMainResponse($nav, array $projects) {
assert_instances_of($projects, 'PhabricatorProject');
if (PhabricatorEnv::getEnvConfig('maniphest.enabled')) {
$unbreak_panel = $this->buildUnbreakNowPanel();
$triage_panel = $this->buildNeedsTriagePanel($projects);
$tasks_panel = $this->buildTasksPanel();
} else {
$unbreak_panel = null;
$triage_panel = null;
$tasks_panel = null;
}
$flagged_panel = $this->buildFlaggedPanel();
$jump_panel = $this->buildJumpPanel();
$revision_panel = $this->buildRevisionPanel();
$app_panel = $this->buildAppPanel();
$audit_panel = $this->buildAuditPanel();
$commit_panel = $this->buildCommitPanel();
$content = array(
$app_panel,
$jump_panel,
$unbreak_panel,
$triage_panel,
$revision_panel,
$tasks_panel,
$flagged_panel,
$audit_panel,
$commit_panel,
);
$nav->appendChild($content);
return $this->buildStandardPageResponse(
$nav,
array(
'title' => 'Phabricator',
));
}
private function buildJumpResponse($nav) {
$request = $this->getRequest();
$jump = $request->getStr('jump');
$response = PhabricatorJumpNavHandler::jumpPostResponse($jump);
if ($response) {
return $response;
} else if ($request->isFormPost()) {
$query = new PhabricatorSearchQuery();
$query->setQuery($jump);
$query->save();
return id(new AphrontRedirectResponse())
->setURI('/search/'.$query->getQueryKey().'/');
}
$nav->appendChild($this->buildJumpPanel($jump));
return $this->buildStandardPageResponse(
$nav,
array(
'title' => 'Jump Nav',
));
}
private function buildFeedResponse($nav, array $projects) {
assert_instances_of($projects, 'PhabricatorProject');
$subnav = new AphrontSideNavFilterView();
$subnav->setBaseURI(new PhutilURI('/feed/'));
$subnav->addFilter('all', 'All Activity', '/feed/');
$subnav->addFilter('projects', 'My Projects');
$nav->appendChild($subnav);
$filter = $subnav->selectFilter($this->subfilter, 'all');
$view = null;
switch ($filter) {
case 'all':
$view = $this->buildFeedView(array());
break;
case 'projects':
if ($projects) {
$phids = mpull($projects, 'getPHID');
$view = $this->buildFeedView($phids);
} else {
$view = new AphrontErrorView();
$view->setSeverity(AphrontErrorView::SEVERITY_NODATA);
$view->setTitle('No Projects');
$view->appendChild('You have not joined any projects.');
}
break;
}
$subnav->appendChild($view);
return $this->buildStandardPageResponse(
$nav,
array(
'title' => 'Feed',
));
}
private function buildUnbreakNowPanel() {
$user = $this->getRequest()->getUser();
$user_phid = $user->getPHID();
$task_query = new ManiphestTaskQuery();
$task_query->withStatus(ManiphestTaskQuery::STATUS_OPEN);
$task_query->withPriority(ManiphestTaskPriority::PRIORITY_UNBREAK_NOW);
$task_query->setLimit(10);
$tasks = $task_query->execute();
if (!$tasks) {
return $this->renderMiniPanel(
'No "Unbreak Now!" Tasks',
'Nothing appears to be critically broken right now.');
}
$panel = new AphrontPanelView();
$panel->setHeader('Unbreak Now!');
$panel->setCaption('Open tasks with "Unbreak Now!" priority.');
$panel->addButton(
phutil_render_tag(
'a',
array(
'href' => '/maniphest/view/all/',
'class' => 'grey button',
),
"View All Unbreak Now \xC2\xBB"));
$panel->appendChild($this->buildTaskListView($tasks));
return $panel;
}
private function buildFlaggedPanel() {
$user = $this->getRequest()->getUser();
$flag_query = id(new PhabricatorFlagQuery())
->setViewer($user)
->withOwnerPHIDs(array($user->getPHID()))
->needHandles(true)
->setLimit(10);
$flags = $flag_query->execute();
if (!$flags) {
return $this->renderMiniPanel(
'No Flags',
"You haven't flagged anything.");
}
$panel = new AphrontPanelView();
$panel->setHeader('Flagged Objects');
$panel->setCaption("Objects you've flagged.");
$flag_view = new PhabricatorFlagListView();
$flag_view->setFlags($flags);
$flag_view->setUser($user);
$panel->appendChild($flag_view);
$panel->addButton(
phutil_render_tag(
'a',
array(
'href' => '/flag/',
'class' => 'grey button',
),
"View All Flags \xC2\xBB"));
return $panel;
}
private function buildNeedsTriagePanel(array $projects) {
assert_instances_of($projects, 'PhabricatorProject');
$user = $this->getRequest()->getUser();
$user_phid = $user->getPHID();
if ($projects) {
$task_query = new ManiphestTaskQuery();
$task_query->withStatus(ManiphestTaskQuery::STATUS_OPEN);
$task_query->withPriority(ManiphestTaskPriority::PRIORITY_TRIAGE);
$task_query->withAnyProjects(mpull($projects, 'getPHID'));
$task_query->setLimit(10);
$tasks = $task_query->execute();
} else {
$tasks = array();
}
if (!$tasks) {
return $this->renderMiniPanel(
'No "Needs Triage" Tasks',
'No tasks in <a href="/project/">projects you are a member of</a> '.
'need triage.');
}
$panel = new AphrontPanelView();
$panel->setHeader('Needs Triage');
$panel->setCaption(
'Open tasks with "Needs Triage" priority in '.
'<a href="/project/">projects you are a member of</a>.');
$panel->addButton(
phutil_render_tag(
'a',
array(
// TODO: This should filter to just your projects' need-triage
// tasks?
'href' => '/maniphest/view/projecttriage/',
'class' => 'grey button',
),
"View All Triage \xC2\xBB"));
$panel->appendChild($this->buildTaskListView($tasks));
return $panel;
}
private function buildRevisionPanel() {
$user = $this->getRequest()->getUser();
$user_phid = $user->getPHID();
$revision_query = new DifferentialRevisionQuery();
$revision_query->withStatus(DifferentialRevisionQuery::STATUS_OPEN);
$revision_query->withResponsibleUsers(array($user_phid));
$revision_query->needRelationships(true);
// NOTE: We need to unlimit this query to hit the responsible user
// fast-path.
$revision_query->setLimit(null);
$revisions = $revision_query->execute();
list($active, $waiting) = DifferentialRevisionQuery::splitResponsible(
$revisions,
$user_phid);
if (!$active) {
return $this->renderMiniPanel(
'No Waiting Revisions',
'No revisions are waiting on you.');
}
$panel = new AphrontPanelView();
$panel->setHeader('Revisions Waiting on You');
$panel->setCaption('Revisions waiting for you for review or commit.');
$panel->addButton(
phutil_render_tag(
'a',
array(
'href' => '/differential/',
'class' => 'button grey',
),
"View Active Revisions \xC2\xBB"));
$revision_view = id(new DifferentialRevisionListView())
->setHighlightAge(true)
->setRevisions($active)
->setFields(DifferentialRevisionListView::getDefaultFields())
->setUser($user)
->loadAssets();
$phids = array_merge(
array($user_phid),
$revision_view->getRequiredHandlePHIDs());
$handles = $this->loadViewerHandles($phids);
$revision_view->setHandles($handles);
$panel->appendChild($revision_view);
return $panel;
}
private function buildTasksPanel() {
$user = $this->getRequest()->getUser();
$user_phid = $user->getPHID();
$task_query = new ManiphestTaskQuery();
$task_query->withStatus(ManiphestTaskQuery::STATUS_OPEN);
$task_query->setGroupBy(ManiphestTaskQuery::GROUP_PRIORITY);
$task_query->withOwners(array($user_phid));
$task_query->setLimit(10);
$tasks = $task_query->execute();
if (!$tasks) {
return $this->renderMiniPanel(
'No Assigned Tasks',
'You have no assigned tasks.');
}
$panel = new AphrontPanelView();
$panel->setHeader('Assigned Tasks');
$panel->setCaption('Tasks assigned to you.');
$panel->addButton(
phutil_render_tag(
'a',
array(
'href' => '/maniphest/',
'class' => 'button grey',
),
"View Active Tasks \xC2\xBB"));
$panel->appendChild($this->buildTaskListView($tasks));
return $panel;
}
private function buildTaskListView(array $tasks) {
assert_instances_of($tasks, 'ManiphestTask');
$user = $this->getRequest()->getUser();
$phids = array_merge(
array_filter(mpull($tasks, 'getOwnerPHID')),
array_mergev(mpull($tasks, 'getProjectPHIDs')));
$handles = $this->loadViewerHandles($phids);
$view = new ManiphestTaskListView();
$view->setTasks($tasks);
$view->setUser($user);
$view->setHandles($handles);
return $view;
}
private function buildFeedView(array $phids) {
$request = $this->getRequest();
$user = $request->getUser();
$user_phid = $user->getPHID();
$feed_query = new PhabricatorFeedQuery();
$feed_query->setViewer($user);
if ($phids) {
$feed_query->setFilterPHIDs($phids);
}
$pager = new AphrontCursorPagerView();
$pager->readFromRequest($request);
$pager->setPageSize(200);
$feed = $feed_query->executeWithCursorPager($pager);
$builder = new PhabricatorFeedBuilder($feed);
$builder->setUser($user);
$feed_view = $builder->buildView();
return
'<div style="padding: 1em 3em;">'.
'<div style="margin: 0 1em;">'.
'<h1 style="font-size: 18px; '.
'border-bottom: 1px solid #aaaaaa; '.
'padding: 0;">Feed</h1>'.
'</div>'.
$feed_view->render().
'<div class="phabricator-feed-frame">'.
$pager->render().
'</div>'.
'</div>';
}
private function buildJumpPanel($query=null) {
$request = $this->getRequest();
$user = $request->getUser();
$uniq_id = celerity_generate_unique_node_id();
Javelin::initBehavior(
'phabricator-autofocus',
array(
'id' => $uniq_id,
));
require_celerity_resource('phabricator-jump-nav');
$doc_href = PhabricatorEnv::getDocLink('article/Jump_Nav_User_Guide.html');
$doc_link = phutil_render_tag(
'a',
array(
'href' => $doc_href,
),
'Jump Nav User Guide');
$jump_input = phutil_render_tag(
'input',
array(
'type' => 'text',
'class' => 'phabricator-jump-nav',
'name' => 'jump',
'id' => $uniq_id,
'value' => $query,
));
$jump_caption = phutil_render_tag(
'p',
array(
'class' => 'phabricator-jump-nav-caption',
),
'Enter the name of an object like <tt>D123</tt> to quickly jump to '.
'it. See '.$doc_link.' or type <tt>help</tt>.');
$panel = new AphrontPanelView();
$panel->addClass('aphront-unpadded-panel-view');
$panel->appendChild(
phabricator_render_form(
$user,
array(
'action' => '/jump/',
'method' => 'POST',
'class' => 'phabricator-jump-nav-form',
),
$jump_input.
$jump_caption));
return $panel;
}
private function buildAppPanel() {
require_celerity_resource('phabricator-app-buttons-css');
$nav_buttons = array();
$nav_buttons[] = array(
'Differential',
'/differential/',
'differential',
'Code Reviews');
if (PhabricatorEnv::getEnvConfig('maniphest.enabled')) {
$nav_buttons[] = array(
'Maniphest',
'/maniphest/',
'maniphest',
'Tasks');
$nav_buttons[] = array(
'Create Task',
'/maniphest/task/create/',
'create-task');
}
$nav_buttons[] = array(
'Upload File',
'/file/',
'upload-file',
'Share Files');
$nav_buttons[] = array(
'Create Paste',
'/paste/',
'create-paste',
'Share Text');
if (PhabricatorEnv::getEnvConfig('phriction.enabled')) {
$nav_buttons[] = array(
'Phriction',
'/w/',
'phriction',
'Browse Wiki');
}
$nav_buttons[] = array(
'Diffusion',
'/diffusion/',
'diffusion',
'Browse Code');
$nav_buttons[] = array(
'Audit',
'/audit/',
'audit',
'Audit Code');
$view = new AphrontNullView();
$view->appendChild('<div class="phabricator-app-buttons">');
foreach ($nav_buttons as $info) {
// Subtitle is optional.
list($name, $uri, $icon, $subtitle) = array_merge($info, array(null));
if ($subtitle) {
$subtitle =
'<div class="phabricator-app-subtitle">'.
phutil_escape_html($subtitle).
'</div>';
}
$button = phutil_render_tag(
'a',
array(
'href' => $uri,
'class' => 'app-button icon-'.$icon,
),
phutil_render_tag(
'div',
array(
'class' => 'app-icon icon-'.$icon,
),
''));
$caption = phutil_render_tag(
'a',
array(
'href' => $uri,
'class' => 'phabricator-button-caption',
),
phutil_escape_html($name).$subtitle);
$view->appendChild(
'<div class="phabricator-app-button">'.
$button.
$caption.
'</div>');
}
$view->appendChild('<div style="clear: both;"></div></div>');
return $view;
}
private function renderMiniPanel($title, $body) {
$panel = new AphrontMiniPanelView();
$panel->appendChild(
phutil_render_tag(
'p',
array(
),
'<strong>'.$title.':</strong> '.$body));
return $panel;
}
public function buildAuditPanel() {
$request = $this->getRequest();
$user = $request->getUser();
$phids = PhabricatorAuditCommentEditor::loadAuditPHIDsForUser($user);
$query = new PhabricatorAuditQuery();
$query->withAuditorPHIDs($phids);
$query->withStatus(PhabricatorAuditQuery::STATUS_OPEN);
$query->withAwaitingUser($user);
$query->needCommitData(true);
$query->setLimit(10);
$audits = $query->execute();
$commits = $query->getCommits();
if (!$audits) {
return $this->renderMinipanel(
'No Audits',
'No commits are waiting for you to audit them.');
}
$view = new PhabricatorAuditListView();
$view->setAudits($audits);
$view->setCommits($commits);
$view->setUser($user);
$phids = $view->getRequiredHandlePHIDs();
$handles = $this->loadViewerHandles($phids);
$view->setHandles($handles);
$panel = new AphrontPanelView();
$panel->setHeader('Audits');
$panel->setCaption('Commits awaiting your audit.');
$panel->appendChild($view);
$panel->addButton(
phutil_render_tag(
'a',
array(
'href' => '/audit/',
'class' => 'button grey',
),
"View Active Audits \xC2\xBB"));
return $panel;
}
public function buildCommitPanel() {
$request = $this->getRequest();
$user = $request->getUser();
$phids = array($user->getPHID());
$query = new PhabricatorAuditCommitQuery();
$query->withAuthorPHIDs($phids);
$query->withStatus(PhabricatorAuditQuery::STATUS_OPEN);
$query->needCommitData(true);
$query->setLimit(10);
$commits = $query->execute();
if (!$commits) {
return $this->renderMinipanel(
'No Problem Commits',
'No one has raised concerns with your commits.');
}
$view = new PhabricatorAuditCommitListView();
$view->setCommits($commits);
$view->setUser($user);
$phids = $view->getRequiredHandlePHIDs();
$handles = $this->loadViewerHandles($phids);
$view->setHandles($handles);
$panel = new AphrontPanelView();
$panel->setHeader('Problem Commits');
$panel->setCaption('Commits which auditors have raised concerns about.');
$panel->appendChild($view);
$panel->addButton(
phutil_render_tag(
'a',
array(
'href' => '/audit/',
'class' => 'button grey',
),
"View Problem Commits \xC2\xBB"));
return $panel;
}
}
diff --git a/src/applications/diviner/application/PhabricatorApplicationDiviner.php b/src/applications/diviner/application/PhabricatorApplicationDiviner.php
index a15ad514a5..203d26e65f 100644
--- a/src/applications/diviner/application/PhabricatorApplicationDiviner.php
+++ b/src/applications/diviner/application/PhabricatorApplicationDiviner.php
@@ -1,73 +1,57 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationDiviner extends PhabricatorApplication {
public function getBaseURI() {
return '/diviner/';
}
public function getAutospriteName() {
return 'diviner';
}
public function getShortDescription() {
return 'Documentation';
}
public function getTitleGlyph() {
return "\xE2\x97\x89";
}
public function getRoutes() {
return array(
'/diviner/' => 'DivinerListController',
);
}
public function getApplicationGroup() {
return self::GROUP_COMMUNICATION;
}
public function buildMainMenuItems(
PhabricatorUser $user,
PhabricatorController $controller = null) {
$items = array();
$application = null;
if ($controller) {
$application = $controller->getCurrentApplication();
}
if ($application && $application->getHelpURI()) {
$class = 'main-menu-item-icon-help';
$item = new PhabricatorMainMenuIconView();
$item->setName(pht('%s Help', $application->getName()));
$item->addClass('autosprite main-menu-item-icon '.$class);
$item->setHref($application->getHelpURI());
$item->setSortOrder(0.1);
$items[] = $item;
}
return $items;
}
}
diff --git a/src/applications/diviner/controller/DivinerListController.php b/src/applications/diviner/controller/DivinerListController.php
index 32548f7e0e..7ac7aa453a 100644
--- a/src/applications/diviner/controller/DivinerListController.php
+++ b/src/applications/diviner/controller/DivinerListController.php
@@ -1,79 +1,63 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DivinerListController extends PhabricatorController {
public function processRequest() {
// TODO: Temporary implementation until Diviner is up and running inside
// Phabricator.
$links = array(
'http://www.phabricator.com/docs/phabricator/' => array(
'name' => 'Phabricator Ducks',
'flavor' => 'Oops, that should say "Docs".',
),
'http://www.phabricator.com/docs/arcanist/' => array(
'name' => 'Arcanist Docs',
'flavor' => 'Words have never been so finely crafted.',
),
'http://www.phabricator.com/docs/libphutil/' => array(
'name' => 'libphutil Docs',
'flavor' => 'Soothing prose; seductive poetry.',
),
'http://www.phabricator.com/docs/javelin/' => array(
'name' => 'Javelin Docs',
'flavor' => 'O, what noble scribe hath penned these words?',
),
);
require_celerity_resource('phabricator-directory-css');
$out = array();
foreach ($links as $href => $link) {
$name = $link['name'];
$flavor = $link['flavor'];
$link = phutil_render_tag(
'a',
array(
'href' => $href,
'target' => '_blank',
),
phutil_escape_html($name));
$out[] =
'<div class="aphront-directory-item">'.
'<h1>'.$link.'</h1>'.
'<p>'.phutil_escape_html($flavor).'</p>'.
'</div>';
}
$out =
'<div class="aphront-directory-list">'.
implode("\n", $out).
'</div>';
return $this->buildApplicationPage(
$out,
array(
'device' => true,
'title' => 'Documentation',
));
}
}
diff --git a/src/applications/draft/storage/PhabricatorDraft.php b/src/applications/draft/storage/PhabricatorDraft.php
index cce1008831..ed952c0d48 100644
--- a/src/applications/draft/storage/PhabricatorDraft.php
+++ b/src/applications/draft/storage/PhabricatorDraft.php
@@ -1,47 +1,31 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorDraft extends PhabricatorDraftDAO {
protected $authorPHID;
protected $draftKey;
protected $draft;
protected $metadata = array();
public function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'metadata' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function replaceOrDelete() {
if ($this->draft == '' && !array_filter($this->metadata)) {
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE authorPHID = %s AND draftKey = %s',
$this->getTableName(),
$this->authorPHID,
$this->draftKey);
return $this;
}
return parent::replace();
}
}
diff --git a/src/applications/draft/storage/PhabricatorDraftDAO.php b/src/applications/draft/storage/PhabricatorDraftDAO.php
index 62880e32b3..1b9f9f8dce 100644
--- a/src/applications/draft/storage/PhabricatorDraftDAO.php
+++ b/src/applications/draft/storage/PhabricatorDraftDAO.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorDraftDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'draft';
}
}
diff --git a/src/applications/drydock/application/PhabricatorApplicationDrydock.php b/src/applications/drydock/application/PhabricatorApplicationDrydock.php
index db92dcaccb..bc65e9deed 100644
--- a/src/applications/drydock/application/PhabricatorApplicationDrydock.php
+++ b/src/applications/drydock/application/PhabricatorApplicationDrydock.php
@@ -1,57 +1,41 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationDrydock extends PhabricatorApplication {
public function getBaseURI() {
return '/drydock/';
}
public function getShortDescription() {
return 'Allocate Software Resources';
}
public function getAutospriteName() {
return 'drydock';
}
public function getTitleGlyph() {
return "\xE2\x98\x82";
}
public function getFlavorText() {
return pht('A nautical adventure.');
}
public function getApplicationGroup() {
return self::GROUP_UTILITIES;
}
public function getRoutes() {
return array(
'/drydock/' => array(
'' => 'DrydockResourceListController',
'resource/' => 'DrydockResourceListController',
'resource/allocate/' => 'DrydockResourceAllocateController',
'lease/' => 'DrydockLeaseListController',
'log/' => 'DrydockLogController',
),
);
}
}
diff --git a/src/applications/drydock/blueprint/DrydockBlueprint.php b/src/applications/drydock/blueprint/DrydockBlueprint.php
index bcfef77d16..6b7deb0ed2 100644
--- a/src/applications/drydock/blueprint/DrydockBlueprint.php
+++ b/src/applications/drydock/blueprint/DrydockBlueprint.php
@@ -1,160 +1,144 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DrydockBlueprint {
private $activeLease;
private $activeResource;
abstract public function getType();
abstract public function getInterface(
DrydockResource $resource,
DrydockLease $lease,
$type);
protected function executeAcquireLease(
DrydockResource $resource,
DrydockLease $lease) {
return;
}
final public function acquireLease(
DrydockResource $resource,
DrydockLease $lease) {
$this->activeResource = $resource;
$this->activeLease = $lease;
$this->log('Acquiring Lease');
try {
$this->executeAcquireLease($resource, $lease);
} catch (Exception $ex) {
$this->logException($ex);
$this->activeResource = null;
$this->activeLease = null;
throw $ex;
}
$lease->setResourceID($resource->getID());
$lease->setStatus(DrydockLeaseStatus::STATUS_ACTIVE);
$lease->save();
$this->activeResource = null;
$this->activeLease = null;
}
protected function logException(Exception $ex) {
$this->log($ex->getMessage());
}
protected function log($message) {
self::writeLog(
$this->activeResource,
$this->activeLease,
$message);
}
public static function writeLog(
DrydockResource $resource = null,
DrydockLease $lease = null,
$message) {
$log = id(new DrydockLog())
->setEpoch(time())
->setMessage($message);
if ($resource) {
$log->setResourceID($resource->getID());
}
if ($lease) {
$log->setLeaseID($lease->getID());
}
$log->save();
}
public function canAllocateResources() {
return false;
}
protected function executeAllocateResource(DrydockLease $lease) {
throw new Exception("This blueprint can not allocate resources!");
}
final public function allocateResource(DrydockLease $lease) {
$this->activeLease = $lease;
$this->activeResource = null;
$this->log('Allocating Resource');
try {
$resource = $this->executeAllocateResource($lease);
} catch (Exception $ex) {
$this->logException($ex);
$this->activeResource = null;
throw $ex;
}
return $resource;
}
public static function getAllBlueprints() {
static $list = null;
if ($list === null) {
$blueprints = id(new PhutilSymbolLoader())
->setType('class')
->setAncestorClass('DrydockBlueprint')
->setConcreteOnly(true)
->selectAndLoadSymbols();
$list = ipull($blueprints, 'name', 'name');
foreach ($list as $class_name => $ignored) {
$list[$class_name] = newv($class_name, array());
}
}
return $list;
}
public static function getAllBlueprintsForResource($type) {
static $groups = null;
if ($groups === null) {
$groups = mgroup(self::getAllBlueprints(), 'getType');
}
return idx($groups, $type, array());
}
protected function newResourceTemplate($name) {
$resource = new DrydockResource();
$resource->setBlueprintClass(get_class($this));
$resource->setType($this->getType());
$resource->setStatus(DrydockResourceStatus::STATUS_PENDING);
$resource->setName($name);
$resource->save();
$this->activeResource = $resource;
$this->log('New Template');
return $resource;
}
}
diff --git a/src/applications/drydock/blueprint/DrydockEC2HostBlueprint.php b/src/applications/drydock/blueprint/DrydockEC2HostBlueprint.php
index 03183f1b6d..e129809efc 100644
--- a/src/applications/drydock/blueprint/DrydockEC2HostBlueprint.php
+++ b/src/applications/drydock/blueprint/DrydockEC2HostBlueprint.php
@@ -1,163 +1,147 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DrydockEC2HostBlueprint extends DrydockRemoteHostBlueprint {
public function canAllocateResources() {
return true;
}
public function executeAllocateResource(DrydockLease $lease) {
$resource = $this->newResourceTemplate('EC2 Host');
$resource->setStatus(DrydockResourceStatus::STATUS_ALLOCATING);
$resource->save();
$xml = $this->executeEC2Query(
'RunInstances',
array(
'ImageId' => 'ami-c7c99982',
'MinCount' => 1,
'MaxCount' => 1,
'KeyName' => 'ec2wc',
'SecurityGroupId.1' => 'sg-6bffff2e',
'InstanceType' => 't1.micro',
));
$instance_id = (string)$xml->instancesSet[0]->item[0]->instanceId[0];
$this->log("Started Instance: {$instance_id}");
$resource->setAttribute('instance.id', $instance_id);
$resource->save();
$n = 1;
do {
$xml = $this->executeEC2Query(
'DescribeInstances',
array(
'InstanceId.1' => $instance_id,
));
$instance = $xml->reservationSet[0]->item[0]->instancesSet[0]->item[0];
$state = (string)$instance->instanceState[0]->name;
if ($state == 'pending') {
sleep(min($n++, 15));
} else if ($state == 'running') {
break;
} else {
$this->log("EC2 host reported in unknown state '{$state}'.");
$resource->setStatus(DrydockResourceStatus::STATUS_BROKEN);
$resource->save();
}
} while (true);
$this->log('Waiting for Init');
$n = 1;
do {
$xml = $this->executeEC2Query(
'DescribeInstanceStatus',
array(
'InstanceId' => $instance_id,
));
$item = $xml->instanceStatusSet[0]->item[0];
$system_status = (string)$item->systemStatus->status[0];
$instance_status = (string)$item->instanceStatus->status[0];
if (($system_status == 'initializing') ||
($instance_status == 'initializing')) {
sleep(min($n++, 15));
} else if (($system_status == 'ok') &&
($instance_status == 'ok')) {
break;
} else {
$this->log(
"EC2 system and instance status in bad states: ".
"'{$system_status}', '{$instance_status}'.");
$resource->setStatus(DrydockResourceStatus::STATUS_BROKEN);
$resource->save();
}
} while (true);
$resource->setAttributes(
array(
'host' => (string)$instance->dnsName,
'user' => 'ec2-user',
'ssh-keyfile' => '/Users/epriestley/.ssh/id_ec2w',
));
$resource->setName($resource->getName().' ('.$instance->dnsName.')');
$resource->save();
$this->log('Waiting for SSH');
// SSH isn't immediately responsive, so wait for it to actually come up.
$cmd = $this->getInterface($resource, new DrydockLease(), 'command');
$n = 1;
do {
list($err) = $cmd->exec('true');
if ($err) {
sleep(min($n++, 15));
} else {
break;
}
} while (true);
$this->log('SSH OK');
$resource->setStatus(DrydockResourceStatus::STATUS_OPEN);
$resource->save();
return $resource;
}
public function getInterface(
DrydockResource $resource,
DrydockLease $lease,
$type) {
switch ($type) {
case 'command':
$ssh = new DrydockSSHCommandInterface();
$ssh->setConfiguration(
array(
'host' => $resource->getAttribute('host'),
'user' => $resource->getAttribute('user'),
'ssh-keyfile' => $resource->getAttribute('ssh-keyfile'),
));
return $ssh;
}
throw new Exception("No interface of type '{$type}'.");
}
private function executeEC2Query($action, array $params) {
$future = new PhutilAWSEC2Future();
$future->setAWSKeys(
PhabricatorEnv::getEnvConfig('amazon-ec2.access-key'),
PhabricatorEnv::getEnvConfig('amazon-ec2.secret-key'));
$future->setRawAWSQuery($action, $params);
return $future->resolve();
}
}
diff --git a/src/applications/drydock/blueprint/DrydockLocalHostBlueprint.php b/src/applications/drydock/blueprint/DrydockLocalHostBlueprint.php
index 8753a11477..e6153054ec 100644
--- a/src/applications/drydock/blueprint/DrydockLocalHostBlueprint.php
+++ b/src/applications/drydock/blueprint/DrydockLocalHostBlueprint.php
@@ -1,38 +1,22 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DrydockLocalHostBlueprint extends DrydockBlueprint {
public function getType() {
return 'host';
}
public function getInterface(
DrydockResource $resource,
DrydockLease $lease,
$type) {
switch ($type) {
case 'command':
return new DrydockLocalCommandInterface();
}
throw new Exception("No interface of type '{$type}'.");
}
}
diff --git a/src/applications/drydock/blueprint/DrydockRemoteHostBlueprint.php b/src/applications/drydock/blueprint/DrydockRemoteHostBlueprint.php
index 63d302da42..7756aa7781 100644
--- a/src/applications/drydock/blueprint/DrydockRemoteHostBlueprint.php
+++ b/src/applications/drydock/blueprint/DrydockRemoteHostBlueprint.php
@@ -1,48 +1,32 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* TODO: Is this concrete-extensible?
*/
class DrydockRemoteHostBlueprint extends DrydockBlueprint {
public function getType() {
return 'host';
}
public function getInterface(
DrydockResource $resource,
DrydockLease $lease,
$type) {
switch ($type) {
case 'command':
$ssh = new DrydockSSHCommandInterface();
$ssh->setConfiguration(
array(
'host' => 'secure.phabricator.com',
'user' => 'ec2-user',
'ssh-keyfile' => '/Users/epriestley/.ssh/id_ec2w',
));
return $ssh;
}
throw new Exception("No interface of type '{$type}'.");
}
}
diff --git a/src/applications/drydock/blueprint/application/DrydockPhabricatorApplicationBlueprint.php b/src/applications/drydock/blueprint/application/DrydockPhabricatorApplicationBlueprint.php
index 4d293460e2..79d829a180 100644
--- a/src/applications/drydock/blueprint/application/DrydockPhabricatorApplicationBlueprint.php
+++ b/src/applications/drydock/blueprint/application/DrydockPhabricatorApplicationBlueprint.php
@@ -1,68 +1,52 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DrydockPhabricatorApplicationBlueprint
extends DrydockBlueprint {
public function getType() {
return 'application';
}
public function canAllocateResources() {
return true;
}
public function executeAllocateResource(DrydockLease $lease) {
$resource = $this->newResourceTemplate('Phabricator');
$resource->setStatus(DrydockResourceStatus::STATUS_ALLOCATING);
$resource->save();
$host = id(new DrydockLease())
->setResourceType('host')
->queueForActivation();
$cmd = $host->waitUntilActive()->getInterface('command');
$cmd->execx(<<<EOINSTALL
yum install git &&
mkdir -p /opt/drydock &&
cd /opt/drydock &&
git clone git://github.com/facebook/libphutil.git &&
git clone git://github.com/facebook/arcanist.git &&
git clone git://github.com/facebook/phabricator.git
EOINSTALL
);
$resource->setStatus(DrydockResourceStatus::STATUS_OPEN);
$resource->save();
return $resource;
}
public function getInterface(
DrydockResource $resource,
DrydockLease $lease,
$type) {
throw new Exception("No interface of type '{$type}'.");
}
}
diff --git a/src/applications/drydock/blueprint/webroot/DrydockApacheWebrootBlueprint.php b/src/applications/drydock/blueprint/webroot/DrydockApacheWebrootBlueprint.php
index b4e5d94073..c64da4ce83 100644
--- a/src/applications/drydock/blueprint/webroot/DrydockApacheWebrootBlueprint.php
+++ b/src/applications/drydock/blueprint/webroot/DrydockApacheWebrootBlueprint.php
@@ -1,129 +1,113 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DrydockApacheWebrootBlueprint
extends DrydockBlueprint {
public function getType() {
return 'webroot';
}
public function canAllocateResources() {
return true;
}
public function executeAcquireLease(
DrydockResource $resource,
DrydockLease $lease) {
$key = Filesystem::readRandomCharacters(12);
$ports = $resource->getAttribute('ports', array());
for ($ii = 2000; ; $ii++) {
if (empty($ports[$ii])) {
$ports[$ii] = $lease->getID();
$port = $ii;
break;
}
}
$resource->setAttribute('ports', $ports);
$resource->save();
$host = $resource->getAttribute('host');
$lease->setAttribute('port', $port);
$lease->setAttribute('key', $key);
$lease->save();
$config = <<<EOCONFIG
Listen *:{$port}
<VirtualHost *:{$port}>
DocumentRoot /opt/drydock/webroot/{$key}/
ServerName {$host}
</VirtualHost>
EOCONFIG;
$cmd = $this->getInterface($resource, $lease, 'command');
$cmd->execx(<<<EOSETUP
sudo mkdir -p %s &&
sudo sh -c %s &&
sudo /etc/init.d/httpd restart
EOSETUP
,
"/opt/drydock/webroot/{$key}/",
csprintf(
'echo %s > %s',
$config,
"/etc/httpd/conf.d/drydock-{$key}.conf"));
$lease->setAttribute('uri', "http://{$host}:{$port}/");
$lease->save();
}
public function executeAllocateResource(DrydockLease $lease) {
$resource = $this->newResourceTemplate('Apache');
$resource->setStatus(DrydockResourceStatus::STATUS_ALLOCATING);
$resource->save();
$allocator = $this->getAllocator('host');
$host = $allocator->allocate();
$cmd = $host->waitUntilActive()->getInterface('command');
$cmd->execx(<<<EOINSTALL
(yes | sudo yum install httpd) && sudo mkdir -p /opt/drydock/webroot/
EOINSTALL
);
$resource->setAttribute('lease.host', $host->getID());
$resource->setAttribute('host', $host->getResource()->getAttribute('host'));
$resource->setStatus(DrydockResourceStatus::STATUS_OPEN);
$resource->save();
return $resource;
}
public function getInterface(
DrydockResource $resource,
DrydockLease $lease,
$type) {
switch ($type) {
case 'webroot':
$iface = new DrydockApacheWebrootInterface();
$iface->setConfiguration(
array(
'uri' => $lease->getAttribute('uri'),
));
return $iface;
case 'command':
$host_lease_id = $resource->getAttribute('lease.host');
$host_lease = id(new DrydockLease())->load($host_lease_id);
$host_lease->attachResource($host_lease->loadResource());
return $host_lease->getInterface($type);
}
throw new Exception("No interface of type '{$type}'.");
}
}
diff --git a/src/applications/drydock/constants/DrydockConstants.php b/src/applications/drydock/constants/DrydockConstants.php
index 17a77e732c..a0961ba880 100644
--- a/src/applications/drydock/constants/DrydockConstants.php
+++ b/src/applications/drydock/constants/DrydockConstants.php
@@ -1,21 +1,5 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DrydockConstants {
}
diff --git a/src/applications/drydock/constants/DrydockLeaseStatus.php b/src/applications/drydock/constants/DrydockLeaseStatus.php
index 7989f866f2..13bf3b6e39 100644
--- a/src/applications/drydock/constants/DrydockLeaseStatus.php
+++ b/src/applications/drydock/constants/DrydockLeaseStatus.php
@@ -1,39 +1,23 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DrydockLeaseStatus extends DrydockConstants {
const STATUS_PENDING = 0;
const STATUS_ACTIVE = 1;
const STATUS_RELEASED = 2;
const STATUS_BROKEN = 3;
const STATUS_EXPIRED = 4;
public static function getNameForStatus($status) {
static $map = array(
self::STATUS_PENDING => 'Pending',
self::STATUS_ACTIVE => 'Active',
self::STATUS_RELEASED => 'Released',
self::STATUS_BROKEN => 'Broken',
self::STATUS_EXPIRED => 'Expired',
);
return idx($map, $status, 'Unknown');
}
}
diff --git a/src/applications/drydock/constants/DrydockResourceStatus.php b/src/applications/drydock/constants/DrydockResourceStatus.php
index 4f5055921b..77d0ca8fd8 100644
--- a/src/applications/drydock/constants/DrydockResourceStatus.php
+++ b/src/applications/drydock/constants/DrydockResourceStatus.php
@@ -1,41 +1,25 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DrydockResourceStatus extends DrydockConstants {
const STATUS_PENDING = 0;
const STATUS_ALLOCATING = 1;
const STATUS_OPEN = 2;
const STATUS_CLOSED = 3;
const STATUS_BROKEN = 4;
const STATUS_DESTROYED = 5;
public static function getNameForStatus($status) {
static $map = array(
self::STATUS_PENDING => 'Pending',
self::STATUS_ALLOCATING => 'Pending',
self::STATUS_OPEN => 'Open',
self::STATUS_CLOSED => 'Closed',
self::STATUS_BROKEN => 'Broken',
self::STATUS_DESTROYED => 'Destroyed',
);
return idx($map, $status, 'Unknown');
}
}
diff --git a/src/applications/drydock/controller/DrydockController.php b/src/applications/drydock/controller/DrydockController.php
index 710d40ec83..957e438cfb 100644
--- a/src/applications/drydock/controller/DrydockController.php
+++ b/src/applications/drydock/controller/DrydockController.php
@@ -1,49 +1,33 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DrydockController extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('Drydock');
$page->setBaseURI('/drydock/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph("\xE2\x98\x82");
$page->appendChild($view);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
final protected function buildSideNav($selected) {
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI('/drydock/'));
$nav->addFilter('resource', 'Resources');
$nav->addFilter('lease', 'Leases');
$nav->addSpacer();
$nav->addFilter('log', 'Logs');
$nav->selectFilter($selected, 'resource');
return $nav;
}
}
diff --git a/src/applications/drydock/controller/DrydockLeaseListController.php b/src/applications/drydock/controller/DrydockLeaseListController.php
index 7b25570b33..19413edf0a 100644
--- a/src/applications/drydock/controller/DrydockLeaseListController.php
+++ b/src/applications/drydock/controller/DrydockLeaseListController.php
@@ -1,99 +1,83 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DrydockLeaseListController extends DrydockController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$nav = $this->buildSideNav('lease');
$pager = new AphrontPagerView();
$pager->setURI(new PhutilURI('/drydock/lease/'), 'page');
$data = id(new DrydockLease())->loadAllWhere(
'1 = 1 ORDER BY id DESC LIMIT %d, %d',
$pager->getOffset(),
$pager->getPageSize() + 1);
$data = $pager->sliceResults($data);
$phids = mpull($data, 'getOwnerPHID');
$handles = $this->loadViewerHandles($phids);
$resource_ids = mpull($data, 'getResourceID');
$resources = array();
if ($resource_ids) {
$resources = id(new DrydockResource())->loadAllWhere(
'id IN (%Ld)',
$resource_ids);
}
$rows = array();
foreach ($data as $lease) {
$resource = idx($resources, $lease->getResourceID());
$rows[] = array(
$lease->getID(),
DrydockLeaseStatus::getNameForStatus($lease->getStatus()),
($lease->getOwnerPHID()
? $handles[$lease->getOwnerPHID()]->renderLink()
: null),
$lease->getResourceID(),
($resource
? phutil_escape_html($resource->getName())
: null),
phabricator_datetime($lease->getDateCreated(), $user),
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'ID',
'Status',
'Owner',
'Resource ID',
'Resource',
'Created',
));
$table->setColumnClasses(
array(
'',
'',
'',
'',
'wide pri',
'right',
));
$panel = new AphrontPanelView();
$panel->setHeader('Drydock Leases');
$panel->appendChild($table);
$panel->appendChild($pager);
$nav->appendChild($panel);
return $this->buildStandardPageResponse(
$nav,
array(
'title' => 'Leases',
));
}
}
diff --git a/src/applications/drydock/controller/DrydockLogController.php b/src/applications/drydock/controller/DrydockLogController.php
index 0c59e7b811..d19181245d 100644
--- a/src/applications/drydock/controller/DrydockLogController.php
+++ b/src/applications/drydock/controller/DrydockLogController.php
@@ -1,87 +1,71 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DrydockLogController extends DrydockController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$nav = $this->buildSideNav('log');
$query = new DrydockLogQuery();
$resource_ids = $request->getStrList('resource');
if ($resource_ids) {
$query->withResourceIDs($resource_ids);
}
$lease_ids = $request->getStrList('lease');
if ($lease_ids) {
$query->withLeaseIDs($lease_ids);
}
$pager = new AphrontPagerView();
$pager->setPageSize(500);
$pager->setOffset($request->getInt('offset'));
$pager->setURI($request->getRequestURI(), 'offset');
$logs = $query->executeWithOffsetPager($pager);
$rows = array();
foreach ($logs as $log) {
$rows[] = array(
$log->getResourceID(),
$log->getLeaseID(),
phutil_escape_html($log->getMessage()),
phabricator_datetime($log->getEpoch(), $user),
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Resource',
'Lease',
'Message',
'Date',
));
$table->setColumnClasses(
array(
'',
'',
'wide',
'',
));
$panel = new AphrontPanelView();
$panel->setHeader('Drydock Logs');
$panel->appendChild($table);
$panel->appendChild($pager);
$nav->appendChild($panel);
return $this->buildStandardPageResponse(
$nav,
array(
'title' => 'Logs',
));
}
}
diff --git a/src/applications/drydock/controller/DrydockResourceAllocateController.php b/src/applications/drydock/controller/DrydockResourceAllocateController.php
index 2195233db0..3350873f4e 100644
--- a/src/applications/drydock/controller/DrydockResourceAllocateController.php
+++ b/src/applications/drydock/controller/DrydockResourceAllocateController.php
@@ -1,135 +1,119 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DrydockResourceAllocateController extends DrydockController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$resource = new DrydockResource();
$json = new PhutilJSON();
$err_attributes = true;
$err_capabilities = true;
$json_attributes = $json->encodeFormatted($resource->getAttributes());
$json_capabilities = $json->encodeFormatted($resource->getCapabilities());
$errors = array();
if ($request->isFormPost()) {
$raw_attributes = $request->getStr('attributes');
$attributes = json_decode($raw_attributes, true);
if (!is_array($attributes)) {
$err_attributes = 'Invalid';
$errors[] = 'Enter attributes as a valid JSON object.';
$json_attributes = $raw_attributes;
} else {
$resource->setAttributes($attributes);
$json_attributes = $json->encodeFormatted($attributes);
$err_attributes = null;
}
$raw_capabilities = $request->getStr('capabilities');
$capabilities = json_decode($raw_capabilities, true);
if (!is_array($capabilities)) {
$err_capabilities = 'Invalid';
$errors[] = 'Enter capabilities as a valid JSON object.';
$json_capabilities = $raw_capabilities;
} else {
$resource->setCapabilities($capabilities);
$json_capabilities = $json->encodeFormatted($capabilities);
$err_capabilities = null;
}
$resource->setBlueprintClass($request->getStr('blueprint'));
$resource->setType($resource->getBlueprint()->getType());
$resource->setOwnerPHID($user->getPHID());
$resource->setName($request->getStr('name'));
if (!$errors) {
$resource->save();
return id(new AphrontRedirectResponse())
->setURI('/drydock/resource/');
}
}
$error_view = null;
if ($errors) {
$error_view = new AphrontErrorView();
$error_view->setTitle('Form Errors');
$error_view->setErrors($errors);
}
$blueprints = id(new PhutilSymbolLoader())
->setType('class')
->setAncestorClass('DrydockBlueprint')
->selectAndLoadSymbols();
$blueprints = ipull($blueprints, 'name', 'name');
$panel = new AphrontPanelView();
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->setHeader('Allocate Drydock Resource');
$form = id(new AphrontFormView())
->setUser($request->getUser())
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Name')
->setName('name')
->setValue($resource->getName()))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Blueprint')
->setOptions($blueprints)
->setName('blueprint')
->setValue($resource->getBlueprintClass()))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Attributes')
->setName('attributes')
->setValue($json_attributes)
->setError($err_attributes)
->setCaption('Specify attributes in JSON.'))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Capabilities')
->setName('capabilities')
->setValue($json_capabilities)
->setError($err_capabilities)
->setCaption('Specify capabilities in JSON.'))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Allocate Resource'));
$panel->appendChild($form);
return $this->buildStandardPageResponse(
array(
$error_view,
$panel,
),
array(
'title' => 'Allocate Resource',
));
}
}
diff --git a/src/applications/drydock/controller/DrydockResourceListController.php b/src/applications/drydock/controller/DrydockResourceListController.php
index b00be65747..56f6bda7a1 100644
--- a/src/applications/drydock/controller/DrydockResourceListController.php
+++ b/src/applications/drydock/controller/DrydockResourceListController.php
@@ -1,98 +1,82 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DrydockResourceListController extends DrydockController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$nav = $this->buildSideNav('resource');
$pager = new AphrontPagerView();
$pager->setURI(new PhutilURI('/drydock/resource/'), 'page');
$data = id(new DrydockResource())->loadAllWhere(
'1 = 1 ORDER BY id DESC LIMIT %d, %d',
$pager->getOffset(),
$pager->getPageSize() + 1);
$data = $pager->sliceResults($data);
$phids = mpull($data, 'getOwnerPHID');
$handles = $this->loadViewerHandles($phids);
$rows = array();
foreach ($data as $resource) {
$rows[] = array(
$resource->getID(),
($resource->getOwnerPHID()
? $handles[$resource->getOwnerPHID()]->renderLink()
: null),
phutil_escape_html($resource->getType()),
DrydockResourceStatus::getNameForStatus($resource->getStatus()),
phutil_escape_html(nonempty($resource->getName(), 'Unnamed')),
phabricator_datetime($resource->getDateCreated(), $user),
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'ID',
'Owner',
'Type',
'Status',
'Resource',
'Created',
));
$table->setColumnClasses(
array(
'',
'',
'',
'',
'pri wide',
'right',
));
$panel = new AphrontPanelView();
$panel->setHeader('Drydock Resources');
$panel->addButton(
phutil_render_tag(
'a',
array(
'href' => '/drydock/resource/allocate/',
'class' => 'green button',
),
'Allocate Resource'));
$panel->appendChild($table);
$panel->appendChild($pager);
$nav->appendChild($panel);
return $this->buildStandardPageResponse(
$nav,
array(
'title' => 'Resources',
));
}
}
diff --git a/src/applications/drydock/interface/DrydockInterface.php b/src/applications/drydock/interface/DrydockInterface.php
index f6287fc7fa..dcf8eb5d21 100644
--- a/src/applications/drydock/interface/DrydockInterface.php
+++ b/src/applications/drydock/interface/DrydockInterface.php
@@ -1,34 +1,18 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DrydockInterface {
private $config;
abstract public function getInterfaceType();
final public function setConfiguration(array $config) {
$this->config = $config;
return $this;
}
final protected function getConfig($key, $default = null) {
return idx($this->config, $key, $default);
}
}
diff --git a/src/applications/drydock/interface/command/DrydockCommandInterface.php b/src/applications/drydock/interface/command/DrydockCommandInterface.php
index c8f0cc4b74..08e96bd017 100644
--- a/src/applications/drydock/interface/command/DrydockCommandInterface.php
+++ b/src/applications/drydock/interface/command/DrydockCommandInterface.php
@@ -1,43 +1,27 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DrydockCommandInterface extends DrydockInterface {
final public function getInterfaceType() {
return 'command';
}
final public function exec($command) {
$argv = func_get_args();
$exec = call_user_func_array(
array($this, 'getExecFuture'),
$argv);
return $exec->resolve();
}
final public function execx($command) {
$argv = func_get_args();
$exec = call_user_func_array(
array($this, 'getExecFuture'),
$argv);
return $exec->resolvex();
}
abstract public function getExecFuture($command);
}
diff --git a/src/applications/drydock/interface/command/DrydockLocalCommandInterface.php b/src/applications/drydock/interface/command/DrydockLocalCommandInterface.php
index ce73f15f54..7456a13efd 100644
--- a/src/applications/drydock/interface/command/DrydockLocalCommandInterface.php
+++ b/src/applications/drydock/interface/command/DrydockLocalCommandInterface.php
@@ -1,26 +1,10 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DrydockLocalCommandInterface extends DrydockCommandInterface {
public function getExecFuture($command) {
$argv = func_get_args();
return newv('ExecFuture', $argv);
}
}
diff --git a/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php b/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php
index db80f9e938..573a7913e3 100644
--- a/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php
+++ b/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php
@@ -1,36 +1,20 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DrydockSSHCommandInterface extends DrydockCommandInterface {
public function getExecFuture($command) {
$argv = func_get_args();
$full_command = call_user_func_array('csprintf', $argv);
// NOTE: The "-t -t" is for psuedo-tty allocation so we can "sudo" on some
// systems, but maybe more trouble than it's worth?
return new ExecFuture(
'ssh -t -t -o StrictHostKeyChecking=no -i %s %s@%s -- %s',
$this->getConfig('ssh-keyfile'),
$this->getConfig('user'),
$this->getConfig('host'),
$full_command);
}
}
diff --git a/src/applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php b/src/applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php
index c6ad522c0e..167bd94d1d 100644
--- a/src/applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php
+++ b/src/applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DrydockApacheWebrootInterface extends DrydockWebrootInterface {
public function getURI() {
return $this->getConfig('uri');
}
}
diff --git a/src/applications/drydock/interface/webroot/DrydockWebrootInterface.php b/src/applications/drydock/interface/webroot/DrydockWebrootInterface.php
index d2ad58d68a..caa3e5bb09 100644
--- a/src/applications/drydock/interface/webroot/DrydockWebrootInterface.php
+++ b/src/applications/drydock/interface/webroot/DrydockWebrootInterface.php
@@ -1,27 +1,11 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DrydockWebrootInterface extends DrydockInterface {
final public function getInterfaceType() {
return 'webroot';
}
abstract public function getURI();
}
diff --git a/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php b/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php
index 649ea87245..616239eeb7 100644
--- a/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php
+++ b/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php
@@ -1,94 +1,78 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DrydockManagementLeaseWorkflow
extends DrydockManagementWorkflow {
public function didConstruct() {
$this
->setName('lease')
->setSynopsis('Lease a resource.')
->setArguments(
array(
array(
'name' => 'type',
'param' => 'resource_type',
'help' => 'Resource type.',
),
array(
'name' => 'attributes',
'param' => 'name=value,...',
'help' => 'Resource specficiation.',
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$resource_type = $args->getArg('type');
if (!$resource_type) {
throw new PhutilArgumentUsageException(
"Specify a resource type with `--type`.");
}
$attributes = $args->getArg('attributes');
if ($attributes) {
$options = new PhutilSimpleOptions();
$attributes = $options->parse($attributes);
}
$lease = new DrydockLease();
$lease->setResourceType($resource_type);
if ($attributes) {
$lease->setAttributes($attributes);
}
$lease->queueForActivation();
$root = dirname(phutil_get_library_root('phabricator'));
$wait = new ExecFuture(
'php -f %s wait-for-lease --id %s',
$root.'/scripts/drydock/drydock_control.php',
$lease->getID());
$cursor = 0;
foreach (Futures(array($wait))->setUpdateInterval(1) as $key => $future) {
if ($future) {
$future->resolvex();
break;
}
$logs = id(new DrydockLogQuery())
->withLeaseIDs(array($lease->getID()))
->withAfterID($cursor)
->setOrder(DrydockLogQuery::ORDER_ID)
->execute();
if ($logs) {
foreach ($logs as $log) {
$console->writeErr("%s\n", $log->getMessage());
}
$cursor = max(mpull($logs, 'getID'));
}
}
$console->writeOut("Acquired Lease %s\n", $lease->getID());
return 0;
}
}
diff --git a/src/applications/drydock/management/DrydockManagementWaitForLeaseWorkflow.php b/src/applications/drydock/management/DrydockManagementWaitForLeaseWorkflow.php
index 35a94ac736..2ff52c3516 100644
--- a/src/applications/drydock/management/DrydockManagementWaitForLeaseWorkflow.php
+++ b/src/applications/drydock/management/DrydockManagementWaitForLeaseWorkflow.php
@@ -1,56 +1,40 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DrydockManagementWaitForLeaseWorkflow
extends DrydockManagementWorkflow {
public function didConstruct() {
$this
->setName('wait-for-lease')
->setSynopsis('Wait for a lease to become available.')
->setArguments(
array(
array(
'name' => 'id',
'param' => 'lease_id',
'help' => 'Lease ID to wait for.',
),
));
}
public function execute(PhutilArgumentParser $args) {
$lease_id = $args->getArg('id');
if (!$lease_id) {
throw new PhutilArgumentUsageException(
"Specify a lease ID with `--id`.");
}
$console = PhutilConsole::getConsole();
$lease = id(new DrydockLease())->load($lease_id);
if (!$lease) {
$console->writeErr("No such lease.\n");
return 1;
} else {
$lease->waitUntilActive();
$console->writeErr("Lease active.\n");
return 0;
}
}
}
diff --git a/src/applications/drydock/management/DrydockManagementWorkflow.php b/src/applications/drydock/management/DrydockManagementWorkflow.php
index d278eec960..ad2cb70fb3 100644
--- a/src/applications/drydock/management/DrydockManagementWorkflow.php
+++ b/src/applications/drydock/management/DrydockManagementWorkflow.php
@@ -1,26 +1,10 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DrydockManagementWorkflow
extends PhutilArgumentWorkflow {
public function isExecutable() {
return true;
}
}
diff --git a/src/applications/drydock/query/DrydockLogQuery.php b/src/applications/drydock/query/DrydockLogQuery.php
index f7c904dc91..49c3e5a8b5 100644
--- a/src/applications/drydock/query/DrydockLogQuery.php
+++ b/src/applications/drydock/query/DrydockLogQuery.php
@@ -1,102 +1,86 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DrydockLogQuery extends PhabricatorOffsetPagedQuery {
const ORDER_EPOCH = 'order-epoch';
const ORDER_ID = 'order-id';
private $resourceIDs;
private $leaseIDs;
private $afterID;
private $order = self::ORDER_EPOCH;
public function withResourceIDs(array $ids) {
$this->resourceIDs = $ids;
return $this;
}
public function withLeaseIDs(array $ids) {
$this->leaseIDs = $ids;
return $this;
}
public function setOrder($order) {
$this->order = $order;
return $this;
}
public function withAfterID($id) {
$this->afterID = $id;
return $this;
}
public function execute() {
$table = new DrydockLog();
$conn_r = $table->establishConnection('r');
$data = queryfx_all(
$conn_r,
'SELECT log.* FROM %T log %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
return $table->loadAllFromArray($data);
}
private function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
if ($this->resourceIDs) {
$where[] = qsprintf(
$conn_r,
'resourceID IN (%Ld)',
$this->resourceIDs);
}
if ($this->leaseIDs) {
$where[] = qsprintf(
$conn_r,
'leaseID IN (%Ld)',
$this->leaseIDs);
}
if ($this->afterID) {
$where[] = qsprintf(
$conn_r,
'id > %d',
$this->afterID);
}
return $this->formatWhereClause($where);
}
private function buildOrderClause(AphrontDatabaseConnection $conn_r) {
switch ($this->order) {
case self::ORDER_EPOCH:
return 'ORDER BY log.epoch DESC';
case self::ORDER_ID:
return 'ORDER BY id ASC';
default:
throw new Exception("Unknown order '{$this->order}'!");
}
}
}
diff --git a/src/applications/drydock/storage/DrydockDAO.php b/src/applications/drydock/storage/DrydockDAO.php
index 21cd59e577..d9de0475ae 100644
--- a/src/applications/drydock/storage/DrydockDAO.php
+++ b/src/applications/drydock/storage/DrydockDAO.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class DrydockDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'drydock';
}
}
diff --git a/src/applications/drydock/storage/DrydockLease.php b/src/applications/drydock/storage/DrydockLease.php
index b18faefe4c..a66ca13a39 100644
--- a/src/applications/drydock/storage/DrydockLease.php
+++ b/src/applications/drydock/storage/DrydockLease.php
@@ -1,167 +1,151 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DrydockLease extends DrydockDAO {
protected $phid;
protected $resourceID;
protected $resourceType;
protected $until;
protected $ownerPHID;
protected $attributes = array();
protected $status = DrydockLeaseStatus::STATUS_PENDING;
protected $taskID;
private $resource;
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'attributes' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function setAttribute($key, $value) {
$this->attributes[$key] = $value;
return $this;
}
public function getAttribute($key, $default = null) {
return idx($this->attributes, $key, $default);
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_DRYL);
}
public function getInterface($type) {
return $this->getResource()->getInterface($this, $type);
}
public function getResource() {
$this->assertActive();
if ($this->resource === null) {
throw new Exception("Resource is not yet loaded.");
}
return $this->resource;
}
public function attachResource(DrydockResource $resource) {
$this->assertActive();
$this->resource = $resource;
return $this;
}
public function loadResource() {
$this->assertActive();
return id(new DrydockResource())->loadOneWhere(
'id = %d',
$this->getResourceID());
}
public function queueForActivation() {
if ($this->getID()) {
throw new Exception(
"Only new leases may be queued for activation!");
}
$this->setStatus(DrydockLeaseStatus::STATUS_PENDING);
$this->save();
// NOTE: Prevent a race where some eager worker quickly grabs the task
// before we can save the Task ID.
$this->openTransaction();
$this->beginReadLocking();
$this->reload();
$task = PhabricatorWorker::scheduleTask(
'DrydockAllocatorWorker',
$this->getID());
$this->setTaskID($task->getID());
$this->save();
$this->endReadLocking();
$this->saveTransaction();
return $this;
}
public function release() {
$this->setStatus(DrydockLeaseStatus::STATUS_RELEASED);
$this->save();
$this->resource = null;
return $this;
}
private function assertActive() {
if ($this->status != DrydockLeaseStatus::STATUS_ACTIVE) {
throw new Exception(
"Lease is not active! You can not interact with resources through ".
"an inactive lease.");
}
}
public static function waitForLeases(array $leases) {
assert_instances_of($leases, 'DrydockLease');
$task_ids = array_filter(mpull($leases, 'getTaskID'));
PhabricatorWorker::waitForTasks($task_ids);
$unresolved = $leases;
while (true) {
foreach ($unresolved as $key => $lease) {
$lease->reload();
switch ($lease->getStatus()) {
case DrydockLeaseStatus::STATUS_ACTIVE:
unset($unresolved[$key]);
break;
case DrydockLeaseStatus::STATUS_RELEASED:
case DrydockLeaseStatus::STATUS_EXPIRED:
case DrydockLeaseStatus::STATUS_BROKEN:
throw new Exception("Lease will never become active!");
case DrydockLeaseStatus::STATUS_PENDING:
break;
}
}
if ($unresolved) {
sleep(1);
} else {
break;
}
}
foreach ($leases as $lease) {
$lease->attachResource($lease->loadResource());
}
}
public function waitUntilActive() {
self::waitForLeases(array($this));
return $this;
}
}
diff --git a/src/applications/drydock/storage/DrydockLog.php b/src/applications/drydock/storage/DrydockLog.php
index efc87f9710..a26e593300 100644
--- a/src/applications/drydock/storage/DrydockLog.php
+++ b/src/applications/drydock/storage/DrydockLog.php
@@ -1,32 +1,16 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DrydockLog extends DrydockDAO {
protected $resourceID;
protected $leaseID;
protected $epoch;
protected $message;
public function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
) + parent::getConfiguration();
}
}
diff --git a/src/applications/drydock/storage/DrydockResource.php b/src/applications/drydock/storage/DrydockResource.php
index aba549e0e1..5ca4b40125 100644
--- a/src/applications/drydock/storage/DrydockResource.php
+++ b/src/applications/drydock/storage/DrydockResource.php
@@ -1,73 +1,57 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DrydockResource extends DrydockDAO {
protected $id;
protected $phid;
protected $blueprintClass;
protected $status;
protected $type;
protected $name;
protected $attributes = array();
protected $capabilities = array();
protected $ownerPHID;
private $blueprint;
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'attributes' => self::SERIALIZATION_JSON,
'capabilities' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_DRYR);
}
public function getAttribute($key, $default = null) {
return idx($this->attributes, $key, $default);
}
public function setAttribute($key, $value) {
$this->attributes[$key] = $value;
return $this;
}
public function getCapability($key, $default = null) {
return idx($this->capbilities, $key, $default);
}
public function getInterface(DrydockLease $lease, $type) {
return $this->getBlueprint()->getInterface($this, $lease, $type);
}
public function getBlueprint() {
if (empty($this->blueprint)) {
$this->blueprint = newv($this->blueprintClass, array());
}
return $this->blueprint;
}
}
diff --git a/src/applications/drydock/worker/DrydockAllocatorWorker.php b/src/applications/drydock/worker/DrydockAllocatorWorker.php
index 46123c41cc..b49844b481 100644
--- a/src/applications/drydock/worker/DrydockAllocatorWorker.php
+++ b/src/applications/drydock/worker/DrydockAllocatorWorker.php
@@ -1,74 +1,58 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DrydockAllocatorWorker extends PhabricatorWorker {
protected function doWork() {
$lease_id = $this->getTaskData();
$lease = id(new DrydockLease())->load($lease_id);
if (!$lease) {
return;
}
$type = $lease->getResourceType();
$candidates = id(new DrydockResource())->loadAllWhere(
'type = %s AND status = %s',
$lease->getResourceType(),
DrydockResourceStatus::STATUS_OPEN);
if ($candidates) {
shuffle($candidates);
$resource = head($candidates);
} else {
$blueprints = DrydockBlueprint::getAllBlueprintsForResource($type);
foreach ($blueprints as $key => $blueprint) {
if (!$blueprint->canAllocateResources()) {
unset($blueprints[$key]);
continue;
}
}
if (!$blueprints) {
$lease->setStatus(DrydockLeaseStatus::STATUS_BROKEN);
$lease->save();
DrydockBlueprint::writeLog(
null,
$lease,
"There are no resources of type '{$type}' available, and no ".
"blueprints which can allocate new ones.");
return;
}
// TODO: Rank intelligently.
shuffle($blueprints);
$blueprint = head($blueprints);
$resource = $blueprint->allocateResource($lease);
}
$blueprint = $resource->getBlueprint();
$blueprint->acquireLease($resource, $lease);
}
}
diff --git a/src/applications/fact/application/PhabricatorApplicationFact.php b/src/applications/fact/application/PhabricatorApplicationFact.php
index 4f2fb42ea8..0bebcf33dc 100644
--- a/src/applications/fact/application/PhabricatorApplicationFact.php
+++ b/src/applications/fact/application/PhabricatorApplicationFact.php
@@ -1,46 +1,30 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationFact extends PhabricatorApplication {
public function getShortDescription() {
return 'Analyze Data';
}
public function getBaseURI() {
return '/fact/';
}
public function getAutospriteName() {
return 'fact';
}
public function getApplicationGroup() {
return self::GROUP_UTILITIES;
}
public function getRoutes() {
return array(
'/fact/' => array(
'' => 'PhabricatorFactHomeController',
'chart/' => 'PhabricatorFactChartController',
),
);
}
}
diff --git a/src/applications/fact/controller/PhabricatorFactChartController.php b/src/applications/fact/controller/PhabricatorFactChartController.php
index 302ab6afda..876866448f 100644
--- a/src/applications/fact/controller/PhabricatorFactChartController.php
+++ b/src/applications/fact/controller/PhabricatorFactChartController.php
@@ -1,106 +1,90 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFactChartController extends PhabricatorFactController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$table = new PhabricatorFactRaw();
$conn_r = $table->establishConnection('r');
$table_name = $table->getTableName();
$series = $request->getStr('y1');
$specs = PhabricatorFactSpec::newSpecsForFactTypes(
PhabricatorFactEngine::loadAllEngines(),
array($series));
$spec = idx($specs, $series);
$data = queryfx_all(
$conn_r,
'SELECT valueX, epoch FROM %T WHERE factType = %s ORDER BY epoch ASC',
$table_name,
$series);
$points = array();
$sum = 0;
foreach ($data as $key => $row) {
$sum += (int)$row['valueX'];
$points[(int)$row['epoch']] = $sum;
}
if (!$points) {
// NOTE: Raphael crashes Safari if you hand it series with no points.
throw new Exception("No data to show!");
}
// Limit amount of data passed to browser.
$count = count($points);
$limit = 2000;
if ($count > $limit) {
$i = 0;
$every = ceil($count / $limit);
foreach ($points as $epoch => $sum) {
$i++;
if ($i % $every && $i != $count) {
unset($points[$epoch]);
}
}
}
$x = array_keys($points);
$y = array_values($points);
$id = celerity_generate_unique_node_id();
$chart = phutil_render_tag(
'div',
array(
'id' => $id,
'style' => 'border: 1px solid #6f6f6f; '.
'margin: 1em 2em; '.
'background: #ffffff; '.
'height: 400px; ',
),
'');
require_celerity_resource('raphael-core');
require_celerity_resource('raphael-g');
require_celerity_resource('raphael-g-line');
Javelin::initBehavior('line-chart', array(
'hardpoint' => $id,
'x' => array($x),
'y' => array($y),
'xformat' => 'epoch',
'colors' => array('#0000ff'),
));
$panel = new AphrontPanelView();
$panel->setHeader('Count of '.$spec->getName());
$panel->appendChild($chart);
return $this->buildStandardPageResponse(
$panel,
array(
'title' => 'Chart',
));
}
}
diff --git a/src/applications/fact/controller/PhabricatorFactController.php b/src/applications/fact/controller/PhabricatorFactController.php
index 3f1cfc7e1c..18c7d8cad5 100644
--- a/src/applications/fact/controller/PhabricatorFactController.php
+++ b/src/applications/fact/controller/PhabricatorFactController.php
@@ -1,34 +1,18 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorFactController extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setBaseURI('/fact/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph("\xCE\xA3");
$page->appendChild($view);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
}
diff --git a/src/applications/fact/controller/PhabricatorFactHomeController.php b/src/applications/fact/controller/PhabricatorFactHomeController.php
index 8060977a6b..4f079392b9 100644
--- a/src/applications/fact/controller/PhabricatorFactHomeController.php
+++ b/src/applications/fact/controller/PhabricatorFactHomeController.php
@@ -1,137 +1,121 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFactHomeController extends PhabricatorFactController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
if ($request->isFormPost()) {
$uri = new PhutilURI('/fact/chart/');
$uri->setQueryParam('y1', $request->getStr('y1'));
return id(new AphrontRedirectResponse())->setURI($uri);
}
$types = array(
'+N:*',
'+N:DREV',
'updated',
);
$engines = PhabricatorFactEngine::loadAllEngines();
$specs = PhabricatorFactSpec::newSpecsForFactTypes($engines, $types);
$facts = id(new PhabricatorFactAggregate())->loadAllWhere(
'factType IN (%Ls)',
$types);
$rows = array();
foreach ($facts as $fact) {
$spec = $specs[$fact->getFactType()];
$name = $spec->getName();
$value = $spec->formatValueForDisplay($user, $fact->getValueX());
$rows[] = array(
phutil_escape_html($name),
phutil_escape_html($value),
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Fact',
'Value',
));
$table->setColumnClasses(
array(
'wide',
'n',
));
$panel = new AphrontPanelView();
$panel->setHeader('Facts!');
$panel->appendChild($table);
$chart_form = $this->buildChartForm();
return $this->buildStandardPageResponse(
array(
$chart_form,
$panel,
),
array(
'title' => 'Facts!',
));
}
private function buildChartForm() {
$request = $this->getRequest();
$user = $request->getUser();
$table = new PhabricatorFactRaw();
$conn_r = $table->establishConnection('r');
$table_name = $table->getTableName();
$facts = queryfx_all(
$conn_r,
'SELECT DISTINCT factType from %T',
$table_name);
$specs = PhabricatorFactSpec::newSpecsForFactTypes(
PhabricatorFactEngine::loadAllEngines(),
ipull($facts, 'factType'));
$options = array();
foreach ($specs as $spec) {
if ($spec->getUnit() == PhabricatorFactSpec::UNIT_COUNT) {
$options[$spec->getType()] = $spec->getName();
}
}
if (!$options) {
return id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
->setTitle(pht('No Chartable Facts'))
->appendChild(
'<p>'.pht(
'There are no facts that can be plotted yet.').'</p>');
}
$form = id(new AphrontFormView())
->setUser($user)
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Y-Axis')
->setName('y1')
->setOptions($options))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Plot Chart'));
$panel = new AphrontPanelView();
$panel->appendChild($form);
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->setHeader('Plot Chart');
return $panel;
}
}
diff --git a/src/applications/fact/daemon/PhabricatorFactDaemon.php b/src/applications/fact/daemon/PhabricatorFactDaemon.php
index e5b25f24b3..64a1c17184 100644
--- a/src/applications/fact/daemon/PhabricatorFactDaemon.php
+++ b/src/applications/fact/daemon/PhabricatorFactDaemon.php
@@ -1,221 +1,205 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFactDaemon extends PhabricatorDaemon {
private $engines;
const RAW_FACT_BUFFER_LIMIT = 128;
public function run() {
$this->setEngines(PhabricatorFactEngine::loadAllEngines());
while (true) {
$iterators = $this->getAllApplicationIterators();
foreach ($iterators as $iterator_name => $iterator) {
$this->processIteratorWithCursor($iterator_name, $iterator);
}
$this->processAggregates();
$this->log("Zzz...");
$this->sleep(60 * 5);
}
}
public static function getAllApplicationIterators() {
$apps = PhabricatorApplication::getAllInstalledApplications();
$iterators = array();
foreach ($apps as $app) {
foreach ($app->getFactObjectsForAnalysis() as $object) {
$iterator = new PhabricatorFactUpdateIterator($object);
$iterators[get_class($object)] = $iterator;
}
}
return $iterators;
}
public function processIteratorWithCursor($iterator_name, $iterator) {
$this->log("Processing cursor '{$iterator_name}'.");
$cursor = id(new PhabricatorFactCursor())->loadOneWhere(
'name = %s',
$iterator_name);
if (!$cursor) {
$cursor = new PhabricatorFactCursor();
$cursor->setName($iterator_name);
$position = null;
} else {
$position = $cursor->getPosition();
}
if ($position) {
$iterator->setPosition($position);
}
$new_cursor_position = $this->processIterator($iterator);
if ($new_cursor_position) {
$cursor->setPosition($new_cursor_position);
$cursor->save();
}
}
public function setEngines(array $engines) {
assert_instances_of($engines, 'PhabricatorFactEngine');
$this->engines = $engines;
return $this;
}
public function processIterator($iterator) {
$result = null;
$raw_facts = array();
foreach ($iterator as $key => $object) {
$phid = $object->getPHID();
$this->log("Processing {$phid}...");
$raw_facts[$phid] = $this->computeRawFacts($object);
if (count($raw_facts) > self::RAW_FACT_BUFFER_LIMIT) {
$this->updateRawFacts($raw_facts);
$raw_facts = array();
}
$result = $key;
}
if ($raw_facts) {
$this->updateRawFacts($raw_facts);
$raw_facts = array();
}
return $result;
}
public function processAggregates() {
$this->log("Processing aggregates.");
$facts = $this->computeAggregateFacts();
$this->updateAggregateFacts($facts);
}
private function computeAggregateFacts() {
$facts = array();
foreach ($this->engines as $engine) {
if (!$engine->shouldComputeAggregateFacts()) {
continue;
}
$facts[] = $engine->computeAggregateFacts();
}
return array_mergev($facts);
}
private function computeRawFacts(PhabricatorLiskDAO $object) {
$facts = array();
foreach ($this->engines as $engine) {
if (!$engine->shouldComputeRawFactsForObject($object)) {
continue;
}
$facts[] = $engine->computeRawFactsForObject($object);
}
return array_mergev($facts);
}
private function updateRawFacts(array $map) {
foreach ($map as $phid => $facts) {
assert_instances_of($facts, 'PhabricatorFactRaw');
}
$phids = array_keys($map);
if (!$phids) {
return;
}
$table = new PhabricatorFactRaw();
$conn = $table->establishConnection('w');
$table_name = $table->getTableName();
$sql = array();
foreach ($map as $phid => $facts) {
foreach ($facts as $fact) {
$sql[] = qsprintf(
$conn,
'(%s, %s, %s, %d, %d, %d)',
$fact->getFactType(),
$fact->getObjectPHID(),
$fact->getObjectA(),
$fact->getValueX(),
$fact->getValueY(),
$fact->getEpoch());
}
}
$table->openTransaction();
queryfx(
$conn,
'DELETE FROM %T WHERE objectPHID IN (%Ls)',
$table_name,
$phids);
if ($sql) {
foreach (array_chunk($sql, 256) as $chunk) {
queryfx(
$conn,
'INSERT INTO %T
(factType, objectPHID, objectA, valueX, valueY, epoch)
VALUES %Q',
$table_name,
implode(', ', $chunk));
}
}
$table->saveTransaction();
}
private function updateAggregateFacts(array $facts) {
if (!$facts) {
return;
}
$table = new PhabricatorFactAggregate();
$conn = $table->establishConnection('w');
$table_name = $table->getTableName();
$sql = array();
foreach ($facts as $fact) {
$sql[] = qsprintf(
$conn,
'(%s, %s, %d)',
$fact->getFactType(),
$fact->getObjectPHID(),
$fact->getValueX());
}
foreach (array_chunk($sql, 256) as $chunk) {
queryfx(
$conn,
'INSERT INTO %T (factType, objectPHID, valueX) VALUES %Q
ON DUPLICATE KEY UPDATE valueX = VALUES(valueX)',
$table_name,
implode(', ', $chunk));
}
}
}
diff --git a/src/applications/fact/engine/PhabricatorFactCountEngine.php b/src/applications/fact/engine/PhabricatorFactCountEngine.php
index 147f27f4fe..133e499267 100644
--- a/src/applications/fact/engine/PhabricatorFactCountEngine.php
+++ b/src/applications/fact/engine/PhabricatorFactCountEngine.php
@@ -1,102 +1,86 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Simple fact engine which counts objects.
*/
final class PhabricatorFactCountEngine extends PhabricatorFactEngine {
public function getFactSpecs(array $fact_types) {
$results = array();
foreach ($fact_types as $type) {
if (!strncmp($type, '+N:', 3)) {
if ($type == '+N:*') {
$name = 'Total Objects';
} else {
$name = 'Total Objects of type '.substr($type, 3);
}
$results[] = id(new PhabricatorFactSimpleSpec($type))
->setName($name)
->setUnit(PhabricatorFactSimpleSpec::UNIT_COUNT);
}
if (!strncmp($type, 'N:', 2)) {
if ($type == 'N:*') {
$name = 'Objects';
} else {
$name = 'Objects of type '.substr($type, 2);
}
$results[] = id(new PhabricatorFactSimpleSpec($type))
->setName($name)
->setUnit(PhabricatorFactSimpleSpec::UNIT_COUNT);
}
}
return $results;
}
public function shouldComputeRawFactsForObject(PhabricatorLiskDAO $object) {
return true;
}
public function computeRawFactsForObject(PhabricatorLiskDAO $object) {
$facts = array();
$phid = $object->getPHID();
$type = phid_get_type($phid);
foreach (array('N:*', 'N:'.$type) as $fact_type) {
$facts[] = id(new PhabricatorFactRaw())
->setFactType($fact_type)
->setObjectPHID($phid)
->setValueX(1)
->setEpoch($object->getDateCreated());
}
return $facts;
}
public function shouldComputeAggregateFacts() {
return true;
}
public function computeAggregateFacts() {
$table = new PhabricatorFactRaw();
$table_name = $table->getTableName();
$conn = $table->establishConnection('r');
$counts = queryfx_all(
$conn,
'SELECT factType, SUM(valueX) N FROM %T WHERE factType LIKE %>
GROUP BY factType',
$table_name,
'N:');
$facts = array();
foreach ($counts as $count) {
$facts[] = id(new PhabricatorFactAggregate())
->setFactType('+'.$count['factType'])
->setValueX($count['N']);
}
return $facts;
}
}
diff --git a/src/applications/fact/engine/PhabricatorFactEngine.php b/src/applications/fact/engine/PhabricatorFactEngine.php
index 7d5dec2eca..99cee6a725 100644
--- a/src/applications/fact/engine/PhabricatorFactEngine.php
+++ b/src/applications/fact/engine/PhabricatorFactEngine.php
@@ -1,55 +1,39 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorFactEngine {
final public static function loadAllEngines() {
$classes = id(new PhutilSymbolLoader())
->setAncestorClass(__CLASS__)
->setConcreteOnly(true)
->selectAndLoadSymbols();
$objects = array();
foreach ($classes as $class) {
$objects[] = newv($class['name'], array());
}
return $objects;
}
public function getFactSpecs(array $fact_types) {
return array();
}
public function shouldComputeRawFactsForObject(PhabricatorLiskDAO $object) {
return false;
}
public function computeRawFactsForObject(PhabricatorLiskDAO $object) {
return array();
}
public function shouldComputeAggregateFacts() {
return false;
}
public function computeAggregateFacts() {
return array();
}
}
diff --git a/src/applications/fact/engine/PhabricatorFactLastUpdatedEngine.php b/src/applications/fact/engine/PhabricatorFactLastUpdatedEngine.php
index 40404dd735..39e8781d51 100644
--- a/src/applications/fact/engine/PhabricatorFactLastUpdatedEngine.php
+++ b/src/applications/fact/engine/PhabricatorFactLastUpdatedEngine.php
@@ -1,50 +1,34 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Engine that records the time facts were last updated.
*/
final class PhabricatorFactLastUpdatedEngine extends PhabricatorFactEngine {
public function getFactSpecs(array $fact_types) {
$results = array();
foreach ($fact_types as $type) {
if ($type == 'updated') {
$results[] = id(new PhabricatorFactSimpleSpec($type))
->setName('Facts Last Updated')
->setUnit(PhabricatorFactSimpleSpec::UNIT_EPOCH);
}
}
return $results;
}
public function shouldComputeAggregateFacts() {
return true;
}
public function computeAggregateFacts() {
$facts = array();
$facts[] = id(new PhabricatorFactAggregate())
->setFactType('updated')
->setValueX(time());
return $facts;
}
}
diff --git a/src/applications/fact/extract/PhabricatorFactUpdateIterator.php b/src/applications/fact/extract/PhabricatorFactUpdateIterator.php
index bdea82dc4d..302026098a 100644
--- a/src/applications/fact/extract/PhabricatorFactUpdateIterator.php
+++ b/src/applications/fact/extract/PhabricatorFactUpdateIterator.php
@@ -1,112 +1,96 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Iterate over objects by update time in a stable way. This iterator only works
* for "normal" Lisk objects: objects with an autoincrement ID and a
* dateModified column.
*/
final class PhabricatorFactUpdateIterator extends PhutilBufferedIterator {
private $cursor;
private $object;
private $position;
private $ignoreUpdatesDuration = 15;
private $set;
public function __construct(LiskDAO $object) {
$this->set = new LiskDAOSet();
$this->object = $object->putInSet($this->set);
}
public function setPosition($position) {
$this->position = $position;
return $this;
}
protected function didRewind() {
$this->cursor = $this->position;
}
protected function getCursorFromObject($object) {
if ($object->hasProperty('dateModified')) {
return $object->getDateModified().':'.$object->getID();
} else {
return $object->getID();
}
}
public function key() {
return $this->getCursorFromObject($this->current());
}
protected function loadPage() {
$this->set->clearSet();
if ($this->object->hasProperty('dateModified')) {
if ($this->cursor) {
list($after_epoch, $after_id) = explode(':', $this->cursor);
} else {
$after_epoch = 0;
$after_id = 0;
}
// NOTE: We ignore recent updates because once we process an update we'll
// never process rows behind it again. We need to read only rows which
// we're sure no new rows will be inserted behind. If we read a row that
// was updated on the current second, another update later on in this
// second could affect an object with a lower ID, and we'd skip that
// update. To avoid this, just ignore any rows which have been updated in
// the last few seconds. This also reduces the amount of work we need to
// do if an object is repeatedly updated; we will just look at the end
// state without processing the intermediate states. Finally, this gives
// us reasonable protections against clock skew between the machine the
// daemon is running on and any machines performing writes.
$page = $this->object->loadAllWhere(
'((dateModified > %d) OR (dateModified = %d AND id > %d))
AND (dateModified < %d - %d)
ORDER BY dateModified ASC, id ASC LIMIT %d',
$after_epoch,
$after_epoch,
$after_id,
time(),
$this->ignoreUpdatesDuration,
$this->getPageSize());
} else {
if ($this->cursor) {
$after_id = $this->cursor;
} else {
$after_id = 0;
}
$page = $this->object->loadAllWhere(
'id > %d ORDER BY id ASC LIMIT %d',
$after_id,
$this->getPageSize());
}
if ($page) {
$this->cursor = $this->getCursorFromObject(end($page));
}
return $page;
}
}
diff --git a/src/applications/fact/management/PhabricatorFactManagementAnalyzeWorkflow.php b/src/applications/fact/management/PhabricatorFactManagementAnalyzeWorkflow.php
index 8f084583d9..2494ec4fea 100644
--- a/src/applications/fact/management/PhabricatorFactManagementAnalyzeWorkflow.php
+++ b/src/applications/fact/management/PhabricatorFactManagementAnalyzeWorkflow.php
@@ -1,84 +1,68 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFactManagementAnalyzeWorkflow
extends PhabricatorFactManagementWorkflow {
public function didConstruct() {
$this
->setName('analyze')
->setSynopsis(pht('Manually invoke fact analyzers.'))
->setArguments(
array(
array(
'name' => 'iterator',
'param' => 'name',
'repeat' => true,
'help' => 'Process only iterator __name__.',
),
array(
'name' => 'all',
'help' => 'Analyze from the beginning, ignoring cursors.',
),
array(
'name' => 'skip-aggregates',
'help' => 'Skip analysis of aggreate facts.',
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$daemon = new PhabricatorFactDaemon(array());
$daemon->setVerbose(true);
$daemon->setEngines(PhabricatorFactEngine::loadAllEngines());
$iterators = PhabricatorFactDaemon::getAllApplicationIterators();
$selected = $args->getArg('iterator');
if ($selected) {
$use = array();
foreach ($selected as $iterator_name) {
if (isset($iterators[$iterator_name])) {
$use[$iterator_name] = $iterators[$iterator_name];
} else {
$console->writeErr(
"%s\n",
pht("Iterator '%s' does not exist.", $iterator_name));
}
}
$iterators = $use;
}
foreach ($iterators as $iterator_name => $iterator) {
if ($args->getArg('all')) {
$daemon->processIterator($iterator);
} else {
$daemon->processIteratorWithCursor($iterator_name, $iterator);
}
}
if (!$args->getArg('skip-aggregates')) {
$daemon->processAggregates();
}
return 0;
}
}
diff --git a/src/applications/fact/management/PhabricatorFactManagementCursorsWorkflow.php b/src/applications/fact/management/PhabricatorFactManagementCursorsWorkflow.php
index b6c69e368d..741f8f9ca4 100644
--- a/src/applications/fact/management/PhabricatorFactManagementCursorsWorkflow.php
+++ b/src/applications/fact/management/PhabricatorFactManagementCursorsWorkflow.php
@@ -1,82 +1,66 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFactManagementCursorsWorkflow
extends PhabricatorFactManagementWorkflow {
public function didConstruct() {
$this
->setName('cursors')
->setSynopsis(pht('Show a list of fact iterators and cursors.'))
->setExamples(
"**cursors**\n".
"**cursors** --reset __cursor__")
->setArguments(
array(
array(
'name' => 'reset',
'param' => 'cursor',
'repeat' => true,
'help' => 'Reset cursor __cursor__.',
)
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$reset = $args->getArg('reset');
if ($reset) {
foreach ($reset as $name) {
$cursor = id(new PhabricatorFactCursor())->loadOneWhere(
'name = %s',
$name);
if ($cursor) {
$console->writeOut("%s\n", pht("Resetting cursor %s...", $name));
$cursor->delete();
} else {
$console->writeErr(
"%s\n",
pht("Cursor %s does not exist or is already reset.", $name));
}
}
return 0;
}
$iterator_map = PhabricatorFactDaemon::getAllApplicationIterators();
if (!$iterator_map) {
$console->writeErr("%s\n", pht("No cursors."));
return 0;
}
$cursors = id(new PhabricatorFactCursor())->loadAllWhere(
'name IN (%Ls)',
array_keys($iterator_map));
$cursors = mpull($cursors, 'getPosition', 'getName');
foreach ($iterator_map as $iterator_name => $iterator) {
$console->writeOut(
"%s (%s)\n",
$iterator_name,
idx($cursors, $iterator_name, 'start'));
}
return 0;
}
}
diff --git a/src/applications/fact/management/PhabricatorFactManagementDestroyWorkflow.php b/src/applications/fact/management/PhabricatorFactManagementDestroyWorkflow.php
index 711dd26e85..d533d9f055 100644
--- a/src/applications/fact/management/PhabricatorFactManagementDestroyWorkflow.php
+++ b/src/applications/fact/management/PhabricatorFactManagementDestroyWorkflow.php
@@ -1,59 +1,43 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFactManagementDestroyWorkflow
extends PhabricatorFactManagementWorkflow {
public function didConstruct() {
$this
->setName('destroy')
->setSynopsis(pht('Destroy all facts.'))
->setArguments(array());
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$question = pht(
'Really destroy all facts? They will need to be rebuilt through '.
'analysis, which may take some time.');
$ok = $console->confirm($question, $default = false);
if (!$ok) {
return 1;
}
$tables = array();
$tables[] = new PhabricatorFactRaw();
$tables[] = new PhabricatorFactAggregate();
foreach ($tables as $table) {
$conn = $table->establishConnection('w');
$name = $table->getTableName();
$console->writeOut("%s\n", pht("Destroying table '%s'...", $name));
queryfx(
$conn,
'TRUNCATE TABLE %T',
$name);
}
$console->writeOut("%s\n", pht('Done.'));
}
}
diff --git a/src/applications/fact/management/PhabricatorFactManagementListWorkflow.php b/src/applications/fact/management/PhabricatorFactManagementListWorkflow.php
index b98a19cbc9..a1644e0121 100644
--- a/src/applications/fact/management/PhabricatorFactManagementListWorkflow.php
+++ b/src/applications/fact/management/PhabricatorFactManagementListWorkflow.php
@@ -1,40 +1,24 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFactManagementListWorkflow
extends PhabricatorFactManagementWorkflow {
public function didConstruct() {
$this
->setName('list')
->setSynopsis(pht('Show a list of fact engines.'))
->setArguments(array());
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$engines = PhabricatorFactEngine::loadAllEngines();
foreach ($engines as $engine) {
$console->writeOut("%s\n", get_class($engine));
}
return 0;
}
}
diff --git a/src/applications/fact/management/PhabricatorFactManagementStatusWorkflow.php b/src/applications/fact/management/PhabricatorFactManagementStatusWorkflow.php
index 07a69a1bde..0f3c289cbd 100644
--- a/src/applications/fact/management/PhabricatorFactManagementStatusWorkflow.php
+++ b/src/applications/fact/management/PhabricatorFactManagementStatusWorkflow.php
@@ -1,63 +1,47 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFactManagementStatusWorkflow
extends PhabricatorFactManagementWorkflow {
public function didConstruct() {
$this
->setName('status')
->setSynopsis(pht('Show status of fact data.'))
->setArguments(array());
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$map = array(
'raw' => new PhabricatorFactRaw(),
'agg' => new PhabricatorFactAggregate(),
);
foreach ($map as $type => $table) {
$conn = $table->establishConnection('r');
$name = $table->getTableName();
$row = queryfx_one(
$conn,
'SELECT COUNT(*) N FROM %T',
$name);
$n = $row['N'];
switch ($type) {
case 'raw':
$desc = pht('There are %d raw fact(s) in storage.', $n);
break;
case 'agg':
$desc = pht('There are %d aggregate fact(s) in storage.', $n);
break;
}
$console->writeOut("%s\n", $desc);
}
return 0;
}
}
diff --git a/src/applications/fact/management/PhabricatorFactManagementWorkflow.php b/src/applications/fact/management/PhabricatorFactManagementWorkflow.php
index 5400a4ff0a..0d15d208ae 100644
--- a/src/applications/fact/management/PhabricatorFactManagementWorkflow.php
+++ b/src/applications/fact/management/PhabricatorFactManagementWorkflow.php
@@ -1,26 +1,10 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorFactManagementWorkflow
extends PhutilArgumentWorkflow {
public function isExecutable() {
return true;
}
}
diff --git a/src/applications/fact/spec/PhabricatorFactSimpleSpec.php b/src/applications/fact/spec/PhabricatorFactSimpleSpec.php
index 450e2b2740..350b6367f1 100644
--- a/src/applications/fact/spec/PhabricatorFactSimpleSpec.php
+++ b/src/applications/fact/spec/PhabricatorFactSimpleSpec.php
@@ -1,54 +1,38 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFactSimpleSpec extends PhabricatorFactSpec {
private $type;
private $name;
private $unit;
public function __construct($type) {
$this->type = $type;
}
public function getType() {
return $this->type;
}
public function setUnit($unit) {
$this->unit = $unit;
return $this;
}
public function getUnit() {
return $this->unit;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
if ($this->name !== null) {
return $this->name;
}
return parent::getName();
}
}
diff --git a/src/applications/fact/spec/PhabricatorFactSpec.php b/src/applications/fact/spec/PhabricatorFactSpec.php
index 52eb37bcea..a0af2420cb 100644
--- a/src/applications/fact/spec/PhabricatorFactSpec.php
+++ b/src/applications/fact/spec/PhabricatorFactSpec.php
@@ -1,68 +1,52 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorFactSpec {
const UNIT_COUNT = 'unit-count';
const UNIT_EPOCH = 'unit-epoch';
public static function newSpecsForFactTypes(
array $engines,
array $fact_types) {
assert_instances_of($engines, 'PhabricatorFactEngine');
$map = array();
foreach ($engines as $engine) {
$specs = $engine->getFactSpecs($fact_types);
$specs = mpull($specs, null, 'getType');
$map += $specs;
}
foreach ($fact_types as $type) {
if (empty($map[$type])) {
$map[$type] = new PhabricatorFactSimpleSpec($type);
}
}
return $map;
}
abstract public function getType();
public function getUnit() {
return null;
}
public function getName() {
$type = $this->getType();
return "Fact ({$type})";
}
public function formatValueForDisplay(PhabricatorUser $user, $value) {
$unit = $this->getUnit();
switch ($unit) {
case self::UNIT_COUNT:
return number_format($value);
case self::UNIT_EPOCH:
return phabricator_datetime($value, $user);
default:
return $value;
}
}
}
diff --git a/src/applications/fact/storage/PhabricatorFactAggregate.php b/src/applications/fact/storage/PhabricatorFactAggregate.php
index 45af1dc66e..252b8d4aca 100644
--- a/src/applications/fact/storage/PhabricatorFactAggregate.php
+++ b/src/applications/fact/storage/PhabricatorFactAggregate.php
@@ -1,26 +1,10 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFactAggregate extends PhabricatorFactDAO {
protected $id;
protected $factType;
protected $objectPHID;
protected $valueX;
}
diff --git a/src/applications/fact/storage/PhabricatorFactCursor.php b/src/applications/fact/storage/PhabricatorFactCursor.php
index 1a68c7b645..2d458b1b4d 100644
--- a/src/applications/fact/storage/PhabricatorFactCursor.php
+++ b/src/applications/fact/storage/PhabricatorFactCursor.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFactCursor extends PhabricatorFactDAO {
protected $id;
protected $name;
protected $position;
}
diff --git a/src/applications/fact/storage/PhabricatorFactDAO.php b/src/applications/fact/storage/PhabricatorFactDAO.php
index b1ec770235..458f7f477d 100644
--- a/src/applications/fact/storage/PhabricatorFactDAO.php
+++ b/src/applications/fact/storage/PhabricatorFactDAO.php
@@ -1,31 +1,15 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorFactDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'fact';
}
public function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
) + parent::getConfiguration();
}
}
diff --git a/src/applications/fact/storage/PhabricatorFactRaw.php b/src/applications/fact/storage/PhabricatorFactRaw.php
index 446d8f5afc..6a7728e1af 100644
--- a/src/applications/fact/storage/PhabricatorFactRaw.php
+++ b/src/applications/fact/storage/PhabricatorFactRaw.php
@@ -1,32 +1,16 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Raw fact about an object.
*/
final class PhabricatorFactRaw extends PhabricatorFactDAO {
protected $id;
protected $factType;
protected $objectPHID;
protected $objectA;
protected $valueX;
protected $valueY;
protected $epoch;
}
diff --git a/src/applications/feed/PhabricatorFeedQuery.php b/src/applications/feed/PhabricatorFeedQuery.php
index 6f540b6c5b..ff32886223 100644
--- a/src/applications/feed/PhabricatorFeedQuery.php
+++ b/src/applications/feed/PhabricatorFeedQuery.php
@@ -1,99 +1,83 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFeedQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $filterPHIDs;
public function setFilterPHIDs(array $phids) {
$this->filterPHIDs = $phids;
return $this;
}
public function loadPage() {
$story_table = new PhabricatorFeedStoryData();
$conn = $story_table->establishConnection('r');
$data = queryfx_all(
$conn,
'SELECT story.* FROM %T story %Q %Q %Q %Q %Q',
$story_table->getTableName(),
$this->buildJoinClause($conn),
$this->buildWhereClause($conn),
$this->buildGroupClause($conn),
$this->buildOrderClause($conn),
$this->buildLimitClause($conn));
return $data;
}
protected function willFilterPage(array $data) {
return PhabricatorFeedStory::loadAllFromRows($data, $this->getViewer());
}
private function buildJoinClause(AphrontDatabaseConnection $conn_r) {
// NOTE: We perform this join unconditionally (even if we have no filter
// PHIDs) to omit rows which have no story references. These story data
// rows are notifications or realtime alerts.
$ref_table = new PhabricatorFeedStoryReference();
return qsprintf(
$conn_r,
'JOIN %T ref ON ref.chronologicalKey = story.chronologicalKey',
$ref_table->getTableName());
}
private function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
if ($this->filterPHIDs) {
$where[] = qsprintf(
$conn_r,
'ref.objectPHID IN (%Ls)',
$this->filterPHIDs);
}
$where[] = $this->buildPagingClause($conn_r);
return $this->formatWhereClause($where);
}
private function buildGroupClause(AphrontDatabaseConnection $conn_r) {
return qsprintf(
$conn_r,
'GROUP BY '.($this->filterPHIDs
? 'ref.chronologicalKey'
: 'story.chronologicalKey'));
}
protected function getPagingColumn() {
return ($this->filterPHIDs
? 'ref.chronologicalKey'
: 'story.chronologicalKey');
}
protected function getPagingValue($item) {
if ($item instanceof PhabricatorFeedStory) {
return $item->getChronologicalKey();
}
return $item['chronologicalKey'];
}
}
diff --git a/src/applications/feed/PhabricatorFeedStoryPublisher.php b/src/applications/feed/PhabricatorFeedStoryPublisher.php
index b3172d6d47..482880c76d 100644
--- a/src/applications/feed/PhabricatorFeedStoryPublisher.php
+++ b/src/applications/feed/PhabricatorFeedStoryPublisher.php
@@ -1,235 +1,219 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFeedStoryPublisher {
private $relatedPHIDs;
private $storyType;
private $storyData;
private $storyTime;
private $storyAuthorPHID;
private $primaryObjectPHID;
private $subscribedPHIDs = array();
private $mailRecipientPHIDs = array();
public function setRelatedPHIDs(array $phids) {
$this->relatedPHIDs = $phids;
return $this;
}
public function setSubscribedPHIDs(array $phids) {
$this->subscribedPHIDs = $phids;
return $this;
}
public function setPrimaryObjectPHID($phid) {
$this->primaryObjectPHID = $phid;
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 setMailRecipientPHIDs(array $phids) {
$this->mailRecipientPHIDs = $phids;
return $this;
}
public function publish() {
$class = $this->storyType;
if (!$class) {
throw new Exception("Call setStoryType() before publishing!");
}
if (!class_exists($class)) {
throw new Exception(
"Story type must be a valid class name and must subclass ".
"PhabricatorFeedStory. ".
"'{$class}' is not a loadable class.");
}
if (!is_subclass_of($class, 'PhabricatorFeedStory')) {
throw new Exception(
"Story type must be a valid class name and must subclass ".
"PhabricatorFeedStory. ".
"'{$class}' is not a subclass of PhabricatorFeedStory.");
}
$chrono_key = $this->generateChronologicalKey();
$story = new PhabricatorFeedStoryData();
$story->setStoryType($this->storyType);
$story->setStoryData($this->storyData);
$story->setAuthorPHID((string)$this->storyAuthorPHID);
$story->setChronologicalKey($chrono_key);
$story->save();
if ($this->relatedPHIDs) {
$ref = new PhabricatorFeedStoryReference();
$sql = array();
$conn = $ref->establishConnection('w');
foreach (array_unique($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));
}
if (PhabricatorEnv::getEnvConfig('notification.enabled')) {
$this->insertNotifications($chrono_key);
$this->sendNotification($chrono_key);
}
$uris = PhabricatorEnv::getEnvConfig('feed.http-hooks', array());
foreach ($uris as $uri) {
$task = PhabricatorWorker::scheduleTask(
'FeedPublisherWorker',
array('chrono_key' => $chrono_key, 'uri' => $uri)
);
}
return $story;
}
private function insertNotifications($chrono_key) {
$subscribed_phids = $this->subscribedPHIDs;
$subscribed_phids = array_diff(
$subscribed_phids,
array($this->storyAuthorPHID));
if (!$subscribed_phids) {
return;
}
if (!$this->primaryObjectPHID) {
throw new Exception(
"You must call setPrimaryObjectPHID() if you setSubscribedPHIDs()!");
}
$notif = new PhabricatorFeedStoryNotification();
$sql = array();
$conn = $notif->establishConnection('w');
$will_receive_mail = array_fill_keys($this->mailRecipientPHIDs, true);
foreach (array_unique($subscribed_phids) as $user_phid) {
if (isset($will_receive_mail[$user_phid])) {
$mark_read = 1;
} else {
$mark_read = 0;
}
$sql[] = qsprintf(
$conn,
'(%s, %s, %s, %d)',
$this->primaryObjectPHID,
$user_phid,
$chrono_key,
$mark_read);
}
queryfx(
$conn,
'INSERT INTO %T
(primaryObjectPHID, userPHID, chronologicalKey, hasViewed)
VALUES %Q',
$notif->getTableName(),
implode(', ', $sql));
}
private function sendNotification($chrono_key) {
$server_uri = PhabricatorEnv::getEnvConfig('notification.server-uri');
$data = array(
'key' => (string)$chrono_key,
);
id(new HTTPSFuture($server_uri, $data))
->setMethod('POST')
->setTimeout(1)
->resolve();
}
/**
* 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)));
// On 32-bit machines, we have to get creative.
if (PHP_INT_SIZE < 8) {
// We're on a 32-bit machine.
if (function_exists('bcadd')) {
// Try to use the 'bc' extension.
return bcadd(bcmul($time, bcpow(2, 32)), $rand);
} else {
// Do the math in MySQL. TODO: If we formalize a bc dependency, get
// rid of this.
$conn_r = id(new PhabricatorFeedStoryData())->establishConnection('r');
$result = queryfx_one(
$conn_r,
'SELECT (%d << 32) + %d as N',
$time,
$rand);
return $result['N'];
}
} else {
// This is a 64 bit machine, so we can just do the math.
return ($time << 32) + $rand;
}
}
}
diff --git a/src/applications/feed/builder/PhabricatorFeedBuilder.php b/src/applications/feed/builder/PhabricatorFeedBuilder.php
index 1387b9feab..f6a550eaeb 100644
--- a/src/applications/feed/builder/PhabricatorFeedBuilder.php
+++ b/src/applications/feed/builder/PhabricatorFeedBuilder.php
@@ -1,84 +1,68 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFeedBuilder {
private $stories;
private $framed;
public function __construct(array $stories) {
assert_instances_of($stories, 'PhabricatorFeedStory');
$this->stories = $stories;
}
public function setFramed($framed) {
$this->framed = $framed;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function buildView() {
if (!$this->user) {
throw new Exception('Call setUser() before buildView()!');
}
$user = $this->user;
$stories = $this->stories;
$null_view = new AphrontNullView();
require_celerity_resource('phabricator-feed-css');
$last_date = null;
foreach ($stories as $story) {
$story->setFramed($this->framed);
$date = ucfirst(phabricator_relative_date($story->getEpoch(), $user));
if ($date !== $last_date) {
if ($last_date !== null) {
$null_view->appendChild(
'<div class="phabricator-feed-story-date-separator"></div>');
}
$last_date = $date;
$null_view->appendChild(
phutil_render_tag(
'div',
array(
'class' => 'phabricator-feed-story-date',
),
phutil_escape_html($date)));
}
$view = $story->renderView();
$view->setViewer($user);
$null_view->appendChild($view);
}
return id(new AphrontNullView())->appendChild(
'<div class="phabricator-feed-frame">'.
$null_view->render().
'</div>');
}
}
diff --git a/src/applications/feed/constants/PhabricatorFeedConstants.php b/src/applications/feed/constants/PhabricatorFeedConstants.php
index 79f1eab938..9895dc7b73 100644
--- a/src/applications/feed/constants/PhabricatorFeedConstants.php
+++ b/src/applications/feed/constants/PhabricatorFeedConstants.php
@@ -1,21 +1,5 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorFeedConstants {
}
diff --git a/src/applications/feed/constants/PhabricatorFeedStoryTypeConstants.php b/src/applications/feed/constants/PhabricatorFeedStoryTypeConstants.php
index 45a320fcf5..b7ec592206 100644
--- a/src/applications/feed/constants/PhabricatorFeedStoryTypeConstants.php
+++ b/src/applications/feed/constants/PhabricatorFeedStoryTypeConstants.php
@@ -1,30 +1,14 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFeedStoryTypeConstants
extends PhabricatorFeedConstants {
const STORY_STATUS = 'PhabricatorFeedStoryStatus';
const STORY_DIFFERENTIAL = 'PhabricatorFeedStoryDifferential';
const STORY_PHRICTION = 'PhabricatorFeedStoryPhriction';
const STORY_MANIPHEST = 'PhabricatorFeedStoryManiphest';
const STORY_PROJECT = 'PhabricatorFeedStoryProject';
const STORY_AUDIT = 'PhabricatorFeedStoryAudit';
const STORY_COMMIT = 'PhabricatorFeedStoryCommit';
}
diff --git a/src/applications/feed/controller/PhabricatorFeedController.php b/src/applications/feed/controller/PhabricatorFeedController.php
index 28f59c43a4..d8d7c3c094 100644
--- a/src/applications/feed/controller/PhabricatorFeedController.php
+++ b/src/applications/feed/controller/PhabricatorFeedController.php
@@ -1,41 +1,25 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorFeedController extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('Feed');
$page->setBaseURI('/feed/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph("\xE2\x88\x9E");
$page->appendChild($view);
$response = new AphrontWebpageResponse();
if (!empty($data['public'])) {
$page->setFrameable(true);
$page->setShowChrome(false);
$response->setFrameable(true);
}
return $response->setContent($page->render());
}
}
diff --git a/src/applications/feed/controller/PhabricatorFeedPublicStreamController.php b/src/applications/feed/controller/PhabricatorFeedPublicStreamController.php
index c004068a5d..4ed50d0aad 100644
--- a/src/applications/feed/controller/PhabricatorFeedPublicStreamController.php
+++ b/src/applications/feed/controller/PhabricatorFeedPublicStreamController.php
@@ -1,53 +1,37 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFeedPublicStreamController
extends PhabricatorFeedController {
public function shouldRequireLogin() {
return false;
}
public function processRequest() {
if (!PhabricatorEnv::getEnvConfig('feed.public')) {
return new Aphront404Response();
}
$request = $this->getRequest();
$viewer = $request->getUser();
$query = new PhabricatorFeedQuery();
$query->setViewer($viewer);
$query->setLimit(100);
$stories = $query->execute();
$builder = new PhabricatorFeedBuilder($stories);
$builder
->setFramed(true)
->setUser($viewer);
$view = $builder->buildView();
return $this->buildStandardPageResponse(
$view,
array(
'title' => 'Public Feed',
'public' => true,
));
}
}
diff --git a/src/applications/feed/storage/PhabricatorFeedDAO.php b/src/applications/feed/storage/PhabricatorFeedDAO.php
index 8e9fa98ae7..a60e6c4cf5 100644
--- a/src/applications/feed/storage/PhabricatorFeedDAO.php
+++ b/src/applications/feed/storage/PhabricatorFeedDAO.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorFeedDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'feed';
}
}
diff --git a/src/applications/feed/storage/PhabricatorFeedStoryData.php b/src/applications/feed/storage/PhabricatorFeedStoryData.php
index 22bafc7bc5..f0a2f0001b 100644
--- a/src/applications/feed/storage/PhabricatorFeedStoryData.php
+++ b/src/applications/feed/storage/PhabricatorFeedStoryData.php
@@ -1,70 +1,54 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFeedStoryData extends PhabricatorFeedDAO {
protected $phid;
protected $storyType;
protected $storyData;
protected $authorPHID;
protected $chronologicalKey;
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'storyData' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_STRY);
}
public function getEpoch() {
if (PHP_INT_SIZE < 8) {
// We're on a 32-bit machine.
if (function_exists('bcadd')) {
// Try to use the 'bc' extension.
return bcdiv($this->chronologicalKey, bcpow(2,32));
} else {
// Do the math in MySQL. TODO: If we formalize a bc dependency, get
// rid of this.
// See: PhabricatorFeedStoryPublisher::generateChronologicalKey()
$conn_r = id($this->establishConnection('r'));
$result = queryfx_one(
$conn_r,
// Insert the chronologicalKey as a string since longs don't seem to
// be supported by qsprintf and ints get maxed on 32 bit machines.
'SELECT (%s >> 32) as N',
$this->chronologicalKey);
return $result['N'];
}
} else {
return $this->chronologicalKey >> 32;
}
}
public function getValue($key, $default = null) {
return idx($this->storyData, $key, $default);
}
}
diff --git a/src/applications/feed/storage/PhabricatorFeedStoryReference.php b/src/applications/feed/storage/PhabricatorFeedStoryReference.php
index 298cbc8986..64e0533375 100644
--- a/src/applications/feed/storage/PhabricatorFeedStoryReference.php
+++ b/src/applications/feed/storage/PhabricatorFeedStoryReference.php
@@ -1,31 +1,15 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFeedStoryReference extends PhabricatorFeedDAO {
protected $objectPHID;
protected $chronologicalKey;
public function getConfiguration() {
return array(
self::CONFIG_IDS => self::IDS_MANUAL,
self::CONFIG_TIMESTAMPS => false,
) + parent::getConfiguration();
}
}
diff --git a/src/applications/feed/story/PhabricatorFeedStory.php b/src/applications/feed/story/PhabricatorFeedStory.php
index d9e5a92f1c..cfe0072b6e 100644
--- a/src/applications/feed/story/PhabricatorFeedStory.php
+++ b/src/applications/feed/story/PhabricatorFeedStory.php
@@ -1,328 +1,312 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Manages rendering and aggregation of a story. A story is an event (like a
* user adding a comment) which may be represented in different forms on
* different channels (like feed, notifications and realtime alerts).
*
* @task load Loading Stories
* @task policy Policy Implementation
*/
abstract class PhabricatorFeedStory implements PhabricatorPolicyInterface {
private $data;
private $hasViewed;
private $framed;
private $handles = array();
private $objects = array();
/* -( Loading Stories )---------------------------------------------------- */
/**
* Given @{class:PhabricatorFeedStoryData} rows, load them into objects and
* construct appropriate @{class:PhabricatorFeedStory} wrappers for each
* data row.
*
* @param list<dict> List of @{class:PhabricatorFeedStoryData} rows from the
* database.
* @return list<PhabricatorFeedStory> List of @{class:PhabricatorFeedStory}
* objects.
* @task load
*/
public static function loadAllFromRows(array $rows, PhabricatorUser $viewer) {
$stories = array();
$data = id(new PhabricatorFeedStoryData())->loadAllFromArray($rows);
foreach ($data as $story_data) {
$class = $story_data->getStoryType();
try {
$ok =
class_exists($class) &&
is_subclass_of($class, 'PhabricatorFeedStory');
} catch (PhutilMissingSymbolException $ex) {
$ok = false;
}
// If the story type isn't a valid class or isn't a subclass of
// PhabricatorFeedStory, decline to load it.
if (!$ok) {
continue;
}
$key = $story_data->getChronologicalKey();
$stories[$key] = newv($class, array($story_data));
}
$object_phids = array();
$key_phids = array();
foreach ($stories as $key => $story) {
$phids = array();
foreach ($story->getRequiredObjectPHIDs() as $phid) {
$phids[$phid] = true;
}
if ($story->getPrimaryObjectPHID()) {
$phids[$story->getPrimaryObjectPHID()] = true;
}
$key_phids[$key] = $phids;
$object_phids += $phids;
}
$objects = id(new PhabricatorObjectHandleData(array_keys($object_phids)))
->setViewer($viewer)
->loadObjects();
foreach ($key_phids as $key => $phids) {
if (!$phids) {
continue;
}
$story_objects = array_select_keys($objects, array_keys($phids));
if (count($story_objects) != count($phids)) {
// An object this story requires either does not exist or is not visible
// to the user. Decline to render the story.
unset($stories[$key]);
unset($key_phids[$key]);
continue;
}
$stories[$key]->setObjects($story_objects);
}
$handle_phids = array();
foreach ($stories as $key => $story) {
foreach ($story->getRequiredHandlePHIDs() as $phid) {
$key_phids[$key][$phid] = true;
}
if ($story->getAuthorPHID()) {
$key_phids[$key][$story->getAuthorPHID()] = true;
}
$handle_phids += $key_phids[$key];
}
$handles = id(new PhabricatorObjectHandleData(array_keys($handle_phids)))
->setViewer($viewer)
->loadHandles();
foreach ($key_phids as $key => $phids) {
if (!$phids) {
continue;
}
$story_handles = array_select_keys($handles, array_keys($phids));
$stories[$key]->setHandles($story_handles);
}
return $stories;
}
public function setObjects(array $objects) {
$this->objects = $objects;
return $this;
}
public function getObject($phid) {
$object = idx($this->objects, $phid);
if (!$object) {
throw new Exception(
"Story is asking for an object it did not request ('{$phid}')!");
}
return $object;
}
public function getPrimaryObject() {
$phid = $this->getPrimaryObjectPHID();
if (!$phid) {
throw new Exception("Story has no primary object!");
}
return $this->getObject($phid);
}
public function getPrimaryObjectPHID() {
return null;
}
final public function __construct(PhabricatorFeedStoryData $data) {
$this->data = $data;
}
abstract public function renderView();
// TODO: Make abstract once all subclasses implement it.
public function renderNotificationView() {
return id(new PhabricatorFeedStoryUnknown($this->data))
->renderNotificationView();
}
public function getRequiredHandlePHIDs() {
return array();
}
public function getRequiredObjectPHIDs() {
return array();
}
public function setHasViewed($has_viewed) {
$this->hasViewed = $has_viewed;
return $this;
}
public function getHasViewed() {
return $this->hasViewed;
}
final public function setFramed($framed) {
$this->framed = $framed;
return $this;
}
final public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
final protected function getObjects() {
return $this->objects;
}
final protected function getHandles() {
return $this->handles;
}
final protected function getHandle($phid) {
if (isset($this->handles[$phid])) {
if ($this->handles[$phid] instanceof PhabricatorObjectHandle) {
return $this->handles[$phid];
}
}
$handle = new PhabricatorObjectHandle();
$handle->setPHID($phid);
$handle->setName("Unloaded Object '{$phid}'");
return $handle;
}
final public function getStoryData() {
return $this->data;
}
final public function getEpoch() {
return $this->getStoryData()->getEpoch();
}
final public function getChronologicalKey() {
return $this->getStoryData()->getChronologicalKey();
}
final public function getValue($key, $default = null) {
return $this->getStoryData()->getValue($key, $default);
}
final public function getAuthorPHID() {
return $this->getStoryData()->getAuthorPHID();
}
final protected function renderHandleList(array $phids) {
$list = array();
foreach ($phids as $phid) {
$list[] = $this->linkTo($phid);
}
return implode(', ', $list);
}
final protected function linkTo($phid) {
$handle = $this->getHandle($phid);
// NOTE: We render our own link here to customize the styling and add
// the '_top' target for framed feeds.
return phutil_render_tag(
'a',
array(
'href' => $handle->getURI(),
'target' => $this->framed ? '_top' : null,
),
phutil_escape_html($handle->getLinkName()));
}
final protected function renderString($str) {
return '<strong>'.phutil_escape_html($str).'</strong>';
}
final protected function renderSummary($text, $len = 128) {
if ($len) {
$text = phutil_utf8_shorten($text, $len);
}
$text = phutil_escape_html($text);
$text = str_replace("\n", '<br />', $text);
return $text;
}
public function getNotificationAggregations() {
return array();
}
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
/**
* @task policy
*/
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
/**
* @task policy
*/
public function getPolicy($capability) {
// If this story's primary object is a policy-aware object, use its policy
// to control story visiblity.
$primary_phid = $this->getPrimaryObjectPHID();
if (isset($this->objects[$primary_phid])) {
$object = $this->objects[$primary_phid];
if ($object instanceof PhabricatorPolicyInterface) {
return $object->getPolicy($capability);
}
}
// TODO: Remove this once all objects are policy-aware. For now, keep
// respecting the `feed.public` setting.
return PhabricatorEnv::getEnvConfig('feed.public')
? PhabricatorPolicies::POLICY_PUBLIC
: PhabricatorPolicies::POLICY_USER;
}
/**
* @task policy
*/
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
}
diff --git a/src/applications/feed/story/PhabricatorFeedStoryAggregate.php b/src/applications/feed/story/PhabricatorFeedStoryAggregate.php
index d1926429da..141e6911a8 100644
--- a/src/applications/feed/story/PhabricatorFeedStoryAggregate.php
+++ b/src/applications/feed/story/PhabricatorFeedStoryAggregate.php
@@ -1,90 +1,74 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorFeedStoryAggregate extends PhabricatorFeedStory {
private $aggregateStories = array();
public function getHasViewed() {
return head($this->getAggregateStories())->getHasViewed();
}
public function getPrimaryObjectPHID() {
return head($this->getAggregateStories())->getPrimaryObjectPHID();
}
public function getRequiredHandlePHIDs() {
$phids = array();
foreach ($this->getAggregateStories() as $story) {
$phids[] = $story->getRequiredHandlePHIDs();
}
return array_mergev($phids);
}
public function getRequiredObjectPHIDs() {
$phids = array();
foreach ($this->getAggregateStories() as $story) {
$phids[] = $story->getRequiredObjectPHIDs();
}
return array_mergev($phids);
}
protected function getAuthorPHIDs() {
$authors = array();
foreach ($this->getAggregateStories() as $story) {
$authors[] = $story->getStoryData()->getAuthorPHID();
}
return array_unique(array_filter($authors));
}
protected function getDataValues($key, $default) {
$result = array();
foreach ($this->getAggregateStories() as $key => $story) {
$result[$key] = $story->getStoryData()->getValue($key, $default);
}
return $result;
}
final public function setAggregateStories(array $aggregate_stories) {
assert_instances_of($aggregate_stories, 'PhabricatorFeedStory');
$this->aggregateStories = $aggregate_stories;
$objects = array();
$handles = array();
foreach ($this->aggregateStories as $story) {
$objects += $story->getObjects();
$handles += $story->getHandles();
}
$this->setObjects($objects);
$this->setHandles($handles);
return $this;
}
final public function getAggregateStories() {
return $this->aggregateStories;
}
final public function getNotificationAggregations() {
throw new Exception(
"You can not get aggregations for an aggregate story.");
}
}
diff --git a/src/applications/feed/story/PhabricatorFeedStoryAudit.php b/src/applications/feed/story/PhabricatorFeedStoryAudit.php
index fad627bc42..c1a643d5a1 100644
--- a/src/applications/feed/story/PhabricatorFeedStoryAudit.php
+++ b/src/applications/feed/story/PhabricatorFeedStoryAudit.php
@@ -1,60 +1,44 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFeedStoryAudit extends PhabricatorFeedStory {
public function getPrimaryObjectPHID() {
return $this->getStoryData()->getValue('commitPHID');
}
public function renderView() {
$author_phid = $this->getAuthorPHID();
$commit_phid = $this->getPrimaryObjectPHID();
$view = new PhabricatorFeedStoryView();
$action = $this->getValue('action');
$verb = PhabricatorAuditActionConstants::getActionPastTenseVerb($action);
$view->setTitle(
$this->linkTo($author_phid).
" {$verb} commit ".
$this->linkTo($commit_phid).
".");
$view->setEpoch($this->getEpoch());
$comments = $this->getValue('content');
if ($comments) {
$full_size = true;
} else {
$full_size = false;
}
if ($full_size) {
$view->setImage($this->getHandle($author_phid)->getImageURI());
$content = $this->renderSummary($this->getValue('content'));
$view->appendChild($content);
} else {
$view->setOneLineStory(true);
}
return $view;
}
}
diff --git a/src/applications/feed/story/PhabricatorFeedStoryCommit.php b/src/applications/feed/story/PhabricatorFeedStoryCommit.php
index 67c340c053..67bc8fcdc7 100644
--- a/src/applications/feed/story/PhabricatorFeedStoryCommit.php
+++ b/src/applications/feed/story/PhabricatorFeedStoryCommit.php
@@ -1,76 +1,60 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFeedStoryCommit extends PhabricatorFeedStory {
public function getPrimaryObjectPHID() {
return $this->getValue('commitPHID');
}
public function getRequiredHandlePHIDs() {
return array(
$this->getValue('committerPHID'),
);
}
public function renderView() {
$data = $this->getStoryData();
$author = null;
if ($data->getValue('authorPHID')) {
$author = $this->linkTo($data->getValue('authorPHID'));
} else {
$author = phutil_escape_html($data->getValue('authorName'));
}
$committer = null;
if ($data->getValue('committerPHID')) {
$committer = $this->linkTo($data->getValue('committerPHID'));
} else if ($data->getValue('committerName')) {
$committer = phutil_escape_html($data->getValue('committerName'));
}
$commit = $this->linkTo($data->getValue('commitPHID'));
if (!$committer) {
$committer = $author;
$author = null;
}
if ($author) {
$title = "{$committer} committed {$commit} (authored by {$author})";
} else {
$title = "{$committer} committed {$commit}";
}
$view = new PhabricatorFeedStoryView();
$view->setTitle($title);
$view->setEpoch($data->getEpoch());
if ($data->getValue('authorPHID')) {
$view->setImage($this->getHandle($data->getAuthorPHID())->getImageURI());
}
$content = $this->renderSummary($data->getValue('summary'));
$view->appendChild($content);
return $view;
}
}
diff --git a/src/applications/feed/story/PhabricatorFeedStoryDifferential.php b/src/applications/feed/story/PhabricatorFeedStoryDifferential.php
index ed685fd84b..25742a8e5f 100644
--- a/src/applications/feed/story/PhabricatorFeedStoryDifferential.php
+++ b/src/applications/feed/story/PhabricatorFeedStoryDifferential.php
@@ -1,96 +1,80 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFeedStoryDifferential extends PhabricatorFeedStory {
public function getPrimaryObjectPHID() {
return $this->getValue('revision_phid');
}
public function renderView() {
$data = $this->getStoryData();
$view = new PhabricatorFeedStoryView();
$line = $this->getLineForData($data);
$view->setTitle($line);
$view->setEpoch($data->getEpoch());
$action = $data->getValue('action');
switch ($action) {
case DifferentialAction::ACTION_CREATE:
case DifferentialAction::ACTION_CLOSE:
$full_size = true;
break;
default:
$full_size = false;
break;
}
if ($full_size) {
$view->setImage($this->getHandle($data->getAuthorPHID())->getImageURI());
$content = $this->renderSummary($data->getValue('feedback_content'));
$view->appendChild($content);
} else {
$view->setOneLineStory(true);
}
return $view;
}
public function renderNotificationView() {
$data = $this->getStoryData();
$view = new PhabricatorNotificationStoryView();
$view->setTitle($this->getLineForData($data));
$view->setEpoch($data->getEpoch());
$view->setViewed($this->getHasViewed());
return $view;
}
private function getLineForData($data) {
$actor_phid = $data->getAuthorPHID();
$revision_phid = $data->getValue('revision_phid');
$action = $data->getValue('action');
$actor_link = $this->linkTo($actor_phid);
$revision_link = $this->linkTo($revision_phid);
$verb = DifferentialAction::getActionPastTenseVerb($action);
$one_line = "{$actor_link} {$verb} revision {$revision_link}";
return $one_line;
}
public function getNotificationAggregations() {
$class = get_class($this);
$phid = $this->getStoryData()->getValue('revision_phid');
$read = (int)$this->getHasViewed();
// Don't aggregate updates separated by more than 2 hours.
$block = (int)($this->getEpoch() / (60 * 60 * 2));
return array(
"{$class}:{$phid}:{$read}:{$block}"
=> 'PhabricatorFeedStoryDifferentialAggregate',
);
}
}
diff --git a/src/applications/feed/story/PhabricatorFeedStoryDifferentialAggregate.php b/src/applications/feed/story/PhabricatorFeedStoryDifferentialAggregate.php
index 51ade007b5..b84c1b7c60 100644
--- a/src/applications/feed/story/PhabricatorFeedStoryDifferentialAggregate.php
+++ b/src/applications/feed/story/PhabricatorFeedStoryDifferentialAggregate.php
@@ -1,86 +1,70 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFeedStoryDifferentialAggregate
extends PhabricatorFeedStoryAggregate {
public function renderView() {
return null;
}
public function renderNotificationView() {
$data = $this->getStoryData();
$task_link = $this->linkTo($data->getValue('revision_phid'));
$authors = $this->getAuthorPHIDs();
// TODO: These aren't really translatable because linkTo() returns a
// string, not an object with a gender.
switch (count($authors)) {
case 1:
$author = $this->linkTo(array_shift($authors));
$title = pht(
'%s made multiple updates to %s',
$author,
$task_link);
break;
case 2:
$author1 = $this->linkTo(array_shift($authors));
$author2 = $this->linkTo(array_shift($authors));
$title = pht(
'%s and %s made multiple updates to %s',
$author1,
$author2,
$task_link);
break;
case 3:
$author1 = $this->linkTo(array_shift($authors));
$author2 = $this->linkTo(array_shift($authors));
$author3 = $this->linkTo(array_shift($authors));
$title = pht(
'%s, %s, and %s made multiple updates to %s',
$author1,
$author2,
$author3,
$task_link);
break;
default:
$author1 = $this->linkTo(array_shift($authors));
$author2 = $this->linkTo(array_shift($authors));
$others = count($authors);
$title = pht(
'%s, %s, and %d others made multiple updates to %s',
$author1,
$author2,
$others,
$task_link);
break;
}
$view = new PhabricatorNotificationStoryView();
$view->setEpoch($this->getEpoch());
$view->setViewed($this->getHasViewed());
$view->setTitle($title);
return $view;
}
}
diff --git a/src/applications/feed/story/PhabricatorFeedStoryManiphest.php b/src/applications/feed/story/PhabricatorFeedStoryManiphest.php
index 73028da9eb..4f701c552d 100644
--- a/src/applications/feed/story/PhabricatorFeedStoryManiphest.php
+++ b/src/applications/feed/story/PhabricatorFeedStoryManiphest.php
@@ -1,123 +1,107 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFeedStoryManiphest
extends PhabricatorFeedStory {
public function getPrimaryObjectPHID() {
return $this->getValue('taskPHID');
}
public function getRequiredHandlePHIDs() {
return array(
$this->getValue('ownerPHID'),
);
}
public function renderView() {
$data = $this->getStoryData();
$view = new PhabricatorFeedStoryView();
$line = $this->getLineForData($data);
$view->setTitle($line);
$view->setEpoch($data->getEpoch());
$action = $data->getValue('action');
switch ($action) {
case ManiphestAction::ACTION_CREATE:
$full_size = true;
break;
default:
$full_size = false;
break;
}
if ($full_size) {
$view->setImage($this->getHandle($data->getAuthorPHID())->getImageURI());
$content = $this->renderSummary($data->getValue('description'));
$view->appendChild($content);
} else {
$view->setOneLineStory(true);
}
return $view;
}
public function renderNotificationView() {
$data = $this->getStoryData();
$view = new PhabricatorNotificationStoryView();
$view->setTitle($this->getLineForData($data));
$view->setEpoch($data->getEpoch());
$view->setViewed($this->getHasViewed());
return $view;
}
private function getLineForData($data) {
$action = $data->getValue('action');
$actor_phid = $data->getAuthorPHID();
$actor_link = $this->linkTo($actor_phid);
$task_phid = $data->getValue('taskPHID');
$task_link = $this->linkTo($task_phid);
$owner_phid = $data->getValue('ownerPHID');
$owner_link = $this->linkTo($owner_phid);
$verb = ManiphestAction::getActionPastTenseVerb($action);
switch ($action) {
case ManiphestAction::ACTION_ASSIGN:
case ManiphestAction::ACTION_REASSIGN:
if ($owner_phid) {
if ($owner_phid == $actor_phid) {
$one_line = "{$actor_link} claimed {$task_link}";
} else {
$one_line = "{$actor_link} {$verb} {$task_link} to {$owner_link}";
}
} else {
$one_line = "{$actor_link} placed {$task_link} up for grabs";
}
break;
default:
$one_line = "{$actor_link} {$verb} {$task_link}";
break;
}
return $one_line;
}
public function getNotificationAggregations() {
$class = get_class($this);
$phid = $this->getStoryData()->getValue('taskPHID');
$read = (int)$this->getHasViewed();
// Don't aggregate updates separated by more than 2 hours.
$block = (int)($this->getEpoch() / (60 * 60 * 2));
return array(
"{$class}:{$phid}:{$read}:{$block}"
=> 'PhabricatorFeedStoryManiphestAggregate',
);
}
}
diff --git a/src/applications/feed/story/PhabricatorFeedStoryManiphestAggregate.php b/src/applications/feed/story/PhabricatorFeedStoryManiphestAggregate.php
index 2d81f260fa..d09ba54c16 100644
--- a/src/applications/feed/story/PhabricatorFeedStoryManiphestAggregate.php
+++ b/src/applications/feed/story/PhabricatorFeedStoryManiphestAggregate.php
@@ -1,86 +1,70 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFeedStoryManiphestAggregate
extends PhabricatorFeedStoryAggregate {
public function renderView() {
return null;
}
public function renderNotificationView() {
$data = $this->getStoryData();
$task_link = $this->linkTo($data->getValue('taskPHID'));
$authors = $this->getAuthorPHIDs();
// TODO: These aren't really translatable because linkTo() returns a
// string, not an object with a gender.
switch (count($authors)) {
case 1:
$author = $this->linkTo(array_shift($authors));
$title = pht(
'%s made multiple updates to %s',
$author,
$task_link);
break;
case 2:
$author1 = $this->linkTo(array_shift($authors));
$author2 = $this->linkTo(array_shift($authors));
$title = pht(
'%s and %s made multiple updates to %s',
$author1,
$author2,
$task_link);
break;
case 3:
$author1 = $this->linkTo(array_shift($authors));
$author2 = $this->linkTo(array_shift($authors));
$author3 = $this->linkTo(array_shift($authors));
$title = pht(
'%s, %s, and %s made multiple updates to %s',
$author1,
$author2,
$author3,
$task_link);
break;
default:
$author1 = $this->linkTo(array_shift($authors));
$author2 = $this->linkTo(array_shift($authors));
$others = count($authors);
$title = pht(
'%s, %s, and %d others made multiple updates to %s',
$author1,
$author2,
$others,
$task_link);
break;
}
$view = new PhabricatorNotificationStoryView();
$view->setEpoch($this->getEpoch());
$view->setViewed($this->getHasViewed());
$view->setTitle($title);
return $view;
}
}
diff --git a/src/applications/feed/story/PhabricatorFeedStoryPhriction.php b/src/applications/feed/story/PhabricatorFeedStoryPhriction.php
index 96557214f0..205e198c70 100644
--- a/src/applications/feed/story/PhabricatorFeedStoryPhriction.php
+++ b/src/applications/feed/story/PhabricatorFeedStoryPhriction.php
@@ -1,63 +1,47 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFeedStoryPhriction extends PhabricatorFeedStory {
public function getPrimaryObjectPHID() {
return $this->getValue('phid');
}
public function renderView() {
$data = $this->getStoryData();
$author_phid = $data->getAuthorPHID();
$document_phid = $data->getValue('phid');
$view = new PhabricatorFeedStoryView();
$action = $data->getValue('action');
$verb = PhrictionActionConstants::getActionPastTenseVerb($action);
$view->setTitle(
$this->linkTo($author_phid).
" {$verb} the document ".
$this->linkTo($document_phid).'.');
$view->setEpoch($data->getEpoch());
$action = $data->getValue('action');
switch ($action) {
case PhrictionActionConstants::ACTION_CREATE:
$full_size = true;
break;
default:
$full_size = false;
break;
}
if ($full_size) {
$view->setImage($this->getHandle($author_phid)->getImageURI());
$content = $this->renderSummary($data->getValue('content'));
$view->appendChild($content);
} else {
$view->setOneLineStory(true);
}
return $view;
}
}
diff --git a/src/applications/feed/story/PhabricatorFeedStoryProject.php b/src/applications/feed/story/PhabricatorFeedStoryProject.php
index 65faa34007..eecf67f3e6 100644
--- a/src/applications/feed/story/PhabricatorFeedStoryProject.php
+++ b/src/applications/feed/story/PhabricatorFeedStoryProject.php
@@ -1,101 +1,85 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFeedStoryProject extends PhabricatorFeedStory {
public function getPrimaryObjectPHID() {
return $this->getValue('projectPHID');
}
public function renderView() {
$data = $this->getStoryData();
$view = new PhabricatorFeedStoryView();
$type = $data->getValue('type');
$old = $data->getValue('old');
$new = $data->getValue('new');
$proj_phid = $data->getValue('projectPHID');
$author_phid = $data->getAuthorPHID();
switch ($type) {
case PhabricatorProjectTransactionType::TYPE_NAME:
if (strlen($old)) {
$action = 'renamed project '.
$this->linkTo($proj_phid).
' from '.
$this->renderString($old).
' to '.
$this->renderString($new).
'.';
} else {
$action = 'created project '.
$this->linkTo($proj_phid).
' (as '.
$this->renderString($new).
').';
}
break;
case PhabricatorProjectTransactionType::TYPE_STATUS:
$action = 'changed project '.
$this->linkTo($proj_phid).
' status from '.
$this->renderString(
PhabricatorProjectStatus::getNameForStatus($old)).
' to '.
$this->renderString(
PhabricatorProjectStatus::getNameForStatus($new)).
'.';
break;
case PhabricatorProjectTransactionType::TYPE_MEMBERS:
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
if ((count($add) == 1) && (count($rem) == 0) &&
(head($add) == $author_phid)) {
$action = 'joined project '.$this->linkTo($proj_phid).'.';
} else if ((count($add) == 0) && (count($rem) == 1) &&
(head($rem) == $author_phid)) {
$action = 'left project '.$this->linkTo($proj_phid).'.';
} else if (empty($rem)) {
$action = 'added members to project '.
$this->linkTo($proj_phid).': '.
$this->renderHandleList($add).'.';
} else if (empty($add)) {
$action = 'removed members from project '.
$this->linkTo($proj_phid).': '.
$this->renderHandleList($rem).'.';
} else {
$action = 'changed members of project '.
$this->linkTo($proj_phid).', added: '.
$this->renderHandleList($add).'; removed: '.
$this->renderHandleList($rem).'.';
}
break;
default:
$action = 'updated project '.$this->linkTo($proj_phid).'.';
break;
}
$view->setTitle($this->linkTo($author_phid).' '.$action);
$view->setOneLineStory(true);
return $view;
}
}
diff --git a/src/applications/feed/story/PhabricatorFeedStoryStatus.php b/src/applications/feed/story/PhabricatorFeedStoryStatus.php
index eca8468960..1cd1cd8f89 100644
--- a/src/applications/feed/story/PhabricatorFeedStoryStatus.php
+++ b/src/applications/feed/story/PhabricatorFeedStoryStatus.php
@@ -1,42 +1,26 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFeedStoryStatus extends PhabricatorFeedStory {
public function getPrimaryObjectPHID() {
return $this->getAuthorPHID();
}
public function renderView() {
$data = $this->getStoryData();
$author_phid = $data->getAuthorPHID();
$view = new PhabricatorFeedStoryView();
$view->setTitle($this->linkTo($author_phid));
$view->setEpoch($data->getEpoch());
$view->setImage($this->getHandle($author_phid)->getImageURI());
$content = $this->renderSummary($data->getValue('content'), $len = null);
$view->appendChild($content);
return $view;
}
}
diff --git a/src/applications/feed/story/PhabricatorFeedStoryUnknown.php b/src/applications/feed/story/PhabricatorFeedStoryUnknown.php
index 9b0e6b12b7..afad4d8d51 100644
--- a/src/applications/feed/story/PhabricatorFeedStoryUnknown.php
+++ b/src/applications/feed/story/PhabricatorFeedStoryUnknown.php
@@ -1,54 +1,38 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFeedStoryUnknown extends PhabricatorFeedStory {
public function renderView() {
$data = $this->getStoryData();
$view = new PhabricatorFeedStoryView();
$view->setTitle('Unknown Story');
$view->setEpoch($data->getEpoch());
$view->appendChild(
'This is an unrenderable feed story of type '.
'"'.phutil_escape_html($data->getStoryType()).'".');
return $view;
}
public function renderNotificationView() {
$data = $this->getStoryData();
$view = new PhabricatorNotificationStoryView();
$view->setTitle('A wild notifcation appeared!');
$view->setEpoch($data->getEpoch());
$view->appendChild(
'This is an unrenderable feed story of type '.
'"'.phutil_escape_html($data->getStoryType()).'".');
return $view;
}
}
diff --git a/src/applications/feed/view/PhabricatorFeedStoryView.php b/src/applications/feed/view/PhabricatorFeedStoryView.php
index 65b133c34d..4e56e47852 100644
--- a/src/applications/feed/view/PhabricatorFeedStoryView.php
+++ b/src/applications/feed/view/PhabricatorFeedStoryView.php
@@ -1,106 +1,90 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFeedStoryView extends PhabricatorFeedView {
private $title;
private $image;
private $phid;
private $epoch;
private $viewer;
private $oneLine;
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function setTitle($title) {
$this->title = $title;
return $this;
}
public function setEpoch($epoch) {
$this->epoch = $epoch;
return $this;
}
public function setImage($image) {
$this->image = $image;
return $this;
}
public function setOneLineStory($one_line) {
$this->oneLine = $one_line;
return $this;
}
public function render() {
$head = phutil_render_tag(
'div',
array(
'class' => 'phabricator-feed-story-head',
),
nonempty($this->title, 'Untitled Story'));
$body = null;
$foot = null;
$image_style = null;
if (!$this->oneLine) {
$body = phutil_render_tag(
'div',
array(
'class' => 'phabricator-feed-story-body',
),
$this->renderChildren());
if ($this->epoch) {
$foot = phabricator_datetime($this->epoch, $this->viewer);
} else {
$foot = '';
}
$foot = phutil_render_tag(
'div',
array(
'class' => 'phabricator-feed-story-foot',
),
$foot);
if ($this->image) {
$image_style = 'background-image: url('.$this->image.')';
}
}
require_celerity_resource('phabricator-feed-css');
return phutil_render_tag(
'div',
array(
'class' => $this->oneLine
? 'phabricator-feed-story phabricator-feed-story-one-line'
: 'phabricator-feed-story',
'style' => $image_style,
),
$head.$body.$foot);
}
}
diff --git a/src/applications/feed/view/PhabricatorFeedView.php b/src/applications/feed/view/PhabricatorFeedView.php
index f223956abe..1a3de291d8 100644
--- a/src/applications/feed/view/PhabricatorFeedView.php
+++ b/src/applications/feed/view/PhabricatorFeedView.php
@@ -1,21 +1,5 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorFeedView extends AphrontView {
}
diff --git a/src/applications/feed/worker/FeedPublisherWorker.php b/src/applications/feed/worker/FeedPublisherWorker.php
index e33dfd68dc..e5b884d8c2 100644
--- a/src/applications/feed/worker/FeedPublisherWorker.php
+++ b/src/applications/feed/worker/FeedPublisherWorker.php
@@ -1,54 +1,38 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class FeedPublisherWorker extends PhabricatorWorker {
protected function doWork() {
$task_data = $this->getTaskData();
$chrono_key = $task_data['chrono_key'];
$uri = $task_data['uri'];
$story = id(new PhabricatorFeedStoryData())
->loadOneWhere('chronologicalKey = %s', $chrono_key);
if (!$story) {
throw new PhabricatorWorkerPermanentFailureException(
'Feed story was deleted.'
);
}
$data = array(
'storyID' => $story->getID(),
'storyType' => $story->getStoryType(),
'storyData' => $story->getStoryData(),
'storyAuthorPHID' => $story->getAuthorPHID(),
'epoch' => $story->getEpoch(),
);
id(new HTTPFuture($uri, $data))
->setMethod('POST')
->setTimeout(30)
->resolvex();
}
public function getWaitBeforeRetry(PhabricatorWorkerTask $task) {
return max($task->getFailureCount(), 1) * 60;
}
}
diff --git a/src/applications/files/PhabricatorImageTransformer.php b/src/applications/files/PhabricatorImageTransformer.php
index 0db67d3126..2bf565d43f 100644
--- a/src/applications/files/PhabricatorImageTransformer.php
+++ b/src/applications/files/PhabricatorImageTransformer.php
@@ -1,186 +1,170 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorImageTransformer {
public function executeThumbTransform(
PhabricatorFile $file,
$x,
$y) {
$image = $this->crudelyScaleTo($file, $x, $y);
return PhabricatorFile::newFromFileData(
$image,
array(
'name' => 'thumb-'.$file->getName(),
));
}
public function executeProfileTransform(
PhabricatorFile $file,
$x,
$min_y,
$max_y) {
$image = $this->crudelyCropTo($file, $x, $min_y, $max_y);
return PhabricatorFile::newFromFileData(
$image,
array(
'name' => 'profile-'.$file->getName(),
));
}
public function executePreviewTransform(
PhabricatorFile $file,
$size) {
$image = $this->generatePreview($file, $size);
return PhabricatorFile::newFromFileData(
$image,
array(
'name' => 'preview-'.$file->getName(),
));
}
private function crudelyCropTo(PhabricatorFile $file, $x, $min_y, $max_y) {
$data = $file->loadFileData();
$img = imagecreatefromstring($data);
$sx = imagesx($img);
$sy = imagesy($img);
$scaled_y = ($x / $sx) * $sy;
if ($scaled_y > $max_y) {
// This image is very tall and thin.
$scaled_y = $max_y;
} else if ($scaled_y < $min_y) {
// This image is very short and wide.
$scaled_y = $min_y;
}
$img = $this->applyScaleTo(
$img,
$x,
$scaled_y);
return $this->saveImageDataInAnyFormat($img, $file->getMimeType());
}
/**
* Very crudely scale an image up or down to an exact size.
*/
private function crudelyScaleTo(PhabricatorFile $file, $dx, $dy) {
$data = $file->loadFileData();
$src = imagecreatefromstring($data);
$dst = $this->applyScaleTo($src, $dx, $dy);
return $this->saveImageDataInAnyFormat($dst, $file->getMimeType());
}
private function applyScaleTo($src, $dx, $dy) {
$x = imagesx($src);
$y = imagesy($src);
$scale = min(($dx / $x), ($dy / $y), 1);
$dst = imagecreatetruecolor($dx, $dy);
imagesavealpha($dst, true);
imagefill($dst, 0, 0, imagecolorallocatealpha($dst, 255, 255, 255, 127));
$sdx = $scale * $x;
$sdy = $scale * $y;
imagecopyresampled(
$dst,
$src,
($dx - $sdx) / 2, ($dy - $sdy) / 2,
0, 0,
$sdx, $sdy,
$x, $y);
return $dst;
}
private function generatePreview(PhabricatorFile $file, $size) {
$data = $file->loadFileData();
$src = imagecreatefromstring($data);
$x = imagesx($src);
$y = imagesy($src);
$scale = min($size / $x, $size / $y, 1);
$dx = max($size / 4, $scale * $x);
$dy = max($size / 4, $scale * $y);
$dst = imagecreatetruecolor($dx, $dy);
imagesavealpha($dst, true);
imagefill($dst, 0, 0, imagecolorallocatealpha($dst, 255, 255, 255, 127));
$sdx = $scale * $x;
$sdy = $scale * $y;
imagecopyresampled(
$dst,
$src,
($dx - $sdx) / 2, ($dy - $sdy) / 2,
0, 0,
$sdx, $sdy,
$x, $y);
return $this->saveImageDataInAnyFormat($dst, $file->getMimeType());
}
private function saveImageDataInAnyFormat($data, $preferred_mime = '') {
switch ($preferred_mime) {
case 'image/gif': // GIF doesn't support true color.
case 'image/png':
if (function_exists('imagepng')) {
ob_start();
imagepng($data);
return ob_get_clean();
}
break;
}
$img = null;
if (function_exists('imagejpeg')) {
ob_start();
imagejpeg($data);
$img = ob_get_clean();
} else if (function_exists('imagepng')) {
ob_start();
imagepng($data);
$img = ob_get_clean();
} else if (function_exists('imagegif')) {
ob_start();
imagegif($data);
$img = ob_get_clean();
} else {
throw new Exception("No image generation functions exist!");
}
return $img;
}
}
diff --git a/src/applications/files/application/PhabricatorApplicationFiles.php b/src/applications/files/application/PhabricatorApplicationFiles.php
index cdad5b2cf0..c91e175a43 100644
--- a/src/applications/files/application/PhabricatorApplicationFiles.php
+++ b/src/applications/files/application/PhabricatorApplicationFiles.php
@@ -1,64 +1,48 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationFiles extends PhabricatorApplication {
public function getBaseURI() {
return '/file/';
}
public function getShortDescription() {
return 'Store and Share Files';
}
public function getAutospriteName() {
return 'files';
}
public function getTitleGlyph() {
return "\xE2\x87\xAA";
}
public function getFlavorText() {
return pht('Blob store for Pokemon pictures.');
}
public function getApplicationGroup() {
return self::GROUP_UTILITIES;
}
public function getRoutes() {
return array(
'/F(?P<id>[1-9]\d*)' => 'PhabricatorFileShortcutController',
'/file/' => array(
'' => 'PhabricatorFileListController',
'filter/(?P<filter>\w+)/' => 'PhabricatorFileListController',
'upload/' => 'PhabricatorFileUploadController',
'dropupload/' => 'PhabricatorFileDropUploadController',
'delete/(?P<id>[1-9]\d*)/' => 'PhabricatorFileDeleteController',
'info/(?P<phid>[^/]+)/' => 'PhabricatorFileInfoController',
'data/(?P<key>[^/]+)/(?P<phid>[^/]+)/.*'
=> 'PhabricatorFileDataController',
'proxy/' => 'PhabricatorFileProxyController',
'xform/(?P<transform>[^/]+)/(?P<phid>[^/]+)/(?P<key>[^/]+)/'
=> 'PhabricatorFileTransformController',
),
);
}
}
diff --git a/src/applications/files/controller/PhabricatorFileController.php b/src/applications/files/controller/PhabricatorFileController.php
index 258c1cd4c1..a850f8c847 100644
--- a/src/applications/files/controller/PhabricatorFileController.php
+++ b/src/applications/files/controller/PhabricatorFileController.php
@@ -1,34 +1,18 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorFileController extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('Files');
$page->setBaseURI('/file/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph("\xE2\x87\xAA");
$page->appendChild($view);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
}
diff --git a/src/applications/files/controller/PhabricatorFileDataController.php b/src/applications/files/controller/PhabricatorFileDataController.php
index dd71090f57..fb3189c72c 100644
--- a/src/applications/files/controller/PhabricatorFileDataController.php
+++ b/src/applications/files/controller/PhabricatorFileDataController.php
@@ -1,81 +1,65 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFileDataController extends PhabricatorFileController {
private $phid;
private $key;
public function willProcessRequest(array $data) {
$this->phid = $data['phid'];
$this->key = $data['key'];
}
public function shouldRequireLogin() {
return false;
}
public function processRequest() {
$request = $this->getRequest();
$alt = PhabricatorEnv::getEnvConfig('security.alternate-file-domain');
$uri = new PhutilURI($alt);
$alt_domain = $uri->getDomain();
if ($alt_domain && ($alt_domain != $request->getHost())) {
return id(new AphrontRedirectResponse())
->setURI($uri->setPath($request->getPath()));
}
$file = id(new PhabricatorFile())->loadOneWhere(
'phid = %s',
$this->phid);
if (!$file) {
return new Aphront404Response();
}
if (!$file->validateSecretKey($this->key)) {
return new Aphront403Response();
}
$data = $file->loadFileData();
$response = new AphrontFileResponse();
$response->setContent($data);
$response->setCacheDurationInSeconds(60 * 60 * 24 * 30);
$is_viewable = $file->isViewableInBrowser();
$force_download = $request->getExists('download');
if ($is_viewable && !$force_download) {
$response->setMimeType($file->getViewableMimeType());
} else {
if (!$request->isHTTPPost()) {
// NOTE: Require POST to download files. We'd rather go full-bore and
// do a real CSRF check, but can't currently authenticate users on the
// file domain. This should blunt any attacks based on iframes, script
// tags, applet tags, etc., at least. Send the user to the "info" page
// if they're using some other method.
return id(new AphrontRedirectResponse())
->setURI(PhabricatorEnv::getProductionURI($file->getBestURI()));
}
$response->setMimeType($file->getMimeType());
$response->setDownload($file->getName());
}
return $response;
}
}
diff --git a/src/applications/files/controller/PhabricatorFileDeleteController.php b/src/applications/files/controller/PhabricatorFileDeleteController.php
index 600a9382ab..127f297680 100644
--- a/src/applications/files/controller/PhabricatorFileDeleteController.php
+++ b/src/applications/files/controller/PhabricatorFileDeleteController.php
@@ -1,60 +1,44 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFileDeleteController extends PhabricatorFileController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$file = id(new PhabricatorFile())->loadOneWhere(
'id = %d',
$this->id);
if (!$file) {
return new Aphront404Response();
}
if (($user->getPHID() != $file->getAuthorPHID()) &&
(!$user->getIsAdmin())) {
return new Aphront403Response();
}
if ($request->isFormPost()) {
$file->delete();
return id(new AphrontRedirectResponse())->setURI('/file/');
}
$dialog = new AphrontDialogView();
$dialog->setUser($user);
$dialog->setTitle('Really delete file?');
$dialog->appendChild(
"<p>Permanently delete '".phutil_escape_html($file->getName())."'? This ".
"action can not be undone.");
$dialog->addSubmitButton('Delete');
$dialog->addCancelButton($file->getInfoURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/files/controller/PhabricatorFileDropUploadController.php b/src/applications/files/controller/PhabricatorFileDropUploadController.php
index 4dca060e7e..b877fdaff5 100644
--- a/src/applications/files/controller/PhabricatorFileDropUploadController.php
+++ b/src/applications/files/controller/PhabricatorFileDropUploadController.php
@@ -1,51 +1,35 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFileDropUploadController
extends PhabricatorFileController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
// NOTE: Throws if valid CSRF token is not present in the request.
$request->validateCSRF();
$data = file_get_contents('php://input');
$name = $request->getStr('name');
$file = PhabricatorFile::newFromXHRUpload(
$data,
array(
'name' => $request->getStr('name'),
'authorPHID' => $user->getPHID(),
));
$view = new AphrontAttachedFileView();
$view->setFile($file);
return id(new AphrontAjaxResponse())->setContent(
array(
'id' => $file->getID(),
'phid' => $file->getPHID(),
'html' => $view->render(),
'uri' => $file->getBestURI(),
));
}
}
diff --git a/src/applications/files/controller/PhabricatorFileInfoController.php b/src/applications/files/controller/PhabricatorFileInfoController.php
index e289f40eb0..c7c9a78461 100644
--- a/src/applications/files/controller/PhabricatorFileInfoController.php
+++ b/src/applications/files/controller/PhabricatorFileInfoController.php
@@ -1,177 +1,161 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFileInfoController extends PhabricatorFileController {
private $phid;
public function willProcessRequest(array $data) {
$this->phid = $data['phid'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$file = id(new PhabricatorFile())->loadOneWhere(
'phid = %s',
$this->phid);
if (!$file) {
return new Aphront404Response();
}
$author_child = null;
if ($file->getAuthorPHID()) {
$author = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$file->getAuthorPHID());
if ($author) {
$author_child = id(new AphrontFormStaticControl())
->setLabel('Author')
->setName('author')
->setValue($author->getUserName());
}
}
$form = new AphrontFormView();
$submit = new AphrontFormSubmitControl();
$form->setAction($file->getViewURI());
if ($file->isViewableInBrowser()) {
$submit->setValue('View File');
} else {
$submit->setValue('Download File');
}
if (($user->getPHID() == $file->getAuthorPHID()) ||
($user->getIsAdmin())) {
$submit->addCancelButton(
'/file/delete/'.$file->getID().'/',
'Delete File');
}
$file_id = 'F'.$file->getID();
$form->setUser($user);
$form
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Name')
->setName('name')
->setValue($file->getName()))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('ID')
->setName('id')
->setValue($file_id)
->setCaption(
'Download this file with: <tt>arc download '.
phutil_escape_html($file_id).'</tt>'))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('PHID')
->setName('phid')
->setValue($file->getPHID()))
->appendChild($author_child)
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Created')
->setName('created')
->setValue(phabricator_datetime($file->getDateCreated(), $user)))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Mime Type')
->setName('mime')
->setValue($file->getMimeType()))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Size')
->setName('size')
->setValue($file->getByteSize().' bytes'))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Engine')
->setName('storageEngine')
->setValue($file->getStorageEngine()))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Format')
->setName('storageFormat')
->setValue($file->getStorageFormat()))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Handle')
->setName('storageHandle')
->setValue($file->getStorageHandle()))
->appendChild(
id($submit));
$panel = new AphrontPanelView();
$panel->setHeader('File Info - '.$file->getName());
$panel->appendChild($form);
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$xform_panel = null;
$transformations = id(new PhabricatorTransformedFile())->loadAllWhere(
'originalPHID = %s',
$file->getPHID());
if ($transformations) {
$transformed_phids = mpull($transformations, 'getTransformedPHID');
$transformed_files = id(new PhabricatorFile())->loadAllWhere(
'phid in (%Ls)',
$transformed_phids);
$transformed_map = mpull($transformed_files, null, 'getPHID');
$rows = array();
foreach ($transformations as $transformed) {
$phid = $transformed->getTransformedPHID();
$rows[] = array(
phutil_escape_html($transformed->getTransform()),
phutil_render_tag(
'a',
array(
'href' => $transformed_map[$phid]->getBestURI(),
),
$phid));
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Transform',
'File',
));
$xform_panel = new AphrontPanelView();
$xform_panel->appendChild($table);
$xform_panel->setWidth(AphrontPanelView::WIDTH_FORM);
$xform_panel->setHeader('Transformations');
}
return $this->buildStandardPageResponse(
array($panel, $xform_panel),
array(
'title' => 'File Info - '.$file->getName(),
));
}
}
diff --git a/src/applications/files/controller/PhabricatorFileListController.php b/src/applications/files/controller/PhabricatorFileListController.php
index d11c7062e3..7f204020af 100644
--- a/src/applications/files/controller/PhabricatorFileListController.php
+++ b/src/applications/files/controller/PhabricatorFileListController.php
@@ -1,355 +1,339 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFileListController extends PhabricatorFileController {
private $filter;
private $showUploader;
private $useBasicUploader = false;
private $listAuthor;
private $listRows;
private $listRowClasses;
private $listHeader;
private $showListPager = true;
private $listPager;
private $pagerOffset;
private $pagerPageSize;
private function setFilter($filter) {
$this->filter = $filter;
return $this;
}
private function getFilter() {
return $this->filter;
}
private function showUploader() {
return $this->getShowUploader();
}
private function getShowUploader() {
return $this->showUploader;
}
private function setShowUploader($show_uploader) {
$this->showUploader = $show_uploader;
return $this;
}
private function useBasicUploader() {
return $this->getUseBasicUploader();
}
private function getUseBasicUploader() {
return $this->useBasicUploader;
}
private function setUseBasicUploader($use_basic_uploader) {
$this->useBasicUploader = $use_basic_uploader;
return $this;
}
private function setListAuthor(PhabricatorUser $list_author) {
$this->listAuthor = $list_author;
return $this;
}
private function getListAuthor() {
return $this->listAuthor;
}
private function getListRows() {
return $this->listRows;
}
private function setListRows($list_rows) {
$this->listRows = $list_rows;
return $this;
}
private function getListRowClasses() {
return $this->listRowClasses;
}
private function setListRowClasses($list_row_classes) {
$this->listRowClasses = $list_row_classes;
return $this;
}
private function getListHeader() {
return $this->listHeader;
}
private function setListHeader($list_header) {
$this->listHeader = $list_header;
return $this;
}
private function showListPager() {
return $this->getShowListPager();
}
private function getShowListPager() {
return $this->showListPager;
}
private function setShowListPager($show_list_pager) {
$this->showListPager = $show_list_pager;
return $this;
}
private function getListPager() {
return $this->listPager;
}
private function setListPager($list_pager) {
$this->listPager = $list_pager;
return $this;
}
private function setPagerOffset($pager_offset) {
$this->pagerOffset = $pager_offset;
return $this;
}
private function getPagerOffset() {
return $this->pagerOffset;
}
private function setPagerPageSize($pager_page_size) {
$this->pagerPageSize = $pager_page_size;
return $this;
}
private function getPagerPageSize() {
return $this->pagerPageSize;
}
public function willProcessRequest(array $data) {
$this->setFilter(idx($data, 'filter', 'upload'));
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
switch ($this->getFilter()) {
case 'upload':
default:
$this->setShowUploader(true);
$this->setUseBasicUploader($request->getExists('basic_uploader'));
$see_all = phutil_render_tag(
'a',
array(
'href' => '/file/filter/all',
),
'See all Files');
$this->setListHeader("Recently Uploaded Files &middot; {$see_all}");
$this->setShowListPager(false);
$this->setPagerOffset(0);
$this->setPagerPageSize(10);
break;
case 'my':
$this->setShowUploader(false);
$this->setListHeader('Files You Uploaded');
$this->setListAuthor($user);
$this->setPagerOffset($request->getInt('page', 0));
break;
case 'all':
$this->setShowUploader(false);
$this->setListHeader('All Files');
$this->setPagerOffset($request->getInt('page', 0));
break;
}
$this->loadListData();
$side_nav = new PhabricatorFileSideNavView();
$side_nav->setSelectedFilter($this->getFilter());
if ($this->showUploader()) {
$side_nav->appendChild($this->renderUploadPanel());
}
$side_nav->appendChild($this->renderList());
return $this->buildStandardPageResponse(
$side_nav,
array(
'title' => 'Files',
));
}
private function loadListData() {
$request = $this->getRequest();
$user = $request->getUser();
$pager = new AphrontPagerView();
$pager->setOffset($this->getPagerOffset());
if ($this->getPagerPageSize()) {
$pager->setPageSize($this->getPagerPageSize());
}
$author = $this->getListAuthor();
if ($author) {
$files = id(new PhabricatorFile())->loadAllWhere(
'authorPHID = %s ORDER BY id DESC LIMIT %d, %d',
$author->getPHID(),
$pager->getOffset(),
$pager->getPageSize() + 1);
} else {
$files = id(new PhabricatorFile())->loadAllWhere(
'1 = 1 ORDER BY id DESC LIMIT %d, %d',
$pager->getOffset(),
$pager->getPageSize() + 1);
}
$files = $pager->sliceResults($files);
$pager->setURI($request->getRequestURI(), 'page');
$this->setListPager($pager);
$phids = mpull($files, 'getAuthorPHID');
$handles = $this->loadViewerHandles($phids);
$highlighted = $request->getStr('h');
$highlighted = explode('-', $highlighted);
$highlighted = array_fill_keys($highlighted, true);
$rows = array();
$rowc = array();
foreach ($files as $file) {
if ($file->isViewableInBrowser()) {
$view_button = phutil_render_tag(
'a',
array(
'class' => 'small button grey',
'href' => $file->getViewURI(),
),
'View');
} else {
$view_button = null;
}
if (isset($highlighted[$file->getID()])) {
$rowc[] = 'highlighted';
} else {
$rowc[] = '';
}
$name = $file->getName();
$rows[] = array(
phutil_escape_html('F'.$file->getID()),
$file->getAuthorPHID()
? $handles[$file->getAuthorPHID()]->renderLink()
: null,
phutil_render_tag(
'a',
array(
// Don't use $file->getBestURI() to improve discoverability of /F.
'href' => '/F'.$file->getID(),
),
($name != '' ? phutil_escape_html($name) : '<em>no name</em>')),
phutil_escape_html(number_format($file->getByteSize()).' bytes'),
phutil_render_tag(
'a',
array(
'class' => 'small button grey',
'href' => '/file/info/'.$file->getPHID().'/',
),
'Info'),
$view_button,
phabricator_date($file->getDateCreated(), $user),
phabricator_time($file->getDateCreated(), $user),
);
}
$this->setListRows($rows);
$this->setListRowClasses($rowc);
}
private function renderList() {
$table = new AphrontTableView($this->getListRows());
$table->setRowClasses($this->getListRowClasses());
$table->setHeaders(
array(
'File ID',
'Author',
'Name',
'Size',
'',
'',
'Created',
'',
));
$table->setColumnClasses(
array(
null,
'',
'wide pri',
'right',
'action',
'action',
'',
'right',
));
$panel = new AphrontPanelView();
$panel->appendChild($table);
$panel->setHeader($this->getListHeader());
if ($this->showListPager()) {
$panel->appendChild($this->getListPager());
}
return $panel;
}
private function renderUploadPanel() {
$request = $this->getRequest();
$user = $request->getUser();
$limit_text = PhabricatorFileUploadView::renderUploadLimit();
if ($this->useBasicUploader()) {
$upload_panel = new PhabricatorFileUploadView();
$upload_panel->setUser($user);
} else {
require_celerity_resource('files-css');
$upload_id = celerity_generate_unique_node_id();
$panel_id = celerity_generate_unique_node_id();
$upload_panel = new AphrontPanelView();
$upload_panel->setHeader('Upload Files');
$upload_panel->setCaption($limit_text);
$upload_panel->setCreateButton('Basic Uploader',
$request->getRequestURI()->setQueryParam('basic_uploader', true)
);
$upload_panel->setWidth(AphrontPanelView::WIDTH_FULL);
$upload_panel->setID($panel_id);
$upload_panel->appendChild(
phutil_render_tag(
'div',
array(
'id' => $upload_id,
'style' => 'display: none;',
'class' => 'files-drag-and-drop',
),
''));
Javelin::initBehavior(
'files-drag-and-drop',
array(
'uri' => '/file/dropupload/',
'browseURI' => '/file/filter/my/',
'control' => $upload_id,
'target' => $panel_id,
'activatedClass' => 'aphront-panel-view-drag-and-drop',
));
}
return $upload_panel;
}
}
diff --git a/src/applications/files/controller/PhabricatorFileProxyController.php b/src/applications/files/controller/PhabricatorFileProxyController.php
index 8c449fcdc8..51b1e98654 100644
--- a/src/applications/files/controller/PhabricatorFileProxyController.php
+++ b/src/applications/files/controller/PhabricatorFileProxyController.php
@@ -1,70 +1,54 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFileProxyController extends PhabricatorFileController {
private $uri;
public function processRequest() {
if (!PhabricatorEnv::getEnvConfig('files.enable-proxy')) {
return new Aphront400Response();
}
$request = $this->getRequest();
$uri = $request->getStr('uri');
$proxy = id(new PhabricatorFileProxyImage())->loadOneWhere(
'uri = %s',
$uri);
if (!$proxy) {
// This write is fine to skip CSRF checks for, we're just building a
// cache of some remote image.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$file = PhabricatorFile::newFromFileDownload(
$uri,
nonempty(basename($uri), 'proxied-file'));
if ($file) {
$proxy = new PhabricatorFileProxyImage();
$proxy->setURI($uri);
$proxy->setFilePHID($file->getPHID());
$proxy->save();
}
unset($unguarded);
}
if ($proxy) {
$file = id(new PhabricatorFile())->loadOneWhere('phid = %s',
$proxy->getFilePHID());
if ($file) {
$view_uri = $file->getBestURI();
} else {
$bad_phid = $proxy->getFilePHID();
throw new Exception(
"Unable to load file with phid {$bad_phid}."
);
}
return id(new AphrontRedirectResponse())->setURI($view_uri);
}
return new Aphront400Response();
}
}
diff --git a/src/applications/files/controller/PhabricatorFileShortcutController.php b/src/applications/files/controller/PhabricatorFileShortcutController.php
index 04b314d325..3d699e10d4 100644
--- a/src/applications/files/controller/PhabricatorFileShortcutController.php
+++ b/src/applications/files/controller/PhabricatorFileShortcutController.php
@@ -1,36 +1,20 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFileShortcutController
extends PhabricatorFileController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$file = id(new PhabricatorFile())->load($this->id);
if (!$file) {
return new Aphront404Response();
}
return id(new AphrontRedirectResponse())->setURI($file->getBestURI());
}
}
diff --git a/src/applications/files/controller/PhabricatorFileTransformController.php b/src/applications/files/controller/PhabricatorFileTransformController.php
index 4a7970f3d8..90f926439a 100644
--- a/src/applications/files/controller/PhabricatorFileTransformController.php
+++ b/src/applications/files/controller/PhabricatorFileTransformController.php
@@ -1,162 +1,146 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFileTransformController
extends PhabricatorFileController {
private $transform;
private $phid;
private $key;
public function willProcessRequest(array $data) {
$this->transform = $data['transform'];
$this->phid = $data['phid'];
$this->key = $data['key'];
}
public function shouldRequireLogin() {
return false;
}
public function processRequest() {
$file = id(new PhabricatorFile())->loadOneWhere('phid = %s', $this->phid);
if (!$file) {
return new Aphront404Response();
}
if (!$file->validateSecretKey($this->key)) {
return new Aphront403Response();
}
$xform = id(new PhabricatorTransformedFile())
->loadOneWhere(
'originalPHID = %s AND transform = %s',
$this->phid,
$this->transform);
if ($xform) {
return $this->buildTransformedFileResponse($xform);
}
$type = $file->getMimeType();
if (!$file->isViewableInBrowser() || !$file->isTransformableImage()) {
return $this->buildDefaultTransformation($file);
}
// We're essentially just building a cache here and don't need CSRF
// protection.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
switch ($this->transform) {
case 'thumb-220x165':
$xformed_file = $this->executeThumbTransform($file, 220, 165);
break;
case 'preview-220':
$xformed_file = $this->executePreviewTransform($file, 220);
break;
case 'thumb-160x120':
$xformed_file = $this->executeThumbTransform($file, 160, 120);
break;
case 'thumb-60x45':
$xformed_file = $this->executeThumbTransform($file, 60, 45);
break;
default:
return new Aphront400Response();
}
if (!$xformed_file) {
return new Aphront400Response();
}
$xform = new PhabricatorTransformedFile();
$xform->setOriginalPHID($this->phid);
$xform->setTransform($this->transform);
$xform->setTransformedPHID($xformed_file->getPHID());
$xform->save();
return $this->buildTransformedFileResponse($xform);
}
private function buildDefaultTransformation(PhabricatorFile $file) {
static $regexps = array(
'@application/zip@' => 'zip',
'@image/@' => 'image',
'@application/pdf@' => 'pdf',
'@.*@' => 'default',
);
$type = $file->getMimeType();
$prefix = 'default';
foreach ($regexps as $regexp => $implied_prefix) {
if (preg_match($regexp, $type)) {
$prefix = $implied_prefix;
break;
}
}
switch ($this->transform) {
case 'thumb-160x120':
$suffix = '160x120';
break;
case 'thumb-60x45':
$suffix = '60x45';
break;
default:
throw new Exception("Unsupported transformation type!");
}
$path = celerity_get_resource_uri(
"/rsrc/image/icon/fatcow/thumbnails/{$prefix}{$suffix}.png");
return id(new AphrontRedirectResponse())
->setURI($path);
}
private function buildTransformedFileResponse(
PhabricatorTransformedFile $xform) {
$file = id(new PhabricatorFile())->loadOneWhere(
'phid = %s',
$xform->getTransformedPHID());
if ($file) {
$uri = $file->getBestURI();
} else {
$bad_phid = $xform->getTransformedPHID();
throw new Exception(
"Unable to load file with phid {$bad_phid}."
);
}
// TODO: We could just delegate to the file view controller instead,
// which would save the client a roundtrip, but is slightly more complex.
return id(new AphrontRedirectResponse())->setURI($uri);
}
private function executePreviewTransform(PhabricatorFile $file, $size) {
$xformer = new PhabricatorImageTransformer();
return $xformer->executePreviewTransform($file, $size);
}
private function executeThumbTransform(PhabricatorFile $file, $x, $y) {
$xformer = new PhabricatorImageTransformer();
return $xformer->executeThumbTransform($file, $x, $y);
}
}
diff --git a/src/applications/files/controller/PhabricatorFileUploadController.php b/src/applications/files/controller/PhabricatorFileUploadController.php
index fd367f9bd0..8055211119 100644
--- a/src/applications/files/controller/PhabricatorFileUploadController.php
+++ b/src/applications/files/controller/PhabricatorFileUploadController.php
@@ -1,46 +1,30 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFileUploadController extends PhabricatorFileController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
if ($request->isFormPost()) {
$file = PhabricatorFile::newFromPHPUpload(
idx($_FILES, 'file'),
array(
'name' => $request->getStr('name'),
'authorPHID' => $user->getPHID(),
));
return id(new AphrontRedirectResponse())->setURI($file->getBestURI());
}
$panel = new PhabricatorFileUploadView();
$panel->setUser($user);
return $this->buildStandardPageResponse(
array($panel),
array(
'title' => 'Upload File',
));
}
}
diff --git a/src/applications/files/engine/PhabricatorFileStorageEngine.php b/src/applications/files/engine/PhabricatorFileStorageEngine.php
index adc3f0d70e..5941088664 100644
--- a/src/applications/files/engine/PhabricatorFileStorageEngine.php
+++ b/src/applications/files/engine/PhabricatorFileStorageEngine.php
@@ -1,104 +1,88 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Defines a storage engine which can write file data somewhere (like a
* database, local disk, Amazon S3, the A:\ drive, or a custom filer) and
* retrieve it later.
*
* You can extend this class to provide new file storage backends.
*
* For more information, see @{article:File Storage Technical Documentation}.
*
* @task meta Engine Metadata
* @task file Managing File Data
* @group filestorage
*/
abstract class PhabricatorFileStorageEngine {
final public function __construct() {
// <empty>
}
/* -( Engine Metadata )---------------------------------------------------- */
/**
* Return a unique, nonempty string which identifies this storage engine.
* This is used to look up the storage engine when files needs to be read or
* deleted. For instance, if you store files by giving them to a duck for
* safe keeping in his nest down by the pond, you might return 'duck' from
* this method.
*
* @return string Unique string for this engine, max length 32.
* @task meta
*/
abstract public function getEngineIdentifier();
/* -( Managing File Data )------------------------------------------------- */
/**
* Write file data to the backing storage and return a handle which can later
* be used to read or delete it. For example, if the backing storage is local
* disk, the handle could be the path to the file.
*
* The caller will provide a $params array, which may be empty or may have
* some metadata keys (like "name" and "author") in it. You should be prepared
* to handle writes which specify no metadata, but might want to optionally
* use some keys in this array for debugging or logging purposes. This is
* the same dictionary passed to @{method:PhabricatorFile::NewFromFileData},
* so you could conceivably do custom things with it.
*
* If you are unable to write for whatever reason (e.g., the disk is full),
* throw an exception. If there are other satisfactory but less-preferred
* storage engines available, they will be tried.
*
* @param string The file data to write.
* @param array File metadata (name, author), if available.
* @return string Unique string which identifies the stored file, max length
* 255.
* @task file
*/
abstract public function writeFile($data, array $params);
/**
* Read the contents of a file previously written by @{method:writeFile}.
*
* @param string The handle returned from @{method:writeFile} when the
* file was written.
* @return string File contents.
* @task file
*/
abstract public function readFile($handle);
/**
* Delete the data for a file previously written by @{method:writeFile}.
*
* @param string The handle returned from @{method:writeFile} when the
* file was written.
* @return void
* @task file
*/
abstract public function deleteFile($handle);
}
diff --git a/src/applications/files/engine/PhabricatorLocalDiskFileStorageEngine.php b/src/applications/files/engine/PhabricatorLocalDiskFileStorageEngine.php
index d613d01673..be43e887bc 100644
--- a/src/applications/files/engine/PhabricatorLocalDiskFileStorageEngine.php
+++ b/src/applications/files/engine/PhabricatorLocalDiskFileStorageEngine.php
@@ -1,140 +1,124 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Local disk storage engine. Keeps files on local disk. This engine is easy
* to set up, but it doesn't work if you have multiple web frontends!
*
* @task impl Implementation
* @task internal Internals
* @group filestorage
*/
final class PhabricatorLocalDiskFileStorageEngine
extends PhabricatorFileStorageEngine {
/* -( Implementation )----------------------------------------------------- */
/**
* This engine identifies as "local-disk".
* @task impl
*/
public function getEngineIdentifier() {
return 'local-disk';
}
/**
* Write the file data to local disk. Returns the relative path as the
* file data handle.
* @task impl
*/
public function writeFile($data, array $params) {
$root = $this->getLocalDiskFileStorageRoot();
// Generate a random, unique file path like "ab/29/1f918a9ac39201ff". We
// put a couple of subdirectories up front to avoid a situation where we
// have one directory with a zillion files in it, since this is generally
// bad news.
do {
$name = md5(mt_rand());
$name = preg_replace('/^(..)(..)(.*)$/', '\\1/\\2/\\3', $name);
if (!Filesystem::pathExists($root.'/'.$name)) {
break;
}
} while (true);
$parent = $root.'/'.dirname($name);
if (!Filesystem::pathExists($parent)) {
execx('mkdir -p %s', $parent);
}
AphrontWriteGuard::willWrite();
Filesystem::writeFile($root.'/'.$name, $data);
return $name;
}
/**
* Read the file data off local disk.
* @task impl
*/
public function readFile($handle) {
$path = $this->getLocalDiskFileStorageFullPath($handle);
return Filesystem::readFile($path);
}
/**
* Deletes the file from local disk, if it exists.
* @task impl
*/
public function deleteFile($handle) {
$path = $this->getLocalDiskFileStorageFullPath($handle);
if (Filesystem::pathExists($path)) {
AphrontWriteGuard::willWrite();
Filesystem::remove($path);
}
}
/* -( Internals )---------------------------------------------------------- */
/**
* Get the configured local disk path for file storage.
*
* @return string Absolute path to somewhere that files can be stored.
* @task internal
*/
private function getLocalDiskFileStorageRoot() {
$root = PhabricatorEnv::getEnvConfig('storage.local-disk.path');
if (!$root || $root == '/' || $root[0] != '/') {
throw new PhabricatorFileStorageConfigurationException(
"Malformed local disk storage root. You must provide an absolute ".
"path, and can not use '/' as the root.");
}
return rtrim($root, '/');
}
/**
* Convert a handle into an absolute local disk path.
*
* @param string File data handle.
* @return string Absolute path to the corresponding file.
* @task internal
*/
private function getLocalDiskFileStorageFullPath($handle) {
// Make sure there's no funny business going on here. Users normally have
// no ability to affect the content of handles, but double-check that
// we're only accessing local storage just in case.
if (!preg_match('@^[a-f0-9]{2}/[a-f0-9]{2}/[a-f0-9]{28}$@', $handle)) {
throw new Exception(
"Local disk filesystem handle '{$handle}' is malformed!");
}
$root = $this->getLocalDiskFileStorageRoot();
return $root.'/'.$handle;
}
}
diff --git a/src/applications/files/engine/PhabricatorMySQLFileStorageEngine.php b/src/applications/files/engine/PhabricatorMySQLFileStorageEngine.php
index 8ca3f7270f..77a46a9703 100644
--- a/src/applications/files/engine/PhabricatorMySQLFileStorageEngine.php
+++ b/src/applications/files/engine/PhabricatorMySQLFileStorageEngine.php
@@ -1,97 +1,81 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* MySQL blob storage engine. This engine is the easiest to set up but doesn't
* scale very well.
*
* It uses the @{class:PhabricatorFileStorageBlob} to actually access the
* underlying database table.
*
* @task impl Implementation
* @task internal Internals
* @group filestorage
*/
final class PhabricatorMySQLFileStorageEngine
extends PhabricatorFileStorageEngine {
/* -( Implementation )----------------------------------------------------- */
/**
* For historical reasons, this engine identifies as "blob".
*
* @task impl
*/
public function getEngineIdentifier() {
return 'blob';
}
/**
* Write file data into the big blob store table in MySQL. Returns the row
* ID as the file data handle.
*
* @task impl
*/
public function writeFile($data, array $params) {
$blob = new PhabricatorFileStorageBlob();
$blob->setData($data);
$blob->save();
return $blob->getID();
}
/**
* Load a stored blob from MySQL.
* @task impl
*/
public function readFile($handle) {
return $this->loadFromMySQLFileStorage($handle)->getData();
}
/**
* Delete a blob from MySQL.
* @task impl
*/
public function deleteFile($handle) {
$this->loadFromMySQLFileStorage($handle)->delete();
}
/* -( Internals )---------------------------------------------------------- */
/**
* Load the Lisk object that stores the file data for a handle.
*
* @param string File data handle.
* @return PhabricatorFileStorageBlob Data DAO.
* @task internal
*/
private function loadFromMySQLFileStorage($handle) {
$blob = id(new PhabricatorFileStorageBlob())->load($handle);
if (!$blob) {
throw new Exception("Unable to load MySQL blob file '{$handle}'!");
}
return $blob;
}
}
diff --git a/src/applications/files/engine/PhabricatorS3FileStorageEngine.php b/src/applications/files/engine/PhabricatorS3FileStorageEngine.php
index 3e9ef01b21..fa7f9c3dc9 100644
--- a/src/applications/files/engine/PhabricatorS3FileStorageEngine.php
+++ b/src/applications/files/engine/PhabricatorS3FileStorageEngine.php
@@ -1,130 +1,114 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Amazon S3 file storage engine. This engine scales well but is relatively
* high-latency since data has to be pulled off S3.
*
* @task impl Implementation
* @task internal Internals
* @group filestorage
*/
final class PhabricatorS3FileStorageEngine
extends PhabricatorFileStorageEngine {
/* -( Implementation )----------------------------------------------------- */
/**
* This engine identifies as "amazon-s3".
*
* @task impl
*/
public function getEngineIdentifier() {
return 'amazon-s3';
}
/**
* Write file data into S3.
* @task impl
*/
public function writeFile($data, array $params) {
$s3 = $this->newS3API();
$name = 'phabricator/'.Filesystem::readRandomCharacters(20);
AphrontWriteGuard::willWrite();
$s3->putObject(
$data,
$this->getBucketName(),
$name,
$acl = 'private');
return $name;
}
/**
* Load a stored blob from S3.
* @task impl
*/
public function readFile($handle) {
$result = $this->newS3API()->getObject(
$this->getBucketName(),
$handle);
return $result->body;
}
/**
* Delete a blob from S3.
* @task impl
*/
public function deleteFile($handle) {
AphrontWriteGuard::willWrite();
$this->newS3API()->deleteObject(
$this->getBucketName(),
$handle);
}
/* -( Internals )---------------------------------------------------------- */
/**
* Retrieve the S3 bucket name.
*
* @task internal
*/
private function getBucketName() {
$bucket = PhabricatorEnv::getEnvConfig('storage.s3.bucket');
if (!$bucket) {
throw new PhabricatorFileStorageConfigurationException(
"No 'storage.s3.bucket' specified!");
}
return $bucket;
}
/**
* Create a new S3 API object.
*
* @task internal
* @phutil-external-symbol class S3
*/
private function newS3API() {
$libroot = dirname(phutil_get_library_root('phabricator'));
require_once $libroot.'/externals/s3/S3.php';
$access_key = PhabricatorEnv::getEnvConfig('amazon-s3.access-key');
$secret_key = PhabricatorEnv::getEnvConfig('amazon-s3.secret-key');
if (!$access_key || !$secret_key) {
throw new PhabricatorFileStorageConfigurationException(
"Specify 'amazon-s3.access-key' and 'amazon-s3.secret-key'!");
}
$s3 = new S3($access_key, $secret_key, $use_ssl = true);
$s3->setExceptions(true);
return $s3;
}
}
diff --git a/src/applications/files/engineselector/PhabricatorDefaultFileStorageEngineSelector.php b/src/applications/files/engineselector/PhabricatorDefaultFileStorageEngineSelector.php
index f3e23f1e20..306772c97a 100644
--- a/src/applications/files/engineselector/PhabricatorDefaultFileStorageEngineSelector.php
+++ b/src/applications/files/engineselector/PhabricatorDefaultFileStorageEngineSelector.php
@@ -1,70 +1,54 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Default storage engine selector. See
* @{class:PhabricatorFileStorageEngineSelector} and @{article:File Storage
* Technical Documentation} for more information.
*
* @group filestorage
*/
final class PhabricatorDefaultFileStorageEngineSelector
extends PhabricatorFileStorageEngineSelector {
/**
* Select viable default storage engines according to configuration. We'll
* select the MySQL and Local Disk storage engines if they are configured
* to allow a given file.
*/
public function selectStorageEngines($data, array $params) {
$length = strlen($data);
$mysql_key = 'storage.mysql-engine.max-size';
$mysql_limit = PhabricatorEnv::getEnvConfig($mysql_key);
$engines = array();
if ($mysql_limit && $length <= $mysql_limit) {
$engines[] = new PhabricatorMySQLFileStorageEngine();
}
$local_key = 'storage.local-disk.path';
$local_path = PhabricatorEnv::getEnvConfig($local_key);
if ($local_path) {
$engines[] = new PhabricatorLocalDiskFileStorageEngine();
}
$s3_key = 'storage.s3.bucket';
if (PhabricatorEnv::getEnvConfig($s3_key)) {
$engines[] = new PhabricatorS3FileStorageEngine();
}
if ($mysql_limit && empty($engines)) {
// If we return no engines, an exception will be thrown but it will be
// a little vague ("No valid storage engines"). Since this is a default
// case, throw a more specific exception.
throw new Exception(
"This file exceeds the configured MySQL storage engine filesize ".
"limit, but no other storage engines are configured. Increase the ".
"MySQL storage engine limit or configure a storage engine suitable ".
"for larger files.");
}
return $engines;
}
}
diff --git a/src/applications/files/engineselector/PhabricatorFileStorageEngineSelector.php b/src/applications/files/engineselector/PhabricatorFileStorageEngineSelector.php
index ca2e8f2043..7c42f4647a 100644
--- a/src/applications/files/engineselector/PhabricatorFileStorageEngineSelector.php
+++ b/src/applications/files/engineselector/PhabricatorFileStorageEngineSelector.php
@@ -1,63 +1,47 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Chooses appropriate storage engine(s) for files. When Phabricator needs
* to write a blob of file data, it uses the configured selector to get a list
* of suitable @{class:PhabricatorFileStorageEngine}s. For more information,
* see @{article:File Storage Technical Documentation}.
*
* @group filestorage
* @task select Selecting Storage Engines
*/
abstract class PhabricatorFileStorageEngineSelector {
final public function __construct() {
// <empty>
}
/* -( Selecting Storage Engines )------------------------------------------ */
/**
* Select valid storage engines for a file. This method will be called by
* Phabricator when it needs to store a file permanently. It must return a
* list of valid @{class:PhabricatorFileStorageEngine}s.
*
* If you are are extending this class to provide a custom selector, you
* probably just want it to look like this:
*
* return array(new MyCustomFileStorageEngine());
*
* ...that is, store every file in whatever storage engine you're using.
* However, you can also provide multiple storage engines, or store some files
* in one engine and some files in a different engine by implementing a more
* complex selector.
*
* @param string File data.
* @param dict Dictionary of optional file metadata. This may be empty, or
* have some additional keys like 'file' and 'author' which
* provide metadata.
* @return list List of @{class:PhabricatorFileStorageEngine}s, ordered by
* preference.
* @task select
*/
abstract public function selectStorageEngines($data, array $params);
}
diff --git a/src/applications/files/exception/PhabricatorFileStorageConfigurationException.php b/src/applications/files/exception/PhabricatorFileStorageConfigurationException.php
index 411d921357..a561dfb06c 100644
--- a/src/applications/files/exception/PhabricatorFileStorageConfigurationException.php
+++ b/src/applications/files/exception/PhabricatorFileStorageConfigurationException.php
@@ -1,28 +1,12 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Thrown by storage engines to indicate an configuration error which should
* abort the storage attempt, as opposed to a transient storage error which
* should be retried on other engines.
*
* @group files
*/
final class PhabricatorFileStorageConfigurationException extends Exception {
}
diff --git a/src/applications/files/exception/PhabricatorFileUploadException.php b/src/applications/files/exception/PhabricatorFileUploadException.php
index 0293f03ca4..6dcf345026 100644
--- a/src/applications/files/exception/PhabricatorFileUploadException.php
+++ b/src/applications/files/exception/PhabricatorFileUploadException.php
@@ -1,47 +1,31 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFileUploadException extends Exception {
public function __construct($code) {
$map = array(
UPLOAD_ERR_INI_SIZE =>
"Uploaded file is too large: file is larger than the ".
"'upload_max_filesize' setting in php.ini.",
UPLOAD_ERR_FORM_SIZE =>
"File is too large.",
UPLOAD_ERR_PARTIAL =>
"File was only partially transferred, upload did not complete.",
UPLOAD_ERR_NO_FILE =>
"No file was uploaded.",
UPLOAD_ERR_NO_TMP_DIR =>
"Unable to write file: temporary directory does not exist.",
UPLOAD_ERR_CANT_WRITE =>
"Unable to write file: failed to write to temporary directory.",
UPLOAD_ERR_EXTENSION =>
"Unable to upload: a PHP extension stopped the upload.",
-1000 =>
"Uploaded file exceeds limit in Phabricator ".
"'storage.upload-size-limit' configuration.",
);
$message = idx($map, $code, "Upload failed: unknown error.");
parent::__construct($message, $code);
}
}
diff --git a/src/applications/files/management/PhabricatorFilesManagementEnginesWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementEnginesWorkflow.php
index 02929377b4..bdc04fee87 100644
--- a/src/applications/files/management/PhabricatorFilesManagementEnginesWorkflow.php
+++ b/src/applications/files/management/PhabricatorFilesManagementEnginesWorkflow.php
@@ -1,46 +1,30 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFilesManagementEnginesWorkflow
extends PhabricatorFilesManagementWorkflow {
public function didConstruct() {
$this
->setName('engines')
->setSynopsis('List available storage engines.')
->setArguments(array());
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$engines = PhabricatorFile::buildAllEngines();
if (!$engines) {
throw new Exception("No storage engines are available.");
}
foreach ($engines as $engine) {
$console->writeOut(
"%s\n",
$engine->getEngineIdentifier());
}
return 0;
}
}
diff --git a/src/applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php
index 8972ac275e..a5d8455957 100644
--- a/src/applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php
+++ b/src/applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php
@@ -1,153 +1,137 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFilesManagementMigrateWorkflow
extends PhabricatorFilesManagementWorkflow {
public function didConstruct() {
$this
->setName('migrate')
->setSynopsis('Migrate files between storage engines.')
->setArguments(
array(
array(
'name' => 'engine',
'param' => 'storage_engine',
'help' => 'Migrate to the named storage engine.',
),
array(
'name' => 'dry-run',
'help' => 'Show what would be migrated.',
),
array(
'name' => 'all',
'help' => 'Migrate all files.',
),
array(
'name' => 'names',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$engine_id = $args->getArg('engine');
if (!$engine_id) {
throw new PhutilArgumentUsageException(
"Specify an engine to migrate to with `--engine`. ".
"Use `files engines` to get a list of engines.");
}
$engine = PhabricatorFile::buildEngine($engine_id);
if ($args->getArg('all')) {
if ($args->getArg('names')) {
throw new PhutilArgumentUsageException(
"Specify either a list of files or `--all`, but not both.");
}
$iterator = new LiskMigrationIterator(new PhabricatorFile());
} else if ($args->getArg('names')) {
$iterator = array();
foreach ($args->getArg('names') as $name) {
$name = trim($name);
$id = preg_replace('/^F/i', '', $name);
if (ctype_digit($id)) {
$file = id(new PhabricatorFile())->loadOneWhere(
'id = %d',
$id);
if (!$file) {
throw new PhutilArgumentUsageException(
"No file exists with id '{$name}'.");
}
} else {
$file = id(new PhabricatorFile())->loadOneWhere(
'phid = %d',
$name);
if (!$file) {
throw new PhutilArgumentUsageException(
"No file exists with PHID '{$name}'.");
}
}
$iterator[] = $file;
}
} else {
throw new PhutilArgumentUsageException(
"Either specify a list of files to migrate, or use `--all` ".
"to migrate all files.");
}
$is_dry_run = $args->getArg('dry-run');
$failed = array();
foreach ($iterator as $file) {
$fid = 'F'.$file->getID();
if ($file->getStorageEngine() == $engine_id) {
$console->writeOut(
"%s: Already stored on '%s'\n",
$fid,
$engine_id);
continue;
}
if ($is_dry_run) {
$console->writeOut(
"%s: Would migrate from '%s' to '%s' (dry run)\n",
$fid,
$file->getStorageEngine(),
$engine_id);
continue;
}
$console->writeOut(
"%s: Migrating from '%s' to '%s'...",
$fid,
$file->getStorageEngine(),
$engine_id);
try {
$file->migrateToEngine($engine);
$console->writeOut("done.\n");
} catch (Exception $ex) {
$console->writeOut("failed!\n");
$console->writeErr("%s\n", (string)$ex);
$failed[] = $file;
}
}
if ($failed) {
$console->writeOut("**Failures!**\n");
$ids = array();
foreach ($failed as $file) {
$ids[] = 'F'.$file->getID();
}
$console->writeOut("%s\n", implode(', ', $ids));
return 1;
} else {
$console->writeOut("**Success!**\n");
return 0;
}
}
}
diff --git a/src/applications/files/management/PhabricatorFilesManagementWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementWorkflow.php
index 968f55208f..9e7eb3da1c 100644
--- a/src/applications/files/management/PhabricatorFilesManagementWorkflow.php
+++ b/src/applications/files/management/PhabricatorFilesManagementWorkflow.php
@@ -1,26 +1,10 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorFilesManagementWorkflow
extends PhutilArgumentWorkflow {
public function isExecutable() {
return true;
}
}
diff --git a/src/applications/files/query/PhabricatorFileQuery.php b/src/applications/files/query/PhabricatorFileQuery.php
index 45b35127ce..13095c660e 100644
--- a/src/applications/files/query/PhabricatorFileQuery.php
+++ b/src/applications/files/query/PhabricatorFileQuery.php
@@ -1,72 +1,56 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFileQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function loadPage() {
$table = new PhabricatorFile();
$conn_r = $table->establishConnection('r');
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T f %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
return $table->loadAllFromArray($data);
}
private function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
$where[] = $this->buildPagingClause($conn_r);
if ($this->ids) {
$where[] = qsprintf(
$conn_r,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids) {
$where[] = qsprintf(
$conn_r,
'phid IN (%Ls)',
$this->phids);
}
return $this->formatWhereClause($where);
}
}
diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php
index 1ffd12cc44..48fdb8c445 100644
--- a/src/applications/files/storage/PhabricatorFile.php
+++ b/src/applications/files/storage/PhabricatorFile.php
@@ -1,523 +1,507 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFile extends PhabricatorFileDAO
implements PhabricatorPolicyInterface {
const STORAGE_FORMAT_RAW = 'raw';
protected $phid;
protected $name;
protected $mimeType;
protected $byteSize;
protected $authorPHID;
protected $secretKey;
protected $contentHash;
protected $storageEngine;
protected $storageFormat;
protected $storageHandle;
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_FILE);
}
public static function readUploadedFileData($spec) {
if (!$spec) {
throw new Exception("No file was uploaded!");
}
$err = idx($spec, 'error');
if ($err) {
throw new PhabricatorFileUploadException($err);
}
$tmp_name = idx($spec, 'tmp_name');
$is_valid = @is_uploaded_file($tmp_name);
if (!$is_valid) {
throw new Exception("File is not an uploaded file.");
}
$file_data = Filesystem::readFile($tmp_name);
$file_size = idx($spec, 'size');
if (strlen($file_data) != $file_size) {
throw new Exception("File size disagrees with uploaded size.");
}
self::validateFileSize(strlen($file_data));
return $file_data;
}
public static function newFromPHPUpload($spec, array $params = array()) {
$file_data = self::readUploadedFileData($spec);
$file_name = nonempty(
idx($params, 'name'),
idx($spec, 'name'));
$params = array(
'name' => $file_name,
) + $params;
return self::newFromFileData($file_data, $params);
}
public static function newFromXHRUpload($data, array $params = array()) {
self::validateFileSize(strlen($data));
return self::newFromFileData($data, $params);
}
private static function validateFileSize($size) {
$limit = PhabricatorEnv::getEnvConfig('storage.upload-size-limit');
if (!$limit) {
return;
}
$limit = phabricator_parse_bytes($limit);
if ($size > $limit) {
throw new PhabricatorFileUploadException(-1000);
}
}
/**
* Given a block of data, try to load an existing file with the same content
* if one exists. If it does not, build a new file.
*
* This method is generally used when we have some piece of semi-trusted data
* like a diff or a file from a repository that we want to show to the user.
* We can't just dump it out because it may be dangerous for any number of
* reasons; instead, we need to serve it through the File abstraction so it
* ends up on the CDN domain if one is configured and so on. However, if we
* simply wrote a new file every time we'd potentially end up with a lot
* of redundant data in file storage.
*
* To solve these problems, we use file storage as a cache and reuse the
* same file again if we've previously written it.
*
* NOTE: This method unguards writes.
*
* @param string Raw file data.
* @param dict Dictionary of file information.
*/
public static function buildFromFileDataOrHash(
$data,
array $params = array()) {
$file = id(new PhabricatorFile())->loadOneWhere(
'name = %s AND contentHash = %s LIMIT 1',
self::normalizeFileName(idx($params, 'name')),
PhabricatorHash::digest($data));
if (!$file) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$file = PhabricatorFile::newFromFileData($data, $params);
unset($unguarded);
}
return $file;
}
public static function newFromFileData($data, array $params = array()) {
$selector = PhabricatorEnv::newObjectFromConfig('storage.engine-selector');
$engines = $selector->selectStorageEngines($data, $params);
if (!$engines) {
throw new Exception("No valid storage engines are available!");
}
$file = new PhabricatorFile();
$data_handle = null;
$engine_identifier = null;
$exceptions = array();
foreach ($engines as $engine) {
$engine_class = get_class($engine);
try {
list($engine_identifier, $data_handle) = $file->writeToEngine(
$engine,
$data,
$params);
// We stored the file somewhere so stop trying to write it to other
// places.
break;
} catch (PhabricatorFileStorageConfigurationException $ex) {
// If an engine is outright misconfigured (or misimplemented), raise
// that immediately since it probably needs attention.
throw $ex;
} catch (Exception $ex) {
phlog($ex);
// If an engine doesn't work, keep trying all the other valid engines
// in case something else works.
$exceptions[$engine_class] = $ex;
}
}
if (!$data_handle) {
throw new PhutilAggregateException(
"All storage engines failed to write file:",
$exceptions);
}
$file_name = idx($params, 'name');
$file_name = self::normalizeFileName($file_name);
// If for whatever reason, authorPHID isn't passed as a param
// (always the case with newFromFileDownload()), store a ''
$authorPHID = idx($params, 'authorPHID');
$file->setName($file_name);
$file->setByteSize(strlen($data));
$file->setAuthorPHID($authorPHID);
$file->setContentHash(PhabricatorHash::digest($data));
$file->setStorageEngine($engine_identifier);
$file->setStorageHandle($data_handle);
// TODO: This is probably YAGNI, but allows for us to do encryption or
// compression later if we want.
$file->setStorageFormat(self::STORAGE_FORMAT_RAW);
if (isset($params['mime-type'])) {
$file->setMimeType($params['mime-type']);
} else {
$tmp = new TempFile();
Filesystem::writeFile($tmp, $data);
$file->setMimeType(Filesystem::getMimeType($tmp));
}
$file->save();
return $file;
}
public function migrateToEngine(PhabricatorFileStorageEngine $engine) {
if (!$this->getID() || !$this->getStorageHandle()) {
throw new Exception(
"You can not migrate a file which hasn't yet been saved.");
}
$data = $this->loadFileData();
$params = array(
'name' => $this->getName(),
);
list($new_identifier, $new_handle) = $this->writeToEngine(
$engine,
$data,
$params);
$old_engine = $this->instantiateStorageEngine();
$old_handle = $this->getStorageHandle();
$this->setStorageEngine($new_identifier);
$this->setStorageHandle($new_handle);
$this->save();
$old_engine->deleteFile($old_handle);
return $this;
}
private function writeToEngine(
PhabricatorFileStorageEngine $engine,
$data,
array $params) {
$engine_class = get_class($engine);
$data_handle = $engine->writeFile($data, $params);
if (!$data_handle || strlen($data_handle) > 255) {
// This indicates an improperly implemented storage engine.
throw new PhabricatorFileStorageConfigurationException(
"Storage engine '{$engine_class}' executed writeFile() but did ".
"not return a valid handle ('{$data_handle}') to the data: it ".
"must be nonempty and no longer than 255 characters.");
}
$engine_identifier = $engine->getEngineIdentifier();
if (!$engine_identifier || strlen($engine_identifier) > 32) {
throw new PhabricatorFileStorageConfigurationException(
"Storage engine '{$engine_class}' returned an improper engine ".
"identifier '{$engine_identifier}': it must be nonempty ".
"and no longer than 32 characters.");
}
return array($engine_identifier, $data_handle);
}
public static function newFromFileDownload($uri, $name) {
$uri = new PhutilURI($uri);
$protocol = $uri->getProtocol();
switch ($protocol) {
case 'http':
case 'https':
break;
default:
// Make sure we are not accessing any file:// URIs or similar.
return null;
}
$timeout = 5;
$file_data = HTTPSFuture::loadContent($uri, $timeout);
if ($file_data === false) {
return null;
}
return self::newFromFileData($file_data, array('name' => $name));
}
public static function normalizeFileName($file_name) {
return preg_replace('/[^a-zA-Z0-9.~_-]/', '_', $file_name);
}
public function delete() {
$engine = $this->instantiateStorageEngine();
$ret = parent::delete();
$engine->deleteFile($this->getStorageHandle());
return $ret;
}
public function loadFileData() {
$engine = $this->instantiateStorageEngine();
$data = $engine->readFile($this->getStorageHandle());
switch ($this->getStorageFormat()) {
case self::STORAGE_FORMAT_RAW:
$data = $data;
break;
default:
throw new Exception("Unknown storage format.");
}
return $data;
}
public function getViewURI() {
if (!$this->getPHID()) {
throw new Exception(
"You must save a file before you can generate a view URI.");
}
$name = phutil_escape_uri($this->getName());
$path = '/file/data/'.$this->getSecretKey().'/'.$this->getPHID().'/'.$name;
return PhabricatorEnv::getCDNURI($path);
}
public function getInfoURI() {
return '/file/info/'.$this->getPHID().'/';
}
public function getBestURI() {
if ($this->isViewableInBrowser()) {
return $this->getViewURI();
} else {
return $this->getInfoURI();
}
}
public function getDownloadURI() {
$uri = id(new PhutilURI($this->getViewURI()))
->setQueryParam('download', true);
return (string) $uri;
}
public function getThumb60x45URI() {
$path = '/file/xform/thumb-60x45/'.$this->getPHID().'/'
.$this->getSecretKey().'/';
return PhabricatorEnv::getCDNURI($path);
}
public function getThumb160x120URI() {
$path = '/file/xform/thumb-160x120/'.$this->getPHID().'/'
.$this->getSecretKey().'/';
return PhabricatorEnv::getCDNURI($path);
}
public function getPreview220URI() {
$path = '/file/xform/preview-220/'.$this->getPHID().'/'
.$this->getSecretKey().'/';
return PhabricatorEnv::getCDNURI($path);
}
public function getThumb220x165URI() {
$path = '/file/xform/thumb-220x165/'.$this->getPHID().'/'
.$this->getSecretKey().'/';
return PhabricatorEnv::getCDNURI($path);
}
public function isViewableInBrowser() {
return ($this->getViewableMimeType() !== null);
}
public function isViewableImage() {
if (!$this->isViewableInBrowser()) {
return false;
}
$mime_map = PhabricatorEnv::getEnvConfig('files.image-mime-types');
$mime_type = $this->getMimeType();
return idx($mime_map, $mime_type);
}
public function isTransformableImage() {
// NOTE: The way the 'gd' extension works in PHP is that you can install it
// with support for only some file types, so it might be able to handle
// PNG but not JPEG. Try to generate thumbnails for whatever we can. Setup
// warns you if you don't have complete support.
$matches = null;
$ok = preg_match(
'@^image/(gif|png|jpe?g)@',
$this->getViewableMimeType(),
$matches);
if (!$ok) {
return false;
}
switch ($matches[1]) {
case 'jpg';
case 'jpeg':
return function_exists('imagejpeg');
break;
case 'png':
return function_exists('imagepng');
break;
case 'gif':
return function_exists('imagegif');
break;
default:
throw new Exception('Unknown type matched as image MIME type.');
}
}
public static function getTransformableImageFormats() {
$supported = array();
if (function_exists('imagejpeg')) {
$supported[] = 'jpg';
}
if (function_exists('imagepng')) {
$supported[] = 'png';
}
if (function_exists('imagegif')) {
$supported[] = 'gif';
}
return $supported;
}
protected function instantiateStorageEngine() {
return self::buildEngine($this->getStorageEngine());
}
public static function buildEngine($engine_identifier) {
$engines = self::buildAllEngines();
foreach ($engines as $engine) {
if ($engine->getEngineIdentifier() == $engine_identifier) {
return $engine;
}
}
throw new Exception(
"Storage engine '{$engine_identifier}' could not be located!");
}
public static function buildAllEngines() {
$engines = id(new PhutilSymbolLoader())
->setType('class')
->setConcreteOnly(true)
->setAncestorClass('PhabricatorFileStorageEngine')
->selectAndLoadSymbols();
$results = array();
foreach ($engines as $engine_class) {
$results[] = newv($engine_class['name'], array());
}
return $results;
}
public function getViewableMimeType() {
$mime_map = PhabricatorEnv::getEnvConfig('files.viewable-mime-types');
$mime_type = $this->getMimeType();
$mime_parts = explode(';', $mime_type);
$mime_type = trim(reset($mime_parts));
return idx($mime_map, $mime_type);
}
public function validateSecretKey($key) {
return ($key == $this->getSecretKey());
}
public function save() {
if (!$this->getSecretKey()) {
$this->setSecretKey($this->generateSecretKey());
}
return parent::save();
}
public function generateSecretKey() {
return Filesystem::readRandomCharacters(20);
}
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
// TODO: Implement proper per-object policies.
return PhabricatorPolicies::POLICY_USER;
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
}
diff --git a/src/applications/files/storage/PhabricatorFileDAO.php b/src/applications/files/storage/PhabricatorFileDAO.php
index c9ed0f39f5..e8a49b7a46 100644
--- a/src/applications/files/storage/PhabricatorFileDAO.php
+++ b/src/applications/files/storage/PhabricatorFileDAO.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorFileDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'file';
}
}
diff --git a/src/applications/files/storage/PhabricatorFileProxyImage.php b/src/applications/files/storage/PhabricatorFileProxyImage.php
index e30258fe2b..180bde5fd1 100644
--- a/src/applications/files/storage/PhabricatorFileProxyImage.php
+++ b/src/applications/files/storage/PhabricatorFileProxyImage.php
@@ -1,34 +1,18 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFileProxyImage extends PhabricatorFileDAO {
protected $uri;
protected $filePHID;
public function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
) + parent::getConfiguration();
}
static public function getProxyImageURI($uri) {
return '/file/proxy/?uri='.phutil_escape_uri($uri);
}
}
diff --git a/src/applications/files/storage/PhabricatorFileStorageBlob.php b/src/applications/files/storage/PhabricatorFileStorageBlob.php
index ff794586fb..7182eb1ea7 100644
--- a/src/applications/files/storage/PhabricatorFileStorageBlob.php
+++ b/src/applications/files/storage/PhabricatorFileStorageBlob.php
@@ -1,63 +1,47 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Simple blob store DAO for @{class:PhabricatorMySQLFileStorageEngine}.
*
* @group filestorage
*/
final class PhabricatorFileStorageBlob extends PhabricatorFileDAO {
// max_allowed_packet defaults to 1 MiB, escaping can make the data twice
// longer, query fits in the rest.
const CHUNK_SIZE = 5e5;
protected $data;
private $fullData;
protected function willWriteData(array &$data) {
parent::willWriteData($data);
$this->fullData = $data['data'];
if (strlen($data['data']) > self::CHUNK_SIZE) {
$data['data'] = substr($data['data'], 0, self::CHUNK_SIZE);
$this->openTransaction();
}
}
protected function didWriteData() {
$size = self::CHUNK_SIZE;
$length = strlen($this->fullData);
if ($length > $size) {
$conn = $this->establishConnection('w');
for ($offset = $size; $offset < $length; $offset += $size) {
queryfx(
$conn,
'UPDATE %T SET data = CONCAT(data, %s) WHERE %C = %d',
$this->getTableName(),
substr($this->fullData, $offset, $size),
$this->getIDKeyForUse(),
$this->getID());
}
$this->saveTransaction();
}
parent::didWriteData();
}
}
diff --git a/src/applications/files/storage/PhabricatorTransformedFile.php b/src/applications/files/storage/PhabricatorTransformedFile.php
index ca5f0aec29..e77f1ddffb 100644
--- a/src/applications/files/storage/PhabricatorTransformedFile.php
+++ b/src/applications/files/storage/PhabricatorTransformedFile.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorTransformedFile extends PhabricatorFileDAO {
protected $originalPHID;
protected $transform;
protected $transformedPHID;
}
diff --git a/src/applications/files/view/PhabricatorFileSideNavView.php b/src/applications/files/view/PhabricatorFileSideNavView.php
index bf8971e8a9..71cbabc626 100644
--- a/src/applications/files/view/PhabricatorFileSideNavView.php
+++ b/src/applications/files/view/PhabricatorFileSideNavView.php
@@ -1,83 +1,67 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFileSideNavView extends AphrontView {
private $selectedFilter;
public function setSelectedFilter($selected_filter) {
$this->selectedFilter = $selected_filter;
return $this;
}
private function getSelectedFilter() {
return $this->selectedFilter;
}
public function render() {
$selected_filter = $this->getSelectedFilter();
if (!$selected_filter) {
throw new Exception("Call setFilter() before render()!");
}
$filters = array(
'Files' => array(),
'upload' => array(
'name' => 'Upload File',
'href' => '/file/filter/upload/'
),
'my' => array(
'name' => 'My Files',
'href' => '/file/filter/my/'
),
'all' => array(
'name' => 'All Files',
'href' => '/file/filter/all/'
),
// TODO: Remove this fairly soon.
'<br />' => null,
'<div style="font-weight: normal; font-size: smaller; '.
'white-space: normal;">NOTE: Macros have moved to a separate '.
'application. Use the "Search" field to jump to it or choose '.
'More Stuff &raquo; Macros from the home page.</span>' => null,
);
$side_nav = new AphrontSideNavView();
foreach ($filters as $filter_key => $filter) {
// more of a label than a filter
if (empty($filter)) {
$side_nav->addNavItem(phutil_render_tag(
'span',
array(),
$filter_key));
continue;
}
$selected = $filter_key == $selected_filter;
$side_nav->addNavItem(
phutil_render_tag(
'a',
array(
'href' => $filter['href'],
'class' => $selected ? 'aphront-side-nav-selected': null,
),
$filter['name'])
);
}
$side_nav->appendChild($this->renderChildren());
return $side_nav->render();
}
}
diff --git a/src/applications/files/view/PhabricatorFileUploadView.php b/src/applications/files/view/PhabricatorFileUploadView.php
index 4c4989f840..b9160bf278 100644
--- a/src/applications/files/view/PhabricatorFileUploadView.php
+++ b/src/applications/files/view/PhabricatorFileUploadView.php
@@ -1,89 +1,73 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFileUploadView extends AphrontView {
private $user;
private function getUser() {
return $this->user;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function render() {
$user = $this->getUser();
if (!$user) {
throw new Exception("Call setUser() before render()!");
}
$form = new AphrontFormView();
$form->setAction('/file/upload/');
$form->setUser($user);
$form
->setEncType('multipart/form-data')
->appendChild(
id(new AphrontFormFileControl())
->setLabel('File')
->setName('file')
->setError(true)
->setCaption(self::renderUploadLimit()))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Name')
->setName('name')
->setCaption('Optional file display name.'))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Upload')
->addCancelButton('/file/'));
$panel = new AphrontPanelView();
$panel->setHeader('Upload File');
$panel->appendChild($form);
$panel->setWidth(AphrontPanelView::WIDTH_FULL);
return $panel->render();
}
public static function renderUploadLimit() {
$limit = PhabricatorEnv::getEnvConfig('storage.upload-size-limit');
$limit = phabricator_parse_bytes($limit);
if ($limit) {
$formatted = phabricator_format_bytes($limit);
return 'Maximum file size: '.phutil_escape_html($formatted);
}
$doc_href = PhabricatorEnv::getDocLink(
'article/Configuring_File_Upload_Limits.html');
$doc_link = phutil_render_tag(
'a',
array(
'href' => $doc_href,
'target' => '_blank',
),
'Configuring File Upload Limits');
return 'Upload limit is not configured, see '.$doc_link.'.';
}
}
diff --git a/src/applications/flag/application/PhabricatorApplicationFlags.php b/src/applications/flag/application/PhabricatorApplicationFlags.php
index 6a9e95e3fb..7b15339617 100644
--- a/src/applications/flag/application/PhabricatorApplicationFlags.php
+++ b/src/applications/flag/application/PhabricatorApplicationFlags.php
@@ -1,74 +1,58 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationFlags extends PhabricatorApplication {
public function getShortDescription() {
return 'Reminders';
}
public function getBaseURI() {
return '/flag/';
}
public function getAutospriteName() {
return 'flags';
}
public function getEventListeners() {
return array(
new PhabricatorFlagsUIEventListener(),
);
}
public function getApplicationGroup() {
return self::GROUP_ORGANIZATION;
}
public function loadStatus(PhabricatorUser $user) {
$status = array();
$flags = id(new PhabricatorFlagQuery())
->withOwnerPHIDs(array($user->getPHID()))
->execute();
$count = count($flags);
$type = $count
? PhabricatorApplicationStatusView::TYPE_INFO
: PhabricatorApplicationStatusView::TYPE_EMPTY;
$status[] = id(new PhabricatorApplicationStatusView())
->setType($type)
->setText(pht('%d Flagged Object(s)', $count))
->setCount($count);
return $status;
}
public function getRoutes() {
return array(
'/flag/' => array(
'' => 'PhabricatorFlagListController',
'view/(?P<view>[^/]+)/' => 'PhabricatorFlagListController',
'edit/(?P<phid>[^/]+)/' => 'PhabricatorFlagEditController',
'delete/(?P<id>[1-9]\d*)/' => 'PhabricatorFlagDeleteController',
),
);
}
}
diff --git a/src/applications/flag/constants/PhabricatorFlagColor.php b/src/applications/flag/constants/PhabricatorFlagColor.php
index 57f56b992f..839a889f97 100644
--- a/src/applications/flag/constants/PhabricatorFlagColor.php
+++ b/src/applications/flag/constants/PhabricatorFlagColor.php
@@ -1,51 +1,35 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFlagColor extends PhabricatorFlagConstants {
const COLOR_RED = 0;
const COLOR_ORANGE = 1;
const COLOR_YELLOW = 2;
const COLOR_GREEN = 3;
const COLOR_BLUE = 4;
const COLOR_PINK = 5;
const COLOR_PURPLE = 6;
const COLOR_CHECKERED = 7;
public static function getColorNameMap() {
return array(
self::COLOR_RED => 'Red',
self::COLOR_ORANGE => 'Orange',
self::COLOR_YELLOW => 'Yellow',
self::COLOR_GREEN => 'Green',
self::COLOR_BLUE => 'Blue',
self::COLOR_PINK => 'Pink',
self::COLOR_PURPLE => 'Purple',
self::COLOR_CHECKERED => 'Checkered',
);
}
public static function getColorName($color) {
return idx(self::getColorNameMap(), $color, 'Unknown');
}
public static function getCSSClass($color) {
return 'phabricator-flag-color-'.(int)$color;
}
}
diff --git a/src/applications/flag/constants/PhabricatorFlagConstants.php b/src/applications/flag/constants/PhabricatorFlagConstants.php
index e2d6b52993..040f8353f5 100644
--- a/src/applications/flag/constants/PhabricatorFlagConstants.php
+++ b/src/applications/flag/constants/PhabricatorFlagConstants.php
@@ -1,21 +1,5 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorFlagConstants {
}
diff --git a/src/applications/flag/controller/PhabricatorFlagController.php b/src/applications/flag/controller/PhabricatorFlagController.php
index 4e9cfb1337..5c0818f9cd 100644
--- a/src/applications/flag/controller/PhabricatorFlagController.php
+++ b/src/applications/flag/controller/PhabricatorFlagController.php
@@ -1,35 +1,19 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorFlagController extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('Flag');
$page->setBaseURI('/flag/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph("\xE2\x9A\x90"); // Subtle!
$page->appendChild($view);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
}
diff --git a/src/applications/flag/controller/PhabricatorFlagDeleteController.php b/src/applications/flag/controller/PhabricatorFlagDeleteController.php
index 7db1913ec5..cf1202c251 100644
--- a/src/applications/flag/controller/PhabricatorFlagDeleteController.php
+++ b/src/applications/flag/controller/PhabricatorFlagDeleteController.php
@@ -1,45 +1,29 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFlagDeleteController extends PhabricatorFlagController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$flag = id(new PhabricatorFlag())->load($this->id);
if (!$flag) {
return new Aphront404Response();
}
if ($flag->getOwnerPHID() != $user->getPHID()) {
return new Aphront400Response();
}
$flag->delete();
return id(new AphrontReloadResponse())->setURI('/flag/');
}
}
diff --git a/src/applications/flag/controller/PhabricatorFlagEditController.php b/src/applications/flag/controller/PhabricatorFlagEditController.php
index 16926e0268..17adca8be9 100644
--- a/src/applications/flag/controller/PhabricatorFlagEditController.php
+++ b/src/applications/flag/controller/PhabricatorFlagEditController.php
@@ -1,104 +1,88 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFlagEditController extends PhabricatorFlagController {
private $phid;
public function willProcessRequest(array $data) {
$this->phid = $data['phid'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$phid = $this->phid;
$handle = PhabricatorObjectHandleData::loadOneHandle($phid, $user);
if (!$handle->isComplete()) {
return new Aphront404Response();
}
$flag = PhabricatorFlagQuery::loadUserFlag($user, $phid);
if (!$flag) {
$flag = new PhabricatorFlag();
$flag->setOwnerPHID($user->getPHID());
$flag->setType($handle->getType());
$flag->setObjectPHID($handle->getPHID());
$flag->setReasonPHID($user->getPHID());
}
if ($request->isDialogFormPost()) {
$flag->setColor($request->getInt('color'));
$flag->setNote($request->getStr('note'));
$flag->save();
return id(new AphrontReloadResponse())->setURI('/flag/');
}
$type_name = $handle->getTypeName();
$dialog = new AphrontDialogView();
$dialog->setUser($user);
$dialog->setTitle("Flag {$type_name}");
require_celerity_resource('phabricator-flag-css');
$form = new AphrontFormLayoutView();
$is_new = !$flag->getID();
if ($is_new) {
$form
->appendChild(
"<p>You can flag this {$type_name} if you want to remember to look ".
"at it later.</p><br />");
}
$radio = new AphrontFormRadioButtonControl();
foreach (PhabricatorFlagColor::getColorNameMap() as $color => $text) {
$class = 'phabricator-flag-radio phabricator-flag-color-'.$color;
$radio->addButton($color, $text, '', $class);
}
$form
->appendChild(
$radio
->setName('color')
->setLabel('Flag Color')
->setValue($flag->getColor()))
->appendChild(
id(new AphrontFormTextAreaControl())
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT)
->setName('note')
->setLabel('Note')
->setValue($flag->getNote()));
$dialog->appendChild($form);
$dialog->addCancelButton($handle->getURI());
$dialog->addSubmitButton(
$is_new ? "Flag {$type_name}" : 'Save');
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/flag/controller/PhabricatorFlagListController.php b/src/applications/flag/controller/PhabricatorFlagListController.php
index da784cb920..851e0cf5de 100644
--- a/src/applications/flag/controller/PhabricatorFlagListController.php
+++ b/src/applications/flag/controller/PhabricatorFlagListController.php
@@ -1,54 +1,38 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFlagListController extends PhabricatorFlagController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI('/flag/view/'));
$nav->addFilter('all', 'Flags');
$nav->selectFilter('all', 'all');
$query = new PhabricatorFlagQuery();
$query->withOwnerPHIDs(array($user->getPHID()));
$query->setViewer($user);
$query->needHandles(true);
$flags = $query->execute();
$view = new PhabricatorFlagListView();
$view->setFlags($flags);
$view->setUser($user);
$panel = new AphrontPanelView();
$panel->setHeader('Flags');
$panel->appendChild($view);
$nav->appendChild($panel);
return $this->buildStandardPageResponse(
$nav,
array(
'title' => 'Flags',
));
}
}
diff --git a/src/applications/flag/events/PhabricatorFlagsUIEventListener.php b/src/applications/flag/events/PhabricatorFlagsUIEventListener.php
index 0b3082a97f..1ec6e7f1c0 100644
--- a/src/applications/flag/events/PhabricatorFlagsUIEventListener.php
+++ b/src/applications/flag/events/PhabricatorFlagsUIEventListener.php
@@ -1,70 +1,54 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFlagsUIEventListener extends PhutilEventListener {
public function register() {
$this->listen(PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS);
}
public function handleEvent(PhutilEvent $event) {
switch ($event->getType()) {
case PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS:
$this->handleActionEvent($event);
break;
}
}
private function handleActionEvent($event) {
$user = $event->getUser();
$object = $event->getValue('object');
if (!$object || !$object->getPHID()) {
// If we have no object, or the object doesn't have a PHID yet, we can't
// flag it.
return;
}
$flag = PhabricatorFlagQuery::loadUserFlag($user, $object->getPHID());
if ($flag) {
$color = PhabricatorFlagColor::getColorName($flag->getColor());
$flag_action = id(new PhabricatorActionView())
->setWorkflow(true)
->setHref('/flag/delete/'.$flag->getID().'/')
->setName(phutil_escape_html('Remove '.$color.' Flag'))
->setIcon('flag-'.$flag->getColor());
} else {
$flag_action = id(new PhabricatorActionView())
->setWorkflow(true)
->setHref('/flag/edit/'.$object->getPHID().'/')
->setName('Flag For Later')
->setIcon('flag-ghost');
if (!$user->isLoggedIn()) {
$flag_action->setDisabled(true);
}
}
$actions = $event->getValue('actions');
$actions[] = $flag_action;
$event->setValue('actions', $actions);
}
}
diff --git a/src/applications/flag/query/PhabricatorFlagQuery.php b/src/applications/flag/query/PhabricatorFlagQuery.php
index 63135af808..004c065604 100644
--- a/src/applications/flag/query/PhabricatorFlagQuery.php
+++ b/src/applications/flag/query/PhabricatorFlagQuery.php
@@ -1,179 +1,163 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFlagQuery {
private $ownerPHIDs;
private $types;
private $objectPHIDs;
private $limit;
private $offset;
private $needHandles;
private $needObjects;
private $viewer;
public function setViewer($viewer) {
$this->viewer = $viewer;
return $this;
}
public function withOwnerPHIDs(array $owner_phids) {
$this->ownerPHIDs = $owner_phids;
return $this;
}
public function withTypes(array $types) {
$this->types = $types;
return $this;
}
public function withObjectPHIDs(array $object_phids) {
$this->objectPHIDs = $object_phids;
return $this;
}
public function needHandles($need) {
$this->needHandles = $need;
return $this;
}
public function needObjects($need) {
$this->needObjects = $need;
return $this;
}
public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
public function setOffset($offset) {
$this->offset = $offset;
return $this;
}
public static function loadUserFlag(PhabricatorUser $user, $object_phid) {
// Specifying the type in the query allows us to use a key.
return id(new PhabricatorFlag())->loadOneWhere(
'ownerPHID = %s AND type = %s AND objectPHID = %s',
$user->getPHID(),
phid_get_type($object_phid),
$object_phid);
}
public function execute() {
$table = new PhabricatorFlag();
$conn_r = $table->establishConnection('r');
$where = $this->buildWhereClause($conn_r);
$limit = $this->buildLimitClause($conn_r);
$order = $this->buildOrderClause($conn_r);
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T flag %Q %Q %Q',
$table->getTableName(),
$where,
$order,
$limit);
$flags = $table->loadAllFromArray($data);
if ($this->needHandles || $this->needObjects) {
$phids = ipull($data, 'objectPHID');
$query = new PhabricatorObjectHandleData($phids);
if ($this->viewer) {
$query->setViewer($this->viewer);
}
if ($this->needHandles) {
$handles = $query->loadHandles();
foreach ($flags as $flag) {
$handle = idx($handles, $flag->getObjectPHID());
if ($handle) {
$flag->attachHandle($handle);
}
}
}
if ($this->needObjects) {
$objects = $query->loadObjects();
foreach ($flags as $flag) {
$object = idx($objects, $flag->getObjectPHID());
if ($object) {
$flag->attachObject($object);
}
}
}
}
return $flags;
}
private function buildWhereClause($conn_r) {
$where = array();
if ($this->ownerPHIDs) {
$where[] = qsprintf(
$conn_r,
'flag.ownerPHID IN (%Ls)',
$this->ownerPHIDs);
}
if ($this->types) {
$where[] = qsprintf(
$conn_r,
'flag.type IN (%Ls)',
$this->types);
}
if ($this->objectPHIDs) {
$where[] = qsprintf(
$conn_r,
'flag.objectPHID IN (%Ls)',
$this->objectPHIDs);
}
if ($where) {
return 'WHERE ('.implode(') AND (', $where).')';
} else {
return '';
}
}
private function buildOrderClause($conn_r) {
return 'ORDER BY id DESC';
}
private function buildLimitClause($conn_r) {
if ($this->limit && $this->offset) {
return qsprintf($conn_r, 'LIMIT %d, %d', $this->offset, $this->limit);
} else if ($this->limit) {
return qsprintf($conn_r, 'LIMIT %d', $this->limit);
} else if ($this->offset) {
return qsprintf($conn_r, 'LIMIT %d, %d', $this->offset, PHP_INT_MAX);
} else {
return '';
}
}
}
diff --git a/src/applications/flag/storage/PhabricatorFlag.php b/src/applications/flag/storage/PhabricatorFlag.php
index 3acda770b3..b323170178 100644
--- a/src/applications/flag/storage/PhabricatorFlag.php
+++ b/src/applications/flag/storage/PhabricatorFlag.php
@@ -1,55 +1,39 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFlag extends PhabricatorFlagDAO {
protected $ownerPHID;
protected $type;
protected $objectPHID;
protected $reasonPHID;
protected $color = PhabricatorFlagColor::COLOR_BLUE;
protected $note;
private $handle = false;
private $object = false;
public function getObject() {
if ($this->object === false) {
throw new Exception('Call attachObject() before getObject()!');
}
return $this->object;
}
public function attachObject($object) {
$this->object = $object;
return $this;
}
public function getHandle() {
if ($this->handle === false) {
throw new Exception('Call attachHandle() before getHandle()!');
}
return $this->handle;
}
public function attachHandle(PhabricatorObjectHandle $handle) {
$this->handle = $handle;
return $this;
}
}
diff --git a/src/applications/flag/storage/PhabricatorFlagDAO.php b/src/applications/flag/storage/PhabricatorFlagDAO.php
index d2024e2f26..6265f581ce 100644
--- a/src/applications/flag/storage/PhabricatorFlagDAO.php
+++ b/src/applications/flag/storage/PhabricatorFlagDAO.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorFlagDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'flag';
}
}
diff --git a/src/applications/flag/view/PhabricatorFlagListView.php b/src/applications/flag/view/PhabricatorFlagListView.php
index a3dc1a35de..f01173f2cb 100644
--- a/src/applications/flag/view/PhabricatorFlagListView.php
+++ b/src/applications/flag/view/PhabricatorFlagListView.php
@@ -1,106 +1,90 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFlagListView extends AphrontView {
private $flags;
private $user;
public function setFlags(array $flags) {
assert_instances_of($flags, 'PhabricatorFlag');
$this->flags = $flags;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function render() {
$user = $this->user;
require_celerity_resource('phabricator-flag-css');
$rows = array();
foreach ($this->flags as $flag) {
$class = PhabricatorFlagColor::getCSSClass($flag->getColor());
$rows[] = array(
phutil_render_tag(
'div',
array(
'class' => 'phabricator-flag-icon '.$class,
),
''),
$flag->getHandle()->renderLink(),
phutil_escape_html($flag->getNote()),
phabricator_datetime($flag->getDateCreated(), $user),
phabricator_render_form(
$user,
array(
'method' => 'POST',
'action' => '/flag/edit/'.$flag->getObjectPHID().'/',
'sigil' => 'workflow',
),
phutil_render_tag(
'button',
array(
'class' => 'small grey',
),
'Edit Flag')),
phabricator_render_form(
$user,
array(
'method' => 'POST',
'action' => '/flag/delete/'.$flag->getID().'/',
'sigil' => 'workflow',
),
phutil_render_tag(
'button',
array(
'class' => 'small grey',
),
'Remove Flag')),
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'',
'Flagged Object',
'Note',
'Flagged On',
'',
'',
));
$table->setColumnClasses(
array(
'narrow',
'wrap pri',
'wrap',
'narrow',
'narrow action',
'narrow action',
));
$table->setNoDataString('No flags.');
return $table->render();
}
}
diff --git a/src/applications/harbormaster/storage/HarbormasterDAO.php b/src/applications/harbormaster/storage/HarbormasterDAO.php
index e48d0cb12c..bfe10cd30a 100644
--- a/src/applications/harbormaster/storage/HarbormasterDAO.php
+++ b/src/applications/harbormaster/storage/HarbormasterDAO.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class HarbormasterDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'harbormaster';
}
}
diff --git a/src/applications/harbormaster/storage/HarbormasterObject.php b/src/applications/harbormaster/storage/HarbormasterObject.php
index f4bac23631..0b8f724212 100644
--- a/src/applications/harbormaster/storage/HarbormasterObject.php
+++ b/src/applications/harbormaster/storage/HarbormasterObject.php
@@ -1,35 +1,19 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HarbormasterObject extends HarbormasterDAO {
protected $phid;
protected $name;
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_TOBJ);
}
}
diff --git a/src/applications/harbormaster/storage/HarbormasterScratchTable.php b/src/applications/harbormaster/storage/HarbormasterScratchTable.php
index a0bf76940b..ad58120c85 100644
--- a/src/applications/harbormaster/storage/HarbormasterScratchTable.php
+++ b/src/applications/harbormaster/storage/HarbormasterScratchTable.php
@@ -1,29 +1,13 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* This is just a test table that unit tests can use if they need to test
* generic database operations. It won't change and break tests and stuff, and
* mistakes in test construction or isolation won't impact the application in
* any way.
*/
final class HarbormasterScratchTable extends HarbormasterDAO {
protected $data;
}
diff --git a/src/applications/help/controller/PhabricatorHelpController.php b/src/applications/help/controller/PhabricatorHelpController.php
index 46ef2482c3..a8ddf088fa 100644
--- a/src/applications/help/controller/PhabricatorHelpController.php
+++ b/src/applications/help/controller/PhabricatorHelpController.php
@@ -1,34 +1,18 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorHelpController extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('Help');
$page->setBaseURI('/help/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph('?');
$page->appendChild($view);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
}
diff --git a/src/applications/help/controller/PhabricatorHelpKeyboardShortcutController.php b/src/applications/help/controller/PhabricatorHelpKeyboardShortcutController.php
index e71dfe1390..73c300cc2f 100644
--- a/src/applications/help/controller/PhabricatorHelpKeyboardShortcutController.php
+++ b/src/applications/help/controller/PhabricatorHelpKeyboardShortcutController.php
@@ -1,69 +1,53 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorHelpKeyboardShortcutController
extends PhabricatorHelpController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$keys = $request->getStr('keys');
$keys = json_decode($keys, true);
if (!is_array($keys)) {
return new Aphront400Response();
}
// There have been at least two users asking for a keyboard shortcut to
// close the dialog, so be explicit that escape works since it isn't
// terribly discoverable.
$keys[] = array(
'keys' => array('esc'),
'description' => 'Close any dialog, including this one.',
);
$rows = array();
foreach ($keys as $shortcut) {
$keystrokes = array();
foreach ($shortcut['keys'] as $stroke) {
$keystrokes[] = '<kbd>'.phutil_escape_html($stroke).'</kbd>';
}
$keystrokes = implode(' or ', $keystrokes);
$rows[] =
'<tr>'.
'<th>'.$keystrokes.'</th>'.
'<td>'.phutil_escape_html($shortcut['description']).'</td>'.
'</tr>';
}
$table =
'<table class="keyboard-shortcut-help">'.
implode('', $rows).
'</table>';
$dialog = id(new AphrontDialogView())
->setUser($user)
->setTitle('Keyboard Shortcuts')
->appendChild($table)
->addCancelButton('#', 'Close');
return id(new AphrontDialogResponse())
->setDialog($dialog);
}
}
diff --git a/src/applications/herald/adapter/HeraldCommitAdapter.php b/src/applications/herald/adapter/HeraldCommitAdapter.php
index 85dfcb30fc..b3dbf7ce46 100644
--- a/src/applications/herald/adapter/HeraldCommitAdapter.php
+++ b/src/applications/herald/adapter/HeraldCommitAdapter.php
@@ -1,237 +1,221 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldCommitAdapter extends HeraldObjectAdapter {
protected $diff;
protected $revision;
protected $repository;
protected $commit;
protected $commitData;
protected $emailPHIDs = array();
protected $auditMap = array();
protected $affectedPaths;
protected $affectedRevision;
protected $affectedPackages;
protected $auditNeededPackages;
public function __construct(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit,
PhabricatorRepositoryCommitData $commit_data) {
$this->repository = $repository;
$this->commit = $commit;
$this->commitData = $commit_data;
}
public function getPHID() {
return $this->commit->getPHID();
}
public function getEmailPHIDs() {
return array_keys($this->emailPHIDs);
}
public function getAuditMap() {
return $this->auditMap;
}
public function getHeraldName() {
return
'r'.
$this->repository->getCallsign().
$this->commit->getCommitIdentifier();
}
public function getHeraldTypeName() {
return HeraldContentTypeConfig::CONTENT_TYPE_COMMIT;
}
public function loadAffectedPaths() {
if ($this->affectedPaths === null) {
$result = PhabricatorOwnerPathQuery::loadAffectedPaths(
$this->repository, $this->commit);
$this->affectedPaths = $result;
}
return $this->affectedPaths;
}
public function loadAffectedPackages() {
if ($this->affectedPackages === null) {
$packages = PhabricatorOwnersPackage::loadAffectedPackages(
$this->repository,
$this->loadAffectedPaths());
$this->affectedPackages = $packages;
}
return $this->affectedPackages;
}
public function loadAuditNeededPackage() {
if ($this->auditNeededPackages === null) {
$status_arr = array(
PhabricatorAuditStatusConstants::AUDIT_REQUIRED,
PhabricatorAuditStatusConstants::CONCERNED,
);
$requests = id(new PhabricatorRepositoryAuditRequest())
->loadAllWhere(
"commitPHID = %s AND auditStatus IN (%Ls)",
$this->commit->getPHID(),
$status_arr);
$packages = mpull($requests, 'getAuditorPHID');
$this->auditNeededPackages = $packages;
}
return $this->auditNeededPackages;
}
public function loadDifferentialRevision() {
if ($this->affectedRevision === null) {
$this->affectedRevision = false;
$data = $this->commitData;
$revision_id = $data->getCommitDetail('differential.revisionID');
if ($revision_id) {
$revision = id(new DifferentialRevision())->load($revision_id);
if ($revision) {
$revision->loadRelationships();
$this->affectedRevision = $revision;
}
}
}
return $this->affectedRevision;
}
public function getHeraldField($field) {
$data = $this->commitData;
switch ($field) {
case HeraldFieldConfig::FIELD_BODY:
return $data->getCommitMessage();
case HeraldFieldConfig::FIELD_AUTHOR:
return $data->getCommitDetail('authorPHID');
case HeraldFieldConfig::FIELD_REVIEWER:
return $data->getCommitDetail('reviewerPHID');
case HeraldFieldConfig::FIELD_DIFF_FILE:
return $this->loadAffectedPaths();
case HeraldFieldConfig::FIELD_REPOSITORY:
return $this->repository->getPHID();
case HeraldFieldConfig::FIELD_DIFF_CONTENT:
// TODO!
return null;
/*
try {
$diff = $this->loadDiff();
} catch (Exception $ex) {
// See rE280053 for an example.
return array(
'<<< Failed to load diff, this usually means the change committed '.
'a binary file as text. >>>',
);
}
$dict = array();
$changes = $diff->getChangesets();
$lines = array();
foreach ($changes as $change) {
$lines = array();
foreach ($change->getHunks() as $hunk) {
$lines[] = $hunk->makeChanges();
}
$dict[$change->getTrueFilename()] = implode("\n", $lines);
}
return $dict;
*/
case HeraldFieldConfig::FIELD_AFFECTED_PACKAGE:
$packages = $this->loadAffectedPackages();
return mpull($packages, 'getPHID');
case HeraldFieldConfig::FIELD_AFFECTED_PACKAGE_OWNER:
$packages = $this->loadAffectedPackages();
$owners = PhabricatorOwnersOwner::loadAllForPackages($packages);
return mpull($owners, 'getUserPHID');
case HeraldFieldConfig::FIELD_NEED_AUDIT_FOR_PACKAGE:
return $this->loadAuditNeededPackage();
case HeraldFieldConfig::FIELD_DIFFERENTIAL_REVISION:
$revision = $this->loadDifferentialRevision();
if (!$revision) {
return null;
}
return $revision->getID();
case HeraldFieldConfig::FIELD_DIFFERENTIAL_REVIEWERS:
$revision = $this->loadDifferentialRevision();
if (!$revision) {
return array();
}
return $revision->getReviewers();
case HeraldFieldConfig::FIELD_DIFFERENTIAL_CCS:
$revision = $this->loadDifferentialRevision();
if (!$revision) {
return array();
}
return $revision->getCCPHIDs();
default:
throw new Exception("Invalid field '{$field}'.");
}
}
public function applyHeraldEffects(array $effects) {
assert_instances_of($effects, 'HeraldEffect');
$result = array();
foreach ($effects as $effect) {
$action = $effect->getAction();
switch ($action) {
case HeraldActionConfig::ACTION_NOTHING:
$result[] = new HeraldApplyTranscript(
$effect,
true,
'Great success at doing nothing.');
break;
case HeraldActionConfig::ACTION_EMAIL:
foreach ($effect->getTarget() as $phid) {
$this->emailPHIDs[$phid] = true;
}
$result[] = new HeraldApplyTranscript(
$effect,
true,
'Added address to email targets.');
break;
case HeraldActionConfig::ACTION_AUDIT:
foreach ($effect->getTarget() as $phid) {
if (empty($this->auditMap[$phid])) {
$this->auditMap[$phid] = array();
}
$this->auditMap[$phid][] = $effect->getRuleID();
}
$result[] = new HeraldApplyTranscript(
$effect,
true,
'Triggered an audit.');
break;
case HeraldActionConfig::ACTION_FLAG:
$result[] = parent::applyFlagEffect(
$effect,
$this->commit->getPHID());
break;
default:
throw new Exception("No rules to handle action '{$action}'.");
}
}
return $result;
}
}
diff --git a/src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php b/src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php
index a5d305a0f1..a4be15d361 100644
--- a/src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php
+++ b/src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php
@@ -1,317 +1,301 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldDifferentialRevisionAdapter extends HeraldObjectAdapter {
protected $revision;
protected $diff;
protected $explicitCCs;
protected $explicitReviewers;
protected $forbiddenCCs;
protected $newCCs = array();
protected $remCCs = array();
protected $emailPHIDs = array();
protected $repository;
protected $affectedPackages;
protected $changesets;
public function __construct(
DifferentialRevision $revision,
DifferentialDiff $diff) {
$revision->loadRelationships();
$this->revision = $revision;
$this->diff = $diff;
}
public function setExplicitCCs($explicit_ccs) {
$this->explicitCCs = $explicit_ccs;
return $this;
}
public function setExplicitReviewers($explicit_reviewers) {
$this->explicitReviewers = $explicit_reviewers;
return $this;
}
public function setForbiddenCCs($forbidden_ccs) {
$this->forbiddenCCs = $forbidden_ccs;
return $this;
}
public function getCCsAddedByHerald() {
return array_diff_key($this->newCCs, $this->remCCs);
}
public function getCCsRemovedByHerald() {
return $this->remCCs;
}
public function getEmailPHIDsAddedByHerald() {
return $this->emailPHIDs;
}
public function getPHID() {
return $this->revision->getPHID();
}
public function getHeraldName() {
return $this->revision->getTitle();
}
public function getHeraldTypeName() {
return HeraldContentTypeConfig::CONTENT_TYPE_DIFFERENTIAL;
}
public function loadRepository() {
if ($this->repository === null) {
$diff = $this->diff;
$repository = false;
if ($diff->getRepositoryUUID()) {
$repository = id(new PhabricatorRepository())->loadOneWhere(
'uuid = %s',
$diff->getRepositoryUUID());
}
if (!$repository && $diff->getArcanistProjectPHID()) {
$project = id(new PhabricatorRepositoryArcanistProject())->loadOneWhere(
'phid = %s',
$diff->getArcanistProjectPHID());
if ($project && $project->getRepositoryID()) {
$repository = id(new PhabricatorRepository())->load(
$project->getRepositoryID());
}
}
$this->repository = $repository;
}
return $this->repository;
}
protected function loadChangesets() {
if ($this->changesets === null) {
$this->changesets = $this->diff->loadChangesets();
}
return $this->changesets;
}
protected function loadAffectedPaths() {
$changesets = $this->loadChangesets();
$paths = array();
foreach ($changesets as $changeset) {
$paths[] = $this->getAbsoluteRepositoryPathForChangeset($changeset);
}
return $paths;
}
protected function getAbsoluteRepositoryPathForChangeset(
DifferentialChangeset $changeset) {
$repository = $this->loadRepository();
if (!$repository) {
return '/'.ltrim($changeset->getFilename(), '/');
}
$diff = $this->diff;
return $changeset->getAbsoluteRepositoryPath($repository, $diff);
}
protected function loadContentDictionary() {
$changesets = $this->loadChangesets();
$hunks = array();
if ($changesets) {
$hunks = id(new DifferentialHunk())->loadAllWhere(
'changesetID in (%Ld)',
mpull($changesets, 'getID'));
}
$dict = array();
$hunks = mgroup($hunks, 'getChangesetID');
$changesets = mpull($changesets, null, 'getID');
foreach ($changesets as $id => $changeset) {
$path = $this->getAbsoluteRepositoryPathForChangeset($changeset);
$content = array();
foreach (idx($hunks, $id, array()) as $hunk) {
$content[] = $hunk->makeChanges();
}
$dict[$path] = implode("\n", $content);
}
return $dict;
}
public function loadAffectedPackages() {
if ($this->affectedPackages === null) {
$this->affectedPackages = array();
$repository = $this->loadRepository();
if ($repository) {
$packages = PhabricatorOwnersPackage::loadAffectedPackages(
$repository,
$this->loadAffectedPaths());
$this->affectedPackages = $packages;
}
}
return $this->affectedPackages;
}
public function getHeraldField($field) {
switch ($field) {
case HeraldFieldConfig::FIELD_TITLE:
return $this->revision->getTitle();
break;
case HeraldFieldConfig::FIELD_BODY:
return $this->revision->getSummary()."\n".
$this->revision->getTestPlan();
break;
case HeraldFieldConfig::FIELD_AUTHOR:
return $this->revision->getAuthorPHID();
break;
case HeraldFieldConfig::FIELD_DIFF_FILE:
return $this->loadAffectedPaths();
case HeraldFieldConfig::FIELD_CC:
if (isset($this->explicitCCs)) {
return array_keys($this->explicitCCs);
} else {
return $this->revision->getCCPHIDs();
}
case HeraldFieldConfig::FIELD_REVIEWERS:
if (isset($this->explicitReviewers)) {
return array_keys($this->explicitReviewers);
} else {
return $this->revision->getReviewers();
}
case HeraldFieldConfig::FIELD_REPOSITORY:
$repository = $this->loadRepository();
if (!$repository) {
return null;
}
return $repository->getPHID();
case HeraldFieldConfig::FIELD_DIFF_CONTENT:
return $this->loadContentDictionary();
case HeraldFieldConfig::FIELD_AFFECTED_PACKAGE:
$packages = $this->loadAffectedPackages();
return mpull($packages, 'getPHID');
case HeraldFieldConfig::FIELD_AFFECTED_PACKAGE_OWNER:
$packages = $this->loadAffectedPackages();
return PhabricatorOwnersOwner::loadAffiliatedUserPHIDs(
mpull($packages, 'getID'));
default:
throw new Exception("Invalid field '{$field}'.");
}
}
public function applyHeraldEffects(array $effects) {
assert_instances_of($effects, 'HeraldEffect');
$result = array();
if ($this->explicitCCs) {
$effect = new HeraldEffect();
$effect->setAction(HeraldActionConfig::ACTION_ADD_CC);
$effect->setTarget(array_keys($this->explicitCCs));
$effect->setReason(
'CCs provided explicitly by revision author or carried over from a '.
'previous version of the revision.');
$result[] = new HeraldApplyTranscript(
$effect,
true,
'Added addresses to CC list.');
}
$forbidden_ccs = array_fill_keys(
nonempty($this->forbiddenCCs, array()),
true);
foreach ($effects as $effect) {
$action = $effect->getAction();
switch ($action) {
case HeraldActionConfig::ACTION_NOTHING:
$result[] = new HeraldApplyTranscript(
$effect,
true,
'OK, did nothing.');
break;
case HeraldActionConfig::ACTION_FLAG:
$result[] = parent::applyFlagEffect(
$effect,
$this->revision->getPHID());
break;
case HeraldActionConfig::ACTION_EMAIL:
case HeraldActionConfig::ACTION_ADD_CC:
$op = ($action == HeraldActionConfig::ACTION_EMAIL) ? 'email' : 'CC';
$base_target = $effect->getTarget();
$forbidden = array();
foreach ($base_target as $key => $fbid) {
if (isset($forbidden_ccs[$fbid])) {
$forbidden[] = $fbid;
unset($base_target[$key]);
} else {
if ($action == HeraldActionConfig::ACTION_EMAIL) {
$this->emailPHIDs[$fbid] = true;
} else {
$this->newCCs[$fbid] = true;
}
}
}
if ($forbidden) {
$failed = clone $effect;
$failed->setTarget($forbidden);
if ($base_target) {
$effect->setTarget($base_target);
$result[] = new HeraldApplyTranscript(
$effect,
true,
'Added these addresses to '.$op.' list. '.
'Others could not be added.');
}
$result[] = new HeraldApplyTranscript(
$failed,
false,
$op.' forbidden, these addresses have unsubscribed.');
} else {
$result[] = new HeraldApplyTranscript(
$effect,
true,
'Added addresses to '.$op.' list.');
}
break;
case HeraldActionConfig::ACTION_REMOVE_CC:
foreach ($effect->getTarget() as $fbid) {
$this->remCCs[$fbid] = true;
}
$result[] = new HeraldApplyTranscript(
$effect,
true,
'Removed addresses from CC list.');
break;
default:
throw new Exception("No rules to handle action '{$action}'.");
}
}
return $result;
}
}
diff --git a/src/applications/herald/adapter/HeraldDryRunAdapter.php b/src/applications/herald/adapter/HeraldDryRunAdapter.php
index fcb68c01b6..4c58374578 100644
--- a/src/applications/herald/adapter/HeraldDryRunAdapter.php
+++ b/src/applications/herald/adapter/HeraldDryRunAdapter.php
@@ -1,48 +1,32 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldDryRunAdapter extends HeraldObjectAdapter {
public function getPHID() {
return 0;
}
public function getHeraldName() {
return 'Dry Run';
}
public function getHeraldTypeName() {
return null;
}
public function getHeraldField($field) {
return null;
}
public function applyHeraldEffects(array $effects) {
assert_instances_of($effects, 'HeraldEffect');
$results = array();
foreach ($effects as $effect) {
$results[] = new HeraldApplyTranscript(
$effect,
false,
'This was a dry run, so no actions were actually taken.');
}
return $results;
}
}
diff --git a/src/applications/herald/adapter/HeraldObjectAdapter.php b/src/applications/herald/adapter/HeraldObjectAdapter.php
index e3d97c21e1..87c6b22f2f 100644
--- a/src/applications/herald/adapter/HeraldObjectAdapter.php
+++ b/src/applications/herald/adapter/HeraldObjectAdapter.php
@@ -1,65 +1,49 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class HeraldObjectAdapter {
abstract public function getPHID();
abstract public function getHeraldName();
abstract public function getHeraldTypeName();
abstract public function getHeraldField($field_name);
abstract public function applyHeraldEffects(array $effects);
public static function applyFlagEffect(HeraldEffect $effect, $phid) {
$color = $effect->getTarget();
// TODO: Silly that we need to load this again here.
$rule = id(new HeraldRule())->load($effect->getRuleID());
$user = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$rule->getAuthorPHID());
$flag = PhabricatorFlagQuery::loadUserFlag($user, $phid);
if ($flag) {
return new HeraldApplyTranscript(
$effect,
false,
'Object already flagged.');
}
$handle = PhabricatorObjectHandleData::loadOneHandle($phid);
$flag = new PhabricatorFlag();
$flag->setOwnerPHID($user->getPHID());
$flag->setType($handle->getType());
$flag->setObjectPHID($handle->getPHID());
// TOOD: Should really be transcript PHID, but it doesn't exist yet.
$flag->setReasonPHID($user->getPHID());
$flag->setColor($color);
$flag->setNote('Flagged by Herald Rule "'.$rule->getName().'".');
$flag->save();
return new HeraldApplyTranscript(
$effect,
true,
'Added flag.');
}
}
diff --git a/src/applications/herald/application/PhabricatorApplicationHerald.php b/src/applications/herald/application/PhabricatorApplicationHerald.php
index 6cd96b8f92..8b456deb7b 100644
--- a/src/applications/herald/application/PhabricatorApplicationHerald.php
+++ b/src/applications/herald/application/PhabricatorApplicationHerald.php
@@ -1,68 +1,52 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationHerald extends PhabricatorApplication {
public function getBaseURI() {
return '/herald/';
}
public function getAutospriteName() {
return 'herald';
}
public function getShortDescription() {
return 'Create Notification Rules';
}
public function getTitleGlyph() {
return "\xE2\x98\xBF";
}
public function getHelpURI() {
return PhabricatorEnv::getDoclink('article/Herald_User_Guide.html');
}
public function getFlavorText() {
return pht('Watch for danger!');
}
public function getApplicationGroup() {
return self::GROUP_ORGANIZATION;
}
public function getRoutes() {
return array(
'/herald/' => array(
'' => 'HeraldHomeController',
'view/(?P<content_type>[^/]+)/(?:(?P<rule_type>[^/]+)/)?'
=> 'HeraldHomeController',
'new/(?:(?P<type>[^/]+)/(?:(?P<rule_type>[^/]+)/)?)?'
=> 'HeraldNewController',
'rule/(?:(?P<id>[1-9]\d*)/)?' => 'HeraldRuleController',
'history/(?:(?P<id>[1-9]\d*)/)?' => 'HeraldRuleEditHistoryController',
'delete/(?P<id>[1-9]\d*)/' => 'HeraldDeleteController',
'test/' => 'HeraldTestConsoleController',
'transcript/' => 'HeraldTranscriptListController',
'transcript/(?P<id>[1-9]\d*)/(?:(?P<filter>\w+)/)?'
=> 'HeraldTranscriptController',
),
);
}
}
diff --git a/src/applications/herald/config/HeraldActionConfig.php b/src/applications/herald/config/HeraldActionConfig.php
index 57bce933f5..ea7a8d3cb4 100644
--- a/src/applications/herald/config/HeraldActionConfig.php
+++ b/src/applications/herald/config/HeraldActionConfig.php
@@ -1,152 +1,136 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldActionConfig {
const ACTION_ADD_CC = 'addcc';
const ACTION_REMOVE_CC = 'remcc';
const ACTION_EMAIL = 'email';
const ACTION_NOTHING = 'nothing';
const ACTION_AUDIT = 'audit';
const ACTION_FLAG = 'flag';
public static function getActionMessageMapForRuleType($rule_type) {
$generic_mappings = array(
self::ACTION_NOTHING => 'Do nothing',
self::ACTION_ADD_CC => 'Add emails to CC',
self::ACTION_REMOVE_CC => 'Remove emails from CC',
self::ACTION_EMAIL => 'Send an email to',
self::ACTION_AUDIT => 'Trigger an Audit',
self::ACTION_FLAG => 'Mark with flag',
);
switch ($rule_type) {
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
$specific_mappings = array(
self::ACTION_AUDIT => 'Trigger an Audit for project',
);
break;
case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
$specific_mappings = array(
self::ACTION_ADD_CC => 'CC me',
self::ACTION_REMOVE_CC => 'Remove me from CC',
self::ACTION_EMAIL => 'Email me',
self::ACTION_AUDIT => 'Trigger an Audit by me',
);
break;
case null:
$specific_mappings = array();
// Use generic mappings, used on transcript.
break;
default:
throw new Exception("Unknown rule type '${rule_type}'");
}
return $specific_mappings + $generic_mappings;
}
public static function getActionMessageMap($content_type,
$rule_type) {
$map = self::getActionMessageMapForRuleType($rule_type);
switch ($content_type) {
case HeraldContentTypeConfig::CONTENT_TYPE_DIFFERENTIAL:
return array_select_keys(
$map,
array(
self::ACTION_ADD_CC,
self::ACTION_REMOVE_CC,
self::ACTION_EMAIL,
self::ACTION_FLAG,
self::ACTION_NOTHING,
));
case HeraldContentTypeConfig::CONTENT_TYPE_COMMIT:
return array_select_keys(
$map,
array(
self::ACTION_EMAIL,
self::ACTION_AUDIT,
self::ACTION_FLAG,
self::ACTION_NOTHING,
));
case HeraldContentTypeConfig::CONTENT_TYPE_MERGE:
return array_select_keys(
$map,
array(
self::ACTION_EMAIL,
self::ACTION_NOTHING,
));
case HeraldContentTypeConfig::CONTENT_TYPE_OWNERS:
return array_select_keys(
$map,
array(
self::ACTION_EMAIL,
self::ACTION_NOTHING,
));
default:
throw new Exception("Unknown content type '{$content_type}'.");
}
}
/**
* Create a HeraldAction to save from data.
*
* $data is of the form:
* array(
* 0 => <action type>
* 1 => array(<targets>)
* )
*/
public static function willSaveAction($rule_type,
$author_phid,
$data) {
$obj = new HeraldAction();
$obj->setAction($data[0]);
// for personal rule types, set the target to be the owner of the rule
if ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL) {
switch ($obj->getAction()) {
case HeraldActionConfig::ACTION_EMAIL:
case HeraldActionConfig::ACTION_ADD_CC:
case HeraldActionConfig::ACTION_REMOVE_CC:
case HeraldActionConfig::ACTION_AUDIT:
$data[1] = array($author_phid => $author_phid);
break;
case HeraldActionConfig::ACTION_FLAG:
// Make sure flag color is valid; set to blue if not.
$color_map = PhabricatorFlagColor::getColorNameMap();
if (empty($color_map[$data[1]])) {
$data[1] = PhabricatorFlagColor::COLOR_BLUE;
}
break;
case HeraldActionConfig::ACTION_NOTHING:
break;
default:
throw new Exception('Unrecognized action type: ' .
$obj->getAction());
}
}
if (is_array($data[1])) {
$obj->setTarget(array_keys($data[1]));
} else {
$obj->setTarget($data[1]);
}
return $obj;
}
}
diff --git a/src/applications/herald/config/HeraldConditionConfig.php b/src/applications/herald/config/HeraldConditionConfig.php
index 9f6729bd6c..55fc1e9718 100644
--- a/src/applications/herald/config/HeraldConditionConfig.php
+++ b/src/applications/herald/config/HeraldConditionConfig.php
@@ -1,142 +1,126 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldConditionConfig {
const CONDITION_CONTAINS = 'contains';
const CONDITION_NOT_CONTAINS = '!contains';
const CONDITION_IS = 'is';
const CONDITION_IS_NOT = '!is';
const CONDITION_IS_ANY = 'isany';
const CONDITION_IS_NOT_ANY = '!isany';
const CONDITION_INCLUDE_ALL = 'all';
const CONDITION_INCLUDE_ANY = 'any';
const CONDITION_INCLUDE_NONE = 'none';
const CONDITION_IS_ME = 'me';
const CONDITION_IS_NOT_ME = '!me';
const CONDITION_REGEXP = 'regexp';
const CONDITION_RULE = 'conditions';
const CONDITION_NOT_RULE = '!conditions';
const CONDITION_EXISTS = 'exists';
const CONDITION_NOT_EXISTS = '!exists';
const CONDITION_REGEXP_PAIR = 'regexp-pair';
public static function getConditionMap() {
static $map = array(
self::CONDITION_CONTAINS => 'contains',
self::CONDITION_NOT_CONTAINS => 'does not contain',
self::CONDITION_IS => 'is',
self::CONDITION_IS_NOT => 'is not',
self::CONDITION_IS_ANY => 'is any of',
self::CONDITION_IS_NOT_ANY => 'is not any of',
self::CONDITION_INCLUDE_ALL => 'include all of',
self::CONDITION_INCLUDE_ANY => 'include any of',
self::CONDITION_INCLUDE_NONE => 'include none of',
self::CONDITION_IS_ME => 'is myself',
self::CONDITION_IS_NOT_ME => 'is not myself',
self::CONDITION_REGEXP => 'matches regexp',
self::CONDITION_RULE => 'matches:',
self::CONDITION_NOT_RULE => 'does not match:',
self::CONDITION_EXISTS => 'exists',
self::CONDITION_NOT_EXISTS => 'does not exist',
self::CONDITION_REGEXP_PAIR => 'matches regexp pair',
);
return $map;
}
public static function getConditionMapForField($field) {
$map = self::getConditionMap();
switch ($field) {
case HeraldFieldConfig::FIELD_TITLE:
case HeraldFieldConfig::FIELD_BODY:
return array_select_keys(
$map,
array(
self::CONDITION_CONTAINS,
self::CONDITION_NOT_CONTAINS,
self::CONDITION_IS,
self::CONDITION_IS_NOT,
self::CONDITION_REGEXP,
));
case HeraldFieldConfig::FIELD_AUTHOR:
case HeraldFieldConfig::FIELD_REPOSITORY:
case HeraldFieldConfig::FIELD_REVIEWER:
case HeraldFieldConfig::FIELD_MERGE_REQUESTER:
return array_select_keys(
$map,
array(
self::CONDITION_IS_ANY,
self::CONDITION_IS_NOT_ANY,
));
case HeraldFieldConfig::FIELD_TAGS:
case HeraldFieldConfig::FIELD_REVIEWERS:
case HeraldFieldConfig::FIELD_CC:
case HeraldFieldConfig::FIELD_DIFFERENTIAL_REVIEWERS:
case HeraldFieldConfig::FIELD_DIFFERENTIAL_CCS:
return array_select_keys(
$map,
array(
self::CONDITION_INCLUDE_ALL,
self::CONDITION_INCLUDE_ANY,
self::CONDITION_INCLUDE_NONE,
));
case HeraldFieldConfig::FIELD_DIFF_FILE:
return array_select_keys(
$map,
array(
self::CONDITION_CONTAINS,
self::CONDITION_REGEXP,
));
case HeraldFieldConfig::FIELD_DIFF_CONTENT:
return array_select_keys(
$map,
array(
self::CONDITION_CONTAINS,
self::CONDITION_REGEXP,
self::CONDITION_REGEXP_PAIR,
));
case HeraldFieldConfig::FIELD_RULE:
return array_select_keys(
$map,
array(
self::CONDITION_RULE,
self::CONDITION_NOT_RULE,
));
case HeraldFieldConfig::FIELD_AFFECTED_PACKAGE:
case HeraldFieldConfig::FIELD_AFFECTED_PACKAGE_OWNER:
case HeraldFieldConfig::FIELD_NEED_AUDIT_FOR_PACKAGE:
return array_select_keys(
$map,
array(
self::CONDITION_INCLUDE_ANY,
self::CONDITION_INCLUDE_NONE,
));
case HeraldFieldConfig::FIELD_DIFFERENTIAL_REVISION:
return array_select_keys(
$map,
array(
self::CONDITION_EXISTS,
self::CONDITION_NOT_EXISTS,
));
default:
throw new Exception("Unknown field type '{$field}'.");
}
}
}
diff --git a/src/applications/herald/config/HeraldContentTypeConfig.php b/src/applications/herald/config/HeraldContentTypeConfig.php
index 3def39c2b4..364a6f0912 100644
--- a/src/applications/herald/config/HeraldContentTypeConfig.php
+++ b/src/applications/herald/config/HeraldContentTypeConfig.php
@@ -1,37 +1,21 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldContentTypeConfig {
const CONTENT_TYPE_DIFFERENTIAL = 'differential';
const CONTENT_TYPE_COMMIT = 'commit';
const CONTENT_TYPE_MERGE = 'merge';
const CONTENT_TYPE_OWNERS = 'owners';
public static function getContentTypeMap() {
static $map = array(
self::CONTENT_TYPE_DIFFERENTIAL => 'Differential Revisions',
self::CONTENT_TYPE_COMMIT => 'Commits',
/* TODO: Deal with this
self::CONTENT_TYPE_MERGE => 'Merge Requests',
self::CONTENT_TYPE_OWNERS => 'Owners Changes',
*/
);
return $map;
}
}
diff --git a/src/applications/herald/config/HeraldFieldConfig.php b/src/applications/herald/config/HeraldFieldConfig.php
index 8f2abb5b99..d2273e27ac 100644
--- a/src/applications/herald/config/HeraldFieldConfig.php
+++ b/src/applications/herald/config/HeraldFieldConfig.php
@@ -1,134 +1,118 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldFieldConfig {
const FIELD_TITLE = 'title';
const FIELD_BODY = 'body';
const FIELD_AUTHOR = 'author';
const FIELD_REVIEWER = 'reviewer';
const FIELD_REVIEWERS = 'reviewers';
const FIELD_CC = 'cc';
const FIELD_TAGS = 'tags';
const FIELD_DIFF_FILE = 'diff-file';
const FIELD_DIFF_CONTENT = 'diff-content';
const FIELD_REPOSITORY = 'repository';
const FIELD_RULE = 'rule';
const FIELD_AFFECTED_PACKAGE = 'affected-package';
const FIELD_AFFECTED_PACKAGE_OWNER = 'affected-package-owner';
const FIELD_NEED_AUDIT_FOR_PACKAGE = 'need-audit-for-package';
const FIELD_DIFFERENTIAL_REVISION = 'differential-revision';
const FIELD_DIFFERENTIAL_REVIEWERS = 'differential-reviewers';
const FIELD_DIFFERENTIAL_CCS = 'differential-ccs';
const FIELD_MERGE_REQUESTER = 'merge-requester';
public static function getFieldMap() {
static $map = array(
self::FIELD_TITLE => 'Title',
self::FIELD_BODY => 'Body',
self::FIELD_AUTHOR => 'Author',
self::FIELD_REVIEWER => 'Reviewer',
self::FIELD_REVIEWERS => 'Reviewers',
self::FIELD_CC => 'CCs',
self::FIELD_TAGS => 'Tags',
self::FIELD_DIFF_FILE => 'Any changed filename',
self::FIELD_DIFF_CONTENT => 'Any changed file content',
self::FIELD_REPOSITORY => 'Repository',
self::FIELD_RULE => 'Another Herald rule',
self::FIELD_AFFECTED_PACKAGE => 'Any affected package',
self::FIELD_AFFECTED_PACKAGE_OWNER => "Any affected package's owner",
self::FIELD_NEED_AUDIT_FOR_PACKAGE =>
'Affected packages that need audit',
self::FIELD_DIFFERENTIAL_REVISION => 'Differential revision',
self::FIELD_DIFFERENTIAL_REVIEWERS => 'Differential reviewers',
self::FIELD_DIFFERENTIAL_CCS => 'Differential CCs',
self::FIELD_MERGE_REQUESTER => 'Merge requester'
);
return $map;
}
public static function getFieldMapForContentType($type) {
$map = self::getFieldMap();
switch ($type) {
case HeraldContentTypeConfig::CONTENT_TYPE_DIFFERENTIAL:
return array_select_keys(
$map,
array(
self::FIELD_TITLE,
self::FIELD_BODY,
self::FIELD_AUTHOR,
self::FIELD_REVIEWERS,
self::FIELD_CC,
self::FIELD_REPOSITORY,
self::FIELD_DIFF_FILE,
self::FIELD_DIFF_CONTENT,
self::FIELD_RULE,
self::FIELD_AFFECTED_PACKAGE,
self::FIELD_AFFECTED_PACKAGE_OWNER,
));
case HeraldContentTypeConfig::CONTENT_TYPE_COMMIT:
return array_select_keys(
$map,
array(
self::FIELD_BODY,
self::FIELD_AUTHOR,
self::FIELD_REVIEWER,
self::FIELD_REPOSITORY,
self::FIELD_DIFF_FILE,
self::FIELD_DIFF_CONTENT,
self::FIELD_RULE,
self::FIELD_AFFECTED_PACKAGE,
self::FIELD_AFFECTED_PACKAGE_OWNER,
self::FIELD_NEED_AUDIT_FOR_PACKAGE,
self::FIELD_DIFFERENTIAL_REVISION,
self::FIELD_DIFFERENTIAL_REVIEWERS,
self::FIELD_DIFFERENTIAL_CCS,
));
case HeraldContentTypeConfig::CONTENT_TYPE_MERGE:
return array_select_keys(
$map,
array(
self::FIELD_BODY,
self::FIELD_AUTHOR,
self::FIELD_REVIEWER,
self::FIELD_REPOSITORY,
self::FIELD_DIFF_FILE,
self::FIELD_DIFF_CONTENT,
self::FIELD_RULE,
self::FIELD_AFFECTED_PACKAGE,
self::FIELD_AFFECTED_PACKAGE_OWNER,
self::FIELD_DIFFERENTIAL_REVISION,
self::FIELD_DIFFERENTIAL_REVIEWERS,
self::FIELD_DIFFERENTIAL_CCS,
self::FIELD_MERGE_REQUESTER,
));
case HeraldContentTypeConfig::CONTENT_TYPE_OWNERS:
return array_select_keys(
$map,
array(
self::FIELD_AFFECTED_PACKAGE,
self::FIELD_AFFECTED_PACKAGE_OWNER,
));
default:
throw new Exception("Unknown content type.");
}
}
}
diff --git a/src/applications/herald/config/HeraldRepetitionPolicyConfig.php b/src/applications/herald/config/HeraldRepetitionPolicyConfig.php
index 5d241d9a6e..2e643021d6 100644
--- a/src/applications/herald/config/HeraldRepetitionPolicyConfig.php
+++ b/src/applications/herald/config/HeraldRepetitionPolicyConfig.php
@@ -1,64 +1,48 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldRepetitionPolicyConfig {
const FIRST = 'first'; // only execute the first time (no repeating)
const EVERY = 'every'; // repeat every time
private static $policyIntMap = array(
self::FIRST => 0,
self::EVERY => 1,
);
private static $policyMap = array(
self::FIRST => 'only the first time',
self::EVERY => 'every time',
);
public static function getMap() {
return self::$policyMap;
}
public static function getMapForContentType($type) {
switch ($type) {
case HeraldContentTypeConfig::CONTENT_TYPE_DIFFERENTIAL:
return array_select_keys(
self::$policyMap,
array(
self::EVERY,
self::FIRST,
));
case HeraldContentTypeConfig::CONTENT_TYPE_COMMIT:
case HeraldContentTypeConfig::CONTENT_TYPE_MERGE:
case HeraldContentTypeConfig::CONTENT_TYPE_OWNERS:
return array();
default:
throw new Exception("Unknown content type '{$type}'.");
}
}
public static function toInt($str) {
return idx(self::$policyIntMap, $str, self::$policyIntMap[self::EVERY]);
}
public static function toString($int) {
return idx(array_flip(self::$policyIntMap), $int, self::EVERY);
}
}
diff --git a/src/applications/herald/config/HeraldRuleTypeConfig.php b/src/applications/herald/config/HeraldRuleTypeConfig.php
index 9e5091d164..7d9e4812f8 100644
--- a/src/applications/herald/config/HeraldRuleTypeConfig.php
+++ b/src/applications/herald/config/HeraldRuleTypeConfig.php
@@ -1,31 +1,15 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldRuleTypeConfig {
const RULE_TYPE_GLOBAL = 'global';
const RULE_TYPE_PERSONAL = 'personal';
public static function getRuleTypeMap() {
static $map = array(
self::RULE_TYPE_GLOBAL => 'Global',
self::RULE_TYPE_PERSONAL => 'Personal',
);
return $map;
}
}
diff --git a/src/applications/herald/config/HeraldValueTypeConfig.php b/src/applications/herald/config/HeraldValueTypeConfig.php
index 7d53ff523a..15bf939715 100644
--- a/src/applications/herald/config/HeraldValueTypeConfig.php
+++ b/src/applications/herald/config/HeraldValueTypeConfig.php
@@ -1,114 +1,98 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldValueTypeConfig {
const VALUE_TEXT = 'text';
const VALUE_NONE = 'none';
const VALUE_EMAIL = 'email';
const VALUE_USER = 'user';
const VALUE_TAG = 'tag';
const VALUE_RULE = 'rule';
const VALUE_REPOSITORY = 'repository';
const VALUE_OWNERS_PACKAGE = 'package';
const VALUE_PROJECT = 'project';
const VALUE_FLAG_COLOR = 'flagcolor';
public static function getValueTypeForFieldAndCondition($field, $condition) {
switch ($condition) {
case HeraldConditionConfig::CONDITION_CONTAINS:
case HeraldConditionConfig::CONDITION_NOT_CONTAINS:
case HeraldConditionConfig::CONDITION_IS:
case HeraldConditionConfig::CONDITION_IS_NOT:
case HeraldConditionConfig::CONDITION_REGEXP:
case HeraldConditionConfig::CONDITION_REGEXP_PAIR:
return self::VALUE_TEXT;
case HeraldConditionConfig::CONDITION_IS_ANY:
case HeraldConditionConfig::CONDITION_IS_NOT_ANY:
switch ($field) {
case HeraldFieldConfig::FIELD_REPOSITORY:
return self::VALUE_REPOSITORY;
default:
return self::VALUE_USER;
}
break;
case HeraldConditionConfig::CONDITION_INCLUDE_ALL:
case HeraldConditionConfig::CONDITION_INCLUDE_ANY:
case HeraldConditionConfig::CONDITION_INCLUDE_NONE:
switch ($field) {
case HeraldFieldConfig::FIELD_REPOSITORY:
return self::VALUE_REPOSITORY;
case HeraldFieldConfig::FIELD_CC:
case HeraldFieldConfig::FIELD_DIFFERENTIAL_CCS:
return self::VALUE_EMAIL;
case HeraldFieldConfig::FIELD_TAGS:
return self::VALUE_TAG;
case HeraldFieldConfig::FIELD_AFFECTED_PACKAGE:
case HeraldFieldConfig::FIELD_NEED_AUDIT_FOR_PACKAGE:
return self::VALUE_OWNERS_PACKAGE;
default:
return self::VALUE_USER;
}
break;
case HeraldConditionConfig::CONDITION_IS_ME:
case HeraldConditionConfig::CONDITION_IS_NOT_ME:
case HeraldConditionConfig::CONDITION_EXISTS:
case HeraldConditionConfig::CONDITION_NOT_EXISTS:
return self::VALUE_NONE;
case HeraldConditionConfig::CONDITION_RULE:
case HeraldConditionConfig::CONDITION_NOT_RULE:
return self::VALUE_RULE;
default:
throw new Exception("Unknown condition.");
}
}
public static function getValueTypeForAction($action, $rule_type) {
$is_personal = ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);
if ($is_personal) {
switch ($action) {
case HeraldActionConfig::ACTION_ADD_CC:
case HeraldActionConfig::ACTION_REMOVE_CC:
case HeraldActionConfig::ACTION_EMAIL:
case HeraldActionConfig::ACTION_NOTHING:
case HeraldActionConfig::ACTION_AUDIT:
return self::VALUE_NONE;
case HeraldActionConfig::ACTION_FLAG:
return self::VALUE_FLAG_COLOR;
default:
throw new Exception("Unknown or invalid action '{$action}'.");
}
} else {
switch ($action) {
case HeraldActionConfig::ACTION_ADD_CC:
case HeraldActionConfig::ACTION_REMOVE_CC:
case HeraldActionConfig::ACTION_EMAIL:
return self::VALUE_EMAIL;
case HeraldActionConfig::ACTION_NOTHING:
return self::VALUE_NONE;
case HeraldActionConfig::ACTION_AUDIT:
return self::VALUE_PROJECT;
case HeraldActionConfig::ACTION_FLAG:
return self::VALUE_FLAG_COLOR;
default:
throw new Exception("Unknown or invalid action '{$action}'.");
}
}
}
}
diff --git a/src/applications/herald/controller/HeraldController.php b/src/applications/herald/controller/HeraldController.php
index ef246503fd..2f7fb64314 100644
--- a/src/applications/herald/controller/HeraldController.php
+++ b/src/applications/herald/controller/HeraldController.php
@@ -1,73 +1,57 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class HeraldController extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('Herald');
$page->setBaseURI('/herald/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph("\xE2\x98\xBF");
$page->appendChild($view);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
protected function renderNav() {
$nav = id(new AphrontSideNavFilterView())
->setBaseURI(new PhutilURI('/herald/'))
->addLabel('My Rules')
->addFilter('new', 'Create Rule');
$rules_map = HeraldContentTypeConfig::getContentTypeMap();
foreach ($rules_map as $key => $value) {
$nav->addFilter("view/{$key}/personal", $value);
}
$nav
->addSpacer()
->addLabel('Global Rules');
foreach ($rules_map as $key => $value) {
$nav->addFilter("view/{$key}/global", $value);
}
$nav
->addSpacer()
->addLabel('Utilities')
->addFilter('test', 'Test Console')
->addFilter('transcript', 'Transcripts')
->addFilter('history', 'Edit Log');
if ($this->getRequest()->getUser()->getIsAdmin()) {
$nav
->addSpacer()
->addLabel('Admin');
foreach ($rules_map as $key => $value) {
$nav->addFilter("view/{$key}/all", $value);
}
}
return $nav;
}
}
diff --git a/src/applications/herald/controller/HeraldDeleteController.php b/src/applications/herald/controller/HeraldDeleteController.php
index 1e680894fe..179ad8c838 100644
--- a/src/applications/herald/controller/HeraldDeleteController.php
+++ b/src/applications/herald/controller/HeraldDeleteController.php
@@ -1,73 +1,57 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldDeleteController extends HeraldController {
private $id;
public function getFilter() {
// note this controller is only used from a dialog-context at the moment
// and there is actually no "delete" filter
return 'delete';
}
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$rule = id(new HeraldRule())->load($this->id);
if (!$rule) {
return new Aphront404Response();
}
$request = $this->getRequest();
$user = $request->getUser();
// Anyone can delete a global rule, but only the rule owner can delete a
// personal one.
if ($rule->getRuleType() == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL) {
if ($user->getPHID() != $rule->getAuthorPHID()) {
return new Aphront400Response();
}
}
if ($request->isFormPost()) {
$rule->openTransaction();
$rule->logEdit($user->getPHID(), 'delete');
$rule->delete();
$rule->saveTransaction();
return id(new AphrontReloadResponse())->setURI('/herald/');
}
$dialog = new AphrontDialogView();
$dialog->setUser($request->getUser());
$dialog->setTitle('Really delete this rule?');
$dialog->appendChild(
"Are you sure you want to delete the rule ".
"'<strong>".phutil_escape_html($rule->getName())."</strong>'?");
$dialog->addSubmitButton('Delete');
$dialog->addCancelButton('/herald/');
$dialog->setSubmitURI($request->getPath());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/herald/controller/HeraldHomeController.php b/src/applications/herald/controller/HeraldHomeController.php
index 9b6e6e87b3..b47928f2ab 100644
--- a/src/applications/herald/controller/HeraldHomeController.php
+++ b/src/applications/herald/controller/HeraldHomeController.php
@@ -1,172 +1,156 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldHomeController extends HeraldController {
private $contentType;
private $ruleType;
public function willProcessRequest(array $data) {
$this->contentType = idx($data, 'content_type');
$this->ruleType = idx($data, 'rule_type');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
if ($request->isFormPost()) {
$phids = $request->getArr('set_phid');
$phid = head($phids);
$uri = $request->getRequestURI();
if ($phid) {
$uri = $uri->alter('phid', nonempty($phid, null));
}
return id(new AphrontRedirectResponse())->setURI($uri);
}
$query = new HeraldRuleQuery();
$content_type_map = HeraldContentTypeConfig::getContentTypeMap();
if (empty($content_type_map[$this->contentType])) {
$this->contentType = head_key($content_type_map);
}
$content_desc = $content_type_map[$this->contentType];
$query->withContentTypes(array($this->contentType));
$is_admin_page = false;
$show_author = false;
$show_rule_type = false;
$can_create = false;
$has_author_filter = false;
$author_filter_phid = null;
switch ($this->ruleType) {
case 'all':
if (!$user->getIsAdmin()) {
return new Aphront400Response();
}
$is_admin_page = true;
$show_rule_type = true;
$show_author = true;
$has_author_filter = true;
$author_filter_phid = $request->getStr('phid');
if ($author_filter_phid) {
$query->withAuthorPHIDs(array($author_filter_phid));
}
$rule_desc = 'All';
break;
case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
$query->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_GLOBAL));
$can_create = true;
$rule_desc = 'Global';
break;
case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
default:
$this->ruleType = HeraldRuleTypeConfig::RULE_TYPE_PERSONAL;
$query->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_PERSONAL));
$query->withAuthorPHIDs(array($user->getPHID()));
$can_create = true;
$rule_desc = 'Personal';
break;
}
$pager = new AphrontPagerView();
$pager->setURI($request->getRequestURI(), 'offset');
$pager->setOffset($request->getStr('offset'));
$rules = $query->executeWithOffsetPager($pager);
$need_phids = mpull($rules, 'getAuthorPHID');
$handles = $this->loadViewerHandles($need_phids);
$list_view = id(new HeraldRuleListView())
->setRules($rules)
->setShowAuthor($show_author)
->setShowRuleType($show_rule_type)
->setHandles($handles)
->setUser($user);
$panel = new AphrontPanelView();
$panel->appendChild($list_view);
$panel->appendChild($pager);
$panel->setHeader("Herald: {$rule_desc} Rules for {$content_desc}");
if ($can_create) {
$panel->addButton(
phutil_render_tag(
'a',
array(
'href' => '/herald/new/'.$this->contentType.'/'.$this->ruleType.'/',
'class' => 'green button',
),
'Create New Herald Rule'));
}
$nav = $this->renderNav();
$nav->selectFilter('view/'.$this->contentType.'/'.$this->ruleType);
if ($has_author_filter) {
$nav->appendChild($this->renderAuthorFilter($author_filter_phid));
}
$nav->appendChild($panel);
return $this->buildStandardPageResponse(
$nav,
array(
'title' => 'Herald',
'admin' => $is_admin_page,
));
}
private function renderAuthorFilter($phid) {
if ($phid) {
$handle = PhabricatorObjectHandleData::loadOneHandle($phid);
$tokens = array(
$phid => $handle->getFullName(),
);
} else {
$tokens = array();
}
$form = id(new AphrontFormView())
->setUser($this->getRequest()->getUser())
->appendChild(
id(new AphrontFormTokenizerControl())
->setName('set_phid')
->setValue($tokens)
->setLimit(1)
->setLabel('Filter Author')
->setDataSource('/typeahead/common/accounts/'))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Apply Filter'));
$filter = new AphrontListFilterView();
$filter->appendChild($form);
return $filter;
}
}
diff --git a/src/applications/herald/controller/HeraldNewController.php b/src/applications/herald/controller/HeraldNewController.php
index 5909604032..c346939ee7 100644
--- a/src/applications/herald/controller/HeraldNewController.php
+++ b/src/applications/herald/controller/HeraldNewController.php
@@ -1,104 +1,88 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldNewController extends HeraldController {
private $contentType;
private $ruleType;
public function willProcessRequest(array $data) {
$this->contentType = idx($data, 'type');
$this->ruleType = idx($data, 'rule_type');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$content_type_map = HeraldContentTypeConfig::getContentTypeMap();
if (empty($content_type_map[$this->contentType])) {
$this->contentType = head_key($content_type_map);
}
$rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap();
if (empty($rule_type_map[$this->ruleType])) {
$this->ruleType = HeraldRuleTypeConfig::RULE_TYPE_PERSONAL;
}
// Reorder array to put "personal" first.
$rule_type_map = array_select_keys(
$rule_type_map,
array(
HeraldRuleTypeConfig::RULE_TYPE_PERSONAL,
)) + $rule_type_map;
$captions = array(
HeraldRuleTypeConfig::RULE_TYPE_PERSONAL =>
'Personal rules notify you about events. You own them, but they can '.
'only affect you.',
HeraldRuleTypeConfig::RULE_TYPE_GLOBAL =>
'Global rules notify anyone about events. No one owns them, and '.
'anyone can edit them. Usually, Global rules are used to notify '.
'mailing lists.',
);
$radio = id(new AphrontFormRadioButtonControl())
->setLabel('Type')
->setName('rule_type')
->setValue($this->ruleType);
foreach ($rule_type_map as $value => $name) {
$radio->addButton(
$value,
$name,
idx($captions, $value));
}
$form = id(new AphrontFormView())
->setUser($user)
->setAction('/herald/rule/')
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('New rule for')
->setName('content_type')
->setValue($this->contentType)
->setOptions($content_type_map))
->appendChild($radio)
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Create Rule')
->addCancelButton('/herald/view/'.$this->contentType.'/'));
$panel = new AphrontPanelView();
$panel->setHeader('Create New Herald Rule');
$panel->setWidth(AphrontPanelView::WIDTH_FULL);
$panel->appendChild($form);
$nav = $this->renderNav();
$nav->selectFilter('new');
$nav->appendChild($panel);
return $this->buildStandardPageResponse(
$nav,
array(
'title' => 'Create Herald Rule',
));
}
}
diff --git a/src/applications/herald/controller/HeraldRuleController.php b/src/applications/herald/controller/HeraldRuleController.php
index 9e8ecea0f4..eddd1fcc66 100644
--- a/src/applications/herald/controller/HeraldRuleController.php
+++ b/src/applications/herald/controller/HeraldRuleController.php
@@ -1,574 +1,558 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldRuleController extends HeraldController {
private $id;
private $filter;
public function willProcessRequest(array $data) {
$this->id = (int)idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$content_type_map = HeraldContentTypeConfig::getContentTypeMap();
$rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap();
if ($this->id) {
$rule = id(new HeraldRule())->load($this->id);
if (!$rule) {
return new Aphront404Response();
}
if (!$this->canEditRule($rule, $user)) {
throw new Exception("You don't own this rule and can't edit it.");
}
} else {
$rule = new HeraldRule();
$rule->setAuthorPHID($user->getPHID());
$rule->setMustMatchAll(true);
$content_type = $request->getStr('content_type');
if (!isset($content_type_map[$content_type])) {
$content_type = HeraldContentTypeConfig::CONTENT_TYPE_DIFFERENTIAL;
}
$rule->setContentType($content_type);
$rule_type = $request->getStr('rule_type');
if (!isset($rule_type_map[$rule_type])) {
$rule_type = HeraldRuleTypeConfig::RULE_TYPE_GLOBAL;
}
$rule->setRuleType($rule_type);
}
$local_version = id(new HeraldRule())->getConfigVersion();
if ($rule->getConfigVersion() > $local_version) {
throw new Exception(
"This rule was created with a newer version of Herald. You can not ".
"view or edit it in this older version. Upgrade your Phabricator ".
"deployment.");
}
// Upgrade rule version to our version, since we might add newly-defined
// conditions, etc.
$rule->setConfigVersion($local_version);
$rule_conditions = $rule->loadConditions();
$rule_actions = $rule->loadActions();
$rule->attachConditions($rule_conditions);
$rule->attachActions($rule_actions);
$e_name = true;
$errors = array();
if ($request->isFormPost() && $request->getStr('save')) {
list($e_name, $errors) = $this->saveRule($rule, $request);
if (!$errors) {
$uri = '/herald/view/'.
$rule->getContentType().'/'.
$rule->getRuleType().'/';
return id(new AphrontRedirectResponse())->setURI($uri);
}
}
if ($errors) {
$error_view = new AphrontErrorView();
$error_view->setTitle('Form Errors');
$error_view->setErrors($errors);
} else {
$error_view = null;
}
$must_match_selector = $this->renderMustMatchSelector($rule);
$repetition_selector = $this->renderRepetitionSelector($rule);
$handles = $this->loadHandlesForRule($rule);
require_celerity_resource('herald-css');
$content_type_name = $content_type_map[$rule->getContentType()];
$rule_type_name = $rule_type_map[$rule->getRuleType()];
$form = id(new AphrontFormView())
->setUser($user)
->setID('herald-rule-edit-form')
->addHiddenInput('content_type', $rule->getContentType())
->addHiddenInput('rule_type', $rule->getRuleType())
->addHiddenInput('save', 1)
->appendChild(
// Build this explicitly (instead of using addHiddenInput())
// so we can add a sigil to it.
javelin_render_tag(
'input',
array(
'type' => 'hidden',
'name' => 'rule',
'sigil' => 'rule',
)))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Rule Name')
->setName('name')
->setError($e_name)
->setValue($rule->getName()));
$form
->appendChild(
id(new AphrontFormMarkupControl())
->setValue(
"This <strong>${rule_type_name}</strong> rule triggers for " .
"<strong>${content_type_name}</strong>."))
->appendChild(
id(new AphrontFormInsetView())
->setTitle('Conditions')
->setRightButton(javelin_render_tag(
'a',
array(
'href' => '#',
'class' => 'button green',
'sigil' => 'create-condition',
'mustcapture' => true
),
'Create New Condition'))
->setDescription(
'When '.$must_match_selector .
' these conditions are met:')
->setContent(javelin_render_tag(
'table',
array(
'sigil' => 'rule-conditions',
'class' => 'herald-condition-table'
),
'')))
->appendChild(
id(new AphrontFormInsetView())
->setTitle('Action')
->setRightButton(javelin_render_tag(
'a',
array(
'href' => '#',
'class' => 'button green',
'sigil' => 'create-action',
'mustcapture' => true,
),
'Create New Action'))
->setDescription('Take these actions '.$repetition_selector.
' this rule matches:')
->setContent(javelin_render_tag(
'table',
array(
'sigil' => 'rule-actions',
'class' => 'herald-action-table',
),
'')))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Save Rule')
->addCancelButton('/herald/view/'.$rule->getContentType().'/'));
$this->setupEditorBehavior($rule, $handles);
$panel = new AphrontPanelView();
$panel->setHeader(
$rule->getID()
? 'Edit Herald Rule'
: 'Create Herald Rule');
$panel->setWidth(AphrontPanelView::WIDTH_WIDE);
$panel->appendChild($form);
$nav = $this->renderNav();
$nav->selectFilter(
'view/'.$rule->getContentType().'/'.$rule->getRuleType());
$nav->appendChild(
array(
$error_view,
$panel,
));
return $this->buildStandardPageResponse(
$nav,
array(
'title' => 'Edit Rule',
));
}
private function canEditRule($rule, $user) {
return
($user->getIsAdmin()) ||
($rule->getRuleType() == HeraldRuleTypeConfig::RULE_TYPE_GLOBAL) ||
($rule->getAuthorPHID() == $user->getPHID());
}
private function saveRule($rule, $request) {
$rule->setName($request->getStr('name'));
$rule->setMustMatchAll(($request->getStr('must_match') == 'all'));
$repetition_policy_param = $request->getStr('repetition_policy');
$rule->setRepetitionPolicy(
HeraldRepetitionPolicyConfig::toInt($repetition_policy_param)
);
$e_name = true;
$errors = array();
if (!strlen($rule->getName())) {
$e_name = "Required";
$errors[] = "Rule must have a name.";
}
$data = json_decode($request->getStr('rule'), true);
if (!is_array($data) ||
!$data['conditions'] ||
!$data['actions']) {
throw new Exception("Failed to decode rule data.");
}
$conditions = array();
foreach ($data['conditions'] as $condition) {
if ($condition === null) {
// We manage this as a sparse array on the client, so may receive
// NULL if conditions have been removed.
continue;
}
$obj = new HeraldCondition();
$obj->setFieldName($condition[0]);
$obj->setFieldCondition($condition[1]);
if (is_array($condition[2])) {
$obj->setValue(array_keys($condition[2]));
} else {
$obj->setValue($condition[2]);
}
$cond_type = $obj->getFieldCondition();
if ($cond_type == HeraldConditionConfig::CONDITION_REGEXP) {
if (@preg_match($obj->getValue(), '') === false) {
$errors[] =
'The regular expression "'.$obj->getValue().'" is not valid. '.
'Regular expressions must have enclosing characters (e.g. '.
'"@/path/to/file@", not "/path/to/file") and be syntactically '.
'correct.';
}
}
if ($cond_type == HeraldConditionConfig::CONDITION_REGEXP_PAIR) {
$json = json_decode($obj->getValue(), true);
if (!is_array($json)) {
$errors[] =
'The regular expression pair "'.$obj->getValue().'" is not '.
'valid JSON. Enter a valid JSON array with two elements.';
} else {
if (count($json) != 2) {
$errors[] =
'The regular expression pair "'.$obj->getValue().'" must have '.
'exactly two elements.';
} else {
$key_regexp = array_shift($json);
$val_regexp = array_shift($json);
if (@preg_match($key_regexp, '') === false) {
$errors[] =
'The first regexp, "'.$key_regexp.'" in the regexp pair '.
'is not a valid regexp.';
}
if (@preg_match($val_regexp, '') === false) {
$errors[] =
'The second regexp, "'.$val_regexp.'" in the regexp pair '.
'is not a valid regexp.';
}
}
}
}
$conditions[] = $obj;
}
$actions = array();
foreach ($data['actions'] as $action) {
if ($action === null) {
// Sparse on the client; removals can give us NULLs.
continue;
}
if (!isset($action[1])) {
// Legitimate for any action which doesn't need a target, like
// "Do nothing".
$action[1] = null;
}
$actions[] = HeraldActionConfig::willSaveAction($rule->getRuleType(),
$rule->getAuthorPHID(),
$action);
}
$rule->attachConditions($conditions);
$rule->attachActions($actions);
if (!$errors) {
try {
$edit_action = $rule->getID() ? 'edit' : 'create';
$rule->openTransaction();
$rule->save();
$rule->saveConditions($conditions);
$rule->saveActions($actions);
$rule->logEdit($request->getUser()->getPHID(), $edit_action);
$rule->saveTransaction();
} catch (AphrontQueryDuplicateKeyException $ex) {
$e_name = "Not Unique";
$errors[] = "Rule name is not unique. Choose a unique name.";
}
}
return array($e_name, $errors);
}
private function setupEditorBehavior($rule, $handles) {
$serial_conditions = array(
array('default', 'default', ''),
);
if ($rule->getConditions()) {
$serial_conditions = array();
foreach ($rule->getConditions() as $condition) {
$value = $condition->getValue();
if (is_array($value)) {
$value_map = array();
foreach ($value as $k => $fbid) {
$value_map[$fbid] = $handles[$fbid]->getName();
}
$value = $value_map;
}
$serial_conditions[] = array(
$condition->getFieldName(),
$condition->getFieldCondition(),
$value,
);
}
}
$serial_actions = array(
array('default', ''),
);
if ($rule->getActions()) {
$serial_actions = array();
foreach ($rule->getActions() as $action) {
switch ($action->getAction()) {
case HeraldActionConfig::ACTION_FLAG:
$current_value = $action->getTarget();
break;
default:
$target_map = array();
foreach ((array)$action->getTarget() as $fbid) {
$target_map[$fbid] = $handles[$fbid]->getName();
}
$current_value = $target_map;
break;
}
$serial_actions[] = array(
$action->getAction(),
$current_value,
);
}
}
$all_rules = $this->loadRulesThisRuleMayDependUpon($rule);
$all_rules = mpull($all_rules, 'getName', 'getID');
asort($all_rules);
$config_info = array();
$config_info['fields']
= HeraldFieldConfig::getFieldMapForContentType($rule->getContentType());
$config_info['conditions'] = HeraldConditionConfig::getConditionMap();
foreach ($config_info['fields'] as $field => $name) {
$config_info['conditionMap'][$field] = array_keys(
HeraldConditionConfig::getConditionMapForField($field));
}
foreach ($config_info['fields'] as $field => $fname) {
foreach ($config_info['conditions'] as $condition => $cname) {
$config_info['values'][$field][$condition] =
HeraldValueTypeConfig::getValueTypeForFieldAndCondition(
$field,
$condition);
}
}
$config_info['actions'] =
HeraldActionConfig::getActionMessageMap($rule->getContentType(),
$rule->getRuleType());
$config_info['rule_type'] = $rule->getRuleType();
foreach ($config_info['actions'] as $action => $name) {
$config_info['targets'][$action] =
HeraldValueTypeConfig::getValueTypeForAction($action,
$rule->getRuleType());
}
Javelin::initBehavior(
'herald-rule-editor',
array(
'root' => 'herald-rule-edit-form',
'conditions' => (object)$serial_conditions,
'actions' => (object)$serial_actions,
'template' => $this->buildTokenizerTemplates() + array(
'rules' => $all_rules,
'colors' => PhabricatorFlagColor::getColorNameMap(),
'defaultColor' => PhabricatorFlagColor::COLOR_BLUE,
),
'author' => array($rule->getAuthorPHID() =>
$handles[$rule->getAuthorPHID()]->getName()),
'info' => $config_info,
));
}
private function loadHandlesForRule($rule) {
$phids = array();
foreach ($rule->getActions() as $action) {
if (!is_array($action->getTarget())) {
continue;
}
foreach ($action->getTarget() as $target) {
$target = (array)$target;
foreach ($target as $phid) {
$phids[] = $phid;
}
}
}
foreach ($rule->getConditions() as $condition) {
$value = $condition->getValue();
if (is_array($value)) {
foreach ($value as $phid) {
$phids[] = $phid;
}
}
}
$phids[] = $rule->getAuthorPHID();
return $this->loadViewerHandles($phids);
}
/**
* Render the selector for the "When (all of | any of) these conditions are
* met:" element.
*/
private function renderMustMatchSelector($rule) {
return AphrontFormSelectControl::renderSelectTag(
$rule->getMustMatchAll() ? 'all' : 'any',
array(
'all' => 'all of',
'any' => 'any of',
),
array(
'name' => 'must_match',
));
}
/**
* Render the selector for "Take these actions (every time | only the first
* time) this rule matches..." element.
*/
private function renderRepetitionSelector($rule) {
// Make the selector for choosing how often this rule should be repeated
$repetition_policy = HeraldRepetitionPolicyConfig::toString(
$rule->getRepetitionPolicy());
$repetition_options = HeraldRepetitionPolicyConfig::getMapForContentType(
$rule->getContentType());
if (empty($repetition_options)) {
// default option is 'every time'
$repetition_selector = idx(
HeraldRepetitionPolicyConfig::getMap(),
HeraldRepetitionPolicyConfig::EVERY);
return $repetition_selector;
} else if (count($repetition_options) == 1) {
// if there's only 1 option, just pick it for the user
$repetition_selector = reset($repetition_options);
return $repetition_selector;
} else {
return AphrontFormSelectControl::renderSelectTag(
$repetition_policy,
$repetition_options,
array(
'name' => 'repetition_policy',
));
}
}
protected function buildTokenizerTemplates() {
$template = new AphrontTokenizerTemplateView();
$template = $template->render();
return array(
'source' => array(
'email' => '/typeahead/common/mailable/',
'user' => '/typeahead/common/users/',
'repository' => '/typeahead/common/repositories/',
'package' => '/typeahead/common/packages/',
'project' => '/typeahead/common/projects/',
),
'markup' => $template,
);
}
/**
* Load rules for the "Another Herald rule..." condition dropdown, which
* allows one rule to depend upon the success or failure of another rule.
*/
private function loadRulesThisRuleMayDependUpon(HeraldRule $rule) {
// Any rule can depend on a global rule.
$all_rules = id(new HeraldRuleQuery())
->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_GLOBAL))
->withContentTypes(array($rule->getContentType()))
->execute();
if ($rule->getRuleType() == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL) {
// Personal rules may depend upon your other personal rules.
$all_rules += id(new HeraldRuleQuery())
->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_PERSONAL))
->withContentTypes(array($rule->getContentType()))
->withAuthorPHIDs(array($rule->getAuthorPHID()))
->execute();
}
// A rule can not depend upon itself.
unset($all_rules[$rule->getID()]);
return $all_rules;
}
}
diff --git a/src/applications/herald/controller/HeraldRuleEditHistoryController.php b/src/applications/herald/controller/HeraldRuleEditHistoryController.php
index 95fb72eafe..a4e4e33207 100644
--- a/src/applications/herald/controller/HeraldRuleEditHistoryController.php
+++ b/src/applications/herald/controller/HeraldRuleEditHistoryController.php
@@ -1,64 +1,48 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldRuleEditHistoryController extends HeraldController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$edit_query = new HeraldEditLogQuery();
if ($this->id) {
$edit_query->withRuleIDs(array($this->id));
}
$pager = new AphrontPagerView();
$pager->setURI($request->getRequestURI(), 'offset');
$pager->setOffset($request->getStr('offset'));
$edits = $edit_query->executeWithOffsetPager($pager);
$need_phids = mpull($edits, 'getEditorPHID');
$handles = $this->loadViewerHandles($need_phids);
$list_view = id(new HeraldRuleEditHistoryView())
->setEdits($edits)
->setHandles($handles)
->setUser($this->getRequest()->getUser());
$panel = new AphrontPanelView();
$panel->setHeader('Edit History');
$panel->appendChild($list_view);
$nav = $this->renderNav();
$nav->selectFilter('history');
$nav->appendChild($panel);
return $this->buildStandardPageResponse(
$nav,
array(
'title' => 'Rule Edit History',
));
}
}
diff --git a/src/applications/herald/controller/HeraldTestConsoleController.php b/src/applications/herald/controller/HeraldTestConsoleController.php
index 028f65971a..f94b165c76 100644
--- a/src/applications/herald/controller/HeraldTestConsoleController.php
+++ b/src/applications/herald/controller/HeraldTestConsoleController.php
@@ -1,150 +1,134 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldTestConsoleController extends HeraldController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$request = $this->getRequest();
$object_name = trim($request->getStr('object_name'));
$e_name = true;
$errors = array();
if ($request->isFormPost()) {
if (!$object_name) {
$e_name = 'Required';
$errors[] = 'An object name is required.';
}
if (!$errors) {
$matches = null;
$object = null;
if (preg_match('/^D(\d+)$/', $object_name, $matches)) {
$object = id(new DifferentialRevision())->load($matches[1]);
if (!$object) {
$e_name = 'Invalid';
$errors[] = 'No Differential Revision with that ID exists.';
}
} else if (preg_match('/^r([A-Z]+)(\w+)$/', $object_name, $matches)) {
$repo = id(new PhabricatorRepository())->loadOneWhere(
'callsign = %s',
$matches[1]);
if (!$repo) {
$e_name = 'Invalid';
$errors[] = 'There is no repository with the callsign '.
$matches[1].'.';
}
$commit = id(new PhabricatorRepositoryCommit())->loadOneWhere(
'repositoryID = %d AND commitIdentifier = %s',
$repo->getID(),
$matches[2]);
if (!$commit) {
$e_name = 'Invalid';
$errors[] = 'There is no commit with that identifier.';
}
$object = $commit;
} else {
$e_name = 'Invalid';
$errors[] = 'This object name is not recognized.';
}
if (!$errors) {
if ($object instanceof DifferentialRevision) {
$adapter = new HeraldDifferentialRevisionAdapter(
$object,
$object->loadActiveDiff());
} else if ($object instanceof PhabricatorRepositoryCommit) {
$data = id(new PhabricatorRepositoryCommitData())->loadOneWhere(
'commitID = %d',
$object->getID());
$adapter = new HeraldCommitAdapter(
$repo,
$object,
$data);
} else {
throw new Exception("Can not build adapter for object!");
}
$rules = HeraldRule::loadAllByContentTypeWithFullData(
$adapter->getHeraldTypeName(),
$object->getPHID());
$engine = new HeraldEngine();
$effects = $engine->applyRules($rules, $adapter);
$dry_run = new HeraldDryRunAdapter();
$engine->applyEffects($effects, $dry_run, $rules);
$xscript = $engine->getTranscript();
return id(new AphrontRedirectResponse())
->setURI('/herald/transcript/'.$xscript->getID().'/');
}
}
}
if ($errors) {
$error_view = new AphrontErrorView();
$error_view->setTitle('Form Errors');
$error_view->setErrors($errors);
} else {
$error_view = null;
}
$form = id(new AphrontFormView())
->setUser($user)
->appendChild(
'<p class="aphront-form-instructions">Enter an object to test rules '.
'for, like a Diffusion commit (e.g., <tt>rX123</tt>) or a '.
'Differential revision (e.g., <tt>D123</tt>). You will be shown the '.
'results of a dry run on the object.</p>')
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Object Name')
->setName('object_name')
->setError($e_name)
->setValue($object_name))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Test Rules'));
$panel = new AphrontPanelView();
$panel->setHeader('Test Herald Rules');
$panel->setWidth(AphrontPanelView::WIDTH_FULL);
$panel->appendChild($form);
$nav = $this->renderNav();
$nav->selectFilter('test');
$nav->appendChild(
array(
$error_view,
$panel,
));
return $this->buildStandardPageResponse(
$nav,
array(
'title' => 'Test Console',
));
}
}
diff --git a/src/applications/herald/controller/HeraldTranscriptController.php b/src/applications/herald/controller/HeraldTranscriptController.php
index 0fd0131173..6ed6e2f3fd 100644
--- a/src/applications/herald/controller/HeraldTranscriptController.php
+++ b/src/applications/herald/controller/HeraldTranscriptController.php
@@ -1,534 +1,518 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldTranscriptController extends HeraldController {
const FILTER_AFFECTED = 'affected';
const FILTER_OWNED = 'owned';
const FILTER_ALL = 'all';
private $id;
private $filter;
private $handles;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
$map = $this->getFilterMap();
$this->filter = idx($data, 'filter');
if (empty($map[$this->filter])) {
$this->filter = self::FILTER_AFFECTED;
}
}
public function processRequest() {
$xscript = id(new HeraldTranscript())->load($this->id);
if (!$xscript) {
throw new Exception('Uknown transcript!');
}
require_celerity_resource('herald-test-css');
$nav = $this->buildSideNav();
$object_xscript = $xscript->getObjectTranscript();
if (!$object_xscript) {
$notice = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
->setTitle('Old Transcript')
->appendChild(
'<p>Details of this transcript have been garbage collected.</p>');
$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);
$handles = $this->loadViewerHandles($phids);
$this->handles = $handles;
if ($xscript->getDryRun()) {
$notice = new AphrontErrorView();
$notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
$notice->setTitle('Dry Run');
$notice->appendChild(
'This was a dry run to test Herald rules, no actions were executed.');
$nav->appendChild($notice);
}
$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);
}
$main_nav = $this->renderNav();
$main_nav->selectFilter('transcript');
$main_nav->appendChild($nav);
return $this->buildStandardPageResponse(
$main_nav,
array(
'title' => 'Transcript',
));
}
protected function renderConditionTestValue($condition, $handles) {
$value = $condition->getTestValue();
if (!is_scalar($value) && $value !== null) {
foreach ($value as $key => $phid) {
$handle = idx($handles, $phid);
if ($handle) {
$value[$key] = $handle->getName();
} else {
// This shouldn't ever really happen as we are supposed to have
// grabbed handles for everything, but be super liberal in what
// we accept here since we expect all sorts of weird issues as we
// version the system.
$value[$key] = 'Unknown Object #'.$phid;
}
}
sort($value);
$value = implode(', ', $value);
}
return
'<span class="condition-test-value">'.
phutil_escape_html($value).
'</span>';
}
private function buildSideNav() {
$nav = new AphrontSideNavView();
$items = array();
$filters = $this->getFilterMap();
foreach ($filters as $key => $name) {
$nav->addNavItem(
phutil_render_tag(
'a',
array(
'href' => '/herald/transcript/'.$this->id.'/'.$key.'/',
'class' =>
($key == $this->filter)
? 'aphront-side-nav-selected'
: null,
),
phutil_escape_html($name)));
}
return $nav;
}
protected function getFilterMap() {
return array(
self::FILTER_AFFECTED => 'Rules that Affected Me',
self::FILTER_OWNED => 'Rules I Own',
self::FILTER_ALL => 'All Rules',
);
}
protected function getFilterPHIDs() {
return array($this->getRequest()->getUser()->getPHID());
/* TODO
$viewer_id = $this->getRequest()->getUser()->getPHID();
$fbids = array();
if ($this->filter == self::FILTER_AFFECTED) {
$fbids[] = $viewer_id;
require_module_lazy('intern/subscriptions');
$datastore = new SubscriberDatabaseStore();
$lists = $datastore->getUserMailmanLists($viewer_id);
foreach ($lists as $list) {
$fbids[] = $list;
}
}
return $fbids;
*/
}
protected function getTranscriptPHIDs($xscript) {
$phids = array();
$object_xscript = $xscript->getObjectTranscript();
if (!$object_xscript) {
return array();
}
$phids[] = $object_xscript->getPHID();
foreach ($xscript->getApplyTranscripts() as $apply_xscript) {
// TODO: This is total hacks. Add another amazing layer of abstraction.
$target = (array)$apply_xscript->getTarget();
foreach ($target as $phid) {
if ($phid) {
$phids[] = $phid;
}
}
}
foreach ($xscript->getRuleTranscripts() as $rule_xscript) {
$phids[] = $rule_xscript->getRuleOwner();
}
$condition_xscripts = $xscript->getConditionTranscripts();
if ($condition_xscripts) {
$condition_xscripts = call_user_func_array(
'array_merge',
$condition_xscripts);
}
foreach ($condition_xscripts as $condition_xscript) {
$value = $condition_xscript->getTestValue();
// TODO: Also total hacks.
if (is_array($value)) {
foreach ($value as $phid) {
if ($phid) { // TODO: Probably need to make sure this "looks like" a
// PHID or decrease the level of hacks here; this used
// to be an is_numeric() check in Facebook land.
$phids[] = $phid;
}
}
}
}
return $phids;
}
protected function filterTranscript($xscript, $filter_phids) {
$filter_owned = ($this->filter == self::FILTER_OWNED);
$filter_affected = ($this->filter == self::FILTER_AFFECTED);
if (!$filter_owned && !$filter_affected) {
// No filtering to be done.
return;
}
if (!$xscript->getObjectTranscript()) {
return;
}
$user_phid = $this->getRequest()->getUser()->getPHID();
$keep_apply_xscripts = array();
$keep_rule_xscripts = array();
$filter_phids = array_fill_keys($filter_phids, true);
$rule_xscripts = $xscript->getRuleTranscripts();
foreach ($xscript->getApplyTranscripts() as $id => $apply_xscript) {
$rule_id = $apply_xscript->getRuleID();
if ($filter_owned) {
if (empty($rule_xscripts[$rule_id])) {
// No associated rule so you can't own this effect.
continue;
}
if ($rule_xscripts[$rule_id]->getRuleOwner() != $user_phid) {
continue;
}
} else if ($filter_affected) {
$targets = (array)$apply_xscript->getTarget();
if (!array_select_keys($filter_phids, $targets)) {
continue;
}
}
$keep_apply_xscripts[$id] = true;
if ($rule_id) {
$keep_rule_xscripts[$rule_id] = true;
}
}
foreach ($rule_xscripts as $rule_id => $rule_xscript) {
if ($filter_owned && $rule_xscript->getRuleOwner() == $user_phid) {
$keep_rule_xscripts[$rule_id] = true;
}
}
$xscript->setRuleTranscripts(
array_intersect_key(
$xscript->getRuleTranscripts(),
$keep_rule_xscripts));
$xscript->setApplyTranscripts(
array_intersect_key(
$xscript->getApplyTranscripts(),
$keep_apply_xscripts));
$xscript->setConditionTranscripts(
array_intersect_key(
$xscript->getConditionTranscripts(),
$keep_rule_xscripts));
}
private function buildApplyTranscriptPanel($xscript) {
$handles = $this->handles;
$action_names = HeraldActionConfig::getActionMessageMapForRuleType(null);
$rows = array();
foreach ($xscript->getApplyTranscripts() as $apply_xscript) {
$target = $apply_xscript->getTarget();
switch ($apply_xscript->getAction()) {
case HeraldActionConfig::ACTION_NOTHING:
$target = '';
break;
case HeraldActionConfig::ACTION_FLAG:
$target = PhabricatorFlagColor::getColorName($target);
break;
default:
if ($target) {
foreach ($target as $k => $phid) {
$target[$k] = $handles[$phid]->getName();
}
$target = implode("\n", $target);
} else {
$target = '<empty>';
}
break;
}
$target = phutil_escape_html($target);
if ($apply_xscript->getApplied()) {
$outcome = '<span class="outcome-success">SUCCESS</span>';
} else {
$outcome = '<span class="outcome-failure">FAILURE</span>';
}
$outcome .= ' '.phutil_escape_html($apply_xscript->getAppliedReason());
$rows[] = array(
phutil_escape_html($action_names[$apply_xscript->getAction()]),
$target,
'<strong>Taken because:</strong> '.
phutil_escape_html($apply_xscript->getReason()).
'<br />'.
'<strong>Outcome:</strong> '.$outcome,
);
}
$table = new AphrontTableView($rows);
$table->setNoDataString('No actions were taken.');
$table->setHeaders(
array(
'Action',
'Target',
'Details',
));
$table->setColumnClasses(
array(
'',
'',
'wide',
));
$panel = new AphrontPanelView();
$panel->setHeader('Actions Taken');
$panel->appendChild($table);
return $panel;
}
private function buildActionTranscriptPanel($xscript) {
$action_xscript = mgroup($xscript->getApplyTranscripts(), 'getRuleID');
$field_names = HeraldFieldConfig::getFieldMap();
$condition_names = HeraldConditionConfig::getConditionMap();
$handles = $this->handles;
$rule_markup = array();
foreach ($xscript->getRuleTranscripts() as $rule_id => $rule) {
$cond_markup = array();
foreach ($xscript->getConditionTranscriptsForRule($rule_id) as $cond) {
if ($cond->getNote()) {
$note =
'<div class="herald-condition-note">'.
phutil_escape_html($cond->getNote()).
'</div>';
} else {
$note = null;
}
if ($cond->getResult()) {
$result =
'<span class="herald-outcome condition-pass">'.
"\xE2\x9C\x93".
'</span>';
} else {
$result =
'<span class="herald-outcome condition-fail">'.
"\xE2\x9C\x98".
'</span>';
}
$cond_markup[] =
'<li>'.
$result.' Condition: '.
phutil_escape_html($field_names[$cond->getFieldName()]).
' '.
phutil_escape_html($condition_names[$cond->getCondition()]).
' '.
$this->renderConditionTestValue($cond, $handles).
$note.
'</li>';
}
if ($rule->getResult()) {
$result = '<span class="herald-outcome rule-pass">PASS</span>';
$class = 'herald-rule-pass';
} else {
$result = '<span class="herald-outcome rule-fail">FAIL</span>';
$class = 'herald-rule-fail';
}
$cond_markup[] =
'<li>'.$result.' '.phutil_escape_html($rule->getReason()).'</li>';
/*
if ($rule->getResult()) {
$actions = idx($action_xscript, $rule_id, array());
if ($actions) {
$cond_markup[] = <li><div class="action-header">Actions</div></li>;
foreach ($actions as $action) {
$target = $action->getTarget();
if ($target) {
foreach ((array)$target as $k => $phid) {
$target[$k] = $handles[$phid]->getName();
}
$target = <strong>: {implode(', ', $target)}</strong>;
}
$cond_markup[] =
<li>
{$action_names[$action->getAction()]}
{$target}
</li>;
}
}
}
*/
$user_phid = $this->getRequest()->getUser()->getPHID();
$name = $rule->getRuleName();
if ($rule->getRuleOwner() == $user_phid) {
// $name = <a href={"/herald/rule/".$rule->getRuleID()."/"}>{$name}</a>;
}
$rule_markup[] =
phutil_render_tag(
'li',
array(
'class' => $class,
),
'<div class="rule-name">'.
'<strong>'.phutil_escape_html($name).'</strong> '.
phutil_escape_html($handles[$rule->getRuleOwner()]->getName()).
'</div>'.
'<ul>'.implode("\n", $cond_markup).'</ul>');
}
$panel = new AphrontPanelView();
$panel->setHeader('Rule Details');
$panel->appendChild(
'<ul class="herald-explain-list">'.
implode("\n", $rule_markup).
'</ul>');
return $panel;
}
private function buildObjectTranscriptPanel($xscript) {
$field_names = HeraldFieldConfig::getFieldMap();
$object_xscript = $xscript->getObjectTranscript();
$data = array();
if ($object_xscript) {
$phid = $object_xscript->getPHID();
$handles = $this->loadViewerHandles(array($phid));
$data += array(
'Object Name' => $object_xscript->getName(),
'Object Type' => $object_xscript->getType(),
'Object PHID' => $phid,
'Object Link' => $handles[$phid]->renderLink(),
);
}
$data += $xscript->getMetadataMap();
if ($object_xscript) {
foreach ($object_xscript->getFields() as $field => $value) {
$field = idx($field_names, $field, '['.$field.'?]');
$data['Field: '.$field] = $value;
}
}
$rows = array();
foreach ($data as $name => $value) {
if (!is_scalar($value) && !is_null($value)) {
$value = implode("\n", $value);
}
if (strlen($value) > 256) {
$value = phutil_render_tag(
'textarea',
array(
'class' => 'herald-field-value-transcript',
),
phutil_escape_html($value));
} else if ($name === 'Object Link') {
// The link cannot be escaped
} else {
$value = phutil_escape_html($value);
}
$rows[] = array(
phutil_escape_html($name),
$value,
);
}
$table = new AphrontTableView($rows);
$table->setColumnClasses(
array(
'header',
'wide',
));
$panel = new AphrontPanelView();
$panel->setHeader('Object Transcript');
$panel->appendChild($table);
return $panel;
}
}
diff --git a/src/applications/herald/controller/HeraldTranscriptListController.php b/src/applications/herald/controller/HeraldTranscriptListController.php
index 055f400d0a..c86fc2dd9b 100644
--- a/src/applications/herald/controller/HeraldTranscriptListController.php
+++ b/src/applications/herald/controller/HeraldTranscriptListController.php
@@ -1,124 +1,108 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldTranscriptListController extends HeraldController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
// Get one page of data together with the pager.
// Pull these objects manually since the serialized fields are gigantic.
$transcript = new HeraldTranscript();
$conn_r = $transcript->establishConnection('r');
$phid = $request->getStr('phid');
$where_clause = '';
if ($phid) {
$where_clause = qsprintf(
$conn_r,
'WHERE objectPHID = %s',
$phid);
}
$pager = new AphrontPagerView();
$pager->setOffset($request->getInt('offset'));
$pager->setURI($request->getRequestURI(), 'offset');
$limit_clause = qsprintf(
$conn_r,
'LIMIT %d, %d',
$pager->getOffset(),
$pager->getPageSize() + 1);
$data = queryfx_all(
$conn_r,
'SELECT id, objectPHID, time, duration, dryRun FROM %T
%Q
ORDER BY id DESC
%Q',
$transcript->getTableName(),
$where_clause,
$limit_clause);
$data = $pager->sliceResults($data);
// Render the table.
$handles = array();
if ($data) {
$phids = ipull($data, 'objectPHID', 'objectPHID');
$handles = $this->loadViewerHandles($phids);
}
$rows = array();
foreach ($data as $xscript) {
$rows[] = array(
phabricator_date($xscript['time'], $user),
phabricator_time($xscript['time'], $user),
$handles[$xscript['objectPHID']]->renderLink(),
$xscript['dryRun'] ? 'Yes' : '',
number_format((int)(1000 * $xscript['duration'])).' ms',
phutil_render_tag(
'a',
array(
'href' => '/herald/transcript/'.$xscript['id'].'/',
'class' => 'button small grey',
),
'View Transcript'),
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Date',
'Time',
'Object',
'Dry Run',
'Duration',
'View',
));
$table->setColumnClasses(
array(
'',
'right',
'wide wrap',
'',
'',
'action',
));
// Render the whole page.
$panel = new AphrontPanelView();
$panel->setHeader('Herald Transcripts');
$panel->appendChild($table);
$panel->appendChild($pager);
$nav = $this->renderNav();
$nav->selectFilter('transcript');
$nav->appendChild($panel);
return $this->buildStandardPageResponse(
$nav,
array(
'title' => 'Herald Transcripts',
));
}
}
diff --git a/src/applications/herald/engine/HeraldEffect.php b/src/applications/herald/engine/HeraldEffect.php
index a00617774a..95b6a87b22 100644
--- a/src/applications/herald/engine/HeraldEffect.php
+++ b/src/applications/herald/engine/HeraldEffect.php
@@ -1,85 +1,69 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldEffect {
protected $objectPHID;
protected $action;
protected $target;
protected $ruleID;
protected $effector;
protected $reason;
public function setObjectPHID($object_phid) {
$this->objectPHID = $object_phid;
return $this;
}
public function getObjectPHID() {
return $this->objectPHID;
}
public function setAction($action) {
$this->action = $action;
return $this;
}
public function getAction() {
return $this->action;
}
public function setTarget($target) {
$this->target = $target;
return $this;
}
public function getTarget() {
return $this->target;
}
public function setRuleID($rule_id) {
$this->ruleID = $rule_id;
return $this;
}
public function getRuleID() {
return $this->ruleID;
}
public function setEffector($effector) {
$this->effector = $effector;
return $this;
}
public function getEffector() {
return $this->effector;
}
public function setReason($reason) {
$this->reason = $reason;
return $this;
}
public function getReason() {
return $this->reason;
}
}
diff --git a/src/applications/herald/engine/HeraldEngine.php b/src/applications/herald/engine/HeraldEngine.php
index 6fc64f5cf8..16f7bcad00 100644
--- a/src/applications/herald/engine/HeraldEngine.php
+++ b/src/applications/herald/engine/HeraldEngine.php
@@ -1,524 +1,508 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldEngine {
protected $rules = array();
protected $results = array();
protected $stack = array();
protected $activeRule = null;
protected $fieldCache = array();
protected $object = null;
public static function loadAndApplyRules(HeraldObjectAdapter $object) {
$content_type = $object->getHeraldTypeName();
$rules = HeraldRule::loadAllByContentTypeWithFullData(
$content_type,
$object->getPHID());
$engine = new HeraldEngine();
$effects = $engine->applyRules($rules, $object);
$engine->applyEffects($effects, $object, $rules);
return $engine->getTranscript();
}
public function applyRules(array $rules, HeraldObjectAdapter $object) {
assert_instances_of($rules, 'HeraldRule');
$t_start = microtime(true);
$rules = mpull($rules, null, 'getID');
$this->transcript = new HeraldTranscript();
$this->transcript->setObjectPHID((string)$object->getPHID());
$this->fieldCache = array();
$this->results = array();
$this->rules = $rules;
$this->object = $object;
$effects = array();
foreach ($rules as $id => $rule) {
$this->stack = array();
try {
if (($rule->getRepetitionPolicy() ==
HeraldRepetitionPolicyConfig::FIRST) &&
$rule->getRuleApplied($object->getPHID())) {
// This rule is only supposed to be applied a single time, and it's
// aleady been applied, so this is an automatic failure.
$xscript = id(new HeraldRuleTranscript())
->setRuleID($id)
->setResult(false)
->setRuleName($rule->getName())
->setRuleOwner($rule->getAuthorPHID())
->setReason(
"This rule is only supposed to be repeated a single time, ".
"and it has already been applied."
);
$this->transcript->addRuleTranscript($xscript);
$rule_matches = false;
} else {
$rule_matches = $this->doesRuleMatch($rule, $object);
}
} catch (HeraldRecursiveConditionsException $ex) {
$names = array();
foreach ($this->stack as $rule_id => $ignored) {
$names[] = '"'.$rules[$rule_id]->getName().'"';
}
$names = implode(', ', $names);
foreach ($this->stack as $rule_id => $ignored) {
$xscript = new HeraldRuleTranscript();
$xscript->setRuleID($rule_id);
$xscript->setResult(false);
$xscript->setReason(
"Rules {$names} are recursively dependent upon one another! ".
"Don't do this! You have formed an unresolvable cycle in the ".
"dependency graph!");
$xscript->setRuleName($rules[$rule_id]->getName());
$xscript->setRuleOwner($rules[$rule_id]->getAuthorPHID());
$this->transcript->addRuleTranscript($xscript);
}
$rule_matches = false;
}
$this->results[$id] = $rule_matches;
if ($rule_matches) {
foreach ($this->getRuleEffects($rule, $object) as $effect) {
$effects[] = $effect;
}
}
}
$object_transcript = new HeraldObjectTranscript();
$object_transcript->setPHID($object->getPHID());
$object_transcript->setName($object->getHeraldName());
$object_transcript->setType($object->getHeraldTypeName());
$object_transcript->setFields($this->fieldCache);
$this->transcript->setObjectTranscript($object_transcript);
$t_end = microtime(true);
$this->transcript->setDuration($t_end - $t_start);
return $effects;
}
public function applyEffects(
array $effects,
HeraldObjectAdapter $object,
array $rules) {
assert_instances_of($effects, 'HeraldEffect');
assert_instances_of($rules, 'HeraldRule');
$this->transcript->setDryRun($object instanceof HeraldDryRunAdapter);
$xscripts = $object->applyHeraldEffects($effects);
foreach ($xscripts as $apply_xscript) {
if (!($apply_xscript instanceof HeraldApplyTranscript)) {
throw new Exception(
"Heraldable must return HeraldApplyTranscripts from ".
"applyHeraldEffect().");
}
$this->transcript->addApplyTranscript($apply_xscript);
}
if (!$this->transcript->getDryRun()) {
$rules = mpull($rules, null, 'getID');
$applied_ids = array();
$first_policy = HeraldRepetitionPolicyConfig::toInt(
HeraldRepetitionPolicyConfig::FIRST);
// Mark all the rules that have had their effects applied as having been
// executed for the current object.
$rule_ids = mpull($xscripts, 'getRuleID');
foreach ($rule_ids as $rule_id) {
if (!$rule_id) {
// Some apply transcripts are purely informational and not associated
// with a rule, e.g. carryover emails from earlier revisions.
continue;
}
$rule = idx($rules, $rule_id);
if (!$rule) {
continue;
}
if ($rule->getRepetitionPolicy() == $first_policy) {
$applied_ids[] = $rule_id;
}
}
if ($applied_ids) {
$conn_w = id(new HeraldRule())->establishConnection('w');
$sql = array();
foreach ($applied_ids as $id) {
$sql[] = qsprintf(
$conn_w,
'(%s, %d)',
$object->getPHID(),
$id);
}
queryfx(
$conn_w,
'INSERT IGNORE INTO %T (phid, ruleID) VALUES %Q',
HeraldRule::TABLE_RULE_APPLIED,
implode(', ', $sql));
}
}
}
public function getTranscript() {
$this->transcript->save();
return $this->transcript;
}
protected function doesRuleMatch(
HeraldRule $rule,
HeraldObjectAdapter $object) {
$id = $rule->getID();
if (isset($this->results[$id])) {
// If we've already evaluated this rule because another rule depends
// on it, we don't need to reevaluate it.
return $this->results[$id];
}
if (isset($this->stack[$id])) {
// We've recursed, fail all of the rules on the stack. This happens when
// there's a dependency cycle with "Rule conditions match for rule ..."
// conditions.
foreach ($this->stack as $rule_id => $ignored) {
$this->results[$rule_id] = false;
}
throw new HeraldRecursiveConditionsException();
}
$this->stack[$id] = true;
$all = $rule->getMustMatchAll();
$conditions = $rule->getConditions();
$result = null;
$local_version = id(new HeraldRule())->getConfigVersion();
if ($rule->getConfigVersion() > $local_version) {
$reason = "Rule could not be processed, it was created with a newer ".
"version of Herald.";
$result = false;
} else if (!$conditions) {
$reason = "Rule failed automatically because it has no conditions.";
$result = false;
} else if ($rule->hasInvalidOwner()) {
$reason = "Rule failed automatically because its owner is invalid ".
"or disabled.";
$result = false;
} else {
foreach ($conditions as $condition) {
$match = $this->doesConditionMatch($rule, $condition, $object);
if (!$all && $match) {
$reason = "Any condition matched.";
$result = true;
break;
}
if ($all && !$match) {
$reason = "Not all conditions matched.";
$result = false;
break;
}
}
if ($result === null) {
if ($all) {
$reason = "All conditions matched.";
$result = true;
} else {
$reason = "No conditions matched.";
$result = false;
}
}
}
$rule_transcript = new HeraldRuleTranscript();
$rule_transcript->setRuleID($rule->getID());
$rule_transcript->setResult($result);
$rule_transcript->setReason($reason);
$rule_transcript->setRuleName($rule->getName());
$rule_transcript->setRuleOwner($rule->getAuthorPHID());
$this->transcript->addRuleTranscript($rule_transcript);
return $result;
}
protected function doesConditionMatch(
HeraldRule $rule,
HeraldCondition $condition,
HeraldObjectAdapter $object) {
$object_value = $this->getConditionObjectValue($condition, $object);
$test_value = $condition->getValue();
$cond = $condition->getFieldCondition();
$transcript = new HeraldConditionTranscript();
$transcript->setRuleID($rule->getID());
$transcript->setConditionID($condition->getID());
$transcript->setFieldName($condition->getFieldName());
$transcript->setCondition($cond);
$transcript->setTestValue($test_value);
$result = null;
switch ($cond) {
case HeraldConditionConfig::CONDITION_CONTAINS:
// "Contains" can take an array of strings, as in "Any changed
// filename" for diffs.
foreach ((array)$object_value as $value) {
$result = (stripos($value, $test_value) !== false);
if ($result) {
break;
}
}
break;
case HeraldConditionConfig::CONDITION_NOT_CONTAINS:
$result = (stripos($object_value, $test_value) === false);
break;
case HeraldConditionConfig::CONDITION_IS:
$result = ($object_value == $test_value);
break;
case HeraldConditionConfig::CONDITION_IS_NOT:
$result = ($object_value != $test_value);
break;
case HeraldConditionConfig::CONDITION_IS_ME:
$result = ($object_value == $rule->getAuthorPHID());
break;
case HeraldConditionConfig::CONDITION_IS_NOT_ME:
$result = ($object_value != $rule->getAuthorPHID());
break;
case HeraldConditionConfig::CONDITION_IS_ANY:
$test_value = array_flip($test_value);
$result = isset($test_value[$object_value]);
break;
case HeraldConditionConfig::CONDITION_IS_NOT_ANY:
$test_value = array_flip($test_value);
$result = !isset($test_value[$object_value]);
break;
case HeraldConditionConfig::CONDITION_INCLUDE_ALL:
if (!is_array($object_value)) {
$transcript->setNote('Object produced bad value!');
$result = false;
} else {
$have = array_select_keys(array_flip($object_value),
$test_value);
$result = (count($have) == count($test_value));
}
break;
case HeraldConditionConfig::CONDITION_INCLUDE_ANY:
$result = (bool)array_select_keys(array_flip($object_value),
$test_value);
break;
case HeraldConditionConfig::CONDITION_INCLUDE_NONE:
$result = !array_select_keys(array_flip($object_value),
$test_value);
break;
case HeraldConditionConfig::CONDITION_EXISTS:
$result = (bool)$object_value;
break;
case HeraldConditionConfig::CONDITION_NOT_EXISTS:
$result = !$object_value;
break;
case HeraldConditionConfig::CONDITION_REGEXP:
foreach ((array)$object_value as $value) {
// We add the 'S' flag because we use the regexp multiple times.
// It shouldn't cause any troubles if the flag is already there
// - /.*/S is evaluated same as /.*/SS.
$result = @preg_match($test_value . 'S', $value);
if ($result === false) {
$transcript->setNote(
"Regular expression is not valid!");
break;
}
if ($result) {
break;
}
}
$result = (bool)$result;
break;
case HeraldConditionConfig::CONDITION_REGEXP_PAIR:
// Match a JSON-encoded pair of regular expressions against a
// dictionary. The first regexp must match the dictionary key, and the
// second regexp must match the dictionary value. If any key/value pair
// in the dictionary matches both regexps, the condition is satisfied.
$regexp_pair = json_decode($test_value, true);
if (!is_array($regexp_pair)) {
$result = false;
$transcript->setNote("Regular expression pair is not valid JSON!");
break;
}
if (count($regexp_pair) != 2) {
$result = false;
$transcript->setNote("Regular expression pair is not a pair!");
break;
}
$key_regexp = array_shift($regexp_pair);
$value_regexp = array_shift($regexp_pair);
foreach ((array)$object_value as $key => $value) {
$key_matches = @preg_match($key_regexp, $key);
if ($key_matches === false) {
$result = false;
$transcript->setNote("First regular expression is invalid!");
break 2;
}
if ($key_matches) {
$value_matches = @preg_match($value_regexp, $value);
if ($value_matches === false) {
$result = false;
$transcript->setNote("Second regular expression is invalid!");
break 2;
}
if ($value_matches) {
$result = true;
break 2;
}
}
}
$result = false;
break;
case HeraldConditionConfig::CONDITION_RULE:
case HeraldConditionConfig::CONDITION_NOT_RULE:
$rule = idx($this->rules, $test_value);
if (!$rule) {
$transcript->setNote(
"Condition references a rule which does not exist!");
$result = false;
} else {
$is_not = ($cond == HeraldConditionConfig::CONDITION_NOT_RULE);
$result = $this->doesRuleMatch($rule, $object);
if ($is_not) {
$result = !$result;
}
}
break;
default:
throw new HeraldInvalidConditionException(
"Unknown condition '{$cond}'.");
}
$transcript->setResult($result);
$this->transcript->addConditionTranscript($transcript);
return $result;
}
protected function getConditionObjectValue(
HeraldCondition $condition,
HeraldObjectAdapter $object) {
$field = $condition->getFieldName();
return $this->getObjectFieldValue($field);
}
public function getObjectFieldValue($field) {
if (isset($this->fieldCache[$field])) {
return $this->fieldCache[$field];
}
$result = null;
switch ($field) {
case HeraldFieldConfig::FIELD_RULE:
$result = null;
break;
case HeraldFieldConfig::FIELD_TITLE:
case HeraldFieldConfig::FIELD_BODY:
case HeraldFieldConfig::FIELD_DIFF_FILE:
case HeraldFieldConfig::FIELD_DIFF_CONTENT:
// TODO: Type should be string.
$result = $this->object->getHeraldField($field);
break;
case HeraldFieldConfig::FIELD_AUTHOR:
case HeraldFieldConfig::FIELD_REPOSITORY:
case HeraldFieldConfig::FIELD_MERGE_REQUESTER:
// TODO: Type should be PHID.
$result = $this->object->getHeraldField($field);
break;
case HeraldFieldConfig::FIELD_TAGS:
case HeraldFieldConfig::FIELD_REVIEWER:
case HeraldFieldConfig::FIELD_REVIEWERS:
case HeraldFieldConfig::FIELD_CC:
case HeraldFieldConfig::FIELD_DIFFERENTIAL_REVIEWERS:
case HeraldFieldConfig::FIELD_DIFFERENTIAL_CCS:
// TODO: Type should be list.
$result = $this->object->getHeraldField($field);
break;
case HeraldFieldConfig::FIELD_AFFECTED_PACKAGE:
case HeraldFieldConfig::FIELD_AFFECTED_PACKAGE_OWNER:
case HeraldFieldConfig::FIELD_NEED_AUDIT_FOR_PACKAGE:
$result = $this->object->getHeraldField($field);
if (!is_array($result)) {
throw new HeraldInvalidFieldException(
"Value of field type {$field} is not an array!");
}
break;
case HeraldFieldConfig::FIELD_DIFFERENTIAL_REVISION:
// TODO: Type should be boolean I guess.
$result = $this->object->getHeraldField($field);
break;
default:
throw new HeraldInvalidConditionException(
"Unknown field type '{$field}'!");
}
$this->fieldCache[$field] = $result;
return $result;
}
protected function getRuleEffects(
HeraldRule $rule,
HeraldObjectAdapter $object) {
$effects = array();
foreach ($rule->getActions() as $action) {
$effect = new HeraldEffect();
$effect->setObjectPHID($object->getPHID());
$effect->setAction($action->getAction());
$effect->setTarget($action->getTarget());
$effect->setRuleID($rule->getID());
$name = $rule->getName();
$id = $rule->getID();
$effect->setReason(
'Conditions were met for Herald rule "'.$name.'" (#'.$id.').');
$effects[] = $effect;
}
return $effects;
}
}
diff --git a/src/applications/herald/engine/engine/HeraldInvalidConditionException.php b/src/applications/herald/engine/engine/HeraldInvalidConditionException.php
index 4c5b6f0b6b..289838bd14 100644
--- a/src/applications/herald/engine/engine/HeraldInvalidConditionException.php
+++ b/src/applications/herald/engine/engine/HeraldInvalidConditionException.php
@@ -1,19 +1,3 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldInvalidConditionException extends Exception {}
diff --git a/src/applications/herald/engine/engine/HeraldInvalidFieldException.php b/src/applications/herald/engine/engine/HeraldInvalidFieldException.php
index 9ef352f46b..4c63fcde77 100644
--- a/src/applications/herald/engine/engine/HeraldInvalidFieldException.php
+++ b/src/applications/herald/engine/engine/HeraldInvalidFieldException.php
@@ -1,19 +1,3 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldInvalidFieldException extends Exception {}
diff --git a/src/applications/herald/engine/engine/HeraldRecursiveConditionsException.php b/src/applications/herald/engine/engine/HeraldRecursiveConditionsException.php
index d65d68a4ae..1ca2ef2460 100644
--- a/src/applications/herald/engine/engine/HeraldRecursiveConditionsException.php
+++ b/src/applications/herald/engine/engine/HeraldRecursiveConditionsException.php
@@ -1,19 +1,3 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldRecursiveConditionsException extends Exception {}
diff --git a/src/applications/herald/query/HeraldEditLogQuery.php b/src/applications/herald/query/HeraldEditLogQuery.php
index 69d1b04862..42db5f4db2 100644
--- a/src/applications/herald/query/HeraldEditLogQuery.php
+++ b/src/applications/herald/query/HeraldEditLogQuery.php
@@ -1,64 +1,48 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldEditLogQuery extends PhabricatorOffsetPagedQuery {
private $ruleIDs;
public function withRuleIDs(array $rule_ids) {
$this->ruleIDs = $rule_ids;
return $this;
}
public function execute() {
$table = new HeraldRuleEdit();
$conn_r = $table->establishConnection('r');
$where = $this->buildWhereClause($conn_r);
$order = $this->buildOrderClause($conn_r);
$limit = $this->buildLimitClause($conn_r);
$data = queryfx_all(
$conn_r,
'SELECT log.* FROM %T log %Q %Q %Q',
$table->getTableName(),
$where,
$order,
$limit);
return $table->loadAllFromArray($data);
}
private function buildWhereClause($conn_r) {
$where = array();
if ($this->ruleIDs) {
$where[] = qsprintf(
$conn_r,
'ruleID IN (%Ld)',
$this->ruleIDs);
}
return $this->formatWhereClause($where);
}
private function buildOrderClause($conn_r) {
return 'ORDER BY id DESC';
}
}
diff --git a/src/applications/herald/query/HeraldRuleQuery.php b/src/applications/herald/query/HeraldRuleQuery.php
index 71332231fe..1335ebc98a 100644
--- a/src/applications/herald/query/HeraldRuleQuery.php
+++ b/src/applications/herald/query/HeraldRuleQuery.php
@@ -1,90 +1,74 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldRuleQuery extends PhabricatorOffsetPagedQuery {
private $authorPHIDs;
private $ruleTypes;
private $contentTypes;
public function withAuthorPHIDs(array $author_phids) {
$this->authorPHIDs = $author_phids;
return $this;
}
public function withRuleTypes(array $types) {
$this->ruleTypes = $types;
return $this;
}
public function withContentTypes(array $types) {
$this->contentTypes = $types;
return $this;
}
public function execute() {
$table = new HeraldRule();
$conn_r = $table->establishConnection('r');
$where = $this->buildWhereClause($conn_r);
$order = $this->buildOrderClause($conn_r);
$limit = $this->buildLimitClause($conn_r);
$data = queryfx_all(
$conn_r,
'SELECT rule.* FROM %T rule %Q %Q %Q',
$table->getTableName(),
$where,
$order,
$limit);
return $table->loadAllFromArray($data);
}
private function buildWhereClause($conn_r) {
$where = array();
if ($this->authorPHIDs) {
$where[] = qsprintf(
$conn_r,
'rule.authorPHID IN (%Ls)',
$this->authorPHIDs);
}
if ($this->ruleTypes) {
$where[] = qsprintf(
$conn_r,
'rule.ruleType IN (%Ls)',
$this->ruleTypes);
}
if ($this->contentTypes) {
$where[] = qsprintf(
$conn_r,
'rule.contentType IN (%Ls)',
$this->contentTypes);
}
return $this->formatWhereClause($where);
}
private function buildOrderClause($conn_r) {
return 'ORDER BY id DESC';
}
}
diff --git a/src/applications/herald/storage/HeraldAction.php b/src/applications/herald/storage/HeraldAction.php
index 8215be08db..bf5ba4c36a 100644
--- a/src/applications/herald/storage/HeraldAction.php
+++ b/src/applications/herald/storage/HeraldAction.php
@@ -1,36 +1,20 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldAction extends HeraldDAO {
protected $ruleID;
protected $action;
protected $target;
protected function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'target' => self::SERIALIZATION_JSON,
),
self::CONFIG_TIMESTAMPS => false,
) + parent::getConfiguration();
}
}
diff --git a/src/applications/herald/storage/HeraldCondition.php b/src/applications/herald/storage/HeraldCondition.php
index 98c8d93c36..c86078c4b3 100644
--- a/src/applications/herald/storage/HeraldCondition.php
+++ b/src/applications/herald/storage/HeraldCondition.php
@@ -1,36 +1,20 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldCondition extends HeraldDAO {
protected $ruleID;
protected $fieldName;
protected $fieldCondition;
protected $value;
protected function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'value' => self::SERIALIZATION_JSON,
),
self::CONFIG_TIMESTAMPS => false,
) + parent::getConfiguration();
}
}
diff --git a/src/applications/herald/storage/HeraldDAO.php b/src/applications/herald/storage/HeraldDAO.php
index 183d25c343..bc4b886a87 100644
--- a/src/applications/herald/storage/HeraldDAO.php
+++ b/src/applications/herald/storage/HeraldDAO.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class HeraldDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'herald';
}
}
diff --git a/src/applications/herald/storage/HeraldRule.php b/src/applications/herald/storage/HeraldRule.php
index f03326a84e..1d0d979547 100644
--- a/src/applications/herald/storage/HeraldRule.php
+++ b/src/applications/herald/storage/HeraldRule.php
@@ -1,240 +1,224 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldRule extends HeraldDAO {
const TABLE_RULE_APPLIED = 'herald_ruleapplied';
protected $name;
protected $authorPHID;
protected $contentType;
protected $mustMatchAll;
protected $repetitionPolicy;
protected $ruleType;
protected $configVersion = 9;
private $ruleApplied = array(); // phids for which this rule has been applied
private $invalidOwner = false;
private $conditions;
private $actions;
public static function loadAllByContentTypeWithFullData(
$content_type,
$object_phid) {
$rules = id(new HeraldRule())->loadAllWhere(
'contentType = %s',
$content_type);
if (!$rules) {
return array();
}
self::flagDisabledUserRules($rules);
$rule_ids = mpull($rules, 'getID');
$conditions = id(new HeraldCondition())->loadAllWhere(
'ruleID in (%Ld)',
$rule_ids);
$actions = id(new HeraldAction())->loadAllWhere(
'ruleID in (%Ld)',
$rule_ids);
$applied = queryfx_all(
id(new HeraldRule())->establishConnection('r'),
'SELECT * FROM %T WHERE phid = %s',
self::TABLE_RULE_APPLIED,
$object_phid);
$applied = ipull($applied, null, 'ruleID');
$conditions = mgroup($conditions, 'getRuleID');
$actions = mgroup($actions, 'getRuleID');
$applied = igroup($applied, 'ruleID');
foreach ($rules as $rule) {
$rule->setRuleApplied($object_phid, isset($applied[$rule->getID()]));
$rule->attachConditions(idx($conditions, $rule->getID(), array()));
$rule->attachActions(idx($actions, $rule->getID(), array()));
}
return $rules;
}
private static function flagDisabledUserRules(array $rules) {
assert_instances_of($rules, 'HeraldRule');
$users = array();
foreach ($rules as $rule) {
if ($rule->getRuleType() != HeraldRuleTypeConfig::RULE_TYPE_PERSONAL) {
continue;
}
$users[$rule->getAuthorPHID()] = true;
}
$handles = id(new PhabricatorObjectHandleData(array_keys($users)))
->loadHandles();
foreach ($rules as $key => $rule) {
if ($rule->getRuleType() != HeraldRuleTypeConfig::RULE_TYPE_PERSONAL) {
continue;
}
$handle = $handles[$rule->getAuthorPHID()];
if (!$handle->isComplete() || $handle->isDisabled()) {
$rule->invalidOwner = true;
}
}
}
public function getRuleApplied($phid) {
if (idx($this->ruleApplied, $phid) === null) {
throw new Exception("Call setRuleApplied() before getRuleApplied()!");
}
return $this->ruleApplied[$phid];
}
public function setRuleApplied($phid, $applied) {
$this->ruleApplied[$phid] = $applied;
return $this;
}
public function loadConditions() {
if (!$this->getID()) {
return array();
}
return id(new HeraldCondition())->loadAllWhere(
'ruleID = %d',
$this->getID());
}
public function attachConditions(array $conditions) {
assert_instances_of($conditions, 'HeraldCondition');
$this->conditions = $conditions;
return $this;
}
public function getConditions() {
// TODO: validate conditions have been attached.
return $this->conditions;
}
public function loadActions() {
if (!$this->getID()) {
return array();
}
return id(new HeraldAction())->loadAllWhere(
'ruleID = %d',
$this->getID());
}
public function attachActions(array $actions) {
// TODO: validate actions have been attached.
assert_instances_of($actions, 'HeraldAction');
$this->actions = $actions;
return $this;
}
public function getActions() {
return $this->actions;
}
public function loadEdits() {
if (!$this->getID()) {
return array();
}
$edits = id(new HeraldRuleEdit())->loadAllWhere(
'ruleID = %d ORDER BY dateCreated DESC',
$this->getID());
return $edits;
}
public function logEdit($editor_phid, $action) {
id(new HeraldRuleEdit())
->setRuleID($this->getID())
->setRuleName($this->getName())
->setEditorPHID($editor_phid)
->setAction($action)
->save();
}
public function saveConditions(array $conditions) {
assert_instances_of($conditions, 'HeraldCondition');
return $this->saveChildren(
id(new HeraldCondition())->getTableName(),
$conditions);
}
public function saveActions(array $actions) {
assert_instances_of($actions, 'HeraldAction');
return $this->saveChildren(
id(new HeraldAction())->getTableName(),
$actions);
}
protected function saveChildren($table_name, array $children) {
assert_instances_of($children, 'HeraldDAO');
if (!$this->getID()) {
throw new Exception("Save rule before saving children.");
}
foreach ($children as $child) {
$child->setRuleID($this->getID());
}
// TODO:
// $this->openTransaction();
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE ruleID = %d',
$table_name,
$this->getID());
foreach ($children as $child) {
$child->save();
}
// $this->saveTransaction();
}
public function delete() {
// TODO:
// $this->openTransaction();
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE ruleID = %d',
id(new HeraldCondition())->getTableName(),
$this->getID());
queryfx(
$this->establishConnection('w'),
'DELETE FROM %T WHERE ruleID = %d',
id(new HeraldAction())->getTableName(),
$this->getID());
parent::delete();
// $this->saveTransaction();
}
public function hasInvalidOwner() {
return $this->invalidOwner;
}
}
diff --git a/src/applications/herald/storage/HeraldRuleEdit.php b/src/applications/herald/storage/HeraldRuleEdit.php
index 463381bf18..bedb2a5beb 100644
--- a/src/applications/herald/storage/HeraldRuleEdit.php
+++ b/src/applications/herald/storage/HeraldRuleEdit.php
@@ -1,26 +1,10 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldRuleEdit extends HeraldDAO {
protected $editorPHID;
protected $ruleID;
protected $ruleName;
protected $action;
}
diff --git a/src/applications/herald/storage/transcript/HeraldApplyTranscript.php b/src/applications/herald/storage/transcript/HeraldApplyTranscript.php
index 0069525cbf..f3a3189338 100644
--- a/src/applications/herald/storage/transcript/HeraldApplyTranscript.php
+++ b/src/applications/herald/storage/transcript/HeraldApplyTranscript.php
@@ -1,80 +1,64 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldApplyTranscript extends HeraldDAO {
protected $action;
protected $target;
protected $ruleID;
protected $effector;
protected $reason;
protected $applied;
protected $appliedReason;
public function __construct(
HeraldEffect $effect,
$applied,
$reason = null) {
$this->setAction($effect->getAction());
$this->setTarget($effect->getTarget());
$this->setRuleID($effect->getRuleID());
$this->setEffector($effect->getEffector());
$this->setReason($effect->getReason());
$this->setApplied($applied);
$this->setAppliedReason($reason);
}
public function getAction() {
return $this->action;
}
public function getTarget() {
return $this->target;
}
public function getRuleID() {
return $this->ruleID;
}
public function getEffector() {
return $this->effector;
}
public function getReason() {
return $this->reason;
}
public function getApplied() {
return $this->applied;
}
public function setAppliedReason($applied_reason) {
$this->appliedReason = $applied_reason;
return $this;
}
public function getAppliedReason() {
return $this->appliedReason;
}
}
diff --git a/src/applications/herald/storage/transcript/HeraldConditionTranscript.php b/src/applications/herald/storage/transcript/HeraldConditionTranscript.php
index ea85fd32c6..f78f31a555 100644
--- a/src/applications/herald/storage/transcript/HeraldConditionTranscript.php
+++ b/src/applications/herald/storage/transcript/HeraldConditionTranscript.php
@@ -1,91 +1,75 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldConditionTranscript {
protected $ruleID;
protected $conditionID;
protected $fieldName;
protected $condition;
protected $testValue;
protected $note;
protected $result;
public function setRuleID($rule_id) {
$this->ruleID = $rule_id;
return $this;
}
public function getRuleID() {
return $this->ruleID;
}
public function setConditionID($condition_id) {
$this->conditionID = $condition_id;
return $this;
}
public function getConditionID() {
return $this->conditionID;
}
public function setFieldName($field_name) {
$this->fieldName = $field_name;
return $this;
}
public function getFieldName() {
return $this->fieldName;
}
public function setCondition($condition) {
$this->condition = $condition;
return $this;
}
public function getCondition() {
return $this->condition;
}
public function setTestValue($test_value) {
$this->testValue = $test_value;
return $this;
}
public function getTestValue() {
return $this->testValue;
}
public function setNote($note) {
$this->note = $note;
return $this;
}
public function getNote() {
return $this->note;
}
public function setResult($result) {
$this->result = $result;
return $this;
}
public function getResult() {
return $this->result;
}
}
diff --git a/src/applications/herald/storage/transcript/HeraldObjectTranscript.php b/src/applications/herald/storage/transcript/HeraldObjectTranscript.php
index 2289ec5f2c..701bb132fd 100644
--- a/src/applications/herald/storage/transcript/HeraldObjectTranscript.php
+++ b/src/applications/herald/storage/transcript/HeraldObjectTranscript.php
@@ -1,61 +1,45 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldObjectTranscript {
protected $phid;
protected $type;
protected $name;
protected $fields;
public function setPHID($phid) {
$this->phid = $phid;
return $this;
}
public function getPHID() {
return $this->phid;
}
public function setType($type) {
$this->type = $type;
return $this;
}
public function getType() {
return $this->type;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function setFields(array $fields) {
$this->fields = $fields;
return $this;
}
public function getFields() {
return $this->fields;
}
}
diff --git a/src/applications/herald/storage/transcript/HeraldRuleTranscript.php b/src/applications/herald/storage/transcript/HeraldRuleTranscript.php
index 3169aa71ca..095907defb 100644
--- a/src/applications/herald/storage/transcript/HeraldRuleTranscript.php
+++ b/src/applications/herald/storage/transcript/HeraldRuleTranscript.php
@@ -1,72 +1,56 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldRuleTranscript {
protected $ruleID;
protected $result;
protected $reason;
protected $ruleName;
protected $ruleOwner;
public function setResult($result) {
$this->result = $result;
return $this;
}
public function getResult() {
return $this->result;
}
public function setReason($reason) {
$this->reason = $reason;
return $this;
}
public function getReason() {
return $this->reason;
}
public function setRuleID($rule_id) {
$this->ruleID = $rule_id;
return $this;
}
public function getRuleID() {
return $this->ruleID;
}
public function setRuleName($rule_name) {
$this->ruleName = $rule_name;
return $this;
}
public function getRuleName() {
return $this->ruleName;
}
public function setRuleOwner($rule_owner) {
$this->ruleOwner = $rule_owner;
return $this;
}
public function getRuleOwner() {
return $this->ruleOwner;
}
}
diff --git a/src/applications/herald/storage/transcript/HeraldTranscript.php b/src/applications/herald/storage/transcript/HeraldTranscript.php
index 1a94539fe7..713f91f0c5 100644
--- a/src/applications/herald/storage/transcript/HeraldTranscript.php
+++ b/src/applications/herald/storage/transcript/HeraldTranscript.php
@@ -1,185 +1,169 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldTranscript extends HeraldDAO {
protected $id;
protected $phid;
protected $objectTranscript;
protected $ruleTranscripts = array();
protected $conditionTranscripts = array();
protected $applyTranscripts = array();
protected $time;
protected $host;
protected $duration;
protected $objectPHID;
protected $dryRun;
const TABLE_SAVED_HEADER = 'herald_savedheader';
public function getXHeraldRulesHeader() {
$ids = array();
foreach ($this->applyTranscripts as $xscript) {
if ($xscript->getApplied()) {
if ($xscript->getRuleID()) {
$ids[] = $xscript->getRuleID();
}
}
}
if (!$ids) {
return 'none';
}
// A rule may have multiple effects, which will cause it to be listed
// multiple times.
$ids = array_unique($ids);
foreach ($ids as $k => $id) {
$ids[$k] = '<'.$id.'>';
}
return implode(', ', $ids);
}
public static function saveXHeraldRulesHeader($phid, $header) {
// Combine any existing header with the new header, listing all rules
// which have ever triggered for this object.
$header = self::combineXHeraldRulesHeaders(
self::loadXHeraldRulesHeader($phid),
$header);
queryfx(
id(new HeraldTranscript())->establishConnection('w'),
'INSERT INTO %T (phid, header) VALUES (%s, %s)
ON DUPLICATE KEY UPDATE header = VALUES(header)',
self::TABLE_SAVED_HEADER,
$phid,
$header);
return $header;
}
private static function combineXHeraldRulesHeaders($u, $v) {
$u = preg_split('/[, ]+/', $u);
$v = preg_split('/[, ]+/', $v);
$combined = array_unique(array_filter(array_merge($u, $v)));
return implode(', ', $combined);
}
public static function loadXHeraldRulesHeader($phid) {
$header = queryfx_one(
id(new HeraldTranscript())->establishConnection('r'),
'SELECT * FROM %T WHERE phid = %s',
self::TABLE_SAVED_HEADER,
$phid);
if ($header) {
return idx($header, 'header');
}
return null;
}
protected function getConfiguration() {
// Ugh. Too much of a mess to deal with.
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_SERIALIZATION => array(
'objectTranscript' => self::SERIALIZATION_PHP,
'ruleTranscripts' => self::SERIALIZATION_PHP,
'conditionTranscripts' => self::SERIALIZATION_PHP,
'applyTranscripts' => self::SERIALIZATION_PHP,
),
) + parent::getConfiguration();
}
public function __construct() {
$this->time = time();
$this->host = php_uname('n');
}
public function addApplyTranscript(HeraldApplyTranscript $transcript) {
$this->applyTranscripts[] = $transcript;
return $this;
}
public function getApplyTranscripts() {
return nonempty($this->applyTranscripts, array());
}
public function setDuration($duration) {
$this->duration = $duration;
return $this;
}
public function setObjectTranscript(HeraldObjectTranscript $transcript) {
$this->objectTranscript = $transcript;
return $this;
}
public function getObjectTranscript() {
return $this->objectTranscript;
}
public function addRuleTranscript(HeraldRuleTranscript $transcript) {
$this->ruleTranscripts[$transcript->getRuleID()] = $transcript;
return $this;
}
public function discardDetails() {
$this->applyTranscripts = null;
$this->ruleTranscripts = null;
$this->objectTranscript = null;
$this->conditionTranscripts = null;
}
public function getRuleTranscripts() {
return nonempty($this->ruleTranscripts, array());
}
public function addConditionTranscript(
HeraldConditionTranscript $transcript) {
$rule_id = $transcript->getRuleID();
$cond_id = $transcript->getConditionID();
$this->conditionTranscripts[$rule_id][$cond_id] = $transcript;
return $this;
}
public function getConditionTranscriptsForRule($rule_id) {
return idx($this->conditionTranscripts, $rule_id, array());
}
public function getMetadataMap() {
return array(
'Run At Epoch' => date('F jS, g:i:s A', $this->time),
'Run On Host' => $this->host,
'Run Duration' => (int)(1000 * $this->duration).' ms',
);
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID('HLXS');
}
}
diff --git a/src/applications/herald/view/HeraldRuleEditHistoryView.php b/src/applications/herald/view/HeraldRuleEditHistoryView.php
index a6c5c574dc..0d00dacde4 100644
--- a/src/applications/herald/view/HeraldRuleEditHistoryView.php
+++ b/src/applications/herald/view/HeraldRuleEditHistoryView.php
@@ -1,95 +1,79 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldRuleEditHistoryView extends AphrontView {
private $edits;
private $handles;
private $user;
public function setEdits(array $edits) {
$this->edits = $edits;
return $this;
}
public function getEdits() {
return $this->edits;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setUser($user) {
$this->user = $user;
return $this;
}
public function render() {
$rows = array();
foreach ($this->edits as $edit) {
$name = nonempty($edit->getRuleName(), 'Unknown Rule');
$rule_name = phutil_render_tag(
'strong',
array(),
phutil_escape_html($name));
switch ($edit->getAction()) {
case 'create':
$details = "Created rule '{$rule_name}'.";
break;
case 'delete':
$details = "Deleted rule '{$rule_name}'.";
break;
case 'edit':
default:
$details = "Edited rule '{$rule_name}'.";
break;
}
$rows[] = array(
$edit->getRuleID(),
$this->handles[$edit->getEditorPHID()]->renderLink(),
$details,
phabricator_datetime($edit->getDateCreated(), $this->user),
);
}
$table = new AphrontTableView($rows);
$table->setNoDataString("No edits for rule.");
$table->setHeaders(
array(
'Rule ID',
'Editor',
'Details',
'Edit Date',
));
$table->setColumnClasses(
array(
'',
'',
'wide',
'',
));
return $table->render();
}
}
diff --git a/src/applications/herald/view/HeraldRuleListView.php b/src/applications/herald/view/HeraldRuleListView.php
index cdefcf30bd..ff3dfccaaa 100644
--- a/src/applications/herald/view/HeraldRuleListView.php
+++ b/src/applications/herald/view/HeraldRuleListView.php
@@ -1,131 +1,115 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class HeraldRuleListView extends AphrontView {
private $rules;
private $handles;
private $showAuthor;
private $showRuleType;
private $user;
public function setRules(array $rules) {
assert_instances_of($rules, 'HeraldRule');
$this->rules = $rules;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setShowAuthor($show_author) {
$this->showAuthor = $show_author;
return $this;
}
public function setShowRuleType($show_rule_type) {
$this->showRuleType = $show_rule_type;
return $this;
}
public function setUser($user) {
$this->user = $user;
return $this;
}
public function render() {
$type_map = HeraldRuleTypeConfig::getRuleTypeMap();
$rows = array();
foreach ($this->rules as $rule) {
if ($rule->getRuleType() == HeraldRuleTypeConfig::RULE_TYPE_GLOBAL) {
$author = null;
} else {
$author = $this->handles[$rule->getAuthorPHID()]->renderLink();
}
$name = phutil_render_tag(
'a',
array(
'href' => '/herald/rule/'.$rule->getID().'/',
),
phutil_escape_html($rule->getName()));
$edit_log = phutil_render_tag(
'a',
array(
'href' => '/herald/history/'.$rule->getID().'/',
),
'View Edit Log');
$delete = javelin_render_tag(
'a',
array(
'href' => '/herald/delete/'.$rule->getID().'/',
'sigil' => 'workflow',
'class' => 'button small grey',
),
'Delete');
$rows[] = array(
$type_map[$rule->getRuleType()],
$author,
$name,
$edit_log,
$delete,
);
}
$table = new AphrontTableView($rows);
$table->setNoDataString("No matching rules.");
$table->setHeaders(
array(
'Rule Type',
'Author',
'Rule Name',
'Edit Log',
'',
));
$table->setColumnClasses(
array(
'',
'',
'wide pri',
'',
'action'
));
$table->setColumnVisibility(
array(
$this->showRuleType,
$this->showAuthor,
true,
true,
true,
));
return $table->render();
}
}
diff --git a/src/applications/macro/application/PhabricatorApplicationMacro.php b/src/applications/macro/application/PhabricatorApplicationMacro.php
index 3d9f5c0407..191cf08872 100644
--- a/src/applications/macro/application/PhabricatorApplicationMacro.php
+++ b/src/applications/macro/application/PhabricatorApplicationMacro.php
@@ -1,51 +1,35 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationMacro extends PhabricatorApplication {
public function getBaseURI() {
return '/macro/';
}
public function getShortDescription() {
return 'Image Macros and Memes';
}
public function getAutospriteName() {
return 'macro';
}
public function getTitleGlyph() {
return "\xE2\x9A\x98";
}
public function getApplicationGroup() {
return self::GROUP_UTILITIES;
}
public function getRoutes() {
return array(
'/macro/' => array(
'' => 'PhabricatorMacroListController',
'edit/(?:(?P<id>[1-9]\d*)/)?' => 'PhabricatorMacroEditController',
'delete/(?P<id>[1-9]\d*)/' => 'PhabricatorMacroDeleteController',
),
);
}
}
diff --git a/src/applications/macro/controller/PhabricatorMacroController.php b/src/applications/macro/controller/PhabricatorMacroController.php
index b334c39d4f..48adf16723 100644
--- a/src/applications/macro/controller/PhabricatorMacroController.php
+++ b/src/applications/macro/controller/PhabricatorMacroController.php
@@ -1,37 +1,21 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorMacroController
extends PhabricatorController {
protected function buildSideNavView(PhabricatorFileImageMacro $macro = null) {
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
$nav->addLabel('Create');
$nav->addFilter('edit', 'Create Macro');
$nav->addSpacer();
$nav->addLabel('Macros');
$nav->addFilter('', 'All Macros');
return $nav;
}
}
diff --git a/src/applications/macro/controller/PhabricatorMacroDeleteController.php b/src/applications/macro/controller/PhabricatorMacroDeleteController.php
index f1115da7ba..2f00e92c3b 100644
--- a/src/applications/macro/controller/PhabricatorMacroDeleteController.php
+++ b/src/applications/macro/controller/PhabricatorMacroDeleteController.php
@@ -1,58 +1,42 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorMacroDeleteController
extends PhabricatorMacroController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$macro = id(new PhabricatorFileImageMacro())->load($this->id);
if (!$macro) {
return new Aphront404Response();
}
$request = $this->getRequest();
if ($request->isDialogFormPost()) {
$macro->delete();
return id(new AphrontRedirectResponse())->setURI(
$this->getApplicationURI());
}
$dialog = new AphrontDialogView();
$dialog
->setUser($request->getUser())
->setTitle('Really delete macro?')
->appendChild(
'<p>Really delete the much-beloved image macro "'.
phutil_escape_html($macro->getName()).'"? It will be sorely missed.'.
'</p>')
->setSubmitURI($this->getApplicationURI('/delete/'.$this->id.'/'))
->addSubmitButton('Delete')
->addCancelButton($this->getApplicationURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/macro/controller/PhabricatorMacroEditController.php b/src/applications/macro/controller/PhabricatorMacroEditController.php
index e198c473c2..7c9cdb4fe9 100644
--- a/src/applications/macro/controller/PhabricatorMacroEditController.php
+++ b/src/applications/macro/controller/PhabricatorMacroEditController.php
@@ -1,145 +1,129 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorMacroEditController
extends PhabricatorMacroController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
if ($this->id) {
$macro = id(new PhabricatorFileImageMacro())->load($this->id);
if (!$macro) {
return new Aphront404Response();
}
} else {
$macro = new PhabricatorFileImageMacro();
}
$errors = array();
$e_name = true;
$file = null;
$request = $this->getRequest();
$user = $request->getUser();
if ($request->isFormPost()) {
$macro->setName($request->getStr('name'));
if (!strlen($macro->getName())) {
$errors[] = 'Macro name is required.';
$e_name = 'Required';
} else if (!preg_match('/^[a-z0-9_-]{3,}$/', $macro->getName())) {
$errors[] = 'Macro must be at least three characters long and contain '.
'only lowercase letters, digits, hyphen and underscore.';
$e_name = 'Invalid';
} else {
$e_name = null;
}
if (!$errors) {
$file = PhabricatorFile::newFromPHPUpload(
idx($_FILES, 'file'),
array(
'name' => $request->getStr('name'),
'authorPHID' => $user->getPHID(),
));
$macro->setFilePHID($file->getPHID());
try {
$macro->save();
return id(new AphrontRedirectResponse())->setURI(
$this->getApplicationURI());
} catch (AphrontQueryDuplicateKeyException $ex) {
$errors[] = 'Macro name is not unique!';
$e_name = 'Duplicate';
}
}
} else if ($this->id) {
$file = id(new PhabricatorFile())
->loadOneWhere('phid = %s', $macro->getFilePHID());
}
$caption = null;
if ($file) {
$caption = phutil_render_tag(
'img',
array(
'src' => $file->getViewURI(),
));
}
if ($errors) {
$error_view = new AphrontErrorView();
$error_view->setTitle('Form Errors');
$error_view->setErrors($errors);
} else {
$error_view = null;
}
$form = new AphrontFormView();
$form->setUser($request->getUser());
$form
->setEncType('multipart/form-data')
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Name')
->setName('name')
->setValue($macro->getName())
->setCaption('This word or phrase will be replaced with the image.')
->setError($e_name))
->appendChild(
id(new AphrontFormFileControl())
->setLabel('File')
->setName('file')
->setCaption($caption)
->setError(true))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Save Image Macro')
->addCancelButton($this->getApplicationURI()));
$panel = new AphrontPanelView();
if ($macro->getID()) {
$title = 'Edit Image Macro';
} else {
$title = 'Create Image Macro';
}
$panel->setHeader($title);
$panel->appendChild($form);
$panel->setWidth(AphrontPanelView::WIDTH_FULL);
$nav = $this->buildSideNavView($macro);
$nav->selectFilter('#', 'edit');
$nav->appendChild($error_view);
$nav->appendChild($panel);
return $this->buildApplicationPage(
$nav,
array(
'title' => $title,
));
}
}
diff --git a/src/applications/macro/controller/PhabricatorMacroListController.php b/src/applications/macro/controller/PhabricatorMacroListController.php
index f9ec3b9e6a..3d73a4e5b7 100644
--- a/src/applications/macro/controller/PhabricatorMacroListController.php
+++ b/src/applications/macro/controller/PhabricatorMacroListController.php
@@ -1,140 +1,124 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorMacroListController
extends PhabricatorMacroController {
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$macro_table = new PhabricatorFileImageMacro();
$filter = $request->getStr('name');
if (strlen($filter)) {
$macros = $macro_table->loadAllWhere(
'name LIKE %~',
$filter);
$nodata = pht(
'There are no macros matching the filter "%s".',
phutil_escape_html($filter));
} else {
$pager = new AphrontPagerView();
$pager->setOffset($request->getInt('page'));
$macros = $macro_table->loadAllWhere(
'1 = 1 ORDER BY id DESC LIMIT %d, %d',
$pager->getOffset(),
$pager->getPageSize());
// Get an exact count since the size here is reasonably going to be a few
// thousand at most in any reasonable case.
$count = queryfx_one(
$macro_table->establishConnection('r'),
'SELECT COUNT(*) N FROM %T',
$macro_table->getTableName());
$count = $count['N'];
$pager->setCount($count);
$pager->setURI($request->getRequestURI(), 'page');
$nodata = pht('There are no image macros yet.');
}
$file_phids = mpull($macros, 'getFilePHID');
$files = array();
if ($file_phids) {
$files = id(new PhabricatorFile())->loadAllWhere(
"phid IN (%Ls)",
$file_phids);
$author_phids = mpull($files, 'getAuthorPHID', 'getPHID');
$this->loadHandles($author_phids);
}
$files_map = mpull($files, null, 'getPHID');
$filter_form = id(new AphrontFormView())
->setMethod('GET')
->setUser($request->getUser())
->appendChild(
id(new AphrontFormTextControl())
->setName('name')
->setLabel('Name')
->setValue($filter))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Filter Image Macros'));
$filter_view = new AphrontListFilterView();
$filter_view->appendChild($filter_form);
$nav = $this->buildSideNavView();
$nav->selectFilter('/');
$nav->appendChild($filter_view);
if ($macros) {
$pinboard = new PhabricatorPinboardView();
foreach ($macros as $macro) {
$file_phid = $macro->getFilePHID();
$file = idx($files_map, $file_phid);
$item = new PhabricatorPinboardItemView();
if ($file) {
$item->setImageURI($file->getThumb220x165URI());
$item->setImageSize(220, 165);
if ($file->getAuthorPHID()) {
$author_handle = $this->getHandle($file->getAuthorPHID());
$item->appendChild(
'Created by '.$author_handle->renderLink());
}
$datetime = phabricator_date($file->getDateCreated(), $viewer);
$item->appendChild(
phutil_render_tag(
'div',
array(),
'Created on '.$datetime));
}
$item->setURI($this->getApplicationURI('/edit/'.$macro->getID().'/'));
$item->setHeader($macro->getName());
$pinboard->addItem($item);
}
$nav->appendChild($pinboard);
} else {
$list = new PhabricatorObjectItemListView();
$list->setNoDataString($nodata);
$nav->appendChild($list);
}
if ($filter === null) {
$nav->appendChild($pager);
}
return $this->buildApplicationPage(
$nav,
array(
'device' => true,
'title' => 'Image Macros',
));
}
}
diff --git a/src/applications/macro/storage/PhabricatorFileImageMacro.php b/src/applications/macro/storage/PhabricatorFileImageMacro.php
index e70a357a4d..8e41e7b471 100644
--- a/src/applications/macro/storage/PhabricatorFileImageMacro.php
+++ b/src/applications/macro/storage/PhabricatorFileImageMacro.php
@@ -1,45 +1,29 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFileImageMacro extends PhabricatorFileDAO {
protected $filePHID;
protected $name;
public function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
) + parent::getConfiguration();
}
static public function newFromImageURI($uri, $file_name, $image_macro_name) {
$file = PhabricatorFile::newFromFileDownload($uri, $file_name);
if (!$file) {
return null;
}
$image_macro = new PhabricatorFileImageMacro();
$image_macro->setName($image_macro_name);
$image_macro->setFilePHID($file->getPHID());
$image_macro->save();
return $image_macro;
}
}
diff --git a/src/applications/mailinglists/application/PhabricatorApplicationMailingLists.php b/src/applications/mailinglists/application/PhabricatorApplicationMailingLists.php
index 2262ffffa8..b593151cdc 100644
--- a/src/applications/mailinglists/application/PhabricatorApplicationMailingLists.php
+++ b/src/applications/mailinglists/application/PhabricatorApplicationMailingLists.php
@@ -1,55 +1,39 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationMailingLists extends PhabricatorApplication {
public function getName() {
return 'Mailing Lists';
}
public function getBaseURI() {
return '/mailinglists/';
}
public function getShortDescription() {
return 'Manage External Lists';
}
public function getAutospriteName() {
return 'mail';
}
public function getApplicationGroup() {
return self::GROUP_ADMIN;
}
public function getRoutes() {
return array(
'/mailinglists/' => array(
'' => 'PhabricatorMailingListsListController',
'edit/(?:(?P<id>[1-9]\d*)/)?'
=> 'PhabricatorMailingListsEditController',
),
);
}
public function getTitleGlyph() {
return '@';
}
}
diff --git a/src/applications/mailinglists/controller/PhabricatorMailingListsEditController.php b/src/applications/mailinglists/controller/PhabricatorMailingListsEditController.php
index eecc9b1db2..1fded3ad11 100644
--- a/src/applications/mailinglists/controller/PhabricatorMailingListsEditController.php
+++ b/src/applications/mailinglists/controller/PhabricatorMailingListsEditController.php
@@ -1,151 +1,135 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorMailingListsEditController
extends PhabricatorController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
if ($this->id) {
$list = id(new PhabricatorMetaMTAMailingList())->load($this->id);
if (!$list) {
return new Aphront404Response();
}
} else {
$list = new PhabricatorMetaMTAMailingList();
}
$e_email = true;
$e_uri = null;
$e_name = true;
$errors = array();
$request = $this->getRequest();
if ($request->isFormPost()) {
$list->setName($request->getStr('name'));
$list->setEmail($request->getStr('email'));
$list->setURI($request->getStr('uri'));
$e_email = null;
$e_name = null;
if (!strlen($list->getEmail())) {
$e_email = 'Required';
$errors[] = 'Email is required.';
}
if (!strlen($list->getName())) {
$e_name = 'Required';
$errors[] = 'Name is required.';
} else if (preg_match('/[ ,]/', $list->getName())) {
$e_name = 'Invalid';
$errors[] = 'Name must not contain spaces or commas.';
}
if ($list->getURI()) {
if (!PhabricatorEnv::isValidWebResource($list->getURI())) {
$e_uri = 'Invalid';
$errors[] = 'Mailing list URI must point to a valid web page.';
}
}
if (!$errors) {
try {
$list->save();
return id(new AphrontRedirectResponse())
->setURI($this->getApplicationURI());
} catch (AphrontQueryDuplicateKeyException $ex) {
$e_email = 'Duplicate';
$errors[] = 'Another mailing list already uses that address.';
}
}
}
$error_view = null;
if ($errors) {
$error_view = id(new AphrontErrorView())
->setTitle('Form Errors')
->setErrors($errors);
}
$form = new AphrontFormView();
$form->setUser($request->getUser());
if ($list->getID()) {
$form->setAction($this->getApplicationURI('/edit/'.$list->getID().'/'));
} else {
$form->setAction($this->getApplicationURI('/edit/'));
}
$form
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Email')
->setName('email')
->setValue($list->getEmail())
->setCaption('Email will be delivered to this address.')
->setError($e_email))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Name')
->setName('name')
->setError($e_name)
->setCaption('Human-readable display and autocomplete name.')
->setValue($list->getName()))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('URI')
->setName('uri')
->setError($e_uri)
->setCaption('Optional link to mailing list archives or info.')
->setValue($list->getURI()))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('PHID')
->setValue(nonempty($list->getPHID(), '-')))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Save')
->addCancelButton($this->getApplicationURI()));
$panel = new AphrontPanelView();
if ($list->getID()) {
$panel->setHeader('Edit Mailing List');
} else {
$panel->setHeader('Create New Mailing List');
}
$panel->appendChild($form);
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
return $this->buildApplicationPage(
array(
$error_view,
$panel,
),
array(
'title' => 'Edit Mailing List',
));
}
}
diff --git a/src/applications/mailinglists/controller/PhabricatorMailingListsListController.php b/src/applications/mailinglists/controller/PhabricatorMailingListsListController.php
index 7d317a4efd..3e2281ec87 100644
--- a/src/applications/mailinglists/controller/PhabricatorMailingListsListController.php
+++ b/src/applications/mailinglists/controller/PhabricatorMailingListsListController.php
@@ -1,86 +1,70 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorMailingListsListController
extends PhabricatorController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$offset = $request->getInt('offset', 0);
$pager = new AphrontPagerView();
$pager->setPageSize(250);
$pager->setOffset($offset);
$pager->setURI($request->getRequestURI(), 'offset');
$list = new PhabricatorMetaMTAMailingList();
$conn_r = $list->establishConnection('r');
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T
ORDER BY name ASC
LIMIT %d, %d',
$list->getTableName(),
$pager->getOffset(), $pager->getPageSize() + 1);
$data = $pager->sliceResults($data);
$lists = $list->loadAllFromArray($data);
$rows = array();
foreach ($lists as $list) {
$rows[] = array(
phutil_escape_html($list->getName()),
phutil_escape_html($list->getEmail()),
phutil_render_tag(
'a',
array(
'class' => 'button grey small',
'href' => $this->getApplicationURI('/edit/'.$list->getID().'/'),
),
'Edit'),
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Name',
'Email',
'',
));
$table->setColumnClasses(
array(
null,
'wide',
'action',
));
$panel = new AphrontPanelView();
$panel->appendChild($table);
$panel->setHeader('Mailing Lists');
$panel->setCreateButton('Add New List', $this->getApplicationURI('/edit/'));
$panel->appendChild($pager);
return $this->buildApplicationPage(
$panel,
array(
'title' => 'Mailing Lists',
));
}
}
diff --git a/src/applications/mailinglists/storage/PhabricatorMetaMTAMailingList.php b/src/applications/mailinglists/storage/PhabricatorMetaMTAMailingList.php
index c429ccbd0d..3ac298eabb 100644
--- a/src/applications/mailinglists/storage/PhabricatorMetaMTAMailingList.php
+++ b/src/applications/mailinglists/storage/PhabricatorMetaMTAMailingList.php
@@ -1,37 +1,21 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorMetaMTAMailingList extends PhabricatorMetaMTADAO {
protected $name;
protected $phid;
protected $email;
protected $uri;
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_MLST);
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
) + parent::getConfiguration();
}
}
diff --git a/src/applications/maniphest/ManiphestReplyHandler.php b/src/applications/maniphest/ManiphestReplyHandler.php
index 5eb9ab23f7..56e262cb88 100644
--- a/src/applications/maniphest/ManiphestReplyHandler.php
+++ b/src/applications/maniphest/ManiphestReplyHandler.php
@@ -1,182 +1,166 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestReplyHandler extends PhabricatorMailReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof ManiphestTask)) {
throw new Exception("Mail receiver is not a ManiphestTask!");
}
}
public function getPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle) {
return $this->getDefaultPrivateReplyHandlerEmailAddress($handle, 'T');
}
public function getPublicReplyHandlerEmailAddress() {
return $this->getDefaultPublicReplyHandlerEmailAddress('T');
}
public function getReplyHandlerDomain() {
return PhabricatorEnv::getEnvConfig(
'metamta.maniphest.reply-handler-domain');
}
public function getReplyHandlerInstructions() {
if ($this->supportsReplies()) {
return "Reply to comment or attach files, or !close, !claim, or ".
"!unsubscribe.";
} else {
return null;
}
}
protected 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);
$body = $this->enhanceBodyWithAttachments($body, $mail->getAttachments());
$xactions = array();
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_EMAIL,
array(
'id' => $mail->getID(),
));
$template = new ManiphestTransaction();
$template->setContentSource($content_source);
$template->setAuthorPHID($user->getPHID());
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);
$task->setPriority(ManiphestTaskPriority::getDefaultPriority());
} 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;
}
$ccs = $mail->loadCCPHIDs();
if ($ccs) {
$old_ccs = $task->getCCPHIDs();
$new_ccs = array_unique(array_merge($old_ccs, $ccs));
$cc_xaction = clone $template;
$cc_xaction->setTransactionType(ManiphestTransactionType::TYPE_CCS);
$cc_xaction->setNewValue($new_ccs);
$xactions[] = $cc_xaction;
}
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK,
array(
'task' => $task,
'mail' => $mail,
'new' => $is_new_task,
'transactions' => $xactions,
));
$event->setUser($user);
PhutilEventEngine::dispatchEvent($event);
$task = $event->getValue('task');
$xactions = $event->getValue('transactions');
$editor = new ManiphestTransactionEditor();
$editor->setActor($user);
$editor->setParentMessageID($mail->getMessageID());
$editor->setExcludeMailRecipientPHIDs(
$this->getExcludeMailRecipientPHIDs());
$editor->applyTransactions($task, $xactions);
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK,
array(
'task' => $task,
'new' => $is_new_task,
'transactions' => $xactions,
));
$event->setUser($user);
PhutilEventEngine::dispatchEvent($event);
}
}
diff --git a/src/applications/maniphest/ManiphestTaskQuery.php b/src/applications/maniphest/ManiphestTaskQuery.php
index 4ab9a2359d..221113abfd 100644
--- a/src/applications/maniphest/ManiphestTaskQuery.php
+++ b/src/applications/maniphest/ManiphestTaskQuery.php
@@ -1,677 +1,661 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Query tasks by specific criteria. This class uses the higher-performance
* but less-general Maniphest indexes to satisfy queries.
*
* @group maniphest
*/
final class ManiphestTaskQuery extends PhabricatorQuery {
private $taskIDs = array();
private $taskPHIDs = array();
private $authorPHIDs = array();
private $ownerPHIDs = array();
private $includeUnowned = null;
private $projectPHIDs = array();
private $xprojectPHIDs = array();
private $subscriberPHIDs = array();
private $anyProjectPHIDs = array();
private $includeNoProject = null;
private $fullTextSearch = '';
private $status = 'status-any';
const STATUS_ANY = 'status-any';
const STATUS_OPEN = 'status-open';
const STATUS_CLOSED = 'status-closed';
const STATUS_RESOLVED = 'status-resolved';
const STATUS_WONTFIX = 'status-wontfix';
const STATUS_INVALID = 'status-invalid';
const STATUS_SPITE = 'status-spite';
const STATUS_DUPLICATE = 'status-duplicate';
private $priority = null;
private $minPriority = null;
private $maxPriority = null;
private $groupBy = 'group-none';
const GROUP_NONE = 'group-none';
const GROUP_PRIORITY = 'group-priority';
const GROUP_OWNER = 'group-owner';
const GROUP_STATUS = 'group-status';
const GROUP_PROJECT = 'group-project';
private $orderBy = 'order-modified';
const ORDER_PRIORITY = 'order-priority';
const ORDER_CREATED = 'order-created';
const ORDER_MODIFIED = 'order-modified';
const ORDER_TITLE = 'order-title';
private $limit = null;
const DEFAULT_PAGE_SIZE = 1000;
private $offset = 0;
private $calculateRows = false;
private $rowCount = null;
private $groupByProjectResults = null; // See comment at bottom for details
public function withAuthors(array $authors) {
$this->authorPHIDs = $authors;
return $this;
}
public function withTaskIDs(array $ids) {
$this->taskIDs = $ids;
return $this;
}
public function withTaskPHIDs(array $phids) {
$this->taskPHIDs = $phids;
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 withAllProjects(array $projects) {
$this->includeNoProject = false;
foreach ($projects as $k => $phid) {
if ($phid == ManiphestTaskOwner::PROJECT_NO_PROJECT) {
$this->includeNoProject = true;
unset($projects[$k]);
}
}
$this->projectPHIDs = $projects;
return $this;
}
public function withoutProjects(array $projects) {
$this->xprojectPHIDs = $projects;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withPriority($priority) {
$this->priority = $priority;
return $this;
}
public function withPrioritiesBetween($min, $max) {
$this->minPriority = $min;
$this->maxPriority = $max;
return $this;
}
public function withSubscribers(array $subscribers) {
$this->subscriberPHIDs = $subscribers;
return $this;
}
public function withFullTextSearch($fulltext_search) {
$this->fullTextSearch = $fulltext_search;
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 getGroupByProjectResults() {
return $this->groupByProjectResults;
}
public function withAnyProjects(array $projects) {
$this->anyProjectPHIDs = $projects;
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->buildTaskIDsWhereClause($conn);
$where[] = $this->buildTaskPHIDsWhereClause($conn);
$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[] = $this->buildAnyProjectWhereClause($conn);
$where[] = $this->buildXProjectWhereClause($conn);
$where[] = $this->buildFullTextWhereClause($conn);
$where = $this->formatWhereClause($where);
$join = array();
$join[] = $this->buildProjectJoinClause($conn);
$join[] = $this->buildAnyProjectJoinClause($conn);
$join[] = $this->buildXProjectJoinClause($conn);
$join[] = $this->buildSubscriberJoinClause($conn);
$join = array_filter($join);
if ($join) {
$join = implode(' ', $join);
} else {
$join = '';
}
$having = '';
$count = '';
$group = '';
if (count($this->projectPHIDs) > 1 || count($this->anyProjectPHIDs) > 1) {
// If we're joining multiple rows, we need to group the results by the
// task IDs.
$group = 'GROUP BY task.id';
} else {
$group = '';
}
if (count($this->projectPHIDs) > 1) {
// 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.
$count = ', COUNT(project.projectPHID) 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);
if ($this->groupBy == self::GROUP_PROJECT) {
$limit = PHP_INT_MAX;
$offset = 0;
}
$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;
}
$tasks = $task_dao->loadAllFromArray($data);
if ($this->groupBy == self::GROUP_PROJECT) {
$tasks = $this->applyGroupByProject($tasks);
}
return $tasks;
}
private function buildTaskIDsWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->taskIDs) {
return null;
}
return qsprintf(
$conn,
'id in (%Ld)',
$this->taskIDs);
}
private function buildTaskPHIDsWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->taskPHIDs) {
return null;
}
return qsprintf(
$conn,
'phid in (%Ls)',
$this->taskPHIDs);
}
private function buildStatusWhereClause(AphrontDatabaseConnection $conn) {
static $map = array(
self::STATUS_RESOLVED => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
self::STATUS_WONTFIX => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX,
self::STATUS_INVALID => ManiphestTaskStatus::STATUS_CLOSED_INVALID,
self::STATUS_SPITE => ManiphestTaskStatus::STATUS_CLOSED_SPITE,
self::STATUS_DUPLICATE => ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE,
);
switch ($this->status) {
case self::STATUS_ANY:
return null;
case self::STATUS_OPEN:
return 'status = 0';
case self::STATUS_CLOSED:
return 'status > 0';
default:
$constant = idx($map, $this->status);
if (!$constant) {
throw new Exception("Unknown status query '{$this->status}'!");
}
return qsprintf(
$conn,
'status = %d',
$constant);
}
}
private function buildPriorityWhereClause(AphrontDatabaseConnection $conn) {
if ($this->priority !== null) {
return qsprintf(
$conn,
'priority = %d',
$this->priority);
} elseif ($this->minPriority !== null && $this->maxPriority !== null) {
return qsprintf(
$conn,
'priority >= %d AND priority <= %d',
$this->minPriority,
$this->maxPriority);
}
return null;
}
private function buildAuthorWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->authorPHIDs) {
return null;
}
return qsprintf(
$conn,
'authorPHID in (%Ls)',
$this->authorPHIDs);
}
private function buildOwnerWhereClause(AphrontDatabaseConnection $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 buildFullTextWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->fullTextSearch) {
return null;
}
// In doing a fulltext search, we first find all the PHIDs that match the
// fulltext search, and then use that to limit the rest of the search
$fulltext_query = new PhabricatorSearchQuery();
$fulltext_query->setQuery($this->fullTextSearch);
$fulltext_query->setParameter('limit', PHP_INT_MAX);
$engine = PhabricatorSearchEngineSelector::newSelector()->newEngine();
$fulltext_results = $engine->executeSearch($fulltext_query);
if (empty($fulltext_results)) {
$fulltext_results = array(null);
}
return qsprintf(
$conn,
'phid IN (%Ls)',
$fulltext_results);
}
private function buildSubscriberWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->subscriberPHIDs) {
return null;
}
return qsprintf(
$conn,
'subscriber.subscriberPHID IN (%Ls)',
$this->subscriberPHIDs);
}
private function buildProjectWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->projectPHIDs && !$this->includeNoProject) {
return null;
}
$parts = array();
if ($this->projectPHIDs) {
$parts[] = qsprintf(
$conn,
'project.projectPHID in (%Ls)',
$this->projectPHIDs);
}
if ($this->includeNoProject) {
$parts[] = qsprintf(
$conn,
'project.projectPHID IS NULL');
}
return '('.implode(') OR (', $parts).')';
}
private function buildProjectJoinClause(AphrontDatabaseConnection $conn) {
if (!$this->projectPHIDs && !$this->includeNoProject) {
return null;
}
$project_dao = new ManiphestTaskProject();
return qsprintf(
$conn,
'%Q JOIN %T project ON project.taskPHID = task.phid',
($this->includeNoProject ? 'LEFT' : ''),
$project_dao->getTableName());
}
private function buildAnyProjectWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->anyProjectPHIDs) {
return null;
}
return qsprintf(
$conn,
'anyproject.projectPHID IN (%Ls)',
$this->anyProjectPHIDs);
}
private function buildAnyProjectJoinClause(AphrontDatabaseConnection $conn) {
if (!$this->anyProjectPHIDs) {
return null;
}
$project_dao = new ManiphestTaskProject();
return qsprintf(
$conn,
'JOIN %T anyproject ON anyproject.taskPHID = task.phid',
$project_dao->getTableName());
}
private function buildXProjectWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->xprojectPHIDs) {
return null;
}
return qsprintf(
$conn,
'xproject.projectPHID IS NULL');
}
private function buildXProjectJoinClause(AphrontDatabaseConnection $conn) {
if (!$this->xprojectPHIDs) {
return null;
}
$project_dao = new ManiphestTaskProject();
return qsprintf(
$conn,
'LEFT JOIN %T xproject ON xproject.taskPHID = task.phid
AND xproject.projectPHID IN (%Ls)',
$project_dao->getTableName(),
$this->xprojectPHIDs);
}
private function buildSubscriberJoinClause(AphrontDatabaseConnection $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(AphrontDatabaseConnection $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;
case self::GROUP_PROJECT:
// NOTE: We have to load the entire result set and apply this grouping
// in the PHP process for now.
break;
default:
throw new Exception("Unknown group query '{$this->groupBy}'!");
}
switch ($this->orderBy) {
case self::ORDER_PRIORITY:
$order[] = 'priority';
$order[] = 'subpriority';
$order[] = 'dateModified';
break;
case self::ORDER_CREATED:
$order[] = 'id';
break;
case self::ORDER_MODIFIED:
$order[] = 'dateModified';
break;
case self::ORDER_TITLE:
$order[] = 'title';
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 'subpriority':
case 'ownerOrdering':
case 'title':
$order[$k] = "task.{$column} ASC";
break;
default:
$order[$k] = "task.{$column} DESC";
break;
}
}
return 'ORDER BY '.implode(', ', $order);
}
/**
* To get paging to work for "group by project", we need to do a bunch of
* server-side magic since there's currently no way to sort by project name on
* the database.
*
* As a consequence of this, moreover, because the list we return from here
* may include a single task multiple times (once for each project it's in),
* sorting gets screwed up in the controller unless we tell it which project
* to put the task in each time it appears. Hence the magic field
* groupByProjectResults.
*
* TODO: Move this all to the database.
*/
private function applyGroupByProject(array $tasks) {
assert_instances_of($tasks, 'ManiphestTask');
$project_phids = array();
foreach ($tasks as $task) {
foreach ($task->getProjectPHIDs() as $phid) {
$project_phids[$phid] = true;
}
}
$handles = id(new PhabricatorObjectHandleData(array_keys($project_phids)))
->loadHandles();
$max = 1;
foreach ($handles as $handle) {
$max = max($max, strlen($handle->getName()));
}
$items = array();
$ii = 0;
foreach ($tasks as $key => $task) {
$phids = $task->getProjectPHIDs();
if ($this->projectPHIDs) {
$phids = array_diff($phids, $this->projectPHIDs);
}
if ($phids) {
foreach ($phids as $phid) {
$items[] = array(
'key' => $key,
'proj' => $phid,
'seq' => sprintf(
'%'.$max.'s%09d',
$handles[$phid]->getName(),
$ii),
);
}
} else {
// Sort "no project" tasks first.
$items[] = array(
'key' => $key,
'proj' => null,
'seq' => sprintf(
'%'.$max.'s%09d',
'',
$ii),
);
}
++$ii;
}
$items = isort($items, 'seq');
$items = array_slice(
$items,
nonempty($this->offset),
nonempty($this->limit, self::DEFAULT_PAGE_SIZE));
$result = array();
$projects = array();
foreach ($items as $item) {
$result[] = $projects[$item['proj']][] = $tasks[$item['key']];
}
$this->groupByProjectResults = $projects;
return $result;
}
}
diff --git a/src/applications/maniphest/application/PhabricatorApplicationManiphest.php b/src/applications/maniphest/application/PhabricatorApplicationManiphest.php
index f4d40a709d..32dc7a7bae 100644
--- a/src/applications/maniphest/application/PhabricatorApplicationManiphest.php
+++ b/src/applications/maniphest/application/PhabricatorApplicationManiphest.php
@@ -1,121 +1,105 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationManiphest extends PhabricatorApplication {
public function getShortDescription() {
return 'Tasks and Bugs';
}
public function getBaseURI() {
return '/maniphest/';
}
public function isEnabled() {
return PhabricatorEnv::getEnvConfig('maniphest.enabled');
}
public function getAutospriteName() {
return 'maniphest';
}
public function getApplicationGroup() {
return self::GROUP_CORE;
}
public function getApplicationOrder() {
return 0.110;
}
public function getFactObjectsForAnalysis() {
return array(
new ManiphestTask(),
);
}
public function getRoutes() {
return array(
'/T(?P<id>[1-9]\d*)' => 'ManiphestTaskDetailController',
'/maniphest/' => array(
'' => 'ManiphestTaskListController',
'view/(?P<view>\w+)/' => 'ManiphestTaskListController',
'report/(?:(?P<view>\w+)/)?' => 'ManiphestReportController',
'batch/' => 'ManiphestBatchEditController',
'task/' => array(
'create/' => 'ManiphestTaskEditController',
'edit/(?P<id>[1-9]\d*)/' => 'ManiphestTaskEditController',
'descriptionchange/(?:(?P<id>[1-9]\d*)/)?' =>
'ManiphestTaskDescriptionChangeController',
'descriptionpreview/' =>
'ManiphestTaskDescriptionPreviewController',
),
'transaction/' => array(
'save/' => 'ManiphestTransactionSaveController',
'preview/(?P<id>[1-9]\d*)/'
=> 'ManiphestTransactionPreviewController',
),
'export/(?P<key>[^/]+)/' => 'ManiphestExportController',
'subpriority/' => 'ManiphestSubpriorityController',
'custom/' => array(
'' => 'ManiphestSavedQueryListController',
'edit/(?:(?P<id>[1-9]\d*)/)?' => 'ManiphestSavedQueryEditController',
'delete/(?P<id>[1-9]\d*)/' => 'ManiphestSavedQueryDeleteController',
),
),
);
}
public function loadStatus(PhabricatorUser $user) {
$status = array();
$query = id(new ManiphestTaskQuery())
->withStatus(ManiphestTaskQuery::STATUS_OPEN)
->withPriority(ManiphestTaskPriority::PRIORITY_UNBREAK_NOW)
->setLimit(1)
->setCalculateRows(true);
$query->execute();
$count = $query->getRowCount();
$type = $count
? PhabricatorApplicationStatusView::TYPE_NEEDS_ATTENTION
: PhabricatorApplicationStatusView::TYPE_EMPTY;
$status[] = id(new PhabricatorApplicationStatusView())
->setType($type)
->setText(pht('%d Unbreak Now Task(s)!', $count))
->setCount($count);
$query = id(new ManiphestTaskQuery())
->withStatus(ManiphestTaskQuery::STATUS_OPEN)
->withOwners(array($user->getPHID()))
->setLimit(1)
->setCalculateRows(true);
$query->execute();
$count = $query->getRowCount();
$type = $count
? PhabricatorApplicationStatusView::TYPE_INFO
: PhabricatorApplicationStatusView::TYPE_EMPTY;
$status[] = id(new PhabricatorApplicationStatusView())
->setType($type)
->setText(pht('%d Assigned Task(s)', $count));
return $status;
}
}
diff --git a/src/applications/maniphest/auxiliaryfield/ManiphestAuxiliaryFieldDefaultSpecification.php b/src/applications/maniphest/auxiliaryfield/ManiphestAuxiliaryFieldDefaultSpecification.php
index faa341effd..b393a6e1de 100644
--- a/src/applications/maniphest/auxiliaryfield/ManiphestAuxiliaryFieldDefaultSpecification.php
+++ b/src/applications/maniphest/auxiliaryfield/ManiphestAuxiliaryFieldDefaultSpecification.php
@@ -1,238 +1,222 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
class ManiphestAuxiliaryFieldDefaultSpecification
extends ManiphestAuxiliaryFieldSpecification {
private $required;
private $fieldType;
private $selectOptions;
private $checkboxLabel;
private $checkboxValue;
private $error;
private $shouldCopyWhenCreatingSimilarTask;
const TYPE_SELECT = 'select';
const TYPE_STRING = 'string';
const TYPE_INT = 'int';
const TYPE_BOOL = 'bool';
public function getFieldType() {
return $this->fieldType;
}
public function setFieldType($val) {
$this->fieldType = $val;
return $this;
}
public function getError() {
return $this->error;
}
public function setError($val) {
$this->error = $val;
return $this;
}
public function getSelectOptions() {
return $this->selectOptions;
}
public function setSelectOptions($array) {
$this->selectOptions = $array;
return $this;
}
public function setRequired($bool) {
$this->required = $bool;
return $this;
}
public function isRequired() {
return $this->required;
}
public function setCheckboxLabel($checkbox_label) {
$this->checkboxLabel = $checkbox_label;
return $this;
}
public function getCheckboxLabel() {
return $this->checkboxLabel;
}
public function setCheckboxValue($checkbox_value) {
$this->checkboxValue = $checkbox_value;
return $this;
}
public function getCheckboxValue() {
return $this->checkboxValue;
}
public function renderControl() {
$control = null;
$type = $this->getFieldType();
switch ($type) {
case self::TYPE_INT:
$control = new AphrontFormTextControl();
break;
case self::TYPE_STRING:
$control = new AphrontFormTextControl();
break;
case self::TYPE_SELECT:
$control = new AphrontFormSelectControl();
$control->setOptions($this->getSelectOptions());
break;
case self::TYPE_BOOL:
$control = new AphrontFormCheckboxControl();
break;
default:
$label = $this->getLabel();
throw new ManiphestAuxiliaryFieldTypeException(
"Field type '{$type}' is not a valid type (for field '{$label}').");
break;
}
if ($type == self::TYPE_BOOL) {
$control->addCheckbox(
'auxiliary['.$this->getAuxiliaryKey().']',
1,
$this->getCheckboxLabel(),
(bool)$this->getValue());
} else {
$control->setValue($this->getValue());
$control->setName('auxiliary['.$this->getAuxiliaryKey().']');
}
$control->setLabel($this->getLabel());
$control->setCaption($this->getCaption());
$control->setError($this->getError());
return $control;
}
public function setValueFromRequest($request) {
$aux_post_values = $request->getArr('auxiliary');
return $this->setValue(idx($aux_post_values, $this->getAuxiliaryKey(), ''));
}
public function getValueForStorage() {
return $this->getValue();
}
public function setValueFromStorage($value) {
return $this->setValue($value);
}
public function validate() {
switch ($this->getFieldType()) {
case self::TYPE_INT:
if (!is_numeric($this->getValue())) {
throw new ManiphestAuxiliaryFieldValidationException(
$this->getLabel().' must be an integer value.'
);
}
break;
case self::TYPE_BOOL:
return true;
case self::TYPE_STRING:
return true;
case self::TYPE_SELECT:
return true;
}
}
public function renderForDetailView() {
switch ($this->getFieldType()) {
case self::TYPE_BOOL:
if ($this->getValue()) {
return phutil_escape_html($this->getCheckboxValue());
} else {
return null;
}
case self::TYPE_SELECT:
$display = idx($this->getSelectOptions(), $this->getValue());
return phutil_escape_html($display);
}
return parent::renderForDetailView();
}
public function renderTransactionDescription(
ManiphestTransaction $transaction,
$target) {
$label = $this->getLabel();
$old = $transaction->getOldValue();
$new = $transaction->getNewValue();
switch ($this->getFieldType()) {
case self::TYPE_BOOL:
if ($new) {
$desc = "set field '{$label}' true";
} else {
$desc = "set field '{$label}' false";
}
break;
case self::TYPE_SELECT:
$old_display = idx($this->getSelectOptions(), $old);
$new_display = idx($this->getSelectOptions(), $new);
if ($old === null) {
$desc = "set field '{$label}' to '{$new_display}'";
} else {
$desc = "changed field '{$label}' ".
"from '{$old_display}' to '{$new_display}'";
}
break;
default:
if (!strlen($old)) {
if (!strlen($new)) {
return null;
}
$desc = "set field '{$label}' to '{$new}'";
} else {
$desc = "updated '{$label}' ".
"from '{$old}' to '{$new}'";
}
break;
}
if ($target == self::RENDER_TARGET_HTML) {
$desc = phutil_escape_html($desc);
}
return $desc;
}
public function setShouldCopyWhenCreatingSimilarTask($copy) {
$this->shouldCopyWhenCreatingSimilarTask = $copy;
return $this;
}
public function shouldCopyWhenCreatingSimilarTask() {
return $this->shouldCopyWhenCreatingSimilarTask;
}
}
diff --git a/src/applications/maniphest/auxiliaryfield/ManiphestAuxiliaryFieldSpecification.php b/src/applications/maniphest/auxiliaryfield/ManiphestAuxiliaryFieldSpecification.php
index 798f7ac5c1..fba027e838 100644
--- a/src/applications/maniphest/auxiliaryfield/ManiphestAuxiliaryFieldSpecification.php
+++ b/src/applications/maniphest/auxiliaryfield/ManiphestAuxiliaryFieldSpecification.php
@@ -1,157 +1,141 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
abstract class ManiphestAuxiliaryFieldSpecification {
const RENDER_TARGET_HTML = 'html';
const RENDER_TARGET_TEXT = 'text';
private $label;
private $auxiliaryKey;
private $caption;
private $value;
public function setLabel($val) {
$this->label = $val;
return $this;
}
public function getLabel() {
return $this->label;
}
public function setAuxiliaryKey($val) {
$this->auxiliaryKey = $val;
return $this;
}
public function getAuxiliaryKey() {
return $this->auxiliaryKey;
}
public function setCaption($val) {
$this->caption = $val;
return $this;
}
public function getCaption() {
return $this->caption;
}
public function setValue($val) {
$this->value = $val;
return $this;
}
public function getValue() {
return $this->value;
}
public function validate() {
return true;
}
public function isRequired() {
return false;
}
public function setType($val) {
$this->type = $val;
return $this;
}
public function getType() {
return $this->type;
}
public function renderControl() {
return null;
}
public function renderForDetailView() {
return phutil_escape_html($this->getValue());
}
/**
* When the user creates a task, the UI prompts them to "Create another
* similar task". This copies some fields (e.g., Owner and CCs) but not other
* fields (e.g., description). If this custom field should also be copied,
* return true from this method.
*
* @return bool True to copy the default value from the template task when
* creating a new similar task.
*/
public function shouldCopyWhenCreatingSimilarTask() {
return false;
}
/**
* Render a verb to appear in email titles when a transaction involving this
* field occurs. Specifically, Maniphest emails are formatted like this:
*
* [Maniphest] [Verb Here] TNNN: Task title here
* ^^^^^^^^^
*
* You should optionally return a title-case verb or short phrase like
* "Created", "Retitled", "Closed", "Resolved", "Commented On",
* "Lowered Priority", etc., which describes the transaction.
*
* @param ManiphestTransaction The transaction which needs description.
* @return string|null A short description of the transaction.
*/
public function renderTransactionEmailVerb(
ManiphestTransaction $transaction) {
return null;
}
/**
* Render a short description of the transaction, to appear above comments
* in the Maniphest transaction log. The string will be rendered after the
* acting user's name. Examples are:
*
* added a comment
* added alincoln to CC
* claimed this task
* created this task
* closed this task out of spite
*
* You should return a similar string, describing the transaction.
*
* Note the ##$target## parameter -- Maniphest needs to render transaction
* descriptions for different targets, like web and email. This method will
* be called with a ##ManiphestAuxiliaryFieldSpecification::RENDER_TARGET_*##
* constant describing the intended target.
*
* @param ManiphestTransaction The transaction which needs description.
* @param const Constant describing the rendering target (e.g., html or text).
* @return string|null Description of the transaction.
*/
public function renderTransactionDescription(
ManiphestTransaction $transaction,
$target) {
return 'updated a custom field';
}
}
diff --git a/src/applications/maniphest/auxiliaryfield/ManiphestAuxiliaryFieldTypeException.php b/src/applications/maniphest/auxiliaryfield/ManiphestAuxiliaryFieldTypeException.php
index b148a65225..913ddc494e 100644
--- a/src/applications/maniphest/auxiliaryfield/ManiphestAuxiliaryFieldTypeException.php
+++ b/src/applications/maniphest/auxiliaryfield/ManiphestAuxiliaryFieldTypeException.php
@@ -1,24 +1,8 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestAuxiliaryFieldTypeException extends Exception {
}
diff --git a/src/applications/maniphest/auxiliaryfield/ManiphestAuxiliaryFieldValidationException.php b/src/applications/maniphest/auxiliaryfield/ManiphestAuxiliaryFieldValidationException.php
index 13a20504d4..501c6fa957 100644
--- a/src/applications/maniphest/auxiliaryfield/ManiphestAuxiliaryFieldValidationException.php
+++ b/src/applications/maniphest/auxiliaryfield/ManiphestAuxiliaryFieldValidationException.php
@@ -1,24 +1,8 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestAuxiliaryFieldValidationException extends Exception {
}
diff --git a/src/applications/maniphest/constants/ManiphestAction.php b/src/applications/maniphest/constants/ManiphestAction.php
index a449e489a0..ed0487161d 100644
--- a/src/applications/maniphest/constants/ManiphestAction.php
+++ b/src/applications/maniphest/constants/ManiphestAction.php
@@ -1,102 +1,86 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestAction extends ManiphestConstants {
/* These actions must be determined when the story
is generated and thus are new */
const ACTION_CREATE = 'create';
const ACTION_REOPEN = 'reopen';
const ACTION_CLOSE = 'close';
const ACTION_UPDATE = 'update';
const ACTION_ASSIGN = 'assign';
/* these actions are determined sufficiently by the transaction
type and thus we use them here*/
const ACTION_COMMENT = ManiphestTransactionType::TYPE_NONE;
const ACTION_CC = ManiphestTransactionType::TYPE_CCS;
const ACTION_PRIORITY = ManiphestTransactionType::TYPE_PRIORITY;
const ACTION_PROJECT = ManiphestTransactionType::TYPE_PROJECTS;
const ACTION_TITLE = ManiphestTransactionType::TYPE_TITLE;
const ACTION_DESCRIPTION = ManiphestTransactionType::TYPE_DESCRIPTION;
const ACTION_REASSIGN = ManiphestTransactionType::TYPE_OWNER;
const ACTION_ATTACH = ManiphestTransactionType::TYPE_ATTACH;
const ACTION_EDGE = ManiphestTransactionType::TYPE_EDGE;
const ACTION_AUXILIARY = ManiphestTransactionType::TYPE_AUXILIARY;
public static function getActionPastTenseVerb($action) {
static $map = array(
self::ACTION_CREATE => 'created',
self::ACTION_CLOSE => 'closed',
self::ACTION_UPDATE => 'updated',
self::ACTION_ASSIGN => 'assigned',
self::ACTION_REASSIGN => 'reassigned',
self::ACTION_COMMENT => 'commented on',
self::ACTION_CC => 'updated cc\'s of',
self::ACTION_PRIORITY => 'changed the priority of',
self::ACTION_PROJECT => 'modified projects of',
self::ACTION_TITLE => 'updated title of',
self::ACTION_DESCRIPTION => 'updated description of',
self::ACTION_ATTACH => 'attached something to',
self::ACTION_EDGE => 'changed related objects of',
self::ACTION_REOPEN => 'reopened',
self::ACTION_AUXILIARY => 'updated an auxiliary field of',
);
return idx($map, $action, "brazenly {$action}'d");
}
/**
* If a group of transactions contain several actions, select the "strongest"
* action. For instance, a close is stronger than an update, because we want
* to render "User U closed task T" instead of "User U updated task T" when
* a user closes a task.
*/
public static function selectStrongestAction(array $actions) {
static $strengths = array(
self::ACTION_AUXILIARY => -1,
self::ACTION_UPDATE => 0,
self::ACTION_CC => 1,
self::ACTION_PROJECT => 2,
self::ACTION_DESCRIPTION => 3,
self::ACTION_TITLE => 4,
self::ACTION_ATTACH => 5,
self::ACTION_EDGE => 5,
self::ACTION_COMMENT => 6,
self::ACTION_PRIORITY => 7,
self::ACTION_REASSIGN => 8,
self::ACTION_ASSIGN => 9,
self::ACTION_REOPEN => 10,
self::ACTION_CREATE => 11,
self::ACTION_CLOSE => 12,
);
$strongest = null;
$strength = -1;
foreach ($actions as $action) {
if ($strengths[$action] > $strength) {
$strength = $strengths[$action];
$strongest = $action;
}
}
return $strongest;
}
}
diff --git a/src/applications/maniphest/constants/ManiphestConstants.php b/src/applications/maniphest/constants/ManiphestConstants.php
index e91bf08237..3a1f2ab4c4 100644
--- a/src/applications/maniphest/constants/ManiphestConstants.php
+++ b/src/applications/maniphest/constants/ManiphestConstants.php
@@ -1,24 +1,8 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
abstract class ManiphestConstants {
}
diff --git a/src/applications/maniphest/constants/ManiphestTaskOwner.php b/src/applications/maniphest/constants/ManiphestTaskOwner.php
index db8ee391a8..642c8b85a5 100644
--- a/src/applications/maniphest/constants/ManiphestTaskOwner.php
+++ b/src/applications/maniphest/constants/ManiphestTaskOwner.php
@@ -1,27 +1,11 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestTaskOwner extends ManiphestConstants {
const OWNER_UP_FOR_GRABS = 'PHID-!!!!-UP-FOR-GRABS';
const PROJECT_NO_PROJECT = 'PHID-!!!!-NO-PROJECT';
}
diff --git a/src/applications/maniphest/constants/ManiphestTaskPriority.php b/src/applications/maniphest/constants/ManiphestTaskPriority.php
index 506e14fe9f..1de52360c0 100644
--- a/src/applications/maniphest/constants/ManiphestTaskPriority.php
+++ b/src/applications/maniphest/constants/ManiphestTaskPriority.php
@@ -1,119 +1,103 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestTaskPriority extends ManiphestConstants {
const PRIORITY_UNBREAK_NOW = 100;
const PRIORITY_TRIAGE = 90;
const PRIORITY_HIGH = 80;
const PRIORITY_NORMAL = 50;
const PRIORITY_LOW = 25;
const PRIORITY_WISH = 0;
/**
* Get the priorities and their full descriptions.
*
* @return map Priorities to descriptions.
*/
public static function getTaskPriorityMap() {
return array(
self::PRIORITY_UNBREAK_NOW => 'Unbreak Now!',
self::PRIORITY_TRIAGE => 'Needs Triage',
self::PRIORITY_HIGH => 'High',
self::PRIORITY_NORMAL => 'Normal',
self::PRIORITY_LOW => 'Low',
self::PRIORITY_WISH => 'Wishlist',
);
}
/**
* Get the priorities and their related short (one-word) descriptions.
*
* @return map Priorities to brief descriptions.
*/
public static function getTaskBriefPriorityMap() {
return array(
self::PRIORITY_UNBREAK_NOW => 'Unbreak!',
self::PRIORITY_TRIAGE => 'Triage',
self::PRIORITY_HIGH => 'High',
self::PRIORITY_NORMAL => 'Normal',
self::PRIORITY_LOW => 'Low',
self::PRIORITY_WISH => 'Wish',
);
}
/**
* Get the priorities and some bits for bitwise fun.
*
* @return map Priorities to bits.
*/
public static function getLoadMap() {
return array(
self::PRIORITY_UNBREAK_NOW => 16,
self::PRIORITY_TRIAGE => 8,
self::PRIORITY_HIGH => 4,
self::PRIORITY_NORMAL => 2,
self::PRIORITY_LOW => 1,
self::PRIORITY_WISH => 0,
);
}
/**
* Get the lowest defined priority.
*
* @return int The value of the lowest priority constant.
*/
public static function getLowestPriority() {
return self::PRIORITY_WISH;
}
/**
* Get the highest defined priority.
*
* @return int The value of the highest priority constant.
*/
public static function getHighestPriority() {
return self::PRIORITY_UNBREAK_NOW;
}
/**
* Return the default priority for this instance of Phabricator.
*
* @return int The value of the default priority constant.
*/
public static function getDefaultPriority() {
return PhabricatorEnv::getEnvConfig(
'maniphest.default-priority',
self::PRIORITY_TRIAGE
);
}
/**
* Retrieve the full name of the priority level provided.
*
* @param int A priority level.
* @return string The priority name if the level is a valid one,
* or `???` if it is not.
*/
public static function getTaskPriorityName($priority) {
return idx(self::getTaskPriorityMap(), $priority, '???');
}
}
diff --git a/src/applications/maniphest/constants/ManiphestTaskStatus.php b/src/applications/maniphest/constants/ManiphestTaskStatus.php
index 813db81e94..681f61287e 100644
--- a/src/applications/maniphest/constants/ManiphestTaskStatus.php
+++ b/src/applications/maniphest/constants/ManiphestTaskStatus.php
@@ -1,54 +1,38 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestTaskStatus extends ManiphestConstants {
const STATUS_OPEN = 0;
const STATUS_CLOSED_RESOLVED = 1;
const STATUS_CLOSED_WONTFIX = 2;
const STATUS_CLOSED_INVALID = 3;
const STATUS_CLOSED_DUPLICATE = 4;
const STATUS_CLOSED_SPITE = 5;
public static function getTaskStatusMap() {
return array(
self::STATUS_OPEN => 'Open',
self::STATUS_CLOSED_RESOLVED => 'Resolved',
self::STATUS_CLOSED_WONTFIX => 'Wontfix',
self::STATUS_CLOSED_INVALID => 'Invalid',
self::STATUS_CLOSED_DUPLICATE => 'Duplicate',
self::STATUS_CLOSED_SPITE => 'Spite',
);
}
public static function getTaskStatusFullName($status) {
$map = array(
self::STATUS_OPEN => 'Open',
self::STATUS_CLOSED_RESOLVED => 'Closed, Resolved',
self::STATUS_CLOSED_WONTFIX => 'Closed, Wontfix',
self::STATUS_CLOSED_INVALID => 'Closed, Invalid',
self::STATUS_CLOSED_DUPLICATE => 'Closed, Duplicate',
self::STATUS_CLOSED_SPITE => 'Closed out of Spite',
);
return idx($map, $status, '???');
}
}
diff --git a/src/applications/maniphest/constants/ManiphestTransactionType.php b/src/applications/maniphest/constants/ManiphestTransactionType.php
index b8c623854b..65b4305950 100644
--- a/src/applications/maniphest/constants/ManiphestTransactionType.php
+++ b/src/applications/maniphest/constants/ManiphestTransactionType.php
@@ -1,50 +1,34 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestTransactionType extends ManiphestConstants {
const TYPE_NONE = 'comment';
const TYPE_STATUS = 'status';
const TYPE_OWNER = 'reassign';
const TYPE_CCS = 'ccs';
const TYPE_PROJECTS = 'projects';
const TYPE_PRIORITY = 'priority';
const TYPE_ATTACH = 'attach';
const TYPE_EDGE = 'edge';
const TYPE_TITLE = 'title';
const TYPE_DESCRIPTION = 'description';
const TYPE_AUXILIARY = 'aux';
public static function getTransactionTypeMap() {
return array(
self::TYPE_NONE => 'Comment',
self::TYPE_STATUS => 'Close Task',
self::TYPE_OWNER => 'Reassign / Claim',
self::TYPE_CCS => 'Add CCs',
self::TYPE_PRIORITY => 'Change Priority',
self::TYPE_ATTACH => 'Upload File',
self::TYPE_PROJECTS => 'Associate Projects',
);
}
}
diff --git a/src/applications/maniphest/controller/ManiphestBatchEditController.php b/src/applications/maniphest/controller/ManiphestBatchEditController.php
index bf538e1185..5fb1938f84 100644
--- a/src/applications/maniphest/controller/ManiphestBatchEditController.php
+++ b/src/applications/maniphest/controller/ManiphestBatchEditController.php
@@ -1,311 +1,295 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestBatchEditController extends ManiphestController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$task_ids = $request->getArr('batch');
$tasks = id(new ManiphestTask())->loadAllWhere(
'id IN (%Ld)',
$task_ids);
$actions = $request->getStr('actions');
if ($actions) {
$actions = json_decode($actions, true);
}
if ($request->isFormPost() && is_array($actions)) {
foreach ($tasks as $task) {
$xactions = $this->buildTransactions($actions, $task);
if ($xactions) {
$editor = new ManiphestTransactionEditor();
$editor->setActor($user);
$editor->applyTransactions($task, $xactions);
}
}
$task_ids = implode(',', mpull($tasks, 'getID'));
return id(new AphrontRedirectResponse())
->setURI('/maniphest/view/custom/?s=oc&tasks='.$task_ids);
}
$panel = new AphrontPanelView();
$panel->setHeader('Maniphest Batch Editor');
$handle_phids = mpull($tasks, 'getOwnerPHID');
$handles = $this->loadViewerHandles($handle_phids);
$list = new ManiphestTaskListView();
$list->setTasks($tasks);
$list->setUser($user);
$list->setHandles($handles);
$template = new AphrontTokenizerTemplateView();
$template = $template->render();
require_celerity_resource('maniphest-batch-editor');
Javelin::initBehavior(
'maniphest-batch-editor',
array(
'root' => 'maniphest-batch-edit-form',
'tokenizerTemplate' => $template,
'sources' => array(
'project' => array(
'src' => '/typeahead/common/projects/',
'placeholder' => 'Type a project name...',
),
'owner' => array(
'src' => '/typeahead/common/searchowner/',
'placeholder' => 'Type a user name...',
'limit' => 1,
),
),
'input' => 'batch-form-actions',
'priorityMap' => ManiphestTaskPriority::getTaskPriorityMap(),
'statusMap' => ManiphestTaskStatus::getTaskStatusMap(),
));
$form = new AphrontFormView();
$form->setUser($user);
$form->setID('maniphest-batch-edit-form');
foreach ($tasks as $task) {
$form->appendChild(
phutil_render_tag(
'input',
array(
'type' => 'hidden',
'name' => 'batch[]',
'value' => $task->getID(),
),
null));
}
$form->appendChild(
phutil_render_tag(
'input',
array(
'type' => 'hidden',
'name' => 'actions',
'id' => 'batch-form-actions',
),
null));
$form->appendChild('<p>These tasks will be edited:</p>');
$form->appendChild($list);
$form->appendChild(
id(new AphrontFormInsetView())
->setTitle('Actions')
->setRightButton(javelin_render_tag(
'a',
array(
'href' => '#',
'class' => 'button green',
'sigil' => 'add-action',
'mustcapture' => true,
),
'Add Another Action'))
->setContent(javelin_render_tag(
'table',
array(
'sigil' => 'maniphest-batch-actions',
'class' => 'maniphest-batch-actions-table',
),
'')))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Update Tasks')
->addCancelButton('/maniphest/', 'Done'));
$panel->appendChild($form);
return $this->buildStandardPageResponse(
$panel,
array(
'title' => 'Batch Editor',
));
}
private function buildTransactions($actions, ManiphestTask $task) {
$value_map = array();
$type_map = array(
'add_comment' => ManiphestTransactionType::TYPE_NONE,
'assign' => ManiphestTransactionType::TYPE_OWNER,
'status' => ManiphestTransactionType::TYPE_STATUS,
'priority' => ManiphestTransactionType::TYPE_PRIORITY,
'add_project' => ManiphestTransactionType::TYPE_PROJECTS,
'remove_project' => ManiphestTransactionType::TYPE_PROJECTS,
);
$edge_edit_types = array(
'add_project' => true,
'remove_project' => true,
);
$xactions = array();
foreach ($actions as $action) {
if (empty($type_map[$action['action']])) {
throw new Exception("Unknown batch edit action '{$action}'!");
}
$type = $type_map[$action['action']];
// Figure out the current value, possibly after modifications by other
// batch actions of the same type. For example, if the user chooses to
// "Add Comment" twice, we should add both comments. More notably, if the
// user chooses "Remove Project..." and also "Add Project...", we should
// avoid restoring the removed project in the second transaction.
if (array_key_exists($type, $value_map)) {
$current = $value_map[$type];
} else {
switch ($type) {
case ManiphestTransactionType::TYPE_NONE:
$current = null;
break;
case ManiphestTransactionType::TYPE_OWNER:
$current = $task->getOwnerPHID();
break;
case ManiphestTransactionType::TYPE_STATUS:
$current = $task->getStatus();
break;
case ManiphestTransactionType::TYPE_PRIORITY:
$current = $task->getPriority();
break;
case ManiphestTransactionType::TYPE_PROJECTS:
$current = $task->getProjectPHIDs();
break;
}
}
// Check if the value is meaningful / provided, and normalize it if
// necessary. This discards, e.g., empty comments and empty owner
// changes.
$value = $action['value'];
switch ($type) {
case ManiphestTransactionType::TYPE_NONE:
if (!strlen($value)) {
continue 2;
}
break;
case ManiphestTransactionType::TYPE_OWNER:
if (empty($value)) {
continue 2;
}
$value = head($value);
if ($value === ManiphestTaskOwner::OWNER_UP_FOR_GRABS) {
$value = null;
}
break;
case ManiphestTransactionType::TYPE_PROJECTS:
if (empty($value)) {
continue 2;
}
break;
}
// If the edit doesn't change anything, go to the next action. This
// check is only valid for changes like "owner", "status", etc, not
// for edge edits, because we should still apply an edit like
// "Remove Projects: A, B" to a task with projects "A, B".
if (empty($edge_edit_types[$action['action']])) {
if ($value == $current) {
continue;
}
}
// Apply the value change; for most edits this is just replacement, but
// some need to merge the current and edited values (add/remove project).
switch ($type) {
case ManiphestTransactionType::TYPE_NONE:
if (strlen($current)) {
$value = $current."\n\n".$value;
}
break;
case ManiphestTransactionType::TYPE_PROJECTS:
$is_remove = ($action['action'] == 'remove_project');
$current = array_fill_keys($current, true);
$value = array_fill_keys($value, true);
$new = $current;
$did_something = false;
if ($is_remove) {
foreach ($value as $phid => $ignored) {
if (isset($new[$phid])) {
unset($new[$phid]);
$did_something = true;
}
}
} else {
foreach ($value as $phid => $ignored) {
if (empty($new[$phid])) {
$new[$phid] = true;
$did_something = true;
}
}
}
if (!$did_something) {
continue 2;
}
$value = array_keys($new);
break;
}
$value_map[$type] = $value;
}
$template = new ManiphestTransaction();
$template->setAuthorPHID($this->getRequest()->getUser()->getPHID());
// TODO: Set content source to "batch edit".
foreach ($value_map as $type => $value) {
$xaction = clone $template;
$xaction->setTransactionType($type);
switch ($type) {
case ManiphestTransactionType::TYPE_NONE:
$xaction->setComments($value);
break;
default:
$xaction->setNewValue($value);
break;
}
$xactions[] = $xaction;
}
return $xactions;
}
}
diff --git a/src/applications/maniphest/controller/ManiphestController.php b/src/applications/maniphest/controller/ManiphestController.php
index 471b229fac..7b597c6b04 100644
--- a/src/applications/maniphest/controller/ManiphestController.php
+++ b/src/applications/maniphest/controller/ManiphestController.php
@@ -1,94 +1,78 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
abstract class ManiphestController extends PhabricatorController {
private $defaultQuery;
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('Maniphest');
$page->setBaseURI('/maniphest/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph("\xE2\x9A\x93");
$page->appendPageObjects(idx($data, 'pageObjects', array()));
$page->appendChild($view);
$page->setSearchDefaultScope(PhabricatorSearchScope::SCOPE_OPEN_TASKS);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
protected function buildBaseSideNav() {
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI('/maniphest/view/'));
$request = $this->getRequest();
$user = $request->getUser();
$custom = id(new ManiphestSavedQuery())->loadAllWhere(
'userPHID = %s ORDER BY isDefault DESC, name ASC',
$user->getPHID());
if ($custom) {
$nav->addLabel('Saved Queries');
foreach ($custom as $query) {
if ($query->getIsDefault()) {
$this->defaultQuery = $query;
}
$nav->addFilter(
'Q:'.$query->getQueryKey(),
$query->getName(),
'/maniphest/view/custom/?key='.$query->getQueryKey());
}
$nav->addFilter('saved', 'Edit...', '/maniphest/custom/');
$nav->addSpacer();
}
$nav->addLabel('User Tasks');
$nav->addFilter('action', 'Assigned');
$nav->addFilter('created', 'Created');
$nav->addFilter('subscribed', 'Subscribed');
$nav->addFilter('triage', 'Need Triage');
$nav->addSpacer();
$nav->addLabel('User Projects');
$nav->addFilter('projecttriage','Need Triage');
$nav->addFilter('projectall', 'All Tasks');
$nav->addSpacer();
$nav->addLabel('All Tasks');
$nav->addFilter('alltriage', 'Need Triage');
$nav->addFilter('all', 'All Tasks');
$nav->addSpacer();
$nav->addLabel('Custom');
$nav->addFilter('custom', 'Custom Query');
$nav->addSpacer();
$nav->addLabel('Reports');
$nav->addFilter('report', 'Reports', '/maniphest/report/');
return $nav;
}
protected function getDefaultQuery() {
return $this->defaultQuery;
}
}
diff --git a/src/applications/maniphest/controller/ManiphestExportController.php b/src/applications/maniphest/controller/ManiphestExportController.php
index 9de203df80..a28407df56 100644
--- a/src/applications/maniphest/controller/ManiphestExportController.php
+++ b/src/applications/maniphest/controller/ManiphestExportController.php
@@ -1,228 +1,212 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestExportController extends ManiphestController {
private $key;
public function willProcessRequest(array $data) {
$this->key = $data['key'];
return $this;
}
/**
* @phutil-external-symbol class PHPExcel
* @phutil-external-symbol class PHPExcel_IOFactory
* @phutil-external-symbol class PHPExcel_Style_NumberFormat
*/
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$ok = @include_once 'PHPExcel.php';
if (!$ok) {
$dialog = new AphrontDialogView();
$dialog->setUser($user);
$dialog->setTitle('Excel Export Not Configured');
$dialog->appendChild(
'<p>This system does not have PHPExcel installed. This software '.
'component is required to export tasks to Excel. Have your system '.
'administrator install it from:</p>'.
'<br />'.
'<p>'.
'<a href="http://www.phpexcel.net/">http://www.phpexcel.net/</a>'.
'</p>'.
'<br />'.
'<p>Your PHP "include_path" needs to be updated to include the '.
'PHPExcel Classes/ directory.</p>');
$dialog->addCancelButton('/maniphest/');
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$query = id(new PhabricatorSearchQuery())->loadOneWhere(
'queryKey = %s',
$this->key);
if (!$query) {
return new Aphront404Response();
}
if (!$request->isDialogFormPost()) {
$dialog = new AphrontDialogView();
$dialog->setUser($user);
$dialog->setTitle('Export Tasks to Excel');
$dialog->appendChild(
'<p>Do you want to export the query results to Excel?</p>');
$dialog->addCancelButton('/maniphest/');
$dialog->addSubmitButton('Export to Excel');
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$query->setParameter('limit', null);
$query->setParameter('offset', null);
$query->setParameter('order', 'p');
$query->setParameter('group', 'n');
list($tasks, $handles) = ManiphestTaskListController::loadTasks($query);
// Ungroup tasks.
$tasks = array_mergev($tasks);
$all_projects = array_mergev(mpull($tasks, 'getProjectPHIDs'));
$project_handles = $this->loadViewerHandles($all_projects);
$handles += $project_handles;
$workbook = new PHPExcel();
$sheet = $workbook->setActiveSheetIndex(0);
$sheet->setTitle('Tasks');
$widths = array(
null,
15,
null,
10,
15,
15,
60,
30,
20,
100,
);
foreach ($widths as $col => $width) {
if ($width !== null) {
$sheet->getColumnDimension($this->col($col))->setWidth($width);
}
}
$status_map = ManiphestTaskStatus::getTaskStatusMap();
$pri_map = ManiphestTaskPriority::getTaskPriorityMap();
$date_format = null;
$rows = array();
$rows[] = array(
'ID',
'Owner',
'Status',
'Priority',
'Date Created',
'Date Updated',
'Title',
'Projects',
'URI',
'Description',
);
$is_date = array(
false,
false,
false,
false,
true,
true,
false,
false,
false,
false,
);
$header_format = array(
'font' => array(
'bold' => true,
),
);
foreach ($tasks as $task) {
$task_owner = null;
if ($task->getOwnerPHID()) {
$task_owner = $handles[$task->getOwnerPHID()]->getName();
}
$projects = array();
foreach ($task->getProjectPHIDs() as $phid) {
$projects[] = $handles[$phid]->getName();
}
$projects = implode(', ', $projects);
$rows[] = array(
'T'.$task->getID(),
$task_owner,
idx($status_map, $task->getStatus(), '?'),
idx($pri_map, $task->getPriority(), '?'),
$this->computeExcelDate($task->getDateCreated()),
$this->computeExcelDate($task->getDateModified()),
$task->getTitle(),
$projects,
PhabricatorEnv::getProductionURI('/T'.$task->getID()),
phutil_utf8_shorten($task->getDescription(), 512),
);
}
foreach ($rows as $row => $cols) {
foreach ($cols as $col => $spec) {
$cell_name = $this->col($col).($row + 1);
$sheet->setCellValue($cell_name, $spec);
if ($row == 0) {
$sheet->getStyle($cell_name)->applyFromArray($header_format);
}
if ($is_date[$col]) {
$code = PHPExcel_Style_NumberFormat::FORMAT_DATE_YYYYMMDD2;
$sheet
->getStyle($cell_name)
->getNumberFormat()
->setFormatCode($code);
}
}
}
$writer = PHPExcel_IOFactory::createWriter($workbook, 'Excel2007');
ob_start();
$writer->save('php://output');
$data = ob_get_clean();
$mime = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
return id(new AphrontFileResponse())
->setMimeType($mime)
->setDownload('maniphest_tasks_'.date('Ymd').'.xlsx')
->setContent($data);
}
private function computeExcelDate($epoch) {
$seconds_per_day = (60 * 60 * 24);
$offset = ($seconds_per_day * 25569);
return ($epoch + $offset) / $seconds_per_day;
}
private function col($n) {
return chr(ord('A') + $n);
}
}
diff --git a/src/applications/maniphest/controller/ManiphestReportController.php b/src/applications/maniphest/controller/ManiphestReportController.php
index 9d0098e1ca..3631ca3ca4 100644
--- a/src/applications/maniphest/controller/ManiphestReportController.php
+++ b/src/applications/maniphest/controller/ManiphestReportController.php
@@ -1,780 +1,764 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestReportController extends ManiphestController {
private $view;
public function willProcessRequest(array $data) {
$this->view = idx($data, 'view');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
if ($request->isFormPost()) {
$uri = $request->getRequestURI();
$project = head($request->getArr('set_project'));
$project = nonempty($project, null);
$uri = $uri->alter('project', $project);
$window = $request->getStr('set_window');
$uri = $uri->alter('window', $window);
return id(new AphrontRedirectResponse())->setURI($uri);
}
$base_nav = $this->buildBaseSideNav();
$base_nav->selectFilter('report', 'report');
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI('/maniphest/report/'));
$nav->addLabel('Open Tasks');
$nav->addFilter('user', 'By User');
$nav->addFilter('project', 'By Project');
$nav->addSpacer();
$nav->addLabel('Burnup');
$nav->addFilter('burn', 'Burnup Rate');
$this->view = $nav->selectFilter($this->view, 'user');
require_celerity_resource('maniphest-report-css');
switch ($this->view) {
case 'burn':
$core = $this->renderBurn();
break;
case 'user':
case 'project':
$core = $this->renderOpenTasks();
break;
default:
return new Aphront404Response();
}
$nav->appendChild($core);
$base_nav->appendChild($nav);
return $this->buildStandardPageResponse(
$base_nav,
array(
'title' => 'Maniphest Reports',
));
}
public function renderBurn() {
$request = $this->getRequest();
$user = $request->getUser();
$handle = null;
$project_phid = $request->getStr('project');
if ($project_phid) {
$phids = array($project_phid);
$handles = $this->loadViewerHandles($phids);
$handle = $handles[$project_phid];
}
$table = new ManiphestTransaction();
$conn = $table->establishConnection('r');
$joins = '';
if ($project_phid) {
$joins = qsprintf(
$conn,
'JOIN %T t ON x.taskID = t.id
JOIN %T p ON p.taskPHID = t.phid AND p.projectPHID = %s',
id(new ManiphestTask())->getTableName(),
id(new ManiphestTaskProject())->getTableName(),
$project_phid);
}
$data = queryfx_all(
$conn,
'SELECT x.oldValue, x.newValue, x.dateCreated FROM %T x %Q
WHERE transactionType = %s
ORDER BY x.dateCreated ASC',
$table->getTableName(),
$joins,
ManiphestTransactionType::TYPE_STATUS);
$stats = array();
$day_buckets = array();
$open_tasks = array();
foreach ($data as $key => $row) {
// NOTE: Hack to avoid json_decode().
$oldv = trim($row['oldValue'], '"');
$newv = trim($row['newValue'], '"');
$old_is_open = ($oldv === (string)ManiphestTaskStatus::STATUS_OPEN);
$new_is_open = ($newv === (string)ManiphestTaskStatus::STATUS_OPEN);
$is_open = ($new_is_open && !$old_is_open);
$is_close = ($old_is_open && !$new_is_open);
$data[$key]['_is_open'] = $is_open;
$data[$key]['_is_close'] = $is_close;
if (!$is_open && !$is_close) {
// This is either some kind of bogus event, or a resolution change
// (e.g., resolved -> invalid). Just skip it.
continue;
}
$day_bucket = phabricator_format_local_time(
$row['dateCreated'],
$user,
'Yz');
$day_buckets[$day_bucket] = $row['dateCreated'];
if (empty($stats[$day_bucket])) {
$stats[$day_bucket] = array(
'open' => 0,
'close' => 0,
);
}
$stats[$day_bucket][$is_close ? 'close' : 'open']++;
}
$template = array(
'open' => 0,
'close' => 0,
);
$rows = array();
$rowc = array();
$last_month = null;
$last_month_epoch = null;
$last_week = null;
$last_week_epoch = null;
$week = null;
$month = null;
$last = last_key($stats) - 1;
$period = $template;
foreach ($stats as $bucket => $info) {
$epoch = $day_buckets[$bucket];
$week_bucket = phabricator_format_local_time(
$epoch,
$user,
'YW');
if ($week_bucket != $last_week) {
if ($week) {
$rows[] = $this->formatBurnRow(
'Week of '.phabricator_date($last_week_epoch, $user),
$week);
$rowc[] = 'week';
}
$week = $template;
$last_week = $week_bucket;
$last_week_epoch = $epoch;
}
$month_bucket = phabricator_format_local_time(
$epoch,
$user,
'Ym');
if ($month_bucket != $last_month) {
if ($month) {
$rows[] = $this->formatBurnRow(
phabricator_format_local_time($last_month_epoch, $user, 'F, Y'),
$month);
$rowc[] = 'month';
}
$month = $template;
$last_month = $month_bucket;
$last_month_epoch = $epoch;
}
$rows[] = $this->formatBurnRow(phabricator_date($epoch, $user), $info);
$rowc[] = null;
$week['open'] += $info['open'];
$week['close'] += $info['close'];
$month['open'] += $info['open'];
$month['close'] += $info['close'];
$period['open'] += $info['open'];
$period['close'] += $info['close'];
}
if ($week) {
$rows[] = $this->formatBurnRow(
'Week To Date',
$week);
$rowc[] = 'week';
}
if ($month) {
$rows[] = $this->formatBurnRow(
'Month To Date',
$month);
$rowc[] = 'month';
}
$rows[] = $this->formatBurnRow(
'All Time',
$period);
$rowc[] = 'aggregate';
$rows = array_reverse($rows);
$rowc = array_reverse($rowc);
$table = new AphrontTableView($rows);
$table->setRowClasses($rowc);
$table->setHeaders(
array(
'Period',
'Opened',
'Closed',
'Change',
));
$table->setColumnClasses(
array(
'right wide',
'n',
'n',
'n',
));
if ($handle) {
$header = "Task Burn Rate for Project ".$handle->renderLink();
$caption = "<p>NOTE: This table reflects tasks <em>currently</em> in ".
"the project. If a task was opened in the past but added to ".
"the project recently, it is counted on the day it was ".
"opened, not the day it was categorized. If a task was part ".
"of this project in the past but no longer is, it is not ".
"counted at all.</p>";
} else {
$header = "Task Burn Rate for All Tasks";
$caption = null;
}
$panel = new AphrontPanelView();
$panel->setHeader($header);
$panel->setCaption($caption);
$panel->appendChild($table);
$tokens = array();
if ($handle) {
$tokens = array(
$handle->getPHID() => $handle->getFullName(),
);
}
$filter = $this->renderReportFilters($tokens, $has_window = false);
$id = celerity_generate_unique_node_id();
$chart = phutil_render_tag(
'div',
array(
'id' => $id,
'style' => 'border: 1px solid #6f6f6f; '.
'margin: 1em 2em; '.
'height: 400px; ',
),
'');
list($burn_x, $burn_y) = $this->buildSeries($data);
require_celerity_resource('raphael-core');
require_celerity_resource('raphael-g');
require_celerity_resource('raphael-g-line');
Javelin::initBehavior('line-chart', array(
'hardpoint' => $id,
'x' => array(
$burn_x,
),
'y' => array(
$burn_y,
),
'xformat' => 'epoch',
));
return array($filter, $chart, $panel);
}
private function renderReportFilters(array $tokens, $has_window) {
$request = $this->getRequest();
$user = $request->getUser();
$form = id(new AphrontFormView())
->setUser($user)
->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/searchproject/')
->setLabel('Project')
->setLimit(1)
->setName('set_project')
->setValue($tokens));
if ($has_window) {
list($window_str, $ignored, $window_error) = $this->getWindow();
$form
->appendChild(
id(new AphrontFormTextControl())
->setLabel('"Recently" Means')
->setName('set_window')
->setCaption(
'Configure the cutoff for the "Recently Closed" column.')
->setValue($window_str)
->setError($window_error));
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Filter By Project'));
$filter = new AphrontListFilterView();
$filter->appendChild($form);
return $filter;
}
private function buildSeries(array $data) {
$out = array();
$counter = 0;
foreach ($data as $row) {
$t = (int)$row['dateCreated'];
if ($row['_is_close']) {
--$counter;
$out[$t] = $counter;
} else if ($row['_is_open']) {
++$counter;
$out[$t] = $counter;
}
}
return array(array_keys($out), array_values($out));
}
private function formatBurnRow($label, $info) {
$delta = $info['open'] - $info['close'];
$fmt = number_format($delta);
if ($delta > 0) {
$fmt = '+'.$fmt;
$fmt = '<span class="red">'.$fmt.'</span>';
} else {
$fmt = '<span class="green">'.$fmt.'</span>';
}
return array(
$label,
number_format($info['open']),
number_format($info['close']),
$fmt);
}
public function renderOpenTasks() {
$request = $this->getRequest();
$user = $request->getUser();
$query = id(new ManiphestTaskQuery())
->withStatus(ManiphestTaskQuery::STATUS_OPEN);
$project_phid = $request->getStr('project');
$project_handle = null;
if ($project_phid) {
$phids = array($project_phid);
$handles = $this->loadViewerHandles($phids);
$project_handle = $handles[$project_phid];
$query->withAnyProjects($phids);
}
$tasks = $query->execute();
$recently_closed = $this->loadRecentlyClosedTasks();
$date = phabricator_date(time(), $user);
switch ($this->view) {
case 'user':
$result = mgroup($tasks, 'getOwnerPHID');
$leftover = idx($result, '', array());
unset($result['']);
$result_closed = mgroup($recently_closed, 'getOwnerPHID');
$leftover_closed = idx($result_closed, '', array());
unset($result_closed['']);
$base_link = '/maniphest/?users=';
$leftover_name = phutil_render_tag(
'a',
array(
'href' => $base_link.ManiphestTaskOwner::OWNER_UP_FOR_GRABS,
),
'<em>(Up For Grabs)</em>');
$col_header = 'User';
$header = 'Open Tasks by User and Priority ('.$date.')';
break;
case 'project':
$result = array();
$leftover = array();
foreach ($tasks as $task) {
$phids = $task->getProjectPHIDs();
if ($phids) {
foreach ($phids as $project_phid) {
$result[$project_phid][] = $task;
}
} else {
$leftover[] = $task;
}
}
$result_closed = array();
$leftover_closed = array();
foreach ($recently_closed as $task) {
$phids = $task->getProjectPHIDs();
if ($phids) {
foreach ($phids as $project_phid) {
$result_closed[$project_phid][] = $task;
}
} else {
$leftover_closed[] = $task;
}
}
$base_link = '/maniphest/view/all/?projects=';
$leftover_name = phutil_render_tag(
'a',
array(
'href' => $base_link.ManiphestTaskOwner::PROJECT_NO_PROJECT,
),
'<em>(No Project)</em>');
$col_header = 'Project';
$header = 'Open Tasks by Project and Priority ('.$date.')';
break;
}
$phids = array_keys($result);
$handles = $this->loadViewerHandles($phids);
$handles = msort($handles, 'getName');
$order = $request->getStr('order', 'name');
list($order, $reverse) = AphrontTableView::parseSort($order);
require_celerity_resource('aphront-tooltip-css');
Javelin::initBehavior('phabricator-tooltips', array());
$rows = array();
$pri_total = array();
foreach (array_merge($handles, array(null)) as $handle) {
if ($handle) {
if (($project_handle) &&
($project_handle->getPHID() == $handle->getPHID())) {
// If filtering by, e.g., "bugs", don't show a "bugs" group.
continue;
}
$tasks = idx($result, $handle->getPHID(), array());
$name = phutil_render_tag(
'a',
array(
'href' => $base_link.$handle->getPHID(),
),
phutil_escape_html($handle->getName()));
$closed = idx($result_closed, $handle->getPHID(), array());
} else {
$tasks = $leftover;
$name = $leftover_name;
$closed = $leftover_closed;
}
$taskv = $tasks;
$tasks = mgroup($tasks, 'getPriority');
$row = array();
$row[] = $name;
$total = 0;
foreach (ManiphestTaskPriority::getTaskPriorityMap() as $pri => $label) {
$n = count(idx($tasks, $pri, array()));
if ($n == 0) {
$row[] = '-';
} else {
$row[] = number_format($n);
}
$total += $n;
}
$row[] = number_format($total);
list($link, $oldest_all) = $this->renderOldest($taskv);
$row[] = $link;
$normal_or_better = array();
foreach ($taskv as $id => $task) {
if ($task->getPriority() < ManiphestTaskPriority::PRIORITY_NORMAL) {
continue;
}
$normal_or_better[$id] = $task;
}
list($link, $oldest_pri) = $this->renderOldest($normal_or_better);
$row[] = $link;
if ($closed) {
$task_ids = implode(',', mpull($closed, 'getID'));
$row[] = phutil_render_tag(
'a',
array(
'href' => '/maniphest/view/custom/?s=oc&tasks='.$task_ids,
'target' => '_blank',
),
phutil_escape_html(number_format(count($closed))));
} else {
$row[] = '-';
}
switch ($order) {
case 'total':
$row['sort'] = $total;
break;
case 'oldest-all':
$row['sort'] = $oldest_all;
break;
case 'oldest-pri':
$row['sort'] = $oldest_pri;
break;
case 'closed':
$row['sort'] = count($closed);
break;
case 'name':
default:
$row['sort'] = $handle ? $handle->getName() : '~';
break;
}
$rows[] = $row;
}
$rows = isort($rows, 'sort');
foreach ($rows as $k => $row) {
unset($rows[$k]['sort']);
}
if ($reverse) {
$rows = array_reverse($rows);
}
$cname = array($col_header);
$cclass = array('pri right wide');
$pri_map = ManiphestTaskPriority::getTaskBriefPriorityMap();
foreach ($pri_map as $pri => $label) {
$cname[] = $label;
$cclass[] = 'n';
}
$cname[] = 'Total';
$cclass[] = 'n';
$cname[] = javelin_render_tag(
'span',
array(
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => 'Oldest open task.',
'size' => 200,
),
),
'Oldest (All)');
$cclass[] = 'n';
$cname[] = javelin_render_tag(
'span',
array(
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => 'Oldest open task, excluding those with Low or Wishlist '.
'priority.',
'size' => 200,
),
),
'Oldest (Pri)');
$cclass[] = 'n';
list($ignored, $window_epoch) = $this->getWindow();
$cname[] = javelin_render_tag(
'span',
array(
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => 'Closed after '.phabricator_datetime($window_epoch, $user),
'size' => 260
),
),
'Recently Closed');
$cclass[] = 'n';
$table = new AphrontTableView($rows);
$table->setHeaders($cname);
$table->setColumnClasses($cclass);
$table->makeSortable(
$request->getRequestURI(),
'order',
$order,
$reverse,
array(
'name',
null,
null,
null,
null,
null,
null,
'total',
'oldest-all',
'oldest-pri',
'closed',
));
$panel = new AphrontPanelView();
$panel->setHeader($header);
$panel->appendChild($table);
$tokens = array();
if ($project_handle) {
$tokens = array(
$project_handle->getPHID() => $project_handle->getFullName(),
);
}
$filter = $this->renderReportFilters($tokens, $has_window = true);
return array($filter, $panel);
}
/**
* Load all the tasks that have been recently closed.
*/
private function loadRecentlyClosedTasks() {
list($ignored, $window_epoch) = $this->getWindow();
$table = new ManiphestTask();
$xtable = new ManiphestTransaction();
$conn_r = $table->establishConnection('r');
$tasks = queryfx_all(
$conn_r,
'SELECT t.* FROM %T t JOIN %T x ON x.taskID = t.id
WHERE t.status != 0
AND x.oldValue IN (null, %s, %s)
AND x.newValue NOT IN (%s, %s)
AND t.dateModified >= %d
AND x.dateCreated >= %d',
$table->getTableName(),
$xtable->getTableName(),
// TODO: Gross. This table is not meant to be queried like this. Build
// real stats tables.
json_encode((int)ManiphestTaskStatus::STATUS_OPEN),
json_encode((string)ManiphestTaskStatus::STATUS_OPEN),
json_encode((int)ManiphestTaskStatus::STATUS_OPEN),
json_encode((string)ManiphestTaskStatus::STATUS_OPEN),
$window_epoch,
$window_epoch);
return id(new ManiphestTask())->loadAllFromArray($tasks);
}
/**
* Parse the "Recently Means" filter into:
*
* - A string representation, like "12 AM 7 days ago" (default);
* - a locale-aware epoch representation; and
* - a possible error.
*/
private function getWindow() {
$request = $this->getRequest();
$user = $request->getUser();
$window_str = $this->getRequest()->getStr('window', '12 AM 7 days ago');
$error = null;
$window_epoch = null;
// Do locale-aware parsing so that the user's timezone is assumed for
// time windows like "3 PM", rather than assuming the server timezone.
$timezone = new DateTimeZone($user->getTimezoneIdentifier());
try {
$date = new DateTime($window_str, $timezone);
$window_epoch = $date->format('U');
} catch (Exception $e) {
$error = 'Invalid';
$window_epoch = time() - (60 * 60 * 24 * 7);
}
// If the time ends up in the future, convert it to the corresponding time
// and equal distance in the past. This is so users can type "6 days" (which
// means "6 days from now") and get the behavior of "6 days ago", rather
// than no results (because the window epoch is in the future). This might
// be a little confusing because it casues "tomorrow" to mean "yesterday"
// and "2022" (or whatever) to mean "ten years ago", but these inputs are
// nonsense anyway.
if ($window_epoch > time()) {
$window_epoch = time() - ($window_epoch - time());
}
return array($window_str, $window_epoch, $error);
}
private function renderOldest(array $tasks) {
assert_instances_of($tasks, 'ManiphestTask');
$oldest = null;
foreach ($tasks as $id => $task) {
if (($oldest === null) ||
($task->getDateCreated() < $tasks[$oldest]->getDateCreated())) {
$oldest = $id;
}
}
if ($oldest === null) {
return array('-', 0);
}
$oldest = $tasks[$oldest];
$raw_age = (time() - $oldest->getDateCreated());
$age = number_format($raw_age / (24 * 60 * 60)).' d';
$link = javelin_render_tag(
'a',
array(
'href' => '/T'.$oldest->getID(),
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => 'T'.$oldest->getID().': '.$oldest->getTitle(),
),
'target' => '_blank',
),
phutil_escape_html($age));
return array($link, $raw_age);
}
}
diff --git a/src/applications/maniphest/controller/ManiphestSavedQueryDeleteController.php b/src/applications/maniphest/controller/ManiphestSavedQueryDeleteController.php
index d4a39ff0d7..dd4f9a35d0 100644
--- a/src/applications/maniphest/controller/ManiphestSavedQueryDeleteController.php
+++ b/src/applications/maniphest/controller/ManiphestSavedQueryDeleteController.php
@@ -1,64 +1,48 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestSavedQueryDeleteController extends ManiphestController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$id = $this->id;
$query = id(new ManiphestSavedQuery())->load($id);
if (!$query) {
return new Aphront404Response();
}
if ($query->getUserPHID() != $user->getPHID()) {
return new Aphront400Response();
}
if ($request->isDialogFormPost()) {
$query->delete();
return id(new AphrontRedirectResponse())->setURI('/maniphest/custom/');
}
$name = $query->getName();
$dialog = id(new AphrontDialogView())
->setUser($user)
->setTitle('Really delete this query?')
->appendChild(
'<p>'.
'Really delete the query "'.phutil_escape_html($name).'"? '.
'It will be lost forever!'.
'</p>')
->addCancelButton('/maniphest/custom/')
->addSubmitButton('Delete');
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/maniphest/controller/ManiphestSavedQueryEditController.php b/src/applications/maniphest/controller/ManiphestSavedQueryEditController.php
index 6a1292f9b9..8fd0e5b32d 100644
--- a/src/applications/maniphest/controller/ManiphestSavedQueryEditController.php
+++ b/src/applications/maniphest/controller/ManiphestSavedQueryEditController.php
@@ -1,120 +1,104 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestSavedQueryEditController extends ManiphestController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$key = $request->getStr('key');
if (!$key) {
$id = nonempty($this->id, $request->getInt('id'));
if (!$id) {
return new Aphront404Response();
}
$query = id(new ManiphestSavedQuery())->load($id);
if (!$query) {
return new Aphront404Response();
}
if ($query->getUserPHID() != $user->getPHID()) {
return new Aphront400Response();
}
} else {
$query = new ManiphestSavedQuery();
$query->setUserPHID($user->getPHID());
$query->setQueryKey($key);
$query->setIsDefault(0);
}
$e_name = true;
$errors = array();
if ($request->isFormPost()) {
$e_name = null;
$query->setName($request->getStr('name'));
if (!strlen($query->getName())) {
$e_name = 'Required';
$errors[] = 'Saved query name is required.';
}
if (!$errors) {
$query->save();
return id(new AphrontRedirectResponse())->setURI('/maniphest/custom/');
}
}
if ($errors) {
$error_view = new AphrontErrorView();
$error_view->setTitle('Form Errors');
$error_view->setErrors($errors);
} else {
$error_view = null;
}
if ($query->getID()) {
$header = 'Edit Saved Query';
$cancel_uri = '/maniphest/custom/';
} else {
$header = 'New Saved Query';
$cancel_uri = '/maniphest/view/custom/?key='.$key;
}
$form = id(new AphrontFormView())
->setUser($user)
->addHiddenInput('key', $key)
->addHiddenInput('id', $query->getID())
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Name')
->setValue($query->getName())
->setName('name')
->setError($e_name))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($cancel_uri)
->setValue('Save'));
$panel = new AphrontPanelView();
$panel->setHeader($header);
$panel->appendChild($form);
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$nav = $this->buildBaseSideNav();
// The side nav won't show "Saved Queries..." until you have at least one.
$nav->selectFilter('saved', 'custom');
$nav->appendChild($error_view);
$nav->appendChild($panel);
return $this->buildStandardPageResponse(
$nav,
array(
'title' => 'Saved Queries',
));
}
}
diff --git a/src/applications/maniphest/controller/ManiphestSavedQueryListController.php b/src/applications/maniphest/controller/ManiphestSavedQueryListController.php
index 8e180f5daa..5f5a82666b 100644
--- a/src/applications/maniphest/controller/ManiphestSavedQueryListController.php
+++ b/src/applications/maniphest/controller/ManiphestSavedQueryListController.php
@@ -1,148 +1,132 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestSavedQueryListController extends ManiphestController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$nav = $this->buildBaseSideNav();
$queries = id(new ManiphestSavedQuery())->loadAllWhere(
'userPHID = %s ORDER BY name ASC',
$user->getPHID());
$default = null;
if ($request->isFormPost()) {
$new_default = null;
foreach ($queries as $query) {
if ($query->getID() == $request->getInt('default')) {
$new_default = $query;
}
}
if ($this->getDefaultQuery()) {
$this->getDefaultQuery()->setIsDefault(0)->save();
}
if ($new_default) {
$new_default->setIsDefault(1)->save();
}
return id(new AphrontRedirectResponse())->setURI('/maniphest/custom/');
}
$rows = array();
foreach ($queries as $query) {
if ($query->getIsDefault()) {
$default = $query;
}
$rows[] = array(
phutil_render_tag(
'input',
array(
'type' => 'radio',
'name' => 'default',
'value' => $query->getID(),
'checked' => ($query->getIsDefault() ? 'checked' : null),
)),
phutil_render_tag(
'a',
array(
'href' => '/maniphest/view/custom/?key='.$query->getQueryKey(),
),
phutil_escape_html($query->getName())),
phutil_render_tag(
'a',
array(
'href' => '/maniphest/custom/edit/'.$query->getID().'/',
'class' => 'grey small button',
),
'Edit'),
javelin_render_tag(
'a',
array(
'href' => '/maniphest/custom/delete/'.$query->getID().'/',
'class' => 'grey small button',
'sigil' => 'workflow',
),
'Delete'),
);
}
$rows[] = array(
phutil_render_tag(
'input',
array(
'type' => 'radio',
'name' => 'default',
'value' => 0,
'checked' => ($default === null ? 'checked' : null),
)),
'<em>No Default</em>',
'',
'',
);
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Default',
'Name',
'Edit',
'Delete',
));
$table->setColumnClasses(
array(
'radio',
'wide pri',
'action',
'action',
));
$panel = new AphrontPanelView();
$panel->setHeader('Saved Custom Queries');
$panel->addButton(
phutil_render_tag(
'button',
array(),
'Save Default Query'));
$panel->appendChild($table);
$form = phabricator_render_form(
$user,
array(
'method' => 'POST',
'action' => $request->getRequestURI(),
),
$panel->render());
$nav->selectFilter('saved', 'saved');
$nav->appendChild($form);
return $this->buildStandardPageResponse(
$nav,
array(
'title' => 'Saved Queries',
));
}
}
diff --git a/src/applications/maniphest/controller/ManiphestSubpriorityController.php b/src/applications/maniphest/controller/ManiphestSubpriorityController.php
index 5793ec214e..07fa1e174d 100644
--- a/src/applications/maniphest/controller/ManiphestSubpriorityController.php
+++ b/src/applications/maniphest/controller/ManiphestSubpriorityController.php
@@ -1,80 +1,64 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestSubpriorityController extends ManiphestController {
public function processRequest() {
$request = $this->getRequest();
if (!$request->validateCSRF()) {
return new Aphront403Response();
}
$task = id(new ManiphestTask())->load($request->getInt('task'));
if (!$task) {
return new Aphront404Response();
}
if ($request->getInt('after')) {
$after_task = id(new ManiphestTask())->load($request->getInt('after'));
if (!$after_task) {
return new Aphront404Response();
}
$after_pri = $after_task->getPriority();
$after_sub = $after_task->getSubpriority();
} else {
$after_pri = $request->getInt('priority');
$after_sub = null;
}
$new_sub = ManiphestTransactionEditor::getNextSubpriority(
$after_pri,
$after_sub);
if ($after_pri != $task->getPriority()) {
$xaction = new ManiphestTransaction();
$xaction->setAuthorPHID($request->getUser()->getPHID());
// TODO: Content source?
$xaction->setTransactionType(ManiphestTransactionType::TYPE_PRIORITY);
$xaction->setNewValue($after_pri);
$editor = new ManiphestTransactionEditor();
$editor->setActor($request->getUser());
$editor->applyTransactions($task, array($xaction));
}
$task->setSubpriority($new_sub);
$task->save();
$pri_class = ManiphestTaskSummaryView::getPriorityClass(
$task->getPriority());
$class = 'maniphest-task-handle maniphest-active-handle '.$pri_class;
$response = array(
'className' => $class,
);
return id(new AphrontAjaxResponse())->setContent($response);
}
}
diff --git a/src/applications/maniphest/controller/ManiphestTaskDescriptionChangeController.php b/src/applications/maniphest/controller/ManiphestTaskDescriptionChangeController.php
index d28f209ab6..a26675f64c 100644
--- a/src/applications/maniphest/controller/ManiphestTaskDescriptionChangeController.php
+++ b/src/applications/maniphest/controller/ManiphestTaskDescriptionChangeController.php
@@ -1,88 +1,72 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestTaskDescriptionChangeController
extends ManiphestController {
private $transactionID;
protected function setTransactionID($transaction_id) {
$this->transactionID = $transaction_id;
return $this;
}
public function getTransactionID() {
return $this->transactionID;
}
public function willProcessRequest(array $data) {
$this->setTransactionID(idx($data, 'id'));
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$is_show_more = false;
if (!$this->getTransactionID()) {
$this->setTransactionID($this->getRequest()->getStr('ref'));
$is_show_more = true;
}
$transaction_id = $this->getTransactionID();
$transaction = id(new ManiphestTransaction())->load($transaction_id);
if (!$transaction) {
return new Aphront404Response();
}
$transactions = array($transaction);
$phids = array();
foreach ($transactions as $xaction) {
foreach ($xaction->extractPHIDs() as $phid) {
$phids[$phid] = $phid;
}
}
$handles = $this->loadViewerHandles($phids);
$engine = new PhabricatorMarkupEngine();
$engine->setViewer($user);
$engine->addObject($transaction, ManiphestTransaction::MARKUP_FIELD_BODY);
$engine->process();
$view = new ManiphestTransactionDetailView();
$view->setTransactionGroup($transactions);
$view->setHandles($handles);
$view->setUser($user);
$view->setMarkupEngine($engine);
$view->setRenderSummaryOnly(true);
$view->setRenderFullSummary(true);
$view->setRangeSpecification($request->getStr('range'));
if ($is_show_more) {
return id(new PhabricatorChangesetResponse())
->setRenderedChangeset($view->render());
} else {
return id(new AphrontAjaxResponse())->setContent($view->render());
}
}
}
diff --git a/src/applications/maniphest/controller/ManiphestTaskDescriptionPreviewController.php b/src/applications/maniphest/controller/ManiphestTaskDescriptionPreviewController.php
index 35989c9e57..64fa264dc8 100644
--- a/src/applications/maniphest/controller/ManiphestTaskDescriptionPreviewController.php
+++ b/src/applications/maniphest/controller/ManiphestTaskDescriptionPreviewController.php
@@ -1,45 +1,29 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestTaskDescriptionPreviewController
extends ManiphestController {
public function processRequest() {
$request = $this->getRequest();
$description = $request->getStr('description');
$task = new ManiphestTask();
$task->setDescription($description);
$output = PhabricatorMarkupEngine::renderOneObject(
$task,
ManiphestTask::MARKUP_FIELD_DESCRIPTION,
$request->getUser());
$content =
'<div class="phabricator-remarkup">'.
$output.
'</div>';
return id(new AphrontAjaxResponse())
->setContent($content);
}
}
diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php
index d93cc1c3de..deb9cd12db 100644
--- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php
+++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php
@@ -1,537 +1,521 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestTaskDetailController extends ManiphestController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$e_title = null;
$priority_map = ManiphestTaskPriority::getTaskPriorityMap();
$task = id(new ManiphestTask())->load($this->id);
if (!$task) {
return new Aphront404Response();
}
$workflow = $request->getStr('workflow');
$parent_task = null;
if ($workflow && is_numeric($workflow)) {
$parent_task = id(new ManiphestTask())->load($workflow);
}
$transactions = id(new ManiphestTransaction())->loadAllWhere(
'taskID = %d ORDER BY id ASC',
$task->getID());
$e_commit = PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT;
$e_dep_on = PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK;
$e_dep_by = PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK;
$e_rev = PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV;
$phid = $task->getPHID();
$query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($phid))
->withEdgeTypes(
array(
$e_commit,
$e_dep_on,
$e_dep_by,
$e_rev,
));
$edges = $query->execute();
$commit_phids = array_keys($edges[$phid][$e_commit]);
$dep_on_tasks = array_keys($edges[$phid][$e_dep_on]);
$dep_by_tasks = array_keys($edges[$phid][$e_dep_by]);
$revs = array_keys($edges[$phid][$e_rev]);
$phids = array_fill_keys($query->getDestinationPHIDs(), true);
foreach ($transactions as $transaction) {
foreach ($transaction->extractPHIDs() as $phid) {
$phids[$phid] = true;
}
}
foreach ($task->getCCPHIDs() as $phid) {
$phids[$phid] = true;
}
foreach ($task->getProjectPHIDs() as $phid) {
$phids[$phid] = true;
}
if ($task->getOwnerPHID()) {
$phids[$task->getOwnerPHID()] = true;
}
$phids[$task->getAuthorPHID()] = true;
$attached = $task->getAttached();
foreach ($attached as $type => $list) {
foreach ($list as $phid => $info) {
$phids[$phid] = true;
}
}
if ($parent_task) {
$phids[$parent_task->getPHID()] = true;
}
$phids = array_keys($phids);
$handles = $this->loadViewerHandles($phids);
$dict = array();
$dict['Status'] =
'<strong>'.
ManiphestTaskStatus::getTaskStatusFullName($task->getStatus()).
'</strong>';
$dict['Assigned To'] = $task->getOwnerPHID()
? $handles[$task->getOwnerPHID()]->renderLink()
: '<em>None</em>';
$dict['Priority'] = ManiphestTaskPriority::getTaskPriorityName(
$task->getPriority());
$cc = $task->getCCPHIDs();
if ($cc) {
$cc_links = array();
foreach ($cc as $phid) {
$cc_links[] = $handles[$phid]->renderLink();
}
$dict['CC'] = implode(', ', $cc_links);
} else {
$dict['CC'] = '<em>None</em>';
}
$dict['Author'] = $handles[$task->getAuthorPHID()]->renderLink();
$source = $task->getOriginalEmailSource();
if ($source) {
$subject = '[T'.$task->getID().'] '.$task->getTitle();
$dict['From Email'] = phutil_render_tag(
'a',
array(
'href' => 'mailto:'.$source.'?subject='.$subject
),
phutil_escape_html($source));
}
$projects = $task->getProjectPHIDs();
if ($projects) {
$project_links = array();
foreach ($projects as $phid) {
$project_links[] = $handles[$phid]->renderLink();
}
$dict['Projects'] = implode(', ', $project_links);
} else {
$dict['Projects'] = '<em>None</em>';
}
$extensions = ManiphestTaskExtensions::newExtensions();
$aux_fields = $extensions->getAuxiliaryFieldSpecifications();
if ($aux_fields) {
$task->loadAndAttachAuxiliaryAttributes();
foreach ($aux_fields as $aux_field) {
$aux_key = $aux_field->getAuxiliaryKey();
$aux_field->setValue($task->getAuxiliaryAttribute($aux_key));
$value = $aux_field->renderForDetailView();
if (strlen($value)) {
$dict[$aux_field->getLabel()] = $value;
}
}
}
if ($dep_by_tasks) {
$dict['Dependent Tasks'] = $this->renderHandleList(
array_select_keys($handles, $dep_by_tasks));
}
if ($dep_on_tasks) {
$dict['Depends On'] = $this->renderHandleList(
array_select_keys($handles, $dep_on_tasks));
}
if ($revs) {
$dict['Revisions'] = $this->renderHandleList(
array_select_keys($handles, $revs));
}
if ($commit_phids) {
$dict['Commits'] = $this->renderHandleList(
array_select_keys($handles, $commit_phids));
}
$file_infos = idx($attached, PhabricatorPHIDConstants::PHID_TYPE_FILE);
if ($file_infos) {
$file_phids = array_keys($file_infos);
$files = id(new PhabricatorFile())->loadAllWhere(
'phid IN (%Ls)',
$file_phids);
$view = new PhabricatorFileLinkListView();
$view->setFiles($files);
$dict['Files'] = $view->render();
}
$context_bar = null;
if ($parent_task) {
$context_bar = new AphrontContextBarView();
$context_bar->addButton(
phutil_render_tag(
'a',
array(
'href' => '/maniphest/task/create/?parent='.$parent_task->getID(),
'class' => 'green button',
),
'Create Another Subtask'));
$context_bar->appendChild(
'Created a subtask of <strong>'.
$handles[$parent_task->getPHID()]->renderLink().
'</strong>');
} else if ($workflow == 'create') {
$context_bar = new AphrontContextBarView();
$context_bar->addButton('<label>Create Another:</label>');
$context_bar->addButton(
phutil_render_tag(
'a',
array(
'href' => '/maniphest/task/create/?template='.$task->getID(),
'class' => 'green button',
),
'Similar Task'));
$context_bar->addButton(
phutil_render_tag(
'a',
array(
'href' => '/maniphest/task/create/',
'class' => 'green button',
),
'Empty Task'));
$context_bar->appendChild('New task created.');
}
$actions = array();
$action = new AphrontHeadsupActionView();
$action->setName('Edit Task');
$action->setURI('/maniphest/task/edit/'.$task->getID().'/');
$action->setClass('action-edit');
$actions[] = $action;
require_celerity_resource('phabricator-flag-css');
$flag = PhabricatorFlagQuery::loadUserFlag($user, $task->getPHID());
if ($flag) {
$class = PhabricatorFlagColor::getCSSClass($flag->getColor());
$color = PhabricatorFlagColor::getColorName($flag->getColor());
$action = new AphrontHeadsupActionView();
$action->setClass('flag-clear '.$class);
$action->setURI('/flag/delete/'.$flag->getID().'/');
$action->setName('Remove '.$color.' Flag');
$action->setWorkflow(true);
$actions[] = $action;
} else {
$action = new AphrontHeadsupActionView();
$action->setClass('phabricator-flag-ghost');
$action->setURI('/flag/edit/'.$task->getPHID().'/');
$action->setName('Flag Task');
$action->setWorkflow(true);
$actions[] = $action;
}
require_celerity_resource('phabricator-object-selector-css');
require_celerity_resource('javelin-behavior-phabricator-object-selector');
$action = new AphrontHeadsupActionView();
$action->setName('Merge Duplicates');
$action->setURI('/search/attach/'.$task->getPHID().'/TASK/merge/');
$action->setWorkflow(true);
$action->setClass('action-merge');
$actions[] = $action;
$action = new AphrontHeadsupActionView();
$action->setName('Create Subtask');
$action->setURI('/maniphest/task/create/?parent='.$task->getID());
$action->setClass('action-branch');
$actions[] = $action;
$action = new AphrontHeadsupActionView();
$action->setName('Edit Dependencies');
$action->setURI('/search/attach/'.$task->getPHID().'/TASK/dependencies/');
$action->setWorkflow(true);
$action->setClass('action-dependencies');
$actions[] = $action;
$action = new AphrontHeadsupActionView();
$action->setName('Edit Differential Revisions');
$action->setURI('/search/attach/'.$task->getPHID().'/DREV/');
$action->setWorkflow(true);
$action->setClass('action-attach');
$actions[] = $action;
$action_list = new AphrontHeadsupActionListView();
$action_list->setActions($actions);
$headsup_panel = new AphrontHeadsupView();
$headsup_panel->setObjectName('T'.$task->getID());
$headsup_panel->setHeader($task->getTitle());
$headsup_panel->setActionList($action_list);
$headsup_panel->setProperties($dict);
$engine = new PhabricatorMarkupEngine();
$engine->setViewer($user);
$engine->addObject($task, ManiphestTask::MARKUP_FIELD_DESCRIPTION);
foreach ($transactions as $xaction) {
if ($xaction->hasComments()) {
$engine->addObject($xaction, ManiphestTransaction::MARKUP_FIELD_BODY);
}
}
$engine->process();
$headsup_panel->appendChild(
'<div class="phabricator-remarkup">'.
$engine->getOutput($task, ManiphestTask::MARKUP_FIELD_DESCRIPTION).
'</div>');
$transaction_types = ManiphestTransactionType::getTransactionTypeMap();
$resolution_types = ManiphestTaskStatus::getTaskStatusMap();
if ($task->getStatus() == ManiphestTaskStatus::STATUS_OPEN) {
$resolution_types = array_select_keys(
$resolution_types,
array(
ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
ManiphestTaskStatus::STATUS_CLOSED_WONTFIX,
ManiphestTaskStatus::STATUS_CLOSED_INVALID,
ManiphestTaskStatus::STATUS_CLOSED_SPITE,
));
} else {
$resolution_types = array(
ManiphestTaskStatus::STATUS_OPEN => 'Reopened',
);
$transaction_types[ManiphestTransactionType::TYPE_STATUS] =
'Reopen Task';
unset($transaction_types[ManiphestTransactionType::TYPE_PRIORITY]);
unset($transaction_types[ManiphestTransactionType::TYPE_OWNER]);
}
$default_claim = array(
$user->getPHID() => $user->getUsername().' ('.$user->getRealName().')',
);
$draft = id(new PhabricatorDraft())->loadOneWhere(
'authorPHID = %s AND draftKey = %s',
$user->getPHID(),
$task->getPHID());
if ($draft) {
$draft_text = $draft->getDraft();
} else {
$draft_text = null;
}
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
if ($is_serious) {
// Prevent tasks from being closed "out of spite" in serious business
// installs.
unset($resolution_types[ManiphestTaskStatus::STATUS_CLOSED_SPITE]);
}
$comment_form = new AphrontFormView();
$comment_form
->setUser($user)
->setAction('/maniphest/transaction/save/')
->setEncType('multipart/form-data')
->addHiddenInput('taskID', $task->getID())
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Action')
->setName('action')
->setOptions($transaction_types)
->setID('transaction-action'))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Resolution')
->setName('resolution')
->setControlID('resolution')
->setControlStyle('display: none')
->setOptions($resolution_types))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel('Assign To')
->setName('assign_to')
->setControlID('assign_to')
->setControlStyle('display: none')
->setID('assign-tokenizer')
->setDisableBehavior(true))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel('CCs')
->setName('ccs')
->setControlID('ccs')
->setControlStyle('display: none')
->setID('cc-tokenizer')
->setDisableBehavior(true))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Priority')
->setName('priority')
->setOptions($priority_map)
->setControlID('priority')
->setControlStyle('display: none')
->setValue($task->getPriority()))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel('Projects')
->setName('projects')
->setControlID('projects')
->setControlStyle('display: none')
->setID('projects-tokenizer')
->setDisableBehavior(true))
->appendChild(
id(new AphrontFormFileControl())
->setLabel('File')
->setName('file')
->setControlID('file')
->setControlStyle('display: none'))
->appendChild(
id(new PhabricatorRemarkupControl())
->setLabel('Comments')
->setName('comments')
->setValue($draft_text)
->setID('transaction-comments'))
->appendChild(
id(new AphrontFormDragAndDropUploadControl())
->setLabel('Attached Files')
->setName('files')
->setActivatedClass('aphront-panel-view-drag-and-drop'))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue($is_serious ? 'Submit' : 'Avast!'));
$control_map = array(
ManiphestTransactionType::TYPE_STATUS => 'resolution',
ManiphestTransactionType::TYPE_OWNER => 'assign_to',
ManiphestTransactionType::TYPE_CCS => 'ccs',
ManiphestTransactionType::TYPE_PRIORITY => 'priority',
ManiphestTransactionType::TYPE_PROJECTS => 'projects',
ManiphestTransactionType::TYPE_ATTACH => 'file',
);
$tokenizer_map = array(
ManiphestTransactionType::TYPE_PROJECTS => array(
'id' => 'projects-tokenizer',
'src' => '/typeahead/common/projects/',
'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'),
'placeholder' => 'Type a project name...',
),
ManiphestTransactionType::TYPE_OWNER => array(
'id' => 'assign-tokenizer',
'src' => '/typeahead/common/users/',
'value' => $default_claim,
'limit' => 1,
'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'),
'placeholder' => 'Type a user name...',
),
ManiphestTransactionType::TYPE_CCS => array(
'id' => 'cc-tokenizer',
'src' => '/typeahead/common/mailable/',
'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'),
'placeholder' => 'Type a user or mailing list...',
),
);
Javelin::initBehavior('maniphest-transaction-controls', array(
'select' => 'transaction-action',
'controlMap' => $control_map,
'tokenizers' => $tokenizer_map,
));
Javelin::initBehavior('maniphest-transaction-preview', array(
'uri' => '/maniphest/transaction/preview/'.$task->getID().'/',
'preview' => 'transaction-preview',
'comments' => 'transaction-comments',
'action' => 'transaction-action',
'map' => $control_map,
'tokenizers' => $tokenizer_map,
));
$comment_panel = new AphrontPanelView();
$comment_panel->appendChild($comment_form);
$comment_panel->addClass('aphront-panel-accent');
$comment_panel->setHeader($is_serious ? 'Add Comment' : 'Weigh In');
$preview_panel =
'<div class="aphront-panel-preview">
<div id="transaction-preview">
<div class="aphront-panel-preview-loading-text">
Loading preview...
</div>
</div>
</div>';
$transaction_view = new ManiphestTransactionListView();
$transaction_view->setTransactions($transactions);
$transaction_view->setHandles($handles);
$transaction_view->setUser($user);
$transaction_view->setAuxiliaryFields($aux_fields);
$transaction_view->setMarkupEngine($engine);
PhabricatorFeedStoryNotification::updateObjectNotificationViews(
$user, $task->getPHID());
return $this->buildStandardPageResponse(
array(
$context_bar,
$headsup_panel,
$transaction_view,
$comment_panel,
$preview_panel,
),
array(
'title' => 'T'.$task->getID().' '.$task->getTitle(),
'pageObjects' => array($task->getPHID()),
));
}
private function renderHandleList(array $handles) {
$links = array();
foreach ($handles as $handle) {
$links[] = $handle->renderLink();
}
return implode('<br />', $links);
}
}
diff --git a/src/applications/maniphest/controller/ManiphestTaskEditController.php b/src/applications/maniphest/controller/ManiphestTaskEditController.php
index a5c5da92e6..cfb4733012 100644
--- a/src/applications/maniphest/controller/ManiphestTaskEditController.php
+++ b/src/applications/maniphest/controller/ManiphestTaskEditController.php
@@ -1,568 +1,552 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestTaskEditController extends ManiphestController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$files = array();
$parent_task = null;
$template_id = null;
if ($this->id) {
$task = id(new ManiphestTask())->load($this->id);
if (!$task) {
return new Aphront404Response();
}
} else {
$task = new ManiphestTask();
$task->setPriority(ManiphestTaskPriority::getDefaultPriority());
$task->setAuthorPHID($user->getPHID());
// These allow task creation with defaults.
if (!$request->isFormPost()) {
$task->setTitle($request->getStr('title'));
$default_projects = $request->getStr('projects');
if ($default_projects) {
$task->setProjectPHIDs(explode(';', $default_projects));
}
}
$file_phids = $request->getArr('files', array());
if (!$file_phids) {
// Allow a single 'file' key instead, mostly since Mac OS X urlencodes
// square brackets in URLs when passed to 'open', so you can't 'open'
// a URL like '?files[]=xyz' and have PHP interpret it correctly.
$phid = $request->getStr('file');
if ($phid) {
$file_phids = array($phid);
}
}
if ($file_phids) {
$files = id(new PhabricatorFile())->loadAllWhere(
'phid IN (%Ls)',
$file_phids);
}
$template_id = $request->getInt('template');
// You can only have a parent task if you're creating a new task.
$parent_id = $request->getInt('parent');
if ($parent_id) {
$parent_task = id(new ManiphestTask())->load($parent_id);
}
}
$errors = array();
$e_title = true;
$extensions = ManiphestTaskExtensions::newExtensions();
$aux_fields = $extensions->getAuxiliaryFieldSpecifications();
if ($request->isFormPost()) {
$changes = array();
$new_title = $request->getStr('title');
$new_desc = $request->getStr('description');
$new_status = $request->getStr('status');
$workflow = '';
if ($task->getID()) {
if ($new_title != $task->getTitle()) {
$changes[ManiphestTransactionType::TYPE_TITLE] = $new_title;
}
if ($new_desc != $task->getDescription()) {
$changes[ManiphestTransactionType::TYPE_DESCRIPTION] = $new_desc;
}
if ($new_status != $task->getStatus()) {
$changes[ManiphestTransactionType::TYPE_STATUS] = $new_status;
}
} else {
$task->setTitle($new_title);
$task->setDescription($new_desc);
$changes[ManiphestTransactionType::TYPE_STATUS] =
ManiphestTaskStatus::STATUS_OPEN;
$workflow = 'create';
}
$owner_tokenizer = $request->getArr('assigned_to');
$owner_phid = reset($owner_tokenizer);
if (!strlen($new_title)) {
$e_title = 'Required';
$errors[] = 'Title is required.';
}
foreach ($aux_fields as $aux_field) {
$aux_field->setValueFromRequest($request);
if ($aux_field->isRequired() && !strlen($aux_field->getValue())) {
$errors[] = $aux_field->getLabel() . ' is required.';
$aux_field->setError('Required');
}
if (strlen($aux_field->getValue())) {
try {
$aux_field->validate();
} catch (Exception $e) {
$errors[] = $e->getMessage();
$aux_field->setError('Invalid');
}
}
}
if ($errors) {
$task->setPriority($request->getInt('priority'));
$task->setOwnerPHID($owner_phid);
$task->setCCPHIDs($request->getArr('cc'));
$task->setProjectPHIDs($request->getArr('projects'));
} else {
if ($request->getInt('priority') != $task->getPriority()) {
$changes[ManiphestTransactionType::TYPE_PRIORITY] =
$request->getInt('priority');
}
if ($owner_phid != $task->getOwnerPHID()) {
$changes[ManiphestTransactionType::TYPE_OWNER] = $owner_phid;
}
if ($request->getArr('cc') != $task->getCCPHIDs()) {
$changes[ManiphestTransactionType::TYPE_CCS] = $request->getArr('cc');
}
$new_proj_arr = $request->getArr('projects');
$new_proj_arr = array_values($new_proj_arr);
sort($new_proj_arr);
$cur_proj_arr = $task->getProjectPHIDs();
$cur_proj_arr = array_values($cur_proj_arr);
sort($cur_proj_arr);
if ($new_proj_arr != $cur_proj_arr) {
$changes[ManiphestTransactionType::TYPE_PROJECTS] = $new_proj_arr;
}
if ($files) {
$file_map = mpull($files, 'getPHID');
$file_map = array_fill_keys($file_map, array());
$changes[ManiphestTransactionType::TYPE_ATTACH] = array(
PhabricatorPHIDConstants::PHID_TYPE_FILE => $file_map,
);
}
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_WEB,
array(
'ip' => $request->getRemoteAddr(),
));
$template = new ManiphestTransaction();
$template->setAuthorPHID($user->getPHID());
$template->setContentSource($content_source);
$transactions = array();
foreach ($changes as $type => $value) {
$transaction = clone $template;
$transaction->setTransactionType($type);
$transaction->setNewValue($value);
$transactions[] = $transaction;
}
if ($aux_fields) {
$task->loadAndAttachAuxiliaryAttributes();
foreach ($aux_fields as $aux_field) {
$transaction = clone $template;
$transaction->setTransactionType(
ManiphestTransactionType::TYPE_AUXILIARY);
$aux_key = $aux_field->getAuxiliaryKey();
$transaction->setMetadataValue('aux:key', $aux_key);
$transaction->setNewValue($aux_field->getValueForStorage());
$transactions[] = $transaction;
}
}
if ($transactions) {
$is_new = !$task->getID();
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK,
array(
'task' => $task,
'new' => $is_new,
'transactions' => $transactions,
));
$event->setUser($user);
$event->setAphrontRequest($request);
PhutilEventEngine::dispatchEvent($event);
$task = $event->getValue('task');
$transactions = $event->getValue('transactions');
$editor = new ManiphestTransactionEditor();
$editor->setActor($user);
$editor->setAuxiliaryFields($aux_fields);
$editor->applyTransactions($task, $transactions);
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK,
array(
'task' => $task,
'new' => $is_new,
'transactions' => $transactions,
));
$event->setUser($user);
$event->setAphrontRequest($request);
PhutilEventEngine::dispatchEvent($event);
}
if ($parent_task) {
id(new PhabricatorEdgeEditor())
->setActor($user)
->addEdge(
$parent_task->getPHID(),
PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK,
$task->getPHID())
->save();
$workflow = $parent_task->getID();
}
$redirect_uri = '/T'.$task->getID();
if ($workflow) {
$redirect_uri .= '?workflow='.$workflow;
}
return id(new AphrontRedirectResponse())
->setURI($redirect_uri);
}
} else {
if ($aux_fields) {
$task->loadAndAttachAuxiliaryAttributes();
foreach ($aux_fields as $aux_field) {
$aux_key = $aux_field->getAuxiliaryKey();
$value = $task->getAuxiliaryAttribute($aux_key);
$aux_field->setValueFromStorage($value);
}
}
if (!$task->getID()) {
$task->setCCPHIDs(array(
$user->getPHID(),
));
if ($template_id) {
$template_task = id(new ManiphestTask())->load($template_id);
if ($template_task) {
$task->setCCPHIDs($template_task->getCCPHIDs());
$task->setProjectPHIDs($template_task->getProjectPHIDs());
$task->setOwnerPHID($template_task->getOwnerPHID());
$task->setPriority($template_task->getPriority());
if ($aux_fields) {
$template_task->loadAndAttachAuxiliaryAttributes();
foreach ($aux_fields as $aux_field) {
if (!$aux_field->shouldCopyWhenCreatingSimilarTask()) {
continue;
}
$aux_key = $aux_field->getAuxiliaryKey();
$value = $template_task->getAuxiliaryAttribute($aux_key);
$aux_field->setValueFromStorage($value);
}
}
}
}
}
}
$phids = array_merge(
array($task->getOwnerPHID()),
$task->getCCPHIDs(),
$task->getProjectPHIDs());
if ($parent_task) {
$phids[] = $parent_task->getPHID();
}
$phids = array_filter($phids);
$phids = array_unique($phids);
$handles = $this->loadViewerHandles($phids);
$tvalues = mpull($handles, 'getFullName', 'getPHID');
$error_view = null;
if ($errors) {
$error_view = new AphrontErrorView();
$error_view->setErrors($errors);
$error_view->setTitle('Form Errors');
}
$priority_map = ManiphestTaskPriority::getTaskPriorityMap();
if ($task->getOwnerPHID()) {
$assigned_value = array(
$task->getOwnerPHID() => $handles[$task->getOwnerPHID()]->getFullName(),
);
} else {
$assigned_value = array();
}
if ($task->getCCPHIDs()) {
$cc_value = array_select_keys($tvalues, $task->getCCPHIDs());
} else {
$cc_value = array();
}
if ($task->getProjectPHIDs()) {
$projects_value = array_select_keys($tvalues, $task->getProjectPHIDs());
} else {
$projects_value = array();
}
$cancel_id = nonempty($task->getID(), $template_id);
if ($cancel_id) {
$cancel_uri = '/T'.$cancel_id;
} else {
$cancel_uri = '/maniphest/';
}
if ($task->getID()) {
$button_name = 'Save Task';
$header_name = 'Edit Task';
} else if ($parent_task) {
$cancel_uri = '/T'.$parent_task->getID();
$button_name = 'Create Task';
$header_name = 'Create New Subtask';
} else {
$button_name = 'Create Task';
$header_name = 'Create New Task';
}
require_celerity_resource('maniphest-task-edit-css');
$project_tokenizer_id = celerity_generate_unique_node_id();
$form = new AphrontFormView();
$form
->setUser($user)
->setAction($request->getRequestURI()->getPath())
->addHiddenInput('template', $template_id);
if ($parent_task) {
$form
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Parent Task')
->setValue($handles[$parent_task->getPHID()]->getFullName()))
->addHiddenInput('parent', $parent_task->getID());
}
$form
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Title')
->setName('title')
->setError($e_title)
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT)
->setValue($task->getTitle()));
if ($task->getID()) {
// Only show this in "edit" mode, not "create" mode, since creating a
// non-open task is kind of silly and it would just clutter up the
// "create" interface.
$form
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Status')
->setName('status')
->setValue($task->getStatus())
->setOptions(ManiphestTaskStatus::getTaskStatusMap()));
}
$form
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel('Assigned To')
->setName('assigned_to')
->setValue($assigned_value)
->setUser($user)
->setDatasource('/typeahead/common/users/')
->setLimit(1))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel('CC')
->setName('cc')
->setValue($cc_value)
->setUser($user)
->setDatasource('/typeahead/common/mailable/'))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Priority')
->setName('priority')
->setOptions($priority_map)
->setValue($task->getPriority()))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel('Projects')
->setName('projects')
->setValue($projects_value)
->setID($project_tokenizer_id)
->setCaption(
javelin_render_tag(
'a',
array(
'href' => '/project/create/',
'mustcapture' => true,
'sigil' => 'project-create',
),
'Create New Project'))
->setDatasource('/typeahead/common/projects/'));
if ($aux_fields) {
foreach ($aux_fields as $aux_field) {
if ($aux_field->isRequired() &&
!$aux_field->getError() &&
!$aux_field->getValue()) {
$aux_field->setError(true);
}
$aux_control = $aux_field->renderControl();
$form->appendChild($aux_control);
}
}
require_celerity_resource('aphront-error-view-css');
Javelin::initBehavior('project-create', array(
'tokenizerID' => $project_tokenizer_id,
));
if ($files) {
$file_display = array();
foreach ($files as $file) {
$file_display[] = phutil_escape_html($file->getName());
}
$file_display = implode('<br />', $file_display);
$form->appendChild(
id(new AphrontFormMarkupControl())
->setLabel('Files')
->setValue($file_display));
foreach ($files as $ii => $file) {
$form->addHiddenInput('files['.$ii.']', $file->getPHID());
}
}
$description_control = new PhabricatorRemarkupControl();
// "Upsell" creating tasks via email in create flows if the instance is
// configured for this awesomeness.
$email_create = PhabricatorEnv::getEnvConfig(
'metamta.maniphest.public-create-email');
if (!$task->getID() && $email_create) {
$email_hint = 'You can also create tasks by sending an email to: '.
'<tt>'.phutil_escape_html($email_create).'</tt>';
$description_control->setCaption($email_hint);
}
$description_control
->setLabel('Description')
->setName('description')
->setID('description-textarea')
->setValue($task->getDescription());
$form
->appendChild($description_control);
if (!$task->getID()) {
$form
->appendChild(
id(new AphrontFormDragAndDropUploadControl())
->setLabel('Attached Files')
->setName('files')
->setActivatedClass('aphront-panel-view-drag-and-drop'));
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($cancel_uri)
->setValue($button_name));
$panel = new AphrontPanelView();
$panel->setWidth(AphrontPanelView::WIDTH_FULL);
$panel->setHeader($header_name);
$panel->appendChild($form);
$description_preview_panel =
'<div class="aphront-panel-preview aphront-panel-preview-full">
<div class="maniphest-description-preview-header">
Description Preview
</div>
<div id="description-preview">
<div class="aphront-panel-preview-loading-text">
Loading preview...
</div>
</div>
</div>';
Javelin::initBehavior(
'maniphest-description-preview',
array(
'preview' => 'description-preview',
'textarea' => 'description-textarea',
'uri' => '/maniphest/task/descriptionpreview/',
));
if ($task->getID()) {
$page_objects = array( $task->getPHID() );
} else {
$page_objects = array();
}
return $this->buildStandardPageResponse(
array(
$error_view,
$panel,
$description_preview_panel
),
array(
'title' => $header_name,
'pageObjects' => $page_objects,
));
}
}
diff --git a/src/applications/maniphest/controller/ManiphestTaskListController.php b/src/applications/maniphest/controller/ManiphestTaskListController.php
index f7bb9e8e61..9120dc7dad 100644
--- a/src/applications/maniphest/controller/ManiphestTaskListController.php
+++ b/src/applications/maniphest/controller/ManiphestTaskListController.php
@@ -1,933 +1,917 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestTaskListController extends ManiphestController {
const DEFAULT_PAGE_SIZE = 1000;
private $view;
public function willProcessRequest(array $data) {
$this->view = idx($data, 'view');
}
private function getArrToStrList($key) {
$arr = $this->getRequest()->getArr($key);
$arr = implode(',', $arr);
return nonempty($arr, null);
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
if ($request->isFormPost()) {
// Redirect to GET so URIs can be copy/pasted.
$task_ids = $request->getStr('set_tasks');
$task_ids = nonempty($task_ids, null);
$search_text = $request->getStr('set_search');
$search_text = nonempty($search_text, null);
$min_priority = $request->getInt('set_lpriority');
$min_priority = nonempty($min_priority, null);
$max_priority = $request->getInt('set_hpriority');
$max_priority = nonempty($max_priority, null);
$uri = $request->getRequestURI()
->alter('users', $this->getArrToStrList('set_users'))
->alter('projects', $this->getArrToStrList('set_projects'))
->alter('aprojects', $this->getArrToStrList('set_aprojects'))
->alter('xprojects', $this->getArrToStrList('set_xprojects'))
->alter('owners', $this->getArrToStrList('set_owners'))
->alter('authors', $this->getArrToStrList('set_authors'))
->alter('lpriority', $min_priority)
->alter('hpriority', $max_priority)
->alter('tasks', $task_ids)
->alter('search', $search_text);
return id(new AphrontRedirectResponse())->setURI($uri);
}
$nav = $this->buildBaseSideNav();
$has_filter = array(
'action' => true,
'created' => true,
'subscribed' => true,
'triage' => true,
'projecttriage' => true,
'projectall' => true,
);
$query = null;
$key = $request->getStr('key');
if (!$key && !$this->view) {
if ($this->getDefaultQuery()) {
$key = $this->getDefaultQuery()->getQueryKey();
}
}
if ($key) {
$query = id(new PhabricatorSearchQuery())->loadOneWhere(
'queryKey = %s',
$key);
}
// If the user is running a saved query, load query parameters from that
// query. Otherwise, build a new query object from the HTTP request.
if ($query) {
$nav->selectFilter('Q:'.$query->getQueryKey(), 'custom');
$this->view = 'custom';
} else {
$this->view = $nav->selectFilter($this->view, 'action');
$query = $this->buildQueryFromRequest();
}
// Execute the query.
list($tasks, $handles, $total_count) = self::loadTasks($query);
// Extract information we need to render the filters from the query.
$search_text = $query->getParameter('fullTextSearch');
$user_phids = $query->getParameter('userPHIDs', array());
$task_ids = $query->getParameter('taskIDs', array());
$owner_phids = $query->getParameter('ownerPHIDs', array());
$author_phids = $query->getParameter('authorPHIDs', array());
$project_phids = $query->getParameter('projectPHIDs', array());
$any_project_phids = $query->getParameter(
'anyProjectPHIDs',
array());
$exclude_project_phids = $query->getParameter(
'excludeProjectPHIDs',
array());
$low_priority = $query->getParameter('lowPriority');
$high_priority = $query->getParameter('highPriority');
$page_size = $query->getParameter('limit');
$page = $query->getParameter('offset');
$q_status = $query->getParameter('status');
$q_group = $query->getParameter('group');
$q_order = $query->getParameter('order');
$form = id(new AphrontFormView())
->setUser($user)
->setAction(
$request->getRequestURI()
->alter('key', null)
->alter(
$this->getStatusRequestKey(),
$this->getStatusRequestValue($q_status))
->alter(
$this->getOrderRequestKey(),
$this->getOrderRequestValue($q_order))
->alter(
$this->getGroupRequestKey(),
$this->getGroupRequestValue($q_group)));
if (isset($has_filter[$this->view])) {
$tokens = array();
foreach ($user_phids as $phid) {
$tokens[$phid] = $handles[$phid]->getFullName();
}
$form->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/searchowner/')
->setName('set_users')
->setLabel('Users')
->setValue($tokens));
}
if ($this->view == 'custom') {
$form->appendChild(
id(new AphrontFormTextControl())
->setName('set_search')
->setLabel('Search')
->setValue($search_text)
);
$form->appendChild(
id(new AphrontFormTextControl())
->setName('set_tasks')
->setLabel('Task IDs')
->setValue(join(',', $task_ids))
);
$tokens = array();
foreach ($owner_phids as $phid) {
$tokens[$phid] = $handles[$phid]->getFullName();
}
$form->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/searchowner/')
->setName('set_owners')
->setLabel('Owners')
->setValue($tokens));
$tokens = array();
foreach ($author_phids as $phid) {
$tokens[$phid] = $handles[$phid]->getFullName();
}
$form->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/users/')
->setName('set_authors')
->setLabel('Authors')
->setValue($tokens));
}
$tokens = array();
foreach ($project_phids as $phid) {
$tokens[$phid] = $handles[$phid]->getFullName();
}
if ($this->view != 'projectall' && $this->view != 'projecttriage') {
$caption = null;
if ($this->view == 'custom') {
$caption = 'Find tasks in ALL of these projects ("AND" query).';
}
$form->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/searchproject/')
->setName('set_projects')
->setLabel('Projects')
->setCaption($caption)
->setValue($tokens));
}
if ($this->view == 'custom') {
$atokens = array();
foreach ($any_project_phids as $phid) {
$atokens[$phid] = $handles[$phid]->getFullName();
}
$form->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/projects/')
->setName('set_aprojects')
->setLabel('Any Projects')
->setCaption('Find tasks in ANY of these projects ("OR" query).')
->setValue($atokens));
$tokens = array();
foreach ($exclude_project_phids as $phid) {
$tokens[$phid] = $handles[$phid]->getFullName();
}
$form->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/projects/')
->setName('set_xprojects')
->setLabel('Exclude Projects')
->setCaption('Find tasks NOT in any of these projects.')
->setValue($tokens));
$priority = ManiphestTaskPriority::getLowestPriority();
if ($low_priority) {
$priority = $low_priority;
}
$form->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Min Priority')
->setName('set_lpriority')
->setValue($priority)
->setOptions(array_reverse(
ManiphestTaskPriority::getTaskPriorityMap(), true)));
$priority = ManiphestTaskPriority::getHighestPriority();
if ($high_priority) {
$priority = $high_priority;
}
$form->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Max Priority')
->setName('set_hpriority')
->setValue($priority)
->setOptions(ManiphestTaskPriority::getTaskPriorityMap()));
}
$form
->appendChild($this->renderStatusControl($q_status))
->appendChild($this->renderGroupControl($q_group))
->appendChild($this->renderOrderControl($q_order));
$submit = id(new AphrontFormSubmitControl())
->setValue('Filter Tasks');
// Only show "Save..." for novel queries which have some kind of query
// parameters set.
if ($this->view === 'custom'
&& empty($key)
&& $request->getRequestURI()->getQueryParams()) {
$submit->addCancelButton(
'/maniphest/custom/edit/?key='.$query->getQueryKey(),
'Save Custom Query...');
}
$form->appendChild($submit);
$create_uri = new PhutilURI('/maniphest/task/create/');
if ($project_phids) {
// If we have project filters selected, use them as defaults for task
// creation.
$create_uri->setQueryParam('projects', implode(';', $project_phids));
}
$filter = new AphrontListFilterView();
$filter->addButton(
phutil_render_tag(
'a',
array(
'href' => (string)$create_uri,
'class' => 'green button',
),
'Create New Task'));
if (empty($key)) {
$filter->appendChild($form);
}
$nav->appendChild($filter);
$have_tasks = false;
foreach ($tasks as $group => $list) {
if (count($list)) {
$have_tasks = true;
break;
}
}
require_celerity_resource('maniphest-task-summary-css');
$list_container = new AphrontNullView();
$list_container->appendChild('<div class="maniphest-list-container">');
if (!$have_tasks) {
$list_container->appendChild(
'<h1 class="maniphest-task-group-header">'.
'No matching tasks.'.
'</h1>');
} else {
$pager = new AphrontPagerView();
$pager->setURI($request->getRequestURI(), 'offset');
$pager->setPageSize($page_size);
$pager->setOffset($page);
$pager->setCount($total_count);
$cur = ($pager->getOffset() + 1);
$max = min($pager->getOffset() + $page_size, $total_count);
$tot = $total_count;
$cur = number_format($cur);
$max = number_format($max);
$tot = number_format($tot);
$list_container->appendChild(
'<div class="maniphest-total-result-count">'.
"Displaying tasks {$cur} - {$max} of {$tot}.".
'</div>');
$selector = new AphrontNullView();
$group = $query->getParameter('group');
$order = $query->getParameter('order');
$is_draggable =
($group == 'priority') ||
($group == 'none' && $order == 'priority');
$lists = new AphrontNullView();
$lists->appendChild('<div class="maniphest-group-container">');
foreach ($tasks as $group => $list) {
$task_list = new ManiphestTaskListView();
$task_list->setShowBatchControls(true);
if ($is_draggable) {
$task_list->setShowSubpriorityControls(true);
}
$task_list->setUser($user);
$task_list->setTasks($list);
$task_list->setHandles($handles);
$count = number_format(count($list));
$lists->appendChild(
javelin_render_tag(
'h1',
array(
'class' => 'maniphest-task-group-header',
'sigil' => 'task-group',
'meta' => array(
'priority' => head($list)->getPriority(),
),
),
phutil_escape_html($group).' ('.$count.')'));
$lists->appendChild($task_list);
}
$lists->appendChild('</div>');
$selector->appendChild($lists);
$selector->appendChild($this->renderBatchEditor($query));
$form_id = celerity_generate_unique_node_id();
$selector = phabricator_render_form(
$user,
array(
'method' => 'POST',
'action' => '/maniphest/batch/',
'id' => $form_id,
),
$selector->render());
$list_container->appendChild($selector);
$list_container->appendChild($pager);
Javelin::initBehavior(
'maniphest-subpriority-editor',
array(
'root' => $form_id,
'uri' => '/maniphest/subpriority/',
));
}
$list_container->appendChild('</div>');
$nav->appendChild($list_container);
return $this->buildStandardPageResponse(
$nav,
array(
'title' => 'Task List',
));
}
public static function loadTasks(PhabricatorSearchQuery $search_query) {
$any_project = false;
$search_text = $search_query->getParameter('fullTextSearch');
$user_phids = $search_query->getParameter('userPHIDs', array());
$task_ids = $search_query->getParameter('taskIDs', array());
$project_phids = $search_query->getParameter('projectPHIDs', array());
$any_project_phids = $search_query->getParameter(
'anyProjectPHIDs',
array());
$xproject_phids = $search_query->getParameter(
'excludeProjectPHIDs',
array());
$owner_phids = $search_query->getParameter('ownerPHIDs', array());
$author_phids = $search_query->getParameter('authorPHIDs', array());
$low_priority = $search_query->getParameter('lowPriority');
$low_priority = nonempty($low_priority,
ManiphestTaskPriority::getLowestPriority());
$high_priority = $search_query->getParameter('highPriority');
$high_priority = nonempty($high_priority,
ManiphestTaskPriority::getHighestPriority());
$query = new ManiphestTaskQuery();
$query->withTaskIDs($task_ids);
if ($project_phids) {
$query->withAllProjects($project_phids);
}
if ($xproject_phids) {
$query->withoutProjects($xproject_phids);
}
if ($any_project_phids) {
$query->withAnyProjects($any_project_phids);
}
if ($owner_phids) {
$query->withOwners($owner_phids);
}
if ($author_phids) {
$query->withAuthors($author_phids);
}
$status = $search_query->getParameter('status', 'all');
if (!empty($status['open']) && !empty($status['closed'])) {
$query->withStatus(ManiphestTaskQuery::STATUS_ANY);
} else if (!empty($status['open'])) {
$query->withStatus(ManiphestTaskQuery::STATUS_OPEN);
} else {
$query->withStatus(ManiphestTaskQuery::STATUS_CLOSED);
}
switch ($search_query->getParameter('view')) {
case 'action':
$query->withOwners($user_phids);
break;
case 'created':
$query->withAuthors($user_phids);
break;
case 'subscribed':
$query->withSubscribers($user_phids);
break;
case 'triage':
$query->withOwners($user_phids);
$query->withPriority(ManiphestTaskPriority::PRIORITY_TRIAGE);
break;
case 'alltriage':
$query->withPriority(ManiphestTaskPriority::PRIORITY_TRIAGE);
break;
case 'all':
break;
case 'projecttriage':
$query->withPriority(ManiphestTaskPriority::PRIORITY_TRIAGE);
break;
case 'projectall':
break;
case 'custom':
$query->withPrioritiesBetween($low_priority, $high_priority);
break;
}
$query->withFullTextSearch($search_text);
$order_map = array(
'priority' => ManiphestTaskQuery::ORDER_PRIORITY,
'created' => ManiphestTaskQuery::ORDER_CREATED,
'title' => ManiphestTaskQuery::ORDER_TITLE,
);
$query->setOrderBy(
idx(
$order_map,
$search_query->getParameter('order'),
ManiphestTaskQuery::ORDER_MODIFIED));
$group_map = array(
'priority' => ManiphestTaskQuery::GROUP_PRIORITY,
'owner' => ManiphestTaskQuery::GROUP_OWNER,
'status' => ManiphestTaskQuery::GROUP_STATUS,
'project' => ManiphestTaskQuery::GROUP_PROJECT,
);
$query->setGroupBy(
idx(
$group_map,
$search_query->getParameter('group'),
ManiphestTaskQuery::GROUP_NONE));
$query->setCalculateRows(true);
$query->setLimit($search_query->getParameter('limit'));
$query->setOffset($search_query->getParameter('offset'));
$data = $query->execute();
$total_row_count = $query->getRowCount();
$project_group_phids = array();
if ($search_query->getParameter('group') == 'project') {
foreach ($data as $task) {
foreach ($task->getProjectPHIDs() as $phid) {
$project_group_phids[] = $phid;
}
}
}
$handle_phids = mpull($data, 'getOwnerPHID');
$handle_phids = array_merge(
$handle_phids,
$project_phids,
$user_phids,
$xproject_phids,
$owner_phids,
$author_phids,
$project_group_phids,
array_mergev(mpull($data, 'getProjectPHIDs')));
$handles = id(new PhabricatorObjectHandleData($handle_phids))
->loadHandles();
switch ($search_query->getParameter('group')) {
case 'priority':
$data = mgroup($data, 'getPriority');
// If we have invalid priorities, they'll all map to "???". Merge
// arrays to prevent them from overwriting each other.
$out = array();
foreach ($data as $pri => $tasks) {
$out[ManiphestTaskPriority::getTaskPriorityName($pri)][] = $tasks;
}
foreach ($out as $pri => $tasks) {
$out[$pri] = array_mergev($tasks);
}
$data = $out;
break;
case 'status':
$data = mgroup($data, 'getStatus');
$out = array();
foreach ($data as $status => $tasks) {
$out[ManiphestTaskStatus::getTaskStatusFullName($status)] = $tasks;
}
$data = $out;
break;
case 'owner':
$data = mgroup($data, 'getOwnerPHID');
$out = array();
foreach ($data as $phid => $tasks) {
if ($phid) {
$out[$handles[$phid]->getFullName()] = $tasks;
} else {
$out['Unassigned'] = $tasks;
}
}
$data = $out;
ksort($data);
// Move "Unassigned" to the top of the list.
if (isset($data['Unassigned'])) {
$data = array('Unassigned' => $out['Unassigned']) + $out;
}
break;
case 'project':
$grouped = array();
foreach ($query->getGroupByProjectResults() as $project => $tasks) {
foreach ($tasks as $task) {
$group = $project ? $handles[$project]->getName() : 'No Project';
$grouped[$group][$task->getID()] = $task;
}
}
$data = $grouped;
ksort($data);
// Move "No Project" to the end of the list.
if (isset($data['No Project'])) {
$noproject = $data['No Project'];
unset($data['No Project']);
$data += array('No Project' => $noproject);
}
break;
default:
$data = array(
'Tasks' => $data,
);
break;
}
return array($data, $handles, $total_row_count);
}
private function renderBatchEditor(PhabricatorSearchQuery $search_query) {
Javelin::initBehavior(
'maniphest-batch-selector',
array(
'selectAll' => 'batch-select-all',
'selectNone' => 'batch-select-none',
'submit' => 'batch-select-submit',
'status' => 'batch-select-status-cell',
));
$select_all = javelin_render_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'class' => 'grey button',
'id' => 'batch-select-all',
),
'Select All');
$select_none = javelin_render_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'class' => 'grey button',
'id' => 'batch-select-none',
),
'Clear Selection');
$submit = phutil_render_tag(
'button',
array(
'id' => 'batch-select-submit',
'disabled' => 'disabled',
'class' => 'disabled',
),
'Batch Edit Selected Tasks &raquo;');
$export = javelin_render_tag(
'a',
array(
'href' => '/maniphest/export/'.$search_query->getQueryKey().'/',
'class' => 'grey button',
),
'Export Tasks to Excel...');
return
'<div class="maniphest-batch-editor">'.
'<div class="batch-editor-header">Batch Task Editor</div>'.
'<table class="maniphest-batch-editor-layout">'.
'<tr>'.
'<td>'.
$select_all.
$select_none.
'</td>'.
'<td>'.
$export.
'</td>'.
'<td id="batch-select-status-cell">'.
'0 Selected Tasks'.
'</td>'.
'<td class="batch-select-submit-cell">'.$submit.'</td>'.
'</tr>'.
'</table>'.
'</table>';
}
private function buildQueryFromRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$status = $this->getStatusValueFromRequest();
$group = $this->getGroupValueFromRequest();
$order = $this->getOrderValueFromRequest();
$user_phids = $request->getStrList(
'users',
array($user->getPHID()));
if ($this->view == 'projecttriage' || $this->view == 'projectall') {
$projects = id(new PhabricatorProjectQuery())
->setViewer($user)
->withMemberPHIDs($user_phids)
->execute();
$any_project_phids = mpull($projects, 'getPHID');
} else {
$any_project_phids = $request->getStrList('aprojects');
}
$project_phids = $request->getStrList('projects');
$exclude_project_phids = $request->getStrList('xprojects');
$task_ids = $request->getStrList('tasks');
if ($task_ids) {
// We only need the integer portion of each task ID, so get rid of any
// non-numeric elements
$numeric_task_ids = array();
foreach ($task_ids as $task_id) {
$task_id = preg_replace('/[a-zA-Z]+/', '', $task_id);
if (!empty($task_id)) {
$numeric_task_ids[] = $task_id;
}
}
if (empty($numeric_task_ids)) {
$numeric_task_ids = array(null);
}
$task_ids = $numeric_task_ids;
}
$owner_phids = $request->getStrList('owners');
$author_phids = $request->getStrList('authors');
$search_string = $request->getStr('search');
$low_priority = $request->getInt('lpriority');
$high_priority = $request->getInt('hpriority');
$page = $request->getInt('offset');
$page_size = self::DEFAULT_PAGE_SIZE;
$query = new PhabricatorSearchQuery();
$query->setQuery('<<maniphest>>');
$query->setParameters(
array(
'fullTextSearch' => $search_string,
'view' => $this->view,
'userPHIDs' => $user_phids,
'projectPHIDs' => $project_phids,
'anyProjectPHIDs' => $any_project_phids,
'excludeProjectPHIDs' => $exclude_project_phids,
'ownerPHIDs' => $owner_phids,
'authorPHIDs' => $author_phids,
'taskIDs' => $task_ids,
'lowPriority' => $low_priority,
'highPriority' => $high_priority,
'group' => $group,
'order' => $order,
'offset' => $page,
'limit' => $page_size,
'status' => $status,
));
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$query->save();
unset($unguarded);
return $query;
}
/* -( Toggle Button Controls )---------------------------------------------
These are a giant mess since we have several different values: the request
key (GET param used in requests), the request value (short names used in
requests to keep URIs readable), and the query value (complex value stored in
the query).
*/
private function getStatusValueFromRequest() {
$map = $this->getStatusMap();
$val = $this->getRequest()->getStr($this->getStatusRequestKey());
return idx($map, $val, head($map));
}
private function getGroupValueFromRequest() {
$map = $this->getGroupMap();
$val = $this->getRequest()->getStr($this->getGroupRequestKey());
return idx($map, $val, head($map));
}
private function getOrderValueFromRequest() {
$map = $this->getOrderMap();
$val = $this->getRequest()->getStr($this->getOrderRequestKey());
return idx($map, $val, head($map));
}
private function getStatusRequestKey() {
return 's';
}
private function getGroupRequestKey() {
return 'g';
}
private function getOrderRequestKey() {
return 'o';
}
private function getStatusRequestValue($value) {
return array_search($value, $this->getStatusMap());
}
private function getGroupRequestValue($value) {
return array_search($value, $this->getGroupMap());
}
private function getOrderRequestValue($value) {
return array_search($value, $this->getOrderMap());
}
private function getStatusMap() {
return array(
'o' => array(
'open' => true,
),
'c' => array(
'closed' => true,
),
'oc' => array(
'open' => true,
'closed' => true,
),
);
}
private function getGroupMap() {
return array(
'p' => 'priority',
'o' => 'owner',
's' => 'status',
'j' => 'project',
'n' => 'none',
);
}
private function getOrderMap() {
return array(
'p' => 'priority',
'u' => 'updated',
'c' => 'created',
't' => 'title',
);
}
private function getStatusButtonMap() {
return array(
'o' => 'Open',
'c' => 'Closed',
'oc' => 'All',
);
}
private function getGroupButtonMap() {
return array(
'p' => 'Priority',
'o' => 'Owner',
's' => 'Status',
'j' => 'Project',
'n' => 'None',
);
}
private function getOrderButtonMap() {
return array(
'p' => 'Priority',
'u' => 'Updated',
'c' => 'Created',
't' => 'Title',
);
}
public function renderStatusControl($value) {
$request = $this->getRequest();
return id(new AphrontFormToggleButtonsControl())
->setLabel('Status')
->setValue($this->getStatusRequestValue($value))
->setBaseURI($request->getRequestURI(), $this->getStatusRequestKey())
->setButtons($this->getStatusButtonMap());
}
public function renderOrderControl($value) {
$request = $this->getRequest();
return id(new AphrontFormToggleButtonsControl())
->setLabel('Order')
->setValue($this->getOrderRequestValue($value))
->setBaseURI($request->getRequestURI(), $this->getOrderRequestKey())
->setButtons($this->getOrderButtonMap());
}
public function renderGroupControl($value) {
$request = $this->getRequest();
return id(new AphrontFormToggleButtonsControl())
->setLabel('Group')
->setValue($this->getGroupRequestValue($value))
->setBaseURI($request->getRequestURI(), $this->getGroupRequestKey())
->setButtons($this->getGroupButtonMap());
}
}
diff --git a/src/applications/maniphest/controller/ManiphestTransactionPreviewController.php b/src/applications/maniphest/controller/ManiphestTransactionPreviewController.php
index e8e26de6af..0263be595c 100644
--- a/src/applications/maniphest/controller/ManiphestTransactionPreviewController.php
+++ b/src/applications/maniphest/controller/ManiphestTransactionPreviewController.php
@@ -1,130 +1,114 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestTransactionPreviewController extends ManiphestController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$comments = $request->getStr('comments');
$task = id(new ManiphestTask())->load($this->id);
if (!$task) {
return new Aphront404Response();
}
id(new PhabricatorDraft())
->setAuthorPHID($user->getPHID())
->setDraftKey($task->getPHID())
->setDraft($comments)
->replaceOrDelete();
$action = $request->getStr('action');
$transaction = new ManiphestTransaction();
$transaction->setAuthorPHID($user->getPHID());
$transaction->setComments($comments);
$transaction->setTransactionType($action);
$value = $request->getStr('value');
// grab phids for handles and set transaction values based on action and
// value (empty or control-specific format) coming in from the wire
switch ($action) {
case ManiphestTransactionType::TYPE_PRIORITY:
$transaction->setOldValue($task->getPriority());
$transaction->setNewValue($value);
break;
case ManiphestTransactionType::TYPE_OWNER:
if ($value) {
$value = current(json_decode($value));
$phids = array($value);
} else {
$phids = array();
}
$transaction->setNewValue($value);
break;
case ManiphestTransactionType::TYPE_CCS:
if ($value) {
$value = json_decode($value);
$phids = $value;
foreach ($task->getCCPHIDs() as $cc_phid) {
$phids[] = $cc_phid;
$value[] = $cc_phid;
}
$transaction->setNewValue($value);
} else {
$phids = array();
$transaction->setNewValue(array());
}
$transaction->setOldValue($task->getCCPHIDs());
break;
case ManiphestTransactionType::TYPE_PROJECTS:
if ($value) {
$value = json_decode($value);
$phids = $value;
foreach ($task->getProjectPHIDs() as $project_phid) {
$phids[] = $project_phid;
$value[] = $project_phid;
}
$transaction->setNewValue($value);
} else {
$phids = array();
$transaction->setNewValue(array());
}
$transaction->setOldValue($task->getProjectPHIDs());
break;
default:
$phids = array();
$transaction->setNewValue($value);
break;
}
$phids[] = $user->getPHID();
$handles = $this->loadViewerHandles($phids);
$transactions = array();
$transactions[] = $transaction;
$engine = new PhabricatorMarkupEngine();
$engine->setViewer($user);
$engine->addObject($transaction, ManiphestTransaction::MARKUP_FIELD_BODY);
$engine->process();
$transaction_view = new ManiphestTransactionListView();
$transaction_view->setTransactions($transactions);
$transaction_view->setHandles($handles);
$transaction_view->setUser($user);
$transaction_view->setMarkupEngine($engine);
$transaction_view->setPreview(true);
return id(new AphrontAjaxResponse())
->setContent($transaction_view->render());
}
}
diff --git a/src/applications/maniphest/controller/ManiphestTransactionSaveController.php b/src/applications/maniphest/controller/ManiphestTransactionSaveController.php
index 18b98c21f4..934f182301 100644
--- a/src/applications/maniphest/controller/ManiphestTransactionSaveController.php
+++ b/src/applications/maniphest/controller/ManiphestTransactionSaveController.php
@@ -1,262 +1,246 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestTransactionSaveController extends ManiphestController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$task = id(new ManiphestTask())->load($request->getStr('taskID'));
if (!$task) {
return new Aphront404Response();
}
$transactions = array();
$action = $request->getStr('action');
// If we have drag-and-dropped files, attach them first in a separate
// transaction. These can come in on any transaction type, which is why we
// handle them separately.
$files = array();
// Look for drag-and-drop uploads first.
$file_phids = $request->getArr('files');
if ($file_phids) {
$files = id(new PhabricatorFile())->loadAllWhere(
'phid in (%Ls)',
$file_phids);
}
// This means "attach a file" even though we store other types of data
// as 'attached'.
if ($action == ManiphestTransactionType::TYPE_ATTACH) {
if (!empty($_FILES['file'])) {
$err = idx($_FILES['file'], 'error');
if ($err != UPLOAD_ERR_NO_FILE) {
$file = PhabricatorFile::newFromPHPUpload(
$_FILES['file'],
array(
'authorPHID' => $user->getPHID(),
));
$files[] = $file;
}
}
}
// If we had explicit or drag-and-drop files, create a transaction
// for those before we deal with whatever else might have happened.
$file_transaction = null;
if ($files) {
$files = mpull($files, 'getPHID', 'getPHID');
$new = $task->getAttached();
foreach ($files as $phid) {
if (empty($new[PhabricatorPHIDConstants::PHID_TYPE_FILE])) {
$new[PhabricatorPHIDConstants::PHID_TYPE_FILE] = array();
}
$new[PhabricatorPHIDConstants::PHID_TYPE_FILE][$phid] = array();
}
$transaction = new ManiphestTransaction();
$transaction
->setAuthorPHID($user->getPHID())
->setTransactionType(ManiphestTransactionType::TYPE_ATTACH);
$transaction->setNewValue($new);
$transactions[] = $transaction;
$file_transaction = $transaction;
}
// Compute new CCs added by @mentions. Several things can cause CCs to
// be added as side effects: mentions, explicit CCs, users who aren't
// CC'd interacting with the task, and ownership changes. We build up a
// list of all the CCs and then construct a transaction for them at the
// end if necessary.
$added_ccs = PhabricatorMarkupEngine::extractPHIDsFromMentions(
array(
$request->getStr('comments'),
));
$cc_transaction = new ManiphestTransaction();
$cc_transaction
->setAuthorPHID($user->getPHID())
->setTransactionType(ManiphestTransactionType::TYPE_CCS);
$force_cc_transaction = false;
$transaction = new ManiphestTransaction();
$transaction
->setAuthorPHID($user->getPHID())
->setComments($request->getStr('comments'))
->setTransactionType($action);
switch ($action) {
case ManiphestTransactionType::TYPE_STATUS:
$transaction->setNewValue($request->getStr('resolution'));
break;
case ManiphestTransactionType::TYPE_OWNER:
$assign_to = $request->getArr('assign_to');
$assign_to = reset($assign_to);
$transaction->setNewValue($assign_to);
break;
case ManiphestTransactionType::TYPE_PROJECTS:
$projects = $request->getArr('projects');
$projects = array_merge($projects, $task->getProjectPHIDs());
$projects = array_filter($projects);
$projects = array_unique($projects);
$transaction->setNewValue($projects);
break;
case ManiphestTransactionType::TYPE_CCS:
// Accumulate the new explicit CCs into the array that we'll add in
// the CC transaction later.
$added_ccs = array_merge($added_ccs, $request->getArr('ccs'));
// Transfer any comments over to the CC transaction.
$cc_transaction->setComments($transaction->getComments());
// Make sure we include this transaction, even if the user didn't
// actually add any CC's, because we'll discard their comment otherwise.
$force_cc_transaction = true;
// Throw away the primary transaction.
$transaction = null;
break;
case ManiphestTransactionType::TYPE_PRIORITY:
$transaction->setNewValue($request->getInt('priority'));
break;
case ManiphestTransactionType::TYPE_NONE:
case ManiphestTransactionType::TYPE_ATTACH:
// If we have a file transaction, just get rid of this secondary
// transaction and put the comments on it instead.
if ($file_transaction) {
$file_transaction->setComments($transaction->getComments());
$transaction = null;
}
break;
default:
throw new Exception('unknown action');
}
if ($transaction) {
$transactions[] = $transaction;
}
// When you interact with a task, we add you to the CC list so you get
// further updates, and possibly assign the task to you if you took an
// ownership action (closing it) but it's currently unowned. We also move
// previous owners to CC if ownership changes. Detect all these conditions
// and create side-effect transactions for them.
$implicitly_claimed = false;
switch ($action) {
case ManiphestTransactionType::TYPE_OWNER:
if ($task->getOwnerPHID() == $transaction->getNewValue()) {
// If this is actually no-op, don't generate the side effect.
break;
}
// Otherwise, when a task is reassigned, move the previous owner to CC.
$added_ccs[] = $task->getOwnerPHID();
break;
case ManiphestTransactionType::TYPE_STATUS:
if (!$task->getOwnerPHID() &&
$request->getStr('resolution') !=
ManiphestTaskStatus::STATUS_OPEN) {
// Closing an unassigned task. Assign the user as the owner of
// this task.
$assign = new ManiphestTransaction();
$assign->setAuthorPHID($user->getPHID());
$assign->setTransactionType(ManiphestTransactionType::TYPE_OWNER);
$assign->setNewValue($user->getPHID());
$transactions[] = $assign;
$implicitly_claimed = true;
}
break;
}
$user_owns_task = false;
if ($implicitly_claimed) {
$user_owns_task = true;
} else {
if ($action == ManiphestTransactionType::TYPE_OWNER) {
if ($transaction->getNewValue() == $user->getPHID()) {
$user_owns_task = true;
}
} else if ($task->getOwnerPHID() == $user->getPHID()) {
$user_owns_task = true;
}
}
if (!$user_owns_task) {
// If we aren't making the user the new task owner and they aren't the
// existing task owner, add them to CC.
$added_ccs[] = $user->getPHID();
}
if ($added_ccs || $force_cc_transaction) {
// We've added CCs, so include a CC transaction. It's safe to do this even
// if we're just "adding" CCs which already exist, because the
// ManiphestTransactionEditor is smart enough to ignore them.
$all_ccs = array_merge($task->getCCPHIDs(), $added_ccs);
$cc_transaction->setNewValue($all_ccs);
$transactions[] = $cc_transaction;
}
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_WEB,
array(
'ip' => $request->getRemoteAddr(),
));
foreach ($transactions as $transaction) {
$transaction->setContentSource($content_source);
}
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK,
array(
'task' => $task,
'new' => false,
'transactions' => $transactions,
));
$event->setUser($user);
$event->setAphrontRequest($request);
PhutilEventEngine::dispatchEvent($event);
$task = $event->getValue('task');
$transactions = $event->getValue('transactions');
$editor = new ManiphestTransactionEditor();
$editor->setActor($user);
$editor->applyTransactions($task, $transactions);
$draft = id(new PhabricatorDraft())->loadOneWhere(
'authorPHID = %s AND draftKey = %s',
$user->getPHID(),
$task->getPHID());
if ($draft) {
$draft->delete();
}
return id(new AphrontRedirectResponse())
->setURI('/T'.$task->getID());
}
}
diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
index 21e9f5836d..014b348c24 100644
--- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php
+++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
@@ -1,468 +1,452 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestTransactionEditor extends PhabricatorEditor {
private $parentMessageID;
private $excludePHIDs = array();
private $auxiliaryFields = array();
public function setAuxiliaryFields(array $fields) {
assert_instances_of($fields, 'ManiphestAuxiliaryFieldSpecification');
$this->auxiliaryFields = $fields;
return $this;
}
public function setParentMessageID($parent_message_id) {
$this->parentMessageID = $parent_message_id;
return $this;
}
public function setExcludePHIDs(array $exclude) {
$this->excludePHIDs = $exclude;
return $this;
}
public function getExcludePHIDs() {
return $this->excludePHIDs;
}
public function applyTransactions(ManiphestTask $task, array $transactions) {
assert_instances_of($transactions, 'ManiphestTransaction');
$email_cc = $task->getCCPHIDs();
$email_to = array();
$email_to[] = $task->getOwnerPHID();
$pri_changed = $this->isCreate($transactions);
foreach ($transactions as $key => $transaction) {
$type = $transaction->getTransactionType();
$new = $transaction->getNewValue();
$email_to[] = $transaction->getAuthorPHID();
$value_is_phid_set = false;
switch ($type) {
case ManiphestTransactionType::TYPE_NONE:
$old = null;
break;
case ManiphestTransactionType::TYPE_STATUS:
$old = $task->getStatus();
break;
case ManiphestTransactionType::TYPE_OWNER:
$old = $task->getOwnerPHID();
break;
case ManiphestTransactionType::TYPE_CCS:
$old = $task->getCCPHIDs();
$value_is_phid_set = true;
break;
case ManiphestTransactionType::TYPE_PRIORITY:
$old = $task->getPriority();
break;
case ManiphestTransactionType::TYPE_EDGE:
$old = $transaction->getOldValue();
break;
case ManiphestTransactionType::TYPE_ATTACH:
$old = $task->getAttached();
break;
case ManiphestTransactionType::TYPE_TITLE:
$old = $task->getTitle();
break;
case ManiphestTransactionType::TYPE_DESCRIPTION:
$old = $task->getDescription();
break;
case ManiphestTransactionType::TYPE_PROJECTS:
$old = $task->getProjectPHIDs();
$value_is_phid_set = true;
break;
case ManiphestTransactionType::TYPE_AUXILIARY:
$aux_key = $transaction->getMetadataValue('aux:key');
if (!$aux_key) {
throw new Exception(
"Expected 'aux:key' metadata on TYPE_AUXILIARY transaction.");
}
$old = $task->getAuxiliaryAttribute($aux_key);
break;
default:
throw new Exception('Unknown action type.');
}
$old_cmp = $old;
$new_cmp = $new;
if ($value_is_phid_set) {
// Normalize the old and new values if they are PHID sets so we don't
// get any no-op transactions where the values differ only by keys,
// order, duplicates, etc.
if (is_array($old)) {
$old = array_filter($old);
$old = array_unique($old);
sort($old);
$old = array_values($old);
$old_cmp = $old;
}
if (is_array($new)) {
$new = array_filter($new);
$new = array_unique($new);
$transaction->setNewValue($new);
$new_cmp = $new;
sort($new_cmp);
$new_cmp = array_values($new_cmp);
}
}
if (($old !== null) && ($old_cmp == $new_cmp)) {
if (count($transactions) > 1 && !$transaction->hasComments()) {
// If we have at least one other transaction and this one isn't
// doing anything and doesn't have any comments, just throw it
// away.
unset($transactions[$key]);
continue;
} else {
$transaction->setOldValue(null);
$transaction->setNewValue(null);
$transaction->setTransactionType(ManiphestTransactionType::TYPE_NONE);
}
} else {
switch ($type) {
case ManiphestTransactionType::TYPE_NONE:
break;
case ManiphestTransactionType::TYPE_STATUS:
$task->setStatus($new);
break;
case ManiphestTransactionType::TYPE_OWNER:
if ($new) {
$handles = id(new PhabricatorObjectHandleData(array($new)))
->loadHandles();
$task->setOwnerOrdering($handles[$new]->getName());
} else {
$task->setOwnerOrdering(null);
}
$task->setOwnerPHID($new);
break;
case ManiphestTransactionType::TYPE_CCS:
$task->setCCPHIDs($new);
break;
case ManiphestTransactionType::TYPE_PRIORITY:
$task->setPriority($new);
$pri_changed = true;
break;
case ManiphestTransactionType::TYPE_ATTACH:
$task->setAttached($new);
break;
case ManiphestTransactionType::TYPE_TITLE:
$task->setTitle($new);
break;
case ManiphestTransactionType::TYPE_DESCRIPTION:
$task->setDescription($new);
break;
case ManiphestTransactionType::TYPE_PROJECTS:
$task->setProjectPHIDs($new);
break;
case ManiphestTransactionType::TYPE_AUXILIARY:
$aux_key = $transaction->getMetadataValue('aux:key');
$task->setAuxiliaryAttribute($aux_key, $new);
break;
case ManiphestTransactionType::TYPE_EDGE:
// Edge edits are accomplished through PhabricatorEdgeEditor, which
// has authority.
break;
default:
throw new Exception('Unknown action type.');
}
$transaction->setOldValue($old);
$transaction->setNewValue($new);
}
}
if ($pri_changed) {
$subpriority = ManiphestTransactionEditor::getNextSubpriority(
$task->getPriority(),
null);
$task->setSubpriority($subpriority);
}
$task->save();
foreach ($transactions as $transaction) {
$transaction->setTaskID($task->getID());
$transaction->save();
}
$email_to[] = $task->getOwnerPHID();
$email_cc = array_merge(
$email_cc,
$task->getCCPHIDs());
$mail = $this->sendEmail($task, $transactions, $email_to, $email_cc);
$this->publishFeedStory(
$task,
$transactions,
$mail->buildRecipientList());
// TODO: Do this offline via workers
PhabricatorSearchManiphestIndexer::indexTask($task);
}
protected function getSubjectPrefix() {
return PhabricatorEnv::getEnvConfig('metamta.maniphest.subject-prefix');
}
private function sendEmail($task, $transactions, $email_to, $email_cc) {
$exclude = $this->getExcludePHIDs();
$email_to = array_filter(array_unique($email_to));
$email_cc = array_filter(array_unique($email_cc));
$phids = array();
foreach ($transactions as $transaction) {
foreach ($transaction->extractPHIDs() as $phid) {
$phids[$phid] = true;
}
}
foreach ($email_to as $phid) {
$phids[$phid] = true;
}
foreach ($email_cc as $phid) {
$phids[$phid] = true;
}
$phids = array_keys($phids);
$handles = id(new PhabricatorObjectHandleData($phids))
->loadHandles();
$view = new ManiphestTransactionDetailView();
$view->setTransactionGroup($transactions);
$view->setHandles($handles);
$view->setAuxiliaryFields($this->auxiliaryFields);
list($action, $main_body) = $view->renderForEmail($with_date = false);
$is_create = $this->isCreate($transactions);
$task_uri = PhabricatorEnv::getProductionURI('/T'.$task->getID());
$reply_handler = $this->buildReplyHandler($task);
$body = new PhabricatorMetaMTAMailBody();
$body->addRawSection($main_body);
if ($is_create) {
$body->addTextSection(pht('TASK DESCRIPTION'), $task->getDescription());
}
$body->addTextSection(pht('TASK DETAIL'), $task_uri);
$body->addReplySection($reply_handler->getReplyHandlerInstructions());
$thread_id = 'maniphest-task-'.$task->getPHID();
$task_id = $task->getID();
$title = $task->getTitle();
$mailtags = $this->getMailTags($transactions);
$template = id(new PhabricatorMetaMTAMail())
->setSubject("T{$task_id}: {$title}")
->setSubjectPrefix($this->getSubjectPrefix())
->setVarySubjectPrefix("[{$action}]")
->setFrom($transaction->getAuthorPHID())
->setParentMessageID($this->parentMessageID)
->addHeader('Thread-Topic', "T{$task_id}: ".$task->getOriginalTitle())
->setThreadID($thread_id, $is_create)
->setRelatedPHID($task->getPHID())
->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs())
->setIsBulk(true)
->setMailTags($mailtags)
->setBody($body->render());
$mails = $reply_handler->multiplexMail(
$template,
array_select_keys($handles, $email_to),
array_select_keys($handles, $email_cc));
foreach ($mails as $mail) {
$mail->saveAndSend();
}
$template->addTos($email_to);
$template->addCCs($email_cc);
return $template;
}
public function buildReplyHandler(ManiphestTask $task) {
$handler_object = PhabricatorEnv::newObjectFromConfig(
'metamta.maniphest.reply-handler');
$handler_object->setMailReceiver($task);
return $handler_object;
}
private function publishFeedStory(
ManiphestTask $task,
array $transactions,
array $mailed_phids) {
assert_instances_of($transactions, 'ManiphestTransaction');
$actions = array(ManiphestAction::ACTION_UPDATE);
$comments = null;
foreach ($transactions as $transaction) {
if ($transaction->hasComments()) {
$comments = $transaction->getComments();
}
$type = $transaction->getTransactionType();
switch ($type) {
case ManiphestTransactionType::TYPE_OWNER:
$actions[] = ManiphestAction::ACTION_ASSIGN;
break;
case ManiphestTransactionType::TYPE_STATUS:
if ($task->getStatus() != ManiphestTaskStatus::STATUS_OPEN) {
$actions[] = ManiphestAction::ACTION_CLOSE;
} else if ($this->isCreate($transactions)) {
$actions[] = ManiphestAction::ACTION_CREATE;
} else {
$actions[] = ManiphestAction::ACTION_REOPEN;
}
break;
default:
$actions[] = $type;
break;
}
}
$action_type = ManiphestAction::selectStrongestAction($actions);
$owner_phid = $task->getOwnerPHID();
$actor_phid = head($transactions)->getAuthorPHID();
$author_phid = $task->getAuthorPHID();
id(new PhabricatorFeedStoryPublisher())
->setStoryType('PhabricatorFeedStoryManiphest')
->setStoryData(array(
'taskPHID' => $task->getPHID(),
'transactionIDs' => mpull($transactions, 'getID'),
'ownerPHID' => $owner_phid,
'action' => $action_type,
'comments' => $comments,
'description' => $task->getDescription(),
))
->setStoryTime(time())
->setStoryAuthorPHID($actor_phid)
->setRelatedPHIDs(
array_merge(
array_filter(
array(
$task->getPHID(),
$author_phid,
$actor_phid,
$owner_phid,
)),
$task->getProjectPHIDs()))
->setPrimaryObjectPHID($task->getPHID())
->setSubscribedPHIDs(
array_merge(
array_filter(
array(
$author_phid,
$owner_phid,
$actor_phid)),
$task->getCCPHIDs()))
->setMailRecipientPHIDs($mailed_phids)
->publish();
}
private function isCreate(array $transactions) {
assert_instances_of($transactions, 'ManiphestTransaction');
$is_create = false;
foreach ($transactions as $transaction) {
$type = $transaction->getTransactionType();
if (($type == ManiphestTransactionType::TYPE_STATUS) &&
($transaction->getOldValue() === null) &&
($transaction->getNewValue() == ManiphestTaskStatus::STATUS_OPEN)) {
$is_create = true;
}
}
return $is_create;
}
private function getMailTags(array $transactions) {
assert_instances_of($transactions, 'ManiphestTransaction');
$tags = array();
foreach ($transactions as $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransactionType::TYPE_STATUS:
$tags[] = MetaMTANotificationType::TYPE_MANIPHEST_STATUS;
break;
case ManiphestTransactionType::TYPE_OWNER:
$tags[] = MetaMTANotificationType::TYPE_MANIPHEST_OWNER;
break;
case ManiphestTransactionType::TYPE_CCS:
$tags[] = MetaMTANotificationType::TYPE_MANIPHEST_CC;
break;
case ManiphestTransactionType::TYPE_PROJECTS:
$tags[] = MetaMTANotificationType::TYPE_MANIPHEST_PROJECTS;
break;
case ManiphestTransactionType::TYPE_PRIORITY:
$tags[] = MetaMTANotificationType::TYPE_MANIPHEST_PRIORITY;
break;
case ManiphestTransactionType::TYPE_NONE:
// this is a comment which we will check separately below for
// content
break;
default:
$tags[] = MetaMTANotificationType::TYPE_MANIPHEST_OTHER;
break;
}
if ($xaction->hasComments()) {
$tags[] = MetaMTANotificationType::TYPE_MANIPHEST_COMMENT;
}
}
return array_unique($tags);
}
public static function getNextSubpriority($pri, $sub) {
if ($sub === null) {
$next = id(new ManiphestTask())->loadOneWhere(
'priority = %d ORDER BY subpriority ASC LIMIT 1',
$pri);
if ($next) {
return $next->getSubpriority() - ((double)(2 << 16));
}
} else {
$next = id(new ManiphestTask())->loadOneWhere(
'priority = %d AND subpriority > %s ORDER BY subpriority ASC LIMIT 1',
$pri,
$sub);
if ($next) {
return ($sub + $next->getSubpriority()) / 2;
}
}
return (double)(2 << 32);
}
}
diff --git a/src/applications/maniphest/event/ManiphestEdgeEventListener.php b/src/applications/maniphest/event/ManiphestEdgeEventListener.php
index 73f0c56711..32a73210ce 100644
--- a/src/applications/maniphest/event/ManiphestEdgeEventListener.php
+++ b/src/applications/maniphest/event/ManiphestEdgeEventListener.php
@@ -1,146 +1,130 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Listener for Maniphest Task edge events. When some workflow causes task
* edges to be added or removed, we consider the edge edit authoritative but
* duplicate the information into a ManiphestTansaction for display.
*
* @group maniphest
*/
final class ManiphestEdgeEventListener extends PhutilEventListener {
private $edges = array();
private $tasks = array();
public function register() {
$this->listen(PhabricatorEventType::TYPE_EDGE_WILLEDITEDGES);
$this->listen(PhabricatorEventType::TYPE_EDGE_DIDEDITEDGES);
}
public function handleEvent(PhutilEvent $event) {
switch ($event->getType()) {
case PhabricatorEventType::TYPE_EDGE_WILLEDITEDGES:
return $this->handleWillEditEvent($event);
case PhabricatorEventType::TYPE_EDGE_DIDEDITEDGES:
return $this->handleDidEditEvent($event);
}
}
private function handleWillEditEvent(PhutilEvent $event) {
// NOTE: Everything is namespaced by `id` so that we aren't left in an
// inconsistent state if an edit fails to complete (e.g., something throws)
// or an edit happens inside another edit.
$id = $event->getValue('id');
$edges = $this->loadAllEdges($event);
$tasks = array();
if ($edges) {
$tasks = id(new ManiphestTask())->loadAllWhere(
'phid IN (%Ls)',
array_keys($edges));
$tasks = mpull($tasks, null, 'getPHID');
}
$this->edges[$id] = $edges;
$this->tasks[$id] = $tasks;
}
private function handleDidEditEvent(PhutilEvent $event) {
$id = $event->getValue('id');
$old_edges = $this->edges[$id];
$tasks = $this->tasks[$id];
unset($this->edges[$id]);
unset($this->tasks[$id]);
$new_edges = $this->loadAllEdges($event);
$editor = new ManiphestTransactionEditor();
$editor->setActor($event->getUser());
foreach ($tasks as $phid => $task) {
$xactions = array();
$old = $old_edges[$phid];
$new = $new_edges[$phid];
$types = array_keys($old + $new);
foreach ($types as $type) {
$old_type = idx($old, $type, array());
$new_type = idx($new, $type, array());
if ($old_type === $new_type) {
continue;
}
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransactionType::TYPE_EDGE)
->setOldValue($old_type)
->setNewValue($new_type)
->setMetadataValue('edge:type', $type)
->setAuthorPHID($event->getUser()->getPHID());
}
if ($xactions) {
$editor->applyTransactions($task, $xactions);
}
}
}
private function filterEdgesBySourceType(array $edges, $type) {
foreach ($edges as $key => $edge) {
if ($edge['src_type'] !== $type) {
unset($edges[$key]);
}
}
return $edges;
}
private function loadAllEdges(PhutilEvent $event) {
$add_edges = $event->getValue('add');
$rem_edges = $event->getValue('rem');
$type_task = PhabricatorPHIDConstants::PHID_TYPE_TASK;
$all_edges = array_merge($add_edges, $rem_edges);
$all_edges = $this->filterEdgesBySourceType($all_edges, $type_task);
if (!$all_edges) {
return;
}
$all_tasks = array();
$all_types = array();
foreach ($all_edges as $edge) {
$all_tasks[$edge['src']] = true;
$all_types[$edge['type']] = true;
}
$all_tasks = array_keys($all_tasks);
$all_types = array_keys($all_types);
return id(new PhabricatorEdgeQuery())
->withSourcePHIDs($all_tasks)
->withEdgeTypes($all_types)
->needEdgeData(true)
->execute();
}
}
diff --git a/src/applications/maniphest/extensions/ManiphestDefaultTaskExtensions.php b/src/applications/maniphest/extensions/ManiphestDefaultTaskExtensions.php
index d6f099f3bb..98355e4de3 100644
--- a/src/applications/maniphest/extensions/ManiphestDefaultTaskExtensions.php
+++ b/src/applications/maniphest/extensions/ManiphestDefaultTaskExtensions.php
@@ -1,51 +1,35 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestDefaultTaskExtensions
extends ManiphestTaskExtensions {
public function getAuxiliaryFieldSpecifications() {
$fields = PhabricatorEnv::getEnvConfig('maniphest.custom-fields');
$specs = array();
foreach ($fields as $aux => $info) {
$spec = new ManiphestAuxiliaryFieldDefaultSpecification();
$spec->setAuxiliaryKey($aux);
$spec->setLabel(idx($info, 'label'));
$spec->setCaption(idx($info, 'caption'));
$spec->setFieldType(idx($info, 'type'));
$spec->setRequired(idx($info, 'required'));
$spec->setCheckboxLabel(idx($info, 'checkbox-label'));
$spec->setCheckboxValue(idx($info, 'checkbox-value', 1));
if ($spec->getFieldType() ==
ManiphestAuxiliaryFieldDefaultSpecification::TYPE_SELECT) {
$spec->setSelectOptions(idx($info, 'options'));
}
$spec->setShouldCopyWhenCreatingSimilarTask(idx($info, 'copy'));
$specs[] = $spec;
}
return $specs;
}
}
diff --git a/src/applications/maniphest/extensions/ManiphestTaskExtensions.php b/src/applications/maniphest/extensions/ManiphestTaskExtensions.php
index 2b64299727..5d26bb0bd0 100644
--- a/src/applications/maniphest/extensions/ManiphestTaskExtensions.php
+++ b/src/applications/maniphest/extensions/ManiphestTaskExtensions.php
@@ -1,36 +1,20 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
abstract class ManiphestTaskExtensions {
final public function __construct() {
// <empty>
}
abstract public function getAuxiliaryFieldSpecifications();
final public static function newExtensions() {
$key = 'maniphest.custom-task-extensions-class';
return PhabricatorEnv::newObjectFromConfig($key);
}
}
diff --git a/src/applications/maniphest/storage/ManiphestDAO.php b/src/applications/maniphest/storage/ManiphestDAO.php
index ff7319d028..c72524a3d2 100644
--- a/src/applications/maniphest/storage/ManiphestDAO.php
+++ b/src/applications/maniphest/storage/ManiphestDAO.php
@@ -1,28 +1,12 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
abstract class ManiphestDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'maniphest';
}
}
diff --git a/src/applications/maniphest/storage/ManiphestSavedQuery.php b/src/applications/maniphest/storage/ManiphestSavedQuery.php
index dde30a1341..0b08baa664 100644
--- a/src/applications/maniphest/storage/ManiphestSavedQuery.php
+++ b/src/applications/maniphest/storage/ManiphestSavedQuery.php
@@ -1,29 +1,13 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestSavedQuery extends ManiphestDAO {
protected $name;
protected $queryKey;
protected $userPHID;
protected $isDefault;
}
diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php
index 32d9c618b3..58681c7aa4 100644
--- a/src/applications/maniphest/storage/ManiphestTask.php
+++ b/src/applications/maniphest/storage/ManiphestTask.php
@@ -1,280 +1,264 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestTask extends ManiphestDAO
implements PhabricatorMarkupInterface {
const MARKUP_FIELD_DESCRIPTION = 'markup:desc';
protected $phid;
protected $authorPHID;
protected $ownerPHID;
protected $ccPHIDs = array();
protected $status;
protected $priority;
protected $subpriority;
protected $title;
protected $originalTitle;
protected $description;
protected $originalEmailSource;
protected $mailKey;
protected $attached = array();
protected $projectPHIDs = array();
private $projectsNeedUpdate;
private $subscribersNeedUpdate;
protected $ownerOrdering;
private $auxiliaryAttributes;
private $auxiliaryDirty = array();
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'ccPHIDs' => self::SERIALIZATION_JSON,
'attached' => self::SERIALIZATION_JSON,
'projectPHIDs' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function loadDependsOnTaskPHIDs() {
return PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getPHID(),
PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK);
}
public function loadDependedOnByTaskPHIDs() {
return PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getPHID(),
PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK);
}
public function getAttachedPHIDs($type) {
return array_keys(idx($this->attached, $type, array()));
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_TASK);
}
public function getCCPHIDs() {
return array_values(nonempty($this->ccPHIDs, array()));
}
public function setProjectPHIDs(array $phids) {
$this->projectPHIDs = array_values($phids);
$this->projectsNeedUpdate = true;
return $this;
}
public function getProjectPHIDs() {
return array_values(nonempty($this->projectPHIDs, array()));
}
public function setCCPHIDs(array $phids) {
$this->ccPHIDs = array_values($phids);
$this->subscribersNeedUpdate = true;
return $this;
}
public function setOwnerPHID($phid) {
$this->ownerPHID = $phid;
$this->subscribersNeedUpdate = true;
return $this;
}
public function getAuxiliaryAttribute($key, $default = null) {
if ($this->auxiliaryAttributes === null) {
throw new Exception("Attach auxiliary attributes before getting them!");
}
return idx($this->auxiliaryAttributes, $key, $default);
}
public function setAuxiliaryAttribute($key, $val) {
if ($this->auxiliaryAttributes === null) {
throw new Exception("Attach auxiliary attributes before setting them!");
}
$this->auxiliaryAttributes[$key] = $val;
$this->auxiliaryDirty[$key] = true;
return $this;
}
public function setTitle($title) {
$this->title = $title;
if (!$this->getID()) {
$this->originalTitle = $title;
}
return $this;
}
public function attachAuxiliaryAttributes(array $attrs) {
if ($this->auxiliaryDirty) {
throw new Exception(
"This object has dirty attributes, you can not attach new attributes ".
"without writing or discarding the dirty attributes.");
}
$this->auxiliaryAttributes = $attrs;
return $this;
}
public function loadAndAttachAuxiliaryAttributes() {
if (!$this->getPHID()) {
$this->auxiliaryAttributes = array();
return $this;
}
$storage = id(new ManiphestTaskAuxiliaryStorage())->loadAllWhere(
'taskPHID = %s',
$this->getPHID());
$this->auxiliaryAttributes = mpull($storage, 'getValue', 'getName');
return $this;
}
public function save() {
if (!$this->mailKey) {
$this->mailKey = Filesystem::readRandomCharacters(20);
}
$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;
}
if ($this->auxiliaryDirty) {
$this->writeAuxiliaryUpdates();
$this->auxiliaryDirty = array();
}
return $result;
}
private function writeAuxiliaryUpdates() {
$table = new ManiphestTaskAuxiliaryStorage();
$conn_w = $table->establishConnection('w');
$update = array();
$remove = array();
foreach ($this->auxiliaryDirty as $key => $dirty) {
$value = $this->getAuxiliaryAttribute($key);
if ($value === null) {
$remove[$key] = true;
} else {
$update[$key] = $value;
}
}
if ($remove) {
queryfx(
$conn_w,
'DELETE FROM %T WHERE taskPHID = %s AND name IN (%Ls)',
$table->getTableName(),
$this->getPHID(),
array_keys($remove));
}
if ($update) {
$sql = array();
foreach ($update as $key => $val) {
$sql[] = qsprintf(
$conn_w,
'(%s, %s, %s)',
$this->getPHID(),
$key,
$val);
}
queryfx(
$conn_w,
'INSERT INTO %T (taskPHID, name, value) VALUES %Q
ON DUPLICATE KEY UPDATE value = VALUES(value)',
$table->getTableName(),
implode(', ', $sql));
}
}
/* -( Markup Interface )--------------------------------------------------- */
/**
* @task markup
*/
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digest($this->getMarkupText($field));
$id = $this->getID();
return "maniphest:T{$id}:{$field}:{$hash}";
}
/**
* @task markup
*/
public function getMarkupText($field) {
return $this->getDescription();
}
/**
* @task markup
*/
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newManiphestMarkupEngine();
}
/**
* @task markup
*/
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
/**
* @task markup
*/
public function shouldUseMarkupCache($field) {
return (bool)$this->getID();
}
}
diff --git a/src/applications/maniphest/storage/ManiphestTaskAuxiliaryStorage.php b/src/applications/maniphest/storage/ManiphestTaskAuxiliaryStorage.php
index 1a99de76c0..83b32f1c2b 100644
--- a/src/applications/maniphest/storage/ManiphestTaskAuxiliaryStorage.php
+++ b/src/applications/maniphest/storage/ManiphestTaskAuxiliaryStorage.php
@@ -1,28 +1,12 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestTaskAuxiliaryStorage extends ManiphestDAO {
protected $taskPHID;
protected $name;
protected $value;
}
diff --git a/src/applications/maniphest/storage/ManiphestTaskProject.php b/src/applications/maniphest/storage/ManiphestTaskProject.php
index 687694934e..546a296071 100644
--- a/src/applications/maniphest/storage/ManiphestTaskProject.php
+++ b/src/applications/maniphest/storage/ManiphestTaskProject.php
@@ -1,67 +1,51 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* This is a DAO for the Task -> 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/ManiphestTaskSubscriber.php b/src/applications/maniphest/storage/ManiphestTaskSubscriber.php
index a5b9222910..f0b45f905b 100644
--- a/src/applications/maniphest/storage/ManiphestTaskSubscriber.php
+++ b/src/applications/maniphest/storage/ManiphestTaskSubscriber.php
@@ -1,65 +1,49 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestTaskSubscriber extends ManiphestDAO {
protected $taskPHID;
protected $subscriberPHID;
public function getConfiguration() {
return array(
self::CONFIG_IDS => 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/ManiphestTransaction.php b/src/applications/maniphest/storage/ManiphestTransaction.php
index c106e37904..8cd36d5a69 100644
--- a/src/applications/maniphest/storage/ManiphestTransaction.php
+++ b/src/applications/maniphest/storage/ManiphestTransaction.php
@@ -1,200 +1,184 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @task markup Markup Interface
* @group maniphest
*/
final class ManiphestTransaction extends ManiphestDAO
implements PhabricatorMarkupInterface {
const MARKUP_FIELD_BODY = 'markup:body';
protected $taskID;
protected $authorPHID;
protected $transactionType;
protected $oldValue;
protected $newValue;
protected $comments;
protected $metadata = array();
protected $contentSource;
public function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'oldValue' => self::SERIALIZATION_JSON,
'newValue' => self::SERIALIZATION_JSON,
'metadata' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function extractPHIDs() {
$phids = array();
switch ($this->getTransactionType()) {
case ManiphestTransactionType::TYPE_CCS:
case ManiphestTransactionType::TYPE_PROJECTS:
foreach ($this->getOldValue() as $phid) {
$phids[] = $phid;
}
foreach ($this->getNewValue() as $phid) {
$phids[] = $phid;
}
break;
case ManiphestTransactionType::TYPE_OWNER:
$phids[] = $this->getOldValue();
$phids[] = $this->getNewValue();
break;
case ManiphestTransactionType::TYPE_EDGE:
$phids = array_merge(
$phids,
array_keys($this->getOldValue() + $this->getNewValue()));
break;
case ManiphestTransactionType::TYPE_ATTACH:
$old = $this->getOldValue();
$new = $this->getNewValue();
if (!is_array($old)) {
$old = array();
}
if (!is_array($new)) {
$new = array();
}
$val = array_merge(array_values($old), array_values($new));
foreach ($val as $stuff) {
foreach ($stuff as $phid => $ignored) {
$phids[] = $phid;
}
}
break;
}
$phids[] = $this->getAuthorPHID();
return $phids;
}
public function getMetadataValue($key, $default = null) {
if (!is_array($this->metadata)) {
return $default;
}
return idx($this->metadata, $key, $default);
}
public function setMetadataValue($key, $value) {
if (!is_array($this->metadata)) {
$this->metadata = array();
}
$this->metadata[$key] = $value;
return $this;
}
public function canGroupWith($target) {
if ($target->getAuthorPHID() != $this->getAuthorPHID()) {
return false;
}
if ($target->hasComments() && $this->hasComments()) {
return false;
}
$ttime = $target->getDateCreated();
$stime = $this->getDateCreated();
if (abs($stime - $ttime) > 60) {
return false;
}
if ($target->getTransactionType() == $this->getTransactionType()) {
$aux_type = ManiphestTransactionType::TYPE_AUXILIARY;
if ($this->getTransactionType() == $aux_type) {
$that_key = $target->getMetadataValue('aux:key');
$this_key = $this->getMetadataValue('aux:key');
if ($that_key == $this_key) {
return false;
}
} else {
return false;
}
}
return true;
}
public function hasComments() {
return (bool)strlen(trim($this->getComments()));
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source->serialize();
return $this;
}
public function getContentSource() {
return PhabricatorContentSource::newFromSerialized($this->contentSource);
}
/* -( Markup Interface )--------------------------------------------------- */
/**
* @task markup
*/
public function getMarkupFieldKey($field) {
if ($this->shouldUseMarkupCache($field)) {
$id = $this->getID();
} else {
$id = PhabricatorHash::digest($this->getMarkupText($field));
}
return "maniphest:x:{$field}:{$id}";
}
/**
* @task markup
*/
public function getMarkupText($field) {
return $this->getComments();
}
/**
* @task markup
*/
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newManiphestMarkupEngine();
}
/**
* @task markup
*/
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
/**
* @task markup
*/
public function shouldUseMarkupCache($field) {
return (bool)$this->getID();
}
}
diff --git a/src/applications/maniphest/view/ManiphestTaskListView.php b/src/applications/maniphest/view/ManiphestTaskListView.php
index 9c3425ca70..b7a52bfe3e 100644
--- a/src/applications/maniphest/view/ManiphestTaskListView.php
+++ b/src/applications/maniphest/view/ManiphestTaskListView.php
@@ -1,73 +1,57 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestTaskListView extends ManiphestView {
private $tasks;
private $handles;
private $user;
private $showBatchControls;
private $showSubpriorityControls;
public function setTasks(array $tasks) {
assert_instances_of($tasks, 'ManiphestTask');
$this->tasks = $tasks;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setShowBatchControls($show_batch_controls) {
$this->showBatchControls = $show_batch_controls;
return $this;
}
public function setShowSubpriorityControls($show_subpriority_controls) {
$this->showSubpriorityControls = $show_subpriority_controls;
return $this;
}
public function render() {
$views = array();
foreach ($this->tasks as $task) {
$view = new ManiphestTaskSummaryView();
$view->setTask($task);
$view->setShowBatchControls($this->showBatchControls);
$view->setShowSubpriorityControls($this->showSubpriorityControls);
$view->setUser($this->user);
$view->setHandles($this->handles);
$views[] = $view->render();
}
return implode("\n", $views);
}
}
diff --git a/src/applications/maniphest/view/ManiphestTaskProjectsView.php b/src/applications/maniphest/view/ManiphestTaskProjectsView.php
index d8b3fa48aa..d83a706e0f 100644
--- a/src/applications/maniphest/view/ManiphestTaskProjectsView.php
+++ b/src/applications/maniphest/view/ManiphestTaskProjectsView.php
@@ -1,75 +1,59 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestTaskProjectsView extends ManiphestView {
private $handles;
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function render() {
require_celerity_resource('phabricator-project-tag-css');
$show = array_slice($this->handles, 0, 2);
$tags = array();
foreach ($show as $handle) {
$tags[] = phutil_render_tag(
'a',
array(
'href' => $handle->getURI(),
'class' => 'phabricator-project-tag',
),
phutil_escape_html(
phutil_utf8_shorten($handle->getName(), 24)));
}
if (count($this->handles) > 2) {
require_celerity_resource('aphront-tooltip-css');
Javelin::initBehavior('phabricator-tooltips');
$all = array();
foreach ($this->handles as $handle) {
$all[] = $handle->getName();
}
$tags[] = javelin_render_tag(
'span',
array(
'class' => 'phabricator-project-tag',
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => implode(', ', $all),
'size' => 200,
),
),
"\xE2\x80\xA6");
}
return implode("\n", $tags);
}
}
diff --git a/src/applications/maniphest/view/ManiphestTaskSummaryView.php b/src/applications/maniphest/view/ManiphestTaskSummaryView.php
index e2dff15f90..de0293881c 100644
--- a/src/applications/maniphest/view/ManiphestTaskSummaryView.php
+++ b/src/applications/maniphest/view/ManiphestTaskSummaryView.php
@@ -1,160 +1,144 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestTaskSummaryView extends ManiphestView {
private $task;
private $handles;
private $user;
private $showBatchControls;
private $showSubpriorityControls;
public function setTask(ManiphestTask $task) {
$this->task = $task;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setShowBatchControls($show_batch_controls) {
$this->showBatchControls = $show_batch_controls;
return $this;
}
public function setShowSubpriorityControls($show_subpriority_controls) {
$this->showSubpriorityControls = $show_subpriority_controls;
return $this;
}
public static function getPriorityClass($priority) {
$classes = array(
ManiphestTaskPriority::PRIORITY_UNBREAK_NOW => 'pri-unbreak',
ManiphestTaskPriority::PRIORITY_TRIAGE => 'pri-triage',
ManiphestTaskPriority::PRIORITY_HIGH => 'pri-high',
ManiphestTaskPriority::PRIORITY_NORMAL => 'pri-normal',
ManiphestTaskPriority::PRIORITY_LOW => 'pri-low',
ManiphestTaskPriority::PRIORITY_WISH => 'pri-wish',
);
return idx($classes, $priority);
}
public function render() {
if (!$this->user) {
throw new Exception("Call setUser() before rendering!");
}
$task = $this->task;
$handles = $this->handles;
require_celerity_resource('maniphest-task-summary-css');
$pri_class = self::getPriorityClass($task->getPriority());
$status_map = ManiphestTaskStatus::getTaskStatusMap();
$batch = null;
if ($this->showBatchControls) {
$batch =
'<td class="maniphest-task-batch">'.
javelin_render_tag(
'input',
array(
'type' => 'checkbox',
'name' => 'batch[]',
'value' => $task->getID(),
'sigil' => 'maniphest-batch',
),
null).
'</td>';
}
$projects_view = new ManiphestTaskProjectsView();
$projects_view->setHandles(
array_select_keys(
$this->handles,
$task->getProjectPHIDs()));
$control_class = null;
$control_sigil = null;
if ($this->showSubpriorityControls) {
$control_class = 'maniphest-active-handle';
$control_sigil = 'maniphest-task-handle';
}
$handle = javelin_render_tag(
'td',
array(
'class' => 'maniphest-task-handle '.$pri_class.' '.$control_class,
'sigil' => $control_sigil,
),
'');
return javelin_render_tag(
'table',
array(
'class' => 'maniphest-task-summary',
'sigil' => 'maniphest-task',
'meta' => array(
'taskID' => $task->getID(),
),
),
'<tr>'.
$handle.
$batch.
'<td class="maniphest-task-number">'.
'T'.$task->getID().
'</td>'.
'<td class="maniphest-task-status">'.
idx($status_map, $task->getStatus(), 'Unknown').
'</td>'.
'<td class="maniphest-task-owner">'.
($task->getOwnerPHID()
? $handles[$task->getOwnerPHID()]->renderLink()
: '<em>None</em>').
'</td>'.
'<td class="maniphest-task-name">'.
phutil_render_tag(
'a',
array(
'href' => '/T'.$task->getID(),
),
phutil_escape_html($task->getTitle())).
'</td>'.
'<td class="maniphest-task-projects">'.
$projects_view->render().
'</td>'.
'<td class="maniphest-task-updated">'.
phabricator_date($task->getDateModified(), $this->user).
'</td>'.
'</tr>');
}
}
diff --git a/src/applications/maniphest/view/ManiphestTransactionDetailView.php b/src/applications/maniphest/view/ManiphestTransactionDetailView.php
index b4003711a7..8a8bd8749d 100644
--- a/src/applications/maniphest/view/ManiphestTransactionDetailView.php
+++ b/src/applications/maniphest/view/ManiphestTransactionDetailView.php
@@ -1,839 +1,823 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestTransactionDetailView extends ManiphestView {
private $transactions;
private $handles;
private $markupEngine;
private $forEmail;
private $preview;
private $commentNumber;
private $rangeSpecification;
private $renderSummaryOnly;
private $renderFullSummary;
private $user;
private $auxiliaryFields;
public function setAuxiliaryFields(array $fields) {
assert_instances_of($fields, 'ManiphestAuxiliaryFieldSpecification');
$this->auxiliaryFields = mpull($fields, null, 'getAuxiliaryKey');
return $this;
}
public function getAuxiliaryField($key) {
return idx($this->auxiliaryFields, $key);
}
public function setTransactionGroup(array $transactions) {
assert_instances_of($transactions, 'ManiphestTransaction');
$this->transactions = $transactions;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setMarkupEngine(PhabricatorMarkupEngine $engine) {
$this->markupEngine = $engine;
return $this;
}
public function setPreview($preview) {
$this->preview = $preview;
return $this;
}
public function setRenderSummaryOnly($render_summary_only) {
$this->renderSummaryOnly = $render_summary_only;
return $this;
}
public function getRenderSummaryOnly() {
return $this->renderSummaryOnly;
}
public function setRenderFullSummary($render_full_summary) {
$this->renderFullSummary = $render_full_summary;
return $this;
}
public function getRenderFullSummary() {
return $this->renderFullSummary;
}
public function setCommentNumber($comment_number) {
$this->commentNumber = $comment_number;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setRangeSpecification($range) {
$this->rangeSpecification = $range;
return $this;
}
public function getRangeSpecification() {
return $this->rangeSpecification;
}
public function renderForEmail($with_date) {
$this->forEmail = true;
$transaction = reset($this->transactions);
$author = $this->renderHandles(array($transaction->getAuthorPHID()));
$action = null;
$descs = array();
$comments = null;
foreach ($this->transactions as $transaction) {
list($verb, $desc, $classes) = $this->describeAction($transaction);
if ($desc === null) {
continue;
}
if ($action === null) {
$action = $verb;
}
$desc = $author.' '.$desc.'.';
if ($with_date) {
// NOTE: This is going into a (potentially multi-recipient) email so
// we can't use a single user's timezone preferences. Use the server's
// instead, but make the timezone explicit.
$datetime = date('M jS \a\t g:i A T', $transaction->getDateCreated());
$desc = "On {$datetime}, {$desc}";
}
$descs[] = $desc;
if ($transaction->hasComments()) {
$comments = $transaction->getComments();
}
}
$descs = implode("\n", $descs);
if ($comments) {
$descs .= "\n".$comments;
}
foreach ($this->transactions as $transaction) {
$supplemental = $this->renderSupplementalInfoForEmail($transaction);
if ($supplemental) {
$descs .= "\n\n".$supplemental;
}
}
$this->forEmail = false;
return array($action, $descs);
}
public function render() {
if (!$this->user) {
throw new Exception("Call setUser() before render()!");
}
$handles = $this->handles;
$transactions = $this->transactions;
require_celerity_resource('maniphest-transaction-detail-css');
$comment_transaction = null;
foreach ($this->transactions as $transaction) {
if ($transaction->hasComments()) {
$comment_transaction = $transaction;
break;
}
}
$any_transaction = reset($transactions);
$author = $this->handles[$any_transaction->getAuthorPHID()];
$more_classes = array();
$descs = array();
foreach ($transactions as $transaction) {
list($verb, $desc, $classes) = $this->describeAction($transaction);
if ($desc === null) {
continue;
}
$more_classes = array_merge($more_classes, $classes);
$full_summary = null;
if ($this->getRenderFullSummary()) {
$full_summary = $this->renderFullSummary($transaction);
}
$descs[] = javelin_render_tag(
'div',
array(
'sigil' => 'maniphest-transaction-description',
),
$author->renderLink().' '.$desc.'.'.$full_summary);
}
if ($this->getRenderSummaryOnly()) {
return implode("\n", $descs);
}
if ($comment_transaction && $comment_transaction->hasComments()) {
$comment_block = $this->markupEngine->getOutput(
$comment_transaction,
ManiphestTransaction::MARKUP_FIELD_BODY);
$comment_block =
'<div class="maniphest-transaction-comments phabricator-remarkup">'.
$comment_block.
'</div>';
} else {
$comment_block = null;
}
$source_transaction = nonempty($comment_transaction, $any_transaction);
$xaction_view = id(new PhabricatorTransactionView())
->setUser($this->user)
->setImageURI($author->getImageURI())
->setContentSource($source_transaction->getContentSource())
->setActions($descs);
foreach ($more_classes as $class) {
$xaction_view->addClass($class);
}
if ($this->preview) {
$xaction_view->setIsPreview($this->preview);
} else {
$xaction_view->setEpoch($any_transaction->getDateCreated());
if ($this->commentNumber) {
$anchor_name = 'comment-'.$this->commentNumber;
$anchor_text = 'T'.$any_transaction->getTaskID().'#'.$anchor_name;
$xaction_view->setAnchor($anchor_name, $anchor_text);
}
}
$xaction_view->appendChild($comment_block);
return $xaction_view->render();
}
private function renderSupplementalInfoForEmail($transaction) {
$handles = $this->handles;
$type = $transaction->getTransactionType();
$new = $transaction->getNewValue();
$old = $transaction->getOldValue();
switch ($type) {
case ManiphestTransactionType::TYPE_DESCRIPTION:
return "NEW DESCRIPTION\n ".trim($new)."\n\n".
"PREVIOUS DESCRIPTION\n ".trim($old);
case ManiphestTransactionType::TYPE_ATTACH:
$old_raw = nonempty($old, array());
$new_raw = nonempty($new, array());
$attach_types = array(
PhabricatorPHIDConstants::PHID_TYPE_DREV,
PhabricatorPHIDConstants::PHID_TYPE_FILE,
);
foreach ($attach_types as $attach_type) {
$old = array_keys(idx($old_raw, $attach_type, array()));
$new = array_keys(idx($new_raw, $attach_type, array()));
if ($old != $new) {
break;
}
}
$added = array_diff($new, $old);
if (!$added) {
break;
}
$links = array();
foreach (array_select_keys($handles, $added) as $handle) {
$links[] = ' '.PhabricatorEnv::getProductionURI($handle->getURI());
}
$links = implode("\n", $links);
switch ($attach_type) {
case PhabricatorPHIDConstants::PHID_TYPE_DREV:
$title = 'ATTACHED REVISIONS';
break;
case PhabricatorPHIDConstants::PHID_TYPE_FILE:
$title = 'ATTACHED FILES';
break;
}
return $title."\n".$links;
case ManiphestTransactionType::TYPE_EDGE:
$add = array_diff_key($new, $old);
if (!$add) {
break;
}
$links = array();
foreach ($add as $phid => $ignored) {
$handle = $handles[$phid];
$links[] = ' '.PhabricatorEnv::getProductionURI($handle->getURI());
}
$links = implode("\n", $links);
$edge_type = $transaction->getMetadataValue('edge:type');
$title = $this->getEdgeEmailTitle($edge_type, $add);
return $title."\n".$links;
default:
break;
}
return null;
}
private function describeAction($transaction) {
$verb = null;
$desc = null;
$classes = array();
$handles = $this->handles;
$type = $transaction->getTransactionType();
$author_phid = $transaction->getAuthorPHID();
$new = $transaction->getNewValue();
$old = $transaction->getOldValue();
switch ($type) {
case ManiphestTransactionType::TYPE_TITLE:
$verb = 'Retitled';
$desc = 'changed the title from '.$this->renderString($old).
' to '.$this->renderString($new);
break;
case ManiphestTransactionType::TYPE_DESCRIPTION:
$verb = 'Edited';
if ($this->forEmail || $this->getRenderFullSummary()) {
$desc = 'updated the task description';
} else {
$desc = 'updated the task description; '.
$this->renderExpandLink($transaction);
}
break;
case ManiphestTransactionType::TYPE_NONE:
$verb = 'Commented On';
$desc = 'added a comment';
break;
case ManiphestTransactionType::TYPE_OWNER:
if ($transaction->getAuthorPHID() == $new) {
$verb = 'Claimed';
$desc = 'claimed this task';
$classes[] = 'claimed';
} else if (!$new) {
$verb = 'Up For Grabs';
$desc = 'placed this task up for grabs';
$classes[] = 'upforgrab';
} else if (!$old) {
$verb = 'Assigned';
$desc = 'assigned this task to '.$this->renderHandles(array($new));
$classes[] = 'assigned';
} else {
$verb = 'Reassigned';
$desc = 'reassigned this task from '.
$this->renderHandles(array($old)).
' to '.
$this->renderHandles(array($new));
$classes[] = 'reassigned';
}
break;
case ManiphestTransactionType::TYPE_CCS:
$added = array_diff($new, $old);
$removed = array_diff($old, $new);
// can only add in preview so just show placeholder if nothing to add
if ($this->preview && empty($added)) {
$verb = 'Changed CC';
$desc = 'changed CCs..';
break;
}
if ($added && !$removed) {
$verb = 'Added CC';
if (count($added) == 1) {
$desc = 'added '.$this->renderHandles($added).' to CC';
} else {
$desc = 'added CCs: '.$this->renderHandles($added);
}
} else if ($removed && !$added) {
$verb = 'Removed CC';
if (count($removed) == 1) {
$desc = 'removed '.$this->renderHandles($removed).' from CC';
} else {
$desc = 'removed CCs: '.$this->renderHandles($removed);
}
} else {
$verb = 'Changed CC';
$desc = 'changed CCs, added: '.$this->renderHandles($added).'; '.
'removed: '.$this->renderHandles($removed);
}
break;
case ManiphestTransactionType::TYPE_EDGE:
$edge_type = $transaction->getMetadataValue('edge:type');
$add = array_diff_key($new, $old);
$rem = array_diff_key($old, $new);
if ($add && !$rem) {
$verb = $this->getEdgeAddVerb($edge_type);
$desc = $this->getEdgeAddList($edge_type, $add);
} else if ($rem && !$add) {
$verb = $this->getEdgeRemVerb($edge_type);
$desc = $this->getEdgeRemList($edge_type, $rem);
} else {
$verb = $this->getEdgeEditVerb($edge_type);
$desc = $this->getEdgeEditList($edge_type, $add, $rem);
}
break;
case ManiphestTransactionType::TYPE_PROJECTS:
$added = array_diff($new, $old);
$removed = array_diff($old, $new);
// can only add in preview so just show placeholder if nothing to add
if ($this->preview && empty($added)) {
$verb = 'Changed Projects';
$desc = 'changed projects..';
break;
}
if ($added && !$removed) {
$verb = 'Added Project';
if (count($added) == 1) {
$desc = 'added project '.$this->renderHandles($added);
} else {
$desc = 'added projects: '.$this->renderHandles($added);
}
} else if ($removed && !$added) {
$verb = 'Removed Project';
if (count($removed) == 1) {
$desc = 'removed project '.$this->renderHandles($removed);
} else {
$desc = 'removed projects: '.$this->renderHandles($removed);
}
} else {
$verb = 'Changed Projects';
$desc = 'changed projects, added: '.$this->renderHandles($added).'; '.
'removed: '.$this->renderHandles($removed);
}
break;
case ManiphestTransactionType::TYPE_STATUS:
if ($new == ManiphestTaskStatus::STATUS_OPEN) {
if ($old) {
$verb = 'Reopened';
$desc = 'reopened this task';
$classes[] = 'reopened';
} else {
$verb = 'Created';
$desc = 'created this task';
$classes[] = 'created';
}
} else if ($new == ManiphestTaskStatus::STATUS_CLOSED_SPITE) {
$verb = 'Spited';
$desc = 'closed this task out of spite';
$classes[] = 'spited';
} else if ($new == ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE) {
$verb = 'Merged';
$desc = 'closed this task as a duplicate';
$classes[] = 'duplicate';
} else {
$verb = 'Closed';
$full = idx(ManiphestTaskStatus::getTaskStatusMap(), $new, '???');
$desc = 'closed this task as "'.$full.'"';
$classes[] = 'closed';
}
break;
case ManiphestTransactionType::TYPE_PRIORITY:
$old_name = ManiphestTaskPriority::getTaskPriorityName($old);
$new_name = ManiphestTaskPriority::getTaskPriorityName($new);
if ($old == ManiphestTaskPriority::PRIORITY_TRIAGE) {
$verb = 'Triaged';
$desc = 'triaged this task as "'.$new_name.'" priority';
} else if ($old > $new) {
$verb = 'Lowered Priority';
$desc = 'lowered the priority of this task from "'.$old_name.'" to '.
'"'.$new_name.'"';
} else {
$verb = 'Raised Priority';
$desc = 'raised the priority of this task from "'.$old_name.'" to '.
'"'.$new_name.'"';
}
if ($new == ManiphestTaskPriority::PRIORITY_UNBREAK_NOW) {
$classes[] = 'unbreaknow';
}
break;
case ManiphestTransactionType::TYPE_ATTACH:
if ($this->preview) {
$verb = 'Changed Attached';
$desc = 'changed attachments..';
break;
}
$old_raw = nonempty($old, array());
$new_raw = nonempty($new, array());
foreach (array(
PhabricatorPHIDConstants::PHID_TYPE_DREV,
PhabricatorPHIDConstants::PHID_TYPE_TASK,
PhabricatorPHIDConstants::PHID_TYPE_FILE) as $attach_type) {
$old = array_keys(idx($old_raw, $attach_type, array()));
$new = array_keys(idx($new_raw, $attach_type, array()));
if ($old != $new) {
break;
}
}
$added = array_diff($new, $old);
$removed = array_diff($old, $new);
$add_desc = $this->renderHandles($added);
$rem_desc = $this->renderHandles($removed);
if ($added && !$removed) {
$verb = 'Attached';
$desc =
'attached '.
$this->getAttachName($attach_type, count($added)).': '.
$add_desc;
} else if ($removed && !$added) {
$verb = 'Detached';
$desc =
'detached '.
$this->getAttachName($attach_type, count($removed)).': '.
$rem_desc;
} else {
$verb = 'Changed Attached';
$desc =
'changed attached '.
$this->getAttachName($attach_type, count($added) + count($removed)).
', added: '.$add_desc.'; '.
'removed: '.$rem_desc;
}
break;
case ManiphestTransactionType::TYPE_AUXILIARY:
$aux_key = $transaction->getMetadataValue('aux:key');
$aux_field = $this->getAuxiliaryField($aux_key);
$verb = null;
if ($aux_field) {
$verb = $aux_field->renderTransactionEmailVerb($transaction);
}
if ($verb === null) {
if ($old === null) {
$verb = "Set Field";
} else if ($new === null) {
$verb = "Removed Field";
} else {
$verb = "Updated Field";
}
}
$desc = null;
if ($aux_field) {
$use_field = $aux_field;
} else {
$use_field = id(new ManiphestAuxiliaryFieldDefaultSpecification())
->setFieldType(
ManiphestAuxiliaryFieldDefaultSpecification::TYPE_STRING);
}
$desc = $use_field->renderTransactionDescription(
$transaction,
$this->forEmail
? ManiphestAuxiliaryFieldSpecification::RENDER_TARGET_TEXT
: ManiphestAuxiliaryFieldSpecification::RENDER_TARGET_HTML);
break;
default:
return array($type, ' brazenly '.$type."'d", $classes);
}
return array($verb, $desc, $classes);
}
private function renderFullSummary($transaction) {
switch ($transaction->getTransactionType()) {
case ManiphestTransactionType::TYPE_DESCRIPTION:
$id = $transaction->getID();
$old_text = wordwrap($transaction->getOldValue(), 80);
$new_text = wordwrap($transaction->getNewValue(), 80);
$engine = new PhabricatorDifferenceEngine();
$changeset = $engine->generateChangesetFromFileContent($old_text,
$new_text);
$whitespace_mode = DifferentialChangesetParser::WHITESPACE_SHOW_ALL;
$parser = new DifferentialChangesetParser();
$parser->setChangeset($changeset);
$parser->setRenderingReference($id);
$parser->setWhitespaceMode($whitespace_mode);
$spec = $this->getRangeSpecification();
list($range_s, $range_e, $mask) =
DifferentialChangesetParser::parseRangeSpecification($spec);
$output = $parser->render($range_s, $range_e, $mask);
return $output;
}
return null;
}
private function renderExpandLink($transaction) {
$id = $transaction->getID();
Javelin::initBehavior('maniphest-transaction-expand');
return javelin_render_tag(
'a',
array(
'href' => '/maniphest/task/descriptionchange/'.$id.'/',
'sigil' => 'maniphest-expand-transaction',
'mustcapture' => true,
),
'show details');
}
private function renderHandles($phids, $full = false) {
$links = array();
foreach ($phids as $phid) {
if ($this->forEmail) {
if ($full) {
$links[] = $this->handles[$phid]->getFullName();
} else {
$links[] = $this->handles[$phid]->getName();
}
} else {
$links[] = $this->handles[$phid]->renderLink();
}
}
return implode(', ', $links);
}
private function renderString($string) {
if ($this->forEmail) {
return '"'.$string.'"';
} else {
return '"'.phutil_escape_html($string).'"';
}
}
/* -( Strings )------------------------------------------------------------ */
/**
* @task strings
*/
private function getAttachName($attach_type, $count) {
switch ($attach_type) {
case PhabricatorPHIDConstants::PHID_TYPE_DREV:
return pht('Differential Revision(s)', $count);
case PhabricatorPHIDConstants::PHID_TYPE_FILE:
return pht('file(s)', $count);
case PhabricatorPHIDConstants::PHID_TYPE_TASK:
return pht('Maniphest Task(s)', $count);
}
}
/**
* @task strings
*/
private function getEdgeEmailTitle($type, array $list) {
$count = count($list);
switch ($type) {
case PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV:
return pht('DIFFERENTIAL %d REVISION(S)', $count);
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK:
return pht('DEPENDS ON %d TASK(S)', $count);
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK:
return pht('DEPENDENT %d TASK(s)', $count);
case PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT:
return pht('ATTACHED %d COMMIT(S)', $count);
default:
return pht('ATTACHED %d OBJECT(S)', $count);
}
}
/**
* @task strings
*/
private function getEdgeAddVerb($type) {
switch ($type) {
case PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV:
return pht('Added Revision');
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK:
return pht('Added Dependency');
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK:
return pht('Added Dependent Task');
case PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT:
return pht('Added Commit');
default:
return pht('Added Object');
}
}
/**
* @task strings
*/
private function getEdgeRemVerb($type) {
switch ($type) {
case PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV:
return pht('Removed Revision');
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK:
return pht('Removed Dependency');
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK:
return pht('Removed Dependent Task');
case PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT:
return pht('Removed Commit');
default:
return pht('Removed Object');
}
}
/**
* @task strings
*/
private function getEdgeEditVerb($type) {
switch ($type) {
case PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV:
return pht('Changed Revisions');
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK:
return pht('Changed Dependencies');
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK:
return pht('Changed Dependent Tasks');
case PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT:
return pht('Changed Commits');
default:
return pht('Changed Objects');
}
}
/**
* @task strings
*/
private function getEdgeAddList($type, array $add) {
$list = $this->renderHandles(array_keys($add), $full = true);
$count = count($add);
switch ($type) {
case PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV:
return pht('added %d revision(s): %s', $count, $list);
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK:
return pht('added %d dependencie(s): %s', $count, $list);
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK:
return pht('added %d dependent task(s): %s', $count, $list);
case PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT:
return pht('added %d commit(s): %s', $count, $list);
default:
return pht('added %d object(s): %s', $count, $list);
}
}
/**
* @task strings
*/
private function getEdgeRemList($type, array $rem) {
$list = $this->renderHandles(array_keys($rem), $full = true);
$count = count($rem);
switch ($type) {
case PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV:
return pht('removed %d revision(s): %s', $count, $list);
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK:
return pht('removed %d dependencie(s): %s', $count, $list);
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK:
return pht('removed %d dependent task(s): %s', $count, $list);
case PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT:
return pht('removed %d commit(s): %s', $count, $list);
default:
return pht('removed %d object(s): %s', $count, $list);
}
}
/**
* @task strings
*/
private function getEdgeEditList($type, array $add, array $rem) {
$add_list = $this->renderHandles(array_keys($add), $full = true);
$rem_list = $this->renderHandles(array_keys($rem), $full = true);
$add_count = count($add_list);
$rem_count = count($rem_list);
switch ($type) {
case PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV:
return pht(
'changed %d revision(s), added %d: %s; removed %d: %s',
$add_count + $rem_count,
$add_count,
$add_list,
$rem_count,
$rem_list);
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK:
return pht(
'changed %d dependencie(s), added %d: %s; removed %d: %s',
$add_count + $rem_count,
$add_count,
$add_list,
$rem_count,
$rem_list);
case PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK:
return pht(
'changed %d dependent task(s), added %d: %s; removed %d: %s',
$add_count + $rem_count,
$add_count,
$add_list,
$rem_count,
$rem_list);
case PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT:
return pht(
'changed %d commit(s), added %d: %s; removed %d: %s',
$add_count + $rem_count,
$add_count,
$add_list,
$rem_count,
$rem_list);
default:
return pht(
'changed %d object(s), added %d: %s; removed %d: %s',
$add_count + $rem_count,
$add_count,
$add_list,
$rem_count,
$rem_list);
}
}
}
diff --git a/src/applications/maniphest/view/ManiphestTransactionListView.php b/src/applications/maniphest/view/ManiphestTransactionListView.php
index 001eee94c5..1dbbc27cf3 100644
--- a/src/applications/maniphest/view/ManiphestTransactionListView.php
+++ b/src/applications/maniphest/view/ManiphestTransactionListView.php
@@ -1,133 +1,117 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
final class ManiphestTransactionListView extends ManiphestView {
private $transactions;
private $handles;
private $user;
private $markupEngine;
private $preview;
private $auxiliaryFields;
public function setTransactions(array $transactions) {
assert_instances_of($transactions, 'ManiphestTransaction');
$this->transactions = $transactions;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setMarkupEngine(PhabricatorMarkupEngine $engine) {
$this->markupEngine = $engine;
return $this;
}
public function setPreview($preview) {
$this->preview = $preview;
return $this;
}
public function setAuxiliaryFields(array $fields) {
assert_instances_of($fields, 'ManiphestAuxiliaryFieldSpecification');
$this->auxiliaryFields = $fields;
return $this;
}
private function getAuxiliaryFields() {
if (empty($this->auxiliaryFields)) {
return array();
}
return $this->auxiliaryFields;
}
public function render() {
$views = array();
$last = null;
$group = array();
$groups = array();
$has_description_transaction = false;
foreach ($this->transactions as $transaction) {
if ($transaction->getTransactionType() ==
ManiphestTransactionType::TYPE_DESCRIPTION) {
$has_description_transaction = true;
}
if ($last === null) {
$last = $transaction;
$group[] = $transaction;
continue;
} else if ($last->canGroupWith($transaction)) {
$group[] = $transaction;
if ($transaction->hasComments()) {
$last = $transaction;
}
} else {
$groups[] = $group;
$last = $transaction;
$group = array($transaction);
}
}
if ($group) {
$groups[] = $group;
}
if ($has_description_transaction) {
require_celerity_resource('differential-changeset-view-css');
require_celerity_resource('syntax-highlighting-css');
$whitespace_mode = DifferentialChangesetParser::WHITESPACE_SHOW_ALL;
Javelin::initBehavior('differential-show-more', array(
'uri' => '/maniphest/task/descriptionchange/',
'whitespace' => $whitespace_mode,
));
}
$sequence = 1;
foreach ($groups as $group) {
$view = new ManiphestTransactionDetailView();
$view->setUser($this->user);
$view->setAuxiliaryFields($this->getAuxiliaryFields());
$view->setTransactionGroup($group);
$view->setHandles($this->handles);
$view->setMarkupEngine($this->markupEngine);
$view->setPreview($this->preview);
$view->setCommentNumber($sequence++);
$views[] = $view->render();
}
return
'<div class="maniphest-transaction-list-view">'.
implode("\n", $views).
'</div>';
}
}
diff --git a/src/applications/maniphest/view/ManiphestView.php b/src/applications/maniphest/view/ManiphestView.php
index 6a8144c9c0..d0b710b492 100644
--- a/src/applications/maniphest/view/ManiphestView.php
+++ b/src/applications/maniphest/view/ManiphestView.php
@@ -1,24 +1,8 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group maniphest
*/
abstract class ManiphestView extends AphrontView {
}
diff --git a/src/applications/meta/application/PhabricatorApplicationApplications.php b/src/applications/meta/application/PhabricatorApplicationApplications.php
index 4b7fbccdbe..b5c6a907f8 100644
--- a/src/applications/meta/application/PhabricatorApplicationApplications.php
+++ b/src/applications/meta/application/PhabricatorApplicationApplications.php
@@ -1,50 +1,34 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationApplications extends PhabricatorApplication {
public function getBaseURI() {
return '/applications/';
}
public function getShortDescription() {
return 'Manage Applications';
}
public function getAutospriteName() {
return 'applications';
}
public function getRoutes() {
return array(
'/applications/' => array(
'' => 'PhabricatorApplicationsListController'
),
);
}
public function getTitleGlyph() {
return "\xE0\xBC\x84";
}
public function shouldAppearInLaunchView() {
return false;
}
}
diff --git a/src/applications/meta/controller/PhabricatorApplicationsListController.php b/src/applications/meta/controller/PhabricatorApplicationsListController.php
index 01fdd88142..b309ee825d 100644
--- a/src/applications/meta/controller/PhabricatorApplicationsListController.php
+++ b/src/applications/meta/controller/PhabricatorApplicationsListController.php
@@ -1,75 +1,59 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationsListController
extends PhabricatorController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$applications = PhabricatorApplication::getAllInstalledApplications();
foreach ($applications as $key => $application) {
if (!$application->shouldAppearInLaunchView()) {
unset($applications[$key]);
}
}
$groups = PhabricatorApplication::getApplicationGroups();
$applications = msort($applications, 'getApplicationOrder');
$applications = mgroup($applications, 'getApplicationGroup');
$applications = array_select_keys($applications, array_keys($groups));
$view = array();
foreach ($applications as $group => $application_list) {
$status = array();
foreach ($application_list as $key => $application) {
$status[$key] = $application->loadStatus($user);
}
$views = array();
foreach ($application_list as $key => $application) {
$views[] = id(new PhabricatorApplicationLaunchView())
->setApplication($application)
->setApplicationStatus(idx($status, $key, array()))
->setUser($user);
}
$view[] = id(new PhabricatorHeaderView())
->setHeader($groups[$group]);
$view[] = phutil_render_tag(
'div',
array(
'class' => 'phabricator-application-list',
),
id(new AphrontNullView())->appendChild($views)->render());
}
return $this->buildApplicationPage(
$view,
array(
'title' => 'Applications',
'device' => true,
));
}
}
diff --git a/src/applications/meta/view/PhabricatorApplicationLaunchView.php b/src/applications/meta/view/PhabricatorApplicationLaunchView.php
index d1a5980492..1e95754b96 100644
--- a/src/applications/meta/view/PhabricatorApplicationLaunchView.php
+++ b/src/applications/meta/view/PhabricatorApplicationLaunchView.php
@@ -1,120 +1,104 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationLaunchView extends AphrontView {
private $user;
private $application;
private $status;
public function setApplication(PhabricatorApplication $application) {
$this->application = $application;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setApplicationStatus(array $status) {
$this->status = $status;
return $this;
}
public function render() {
$application = $this->application;
require_celerity_resource('phabricator-application-launch-view-css');
$content = array();
$content[] = phutil_render_tag(
'span',
array(
'class' => 'phabricator-application-launch-name',
),
phutil_escape_html($application->getName()));
$content[] = phutil_render_tag(
'span',
array(
'class' => 'phabricator-application-launch-description',
),
phutil_escape_html($application->getShortDescription()));
$count = 0;
$content[] = '<span class="phabricator-application-status-block">';
if ($this->status) {
foreach ($this->status as $status) {
$count += $status->getCount();
$content[] = $status;
}
} else {
$flavor = $application->getFlavorText();
if ($flavor !== null) {
$content[] = phutil_render_tag(
'span',
array(
'class' => 'phabricator-application-flavor-text',
),
phutil_escape_html($flavor));
}
}
$content[] = '</span>';
if ($count) {
$content[] = phutil_render_tag(
'span',
array(
'class' => 'phabricator-application-launch-attention',
),
phutil_escape_html($count));
}
$classes = array();
$classes[] = 'phabricator-application-launch-icon';
$styles = array();
if ($application->getIconURI()) {
$styles[] = 'background-image: url('.$application->getIconURI().')';
} else {
$autosprite = $application->getAutospriteName();
$classes[] = 'autosprite';
$classes[] = 'app-'.$autosprite.'-large';
}
$icon = phutil_render_tag(
'span',
array(
'class' => implode(' ', $classes),
'style' => nonempty(implode('; ', $styles), null),
),
'');
return phutil_render_tag(
'a',
array(
'class' => 'phabricator-application-launch-container',
'href' => $application->getBaseURI(),
),
$icon.
$this->renderSingleView($content));
}
}
diff --git a/src/applications/meta/view/PhabricatorApplicationStatusView.php b/src/applications/meta/view/PhabricatorApplicationStatusView.php
index bb2d4f24f2..81718177eb 100644
--- a/src/applications/meta/view/PhabricatorApplicationStatusView.php
+++ b/src/applications/meta/view/PhabricatorApplicationStatusView.php
@@ -1,64 +1,48 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationStatusView extends AphrontView {
private $count;
private $text;
private $type;
const TYPE_NEEDS_ATTENTION = 'needs';
const TYPE_INFO = 'info';
const TYPE_OKAY = 'okay';
const TYPE_WARNING = 'warning';
const TYPE_EMPTY = 'empty';
public function setType($type) {
$this->type = $type;
return $this;
}
public function setText($text) {
$this->text = $text;
return $this;
}
public function setCount($count) {
$this->count = $count;
return $this;
}
public function getCount() {
return $this->count;
}
public function render() {
$classes = array(
'phabricator-application-status',
'phabricator-application-status-type-'.$this->type,
);
return phutil_render_tag(
'span',
array(
'class' => implode(' ', $classes),
),
phutil_escape_html($this->text));
}
}
diff --git a/src/applications/metamta/PhabricatorMetaMTAEmailBodyParser.php b/src/applications/metamta/PhabricatorMetaMTAEmailBodyParser.php
index e384ef1290..fe7c04afbe 100644
--- a/src/applications/metamta/PhabricatorMetaMTAEmailBodyParser.php
+++ b/src/applications/metamta/PhabricatorMetaMTAEmailBodyParser.php
@@ -1,69 +1,53 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorMetaMTAEmailBodyParser {
public function stripTextBody($body) {
return $this->stripSignature($this->stripQuotedText($body));
}
private function stripQuotedText($body) {
$body = preg_replace(
'/^\s*On\b.*\bwrote:.*?/msU',
'',
$body);
// Outlook english
$body = preg_replace(
'/^\s*-----Original Message-----.*?/imsU',
'',
$body);
// Outlook danish
$body = preg_replace(
'/^\s*-----Oprindelig Meddelelse-----.*?/imsU',
'',
$body);
return rtrim($body);
}
private function stripSignature($body) {
// Quasi-"standard" delimiter, for lols see:
// https://bugzilla.mozilla.org/show_bug.cgi?id=58406
$body = preg_replace(
'/^-- +$.*/sm',
'',
$body);
// HTC Mail application (mobile)
$body = preg_replace(
'/^\s*^Sent from my HTC smartphone.*/sm',
'',
$body);
// Apple iPhone
$body = preg_replace(
'/^\s*^Sent from my iPhone\s*$.*/sm',
'',
$body);
return rtrim($body);
}
}
diff --git a/src/applications/metamta/PhabricatorMetaMTAWorker.php b/src/applications/metamta/PhabricatorMetaMTAWorker.php
index 052c799755..1b7b472d0f 100644
--- a/src/applications/metamta/PhabricatorMetaMTAWorker.php
+++ b/src/applications/metamta/PhabricatorMetaMTAWorker.php
@@ -1,60 +1,44 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorMetaMTAWorker
extends PhabricatorWorker {
private $message;
public function getWaitBeforeRetry(PhabricatorWorkerTask $task) {
$message = $this->loadMessage();
if (!$message) {
return null;
}
$wait = max($message->getNextRetry() - time(), 0) + 15;
return $wait;
}
public function doWork() {
$message = $this->loadMessage();
if (!$message
|| $message->getStatus() != PhabricatorMetaMTAMail::STATUS_QUEUE) {
return;
}
$id = $message->getID();
$message->sendNow();
// task failed if the message is still queued
// (instead of sent, void, or failed)
if ($message->getStatus() == PhabricatorMetaMTAMail::STATUS_QUEUE) {
throw new Exception('Failed to send message');
}
}
private function loadMessage() {
if (!$this->message) {
$message_id = $this->getTaskData();
$this->message = id(new PhabricatorMetaMTAMail())->load($message_id);
if (!$this->message) {
return null;
}
}
return $this->message;
}
}
diff --git a/src/applications/metamta/__tests__/PhabricatorMetaMTAEmailBodyParserTestCase.php b/src/applications/metamta/__tests__/PhabricatorMetaMTAEmailBodyParserTestCase.php
index 190cddff0f..b987c4a6db 100644
--- a/src/applications/metamta/__tests__/PhabricatorMetaMTAEmailBodyParserTestCase.php
+++ b/src/applications/metamta/__tests__/PhabricatorMetaMTAEmailBodyParserTestCase.php
@@ -1,125 +1,109 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorMetaMTAEmailBodyParserTestCase
extends PhabricatorTestCase {
public function testQuotedTextStripping() {
$bodies = $this->getEmailBodies();
foreach ($bodies as $body) {
$parser = new PhabricatorMetaMTAEmailBodyParser();
$stripped = $parser->stripTextBody($body);
$this->assertEqual("OKAY", $stripped);
}
}
private function getEmailBodies() {
$trailing_space = ' ';
return array(
<<<EOEMAIL
OKAY
On May 30, 2011, at 8:36 PM, Someone wrote:
> ...
EOEMAIL
,
<<<EOEMAIL
OKAY
On Fri, May 27, 2011 at 9:39 AM, Someone <
somebody@somewhere.com> wrote:
> ...
EOEMAIL
,
<<<EOEMAIL
OKAY
On Fri, May 27, 2011 at 9:39 AM, Someone
<somebody@somewhere.com> wrote:
> ...
EOEMAIL
,
<<<EOEMAIL
OKAY
-----Oprindelig Meddelelse-----
> ...
EOEMAIL
,
<<<EOEMAIL
OKAY
-----Original Message-----
> ...
EOEMAIL
,
<<<EOEMAIL
OKAY
-----oprindelig meddelelse-----
> ...
EOEMAIL
,
<<<EOEMAIL
OKAY
-----original message-----
> ...
EOEMAIL
,
<<<EOEMAIL
OKAY
Sent from my HTC smartphone on the Now Network from Sprint!
-Reply message ----- From: "somebody (someone)" <
somebody@somewhere.com>
To: <somebody@somewhere.com>
Subject: Some Text Date: Mon, Apr 2, 2012 1:42 pm
> ...
EOEMAIL
,
<<<EOEMAIL
OKAY
--{$trailing_space}
Abraham Lincoln
Supreme Galactic Emperor
EOEMAIL
,
<<<EOEMAIL
OKAY
Sent from my iPhone
EOEMAIL
,
);
}
}
diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php
index 72500c4da7..433a41a5cb 100644
--- a/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php
@@ -1,39 +1,23 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorMailImplementationAdapter {
abstract public function setFrom($email, $name = '');
abstract public function addReplyTo($email, $name = '');
abstract public function addTos(array $emails);
abstract public function addCCs(array $emails);
abstract public function addAttachment($data, $filename, $mimetype);
abstract public function addHeader($header_name, $header_value);
abstract public function setBody($body);
abstract public function setSubject($subject);
abstract public function setIsHTML($is_html);
/**
* Some mailers, notably Amazon SES, do not support us setting a specific
* Message-ID header.
*/
abstract public function supportsMessageIDHeader();
abstract public function send();
}
diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationAmazonSESAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationAmazonSESAdapter.php
index b383519f96..59c7b08933 100644
--- a/src/applications/metamta/adapter/PhabricatorMailImplementationAmazonSESAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailImplementationAmazonSESAdapter.php
@@ -1,52 +1,36 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorMailImplementationAmazonSESAdapter
extends PhabricatorMailImplementationPHPMailerLiteAdapter {
private $message;
private $isHTML;
public function __construct() {
parent::__construct();
$this->mailer->Mailer = 'amazon-ses';
$this->mailer->customMailer = $this;
}
public function supportsMessageIDHeader() {
// Amazon SES will ignore any Message-ID we provide.
return false;
}
/**
* @phutil-external-symbol class SimpleEmailService
*/
public function executeSend($body) {
$key = PhabricatorEnv::getEnvConfig('amazon-ses.access-key');
$secret = PhabricatorEnv::getEnvConfig('amazon-ses.secret-key');
$root = phutil_get_library_root('phabricator');
$root = dirname($root);
require_once $root.'/externals/amazon-ses/ses.php';
$service = new SimpleEmailService($key, $secret);
$service->enableUseExceptions(true);
return $service->sendRawEmail($body);
}
}
diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php
index f0858d2ab5..6e21df30e7 100644
--- a/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php
@@ -1,111 +1,95 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* TODO: Should be final, but inherited by SES.
*/
class PhabricatorMailImplementationPHPMailerLiteAdapter
extends PhabricatorMailImplementationAdapter {
/**
* @phutil-external-symbol class PHPMailerLite
*/
public function __construct() {
$root = phutil_get_library_root('phabricator');
$root = dirname($root);
require_once $root.'/externals/phpmailer/class.phpmailer-lite.php';
$this->mailer = new PHPMailerLite($use_exceptions = true);
$this->mailer->CharSet = 'utf-8';
// By default, PHPMailerLite sends one mail per recipient. We handle
// multiplexing higher in the stack, so tell it to send mail exactly
// like we ask.
$this->mailer->SingleTo = false;
}
public function supportsMessageIDHeader() {
return true;
}
public function setFrom($email, $name = '') {
$this->mailer->SetFrom($email, $name, $crazy_side_effects = false);
return $this;
}
public function addReplyTo($email, $name = '') {
$this->mailer->AddReplyTo($email, $name);
return $this;
}
public function addTos(array $emails) {
foreach ($emails as $email) {
$this->mailer->AddAddress($email);
}
return $this;
}
public function addCCs(array $emails) {
foreach ($emails as $email) {
$this->mailer->AddCC($email);
}
return $this;
}
public function addAttachment($data, $filename, $mimetype) {
$this->mailer->AddStringAttachment(
$data,
$filename,
'base64',
$mimetype
);
return $this;
}
public function addHeader($header_name, $header_value) {
if (strtolower($header_name) == 'message-id') {
$this->mailer->MessageID = $header_value;
} else {
$this->mailer->AddCustomHeader($header_name.': '.$header_value);
}
return $this;
}
public function setBody($body) {
$this->mailer->Body = $body;
return $this;
}
public function setSubject($subject) {
$this->mailer->Subject = $subject;
return $this;
}
public function setIsHTML($is_html) {
$this->mailer->IsHTML($is_html);
return $this;
}
public function hasValidRecipients() {
return true;
}
public function send() {
return $this->mailer->Send();
}
}
diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php
index c563ddef3c..7e41491ace 100644
--- a/src/applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php
@@ -1,174 +1,158 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Mail adapter that uses SendGrid's web API to deliver email.
*/
final class PhabricatorMailImplementationSendGridAdapter
extends PhabricatorMailImplementationAdapter {
private $params = array();
public function setFrom($email, $name = '') {
$this->params['from'] = $email;
$this->params['from-name'] = $name;
return $this;
}
public function addReplyTo($email, $name = '') {
if (empty($this->params['reply-to'])) {
$this->params['reply-to'] = array();
}
$this->params['reply-to'][] = array(
'email' => $email,
'name' => $name,
);
return $this;
}
public function addTos(array $emails) {
foreach ($emails as $email) {
$this->params['tos'][] = $email;
}
return $this;
}
public function addCCs(array $emails) {
foreach ($emails as $email) {
$this->params['ccs'][] = $email;
}
return $this;
}
public function addAttachment($data, $filename, $mimetype) {
if (empty($this->params['files'])) {
$this->params['files'] = array();
}
$this->params['files'][$filename] = $data;
}
public function addHeader($header_name, $header_value) {
$this->params['headers'][] = array($header_name, $header_value);
return $this;
}
public function setBody($body) {
$this->params['body'] = $body;
return $this;
}
public function setSubject($subject) {
$this->params['subject'] = $subject;
return $this;
}
public function setIsHTML($is_html) {
$this->params['is-html'] = $is_html;
return $this;
}
public function supportsMessageIDHeader() {
return false;
}
public function send() {
$user = PhabricatorEnv::getEnvConfig('sendgrid.api-user');
$key = PhabricatorEnv::getEnvConfig('sendgrid.api-key');
if (!$user || !$key) {
throw new Exception(
"Configure 'sendgrid.api-user' and 'sendgrid.api-key' to use ".
"SendGrid for mail delivery.");
}
$params = array();
$ii = 0;
foreach (idx($this->params, 'tos', array()) as $to) {
$params['to['.($ii++).']'] = $to;
}
$params['subject'] = idx($this->params, 'subject');
if (idx($this->params, 'is-html')) {
$params['html'] = idx($this->params, 'body');
} else {
$params['text'] = idx($this->params, 'body');
}
$params['from'] = idx($this->params, 'from');
if (idx($this->params, 'from-name')) {
$params['fromname'] = $this->params['from-name'];
}
if (idx($this->params, 'reply-to')) {
$replyto = $this->params['reply-to'];
// Pick off the email part, no support for the name part in this API.
$params['replyto'] = $replyto[0]['email'];
}
foreach (idx($this->params, 'files', array()) as $name => $data) {
$params['files['.$name.']'] = $data;
}
$headers = idx($this->params, 'headers', array());
// See SendGrid Support Ticket #29390; there's no explicit REST API support
// for CC right now but it works if you add a generic "Cc" header.
//
// SendGrid said this is supported:
// "You can use CC as you are trying to do there [by adding a generic
// header]. It is supported despite our limited documentation to this
// effect, I am glad you were able to figure it out regardless. ..."
if (idx($this->params, 'ccs')) {
$headers[] = array('Cc', implode(', ', $this->params['ccs']));
}
if ($headers) {
// Convert to dictionary.
$headers = ipull($headers, 1, 0);
$headers = json_encode($headers);
$params['headers'] = $headers;
}
$params['api_user'] = $user;
$params['api_key'] = $key;
$future = new HTTPSFuture(
'https://sendgrid.com/api/mail.send.json',
$params);
$future->setMethod('POST');
list($body) = $future->resolvex();
$response = json_decode($body, true);
if (!is_array($response)) {
throw new Exception("Failed to JSON decode response: {$body}");
}
if ($response['message'] !== 'success') {
$errors = implode(";", $response['errors']);
throw new Exception("Request failed with errors: {$errors}.");
}
return true;
}
}
diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php
index 9336f9fcd7..48e7acf58f 100644
--- a/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php
@@ -1,106 +1,90 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Mail adapter that doesn't actually send any email, for writing unit tests
* against.
*/
final class PhabricatorMailImplementationTestAdapter
extends PhabricatorMailImplementationAdapter {
private $guts = array();
private $config;
public function __construct(array $config = array()) {
$this->config = $config;
}
public function setFrom($email, $name = '') {
$this->guts['from'] = $email;
$this->guts['from-name'] = $name;
return $this;
}
public function addReplyTo($email, $name = '') {
if (empty($this->guts['reply-to'])) {
$this->guts['reply-to'] = array();
}
$this->guts['reply-to'][] = array(
'email' => $email,
'name' => $name,
);
return $this;
}
public function addTos(array $emails) {
foreach ($emails as $email) {
$this->guts['tos'][] = $email;
}
return $this;
}
public function addCCs(array $emails) {
foreach ($emails as $email) {
$this->guts['ccs'][] = $email;
}
return $this;
}
public function addAttachment($data, $filename, $mimetype) {
$this->guts['attachments'][] = array(
'data' => $data,
'filename' => $filename,
'mimetype' => $mimetype
);
return $this;
}
public function addHeader($header_name, $header_value) {
$this->guts['headers'][] = array($header_name, $header_value);
return $this;
}
public function setBody($body) {
$this->guts['body'] = $body;
return $this;
}
public function setSubject($subject) {
$this->guts['subject'] = $subject;
return $this;
}
public function setIsHTML($is_html) {
$this->guts['is-html'] = $is_html;
return $this;
}
public function supportsMessageIDHeader() {
return $this->config['supportsMessageIDHeader'];
}
public function send() {
$this->guts['did-send'] = true;
return true;
}
public function getGuts() {
return $this->guts;
}
}
diff --git a/src/applications/metamta/application/PhabricatorApplicationMetaMTA.php b/src/applications/metamta/application/PhabricatorApplicationMetaMTA.php
index ae27f27ca6..af68ecd302 100644
--- a/src/applications/metamta/application/PhabricatorApplicationMetaMTA.php
+++ b/src/applications/metamta/application/PhabricatorApplicationMetaMTA.php
@@ -1,58 +1,42 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationMetaMTA extends PhabricatorApplication {
public function getBaseURI() {
return '/mail/';
}
public function getShortDescription() {
return 'View Mail Logs';
}
public function getAutospriteName() {
return 'mail';
}
public function getFlavorText() {
return pht('Yo dawg, we heard you like MTAs.');
}
public function getApplicationGroup() {
return self::GROUP_ADMIN;
}
public function getRoutes() {
return array(
$this->getBaseURI() => array(
'' => 'PhabricatorMetaMTAListController',
'send/' => 'PhabricatorMetaMTASendController',
'view/(?P<id>[1-9]\d*)/' => 'PhabricatorMetaMTAViewController',
'receive/' => 'PhabricatorMetaMTAReceiveController',
'received/' => 'PhabricatorMetaMTAReceivedListController',
'sendgrid/' => 'PhabricatorMetaMTASendGridReceiveController',
),
);
}
public function getTitleGlyph() {
return '@';
}
}
diff --git a/src/applications/metamta/constants/MetaMTAConstants.php b/src/applications/metamta/constants/MetaMTAConstants.php
index 15b55adaba..036e8b2880 100644
--- a/src/applications/metamta/constants/MetaMTAConstants.php
+++ b/src/applications/metamta/constants/MetaMTAConstants.php
@@ -1,21 +1,5 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class MetaMTAConstants {
}
diff --git a/src/applications/metamta/constants/MetaMTANotificationType.php b/src/applications/metamta/constants/MetaMTANotificationType.php
index c9af0d600c..a50b9d0f41 100644
--- a/src/applications/metamta/constants/MetaMTANotificationType.php
+++ b/src/applications/metamta/constants/MetaMTANotificationType.php
@@ -1,38 +1,22 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class MetaMTANotificationType
extends MetaMTAConstants {
const TYPE_DIFFERENTIAL_REVIEWERS = 'differential-reviewers';
const TYPE_DIFFERENTIAL_CLOSED = 'differential-committed';
const TYPE_DIFFERENTIAL_CC = 'differential-cc';
const TYPE_DIFFERENTIAL_COMMENT = 'differential-comment';
const TYPE_DIFFERENTIAL_UPDATED = 'differential-updated';
const TYPE_DIFFERENTIAL_REVIEW_REQUEST = 'differential-review-request';
const TYPE_DIFFERENTIAL_OTHER = 'differential-other';
const TYPE_MANIPHEST_STATUS = 'maniphest-status';
const TYPE_MANIPHEST_OWNER = 'maniphest-owner';
const TYPE_MANIPHEST_PRIORITY = 'maniphest-priority';
const TYPE_MANIPHEST_CC = 'maniphest-cc';
const TYPE_MANIPHEST_PROJECTS = 'maniphest-projects';
const TYPE_MANIPHEST_COMMENT = 'maniphest-comment';
const TYPE_MANIPHEST_OTHER = 'maniphest-other';
}
diff --git a/src/applications/metamta/contentsource/PhabricatorContentSource.php b/src/applications/metamta/contentsource/PhabricatorContentSource.php
index c190df5e8e..aae5d4291d 100644
--- a/src/applications/metamta/contentsource/PhabricatorContentSource.php
+++ b/src/applications/metamta/contentsource/PhabricatorContentSource.php
@@ -1,76 +1,60 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorContentSource {
const SOURCE_UNKNOWN = 'unknown';
const SOURCE_WEB = 'web';
const SOURCE_EMAIL = 'email';
const SOURCE_CONDUIT = 'conduit';
const SOURCE_MOBILE = 'mobile';
const SOURCE_TABLET = 'tablet';
const SOURCE_FAX = 'fax';
private $source;
private $params = array();
private function __construct() {
// <empty>
}
public static function newForSource($source, array $params) {
$obj = new PhabricatorContentSource();
$obj->source = $source;
$obj->params = $params;
return $obj;
}
public static function newFromSerialized($serialized) {
$dict = json_decode($serialized, true);
if (!is_array($dict)) {
$dict = array();
}
$obj = new PhabricatorContentSource();
$obj->source = idx($dict, 'source', self::SOURCE_UNKNOWN);
$obj->params = idx($dict, 'params', array());
return $obj;
}
public function serialize() {
return json_encode(array(
'source' => $this->getSource(),
'params' => $this->getParams(),
));
}
public function getSource() {
return $this->source;
}
public function getParams() {
return $this->params;
}
public function getParam($key, $default = null) {
return idx($this->params, $key, $default);
}
}
diff --git a/src/applications/metamta/contentsource/PhabricatorContentSourceView.php b/src/applications/metamta/contentsource/PhabricatorContentSourceView.php
index 5ad1f0d297..b353842371 100644
--- a/src/applications/metamta/contentsource/PhabricatorContentSourceView.php
+++ b/src/applications/metamta/contentsource/PhabricatorContentSourceView.php
@@ -1,62 +1,46 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorContentSourceView extends AphrontView {
private $contentSource;
private $user;
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function render() {
require_celerity_resource('phabricator-content-source-view-css');
$map = array(
PhabricatorContentSource::SOURCE_WEB => 'Web',
PhabricatorContentSource::SOURCE_CONDUIT => 'Conduit',
PhabricatorContentSource::SOURCE_EMAIL => 'Email',
PhabricatorContentSource::SOURCE_MOBILE => 'Mobile',
PhabricatorContentSource::SOURCE_TABLET => 'Tablet',
PhabricatorContentSource::SOURCE_FAX => 'Fax',
);
$source = $this->contentSource->getSource();
$type = idx($map, $source, null);
if (!$type) {
return;
}
return phutil_render_tag(
'span',
array(
'class' => "phabricator-content-source-view",
),
"Via {$type}");
}
}
diff --git a/src/applications/metamta/controller/PhabricatorMetaMTAController.php b/src/applications/metamta/controller/PhabricatorMetaMTAController.php
index a1c700ac7c..46b1b1b1aa 100644
--- a/src/applications/metamta/controller/PhabricatorMetaMTAController.php
+++ b/src/applications/metamta/controller/PhabricatorMetaMTAController.php
@@ -1,44 +1,28 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorMetaMTAController extends PhabricatorController {
public function shouldRequireAdmin() {
return true;
}
public function buildSideNavView() {
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
$nav->addLabel('Mail Logs');
$nav->addFilter('sent', 'Sent Mail', $this->getApplicationURI());
$nav->addFilter('received', 'Received Mail');
$nav->addSpacer();
if ($this->getRequest()->getUser()->getIsAdmin()) {
$nav->addLabel('Diagnostics');
$nav->addFilter('send', 'Send Test');
$nav->addFilter('receive', 'Receive Test');
}
return $nav;
}
}
diff --git a/src/applications/metamta/controller/PhabricatorMetaMTAListController.php b/src/applications/metamta/controller/PhabricatorMetaMTAListController.php
index b716b118a7..bcd6897ea0 100644
--- a/src/applications/metamta/controller/PhabricatorMetaMTAListController.php
+++ b/src/applications/metamta/controller/PhabricatorMetaMTAListController.php
@@ -1,135 +1,119 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorMetaMTAListController
extends PhabricatorMetaMTAController {
public function processRequest() {
// Get a page of mails together with pager.
$request = $this->getRequest();
$user = $request->getUser();
$offset = $request->getInt('offset', 0);
$related_phid = $request->getStr('phid');
$status = $request->getStr('status');
$pager = new AphrontPagerView();
$pager->setOffset($offset);
$pager->setURI($request->getRequestURI(), 'offset');
$mail = new PhabricatorMetaMTAMail();
$conn_r = $mail->establishConnection('r');
$wheres = array();
if ($status) {
$wheres[] = qsprintf(
$conn_r,
'status = %s',
$status);
}
if ($related_phid) {
$wheres[] = qsprintf(
$conn_r,
'relatedPHID = %s',
$related_phid);
}
if (count($wheres)) {
$where_clause = 'WHERE '.implode($wheres, ' AND ');
} else {
$where_clause = 'WHERE 1 = 1';
}
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T
%Q
ORDER BY id DESC
LIMIT %d, %d',
$mail->getTableName(),
$where_clause,
$pager->getOffset(), $pager->getPageSize() + 1);
$data = $pager->sliceResults($data);
$mails = $mail->loadAllFromArray($data);
// Render the details table.
$rows = array();
foreach ($mails as $mail) {
$next_retry = $mail->getNextRetry() - time();
if ($next_retry <= 0) {
$next_retry = "None";
} else {
$next_retry = phabricator_format_relative_time_detailed($next_retry);
}
$rows[] = array(
PhabricatorMetaMTAMail::getReadableStatus($mail->getStatus()),
$mail->getRetryCount(),
$next_retry,
phabricator_datetime($mail->getDateCreated(), $user),
phabricator_format_relative_time_detailed(
time() - $mail->getDateModified()),
phutil_escape_html($mail->getSubject()),
phutil_render_tag(
'a',
array(
'class' => 'button small grey',
'href' => $this->getApplicationURI('/view/'.$mail->getID().'/'),
),
'View'),
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Status',
'Retry',
'Next',
'Created',
'Updated',
'Subject',
'',
));
$table->setColumnClasses(
array(
null,
null,
null,
null,
null,
'wide',
'action',
));
// Render the whole page.
$panel = new AphrontPanelView();
$panel->appendChild($table);
$panel->setHeader('MetaMTA Messages');
$panel->appendChild($pager);
$nav = $this->buildSideNavView();
$nav->selectFilter('sent');
$nav->appendChild($panel);
return $this->buildApplicationPage(
$nav,
array(
'title' => 'Sent Mail',
));
}
}
diff --git a/src/applications/metamta/controller/PhabricatorMetaMTAReceiveController.php b/src/applications/metamta/controller/PhabricatorMetaMTAReceiveController.php
index 0a80ab4d88..6afb4b3c17 100644
--- a/src/applications/metamta/controller/PhabricatorMetaMTAReceiveController.php
+++ b/src/applications/metamta/controller/PhabricatorMetaMTAReceiveController.php
@@ -1,94 +1,78 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorMetaMTAReceiveController
extends PhabricatorMetaMTAController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
if ($request->isFormPost()) {
$receiver = PhabricatorMetaMTAReceivedMail::loadReceiverObject(
$request->getStr('obj'));
if (!$receiver) {
throw new Exception("No such task or revision!");
}
$hash = PhabricatorMetaMTAReceivedMail::computeMailHash(
$receiver->getMailKey(),
$user->getPHID());
$received = new PhabricatorMetaMTAReceivedMail();
$received->setHeaders(
array(
'to' => $request->getStr('obj').'+'.$user->getID().'+'.$hash.'@',
));
$received->setBodies(
array(
'text' => $request->getStr('body'),
));
$received->save();
$received->processReceivedMail();
$phid = $receiver->getPHID();
$handles = $this->loadViewerHandles(array($phid));
$uri = $handles[$phid]->getURI();
return id(new AphrontRedirectResponse())->setURI($uri);
}
$form = new AphrontFormView();
$form->setUser($request->getUser());
$form->setAction($this->getApplicationURI('/receive/'));
$form
->appendChild(
'<p class="aphront-form-instructions">This form will simulate '.
'sending mail to an object.</p>')
->appendChild(
id(new AphrontFormTextControl())
->setLabel('To')
->setName('obj')
->setCaption('e.g. <tt>D1234</tt> or <tt>T1234</tt>'))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Body')
->setName('body'))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Receive Mail'));
$panel = new AphrontPanelView();
$panel->setHeader('Receive Email');
$panel->appendChild($form);
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$nav = $this->buildSideNavView();
$nav->selectFilter('receive');
$nav->appendChild($panel);
return $this->buildApplicationPage(
$nav,
array(
'title' => 'Receive Test',
));
}
}
diff --git a/src/applications/metamta/controller/PhabricatorMetaMTAReceivedListController.php b/src/applications/metamta/controller/PhabricatorMetaMTAReceivedListController.php
index b79d698def..926bfb0be4 100644
--- a/src/applications/metamta/controller/PhabricatorMetaMTAReceivedListController.php
+++ b/src/applications/metamta/controller/PhabricatorMetaMTAReceivedListController.php
@@ -1,96 +1,80 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorMetaMTAReceivedListController
extends PhabricatorMetaMTAController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$pager = new AphrontPagerView();
$pager->setOffset($request->getInt('page'));
$pager->setURI($request->getRequestURI(), 'page');
$mails = id(new PhabricatorMetaMTAReceivedMail())->loadAllWhere(
'1 = 1 ORDER BY id DESC LIMIT %d, %d',
$pager->getOffset(),
$pager->getPageSize() + 1);
$mails = $pager->sliceResults($mails);
$phids = array_merge(
mpull($mails, 'getAuthorPHID'),
mpull($mails, 'getRelatedPHID')
);
$phids = array_unique(array_filter($phids));
$handles = $this->loadViewerHandles($phids);
$rows = array();
foreach ($mails as $mail) {
$rows[] = array(
$mail->getID(),
phabricator_date($mail->getDateCreated(), $user),
phabricator_time($mail->getDateCreated(), $user),
$mail->getAuthorPHID()
? $handles[$mail->getAuthorPHID()]->renderLink()
: '-',
$mail->getRelatedPHID()
? $handles[$mail->getRelatedPHID()]->renderLink()
: '-',
phutil_escape_html($mail->getMessage()),
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'ID',
'Date',
'Time',
'Author',
'Object',
'Message',
));
$table->setColumnClasses(
array(
null,
null,
'right',
null,
null,
'wide',
));
$panel = new AphrontPanelView();
$panel->setHeader('Received Mail');
$panel->appendChild($table);
$panel->appendChild($pager);
$nav = $this->buildSideNavView();
$nav->selectFilter('received');
$nav->appendChild($panel);
return $this->buildApplicationPage(
$nav,
array(
'title' => 'Received Mail',
));
}
}
diff --git a/src/applications/metamta/controller/PhabricatorMetaMTASendController.php b/src/applications/metamta/controller/PhabricatorMetaMTASendController.php
index 280671aba5..ae3b1afc68 100644
--- a/src/applications/metamta/controller/PhabricatorMetaMTASendController.php
+++ b/src/applications/metamta/controller/PhabricatorMetaMTASendController.php
@@ -1,188 +1,172 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorMetaMTASendController
extends PhabricatorMetaMTAController {
public function processRequest() {
$request = $this->getRequest();
if ($request->isFormPost()) {
$mail = new PhabricatorMetaMTAMail();
$mail->addTos($request->getArr('to'));
$mail->addCCs($request->getArr('cc'));
$mail->setSubject($request->getStr('subject'));
$mail->setBody($request->getStr('body'));
$files = $request->getArr('files');
if ($files) {
foreach ($files as $phid) {
$file = id(new PhabricatorFile())->loadOneWhere('phid = %s', $phid);
$mail->addAttachment(new PhabricatorMetaMTAAttachment(
$file->loadFileData(),
$file->getName(),
$file->getMimeType()
));
}
}
$mail->setFrom($request->getUser()->getPHID());
$mail->setSimulatedFailureCount($request->getInt('failures'));
$mail->setIsHTML($request->getInt('html'));
$mail->setIsBulk($request->getInt('bulk'));
$mail->setMailTags($request->getStrList('mailtags'));
$mail->save();
if ($request->getInt('immediately')) {
$mail->sendNow();
}
return id(new AphrontRedirectResponse())
->setURI($this->getApplicationURI('/view/'.$mail->getID().'/'));
}
$failure_caption =
"Enter a number to simulate that many consecutive send failures before ".
"really attempting to deliver via the underlying MTA.";
$doclink_href = PhabricatorEnv::getDoclink(
'article/Configuring_Outbound_Email.html');
$doclink = phutil_render_tag(
'a',
array(
'href' => $doclink_href,
'target' => '_blank',
),
'Configuring Outbound Email');
$instructions =
'<p class="aphront-form-instructions">This form will send a normal '.
'email using the settings you have configured for Phabricator. For more '.
'information, see '.$doclink.'.</p>';
$adapter = PhabricatorEnv::getEnvConfig('metamta.mail-adapter');
$warning = null;
if ($adapter == 'PhabricatorMailImplementationTestAdapter') {
$warning = new AphrontErrorView();
$warning->setTitle('Email is Disabled');
$warning->setSeverity(AphrontErrorView::SEVERITY_WARNING);
$warning->appendChild(
'<p>This installation of Phabricator is currently set to use '.
'<tt>PhabricatorMailImplementationTestAdapter</tt> to deliver '.
'outbound email. This completely disables outbound email! All '.
'outbound email will be thrown in a deep, dark hole until you '.
'configure a real adapter.</p>');
}
$phdlink_href = PhabricatorEnv::getDoclink(
'article/Managing_Daemons_with_phd.html');
$phdlink = phutil_render_tag(
'a',
array(
'href' => $phdlink_href,
'target' => '_blank',
),
'"phd start"');
$form = new AphrontFormView();
$form->setUser($request->getUser());
$form
->appendChild($instructions)
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Adapter')
->setValue($adapter))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel('To')
->setName('to')
->setDatasource('/typeahead/common/mailable/'))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel('CC')
->setName('cc')
->setDatasource('/typeahead/common/mailable/'))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Subject')
->setName('subject'))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Body')
->setName('body'))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Mail Tags')
->setName('mailtags')
->setCaption(
'Example: <tt>differential-cc, differential-comment</tt>'))
->appendChild(
id(new AphrontFormDragAndDropUploadControl())
->setLabel('Attach Files')
->setName('files')
->setActivatedClass('aphront-panel-view-drag-and-drop'))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Simulate Failures')
->setName('failures')
->setCaption($failure_caption))
->appendChild(
id(new AphrontFormCheckboxControl())
->setLabel('HTML')
->addCheckbox('html', '1', 'Send as HTML email.'))
->appendChild(
id(new AphrontFormCheckboxControl())
->setLabel('Bulk')
->addCheckbox('bulk', '1', 'Send with bulk email headers.'))
->appendChild(
id(new AphrontFormCheckboxControl())
->setLabel('Send Now')
->addCheckbox(
'immediately',
'1',
'Send immediately. (Do not enqueue for daemons.)',
PhabricatorEnv::getEnvConfig('metamta.send-immediately'))
->setCaption('Daemons can be started with '.$phdlink.'.')
)
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Send Mail'));
$panel = new AphrontPanelView();
$panel->setHeader('Send Email');
$panel->appendChild($form);
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$nav = $this->buildSideNavView();
$nav->selectFilter('send');
$nav->appendChild(
array(
$warning,
$panel,
));
return $this->buildApplicationPage(
$nav,
array(
'title' => 'Send Test',
));
}
}
diff --git a/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php b/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php
index da6be312a7..4b81a3c08b 100644
--- a/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php
+++ b/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php
@@ -1,82 +1,66 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorMetaMTASendGridReceiveController
extends PhabricatorMetaMTAController {
public function shouldRequireLogin() {
return false;
}
public function shouldRequireAdmin() {
return false;
}
public function processRequest() {
// No CSRF for SendGrid.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$request = $this->getRequest();
$user = $request->getUser();
$raw_headers = $request->getStr('headers');
$raw_headers = explode("\n", rtrim($raw_headers));
$raw_dict = array();
foreach (array_filter($raw_headers) as $header) {
list($name, $value) = explode(':', $header, 2);
$raw_dict[$name] = ltrim($value);
}
$headers = array(
'to' => $request->getStr('to'),
'from' => $request->getStr('from'),
'subject' => $request->getStr('subject'),
) + $raw_dict;
$received = new PhabricatorMetaMTAReceivedMail();
$received->setHeaders($headers);
$received->setBodies(array(
'text' => $request->getStr('text'),
'html' => $request->getStr('from'),
));
$file_phids = array();
foreach ($_FILES as $file_raw) {
try {
$file = PhabricatorFile::newFromPHPUpload(
$file_raw,
array(
'authorPHID' => $user->getPHID(),
));
$file_phids[] = $file->getPHID();
} catch (Exception $ex) {
phlog($ex);
}
}
$received->setAttachments($file_phids);
$received->save();
$received->processReceivedMail();
$response = new AphrontWebpageResponse();
$response->setContent("Got it! Thanks, SendGrid!\n");
return $response;
}
}
diff --git a/src/applications/metamta/controller/PhabricatorMetaMTAViewController.php b/src/applications/metamta/controller/PhabricatorMetaMTAViewController.php
index ca7321b1a4..dd15b099e1 100644
--- a/src/applications/metamta/controller/PhabricatorMetaMTAViewController.php
+++ b/src/applications/metamta/controller/PhabricatorMetaMTAViewController.php
@@ -1,83 +1,67 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorMetaMTAViewController
extends PhabricatorMetaMTAController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$mail = id(new PhabricatorMetaMTAMail())->load($this->id);
if (!$mail) {
return new Aphront404Response();
}
$status = PhabricatorMetaMTAMail::getReadableStatus($mail->getStatus());
$form = new AphrontFormView();
$form->setUser($request->getUser());
$form
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Subject')
->setValue($mail->getSubject()))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Created')
->setValue(phabricator_datetime($mail->getDateCreated(), $user)))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Status')
->setValue($status))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Retry Count')
->setValue($mail->getRetryCount()))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Message')
->setValue($mail->getMessage()))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Related PHID')
->setValue($mail->getRelatedPHID()))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($this->getApplicationURI(), 'Done'));
$panel = new AphrontPanelView();
$panel->setHeader('View Email');
$panel->appendChild($form);
$panel->setWidth(AphrontPanelView::WIDTH_WIDE);
return $this->buildApplicationPage(
$panel,
array(
'title' => 'View Mail',
));
}
}
diff --git a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php
index 7ea011f1a9..83c2742020 100644
--- a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php
+++ b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php
@@ -1,332 +1,316 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorMailReplyHandler {
private $mailReceiver;
private $actor;
private $excludePHIDs = array();
final public function setMailReceiver($mail_receiver) {
$this->validateMailReceiver($mail_receiver);
$this->mailReceiver = $mail_receiver;
return $this;
}
final public function getMailReceiver() {
return $this->mailReceiver;
}
final public function setActor(PhabricatorUser $actor) {
$this->actor = $actor;
return $this;
}
final public function getActor() {
return $this->actor;
}
final public function setExcludeMailRecipientPHIDs(array $exclude) {
$this->excludePHIDs = $exclude;
return $this;
}
final public function getExcludeMailRecipientPHIDs() {
return $this->excludePHIDs;
}
abstract public function validateMailReceiver($mail_receiver);
abstract public function getPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle);
abstract public function getReplyHandlerDomain();
abstract public function getReplyHandlerInstructions();
abstract protected function receiveEmail(
PhabricatorMetaMTAReceivedMail $mail);
public function processEmail(PhabricatorMetaMTAReceivedMail $mail) {
$error = $this->sanityCheckEmail($mail);
if ($error) {
if ($this->shouldSendErrorEmail($mail)) {
$this->sendErrorEmail($error, $mail);
}
return null;
}
return $this->receiveEmail($mail);
}
private function sanityCheckEmail(PhabricatorMetaMTAReceivedMail $mail) {
$body = $mail->getCleanTextBody();
$attachments = $mail->getAttachments();
if (empty($body) && empty($attachments)) {
return 'Empty email body. Email should begin with an !action and / or '.
'text to comment. Inline replies and signatures are ignored.';
}
return null;
}
/**
* Only send an error email if the user is talking to just Phabricator. We
* can assume if there is only one To address it is a Phabricator address
* since this code is running and everything.
*/
private function shouldSendErrorEmail(PhabricatorMetaMTAReceivedMail $mail) {
return count($mail->getToAddresses() == 1) &&
count($mail->getCCAddresses() == 0);
}
private function sendErrorEmail($error,
PhabricatorMetaMTAReceivedMail $mail) {
$template = new PhabricatorMetaMTAMail();
$template->setSubject('Exception: unable to process your mail request');
$template->setBody($this->buildErrorMailBody($error, $mail));
$template->setRelatedPHID($mail->getRelatedPHID());
$phid = $this->getActor()->getPHID();
$tos = array(
$phid => PhabricatorObjectHandleData::loadOneHandle($phid)
);
$mails = $this->multiplexMail($template, $tos, array());
foreach ($mails as $email) {
$email->saveAndSend();
}
return true;
}
private function buildErrorMailBody($error,
PhabricatorMetaMTAReceivedMail $mail) {
$original_body = $mail->getRawTextBody();
$main_body = <<<EOBODY
Your request failed because an error was encoutered while processing it:
ERROR: {$error}
-- Original Body -------------------------------------------------------------
{$original_body}
EOBODY;
$body = new PhabricatorMetaMTAMailBody();
$body->addRawSection($main_body);
$body->addReplySection($this->getReplyHandlerInstructions());
return $body->render();
}
public function supportsPrivateReplies() {
return (bool)$this->getReplyHandlerDomain() &&
!$this->supportsPublicReplies();
}
public function supportsPublicReplies() {
if (!PhabricatorEnv::getEnvConfig('metamta.public-replies')) {
return false;
}
if (!$this->getReplyHandlerDomain()) {
return false;
}
return (bool)$this->getPublicReplyHandlerEmailAddress();
}
final public function supportsReplies() {
return $this->supportsPrivateReplies() ||
$this->supportsPublicReplies();
}
public function getPublicReplyHandlerEmailAddress() {
return null;
}
final public function getRecipientsSummary(
array $to_handles,
array $cc_handles) {
assert_instances_of($to_handles, 'PhabricatorObjectHandle');
assert_instances_of($cc_handles, 'PhabricatorObjectHandle');
$body = '';
if (PhabricatorEnv::getEnvConfig('metamta.recipients.show-hints')) {
if ($to_handles) {
$body .= "To: ".implode(', ', mpull($to_handles, 'getName'))."\n";
}
if ($cc_handles) {
$body .= "Cc: ".implode(', ', mpull($cc_handles, 'getName'))."\n";
}
}
return $body;
}
final public function multiplexMail(
PhabricatorMetaMTAMail $mail_template,
array $to_handles,
array $cc_handles) {
assert_instances_of($to_handles, 'PhabricatorObjectHandle');
assert_instances_of($cc_handles, 'PhabricatorObjectHandle');
$result = array();
// If MetaMTA is configured to always multiplex, skip the single-email
// case.
if (!PhabricatorMetaMTAMail::shouldMultiplexAllMail()) {
// If private replies are not supported, simply send one email to all
// recipients and CCs. This covers cases where we have no reply handler,
// or we have a public reply handler.
if (!$this->supportsPrivateReplies()) {
$mail = clone $mail_template;
$mail->addTos(mpull($to_handles, 'getPHID'));
$mail->addCCs(mpull($cc_handles, 'getPHID'));
if ($this->supportsPublicReplies()) {
$reply_to = $this->getPublicReplyHandlerEmailAddress();
$mail->setReplyTo($reply_to);
}
$result[] = $mail;
return $result;
}
}
$tos = mpull($to_handles, null, 'getPHID');
$ccs = mpull($cc_handles, null, 'getPHID');
// Merge all the recipients together. TODO: We could keep the CCs as real
// CCs and send to a "noreply@domain.com" type address, but keep it simple
// for now.
$recipients = $tos + $ccs;
// When multiplexing mail, explicitly include To/Cc information in the
// message body and headers.
$mail_template = clone $mail_template;
$mail_template->addPHIDHeaders('X-Phabricator-To', array_keys($tos));
$mail_template->addPHIDHeaders('X-Phabricator-Cc', array_keys($ccs));
$body = $mail_template->getBody();
$body .= "\n";
$body .= $this->getRecipientsSummary($to_handles, $cc_handles);
foreach ($recipients as $phid => $recipient) {
$mail = clone $mail_template;
if (isset($to_handles[$phid])) {
$mail->addTos(array($phid));
} else if (isset($cc_handles[$phid])) {
$mail->addCCs(array($phid));
} else {
// not good - they should be a to or a cc
continue;
}
$mail->setBody($body);
$reply_to = null;
if (!$reply_to && $this->supportsPrivateReplies()) {
$reply_to = $this->getPrivateReplyHandlerEmailAddress($recipient);
}
if (!$reply_to && $this->supportsPublicReplies()) {
$reply_to = $this->getPublicReplyHandlerEmailAddress();
}
if ($reply_to) {
$mail->setReplyTo($reply_to);
}
$result[] = $mail;
}
return $result;
}
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());
$address = "{$prefix}{$receiver_id}+public+{$hash}@{$domain}";
return $this->getSingleReplyHandlerPrefix($address);
}
protected function getSingleReplyHandlerPrefix($address) {
$single_handle_prefix = PhabricatorEnv::getEnvConfig(
'metamta.single-reply-handler-prefix');
return ($single_handle_prefix)
? $single_handle_prefix . '+' . $address
: $address;
}
protected function getDefaultPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle,
$prefix) {
if ($handle->getType() != PhabricatorPHIDConstants::PHID_TYPE_USER) {
// You must be a real user to get a private reply handler address.
return null;
}
$receiver = $this->getMailReceiver();
$receiver_id = $receiver->getID();
$user_id = $handle->getAlternateID();
$hash = PhabricatorMetaMTAReceivedMail::computeMailHash(
$receiver->getMailKey(),
$handle->getPHID());
$domain = $this->getReplyHandlerDomain();
$address = "{$prefix}{$receiver_id}+{$user_id}+{$hash}@{$domain}";
return $this->getSingleReplyHandlerPrefix($address);
}
final protected function enhanceBodyWithAttachments($body,
array $attachments) {
if (!$attachments) {
return $body;
}
$files = id(new PhabricatorFile())
->loadAllWhere('phid in (%Ls)', $attachments);
// if we have some text then double return before adding our file list
if ($body) {
$body .= "\n\n";
}
foreach ($files as $file) {
$file_str = sprintf('- {F%d, layout=link}', $file->getID());
$body .= $file_str."\n";
}
return rtrim($body);
}
}
diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAAttachment.php b/src/applications/metamta/storage/PhabricatorMetaMTAAttachment.php
index c9b130d48c..2c431d3cd2 100644
--- a/src/applications/metamta/storage/PhabricatorMetaMTAAttachment.php
+++ b/src/applications/metamta/storage/PhabricatorMetaMTAAttachment.php
@@ -1,56 +1,40 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorMetaMTAAttachment {
protected $data;
protected $filename;
protected $mimetype;
public function __construct($data, $filename, $mimetype) {
$this->setData($data);
$this->setFilename($filename);
$this->setMimeType($mimetype);
}
public function getData() {
return $this->data;
}
public function setData($data) {
$this->data = $data;
return $this;
}
public function getFilename() {
return $this->filename;
}
public function setFilename($filename) {
$this->filename = $filename;
return $this;
}
public function getMimeType() {
return $this->mimetype;
}
public function setMimeType($mimetype) {
$this->mimetype = $mimetype;
return $this;
}
}
diff --git a/src/applications/metamta/storage/PhabricatorMetaMTADAO.php b/src/applications/metamta/storage/PhabricatorMetaMTADAO.php
index d8fc80b604..2eae336ca5 100644
--- a/src/applications/metamta/storage/PhabricatorMetaMTADAO.php
+++ b/src/applications/metamta/storage/PhabricatorMetaMTADAO.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorMetaMTADAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'metamta';
}
}
diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php
index de0a662b14..adb2cffdb7 100644
--- a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php
+++ b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php
@@ -1,936 +1,920 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* See #394445 for an explanation of why this thing even exists.
*
* @task recipients Managing Recipients
*/
final class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO {
const STATUS_QUEUE = 'queued';
const STATUS_SENT = 'sent';
const STATUS_FAIL = 'fail';
const STATUS_VOID = 'void';
const MAX_RETRIES = 250;
const RETRY_DELAY = 5;
protected $parameters;
protected $status;
protected $message;
protected $retryCount;
protected $nextRetry;
protected $relatedPHID;
private $excludePHIDs = array();
public function __construct() {
$this->status = self::STATUS_QUEUE;
$this->retryCount = 0;
$this->nextRetry = time();
$this->parameters = array();
parent::__construct();
}
public function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'parameters' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
protected function setParam($param, $value) {
$this->parameters[$param] = $value;
return $this;
}
protected function getParam($param, $default = null) {
return idx($this->parameters, $param, $default);
}
/**
* Set tags (@{class:MetaMTANotificationType} constants) which identify the
* content of this mail in a general way. These tags are used to allow users
* to opt out of receiving certain types of mail, like updates when a task's
* projects change.
*
* @param list<const> List of @{class:MetaMTANotificationType} constants.
* @return this
*/
public function setMailTags(array $tags) {
$this->setParam('mailtags', $tags);
return $this;
}
/**
* In Gmail, conversations will be broken if you reply to a thread and the
* server sends back a response without referencing your Message-ID, even if
* it references a Message-ID earlier in the thread. To avoid this, use the
* parent email's message ID explicitly if it's available. This overwrites the
* "In-Reply-To" and "References" headers we would otherwise generate. This
* needs to be set whenever an action is triggered by an email message. See
* T251 for more details.
*
* @param string The "Message-ID" of the email which precedes this one.
* @return this
*/
public function setParentMessageID($id) {
$this->setParam('parent-message-id', $id);
return $this;
}
public function getParentMessageID() {
return $this->getParam('parent-message-id');
}
public function getSubject() {
return $this->getParam('subject');
}
public function addTos(array $phids) {
$phids = array_unique($phids);
$this->setParam('to', $phids);
return $this;
}
public function addRawTos(array $raw_email) {
$this->setParam('raw-to', $raw_email);
return $this;
}
public function addCCs(array $phids) {
$phids = array_unique($phids);
$this->setParam('cc', $phids);
return $this;
}
public function setExcludeMailRecipientPHIDs($exclude) {
$this->excludePHIDs = $exclude;
return $this;
}
private function getExcludeMailRecipientPHIDs() {
return $this->excludePHIDs;
}
public function getTranslation(array $objects) {
$default_translation = PhabricatorEnv::getEnvConfig('translation.provider');
$return = null;
$recipients = array_merge(
idx($this->parameters, 'to', array()),
idx($this->parameters, 'cc', array()));
foreach (array_select_keys($objects, $recipients) as $object) {
$translation = null;
if ($object instanceof PhabricatorUser) {
$translation = $object->getTranslation();
}
if (!$translation) {
$translation = $default_translation;
}
if ($return && $translation != $return) {
return $default_translation;
}
$return = $translation;
}
if (!$return) {
$return = $default_translation;
}
return $return;
}
public function addPHIDHeaders($name, array $phids) {
foreach ($phids as $phid) {
$this->addHeader($name, '<'.$phid.'>');
}
return $this;
}
public function addHeader($name, $value) {
$this->parameters['headers'][] = array($name, $value);
return $this;
}
public function addAttachment(PhabricatorMetaMTAAttachment $attachment) {
$this->parameters['attachments'][] = $attachment;
return $this;
}
public function getAttachments() {
return $this->getParam('attachments');
}
public function setAttachments(array $attachments) {
assert_instances_of($attachments, 'PhabricatorMetaMTAAttachment');
$this->setParam('attachments', $attachments);
return $this;
}
public function setFrom($from) {
$this->setParam('from', $from);
return $this;
}
public function setReplyTo($reply_to) {
$this->setParam('reply-to', $reply_to);
return $this;
}
public function setSubject($subject) {
$this->setParam('subject', $subject);
return $this;
}
public function setSubjectPrefix($prefix) {
$this->setParam('subject-prefix', $prefix);
return $this;
}
public function setVarySubjectPrefix($prefix) {
$this->setParam('vary-subject-prefix', $prefix);
return $this;
}
public function setBody($body) {
$this->setParam('body', $body);
return $this;
}
public function getBody() {
return $this->getParam('body');
}
public function setIsHTML($html) {
$this->setParam('is-html', $html);
return $this;
}
public function getSimulatedFailureCount() {
return nonempty($this->getParam('simulated-failures'), 0);
}
public function setSimulatedFailureCount($count) {
$this->setParam('simulated-failures', $count);
return $this;
}
public function getWorkerTaskID() {
return $this->getParam('worker-task');
}
public function setWorkerTaskID($id) {
$this->setParam('worker-task', $id);
return $this;
}
/**
* Flag that this is an auto-generated bulk message and should have bulk
* headers added to it if appropriate. Broadly, this means some flavor of
* "Precedence: bulk" or similar, but is implementation and configuration
* dependent.
*
* @param bool True if the mail is automated bulk mail.
* @return this
*/
public function setIsBulk($is_bulk) {
$this->setParam('is-bulk', $is_bulk);
return $this;
}
/**
* Use this method to set an ID used for message threading. MetaMTA will
* set appropriate headers (Message-ID, In-Reply-To, References and
* Thread-Index) based on the capabilities of the underlying mailer.
*
* @param string Unique identifier, appropriate for use in a Message-ID,
* In-Reply-To or References headers.
* @param bool If true, indicates this is the first message in the thread.
* @return this
*/
public function setThreadID($thread_id, $is_first_message = false) {
$this->setParam('thread-id', $thread_id);
$this->setParam('is-first-message', $is_first_message);
return $this;
}
/**
* Save a newly created mail to the database and attempt to send it
* immediately if the server is configured for immediate sends. When
* applications generate new mail they should generally use this method to
* deliver it. If the server doesn't use immediate sends, this has the same
* effect as calling save(): the mail will eventually be delivered by the
* MetaMTA daemon.
*
* @return this
*/
public function saveAndSend() {
$ret = null;
if (PhabricatorEnv::getEnvConfig('metamta.send-immediately')) {
$ret = $this->sendNow();
} else {
$ret = $this->save();
}
return $ret;
}
protected function didWriteData() {
parent::didWriteData();
if (!$this->getWorkerTaskID()) {
$mailer_task = PhabricatorWorker::scheduleTask(
'PhabricatorMetaMTAWorker',
$this->getID());
$this->setWorkerTaskID($mailer_task->getID());
$this->save();
}
}
public function buildDefaultMailer() {
return PhabricatorEnv::newObjectFromConfig('metamta.mail-adapter');
}
/**
* Attempt to deliver an email immediately, in this process.
*
* @param bool Try to deliver this email even if it has already been
* delivered or is in backoff after a failed delivery attempt.
* @param PhabricatorMailImplementationAdapter Use a specific mail adapter,
* instead of the default.
*
* @return void
*/
public function sendNow(
$force_send = false,
PhabricatorMailImplementationAdapter $mailer = null) {
if ($mailer === null) {
$mailer = $this->buildDefaultMailer();
}
if (!$force_send) {
if ($this->getStatus() != self::STATUS_QUEUE) {
throw new Exception("Trying to send an already-sent mail!");
}
if (time() < $this->getNextRetry()) {
throw new Exception("Trying to send an email before next retry!");
}
}
try {
$params = $this->parameters;
$phids = array();
foreach ($params as $key => $value) {
switch ($key) {
case 'to':
$params[$key] = $this->buildToList();
break;
case 'cc':
$params[$key] = $this->buildCCList();
break;
}
}
foreach ($params as $key => $value) {
switch ($key) {
case 'from':
$value = array($value);
/* fallthrough */
case 'to':
case 'cc':
foreach ($value as $phid) {
$type = phid_get_type($phid);
$phids[$phid] = $type;
}
break;
}
}
$this->loadEmailAndNameDataFromPHIDs($phids);
$default = PhabricatorEnv::getEnvConfig('metamta.default-address');
if (empty($params['from'])) {
$mailer->setFrom($default);
} else {
$from = $params['from'];
if (!PhabricatorEnv::getEnvConfig('metamta.can-send-as-user')) {
if (empty($params['reply-to'])) {
$params['reply-to'] = $phids[$from]['email'];
$params['reply-to-name'] = $phids[$from]['name'];
}
$mailer->setFrom(
$default,
$phids[$from]['name']);
unset($params['from']);
}
}
$is_first = idx($params, 'is-first-message');
unset($params['is-first-message']);
$is_threaded = (bool)idx($params, 'thread-id');
$reply_to_name = idx($params, 'reply-to-name', '');
unset($params['reply-to-name']);
$add_cc = array();
$add_to = array();
foreach ($params as $key => $value) {
switch ($key) {
case 'from':
$mailer->setFrom($phids[$from]['email']);
break;
case 'reply-to':
$mailer->addReplyTo($value, $reply_to_name);
break;
case 'to':
$to_emails = $this->filterSendable($value, $phids);
if ($to_emails) {
$add_to = array_merge($add_to, $to_emails);
}
break;
case 'raw-to':
$add_to = array_merge($add_to, $value);
break;
case 'cc':
$cc_emails = $this->filterSendable($value, $phids);
if ($cc_emails) {
$add_cc = $cc_emails;
}
break;
case 'headers':
foreach ($value as $pair) {
list($header_key, $header_value) = $pair;
// NOTE: If we have \n in a header, SES rejects the email.
$header_value = str_replace("\n", " ", $header_value);
$mailer->addHeader($header_key, $header_value);
}
break;
case 'attachments':
foreach ($value as $attachment) {
$mailer->addAttachment(
$attachment->getData(),
$attachment->getFilename(),
$attachment->getMimeType()
);
}
break;
case 'body':
$mailer->setBody($value);
break;
case 'subject':
// Only try to use preferences if everything is multiplexed, so we
// get consistent behavior.
$use_prefs = self::shouldMultiplexAllMail();
$prefs = null;
if ($use_prefs) {
// If multiplexing is enabled, some recipients will be in "Cc"
// rather than "To". We'll move them to "To" later (or supply a
// dummy "To") but need to look for the recipient in either the
// "To" or "Cc" fields here.
$target_phid = head(idx($params, 'to', array()));
if (!$target_phid) {
$target_phid = head(idx($params, 'cc', array()));
}
if ($target_phid) {
$user = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$target_phid);
if ($user) {
$prefs = $user->loadPreferences();
}
}
}
$subject = array();
if ($is_threaded) {
$add_re = PhabricatorEnv::getEnvConfig('metamta.re-prefix');
if ($prefs) {
$add_re = $prefs->getPreference(
PhabricatorUserPreferences::PREFERENCE_RE_PREFIX,
$add_re);
}
if ($add_re) {
$subject[] = 'Re:';
}
}
$subject[] = trim(idx($params, 'subject-prefix'));
$vary_prefix = idx($params, 'vary-subject-prefix');
if ($vary_prefix != '') {
$use_subject = PhabricatorEnv::getEnvConfig(
'metamta.vary-subjects');
if ($prefs) {
$use_subject = $prefs->getPreference(
PhabricatorUserPreferences::PREFERENCE_VARY_SUBJECT,
$use_subject);
}
if ($use_subject) {
$subject[] = $vary_prefix;
}
}
$subject[] = $value;
$mailer->setSubject(implode(' ', array_filter($subject)));
break;
case 'is-html':
if ($value) {
$mailer->setIsHTML(true);
}
break;
case 'is-bulk':
if ($value) {
if (PhabricatorEnv::getEnvConfig('metamta.precedence-bulk')) {
$mailer->addHeader('Precedence', 'bulk');
}
}
break;
case 'thread-id':
// NOTE: Gmail freaks out about In-Reply-To and References which
// aren't in the form "<string@domain.tld>"; this is also required
// by RFC 2822, although some clients are more liberal in what they
// accept.
$domain = PhabricatorEnv::getEnvConfig('metamta.domain');
$value = '<'.$value.'@'.$domain.'>';
if ($is_first && $mailer->supportsMessageIDHeader()) {
$mailer->addHeader('Message-ID', $value);
} else {
$in_reply_to = $value;
$references = array($value);
$parent_id = $this->getParentMessageID();
if ($parent_id) {
$in_reply_to = $parent_id;
// By RFC 2822, the most immediate parent should appear last
// in the "References" header, so this order is intentional.
$references[] = $parent_id;
}
$references = implode(' ', $references);
$mailer->addHeader('In-Reply-To', $in_reply_to);
$mailer->addHeader('References', $references);
}
$thread_index = $this->generateThreadIndex($value, $is_first);
$mailer->addHeader('Thread-Index', $thread_index);
break;
case 'mailtags':
// Handled below.
break;
case 'subject-prefix':
case 'vary-subject-prefix':
// Handled above.
break;
default:
// Just discard.
}
}
if (!$add_to && !$add_cc) {
$this->setStatus(self::STATUS_VOID);
$this->setMessage(
"Message has no valid recipients: all To/CC are disabled or ".
"configured not to receive this mail.");
return $this->save();
}
$mailer->addHeader('X-Phabricator-Sent-This-Message', 'Yes');
$mailer->addHeader('X-Mail-Transport-Agent', 'MetaMTA');
// Some clients respect this to suppress OOF and other auto-responses.
$mailer->addHeader('X-Auto-Response-Suppress', 'All');
// If the message has mailtags, filter out any recipients who don't want
// to receive this type of mail.
$mailtags = $this->getParam('mailtags');
if ($mailtags) {
$tag_header = array();
foreach ($mailtags as $mailtag) {
$tag_header[] = '<'.$mailtag.'>';
}
$tag_header = implode(', ', $tag_header);
$mailer->addHeader('X-Phabricator-Mail-Tags', $tag_header);
}
// Some mailers require a valid "To:" in order to deliver mail. If we
// don't have any "To:", try to fill it in with a placeholder "To:".
// If that also fails, move the "Cc:" line to "To:".
if (!$add_to) {
$placeholder_key = 'metamta.placeholder-to-recipient';
$placeholder = PhabricatorEnv::getEnvConfig($placeholder_key);
if ($placeholder !== null) {
$add_to = array($placeholder);
} else {
$add_to = $add_cc;
$add_cc = array();
}
}
$mailer->addTos($add_to);
if ($add_cc) {
$mailer->addCCs($add_cc);
}
} catch (Exception $ex) {
$this->setStatus(self::STATUS_FAIL);
$this->setMessage($ex->getMessage());
return $this->save();
}
if ($this->getRetryCount() < $this->getSimulatedFailureCount()) {
$ok = false;
$error = 'Simulated failure.';
} else {
try {
$ok = $mailer->send();
$error = null;
} catch (Exception $ex) {
$ok = false;
$error = $ex->getMessage()."\n".$ex->getTraceAsString();
}
}
if (!$ok) {
$this->setMessage($error);
if ($this->getRetryCount() > self::MAX_RETRIES) {
$this->setStatus(self::STATUS_FAIL);
} else {
$this->setRetryCount($this->getRetryCount() + 1);
$next_retry = time() + ($this->getRetryCount() * self::RETRY_DELAY);
$this->setNextRetry($next_retry);
}
} else {
$this->setStatus(self::STATUS_SENT);
}
return $this->save();
}
public static function getReadableStatus($status_code) {
static $readable = array(
self::STATUS_QUEUE => "Queued for Delivery",
self::STATUS_FAIL => "Delivery Failed",
self::STATUS_SENT => "Sent",
self::STATUS_VOID => "Void",
);
$status_code = coalesce($status_code, '?');
return idx($readable, $status_code, $status_code);
}
private function generateThreadIndex($seed, $is_first_mail) {
// When threading, Outlook ignores the 'References' and 'In-Reply-To'
// headers that most clients use. Instead, it uses a custom 'Thread-Index'
// header. The format of this header is something like this (from
// camel-exchange-folder.c in Evolution Exchange):
/* A new post to a folder gets a 27-byte-long thread index. (The value
* is apparently unique but meaningless.) Each reply to a post gets a
* 32-byte-long thread index whose first 27 bytes are the same as the
* parent's thread index. Each reply to any of those gets a
* 37-byte-long thread index, etc. The Thread-Index header contains a
* base64 representation of this value.
*/
// The specific implementation uses a 27-byte header for the first email
// a recipient receives, and a random 5-byte suffix (32 bytes total)
// thereafter. This means that all the replies are (incorrectly) siblings,
// but it would be very difficult to keep track of the entire tree and this
// gets us reasonable client behavior.
$base = substr(md5($seed), 0, 27);
if (!$is_first_mail) {
// Not totally sure, but it seems like outlook orders replies by
// thread-index rather than timestamp, so to get these to show up in the
// right order we use the time as the last 4 bytes.
$base .= ' '.pack('N', time());
}
return base64_encode($base);
}
private function loadEmailAndNameDataFromPHIDs(array &$phids) {
$users = array();
$mlsts = array();
// first iteration - group by types to do data fetches
foreach ($phids as $phid => $type) {
switch ($type) {
case PhabricatorPHIDConstants::PHID_TYPE_USER:
$users[] = $phid;
break;
case PhabricatorPHIDConstants::PHID_TYPE_MLST:
$mlsts[] = $phid;
break;
}
}
$user_emails = array();
if ($users) {
$user_emails = id(new PhabricatorUserEmail())->loadAllWhere(
'userPHID IN (%Ls) AND isPrimary = 1', $users);
$users = id(new PhabricatorUser())->loadAllWhere(
'phid IN (%Ls)', $users);
$user_emails = mpull($user_emails, null, 'getUserPHID');
$users = mpull($users, null, 'getPHID');
}
if ($mlsts) {
$mlsts = id(new PhabricatorMetaMTAMailingList())->loadAllWhere(
'phid IN (%Ls)', $mlsts);
$mlsts = mpull($mlsts, null, 'getPHID');
}
// second iteration - create entries for each phid
$default = PhabricatorEnv::getEnvConfig('metamta.default-address');
foreach ($phids as $phid => &$value) {
$name = '';
$email = $default;
$is_mailable = false;
switch ($value) {
case PhabricatorPHIDConstants::PHID_TYPE_USER:
$user = $users[$phid];
if ($user) {
$name = $this->getUserName($user);
$is_mailable = !$user->getIsDisabled()
&& !$user->getIsSystemAgent();
}
$email = $user_emails[$phid] ?
$user_emails[$phid]->getAddress() :
$default;
break;
case PhabricatorPHIDConstants::PHID_TYPE_MLST:
$mlst = $mlsts[$phid];
if ($mlst) {
$name = $mlst->getName();
$email = $mlst->getEmail();
$is_mailable = true;
}
break;
}
$value = array(
'name' => $name,
'email' => $email,
'mailable' => $is_mailable,
);
}
}
/**
* Small helper function to make sure we format the username properly as
* specified by the `metamta.user-address-format` configuration value.
*/
private function getUserName($user) {
$format = PhabricatorEnv::getEnvConfig(
'metamta.user-address-format',
'full'
);
switch ($format) {
case 'short':
$name = $user->getUserName();
break;
case 'real':
$name = $user->getRealName();
break;
case 'full':
default:
$name = $user->getFullName();
break;
}
return $name;
}
private function filterSendable($value, $phids) {
$result = array();
foreach ($value as $phid) {
if (isset($phids[$phid]) && $phids[$phid]['mailable']) {
$result[$phid] = $phids[$phid]['email'];
}
}
return $result;
}
public static function shouldMultiplexAllMail() {
return PhabricatorEnv::getEnvConfig('metamta.one-mail-per-recipient');
}
/* -( Managing Recipients )------------------------------------------------ */
/**
* Get all of the recipients for this mail, after preference filters are
* applied. This list has all objects to whom delivery will be attempted, but
* does not exclude recipeints two whom delivery may be impossible.
*
* @return list<phid> A list of all recipients to whom delivery will be
* attempted.
* @task recipients
*/
public function buildRecipientList() {
return $this->resolveRecipients(
array_merge(
$this->getRawToPHIDs(),
$this->getRawCCPHIDs()));
}
/**
* Filter out duplicate, invalid, or excluded recipients from a PHID list.
*
* @param list<phid> Unfiltered recipients.
* @return list<phid> Filtered recipients.
*
* @task recipients
*/
private function resolveRecipients(array $phids) {
$phids = array_combine($phids, $phids);
// Exclude PHIDs explicitly marked for exclusion. We use this to prevent
// recipients of an accidental "Reply All" from receiving the followup
// mail from Phabricator.
$exclude = $this->getExcludeMailRecipientPHIDs();
$exclude = array_fill_keys($exclude, true);
$phids = array_diff_key($phids, $exclude);
// If the actor is a recipient and has configured their preferences not to
// send them mail about their own actions, drop them from the recipient
// list.
$from = $this->getParam('from');
if (isset($phids[$from])) {
$from_user = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$from);
if ($from_user) {
$pref_key = PhabricatorUserPreferences::PREFERENCE_NO_SELF_MAIL;
$exclude_self = $from_user
->loadPreferences()
->getPreference($pref_key);
if ($exclude_self) {
unset($phids[$from]);
}
}
}
// Exclude all recipients who have set preferences to not receive this type
// of email (for example, a user who says they don't want emails about task
// CC changes).
$tags = $this->getParam('mailtags');
if ($tags && $phids) {
$all_prefs = id(new PhabricatorUserPreferences())->loadAllWhere(
'userPHID in (%Ls)',
$phids);
$all_prefs = mpull($all_prefs, null, 'getUserPHID');
foreach ($phids as $phid) {
$prefs = idx($all_prefs, $phid);
if (!$prefs) {
continue;
}
$user_mailtags = $prefs->getPreference(
PhabricatorUserPreferences::PREFERENCE_MAILTAGS,
array());
// The user must have elected to receive mail for at least one
// of the mailtags.
$send = false;
foreach ($tags as $tag) {
if (idx($user_mailtags, $tag, true)) {
$send = true;
break;
}
}
if (!$send) {
unset($phids[$phid]);
}
}
}
return array_keys($phids);
}
/**
* @task recipients
*/
private function buildToList() {
return $this->resolveRecipients($this->getRawToPHIDs());
}
/**
* @task recipients
*/
private function buildCCList() {
return $this->resolveRecipients($this->getRawCCPHIDs());
}
/**
* @task recipients
*/
private function getRawToPHIDs() {
$to = $this->getParam('to', array());
return $this->filterRawPHIDList($to);
}
/**
* @task recipients
*/
private function getRawCCPHIDs() {
$cc = $this->getParam('cc', array());
return $this->filterRawPHIDList($cc);
}
/**
* @task recipients
*/
private function filterRawPHIDList(array $list) {
$list = array_filter($list);
$list = array_unique($list);
return array_values($list);
}
}
diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php
index fcd6f60bde..d8bdcaedc6 100644
--- a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php
+++ b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php
@@ -1,400 +1,384 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO {
protected $headers = array();
protected $bodies = array();
protected $attachments = array();
protected $relatedPHID;
protected $authorPHID;
protected $message;
public function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'headers' => self::SERIALIZATION_JSON,
'bodies' => self::SERIALIZATION_JSON,
'attachments' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function setHeaders(array $headers) {
// Normalize headers to lowercase.
$normalized = array();
foreach ($headers as $name => $value) {
$normalized[strtolower($name)] = $value;
}
$this->headers = $normalized;
return $this;
}
public function getMessageID() {
return idx($this->headers, 'message-id');
}
public function getSubject() {
return idx($this->headers, 'subject');
}
public function getCCAddresses() {
return $this->getRawEmailAddresses(idx($this->headers, 'cc'));
}
public function getToAddresses() {
return $this->getRawEmailAddresses(idx($this->headers, 'to'));
}
private function loadExcludeMailRecipientPHIDs() {
$addresses = array_merge(
$this->getToAddresses(),
$this->getCCAddresses()
);
return $this->loadPHIDsFromAddresses($addresses);
}
final public function loadCCPHIDs() {
return $this->loadPHIDsFromAddresses($this->getCCAddresses());
}
private function loadPHIDsFromAddresses(array $addresses) {
$users = id(new PhabricatorUserEmail())
->loadAllWhere('address IN (%Ls)', $addresses);
$user_phids = mpull($users, 'getUserPHID');
$mailing_lists = id(new PhabricatorMetaMTAMailingList())
->loadAllWhere('email in (%Ls)', $addresses);
$mailing_list_phids = mpull($mailing_lists, 'getPHID');
return array_merge($user_phids, $mailing_list_phids);
}
/**
* Parses "to" addresses, looking for a public create email address
* first and if not found parsing the "to" address for reply handler
* information: receiver name, user id, and hash.
*/
private function getPhabricatorToInformation() {
// Only one "public" create address so far
$create_task = PhabricatorEnv::getEnvConfig(
'metamta.maniphest.public-create-email');
// For replies, look for an object address with a format like:
// D291+291+b0a41ca848d66dcc@example.com
$single_handle_prefix = PhabricatorEnv::getEnvConfig(
'metamta.single-reply-handler-prefix');
$prefixPattern = ($single_handle_prefix)
? preg_quote($single_handle_prefix, '/') . '\+'
: '';
$pattern = "/^{$prefixPattern}((?:D|T|C)\d+)\+([\w]+)\+([a-f0-9]{16})@/U";
$phabricator_address = null;
$receiver_name = null;
$user_id = null;
$hash = null;
foreach ($this->getToAddresses() as $address) {
if ($address == $create_task) {
$phabricator_address = $address;
// it's okay to stop here because we just need to map a create
// address to an application and don't need / won't have more
// information in these cases.
break;
}
$matches = null;
$ok = preg_match(
$pattern,
$address,
$matches);
if ($ok) {
$phabricator_address = $address;
$receiver_name = $matches[1];
$user_id = $matches[2];
$hash = $matches[3];
break;
}
}
return array(
$phabricator_address,
$receiver_name,
$user_id,
$hash
);
}
public function processReceivedMail() {
// If Phabricator sent the mail, always drop it immediately. This prevents
// loops where, e.g., the public bug address is also a user email address
// and creating a bug sends them an email, which loops.
$is_phabricator_mail = idx(
$this->headers,
'x-phabricator-sent-this-message');
if ($is_phabricator_mail) {
$message = "Ignoring email with 'X-Phabricator-Sent-This-Message' ".
"header to avoid loops.";
return $this->setMessage($message)->save();
}
list($to,
$receiver_name,
$user_id,
$hash) = $this->getPhabricatorToInformation();
if (!$to) {
$raw_to = idx($this->headers, 'to');
return $this->setMessage("Unrecognized 'to' format: {$raw_to}")->save();
}
$from = idx($this->headers, 'from');
// TODO -- make this a switch statement / better if / when we add more
// public create email addresses!
$create_task = PhabricatorEnv::getEnvConfig(
'metamta.maniphest.public-create-email');
if ($create_task && $to == $create_task) {
$receiver = new ManiphestTask();
$user = $this->lookupPublicUser();
if ($user) {
$this->setAuthorPHID($user->getPHID());
} else {
$default_author = PhabricatorEnv::getEnvConfig(
'metamta.maniphest.default-public-author');
if ($default_author) {
$user = id(new PhabricatorUser())->loadOneWhere(
'username = %s',
$default_author);
if ($user) {
$receiver->setOriginalEmailSource($from);
} else {
throw new Exception(
"Phabricator is misconfigured, the configuration key ".
"'metamta.maniphest.default-public-author' is set to user ".
"'{$default_author}' but that user does not exist.");
}
} else {
// 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();
}
}
$receiver->setAuthorPHID($user->getPHID());
$receiver->setPriority(ManiphestTaskPriority::PRIORITY_TRIAGE);
$editor = new ManiphestTransactionEditor();
$editor->setActor($user);
$handler = $editor->buildReplyHandler($receiver);
$handler->setActor($user);
$handler->setExcludeMailRecipientPHIDs(
$this->loadExcludeMailRecipientPHIDs());
$handler->processEmail($this);
$this->setRelatedPHID($receiver->getPHID());
$this->setMessage('OK');
return $this->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()) {
return $this->setMessage("User '{$user_id}' is disabled")->save();
}
$this->setAuthorPHID($user->getPHID());
$receiver = self::loadReceiverObject($receiver_name);
if (!$receiver) {
return $this->setMessage("Invalid object '{$receiver_name}'")->save();
}
$this->setRelatedPHID($receiver->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);
// See note at computeOldMailHash().
$old_hash = self::computeOldMailHash($receiver->getMailKey(), $check_phid);
if ($expect_hash != $hash && $old_hash != $hash) {
return $this->setMessage("Invalid mail hash!")->save();
}
if ($receiver instanceof ManiphestTask) {
$editor = new ManiphestTransactionEditor();
$editor->setActor($user);
$handler = $editor->buildReplyHandler($receiver);
} else if ($receiver instanceof DifferentialRevision) {
$handler = DifferentialMail::newReplyHandlerForRevision($receiver);
} else if ($receiver instanceof PhabricatorRepositoryCommit) {
$handler = PhabricatorAuditCommentEditor::newReplyHandlerForCommit(
$receiver);
}
$handler->setActor($user);
$handler->setExcludeMailRecipientPHIDs(
$this->loadExcludeMailRecipientPHIDs());
$handler->processEmail($this);
$this->setMessage('OK');
return $this->save();
}
public function getCleanTextBody() {
$body = idx($this->bodies, 'text');
$parser = new PhabricatorMetaMTAEmailBodyParser();
return $parser->stripTextBody($body);
}
public function getRawTextBody() {
return idx($this->bodies, 'text');
}
public static function loadReceiverObject($receiver_name) {
if (!$receiver_name) {
return null;
}
$receiver_type = $receiver_name[0];
$receiver_id = substr($receiver_name, 1);
$class_obj = null;
switch ($receiver_type) {
case 'T':
$class_obj = new ManiphestTask();
break;
case 'D':
$class_obj = new DifferentialRevision();
break;
case 'C':
$class_obj = new PhabricatorRepositoryCommit();
break;
default:
return null;
}
return $class_obj->load($receiver_id);
}
public static function computeMailHash($mail_key, $phid) {
$global_mail_key = PhabricatorEnv::getEnvConfig('phabricator.mail-key');
$hash = PhabricatorHash::digest($mail_key.$global_mail_key.$phid);
return substr($hash, 0, 16);
}
public static function computeOldMailHash($mail_key, $phid) {
// TODO: Remove this method entirely in a couple of months. We've moved from
// plain sha1 to sha1+hmac to make the codebase more auditable for good uses
// of hash functions, but still accept the old hashes on email replies to
// avoid breaking things. Once we've been sending only hmac hashes for a
// while, remove this and start rejecting old hashes. See T547.
$global_mail_key = PhabricatorEnv::getEnvConfig('phabricator.mail-key');
$hash = sha1($mail_key.$global_mail_key.$phid);
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" <alincoln@logcab.in>'.
*/
private function getRawEmailAddress($address) {
$matches = null;
$ok = preg_match('/<(.*)>/', $address, $matches);
if ($ok) {
$address = $matches[1];
}
return $address;
}
private function getRawEmailAddresses($addresses) {
$raw_addresses = array();
foreach (explode(',', $addresses) as $address) {
$raw_addresses[] = $this->getRawEmailAddress($address);
}
return $raw_addresses;
}
private function lookupPublicUser() {
$from = idx($this->headers, 'from');
$from = $this->getRawEmailAddress($from);
$user = PhabricatorUser::loadOneWithEmailAddress($from);
// If Phabricator is configured to allow "Reply-To" authentication, try
// the "Reply-To" address if we failed to match the "From" address.
$config_key = 'metamta.insecure-auth-with-reply-to';
$allow_reply_to = PhabricatorEnv::getEnvConfig($config_key);
if (!$user && $allow_reply_to) {
$reply_to = idx($this->headers, 'reply-to');
$reply_to = $this->getRawEmailAddress($reply_to);
if ($reply_to) {
$user = PhabricatorUser::loadOneWithEmailAddress($reply_to);
}
}
return $user;
}
}
diff --git a/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php b/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php
index 474f6fcfe1..f309d178db 100644
--- a/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php
+++ b/src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php
@@ -1,163 +1,147 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorMetaMTAMailTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testRecipients() {
$user = $this->generateNewTestUser();
$phid = $user->getPHID();
$prefs = $user->loadPreferences();
$mailer = new PhabricatorMailImplementationTestAdapter();
$mail = new PhabricatorMetaMTAMail();
$mail->addTos(array($phid));
$this->assertEqual(
true,
in_array($phid, $mail->buildRecipientList()),
'"To" is a recipient.');
// Test that the "No Self Mail" preference works correctly.
$mail->setFrom($phid);
$this->assertEqual(
true,
in_array($phid, $mail->buildRecipientList()),
'"From" does not exclude recipients by default.');
$prefs->setPreference(
PhabricatorUserPreferences::PREFERENCE_NO_SELF_MAIL,
true);
$prefs->save();
$this->assertEqual(
false,
in_array($phid, $mail->buildRecipientList()),
'"From" excludes recipients with no-self-mail set.');
$prefs->unsetPreference(
PhabricatorUserPreferences::PREFERENCE_NO_SELF_MAIL);
$prefs->save();
$this->assertEqual(
true,
in_array($phid, $mail->buildRecipientList()),
'"From" does not exclude recipients by default.');
// Test that explicit exclusion works correctly.
$mail->setExcludeMailRecipientPHIDs(array($phid));
$this->assertEqual(
false,
in_array($phid, $mail->buildRecipientList()),
'Explicit exclude excludes recipients.');
$mail->setExcludeMailRecipientPHIDs(array());
// Test that mail tag preferences exclude recipients.
$prefs->setPreference(
PhabricatorUserPreferences::PREFERENCE_MAILTAGS,
array(
'test-tag' => false,
));
$prefs->save();
$mail->setMailTags(array('test-tag'));
$this->assertEqual(
false,
in_array($phid, $mail->buildRecipientList()),
'Tag preference excludes recipients.');
$prefs->unsetPreference(PhabricatorUserPreferences::PREFERENCE_MAILTAGS);
$prefs->save();
$this->assertEqual(
true,
in_array($phid, $mail->buildRecipientList()),
'Recipients restored after tag preference removed.');
}
public function testThreadIDHeaders() {
$this->runThreadIDHeadersWithConfiguration(true, true);
$this->runThreadIDHeadersWithConfiguration(true, false);
$this->runThreadIDHeadersWithConfiguration(false, true);
$this->runThreadIDHeadersWithConfiguration(false, false);
}
private function runThreadIDHeadersWithConfiguration(
$supports_message_id,
$is_first_mail) {
$mailer = new PhabricatorMailImplementationTestAdapter(
array(
'supportsMessageIDHeader' => $supports_message_id,
));
$thread_id = '<somethread-12345@somedomain.tld>';
$mail = new PhabricatorMetaMTAMail();
$mail->setThreadID($thread_id, $is_first_mail);
$mail->sendNow($force = true, $mailer);
$guts = $mailer->getGuts();
$dict = ipull($guts['headers'], 1, 0);
if ($is_first_mail && $supports_message_id) {
$expect_message_id = true;
$expect_in_reply_to = false;
$expect_references = false;
} else {
$expect_message_id = false;
$expect_in_reply_to = true;
$expect_references = true;
}
$case = "<message-id = ".($supports_message_id ? 'Y' : 'N').", ".
"first = ".($is_first_mail ? 'Y' : 'N').">";
$this->assertEqual(
true,
isset($dict['Thread-Index']),
"Expect Thread-Index header for case {$case}.");
$this->assertEqual(
$expect_message_id,
isset($dict['Message-ID']),
"Expectation about existence of Message-ID header for case {$case}.");
$this->assertEqual(
$expect_in_reply_to,
isset($dict['In-Reply-To']),
"Expectation about existence of In-Reply-To header for case {$case}.");
$this->assertEqual(
$expect_references,
isset($dict['References']),
"Expectation about existence of References header for case {$case}.");
}
}
diff --git a/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php b/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php
index 556a0d0448..3cc23d35db 100644
--- a/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php
+++ b/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php
@@ -1,136 +1,120 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Render the body of an application email by building it up section-by-section.
*
* @task compose Composition
* @task render Rendering
* @group metamta
*/
final class PhabricatorMetaMTAMailBody {
private $sections = array();
/* -( Composition )-------------------------------------------------------- */
/**
* Add a raw block of text to the email. This will be rendered as-is.
*
* @param string Block of text.
* @return this
* @task compose
*/
public function addRawSection($text) {
if (strlen($text)) {
$this->sections[] = rtrim($text);
}
return $this;
}
/**
* Add a block of text with a section header. This is rendered like this:
*
* HEADER
* Text is indented.
*
* @param string Header text.
* @param string Section text.
* @return this
* @task compose
*/
public function addTextSection($header, $text) {
$this->sections[] = $header."\n".$this->indent($text);
return $this;
}
/**
* Add a Herald section with a rule management URI and a transcript URI.
*
* @param string URI to rule management.
* @param string URI to rule transcripts.
* @return this
* @task compose
*/
public function addHeraldSection($rules_uri, $xscript_uri) {
if (!PhabricatorEnv::getEnvConfig('metamta.herald.show-hints')) {
return $this;
}
$this->addTextSection(
pht('MANAGE HERALD RULES'),
PhabricatorEnv::getProductionURI($rules_uri));
$this->addTextSection(
pht('WHY DID I GET THIS EMAIL?'),
PhabricatorEnv::getProductionURI($xscript_uri));
return $this;
}
/**
* Add a section with reply handler instructions.
*
* @param string Reply handler instructions.
* @return this
* @task compose
*/
public function addReplySection($instructions) {
if (!PhabricatorEnv::getEnvConfig('metamta.reply.show-hints')) {
return $this;
}
if (!strlen($instructions)) {
return $this;
}
$this->addTextSection(pht('REPLY HANDLER ACTIONS'), $instructions);
return $this;
}
/* -( Rendering )---------------------------------------------------------- */
/**
* Render the email body.
*
* @return string Rendered body.
* @task render
*/
public function render() {
return implode("\n\n", $this->sections)."\n";
}
/**
* Indent a block of text for rendering under a section heading.
*
* @param string Text to indent.
* @return string Indented text.
* @task render
*/
private function indent($text) {
return rtrim(" ".str_replace("\n", "\n ", $text));
}
}
diff --git a/src/applications/metamta/view/__tests__/PhabricatorMetaMTAMailBodyTestCase.php b/src/applications/metamta/view/__tests__/PhabricatorMetaMTAMailBodyTestCase.php
index 4c9f0cf08c..e09a7737a5 100644
--- a/src/applications/metamta/view/__tests__/PhabricatorMetaMTAMailBodyTestCase.php
+++ b/src/applications/metamta/view/__tests__/PhabricatorMetaMTAMailBodyTestCase.php
@@ -1,100 +1,84 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group metamta
*/
final class PhabricatorMetaMTAMailBodyTestCase extends PhabricatorTestCase {
public function testBodyRender() {
$expect = <<<EOTEXT
salmon
HEADER
bass
trout
MANAGE HERALD RULES
http://test.com/rules/
WHY DID I GET THIS EMAIL?
http://test.com/xscript/
REPLY HANDLER ACTIONS
pike
EOTEXT;
$this->assertEmail($expect, true, true);
}
public function testBodyRenderNoHerald() {
$expect = <<<EOTEXT
salmon
HEADER
bass
trout
REPLY HANDLER ACTIONS
pike
EOTEXT;
$this->assertEmail($expect, false, true);
}
public function testBodyRenderNoReply() {
$expect = <<<EOTEXT
salmon
HEADER
bass
trout
MANAGE HERALD RULES
http://test.com/rules/
WHY DID I GET THIS EMAIL?
http://test.com/xscript/
EOTEXT;
$this->assertEmail($expect, true, false);
}
private function assertEmail($expect, $herald_hints, $reply_hints) {
$env = PhabricatorEnv::beginScopedEnv();
$env->overrideEnvConfig('phabricator.production-uri', 'http://test.com/');
$env->overrideEnvConfig('metamta.herald.show-hints', $herald_hints);
$env->overrideEnvConfig('metamta.reply.show-hints', $reply_hints);
$body = new PhabricatorMetaMTAMailBody();
$body->addRawSection("salmon");
$body->addTextSection("HEADER", "bass\ntrout\n");
$body->addHeraldSection("/rules/", "/xscript/");
$body->addReplySection("pike");
$this->assertEqual($expect, $body->render());
}
}
diff --git a/src/applications/notification/PhabricatorNotificationQuery.php b/src/applications/notification/PhabricatorNotificationQuery.php
index f0111e6152..2154ded0b2 100644
--- a/src/applications/notification/PhabricatorNotificationQuery.php
+++ b/src/applications/notification/PhabricatorNotificationQuery.php
@@ -1,126 +1,110 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @task config Configuring the Query
* @task exec Query Execution
*/
final class PhabricatorNotificationQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $userPHID;
private $keys;
private $unread;
/* -( Configuring the Query )---------------------------------------------- */
public function setUserPHID($user_phid) {
$this->userPHID = $user_phid;
return $this;
}
public function withKeys(array $keys) {
$this->keys = $keys;
return $this;
}
/**
* Filter results by read/unread status. Note that `true` means to return
* only unread notifications, while `false` means to return only //read//
* notifications. The default is `null`, which returns both.
*
* @param mixed True or false to filter results by read status. Null to remove
* the filter.
* @return this
* @task config
*/
public function withUnread($unread) {
$this->unread = $unread;
return $this;
}
/* -( Query Execution )---------------------------------------------------- */
public function loadPage() {
if (!$this->userPHID) {
throw new Exception("Call setUser() before executing the query");
}
$story_table = new PhabricatorFeedStoryData();
$notification_table = new PhabricatorFeedStoryNotification();
$conn = $story_table->establishConnection('r');
$data = queryfx_all(
$conn,
"SELECT story.*, notif.hasViewed FROM %T notif
JOIN %T story ON notif.chronologicalKey = story.chronologicalKey
%Q
ORDER BY notif.chronologicalKey DESC
%Q",
$notification_table->getTableName(),
$story_table->getTableName(),
$this->buildWhereClause($conn),
$this->buildLimitClause($conn));
$viewed_map = ipull($data, 'hasViewed', 'chronologicalKey');
$stories = PhabricatorFeedStory::loadAllFromRows(
$data,
$this->getViewer());
foreach ($stories as $key => $story) {
$story->setHasViewed($viewed_map[$key]);
}
return $stories;
}
private function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
if ($this->userPHID) {
$where[] = qsprintf(
$conn_r,
'notif.userPHID = %s',
$this->userPHID);
}
if ($this->unread !== null) {
$where[] = qsprintf(
$conn_r,
'notif.hasViewed = %d',
(int)!$this->unread);
}
if ($this->keys) {
$where[] = qsprintf(
$conn_r,
'notif.chronologicalKey IN (%Ls)',
$this->keys);
}
return $this->formatWhereClause($where);
}
}
diff --git a/src/applications/notification/builder/PhabricatorNotificationBuilder.php b/src/applications/notification/builder/PhabricatorNotificationBuilder.php
index c22a346985..c8f48402e4 100644
--- a/src/applications/notification/builder/PhabricatorNotificationBuilder.php
+++ b/src/applications/notification/builder/PhabricatorNotificationBuilder.php
@@ -1,148 +1,132 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorNotificationBuilder {
private $stories;
public function __construct(array $stories) {
$this->stories = $stories;
}
public function buildView() {
$stories = $this->stories;
$stories = mpull($stories, null, 'getChronologicalKey');
// Aggregate notifications. Generally, we can aggregate notifications only
// by object, e.g. "a updated T123" and "b updated T123" can become
// "a and b updated T123", but we can't combine "a updated T123" and
// "a updated T234" into "a updated T123 and T234" because there would be
// nowhere sensible for the notification to link to, and no reasonable way
// to unambiguously clear it.
// Each notification emits keys it can aggregate on. For instance, if this
// story is "a updated T123", it might emit a key like this:
//
// task:phid123:unread => PhabricatorFeedStoryManiphestAggregate
//
// All the unread notifications about the task with PHID "phid123" will
// emit the same key, telling us we can aggregate them into a single
// story of type "PhabricatorFeedStoryManiphestAggregate", which could
// read like "a and b updated T123".
//
// A story might be able to aggregate in multiple ways. Although this is
// unlikely for stories in a notification context, stories in a feed context
// can also aggregate by actor:
//
// task:phid123 => PhabricatorFeedStoryManiphestAggregate
// actor:user123 => PhabricatorFeedStoryActorAggregate
//
// This means the story can either become "a and b updated T123" or
// "a updated T123 and T456". When faced with multiple possibilities, it's
// our job to choose the best aggregation.
//
// For now, we use a simple greedy algorithm and repeatedly select the
// aggregate story which consumes the largest number of individual stories
// until no aggregate story exists that consumes more than one story.
// Build up a map of all the possible aggregations.
$chronokey_map = array();
$aggregation_map = array();
$agg_types = array();
foreach ($stories as $chronokey => $story) {
$chronokey_map[$chronokey] = $story->getNotificationAggregations();
foreach ($chronokey_map[$chronokey] as $key => $type) {
$agg_types[$key] = $type;
$aggregation_map[$key]['keys'][$chronokey] = true;
}
}
// Repeatedly select the largest available aggregation until none remain.
$aggregated_stories = array();
while ($aggregation_map) {
// Count the size of each aggregation, removing any which will consume
// fewer than 2 stories.
foreach ($aggregation_map as $key => $dict) {
$size = count($dict['keys']);
if ($size > 1) {
$aggregation_map[$key]['size'] = $size;
} else {
unset($aggregation_map[$key]);
}
}
// If we're out of aggregations, break out.
if (!$aggregation_map) {
break;
}
// Select the aggregation we're going to make, and remove it from the
// map.
$aggregation_map = isort($aggregation_map, 'size');
$agg_info = idx(last($aggregation_map), 'keys');
$agg_key = last_key($aggregation_map);
unset($aggregation_map[$agg_key]);
// Select all the stories it aggregates, and remove them from the master
// list of stories and from all other possible aggregations.
$sub_stories = array();
foreach ($agg_info as $chronokey => $ignored) {
$sub_stories[$chronokey] = $stories[$chronokey];
unset($stories[$chronokey]);
foreach ($chronokey_map[$chronokey] as $key => $type) {
unset($aggregation_map[$key]['keys'][$chronokey]);
}
unset($chronokey_map[$chronokey]);
}
// Build the aggregate story.
krsort($sub_stories);
$story_class = $agg_types[$agg_key];
$conv = array(head($sub_stories)->getStoryData());
$new_story = newv($story_class, $conv);
$new_story->setAggregateStories($sub_stories);
$aggregated_stories[] = $new_story;
}
// Combine the aggregate stories back into the list of stories.
$stories = array_merge($stories, $aggregated_stories);
$stories = mpull($stories, null, 'getChronologicalKey');
krsort($stories);
$null_view = new AphrontNullView();
foreach ($stories as $story) {
$view = $story->renderNotificationView();
$null_view->appendChild($view);
}
return $null_view;
}
}
diff --git a/src/applications/notification/controller/PhabricatorNotificationClearController.php b/src/applications/notification/controller/PhabricatorNotificationClearController.php
index 070972974c..e212919405 100644
--- a/src/applications/notification/controller/PhabricatorNotificationClearController.php
+++ b/src/applications/notification/controller/PhabricatorNotificationClearController.php
@@ -1,50 +1,34 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorNotificationClearController
extends PhabricatorNotificationController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
if ($request->isDialogFormPost()) {
$table = new PhabricatorFeedStoryNotification();
queryfx(
$table->establishConnection('w'),
'UPDATE %T SET hasViewed = 1 WHERE
userPHID = %s AND hasViewed = 0',
$table->getTableName(),
$user->getPHID());
return id(new AphrontReloadResponse())
->setURI('/notification/');
}
$dialog = new AphrontDialogView();
$dialog->setUser($user);
$dialog->setTitle('Really mark all notifications as read?');
$dialog->appendChild(
"You can't ignore your problems forever, you know.");
$dialog->addCancelButton('/notification/');
$dialog->addSubmitButton('Mark All Read');
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/notification/controller/PhabricatorNotificationController.php b/src/applications/notification/controller/PhabricatorNotificationController.php
index ae82ac8b10..8bf67bfae1 100644
--- a/src/applications/notification/controller/PhabricatorNotificationController.php
+++ b/src/applications/notification/controller/PhabricatorNotificationController.php
@@ -1,37 +1,21 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorNotificationController
extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('Notification');
$page->setBaseURI('/notification/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph('!');
$page->appendChild($view);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
}
diff --git a/src/applications/notification/controller/PhabricatorNotificationIndividualController.php b/src/applications/notification/controller/PhabricatorNotificationIndividualController.php
index afb5710d31..0186ccd3c1 100644
--- a/src/applications/notification/controller/PhabricatorNotificationIndividualController.php
+++ b/src/applications/notification/controller/PhabricatorNotificationIndividualController.php
@@ -1,50 +1,34 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorNotificationIndividualController
extends PhabricatorNotificationController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$stories = id(new PhabricatorNotificationQuery())
->setViewer($user)
->setUserPHID($user->getPHID())
->withKeys(array($request->getStr('key')))
->execute();
if (!$stories) {
return id(new AphrontAjaxResponse())->setContent(
array(
'pertinent' => false,
));
}
$builder = new PhabricatorNotificationBuilder($stories);
$content = $builder->buildView()->render();
$response = array(
'pertinent' => true,
'primaryObjectPHID' => head($stories)->getPrimaryObjectPHID(),
'content' => $content,
);
return id(new AphrontAjaxResponse())->setContent($response);
}
}
diff --git a/src/applications/notification/controller/PhabricatorNotificationListController.php b/src/applications/notification/controller/PhabricatorNotificationListController.php
index aa8a033add..3c1bfeedf6 100644
--- a/src/applications/notification/controller/PhabricatorNotificationListController.php
+++ b/src/applications/notification/controller/PhabricatorNotificationListController.php
@@ -1,100 +1,84 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorNotificationListController
extends PhabricatorNotificationController {
private $filter;
public function willProcessRequest(array $data) {
$this->filter = idx($data, 'filter');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI('/notification/'));
$nav->addFilter('all', 'All Notifications');
$nav->addFilter('unread', 'Unread Notifications');
$filter = $nav->selectFilter($this->filter, 'all');
$pager = new AphrontPagerView();
$pager->setURI($request->getRequestURI(), 'offset');
$pager->setOffset($request->getInt('offset'));
$query = new PhabricatorNotificationQuery();
$query->setViewer($user);
$query->setUserPHID($user->getPHID());
switch ($filter) {
case 'unread':
$query->withUnread(true);
$header = pht('Unread Notifications');
$no_data = pht('You have no unread notifications.');
break;
default:
$header = pht('Notifications');
$no_data = pht('You have no notifications.');
break;
}
$notifications = $query->executeWithOffsetPager($pager);
if ($notifications) {
$builder = new PhabricatorNotificationBuilder($notifications);
$view = $builder->buildView();
} else {
$view =
'<div class="phabricator-notification no-notifications">'.
$no_data.
'</div>';
}
$view = array(
'<div class="phabricator-notification-list">',
$view,
'</div>',
);
$panel = new AphrontPanelView();
$panel->setHeader($header);
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->addButton(
javelin_render_tag(
'a',
array(
'href' => '/notification/clear/',
'class' => 'button',
'sigil' => 'workflow',
),
'Mark All Read'));
$panel->appendChild($view);
$panel->appendChild($pager);
$nav->appendChild($panel);
return $this->buildStandardPageResponse(
$nav,
array(
'title' => 'Notifications',
));
}
}
diff --git a/src/applications/notification/controller/PhabricatorNotificationPanelController.php b/src/applications/notification/controller/PhabricatorNotificationPanelController.php
index 3eb87e09b3..f0649b2dd9 100644
--- a/src/applications/notification/controller/PhabricatorNotificationPanelController.php
+++ b/src/applications/notification/controller/PhabricatorNotificationPanelController.php
@@ -1,65 +1,49 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorNotificationPanelController
extends PhabricatorNotificationController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$query = new PhabricatorNotificationQuery();
$query->setViewer($user);
$query->setUserPHID($user->getPHID());
$query->setLimit(15);
$stories = $query->execute();
if ($stories) {
$builder = new PhabricatorNotificationBuilder($stories);
$notifications_view = $builder->buildView();
$content = $notifications_view->render();
} else {
$content =
'<div class="phabricator-notification no-notifications">'.
'You have no notifications.'.
'</div>';
}
$content .=
'<div class="phabricator-notification view-all-notifications">'.
phutil_render_tag(
'a',
array(
'href' => '/notification/',
),
'View All Notifications').
'</div>';
$unread_count = id(new PhabricatorFeedStoryNotification())
->countUnread($user);
$json = array(
'content' => $content,
'number' => $unread_count,
);
return id(new AphrontAjaxResponse())->setContent($json);
}
}
diff --git a/src/applications/notification/controller/PhabricatorNotificationStatusController.php b/src/applications/notification/controller/PhabricatorNotificationStatusController.php
index 83484abb8a..31a0de4552 100644
--- a/src/applications/notification/controller/PhabricatorNotificationStatusController.php
+++ b/src/applications/notification/controller/PhabricatorNotificationStatusController.php
@@ -1,94 +1,78 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorNotificationStatusController
extends PhabricatorNotificationController {
public function processRequest() {
$uri = PhabricatorEnv::getEnvConfig('notification.server-uri');
$uri = new PhutilURI($uri);
$uri->setPath('/status/');
$future = id(new HTTPSFuture($uri))
->setTimeout(3);
try {
list($body) = $future->resolvex();
$body = json_decode($body, true);
if (!is_array($body)) {
throw new Exception("Expected JSON response from server!");
}
$status = $this->renderServerStatus($body);
} catch (Exception $ex) {
$status = new AphrontErrorView();
$status->setTitle("Notification Server Issue");
$status->appendChild(
'Unable to determine server status. This probably means the server '.
'is not in great shape. The specific issue encountered was:'.
'<br />'.
'<br />'.
'<strong>'.phutil_escape_html(get_class($ex)).'</strong> '.
nl2br(phutil_escape_html($ex->getMessage())));
}
return $this->buildStandardPageResponse(
$status,
array(
'title' => 'Aphlict Server Status',
));
}
private function renderServerStatus(array $status) {
$rows = array();
foreach ($status as $key => $value) {
$label = phutil_escape_html($key);
switch ($key) {
case 'uptime':
$value /= 1000;
$value = phabricator_format_relative_time_detailed($value);
break;
case 'log':
$value = phutil_escape_html($value);
break;
default:
$value = phutil_escape_html(number_format($value));
break;
}
$rows[] = array($label, $value);
}
$table = new AphrontTableView($rows);
$table->setColumnClasses(
array(
'header',
'wide',
));
$panel = new AphrontPanelView();
$panel->setHeader('Server Status');
$panel->appendChild($table);
return $panel;
}
}
diff --git a/src/applications/notification/storage/PhabricatorFeedStoryNotification.php b/src/applications/notification/storage/PhabricatorFeedStoryNotification.php
index c239c1e834..fdb3e2ddd0 100644
--- a/src/applications/notification/storage/PhabricatorFeedStoryNotification.php
+++ b/src/applications/notification/storage/PhabricatorFeedStoryNotification.php
@@ -1,75 +1,59 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFeedStoryNotification extends PhabricatorFeedDAO {
protected $userPHID;
protected $primaryObjectPHID;
protected $chronologicalKey;
protected $hasViewed;
public function getConfiguration() {
return array(
self::CONFIG_IDS => self::IDS_MANUAL,
self::CONFIG_TIMESTAMPS => false,
) + parent::getConfiguration();
}
static public function updateObjectNotificationViews(PhabricatorUser $user,
$object_phid) {
if (PhabricatorEnv::getEnvConfig('notification.enabled')) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$notification_table = new PhabricatorFeedStoryNotification();
$conn = $notification_table->establishConnection('w');
queryfx(
$conn,
"UPDATE %T
SET hasViewed = 1
WHERE userPHID = %s
AND primaryObjectPHID = %s
AND hasViewed = 0",
$notification_table->getTableName(),
$user->getPHID(),
$object_phid);
unset($unguarded);
}
}
/* should only be called when notifications are enabled */
public function countUnread(
PhabricatorUser $user) {
$conn = $this->establishConnection('r');
$data = queryfx_one(
$conn,
"SELECT COUNT(*) as count
FROM %T
WHERE userPHID = %s
AND hasViewed=0",
$this->getTableName(),
$user->getPHID());
return $data['count'];
}
}
diff --git a/src/applications/notification/view/PhabricatorNotificationStoryView.php b/src/applications/notification/view/PhabricatorNotificationStoryView.php
index 90d01ef8e5..f9673a1d0d 100644
--- a/src/applications/notification/view/PhabricatorNotificationStoryView.php
+++ b/src/applications/notification/view/PhabricatorNotificationStoryView.php
@@ -1,59 +1,43 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorNotificationStoryView
extends PhabricatorNotificationView {
private $title;
private $phid;
private $epoch;
private $viewed;
public function setTitle($title) {
$this->title = $title;
return $this;
}
public function setEpoch($epoch) {
$this->epoch = $epoch;
return $this;
}
public function setViewed($viewed) {
$this->viewed = $viewed;
}
public function render() {
$classes = array(
'phabricator-notification',
);
if (!$this->viewed) {
$classes[] = 'phabricator-notification-unread';
}
return phutil_render_tag(
'div',
array(
'class' => implode(' ', $classes),
),
$this->title);
}
}
diff --git a/src/applications/notification/view/PhabricatorNotificationView.php b/src/applications/notification/view/PhabricatorNotificationView.php
index 5f815f7004..389cca9367 100644
--- a/src/applications/notification/view/PhabricatorNotificationView.php
+++ b/src/applications/notification/view/PhabricatorNotificationView.php
@@ -1,21 +1,5 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorNotificationView extends AphrontView {
}
diff --git a/src/applications/oauthserver/PhabricatorOAuthResponse.php b/src/applications/oauthserver/PhabricatorOAuthResponse.php
index 49f574d47b..e51b1d45d8 100644
--- a/src/applications/oauthserver/PhabricatorOAuthResponse.php
+++ b/src/applications/oauthserver/PhabricatorOAuthResponse.php
@@ -1,123 +1,107 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/*
* @group oauthserver
*/
final class PhabricatorOAuthResponse extends AphrontResponse {
private $state;
private $content;
private $clientURI;
private $error;
private $errorDescription;
private function getState() {
return $this->state;
}
public function setState($state) {
$this->state = $state;
return $this;
}
private function getContent() {
return $this->content;
}
public function setContent($content) {
$this->content = $content;
return $this;
}
private function getClientURI() {
return $this->clientURI;
}
public function setClientURI(PhutilURI $uri) {
$this->setHTTPResponseCode(302);
$this->clientURI = $uri;
return $this;
}
private function getFullURI() {
$base_uri = $this->getClientURI();
$query_params = $this->buildResponseDict();
foreach ($query_params as $key => $value) {
$base_uri->setQueryParam($key, $value);
}
return $base_uri;
}
private function getError() {
return $this->error;
}
public function setError($error) {
// errors sometimes redirect to the client (302) but otherwise
// the spec says all code 400
if (!$this->getClientURI()) {
$this->setHTTPResponseCode(400);
}
$this->error = $error;
return $this;
}
private function getErrorDescription() {
return $this->errorDescription;
}
public function setErrorDescription($error_description) {
$this->errorDescription = $error_description;
return $this;
}
public function __construct() {
$this->setHTTPResponseCode(200); // assume the best
return $this;
}
public function getHeaders() {
$headers = array(
array('Content-Type', 'application/json'),
);
if ($this->getClientURI()) {
$headers[] = array('Location', $this->getFullURI());
}
// TODO -- T844 set headers with X-Auth-Scopes, etc
$headers = array_merge(parent::getHeaders(), $headers);
return $headers;
}
private function buildResponseDict() {
if ($this->getError()) {
$content = array(
'error' => $this->getError(),
'error_description' => $this->getErrorDescription(),
);
$this->setContent($content);
}
$content = $this->getContent();
if (!$content) {
return '';
}
if ($this->getState()) {
$content['state'] = $this->getState();
}
return $content;
}
public function buildResponseString() {
return $this->encodeJSONForHTTPResponse($this->buildResponseDict());
}
}
diff --git a/src/applications/oauthserver/PhabricatorOAuthServer.php b/src/applications/oauthserver/PhabricatorOAuthServer.php
index 6f4de14fd7..f7b77c8378 100644
--- a/src/applications/oauthserver/PhabricatorOAuthServer.php
+++ b/src/applications/oauthserver/PhabricatorOAuthServer.php
@@ -1,256 +1,240 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Implements core OAuth 2.0 Server logic.
*
* This class should be used behind business logic that parses input to
* determine pertinent @{class:PhabricatorUser} $user,
* @{class:PhabricatorOAuthServerClient} $client(s),
* @{class:PhabricatorOAuthServerAuthorizationCode} $code(s), and.
* @{class:PhabricatorOAuthServerAccessToken} $token(s).
*
* For an OAuth 2.0 server, there are two main steps:
*
* 1) Authorization - the user authorizes a given client to access the data
* the OAuth 2.0 server protects. Once this is achieved / if it has
* been achived already, the OAuth server sends the client an authorization
* code.
* 2) Access Token - the client should send the authorization code received in
* step 1 along with its id and secret to the OAuth server to receive an
* access token. This access token can later be used to access Phabricator
* data on behalf of the user.
*
* @task auth Authorizing @{class:PhabricatorOAuthServerClient}s and
* generating @{class:PhabricatorOAuthServerAuthorizationCode}s
* @task token Validating @{class:PhabricatorOAuthServerAuthorizationCode}s
* and generating @{class:PhabricatorOAuthServerAccessToken}s
* @task internal Internals
*
* @group oauthserver
*/
final class PhabricatorOAuthServer {
const AUTHORIZATION_CODE_TIMEOUT = 300;
const ACCESS_TOKEN_TIMEOUT = 3600;
private $user;
private $client;
/**
* @group internal
*/
private function getUser() {
if (!$this->user) {
throw new Exception('You must setUser before you can getUser!');
}
return $this->user;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
/**
* @group internal
*/
private function getClient() {
if (!$this->client) {
throw new Exception('You must setClient before you can getClient!');
}
return $this->client;
}
public function setClient(PhabricatorOAuthServerClient $client) {
$this->client = $client;
return $this;
}
/**
* @task auth
* @return tuple <bool hasAuthorized, ClientAuthorization or null>
*/
public function userHasAuthorizedClient(array $scope) {
$authorization = id(new PhabricatorOAuthClientAuthorization())->
loadOneWhere('userPHID = %s AND clientPHID = %s',
$this->getUser()->getPHID(),
$this->getClient()->getPHID());
if (empty($authorization)) {
return array(false, null);
}
if ($scope) {
$missing_scope = array_diff_key($scope,
$authorization->getScope());
} else {
$missing_scope = false;
}
if ($missing_scope) {
return array(false, $authorization);
}
return array(true, $authorization);
}
/**
* @task auth
*/
public function authorizeClient(array $scope) {
$authorization = new PhabricatorOAuthClientAuthorization();
$authorization->setUserPHID($this->getUser()->getPHID());
$authorization->setClientPHID($this->getClient()->getPHID());
$authorization->setScope($scope);
$authorization->save();
return $authorization;
}
/**
* @task auth
*/
public function generateAuthorizationCode(PhutilURI $redirect_uri) {
$code = Filesystem::readRandomCharacters(32);
$client = $this->getClient();
$authorization_code = new PhabricatorOAuthServerAuthorizationCode();
$authorization_code->setCode($code);
$authorization_code->setClientPHID($client->getPHID());
$authorization_code->setClientSecret($client->getSecret());
$authorization_code->setUserPHID($this->getUser()->getPHID());
$authorization_code->setRedirectURI((string) $redirect_uri);
$authorization_code->save();
return $authorization_code;
}
/**
* @task token
*/
public function generateAccessToken() {
$token = Filesystem::readRandomCharacters(32);
$access_token = new PhabricatorOAuthServerAccessToken();
$access_token->setToken($token);
$access_token->setUserPHID($this->getUser()->getPHID());
$access_token->setClientPHID($this->getClient()->getPHID());
$access_token->save();
return $access_token;
}
/**
* @task token
*/
public function validateAuthorizationCode(
PhabricatorOAuthServerAuthorizationCode $test_code,
PhabricatorOAuthServerAuthorizationCode $valid_code) {
// check that all the meta data matches
if ($test_code->getClientPHID() != $valid_code->getClientPHID()) {
return false;
}
if ($test_code->getClientSecret() != $valid_code->getClientSecret()) {
return false;
}
// check that the authorization code hasn't timed out
$created_time = $test_code->getDateCreated();
$must_be_used_by = $created_time + self::AUTHORIZATION_CODE_TIMEOUT;
return (time() < $must_be_used_by);
}
/**
* @task token
*/
public function validateAccessToken(
PhabricatorOAuthServerAccessToken $token,
$required_scope) {
$created_time = $token->getDateCreated();
$must_be_used_by = $created_time + self::ACCESS_TOKEN_TIMEOUT;
$expired = time() > $must_be_used_by;
$authorization = id(new PhabricatorOAuthClientAuthorization())
->loadOneWhere(
'userPHID = %s AND clientPHID = %s',
$token->getUserPHID(),
$token->getClientPHID());
if (!$authorization) {
return false;
}
$token_scope = $authorization->getScope();
if (!isset($token_scope[$required_scope])) {
return false;
}
$valid = true;
if ($expired) {
$valid = false;
// check if the scope includes "offline_access", which makes the
// token valid despite being expired
if (isset(
$token_scope[PhabricatorOAuthServerScope::SCOPE_OFFLINE_ACCESS]
)) {
$valid = true;
}
}
return $valid;
}
/**
* See http://tools.ietf.org/html/draft-ietf-oauth-v2-23#section-3.1.2
* for details on what makes a given redirect URI "valid".
*/
public function validateRedirectURI(PhutilURI $uri) {
if (PhabricatorEnv::isValidRemoteWebResource($uri)) {
if ($uri->getFragment()) {
return false;
}
if ($uri->getDomain()) {
return true;
}
}
return false;
}
/**
* If there's a URI specified in an OAuth request, it must be validated in
* its own right. Further, it must have the same domain and (at least) the
* same query parameters as the primary URI.
*/
public function validateSecondaryRedirectURI(PhutilURI $secondary_uri,
PhutilURI $primary_uri) {
$valid = $this->validateRedirectURI($secondary_uri);
if ($valid) {
$valid_domain = ($secondary_uri->getDomain() ==
$primary_uri->getDomain());
$good_params = $primary_uri->getQueryParams();
$test_params = $secondary_uri->getQueryParams();
$missing_params = array_diff_key($good_params, $test_params);
$valid = $valid_domain && empty($missing_params);
}
return $valid;
}
}
diff --git a/src/applications/oauthserver/PhabricatorOAuthServerScope.php b/src/applications/oauthserver/PhabricatorOAuthServerScope.php
index 2a34f3675c..5b6124632d 100644
--- a/src/applications/oauthserver/PhabricatorOAuthServerScope.php
+++ b/src/applications/oauthserver/PhabricatorOAuthServerScope.php
@@ -1,123 +1,107 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorOAuthServerScope {
const SCOPE_OFFLINE_ACCESS = 'offline_access';
const SCOPE_WHOAMI = 'whoami';
const SCOPE_NOT_ACCESSIBLE = 'not_accessible';
/*
* Note this does not contain SCOPE_NOT_ACCESSIBLE which is magic
* used to simplify code for data that is not currently accessible
* via OAuth.
*/
static public function getScopesDict() {
return array(
self::SCOPE_OFFLINE_ACCESS => 1,
self::SCOPE_WHOAMI => 1,
);
}
static public function getCheckboxControl($current_scopes) {
$scopes = self::getScopesDict();
$scope_keys = array_keys($scopes);
sort($scope_keys);
$checkboxes = new AphrontFormCheckboxControl();
foreach ($scope_keys as $scope) {
$checkboxes->addCheckbox(
$name = $scope,
$value = 1,
$label = self::getCheckboxLabel($scope),
$checked = isset($current_scopes[$scope])
);
}
$checkboxes->setLabel('Scope');
return $checkboxes;
}
static private function getCheckboxLabel($scope) {
$label = null;
switch ($scope) {
case self::SCOPE_OFFLINE_ACCESS:
$label = 'Make access tokens granted to this client never expire.';
break;
case self::SCOPE_WHOAMI:
$label = 'Read access to Conduit method user.whoami.';
break;
}
return $label;
}
static public function getScopesFromRequest(AphrontRequest $request) {
$scopes = self::getScopesDict();
$requested_scopes = array();
foreach ($scopes as $scope => $bit) {
if ($request->getBool($scope)) {
$requested_scopes[$scope] = 1;
}
}
return $requested_scopes;
}
/**
* A scopes list is considered valid if each scope is a known scope
* and each scope is seen only once. Otherwise, the list is invalid.
*/
static public function validateScopesList($scope_list) {
$scopes = explode(' ', $scope_list);
$known_scopes = self::getScopesDict();
$seen_scopes = array();
foreach ($scopes as $scope) {
if (!isset($known_scopes[$scope])) {
return false;
}
if (isset($seen_scopes[$scope])) {
return false;
}
$seen_scopes[$scope] = 1;
}
return true;
}
/**
* A scopes dictionary is considered valid if each key is a known scope.
* Otherwise, the dictionary is invalid.
*/
static public function validateScopesDict($scope_dict) {
$known_scopes = self::getScopesDict();
$unknown_scopes = array_diff_key($scope_dict,
$known_scopes);
return empty($unknown_scopes);
}
/**
* Transforms a space-delimited scopes list into a scopes dict. The list
* should be validated by @{method:validateScopesList} before
* transformation.
*/
static public function scopesListToDict($scope_list) {
$scopes = explode(' ', $scope_list);
return array_fill_keys($scopes, 1);
}
}
diff --git a/src/applications/oauthserver/__tests__/PhabricatorOAuthServerTestCase.php b/src/applications/oauthserver/__tests__/PhabricatorOAuthServerTestCase.php
index fd195be301..1a33f60ede 100644
--- a/src/applications/oauthserver/__tests__/PhabricatorOAuthServerTestCase.php
+++ b/src/applications/oauthserver/__tests__/PhabricatorOAuthServerTestCase.php
@@ -1,85 +1,69 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorOAuthServerTestCase
extends PhabricatorTestCase {
public function testValidateRedirectURI() {
static $map = array(
'http://www.google.com' => true,
'http://www.google.com/' => true,
'http://www.google.com/auth' => true,
'www.google.com' => false,
'http://www.google.com/auth#invalid' => false
);
$server = new PhabricatorOAuthServer();
foreach ($map as $input => $expected) {
$uri = new PhutilURI($input);
$result = $server->validateRedirectURI($uri);
$this->assertEqual(
$expected,
$result,
"Validation of redirect URI '{$input}'"
);
}
}
public function testValidateSecondaryRedirectURI() {
$server = new PhabricatorOAuthServer();
$primary_uri = new PhutilURI('http://www.google.com');
static $test_domain_map = array(
'http://www.google.com' => true,
'http://www.google.com/' => true,
'http://www.google.com/auth' => true,
'http://www.google.com/?auth' => true,
'www.google.com' => false,
'http://www.google.com/auth#invalid' => false,
'http://www.example.com' => false
);
foreach ($test_domain_map as $input => $expected) {
$uri = new PhutilURI($input);
$this->assertEqual(
$expected,
$server->validateSecondaryRedirectURI($uri, $primary_uri),
"Validation of redirect URI '{$input}' ".
"relative to '{$primary_uri}'"
);
}
$primary_uri = new PhutilURI('http://www.google.com/?auth');
static $test_query_map = array(
'http://www.google.com' => false,
'http://www.google.com/' => false,
'http://www.google.com/auth' => false,
'http://www.google.com/?auth' => true,
'http://www.google.com/?auth&stuff' => true,
'http://www.google.com/?stuff' => false,
);
foreach ($test_query_map as $input => $expected) {
$uri = new PhutilURI($input);
$this->assertEqual(
$expected,
$server->validateSecondaryRedirectURI($uri, $primary_uri),
"Validation of secondary redirect URI '{$input}' ".
"relative to '{$primary_uri}'"
);
}
}
}
diff --git a/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php b/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php
index 373c9e167c..da9f6438be 100644
--- a/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php
+++ b/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php
@@ -1,225 +1,209 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group oauthserver
*/
final class PhabricatorOAuthServerAuthController
extends PhabricatorAuthController {
public function shouldRequireLogin() {
return true;
}
public function processRequest() {
$request = $this->getRequest();
$current_user = $request->getUser();
$server = new PhabricatorOAuthServer();
$client_phid = $request->getStr('client_id');
$scope = $request->getStr('scope');
$redirect_uri = $request->getStr('redirect_uri');
$state = $request->getStr('state');
$response_type = $request->getStr('response_type');
$response = new PhabricatorOAuthResponse();
// state is an opaque value the client sent us for their own purposes
// we just need to send it right back to them in the response!
if ($state) {
$response->setState($state);
}
if (!$client_phid) {
$response->setError('invalid_request');
$response->setErrorDescription(
'Required parameter client_id not specified.'
);
return $response;
}
$server->setUser($current_user);
// one giant try / catch around all the exciting database stuff so we
// can return a 'server_error' response if something goes wrong!
try {
$client = id(new PhabricatorOAuthServerClient())
->loadOneWhere('phid = %s', $client_phid);
if (!$client) {
$response->setError('invalid_request');
$response->setErrorDescription(
'Client with id '.$client_phid.' not found.'
);
return $response;
}
$server->setClient($client);
if ($redirect_uri) {
$client_uri = new PhutilURI($client->getRedirectURI());
$redirect_uri = new PhutilURI($redirect_uri);
if (!($server->validateSecondaryRedirectURI($redirect_uri,
$client_uri))) {
$response->setError('invalid_request');
$response->setErrorDescription(
'The specified redirect URI is invalid. The redirect URI '.
'must be a fully-qualified domain with no fragments and '.
'must have the same domain and at least the same query '.
'parameters as the redirect URI the client registered.'
);
return $response;
}
$uri = $redirect_uri;
$access_token_uri = $uri;
} else {
$uri = new PhutilURI($client->getRedirectURI());
$access_token_uri = null;
}
// we've now validated this request enough overall such that we
// can safely redirect to the client with the response
$response->setClientURI($uri);
if (empty($response_type)) {
$response->setError('invalid_request');
$response->setErrorDescription(
'Required parameter response_type not specified.'
);
return $response;
}
if ($response_type != 'code') {
$response->setError('unsupported_response_type');
$response->setErrorDescription(
'The authorization server does not support obtaining an '.
'authorization code using the specified response_type. '.
'You must specify the response_type as "code".'
);
return $response;
}
if ($scope) {
if (!PhabricatorOAuthServerScope::validateScopesList($scope)) {
$response->setError('invalid_scope');
$response->setErrorDescription(
'The requested scope is invalid, unknown, or malformed.'
);
return $response;
}
$scope = PhabricatorOAuthServerScope::scopesListToDict($scope);
}
list($is_authorized,
$authorization) = $server->userHasAuthorizedClient($scope);
if ($is_authorized) {
$return_auth_code = true;
$unguarded_write = AphrontWriteGuard::beginScopedUnguardedWrites();
} else if ($request->isFormPost()) {
$scope = PhabricatorOAuthServerScope::getScopesFromRequest($request);
if ($authorization) {
$authorization->setScope($scope)->save();
} else {
$authorization = $server->authorizeClient($scope);
}
$return_auth_code = true;
$unguarded_write = null;
} else {
$return_auth_code = false;
$unguarded_write = null;
}
if ($return_auth_code) {
// step 1 -- generate authorization code
$auth_code =
$server->generateAuthorizationCode($access_token_uri);
// step 2 return it
$content = array(
'code' => $auth_code->getCode(),
'scope' => $authorization->getScopeString(),
);
$response->setContent($content);
return $response;
}
unset($unguarded_write);
} catch (Exception $e) {
// Note we could try harder to determine between a server_error
// vs temporarily_unavailable. Good enough though.
$response->setError('server_error');
$response->setErrorDescription(
'The authorization server encountered an unexpected condition '.
'which prevented it from fulfilling the request. '
);
return $response;
}
// display time -- make a nice form for the user to grant the client
// access to the granularity specified by $scope
$name = phutil_escape_html($client->getName());
$title = 'Authorize ' . $name . '?';
$panel = new AphrontPanelView();
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->setHeader($title);
$description =
"Do want to authorize {$name} to access your ".
"Phabricator account data?";
if ($scope) {
if ($authorization) {
$desired_scopes = array_merge($scope,
$authorization->getScope());
} else {
$desired_scopes = $scope;
}
if (!PhabricatorOAuthServerScope::validateScopesDict($desired_scopes)) {
$response->setError('invalid_scope');
$response->setErrorDescription(
'The requested scope is invalid, unknown, or malformed.'
);
return $response;
}
} else {
$desired_scopes = array(
PhabricatorOAuthServerScope::SCOPE_WHOAMI => 1,
PhabricatorOAuthServerScope::SCOPE_OFFLINE_ACCESS => 1
);
}
$cancel_uri = clone $uri;
$cancel_params = array(
'error' => 'access_denied',
'error_description' =>
'The resource owner (aka the user) denied the request.'
);
$cancel_uri->setQueryParams($cancel_params);
$form = id(new AphrontFormView())
->setUser($current_user)
->appendChild(
id(new AphrontFormStaticControl())
->setValue($description)
)
->appendChild(
PhabricatorOAuthServerScope::getCheckboxControl($desired_scopes)
)
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Authorize')
->addCancelButton($cancel_uri)
);
$panel->appendChild($form);
return $this->buildStandardPageResponse(
$panel,
array('title' => $title));
}
}
diff --git a/src/applications/oauthserver/controller/PhabricatorOAuthServerController.php b/src/applications/oauthserver/controller/PhabricatorOAuthServerController.php
index 2a6843e9b3..77805b31f8 100644
--- a/src/applications/oauthserver/controller/PhabricatorOAuthServerController.php
+++ b/src/applications/oauthserver/controller/PhabricatorOAuthServerController.php
@@ -1,84 +1,68 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorOAuthServerController
extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
$user = $this->getRequest()->getUser();
$page = $this->buildStandardPageView();
$page->setApplicationName('OAuth Server');
$page->setBaseURI('/oauthserver/');
$page->setTitle(idx($data, 'title'));
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI('/oauthserver/'));
$nav->addLabel('Client Authorizations');
$nav->addFilter('clientauthorization',
'My Authorizations');
$nav->addSpacer();
$nav->addLabel('Clients');
$nav->addFilter('client/create',
'Create Client');
foreach ($this->getExtraClientFilters() as $filter) {
$nav->addFilter($filter['url'],
$filter['label']);
}
$nav->addFilter('client',
'My Clients');
$nav->selectFilter($this->getFilter(),
'clientauthorization');
$nav->appendChild($view);
$page->appendChild($nav);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
protected function getFilter() {
return 'clientauthorization';
}
protected function getExtraClientFilters() {
return array();
}
protected function getHighlightPHIDs() {
$phids = array();
$request = $this->getRequest();
$edited = $request->getStr('edited');
$new = $request->getStr('new');
if ($edited) {
$phids[$edited] = $edited;
}
if ($new) {
$phids[$new] = $new;
}
return $phids;
}
protected function buildErrorView($error_message) {
$error = new AphrontErrorView();
$error->setSeverity(AphrontErrorView::SEVERITY_ERROR);
$error->setTitle($error_message);
return $error;
}
}
diff --git a/src/applications/oauthserver/controller/PhabricatorOAuthServerTestController.php b/src/applications/oauthserver/controller/PhabricatorOAuthServerTestController.php
index 7036d8b393..d1cec63a19 100644
--- a/src/applications/oauthserver/controller/PhabricatorOAuthServerTestController.php
+++ b/src/applications/oauthserver/controller/PhabricatorOAuthServerTestController.php
@@ -1,69 +1,53 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group oauthserver
*/
final class PhabricatorOAuthServerTestController
extends PhabricatorOAuthServerController {
public function shouldRequireLogin() {
return true;
}
public function processRequest() {
$request = $this->getRequest();
$current_user = $request->getUser();
$server = new PhabricatorOAuthServer();
$panels = array();
$results = array();
if ($request->isFormPost()) {
$action = $request->getStr('action');
switch ($action) {
case 'testclientauthorization':
$user_phid = $current_user->getPHID();
$client_phid = $request->getStr('client_phid');
$client = id(new PhabricatorOAuthServerClient)
->loadOneWhere('phid = %s', $client_phid);
if (!$client) {
throw new Exception('Failed to load client!');
}
if ($client->getCreatorPHID() != $user_phid ||
$current_user->getPHID() != $user_phid) {
throw new Exception(
'Only allowed to make test data for yourself '.
'for clients you own!'
);
}
// blankclientauthorizations don't get scope
$scope = array();
$server->setUser($current_user);
$server->setClient($client);
$authorization = $server->authorizeClient($scope);
return id(new AphrontRedirectResponse())
->setURI('/oauthserver/clientauthorization/?edited='.
$authorization->getPHID());
break;
default:
break;
}
}
}
}
diff --git a/src/applications/oauthserver/controller/PhabricatorOAuthServerTokenController.php b/src/applications/oauthserver/controller/PhabricatorOAuthServerTokenController.php
index 7bc06b8785..2472d75109 100644
--- a/src/applications/oauthserver/controller/PhabricatorOAuthServerTokenController.php
+++ b/src/applications/oauthserver/controller/PhabricatorOAuthServerTokenController.php
@@ -1,160 +1,144 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group oauthserver
*/
final class PhabricatorOAuthServerTokenController
extends PhabricatorAuthController {
public function shouldRequireLogin() {
return false;
}
public function processRequest() {
$request = $this->getRequest();
$grant_type = $request->getStr('grant_type');
$code = $request->getStr('code');
$redirect_uri = $request->getStr('redirect_uri');
$client_phid = $request->getStr('client_id');
$client_secret = $request->getStr('client_secret');
$response = new PhabricatorOAuthResponse();
$server = new PhabricatorOAuthServer();
if ($grant_type != 'authorization_code') {
$response->setError('unsupported_grant_type');
$response->setErrorDescription(
'Only grant_type authorization_code is supported.'
);
return $response;
}
if (!$code) {
$response->setError('invalid_request');
$response->setErrorDescription(
'Required parameter code missing.'
);
return $response;
}
if (!$client_phid) {
$response->setError('invalid_request');
$response->setErrorDescription(
'Required parameter client_id missing.'
);
return $response;
}
if (!$client_secret) {
$response->setError('invalid_request');
$response->setErrorDescription(
'Required parameter client_secret missing.'
);
return $response;
}
// one giant try / catch around all the exciting database stuff so we
// can return a 'server_error' response if something goes wrong!
try {
$auth_code = id(new PhabricatorOAuthServerAuthorizationCode())
->loadOneWhere('code = %s',
$code);
if (!$auth_code) {
$response->setError('invalid_grant');
$response->setErrorDescription(
'Authorization code '.$code.' not found.'
);
return $response;
}
// if we have an auth code redirect URI, there must be a redirect_uri
// in the request and it must match the auth code redirect uri *exactly*
$auth_code_redirect_uri = $auth_code->getRedirectURI();
if ($auth_code_redirect_uri) {
$auth_code_redirect_uri = new PhutilURI($auth_code_redirect_uri);
$redirect_uri = new PhutilURI($redirect_uri);
if (!$redirect_uri->getDomain() ||
$redirect_uri != $auth_code_redirect_uri) {
$response->setError('invalid_grant');
$response->setErrorDescription(
'Redirect uri in request must exactly match redirect uri '.
'from authorization code.'
);
return $response;
}
} else if ($redirect_uri) {
$response->setError('invalid_grant');
$response->setErrorDescription(
'Redirect uri in request and no redirect uri in authorization '.
'code. The two must exactly match.'
);
return $response;
}
$client = id(new PhabricatorOAuthServerClient())
->loadOneWhere('phid = %s',
$client_phid);
if (!$client) {
$response->setError('invalid_client');
$response->setErrorDescription(
'Client with client_id '.$client_phid.' not found.'
);
return $response;
}
$server->setClient($client);
$user_phid = $auth_code->getUserPHID();
$user = id(new PhabricatorUser())
->loadOneWhere('phid = %s', $user_phid);
if (!$user) {
$response->setError('invalid_grant');
$response->setErrorDescription(
'User with phid '.$user_phid.' not found.'
);
return $response;
}
$server->setUser($user);
$test_code = new PhabricatorOAuthServerAuthorizationCode();
$test_code->setClientSecret($client_secret);
$test_code->setClientPHID($client_phid);
$is_good_code = $server->validateAuthorizationCode($auth_code,
$test_code);
if (!$is_good_code) {
$response->setError('invalid_grant');
$response->setErrorDescription(
'Invalid authorization code '.$code.'.'
);
return $response;
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$access_token = $server->generateAccessToken();
$auth_code->delete();
unset($unguarded);
$result = array(
'access_token' => $access_token->getToken(),
'token_type' => 'Bearer',
'expires_in' => PhabricatorOAuthServer::ACCESS_TOKEN_TIMEOUT,
);
return $response->setContent($result);
} catch (Exception $e) {
$response->setError('server_error');
$response->setErrorDescription(
'The authorization server encountered an unexpected condition '.
'which prevented it from fulfilling the request.'
);
return $response;
}
}
}
diff --git a/src/applications/oauthserver/controller/client/PhabricatorOAuthClientBaseController.php b/src/applications/oauthserver/controller/client/PhabricatorOAuthClientBaseController.php
index d54b4155f5..3bbb2f2fd2 100644
--- a/src/applications/oauthserver/controller/client/PhabricatorOAuthClientBaseController.php
+++ b/src/applications/oauthserver/controller/client/PhabricatorOAuthClientBaseController.php
@@ -1,41 +1,25 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group oauthserver
*/
abstract class PhabricatorOAuthClientBaseController
extends PhabricatorOAuthServerController {
private $clientPHID;
protected function getClientPHID() {
return $this->clientPHID;
}
private function setClientPHID($phid) {
$this->clientPHID = $phid;
return $this;
}
public function shouldRequireLogin() {
return true;
}
public function willProcessRequest(array $data) {
$this->setClientPHID(idx($data, 'phid'));
}
}
diff --git a/src/applications/oauthserver/controller/client/PhabricatorOAuthClientDeleteController.php b/src/applications/oauthserver/controller/client/PhabricatorOAuthClientDeleteController.php
index e0aeb0a82e..cb893c78ea 100644
--- a/src/applications/oauthserver/controller/client/PhabricatorOAuthClientDeleteController.php
+++ b/src/applications/oauthserver/controller/client/PhabricatorOAuthClientDeleteController.php
@@ -1,65 +1,49 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group oauthserver
*/
final class PhabricatorOAuthClientDeleteController
extends PhabricatorOAuthClientBaseController {
public function processRequest() {
$phid = $this->getClientPHID();
$title = 'Delete OAuth Client';
$request = $this->getRequest();
$current_user = $request->getUser();
$client = id(new PhabricatorOAuthServerClient())
->loadOneWhere('phid = %s',
$phid);
if (empty($client)) {
return new Aphront404Response();
}
if ($client->getCreatorPHID() != $current_user->getPHID()) {
$message = 'Access denied to client with phid '.$phid.'. '.
'Only the user who created the client has permission to '.
'delete the client.';
return id(new Aphront403Response())
->setForbiddenText($message);
}
if ($request->isFormPost()) {
$client->delete();
return id(new AphrontRedirectResponse())
->setURI('/oauthserver/client/?deleted=1');
}
$client_name = phutil_escape_html($client->getName());
$title .= ' '.$client_name;
$dialog = new AphrontDialogView();
$dialog->setUser($current_user);
$dialog->setTitle($title);
$dialog->appendChild(
'<p>Are you sure you want to delete this client?</p>'
);
$dialog->addSubmitButton();
$dialog->addCancelButton($client->getEditURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/oauthserver/controller/client/PhabricatorOAuthClientEditController.php b/src/applications/oauthserver/controller/client/PhabricatorOAuthClientEditController.php
index ab3201636e..5425be4d8d 100644
--- a/src/applications/oauthserver/controller/client/PhabricatorOAuthClientEditController.php
+++ b/src/applications/oauthserver/controller/client/PhabricatorOAuthClientEditController.php
@@ -1,198 +1,182 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group oauthserver
*/
final class PhabricatorOAuthClientEditController
extends PhabricatorOAuthClientBaseController {
private $isEdit;
protected function isClientEdit() {
return $this->isEdit;
}
private function setIsClientEdit($is_edit) {
$this->isEdit = (bool) $is_edit;
return $this;
}
protected function getExtraClientFilters() {
if ($this->isClientEdit()) {
$filters = array(
array('url' => $this->getFilter(),
'label' => 'Edit Client')
);
} else {
$filters = array();
}
return $filters;
}
public function getFilter() {
if ($this->isClientEdit()) {
$filter = 'client/edit/'.$this->getClientPHID();
} else {
$filter = 'client/create';
}
return $filter;
}
public function processRequest() {
$request = $this->getRequest();
$current_user = $request->getUser();
$error = null;
$bad_redirect = false;
$phid = $this->getClientPHID();
// if we have a phid, then we're editing
$this->setIsClientEdit($phid);
if ($this->isClientEdit()) {
$client = id(new PhabricatorOAuthServerClient())
->loadOneWhere('phid = %s',
$phid);
$title = 'Edit OAuth Client';
// validate the client
if (empty($client)) {
return new Aphront404Response();
}
if ($client->getCreatorPHID() != $current_user->getPHID()) {
$message = 'Access denied to edit client with id '.$phid.'. '.
'Only the user who created the client has permission to '.
'edit the client.';
return id(new Aphront403Response())
->setForbiddenText($message);
}
$submit_button = 'Save OAuth Client';
$secret = null;
// new client - much simpler
} else {
$client = new PhabricatorOAuthServerClient();
$title = 'Create OAuth Client';
$submit_button = 'Create OAuth Client';
$secret = Filesystem::readRandomCharacters(32);
}
if ($request->isFormPost()) {
$redirect_uri = $request->getStr('redirect_uri');
$client->setName($request->getStr('name'));
$client->setRedirectURI($redirect_uri);
if ($secret) {
$client->setSecret($secret);
}
$client->setCreatorPHID($current_user->getPHID());
$uri = new PhutilURI($redirect_uri);
$server = new PhabricatorOAuthServer();
if (!$server->validateRedirectURI($uri)) {
$error = new AphrontErrorView();
$error->setSeverity(AphrontErrorView::SEVERITY_ERROR);
$error->setTitle(
'Redirect URI must be a fully qualified domain name '.
'with no fragments. See '.
'http://tools.ietf.org/html/draft-ietf-oauth-v2-23#section-3.1.2 '.
'for more information on the correct format.'
);
$bad_redirect = true;
} else {
$client->save();
// refresh the phid in case its a create
$phid = $client->getPHID();
if ($this->isClientEdit()) {
return id(new AphrontRedirectResponse())
->setURI('/oauthserver/client/?edited='.$phid);
} else {
return id(new AphrontRedirectResponse())
->setURI('/oauthserver/client/?new='.$phid);
}
}
}
$panel = new AphrontPanelView();
if ($this->isClientEdit()) {
$delete_button = phutil_render_tag(
'a',
array(
'href' => $client->getDeleteURI(),
'class' => 'grey button',
),
'Delete OAuth Client');
$panel->addButton($delete_button);
}
$panel->setHeader($title);
$form = id(new AphrontFormView())
->setUser($current_user)
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Name')
->setName('name')
->setValue($client->getName())
);
if ($this->isClientEdit()) {
$form
->appendChild(
id(new AphrontFormTextControl())
->setLabel('ID')
->setValue($phid)
)
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Secret')
->setValue($client->getSecret())
);
}
$form
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Redirect URI')
->setName('redirect_uri')
->setValue($client->getRedirectURI())
->setError($bad_redirect)
);
if ($this->isClientEdit()) {
$created = phabricator_datetime($client->getDateCreated(),
$current_user);
$updated = phabricator_datetime($client->getDateModified(),
$current_user);
$form
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Created')
->setValue($created)
)
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Last Updated')
->setValue($updated)
);
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue($submit_button)
);
$panel->appendChild($form);
return $this->buildStandardPageResponse(
array($error,
$panel
),
array('title' => $title)
);
}
}
diff --git a/src/applications/oauthserver/controller/client/PhabricatorOAuthClientListController.php b/src/applications/oauthserver/controller/client/PhabricatorOAuthClientListController.php
index 4f7d9bfd50..b10d5c3c9a 100644
--- a/src/applications/oauthserver/controller/client/PhabricatorOAuthClientListController.php
+++ b/src/applications/oauthserver/controller/client/PhabricatorOAuthClientListController.php
@@ -1,153 +1,137 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group oauthserver
*/
final class PhabricatorOAuthClientListController
extends PhabricatorOAuthClientBaseController {
public function getFilter() {
return 'client';
}
public function processRequest() {
$title = 'OAuth Clients';
$request = $this->getRequest();
$current_user = $request->getUser();
$offset = $request->getInt('offset', 0);
$page_size = 100;
$pager = new AphrontPagerView();
$request_uri = $request->getRequestURI();
$pager->setURI($request_uri, 'offset');
$pager->setPageSize($page_size);
$pager->setOffset($offset);
$query = new PhabricatorOAuthServerClientQuery();
$query->withCreatorPHIDs(array($current_user->getPHID()));
$clients = $query->executeWithOffsetPager($pager);
$rows = array();
$rowc = array();
$highlight = $this->getHighlightPHIDs();
foreach ($clients as $client) {
$row = array(
phutil_render_tag(
'a',
array(
'href' => $client->getViewURI(),
),
phutil_escape_html($client->getName())
),
$client->getPHID(),
$client->getSecret(),
phutil_render_tag(
'a',
array(
'href' => $client->getRedirectURI(),
),
phutil_escape_html($client->getRedirectURI())
),
phutil_render_tag(
'a',
array(
'class' => 'small button grey',
'href' => $client->getEditURI(),
),
'Edit'
),
);
$rows[] = $row;
if (isset($highlight[$client->getPHID()])) {
$rowc[] = 'highlighted';
} else {
$rowc[] = '';
}
}
$panel = $this->buildClientList($rows, $rowc, $title);
return $this->buildStandardPageResponse(
array(
$this->getNoticeView(),
$panel->appendChild($pager)
),
array('title' => $title)
);
}
private function buildClientList($rows, $rowc, $title) {
$table = new AphrontTableView($rows);
$table->setRowClasses($rowc);
$table->setHeaders(
array(
'Client',
'ID',
'Secret',
'Redirect URI',
'',
));
$table->setColumnClasses(
array(
'',
'',
'',
'',
'action',
));
if (empty($rows)) {
$table->setNoDataString(
'You have not created any clients for this OAuthServer.'
);
}
$panel = new AphrontPanelView();
$panel->appendChild($table);
$panel->setHeader($title);
return $panel;
}
private function getNoticeView() {
$edited = $this->getRequest()->getStr('edited');
$new = $this->getRequest()->getStr('new');
$deleted = $this->getRequest()->getBool('deleted');
if ($edited) {
$edited = phutil_escape_html($edited);
$title = 'Successfully edited client with id '.$edited.'.';
} else if ($new) {
$new = phutil_escape_html($new);
$title = 'Successfully created client with id '.$new.'.';
} else if ($deleted) {
$title = 'Successfully deleted client.';
} else {
$title = null;
}
if ($title) {
$view = new AphrontErrorView();
$view->setTitle($title);
$view->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
} else {
$view = null;
}
return $view;
}
}
diff --git a/src/applications/oauthserver/controller/client/PhabricatorOAuthClientViewController.php b/src/applications/oauthserver/controller/client/PhabricatorOAuthClientViewController.php
index 2a6cdabf3d..b9c2f277b6 100644
--- a/src/applications/oauthserver/controller/client/PhabricatorOAuthClientViewController.php
+++ b/src/applications/oauthserver/controller/client/PhabricatorOAuthClientViewController.php
@@ -1,134 +1,118 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group oauthserver
*/
final class PhabricatorOAuthClientViewController
extends PhabricatorOAuthClientBaseController {
protected function getFilter() {
return 'client/view/'.$this->getClientPHID();
}
protected function getExtraClientFilters() {
return array(
array('url' => $this->getFilter(),
'label' => 'View Client')
);
}
public function processRequest() {
$request = $this->getRequest();
$current_user = $request->getUser();
$error = null;
$phid = $this->getClientPHID();
$client = id(new PhabricatorOAuthServerClient())
->loadOneWhere('phid = %s',
$phid);
$title = 'View OAuth Client';
// validate the client
if (empty($client)) {
$message = 'No client found with id '.$phid.'.';
return $this->buildStandardPageResponse(
$this->buildErrorView($message),
array('title' => $title)
);
}
$panel = new AphrontPanelView();
$panel->setHeader($title);
$form = id(new AphrontFormView())
->setUser($current_user)
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Name')
->setValue($client->getName())
)
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('ID')
->setValue($phid)
);
if ($current_user->getPHID() == $client->getCreatorPHID()) {
$form
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Secret')
->setValue($client->getSecret())
);
}
$form
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Redirect URI')
->setValue($client->getRedirectURI())
);
$created = phabricator_datetime($client->getDateCreated(),
$current_user);
$updated = phabricator_datetime($client->getDateModified(),
$current_user);
$form
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Created')
->setValue($created)
)
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Last Updated')
->setValue($updated)
);
$panel->appendChild($form);
$admin_panel = null;
if ($client->getCreatorPHID() == $current_user->getPHID()) {
$edit_button = phutil_render_tag(
'a',
array(
'href' => $client->getEditURI(),
'class' => 'grey button',
),
'Edit OAuth Client');
$panel->addButton($edit_button);
$create_authorization_form = id(new AphrontFormView())
->setUser($current_user)
->addHiddenInput('action', 'testclientauthorization')
->addHiddenInput('client_phid', $phid)
->setAction('/oauthserver/test/')
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Create Scopeless Test Authorization')
);
$admin_panel = id(new AphrontPanelView())
->setHeader('Admin Tools')
->appendChild($create_authorization_form);
}
return $this->buildStandardPageResponse(
array($error,
$panel,
$admin_panel
),
array('title' => $title)
);
}
}
diff --git a/src/applications/oauthserver/controller/clientauthorization/PhabricatorOAuthClientAuthorizationBaseController.php b/src/applications/oauthserver/controller/clientauthorization/PhabricatorOAuthClientAuthorizationBaseController.php
index a69630101c..c508f2cb9f 100644
--- a/src/applications/oauthserver/controller/clientauthorization/PhabricatorOAuthClientAuthorizationBaseController.php
+++ b/src/applications/oauthserver/controller/clientauthorization/PhabricatorOAuthClientAuthorizationBaseController.php
@@ -1,41 +1,25 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group oauthserver
*/
abstract class PhabricatorOAuthClientAuthorizationBaseController
extends PhabricatorOAuthServerController {
private $authorizationPHID;
protected function getAuthorizationPHID() {
return $this->authorizationPHID;
}
private function setAuthorizationPHID($phid) {
$this->authorizationPHID = $phid;
return $this;
}
public function shouldRequireLogin() {
return true;
}
public function willProcessRequest(array $data) {
$this->setAuthorizationPHID(idx($data, 'phid'));
}
}
diff --git a/src/applications/oauthserver/controller/clientauthorization/PhabricatorOAuthClientAuthorizationDeleteController.php b/src/applications/oauthserver/controller/clientauthorization/PhabricatorOAuthClientAuthorizationDeleteController.php
index 5c2cda42f0..94c9839d79 100644
--- a/src/applications/oauthserver/controller/clientauthorization/PhabricatorOAuthClientAuthorizationDeleteController.php
+++ b/src/applications/oauthserver/controller/clientauthorization/PhabricatorOAuthClientAuthorizationDeleteController.php
@@ -1,75 +1,59 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group oauthserver
*/
final class PhabricatorOAuthClientAuthorizationDeleteController
extends PhabricatorOAuthClientAuthorizationBaseController {
public function processRequest() {
$phid = $this->getAuthorizationPHID();
$title = 'Delete OAuth Client Authorization';
$request = $this->getRequest();
$current_user = $request->getUser();
$authorization = id(new PhabricatorOAuthClientAuthorization())
->loadOneWhere('phid = %s',
$phid);
if (empty($authorization)) {
return new Aphront404Response();
}
if ($authorization->getUserPHID() != $current_user->getPHID()) {
$message = 'Access denied to client authorization with phid '.$phid.'. '.
'Only the user who authorized the client has permission to '.
'delete the authorization.';
return id(new Aphront403Response())
->setForbiddenText($message);
}
if ($request->isFormPost()) {
$authorization->delete();
return id(new AphrontRedirectResponse())
->setURI('/oauthserver/clientauthorization/?notice=deleted');
}
$client_phid = $authorization->getClientPHID();
$client = id(new PhabricatorOAuthServerClient())
->loadOneWhere('phid = %s',
$client_phid);
if ($client) {
$client_name = phutil_escape_html($client->getName());
$title .= ' for '.$client_name;
} else {
// the client does not exist so token is dead already (but
// let's let the user clean this up anyway in that case)
$client_name = '';
}
$dialog = new AphrontDialogView();
$dialog->setUser($current_user);
$dialog->setTitle($title);
$dialog->appendChild(
'<p>Are you sure you want to delete this client authorization?</p>'
);
$dialog->addSubmitButton();
$dialog->addCancelButton($authorization->getEditURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/oauthserver/controller/clientauthorization/PhabricatorOAuthClientAuthorizationEditController.php b/src/applications/oauthserver/controller/clientauthorization/PhabricatorOAuthClientAuthorizationEditController.php
index 685a2032ad..06d2969e67 100644
--- a/src/applications/oauthserver/controller/clientauthorization/PhabricatorOAuthClientAuthorizationEditController.php
+++ b/src/applications/oauthserver/controller/clientauthorization/PhabricatorOAuthClientAuthorizationEditController.php
@@ -1,114 +1,98 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group oauthserver
*/
final class PhabricatorOAuthClientAuthorizationEditController
extends PhabricatorOAuthClientAuthorizationBaseController {
public function processRequest() {
$phid = $this->getAuthorizationPHID();
$title = 'Edit OAuth Client Authorization';
$request = $this->getRequest();
$current_user = $request->getUser();
$authorization = id(new PhabricatorOAuthClientAuthorization())
->loadOneWhere('phid = %s',
$phid);
if (empty($authorization)) {
return new Aphront404Response();
}
if ($authorization->getUserPHID() != $current_user->getPHID()) {
$message = 'Access denied to client authorization with phid '.$phid.'. '.
'Only the user who authorized the client has permission to '.
'edit the authorization.';
return id(new Aphront403Response())
->setForbiddenText($message);
}
if ($request->isFormPost()) {
$scopes = PhabricatorOAuthServerScope::getScopesFromRequest($request);
$authorization->setScope($scopes);
$authorization->save();
return id(new AphrontRedirectResponse())
->setURI('/oauthserver/clientauthorization/?edited='.$phid);
}
$client_phid = $authorization->getClientPHID();
$client = id(new PhabricatorOAuthServerClient())
->loadOneWhere('phid = %s',
$client_phid);
$created = phabricator_datetime($authorization->getDateCreated(),
$current_user);
$updated = phabricator_datetime($authorization->getDateModified(),
$current_user);
$panel = new AphrontPanelView();
$delete_button = phutil_render_tag(
'a',
array(
'href' => $authorization->getDeleteURI(),
'class' => 'grey button',
),
'Delete OAuth Client Authorization');
$panel->addButton($delete_button);
$panel->setHeader($title);
$form = id(new AphrontFormView())
->setUser($current_user)
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel('Client')
->setValue(
phutil_render_tag(
'a',
array(
'href' => $client->getViewURI(),
),
phutil_escape_html($client->getName())))
)
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Created')
->setValue($created)
)
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Last Updated')
->setValue($updated)
)
->appendChild(
PhabricatorOAuthServerScope::getCheckboxControl(
$authorization->getScope()
)
)
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Save OAuth Client Authorization')
->addCancelButton('/oauthserver/clientauthorization/')
);
$panel->appendChild($form);
return $this->buildStandardPageResponse(
$panel,
array('title' => $title)
);
}
}
diff --git a/src/applications/oauthserver/controller/clientauthorization/PhabricatorOAuthClientAuthorizationListController.php b/src/applications/oauthserver/controller/clientauthorization/PhabricatorOAuthClientAuthorizationListController.php
index 446290102d..9468457353 100644
--- a/src/applications/oauthserver/controller/clientauthorization/PhabricatorOAuthClientAuthorizationListController.php
+++ b/src/applications/oauthserver/controller/clientauthorization/PhabricatorOAuthClientAuthorizationListController.php
@@ -1,174 +1,158 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group oauthserver
*/
final class PhabricatorOAuthClientAuthorizationListController
extends PhabricatorOAuthClientAuthorizationBaseController {
protected function getFilter() {
return 'clientauthorization';
}
public function processRequest() {
$title = 'OAuth Client Authorizations';
$request = $this->getRequest();
$current_user = $request->getUser();
$offset = $request->getInt('offset', 0);
$page_size = 100;
$pager = new AphrontPagerView();
$request_uri = $request->getRequestURI();
$pager->setURI($request_uri, 'offset');
$pager->setPageSize($page_size);
$pager->setOffset($offset);
$query = new PhabricatorOAuthClientAuthorizationQuery();
$query->withUserPHIDs(array($current_user->getPHID()));
$authorizations = $query->executeWithOffsetPager($pager);
$client_authorizations = mpull($authorizations, null, 'getClientPHID');
$client_phids = array_keys($client_authorizations);
if ($client_phids) {
$clients = id(new PhabricatorOAuthServerClient())
->loadAllWhere('phid in (%Ls)',
$client_phids);
} else {
$clients = array();
}
$client_dict = mpull($clients, null, 'getPHID');
$rows = array();
$rowc = array();
$highlight = $this->getHighlightPHIDs();
foreach ($client_authorizations as $client_phid => $authorization) {
$client = $client_dict[$client_phid];
$created = phabricator_datetime($authorization->getDateCreated(),
$current_user);
$updated = phabricator_datetime($authorization->getDateModified(),
$current_user);
$scope_doc_href = PhabricatorEnv::getDoclink(
'article/Using_the_Phabricator_OAuth_Server.html#scopes'
);
$row = array(
phutil_render_tag(
'a',
array(
'href' => $client->getViewURI(),
),
phutil_escape_html($client->getName())
),
phutil_render_tag(
'a',
array(
'href' => $scope_doc_href,
),
$authorization->getScopeString()
),
phabricator_datetime(
$authorization->getDateCreated(),
$current_user
),
phabricator_datetime(
$authorization->getDateModified(),
$current_user
),
phutil_render_tag(
'a',
array(
'class' => 'small button grey',
'href' => $authorization->getEditURI(),
),
'Edit'
),
);
$rows[] = $row;
if (isset($highlight[$authorization->getPHID()])) {
$rowc[] = 'highlighted';
} else {
$rowc[] = '';
}
}
$panel = $this->buildClientAuthorizationList($rows, $rowc, $title);
return $this->buildStandardPageResponse(
array(
$this->getNoticeView(),
$panel->appendChild($pager),
),
array('title' => $title)
);
}
private function buildClientAuthorizationList($rows, $rowc, $title) {
$table = new AphrontTableView($rows);
$table->setRowClasses($rowc);
$table->setHeaders(
array(
'Client',
'Scope',
'Created',
'Updated',
'',
));
$table->setColumnClasses(
array(
'wide pri',
'',
'',
'',
'action',
));
if (empty($rows)) {
$table->setNoDataString(
'You have not authorized any clients for this OAuthServer.'
);
}
$panel = new AphrontPanelView();
$panel->appendChild($table);
$panel->setHeader($title);
return $panel;
}
private function getNoticeView() {
$edited = $this->getRequest()->getStr('edited');
$deleted = $this->getRequest()->getBool('deleted');
if ($edited) {
$edited = phutil_escape_html($edited);
$title = 'Successfully edited client authorization.';
} else if ($deleted) {
$title = 'Successfully deleted client authorization.';
} else {
$title = null;
}
if ($title) {
$view = new AphrontErrorView();
$view->setTitle($title);
$view->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
} else {
$view = null;
}
return $view;
}
}
diff --git a/src/applications/oauthserver/query/PhabricatorOAuthClientAuthorizationQuery.php b/src/applications/oauthserver/query/PhabricatorOAuthClientAuthorizationQuery.php
index 237743ceb3..cb8fe083c7 100644
--- a/src/applications/oauthserver/query/PhabricatorOAuthClientAuthorizationQuery.php
+++ b/src/applications/oauthserver/query/PhabricatorOAuthClientAuthorizationQuery.php
@@ -1,60 +1,44 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorOAuthClientAuthorizationQuery
extends PhabricatorOffsetPagedQuery {
private $userPHIDs;
public function withUserPHIDs(array $phids) {
$this->userPHIDs = $phids;
return $this;
}
private function getUserPHIDs() {
return $this->userPHIDs;
}
public function execute() {
$table = new PhabricatorOAuthClientAuthorization();
$conn_r = $table->establishConnection('r');
$where_clause = $this->buildWhereClause($conn_r);
$limit_clause = $this->buildLimitClause($conn_r);
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T auth %Q %Q',
$table->getTableName(),
$where_clause,
$limit_clause);
return $table->loadAllFromArray($data);
}
private function buildWhereClause($conn_r) {
$where = array();
if ($this->getUserPHIDs()) {
$where[] = qsprintf(
$conn_r,
'userPHID IN (%Ls)',
$this->getUserPHIDs());
}
return $this->formatWhereClause($where);
}
}
diff --git a/src/applications/oauthserver/query/PhabricatorOAuthServerClientQuery.php b/src/applications/oauthserver/query/PhabricatorOAuthServerClientQuery.php
index cb6bc4733d..0e2272e6e4 100644
--- a/src/applications/oauthserver/query/PhabricatorOAuthServerClientQuery.php
+++ b/src/applications/oauthserver/query/PhabricatorOAuthServerClientQuery.php
@@ -1,60 +1,44 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorOAuthServerClientQuery
extends PhabricatorOffsetPagedQuery {
private $creatorPHIDs;
public function withCreatorPHIDs(array $phids) {
$this->creatorPHIDs = $phids;
return $this;
}
private function getCreatorPHIDs() {
return $this->creatorPHIDs;
}
public function execute() {
$table = new PhabricatorOAuthServerClient();
$conn_r = $table->establishConnection('r');
$where_clause = $this->buildWhereClause($conn_r);
$limit_clause = $this->buildLimitClause($conn_r);
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T client %Q %Q',
$table->getTableName(),
$where_clause,
$limit_clause);
return $table->loadAllFromArray($data);
}
private function buildWhereClause($conn_r) {
$where = array();
if ($this->getCreatorPHIDs()) {
$where[] = qsprintf(
$conn_r,
'creatorPHID IN (%Ls)',
$this->getCreatorPHIDs());
}
return $this->formatWhereClause($where);
}
}
diff --git a/src/applications/oauthserver/storage/PhabricatorOAuthClientAuthorization.php b/src/applications/oauthserver/storage/PhabricatorOAuthClientAuthorization.php
index 435e84a631..7a420a8152 100644
--- a/src/applications/oauthserver/storage/PhabricatorOAuthClientAuthorization.php
+++ b/src/applications/oauthserver/storage/PhabricatorOAuthClientAuthorization.php
@@ -1,59 +1,43 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group oauthserver
*/
final class PhabricatorOAuthClientAuthorization
extends PhabricatorOAuthServerDAO {
protected $id;
protected $phid;
protected $userPHID;
protected $clientPHID;
protected $scope;
public function getEditURI() {
return '/oauthserver/clientauthorization/edit/'.$this->getPHID().'/';
}
public function getDeleteURI() {
return '/oauthserver/clientauthorization/delete/'.$this->getPHID().'/';
}
public function getScopeString() {
$scope = $this->getScope();
$scopes = array_keys($scope);
sort($scopes);
return implode(' ', $scopes);
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'scope' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_OASA);
}
}
diff --git a/src/applications/oauthserver/storage/PhabricatorOAuthServerAccessToken.php b/src/applications/oauthserver/storage/PhabricatorOAuthServerAccessToken.php
index 6831ed40cd..c3ffc05d25 100644
--- a/src/applications/oauthserver/storage/PhabricatorOAuthServerAccessToken.php
+++ b/src/applications/oauthserver/storage/PhabricatorOAuthServerAccessToken.php
@@ -1,28 +1,12 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group oauthserver
*/
final class PhabricatorOAuthServerAccessToken
extends PhabricatorOAuthServerDAO {
protected $id;
protected $token;
protected $userPHID;
protected $clientPHID;
}
diff --git a/src/applications/oauthserver/storage/PhabricatorOAuthServerAuthorizationCode.php b/src/applications/oauthserver/storage/PhabricatorOAuthServerAuthorizationCode.php
index d9b67bef7b..031674ba34 100644
--- a/src/applications/oauthserver/storage/PhabricatorOAuthServerAuthorizationCode.php
+++ b/src/applications/oauthserver/storage/PhabricatorOAuthServerAuthorizationCode.php
@@ -1,31 +1,15 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group oauthserver
*/
final class PhabricatorOAuthServerAuthorizationCode
extends PhabricatorOAuthServerDAO {
protected $id;
protected $code;
protected $clientPHID;
protected $clientSecret;
protected $userPHID;
protected $redirectURI;
}
diff --git a/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php b/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php
index 599b4f0b99..1c6f4aac17 100644
--- a/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php
+++ b/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php
@@ -1,55 +1,39 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group oauthserver
*/
final class PhabricatorOAuthServerClient
extends PhabricatorOAuthServerDAO {
protected $id;
protected $phid;
protected $secret;
protected $name;
protected $redirectURI;
protected $creatorPHID;
public function getEditURI() {
return '/oauthserver/client/edit/'.$this->getPHID().'/';
}
public function getViewURI() {
return '/oauthserver/client/view/'.$this->getPHID().'/';
}
public function getDeleteURI() {
return '/oauthserver/client/delete/'.$this->getPHID().'/';
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_OASC);
}
}
diff --git a/src/applications/oauthserver/storage/PhabricatorOAuthServerDAO.php b/src/applications/oauthserver/storage/PhabricatorOAuthServerDAO.php
index 58df509513..374c5965e7 100644
--- a/src/applications/oauthserver/storage/PhabricatorOAuthServerDAO.php
+++ b/src/applications/oauthserver/storage/PhabricatorOAuthServerDAO.php
@@ -1,26 +1,10 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group oauthserver
*/
abstract class PhabricatorOAuthServerDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'oauth_server';
}
}
diff --git a/src/applications/owners/OwnersPackageReplyHandler.php b/src/applications/owners/OwnersPackageReplyHandler.php
index 6878f5ab18..31bdf4cb00 100644
--- a/src/applications/owners/OwnersPackageReplyHandler.php
+++ b/src/applications/owners/OwnersPackageReplyHandler.php
@@ -1,48 +1,32 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class OwnersPackageReplyHandler extends PhabricatorMailReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof PhabricatorOwnersPackage)) {
throw new Exception("Receiver is not a PhabricatorOwnersPackage!");
}
}
public function getPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle) {
return null;
}
public function getPublicReplyHandlerEmailAddress() {
return null;
}
public function getReplyHandlerDomain() {
return null;
}
public function getReplyHandlerInstructions() {
return null;
}
protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
return;
}
}
diff --git a/src/applications/owners/application/PhabricatorApplicationOwners.php b/src/applications/owners/application/PhabricatorApplicationOwners.php
index 8568c56f91..4d67d0f8cb 100644
--- a/src/applications/owners/application/PhabricatorApplicationOwners.php
+++ b/src/applications/owners/application/PhabricatorApplicationOwners.php
@@ -1,62 +1,46 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationOwners extends PhabricatorApplication {
public function getBaseURI() {
return '/owners/';
}
public function getAutospriteName() {
return 'owners';
}
public function getShortDescription() {
return 'Group Source Code';
}
public function getTitleGlyph() {
return "\xE2\x98\x81";
}
public function getHelpURI() {
return PhabricatorEnv::getDoclink('article/Owners_Tool_User_Guide.html');
}
public function getFlavorText() {
return pht('Adopt today!');
}
public function getApplicationGroup() {
return self::GROUP_ORGANIZATION;
}
public function getRoutes() {
return array(
'/owners/' => array(
'' => 'PhabricatorOwnersListController',
'view/(?P<view>[^/]+)/' => 'PhabricatorOwnersListController',
'edit/(?P<id>[1-9]\d*)/' => 'PhabricatorOwnersEditController',
'new/' => 'PhabricatorOwnersEditController',
'package/(?P<id>[1-9]\d*)/' => 'PhabricatorOwnersDetailController',
'delete/(?P<id>[1-9]\d*)/' => 'PhabricatorOwnersDeleteController',
),
);
}
}
diff --git a/src/applications/owners/controller/PhabricatorOwnersController.php b/src/applications/owners/controller/PhabricatorOwnersController.php
index e3c5002e73..2b6c9df6c3 100644
--- a/src/applications/owners/controller/PhabricatorOwnersController.php
+++ b/src/applications/owners/controller/PhabricatorOwnersController.php
@@ -1,75 +1,59 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorOwnersController extends PhabricatorController {
private $filter;
private function getSideNavFilter() {
return $this->filter;
}
protected function setSideNavFilter($filter) {
$this->filter = $filter;
return $this;
}
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('Owners');
$page->setBaseURI('/owners/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph("\xE2\x98\x81");
$nav = $this->renderSideNav();
$nav->appendChild($view);
$page->appendChild($nav);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
public function renderSideNav() {
$package_views = array(
array('name' => 'Owned',
'key' => 'view/owned'),
array('name' => 'All',
'key' => 'view/all'),
);
$package_views =
array_merge($this->getExtraPackageViews(),
$package_views);
$base_uri = new PhutilURI('/owners/');
$nav = new AphrontSideNavFilterView();
$nav->setBaseUri($base_uri);
$nav->addLabel('Packages');
$nav->addFilters($package_views);
$filter = $this->getSideNavFilter();
$nav->selectFilter($filter, 'view/owned');
return $nav;
}
protected function getExtraPackageViews() {
return array();
}
}
diff --git a/src/applications/owners/controller/PhabricatorOwnersDeleteController.php b/src/applications/owners/controller/PhabricatorOwnersDeleteController.php
index 949fd8fbcc..4aa31e8d51 100644
--- a/src/applications/owners/controller/PhabricatorOwnersDeleteController.php
+++ b/src/applications/owners/controller/PhabricatorOwnersDeleteController.php
@@ -1,57 +1,41 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorOwnersDeleteController
extends PhabricatorOwnersController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$package = id(new PhabricatorOwnersPackage())->load($this->id);
if (!$package) {
return new Aphront404Response();
}
if ($request->isDialogFormPost()) {
$package->attachActorPHID($user->getPHID());
$package->delete();
return id(new AphrontRedirectResponse())->setURI('/owners/');
}
$dialog = id(new AphrontDialogView())
->setUser($user)
->setTitle('Really delete this package?')
->appendChild(
'<p>Are you sure you want to delete the "'.
phutil_escape_html($package->getName()).'" package? This operation '.
'can not be undone.</p>')
->addSubmitButton('Delete')
->addCancelButton('/owners/package/'.$package->getID().'/')
->setSubmitURI($request->getRequestURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/owners/controller/PhabricatorOwnersDetailController.php b/src/applications/owners/controller/PhabricatorOwnersDetailController.php
index edc58ca108..a7b0f7a277 100644
--- a/src/applications/owners/controller/PhabricatorOwnersDetailController.php
+++ b/src/applications/owners/controller/PhabricatorOwnersDetailController.php
@@ -1,250 +1,234 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorOwnersDetailController
extends PhabricatorOwnersController {
private $id;
private $package;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$package = id(new PhabricatorOwnersPackage())->load($this->id);
if (!$package) {
return new Aphront404Response();
}
$this->package = $package;
$paths = $package->loadPaths();
$owners = $package->loadOwners();
$repository_phids = array();
foreach ($paths as $path) {
$repository_phids[$path->getRepositoryPHID()] = true;
}
if ($repository_phids) {
$repositories = id(new PhabricatorRepository())->loadAllWhere(
'phid in (%Ls)',
array_keys($repository_phids));
$repositories = mpull($repositories, null, 'getPHID');
} else {
$repositories = array();
}
$phids = array();
foreach ($owners as $owner) {
$phids[$owner->getUserPHID()] = true;
}
$phids = array_keys($phids);
$handles = $this->loadViewerHandles($phids);
$rows = array();
$rows[] = array(
'Name',
phutil_escape_html($package->getName()));
$rows[] = array(
'Description',
phutil_escape_html($package->getDescription()));
$primary_owner = null;
$primary_phid = $package->getPrimaryOwnerPHID();
if ($primary_phid && isset($handles[$primary_phid])) {
$primary_owner =
'<strong>'.$handles[$primary_phid]->renderLink().'</strong>';
}
$rows[] = array(
'Primary Owner',
$primary_owner,
);
$owner_links = array();
foreach ($owners as $owner) {
$owner_links[] = $handles[$owner->getUserPHID()]->renderLink();
}
$owner_links = implode('<br />', $owner_links);
$rows[] = array(
'Owners',
$owner_links);
$rows[] = array(
'Auditing',
$package->getAuditingEnabled() ? 'Enabled' : 'Disabled',
);
$path_links = array();
foreach ($paths as $path) {
$repo = $repositories[$path->getRepositoryPHID()];
$href = DiffusionRequest::generateDiffusionURI(
array(
'callsign' => $repo->getCallsign(),
'branch' => $repo->getDefaultBranch(),
'path' => $path->getPath(),
'action' => 'browse'
));
$repo_name = '<strong>'.phutil_escape_html($repo->getName()).
'</strong>';
$path_link = phutil_render_tag(
'a',
array(
'href' => (string) $href,
),
phutil_escape_html($path->getPath()));
$path_links[] = $repo_name.' '.$path_link;
}
$path_links = implode('<br />', $path_links);
$rows[] = array(
'Paths',
$path_links);
$table = new AphrontTableView($rows);
$table->setColumnClasses(
array(
'header',
'wide',
));
$panel = new AphrontPanelView();
$panel->setHeader(
'Package Details for "'.phutil_escape_html($package->getName()).'"');
$panel->addButton(
javelin_render_tag(
'a',
array(
'href' => '/owners/delete/'.$package->getID().'/',
'class' => 'button grey',
'sigil' => 'workflow',
),
'Delete Package'));
$panel->addButton(
phutil_render_tag(
'a',
array(
'href' => '/owners/edit/'.$package->getID().'/',
'class' => 'button',
),
'Edit Package'));
$panel->appendChild($table);
$key = 'package/'.$package->getID();
$this->setSideNavFilter($key);
$commit_views = array();
$commit_uri = id(new PhutilURI('/audit/view/packagecommits/'))
->setQueryParams(
array(
'phid' => $package->getPHID(),
));
$attention_query = id(new PhabricatorAuditCommitQuery())
->withPackagePHIDs(array($package->getPHID()))
->withStatus(PhabricatorAuditCommitQuery::STATUS_OPEN)
->needCommitData(true)
->needAudits(true)
->setLimit(10);
$attention_commits = $attention_query->execute();
if ($attention_commits) {
$view = new PhabricatorAuditCommitListView();
$view->setUser($user);
$view->setCommits($attention_commits);
$commit_views[] = array(
'view' => $view,
'header' => 'Commits in this Package that Need Attention',
'button' => phutil_render_tag(
'a',
array(
'href' => $commit_uri->alter('status', 'open'),
'class' => 'button grey',
),
'View All Problem Commits'),
);
}
$all_query = id(new PhabricatorAuditCommitQuery())
->withPackagePHIDs(array($package->getPHID()))
->needCommitData(true)
->needAudits(true)
->setLimit(100);
$all_commits = $all_query->execute();
$view = new PhabricatorAuditCommitListView();
$view->setUser($user);
$view->setCommits($all_commits);
$view->setNoDataString('No commits in this package.');
$commit_views[] = array(
'view' => $view,
'header' => 'Recent Commits in Package',
'button' => phutil_render_tag(
'a',
array(
'href' => $commit_uri,
'class' => 'button grey',
),
'View All Package Commits'),
);
$phids = array();
foreach ($commit_views as $commit_view) {
$phids[] = $commit_view['view']->getRequiredHandlePHIDs();
}
$phids = array_mergev($phids);
$handles = $this->loadViewerHandles($phids);
$commit_panels = array();
foreach ($commit_views as $commit_view) {
$commit_panel = new AphrontPanelView();
$commit_panel->setHeader(phutil_escape_html($commit_view['header']));
if (isset($commit_view['button'])) {
$commit_panel->addButton($commit_view['button']);
}
$commit_view['view']->setHandles($handles);
$commit_panel->appendChild($commit_view['view']);
$commit_panels[] = $commit_panel;
}
return $this->buildStandardPageResponse(
array(
$panel,
$commit_panels,
),
array(
'title' => "Package '".$package->getName()."'",
));
}
protected function getExtraPackageViews() {
$package = $this->package;
return array(
array('name' => 'Details',
'key' => 'package/'.$package->getID(),
));
}
}
diff --git a/src/applications/owners/controller/PhabricatorOwnersEditController.php b/src/applications/owners/controller/PhabricatorOwnersEditController.php
index f11b1b20d8..94af80fffb 100644
--- a/src/applications/owners/controller/PhabricatorOwnersEditController.php
+++ b/src/applications/owners/controller/PhabricatorOwnersEditController.php
@@ -1,286 +1,270 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorOwnersEditController
extends PhabricatorOwnersController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
if ($this->id) {
$package = id(new PhabricatorOwnersPackage())->load($this->id);
if (!$package) {
return new Aphront404Response();
}
} else {
$package = new PhabricatorOwnersPackage();
$package->setPrimaryOwnerPHID($user->getPHID());
}
$e_name = true;
$e_primary = true;
$errors = array();
if ($request->isFormPost()) {
$package->setName($request->getStr('name'));
$package->setDescription($request->getStr('description'));
$old_auditing_enabled = $package->getAuditingEnabled();
$package->setAuditingEnabled($request->getStr('auditing') === 'enabled');
$primary = $request->getArr('primary');
$primary = reset($primary);
$old_primary = $package->getPrimaryOwnerPHID();
$package->setPrimaryOwnerPHID($primary);
$owners = $request->getArr('owners');
if ($primary) {
array_unshift($owners, $primary);
}
$owners = array_unique($owners);
$paths = $request->getArr('path');
$repos = $request->getArr('repo');
$path_refs = array();
for ($ii = 0; $ii < count($paths); $ii++) {
if (empty($paths[$ii]) || empty($repos[$ii])) {
continue;
}
$path_refs[] = array(
'repositoryPHID' => $repos[$ii],
'path' => $paths[$ii],
);
}
if (!strlen($package->getName())) {
$e_name = 'Required';
$errors[] = 'Package name is required.';
} else {
$e_name = null;
}
if (!$package->getPrimaryOwnerPHID()) {
$e_primary = 'Required';
$errors[] = 'Package must have a primary owner.';
} else {
$e_primary = null;
}
if (!$path_refs) {
$errors[] = 'Package must include at least one path.';
}
if (!$errors) {
$package->attachUnsavedOwners($owners);
$package->attachUnsavedPaths($path_refs);
$package->attachOldAuditingEnabled($old_auditing_enabled);
$package->attachOldPrimaryOwnerPHID($old_primary);
$package->attachActorPHID($user->getPHID());
try {
$package->save();
return id(new AphrontRedirectResponse())
->setURI('/owners/package/'.$package->getID().'/');
} catch (AphrontQueryDuplicateKeyException $ex) {
$e_name = 'Duplicate';
$errors[] = 'Package name must be unique.';
}
}
} else {
$owners = $package->loadOwners();
$owners = mpull($owners, 'getUserPHID');
$paths = $package->loadPaths();
$path_refs = array();
foreach ($paths as $path) {
$path_refs[] = array(
'repositoryPHID' => $path->getRepositoryPHID(),
'path' => $path->getPath(),
);
}
}
$error_view = null;
if ($errors) {
$error_view = new AphrontErrorView();
$error_view->setTitle('Package Errors');
$error_view->setErrors($errors);
}
$handles = $this->loadViewerHandles($owners);
$primary = $package->getPrimaryOwnerPHID();
if ($primary && isset($handles[$primary])) {
$token_primary_owner = array(
$primary => $handles[$primary]->getFullName(),
);
} else {
$token_primary_owner = array();
}
$token_all_owners = array_select_keys($handles, $owners);
$token_all_owners = mpull($token_all_owners, 'getFullName');
if ($package->getID()) {
$title = 'Edit Package';
$side_nav_filter = 'edit/'.$this->id;
} else {
$title = 'New Package';
$side_nav_filter = 'new';
}
$this->setSideNavFilter($side_nav_filter);
$repos = id(new PhabricatorRepository())->loadAll();
$default_paths = array();
foreach ($repos as $repo) {
$default_path = $repo->getDetail('default-owners-path');
if ($default_path) {
$default_paths[$repo->getPHID()] = $default_path;
}
}
$repos = mpull($repos, 'getCallsign', 'getPHID');
$template = new AphrontTypeaheadTemplateView();
$template = $template->render();
Javelin::initBehavior(
'owners-path-editor',
array(
'root' => 'path-editor',
'table' => 'paths',
'add_button' => 'addpath',
'repositories' => $repos,
'input_template' => $template,
'pathRefs' => $path_refs,
'completeURI' => '/diffusion/services/path/complete/',
'validateURI' => '/diffusion/services/path/validate/',
'repositoryDefaultPaths' => $default_paths,
));
require_celerity_resource('owners-path-editor-css');
$cancel_uri = $package->getID()
? '/owners/package/'.$package->getID().'/'
: '/owners/';
$form = id(new AphrontFormView())
->setUser($user)
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Name')
->setName('name')
->setValue($package->getName())
->setError($e_name))
->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/usersorprojects/')
->setLabel('Primary Owner')
->setName('primary')
->setLimit(1)
->setValue($token_primary_owner)
->setError($e_primary))
->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/usersorprojects/')
->setLabel('Owners')
->setName('owners')
->setValue($token_all_owners))
->appendChild(
id(new AphrontFormSelectControl())
->setName('auditing')
->setLabel('Auditing')
->setCaption('With auditing enabled, all future commits that touch '.
'this package will be reviewed to make sure an owner '.
'of the package is involved and the commit message has '.
'a valid revision, reviewed by, and author.')
->setOptions(array(
'disabled' => 'Disabled',
'enabled' => 'Enabled',
))
->setValue(
$package->getAuditingEnabled()
? 'enabled'
: 'disabled'))
->appendChild(
id(new AphrontFormInsetView())
->setTitle('Paths')
->addDivAttributes(array('id' => 'path-editor'))
->setRightButton(javelin_render_tag(
'a',
array(
'href' => '#',
'class' => 'button green',
'sigil' => 'addpath',
'mustcapture' => true,
),
'Add New Path'))
->setDescription('Specify the files and directories which comprise '.
'this package.')
->setContent(javelin_render_tag(
'table',
array(
'class' => 'owners-path-editor-table',
'sigil' => 'paths',
),
'')))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Description')
->setName('description')
->setValue($package->getDescription()))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($cancel_uri)
->setValue('Save Package'));
$panel = new AphrontPanelView();
$panel->setHeader($title);
$panel->setWidth(AphrontPanelView::WIDTH_WIDE);
$panel->appendChild($error_view);
$panel->appendChild($form);
return $this->buildStandardPageResponse(
$panel,
array(
'title' => $title,
));
}
protected function getExtraPackageViews() {
if ($this->id) {
$extra = array(array('name' => 'Edit',
'key' => 'edit/'.$this->id));
} else {
$extra = array(array('name' => 'New',
'key' => 'new'));
}
return $extra;
}
}
diff --git a/src/applications/owners/controller/PhabricatorOwnersListController.php b/src/applications/owners/controller/PhabricatorOwnersListController.php
index 1a501bcff2..8dc157715c 100644
--- a/src/applications/owners/controller/PhabricatorOwnersListController.php
+++ b/src/applications/owners/controller/PhabricatorOwnersListController.php
@@ -1,340 +1,324 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorOwnersListController
extends PhabricatorOwnersController {
protected $view;
public function willProcessRequest(array $data) {
$this->view = idx($data, 'view', 'owned');
$this->setSideNavFilter('view/'.$this->view);
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$package = new PhabricatorOwnersPackage();
$owner = new PhabricatorOwnersOwner();
$path = new PhabricatorOwnersPath();
$repository_phid = '';
if ($request->getStr('repository') != '') {
$repository_phid = id(new PhabricatorRepository())
->loadOneWhere('callsign = %s', $request->getStr('repository'))
->getPHID();
}
switch ($this->view) {
case 'search':
$packages = array();
$conn_r = $package->establishConnection('r');
$where = array('1 = 1');
$join = array();
if ($request->getStr('name')) {
$where[] = qsprintf(
$conn_r,
'p.name LIKE %~',
$request->getStr('name'));
}
if ($repository_phid || $request->getStr('path')) {
$join[] = qsprintf(
$conn_r,
'JOIN %T path ON path.packageID = p.id',
$path->getTableName());
if ($repository_phid) {
$where[] = qsprintf(
$conn_r,
'path.repositoryPHID = %s',
$repository_phid);
}
if ($request->getStr('path')) {
$where[] = qsprintf(
$conn_r,
'path.path LIKE %~ OR %s LIKE CONCAT(path.path, %s)',
$request->getStr('path'),
$request->getStr('path'),
'%');
}
}
if ($request->getArr('owner')) {
$join[] = qsprintf(
$conn_r,
'JOIN %T o ON o.packageID = p.id',
$owner->getTableName());
$where[] = qsprintf(
$conn_r,
'o.userPHID IN (%Ls)',
$request->getArr('owner'));
}
$data = queryfx_all(
$conn_r,
'SELECT p.* FROM %T p %Q WHERE %Q GROUP BY p.id',
$package->getTableName(),
implode(' ', $join),
'('.implode(') AND (', $where).')');
$packages = $package->loadAllFromArray($data);
$header = 'Search Results';
$nodata = 'No packages match your query.';
break;
case 'owned':
$data = queryfx_all(
$package->establishConnection('r'),
'SELECT p.* FROM %T p JOIN %T o ON p.id = o.packageID
WHERE o.userPHID = %s GROUP BY p.id',
$package->getTableName(),
$owner->getTableName(),
$user->getPHID());
$packages = $package->loadAllFromArray($data);
$header = 'Owned Packages';
$nodata = 'No owned packages';
break;
case 'all':
$packages = $package->loadAll();
$header = 'All Packages';
$nodata = 'There are no defined packages.';
break;
}
$content = $this->renderPackageTable(
$packages,
$header,
$nodata);
$filter = new AphrontListFilterView();
$filter->addButton(
phutil_render_tag(
'a',
array(
'href' => '/owners/new/',
'class' => 'green button',
),
'Create New Package'));
$owners_search_value = array();
if ($request->getArr('owner')) {
$phids = $request->getArr('owner');
$phid = reset($phids);
$handles = $this->loadViewerHandles(array($phid));
$owners_search_value = array(
$phid => $handles[$phid]->getFullName(),
);
}
$callsigns = array('' => '(Any Repository)');
$repositories = id(new PhabricatorRepository())
->loadAllWhere('1 = 1 ORDER BY callsign');
foreach ($repositories as $repository) {
$callsigns[$repository->getCallsign()] =
$repository->getCallsign().': '.$repository->getName();
}
$form = id(new AphrontFormView())
->setUser($user)
->setAction('/owners/view/search/')
->setMethod('GET')
->appendChild(
id(new AphrontFormTextControl())
->setName('name')
->setLabel('Name')
->setValue($request->getStr('name')))
->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/usersorprojects/')
->setLimit(1)
->setName('owner')
->setLabel('Owner')
->setValue($owners_search_value))
->appendChild(
id(new AphrontFormSelectControl())
->setName('repository')
->setLabel('Repository')
->setOptions($callsigns)
->setValue($request->getStr('repository')))
->appendChild(
id(new AphrontFormTextControl())
->setName('path')
->setLabel('Path')
->setValue($request->getStr('path')))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Search for Packages'));
$filter->appendChild($form);
return $this->buildStandardPageResponse(
array(
$filter,
$content,
),
array(
'title' => 'Package Index',
));
}
private function renderPackageTable(array $packages, $header, $nodata) {
assert_instances_of($packages, 'PhabricatorOwnersPackage');
if ($packages) {
$package_ids = mpull($packages, 'getID');
$owners = id(new PhabricatorOwnersOwner())->loadAllWhere(
'packageID IN (%Ld)',
$package_ids);
$paths = id(new PhabricatorOwnersPath())->loadAllWhere(
'packageID in (%Ld)',
$package_ids);
$phids = array();
foreach ($owners as $owner) {
$phids[$owner->getUserPHID()] = true;
}
$phids = array_keys($phids);
$handles = $this->loadViewerHandles($phids);
$repository_phids = array();
foreach ($paths as $path) {
$repository_phids[$path->getRepositoryPHID()] = true;
}
if ($repository_phids) {
$repositories = id(new PhabricatorRepository())->loadAllWhere(
'phid in (%Ls)',
array_keys($repository_phids));
} else {
$repositories = array();
}
$repositories = mpull($repositories, null, 'getPHID');
$owners = mgroup($owners, 'getPackageID');
$paths = mgroup($paths, 'getPackageID');
} else {
$handles = array();
$repositories = array();
$owners = array();
$paths = array();
}
$rows = array();
foreach ($packages as $package) {
$pkg_owners = idx($owners, $package->getID(), array());
foreach ($pkg_owners as $key => $owner) {
$pkg_owners[$key] = $handles[$owner->getUserPHID()]->renderLink();
if ($owner->getUserPHID() == $package->getPrimaryOwnerPHID()) {
$pkg_owners[$key] = '<strong>'.$pkg_owners[$key].'</strong>';
}
}
$pkg_owners = implode('<br />', $pkg_owners);
$pkg_paths = idx($paths, $package->getID(), array());
foreach ($pkg_paths as $key => $path) {
$repo = $repositories[$path->getRepositoryPHID()];
if ($repo) {
$href = DiffusionRequest::generateDiffusionURI(
array(
'callsign' => $repo->getCallsign(),
'branch' => $repo->getDefaultBranch(),
'path' => $path->getPath(),
'action' => 'browse',
));
$pkg_paths[$key] =
'<strong>'.phutil_escape_html($repo->getName()).'</strong> '.
phutil_render_tag(
'a',
array(
'href' => (string) $href,
),
phutil_escape_html($path->getPath()));
} else {
$pkg_paths[$key] = phutil_escape_html($path->getPath());
}
}
$pkg_paths = implode('<br />', $pkg_paths);
$rows[] = array(
phutil_render_tag(
'a',
array(
'href' => '/owners/package/'.$package->getID().'/',
),
phutil_escape_html($package->getName())),
$pkg_owners,
$pkg_paths,
phutil_render_tag(
'a',
array(
'href' => '/audit/view/packagecommits/?phid='.$package->getPHID(),
),
phutil_escape_html('Related Commits'))
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Name',
'Owners',
'Paths',
'Related Commits',
));
$table->setColumnClasses(
array(
'pri',
'',
'wide wrap',
'narrow',
));
$panel = new AphrontPanelView();
$panel->setHeader($header);
$panel->appendChild($table);
return $panel;
}
protected function getExtraPackageViews() {
switch ($this->view) {
case 'search':
$extra = array(array('name' => 'Search Results',
'key' => 'view/search'));
break;
default:
$extra = array();
break;
}
return $extra;
}
}
diff --git a/src/applications/owners/mail/PackageCreateMail.php b/src/applications/owners/mail/PackageCreateMail.php
index 03d0a8650d..486704afd8 100644
--- a/src/applications/owners/mail/PackageCreateMail.php
+++ b/src/applications/owners/mail/PackageCreateMail.php
@@ -1,28 +1,12 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PackageCreateMail extends PackageMail {
protected function isNewThread() {
return true;
}
protected function getVerb() {
return 'Created';
}
}
diff --git a/src/applications/owners/mail/PackageDeleteMail.php b/src/applications/owners/mail/PackageDeleteMail.php
index 4254020245..c7ced65b3e 100644
--- a/src/applications/owners/mail/PackageDeleteMail.php
+++ b/src/applications/owners/mail/PackageDeleteMail.php
@@ -1,29 +1,13 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PackageDeleteMail extends PackageMail {
protected function getVerb() {
return "Deleted";
}
protected function isNewThread() {
return false;
}
}
diff --git a/src/applications/owners/mail/PackageMail.php b/src/applications/owners/mail/PackageMail.php
index 36c3391c11..2a9d57c258 100644
--- a/src/applications/owners/mail/PackageMail.php
+++ b/src/applications/owners/mail/PackageMail.php
@@ -1,220 +1,204 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PackageMail {
protected $package;
protected $handles;
protected $owners;
protected $paths;
protected $mailTo;
public function __construct(PhabricatorOwnersPackage $package) {
$this->package = $package;
}
abstract protected function getVerb();
abstract protected function isNewThread();
final protected function getPackage() {
return $this->package;
}
final protected function getHandles() {
return $this->handles;
}
final protected function getOwners() {
return $this->owners;
}
final protected function getPaths() {
return $this->paths;
}
final protected function getMailTo() {
return $this->mailTo;
}
final protected function renderPackageTitle() {
return $this->getPackage()->getName();
}
final protected function renderRepoSubSection($repository_phid, $paths) {
$handles = $this->getHandles();
$section = array();
$section[] = ' In repository '.$handles[$repository_phid]->getName().
' - '. PhabricatorEnv::getProductionURI($handles[$repository_phid]
->getURI());
foreach ($paths as $path => $ignored) {
$section[] = ' '.$path;
}
return implode("\n", $section);
}
protected function needSend() {
return true;
}
protected function loadData() {
$package = $this->getPackage();
$owners = $package->loadOwners();
$this->owners = $owners;
$owner_phids = mpull($owners, 'getUserPHID');
$primary_owner_phid = $package->getPrimaryOwnerPHID();
$mail_to = $owner_phids;
if (!in_array($primary_owner_phid, $owner_phids)) {
$mail_to[] = $primary_owner_phid;
}
$this->mailTo = $mail_to;
$paths = $package->loadPaths();
$this->paths = mgroup($paths, 'getRepositoryPHID', 'getPath');
$phids = array_merge(
$this->mailTo,
array($package->getActorPHID()),
array_keys($this->paths));
$this->handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
}
final protected function renderSummarySection() {
$package = $this->getPackage();
$handles = $this->getHandles();
$section = array();
$section[] = $handles[$package->getActorPHID()]->getName().' '.
strtolower($this->getVerb()).' '.$this->renderPackageTitle().'.';
$section[] = '';
$section[] = 'PACKAGE DETAIL';
$section[] = ' '.PhabricatorEnv::getProductionURI(
'/owners/package/'.$package->getID().'/');
return implode("\n", $section);
}
protected function renderDescriptionSection() {
return "PACKAGE DESCRIPTION\n".
' '.$this->getPackage()->getDescription();
}
protected function renderPrimaryOwnerSection() {
$handles = $this->getHandles();
return "PRIMARY OWNER\n".
' '.$handles[$this->getPackage()->getPrimaryOwnerPHID()]->getName();
}
protected function renderOwnersSection() {
$handles = $this->getHandles();
$owners = $this->getOwners();
if (!$owners) {
return null;
}
$owners = mpull($owners, 'getUserPHID');
$owners = array_select_keys($handles, $owners);
$owners = mpull($owners, 'getName');
return "OWNERS\n".
' '.implode(', ', $owners);
}
protected function renderAuditingEnabledSection() {
return "AUDITING ENABLED STATUS\n".
' '.($this->getPackage()->getAuditingEnabled() ? 'Enabled' : 'Disabled');
}
protected function renderPathsSection() {
$section = array();
$section[] = 'PATHS';
foreach ($this->paths as $repository_phid => $paths) {
$section[] = $this->renderRepoSubSection($repository_phid, $paths);
}
return implode("\n", $section);
}
final protected function renderBody() {
$body = array();
$body[] = $this->renderSummarySection();
$body[] = $this->renderDescriptionSection();
$body[] = $this->renderPrimaryOwnerSection();
$body[] = $this->renderOwnersSection();
$body[] = $this->renderAuditingEnabledSection();
$body[] = $this->renderPathsSection();
$body = array_filter($body);
return implode("\n\n", $body)."\n";
}
final public function send() {
$mails = $this->prepareMails();
foreach ($mails as $mail) {
$mail->saveAndSend();
}
}
final public function prepareMails() {
if (!$this->needSend()) {
return array();
}
$this->loadData();
$package = $this->getPackage();
$prefix = PhabricatorEnv::getEnvConfig('metamta.package.subject-prefix');
$verb = $this->getVerb();
$threading = $this->getMailThreading();
list($thread_id, $thread_topic) = $threading;
$template = id(new PhabricatorMetaMTAMail())
->setSubject($this->renderPackageTitle())
->setSubjectPrefix($prefix)
->setVarySubjectPrefix("[{$verb}]")
->setFrom($package->getActorPHID())
->setThreadID($thread_id, $this->isNewThread())
->addHeader('Thread-Topic', $thread_topic)
->setRelatedPHID($package->getPHID())
->setIsBulk(true)
->setBody($this->renderBody());
$reply_handler = $this->newReplyHandler();
$mails = $reply_handler->multiplexMail(
$template,
array_select_keys($this->getHandles(), $this->getMailTo()),
array());
return $mails;
}
private function getMailThreading() {
return array(
'package-'.$this->getPackage()->getPHID(),
'Package '.$this->getPackage()->getOriginalName(),
);
}
private function newReplyHandler() {
$reply_handler = PhabricatorEnv::newObjectFromConfig(
'metamta.package.reply-handler');
$reply_handler->setMailReceiver($this->getPackage());
return $reply_handler;
}
}
diff --git a/src/applications/owners/mail/PackageModifyMail.php b/src/applications/owners/mail/PackageModifyMail.php
index 3b8ead8be1..e7e6046ea2 100644
--- a/src/applications/owners/mail/PackageModifyMail.php
+++ b/src/applications/owners/mail/PackageModifyMail.php
@@ -1,173 +1,157 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PackageModifyMail extends PackageMail {
protected $addOwners;
protected $removeOwners;
protected $allOwners;
protected $touchedRepos;
protected $addPaths;
protected $removePaths;
public function __construct(
PhabricatorOwnersPackage $package,
$add_owners,
$remove_owners,
$all_owners,
$touched_repos,
$add_paths,
$remove_paths) {
$this->package = $package;
$this->addOwners = $add_owners;
$this->removeOwners = $remove_owners;
$this->allOwners = $all_owners;
$this->touchedRepos = $touched_repos;
$this->addPaths = $add_paths;
$this->removePaths = $remove_paths;
}
protected function getVerb() {
return 'Modified';
}
protected function isNewThread() {
return false;
}
protected function needSend() {
$package = $this->getPackage();
if ($package->getOldPrimaryOwnerPHID() !== $package->getPrimaryOwnerPHID()
|| $package->getOldAuditingEnabled() != $package->getAuditingEnabled()
|| $this->addOwners
|| $this->removeOwners
|| $this->addPaths
|| $this->removePaths) {
return true;
} else {
return false;
}
}
protected function loadData() {
$this->mailTo = $this->allOwners;
$phids = array_merge(
$this->allOwners,
$this->touchedRepos,
array(
$this->getPackage()->getActorPHID(),
));
$this->handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
}
protected function renderDescriptionSection() {
return null;
}
protected function renderPrimaryOwnerSection() {
$package = $this->getPackage();
$handles = $this->getHandles();
$old_primary_owner_phid = $package->getOldPrimaryOwnerPHID();
$primary_owner_phid = $package->getPrimaryOwnerPHID();
if ($old_primary_owner_phid == $primary_owner_phid) {
return null;
}
$section = array();
$section[] = 'PRIMARY OWNER CHANGE';
$section[] = ' Old owner: ' .
$handles[$old_primary_owner_phid]->getName();
$section[] = ' New owner: ' .
$handles[$primary_owner_phid]->getName();
return implode("\n", $section);
}
protected function renderOwnersSection() {
$section = array();
$add_owners = $this->addOwners;
$remove_owners = $this->removeOwners;
$handles = $this->getHandles();
if ($add_owners) {
$add_owners = array_select_keys($handles, $add_owners);
$add_owners = mpull($add_owners, 'getName');
$section[] = 'ADDED OWNERS';
$section[] = ' '.implode(', ', $add_owners);
}
if ($remove_owners) {
if ($add_owners) {
$section[] = '';
}
$remove_owners = array_select_keys($handles, $remove_owners);
$remove_owners = mpull($remove_owners, 'getName');
$section[] = 'REMOVED OWNERS';
$section[] = ' '.implode(', ', $remove_owners);
}
if ($section) {
return implode("\n", $section);
} else {
return null;
}
}
protected function renderAuditingEnabledSection() {
$package = $this->getPackage();
$old_auditing_enabled = $package->getOldAuditingEnabled();
$auditing_enabled = $package->getAuditingEnabled();
if ($old_auditing_enabled == $auditing_enabled) {
return null;
}
$section = array();
$section[] = 'AUDITING ENABLED STATUS CHANGE';
$section[] = ' Old value: '.
($old_auditing_enabled ? 'Enabled' : 'Disabled');
$section[] = ' New value: '.
($auditing_enabled ? 'Enabled' : 'Disabled');
return implode("\n", $section);
}
protected function renderPathsSection() {
$section = array();
if ($this->addPaths) {
$section[] = 'ADDED PATHS';
foreach ($this->addPaths as $repository_phid => $paths) {
$section[] = $this->renderRepoSubSection($repository_phid, $paths);
}
}
if ($this->removePaths) {
if ($this->addPaths) {
$section[] = '';
}
$section[] = 'REMOVED PATHS';
foreach ($this->removePaths as $repository_phid => $paths) {
$section[] = $this->renderRepoSubSection($repository_phid, $paths);
}
}
return implode("\n", $section);
}
}
diff --git a/src/applications/owners/query/PhabricatorOwnerPathQuery.php b/src/applications/owners/query/PhabricatorOwnerPathQuery.php
index 5dcefc62d3..f96e2eebdd 100644
--- a/src/applications/owners/query/PhabricatorOwnerPathQuery.php
+++ b/src/applications/owners/query/PhabricatorOwnerPathQuery.php
@@ -1,46 +1,30 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorOwnerPathQuery {
public static function loadAffectedPaths(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
$drequest = DiffusionRequest::newFromDictionary(
array(
'repository' => $repository,
'commit' => $commit->getCommitIdentifier(),
));
$path_query = DiffusionPathChangeQuery::newFromDiffusionRequest(
$drequest);
$paths = $path_query->loadChanges();
$result = array();
foreach ($paths as $path) {
$basic_path = '/' . $path->getPath();
if ($path->getFileType() == DifferentialChangeType::FILE_DIRECTORY) {
$basic_path = rtrim($basic_path, '/') . '/';
}
$result[] = $basic_path;
}
return $result;
}
}
diff --git a/src/applications/owners/query/PhabricatorOwnersPackageQuery.php b/src/applications/owners/query/PhabricatorOwnersPackageQuery.php
index 0c0d7555c1..db85ce5a35 100644
--- a/src/applications/owners/query/PhabricatorOwnersPackageQuery.php
+++ b/src/applications/owners/query/PhabricatorOwnersPackageQuery.php
@@ -1,85 +1,69 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorOwnersPackageQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ownerPHIDs;
/**
* Owners are direct owners, and members of owning projects.
*/
public function withOwnerPHIDs(array $phids) {
$this->ownerPHIDs = $phids;
return $this;
}
protected function loadPage() {
$table = new PhabricatorOwnersPackage();
$conn_r = $table->establishConnection('r');
$data = queryfx_all(
$conn_r,
'SELECT p.* FROM %T p %Q %Q %Q %Q',
$table->getTableName(),
$this->buildJoinClause($conn_r),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
return $table->loadAllFromArray($data);
}
private function buildJoinClause(AphrontDatabaseConnection $conn_r) {
$joins = array();
if ($this->ownerPHIDs) {
$joins[] = qsprintf(
$conn_r,
'JOIN %T o ON o.packageID = p.id',
id(new PhabricatorOwnersOwner())->getTableName());
}
return implode('', $joins);
}
private function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
if ($this->ownerPHIDs) {
$base_phids = $this->ownerPHIDs;
$query = new PhabricatorProjectQuery();
$query->setViewer($this->getViewer());
$query->withMemberPHIDs($base_phids);
$projects = $query->execute();
$project_phids = mpull($projects, 'getPHID');
$all_phids = array_merge($base_phids, $project_phids);
$where[] = qsprintf(
$conn_r,
'o.userPHID IN (%Ls)',
$all_phids);
}
$where[] = $this->buildPagingClause($conn_r);
return $this->formatWhereClause($where);
}
}
diff --git a/src/applications/owners/storage/PhabricatorOwnersDAO.php b/src/applications/owners/storage/PhabricatorOwnersDAO.php
index 3aebf8bf96..dafba266c8 100644
--- a/src/applications/owners/storage/PhabricatorOwnersDAO.php
+++ b/src/applications/owners/storage/PhabricatorOwnersDAO.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorOwnersDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'owners';
}
}
diff --git a/src/applications/owners/storage/PhabricatorOwnersOwner.php b/src/applications/owners/storage/PhabricatorOwnersOwner.php
index 95c4e503ee..a60503d310 100644
--- a/src/applications/owners/storage/PhabricatorOwnersOwner.php
+++ b/src/applications/owners/storage/PhabricatorOwnersOwner.php
@@ -1,72 +1,56 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorOwnersOwner extends PhabricatorOwnersDAO {
protected $packageID;
// this can be a project or a user. We assume that all members of a project
// owner also own the package; use the loadAffiliatedUserPHIDs method if
// you want to recursively grab all user ids that own a package
protected $userPHID;
public function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
) + parent::getConfiguration();
}
public static function loadAllForPackages(array $packages) {
assert_instances_of($packages, 'PhabricatorOwnersPackage');
if (!$packages) {
return array();
}
return id(new PhabricatorOwnersOwner())->loadAllWhere(
'packageID IN (%Ls)',
mpull($packages, 'getID'));
}
// Loads all user phids affiliated with a set of packages. This includes both
// user owners and all members of any project owners
public static function loadAffiliatedUserPHIDs(array $package_ids) {
if (!$package_ids) {
return array();
}
$owners = id(new PhabricatorOwnersOwner())->loadAllWhere(
'packageID IN (%Ls)',
$package_ids);
$all_phids = phid_group_by_type(mpull($owners, 'getUserPHID'));
$user_phids = idx($all_phids,
PhabricatorPHIDConstants::PHID_TYPE_USER,
array());
$users_in_project_phids = array();
$project_phids = idx($all_phids, PhabricatorPHIDConstants::PHID_TYPE_PROJ);
if ($project_phids) {
$query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($project_phids)
->withEdgeTypes(array(PhabricatorEdgeConfig::TYPE_PROJ_MEMBER));
$query->execute();
$users_in_project_phids = $query->getDestinationPHIDs();
}
return array_unique(array_merge($users_in_project_phids, $user_phids));
}
}
diff --git a/src/applications/owners/storage/PhabricatorOwnersPackage.php b/src/applications/owners/storage/PhabricatorOwnersPackage.php
index 59b0b2d35e..20d68754a6 100644
--- a/src/applications/owners/storage/PhabricatorOwnersPackage.php
+++ b/src/applications/owners/storage/PhabricatorOwnersPackage.php
@@ -1,370 +1,354 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorOwnersPackage extends PhabricatorOwnersDAO
implements PhabricatorPolicyInterface {
protected $phid;
protected $name;
protected $originalName;
protected $auditingEnabled;
protected $description;
protected $primaryOwnerPHID;
private $unsavedOwners;
private $unsavedPaths;
private $actorPHID;
private $oldPrimaryOwnerPHID;
private $oldAuditingEnabled;
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
return PhabricatorPolicies::POLICY_USER;
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
public function getConfiguration() {
return array(
// This information is better available from the history table.
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_AUX_PHID => true,
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID('OPKG');
}
public function attachUnsavedOwners(array $owners) {
$this->unsavedOwners = $owners;
return $this;
}
public function attachUnsavedPaths(array $paths) {
$this->unsavedPaths = $paths;
return $this;
}
public function attachActorPHID($actor_phid) {
$this->actorPHID = $actor_phid;
return $this;
}
public function getActorPHID() {
return $this->actorPHID;
}
public function attachOldPrimaryOwnerPHID($old_primary) {
$this->oldPrimaryOwnerPHID = $old_primary;
return $this;
}
public function getOldPrimaryOwnerPHID() {
return $this->oldPrimaryOwnerPHID;
}
public function attachOldAuditingEnabled($auditing_enabled) {
$this->oldAuditingEnabled = $auditing_enabled;
return $this;
}
public function getOldAuditingEnabled() {
return $this->oldAuditingEnabled;
}
public function setName($name) {
$this->name = $name;
if (!$this->getID()) {
$this->originalName = $name;
}
return $this;
}
public function loadOwners() {
if (!$this->getID()) {
return array();
}
return id(new PhabricatorOwnersOwner())->loadAllWhere(
'packageID = %d',
$this->getID());
}
public function loadPaths() {
if (!$this->getID()) {
return array();
}
return id(new PhabricatorOwnersPath())->loadAllWhere(
'packageID = %d',
$this->getID());
}
public static function loadAffectedPackages(
PhabricatorRepository $repository,
array $paths) {
if (!$paths) {
return array();
}
$fragments = array(
'/' => true,
);
foreach ($paths as $path) {
$fragments += self::splitPath($path);
}
return self::loadPackagesForPaths($repository, array_keys($fragments));
}
public static function loadOwningPackages($repository, $path) {
if (empty($path)) {
return array();
}
$fragments = self::splitPath($path);
return self::loadPackagesForPaths($repository, array_keys($fragments), 1);
}
private static function loadPackagesForPaths(
PhabricatorRepository $repository,
array $paths,
$limit = 0) {
$package = new PhabricatorOwnersPackage();
$path = new PhabricatorOwnersPath();
$conn = $package->establishConnection('r');
$repository_clause = qsprintf(
$conn,
'AND p.repositoryPHID = %s',
$repository->getPHID());
// NOTE: The list of $paths may be very large if we're coming from
// the OwnersWorker and processing, e.g., an SVN commit which created a new
// branch. Break it apart so that it will fit within 'max_allowed_packet',
// and then merge results in PHP.
$ids = array();
foreach (array_chunk($paths, 128) as $chunk) {
$rows = queryfx_all(
$conn,
'SELECT pkg.id id, LENGTH(p.path) len
FROM %T pkg JOIN %T p ON p.packageID = pkg.id
WHERE p.path IN (%Ls) %Q',
$package->getTableName(),
$path->getTableName(),
$chunk,
$repository_clause);
foreach ($rows as $row) {
$id = (int)$row['id'];
$len = (int)$row['len'];
if (isset($ids[$id])) {
$ids[$id] = max($len, $ids[$id]);
} else {
$ids[$id] = $len;
}
}
}
if (!$ids) {
return array();
}
arsort($ids);
if ($limit) {
$ids = array_slice($ids, 0, $limit, $preserve_keys = true);
}
$ids = array_keys($ids);
$packages = $package->loadAllWhere('id in (%Ld)', $ids);
$packages = array_select_keys($packages, $ids);
return $packages;
}
public function save() {
if ($this->getID()) {
$is_new = false;
} else {
$is_new = true;
}
$this->openTransaction();
$ret = parent::save();
$add_owners = array();
$remove_owners = array();
$all_owners = array();
if ($this->unsavedOwners) {
$new_owners = array_fill_keys($this->unsavedOwners, true);
$cur_owners = array();
foreach ($this->loadOwners() as $owner) {
if (empty($new_owners[$owner->getUserPHID()])) {
$remove_owners[$owner->getUserPHID()] = true;
$owner->delete();
continue;
}
$cur_owners[$owner->getUserPHID()] = true;
}
$add_owners = array_diff_key($new_owners, $cur_owners);
$all_owners = array_merge(
array($this->getPrimaryOwnerPHID() => true),
$new_owners,
$remove_owners);
foreach ($add_owners as $phid => $ignored) {
$owner = new PhabricatorOwnersOwner();
$owner->setPackageID($this->getID());
$owner->setUserPHID($phid);
$owner->save();
}
unset($this->unsavedOwners);
}
$add_paths = array();
$remove_paths = array();
$touched_repos = array();
if ($this->unsavedPaths) {
$new_paths = igroup($this->unsavedPaths, 'repositoryPHID', 'path');
$cur_paths = $this->loadPaths();
foreach ($cur_paths as $key => $path) {
if (empty($new_paths[$path->getRepositoryPHID()][$path->getPath()])) {
$touched_repos[$path->getRepositoryPHID()] = true;
$remove_paths[$path->getRepositoryPHID()][$path->getPath()] = true;
$path->delete();
unset($cur_paths[$key]);
}
}
$cur_paths = mgroup($cur_paths, 'getRepositoryPHID', 'getPath');
foreach ($new_paths as $repository_phid => $paths) {
// get repository object for path validation
$repository = id(new PhabricatorRepository())->loadOneWhere(
'phid = %s',
$repository_phid);
if (!$repository) {
continue;
}
foreach ($paths as $path => $ignored) {
$path = ltrim($path, '/');
// build query to validate path
$drequest = DiffusionRequest::newFromDictionary(
array(
'repository' => $repository,
'path' => $path,
));
$query = DiffusionBrowseQuery::newFromDiffusionRequest($drequest);
$query->needValidityOnly(true);
$valid = $query->loadPaths();
$is_directory = true;
if (!$valid) {
switch ($query->getReasonForEmptyResultSet()) {
case DiffusionBrowseQuery::REASON_IS_FILE:
$valid = true;
$is_directory = false;
break;
case DiffusionBrowseQuery::REASON_IS_EMPTY:
$valid = true;
break;
}
}
if ($is_directory && substr($path, -1) != '/') {
$path .= '/';
}
if (substr($path, 0, 1) != '/') {
$path = '/'.$path;
}
if (empty($cur_paths[$repository_phid][$path]) && $valid) {
$touched_repos[$repository_phid] = true;
$add_paths[$repository_phid][$path] = true;
$obj = new PhabricatorOwnersPath();
$obj->setPackageID($this->getID());
$obj->setRepositoryPHID($repository_phid);
$obj->setPath($path);
$obj->save();
}
}
}
unset($this->unsavedPaths);
}
$this->saveTransaction();
if ($is_new) {
$mail = new PackageCreateMail($this);
} else {
$mail = new PackageModifyMail(
$this,
array_keys($add_owners),
array_keys($remove_owners),
array_keys($all_owners),
array_keys($touched_repos),
$add_paths,
$remove_paths);
}
$mail->send();
return $ret;
}
public function delete() {
$mails = id(new PackageDeleteMail($this))->prepareMails();
$this->openTransaction();
foreach ($this->loadOwners() as $owner) {
$owner->delete();
}
foreach ($this->loadPaths() as $path) {
$path->delete();
}
$ret = parent::delete();
$this->saveTransaction();
foreach ($mails as $mail) {
$mail->saveAndSend();
}
return $ret;
}
private static function splitPath($path) {
$result = array();
$trailing_slash = preg_match('@/$@', $path) ? '/' : '';
$path = trim($path, '/');
$parts = explode('/', $path);
while (count($parts)) {
$result['/'.implode('/', $parts).$trailing_slash] = true;
$trailing_slash = '/';
array_pop($parts);
}
return $result;
}
}
diff --git a/src/applications/owners/storage/PhabricatorOwnersPath.php b/src/applications/owners/storage/PhabricatorOwnersPath.php
index cc0de81f36..58319ae874 100644
--- a/src/applications/owners/storage/PhabricatorOwnersPath.php
+++ b/src/applications/owners/storage/PhabricatorOwnersPath.php
@@ -1,31 +1,15 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorOwnersPath extends PhabricatorOwnersDAO {
protected $packageID;
protected $repositoryPHID;
protected $path;
public function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
) + parent::getConfiguration();
}
}
diff --git a/src/applications/paste/application/PhabricatorApplicationPaste.php b/src/applications/paste/application/PhabricatorApplicationPaste.php
index 0a83fdcc21..d38b99fe1a 100644
--- a/src/applications/paste/application/PhabricatorApplicationPaste.php
+++ b/src/applications/paste/application/PhabricatorApplicationPaste.php
@@ -1,48 +1,32 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationPaste extends PhabricatorApplication {
public function getBaseURI() {
return '/paste/';
}
public function getAutospriteName() {
return 'paste';
}
public function getTitleGlyph() {
return "\xE2\x9C\x8E";
}
public function getApplicationGroup() {
return self::GROUP_UTILITIES;
}
public function getRoutes() {
return array(
'/P(?P<id>[1-9]\d*)' => 'PhabricatorPasteViewController',
'/paste/' => array(
'' => 'PhabricatorPasteEditController',
'edit/(?P<id>[1-9]\d*)/' => 'PhabricatorPasteEditController',
'filter/(?P<filter>\w+)/' => 'PhabricatorPasteListController',
),
);
}
}
diff --git a/src/applications/paste/conduit/ConduitAPI_paste_Method.php b/src/applications/paste/conduit/ConduitAPI_paste_Method.php
index 4945a09779..830135d567 100644
--- a/src/applications/paste/conduit/ConduitAPI_paste_Method.php
+++ b/src/applications/paste/conduit/ConduitAPI_paste_Method.php
@@ -1,40 +1,24 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
abstract class ConduitAPI_paste_Method extends ConduitAPIMethod {
protected function buildPasteInfoDictionary(PhabricatorPaste $paste) {
return array(
'id' => $paste->getID(),
'objectName' => 'P'.$paste->getID(),
'phid' => $paste->getPHID(),
'authorPHID' => $paste->getAuthorPHID(),
'filePHID' => $paste->getFilePHID(),
'title' => $paste->getTitle(),
'dateCreated' => $paste->getDateCreated(),
'language' => $paste->getLanguage(),
'uri' => PhabricatorEnv::getProductionURI('/P'.$paste->getID()),
'parentPHID' => $paste->getParentPHID(),
'content' => $paste->getContent(),
);
}
}
diff --git a/src/applications/paste/conduit/ConduitAPI_paste_create_Method.php b/src/applications/paste/conduit/ConduitAPI_paste_create_Method.php
index d4525ab33b..691d1ee92a 100644
--- a/src/applications/paste/conduit/ConduitAPI_paste_create_Method.php
+++ b/src/applications/paste/conduit/ConduitAPI_paste_create_Method.php
@@ -1,81 +1,65 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_paste_create_Method extends ConduitAPI_paste_Method {
public function getMethodDescription() {
return 'Create a new paste.';
}
public function defineParamTypes() {
return array(
'content' => 'required string',
'title' => 'optional string',
'language' => 'optional string',
);
}
public function defineReturnType() {
return 'nonempty dict';
}
public function defineErrorTypes() {
return array(
'ERR-NO-PASTE' => 'Paste may not be empty.',
);
}
protected function execute(ConduitAPIRequest $request) {
$content = $request->getValue('content');
$title = $request->getValue('title');
$language = $request->getValue('language');
if (!strlen($content)) {
throw new ConduitException('ERR-NO-PASTE');
}
$title = nonempty($title, 'Masterwork From Distant Lands');
$language = nonempty($language, '');
$user = $request->getUser();
$paste_file = PhabricatorFile::newFromFileData(
$content,
array(
'name' => $title,
'mime-type' => 'text/plain; charset=utf-8',
'authorPHID' => $user->getPHID(),
));
$paste = new PhabricatorPaste();
$paste->setTitle($title);
$paste->setLanguage($language);
$paste->setFilePHID($paste_file->getPHID());
$paste->setAuthorPHID($user->getPHID());
$paste->setViewPolicy(PhabricatorPolicies::POLICY_USER);
$paste->save();
$paste->attachContent($content);
return $this->buildPasteInfoDictionary($paste);
}
}
diff --git a/src/applications/paste/conduit/ConduitAPI_paste_info_Method.php b/src/applications/paste/conduit/ConduitAPI_paste_info_Method.php
index 1c0a447109..0ced162308 100644
--- a/src/applications/paste/conduit/ConduitAPI_paste_info_Method.php
+++ b/src/applications/paste/conduit/ConduitAPI_paste_info_Method.php
@@ -1,65 +1,49 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_paste_info_Method extends ConduitAPI_paste_Method {
public function getMethodStatus() {
return self::METHOD_STATUS_DEPRECATED;
}
public function getMethodStatusDescription() {
return "Replaced by 'paste.query'.";
}
public function getMethodDescription() {
return "Retrieve an array of information about a paste.";
}
public function defineParamTypes() {
return array(
'paste_id' => 'required id',
);
}
public function defineReturnType() {
return 'nonempty dict';
}
public function defineErrorTypes() {
return array(
'ERR_BAD_PASTE' => 'No such paste exists',
);
}
protected function execute(ConduitAPIRequest $request) {
$paste_id = $request->getValue('paste_id');
$paste = id(new PhabricatorPasteQuery())
->setViewer($request->getUser())
->withIDs(array($paste_id))
->needContent(true)
->executeOne();
if (!$paste) {
throw new ConduitException('ERR_BAD_PASTE');
}
return $this->buildPasteInfoDictionary($paste);
}
}
diff --git a/src/applications/paste/conduit/ConduitAPI_paste_query_Method.php b/src/applications/paste/conduit/ConduitAPI_paste_query_Method.php
index 6c961560b3..836dbd4391 100644
--- a/src/applications/paste/conduit/ConduitAPI_paste_query_Method.php
+++ b/src/applications/paste/conduit/ConduitAPI_paste_query_Method.php
@@ -1,82 +1,66 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group conduit
*/
final class ConduitAPI_paste_query_Method extends ConduitAPI_paste_Method {
public function getMethodDescription() {
return "Query Pastes.";
}
public function defineParamTypes() {
return array(
'ids' => 'optional list<int>',
'phids' => 'optional list<phid>',
'authorPHIDs' => 'optional list<phid>',
'after' => 'optional int',
'limit' => 'optional int, default = 100',
);
}
public function defineReturnType() {
return 'list<dict>';
}
public function defineErrorTypes() {
return array();
}
protected function execute(ConduitAPIRequest $request) {
$query = id(new PhabricatorPasteQuery())
->setViewer($request->getUser())
->needContent(true);
if ($request->getValue('ids')) {
$query->withIDs($request->getValue('ids'));
}
if ($request->getValue('phids')) {
$query->withPHIDs($request->getValue('phids'));
}
if ($request->getValue('authorPHIDs')) {
$query->withAuthorPHIDs($request->getValue('authorPHIDs'));
}
if ($request->getValue('after')) {
$query->setAfterID($request->getValue('after'));
}
$limit = $request->getValue('limit', 100);
if ($limit) {
$query->setLimit($limit);
}
$pastes = $query->execute();
$results = array();
foreach ($pastes as $paste) {
$results[$paste->getPHID()] = $this->buildPasteInfoDictionary($paste);
}
return $results;
}
}
diff --git a/src/applications/paste/controller/PhabricatorPasteController.php b/src/applications/paste/controller/PhabricatorPasteController.php
index af6a031709..de0d7345c9 100644
--- a/src/applications/paste/controller/PhabricatorPasteController.php
+++ b/src/applications/paste/controller/PhabricatorPasteController.php
@@ -1,50 +1,34 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorPasteController extends PhabricatorController {
public function buildSideNavView(PhabricatorPaste $paste = null) {
$user = $this->getRequest()->getUser();
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($this->getApplicationURI('filter/')));
if ($paste) {
$nav->addFilter('paste', 'P'.$paste->getID(), '/P'.$paste->getID());
$nav->addSpacer();
}
$nav->addLabel('Create');
$nav->addFilter(
'edit',
'New Paste',
$this->getApplicationURI(),
$relative = false,
$class = ($user->isLoggedIn() ? null : 'disabled'));
$nav->addSpacer();
$nav->addLabel('Pastes');
if ($user->isLoggedIn()) {
$nav->addFilter('my', 'My Pastes');
}
$nav->addFilter('all', 'All Pastes');
return $nav;
}
}
diff --git a/src/applications/paste/controller/PhabricatorPasteEditController.php b/src/applications/paste/controller/PhabricatorPasteEditController.php
index 6e60e53204..4b83dc3d99 100644
--- a/src/applications/paste/controller/PhabricatorPasteEditController.php
+++ b/src/applications/paste/controller/PhabricatorPasteEditController.php
@@ -1,219 +1,203 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorPasteEditController extends PhabricatorPasteController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$parent = null;
$parent_id = null;
if (!$this->id) {
$is_create = true;
$paste = new PhabricatorPaste();
$parent_id = $request->getStr('parent');
if ($parent_id) {
// NOTE: If the Paste is forked from a paste which the user no longer
// has permission to see, we still let them edit it.
$parent = id(new PhabricatorPasteQuery())
->setViewer($user)
->withIDs(array($parent_id))
->needContent(true)
->execute();
$parent = head($parent);
if ($parent) {
$paste->setParentPHID($parent->getPHID());
$paste->setViewPolicy($parent->getViewPolicy());
}
}
$paste->setAuthorPHID($user->getPHID());
} else {
$is_create = false;
$paste = id(new PhabricatorPasteQuery())
->setViewer($user)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->withIDs(array($this->id))
->executeOne();
if (!$paste) {
return new Aphront404Response();
}
}
$text = null;
$e_text = true;
$errors = array();
if ($request->isFormPost()) {
if ($is_create) {
$text = $request->getStr('text');
if (!strlen($text)) {
$e_text = 'Required';
$errors[] = 'The paste may not be blank.';
} else {
$e_text = null;
}
}
$paste->setTitle($request->getStr('title'));
$paste->setLanguage($request->getStr('language'));
$paste->setViewPolicy($request->getStr('can_view'));
// NOTE: The author is the only editor and can always view the paste,
// so it's impossible for them to choose an invalid policy.
if (!$errors) {
if ($is_create) {
$paste_file = PhabricatorFile::newFromFileData(
$text,
array(
'name' => $paste->getTitle(),
'mime-type' => 'text/plain; charset=utf-8',
'authorPHID' => $user->getPHID(),
));
$paste->setFilePHID($paste_file->getPHID());
}
$paste->save();
return id(new AphrontRedirectResponse())->setURI($paste->getURI());
}
} else {
if ($is_create && $parent) {
$paste->setTitle('Fork of '.$parent->getFullName());
$paste->setLanguage($parent->getLanguage());
$text = $parent->getContent();
}
}
$error_view = null;
if ($errors) {
$error_view = id(new AphrontErrorView())
->setTitle('A fatal omission!')
->setErrors($errors);
}
$form = new AphrontFormView();
$form->setFlexible(true);
$langs = array(
'' => '(Detect From Filename in Title)',
) + PhabricatorEnv::getEnvConfig('pygments.dropdown-choices');
$form
->setUser($user)
->addHiddenInput('parent', $parent_id)
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Title')
->setValue($paste->getTitle())
->setName('title'))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Language')
->setName('language')
->setValue($paste->getLanguage())
->setOptions($langs));
$policies = id(new PhabricatorPolicyQuery())
->setViewer($user)
->setObject($paste)
->execute();
$form->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
->setPolicyObject($paste)
->setPolicies($policies)
->setName('can_view'));
if ($is_create) {
$form
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Text')
->setError($e_text)
->setValue($text)
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)
->setCustomClass('PhabricatorMonospaced')
->setName('text'));
} else {
$fork_link = phutil_render_tag(
'a',
array(
'href' => $this->getApplicationURI('?parent='.$paste->getID())
),
'Fork'
);
$form
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel('Text')
->setValue(
'Paste text can not be edited. '.
$fork_link.' to create a new paste.'
));
}
$submit = new AphrontFormSubmitControl();
if (!$is_create) {
$submit->addCancelButton($paste->getURI());
$submit->setValue('Save Paste');
$title = 'Edit '.$paste->getFullName();
} else {
$submit->setValue('Create Paste');
$title = 'Create Paste';
}
$form
->appendChild($submit);
$nav = $this->buildSideNavView();
$nav->selectFilter('edit');
$nav->appendChild(
array(
id(new PhabricatorHeaderView())->setHeader($title),
$error_view,
$form,
));
return $this->buildApplicationPage(
$nav,
array(
'title' => $title,
'device' => true,
));
}
}
diff --git a/src/applications/paste/controller/PhabricatorPasteListController.php b/src/applications/paste/controller/PhabricatorPasteListController.php
index 373f95b2ad..3c2d43714e 100644
--- a/src/applications/paste/controller/PhabricatorPasteListController.php
+++ b/src/applications/paste/controller/PhabricatorPasteListController.php
@@ -1,98 +1,82 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorPasteListController extends PhabricatorPasteController {
public function shouldRequireLogin() {
return false;
}
private $filter;
public function willProcessRequest(array $data) {
$this->filter = idx($data, 'filter');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$query = new PhabricatorPasteQuery();
$query->setViewer($user);
$nav = $this->buildSideNavView();
$filter = $nav->selectFilter($this->filter, 'my');
switch ($filter) {
case 'my':
$query->withAuthorPHIDs(array($user->getPHID()));
$title = pht('My Pastes');
$nodata = pht("You haven't created any Pastes yet.");
break;
case 'all':
$title = pht('All Pastes');
$nodata = pht("There are no Pastes yet.");
break;
}
$pager = new AphrontCursorPagerView();
$pager->readFromRequest($request);
$pastes = $query->executeWithCursorPager($pager);
$list = $this->buildPasteList($pastes);
$list->setHeader($title);
$list->setPager($pager);
$list->setNoDataString($nodata);
$nav->appendChild($list);
return $this->buildApplicationPage(
$nav,
array(
'title' => $title,
'device' => true,
)
);
}
private function buildPasteList(array $pastes) {
assert_instances_of($pastes, 'PhabricatorPaste');
$user = $this->getRequest()->getUser();
$this->loadHandles(mpull($pastes, 'getAuthorPHID'));
$list = new PhabricatorObjectItemListView();
foreach ($pastes as $paste) {
$created = phabricator_datetime($paste->getDateCreated(), $user);
$item = id(new PhabricatorObjectItemView())
->setHeader($paste->getFullName())
->setHref('/P'.$paste->getID())
->addDetail(
pht('Author'),
$this->getHandle($paste->getAuthorPHID())->renderLink())
->addAttribute(pht('Created %s', $created));
$list->addItem($item);
}
return $list;
}
}
diff --git a/src/applications/paste/controller/PhabricatorPasteViewController.php b/src/applications/paste/controller/PhabricatorPasteViewController.php
index 220e533280..ce5f577c2c 100644
--- a/src/applications/paste/controller/PhabricatorPasteViewController.php
+++ b/src/applications/paste/controller/PhabricatorPasteViewController.php
@@ -1,192 +1,176 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorPasteViewController extends PhabricatorPasteController {
public function shouldAllowPublic() {
return true;
}
private $id;
private $handles;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$paste = id(new PhabricatorPasteQuery())
->setViewer($user)
->withIDs(array($this->id))
->executeOne();
if (!$paste) {
return new Aphront404Response();
}
$file = id(new PhabricatorFile())->loadOneWhere(
'phid = %s',
$paste->getFilePHID());
if (!$file) {
return new Aphront400Response();
}
$forks = id(new PhabricatorPasteQuery())
->setViewer($user)
->withParentPHIDs(array($paste->getPHID()))
->execute();
$fork_phids = mpull($forks, 'getPHID');
$this->loadHandles(
array_merge(
array(
$paste->getAuthorPHID(),
$paste->getParentPHID(),
),
$fork_phids));
$header = $this->buildHeaderView($paste);
$actions = $this->buildActionView($user, $paste, $file);
$properties = $this->buildPropertyView($paste, $fork_phids);
$source_code = $this->buildSourceCodeView($paste, $file);
$nav = $this->buildSideNavView($paste);
$nav->selectFilter('paste');
$nav->appendChild(
array(
$header,
$actions,
$properties,
$source_code,
));
return $this->buildApplicationPage(
$nav,
array(
'title' => $paste->getFullName(),
'device' => true,
));
}
private function buildHeaderView(PhabricatorPaste $paste) {
return id(new PhabricatorHeaderView())
->setObjectName('P'.$paste->getID())
->setHeader($paste->getTitle());
}
private function buildActionView(
PhabricatorUser $user,
PhabricatorPaste $paste,
PhabricatorFile $file) {
$can_edit = PhabricatorPolicyFilter::hasCapability(
$user,
$paste,
PhabricatorPolicyCapability::CAN_EDIT);
$can_fork = $user->isLoggedIn();
return id(new PhabricatorActionListView())
->setUser($user)
->setObject($paste)
->addAction(
id(new PhabricatorActionView())
->setName(pht('Fork This Paste'))
->setIcon('fork')
->setDisabled(!$can_fork)
->setWorkflow(!$can_fork)
->setHref($this->getApplicationURI('?parent='.$paste->getID())))
->addAction(
id(new PhabricatorActionView())
->setName(pht('View Raw File'))
->setIcon('file')
->setHref($file->getBestURI()))
->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Paste'))
->setIcon('edit')
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit)
->setHref($this->getApplicationURI('/edit/'.$paste->getID().'/')));
}
private function buildPropertyView(
PhabricatorPaste $paste,
array $child_phids) {
$user = $this->getRequest()->getUser();
$properties = new PhabricatorPropertyListView();
$properties->addProperty(
pht('Author'),
$this->getHandle($paste->getAuthorPHID())->renderLink());
$properties->addProperty(
pht('Created'),
phabricator_datetime($paste->getDateCreated(), $user));
if ($paste->getParentPHID()) {
$properties->addProperty(
pht('Forked From'),
$this->getHandle($paste->getParentPHID())->renderLink());
}
if ($child_phids) {
$properties->addProperty(
pht('Forks'),
$this->renderHandlesForPHIDs($child_phids));
}
$descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions(
$user,
$paste);
$properties->addProperty(
pht('Visible To'),
$descriptions[PhabricatorPolicyCapability::CAN_VIEW]);
return $properties;
}
private function buildSourceCodeView(
PhabricatorPaste $paste,
PhabricatorFile $file) {
$language = $paste->getLanguage();
$source = $file->loadFileData();
if (empty($language)) {
$source = PhabricatorSyntaxHighlighter::highlightWithFilename(
$paste->getTitle(),
$source);
} else {
$source = PhabricatorSyntaxHighlighter::highlightWithLanguage(
$language,
$source);
}
$lines = explode("\n", $source);
return id(new PhabricatorSourceCodeView())
->setLines($lines);
}
}
diff --git a/src/applications/paste/query/PhabricatorPasteQuery.php b/src/applications/paste/query/PhabricatorPasteQuery.php
index 0666e60a08..6a9ff88d9b 100644
--- a/src/applications/paste/query/PhabricatorPasteQuery.php
+++ b/src/applications/paste/query/PhabricatorPasteQuery.php
@@ -1,123 +1,107 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorPasteQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $authorPHIDs;
private $parentPHIDs;
private $needContent;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withAuthorPHIDs(array $phids) {
$this->authorPHIDs = $phids;
return $this;
}
public function withParentPHIDs(array $phids) {
$this->parentPHIDs = $phids;
return $this;
}
public function needContent($need_content) {
$this->needContent = $need_content;
return $this;
}
public function loadPage() {
$table = new PhabricatorPaste();
$conn_r = $table->establishConnection('r');
$data = queryfx_all(
$conn_r,
'SELECT paste.* FROM %T paste %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
$pastes = $table->loadAllFromArray($data);
if ($pastes && $this->needContent) {
$file_phids = mpull($pastes, 'getFilePHID');
$files = id(new PhabricatorFile())->loadAllWhere(
'phid IN (%Ls)',
$file_phids);
$files = mpull($files, null, 'getPHID');
foreach ($pastes as $paste) {
$file = idx($files, $paste->getFilePHID());
if ($file) {
$paste->attachContent($file->loadFileData());
} else {
$paste->attachContent('');
}
}
}
return $pastes;
}
protected function buildWhereClause($conn_r) {
$where = array();
$where[] = $this->buildPagingClause($conn_r);
if ($this->ids) {
$where[] = qsprintf(
$conn_r,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids) {
$where[] = qsprintf(
$conn_r,
'phid IN (%Ls)',
$this->phids);
}
if ($this->authorPHIDs) {
$where[] = qsprintf(
$conn_r,
'authorPHID IN (%Ls)',
$this->authorPHIDs);
}
if ($this->parentPHIDs) {
$where[] = qsprintf(
$conn_r,
'parentPHID IN (%Ls)',
$this->parentPHIDs);
}
return $this->formatWhereClause($where);
}
}
diff --git a/src/applications/paste/storage/PhabricatorPaste.php b/src/applications/paste/storage/PhabricatorPaste.php
index 2290b6b94d..0db9ec1596 100644
--- a/src/applications/paste/storage/PhabricatorPaste.php
+++ b/src/applications/paste/storage/PhabricatorPaste.php
@@ -1,85 +1,69 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorPaste extends PhabricatorPasteDAO
implements PhabricatorPolicyInterface {
protected $phid;
protected $title;
protected $authorPHID;
protected $filePHID;
protected $language;
protected $parentPHID;
protected $viewPolicy;
private $content;
public function getURI() {
return '/P'.$this->getID();
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_PSTE);
}
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
return $this->viewPolicy;
}
return PhabricatorPolicies::POLICY_NOONE;
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
return ($user->getPHID() == $this->getAuthorPHID());
}
public function getFullName() {
$title = $this->getTitle();
if (!$title) {
$title = '(An Untitled Masterwork)';
}
return 'P'.$this->getID().' '.$title;
}
public function getContent() {
if ($this->content === null) {
throw new Exception("Call attachContent() before getContent()!");
}
return $this->content;
}
public function attachContent($content) {
$this->content = $content;
return $this;
}
}
diff --git a/src/applications/paste/storage/PhabricatorPasteDAO.php b/src/applications/paste/storage/PhabricatorPasteDAO.php
index 940e88d875..dd61ff7920 100644
--- a/src/applications/paste/storage/PhabricatorPasteDAO.php
+++ b/src/applications/paste/storage/PhabricatorPasteDAO.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorPasteDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'pastebin';
}
}
diff --git a/src/applications/people/PhabricatorPeopleQuery.php b/src/applications/people/PhabricatorPeopleQuery.php
index 727caaac1c..215eafbcec 100644
--- a/src/applications/people/PhabricatorPeopleQuery.php
+++ b/src/applications/people/PhabricatorPeopleQuery.php
@@ -1,124 +1,108 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorPeopleQuery extends PhabricatorOffsetPagedQuery {
private $usernames;
private $realnames;
private $emails;
private $phids;
private $ids;
private $needPrimaryEmail;
public function withIds(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPhids(array $phids) {
$this->phids = $phids;
return $this;
}
public function withEmails(array $emails) {
$this->emails = $emails;
return $this;
}
public function withRealnames(array $realnames) {
$this->realnames = $realnames;
return $this;
}
public function withUsernames(array $usernames) {
$this->usernames = $usernames;
return $this;
}
public function needPrimaryEmail($need) {
$this->needPrimaryEmail = $need;
return $this;
}
public function execute() {
$table = new PhabricatorUser();
$conn_r = $table->establishConnection('r');
$joins_clause = $this->buildJoinsClause($conn_r);
$where_clause = $this->buildWhereClause($conn_r);
$limit_clause = $this->buildLimitClause($conn_r);
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T user %Q %Q %Q',
$table->getTableName(),
$joins_clause,
$where_clause,
$limit_clause);
if ($this->needPrimaryEmail) {
$table->putInSet(new LiskDAOSet());
}
$users = $table->loadAllFromArray($data);
return $users;
}
private function buildJoinsClause($conn_r) {
$joins = array();
if ($this->emails) {
$email_table = new PhabricatorUserEmail();
$joins[] = qsprintf(
$conn_r,
'JOIN %T email ON email.userPHID = user.PHID',
$email_table->getTableName());
}
$joins = implode(' ', $joins);
return $joins;
}
private function buildWhereClause($conn_r) {
$where = array();
if ($this->usernames) {
$where[] = qsprintf($conn_r,
'user.userName IN (%Ls)',
$this->usernames);
}
if ($this->emails) {
$where[] = qsprintf($conn_r,
'email.address IN (%Ls)',
$this->emails);
}
if ($this->realnames) {
$where[] = qsprintf($conn_r,
'user.realName IN (%Ls)',
$this->realnames);
}
if ($this->phids) {
$where[] = qsprintf($conn_r,
'user.phid IN (%Ls)',
$this->phids);
}
if ($this->ids) {
$where[] = qsprintf($conn_r,
'user.id IN (%Ld)',
$this->ids);
}
return $this->formatWhereClause($where);
}
}
diff --git a/src/applications/people/PhabricatorUserEditor.php b/src/applications/people/PhabricatorUserEditor.php
index ec734cc6b9..af0c48ae82 100644
--- a/src/applications/people/PhabricatorUserEditor.php
+++ b/src/applications/people/PhabricatorUserEditor.php
@@ -1,557 +1,541 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Editor class for creating and adjusting users. This class guarantees data
* integrity and writes logs when user information changes.
*
* @task config Configuration
* @task edit Creating and Editing Users
* @task role Editing Roles
* @task email Adding, Removing and Changing Email
* @task internal Internals
*/
final class PhabricatorUserEditor extends PhabricatorEditor {
private $logs = array();
/* -( Creating and Editing Users )----------------------------------------- */
/**
* @task edit
*/
public function createNewUser(
PhabricatorUser $user,
PhabricatorUserEmail $email) {
if ($user->getID()) {
throw new Exception("User has already been created!");
}
if ($email->getID()) {
throw new Exception("Email has already been created!");
}
if (!PhabricatorUser::validateUsername($user->getUsername())) {
$valid = PhabricatorUser::describeValidUsername();
throw new Exception("Username is invalid! {$valid}");
}
// Always set a new user's email address to primary.
$email->setIsPrimary(1);
$this->willAddEmail($email);
$user->openTransaction();
try {
$user->save();
$email->setUserPHID($user->getPHID());
$email->save();
} catch (AphrontQueryDuplicateKeyException $ex) {
// We might have written the user but failed to write the email; if
// so, erase the IDs we attached.
$user->setID(null);
$user->setPHID(null);
$user->killTransaction();
throw $ex;
}
$log = PhabricatorUserLog::newLog(
$this->requireActor(),
$user,
PhabricatorUserLog::ACTION_CREATE);
$log->setNewValue($email->getAddress());
$log->save();
$user->saveTransaction();
return $this;
}
/**
* @task edit
*/
public function updateUser(
PhabricatorUser $user,
PhabricatorUserEmail $email = null) {
if (!$user->getID()) {
throw new Exception("User has not been created yet!");
}
$user->openTransaction();
$user->save();
if ($email) {
$email->save();
}
$log = PhabricatorUserLog::newLog(
$this->requireActor(),
$user,
PhabricatorUserLog::ACTION_EDIT);
$log->save();
$user->saveTransaction();
return $this;
}
/**
* @task edit
*/
public function changePassword(
PhabricatorUser $user,
PhutilOpaqueEnvelope $envelope) {
if (!$user->getID()) {
throw new Exception("User has not been created yet!");
}
$user->openTransaction();
$user->reload();
$user->setPassword($envelope);
$user->save();
$log = PhabricatorUserLog::newLog(
$this->requireActor(),
$user,
PhabricatorUserLog::ACTION_CHANGE_PASSWORD);
$log->save();
$user->saveTransaction();
}
/**
* @task edit
*/
public function changeUsername(PhabricatorUser $user, $username) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception("User has not been created yet!");
}
if (!PhabricatorUser::validateUsername($username)) {
$valid = PhabricatorUser::describeValidUsername();
throw new Exception("Username is invalid! {$valid}");
}
$old_username = $user->getUsername();
$user->openTransaction();
$user->reload();
$user->setUsername($username);
try {
$user->save();
} catch (AphrontQueryDuplicateKeyException $ex) {
$user->setUsername($old_username);
$user->killTransaction();
throw $ex;
}
$log = PhabricatorUserLog::newLog(
$actor,
$user,
PhabricatorUserLog::ACTION_CHANGE_USERNAME);
$log->setOldValue($old_username);
$log->setNewValue($username);
$log->save();
$user->saveTransaction();
$user->sendUsernameChangeEmail($actor, $old_username);
}
/* -( Editing Roles )------------------------------------------------------ */
/**
* @task role
*/
public function makeAdminUser(PhabricatorUser $user, $admin) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception("User has not been created yet!");
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
if ($user->getIsAdmin() == $admin) {
$user->endWriteLocking();
$user->killTransaction();
return $this;
}
$log = PhabricatorUserLog::newLog(
$actor,
$user,
PhabricatorUserLog::ACTION_ADMIN);
$log->setOldValue($user->getIsAdmin());
$log->setNewValue($admin);
$user->setIsAdmin($admin);
$user->save();
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
return $this;
}
/**
* @task role
*/
public function makeSystemAgentUser(PhabricatorUser $user, $system_agent) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception("User has not been created yet!");
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
if ($user->getIsSystemAgent() == $system_agent) {
$user->endWriteLocking();
$user->killTransaction();
return $this;
}
$log = PhabricatorUserLog::newLog(
$actor,
$user,
PhabricatorUserLog::ACTION_SYSTEM_AGENT);
$log->setOldValue($user->getIsSystemAgent());
$log->setNewValue($system_agent);
$user->setIsSystemAgent($system_agent);
$user->save();
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
return $this;
}
/**
* @task role
*/
public function disableUser(PhabricatorUser $user, $disable) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception("User has not been created yet!");
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
if ($user->getIsDisabled() == $disable) {
$user->endWriteLocking();
$user->killTransaction();
return $this;
}
$log = PhabricatorUserLog::newLog(
$actor,
$user,
PhabricatorUserLog::ACTION_DISABLE);
$log->setOldValue($user->getIsDisabled());
$log->setNewValue($disable);
$user->setIsDisabled($disable);
$user->save();
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
return $this;
}
/**
* @task role
*/
public function deleteUser(PhabricatorUser $user, $disable) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception("User has not been created yet!");
}
if ($actor->getPHID() == $user->getPHID()) {
throw new Exception("You can not delete yourself!");
}
$user->openTransaction();
$ldaps = id(new PhabricatorUserLDAPInfo())->loadAllWhere(
'userID = %d',
$user->getID());
foreach ($ldaps as $ldap) {
$ldap->delete();
}
$oauths = id(new PhabricatorUserOAuthInfo())->loadAllWhere(
'userID = %d',
$user->getID());
foreach ($oauths as $oauth) {
$oauth->delete();
}
$prefs = id(new PhabricatorUserPreferences())->loadAllWhere(
'userPHID = %s',
$user->getPHID());
foreach ($prefs as $pref) {
$pref->delete();
}
$profiles = id(new PhabricatorUserProfile())->loadAllWhere(
'userPHID = %s',
$user->getPHID());
foreach ($profiles as $profile) {
$profile->delete();
}
$keys = id(new PhabricatorUserSSHKey())->loadAllWhere(
'userPHID = %s',
$user->getPHID());
foreach ($keys as $key) {
$key->delete();
}
$emails = id(new PhabricatorUserEmail())->loadAllWhere(
'userPHID = %s',
$user->getPHID());
foreach ($emails as $email) {
$email->delete();
}
$log = PhabricatorUserLog::newLog(
$actor,
$user,
PhabricatorUserLog::ACTION_DELETE);
$log->save();
$user->delete();
$user->saveTransaction();
return $this;
}
/* -( Adding, Removing and Changing Email )-------------------------------- */
/**
* @task email
*/
public function addEmail(
PhabricatorUser $user,
PhabricatorUserEmail $email) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception("User has not been created yet!");
}
if ($email->getID()) {
throw new Exception("Email has already been created!");
}
// Use changePrimaryEmail() to change primary email.
$email->setIsPrimary(0);
$email->setUserPHID($user->getPHID());
$this->willAddEmail($email);
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
try {
$email->save();
} catch (AphrontQueryDuplicateKeyException $ex) {
$user->endWriteLocking();
$user->killTransaction();
throw $ex;
}
$log = PhabricatorUserLog::newLog(
$actor,
$user,
PhabricatorUserLog::ACTION_EMAIL_ADD);
$log->setNewValue($email->getAddress());
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
return $this;
}
/**
* @task email
*/
public function removeEmail(
PhabricatorUser $user,
PhabricatorUserEmail $email) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception("User has not been created yet!");
}
if (!$email->getID()) {
throw new Exception("Email has not been created yet!");
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
$email->reload();
if ($email->getIsPrimary()) {
throw new Exception("Can't remove primary email!");
}
if ($email->getUserPHID() != $user->getPHID()) {
throw new Exception("Email not owned by user!");
}
$email->delete();
$log = PhabricatorUserLog::newLog(
$actor,
$user,
PhabricatorUserLog::ACTION_EMAIL_REMOVE);
$log->setOldValue($email->getAddress());
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
return $this;
}
/**
* @task email
*/
public function changePrimaryEmail(
PhabricatorUser $user,
PhabricatorUserEmail $email) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception("User has not been created yet!");
}
if (!$email->getID()) {
throw new Exception("Email has not been created yet!");
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
$email->reload();
if ($email->getUserPHID() != $user->getPHID()) {
throw new Exception("User does not own email!");
}
if ($email->getIsPrimary()) {
throw new Exception("Email is already primary!");
}
if (!$email->getIsVerified()) {
throw new Exception("Email is not verified!");
}
$old_primary = $user->loadPrimaryEmail();
if ($old_primary) {
$old_primary->setIsPrimary(0);
$old_primary->save();
}
$email->setIsPrimary(1);
$email->save();
$log = PhabricatorUserLog::newLog(
$actor,
$user,
PhabricatorUserLog::ACTION_EMAIL_PRIMARY);
$log->setOldValue($old_primary ? $old_primary->getAddress() : null);
$log->setNewValue($email->getAddress());
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
if ($old_primary) {
$old_primary->sendOldPrimaryEmail($user, $email);
}
$email->sendNewPrimaryEmail($user);
return $this;
}
/* -( Internals )---------------------------------------------------------- */
/**
* @task internal
*/
private function willAddEmail(PhabricatorUserEmail $email) {
// Hard check before write to prevent creation of disallowed email
// addresses. Normally, the application does checks and raises more
// user friendly errors for us, but we omit the courtesy checks on some
// pathways like administrative scripts for simplicity.
if (!PhabricatorUserEmail::isAllowedAddress($email->getAddress())) {
throw new Exception(PhabricatorUserEmail::describeAllowedAddresses());
}
}
}
diff --git a/src/applications/people/application/PhabricatorApplicationPeople.php b/src/applications/people/application/PhabricatorApplicationPeople.php
index 7b9820c9ba..a0195d9227 100644
--- a/src/applications/people/application/PhabricatorApplicationPeople.php
+++ b/src/applications/people/application/PhabricatorApplicationPeople.php
@@ -1,90 +1,74 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationPeople extends PhabricatorApplication {
public function getShortDescription() {
return 'User Accounts';
}
public function getBaseURI() {
return '/people/';
}
public function getTitleGlyph() {
return "\xE2\x99\x9F";
}
public function getAutospriteName() {
return 'people';
}
public function getFlavorText() {
return pht('Sort of a social utility.');
}
public function getApplicationGroup() {
return self::GROUP_ADMIN;
}
public function getRoutes() {
return array(
'/people/' => array(
'' => 'PhabricatorPeopleListController',
'logs/' => 'PhabricatorPeopleLogsController',
'edit/(?:(?P<id>[1-9]\d*)/(?:(?P<view>\w+)/)?)?'
=> 'PhabricatorPeopleEditController',
'ldap/' => 'PhabricatorPeopleLdapController',
),
'/p/(?P<username>[\w._-]+)/(?:(?P<page>\w+)/)?'
=> 'PhabricatorPeopleProfileController',
'/emailverify/(?P<code>[^/]+)/' =>
'PhabricatorEmailVerificationController',
);
}
public function buildMainMenuItems(
PhabricatorUser $user,
PhabricatorController $controller = null) {
$items = array();
if (($controller instanceof PhabricatorPeopleProfileController) &&
($controller->getProfileUser()) &&
($controller->getProfileUser()->getPHID() == $user->getPHID())) {
$class = 'main-menu-item-icon-profile-selected';
} else {
$class = 'main-menu-item-icon-profile-not-selected';
}
if ($user->isLoggedIn()) {
$image = $user->loadProfileImageURI();
$item = new PhabricatorMainMenuIconView();
$item->setName($user->getUsername());
$item->addClass('main-menu-item-icon-profile '.$class);
$item->addStyle('background-image: url('.$image.')');
$item->setHref('/p/'.$user->getUsername().'/');
$item->setSortOrder(0.0);
$items[] = $item;
}
return $items;
}
}
diff --git a/src/applications/people/controller/PhabricatorEmailVerificationController.php b/src/applications/people/controller/PhabricatorEmailVerificationController.php
index 78c3873d23..9f69293dcc 100644
--- a/src/applications/people/controller/PhabricatorEmailVerificationController.php
+++ b/src/applications/people/controller/PhabricatorEmailVerificationController.php
@@ -1,97 +1,81 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorEmailVerificationController
extends PhabricatorPeopleController {
private $code;
public function willProcessRequest(array $data) {
$this->code = $data['code'];
}
public function shouldRequireEmailVerification() {
// Since users need to be able to hit this endpoint in order to verify
// email, we can't ever require email verification here.
return false;
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'userPHID = %s AND verificationCode = %s',
$user->getPHID(),
$this->code);
$home_link = phutil_render_tag(
'a',
array(
'href' => '/',
),
'Continue to Phabricator');
$home_link = '<br /><p><strong>'.$home_link.'</strong></p>';
$settings_link = phutil_render_tag(
'a',
array(
'href' => '/settings/panel/email/',
),
'Return to Email Settings');
$settings_link = '<br /><p><strong>'.$settings_link.'</strong></p>';
if (!$email) {
$content = id(new AphrontErrorView())
->setTitle('Unable To Verify')
->appendChild(
'<p>The verification code is incorrect, the email address has '.
'been removed, or the email address is owned by another user. Make '.
'sure you followed the link in the email correctly.</p>');
} else if ($email->getIsVerified()) {
$content = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
->setTitle('Address Already Verified')
->appendChild(
'<p>This email address has already been verified.</p>'.
$settings_link);
} else {
$guard = AphrontWriteGuard::beginScopedUnguardedWrites();
$email->setIsVerified(1);
$email->save();
unset($guard);
$content = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
->setTitle('Address Verified')
->appendChild(
'<p>This email address has now been verified. Thanks!</p>'.
$home_link.
$settings_link);
}
return $this->buildApplicationPage(
$content,
array(
'title' => 'Verify Email',
));
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleController.php b/src/applications/people/controller/PhabricatorPeopleController.php
index 80f639da7f..d454464562 100644
--- a/src/applications/people/controller/PhabricatorPeopleController.php
+++ b/src/applications/people/controller/PhabricatorPeopleController.php
@@ -1,48 +1,32 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorPeopleController extends PhabricatorController {
public function buildSideNavView() {
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
$is_admin = $this->getRequest()->getUser()->getIsAdmin();
if ($is_admin) {
$nav->addLabel('Create Users');
$nav->addFilter('edit', 'Create New User');
if (PhabricatorEnv::getEnvConfig('ldap.auth-enabled') === true) {
$nav->addFilter('ldap', 'Import from LDAP');
}
$nav->addSpacer();
}
$nav->addLabel('Directory');
$nav->addFilter('people', 'User Directory', $this->getApplicationURI());
if ($is_admin) {
$nav->addSpacer();
$nav->addLabel('Logs');
$nav->addFilter('logs', 'Activity Logs');
}
return $nav;
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleEditController.php b/src/applications/people/controller/PhabricatorPeopleEditController.php
index b15bb157c3..25d4ae867c 100644
--- a/src/applications/people/controller/PhabricatorPeopleEditController.php
+++ b/src/applications/people/controller/PhabricatorPeopleEditController.php
@@ -1,696 +1,680 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorPeopleEditController
extends PhabricatorPeopleController {
public function shouldRequireAdmin() {
return true;
}
private $id;
private $view;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
$this->view = idx($data, 'view');
}
public function processRequest() {
$request = $this->getRequest();
$admin = $request->getUser();
if ($this->id) {
$user = id(new PhabricatorUser())->load($this->id);
if (!$user) {
return new Aphront404Response();
}
$base_uri = '/people/edit/'.$user->getID().'/';
} else {
$user = new PhabricatorUser();
$base_uri = '/people/edit/';
}
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($base_uri));
$nav->addLabel('User Information');
$nav->addFilter('basic', 'Basic Information');
$nav->addFilter('role', 'Edit Roles');
$nav->addFilter('cert', 'Conduit Certificate');
$nav->addFilter('profile', 'View Profile', '/p/'.$user->getUsername().'/');
$nav->addSpacer();
$nav->addLabel('Special');
$nav->addFilter('rename', 'Change Username');
$nav->addFilter('delete', 'Delete User');
if (!$user->getID()) {
$this->view = 'basic';
}
$view = $nav->selectFilter($this->view, 'basic');
$content = array();
if ($request->getStr('saved')) {
$notice = new AphrontErrorView();
$notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
$notice->setTitle('Changes Saved');
$notice->appendChild('<p>Your changes were saved.</p>');
$content[] = $notice;
}
switch ($view) {
case 'basic':
$response = $this->processBasicRequest($user);
break;
case 'role':
$response = $this->processRoleRequest($user);
break;
case 'cert':
$response = $this->processCertificateRequest($user);
break;
case 'rename':
$response = $this->processRenameRequest($user);
break;
case 'delete':
$response = $this->processDeleteRequest($user);
break;
default:
return new Aphront404Response();
}
if ($response instanceof AphrontResponse) {
return $response;
}
$content[] = $response;
if ($user->getID()) {
$nav->appendChild($content);
} else {
$nav = $this->buildSideNavView();
$nav->selectFilter('edit');
$nav->appendChild($content);
}
return $this->buildApplicationPage(
$nav,
array(
'title' => 'Edit User',
));
}
private function processBasicRequest(PhabricatorUser $user) {
$request = $this->getRequest();
$admin = $request->getUser();
$e_username = true;
$e_realname = true;
$e_email = true;
$errors = array();
$welcome_checked = true;
$new_email = null;
$request = $this->getRequest();
if ($request->isFormPost()) {
$welcome_checked = $request->getInt('welcome');
$is_new = !$user->getID();
if ($is_new) {
$user->setUsername($request->getStr('username'));
$new_email = $request->getStr('email');
if (!strlen($new_email)) {
$errors[] = 'Email is required.';
$e_email = 'Required';
} else if (!PhabricatorUserEmail::isAllowedAddress($new_email)) {
$e_email = 'Invalid';
$errors[] = PhabricatorUserEmail::describeAllowedAddresses();
} else {
$e_email = null;
}
}
$user->setRealName($request->getStr('realname'));
if (!strlen($user->getUsername())) {
$errors[] = "Username is required.";
$e_username = 'Required';
} else if (!PhabricatorUser::validateUsername($user->getUsername())) {
$errors[] = PhabricatorUser::describeValidUsername();
$e_username = 'Invalid';
} else {
$e_username = null;
}
if (!strlen($user->getRealName())) {
$errors[] = 'Real name is required.';
$e_realname = 'Required';
} else {
$e_realname = null;
}
if (!$errors) {
try {
if (!$is_new) {
id(new PhabricatorUserEditor())
->setActor($admin)
->updateUser($user);
} else {
$email = id(new PhabricatorUserEmail())
->setAddress($new_email)
->setIsVerified(0);
id(new PhabricatorUserEditor())
->setActor($admin)
->createNewUser($user, $email);
if ($request->getStr('role') == 'agent') {
id(new PhabricatorUserEditor())
->setActor($admin)
->makeSystemAgentUser($user, true);
}
}
if ($welcome_checked) {
$user->sendWelcomeEmail($admin);
}
$response = id(new AphrontRedirectResponse())
->setURI('/people/edit/'.$user->getID().'/?saved=true');
return $response;
} catch (AphrontQueryDuplicateKeyException $ex) {
$errors[] = 'Username and email must be unique.';
$same_username = id(new PhabricatorUser())
->loadOneWhere('username = %s', $user->getUsername());
$same_email = id(new PhabricatorUserEmail())
->loadOneWhere('address = %s', $new_email);
if ($same_username) {
$e_username = 'Duplicate';
}
if ($same_email) {
$e_email = 'Duplicate';
}
}
}
}
$error_view = null;
if ($errors) {
$error_view = id(new AphrontErrorView())
->setTitle('Form Errors')
->setErrors($errors);
}
$form = new AphrontFormView();
$form->setUser($admin);
if ($user->getID()) {
$form->setAction('/people/edit/'.$user->getID().'/');
} else {
$form->setAction('/people/edit/');
}
if ($user->getID()) {
$is_immutable = true;
} else {
$is_immutable = false;
}
$form
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Username')
->setName('username')
->setValue($user->getUsername())
->setError($e_username)
->setDisabled($is_immutable))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Real Name')
->setName('realname')
->setValue($user->getRealName())
->setError($e_realname));
if (!$user->getID()) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel('Email')
->setName('email')
->setDisabled($is_immutable)
->setValue($new_email)
->setCaption(PhabricatorUserEmail::describeAllowedAddresses())
->setError($e_email));
} else {
$email = $user->loadPrimaryEmail();
if ($email) {
$status = $email->getIsVerified() ? 'Verified' : 'Unverified';
} else {
$status = 'No Email Address';
}
$form->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Email')
->setValue($status));
$form->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox(
'welcome',
1,
'Re-send "Welcome to Phabricator" email.',
false));
}
$form->appendChild($this->getRoleInstructions());
if (!$user->getID()) {
$form
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Role')
->setName('role')
->setValue('user')
->setOptions(
array(
'user' => 'Normal User',
'agent' => 'System Agent',
))
->setCaption(
'You can create a "system agent" account for bots, scripts, '.
'etc.'))
->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox(
'welcome',
1,
'Send "Welcome to Phabricator" email.',
$welcome_checked));
} else {
$roles = array();
if ($user->getIsSystemAgent()) {
$roles[] = 'System Agent';
}
if ($user->getIsAdmin()) {
$roles[] = 'Admin';
}
if ($user->getIsDisabled()) {
$roles[] = 'Disabled';
}
if (!$roles) {
$roles[] = 'Normal User';
}
$roles = implode(', ', $roles);
$form->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Roles')
->setValue($roles));
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Save'));
$panel = new AphrontPanelView();
if ($user->getID()) {
$panel->setHeader('Edit User');
} else {
$panel->setHeader('Create New User');
}
$panel->appendChild($form);
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
return array($error_view, $panel);
}
private function processRoleRequest(PhabricatorUser $user) {
$request = $this->getRequest();
$admin = $request->getUser();
$is_self = ($user->getID() == $admin->getID());
$errors = array();
if ($request->isFormPost()) {
$log_template = PhabricatorUserLog::newLog(
$admin,
$user,
null);
$logs = array();
if ($is_self) {
$errors[] = "You can not edit your own role.";
} else {
$new_admin = (bool)$request->getBool('is_admin');
$old_admin = (bool)$user->getIsAdmin();
if ($new_admin != $old_admin) {
id(new PhabricatorUserEditor())
->setActor($admin)
->makeAdminUser($user, $new_admin);
}
$new_disabled = (bool)$request->getBool('is_disabled');
$old_disabled = (bool)$user->getIsDisabled();
if ($new_disabled != $old_disabled) {
id(new PhabricatorUserEditor())
->setActor($admin)
->disableUser($user, $new_disabled);
}
}
if (!$errors) {
return id(new AphrontRedirectResponse())
->setURI($request->getRequestURI()->alter('saved', 'true'));
}
}
$error_view = null;
if ($errors) {
$error_view = id(new AphrontErrorView())
->setTitle('Form Errors')
->setErrors($errors);
}
$form = id(new AphrontFormView())
->setUser($admin)
->setAction($request->getRequestURI()->alter('saved', null));
if ($is_self) {
$form->appendChild(
'<p class="aphront-form-instructions">NOTE: You can not edit your own '.
'role.</p>');
}
$form
->appendChild($this->getRoleInstructions())
->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox(
'is_admin',
1,
'Administrator',
$user->getIsAdmin())
->setDisabled($is_self))
->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox(
'is_disabled',
1,
'Disabled',
$user->getIsDisabled())
->setDisabled($is_self))
->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox(
'is_agent',
1,
'System Agent (Bot/Script User)',
$user->getIsSystemAgent())
->setDisabled(true));
if (!$is_self) {
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Edit Role'));
}
$panel = new AphrontPanelView();
$panel->setHeader('Edit Role');
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->appendChild($form);
return array($error_view, $panel);
}
private function processCertificateRequest($user) {
$request = $this->getRequest();
$admin = $request->getUser();
$form = new AphrontFormView();
$form
->setUser($admin)
->setAction($request->getRequestURI())
->appendChild(
'<p class="aphront-form-instructions">You can use this certificate '.
'to write scripts or bots which interface with Phabricator over '.
'Conduit.</p>');
if ($user->getIsSystemAgent()) {
$form
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Username')
->setValue($user->getUsername()))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Certificate')
->setValue($user->getConduitCertificate()));
} else {
$form->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Certificate')
->setValue(
'You may only view the certificates of System Agents.'));
}
$panel = new AphrontPanelView();
$panel->setHeader('Conduit Certificate');
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->appendChild($form);
return array($panel);
}
private function processRenameRequest(PhabricatorUser $user) {
$request = $this->getRequest();
$admin = $request->getUser();
$e_username = true;
$username = $user->getUsername();
$errors = array();
if ($request->isFormPost()) {
$username = $request->getStr('username');
if (!strlen($username)) {
$e_username = 'Required';
$errors[] = 'New username is required.';
} else if ($username == $user->getUsername()) {
$e_username = 'Invalid';
$errors[] = 'New username must be different from old username.';
} else if (!PhabricatorUser::validateUsername($username)) {
$e_username = 'Invalid';
$errors[] = PhabricatorUser::describeValidUsername();
}
if (!$errors) {
try {
id(new PhabricatorUserEditor())
->setActor($admin)
->changeUsername($user, $username);
return id(new AphrontRedirectResponse())
->setURI($request->getRequestURI()->alter('saved', true));
} catch (AphrontQueryDuplicateKeyException $ex) {
$e_username = 'Not Unique';
$errors[] = 'Another user already has that username.';
}
}
}
if ($errors) {
$errors = id(new AphrontErrorView())
->setTitle('Form Errors')
->setErrors($errors);
} else {
$errors = null;
}
$form = new AphrontFormView();
$form
->setUser($admin)
->setAction($request->getRequestURI())
->appendChild(
'<p class="aphront-form-instructions">'.
'<strong>Be careful when renaming users!</strong> '.
'The old username will no longer be tied to the user, so anything '.
'which uses it (like old commit messages) will no longer associate '.
'correctly. And if you give a user a username which some other user '.
'used to have, username lookups will begin returning the wrong '.
'user.'.
'</p>'.
'<p class="aphront-form-instructions">'.
'It is generally safe to rename newly created users (and test users '.
'and so on), but less safe to rename established users and unsafe '.
'to reissue a username.'.
'</p>'.
'<p class="aphront-form-instructions">'.
'Users who rely on password auth will need to reset their password '.
'after their username is changed (their username is part of the '.
'salt in the password hash). They will receive an email with '.
'instructions on how to do this.'.
'</p>')
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Old Username')
->setValue($user->getUsername()))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('New Username')
->setValue($username)
->setName('username')
->setError($e_username))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Change Username'));
$panel = new AphrontPanelView();
$panel->setHeader('Change Username');
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->appendChild($form);
return array($errors, $panel);
}
private function processDeleteRequest(PhabricatorUser $user) {
$request = $this->getRequest();
$admin = $request->getUser();
if ($user->getPHID() == $admin->getPHID()) {
$error = new AphrontErrorView();
$error->setTitle('You Shall Journey No Farther');
$error->appendChild(
'<p>As you stare into the gaping maw of the abyss, something holds '.
'you back.</p>'.
'<p>You can not delete your own account.</p>');
return $error;
}
$e_username = true;
$username = null;
$errors = array();
if ($request->isFormPost()) {
$username = $request->getStr('username');
if (!strlen($username)) {
$e_username = 'Required';
$errors[] = 'You must type the username to confirm deletion.';
} else if ($username != $user->getUsername()) {
$e_username = 'Invalid';
$errors[] = 'You must type the username correctly.';
}
if (!$errors) {
id(new PhabricatorUserEditor())
->setActor($admin)
->deleteUser($user);
return id(new AphrontRedirectResponse())->setURI('/people/');
}
}
if ($errors) {
$errors = id(new AphrontErrorView())
->setTitle('Form Errors')
->setErrors($errors);
} else {
$errors = null;
}
$form = new AphrontFormView();
$form
->setUser($admin)
->setAction($request->getRequestURI())
->appendChild(
'<p class="aphront-form-instructions">'.
'<strong>Be careful when deleting users!</strong> '.
'If this user interacted with anything, it is generally better '.
'to disable them, not delete them. If you delete them, it will '.
'no longer be possible to search for their objects, for example, '.
'and you will lose other information about their history. Disabling '.
'them instead will prevent them from logging in but not destroy '.
'any of their data.'.
'</p>'.
'<p class="aphront-form-instructions">'.
'It is generally safe to delete newly created users (and test users '.
'and so on), but less safe to delete established users. If '.
'possible, disable them instead.'.
'</p>')
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Username')
->setValue($user->getUsername()))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Confirm')
->setValue($username)
->setName('username')
->setCaption("Type the username again to confirm deletion.")
->setError($e_username))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Delete User'));
$panel = new AphrontPanelView();
$panel->setHeader('Delete User');
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->appendChild($form);
return array($errors, $panel);
}
private function getRoleInstructions() {
$roles_link = phutil_render_tag(
'a',
array(
'href' => PhabricatorEnv::getDoclink(
'article/User_Guide_Account_Roles.html'),
'target' => '_blank',
),
'User Guide: Account Roles');
return
'<p class="aphront-form-instructions">'.
'For a detailed explanation of account roles, see '.
$roles_link.'.'.
'</p>';
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleLdapController.php b/src/applications/people/controller/PhabricatorPeopleLdapController.php
index 817c981fcf..d0f1d715a9 100644
--- a/src/applications/people/controller/PhabricatorPeopleLdapController.php
+++ b/src/applications/people/controller/PhabricatorPeopleLdapController.php
@@ -1,202 +1,186 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorPeopleLdapController
extends PhabricatorPeopleController {
public function shouldRequireAdmin() {
return true;
}
private $view;
public function processRequest() {
$request = $this->getRequest();
$admin = $request->getUser();
$content = array();
$form = id(new AphrontFormView())
->setAction($request->getRequestURI()
->alter('search', 'true')->alter('import', null))
->setUser($admin)
->appendChild(
id(new AphrontFormTextControl())
->setLabel('LDAP username')
->setName('username'))
->appendChild(
id(new AphrontFormPasswordControl())
->setLabel('Password')
->setName('password'))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('LDAP query')
->setCaption('A filter such as (objectClass=*)')
->setName('query'))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Search'));
$panel = new AphrontPanelView();
$panel->setHeader('Import LDAP Users');
$panel->appendChild($form);
if ($request->getStr('import')) {
$content[] = $this->processImportRequest($request);
}
$content[] = $panel;
if ($request->getStr('search')) {
$content[] = $this->processSearchRequest($request);
}
$nav = $this->buildSideNavView();
$nav->selectFilter('ldap');
$nav->appendChild($content);
return $this->buildApplicationPage(
$nav,
array(
'title' => 'Import Ldap Users',
));
}
private function processImportRequest($request) {
$admin = $request->getUser();
$usernames = $request->getArr('usernames');
$emails = $request->getArr('email');
$names = $request->getArr('name');
$panel = new AphrontErrorView();
$panel->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
$panel->setTitle("Import Successful");
$errors = array("Successfully imported users from LDAP");
foreach ($usernames as $username) {
$user = new PhabricatorUser();
$user->setUsername($username);
$user->setRealname($names[$username]);
$email_obj = id(new PhabricatorUserEmail())
->setAddress($emails[$username])
->setIsVerified(1);
try {
id(new PhabricatorUserEditor())
->setActor($admin)
->createNewUser($user, $email_obj);
$ldap_info = new PhabricatorUserLDAPInfo();
$ldap_info->setLDAPUsername($username);
$ldap_info->setUserID($user->getID());
$ldap_info->save();
$errors[] = 'Successfully added ' . $username;
} catch (Exception $ex) {
$errors[] = 'Failed to add ' . $username . ' ' . $ex->getMessage();
}
}
$panel->setErrors($errors);
return $panel;
}
private function processSearchRequest($request) {
$panel = new AphrontPanelView();
$admin = $request->getUser();
$username = $request->getStr('username');
$password = $request->getStr('password');
$search = $request->getStr('query');
try {
$ldap_provider = new PhabricatorLDAPProvider();
$envelope = new PhutilOpaqueEnvelope($password);
$ldap_provider->auth($username, $envelope);
$results = $ldap_provider->search($search);
foreach ($results as $key => $result) {
$results[$key][] = $this->renderUserInputs($result);
}
$form = id(new AphrontFormView())
->setUser($admin);
$table = new AphrontTableView($results);
$table->setHeaders(
array(
'Username',
'Email',
'RealName',
'Import?',
));
$form->appendChild($table);
$form->setAction($request->getRequestURI()
->alter('import', 'true')->alter('search', null))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Import'));
$panel->appendChild($form);
} catch (Exception $ex) {
$error_view = new AphrontErrorView();
$error_view->setTitle('LDAP Search Failed');
$error_view->setErrors(array($ex->getMessage()));
return $error_view;
}
return $panel;
}
private function renderUserInputs($user) {
$username = $user[0];
$inputs = phutil_render_tag(
'input',
array(
'type' => 'checkbox',
'name' => 'usernames[]',
'value' =>$username,
),
'');
$inputs .= phutil_render_tag(
'input',
array(
'type' => 'hidden',
'name' => "email[$username]",
'value' =>$user[1],
),
'');
$inputs .= phutil_render_tag(
'input',
array(
'type' => 'hidden',
'name' => "name[$username]",
'value' =>$user[2],
),
'');
return $inputs;
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleListController.php b/src/applications/people/controller/PhabricatorPeopleListController.php
index 82a0be1268..8ec8bf07d3 100644
--- a/src/applications/people/controller/PhabricatorPeopleListController.php
+++ b/src/applications/people/controller/PhabricatorPeopleListController.php
@@ -1,155 +1,139 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorPeopleListController
extends PhabricatorPeopleController {
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$is_admin = $viewer->getIsAdmin();
$user = new PhabricatorUser();
$count = queryfx_one(
$user->establishConnection('r'),
'SELECT COUNT(*) N FROM %T',
$user->getTableName());
$count = idx($count, 'N', 0);
$pager = new AphrontPagerView();
$pager->setOffset($request->getInt('page', 0));
$pager->setCount($count);
$pager->setURI($request->getRequestURI(), 'page');
$users = id(new PhabricatorPeopleQuery())
->needPrimaryEmail(true)
->executeWithOffsetPager($pager);
$rows = array();
foreach ($users as $user) {
$primary_email = $user->loadPrimaryEmail();
if ($primary_email && $primary_email->getIsVerified()) {
$email = 'Verified';
} else {
$email = 'Unverified';
}
$status = array();
if ($user->getIsDisabled()) {
$status[] = 'Disabled';
}
if ($user->getIsAdmin()) {
$status[] = 'Admin';
}
if ($user->getIsSystemAgent()) {
$status[] = 'System Agent';
}
$status = implode(', ', $status);
$rows[] = array(
phabricator_date($user->getDateCreated(), $viewer),
phabricator_time($user->getDateCreated(), $viewer),
phutil_render_tag(
'a',
array(
'href' => '/p/'.$user->getUsername().'/',
),
phutil_escape_html($user->getUserName())),
phutil_escape_html($user->getRealName()),
$status,
$email,
phutil_render_tag(
'a',
array(
'class' => 'button grey small',
'href' => '/people/edit/'.$user->getID().'/',
),
'Administrate User'),
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Join Date',
'Time',
'Username',
'Real Name',
'Roles',
'Email',
'',
));
$table->setColumnClasses(
array(
null,
'right',
'pri',
'wide',
null,
null,
'action',
));
$table->setColumnVisibility(
array(
true,
true,
true,
true,
$is_admin,
$is_admin,
$is_admin,
));
$panel = new AphrontPanelView();
$panel->setHeader('People ('.number_format($count).')');
$panel->appendChild($table);
$panel->appendChild($pager);
if ($is_admin) {
$panel->addButton(
phutil_render_tag(
'a',
array(
'href' => '/people/edit/',
'class' => 'button green',
),
'Create New Account'));
if (PhabricatorEnv::getEnvConfig('ldap.auth-enabled')) {
$panel->addButton(
phutil_render_tag(
'a',
array(
'href' => '/people/ldap/',
'class' => 'button green'
),
'Import from LDAP'));
}
}
$nav = $this->buildSideNavView();
$nav->selectFilter('people');
$nav->appendChild($panel);
return $this->buildApplicationPage(
$nav,
array(
'title' => 'People',
));
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleLogsController.php b/src/applications/people/controller/PhabricatorPeopleLogsController.php
index 9d0839d5b6..1621bd58fe 100644
--- a/src/applications/people/controller/PhabricatorPeopleLogsController.php
+++ b/src/applications/people/controller/PhabricatorPeopleLogsController.php
@@ -1,250 +1,234 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorPeopleLogsController
extends PhabricatorPeopleController {
public function shouldRequireAdmin() {
return true;
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$filter_activity = $request->getStr('activity');
$filter_ip = $request->getStr('ip');
$filter_session = $request->getStr('session');
$filter_user = $request->getArr('user', array());
$filter_actor = $request->getArr('actor', array());
$user_value = array();
$actor_value = array();
$phids = array_merge($filter_user, $filter_actor);
if ($phids) {
$handles = $this->loadViewerHandles($phids);
if ($filter_user) {
$filter_user = reset($filter_user);
$user_value = array(
$filter_user => $handles[$filter_user]->getFullName(),
);
}
if ($filter_actor) {
$filter_actor = reset($filter_actor);
$actor_value = array(
$filter_actor => $handles[$filter_actor]->getFullName(),
);
}
}
$form = new AphrontFormView();
$form
->setUser($user)
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel('Filter Actor')
->setName('actor')
->setLimit(1)
->setValue($actor_value)
->setDatasource('/typeahead/common/accounts/'))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel('Filter User')
->setName('user')
->setLimit(1)
->setValue($user_value)
->setDatasource('/typeahead/common/accounts/'))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Show Activity')
->setName('activity')
->setValue($filter_activity)
->setOptions(
array(
'' => 'All Activity',
'admin' => 'Admin Activity',
)))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Filter IP')
->setName('ip')
->setValue($filter_ip)
->setCaption(
'Enter an IP (or IP prefix) to show only activity by that remote '.
'address.'))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Filter Session')
->setName('session')
->setValue($filter_session))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Filter Logs'));
$log_table = new PhabricatorUserLog();
$conn_r = $log_table->establishConnection('r');
$where_clause = array();
$where_clause[] = '1 = 1';
if ($filter_user) {
$where_clause[] = qsprintf(
$conn_r,
'userPHID = %s',
$filter_user);
}
if ($filter_actor) {
$where_clause[] = qsprintf(
$conn_r,
'actorPHID = %s',
$filter_actor);
}
if ($filter_activity == 'admin') {
$where_clause[] = qsprintf(
$conn_r,
'action NOT IN (%Ls)',
array(
PhabricatorUserLog::ACTION_LOGIN,
PhabricatorUserLog::ACTION_LOGOUT,
PhabricatorUserLog::ACTION_LOGIN_FAILURE,
));
}
if ($filter_ip) {
$where_clause[] = qsprintf(
$conn_r,
'remoteAddr LIKE %>',
$filter_ip);
}
if ($filter_session) {
$where_clause[] = qsprintf(
$conn_r,
'session = %s',
$filter_session);
}
$where_clause = '('.implode(') AND (', $where_clause).')';
$pager = new AphrontPagerView();
$pager->setURI($request->getRequestURI(), 'page');
$pager->setOffset($request->getInt('page'));
$pager->setPageSize(500);
$logs = $log_table->loadAllWhere(
'(%Q) ORDER BY dateCreated DESC LIMIT %d, %d',
$where_clause,
$pager->getOffset(),
$pager->getPageSize() + 1);
$logs = $pager->sliceResults($logs);
$phids = array();
foreach ($logs as $log) {
$phids[$log->getActorPHID()] = true;
$phids[$log->getUserPHID()] = true;
}
$phids = array_keys($phids);
$handles = $this->loadViewerHandles($phids);
$rows = array();
foreach ($logs as $log) {
$rows[] = array(
phabricator_date($log->getDateCreated(),$user),
phabricator_time($log->getDateCreated(),$user),
$log->getAction(),
$log->getActorPHID()
? phutil_escape_html($handles[$log->getActorPHID()]->getName())
: null,
phutil_escape_html($handles[$log->getUserPHID()]->getName()),
json_encode($log->getOldValue(), true),
json_encode($log->getNewValue(), true),
phutil_render_tag(
'a',
array(
'href' => $request
->getRequestURI()
->alter('ip', $log->getRemoteAddr()),
),
phutil_escape_html($log->getRemoteAddr())),
phutil_render_tag(
'a',
array(
'href' => $request
->getRequestURI()
->alter('session', $log->getSession()),
),
phutil_escape_html($log->getSession())),
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Date',
'Time',
'Action',
'Actor',
'User',
'Old',
'New',
'IP',
'Session',
));
$table->setColumnClasses(
array(
'',
'right',
'',
'',
'',
'wrap',
'wrap',
'',
'wide',
));
$panel = new AphrontPanelView();
$panel->setHeader('Activity Logs');
$panel->appendChild($table);
$panel->appendChild($pager);
$filter = new AphrontListFilterView();
$filter->appendChild($form);
$nav = $this->buildSideNavView();
$nav->selectFilter('logs');
$nav->appendChild(
array(
$filter,
$panel,
));
return $this->buildApplicationPage(
$nav,
array(
'title' => 'Activity Logs',
));
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleProfileController.php b/src/applications/people/controller/PhabricatorPeopleProfileController.php
index 54b5a5d501..a51e71abf5 100644
--- a/src/applications/people/controller/PhabricatorPeopleProfileController.php
+++ b/src/applications/people/controller/PhabricatorPeopleProfileController.php
@@ -1,237 +1,221 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorPeopleProfileController
extends PhabricatorPeopleController {
private $username;
private $page;
private $profileUser;
public function willProcessRequest(array $data) {
$this->username = idx($data, 'username');
$this->page = idx($data, 'page');
}
public function getProfileUser() {
return $this->profileUser;
}
public function processRequest() {
$viewer = $this->getRequest()->getUser();
$user = id(new PhabricatorUser())->loadOneWhere(
'userName = %s',
$this->username);
if (!$user) {
return new Aphront404Response();
}
$this->profileUser = $user;
require_celerity_resource('phabricator-profile-css');
$profile = id(new PhabricatorUserProfile())->loadOneWhere(
'userPHID = %s',
$user->getPHID());
if (!$profile) {
$profile = new PhabricatorUserProfile();
}
$username = phutil_escape_uri($user->getUserName());
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI('/p/'.$username.'/'));
$nav->addFilter('feed', 'Feed');
$nav->addFilter('about', 'About');
$nav->addSpacer();
$nav->addLabel('Activity');
$external_arrow = "\xE2\x86\x97";
$nav->addFilter(
null,
"Revisions {$external_arrow}",
'/differential/filter/revisions/'.$username.'/');
$nav->addFilter(
null,
"Tasks {$external_arrow}",
'/maniphest/view/action/?users='.$user->getPHID());
$nav->addFilter(
null,
"Commits {$external_arrow}",
'/audit/view/author/'.$username.'/');
$oauths = id(new PhabricatorUserOAuthInfo())->loadAllWhere(
'userID = %d',
$user->getID());
$oauths = mpull($oauths, null, 'getOAuthProvider');
$providers = PhabricatorOAuthProvider::getAllProviders();
$added_spacer = false;
foreach ($providers as $provider) {
if (!$provider->isProviderEnabled()) {
continue;
}
$provider_key = $provider->getProviderKey();
if (!isset($oauths[$provider_key])) {
continue;
}
$name = $provider->getProviderName().' Profile';
$href = $oauths[$provider_key]->getAccountURI();
if ($href) {
if (!$added_spacer) {
$nav->addSpacer();
$nav->addLabel('Linked Accounts');
$added_spacer = true;
}
$nav->addFilter(null, $name.' '.$external_arrow, $href);
}
}
$this->page = $nav->selectFilter($this->page, 'feed');
switch ($this->page) {
case 'feed':
$content = $this->renderUserFeed($user);
break;
case 'about':
$content = $this->renderBasicInformation($user, $profile);
break;
default:
throw new Exception("Unknown page '{$this->page}'!");
}
$picture = $user->loadProfileImageURI();
$header = new PhabricatorProfileHeaderView();
$header
->setProfilePicture($picture)
->setName($user->getUserName().' ('.$user->getRealName().')')
->setDescription($profile->getTitle());
if ($user->getIsDisabled()) {
$header->setStatus('Disabled');
} else {
$statuses = id(new PhabricatorUserStatus())->loadCurrentStatuses(
array($user->getPHID()));
if ($statuses) {
$header->setStatus(reset($statuses)->getTerseSummary($viewer));
}
}
$header->appendChild($nav);
$nav->appendChild(
'<div style="padding: 1em;">'.$content.'</div>');
if ($user->getPHID() == $viewer->getPHID()) {
$nav->addSpacer();
$nav->addFilter(null, 'Edit Profile...', '/settings/panel/profile/');
}
if ($viewer->getIsAdmin()) {
$nav->addSpacer();
$nav->addFilter(
null,
'Administrate User...',
'/people/edit/'.$user->getID().'/');
}
return $this->buildApplicationPage(
$header,
array(
'title' => $user->getUsername(),
));
}
private function renderBasicInformation($user, $profile) {
$blurb = nonempty(
$profile->getBlurb(),
'//Nothing is known about this rare specimen.//');
$engine = PhabricatorMarkupEngine::newProfileMarkupEngine();
$blurb = $engine->markupText($blurb);
$viewer = $this->getRequest()->getUser();
$content =
'<div class="phabricator-profile-info-group">
<h1 class="phabricator-profile-info-header">Basic Information</h1>
<div class="phabricator-profile-info-pane">
<table class="phabricator-profile-info-table">
<tr>
<th>PHID</th>
<td>'.phutil_escape_html($user->getPHID()).'</td>
</tr>
<tr>
<th>User Since</th>
<td>'.phabricator_datetime($user->getDateCreated(),
$viewer).
'</td>
</tr>
</table>
</div>
</div>';
$content .=
'<div class="phabricator-profile-info-group">
<h1 class="phabricator-profile-info-header">Flavor Text</h1>
<div class="phabricator-profile-info-pane">
<table class="phabricator-profile-info-table">
<tr>
<th>Blurb</th>
<td>'.$blurb.'</td>
</tr>
</table>
</div>
</div>';
return $content;
}
private function renderUserFeed(PhabricatorUser $user) {
$viewer = $this->getRequest()->getUser();
$query = new PhabricatorFeedQuery();
$query->setFilterPHIDs(
array(
$user->getPHID(),
));
$query->setLimit(100);
$query->setViewer($viewer);
$stories = $query->execute();
$builder = new PhabricatorFeedBuilder($stories);
$builder->setUser($viewer);
$view = $builder->buildView();
return
'<div class="phabricator-profile-info-group">
<h1 class="phabricator-profile-info-header">Activity Feed</h1>
<div class="phabricator-profile-info-pane">
'.$view->render().'
</div>
</div>';
}
}
diff --git a/src/applications/people/exception/PhabricatorUserStatusInvalidEpochException.php b/src/applications/people/exception/PhabricatorUserStatusInvalidEpochException.php
index e235f68a00..be495f4e83 100644
--- a/src/applications/people/exception/PhabricatorUserStatusInvalidEpochException.php
+++ b/src/applications/people/exception/PhabricatorUserStatusInvalidEpochException.php
@@ -1,20 +1,4 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorUserStatusInvalidEpochException extends Exception {
}
diff --git a/src/applications/people/exception/PhabricatorUserStatusOverlapException.php b/src/applications/people/exception/PhabricatorUserStatusOverlapException.php
index 990fdceedf..2b67977c64 100644
--- a/src/applications/people/exception/PhabricatorUserStatusOverlapException.php
+++ b/src/applications/people/exception/PhabricatorUserStatusOverlapException.php
@@ -1,20 +1,4 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorUserStatusOverlapException extends Exception {
}
diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php
index 71383fc17f..01d760f9f5 100644
--- a/src/applications/people/storage/PhabricatorUser.php
+++ b/src/applications/people/storage/PhabricatorUser.php
@@ -1,662 +1,646 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorUser extends PhabricatorUserDAO implements PhutilPerson {
const SESSION_TABLE = 'phabricator_session';
const NAMETOKEN_TABLE = 'user_nametoken';
protected $phid;
protected $userName;
protected $realName;
protected $sex;
protected $translation;
protected $passwordSalt;
protected $passwordHash;
protected $profileImagePHID;
protected $timezoneIdentifier = '';
protected $consoleEnabled = 0;
protected $consoleVisible = 0;
protected $consoleTab = '';
protected $conduitCertificate;
protected $isSystemAgent = 0;
protected $isAdmin = 0;
protected $isDisabled = 0;
private $preferences = null;
protected function readField($field) {
switch ($field) {
case 'timezoneIdentifier':
// If the user hasn't set one, guess the server's time.
return nonempty(
$this->timezoneIdentifier,
date_default_timezone_get());
// Make sure these return booleans.
case 'isAdmin':
return (bool)$this->isAdmin;
case 'isDisabled':
return (bool)$this->isDisabled;
case 'isSystemAgent':
return (bool)$this->isSystemAgent;
default:
return parent::readField($field);
}
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_PARTIAL_OBJECTS => true,
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_USER);
}
public function setPassword(PhutilOpaqueEnvelope $envelope) {
if (!$this->getPHID()) {
throw new Exception(
"You can not set a password for an unsaved user because their PHID ".
"is a salt component in the password hash.");
}
if (!strlen($envelope->openEnvelope())) {
$this->setPasswordHash('');
} else {
$this->setPasswordSalt(md5(mt_rand()));
$hash = $this->hashPassword($envelope);
$this->setPasswordHash($hash);
}
return $this;
}
// To satisfy PhutilPerson.
public function getSex() {
return $this->sex;
}
public function getTranslation() {
try {
if ($this->translation &&
class_exists($this->translation) &&
is_subclass_of($this->translation, 'PhabricatorTranslation')) {
return $this->translation;
}
} catch (PhutilMissingSymbolException $ex) {
return null;
}
return null;
}
public function isLoggedIn() {
return !($this->getPHID() === null);
}
public function save() {
if (!$this->getConduitCertificate()) {
$this->setConduitCertificate($this->generateConduitCertificate());
}
$result = parent::save();
$this->updateNameTokens();
PhabricatorSearchUserIndexer::indexUser($this);
return $result;
}
private function generateConduitCertificate() {
return Filesystem::readRandomCharacters(255);
}
public function comparePassword(PhutilOpaqueEnvelope $envelope) {
if (!strlen($envelope->openEnvelope())) {
return false;
}
if (!strlen($this->getPasswordHash())) {
return false;
}
$password_hash = $this->hashPassword($envelope);
return ($password_hash === $this->getPasswordHash());
}
private function hashPassword(PhutilOpaqueEnvelope $envelope) {
$hash = $this->getUsername().
$envelope->openEnvelope().
$this->getPHID().
$this->getPasswordSalt();
for ($ii = 0; $ii < 1000; $ii++) {
$hash = md5($hash);
}
return $hash;
}
const CSRF_CYCLE_FREQUENCY = 3600;
const CSRF_TOKEN_LENGTH = 16;
const EMAIL_CYCLE_FREQUENCY = 86400;
const EMAIL_TOKEN_LENGTH = 24;
public function getCSRFToken($offset = 0) {
return $this->generateToken(
time() + (self::CSRF_CYCLE_FREQUENCY * $offset),
self::CSRF_CYCLE_FREQUENCY,
PhabricatorEnv::getEnvConfig('phabricator.csrf-key'),
self::CSRF_TOKEN_LENGTH);
}
public function validateCSRFToken($token) {
if (!$this->getPHID()) {
return true;
}
// When the user posts a form, we check that it contains a valid CSRF token.
// Tokens cycle each hour (every CSRF_CYLCE_FREQUENCY seconds) and we accept
// either the current token, the next token (users can submit a "future"
// token if you have two web frontends that have some clock skew) or any of
// the last 6 tokens. This means that pages are valid for up to 7 hours.
// There is also some Javascript which periodically refreshes the CSRF
// tokens on each page, so theoretically pages should be valid indefinitely.
// However, this code may fail to run (if the user loses their internet
// connection, or there's a JS problem, or they don't have JS enabled).
// Choosing the size of the window in which we accept old CSRF tokens is
// an issue of balancing concerns between security and usability. We could
// choose a very narrow (e.g., 1-hour) window to reduce vulnerability to
// attacks using captured CSRF tokens, but it's also more likely that real
// users will be affected by this, e.g. if they close their laptop for an
// hour, open it back up, and try to submit a form before the CSRF refresh
// can kick in. Since the user experience of submitting a form with expired
// CSRF is often quite bad (you basically lose data, or it's a big pain to
// recover at least) and I believe we gain little additional protection
// by keeping the window very short (the overwhelming value here is in
// preventing blind attacks, and most attacks which can capture CSRF tokens
// can also just capture authentication information [sniffing networks]
// or act as the user [xss]) the 7 hour default seems like a reasonable
// balance. Other major platforms have much longer CSRF token lifetimes,
// like Rails (session duration) and Django (forever), which suggests this
// is a reasonable analysis.
$csrf_window = 6;
for ($ii = -$csrf_window; $ii <= 1; $ii++) {
$valid = $this->getCSRFToken($ii);
if ($token == $valid) {
return true;
}
}
return false;
}
private function generateToken($epoch, $frequency, $key, $len) {
$time_block = floor($epoch / $frequency);
$vec = $this->getPHID().$this->getPasswordHash().$key.$time_block;
return substr(PhabricatorHash::digest($vec), 0, $len);
}
/**
* Issue a new session key to this user. Phabricator supports different
* types of sessions (like "web" and "conduit") and each session type may
* have multiple concurrent sessions (this allows a user to be logged in on
* multiple browsers at the same time, for instance).
*
* Note that this method is transport-agnostic and does not set cookies or
* issue other types of tokens, it ONLY generates a new session key.
*
* You can configure the maximum number of concurrent sessions for various
* session types in the Phabricator configuration.
*
* @param string Session type, like "web".
* @return string Newly generated session key.
*/
public function establishSession($session_type) {
$conn_w = $this->establishConnection('w');
if (strpos($session_type, '-') !== false) {
throw new Exception("Session type must not contain hyphen ('-')!");
}
// We allow multiple sessions of the same type, so when a caller requests
// a new session of type "web", we give them the first available session in
// "web-1", "web-2", ..., "web-N", up to some configurable limit. If none
// of these sessions is available, we overwrite the oldest session and
// reissue a new one in its place.
$session_limit = 1;
switch ($session_type) {
case 'web':
$session_limit = PhabricatorEnv::getEnvConfig('auth.sessions.web');
break;
case 'conduit':
$session_limit = PhabricatorEnv::getEnvConfig('auth.sessions.conduit');
break;
default:
throw new Exception("Unknown session type '{$session_type}'!");
}
$session_limit = (int)$session_limit;
if ($session_limit <= 0) {
throw new Exception(
"Session limit for '{$session_type}' must be at least 1!");
}
// NOTE: Session establishment is sensitive to race conditions, as when
// piping `arc` to `arc`:
//
// arc export ... | arc paste ...
//
// To avoid this, we overwrite an old session only if it hasn't been
// re-established since we read it.
// Consume entropy to generate a new session key, forestalling the eventual
// heat death of the universe.
$session_key = Filesystem::readRandomCharacters(40);
// Load all the currently active sessions.
$sessions = queryfx_all(
$conn_w,
'SELECT type, sessionKey, sessionStart FROM %T
WHERE userPHID = %s AND type LIKE %>',
PhabricatorUser::SESSION_TABLE,
$this->getPHID(),
$session_type.'-');
$sessions = ipull($sessions, null, 'type');
$sessions = isort($sessions, 'sessionStart');
$existing_sessions = array_keys($sessions);
// UNGUARDED WRITES: Logging-in users don't have CSRF stuff yet.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$retries = 0;
while (true) {
// Choose which 'type' we'll actually establish, i.e. what number we're
// going to append to the basic session type. To do this, just check all
// the numbers sequentially until we find an available session.
$establish_type = null;
for ($ii = 1; $ii <= $session_limit; $ii++) {
$try_type = $session_type.'-'.$ii;
if (!in_array($try_type, $existing_sessions)) {
$establish_type = $try_type;
$expect_key = $session_key;
$existing_sessions[] = $try_type;
// Ensure the row exists so we can issue an update below. We don't
// care if we race here or not.
queryfx(
$conn_w,
'INSERT IGNORE INTO %T (userPHID, type, sessionKey, sessionStart)
VALUES (%s, %s, %s, 0)',
self::SESSION_TABLE,
$this->getPHID(),
$establish_type,
$session_key);
break;
}
}
// If we didn't find an available session, choose the oldest session and
// overwrite it.
if (!$establish_type) {
$oldest = reset($sessions);
$establish_type = $oldest['type'];
$expect_key = $oldest['sessionKey'];
}
// This is so that we'll only overwrite the session if it hasn't been
// refreshed since we read it. If it has, the session key will be
// different and we know we're racing other processes. Whichever one
// won gets the session, we go back and try again.
queryfx(
$conn_w,
'UPDATE %T SET sessionKey = %s, sessionStart = UNIX_TIMESTAMP()
WHERE userPHID = %s AND type = %s AND sessionKey = %s',
self::SESSION_TABLE,
$session_key,
$this->getPHID(),
$establish_type,
$expect_key);
if ($conn_w->getAffectedRows()) {
// The update worked, so the session is valid.
break;
} else {
// We know this just got grabbed, so don't try it again.
unset($sessions[$establish_type]);
}
if (++$retries > $session_limit) {
throw new Exception("Failed to establish a session!");
}
}
$log = PhabricatorUserLog::newLog(
$this,
$this,
PhabricatorUserLog::ACTION_LOGIN);
$log->setDetails(
array(
'session_type' => $session_type,
'session_issued' => $establish_type,
));
$log->setSession($session_key);
$log->save();
return $session_key;
}
public function destroySession($session_key) {
$conn_w = $this->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE userPHID = %s AND sessionKey = %s',
self::SESSION_TABLE,
$this->getPHID(),
$session_key);
}
private function generateEmailToken(
PhabricatorUserEmail $email,
$offset = 0) {
$key = implode(
'-',
array(
PhabricatorEnv::getEnvConfig('phabricator.csrf-key'),
$this->getPHID(),
$email->getVerificationCode(),
));
return $this->generateToken(
time() + ($offset * self::EMAIL_CYCLE_FREQUENCY),
self::EMAIL_CYCLE_FREQUENCY,
$key,
self::EMAIL_TOKEN_LENGTH);
}
public function validateEmailToken(
PhabricatorUserEmail $email,
$token) {
for ($ii = -1; $ii <= 1; $ii++) {
$valid = $this->generateEmailToken($email, $ii);
if ($token == $valid) {
return true;
}
}
return false;
}
public function getEmailLoginURI(PhabricatorUserEmail $email = null) {
if (!$email) {
$email = $this->loadPrimaryEmail();
if (!$email) {
throw new Exception("User has no primary email!");
}
}
$token = $this->generateEmailToken($email);
$uri = PhabricatorEnv::getProductionURI('/login/etoken/'.$token.'/');
$uri = new PhutilURI($uri);
return $uri->alter('email', $email->getAddress());
}
public function loadPrimaryEmailAddress() {
$email = $this->loadPrimaryEmail();
if (!$email) {
throw new Exception("User has no primary email address!");
}
return $email->getAddress();
}
public function loadPrimaryEmail() {
return $this->loadOneRelative(
new PhabricatorUserEmail(),
'userPHID',
'getPHID',
'(isPrimary = 1)');
}
public function loadPreferences() {
if ($this->preferences) {
return $this->preferences;
}
$preferences = id(new PhabricatorUserPreferences())->loadOneWhere(
'userPHID = %s',
$this->getPHID());
if (!$preferences) {
$preferences = new PhabricatorUserPreferences();
$preferences->setUserPHID($this->getPHID());
$default_dict = array(
PhabricatorUserPreferences::PREFERENCE_TITLES => 'glyph',
PhabricatorUserPreferences::PREFERENCE_EDITOR => '',
PhabricatorUserPreferences::PREFERENCE_MONOSPACED => '');
$preferences->setPreferences($default_dict);
}
$this->preferences = $preferences;
return $preferences;
}
public function loadEditorLink($path, $line, $callsign) {
$editor = $this->loadPreferences()->getPreference(
PhabricatorUserPreferences::PREFERENCE_EDITOR);
if ($editor) {
return strtr($editor, array(
'%%' => '%',
'%f' => phutil_escape_uri($path),
'%l' => phutil_escape_uri($line),
'%r' => phutil_escape_uri($callsign),
));
}
}
private static function tokenizeName($name) {
if (function_exists('mb_strtolower')) {
$name = mb_strtolower($name, 'UTF-8');
} else {
$name = strtolower($name);
}
$name = trim($name);
if (!strlen($name)) {
return array();
}
return preg_split('/\s+/', $name);
}
/**
* Populate the nametoken table, which used to fetch typeahead results. When
* a user types "linc", we want to match "Abraham Lincoln" from on-demand
* typeahead sources. To do this, we need a separate table of name fragments.
*/
public function updateNameTokens() {
$tokens = array_merge(
self::tokenizeName($this->getRealName()),
self::tokenizeName($this->getUserName()));
$tokens = array_unique($tokens);
$table = self::NAMETOKEN_TABLE;
$conn_w = $this->establishConnection('w');
$sql = array();
foreach ($tokens as $token) {
$sql[] = qsprintf(
$conn_w,
'(%d, %s)',
$this->getID(),
$token);
}
queryfx(
$conn_w,
'DELETE FROM %T WHERE userID = %d',
$table,
$this->getID());
if ($sql) {
queryfx(
$conn_w,
'INSERT INTO %T (userID, token) VALUES %Q',
$table,
implode(', ', $sql));
}
}
public function sendWelcomeEmail(PhabricatorUser $admin) {
$admin_username = $admin->getUserName();
$admin_realname = $admin->getRealName();
$user_username = $this->getUserName();
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
$base_uri = PhabricatorEnv::getProductionURI('/');
$uri = $this->getEmailLoginURI();
$body = <<<EOBODY
Welcome to Phabricator!
{$admin_username} ({$admin_realname}) has created an account for you.
Username: {$user_username}
To login to Phabricator, follow this link and set a password:
{$uri}
After you have set a password, you can login in the future by going here:
{$base_uri}
EOBODY;
if (!$is_serious) {
$body .= <<<EOBODY
Love,
Phabricator
EOBODY;
}
$mail = id(new PhabricatorMetaMTAMail())
->addTos(array($this->getPHID()))
->setSubject('[Phabricator] Welcome to Phabricator')
->setBody($body)
->setFrom($admin->getPHID())
->saveAndSend();
}
public function sendUsernameChangeEmail(
PhabricatorUser $admin,
$old_username) {
$admin_username = $admin->getUserName();
$admin_realname = $admin->getRealName();
$new_username = $this->getUserName();
$password_instructions = null;
if (PhabricatorEnv::getEnvConfig('auth.password-auth-enabled')) {
$uri = $this->getEmailLoginURI();
$password_instructions = <<<EOTXT
If you use a password to login, you'll need to reset it before you can login
again. You can reset your password by following this link:
{$uri}
And, of course, you'll need to use your new username to login from now on. If
you use OAuth to login, nothing should change.
EOTXT;
}
$body = <<<EOBODY
{$admin_username} ({$admin_realname}) has changed your Phabricator username.
Old Username: {$old_username}
New Username: {$new_username}
{$password_instructions}
EOBODY;
$mail = id(new PhabricatorMetaMTAMail())
->addTos(array($this->getPHID()))
->setSubject('[Phabricator] Username Changed')
->setBody($body)
->setFrom($admin->getPHID())
->saveAndSend();
}
public static function describeValidUsername() {
return 'Usernames must contain only numbers, letters, period, underscore '.
'and hyphen, and can not end with a period.';
}
public static function validateUsername($username) {
// NOTE: If you update this, make sure to update:
//
// - Remarkup rule for @mentions.
// - Routing rule for "/p/username/".
// - Unit tests, obviously.
// - describeValidUsername() method, above.
return (bool)preg_match('/^[a-zA-Z0-9._-]*[a-zA-Z0-9_-]$/', $username);
}
public static function getDefaultProfileImageURI() {
return celerity_get_resource_uri('/rsrc/image/avatar.png');
}
public function loadProfileImageURI() {
$src_phid = $this->getProfileImagePHID();
$file = id(new PhabricatorFile())->loadOneWhere('phid = %s', $src_phid);
if ($file) {
return $file->getBestURI();
}
return self::getDefaultProfileImageURI();
}
public function getFullName() {
return $this->getUsername().' ('.$this->getRealName().')';
}
public function __toString() {
return $this->getUsername();
}
public static function loadOneWithEmailAddress($address) {
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$address);
if (!$email) {
return null;
}
return id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$email->getUserPHID());
}
}
diff --git a/src/applications/people/storage/PhabricatorUserDAO.php b/src/applications/people/storage/PhabricatorUserDAO.php
index 6f2357d54c..64499055a0 100644
--- a/src/applications/people/storage/PhabricatorUserDAO.php
+++ b/src/applications/people/storage/PhabricatorUserDAO.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorUserDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'user';
}
}
diff --git a/src/applications/people/storage/PhabricatorUserEmail.php b/src/applications/people/storage/PhabricatorUserEmail.php
index 3daa8faf9a..889d8b3bd1 100644
--- a/src/applications/people/storage/PhabricatorUserEmail.php
+++ b/src/applications/people/storage/PhabricatorUserEmail.php
@@ -1,215 +1,199 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @task restrictions Domain Restrictions
* @task email Email About Email
*/
final class PhabricatorUserEmail extends PhabricatorUserDAO {
protected $userPHID;
protected $address;
protected $isVerified;
protected $isPrimary;
protected $verificationCode;
public function getVerificationURI() {
return '/emailverify/'.$this->getVerificationCode().'/';
}
public function save() {
if (!$this->verificationCode) {
$this->setVerificationCode(Filesystem::readRandomCharacters(24));
}
return parent::save();
}
/* -( Domain Restrictions )------------------------------------------------ */
/**
* @task restrictions
*/
public static function isAllowedAddress($address) {
$allowed_domains = PhabricatorEnv::getEnvConfig('auth.email-domains');
if (!$allowed_domains) {
return true;
}
$addr_obj = new PhutilEmailAddress($address);
$domain = $addr_obj->getDomainName();
if (!$domain) {
return false;
}
return in_array($domain, $allowed_domains);
}
/**
* @task restrictions
*/
public static function describeAllowedAddresses() {
$domains = PhabricatorEnv::getEnvConfig('auth.email-domains');
if (!$domains) {
return null;
}
if (count($domains) == 1) {
return 'Email address must be @'.head($domains);
} else {
return 'Email address must be at one of: '.
implode(', ', $domains);
}
}
/**
* Check if this install requires email verification.
*
* @return bool True if email addresses must be verified.
*
* @task restrictions
*/
public static function isEmailVerificationRequired() {
// NOTE: Configuring required email domains implies required verification.
return PhabricatorEnv::getEnvConfig('auth.require-email-verification') ||
PhabricatorEnv::getEnvConfig('auth.email-domains');
}
/* -( Email About Email )-------------------------------------------------- */
/**
* Send a verification email from $user to this address.
*
* @param PhabricatorUser The user sending the verification.
* @return this
* @task email
*/
public function sendVerificationEmail(PhabricatorUser $user) {
$username = $user->getUsername();
$address = $this->getAddress();
$link = PhabricatorEnv::getProductionURI($this->getVerificationURI());
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
$signature = null;
if (!$is_serious) {
$signature = <<<EOSIGNATURE
Get Well Soon,
Phabricator
EOSIGNATURE;
}
$body = <<<EOBODY
Hi {$username},
Please verify that you own this email address ({$address}) by clicking this
link:
{$link}
{$signature}
EOBODY;
id(new PhabricatorMetaMTAMail())
->addRawTos(array($address))
->setSubject('[Phabricator] Email Verification')
->setBody($body)
->setRelatedPHID($user->getPHID())
->saveAndSend();
return $this;
}
/**
* Send a notification email from $user to this address, informing the
* recipient that this is no longer their account's primary address.
*
* @param PhabricatorUser The user sending the notification.
* @param PhabricatorUserEmail New primary email address.
* @return this
* @task email
*/
public function sendOldPrimaryEmail(
PhabricatorUser $user,
PhabricatorUserEmail $new) {
$username = $user->getUsername();
$old_address = $this->getAddress();
$new_address = $new->getAddress();
$body = <<<EOBODY
Hi {$username},
This email address ({$old_address}) is no longer your primary email address.
Going forward, Phabricator will send all email to your new primary email
address ({$new_address}).
EOBODY;
id(new PhabricatorMetaMTAMail())
->addRawTos(array($old_address))
->setSubject('[Phabricator] Primary Address Changed')
->setBody($body)
->setFrom($user->getPHID())
->setRelatedPHID($user->getPHID())
->saveAndSend();
}
/**
* Send a notification email from $user to this address, informing the
* recipient that this is now their account's new primary email address.
*
* @param PhabricatorUser The user sending the verification.
* @return this
* @task email
*/
public function sendNewPrimaryEmail(PhabricatorUser $user) {
$username = $user->getUsername();
$new_address = $this->getAddress();
$body = <<<EOBODY
Hi {$username},
This is now your primary email address ({$new_address}). Going forward,
Phabricator will send all email here.
EOBODY;
id(new PhabricatorMetaMTAMail())
->addRawTos(array($new_address))
->setSubject('[Phabricator] Primary Address Changed')
->setBody($body)
->setFrom($user->getPHID())
->setRelatedPHID($user->getPHID())
->saveAndSend();
return $this;
}
}
diff --git a/src/applications/people/storage/PhabricatorUserLDAPInfo.php b/src/applications/people/storage/PhabricatorUserLDAPInfo.php
index a7c901fa23..2173c48b9c 100644
--- a/src/applications/people/storage/PhabricatorUserLDAPInfo.php
+++ b/src/applications/people/storage/PhabricatorUserLDAPInfo.php
@@ -1,22 +1,6 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorUserLDAPInfo extends PhabricatorUserDAO {
protected $userID;
protected $ldapUsername;
}
diff --git a/src/applications/people/storage/PhabricatorUserLog.php b/src/applications/people/storage/PhabricatorUserLog.php
index 238165de7c..9887f26911 100644
--- a/src/applications/people/storage/PhabricatorUserLog.php
+++ b/src/applications/people/storage/PhabricatorUserLog.php
@@ -1,119 +1,103 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorUserLog extends PhabricatorUserDAO {
const ACTION_LOGIN = 'login';
const ACTION_LOGOUT = 'logout';
const ACTION_LOGIN_FAILURE = 'login-fail';
const ACTION_RESET_PASSWORD = 'reset-pass';
const ACTION_CREATE = 'create';
const ACTION_EDIT = 'edit';
const ACTION_ADMIN = 'admin';
const ACTION_SYSTEM_AGENT = 'system-agent';
const ACTION_DISABLE = 'disable';
const ACTION_DELETE = 'delete';
const ACTION_CONDUIT_CERTIFICATE = 'conduit-cert';
const ACTION_CONDUIT_CERTIFICATE_FAILURE = 'conduit-cert-fail';
const ACTION_EMAIL_PRIMARY = 'email-primary';
const ACTION_EMAIL_REMOVE = 'email-remove';
const ACTION_EMAIL_ADD = 'email-add';
const ACTION_CHANGE_PASSWORD = 'change-password';
const ACTION_CHANGE_USERNAME = 'change-username';
protected $actorPHID;
protected $userPHID;
protected $action;
protected $oldValue;
protected $newValue;
protected $details = array();
protected $remoteAddr;
protected $session;
public static function newLog(
PhabricatorUser $actor = null,
PhabricatorUser $user = null,
$action) {
$log = new PhabricatorUserLog();
if ($actor) {
$log->setActorPHID($actor->getPHID());
}
if ($user) {
$log->setUserPHID($user->getPHID());
} else {
$log->setUserPHID('');
}
if ($action) {
$log->setAction($action);
}
return $log;
}
public static function loadRecentEventsFromThisIP($action, $timespan) {
return id(new PhabricatorUserLog())->loadAllWhere(
'action = %s AND remoteAddr = %s AND dateCreated > %d
ORDER BY dateCreated DESC',
$action,
idx($_SERVER, 'REMOTE_ADDR'),
time() - $timespan);
}
public function save() {
if (!$this->remoteAddr) {
$this->remoteAddr = idx($_SERVER, 'REMOTE_ADDR', '');
}
if (!$this->session) {
$this->setSession(idx($_COOKIE, 'phsid'));
}
$this->details['host'] = php_uname('n');
$this->details['user_agent'] = idx($_SERVER, 'HTTP_USER_AGENT');
return parent::save();
}
public function setSession($session) {
// Store the hash of the session, not the actual session key, so that
// seeing the logs doesn't compromise all the sessions which appear in
// them. This just prevents casual leaks, like in a screenshot.
if (strlen($session)) {
$this->session = PhabricatorHash::digest($session);
}
return $this;
}
public function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'oldValue' => self::SERIALIZATION_JSON,
'newValue' => self::SERIALIZATION_JSON,
'details' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
}
diff --git a/src/applications/people/storage/PhabricatorUserOAuthInfo.php b/src/applications/people/storage/PhabricatorUserOAuthInfo.php
index 6d84f0a703..591bbc2641 100644
--- a/src/applications/people/storage/PhabricatorUserOAuthInfo.php
+++ b/src/applications/people/storage/PhabricatorUserOAuthInfo.php
@@ -1,60 +1,44 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorUserOAuthInfo extends PhabricatorUserDAO {
const TOKEN_STATUS_NONE = 'none';
const TOKEN_STATUS_GOOD = 'good';
const TOKEN_STATUS_FAIL = 'fail';
const TOKEN_STATUS_EXPIRED = 'xpyr';
protected $userID;
protected $oauthProvider;
protected $oauthUID;
protected $accountURI;
protected $accountName;
protected $token;
protected $tokenExpires;
protected $tokenScope;
protected $tokenStatus;
public function getTokenStatus() {
if (!$this->token) {
return self::TOKEN_STATUS_NONE;
}
if ($this->tokenExpires && $this->tokenExpires <= time()) {
return self::TOKEN_STATUS_EXPIRED;
}
return $this->tokenStatus;
}
public static function getReadableTokenStatus($status) {
static $map = array(
self::TOKEN_STATUS_NONE => 'No Token',
self::TOKEN_STATUS_GOOD => 'Token Good',
self::TOKEN_STATUS_FAIL => 'Token Failed',
self::TOKEN_STATUS_EXPIRED => 'Token Expired',
);
return idx($map, $status, 'Unknown');
}
}
diff --git a/src/applications/people/storage/PhabricatorUserProfile.php b/src/applications/people/storage/PhabricatorUserProfile.php
index 757f0e27ea..52f2257856 100644
--- a/src/applications/people/storage/PhabricatorUserProfile.php
+++ b/src/applications/people/storage/PhabricatorUserProfile.php
@@ -1,26 +1,10 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorUserProfile extends PhabricatorUserDAO {
protected $userPHID;
protected $title;
protected $blurb;
protected $profileImagePHID;
}
diff --git a/src/applications/people/storage/PhabricatorUserStatus.php b/src/applications/people/storage/PhabricatorUserStatus.php
index 14cfee4477..af765da39f 100644
--- a/src/applications/people/storage/PhabricatorUserStatus.php
+++ b/src/applications/people/storage/PhabricatorUserStatus.php
@@ -1,97 +1,81 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorUserStatus extends PhabricatorUserDAO {
protected $userPHID;
protected $dateFrom;
protected $dateTo;
protected $status;
protected $description;
const STATUS_AWAY = 1;
const STATUS_SPORADIC = 2;
public function getStatusOptions() {
return array(
self::STATUS_AWAY => pht('Away'),
self::STATUS_SPORADIC => pht('Sporadic'),
);
}
public function getTextStatus() {
$options = $this->getStatusOptions();
return $options[$this->status];
}
public function getTerseSummary(PhabricatorUser $viewer) {
$until = phabricator_date($this->dateTo, $viewer);
if ($this->status == PhabricatorUserStatus::STATUS_SPORADIC) {
return 'Sporadic until '.$until;
} else {
return 'Away until '.$until;
}
}
public function setTextStatus($status) {
$statuses = array_flip($this->getStatusOptions());
return $this->setStatus($statuses[$status]);
}
public function loadCurrentStatuses($user_phids) {
$statuses = $this->loadAllWhere(
'userPHID IN (%Ls) AND UNIX_TIMESTAMP() BETWEEN dateFrom AND dateTo',
$user_phids);
return mpull($statuses, null, 'getUserPHID');
}
/**
* Validates data and throws exceptions for non-sensical status
* windows and attempts to create an overlapping status.
*/
public function save() {
if ($this->getDateTo() <= $this->getDateFrom()) {
throw new PhabricatorUserStatusInvalidEpochException();
}
$this->openTransaction();
$this->beginWriteLocking();
if ($this->shouldInsertWhenSaved()) {
$overlap = $this->loadAllWhere(
'userPHID = %s AND dateFrom < %d AND dateTo > %d',
$this->getUserPHID(),
$this->getDateTo(),
$this->getDateFrom());
if ($overlap) {
$this->endWriteLocking();
$this->killTransaction();
throw new PhabricatorUserStatusOverlapException();
}
}
parent::save();
$this->endWriteLocking();
return $this->saveTransaction();
}
}
diff --git a/src/applications/people/storage/__tests__/PhabricatorUserTestCase.php b/src/applications/people/storage/__tests__/PhabricatorUserTestCase.php
index 09c8437fee..3aebe4fe59 100644
--- a/src/applications/people/storage/__tests__/PhabricatorUserTestCase.php
+++ b/src/applications/people/storage/__tests__/PhabricatorUserTestCase.php
@@ -1,65 +1,49 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorUserTestCase extends PhabricatorTestCase {
public function testUsernameValidation() {
$map = array(
'alincoln' => true,
'alincoln69' => true,
'hd3' => true,
'Alincoln' => true,
'a.lincoln' => true,
'alincoln!' => false,
'' => false,
// These are silly, but permitted.
'7' => true,
'0' => true,
'____' => true,
'-' => true,
// These are not permitted because they make capturing @mentions
// ambiguous.
'joe.' => false,
// We can never allow these because they invalidate usernames as tokens
// in commit messages ("Reviewers: alincoln, usgrant"), or as parameters
// in URIs ("/p/alincoln/", "?user=alincoln"), or make them unsafe in
// HTML. Theoretically we escape all the HTML/URI stuff, but these
// restrictions make attacks more difficult and are generally reasonable,
// since usernames like "<^, ,^>" don't seem very important to support.
'<script>' => false,
'a lincoln' => false,
' alincoln' => false,
'alincoln ' => false,
'a,lincoln' => false,
'a&lincoln' => false,
'a/lincoln' => false,
);
foreach ($map as $name => $expect) {
$this->assertEqual(
$expect,
PhabricatorUser::validateUsername($name),
"Validity of '{$name}'.");
}
}
}
diff --git a/src/applications/phame/application/PhabricatorApplicationPhame.php b/src/applications/phame/application/PhabricatorApplicationPhame.php
index cd0a684c7c..28ddb1fff5 100644
--- a/src/applications/phame/application/PhabricatorApplicationPhame.php
+++ b/src/applications/phame/application/PhabricatorApplicationPhame.php
@@ -1,81 +1,65 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationPhame extends PhabricatorApplication {
public function getBaseURI() {
return '/phame/';
}
public function getAutospriteName() {
return 'phame';
}
public function getShortDescription() {
return 'Blog';
}
public function getTitleGlyph() {
return "\xe2\x9c\xa9";
}
public function getHelpURI() {
return PhabricatorEnv::getDoclink('article/Phame_User_Guide.html');
}
public function getApplicationGroup() {
return self::GROUP_COMMUNICATION;
}
public function getRoutes() {
return array(
'/phame/' => array(
'' => 'PhamePostListController',
'r/(?P<id>\d+)/(?P<hash>[^/]+)/(?P<name>.*)'
=> 'PhameResourceController',
'live/(?P<id>[^/]+)/(?P<more>.*)' => 'PhameBlogLiveController',
'post/' => array(
'(?:(?P<filter>draft|all)/)?' => 'PhamePostListController',
'blogger/(?P<bloggername>[\w\.-_]+)/' => 'PhamePostListController',
'delete/(?P<id>[^/]+)/' => 'PhamePostDeleteController',
'edit/(?:(?P<id>[^/]+)/)?' => 'PhamePostEditController',
'view/(?P<id>\d+)/' => 'PhamePostViewController',
'publish/(?P<id>\d+)/' => 'PhamePostPublishController',
'unpublish/(?P<id>\d+)/' => 'PhamePostUnpublishController',
'notlive/(?P<id>\d+)/' => 'PhamePostNotLiveController',
'preview/' => 'PhamePostPreviewController',
'framed/(?P<id>\d+)/' => 'PhamePostFramedController',
'new/' => 'PhamePostNewController',
'move/(?P<id>\d+)/' => 'PhamePostNewController'
),
'blog/' => array(
'(?:(?P<filter>user|all)/)?' => 'PhameBlogListController',
'delete/(?P<id>[^/]+)/' => 'PhameBlogDeleteController',
'edit/(?P<id>[^/]+)/' => 'PhameBlogEditController',
'view/(?P<id>[^/]+)/' => 'PhameBlogViewController',
'new/' => 'PhameBlogEditController',
),
'posts/' => array(
'(?P<bloggername>\w+)/(?P<phametitle>.+/)'
=> 'PhamePostViewController',
),
),
);
}
}
diff --git a/src/applications/phame/controller/PhameController.php b/src/applications/phame/controller/PhameController.php
index 2ce962e16b..70679c17f3 100644
--- a/src/applications/phame/controller/PhameController.php
+++ b/src/applications/phame/controller/PhameController.php
@@ -1,89 +1,73 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phame
*/
abstract class PhameController extends PhabricatorController {
protected function renderSideNavFilterView() {
$base_uri = new PhutilURI($this->getApplicationURI());
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI($base_uri);
$nav->addLabel('Create');
$nav->addFilter('post/new', 'New Post');
$nav->addFilter('blog/new', 'New Blog');
$nav->addSpacer();
$nav->addLabel('Posts');
$nav->addFilter('post/draft', 'My Drafts');
$nav->addFilter('post', 'My Posts');
$nav->addFilter('post/all', 'All Posts');
$nav->addSpacer();
$nav->addLabel('Blogs');
$nav->addFilter('blog/user', 'Joinable Blogs');
$nav->addFilter('blog/all', 'All Blogs');
$nav->selectFilter(null);
return $nav;
}
protected function renderPostList(
array $posts,
PhabricatorUser $user,
$nodata) {
assert_instances_of($posts, 'PhamePost');
$list = id(new PhabricatorObjectItemListView())
->setNoDataString($nodata);
foreach ($posts as $post) {
$item = id(new PhabricatorObjectItemView())
->setHeader($post->getTitle())
->setHref($this->getApplicationURI('post/view/'.$post->getID().'/'))
->addDetail(
pht('Blogger'),
$this->getHandle($post->getBloggerPHID())->renderLink())
->addDetail(
pht('Blog'),
$post->getBlog()
? $this->getHandle($post->getBlog()->getPHID())->renderLink()
: '-');
if ($post->isDraft()) {
$item->addAttribute(pht('Draft'));
} else {
$date_published = phabricator_datetime(
$post->getDatePublished(),
$user);
$item->addAttribute(pht('Published on %s', $date_published));
}
$list->addItem($item);
}
return $list;
}
}
diff --git a/src/applications/phame/controller/PhameResourceController.php b/src/applications/phame/controller/PhameResourceController.php
index f933adf759..dd22d3066c 100644
--- a/src/applications/phame/controller/PhameResourceController.php
+++ b/src/applications/phame/controller/PhameResourceController.php
@@ -1,81 +1,65 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phame
*/
final class PhameResourceController extends CelerityResourceController {
private $id;
private $hash;
private $name;
private $root;
protected function getRootDirectory() {
return $this->root;
}
public function willProcessRequest(array $data) {
$this->id = $data['id'];
$this->hash = $data['hash'];
$this->name = $data['name'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
// We require a visible blog associated with a given skin to serve
// resources, so you can't go fishing around where you shouldn't be.
$blog = id(new PhameBlogQuery())
->setViewer($user)
->withIDs(array($this->id))
->executeOne();
if (!$blog) {
return new Aphront404Response();
}
$skin = $blog->getSkinRenderer($request);
$spec = $skin->getSpecification();
$this->root = $spec->getRootDirectory().DIRECTORY_SEPARATOR;
return $this->serveResource($this->name, $package_hash = null);
}
protected function buildResourceTransformer() {
$xformer = new CelerityResourceTransformer();
$xformer->setMinify(false);
$xformer->setTranslateURICallback(array($this, 'translateResourceURI'));
return $xformer;
}
public function translateResourceURI(array $matches) {
$uri = trim($matches[1], "'\" \r\t\n");
if (Filesystem::pathExists($this->root.$uri)) {
$hash = filemtime($this->root.$uri);
} else {
$hash = '-';
}
$uri = '/phame/r/'.$this->id.'/'.$hash.'/'.$uri;
return 'url('.$uri.')';
}
}
diff --git a/src/applications/phame/controller/blog/PhameBlogDeleteController.php b/src/applications/phame/controller/blog/PhameBlogDeleteController.php
index 2390e11970..eb6f4695a8 100644
--- a/src/applications/phame/controller/blog/PhameBlogDeleteController.php
+++ b/src/applications/phame/controller/blog/PhameBlogDeleteController.php
@@ -1,66 +1,50 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phame
*/
final class PhameBlogDeleteController extends PhameController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$blog = id(new PhameBlogQuery())
->setViewer($user)
->withIDs(array($this->id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$blog) {
return new Aphront404Response();
}
if ($request->isFormPost()) {
$blog->delete();
return id(new AphrontRedirectResponse())
->setURI($this->getApplicationURI());
}
$cancel_uri = $this->getApplicationURI('/blog/view/'.$blog->getID().'/');
$dialog = id(new AphrontDialogView())
->setUser($user)
->setTitle(pht('Delete Blog?'))
->appendChild(
pht(
'Really delete the blog "%s"? It will be gone forever.',
phutil_escape_html($blog->getName())))
->addSubmitButton(pht('Delete'))
->addCancelButton($cancel_uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/phame/controller/blog/PhameBlogEditController.php b/src/applications/phame/controller/blog/PhameBlogEditController.php
index f8e4017340..4784b989ad 100644
--- a/src/applications/phame/controller/blog/PhameBlogEditController.php
+++ b/src/applications/phame/controller/blog/PhameBlogEditController.php
@@ -1,211 +1,195 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phame
*/
final class PhameBlogEditController
extends PhameController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
if ($this->id) {
$blog = id(new PhameBlogQuery())
->setViewer($user)
->withIDs(array($this->id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_EDIT
))
->executeOne();
if (!$blog) {
return new Aphront404Response();
}
$submit_button = pht('Save Changes');
$page_title = pht('Edit Blog');
$cancel_uri = $this->getApplicationURI('blog/view/'.$blog->getID().'/');
} else {
$blog = id(new PhameBlog())
->setCreatorPHID($user->getPHID());
$blog->setViewPolicy(PhabricatorPolicies::POLICY_USER);
$blog->setEditPolicy(PhabricatorPolicies::POLICY_USER);
$blog->setJoinPolicy(PhabricatorPolicies::POLICY_USER);
$submit_button = pht('Create Blog');
$page_title = pht('Create Blog');
$cancel_uri = $this->getApplicationURI();
}
$e_name = true;
$e_custom_domain = null;
$errors = array();
if ($request->isFormPost()) {
$name = $request->getStr('name');
$description = $request->getStr('description');
$custom_domain = $request->getStr('custom_domain');
$skin = $request->getStr('skin');
if (empty($name)) {
$errors[] = 'You must give the blog a name.';
$e_name = 'Required';
} else {
$e_name = null;
}
$blog->setName($name);
$blog->setDescription($description);
$blog->setDomain(nonempty($custom_domain, null));
$blog->setSkin($skin);
if (!empty($custom_domain)) {
$error = $blog->validateCustomDomain($custom_domain);
if ($error) {
$errors[] = $error;
$e_custom_domain = 'Invalid';
}
}
$blog->setViewPolicy($request->getStr('can_view'));
$blog->setEditPolicy($request->getStr('can_edit'));
$blog->setJoinPolicy($request->getStr('can_join'));
// Don't let users remove their ability to edit blogs.
PhabricatorPolicyFilter::mustRetainCapability(
$user,
$blog,
PhabricatorPolicyCapability::CAN_EDIT);
if (!$errors) {
try {
$blog->save();
return id(new AphrontRedirectResponse())
->setURI($this->getApplicationURI('blog/view/'.$blog->getID().'/'));
} catch (AphrontQueryDuplicateKeyException $ex) {
$errors[] = 'Domain must be unique.';
$e_custom_domain = 'Not Unique';
}
}
}
$policies = id(new PhabricatorPolicyQuery())
->setViewer($user)
->setObject($blog)
->execute();
$skins = PhameSkinSpecification::loadAllSkinSpecifications();
$skins = mpull($skins, 'getName');
$form = id(new AphrontFormView())
->setUser($user)
->setFlexible(true)
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Name')
->setName('name')
->setValue($blog->getName())
->setID('blog-name')
->setError($e_name)
)
->appendChild(
id(new PhabricatorRemarkupControl())
->setLabel('Description')
->setName('description')
->setValue($blog->getDescription())
->setID('blog-description')
)
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
->setPolicyObject($blog)
->setPolicies($policies)
->setName('can_view'))
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
->setPolicyObject($blog)
->setPolicies($policies)
->setName('can_edit'))
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setCapability(PhabricatorPolicyCapability::CAN_JOIN)
->setPolicyObject($blog)
->setPolicies($policies)
->setName('can_join'))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Custom Domain')
->setName('custom_domain')
->setValue($blog->getDomain())
->setCaption('Must include at least one dot (.), e.g. '.
'blog.example.com')
->setError($e_custom_domain)
)
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Skin')
->setName('skin')
->setValue($blog->getSkin())
->setOptions($skins)
)
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($cancel_uri)
->setValue($submit_button)
);
if ($errors) {
$error_view = id(new AphrontErrorView())
->setTitle('Form Errors')
->setErrors($errors);
} else {
$error_view = null;
}
$header = id(new PhabricatorHeaderView())
->setHeader($page_title);
$nav = $this->renderSideNavFilterView();
$nav->selectFilter($this->id ? null : 'blog/new');
$nav->appendChild(
array(
$header,
$error_view,
$form,
));
return $this->buildApplicationPage(
$nav,
array(
'title' => $page_title,
));
}
}
diff --git a/src/applications/phame/controller/blog/PhameBlogListController.php b/src/applications/phame/controller/blog/PhameBlogListController.php
index d066f69666..5ca4defd7e 100644
--- a/src/applications/phame/controller/blog/PhameBlogListController.php
+++ b/src/applications/phame/controller/blog/PhameBlogListController.php
@@ -1,105 +1,89 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phame
*/
final class PhameBlogListController extends PhameController {
private $filter;
public function willProcessRequest(array $data) {
$this->filter = idx($data, 'filter');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$nav = $this->renderSideNavFilterView(null);
$filter = $nav->selectFilter('blog/'.$this->filter, 'blog/user');
$query = id(new PhameBlogQuery())
->setViewer($user);
switch ($filter) {
case 'blog/all':
$title = pht('All Blogs');
$nodata = pht('No blogs have been created.');
break;
case 'blog/user':
$title = pht('Joinable Blogs');
$nodata = pht('There are no blogs you can contribute to.');
$query->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_JOIN,
));
break;
default:
throw new Exception("Unknown filter '{$filter}'!");
}
$pager = id(new AphrontPagerView())
->setURI($request->getRequestURI(), 'offset')
->setOffset($request->getInt('offset'));
$blogs = $query->executeWithOffsetPager($pager);
$header = id(new PhabricatorHeaderView())
->setHeader($title);
$blog_list = $this->renderBlogList($blogs, $user, $nodata);
$blog_list->setPager($pager);
$nav->appendChild(
array(
$header,
$blog_list,
));
return $this->buildApplicationPage(
$nav,
array(
'title' => $title,
'device' => true,
));
}
private function renderBlogList(
array $blogs,
PhabricatorUser $user,
$nodata) {
$view = new PhabricatorObjectItemListView();
$view->setNoDataString($nodata);
foreach ($blogs as $blog) {
$item = id(new PhabricatorObjectItemView())
->setHeader($blog->getName())
->setHref($this->getApplicationURI('blog/view/'.$blog->getID().'/'))
->addDetail(
pht('Custom Domain'),
phutil_escape_html($blog->getDomain()));
$view->addItem($item);
}
return $view;
}
}
diff --git a/src/applications/phame/controller/blog/PhameBlogLiveController.php b/src/applications/phame/controller/blog/PhameBlogLiveController.php
index 02e09b7344..40f0bfe3bd 100644
--- a/src/applications/phame/controller/blog/PhameBlogLiveController.php
+++ b/src/applications/phame/controller/blog/PhameBlogLiveController.php
@@ -1,72 +1,56 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phame
*/
final class PhameBlogLiveController extends PhameController {
private $id;
private $more;
public function shouldAllowPublic() {
return true;
}
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
$this->more = idx($data, 'more', '');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$blog = id(new PhameBlogQuery())
->setViewer($user)
->withIDs(array($this->id))
->executeOne();
if (!$blog) {
return new Aphront404Response();
}
if ($blog->getDomain() && ($request->getHost() != $blog->getDomain())) {
return id(new AphrontRedirectResponse())
->setURI('http://'.$blog->getDomain().'/'.$this->more);
}
$phame_request = clone $request;
$phame_request->setPath('/'.ltrim($this->more, '/'));
if ($blog->getDomain()) {
$uri = new PhutilURI('http://'.$blog->getDomain().'/');
} else {
$uri = '/phame/live/'.$blog->getID().'/';
$uri = PhabricatorEnv::getURI($uri);
}
$skin = $blog->getSkinRenderer($phame_request);
$skin
->setBlog($blog)
->setBaseURI((string)$uri);
$skin->willProcessRequest(array());
return $skin->processRequest();
}
}
diff --git a/src/applications/phame/controller/blog/PhameBlogViewController.php b/src/applications/phame/controller/blog/PhameBlogViewController.php
index 8475cee454..15fc82ad2a 100644
--- a/src/applications/phame/controller/blog/PhameBlogViewController.php
+++ b/src/applications/phame/controller/blog/PhameBlogViewController.php
@@ -1,172 +1,156 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phame
*/
final class PhameBlogViewController extends PhameController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$blog = id(new PhameBlogQuery())
->setViewer($user)
->withIDs(array($this->id))
->executeOne();
if (!$blog) {
return new Aphront404Response();
}
$pager = id(new AphrontCursorPagerView())
->readFromRequest($request);
$posts = id(new PhamePostQuery())
->setViewer($user)
->withBlogPHIDs(array($blog->getPHID()))
->executeWithCursorPager($pager);
$nav = $this->renderSideNavFilterView(null);
$header = id(new PhabricatorHeaderView())
->setHeader($blog->getName());
$handle_phids = array_merge(
mpull($posts, 'getBloggerPHID'),
mpull($posts, 'getBlogPHID'));
$this->loadHandles($handle_phids);
$actions = $this->renderActions($blog, $user);
$properties = $this->renderProperties($blog, $user);
$post_list = $this->renderPostList(
$posts,
$user,
pht('This blog has no visible posts.'));
$nav->appendChild(
array(
$header,
$actions,
$properties,
$post_list,
));
return $this->buildApplicationPage(
$nav,
array(
'device' => true,
'title' => $blog->getName(),
));
}
private function renderProperties(PhameBlog $blog, PhabricatorUser $user) {
$properties = new PhabricatorPropertyListView();
$properties->addProperty(
pht('Skin'),
phutil_escape_html($blog->getSkin()));
$properties->addProperty(
pht('Domain'),
phutil_escape_html($blog->getDomain()));
$descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions(
$user,
$blog);
$properties->addProperty(
pht('Visible To'),
$descriptions[PhabricatorPolicyCapability::CAN_VIEW]);
$properties->addProperty(
pht('Editable By'),
$descriptions[PhabricatorPolicyCapability::CAN_EDIT]);
$properties->addProperty(
pht('Joinable By'),
$descriptions[PhabricatorPolicyCapability::CAN_JOIN]);
$engine = id(new PhabricatorMarkupEngine())
->setViewer($user)
->addObject($blog, PhameBlog::MARKUP_FIELD_DESCRIPTION)
->process();
$properties->addTextContent(
'<div class="phabricator-remarkup">'.
$engine->getOutput($blog, PhameBlog::MARKUP_FIELD_DESCRIPTION).
'</div>');
return $properties;
}
private function renderActions(PhameBlog $blog, PhabricatorUser $user) {
$actions = id(new PhabricatorActionListView())
->setObject($blog)
->setUser($user);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$user,
$blog,
PhabricatorPolicyCapability::CAN_EDIT);
$can_join = PhabricatorPolicyFilter::hasCapability(
$user,
$blog,
PhabricatorPolicyCapability::CAN_JOIN);
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('new')
->setHref($this->getApplicationURI('post/edit/?blog='.$blog->getID()))
->setName(pht('Write Post'))
->setDisabled(!$can_join)
->setWorkflow(!$can_join));
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('world')
->setHref($this->getApplicationURI('live/'.$blog->getID().'/'))
->setName(pht('View Live')));
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('edit')
->setHref($this->getApplicationURI('blog/edit/'.$blog->getID().'/'))
->setName('Edit Blog')
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('delete')
->setHref($this->getApplicationURI('blog/delete/'.$blog->getID().'/'))
->setName('Delete Blog')
->setDisabled(!$can_edit)
->setWorkflow(true));
return $actions;
}
}
diff --git a/src/applications/phame/controller/post/PhamePostDeleteController.php b/src/applications/phame/controller/post/PhamePostDeleteController.php
index c54006750a..26719ff61c 100644
--- a/src/applications/phame/controller/post/PhamePostDeleteController.php
+++ b/src/applications/phame/controller/post/PhamePostDeleteController.php
@@ -1,66 +1,50 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phame
*/
final class PhamePostDeleteController extends PhameController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$post = id(new PhamePostQuery())
->setViewer($user)
->withIDs(array($this->id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$post) {
return new Aphront404Response();
}
if ($request->isFormPost()) {
$post->delete();
return id(new AphrontRedirectResponse())
->setURI('/phame/post/');
}
$cancel_uri = $this->getApplicationURI('/post/view/'.$post->getID().'/');
$dialog = id(new AphrontDialogView())
->setUser($user)
->setTitle(pht('Delete Post?'))
->appendChild(
pht(
'Really delete the post "%s"? It will be gone forever.',
phutil_escape_html($post->getTitle())))
->addSubmitButton(pht('Delete'))
->addCancelButton($cancel_uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/phame/controller/post/PhamePostEditController.php b/src/applications/phame/controller/post/PhamePostEditController.php
index 8903d4a6b6..e5906a4e6f 100644
--- a/src/applications/phame/controller/post/PhamePostEditController.php
+++ b/src/applications/phame/controller/post/PhamePostEditController.php
@@ -1,215 +1,199 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phame
*/
final class PhamePostEditController
extends PhameController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
if ($this->id) {
$post = id(new PhamePostQuery())
->setViewer($user)
->withIDs(array($this->id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$post) {
return new Aphront404Response();
}
$cancel_uri = $this->getApplicationURI('/post/view/'.$this->id.'/');
$submit_button = pht('Save Changes');
$page_title = pht('Edit Post');
} else {
$blog = id(new PhameBlogQuery())
->setViewer($user)
->withIDs(array($request->getInt('blog')))
->executeOne();
if (!$blog) {
return new Aphront404Response();
}
$post = id(new PhamePost())
->setBloggerPHID($user->getPHID())
->setBlogPHID($blog->getPHID())
->setBlog($blog)
->setDatePublished(0)
->setVisibility(PhamePost::VISIBILITY_DRAFT);
$cancel_uri = $this->getApplicationURI('/blog/view/'.$blog->getID().'/');
$submit_button = pht('Save Draft');
$page_title = pht('Create Post');
}
$e_phame_title = null;
$e_title = true;
$errors = array();
if ($request->isFormPost()) {
$comments = $request->getStr('comments_widget');
$data = array('comments_widget' => $comments);
$phame_title = $request->getStr('phame_title');
$phame_title = PhabricatorSlug::normalize($phame_title);
$title = $request->getStr('title');
$post->setTitle($title);
$post->setPhameTitle($phame_title);
$post->setBody($request->getStr('body'));
$post->setConfigData($data);
if ($phame_title == '/') {
$errors[] = 'Phame title must be nonempty.';
$e_phame_title = 'Required';
}
if (!strlen($title)) {
$errors[] = 'Title must be nonempty.';
$e_title = 'Required';
} else {
$e_title = null;
}
if (!$errors) {
try {
$post->save();
$uri = $this->getApplicationURI('/post/view/'.$post->getID().'/');
return id(new AphrontRedirectResponse())->setURI($uri);
} catch (AphrontQueryDuplicateKeyException $e) {
$e_phame_title = 'Not Unique';
$errors[] = 'Another post already uses this slug. '.
'Each post must have a unique slug.';
}
}
}
$handle = PhabricatorObjectHandleData::loadOneHandle(
$post->getBlogPHID(),
$user);
$form = id(new AphrontFormView())
->setUser($user)
->setFlexible(true)
->addHiddenInput('blog', $request->getInt('blog'))
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel('Blog')
->setValue($handle->renderLink()))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Title')
->setName('title')
->setValue($post->getTitle())
->setID('post-title')
->setError($e_title)
)
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Phame Title')
->setName('phame_title')
->setValue(rtrim($post->getPhameTitle(), '/'))
->setID('post-phame-title')
->setCaption('Up to 64 alphanumeric characters '.
'with underscores for spaces. '.
'Formatting is enforced.')
->setError($e_phame_title)
)
->appendChild(
id(new PhabricatorRemarkupControl())
->setLabel('Body')
->setName('body')
->setValue($post->getBody())
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)
->setID('post-body')
)
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Comments Widget')
->setName('comments_widget')
->setvalue($post->getCommentsWidget())
->setOptions($post->getCommentsWidgetOptionsForSelect())
)
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($cancel_uri)
->setValue($submit_button)
);
$preview_panel =
'<div class="aphront-panel-preview">
<div class="phame-post-preview-header">
Post Preview
</div>
<div id="post-preview">
<div class="aphront-panel-preview-loading-text">
Loading preview...
</div>
</div>
</div>';
require_celerity_resource('phame-css');
Javelin::initBehavior(
'phame-post-preview',
array(
'preview' => 'post-preview',
'body' => 'post-body',
'title' => 'post-title',
'phame_title' => 'post-phame-title',
'uri' => '/phame/post/preview/',
));
$header = id(new PhabricatorHeaderView())->setHeader($page_title);
if ($errors) {
$error_view = id(new AphrontErrorView())
->setTitle('Errors saving post.')
->setErrors($errors);
} else {
$error_view = null;
}
$nav = $this->renderSideNavFilterView(null);
$nav->appendChild(
array(
$header,
$error_view,
$form,
$preview_panel,
));
return $this->buildApplicationPage(
$nav,
array(
'title' => $page_title,
'device' => true,
));
}
}
diff --git a/src/applications/phame/controller/post/PhamePostFramedController.php b/src/applications/phame/controller/post/PhamePostFramedController.php
index f73a87c350..6b7569749c 100644
--- a/src/applications/phame/controller/post/PhamePostFramedController.php
+++ b/src/applications/phame/controller/post/PhamePostFramedController.php
@@ -1,63 +1,47 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phame
*/
final class PhamePostFramedController extends PhameController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$post = id(new PhamePostQuery())
->setViewer($user)
->withIDs(array($this->id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$post) {
return new Aphront404Response();
}
$blog = $post->getBlog();
$phame_request = $request->setPath('/post/'.$post->getPhameTitle());
$skin = $post->getBlog()->getSkinRenderer($phame_request);
$uri = clone $request->getRequestURI();
$uri->setPath('/phame/live/'.$blog->getID().'/');
$skin
->setPreview(true)
->setBlog($post->getBlog())
->setBaseURI((string)$uri);
$response = $skin->processRequest();
$response->setFrameable(true);
return $response;
}
}
diff --git a/src/applications/phame/controller/post/PhamePostListController.php b/src/applications/phame/controller/post/PhamePostListController.php
index ed437ebb5d..0d2d3e9f7b 100644
--- a/src/applications/phame/controller/post/PhamePostListController.php
+++ b/src/applications/phame/controller/post/PhamePostListController.php
@@ -1,109 +1,93 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phame
*/
final class PhamePostListController extends PhameController {
private $bloggername;
private $filter;
public function willProcessRequest(array $data) {
$this->filter = idx($data, 'filter', 'blogger');
$this->bloggername = idx($data, 'bloggername');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$query = id(new PhamePostQuery())
->setViewer($user);
$nav = $this->renderSideNavFilterView();
switch ($this->filter) {
case 'draft':
$query->withBloggerPHIDs(array($user->getPHID()));
$query->withVisibility(PhamePost::VISIBILITY_DRAFT);
$nodata = pht('You have no unpublished drafts.');
$title = pht('Unpublished Drafts');
$nav->selectFilter('post/draft');
break;
case 'blogger':
if ($this->bloggername) {
$blogger = id(new PhabricatorUser())->loadOneWhere(
'username = %s',
$this->bloggername);
if (!$blogger) {
return new Aphront404Response();
}
} else {
$blogger = $user;
}
$query->withBloggerPHIDs(array($blogger->getPHID()));
if ($blogger->getPHID() == $user->getPHID()) {
$nav->selectFilter('post');
$nodata = pht('You have not written any posts.');
} else {
$nodata = pht('%s has not written any posts.', $blogger);
}
$title = pht('Posts By %s', $blogger);
break;
case 'all':
$nodata = pht('There are no visible posts.');
$title = pht('Posts');
$nav->selectFilter('post/all');
break;
default:
throw new Exception("Unknown filter '{$this->filter}'!");
}
$pager = id(new AphrontCursorPagerView())
->readFromRequest($request);
$posts = $query->executeWithCursorPager($pager);
$handle_phids = array_merge(
mpull($posts, 'getBloggerPHID'),
mpull($posts, 'getBlogPHID'));
$this->loadHandles($handle_phids);
$header = id(new PhabricatorHeaderView())
->setHeader($title);
$post_list = $this->renderPostList($posts, $user, $nodata);
$nav->appendChild(
array(
$header,
$post_list,
));
return $this->buildApplicationPage(
$nav,
array(
'title' => $title,
'device' => true,
));
}
}
diff --git a/src/applications/phame/controller/post/PhamePostNewController.php b/src/applications/phame/controller/post/PhamePostNewController.php
index 60b7056e67..592fcf7674 100644
--- a/src/applications/phame/controller/post/PhamePostNewController.php
+++ b/src/applications/phame/controller/post/PhamePostNewController.php
@@ -1,140 +1,124 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phame
*/
final class PhamePostNewController extends PhameController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$post = null;
$view_uri = null;
if ($this->id) {
$post = id(new PhamePostQuery())
->setViewer($user)
->withIDs(array($this->id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$post) {
return new Aphront404Response();
}
$view_uri = '/post/view/'.$post->getID().'/';
$view_uri = $this->getApplicationURI($view_uri);
if ($request->isFormPost()) {
$blog = id(new PhameBlogQuery())
->setViewer($user)
->withIDs(array($request->getInt('blog')))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_JOIN,
))
->executeOne();
if ($blog) {
$post->setBlogPHID($blog->getPHID());
$post->save();
return id(new AphrontRedirectResponse())->setURI($view_uri);
}
}
$title = pht('Move Post');
} else {
$title = pht('Create Post');
}
$blogs = id(new PhameBlogQuery())
->setViewer($user)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_JOIN,
))
->execute();
$nav = $this->renderSideNavFilterView();
$nav->selectFilter('post/new');
$nav->appendChild(
id(new PhabricatorHeaderView())->setHeader($title));
if (!$blogs) {
$notification = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_NODATA)
->appendChild(
pht('You do not have permission to join any blogs. Create a blog '.
'first, then you can post to it.'));
$nav->appendChild($notification);
} else {
$options = mpull($blogs, 'getName', 'getID');
asort($options);
$selected_value = null;
if ($post && $post->getBlog()) {
$selected_value = $post->getBlog()->getID();
}
$form = id(new AphrontFormView())
->setUser($user)
->setFlexible(true)
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Blog')
->setName('blog')
->setOptions($options)
->setValue($selected_value));
if ($post) {
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Move Post'))
->addCancelButton($view_uri));
} else {
$form
->setAction($this->getApplicationURI('post/edit/'))
->setMethod('GET')
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Continue')));
}
$nav->appendChild($form);
}
return $this->buildApplicationPage(
$nav,
array(
'title' => $title,
'device' => true,
));
}
}
diff --git a/src/applications/phame/controller/post/PhamePostNotLiveController.php b/src/applications/phame/controller/post/PhamePostNotLiveController.php
index 8d5cb3715b..a255d79d52 100644
--- a/src/applications/phame/controller/post/PhamePostNotLiveController.php
+++ b/src/applications/phame/controller/post/PhamePostNotLiveController.php
@@ -1,79 +1,63 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phame
*/
final class PhamePostNotLiveController extends PhameController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$post = id(new PhamePostQuery())
->setViewer($user)
->withIDs(array($this->id))
->executeOne();
if (!$post) {
return new Aphront404Response();
}
$reasons = array();
if (!$post->getBlog()) {
$reasons[] =
'<p>'.pht('You can not view the live version of this post because it '.
'is not associated with a blog. Move the post to a blog in order to '.
'view it live.').'</p>';
}
if ($post->isDraft()) {
$reasons[] =
'<p>'.pht('You can not view the live version of this post because it '.
'is still a draft. Use "Preview/Publish" to publish the post.').'</p>';
}
if ($reasons) {
$cancel_uri = $this->getApplicationURI('/post/view/'.$post->getID().'/');
$dialog = id(new AphrontDialogView())
->setUser($user)
->setTitle(pht('Post Not Live'))
->addCancelButton($cancel_uri);
foreach ($reasons as $reason) {
$dialog->appendChild($reason);
}
return id(new AphrontDialogResponse())->setDialog($dialog);
}
// No reason this can't go live, maybe an old link. Kick them live and see
// what happens.
$blog = $post->getBlog();
$live_uri = 'http://'.$blog->getDomain().'/'.$post->getPhameTitle();
return id(new AphrontRedirectResponse())->setURI($live_uri);
}
}
diff --git a/src/applications/phame/controller/post/PhamePostPreviewController.php b/src/applications/phame/controller/post/PhamePostPreviewController.php
index b69e0778e1..38b007178d 100644
--- a/src/applications/phame/controller/post/PhamePostPreviewController.php
+++ b/src/applications/phame/controller/post/PhamePostPreviewController.php
@@ -1,46 +1,30 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phame
*/
final class PhamePostPreviewController
extends PhameController {
protected function getSideNavFilter() {
return null;
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$body = $request->getStr('body');
$post = id(new PhamePost())
->setBody($body);
$content = PhabricatorMarkupEngine::renderOneObject(
$post,
PhamePost::MARKUP_FIELD_BODY,
$user);
$content = '<div class="phabricator-remarkup">'.$content.'</div>';
return id(new AphrontAjaxResponse())->setContent($content);
}
}
diff --git a/src/applications/phame/controller/post/PhamePostPublishController.php b/src/applications/phame/controller/post/PhamePostPublishController.php
index 05e8329615..2c0cd075a4 100644
--- a/src/applications/phame/controller/post/PhamePostPublishController.php
+++ b/src/applications/phame/controller/post/PhamePostPublishController.php
@@ -1,103 +1,87 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phame
*/
final class PhamePostPublishController extends PhameController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$post = id(new PhamePostQuery())
->setViewer($user)
->withIDs(array($this->id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$post) {
return new Aphront404Response();
}
$view_uri = $this->getApplicationURI('/post/view/'.$post->getID().'/');
if ($request->isFormPost()) {
$post->setVisibility(PhamePost::VISIBILITY_PUBLISHED);
$post->setDatePublished(time());
$post->save();
return id(new AphrontRedirectResponse())->setURI($view_uri);
}
$header = id(new PhabricatorHeaderView())
->setHeader('Preview Post');
$form = id(new AphrontFormView())
->setUser($user)
->setFlexible(true)
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Publish Post'))
->addCancelButton($view_uri));
$frame = $this->renderPreviewFrame($post);
$nav = $this->renderSideNavFilterView(null);
$nav->appendChild(
array(
$header,
$form,
$frame,
));
return $this->buildApplicationPage(
$nav,
array(
'title' => pht('Preview Post'),
'device' => true,
));
}
private function renderPreviewFrame(PhamePost $post) {
// TODO: Clean up this CSS.
return phutil_render_tag(
'div',
array(
'style' => 'text-align: center; padding: 1em;',
),
phutil_render_tag(
'iframe',
array(
'style' => 'width: 100%; height: 600px; '.
'border: 1px solid #303030; background: #303030;',
'src' => $this->getApplicationURI('/post/framed/'.$post->getID().'/'),
),
''));
}
}
diff --git a/src/applications/phame/controller/post/PhamePostUnpublishController.php b/src/applications/phame/controller/post/PhamePostUnpublishController.php
index b9dfb5906e..efc8153dab 100644
--- a/src/applications/phame/controller/post/PhamePostUnpublishController.php
+++ b/src/applications/phame/controller/post/PhamePostUnpublishController.php
@@ -1,70 +1,54 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phame
*/
final class PhamePostUnpublishController extends PhameController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$post = id(new PhamePostQuery())
->setViewer($user)
->withIDs(array($this->id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$post) {
return new Aphront404Response();
}
if ($request->isFormPost()) {
$post->setVisibility(PhamePost::VISIBILITY_DRAFT);
$post->setDatePublished(0);
$post->save();
return id(new AphrontRedirectResponse())
->setURI($this->getApplicationURI('/post/view/'.$post->getID().'/'));
}
$cancel_uri = $this->getApplicationURI('/post/view/'.$post->getID().'/');
$dialog = id(new AphrontDialogView())
->setUser($user)
->setTitle(pht('Unpublish Post?'))
->appendChild(
pht(
'The post "%s" will no longer be visible to other users until you '.
'republish it.',
phutil_escape_html($post->getTitle())))
->addSubmitButton(pht('Unpublish'))
->addCancelButton($cancel_uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/phame/controller/post/PhamePostViewController.php b/src/applications/phame/controller/post/PhamePostViewController.php
index 626a1704c4..967316014c 100644
--- a/src/applications/phame/controller/post/PhamePostViewController.php
+++ b/src/applications/phame/controller/post/PhamePostViewController.php
@@ -1,209 +1,193 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phame
*/
final class PhamePostViewController extends PhameController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$post = id(new PhamePostQuery())
->setViewer($user)
->withIDs(array($this->id))
->executeOne();
if (!$post) {
return new Aphront404Response();
}
$nav = $this->renderSideNavFilterView();
$nav->appendChild(
id(new PhabricatorHeaderView())
->setHeader($post->getTitle()));
if ($post->isDraft()) {
$nav->appendChild(
id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
->setTitle(pht('Draft Post'))
->appendChild(
pht('Only you can see this draft until you publish it. '.
'Use "Preview / Publish" to publish this post.')));
}
if (!$post->getBlog()) {
$nav->appendChild(
id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_WARNING)
->setTitle(pht('Not On A Blog'))
->appendChild(
pht('This post is not associated with a blog (the blog may have '.
'been deleted). Use "Move Post" to move it to a new blog.')));
}
$this->loadHandles(
array(
$post->getBlogPHID(),
$post->getBloggerPHID(),
));
$actions = $this->renderActions($post, $user);
$properties = $this->renderProperties($post, $user);
$nav->appendChild(
array(
$actions,
$properties,
));
return $this->buildApplicationPage(
$nav,
array(
'title' => $post->getTitle(),
'device' => true,
));
}
private function renderActions(
PhamePost $post,
PhabricatorUser $user) {
$actions = id(new PhabricatorActionListView())
->setObject($post)
->setUser($user);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$user,
$post,
PhabricatorPolicyCapability::CAN_EDIT);
$id = $post->getID();
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('edit')
->setHref($this->getApplicationURI('post/edit/'.$id.'/'))
->setName('Edit Post')
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('move')
->setHref($this->getApplicationURI('post/move/'.$id.'/'))
->setName('Move Post')
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
if ($post->isDraft()) {
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('preview')
->setHref($this->getApplicationURI('post/publish/'.$id.'/'))
->setName(pht('Preview / Publish')));
} else {
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('unpublish')
->setHref($this->getApplicationURI('post/unpublish/'.$id.'/'))
->setName(pht('Unpublish'))
->setWorkflow(true));
}
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('delete')
->setHref($this->getApplicationURI('post/delete/'.$id.'/'))
->setName('Delete Post')
->setDisabled(!$can_edit)
->setWorkflow(true));
$blog = $post->getBlog();
$can_view_live = $blog && !$post->isDraft();
if ($can_view_live) {
$live_uri = 'live/'.$blog->getID().'/post/'.$post->getPhameTitle();
} else {
$live_uri = 'post/notlive/'.$post->getID().'/';
}
$live_uri = $this->getApplicationURI($live_uri);
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('world')
->setHref($live_uri)
->setName(pht('View Live'))
->setDisabled(!$can_view_live)
->setWorkflow(!$can_view_live));
return $actions;
}
private function renderProperties(
PhamePost $post,
PhabricatorUser $user) {
$properties = new PhabricatorPropertyListView();
$descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions(
$user,
$post);
$properties->addProperty(
pht('Blog'),
$post->getBlogPHID()
? $this->getHandle($post->getBlogPHID())->renderLink()
: null);
$properties->addProperty(
pht('Blogger'),
$this->getHandle($post->getBloggerPHID())->renderLink());
$properties->addProperty(
pht('Visible To'),
$descriptions[PhabricatorPolicyCapability::CAN_VIEW]);
$properties->addProperty(
pht('Published'),
$post->isDraft()
? pht('Draft')
: phabricator_datetime($post->getDatePublished(), $user));
$engine = id(new PhabricatorMarkupEngine())
->setViewer($user)
->addObject($post, PhamePost::MARKUP_FIELD_BODY)
->process();
$properties->addTextContent(
'<div class="phabricator-remarkup">'.
$engine->getOutput($post, PhamePost::MARKUP_FIELD_BODY).
'</div>');
return $properties;
}
}
diff --git a/src/applications/phame/query/PhameBlogQuery.php b/src/applications/phame/query/PhameBlogQuery.php
index 8dff47795c..89a9a3d86c 100644
--- a/src/applications/phame/query/PhameBlogQuery.php
+++ b/src/applications/phame/query/PhameBlogQuery.php
@@ -1,94 +1,78 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phame
*/
final class PhameBlogQuery extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $domain;
private $needBloggers;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withDomain($domain) {
$this->domain = $domain;
return $this;
}
public function loadPage() {
$table = new PhameBlog();
$conn_r = $table->establishConnection('r');
$where_clause = $this->buildWhereClause($conn_r);
$order_clause = $this->buildOrderClause($conn_r);
$limit_clause = $this->buildLimitClause($conn_r);
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T b %Q %Q %Q',
$table->getTableName(),
$where_clause,
$order_clause,
$limit_clause);
$blogs = $table->loadAllFromArray($data);
return $blogs;
}
private function buildWhereClause($conn_r) {
$where = array();
if ($this->ids) {
$where[] = qsprintf(
$conn_r,
'id IN (%Ls)',
$this->ids);
}
if ($this->phids) {
$where[] = qsprintf(
$conn_r,
'phid IN (%Ls)',
$this->phids);
}
if ($this->domain) {
$where[] = qsprintf(
$conn_r,
'domain = %s',
$this->domain);
}
$where[] = $this->buildPagingClause($conn_r);
return $this->formatWhereClause($where);
}
}
diff --git a/src/applications/phame/query/PhamePostQuery.php b/src/applications/phame/query/PhamePostQuery.php
index 045c3eaa58..079f8638f8 100644
--- a/src/applications/phame/query/PhamePostQuery.php
+++ b/src/applications/phame/query/PhamePostQuery.php
@@ -1,147 +1,131 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phame
*/
final class PhamePostQuery extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $blogPHIDs;
private $bloggerPHIDs;
private $phameTitles;
private $visibility;
private $phids;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withBloggerPHIDs(array $blogger_phids) {
$this->bloggerPHIDs = $blogger_phids;
return $this;
}
public function withBlogPHIDs(array $blog_phids) {
$this->blogPHIDs = $blog_phids;
return $this;
}
public function withPhameTitles(array $phame_titles) {
$this->phameTitles = $phame_titles;
return $this;
}
public function withVisibility($visibility) {
$this->visibility = $visibility;
return $this;
}
protected function loadPage() {
$table = new PhamePost();
$conn_r = $table->establishConnection('r');
$where_clause = $this->buildWhereClause($conn_r);
$order_clause = $this->buildOrderClause($conn_r);
$limit_clause = $this->buildLimitClause($conn_r);
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T p %Q %Q %Q',
$table->getTableName(),
$where_clause,
$order_clause,
$limit_clause);
$posts = $table->loadAllFromArray($data);
if ($posts) {
// We require these to do visibility checks, so load them unconditionally.
$blog_phids = mpull($posts, 'getBlogPHID');
$blogs = id(new PhameBlogQuery())
->setViewer($this->getViewer())
->withPHIDs($blog_phids)
->execute();
$blogs = mpull($blogs, null, 'getPHID');
foreach ($posts as $post) {
if (isset($blogs[$post->getBlogPHID()])) {
$post->setBlog($blogs[$post->getBlogPHID()]);
}
}
}
return $posts;
}
private function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
if ($this->ids) {
$where[] = qsprintf(
$conn_r,
'p.id IN (%Ld)',
$this->ids);
}
if ($this->phids) {
$where[] = qsprintf(
$conn_r,
'p.phid IN (%Ls)',
$this->phids);
}
if ($this->bloggerPHIDs) {
$where[] = qsprintf(
$conn_r,
'p.bloggerPHID IN (%Ls)',
$this->bloggerPHIDs);
}
if ($this->phameTitles) {
$where[] = qsprintf(
$conn_r,
'p.phameTitle IN (%Ls)',
$this->phameTitles);
}
if ($this->visibility !== null) {
$where[] = qsprintf(
$conn_r,
'p.visibility = %d',
$this->visibility);
}
if ($this->blogPHIDs) {
$where[] = qsprintf(
$conn_r,
'p.blogPHID in (%Ls)',
$this->blogPHIDs);
}
$where[] = $this->buildPagingClause($conn_r);
return $this->formatWhereClause($where);
}
}
diff --git a/src/applications/phame/skins/PhameBasicBlogSkin.php b/src/applications/phame/skins/PhameBasicBlogSkin.php
index fdfeedd35e..addf25eedd 100644
--- a/src/applications/phame/skins/PhameBasicBlogSkin.php
+++ b/src/applications/phame/skins/PhameBasicBlogSkin.php
@@ -1,285 +1,269 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @task paging Paging
* @task internal Internals
* @group phame
*/
abstract class PhameBasicBlogSkin extends PhameBlogSkin {
private $pager;
public function processRequest() {
$request = $this->getRequest();
$content = $this->renderContent($request);
if (!$content) {
$content = $this->render404Page();
}
$content = array(
$this->renderHeader(),
$content,
$this->renderFooter(),
);
$view = id(new PhabricatorBarePageView())
->setRequest($request)
->setController($this)
->setDeviceReady(true)
->setTitle($this->getBlog()->getName());
if ($this->getPreview()) {
$view->setFrameable(true);
}
$view->appendChild($content);
$response = new AphrontWebpageResponse();
$response->setContent($view->render());
return $response;
}
public function getSkinName() {
return get_class($this);
}
abstract protected function renderHeader();
abstract protected function renderFooter();
protected function renderPostDetail(PhamePostView $post) {
return $post;
}
protected function renderPostList(array $posts) {
$summaries = array();
foreach ($posts as $post) {
$summaries[] = $post->renderWithSummary();
}
$list = phutil_render_tag(
'div',
array(
'class' => 'phame-post-list',
),
id(new AphrontNullView())->appendChild($summaries)->render());
$pager = $this->renderOlderPageLink().$this->renderNewerPageLink();
if ($pager) {
$pager = phutil_render_tag(
'div',
array(
'class' => 'phame-pager',
));
}
return $list.$pager;
}
protected function render404Page() {
return '<h2>404 Not Found</h2>';
}
final public function getResourceURI($resource) {
$root = $this->getSpecification()->getRootDirectory();
$path = $root.DIRECTORY_SEPARATOR.$resource;
$data = Filesystem::readFile($path);
$hash = PhabricatorHash::digest($data);
$hash = substr($hash, 0, 6);
$id = $this->getBlog()->getID();
$uri = '/phame/r/'.$id.'/'.$hash.'/'.$resource;
$uri = PhabricatorEnv::getCDNURI($uri);
return $uri;
}
/* -( Paging )------------------------------------------------------------- */
/**
* @task paging
*/
public function getPageSize() {
return 100;
}
/**
* @task paging
*/
protected function getOlderPageURI() {
if ($this->pager) {
$next = $this->pager->getNextPageID();
if ($next) {
return $this->getURI('older/'.$next.'/');
}
}
return null;
}
/**
* @task paging
*/
protected function renderOlderPageLink() {
$uri = $this->getOlderPageURI();
if (!$uri) {
return null;
}
return phutil_render_tag(
'a',
array(
'class' => 'phame-page-link phame-page-older',
'href' => $uri,
),
pht("\xE2\x80\xB9 Older"));
}
/**
* @task paging
*/
protected function getNewerPageURI() {
if ($this->pager) {
$next = $this->pager->getNextPageID();
if ($next) {
return $this->getURI('newer/'.$next.'/');
}
}
return null;
}
/**
* @task paging
*/
protected function renderNewerPageLink() {
$uri = $this->getNewerPageURI();
if (!$uri) {
return null;
}
return phutil_render_tag(
'a',
array(
'class' => 'phame-page-link phame-page-newer',
'href' => $uri,
),
pht("Newer \xE2\x80\xBA"));
}
/* -( Internals )---------------------------------------------------------- */
/**
* @task internal
*/
protected function renderContent(AphrontRequest $request) {
$user = $request->getUser();
$matches = null;
$path = $request->getPath();
if (preg_match('@^/post/(?P<name>.*)$@', $path, $matches)) {
$post = id(new PhamePostQuery())
->setViewer($user)
->withBlogPHIDs(array($this->getBlog()->getPHID()))
->withPhameTitles(array($matches['name']))
->executeOne();
if ($post) {
$view = head($this->buildPostViews(array($post)));
return $this->renderPostDetail($view);
}
} else {
$pager = new AphrontCursorPagerView();
if (preg_match('@^/older/(?P<before>\d+)/$@', $path, $matches)) {
$pager->setBeforeID($matches['before']);
} else if (preg_match('@^/newer/(?P<after>\d)/$@', $path, $matches)) {
$pager->setAfterID($matches['after']);
} else if (preg_match('@^/$@', $path, $matches)) {
// Just show the first page.
} else {
return null;
}
$pager->setPageSize($this->getPageSize());
$posts = id(new PhamePostQuery())
->setViewer($user)
->withBlogPHIDs(array($this->getBlog()->getPHID()))
->executeWithCursorPager($pager);
$this->pager = $pager;
if ($posts) {
$views = $this->buildPostViews($posts);
return $this->renderPostList($views);
}
}
return null;
}
private function buildPostViews(array $posts) {
assert_instances_of($posts, 'PhamePost');
$user = $this->getRequest()->getUser();
$engine = id(new PhabricatorMarkupEngine())
->setViewer($user);
$phids = array();
foreach ($posts as $post) {
$engine->addObject($post, PhamePost::MARKUP_FIELD_BODY);
$engine->addObject($post, PhamePost::MARKUP_FIELD_SUMMARY);
$phids[] = $post->getBloggerPHID();
}
$handles = id(new PhabricatorObjectHandleData($phids))
->loadHandles();
$engine->process();
$views = array();
foreach ($posts as $post) {
$view = id(new PhamePostView())
->setViewer($user)
->setSkin($this)
->setPost($post)
->setBody($engine->getOutput($post, PhamePost::MARKUP_FIELD_BODY))
->setSummary($engine->getOutput($post, PhamePost::MARKUP_FIELD_SUMMARY))
->setAuthor($handles[$post->getBloggerPHID()]);
$post->makeEphemeral();
if (!$post->getDatePublished()) {
$post->setDatePublished(time());
}
$views[] = $view;
}
return $views;
}
}
diff --git a/src/applications/phame/skins/PhameBasicTemplateBlogSkin.php b/src/applications/phame/skins/PhameBasicTemplateBlogSkin.php
index e94b442ac9..28ae0e99ea 100644
--- a/src/applications/phame/skins/PhameBasicTemplateBlogSkin.php
+++ b/src/applications/phame/skins/PhameBasicTemplateBlogSkin.php
@@ -1,139 +1,123 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phame
*/
final class PhameBasicTemplateBlogSkin extends PhameBasicBlogSkin {
private $cssResources;
public function processRequest() {
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/support/phame/libskin.php';
$css = $this->getPath('css/');
if (Filesystem::pathExists($css)) {
$this->cssResources = array();
foreach (Filesystem::listDirectory($css) as $path) {
if (!preg_match('/.css$/', $path)) {
continue;
}
$this->cssResources[] = phutil_render_tag(
'link',
array(
'rel' => 'stylesheet',
'type' => 'text/css',
'href' => $this->getResourceURI('css/'.$path),
));
}
$this->cssResources = implode("\n", $this->cssResources);
}
$request = $this->getRequest();
$content = $this->renderContent($request);
if (!$content) {
$content = $this->render404Page();
}
$content = array(
$this->renderHeader(),
$content,
$this->renderFooter(),
);
$response = new AphrontWebpageResponse();
$response->setContent(implode("\n", $content));
return $response;
}
public function getCSSResources() {
return $this->cssResources;
}
public function getName() {
return $this->getSpecification()->getName();
}
public function getPath($to_file = null) {
$path = $this->getSpecification()->getRootDirectory();
if ($to_file) {
$path = $path.DIRECTORY_SEPARATOR.$to_file;
}
return $path;
}
private function renderTemplate($__template__, array $__scope__) {
chdir($this->getPath());
ob_start();
if (Filesystem::pathExists($this->getPath($__template__))) {
// Fool lint.
$__evil__ = 'extract';
$__evil__($__scope__ + $this->getDefaultScope());
require $this->getPath($__template__);
}
return ob_get_clean();
}
private function getDefaultScope() {
return array(
'skin' => $this,
'blog' => $this->getBlog(),
'uri' => $this->getURI(''),
);
}
protected function renderHeader() {
return $this->renderTemplate(
'header.php',
array(
'title' => $this->getBlog()->getName(),
));
}
protected function renderFooter() {
return $this->renderTemplate('footer.php', array());
}
protected function render404Page() {
return $this->renderTemplate('404.php', array());
}
protected function renderPostDetail(PhamePostView $post) {
return $this->renderTemplate(
'post-detail.php',
array(
'post' => $post,
));
}
protected function renderPostList(array $posts) {
return $this->renderTemplate(
'post-list.php',
array(
'posts' => $posts,
'older' => $this->renderNewerPageLink(),
'newer' => $this->renderOlderPageLink(),
));
}
}
diff --git a/src/applications/phame/skins/PhameBlogSkin.php b/src/applications/phame/skins/PhameBlogSkin.php
index fcd7de16dd..9388e24fb4 100644
--- a/src/applications/phame/skins/PhameBlogSkin.php
+++ b/src/applications/phame/skins/PhameBlogSkin.php
@@ -1,65 +1,49 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phame
*/
abstract class PhameBlogSkin extends PhabricatorController {
private $blog;
private $baseURI;
private $preview;
private $specification;
public function setSpecification(PhameSkinSpecification $specification) {
$this->specification = $specification;
return $this;
}
public function getSpecification() {
return $this->specification;
}
public function setPreview($preview) {
$this->preview = $preview;
return $this;
}
public function getPreview() {
return $this->preview;
}
final public function setBaseURI($base_uri) {
$this->baseURI = $base_uri;
return $this;
}
final public function getURI($path) {
return $this->baseURI.$path;
}
final public function setBlog(PhameBlog $blog) {
$this->blog = $blog;
return $this;
}
final public function getBlog() {
return $this->blog;
}
}
diff --git a/src/applications/phame/skins/PhameSkinSpecification.php b/src/applications/phame/skins/PhameSkinSpecification.php
index 114fb198c9..326200dc98 100644
--- a/src/applications/phame/skins/PhameSkinSpecification.php
+++ b/src/applications/phame/skins/PhameSkinSpecification.php
@@ -1,190 +1,174 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phame
*/
final class PhameSkinSpecification {
const TYPE_ADVANCED = 'advanced';
const TYPE_BASIC = 'basic';
private $type;
private $rootDirectory;
private $skinClass;
private $phutilLibraries = array();
private $name;
private $config;
public static function loadAllSkinSpecifications() {
static $specs;
if ($specs === null) {
$paths = PhabricatorEnv::getEnvConfig('phame.skins');
$base = dirname(phutil_get_library_root('phabricator'));
$specs = array();
foreach ($paths as $path) {
$path = Filesystem::resolvePath($path, $base);
foreach (Filesystem::listDirectory($path) as $skin_directory) {
$skin_path = $path.DIRECTORY_SEPARATOR.$skin_directory;
if (!is_dir($skin_path)) {
continue;
}
$spec = self::loadSkinSpecification($skin_path);
if (!$spec) {
continue;
}
$name = trim($skin_directory, DIRECTORY_SEPARATOR);
$spec->setName($name);
if (isset($specs[$name])) {
$that_dir = $specs[$name]->getRootDirectory();
$this_dir = $spec->getRootDirectory();
throw new Exception(
"Two skins have the same name ('{$name}'), in '{$this_dir}' and ".
"'{$that_dir}'. Rename one or adjust your 'phame.skins' ".
"configuration.");
}
$specs[$name] = $spec;
}
}
}
return $specs;
}
public static function loadOneSkinSpecification($name) {
$paths = PhabricatorEnv::getEnvConfig('phame.skins');
$base = dirname(phutil_get_library_root('phabricator'));
foreach ($paths as $path) {
$path = Filesystem::resolvePath($path, $base);
$skin_path = $path.DIRECTORY_SEPARATOR.$name;
if (is_dir($skin_path)) {
$spec = self::loadSkinSpecification($skin_path);
if ($spec) {
$spec->setName($name);
return $spec;
}
}
}
return null;
}
public static function loadSkinSpecification($path) {
$config_path = $path.DIRECTORY_SEPARATOR.'skin.json';
$config = array();
if (Filesystem::pathExists($config_path)) {
$config = Filesystem::readFile($config_path);
$config = json_decode($config, true);
if (!is_array($config)) {
throw new Exception(
"Skin configuration file '{$config_path}' is not a valid JSON file.");
}
$type = idx($config, 'type', self::TYPE_BASIC);
} else {
$type = self::TYPE_BASIC;
}
$spec = new PhameSkinSpecification();
$spec->setRootDirectory($path);
$spec->setConfig($config);
switch ($type) {
case self::TYPE_BASIC:
$spec->setSkinClass('PhameBasicTemplateBlogSkin');
break;
case self::TYPE_ADVANCED:
$spec->setSkinClass($config['class']);
$spec->addPhutilLibrary($path.DIRECTORY_SEPARATOR.'src');
break;
default:
throw new Exception("Unknown skin type!");
}
$spec->setType($type);
return $spec;
}
public function setConfig(array $config) {
$this->config = $config;
return $this;
}
public function getConfig($key, $default = null) {
return idx($this->config, $key, $default);
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->getConfig('name', $this->name);
}
public function setRootDirectory($root_directory) {
$this->rootDirectory = $root_directory;
return $this;
}
public function getRootDirectory() {
return $this->rootDirectory;
}
public function setType($type) {
$this->type = $type;
return $this;
}
public function getType() {
return $this->type;
}
public function setSkinClass($skin_class) {
$this->skinClass = $skin_class;
return $this;
}
public function getSkinClass() {
return $this->skinClass;
}
public function addPhutilLibrary($library) {
$this->phutilLibraries[] = $library;
return $this;
}
public function buildSkin(AphrontRequest $request) {
foreach ($this->phutilLibraries as $library) {
phutil_load_library($library);
}
return newv($this->getSkinClass(), array($request, $this));
}
}
diff --git a/src/applications/phame/storage/PhameBlog.php b/src/applications/phame/storage/PhameBlog.php
index b006d67206..5ba923f211 100644
--- a/src/applications/phame/storage/PhameBlog.php
+++ b/src/applications/phame/storage/PhameBlog.php
@@ -1,305 +1,289 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phame
*/
final class PhameBlog extends PhameDAO
implements PhabricatorPolicyInterface, PhabricatorMarkupInterface {
const MARKUP_FIELD_DESCRIPTION = 'markup:description';
const SKIN_DEFAULT = 'oblivious';
protected $id;
protected $phid;
protected $name;
protected $description;
protected $domain;
protected $configData;
protected $creatorPHID;
protected $viewPolicy;
protected $editPolicy;
protected $joinPolicy;
private $bloggerPHIDs;
private $bloggers;
static private $requestBlog;
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'configData' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_BLOG);
}
public function getSkinRenderer(AphrontRequest $request) {
$spec = PhameSkinSpecification::loadOneSkinSpecification(
$this->getSkin());
if (!$spec) {
$spec = PhameSkinSpecification::loadOneSkinSpecification(
self::SKIN_DEFAULT);
}
if (!$spec) {
throw new Exception(
"This blog has an invalid skin, and the default skin failed to ".
"load.");
}
$skin = newv($spec->getSkinClass(), array($request));
$skin->setSpecification($spec);
return $skin;
}
/**
* Makes sure a given custom blog uri is properly configured in DNS
* to point at this Phabricator instance. If there is an error in
* the configuration, return a string describing the error and how
* to fix it. If there is no error, return an empty string.
*
* @return string
*/
public function validateCustomDomain($custom_domain) {
$example_domain = '(e.g. blog.example.com)';
$valid = '';
// note this "uri" should be pretty busted given the desired input
// so just use it to test if there's a protocol specified
$uri = new PhutilURI($custom_domain);
if ($uri->getProtocol()) {
return 'Do not specify a protocol, just the domain. '.$example_domain;
}
if (strpos($custom_domain, '/') !== false) {
return 'Do not specify a path, just the domain. '.$example_domain;
}
if (strpos($custom_domain, '.') === false) {
return 'Custom domain must contain at least one dot (.) because '.
'some browsers fail to set cookies on domains such as '.
'http://example. '.$example_domain;
}
return $valid;
}
public function loadBloggerPHIDs() {
if (!$this->getPHID()) {
return $this;
}
if ($this->bloggerPHIDs) {
return $this;
}
$this->bloggerPHIDs = PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getPHID(),
PhabricatorEdgeConfig::TYPE_BLOG_HAS_BLOGGER
);
return $this;
}
public function getBloggerPHIDs() {
if ($this->bloggerPHIDs === null) {
throw new Exception(
'You must loadBloggerPHIDs before you can getBloggerPHIDs!'
);
}
return $this->bloggerPHIDs;
}
public function loadBloggers() {
if ($this->bloggers) {
return $this->bloggers;
}
$blogger_phids = $this->loadBloggerPHIDs()->getBloggerPHIDs();
if (empty($blogger_phids)) {
return array();
}
$bloggers = id(new PhabricatorObjectHandleData($blogger_phids))
->loadHandles();
$this->attachBloggers($bloggers);
return $this;
}
public function attachBloggers(array $bloggers) {
assert_instances_of($bloggers, 'PhabricatorObjectHandle');
$this->bloggers = $bloggers;
return $this;
}
public function getBloggers() {
if ($this->bloggers === null) {
throw new Exception(
'You must loadBloggers or attachBloggers before you can getBloggers!'
);
}
return $this->bloggers;
}
public function getSkin() {
$config = coalesce($this->getConfigData(), array());
return idx($config, 'skin', self::SKIN_DEFAULT);
}
public function setSkin($skin) {
$config = coalesce($this->getConfigData(), array());
$config['skin'] = $skin;
return $this->setConfigData($config);
}
static public function getSkinOptionsForSelect() {
$classes = id(new PhutilSymbolLoader())
->setAncestorClass('PhameBlogSkin')
->setType('class')
->setConcreteOnly(true)
->selectSymbolsWithoutLoading();
return ipull($classes, 'name', 'name');
}
public function getPostListURI() {
return $this->getActionURI('posts');
}
public function getEditURI() {
return $this->getActionURI('edit');
}
public function getEditFilter() {
return 'blog/edit/'.$this->getPHID();
}
public function getDeleteURI() {
return $this->getActionURI('delete');
}
private function getActionURI($action) {
return '/phame/blog/'.$action.'/'.$this->getPHID().'/';
}
public static function setRequestBlog(PhameBlog $blog) {
self::$requestBlog = $blog;
}
public static function getRequestBlog() {
return self::$requestBlog;
}
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
PhabricatorPolicyCapability::CAN_JOIN,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
case PhabricatorPolicyCapability::CAN_JOIN:
return $this->getJoinPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
$can_edit = PhabricatorPolicyCapability::CAN_EDIT;
$can_join = PhabricatorPolicyCapability::CAN_JOIN;
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
// Users who can edit or post to a blog can always view it.
if (PhabricatorPolicyFilter::hasCapability($user, $this, $can_edit)) {
return true;
}
if (PhabricatorPolicyFilter::hasCapability($user, $this, $can_join)) {
return true;
}
break;
case PhabricatorPolicyCapability::CAN_JOIN:
// Users who can edit a blog can always post to it.
if (PhabricatorPolicyFilter::hasCapability($user, $this, $can_edit)) {
return true;
}
break;
}
return false;
}
/* -( PhabricatorMarkupInterface Implementation )-------------------------- */
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digest($this->getMarkupText($field));
return $this->getPHID().':'.$field.':'.$hash;
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newPhameMarkupEngine();
}
public function getMarkupText($field) {
return $this->getDescription();
}
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
public function shouldUseMarkupCache($field) {
return (bool)$this->getPHID();
}
}
diff --git a/src/applications/phame/storage/PhameDAO.php b/src/applications/phame/storage/PhameDAO.php
index 74809be311..3e2e72fcb8 100644
--- a/src/applications/phame/storage/PhameDAO.php
+++ b/src/applications/phame/storage/PhameDAO.php
@@ -1,28 +1,12 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phame
*/
abstract class PhameDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'phame';
}
}
diff --git a/src/applications/phame/storage/PhamePost.php b/src/applications/phame/storage/PhamePost.php
index 7a707789a3..9e5097f39c 100644
--- a/src/applications/phame/storage/PhamePost.php
+++ b/src/applications/phame/storage/PhamePost.php
@@ -1,217 +1,201 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phame
*/
final class PhamePost extends PhameDAO
implements PhabricatorPolicyInterface, PhabricatorMarkupInterface {
const MARKUP_FIELD_BODY = 'markup:body';
const MARKUP_FIELD_SUMMARY = 'markup:summary';
const VISIBILITY_DRAFT = 0;
const VISIBILITY_PUBLISHED = 1;
protected $id;
protected $phid;
protected $bloggerPHID;
protected $title;
protected $phameTitle;
protected $body;
protected $visibility;
protected $configData;
protected $datePublished;
protected $blogPHID;
private $blog;
public function setBlog(PhameBlog $blog) {
$this->blog = $blog;
return $this;
}
public function getBlog() {
return $this->blog;
}
public function getViewURI($blogger_name = '') {
// go for the pretty uri if we can
if ($blogger_name) {
$phame_title = PhabricatorSlug::normalize($this->getPhameTitle());
$uri = phutil_escape_uri('/phame/posts/'.$blogger_name.'/'.$phame_title);
} else {
$uri = $this->getActionURI('view');
}
return $uri;
}
public function getEditURI() {
return $this->getActionURI('edit');
}
public function getDeleteURI() {
return $this->getActionURI('delete');
}
public function getChangeVisibilityURI() {
return $this->getActionURI('changevisibility');
}
private function getActionURI($action) {
return '/phame/post/'.$action.'/'.$this->getPHID().'/';
}
public function isDraft() {
return $this->getVisibility() == self::VISIBILITY_DRAFT;
}
public function getHumanName() {
if ($this->isDraft()) {
$name = 'draft';
} else {
$name = 'post';
}
return $name;
}
public function getCommentsWidget() {
$config_data = $this->getConfigData();
if (empty($config_data)) {
return 'none';
}
return idx($config_data, 'comments_widget', 'none');
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'configData' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_POST);
}
public static function getVisibilityOptionsForSelect() {
return array(
self::VISIBILITY_DRAFT => 'Draft: visible only to me.',
self::VISIBILITY_PUBLISHED => 'Published: visible to the whole world.',
);
}
public function getCommentsWidgetOptionsForSelect() {
$current = $this->getCommentsWidget();
$options = array();
if ($current == 'facebook' ||
PhabricatorEnv::getEnvConfig('facebook.application-id')) {
$options['facebook'] = 'Facebook';
}
if ($current == 'disqus' ||
PhabricatorEnv::getEnvConfig('disqus.shortname')) {
$options['disqus'] = 'Disqus';
}
$options['none'] = 'None';
return $options;
}
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
// Draft posts are visible only to the author. Published posts are visible
// to whoever the blog is visible to.
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
if (!$this->isDraft() && $this->getBlog()) {
return $this->getBlog()->getViewPolicy();
} else {
return PhabricatorPolicies::POLICY_NOONE;
}
break;
case PhabricatorPolicyCapability::CAN_EDIT:
return PhabricatorPolicies::POLICY_NOONE;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
// A blog post's author can always view it, and is the only user allowed
// to edit it.
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
case PhabricatorPolicyCapability::CAN_EDIT:
return ($user->getPHID() == $this->getBloggerPHID());
}
}
/* -( PhabricatorMarkupInterface Implementation )-------------------------- */
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digest($this->getMarkupText($field));
return $this->getPHID().':'.$field.':'.$hash;
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newPhameMarkupEngine();
}
public function getMarkupText($field) {
switch ($field) {
case self::MARKUP_FIELD_BODY:
return $this->getBody();
case self::MARKUP_FIELD_SUMMARY:
return PhabricatorMarkupEngine::summarize($this->getBody());
}
}
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
public function shouldUseMarkupCache($field) {
return (bool)$this->getPHID();
}
}
diff --git a/src/applications/phame/view/PhamePostView.php b/src/applications/phame/view/PhamePostView.php
index ae26615d48..03f9938d6a 100644
--- a/src/applications/phame/view/PhamePostView.php
+++ b/src/applications/phame/view/PhamePostView.php
@@ -1,265 +1,249 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phame
*/
final class PhamePostView extends AphrontView {
private $post;
private $author;
private $viewer;
private $body;
private $skin;
private $summary;
public function setSkin(PhameBlogSkin $skin) {
$this->skin = $skin;
return $this;
}
public function getSkin() {
return $this->skin;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setAuthor(PhabricatorObjectHandle $author) {
$this->author = $author;
return $this;
}
public function getAuthor() {
return $this->author;
}
public function setPost(PhamePost $post) {
$this->post = $post;
return $this;
}
public function getPost() {
return $this->post;
}
public function setBody($body) {
$this->body = $body;
return $this;
}
public function getBody() {
return $this->body;
}
public function setSummary($summary) {
$this->summary = $summary;
return $this;
}
public function getSummary() {
return $this->summary;
}
public function renderTitle() {
$href = $this->getSkin()->getURI('post/'.$this->getPost()->getPhameTitle());
return phutil_render_tag(
'h2',
array(
'class' => 'phame-post-title',
),
phutil_render_tag(
'a',
array(
'href' => $href,
),
phutil_escape_html($this->getPost()->getTitle())));
}
public function renderDatePublished() {
return phutil_render_tag(
'div',
array(
'class' => 'phame-post-date',
),
phutil_escape_html(
pht(
'Published on %s by %s',
phabricator_datetime(
$this->getPost()->getDatePublished(),
$this->getViewer()),
$this->getAuthor()->getName())));
}
public function renderBody() {
return phutil_render_tag(
'div',
array(
'class' => 'phame-post-body',
),
$this->getBody());
}
public function renderSummary() {
return phutil_render_tag(
'div',
array(
'class' => 'phame-post-body',
),
$this->getSummary());
}
public function renderComments() {
$post = $this->getPost();
switch ($post->getCommentsWidget()) {
case 'facebook':
$comments = $this->renderFacebookComments();
break;
case 'disqus':
$comments = $this->renderDisqusComments();
break;
case 'none':
default:
$comments = null;
break;
}
return $comments;
}
public function render() {
return phutil_render_tag(
'div',
array(
'class' => 'phame-post',
),
$this->renderTitle().
$this->renderDatePublished().
$this->renderBody().
$this->renderComments());
}
public function renderWithSummary() {
return phutil_render_tag(
'div',
array(
'class' => 'phame-post',
),
$this->renderTitle().
$this->renderDatePublished().
$this->renderSummary());
}
private function renderFacebookComments() {
$fb_id = PhabricatorEnv::getEnvConfig('facebook.application-id');
if (!$fb_id) {
return null;
}
$fb_root = phutil_render_tag('div',
array(
'id' => 'fb-root',
),
''
);
$c_uri = '//connect.facebook.net/en_US/all.js#xfbml=1&appId='.$fb_id;
$fb_js = jsprintf(
'<script>(function(d, s, id) {'.
' var js, fjs = d.getElementsByTagName(s)[0];'.
' if (d.getElementById(id)) return;'.
' js = d.createElement(s); js.id = id;'.
' js.src = %s;'.
' fjs.parentNode.insertBefore(js, fjs);'.
'}(document, \'script\', \'facebook-jssdk\'));</script>',
$c_uri
);
$uri = $this->getSkin()->getURI('post/'.$this->getPost()->getPhameTitle());
$fb_comments = phutil_render_tag('div',
array(
'class' => 'fb-comments',
'data-href' => $uri,
'data-num-posts' => 5,
),
''
);
return phutil_render_tag(
'div',
array(
'class' => 'phame-comments-facebook',
),
$fb_root.
$fb_js.
$fb_comments);
}
private function renderDisqusComments() {
$disqus_shortname = PhabricatorEnv::getEnvConfig('disqus.shortname');
if (!$disqus_shortname) {
return null;
}
$post = $this->getPost();
$disqus_thread = phutil_render_tag('div',
array(
'id' => 'disqus_thread'
)
);
// protip - try some var disqus_developer = 1; action to test locally
$disqus_js = jsprintf(
'<script>'.
' var disqus_shortname = "phabricator";'.
' var disqus_identifier = %s;'.
' var disqus_url = %s;'.
' var disqus_title = %s;'.
'(function() {'.
' var dsq = document.createElement("script");'.
' dsq.type = "text/javascript";'.
' dsq.async = true;'.
' dsq.src = "http://" + disqus_shortname + ".disqus.com/embed.js";'.
'(document.getElementsByTagName("head")[0] ||'.
' document.getElementsByTagName("body")[0]).appendChild(dsq);'.
'})(); </script>',
$post->getPHID(),
$this->getSkin()->getURI('post/'.$this->getPost()->getPhameTitle()),
$post->getTitle()
);
return phutil_render_tag(
'div',
array(
'class' => 'phame-comments-disqus',
),
$disqus_thread.
$disqus_js);
}
}
diff --git a/src/applications/phid/PhabricatorObjectHandle.php b/src/applications/phid/PhabricatorObjectHandle.php
index fb28c92bc9..a9a94c388b 100644
--- a/src/applications/phid/PhabricatorObjectHandle.php
+++ b/src/applications/phid/PhabricatorObjectHandle.php
@@ -1,226 +1,210 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorObjectHandle {
private $uri;
private $phid;
private $type;
private $name;
private $email;
private $fullName;
private $imageURI;
private $timestamp;
private $alternateID;
private $status = PhabricatorObjectHandleStatus::STATUS_OPEN;
private $complete;
private $disabled;
public function setURI($uri) {
$this->uri = $uri;
return $this;
}
public function getURI() {
return $this->uri;
}
public function setPHID($phid) {
$this->phid = $phid;
return $this;
}
public function getPHID() {
return $this->phid;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function setStatus($status) {
$this->status = $status;
return $this;
}
public function getStatus() {
return $this->status;
}
public function setFullName($full_name) {
$this->fullName = $full_name;
return $this;
}
public function getFullName() {
if ($this->fullName !== null) {
return $this->fullName;
}
return $this->getName();
}
public function setType($type) {
$this->type = $type;
return $this;
}
public function getType() {
return $this->type;
}
public function setImageURI($uri) {
$this->imageURI = $uri;
return $this;
}
public function getImageURI() {
return $this->imageURI;
}
public function setTimestamp($timestamp) {
$this->timestamp = $timestamp;
return $this;
}
public function getTimestamp() {
return $this->timestamp;
}
public function setAlternateID($alternate_id) {
$this->alternateID = $alternate_id;
return $this;
}
public function getAlternateID() {
return $this->alternateID;
}
public function getTypeName() {
static $map = array(
PhabricatorPHIDConstants::PHID_TYPE_USER => 'User',
PhabricatorPHIDConstants::PHID_TYPE_TASK => 'Task',
PhabricatorPHIDConstants::PHID_TYPE_DREV => 'Revision',
PhabricatorPHIDConstants::PHID_TYPE_CMIT => 'Commit',
PhabricatorPHIDConstants::PHID_TYPE_WIKI => 'Phriction',
);
return idx($map, $this->getType());
}
/**
* Set whether or not the underlying object is complete. See
* @{method:isComplete} for an explanation of what it means to be complete.
*
* @param bool True if the handle represents a complete object.
* @return this
*/
public function setComplete($complete) {
$this->complete = $complete;
return $this;
}
/**
* Determine if the handle represents an object which was completely loaded
* (i.e., the underlying object exists) vs an object which could not be
* completely loaded (e.g., the type or data for the PHID could not be
* identified or located).
*
* Basically, @{class:PhabricatorObjectHandleData} gives you back a handle for
* any PHID you give it, but it gives you a complete handle only for valid
* PHIDs.
*
* @return bool True if the handle represents a complete object.
*/
public function isComplete() {
return $this->complete;
}
/**
* Set whether or not the underlying object is disabled. See
* @{method:isDisabled} for an explanation of what it means to be disabled.
*
* @param bool True if the handle represents a disabled object.
* @return this
*/
public function setDisabled($disabled) {
$this->disabled = $disabled;
return $this;
}
/**
* Determine if the handle represents an object which has been disabled --
* for example, disabled users, archived projects, etc. These objects are
* complete and exist, but should be excluded from some system interactions
* (for instance, they usually should not appear in typeaheads, and should
* not have mail/notifications delivered to or about them).
*
* @return bool True if the handle represents a disabled object.
*/
public function isDisabled() {
return $this->disabled;
}
public function renderLink() {
$name = $this->getLinkName();
$class = null;
$title = null;
if ($this->status != PhabricatorObjectHandleStatus::STATUS_OPEN) {
$class .= ' handle-status-'.$this->status;
$title = $this->status;
}
if ($this->disabled) {
$class .= ' handle-disabled';
$title = 'disabled'; // Overwrite status.
}
return phutil_render_tag(
'a',
array(
'href' => $this->getURI(),
'class' => $class,
'title' => $title,
),
phutil_escape_html($name));
}
public function getLinkName() {
switch ($this->getType()) {
case PhabricatorPHIDConstants::PHID_TYPE_USER:
case PhabricatorPHIDConstants::PHID_TYPE_CMIT:
$name = $this->getName();
break;
default:
$name = $this->getFullName();
break;
}
return $name;
}
}
diff --git a/src/applications/phid/PhabricatorPHIDConstants.php b/src/applications/phid/PhabricatorPHIDConstants.php
index 27318a06fb..e1926fca49 100644
--- a/src/applications/phid/PhabricatorPHIDConstants.php
+++ b/src/applications/phid/PhabricatorPHIDConstants.php
@@ -1,47 +1,31 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorPHIDConstants {
const PHID_TYPE_USER = 'USER';
const PHID_TYPE_MLST = 'MLST';
const PHID_TYPE_DREV = 'DREV';
const PHID_TYPE_TASK = 'TASK';
const PHID_TYPE_FILE = 'FILE';
const PHID_TYPE_PROJ = 'PROJ';
const PHID_TYPE_UNKNOWN = '????';
const PHID_TYPE_MAGIC = '!!!!';
const PHID_TYPE_REPO = 'REPO';
const PHID_TYPE_CMIT = 'CMIT';
const PHID_TYPE_OPKG = 'OPKG';
const PHID_TYPE_PSTE = 'PSTE';
const PHID_TYPE_STRY = 'STRY';
const PHID_TYPE_POLL = 'POLL';
const PHID_TYPE_WIKI = 'WIKI';
const PHID_TYPE_APRJ = 'APRJ';
const PHID_TYPE_ACMT = 'ACMT';
const PHID_TYPE_DRYR = 'DRYR';
const PHID_TYPE_DRYL = 'DRYL';
const PHID_TYPE_OASC = 'OASC';
const PHID_TYPE_OASA = 'OASA';
const PHID_TYPE_POST = 'POST';
const PHID_TYPE_TOBJ = 'TOBJ';
const PHID_TYPE_BLOG = 'BLOG';
const PHID_TYPE_QUES = 'QUES';
const PHID_TYPE_ANSW = 'ANSW';
}
diff --git a/src/applications/phid/application/PhabricatorApplicationPHID.php b/src/applications/phid/application/PhabricatorApplicationPHID.php
index 5da3a8dd5a..2a982ac229 100644
--- a/src/applications/phid/application/PhabricatorApplicationPHID.php
+++ b/src/applications/phid/application/PhabricatorApplicationPHID.php
@@ -1,53 +1,37 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationPHID extends PhabricatorApplication {
public function getName() {
return 'PHID Manager';
}
public function getBaseURI() {
return '/phid/';
}
public function getAutospriteName() {
return 'phid';
}
public function getShortDescription() {
return 'Lookup PHIDs';
}
public function getTitleGlyph() {
return "#";
}
public function getApplicationGroup() {
return self::GROUP_DEVELOPER;
}
public function getRoutes() {
return array(
'/phid/' => array(
'' => 'PhabricatorPHIDLookupController',
),
);
}
}
diff --git a/src/applications/phid/controller/PhabricatorPHIDController.php b/src/applications/phid/controller/PhabricatorPHIDController.php
index f771b278f7..2da5ff09a5 100644
--- a/src/applications/phid/controller/PhabricatorPHIDController.php
+++ b/src/applications/phid/controller/PhabricatorPHIDController.php
@@ -1,34 +1,18 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorPHIDController extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('PHID');
$page->setBaseURI('/phid/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph('#');
$page->appendChild($view);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
}
diff --git a/src/applications/phid/controller/PhabricatorPHIDLookupController.php b/src/applications/phid/controller/PhabricatorPHIDLookupController.php
index 524bccc80e..7fa5e64e12 100644
--- a/src/applications/phid/controller/PhabricatorPHIDLookupController.php
+++ b/src/applications/phid/controller/PhabricatorPHIDLookupController.php
@@ -1,103 +1,87 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorPHIDLookupController
extends PhabricatorPHIDController {
public function processRequest() {
$request = $this->getRequest();
$phids = $request->getStrList('phids');
if ($phids) {
$handles = $this->loadViewerHandles($phids);
$rows = array();
foreach ($handles as $handle) {
if ($handle->getURI()) {
$link = phutil_render_tag(
'a',
array(
'href' => $handle->getURI(),
),
phutil_escape_html($handle->getURI()));
} else {
$link = null;
}
$rows[] = array(
phutil_escape_html($handle->getPHID()),
phutil_escape_html($handle->getType()),
phutil_escape_html($handle->getName()),
$link,
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'PHID',
'Type',
'Name',
'URI',
));
$table->setColumnClasses(
array(
null,
null,
null,
'wide',
));
$panel = new AphrontPanelView();
$panel->setHeader('PHID Handles');
$panel->appendChild($table);
return $this->buildStandardPageResponse(
$panel,
array(
'title' => 'PHID Lookup Results',
));
}
$lookup_form = new AphrontFormView();
$lookup_form->setUser($request->getUser());
$lookup_form
->setAction('/phid/')
->appendChild(
id(new AphrontFormTextAreaControl())
->setName('phids')
->setCaption('Enter PHIDs separated by spaces or commas.'))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Lookup PHIDs'));
$lookup_panel = new AphrontPanelView();
$lookup_panel->setHeader('Lookup PHIDs');
$lookup_panel->appendChild($lookup_form);
$lookup_panel->setWidth(AphrontPanelView::WIDTH_WIDE);
return $this->buildStandardPageResponse(
array(
$lookup_panel,
),
array(
'title' => 'PHID Lookup',
));
}
}
diff --git a/src/applications/phid/handle/PhabricatorObjectHandleData.php b/src/applications/phid/handle/PhabricatorObjectHandleData.php
index c7a9ae2888..aac83ecf40 100644
--- a/src/applications/phid/handle/PhabricatorObjectHandleData.php
+++ b/src/applications/phid/handle/PhabricatorObjectHandleData.php
@@ -1,593 +1,577 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorObjectHandleData {
private $phids;
private $viewer;
public function __construct(array $phids) {
$this->phids = array_unique($phids);
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public static function loadOneHandle($phid, $viewer = null) {
$query = new PhabricatorObjectHandleData(array($phid));
if ($viewer) {
$query->setViewer($viewer);
}
$handles = $query->loadHandles();
return $handles[$phid];
}
public function loadObjects() {
$types = array();
foreach ($this->phids as $phid) {
$type = phid_get_type($phid);
$types[$type][] = $phid;
}
$objects = array();
foreach ($types as $type => $phids) {
switch ($type) {
case PhabricatorPHIDConstants::PHID_TYPE_USER:
$user_dao = new PhabricatorUser();
$users = $user_dao->loadAllWhere(
'phid in (%Ls)',
$phids);
foreach ($users as $user) {
$objects[$user->getPHID()] = $user;
}
break;
case PhabricatorPHIDConstants::PHID_TYPE_CMIT:
$commit_dao = new PhabricatorRepositoryCommit();
$commits = $commit_dao->loadAllWhere(
'phid IN (%Ls)',
$phids);
$commit_data = array();
if ($commits) {
$data_dao = new PhabricatorRepositoryCommitData();
$commit_data = $data_dao->loadAllWhere(
'commitID IN (%Ld)',
mpull($commits, 'getID'));
$commit_data = mpull($commit_data, null, 'getCommitID');
}
foreach ($commits as $commit) {
$data = idx($commit_data, $commit->getID());
if ($data) {
$commit->attachCommitData($data);
$objects[$commit->getPHID()] = $commit;
} else {
// If we couldn't load the commit data, just act as though we
// couldn't load the object at all so we don't load half an object.
}
}
break;
case PhabricatorPHIDConstants::PHID_TYPE_TASK:
$task_dao = new ManiphestTask();
$tasks = $task_dao->loadAllWhere(
'phid IN (%Ls)',
$phids);
foreach ($tasks as $task) {
$objects[$task->getPHID()] = $task;
}
break;
case PhabricatorPHIDConstants::PHID_TYPE_DREV:
$revision_dao = new DifferentialRevision();
$revisions = $revision_dao->loadAllWhere(
'phid IN (%Ls)',
$phids);
foreach ($revisions as $revision) {
$objects[$revision->getPHID()] = $revision;
}
break;
case PhabricatorPHIDConstants::PHID_TYPE_QUES:
$questions = id(new PonderQuestionQuery())
->withPHIDs($phids)
->execute();
foreach ($questions as $question) {
$objects[$question->getPHID()] = $question;
}
break;
}
}
return $objects;
}
public function loadHandles() {
$types = phid_group_by_type($this->phids);
$handles = array();
$external_loaders = PhabricatorEnv::getEnvConfig('phid.external-loaders');
foreach ($types as $type => $phids) {
switch ($type) {
case PhabricatorPHIDConstants::PHID_TYPE_MAGIC:
// Black magic!
foreach ($phids as $phid) {
$handle = new PhabricatorObjectHandle();
$handle->setPHID($phid);
$handle->setType($type);
switch ($phid) {
case ManiphestTaskOwner::OWNER_UP_FOR_GRABS:
$handle->setName('Up For Grabs');
$handle->setFullName('upforgrabs (Up For Grabs)');
$handle->setComplete(true);
break;
case ManiphestTaskOwner::PROJECT_NO_PROJECT:
$handle->setName('No Project');
$handle->setFullName('noproject (No Project)');
$handle->setComplete(true);
break;
default:
$handle->setName('Foul Magicks');
break;
}
$handles[$phid] = $handle;
}
break;
case PhabricatorPHIDConstants::PHID_TYPE_USER:
$object = new PhabricatorUser();
$users = $object->loadAllWhere('phid IN (%Ls)', $phids);
$users = mpull($users, null, 'getPHID');
$image_phids = mpull($users, 'getProfileImagePHID');
$image_phids = array_unique(array_filter($image_phids));
$images = array();
if ($image_phids) {
$images = id(new PhabricatorFile())->loadAllWhere(
'phid IN (%Ls)',
$image_phids);
$images = mpull($images, 'getBestURI', 'getPHID');
}
$statuses = id(new PhabricatorUserStatus())->loadCurrentStatuses(
$phids);
foreach ($phids as $phid) {
$handle = new PhabricatorObjectHandle();
$handle->setPHID($phid);
$handle->setType($type);
if (empty($users[$phid])) {
$handle->setName('Unknown User');
} else {
$user = $users[$phid];
$handle->setName($user->getUsername());
$handle->setURI('/p/'.$user->getUsername().'/');
$handle->setFullName(
$user->getUsername().' ('.$user->getRealName().')');
$handle->setAlternateID($user->getID());
$handle->setComplete(true);
if (isset($statuses[$phid])) {
$status = $statuses[$phid]->getTextStatus();
if ($this->viewer) {
$date = $statuses[$phid]->getDateTo();
$status .= ' until '.phabricator_date($date, $this->viewer);
}
$handle->setStatus($status);
}
$handle->setDisabled($user->getIsDisabled());
$img_uri = idx($images, $user->getProfileImagePHID());
if ($img_uri) {
$handle->setImageURI($img_uri);
} else {
$handle->setImageURI(
PhabricatorUser::getDefaultProfileImageURI());
}
}
$handles[$phid] = $handle;
}
break;
case PhabricatorPHIDConstants::PHID_TYPE_MLST:
$object = new PhabricatorMetaMTAMailingList();
$lists = $object->loadAllWhere('phid IN (%Ls)', $phids);
$lists = mpull($lists, null, 'getPHID');
foreach ($phids as $phid) {
$handle = new PhabricatorObjectHandle();
$handle->setPHID($phid);
$handle->setType($type);
if (empty($lists[$phid])) {
$handle->setName('Unknown Mailing List');
} else {
$list = $lists[$phid];
$handle->setName($list->getName());
$handle->setURI($list->getURI());
$handle->setFullName($list->getName());
$handle->setComplete(true);
}
$handles[$phid] = $handle;
}
break;
case PhabricatorPHIDConstants::PHID_TYPE_DREV:
$object = new DifferentialRevision();
$revs = $object->loadAllWhere('phid in (%Ls)', $phids);
$revs = mpull($revs, null, 'getPHID');
foreach ($phids as $phid) {
$handle = new PhabricatorObjectHandle();
$handle->setPHID($phid);
$handle->setType($type);
if (empty($revs[$phid])) {
$handle->setName('Unknown Revision');
} else {
$rev = $revs[$phid];
$handle->setName($rev->getTitle());
$handle->setURI('/D'.$rev->getID());
$handle->setFullName('D'.$rev->getID().': '.$rev->getTitle());
$handle->setComplete(true);
$status = $rev->getStatus();
if (($status == ArcanistDifferentialRevisionStatus::CLOSED) ||
($status == ArcanistDifferentialRevisionStatus::ABANDONED)) {
$closed = PhabricatorObjectHandleStatus::STATUS_CLOSED;
$handle->setStatus($closed);
}
}
$handles[$phid] = $handle;
}
break;
case PhabricatorPHIDConstants::PHID_TYPE_CMIT:
$object = new PhabricatorRepositoryCommit();
$commits = $object->loadAllWhere('phid in (%Ls)', $phids);
$commits = mpull($commits, null, 'getPHID');
$repository_ids = array();
$callsigns = array();
if ($commits) {
$repository_ids = mpull($commits, 'getRepositoryID');
$repositories = id(new PhabricatorRepository())->loadAllWhere(
'id in (%Ld)', array_unique($repository_ids));
$callsigns = mpull($repositories, 'getCallsign');
}
foreach ($phids as $phid) {
$handle = new PhabricatorObjectHandle();
$handle->setPHID($phid);
$handle->setType($type);
if (empty($commits[$phid]) ||
!isset($callsigns[$repository_ids[$phid]])) {
$handle->setName('Unknown Commit');
} else {
$commit = $commits[$phid];
$callsign = $callsigns[$repository_ids[$phid]];
$repository = $repositories[$repository_ids[$phid]];
$commit_identifier = $commit->getCommitIdentifier();
// In case where the repository for the commit was deleted,
// we don't have have info about the repository anymore.
if ($repository) {
$name = $repository->formatCommitName($commit_identifier);
$handle->setName($name);
} else {
$handle->setName('Commit '.'r'.$callsign.$commit_identifier);
}
$handle->setURI('/r'.$callsign.$commit_identifier);
$handle->setFullName('r'.$callsign.$commit_identifier);
$handle->setTimestamp($commit->getEpoch());
$handle->setComplete(true);
}
$handles[$phid] = $handle;
}
break;
case PhabricatorPHIDConstants::PHID_TYPE_TASK:
$object = new ManiphestTask();
$tasks = $object->loadAllWhere('phid in (%Ls)', $phids);
$tasks = mpull($tasks, null, 'getPHID');
foreach ($phids as $phid) {
$handle = new PhabricatorObjectHandle();
$handle->setPHID($phid);
$handle->setType($type);
if (empty($tasks[$phid])) {
$handle->setName('Unknown Task');
} else {
$task = $tasks[$phid];
$handle->setName($task->getTitle());
$handle->setURI('/T'.$task->getID());
$handle->setFullName('T'.$task->getID().': '.$task->getTitle());
$handle->setComplete(true);
$handle->setAlternateID($task->getID());
if ($task->getStatus() != ManiphestTaskStatus::STATUS_OPEN) {
$closed = PhabricatorObjectHandleStatus::STATUS_CLOSED;
$handle->setStatus($closed);
}
}
$handles[$phid] = $handle;
}
break;
case PhabricatorPHIDConstants::PHID_TYPE_FILE:
$object = new PhabricatorFile();
$files = $object->loadAllWhere('phid IN (%Ls)', $phids);
$files = mpull($files, null, 'getPHID');
foreach ($phids as $phid) {
$handle = new PhabricatorObjectHandle();
$handle->setPHID($phid);
$handle->setType($type);
if (empty($files[$phid])) {
$handle->setName('Unknown File');
} else {
$file = $files[$phid];
$handle->setName($file->getName());
$handle->setURI($file->getBestURI());
$handle->setComplete(true);
}
$handles[$phid] = $handle;
}
break;
case PhabricatorPHIDConstants::PHID_TYPE_PROJ:
$object = new PhabricatorProject();
if ($this->viewer) {
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->viewer)
->withPHIDs($phids)
->execute();
} else {
$projects = $object->loadAllWhere('phid IN (%Ls)', $phids);
}
$projects = mpull($projects, null, 'getPHID');
foreach ($phids as $phid) {
$handle = new PhabricatorObjectHandle();
$handle->setPHID($phid);
$handle->setType($type);
if (empty($projects[$phid])) {
$handle->setName('Unknown Project');
} else {
$project = $projects[$phid];
$handle->setName($project->getName());
$handle->setURI('/project/view/'.$project->getID().'/');
$handle->setComplete(true);
}
$handles[$phid] = $handle;
}
break;
case PhabricatorPHIDConstants::PHID_TYPE_REPO:
$object = new PhabricatorRepository();
$repositories = $object->loadAllWhere('phid in (%Ls)', $phids);
$repositories = mpull($repositories, null, 'getPHID');
foreach ($phids as $phid) {
$handle = new PhabricatorObjectHandle();
$handle->setPHID($phid);
$handle->setType($type);
if (empty($repositories[$phid])) {
$handle->setName('Unknown Repository');
} else {
$repository = $repositories[$phid];
$handle->setName($repository->getCallsign());
$handle->setURI('/diffusion/'.$repository->getCallsign().'/');
$handle->setComplete(true);
}
$handles[$phid] = $handle;
}
break;
case PhabricatorPHIDConstants::PHID_TYPE_OPKG:
$object = new PhabricatorOwnersPackage();
$packages = $object->loadAllWhere('phid in (%Ls)', $phids);
$packages = mpull($packages, null, 'getPHID');
foreach ($phids as $phid) {
$handle = new PhabricatorObjectHandle();
$handle->setPHID($phid);
$handle->setType($type);
if (empty($packages[$phid])) {
$handle->setName('Unknown Package');
} else {
$package = $packages[$phid];
$handle->setName($package->getName());
$handle->setURI('/owners/package/'.$package->getID().'/');
$handle->setComplete(true);
}
$handles[$phid] = $handle;
}
break;
case PhabricatorPHIDConstants::PHID_TYPE_APRJ:
$project_dao = new PhabricatorRepositoryArcanistProject();
$projects = $project_dao->loadAllWhere(
'phid IN (%Ls)',
$phids);
$projects = mpull($projects, null, 'getPHID');
foreach ($phids as $phid) {
$handle = new PhabricatorObjectHandle();
$handle->setPHID($phid);
$handle->setType($type);
if (empty($projects[$phid])) {
$handle->setName('Unknown Arcanist Project');
} else {
$project = $projects[$phid];
$handle->setName($project->getName());
$handle->setComplete(true);
}
$handles[$phid] = $handle;
}
break;
case PhabricatorPHIDConstants::PHID_TYPE_WIKI:
$document_dao = new PhrictionDocument();
$content_dao = new PhrictionContent();
$conn = $document_dao->establishConnection('r');
$documents = queryfx_all(
$conn,
'SELECT * FROM %T document JOIN %T content
ON document.contentID = content.id
WHERE document.phid IN (%Ls)',
$document_dao->getTableName(),
$content_dao->getTableName(),
$phids);
$documents = ipull($documents, null, 'phid');
foreach ($phids as $phid) {
$handle = new PhabricatorObjectHandle();
$handle->setPHID($phid);
$handle->setType($type);
if (empty($documents[$phid])) {
$handle->setName('Unknown Document');
} else {
$info = $documents[$phid];
$handle->setName($info['title']);
$handle->setURI(PhrictionDocument::getSlugURI($info['slug']));
$handle->setComplete(true);
}
$handles[$phid] = $handle;
}
break;
case PhabricatorPHIDConstants::PHID_TYPE_QUES:
$questions = id(new PonderQuestionQuery())
->withPHIDs($phids)
->execute();
$questions = mpull($questions, null, 'getPHID');
foreach ($phids as $phid) {
$handle = new PhabricatorObjectHandle();
$handle->setPHID($phid);
$handle->setType($type);
if (empty($questions[$phid])) {
$handle->setName('Unknown Ponder Question');
} else {
$question = $questions[$phid];
$handle->setName(phutil_utf8_shorten($question->getTitle(), 60));
$handle->setURI(new PhutilURI('/Q' . $question->getID()));
$handle->setComplete(true);
}
$handles[$phid] = $handle;
}
break;
case PhabricatorPHIDConstants::PHID_TYPE_PSTE:
$pastes = id(new PhabricatorPasteQuery())
->withPHIDs($phids)
->setViewer($this->viewer)
->execute();
$pastes = mpull($pastes, null, 'getPHID');
foreach ($phids as $phid) {
$handle = new PhabricatorObjectHandle();
$handle->setPHID($phid);
$handle->setType($type);
if (empty($pastes[$phid])) {
$handle->setName('Unknown Paste');
} else {
$paste = $pastes[$phid];
$handle->setName($paste->getTitle());
$handle->setFullName($paste->getFullName());
$handle->setURI('/P'.$paste->getID());
$handle->setComplete(true);
}
$handles[$phid] = $handle;
}
break;
case PhabricatorPHIDConstants::PHID_TYPE_BLOG:
$blogs = id(new PhameBlogQuery())
->withPHIDs($phids)
->setViewer($this->viewer)
->execute();
$blogs = mpull($blogs, null, 'getPHID');
foreach ($phids as $phid) {
$handle = new PhabricatorObjectHandle();
$handle->setPHID($phid);
$handle->setType($type);
if (empty($blogs[$phid])) {
$handle->setName('Unknown Blog');
} else {
$blog = $blogs[$phid];
$handle->setName($blog->getName());
$handle->setFullName($blog->getName());
$handle->setURI('/phame/blog/view/'.$blog->getID().'/');
$handle->setComplete(true);
}
$handles[$phid] = $handle;
}
break;
case PhabricatorPHIDConstants::PHID_TYPE_POST:
$posts = id(new PhamePostQuery())
->withPHIDs($phids)
->setViewer($this->viewer)
->execute();
$posts = mpull($posts, null, 'getPHID');
foreach ($phids as $phid) {
$handle = new PhabricatorObjectHandle();
$handle->setPHID($phid);
$handle->setType($type);
if (empty($posts[$phid])) {
$handle->setName('Unknown Post');
} else {
$post = $posts[$phid];
$handle->setName($post->getTitle());
$handle->setFullName($post->getTitle());
$handle->setURI('/phame/post/view/'.$post->getID().'/');
$handle->setComplete(true);
}
$handles[$phid] = $handle;
}
break;
default:
$loader = null;
if (isset($external_loaders[$type])) {
$loader = $external_loaders[$type];
} else if (isset($external_loaders['*'])) {
$loader = $external_loaders['*'];
}
if ($loader) {
$object = newv($loader, array());
$handles += $object->loadHandles($phids);
break;
}
foreach ($phids as $phid) {
$handle = new PhabricatorObjectHandle();
$handle->setType($type);
$handle->setPHID($phid);
$handle->setName('Unknown Object');
$handle->setFullName('An Unknown Object');
$handles[$phid] = $handle;
}
break;
}
}
return $handles;
}
}
diff --git a/src/applications/phid/handle/const/PhabricatorObjectHandleConstants.php b/src/applications/phid/handle/const/PhabricatorObjectHandleConstants.php
index 347db8dfcc..053c86657c 100644
--- a/src/applications/phid/handle/const/PhabricatorObjectHandleConstants.php
+++ b/src/applications/phid/handle/const/PhabricatorObjectHandleConstants.php
@@ -1,21 +1,5 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorObjectHandleConstants {
}
diff --git a/src/applications/phid/handle/const/PhabricatorObjectHandleStatus.php b/src/applications/phid/handle/const/PhabricatorObjectHandleStatus.php
index 4b9e421e0c..37407428b4 100644
--- a/src/applications/phid/handle/const/PhabricatorObjectHandleStatus.php
+++ b/src/applications/phid/handle/const/PhabricatorObjectHandleStatus.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorObjectHandleStatus
extends PhabricatorObjectHandleConstants {
const STATUS_OPEN = 'open';
const STATUS_CLOSED = 'closed';
}
diff --git a/src/applications/phid/handle/view/PhabricatorHandleObjectSelectorDataView.php b/src/applications/phid/handle/view/PhabricatorHandleObjectSelectorDataView.php
index 2c0a14a115..ac0a31244b 100644
--- a/src/applications/phid/handle/view/PhabricatorHandleObjectSelectorDataView.php
+++ b/src/applications/phid/handle/view/PhabricatorHandleObjectSelectorDataView.php
@@ -1,35 +1,19 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorHandleObjectSelectorDataView {
private $handle;
public function __construct($handle) {
$this->handle = $handle;
}
public function renderData() {
$handle = $this->handle;
return array(
'phid' => $handle->getPHID(),
'name' => $handle->getFullName(),
'uri' => $handle->getURI(),
);
}
}
diff --git a/src/applications/phid/storage/PhabricatorPHID.php b/src/applications/phid/storage/PhabricatorPHID.php
index 45d6faf177..d73490846d 100644
--- a/src/applications/phid/storage/PhabricatorPHID.php
+++ b/src/applications/phid/storage/PhabricatorPHID.php
@@ -1,73 +1,57 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorPHID {
protected $phid;
protected $phidType;
protected $ownerPHID;
protected $parentPHID;
public static function generateNewPHID($type) {
if (!$type) {
throw new Exception("Can not generate PHID with no type.");
}
$uniq = Filesystem::readRandomCharacters(20);
return 'PHID-'.$type.'-'.$uniq;
}
public static function fromObjectName($name) {
$object = null;
$match = null;
if (preg_match('/^PHID-[A-Z]+-.{20}$/', $name)) {
// It's already a PHID! Yay.
return $name;
}
if (preg_match('/^r([A-Z]+)(\S*)$/', $name, $match)) {
$repository = id(new PhabricatorRepository())
->loadOneWhere('callsign = %s', $match[1]);
if ($match[2] == '') {
$object = $repository;
} else if ($repository) {
$object = id(new PhabricatorRepositoryCommit())->loadOneWhere(
'repositoryID = %d AND commitIdentifier = %s',
$repository->getID(),
$match[2]);
if (!$object) {
try {
$object = id(new PhabricatorRepositoryCommit())->loadOneWhere(
'repositoryID = %d AND commitIdentifier LIKE %>',
$repository->getID(),
$match[2]);
} catch (AphrontQueryCountException $ex) {
// Ambiguous; return nothing.
}
}
}
} else if (preg_match('/^d(\d+)$/i', $name, $match)) {
$object = id(new DifferentialRevision())->load($match[1]);
} else if (preg_match('/^t(\d+)$/i', $name, $match)) {
$object = id(new ManiphestTask())->load($match[1]);
}
if ($object) {
return $object->getPHID();
}
return null;
}
}
diff --git a/src/applications/phid/utils.php b/src/applications/phid/utils.php
index a006fb47c7..060c7add59 100644
--- a/src/applications/phid/utils.php
+++ b/src/applications/phid/utils.php
@@ -1,54 +1,38 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Look up the type of a PHID. Returns
* PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN if it fails to look up the type
*
* @param phid Anything.
* @return A value from PhabricatorPHIDConstants (ideally)
*/
function phid_get_type($phid) {
$matches = null;
if (is_string($phid) && preg_match('/^PHID-([^-]{4})-/', $phid, $matches)) {
return $matches[1];
}
return PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN;
}
/**
* Group a list of phids by type. Given:
*
* phid_group_by_type([PHID-USER-1, PHID-USER-2, PHID-PROJ-3])
*
* phid_group_by_type would return:
*
* [PhabricatorPHIDConstants::PHID_TYPE_USER => [PHID-USER-1, PHID-USER-2],
* PhabricatorPHIDConstants::PHID_TYPE_PROJ => [PHID-PROJ-3]]
*
* @param phids array of phids
* @return map of phid type => list of phids
*/
function phid_group_by_type($phids) {
$result = array();
foreach ($phids as $phid) {
$type = phid_get_type($phid);
$result[$type][] = $phid;
}
return $result;
}
diff --git a/src/applications/phortune/control/PhortuneMonthYearExpiryControl.php b/src/applications/phortune/control/PhortuneMonthYearExpiryControl.php
index ab55e6decd..47bf46fc34 100644
--- a/src/applications/phortune/control/PhortuneMonthYearExpiryControl.php
+++ b/src/applications/phortune/control/PhortuneMonthYearExpiryControl.php
@@ -1,116 +1,100 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhortuneMonthYearExpiryControl extends AphrontFormControl {
private $user;
private $monthValue;
private $yearValue;
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
private function getUser() {
return $this->user;
}
public function setMonthInputValue($value) {
$this->monthValue = $value;
return $this;
}
private function getMonthInputValue() {
return $this->monthValue;
}
private function getCurrentMonth() {
return phabricator_format_local_time(
time(),
$this->getUser(),
'm');
}
public function setYearInputValue($value) {
$this->yearValue = $value;
return $this;
}
private function getYearInputValue() {
return $this->yearValue;
}
private function getCurrentYear() {
return phabricator_format_local_time(
time(),
$this->getUser(),
'Y');
}
protected function getCustomControlClass() {
return 'aphront-form-control-text';
}
protected function renderInput() {
if (!$this->getUser()) {
throw new Exception('You must setUser() before render()!');
}
// represent months like a credit card does
$months = array(
'01' => '01',
'02' => '02',
'03' => '03',
'04' => '04',
'05' => '05',
'06' => '06',
'07' => '07',
'08' => '08',
'09' => '09',
'10' => '10',
'11' => '11',
'12' => '12',
);
$current_year = $this->getCurrentYear();
$years = range($current_year, $current_year + 20);
$years = array_combine($years, $years);
if ($this->getMonthInputValue()) {
$selected_month = $this->getMonthInputValue();
} else {
$selected_month = $this->getCurrentMonth();
}
$months_sel = AphrontFormSelectControl::renderSelectTag(
$selected_month,
$months,
array(
'sigil' => 'month-input',
));
$years_sel = AphrontFormSelectControl::renderSelectTag(
$this->getYearInputValue(),
$years,
array(
'sigil' => 'year-input',
));
return self::renderSingleView(
array(
$months_sel,
$years_sel
)
);
}
}
diff --git a/src/applications/phortune/stripe/controller/PhortuneStripeBaseController.php b/src/applications/phortune/stripe/controller/PhortuneStripeBaseController.php
index 858c9c6f72..f98c4087b1 100644
--- a/src/applications/phortune/stripe/controller/PhortuneStripeBaseController.php
+++ b/src/applications/phortune/stripe/controller/PhortuneStripeBaseController.php
@@ -1,33 +1,17 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhortuneStripeBaseController extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('Phortune - Stripe');
$page->setBaseURI('/phortune/stripe/');
$page->setTitle(idx($data, 'title'));
$page->appendChild($view);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
}
diff --git a/src/applications/phortune/stripe/controller/PhortuneStripeTestPaymentFormController.php b/src/applications/phortune/stripe/controller/PhortuneStripeTestPaymentFormController.php
index 94b23e1cd1..1d71456830 100644
--- a/src/applications/phortune/stripe/controller/PhortuneStripeTestPaymentFormController.php
+++ b/src/applications/phortune/stripe/controller/PhortuneStripeTestPaymentFormController.php
@@ -1,164 +1,148 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhortuneStripeTestPaymentFormController
extends PhortuneStripeBaseController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$title = 'Test Payment Form';
$error_view = null;
$card_number_error = null;
$card_cvc_error = null;
$card_expiration_error = null;
$stripe_key = $request->getStr('stripeKey');
if (!$stripe_key) {
$error_view = id(new AphrontErrorView())
->setTitle('Missing stripeKey parameter in URI');
}
if (!$error_view && $request->isFormPost()) {
$card_errors = $request->getStr('cardErrors');
$stripe_token = $request->getStr('stripeToken');
if ($card_errors) {
$raw_errors = json_decode($card_errors);
list($card_number_error,
$card_cvc_error,
$card_expiration_error,
$messages) = $this->parseRawErrors($raw_errors);
$error_view = id(new AphrontErrorView())
->setTitle('There were errors processing your card.')
->setErrors($messages);
} else if (!$stripe_token) {
// this shouldn't happen, so show the user a very generic error
// message and log that this error occurred...!
$error_view = id(new AphrontErrorView())
->setTitle('There was an unknown error processing your card.')
->setErrors(array('Please try again.'));
$error = 'payment form submitted but no stripe token and no errors';
$this->logStripeError($error);
} else {
// success -- do something with $stripe_token!!
}
} else if (!$error_view) {
$error_view = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
->setTitle(
'If you are using a test stripe key, use 4242424242424242, '.
'any three digits for CVC, and any valid expiration date to '.
'test!');
}
$view = id(new AphrontPanelView())
->setWidth(AphrontPanelView::WIDTH_FORM)
->setHeader($title);
$form = id(new PhortuneStripePaymentFormView())
->setUser($user)
->setStripeKey($stripe_key)
->setCardNumberError($card_number_error)
->setCardCVCError($card_cvc_error)
->setCardExpirationError($card_expiration_error);
$view->appendChild($form);
return
$this->buildStandardPageResponse(
array(
$error_view,
$view,
),
array(
'title' => $title,
)
);
}
/**
* Stripe JS and calls to Stripe handle all errors with processing this
* form. This function takes the raw errors - in the form of an array
* where each elementt is $type => $message - and figures out what if
* any fields were invalid and pulls the messages into a flat object.
*
* See https://stripe.com/docs/api#errors for more information on possible
* errors.
*/
private function parseRawErrors($errors) {
$card_number_error = null;
$card_cvc_error = null;
$card_expiration_error = null;
$messages = array();
foreach ($errors as $index => $error) {
$type = key($error);
$msg = reset($error);
$messages[] = $msg;
switch ($type) {
case 'number':
case 'invalid_number':
case 'incorrect_number':
$card_number_error = true;
break;
case 'cvc':
case 'invalid_cvc':
case 'incorrect_cvc':
$card_cvc_error = true;
break;
case 'expiry':
case 'invalid_expiry_month':
case 'invalid_expiry_year':
$card_expiration_error = true;
break;
case 'card_declined':
case 'expired_card':
case 'duplicate_transaction':
case 'processing_error':
// these errors don't map well to field(s) being bad
break;
case 'invalid_amount':
case 'missing':
default:
// these errors only happen if we (not the user) messed up so log it
$error = sprintf(
'error_type: %s error_message: %s',
$type,
$msg
);
$this->logStripeError($error);
break;
}
}
// append a helpful "fix this" to the messages to be displayed to the user
$messages[] = pht(
'Please fix these errors and try again.',
count($messages));
return array(
$card_number_error,
$card_cvc_error,
$card_expiration_error,
$messages
);
}
private function logStripeError($message) {
phlog('STRIPE-ERROR '.$message);
}
}
diff --git a/src/applications/phortune/stripe/view/PhortuneStripePaymentFormView.php b/src/applications/phortune/stripe/view/PhortuneStripePaymentFormView.php
index 0c2c4106f2..37232e92ef 100644
--- a/src/applications/phortune/stripe/view/PhortuneStripePaymentFormView.php
+++ b/src/applications/phortune/stripe/view/PhortuneStripePaymentFormView.php
@@ -1,158 +1,142 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhortuneStripePaymentFormView extends AphrontView {
private $user;
private $stripeKey;
private $cardNumberError;
private $cardCVCError;
private $cardExpirationError;
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
private function getUser() {
return $this->user;
}
public function setStripeKey($key) {
$this->stripeKey = $key;
return $this;
}
private function getStripeKey() {
return $this->stripeKey;
}
public function setCardNumberError($error) {
$this->cardNumberError = $error;
return $this;
}
private function getCardNumberError() {
return $this->cardNumberError;
}
public function setCardCVCError($error) {
$this->cardCVCError = $error;
return $this;
}
private function getCardCVCError() {
return $this->cardCVCError;
}
public function setCardExpirationError($error) {
$this->cardExpirationError = $error;
return $this;
}
private function getCardExpirationError() {
return $this->cardExpirationError;
}
public function render() {
$form_id = celerity_generate_unique_node_id();
require_celerity_resource('stripe-payment-form-css');
require_celerity_resource('aphront-tooltip-css');
Javelin::initBehavior('phabricator-tooltips');
$form = id(new AphrontFormView())
->setID($form_id)
->setUser($this->getUser())
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel('')
->setValue(
javelin_render_tag(
'div',
array(
'class' => 'credit-card-logos',
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => 'We support Visa, Mastercard, American Express, '.
'Discover, JCB, and Diners Club.',
'size' => 440,
)
)
)
)
)
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Card Number')
->setDisableAutocomplete(true)
->setSigil('number-input')
->setError($this->getCardNumberError())
)
->appendChild(
id(new AphrontFormTextControl())
->setLabel('CVC')
->setDisableAutocomplete(true)
->setSigil('cvc-input')
->setError($this->getCardCVCError())
)
->appendChild(
id(new PhortuneMonthYearExpiryControl())
->setLabel('Expiration')
->setUser($this->getUser())
->setError($this->getCardExpirationError())
)
->appendChild(
javelin_render_tag(
'input',
array(
'hidden' => true,
'name' => 'stripeToken',
'sigil' => 'stripe-token-input',
)
)
)
->appendChild(
javelin_render_tag(
'input',
array(
'hidden' => true,
'name' => 'cardErrors',
'sigil' => 'card-errors-input'
)
)
)
->appendChild(
phutil_render_tag(
'input',
array(
'hidden' => true,
'name' => 'stripeKey',
'value' => $this->getStripeKey(),
)
)
)
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Submit Payment')
);
Javelin::initBehavior(
'stripe-payment-form',
array(
'stripePublishKey' => $this->getStripeKey(),
'root' => $form_id,
)
);
return $form->render();
}
}
diff --git a/src/applications/phriction/application/PhabricatorApplicationPhriction.php b/src/applications/phriction/application/PhabricatorApplicationPhriction.php
index 77e26f6997..c5bde16d41 100644
--- a/src/applications/phriction/application/PhabricatorApplicationPhriction.php
+++ b/src/applications/phriction/application/PhabricatorApplicationPhriction.php
@@ -1,73 +1,57 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationPhriction extends PhabricatorApplication {
public function getShortDescription() {
return 'Wiki';
}
public function getBaseURI() {
return '/w/';
}
public function getAutospriteName() {
return 'phriction';
}
public function getHelpURI() {
return PhabricatorEnv::getDoclink('article/Phriction_User_Guide.html');
}
public function isEnabled() {
return PhabricatorEnv::getEnvConfig('phriction.enabled');
}
public function getRoutes() {
return array(
// Match "/w/" with slug "/".
'/w(?P<slug>/)' => 'PhrictionDocumentController',
// Match "/w/x/y/z/" with slug "x/y/z/".
'/w/(?P<slug>.+/)' => 'PhrictionDocumentController',
'/phriction/' => array(
'' => 'PhrictionListController',
'list/(?P<view>[^/]+)/' => 'PhrictionListController',
'history(?P<slug>/)' => 'PhrictionHistoryController',
'history/(?P<slug>.+/)' => 'PhrictionHistoryController',
'edit/(?:(?P<id>[1-9]\d*)/)?' => 'PhrictionEditController',
'delete/(?P<id>[1-9]\d*)/' => 'PhrictionDeleteController',
'preview/' => 'PhrictionDocumentPreviewController',
'diff/(?P<id>[1-9]\d*)/' => 'PhrictionDiffController',
),
);
}
public function getApplicationGroup() {
return self::GROUP_COMMUNICATION;
}
public function getApplicationOrder() {
return 0.140;
}
}
diff --git a/src/applications/phriction/constants/PhrictionActionConstants.php b/src/applications/phriction/constants/PhrictionActionConstants.php
index 5db36e7ada..d8681224a7 100644
--- a/src/applications/phriction/constants/PhrictionActionConstants.php
+++ b/src/applications/phriction/constants/PhrictionActionConstants.php
@@ -1,38 +1,22 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phriction
*/
final class PhrictionActionConstants extends PhrictionConstants {
const ACTION_CREATE = 'create';
const ACTION_EDIT = 'edit';
const ACTION_DELETE = 'delete';
public static function getActionPastTenseVerb($action) {
static $map = array(
self::ACTION_CREATE => 'created',
self::ACTION_EDIT => 'edited',
self::ACTION_DELETE => 'deleted',
);
return idx($map, $action, "brazenly {$action}'d");
}
}
diff --git a/src/applications/phriction/constants/PhrictionChangeType.php b/src/applications/phriction/constants/PhrictionChangeType.php
index dfe400d94c..867e805f7d 100644
--- a/src/applications/phriction/constants/PhrictionChangeType.php
+++ b/src/applications/phriction/constants/PhrictionChangeType.php
@@ -1,40 +1,24 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phriction
*/
final class PhrictionChangeType extends PhrictionConstants {
const CHANGE_EDIT = 0;
const CHANGE_DELETE = 1;
const CHANGE_MOVE_HERE = 2;
const CHANGE_MOVE_AWAY = 3;
public static function getChangeTypeLabel($const) {
static $map = array(
self::CHANGE_EDIT => 'Edit',
self::CHANGE_DELETE => 'Delete',
self::CHANGE_MOVE_HERE => 'Move Here',
self::CHANGE_MOVE_AWAY => 'Move Away',
);
return idx($map, $const, '???');
}
}
diff --git a/src/applications/phriction/constants/PhrictionConstants.php b/src/applications/phriction/constants/PhrictionConstants.php
index 76f98e4109..445c453d30 100644
--- a/src/applications/phriction/constants/PhrictionConstants.php
+++ b/src/applications/phriction/constants/PhrictionConstants.php
@@ -1,24 +1,8 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phriction
*/
abstract class PhrictionConstants {
}
diff --git a/src/applications/phriction/constants/PhrictionDocumentStatus.php b/src/applications/phriction/constants/PhrictionDocumentStatus.php
index 328dcb58b2..de75ee9107 100644
--- a/src/applications/phriction/constants/PhrictionDocumentStatus.php
+++ b/src/applications/phriction/constants/PhrictionDocumentStatus.php
@@ -1,36 +1,20 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phriction
*/
final class PhrictionDocumentStatus extends PhrictionConstants {
const STATUS_EXISTS = 0;
const STATUS_DELETED = 1;
public static function getConduitConstant($const) {
static $map = array(
self::STATUS_EXISTS => 'exists',
self::STATUS_DELETED => 'deleted',
);
return idx($map, $const, 'unknown');
}
}
diff --git a/src/applications/phriction/controller/PhrictionController.php b/src/applications/phriction/controller/PhrictionController.php
index 0c1e6c329e..32d9dea2cf 100644
--- a/src/applications/phriction/controller/PhrictionController.php
+++ b/src/applications/phriction/controller/PhrictionController.php
@@ -1,39 +1,23 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phriction
*/
abstract class PhrictionController extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('Phriction');
$page->setBaseURI('/w/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph("\xE2\x9A\xA1");
$page->appendChild($view);
$page->setSearchDefaultScope(PhabricatorSearchScope::SCOPE_WIKI);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
}
diff --git a/src/applications/phriction/controller/PhrictionDeleteController.php b/src/applications/phriction/controller/PhrictionDeleteController.php
index 7f2fdd1c75..13d43d25b8 100644
--- a/src/applications/phriction/controller/PhrictionDeleteController.php
+++ b/src/applications/phriction/controller/PhrictionDeleteController.php
@@ -1,61 +1,45 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phriction
*/
final class PhrictionDeleteController extends PhrictionController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$document = id(new PhrictionDocument())->load($this->id);
if (!$document) {
return new Aphront404Response();
}
$document_uri = PhrictionDocument::getSlugURI($document->getSlug());
if ($request->isFormPost()) {
$editor = id(PhrictionDocumentEditor::newForSlug($document->getSlug()))
->setActor($user)
->delete();
return id(new AphrontRedirectResponse())->setURI($document_uri);
}
$dialog = id(new AphrontDialogView())
->setUser($user)
->setTitle('Delete document?')
->appendChild(
'Really delete this document? You can recover it later by reverting '.
'to a previous version.')
->addSubmitButton('Delete')
->addCancelButton($document_uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/phriction/controller/PhrictionDiffController.php b/src/applications/phriction/controller/PhrictionDiffController.php
index b9b3a2dfba..ece241c40a 100644
--- a/src/applications/phriction/controller/PhrictionDiffController.php
+++ b/src/applications/phriction/controller/PhrictionDiffController.php
@@ -1,276 +1,260 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phriction
*/
final class PhrictionDiffController
extends PhrictionController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$document = id(new PhrictionDocument())->load($this->id);
if (!$document) {
return new Aphront404Response();
}
$current = id(new PhrictionContent())->load($document->getContentID());
$l = $request->getInt('l');
$r = $request->getInt('r');
$ref = $request->getStr('ref');
if ($ref) {
list($l, $r) = explode(',', $ref);
}
$content = id(new PhrictionContent())->loadAllWhere(
'documentID = %d AND version IN (%Ld)',
$document->getID(),
array($l, $r));
$content = mpull($content, null, 'getVersion');
$content_l = idx($content, $l, null);
$content_r = idx($content, $r, null);
if (!$content_l || !$content_r) {
return new Aphront404Response();
}
$text_l = $content_l->getContent();
$text_r = $content_r->getContent();
$text_l = wordwrap($text_l, 80);
$text_r = wordwrap($text_r, 80);
$engine = new PhabricatorDifferenceEngine();
$changeset = $engine->generateChangesetFromFileContent($text_l, $text_r);
$changeset->setOldProperties(
array(
'Title' => $content_l->getTitle(),
));
$changeset->setNewProperties(
array(
'Title' => $content_r->getTitle(),
));
$whitespace_mode = DifferentialChangesetParser::WHITESPACE_SHOW_ALL;
$parser = new DifferentialChangesetParser();
$parser->setChangeset($changeset);
$parser->setRenderingReference("{$l},{$r}");
$parser->setWhitespaceMode($whitespace_mode);
$spec = $request->getStr('range');
list($range_s, $range_e, $mask) =
DifferentialChangesetParser::parseRangeSpecification($spec);
$output = $parser->render($range_s, $range_e, $mask);
if ($request->isAjax()) {
return id(new PhabricatorChangesetResponse())
->setRenderedChangeset($output);
}
require_celerity_resource('differential-changeset-view-css');
require_celerity_resource('syntax-highlighting-css');
require_celerity_resource('phriction-document-css');
Javelin::initBehavior('differential-show-more', array(
'uri' => '/phriction/diff/'.$document->getID().'/',
'whitespace' => $whitespace_mode,
));
$slug = $document->getSlug();
$revert_l = $this->renderRevertButton($content_l, $current);
$revert_r = $this->renderRevertButton($content_r, $current);
$crumbs = new AphrontCrumbsView();
$crumbs->setCrumbs(
array(
'Phriction',
phutil_render_tag(
'a',
array(
'href' => PhrictionDocument::getSlugURI($slug),
),
phutil_escape_html($current->getTitle())),
phutil_render_tag(
'a',
array(
'href' => '/phriction/history/'.$document->getSlug().'/',
),
'History'),
phutil_escape_html("Changes Between Version {$l} and Version {$r}"),
));
$comparison_table = $this->renderComparisonTable(
array(
$content_r,
$content_l,
));
$navigation_table = null;
if ($l + 1 == $r) {
$nav_l = ($l > 1);
$nav_r = ($r != $current->getVersion());
$uri = $request->getRequestURI();
if ($nav_l) {
$link_l = phutil_render_tag(
'a',
array(
'href' => $uri->alter('l', $l - 1)->alter('r', $r - 1),
),
"\xC2\xAB Previous Change");
} else {
$link_l = 'Original Change';
}
$link_r = null;
if ($nav_r) {
$link_r = phutil_render_tag(
'a',
array(
'href' => $uri->alter('l', $l + 1)->alter('r', $r + 1),
),
"Next Change \xC2\xBB");
} else {
$link_r = 'Most Recent Change';
}
$navigation_table =
'<table class="phriction-history-nav-table">
<tr>
<td class="nav-prev">'.$link_l.'</td>
<td class="nav-next">'.$link_r.'</td>
</tr>
</table>';
}
$output =
'<div class="phriction-document-history-diff">'.
$comparison_table->render().
'<br />'.
'<br />'.
$navigation_table.
'<table class="phriction-revert-table">'.
'<tr><td>'.$revert_l.'</td><td>'.$revert_r.'</td>'.
'</table>'.
$output.
'</div>';
return $this->buildStandardPageResponse(
array(
$crumbs,
$output,
),
array(
'title' => 'Document History',
));
}
private function renderRevertButton(
PhrictionContent $content,
PhrictionContent $current) {
$document_id = $content->getDocumentID();
$version = $content->getVersion();
if ($content->getChangeType() == PhrictionChangeType::CHANGE_DELETE) {
// Don't show an edit/revert button for changes which deleted the content
// since it's silly.
return null;
}
if ($content->getID() == $current->getID()) {
return phutil_render_tag(
'a',
array(
'href' => '/phriction/edit/'.$document_id.'/',
'class' => 'button',
),
'Edit Current Version');
}
return phutil_render_tag(
'a',
array(
'href' => '/phriction/edit/'.$document_id.'/?revert='.$version,
'class' => 'button',
),
'Revert to Version '.phutil_escape_html($version).'...');
}
private function renderComparisonTable(array $content) {
assert_instances_of($content, 'PhrictionContent');
$user = $this->getRequest()->getUser();
$phids = mpull($content, 'getAuthorPHID');
$handles = $this->loadViewerHandles($phids);
$rows = array();
foreach ($content as $c) {
$rows[] = array(
phabricator_date($c->getDateCreated(), $user),
phabricator_time($c->getDateCreated(), $user),
phutil_escape_html('Version '.$c->getVersion()),
$handles[$c->getAuthorPHID()]->renderLink(),
phutil_escape_html($c->getDescription()),
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Date',
'Time',
'Version',
'Author',
'Description',
));
$table->setColumnClasses(
array(
'',
'right',
'pri',
'',
'wide',
));
return $table;
}
}
diff --git a/src/applications/phriction/controller/PhrictionDocumentController.php b/src/applications/phriction/controller/PhrictionDocumentController.php
index 3ff4ea3b60..427873e174 100644
--- a/src/applications/phriction/controller/PhrictionDocumentController.php
+++ b/src/applications/phriction/controller/PhrictionDocumentController.php
@@ -1,399 +1,383 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phriction
*/
final class PhrictionDocumentController
extends PhrictionController {
private $slug;
public function willProcessRequest(array $data) {
$this->slug = $data['slug'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$slug = PhabricatorSlug::normalize($this->slug);
if ($slug != $this->slug) {
$uri = PhrictionDocument::getSlugURI($slug);
// Canonicalize pages to their one true URI.
return id(new AphrontRedirectResponse())->setURI($uri);
}
require_celerity_resource('phriction-document-css');
$document = id(new PhrictionDocument())->loadOneWhere(
'slug = %s',
$slug);
$breadcrumbs = $this->renderBreadcrumbs($slug);
$version_note = null;
if (!$document) {
if (PhrictionDocument::isProjectSlug($slug)) {
$project = id(new PhabricatorProject())->loadOneWhere(
'phrictionSlug = %s',
PhrictionDocument::getProjectSlugIdentifier($slug));
if (!$project) {
return new Aphront404Response();
}
}
$create_uri = '/phriction/edit/?slug='.$slug;
$create_sentence =
'You can <strong>'.
phutil_render_tag(
'a',
array(
'href' => $create_uri,
),
'create a new document').
'</strong>.';
$button = phutil_render_tag(
'a',
array(
'href' => $create_uri,
'class' => 'green button',
),
'Create Page');
$page_content =
'<div class="phriction-content">'.
'<em>No content here!</em><br />'.
'No document found at <tt>'.phutil_escape_html($slug).'</tt>. '.
$create_sentence.
'</div>';
$page_title = 'Page Not Found';
$buttons = $button;
} else {
$version = $request->getInt('v');
if ($version) {
$content = id(new PhrictionContent())->loadOneWhere(
'documentID = %d AND version = %d',
$document->getID(),
$version);
if (!$content) {
return new Aphront404Response();
}
if ($content->getID() != $document->getContentID()) {
$version_note = new AphrontErrorView();
$version_note->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
$version_note->setTitle('Older Version');
$version_note->appendChild(
'You are viewing an older version of this document, as it '.
'appeared on '.
phabricator_datetime($content->getDateCreated(), $user).'.');
}
} else {
$content = id(new PhrictionContent())->load($document->getContentID());
}
$page_title = $content->getTitle();
$project_phid = null;
if (PhrictionDocument::isProjectSlug($slug)) {
$project = id(new PhabricatorProject())->loadOneWhere(
'phrictionSlug = %s',
PhrictionDocument::getProjectSlugIdentifier($slug));
if ($project) {
$project_phid = $project->getPHID();
}
}
$phids = array_filter(
array(
$content->getAuthorPHID(),
$project_phid,
));
$handles = $this->loadViewerHandles($phids);
$age = time() - $content->getDateCreated();
$age = floor($age / (60 * 60 * 24));
if ($age < 1) {
$when = 'today';
} else if ($age == 1) {
$when = 'yesterday';
} else {
$when = "{$age} days ago";
}
$project_info = null;
if ($project_phid) {
$project_info =
'<br />This document is about the project '.
$handles[$project_phid]->renderLink().'.';
}
$byline =
'<div class="phriction-byline">'.
"Last updated {$when} by ".
$handles[$content->getAuthorPHID()]->renderLink().'.'.
$project_info.
'</div>';
$doc_status = $document->getStatus();
if ($doc_status == PhrictionDocumentStatus::STATUS_EXISTS) {
$core_content = $content->renderContent($user);
} else if ($doc_status == PhrictionDocumentStatus::STATUS_DELETED) {
$notice = new AphrontErrorView();
$notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
$notice->setTitle('Document Deleted');
$notice->appendChild(
'This document has been deleted. You can edit it to put new content '.
'here, or use history to revert to an earlier version.');
$core_content = $notice->render();
} else {
throw new Exception("Unknown document status '{$doc_status}'!");
}
$page_content =
'<div class="phriction-content">'.
$byline.
$core_content.
'</div>';
$edit_button = phutil_render_tag(
'a',
array(
'href' => '/phriction/edit/'.$document->getID().'/',
'class' => 'button',
),
'Edit Document');
$history_button = phutil_render_tag(
'a',
array(
'href' => PhrictionDocument::getSlugURI($slug, 'history'),
'class' => 'button grey',
),
'View History');
// these float right so history_button which is right most goes first
$buttons = $history_button.$edit_button;
}
if ($version_note) {
$version_note = $version_note->render();
}
$children = $this->renderChildren($slug);
$page =
'<div class="phriction-header">'.
$buttons.
'<h1>'.phutil_escape_html($page_title).'</h1>'.
$breadcrumbs.
'</div>'.
$version_note.
$page_content.
$children;
return $this->buildStandardPageResponse(
$page,
array(
'title' => 'Phriction - '.$page_title,
));
}
private function renderBreadcrumbs($slug) {
$ancestor_handles = array();
$ancestral_slugs = PhabricatorSlug::getAncestry($slug);
$ancestral_slugs[] = $slug;
if ($ancestral_slugs) {
$empty_slugs = array_fill_keys($ancestral_slugs, null);
$ancestors = id(new PhrictionDocument())->loadAllWhere(
'slug IN (%Ls)',
$ancestral_slugs);
$ancestors = mpull($ancestors, null, 'getSlug');
$ancestor_phids = mpull($ancestors, 'getPHID');
$handles = array();
if ($ancestor_phids) {
$handles = $this->loadViewerHandles($ancestor_phids);
}
$ancestor_handles = array();
foreach ($ancestral_slugs as $slug) {
if (isset($ancestors[$slug])) {
$ancestor_handles[] = $handles[$ancestors[$slug]->getPHID()];
} else {
$handle = new PhabricatorObjectHandle();
$handle->setName(PhabricatorSlug::getDefaultTitle($slug));
$handle->setURI(PhrictionDocument::getSlugURI($slug));
$ancestor_handles[] = $handle;
}
}
}
$breadcrumbs = array();
foreach ($ancestor_handles as $ancestor_handle) {
$breadcrumbs[] = $ancestor_handle->renderLink();
}
$list = phutil_render_tag(
'a',
array(
'href' => '/phriction/',
),
'Document Index');
return
'<div class="phriction-breadcrumbs">'.
$list.' &middot; '.
'<span class="phriction-document-crumbs">'.
implode(" \xC2\xBB ", $breadcrumbs).
'</span>'.
'</div>';
}
private function renderChildren($slug) {
$document_dao = new PhrictionDocument();
$content_dao = new PhrictionContent();
$conn = $document_dao->establishConnection('r');
$limit = 50;
$d_child = PhabricatorSlug::getDepth($slug) + 1;
$d_grandchild = PhabricatorSlug::getDepth($slug) + 2;
// Select children and grandchildren.
$children = queryfx_all(
$conn,
'SELECT d.slug, d.depth, c.title FROM %T d JOIN %T c
ON d.contentID = c.id
WHERE d.slug LIKE %> AND d.depth IN (%d, %d)
AND d.status = %d
ORDER BY d.depth, c.title LIMIT %d',
$document_dao->getTableName(),
$content_dao->getTableName(),
($slug == '/' ? '' : $slug),
$d_child,
$d_grandchild,
PhrictionDocumentStatus::STATUS_EXISTS,
$limit);
if (!$children) {
return;
}
// We're going to render in one of three modes to try to accommodate
// different information scales:
//
// - If we found fewer than $limit rows, we know we have all the children
// and grandchildren and there aren't all that many. We can just render
// everything.
// - If we found $limit rows but the results included some grandchildren,
// we just throw them out and render only the children, as we know we
// have them all.
// - If we found $limit rows and the results have no grandchildren, we
// have a ton of children. Render them and then let the user know that
// this is not an exhaustive list.
if (count($children) == $limit) {
$more_children = true;
foreach ($children as $child) {
if ($child['depth'] == $d_grandchild) {
$more_children = false;
}
}
$show_grandchildren = false;
} else {
$show_grandchildren = true;
$more_children = false;
}
$grandchildren = array();
foreach ($children as $key => $child) {
if ($child['depth'] == $d_child) {
continue;
} else {
unset($children[$key]);
if ($show_grandchildren) {
$ancestors = PhabricatorSlug::getAncestry($child['slug']);
$grandchildren[end($ancestors)][] = $child;
}
}
}
// Fill in any missing children.
$known_slugs = ipull($children, null, 'slug');
foreach ($grandchildren as $slug => $ignored) {
if (empty($known_slugs[$slug])) {
$children[] = array(
'slug' => $slug,
'depth' => $d_child,
'title' => PhabricatorSlug::getDefaultTitle($slug),
'empty' => true,
);
}
}
$children = isort($children, 'title');
$list = array();
$list[] = '<ul>';
foreach ($children as $child) {
$list[] = $this->renderChildDocumentLink($child);
$grand = idx($grandchildren, $child['slug'], array());
if ($grand) {
$list[] = '<ul>';
foreach ($grand as $grandchild) {
$list[] = $this->renderChildDocumentLink($grandchild);
}
$list[] = '</ul>';
}
}
if ($more_children) {
$list[] = '<li>More...</li>';
}
$list[] = '</ul>';
$list = implode("\n", $list);
return
'<div class="phriction-children">'.
'<div class="phriction-children-header">Document Hierarchy</div>'.
$list.
'</div>';
}
private function renderChildDocumentLink(array $info) {
$title = nonempty($info['title'], '(Untitled Document)');
$item = phutil_render_tag(
'a',
array(
'href' => PhrictionDocument::getSlugURI($info['slug']),
),
phutil_escape_html($title));
if (isset($info['empty'])) {
$item = '<em>'.$item.'</em>';
}
return '<li>'.$item.'</li>';
}
}
diff --git a/src/applications/phriction/controller/PhrictionDocumentPreviewController.php b/src/applications/phriction/controller/PhrictionDocumentPreviewController.php
index 51f5f314e0..6750994e10 100644
--- a/src/applications/phriction/controller/PhrictionDocumentPreviewController.php
+++ b/src/applications/phriction/controller/PhrictionDocumentPreviewController.php
@@ -1,45 +1,29 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phriction
*/
final class PhrictionDocumentPreviewController
extends PhrictionController {
public function processRequest() {
$request = $this->getRequest();
$document = $request->getStr('document');
$draft_key = $request->getStr('draftkey');
if ($draft_key) {
id(new PhabricatorDraft())
->setAuthorPHID($request->getUser()->getPHID())
->setDraftKey($draft_key)
->setDraft($document)
->replaceOrDelete();
}
$content_obj = new PhrictionContent();
$content_obj->setContent($document);
$content = $content_obj->renderContent($request->getUser());
return id(new AphrontAjaxResponse())->setContent($content);
}
}
diff --git a/src/applications/phriction/controller/PhrictionEditController.php b/src/applications/phriction/controller/PhrictionEditController.php
index 9e7c351417..774388fd1e 100644
--- a/src/applications/phriction/controller/PhrictionEditController.php
+++ b/src/applications/phriction/controller/PhrictionEditController.php
@@ -1,292 +1,276 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phriction
*/
final class PhrictionEditController
extends PhrictionController {
private $id;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
if ($this->id) {
$document = id(new PhrictionDocument())->load($this->id);
if (!$document) {
return new Aphront404Response();
}
$revert = $request->getInt('revert');
if ($revert) {
$content = id(new PhrictionContent())->loadOneWhere(
'documentID = %d AND version = %d',
$document->getID(),
$revert);
if (!$content) {
return new Aphront404Response();
}
} else {
$content = id(new PhrictionContent())->load($document->getContentID());
}
} else {
$slug = $request->getStr('slug');
$slug = PhabricatorSlug::normalize($slug);
if (!$slug) {
return new Aphront404Response();
}
$document = id(new PhrictionDocument())->loadOneWhere(
'slug = %s',
$slug);
if ($document) {
$content = id(new PhrictionContent())->load($document->getContentID());
} else {
if (PhrictionDocument::isProjectSlug($slug)) {
$project = id(new PhabricatorProject())->loadOneWhere(
'phrictionSlug = %s',
PhrictionDocument::getProjectSlugIdentifier($slug));
if (!$project) {
return new Aphront404Response();
}
}
$document = new PhrictionDocument();
$document->setSlug($slug);
$content = new PhrictionContent();
$content->setSlug($slug);
$default_title = PhabricatorSlug::getDefaultTitle($slug);
$content->setTitle($default_title);
}
}
if ($request->getBool('nodraft')) {
$draft = null;
$draft_key = null;
} else {
if ($document->getPHID()) {
$draft_key = $document->getPHID().':'.$content->getVersion();
} else {
$draft_key = 'phriction:'.$content->getSlug();
}
$draft = id(new PhabricatorDraft())->loadOneWhere(
'authorPHID = %s AND draftKey = %s',
$user->getPHID(),
$draft_key);
}
require_celerity_resource('phriction-document-css');
$e_title = true;
$notes = null;
$errors = array();
if ($request->isFormPost()) {
$title = $request->getStr('title');
$notes = $request->getStr('description');
if (!strlen($title)) {
$e_title = 'Required';
$errors[] = 'Document title is required.';
} else {
$e_title = null;
}
if ($document->getID()) {
if ($content->getTitle() == $title &&
$content->getContent() == $request->getStr('content')) {
$dialog = new AphrontDialogView();
$dialog->setUser($user);
$dialog->setTitle('No Edits');
$dialog->appendChild(
'<p>You did not make any changes to the document.</p>');
$dialog->addCancelButton($request->getRequestURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
} else if (!strlen($request->getStr('content'))) {
// We trigger this only for new pages. For existing pages, deleting
// all the content counts as deleting the page.
$dialog = new AphrontDialogView();
$dialog->setUser($user);
$dialog->setTitle('Empty Page');
$dialog->appendChild(
'<p>You can not create an empty document.</p>');
$dialog->addCancelButton($request->getRequestURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
if (!count($errors)) {
$editor = id(PhrictionDocumentEditor::newForSlug($document->getSlug()))
->setActor($user)
->setTitle($title)
->setContent($request->getStr('content'))
->setDescription($notes);
$editor->save();
if ($draft) {
$draft->delete();
}
$uri = PhrictionDocument::getSlugURI($document->getSlug());
return id(new AphrontRedirectResponse())->setURI($uri);
}
}
$error_view = null;
if ($errors) {
$error_view = id(new AphrontErrorView())
->setTitle('Form Errors')
->setErrors($errors);
}
if ($document->getID()) {
$panel_header = 'Edit Phriction Document';
$submit_button = 'Save Changes';
$delete_button = phutil_render_tag(
'a',
array(
'href' => '/phriction/delete/'.$document->getID().'/',
'class' => 'grey button',
),
'Delete Document');
} else {
$panel_header = 'Create New Phriction Document';
$submit_button = 'Create Document';
$delete_button = null;
}
$uri = $document->getSlug();
$uri = PhrictionDocument::getSlugURI($uri);
$uri = PhabricatorEnv::getProductionURI($uri);
$cancel_uri = PhrictionDocument::getSlugURI($document->getSlug());
if ($draft &&
strlen($draft->getDraft()) &&
($draft->getDraft() != $content->getContent())) {
$content_text = $draft->getDraft();
$discard = phutil_render_tag(
'a',
array(
'href' => $request->getRequestURI()->alter('nodraft', true),
),
'discard this draft');
$draft_note = new AphrontErrorView();
$draft_note->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
$draft_note->setTitle('Recovered Draft');
$draft_note->appendChild(
'<p>Showing a saved draft of your edits, you can '.$discard.'.</p>');
} else {
$content_text = $content->getContent();
$draft_note = null;
}
$form = id(new AphrontFormView())
->setUser($user)
->setWorkflow(true)
->setAction($request->getRequestURI()->getPath())
->addHiddenInput('slug', $document->getSlug())
->addHiddenInput('nodraft', $request->getBool('nodraft'))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Title')
->setValue($content->getTitle())
->setError($e_title)
->setName('title'))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('URI')
->setValue($uri))
->appendChild(
id(new PhabricatorRemarkupControl())
->setLabel('Content')
->setValue($content_text)
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)
->setName('content')
->setID('document-textarea'))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Edit Notes')
->setValue($notes)
->setError(null)
->setName('description'))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($cancel_uri)
->setValue($submit_button));
$panel = id(new AphrontPanelView())
->setWidth(AphrontPanelView::WIDTH_WIDE)
->setHeader($panel_header)
->appendChild($form);
if ($delete_button) {
$panel->addButton($delete_button);
}
$preview_panel =
'<div class="aphront-panel-preview aphront-panel-preview-wide">
<div class="phriction-document-preview-header">
Document Preview
</div>
<div id="document-preview">
<div class="aphront-panel-preview-loading-text">
Loading preview...
</div>
</div>
</div>';
Javelin::initBehavior(
'phriction-document-preview',
array(
'preview' => 'document-preview',
'textarea' => 'document-textarea',
'uri' => '/phriction/preview/?draftkey='.$draft_key,
));
return $this->buildStandardPageResponse(
array(
$draft_note,
$error_view,
$panel,
$preview_panel,
),
array(
'title' => 'Edit Document',
));
}
}
diff --git a/src/applications/phriction/controller/PhrictionHistoryController.php b/src/applications/phriction/controller/PhrictionHistoryController.php
index 18be906fb0..6c1a6f2d58 100644
--- a/src/applications/phriction/controller/PhrictionHistoryController.php
+++ b/src/applications/phriction/controller/PhrictionHistoryController.php
@@ -1,170 +1,154 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phriction
*/
final class PhrictionHistoryController
extends PhrictionController {
private $slug;
public function willProcessRequest(array $data) {
$this->slug = $data['slug'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$document = id(new PhrictionDocument())->loadOneWhere(
'slug = %s',
PhabricatorSlug::normalize($this->slug));
if (!$document) {
return new Aphront404Response();
}
$current = id(new PhrictionContent())->load($document->getContentID());
$pager = new AphrontPagerView();
$pager->setOffset($request->getInt('page'));
$pager->setURI($request->getRequestURI(), 'page');
$history = id(new PhrictionContent())->loadAllWhere(
'documentID = %d ORDER BY version DESC LIMIT %d, %d',
$document->getID(),
$pager->getOffset(),
$pager->getPageSize() + 1);
$history = $pager->sliceResults($history);
$author_phids = mpull($history, 'getAuthorPHID');
$handles = $this->loadViewerHandles($author_phids);
$rows = array();
foreach ($history as $content) {
$slug_uri = PhrictionDocument::getSlugURI($document->getSlug());
$version = $content->getVersion();
$diff_uri = new PhutilURI('/phriction/diff/'.$document->getID().'/');
$vs_previous = '<em>Created</em>';
if ($content->getVersion() != 1) {
$uri = $diff_uri
->alter('l', $content->getVersion() - 1)
->alter('r', $content->getVersion());
$vs_previous = phutil_render_tag(
'a',
array(
'href' => $uri,
),
'Show Change');
}
$vs_head = '<em>Current</em>';
if ($content->getID() != $document->getContentID()) {
$uri = $diff_uri
->alter('l', $content->getVersion())
->alter('r', $current->getVersion());
$vs_head = phutil_render_tag(
'a',
array(
'href' => $uri,
),
'Show Later Changes');
}
$change_type = PhrictionChangeType::getChangeTypeLabel(
$content->getChangeType());
$rows[] = array(
phabricator_date($content->getDateCreated(), $user),
phabricator_time($content->getDateCreated(), $user),
phutil_render_tag(
'a',
array(
'href' => $slug_uri.'?v='.$version,
),
'Version '.$version),
$handles[$content->getAuthorPHID()]->renderLink(),
$change_type,
phutil_escape_html($content->getDescription()),
$vs_previous,
$vs_head,
);
}
$crumbs = new AphrontCrumbsView();
$crumbs->setCrumbs(
array(
'Phriction',
phutil_render_tag(
'a',
array(
'href' => PhrictionDocument::getSlugURI($document->getSlug()),
),
phutil_escape_html($current->getTitle())
),
'History',
));
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Date',
'Time',
'Version',
'Author',
'Type',
'Description',
'Against Previous',
'Against Current',
));
$table->setColumnClasses(
array(
'',
'right',
'pri',
'',
'',
'wide',
'',
'',
));
$panel = new AphrontPanelView();
$panel->setHeader('Document History');
$panel->appendChild($table);
$panel->appendChild($pager);
return $this->buildStandardPageResponse(
array(
$crumbs,
$panel,
),
array(
'title' => 'Document History',
));
}
}
diff --git a/src/applications/phriction/controller/PhrictionListController.php b/src/applications/phriction/controller/PhrictionListController.php
index 4306474e3f..255f82f79e 100644
--- a/src/applications/phriction/controller/PhrictionListController.php
+++ b/src/applications/phriction/controller/PhrictionListController.php
@@ -1,175 +1,159 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phriction
*/
final class PhrictionListController
extends PhrictionController {
private $view;
public function willProcessRequest(array $data) {
$this->view = idx($data, 'view');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$views = array(
'all' => 'All Documents',
'updates' => 'Recently Updated',
);
if (empty($views[$this->view])) {
$this->view = 'all';
}
$nav = new AphrontSideNavView();
foreach ($views as $view => $name) {
$nav->addNavItem(
phutil_render_tag(
'a',
array(
'href' => '/phriction/list/'.$view.'/',
'class' => ($this->view == $view)
? 'aphront-side-nav-selected'
: null,
),
phutil_escape_html($name)));
}
$pager = new AphrontPagerView();
$pager->setURI($request->getRequestURI(), 'page');
$pager->setOffset($request->getInt('page'));
$documents = $this->loadDocuments($pager);
$content = mpull($documents, 'getContent');
$phids = mpull($content, 'getAuthorPHID');
$handles = $this->loadViewerHandles($phids);
$rows = array();
foreach ($documents as $document) {
$content = $document->getContent();
$rows[] = array(
$handles[$content->getAuthorPHID()]->renderLink(),
phutil_render_tag(
'a',
array(
'href' => PhrictionDocument::getSlugURI($document->getSlug()),
),
phutil_escape_html($content->getTitle())),
phabricator_date($content->getDateCreated(), $user),
phabricator_time($content->getDateCreated(), $user),
);
}
$document_table = new AphrontTableView($rows);
$document_table->setHeaders(
array(
'Last Editor',
'Title',
'Last Update',
'Time',
));
$document_table->setColumnClasses(
array(
'',
'wide pri',
'right',
'right',
));
$view_headers = array(
'all' => 'All Documents',
'updates' => 'Recently Updated Documents',
);
$view_header = $view_headers[$this->view];
$panel = new AphrontPanelView();
$panel->setHeader($view_header);
$panel->appendChild($document_table);
$panel->appendChild($pager);
$nav->appendChild($panel);
return $this->buildStandardPageResponse($nav,
array(
'title' => 'Phriction Main'
));
}
private function loadDocuments(AphrontPagerView $pager) {
// TODO: Do we want/need a query object for this?
$document_dao = new PhrictionDocument();
$content_dao = new PhrictionContent();
$conn = $document_dao->establishConnection('r');
switch ($this->view) {
case 'all':
$data = queryfx_all(
$conn,
'SELECT * FROM %T ORDER BY id DESC LIMIT %d, %d',
$document_dao->getTableName(),
$pager->getOffset(),
$pager->getPageSize() + 1);
break;
case 'updates':
// TODO: This query is a little suspicious, verify we don't need to key
// or change it once we get more data.
$data = queryfx_all(
$conn,
'SELECT d.* FROM %T d JOIN %T c ON c.documentID = d.id
GROUP BY c.documentID
ORDER BY MAX(c.id) DESC LIMIT %d, %d',
$document_dao->getTableName(),
$content_dao->getTableName(),
$pager->getOffset(),
$pager->getPageSize() + 1);
break;
default:
throw new Exception("Unknown view '{$this->view}'!");
}
$data = $pager->sliceResults($data);
$documents = $document_dao->loadAllFromArray($data);
if ($documents) {
$content = $content_dao->loadAllWhere(
'documentID IN (%Ld)',
mpull($documents, 'getID'));
$content = mpull($content, null, 'getDocumentID');
foreach ($documents as $document) {
$document->attachContent($content[$document->getID()]);
}
}
return $documents;
}
}
diff --git a/src/applications/phriction/editor/PhrictionDocumentEditor.php b/src/applications/phriction/editor/PhrictionDocumentEditor.php
index adb9f52ac8..290a54745c 100644
--- a/src/applications/phriction/editor/PhrictionDocumentEditor.php
+++ b/src/applications/phriction/editor/PhrictionDocumentEditor.php
@@ -1,230 +1,214 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Create or update Phriction documents.
*
* @group phriction
*/
final class PhrictionDocumentEditor extends PhabricatorEditor {
private $document;
private $content;
private $newTitle;
private $newContent;
private $description;
private function __construct() {
// <restricted>
}
public static function newForSlug($slug) {
$slug = PhabricatorSlug::normalize($slug);
$document = id(new PhrictionDocument())->loadOneWhere(
'slug = %s',
$slug);
$content = null;
if ($document) {
$content = id(new PhrictionContent())->load($document->getContentID());
} else {
$document = new PhrictionDocument();
$document->setSlug($slug);
}
if (!$content) {
$default_title = PhabricatorSlug::getDefaultTitle($slug);
$content = new PhrictionContent();
$content->setSlug($slug);
$content->setTitle($default_title);
$content->setContent('');
}
$obj = new PhrictionDocumentEditor();
$obj->document = $document;
$obj->content = $content;
return $obj;
}
public function setTitle($title) {
$this->newTitle = $title;
return $this;
}
public function setContent($content) {
$this->newContent = $content;
return $this;
}
public function setDescription($description) {
$this->description = $description;
return $this;
}
public function getDocument() {
return $this->document;
}
public function delete() {
$actor = $this->requireActor();
// TODO: Should we do anything about deleting an already-deleted document?
// We currently allow it.
$document = $this->document;
$content = $this->content;
$new_content = $this->buildContentTemplate($document, $content);
$new_content->setChangeType(PhrictionChangeType::CHANGE_DELETE);
$new_content->setContent('');
return $this->updateDocument($document, $content, $new_content);
}
public function save() {
$actor = $this->requireActor();
if ($this->newContent === '') {
// If this is an edit which deletes all the content, just treat it as
// a delete. NOTE: null means "don't change the content", not "delete
// the page"! Thus the strict type check.
return $this->delete();
}
$document = $this->document;
$content = $this->content;
$new_content = $this->buildContentTemplate($document, $content);
return $this->updateDocument($document, $content, $new_content);
}
private function buildContentTemplate(
PhrictionDocument $document,
PhrictionContent $content) {
$new_content = new PhrictionContent();
$new_content->setSlug($document->getSlug());
$new_content->setAuthorPHID($this->getActor()->getPHID());
$new_content->setChangeType(PhrictionChangeType::CHANGE_EDIT);
$new_content->setTitle(
coalesce(
$this->newTitle,
$content->getTitle()));
$new_content->setContent(
coalesce(
$this->newContent,
$content->getContent()));
if (strlen($this->description)) {
$new_content->setDescription($this->description);
}
return $new_content;
}
private function updateDocument($document, $content, $new_content) {
$is_new = false;
if (!$document->getID()) {
$is_new = true;
}
$new_content->setVersion($content->getVersion() + 1);
$change_type = $new_content->getChangeType();
switch ($change_type) {
case PhrictionChangeType::CHANGE_EDIT:
$doc_status = PhrictionDocumentStatus::STATUS_EXISTS;
$feed_action = $is_new
? PhrictionActionConstants::ACTION_CREATE
: PhrictionActionConstants::ACTION_EDIT;
break;
case PhrictionChangeType::CHANGE_DELETE:
$doc_status = PhrictionDocumentStatus::STATUS_DELETED;
$feed_action = PhrictionActionConstants::ACTION_DELETE;
if ($is_new) {
throw new Exception(
"You can not delete a document which doesn't exist yet!");
}
break;
default:
throw new Exception(
"Unsupported content change type '{$change_type}'!");
}
$document->setStatus($doc_status);
// TODO: This should be transactional.
if ($is_new) {
$document->save();
}
$new_content->setDocumentID($document->getID());
$new_content->save();
$document->setContentID($new_content->getID());
$document->save();
$document->attachContent($new_content);
PhabricatorSearchPhrictionIndexer::indexDocument($document);
$project_phid = null;
$slug = $document->getSlug();
if (PhrictionDocument::isProjectSlug($slug)) {
$project = id(new PhabricatorProject())->loadOneWhere(
'phrictionSlug = %s',
PhrictionDocument::getProjectSlugIdentifier($slug));
if ($project) {
$project_phid = $project->getPHID();
}
}
$related_phids = array(
$document->getPHID(),
$this->getActor()->getPHID(),
);
if ($project_phid) {
$related_phids[] = $project_phid;
}
id(new PhabricatorFeedStoryPublisher())
->setRelatedPHIDs($related_phids)
->setStoryAuthorPHID($this->getActor()->getPHID())
->setStoryTime(time())
->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_PHRICTION)
->setStoryData(
array(
'phid' => $document->getPHID(),
'action' => $feed_action,
'content' => phutil_utf8_shorten($new_content->getContent(), 140),
'project' => $project_phid,
))
->publish();
return $this;
}
}
diff --git a/src/applications/phriction/storage/PhrictionContent.php b/src/applications/phriction/storage/PhrictionContent.php
index 18d907db69..0307455b7f 100644
--- a/src/applications/phriction/storage/PhrictionContent.php
+++ b/src/applications/phriction/storage/PhrictionContent.php
@@ -1,119 +1,103 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @task markup Markup Interface
*
* @group phriction
*/
final class PhrictionContent extends PhrictionDAO
implements PhabricatorMarkupInterface {
const MARKUP_FIELD_BODY = 'markup:body';
protected $id;
protected $documentID;
protected $version;
protected $authorPHID;
protected $title;
protected $slug;
protected $content;
protected $description;
protected $changeType;
protected $changeRef;
public function renderContent(PhabricatorUser $viewer) {
return PhabricatorMarkupEngine::renderOneObject(
$this,
self::MARKUP_FIELD_BODY,
$viewer);
}
/* -( Markup Interface )--------------------------------------------------- */
/**
* @task markup
*/
public function getMarkupFieldKey($field) {
if ($this->shouldUseMarkupCache($field)) {
$id = $this->getID();
} else {
$id = PhabricatorHash::digest($this->getMarkupText($field));
}
return "phriction:{$field}:{$id}";
}
/**
* @task markup
*/
public function getMarkupText($field) {
return $this->getContent();
}
/**
* @task markup
*/
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newPhrictionMarkupEngine();
}
/**
* @task markup
*/
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
$toc = PhutilRemarkupEngineRemarkupHeaderBlockRule::renderTableOfContents(
$engine);
if ($toc) {
$toc =
'<div class="phabricator-remarkup-toc">'.
'<div class="phabricator-remarkup-toc-header">'.
'Table of Contents'.
'</div>'.
$toc.
'</div>';
}
return
'<div class="phabricator-remarkup">'.
$toc.
$output.
'</div>';
}
/**
* @task markup
*/
public function shouldUseMarkupCache($field) {
return (bool)$this->getID();
}
}
diff --git a/src/applications/phriction/storage/PhrictionDAO.php b/src/applications/phriction/storage/PhrictionDAO.php
index 4b257a5ba8..76a6338127 100644
--- a/src/applications/phriction/storage/PhrictionDAO.php
+++ b/src/applications/phriction/storage/PhrictionDAO.php
@@ -1,28 +1,12 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phriction
*/
abstract class PhrictionDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'phriction';
}
}
diff --git a/src/applications/phriction/storage/PhrictionDocument.php b/src/applications/phriction/storage/PhrictionDocument.php
index f71b5d8543..21f742c088 100644
--- a/src/applications/phriction/storage/PhrictionDocument.php
+++ b/src/applications/phriction/storage/PhrictionDocument.php
@@ -1,102 +1,86 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phriction
*/
final class PhrictionDocument extends PhrictionDAO {
protected $id;
protected $phid;
protected $slug;
protected $depth;
protected $contentID;
protected $status;
private $contentObject;
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_TIMESTAMPS => false,
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_WIKI);
}
public static function getSlugURI($slug, $type = 'document') {
static $types = array(
'document' => '/w/',
'history' => '/phriction/history/',
);
if (empty($types[$type])) {
throw new Exception("Unknown URI type '{$type}'!");
}
$prefix = $types[$type];
if ($slug == '/') {
return $prefix;
} else {
return $prefix.$slug;
}
}
public function setSlug($slug) {
$this->slug = PhabricatorSlug::normalize($slug);
$this->depth = PhabricatorSlug::getDepth($slug);
return $this;
}
public function attachContent(PhrictionContent $content) {
$this->contentObject = $content;
return $this;
}
public function getContent() {
if (!$this->contentObject) {
throw new Exception("Attach content with attachContent() first.");
}
return $this->contentObject;
}
public static function isProjectSlug($slug) {
$slug = PhabricatorSlug::normalize($slug);
$prefix = 'projects/';
if ($slug == $prefix) {
// The 'projects/' document is not itself a project slug.
return false;
}
return !strncmp($slug, $prefix, strlen($prefix));
}
public static function getProjectSlugIdentifier($slug) {
if (!self::isProjectSlug($slug)) {
throw new Exception("Slug '{$slug}' is not a project slug!");
}
$slug = PhabricatorSlug::normalize($slug);
$parts = explode('/', $slug);
return $parts[1].'/';
}
}
diff --git a/src/applications/phriction/storage/__tests__/PhrictionDocumentTestCase.php b/src/applications/phriction/storage/__tests__/PhrictionDocumentTestCase.php
index 5bca5f230b..bca9507284 100644
--- a/src/applications/phriction/storage/__tests__/PhrictionDocumentTestCase.php
+++ b/src/applications/phriction/storage/__tests__/PhrictionDocumentTestCase.php
@@ -1,67 +1,51 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group phriction
*/
final class PhrictionDocumentTestCase extends PhabricatorTestCase {
public function testProjectSlugs() {
$slugs = array(
'/' => false,
'zebra/' => false,
'projects/' => false,
'projects/a/' => true,
'projects/a/b/' => true,
'stuff/projects/a/' => false,
);
foreach ($slugs as $slug => $expect) {
$this->assertEqual(
$expect,
PhrictionDocument::isProjectSlug($slug),
"Is '{$slug}' a project slug?");
}
}
public function testProjectSlugIdentifiers() {
$slugs = array(
'projects/' => null,
'derp/' => null,
'projects/a/' => 'a/',
'projects/a/b/' => 'a/',
);
foreach ($slugs as $slug => $expect) {
$ex = null;
$result = null;
try {
$result = PhrictionDocument::getProjectSlugIdentifier($slug);
} catch (Exception $e) {
$ex = $e;
}
if ($expect === null) {
$this->assertEqual(true, (bool)$ex, "Slug '{$slug}' is invalid.");
} else {
$this->assertEqual($expect, $result, "Slug '{$slug}' identifier.");
}
}
}
}
diff --git a/src/applications/policy/__tests__/PhabricatorPolicyAwareTestQuery.php b/src/applications/policy/__tests__/PhabricatorPolicyAwareTestQuery.php
index e1d869d09b..f4e37a6ef8 100644
--- a/src/applications/policy/__tests__/PhabricatorPolicyAwareTestQuery.php
+++ b/src/applications/policy/__tests__/PhabricatorPolicyAwareTestQuery.php
@@ -1,52 +1,36 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Configurable test query for implementing Policy unit tests.
*/
final class PhabricatorPolicyAwareTestQuery
extends PhabricatorPolicyAwareQuery {
private $results;
private $offset = 0;
public function setResults(array $results) {
$this->results = $results;
return $this;
}
protected function willExecute() {
$this->offset = 0;
}
public function loadPage() {
if ($this->getRawResultLimit()) {
return array_slice(
$this->results,
$this->offset,
$this->getRawResultLimit());
} else {
return array_slice($this->results, $this->offset);
}
}
public function nextPage(array $page) {
$this->offset += count($page);
}
}
diff --git a/src/applications/policy/__tests__/PhabricatorPolicyTestCase.php b/src/applications/policy/__tests__/PhabricatorPolicyTestCase.php
index 2eb49697a6..13bb00efbd 100644
--- a/src/applications/policy/__tests__/PhabricatorPolicyTestCase.php
+++ b/src/applications/policy/__tests__/PhabricatorPolicyTestCase.php
@@ -1,251 +1,235 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorPolicyTestCase extends PhabricatorTestCase {
/**
* Verify that any user can view an object with POLICY_PUBLIC.
*/
public function testPublicPolicyEnabled() {
$env = PhabricatorEnv::beginScopedEnv();
$env->overrideEnvConfig('policy.allow-public', true);
$this->expectVisibility(
$this->buildObject(PhabricatorPolicies::POLICY_PUBLIC),
array(
'public' => true,
'user' => true,
'admin' => true,
),
'Public Policy (Enabled in Config)');
}
/**
* Verify that POLICY_PUBLIC is interpreted as POLICY_USER when public
* policies are disallowed.
*/
public function testPublicPolicyDisabled() {
$env = PhabricatorEnv::beginScopedEnv();
$env->overrideEnvConfig('policy.allow-public', false);
$this->expectVisibility(
$this->buildObject(PhabricatorPolicies::POLICY_PUBLIC),
array(
'public' => false,
'user' => true,
'admin' => true,
),
'Public Policy (Disabled in Config)');
}
/**
* Verify that any logged-in user can view an object with POLICY_USER, but
* logged-out users can not.
*/
public function testUsersPolicy() {
$this->expectVisibility(
$this->buildObject(PhabricatorPolicies::POLICY_USER),
array(
'public' => false,
'user' => true,
'admin' => true,
),
'User Policy');
}
/**
* Verify that only administrators can view an object with POLICY_ADMIN.
*/
public function testAdminPolicy() {
$this->expectVisibility(
$this->buildObject(PhabricatorPolicies::POLICY_ADMIN),
array(
'public' => false,
'user' => false,
'admin' => true,
),
'Admin Policy');
}
/**
* Verify that no one can view an object with POLICY_NOONE.
*/
public function testNoOnePolicy() {
$this->expectVisibility(
$this->buildObject(PhabricatorPolicies::POLICY_NOONE),
array(
'public' => false,
'user' => false,
'admin' => false,
),
'No One Policy');
}
/**
* Test offset-based filtering.
*/
public function testOffsets() {
$results = array(
$this->buildObject(PhabricatorPolicies::POLICY_NOONE),
$this->buildObject(PhabricatorPolicies::POLICY_NOONE),
$this->buildObject(PhabricatorPolicies::POLICY_NOONE),
$this->buildObject(PhabricatorPolicies::POLICY_USER),
$this->buildObject(PhabricatorPolicies::POLICY_USER),
$this->buildObject(PhabricatorPolicies::POLICY_USER),
);
$query = new PhabricatorPolicyAwareTestQuery();
$query->setResults($results);
$query->setViewer($this->buildUser('user'));
$this->assertEqual(
3,
count($query->setLimit(3)->setOffset(0)->execute()),
'Invisible objects are ignored.');
$this->assertEqual(
0,
count($query->setLimit(3)->setOffset(3)->execute()),
'Offset pages through visible objects only.');
$this->assertEqual(
2,
count($query->setLimit(3)->setOffset(1)->execute()),
'Offsets work correctly.');
$this->assertEqual(
2,
count($query->setLimit(0)->setOffset(1)->execute()),
'Offset with no limit works.');
}
/**
* Test limits.
*/
public function testLimits() {
$results = array(
$this->buildObject(PhabricatorPolicies::POLICY_USER),
$this->buildObject(PhabricatorPolicies::POLICY_USER),
$this->buildObject(PhabricatorPolicies::POLICY_USER),
$this->buildObject(PhabricatorPolicies::POLICY_USER),
$this->buildObject(PhabricatorPolicies::POLICY_USER),
$this->buildObject(PhabricatorPolicies::POLICY_USER),
);
$query = new PhabricatorPolicyAwareTestQuery();
$query->setResults($results);
$query->setViewer($this->buildUser('user'));
$this->assertEqual(
3,
count($query->setLimit(3)->setOffset(0)->execute()),
'Limits work.');
$this->assertEqual(
2,
count($query->setLimit(3)->setOffset(4)->execute()),
'Limit + offset work.');
}
/**
* Test an object for visibility across multiple user specifications.
*/
private function expectVisibility(
PhabricatorPolicyTestObject $object,
array $map,
$description) {
foreach ($map as $spec => $expect) {
$viewer = $this->buildUser($spec);
$query = new PhabricatorPolicyAwareTestQuery();
$query->setResults(array($object));
$query->setViewer($viewer);
$caught = null;
try {
$result = $query->executeOne();
} catch (PhabricatorPolicyException $ex) {
$caught = $ex;
}
if ($expect) {
$this->assertEqual(
$object,
$result,
"{$description} with user {$spec} should succeed.");
} else {
$this->assertEqual(
true,
$caught instanceof PhabricatorPolicyException,
"{$description} with user {$spec} should fail.");
}
}
}
/**
* Build a test object to spec.
*/
private function buildObject($policy) {
$object = new PhabricatorPolicyTestObject();
$object->setCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
));
$object->setPolicies(
array(
PhabricatorPolicyCapability::CAN_VIEW => $policy,
));
return $object;
}
/**
* Build a test user to spec.
*/
private function buildUser($spec) {
$user = new PhabricatorUser();
switch ($spec) {
case 'public':
break;
case 'user':
$user->setPHID(1);
break;
case 'admin':
$user->setPHID(1);
$user->setIsAdmin(true);
break;
default:
throw new Exception("Unknown user spec '{$spec}'.");
}
return $user;
}
}
diff --git a/src/applications/policy/__tests__/PhabricatorPolicyTestObject.php b/src/applications/policy/__tests__/PhabricatorPolicyTestObject.php
index d9f310ff0d..826c9c3e54 100644
--- a/src/applications/policy/__tests__/PhabricatorPolicyTestObject.php
+++ b/src/applications/policy/__tests__/PhabricatorPolicyTestObject.php
@@ -1,57 +1,41 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Configurable test object for implementing Policy unit tests.
*/
final class PhabricatorPolicyTestObject
implements PhabricatorPolicyInterface {
private $capabilities = array();
private $policies = array();
private $automaticCapabilities = array();
public function getCapabilities() {
return $this->capabilities;
}
public function getPolicy($capability) {
return idx($this->policies, $capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
$auto = idx($this->automaticCapabilities, $capability, array());
return idx($auto, $viewer->getPHID());
}
public function setCapabilities(array $capabilities) {
$this->capabilities = $capabilities;
return $this;
}
public function setPolicies(array $policy_map) {
$this->policies = $policy_map;
return $this;
}
public function setAutomaticCapabilities(array $auto_map) {
$this->automaticCapabilities = $auto_map;
return $this;
}
}
diff --git a/src/applications/policy/constants/PhabricatorPolicies.php b/src/applications/policy/constants/PhabricatorPolicies.php
index 3fc3f818c5..afa1181393 100644
--- a/src/applications/policy/constants/PhabricatorPolicies.php
+++ b/src/applications/policy/constants/PhabricatorPolicies.php
@@ -1,26 +1,10 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorPolicies extends PhabricatorPolicyConstants {
const POLICY_PUBLIC = 'public';
const POLICY_USER = 'users';
const POLICY_ADMIN = 'admin';
const POLICY_NOONE = 'no-one';
}
diff --git a/src/applications/policy/constants/PhabricatorPolicyCapability.php b/src/applications/policy/constants/PhabricatorPolicyCapability.php
index fd7e0bbc08..55261c5390 100644
--- a/src/applications/policy/constants/PhabricatorPolicyCapability.php
+++ b/src/applications/policy/constants/PhabricatorPolicyCapability.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorPolicyCapability extends PhabricatorPolicyConstants {
const CAN_VIEW = 'view';
const CAN_EDIT = 'edit';
const CAN_JOIN = 'join';
}
diff --git a/src/applications/policy/constants/PhabricatorPolicyConstants.php b/src/applications/policy/constants/PhabricatorPolicyConstants.php
index f5b421b255..5aa46079fb 100644
--- a/src/applications/policy/constants/PhabricatorPolicyConstants.php
+++ b/src/applications/policy/constants/PhabricatorPolicyConstants.php
@@ -1,21 +1,5 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorPolicyConstants {
}
diff --git a/src/applications/policy/constants/PhabricatorPolicyType.php b/src/applications/policy/constants/PhabricatorPolicyType.php
index 819e9fc588..c458bed1cc 100644
--- a/src/applications/policy/constants/PhabricatorPolicyType.php
+++ b/src/applications/policy/constants/PhabricatorPolicyType.php
@@ -1,46 +1,30 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorPolicyType extends PhabricatorPolicyConstants {
const TYPE_GLOBAL = 'global';
const TYPE_PROJECT = 'project';
const TYPE_MASKED = 'masked';
public static function getPolicyTypeOrder($type) {
static $map = array(
self::TYPE_GLOBAL => 0,
self::TYPE_PROJECT => 1,
self::TYPE_MASKED => 9,
);
return idx($map, $type, 9);
}
public static function getPolicyTypeName($type) {
switch ($type) {
case self::TYPE_GLOBAL:
return pht('Global Policies');
case self::TYPE_PROJECT:
return pht('Members of Project');
case self::TYPE_MASKED:
default:
return pht('Other Policies');
}
}
}
diff --git a/src/applications/policy/exception/PhabricatorPolicyException.php b/src/applications/policy/exception/PhabricatorPolicyException.php
index ac9c12d0bf..2e43cc0ed3 100644
--- a/src/applications/policy/exception/PhabricatorPolicyException.php
+++ b/src/applications/policy/exception/PhabricatorPolicyException.php
@@ -1,21 +1,5 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorPolicyException extends Exception {
}
diff --git a/src/applications/policy/filter/PhabricatorPolicy.php b/src/applications/policy/filter/PhabricatorPolicy.php
index e917720969..b901efbfbc 100644
--- a/src/applications/policy/filter/PhabricatorPolicy.php
+++ b/src/applications/policy/filter/PhabricatorPolicy.php
@@ -1,119 +1,103 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorPolicy {
private $phid;
private $name;
private $type;
private $href;
public function setType($type) {
$this->type = $type;
return $this;
}
public function getType() {
return $this->type;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function setPHID($phid) {
$this->phid = $phid;
return $this;
}
public function getPHID() {
return $this->phid;
}
public function setHref($href) {
$this->href = $href;
return $this;
}
public function getHref() {
return $this->href;
}
public function getSortKey() {
return sprintf(
'%02d%s',
PhabricatorPolicyType::getPolicyTypeOrder($this->getType()),
$this->getSortName());
}
private function getSortName() {
if ($this->getType() == PhabricatorPolicyType::TYPE_GLOBAL) {
static $map = array(
PhabricatorPolicies::POLICY_PUBLIC => 0,
PhabricatorPolicies::POLICY_USER => 1,
PhabricatorPolicies::POLICY_ADMIN => 2,
PhabricatorPolicies::POLICY_NOONE => 3,
);
return idx($map, $this->getPHID());
}
return $this->getName();
}
public function getFullName() {
switch ($this->getType()) {
case PhabricatorPolicyType::TYPE_PROJECT:
return pht('Project: %s', $this->getName());
case PhabricatorPolicyType::TYPE_MASKED:
return pht('Other: %s', $this->getName());
default:
return $this->getName();
}
}
public function renderDescription() {
if ($this->getHref()) {
$desc = phutil_render_tag(
'a',
array(
'href' => $this->getHref(),
),
phutil_escape_html($this->getName()));
} else {
$desc = phutil_escape_html($this->getName());
}
switch ($this->getType()) {
case PhabricatorPolicyType::TYPE_PROJECT:
return pht('%s (Project)', $desc);
case PhabricatorPolicyType::TYPE_MASKED:
return pht(
'%s (You do not have permission to view policy details.)',
$desc);
default:
return $desc;
}
}
}
diff --git a/src/applications/policy/filter/PhabricatorPolicyFilter.php b/src/applications/policy/filter/PhabricatorPolicyFilter.php
index 541ea76264..70525774e1 100644
--- a/src/applications/policy/filter/PhabricatorPolicyFilter.php
+++ b/src/applications/policy/filter/PhabricatorPolicyFilter.php
@@ -1,287 +1,271 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorPolicyFilter {
private $viewer;
private $objects;
private $capabilities;
private $raisePolicyExceptions;
private $userProjects;
public static function mustRetainCapability(
PhabricatorUser $user,
PhabricatorPolicyInterface $object,
$capability) {
if (!self::hasCapability($user, $object, $capability)) {
throw new Exception(
"You can not make that edit, because it would remove your ability ".
"to '{$capability}' the object.");
}
}
public static function requireCapability(
PhabricatorUser $user,
PhabricatorPolicyInterface $object,
$capability) {
$filter = new PhabricatorPolicyFilter();
$filter->setViewer($user);
$filter->requireCapabilities(array($capability));
$filter->raisePolicyExceptions(true);
$filter->apply(array($object));
}
public static function hasCapability(
PhabricatorUser $user,
PhabricatorPolicyInterface $object,
$capability) {
$filter = new PhabricatorPolicyFilter();
$filter->setViewer($user);
$filter->requireCapabilities(array($capability));
$result = $filter->apply(array($object));
return (count($result) == 1);
}
public function setViewer(PhabricatorUser $user) {
$this->viewer = $user;
return $this;
}
public function requireCapabilities(array $capabilities) {
$this->capabilities = $capabilities;
return $this;
}
public function raisePolicyExceptions($raise) {
$this->raisePolicyExceptions = $raise;
return $this;
}
public function apply(array $objects) {
assert_instances_of($objects, 'PhabricatorPolicyInterface');
$viewer = $this->viewer;
$capabilities = $this->capabilities;
if (!$viewer || !$capabilities) {
throw new Exception(
'Call setViewer() and requireCapabilities() before apply()!');
}
$filtered = array();
$need_projects = array();
foreach ($objects as $key => $object) {
$object_capabilities = $object->getCapabilities();
foreach ($capabilities as $capability) {
if (!in_array($capability, $object_capabilities)) {
throw new Exception(
"Testing for capability '{$capability}' on an object which does ".
"not have that capability!");
}
$policy = $object->getPolicy($capability);
$type = phid_get_type($policy);
if ($type == PhabricatorPHIDConstants::PHID_TYPE_PROJ) {
$need_projects[] = $policy;
}
}
}
if ($need_projects) {
$need_projects = array_unique($need_projects);
// If projects have recursive policies, automatically fail them rather
// than looping. This will fall back to automatic capabilities and
// resolve the policies in a sensible way.
static $querying_projects = array();
foreach ($need_projects as $key => $project) {
if (empty($querying_projects[$project])) {
$querying_projects[$project] = true;
continue;
}
unset($need_projects[$key]);
}
if ($need_projects) {
$caught = null;
try {
$projects = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withMemberPHIDs(array($viewer->getPHID()))
->withPHIDs($need_projects)
->execute();
} catch (Exception $ex) {
$caught = $ex;
}
foreach ($need_projects as $key => $project) {
unset($querying_projects[$project]);
}
if ($caught) {
throw $caught;
}
$projects = mpull($projects, null, 'getPHID');
$this->userProjects[$viewer->getPHID()] = $projects;
}
}
foreach ($objects as $key => $object) {
$object_capabilities = $object->getCapabilities();
foreach ($capabilities as $capability) {
if (!$this->checkCapability($object, $capability)) {
// If we're missing any capability, move on to the next object.
continue 2;
}
// If we make it here, we have all of the required capabilities.
$filtered[$key] = $object;
}
}
return $filtered;
}
private function checkCapability(
PhabricatorPolicyInterface $object,
$capability) {
$policy = $object->getPolicy($capability);
if (!$policy) {
// TODO: Formalize this somehow?
$policy = PhabricatorPolicies::POLICY_USER;
}
if ($policy == PhabricatorPolicies::POLICY_PUBLIC) {
// If the object is set to "public" but that policy is disabled for this
// install, restrict the policy to "user".
if (!PhabricatorEnv::getEnvConfig('policy.allow-public')) {
$policy = PhabricatorPolicies::POLICY_USER;
}
// If the object is set to "public" but the capability is anything other
// than "view", restrict the policy to "user".
if ($capability != PhabricatorPolicyCapability::CAN_VIEW) {
$policy = PhabricatorPolicies::POLICY_USER;
}
}
$viewer = $this->viewer;
if ($object->hasAutomaticCapability($capability, $viewer)) {
return true;
}
switch ($policy) {
case PhabricatorPolicies::POLICY_PUBLIC:
return true;
case PhabricatorPolicies::POLICY_USER:
if ($viewer->getPHID()) {
return true;
} else {
$this->rejectObject($object, $policy, $capability);
}
break;
case PhabricatorPolicies::POLICY_ADMIN:
if ($viewer->getIsAdmin()) {
return true;
} else {
$this->rejectObject($object, $policy, $capability);
}
break;
case PhabricatorPolicies::POLICY_NOONE:
$this->rejectObject($object, $policy, $capability);
break;
default:
$type = phid_get_type($policy);
if ($type == PhabricatorPHIDConstants::PHID_TYPE_PROJ) {
if (isset($this->userProjects[$viewer->getPHID()][$policy])) {
return true;
} else {
$this->rejectObject($object, $policy, $capability);
}
} else {
throw new Exception("Object has unknown policy '{$policy}'!");
}
}
return false;
}
private function rejectImpossiblePolicy(
PhabricatorPolicyInterface $object,
$policy,
$capability) {
if (!$this->raisePolicyExceptions) {
return;
}
// TODO: clean this up
$verb = $capability;
throw new PhabricatorPolicyException(
"This object has an impossible {$verb} policy.");
}
private function rejectObject($object, $policy, $capability) {
if (!$this->raisePolicyExceptions) {
return;
}
// TODO: clean this up
$verb = $capability;
$message = "You do not have permission to {$verb} this object.";
switch ($policy) {
case PhabricatorPolicies::POLICY_PUBLIC:
$who = "This is curious, since anyone can {$verb} the object.";
break;
case PhabricatorPolicies::POLICY_USER:
$who = "To {$verb} this object, you must be logged in.";
break;
case PhabricatorPolicies::POLICY_ADMIN:
$who = "To {$verb} this object, you must be an administrator.";
break;
case PhabricatorPolicies::POLICY_NOONE:
$who = "No one can {$verb} this object.";
break;
default:
$type = phid_get_type($policy);
if ($type == PhabricatorPHIDConstants::PHID_TYPE_PROJ) {
$handle = PhabricatorObjectHandleData::loadOneHandle(
$policy,
$this->viewer);
$who = "To {$verb} this object, you must be a member of project ".
"'".$handle->getFullName()."'.";
} else {
$who = "It is unclear who can {$verb} this object.";
}
break;
}
throw new PhabricatorPolicyException("{$message} {$who}");
}
}
diff --git a/src/applications/policy/interface/PhabricatorPolicyInterface.php b/src/applications/policy/interface/PhabricatorPolicyInterface.php
index e4ea7a9753..b56d223c62 100644
--- a/src/applications/policy/interface/PhabricatorPolicyInterface.php
+++ b/src/applications/policy/interface/PhabricatorPolicyInterface.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
interface PhabricatorPolicyInterface {
public function getCapabilities();
public function getPolicy($capability);
public function hasAutomaticCapability($capability, PhabricatorUser $viewer);
}
diff --git a/src/applications/policy/query/PhabricatorPolicyQuery.php b/src/applications/policy/query/PhabricatorPolicyQuery.php
index 642820bf9c..20d14b4dc4 100644
--- a/src/applications/policy/query/PhabricatorPolicyQuery.php
+++ b/src/applications/policy/query/PhabricatorPolicyQuery.php
@@ -1,172 +1,156 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorPolicyQuery extends PhabricatorQuery {
private $viewer;
private $object;
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function setObject(PhabricatorPolicyInterface $object) {
$this->object = $object;
return $this;
}
public static function renderPolicyDescriptions(
PhabricatorUser $viewer,
PhabricatorPolicyInterface $object) {
$results = array();
$policies = null;
$global = self::getGlobalPolicies();
$capabilities = $object->getCapabilities();
foreach ($capabilities as $capability) {
$policy = $object->getPolicy($capability);
if (!$policy) {
continue;
}
if (isset($global[$policy])) {
$results[$capability] = $global[$policy]->renderDescription();
continue;
}
if ($policies === null) {
// This slightly overfetches data, but it shouldn't generally
// be a problem.
$policies = id(new PhabricatorPolicyQuery())
->setViewer($viewer)
->setObject($object)
->execute();
}
$results[$capability] = $policies[$policy]->renderDescription();
}
return $results;
}
public function execute() {
if (!$this->viewer) {
throw new Exception('Call setViewer() before execute()!');
}
if (!$this->object) {
throw new Exception('Call setObject() before execute()!');
}
$results = $this->getGlobalPolicies();
if ($this->viewer->getPHID()) {
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->viewer)
->withMemberPHIDs(array($this->viewer->getPHID()))
->execute();
if ($projects) {
foreach ($projects as $project) {
$results[] = id(new PhabricatorPolicy())
->setType(PhabricatorPolicyType::TYPE_PROJECT)
->setPHID($project->getPHID())
->setHref('/project/view/'.$project->getID().'/')
->setName($project->getName());
}
}
}
$results = mpull($results, null, 'getPHID');
$other_policies = array();
$capabilities = $this->object->getCapabilities();
foreach ($capabilities as $capability) {
$policy = $this->object->getPolicy($capability);
if (!$policy) {
continue;
}
$other_policies[$policy] = $policy;
}
// If this install doesn't have "Public" enabled, remove it as an option
// unless the object already has a "Public" policy. In this case we retain
// the policy but enforce it as thought it was "All Users".
$show_public = PhabricatorEnv::getEnvConfig('policy.allow-public');
if (!$show_public &&
empty($other_policies[PhabricatorPolicies::POLICY_PUBLIC])) {
unset($results[PhabricatorPolicies::POLICY_PUBLIC]);
}
$other_policies = array_diff_key($other_policies, $results);
if ($other_policies) {
$handles = id(new PhabricatorObjectHandleData($other_policies))
->setViewer($this->viewer)
->loadHandles();
foreach ($other_policies as $phid) {
$handle = $handles[$phid];
$results[$phid] = id(new PhabricatorPolicy())
->setType(PhabricatorPolicyType::TYPE_MASKED)
->setPHID($handle->getPHID())
->setHref($handle->getLink())
->setName($handle->getFullName());
}
}
$results = msort($results, 'getSortKey');
return $results;
}
private static function getGlobalPolicies() {
static $constants = array(
PhabricatorPolicies::POLICY_PUBLIC,
PhabricatorPolicies::POLICY_USER,
PhabricatorPolicies::POLICY_ADMIN,
PhabricatorPolicies::POLICY_NOONE,
);
$results = array();
foreach ($constants as $constant) {
$results[$constant] = id(new PhabricatorPolicy())
->setType(PhabricatorPolicyType::TYPE_GLOBAL)
->setPHID($constant)
->setName(self::getGlobalPolicyName($constant));
}
return $results;
}
private static function getGlobalPolicyName($policy) {
switch ($policy) {
case PhabricatorPolicies::POLICY_PUBLIC:
return pht('Public (No Login Required)');
case PhabricatorPolicies::POLICY_USER:
return pht('All Users');
case PhabricatorPolicies::POLICY_ADMIN:
return pht('Administrators');
case PhabricatorPolicies::POLICY_NOONE:
return pht('No One');
default:
return pht('Unknown Policy');
}
}
}
diff --git a/src/applications/ponder/PonderConstants.php b/src/applications/ponder/PonderConstants.php
index 522cc2be7e..0d0c5e2565 100644
--- a/src/applications/ponder/PonderConstants.php
+++ b/src/applications/ponder/PonderConstants.php
@@ -1,26 +1,10 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderConstants {
const UP_VOTE = 1;
const NONE_VOTE = 0;
const DOWN_VOTE = -1;
const ANSWERED_LITERAL = "answered";
const ASKED_LITERAL = "asked";
}
diff --git a/src/applications/ponder/PonderReplyHandler.php b/src/applications/ponder/PonderReplyHandler.php
index 38aff689c6..da85ac773b 100644
--- a/src/applications/ponder/PonderReplyHandler.php
+++ b/src/applications/ponder/PonderReplyHandler.php
@@ -1,48 +1,32 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderReplyHandler extends PhabricatorMailReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof PonderQuestion)) {
throw new Exception("Mail receiver is not a PonderQuestion!");
}
}
public function getPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle) {
return $this->getDefaultPrivateReplyHandlerEmailAddress($handle, 'Q');
}
public function getPublicReplyHandlerEmailAddress() {
return $this->getDefaultPublicReplyHandlerEmailAddress('Q');
}
public function getReplyHandlerDomain() {
return PhabricatorEnv::getEnvConfig(
'metamta.maniphest.reply-handler-domain');
}
public function getReplyHandlerInstructions() {
return null;
}
protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
// ignore this entirely for now
}
}
diff --git a/src/applications/ponder/application/PhabricatorApplicationPonder.php b/src/applications/ponder/application/PhabricatorApplicationPonder.php
index e362213301..349d14a4a9 100644
--- a/src/applications/ponder/application/PhabricatorApplicationPonder.php
+++ b/src/applications/ponder/application/PhabricatorApplicationPonder.php
@@ -1,67 +1,51 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationPonder extends PhabricatorApplication {
public function getBaseURI() {
return '/ponder/';
}
public function getShortDescription() {
return 'Find Answers';
}
public function getAutospriteName() {
return 'ponder';
}
public function getFactObjectsForAnalysis() {
return array(
new PonderQuestion(),
);
}
public function loadStatus(PhabricatorUser $user) {
// replace with "x new unanswered questions" or some such
$status = array();
return $status;
}
public function getApplicationGroup() {
return self::GROUP_COMMUNICATION;
}
public function getroutes() {
return array(
'/Q(?P<id>[1-9]\d*)' => 'PonderQuestionViewController',
'/ponder/' => array(
'(?P<page>feed/)?' => 'PonderFeedController',
'(?P<page>questions)/' => 'PonderFeedController',
'(?P<page>answers)/' => 'PonderFeedController',
'answer/add/' => 'PonderAnswerSaveController',
'answer/preview/' => 'PonderAnswerPreviewController',
'question/ask/' => 'PonderQuestionAskController',
'question/preview/' => 'PonderQuestionPreviewController',
'comment/add/' => 'PonderCommentSaveController',
'(?P<kind>question)/vote/' => 'PonderVoteSaveController',
'(?P<kind>answer)/vote/' => 'PonderVoteSaveController'
));
}
}
diff --git a/src/applications/ponder/controller/PonderAnswerPreviewController.php b/src/applications/ponder/controller/PonderAnswerPreviewController.php
index 6fc829c179..3f3e6b908b 100644
--- a/src/applications/ponder/controller/PonderAnswerPreviewController.php
+++ b/src/applications/ponder/controller/PonderAnswerPreviewController.php
@@ -1,54 +1,38 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderAnswerPreviewController
extends PonderController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$question_id = $request->getInt('question_id');
$question = PonderQuestionQuery::loadSingle($user, $question_id);
if (!$question) {
return new Aphront404Response();
}
$author_phid = $user->getPHID();
$object_phids = array($author_phid);
$handles = $this->loadViewerHandles($object_phids);
$answer = new PonderAnswer();
$answer->setContent($request->getStr('content'));
$answer->setAuthorPHID($author_phid);
$view = new PonderPostBodyView();
$view
->setQuestion($question)
->setTarget($answer)
->setPreview(true)
->setUser($user)
->setHandles($handles)
->setAction(PonderConstants::ANSWERED_LITERAL);
return id(new AphrontAjaxResponse())
->setContent($view->render());
}
}
diff --git a/src/applications/ponder/controller/PonderAnswerSaveController.php b/src/applications/ponder/controller/PonderAnswerSaveController.php
index a62dfcf224..85a311bcc6 100644
--- a/src/applications/ponder/controller/PonderAnswerSaveController.php
+++ b/src/applications/ponder/controller/PonderAnswerSaveController.php
@@ -1,71 +1,55 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderAnswerSaveController extends PonderController {
public function processRequest() {
$request = $this->getRequest();
if (!$request->isFormPost()) {
return new Aphront400Response();
}
$user = $request->getUser();
$question_id = $request->getInt('question_id');
$question = PonderQuestionQuery::loadSingle($user, $question_id);
if (!$question) {
return new Aphront404Response();
}
$answer = $request->getStr('answer');
// Only want answers with some non whitespace content
if (!strlen(trim($answer))) {
$dialog = new AphrontDialogView();
$dialog->setUser($request->getUser());
$dialog->setTitle('Empty answer');
$dialog->appendChild('<p>Your answer must not be empty.</p>');
$dialog->addCancelButton('/Q'.$question_id);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_WEB,
array(
'ip' => $request->getRemoteAddr(),
));
$res = new PonderAnswer();
$res
->setContent($answer)
->setAuthorPHID($user->getPHID())
->setVoteCount(0)
->setQuestionID($question_id)
->setContentSource($content_source);
id(new PonderAnswerEditor())
->setActor($user)
->setQuestion($question)
->setAnswer($res)
->saveAnswer();
return id(new AphrontRedirectResponse())->setURI(
id(new PhutilURI('/Q'. $question->getID())));
}
}
diff --git a/src/applications/ponder/controller/PonderAnswerViewController.php b/src/applications/ponder/controller/PonderAnswerViewController.php
index 90f3e369fa..23183fbada 100644
--- a/src/applications/ponder/controller/PonderAnswerViewController.php
+++ b/src/applications/ponder/controller/PonderAnswerViewController.php
@@ -1,41 +1,25 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderAnswerViewController extends PonderController {
private $answerID;
public function willProcessRequest(array $data) {
$this->answerID = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$answer = id(new PonderAnswer())->load($this->answerID);
if (!$answer) {
return new Aphront404Response();
}
$question_id = $answer->getQuestionID();
return id(new AphrontRedirectResponse())
->setURI('/Q'.$question_id . '#A' . $answer->getID());
}
}
diff --git a/src/applications/ponder/controller/PonderCommentSaveController.php b/src/applications/ponder/controller/PonderCommentSaveController.php
index d852a99345..151ec30c17 100644
--- a/src/applications/ponder/controller/PonderCommentSaveController.php
+++ b/src/applications/ponder/controller/PonderCommentSaveController.php
@@ -1,72 +1,56 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderCommentSaveController extends PonderController {
public function processRequest() {
$request = $this->getRequest();
if (!$request->isFormPost()) {
return new Aphront400Response();
}
$user = $request->getUser();
$question_id = $request->getInt('question_id');
$question = PonderQuestionQuery::loadSingle($user, $question_id);
if (!$question) {
return new Aphront404Response();
}
$target = $request->getStr('target');
$objects = id(new PhabricatorObjectHandleData(array($target)))
->loadHandles();
if (!$objects) {
return new Aphront404Response();
}
$content = $request->getStr('content');
if (!strlen(trim($content))) {
$dialog = new AphrontDialogView();
$dialog->setUser($request->getUser());
$dialog->setTitle('Empty comment');
$dialog->appendChild('<p>Your comment must not be empty.</p>');
$dialog->addCancelButton('/Q'.$question_id);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$res = new PonderComment();
$res
->setContent($content)
->setAuthorPHID($user->getPHID())
->setTargetPHID($target);
id(new PonderCommentEditor())
->setQuestion($question)
->setComment($res)
->setTargetPHID($target)
->setActor($user)
->save();
return id(new AphrontRedirectResponse())
->setURI(
id(new PhutilURI('/Q'. $question->getID())));
}
}
diff --git a/src/applications/ponder/controller/PonderController.php b/src/applications/ponder/controller/PonderController.php
index b8625d0a3d..e17257685f 100644
--- a/src/applications/ponder/controller/PonderController.php
+++ b/src/applications/ponder/controller/PonderController.php
@@ -1,64 +1,48 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PonderController extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('Ponder!');
$page->setBaseURI('/ponder/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph("\xE2\x97\xB3");
$page->appendChild($view);
$page->setSearchDefaultScope(PhabricatorSearchScope::SCOPE_QUESTIONS);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
protected function buildSideNavView(PonderQuestion $question = null) {
$side_nav = new AphrontSideNavFilterView();
$side_nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
if ($question && $question->getID()) {
$side_nav->addFilter(
null,
'Q'.$question->getID(),
'Q'.$question->getID());
$side_nav->addSpacer();
}
$side_nav->addLabel('Create');
$side_nav->addFilter('question/ask', 'Ask a Question');
$side_nav->addSpacer();
$side_nav->addLabel('Questions');
$side_nav->addFilter('feed', 'All Questions');
$side_nav->addSpacer();
$side_nav->addLabel('User');
$side_nav->addFilter('questions', 'Your Questions');
$side_nav->addFilter('answers', 'Your Answers');
return $side_nav;
}
}
diff --git a/src/applications/ponder/controller/PonderFeedController.php b/src/applications/ponder/controller/PonderFeedController.php
index 952ee998c8..baa14bea08 100644
--- a/src/applications/ponder/controller/PonderFeedController.php
+++ b/src/applications/ponder/controller/PonderFeedController.php
@@ -1,144 +1,128 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderFeedController extends PonderController {
private $page;
private $answerOffset;
const PROFILE_ANSWER_PAGE_SIZE = 10;
public function willProcessRequest(array $data) {
$this->page = idx($data, 'page');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$this->answerOffset = $request->getInt('aoff');
$pages = array(
'feed' => 'All Questions',
'questions' => 'Your Questions',
'answers' => 'Your Answers',
);
$side_nav = $this->buildSideNavView();
$this->page = $side_nav->selectFilter($this->page, 'feed');
$title = $pages[$this->page];
switch ($this->page) {
case 'feed':
case 'questions':
$pager = new AphrontPagerView();
$pager->setOffset($request->getStr('offset'));
$pager->setURI($request->getRequestURI(), 'offset');
$query = new PonderQuestionQuery();
if ($this->page == 'feed') {
$query
->setOrder(PonderQuestionQuery::ORDER_HOTTEST);
} else {
$query
->setOrder(PonderQuestionQuery::ORDER_CREATED)
->withAuthorPHIDs(array($user->getPHID()));
}
$questions = $query->executeWithOffsetPager($pager);
$this->loadHandles(mpull($questions, 'getAuthorPHID'));
$view = $this->buildQuestionListView($questions);
$view->setPager($pager);
$side_nav->appendChild(
id(new PhabricatorHeaderView())->setHeader($title));
$side_nav->appendChild($view);
break;
case 'answers':
$answers = PonderAnswerQuery::loadByAuthorWithQuestions(
$user,
$user->getPHID(),
$this->answerOffset,
self::PROFILE_ANSWER_PAGE_SIZE + 1
);
$side_nav->appendChild(
id(new PonderUserProfileView())
->setUser($user)
->setAnswers($answers)
->setAnswerOffset($this->answerOffset)
->setPageSize(self::PROFILE_ANSWER_PAGE_SIZE)
->setURI(new PhutilURI("/ponder/profile/"), "aoff")
);
break;
}
return $this->buildApplicationPage(
$side_nav,
array(
'device' => true,
'title' => $title,
));
}
private function buildQuestionListView(array $questions) {
assert_instances_of($questions, 'PonderQuestion');
$user = $this->getRequest()->getUser();
$view = new PhabricatorObjectItemListView();
$view->setNoDataString(pht('No matching questions.'));
foreach ($questions as $question) {
$item = new PhabricatorObjectItemView();
$item->setHeader('Q'.$question->getID().' '.$question->getTitle());
$item->setHref('/Q'.$question->getID());
$desc = $question->getContent();
if ($desc) {
$item->addDetail(
pht('Description'),
phutil_escape_html(phutil_utf8_shorten($desc, 128)));
}
$item->addDetail(
pht('Author'),
$this->getHandle($question->getAuthorPHID())->renderLink());
$item->addDetail(
pht('Votes'),
$question->getVoteCount());
$item->addDetail(
pht('Answers'),
$question->getAnswerCount());
$created = pht(
'Created %s',
phabricator_date($question->getDateCreated(), $user));
$item->addAttribute($created);
$view->addItem($item);
}
return $view;
}
}
diff --git a/src/applications/ponder/controller/PonderQuestionAskController.php b/src/applications/ponder/controller/PonderQuestionAskController.php
index e3b0d8282a..0a9a4dfa0b 100644
--- a/src/applications/ponder/controller/PonderQuestionAskController.php
+++ b/src/applications/ponder/controller/PonderQuestionAskController.php
@@ -1,130 +1,114 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderQuestionAskController extends PonderController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$question = id(new PonderQuestion())
->setAuthorPHID($user->getPHID())
->setVoteCount(0)
->setAnswerCount(0)
->setHeat(0.0);
$errors = array();
$e_title = true;
if ($request->isFormPost()) {
$question->setTitle($request->getStr('title'));
$question->setContent($request->getStr('content'));
$len = phutil_utf8_strlen($question->getTitle());
if ($len < 1) {
$errors[] = pht('Title must not be empty.');
$e_title = pht('Required');
} else if ($len > 255) {
$errors[] = pht('Title is too long.');
$e_title = pht('Too Long');
}
if (!$errors) {
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_WEB,
array(
'ip' => $request->getRemoteAddr(),
));
$question->setContentSource($content_source);
id(new PonderQuestionEditor())
->setQuestion($question)
->setActor($user)
->save();
return id(new AphrontRedirectResponse())
->setURI('/Q'.$question->getID());
}
}
$error_view = null;
if ($errors) {
$error_view = id(new AphrontErrorView())
->setTitle('Form Errors')
->setErrors($errors);
}
$header = id(new PhabricatorHeaderView())->setHeader(pht('Ask Question'));
$form = id(new AphrontFormView())
->setUser($user)
->setFlexible(true)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Question'))
->setName('title')
->setValue($question->getTitle())
->setError($e_title))
->appendChild(
id(new PhabricatorRemarkupControl())
->setName('content')
->setID('content')
->setValue($question->getContent())
->setLabel(pht('Description')))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Ask Away!'));
$preview =
'<div class="aphront-panel-flush">'.
'<div id="question-preview">'.
'<span class="aphront-panel-preview-loading-text">'.
pht('Loading question preview...').
'</span>'.
'</div>'.
'</div>';
Javelin::initBehavior(
'ponder-feedback-preview',
array(
'uri' => '/ponder/question/preview/',
'content' => 'content',
'preview' => 'question-preview',
'question_id' => null
));
$nav = $this->buildSideNavView($question);
$nav->selectFilter($question->getID() ? null : 'question/ask');
$nav->appendChild(
array(
$header,
$error_view,
$form,
$preview,
));
return $this->buildApplicationPage(
$nav,
array(
'device' => true,
'title' => 'Ask a Question',
)
);
}
}
diff --git a/src/applications/ponder/controller/PonderQuestionPreviewController.php b/src/applications/ponder/controller/PonderQuestionPreviewController.php
index 64b64b7500..e09272e912 100644
--- a/src/applications/ponder/controller/PonderQuestionPreviewController.php
+++ b/src/applications/ponder/controller/PonderQuestionPreviewController.php
@@ -1,51 +1,35 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderQuestionPreviewController
extends PonderController {
const VERB_ASKED = "asked";
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$author_phid = $user->getPHID();
$object_phids = array($author_phid);
$handles = $this->loadViewerHandles($object_phids);
$question = new PonderQuestion();
$question->setContent($request->getStr('content'));
$question->setAuthorPHID($author_phid);
$view = new PonderPostBodyView();
$view
->setQuestion($question)
->setTarget($question)
->setPreview(true)
->setUser($user)
->setHandles($handles)
->setAction(self::VERB_ASKED);
return id(new AphrontAjaxResponse())
->setContent($view->render());
}
}
diff --git a/src/applications/ponder/controller/PonderQuestionViewController.php b/src/applications/ponder/controller/PonderQuestionViewController.php
index 6bfef7d023..47a0d08599 100644
--- a/src/applications/ponder/controller/PonderQuestionViewController.php
+++ b/src/applications/ponder/controller/PonderQuestionViewController.php
@@ -1,148 +1,132 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderQuestionViewController extends PonderController {
private $questionID;
public function willProcessRequest(array $data) {
$this->questionID = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$question = PonderQuestionQuery::loadSingle($user, $this->questionID);
if (!$question) {
return new Aphront404Response();
}
$question->attachRelated();
$question->attachVotes($user->getPHID());
$object_phids = array($user->getPHID(), $question->getAuthorPHID());
$answers = $question->getAnswers();
$comments = $question->getComments();
foreach ($comments as $comment) {
$object_phids[] = $comment->getAuthorPHID();
}
foreach ($answers as $answer) {
$object_phids[] = $answer->getAuthorPHID();
$comments = $answer->getComments();
foreach ($comments as $comment) {
$object_phids[] = $comment->getAuthorPHID();
}
}
$subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID(
$question->getPHID());
$object_phids = array_merge($object_phids, $subscribers);
$handles = $this->loadViewerHandles($object_phids);
$this->loadHandles($object_phids);
$detail_panel = new PonderQuestionDetailView();
$detail_panel
->setQuestion($question)
->setUser($user)
->setHandles($handles);
$responses_panel = new PonderAnswerListView();
$responses_panel
->setQuestion($question)
->setHandles($handles)
->setUser($user)
->setAnswers($answers);
$answer_add_panel = new PonderAddAnswerView();
$answer_add_panel
->setQuestion($question)
->setUser($user)
->setActionURI("/ponder/answer/add/");
$header = id(new PhabricatorHeaderView())
->setObjectName('Q'.$question->getID())
->setHeader($question->getTitle());
$actions = $this->buildActionListView($question);
$properties = $this->buildPropertyListView($question, $subscribers);
$nav = $this->buildSideNavView($question);
$nav->appendChild(
array(
$header,
$actions,
$properties,
$detail_panel,
$responses_panel,
$answer_add_panel
));
$nav->selectFilter(null);
return $this->buildApplicationPage(
$nav,
array(
'device' => true,
'title' => 'Q'.$question->getID().' '.$question->getTitle()
));
}
private function buildActionListView(PonderQuestion $question) {
$viewer = $this->getRequest()->getUser();
$view = new PhabricatorActionListView();
$view->setUser($viewer);
$view->setObject($question);
return $view;
}
private function buildPropertyListView(
PonderQuestion $question,
array $subscribers) {
$viewer = $this->getRequest()->getUser();
$view = new PhabricatorPropertyListView();
$view->addProperty(
pht('Author'),
$this->getHandle($question->getAuthorPHID())->renderLink());
$view->addProperty(
pht('Created'),
phabricator_datetime($question->getDateCreated(), $viewer));
if ($subscribers) {
foreach ($subscribers as $key => $subscriber) {
$subscribers[$key] = $this->getHandle($subscriber)->renderLink();
}
$subscribers = implode(', ', $subscribers);
}
$view->addProperty(
pht('Subscribers'),
nonempty($subscribers, '<em>'.pht('None').'</em>'));
return $view;
}
}
diff --git a/src/applications/ponder/controller/PonderVoteSaveController.php b/src/applications/ponder/controller/PonderVoteSaveController.php
index 28174eb787..73bca07bee 100644
--- a/src/applications/ponder/controller/PonderVoteSaveController.php
+++ b/src/applications/ponder/controller/PonderVoteSaveController.php
@@ -1,58 +1,42 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderVoteSaveController extends PonderController {
private $kind;
public function willProcessRequest(array $data) {
$this->kind = $data['kind'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$newvote = $request->getInt("vote");
$phid = $request->getStr("phid");
if (1 < $newvote || $newvote < -1) {
return new Aphront400Response();
}
$target = null;
if ($this->kind == "question") {
$target = PonderQuestionQuery::loadSingleByPHID($user, $phid);
}
else if ($this->kind == "answer") {
$target = PonderAnswerQuery::loadSingleByPHID($user, $phid);
}
if (!$target) {
return new Aphront404Response();
}
$editor = id(new PonderVoteEditor())
->setVotable($target)
->setActor($user)
->setVote($newvote)
->saveVote();
return id(new AphrontAjaxResponse())->setContent(".");
}
}
diff --git a/src/applications/ponder/editor/PonderAnswerEditor.php b/src/applications/ponder/editor/PonderAnswerEditor.php
index 66fb8e3b89..89d3bb90a8 100644
--- a/src/applications/ponder/editor/PonderAnswerEditor.php
+++ b/src/applications/ponder/editor/PonderAnswerEditor.php
@@ -1,116 +1,100 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderAnswerEditor extends PhabricatorEditor {
private $question;
private $answer;
private $shouldEmail = true;
public function setQuestion($question) {
$this->question = $question;
return $this;
}
public function setAnswer($answer) {
$this->answer = $answer;
return $this;
}
public function saveAnswer() {
$actor = $this->requireActor();
if (!$this->question) {
throw new Exception("Must set question before saving answer");
}
if (!$this->answer) {
throw new Exception("Must set answer before saving it");
}
$question = $this->question;
$answer = $this->answer;
$conn = $answer->establishConnection('w');
$trans = $conn->openTransaction();
$trans->beginReadLocking();
$question->reload();
queryfx($conn,
'UPDATE %T as t
SET t.`answerCount` = t.`answerCount` + 1
WHERE t.`PHID` = %s',
$question->getTableName(),
$question->getPHID());
$answer->setQuestionID($question->getID());
$answer->save();
$trans->endReadLocking();
$trans->saveTransaction();
$question->attachRelated();
PhabricatorSearchPonderIndexer::indexQuestion($question);
// subscribe author and @mentions
$subeditor = id(new PhabricatorSubscriptionsEditor())
->setObject($question)
->setActor($actor);
$subeditor->subscribeExplicit(array($answer->getAuthorPHID()));
$content = $answer->getContent();
$at_mention_phids = PhabricatorMarkupEngine::extractPHIDsFromMentions(
array($content)
);
$subeditor->subscribeImplicit($at_mention_phids);
$subeditor->save();
if ($this->shouldEmail) {
// now load subscribers, including implicitly-added @mention victims
$subscribers = PhabricatorSubscribersQuery
::loadSubscribersForPHID($question->getPHID());
// @mention emails (but not for anyone who has explicitly unsubscribed)
if (array_intersect($at_mention_phids, $subscribers)) {
id(new PonderMentionMail(
$question,
$answer,
$actor))
->setToPHIDs($at_mention_phids)
->send();
}
$other_subs =
array_diff(
$subscribers,
$at_mention_phids
);
// 'Answered' emails for subscribers who are not @mentiond (and excluding
// author depending on their MetaMTA settings).
if ($other_subs) {
id(new PonderAnsweredMail(
$question,
$answer,
$actor))
->setToPHIDs($other_subs)
->send();
}
}
}
}
diff --git a/src/applications/ponder/editor/PonderCommentEditor.php b/src/applications/ponder/editor/PonderCommentEditor.php
index de28add69a..14c0a0cebc 100644
--- a/src/applications/ponder/editor/PonderCommentEditor.php
+++ b/src/applications/ponder/editor/PonderCommentEditor.php
@@ -1,123 +1,107 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderCommentEditor extends PhabricatorEditor {
private $question;
private $comment;
private $targetPHID;
private $shouldEmail = true;
public function setComment(PonderComment $comment) {
$this->comment = $comment;
return $this;
}
public function setQuestion(PonderQuestion $question) {
$this->question = $question;
return $this;
}
public function setTargetPHID($target) {
$this->targetPHID = $target;
return $this;
}
public function save() {
$actor = $this->requireActor();
if (!$this->comment) {
throw new Exception("Must set comment before saving it");
}
if (!$this->question) {
throw new Exception("Must set question before saving comment");
}
if (!$this->targetPHID) {
throw new Exception("Must set target before saving comment");
}
$comment = $this->comment;
$question = $this->question;
$target = $this->targetPHID;
$comment->save();
$question->attachRelated();
PhabricatorSearchPonderIndexer::indexQuestion($question);
// subscribe author and @mentions
$subeditor = id(new PhabricatorSubscriptionsEditor())
->setObject($question)
->setActor($actor);
$subeditor->subscribeExplicit(array($comment->getAuthorPHID()));
$content = $comment->getContent();
$at_mention_phids = PhabricatorMarkupEngine::extractPHIDsFromMentions(
array($content)
);
$subeditor->subscribeImplicit($at_mention_phids);
$subeditor->save();
if ($this->shouldEmail) {
// now load subscribers, including implicitly-added @mention victims
$subscribers = PhabricatorSubscribersQuery
::loadSubscribersForPHID($question->getPHID());
// @mention emails (but not for anyone who has explicitly unsubscribed)
if (array_intersect($at_mention_phids, $subscribers)) {
id(new PonderMentionMail(
$question,
$comment,
$actor))
->setToPHIDs($at_mention_phids)
->send();
}
if ($target === $question->getPHID()) {
$target = $question;
}
else {
$answers_by_phid = mgroup($question->getAnswers(), 'getPHID');
$target = head($answers_by_phid[$target]);
}
// only send emails to others in the same thread
$thread = mpull($target->getComments(), 'getAuthorPHID');
$thread[] = $target->getAuthorPHID();
$thread[] = $question->getAuthorPHID();
$other_subs =
array_diff(
array_intersect($thread, $subscribers),
$at_mention_phids
);
// 'Comment' emails for subscribers who are in the same comment thread,
// including the author of the parent question and/or answer, excluding
// @mentions (and excluding the author, depending on their MetaMTA
// settings).
if ($other_subs) {
id(new PonderCommentMail(
$question,
$comment,
$actor))
->setToPHIDs($other_subs)
->send();
}
}
}
}
diff --git a/src/applications/ponder/editor/PonderQuestionEditor.php b/src/applications/ponder/editor/PonderQuestionEditor.php
index b37a39a5ca..e17bce0ee7 100644
--- a/src/applications/ponder/editor/PonderQuestionEditor.php
+++ b/src/applications/ponder/editor/PonderQuestionEditor.php
@@ -1,70 +1,54 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderQuestionEditor extends PhabricatorEditor {
private $question;
private $shouldEmail = true;
public function setQuestion(PonderQuestion $question) {
$this->question = $question;
return $this;
}
public function setShouldEmail($se) {
$this->shouldEmail = $se;
return $this;
}
public function save() {
$actor = $this->requireActor();
if (!$this->question) {
throw new Exception("Must set question before saving it");
}
$question = $this->question;
$question->save();
// search index
$question->attachRelated();
PhabricatorSearchPonderIndexer::indexQuestion($question);
// subscribe author and @mentions
$subeditor = id(new PhabricatorSubscriptionsEditor())
->setObject($question)
->setActor($actor);
$subeditor->subscribeExplicit(array($question->getAuthorPHID()));
$content = $question->getContent();
$at_mention_phids = PhabricatorMarkupEngine::extractPHIDsFromMentions(
array($content)
);
$subeditor->subscribeImplicit($at_mention_phids);
$subeditor->save();
if ($this->shouldEmail && $at_mention_phids) {
id(new PonderMentionMail(
$question,
$question,
$actor))
->setToPHIDs($at_mention_phids)
->send();
}
}
}
diff --git a/src/applications/ponder/editor/PonderVoteEditor.php b/src/applications/ponder/editor/PonderVoteEditor.php
index 35623f81d2..baa81a476e 100644
--- a/src/applications/ponder/editor/PonderVoteEditor.php
+++ b/src/applications/ponder/editor/PonderVoteEditor.php
@@ -1,94 +1,78 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderVoteEditor extends PhabricatorEditor {
private $answer;
private $votable;
private $anwer;
private $vote;
public function setAnswer($answer) {
$this->answer = $answer;
return $this;
}
public function setVotable($votable) {
$this->votable = $votable;
return $this;
}
public function setVote($vote) {
$this->vote = $vote;
return $this;
}
public function saveVote() {
$actor = $this->requireActor();
if (!$this->votable) {
throw new Exception("Must set votable before saving vote");
}
$votable = $this->votable;
$newvote = $this->vote;
// prepare vote add, or update if this user is amending an
// earlier vote
$editor = id(new PhabricatorEdgeEditor())
->setActor($actor)
->addEdge(
$actor->getPHID(),
$votable->getUserVoteEdgeType(),
$votable->getVotablePHID(),
array('data' => $newvote))
->removeEdge(
$actor->getPHID(),
$votable->getUserVoteEdgeType(),
$votable->getVotablePHID());
$conn = $votable->establishConnection('w');
$trans = $conn->openTransaction();
$trans->beginReadLocking();
$votable->reload();
$curvote = (int)PhabricatorEdgeQuery::loadSingleEdgeData(
$actor->getPHID(),
$votable->getUserVoteEdgeType(),
$votable->getVotablePHID());
if (!$curvote) {
$curvote = PonderConstants::NONE_VOTE;
}
// adjust votable's score by this much
$delta = $newvote - $curvote;
queryfx($conn,
'UPDATE %T as t
SET t.`voteCount` = t.`voteCount` + %d
WHERE t.`PHID` = %s',
$votable->getTableName(),
$delta,
$votable->getVotablePHID());
$editor->save();
$trans->endReadLocking();
$trans->saveTransaction();
}
}
diff --git a/src/applications/ponder/mail/PonderAnsweredMail.php b/src/applications/ponder/mail/PonderAnsweredMail.php
index 8c2b4ae5c5..a0b8a11f80 100644
--- a/src/applications/ponder/mail/PonderAnsweredMail.php
+++ b/src/applications/ponder/mail/PonderAnsweredMail.php
@@ -1,53 +1,37 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderAnsweredMail extends PonderMail {
public function __construct(
PonderQuestion $question,
PonderAnswer $target,
PhabricatorUser $actor) {
$this->setQuestion($question);
$this->setTarget($target);
$this->setActorHandle($actor);
}
protected function renderVaryPrefix() {
return "[Answered]";
}
protected function renderBody() {
$question = $this->getQuestion();
$target = $this->getTarget();
$actor = $this->getActorName();
$name = $question->getTitle();
$body = array();
$body[] = "{$actor} answered a question that you are subscribed to.";
$body[] = null;
$content = $target->getContent();
if (strlen($content)) {
$body[] = $this->formatText($content);
$body[] = null;
}
return implode("\n", $body);
}
}
diff --git a/src/applications/ponder/mail/PonderCommentMail.php b/src/applications/ponder/mail/PonderCommentMail.php
index 312192e412..57b55ececf 100644
--- a/src/applications/ponder/mail/PonderCommentMail.php
+++ b/src/applications/ponder/mail/PonderCommentMail.php
@@ -1,53 +1,37 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderCommentMail extends PonderMail {
public function __construct(
PonderQuestion $question,
PonderComment $target,
PhabricatorUser $actor) {
$this->setQuestion($question);
$this->setTarget($target);
$this->setActorHandle($actor);
}
protected function renderVaryPrefix() {
return "[Commented]";
}
protected function renderBody() {
$question = $this->getQuestion();
$target = $this->getTarget();
$actor = $this->getActorName();
$name = $question->getTitle();
$body = array();
$body[] = "{$actor} commented on a question that you are subscribed to.";
$body[] = null;
$content = $target->getContent();
if (strlen($content)) {
$body[] = $this->formatText($content);
$body[] = null;
}
return implode("\n", $body);
}
}
diff --git a/src/applications/ponder/mail/PonderMail.php b/src/applications/ponder/mail/PonderMail.php
index d9f10d2333..2b79f57f0a 100644
--- a/src/applications/ponder/mail/PonderMail.php
+++ b/src/applications/ponder/mail/PonderMail.php
@@ -1,145 +1,129 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PonderMail {
protected $to = array();
protected $actorHandle;
protected $question;
protected $target;
protected $isFirstMailAboutQuestion;
// protected $replyHandler;
protected $parentMessageID;
protected function renderSubject() {
$question = $this->getQuestion();
$title = $question->getTitle();
$id = $question->getID();
return "Q{$id}: {$title}";
}
abstract protected function renderVaryPrefix();
abstract protected function renderBody();
public function setActorHandle($actor_handle) {
$this->actorHandle = $actor_handle;
return $this;
}
public function getActorHandle() {
return $this->actorHandle;
}
protected function getActorName() {
return $this->actorHandle->getRealName();
}
protected function getSubjectPrefix() {
return "[Ponder]";
}
public function setToPHIDs(array $to) {
$this->to = $to;
return $this;
}
protected function getToPHIDs() {
return $this->to;
}
public function setQuestion($question) {
$this->question = $question;
return $this;
}
public function getQuestion() {
return $this->question;
}
public function setTarget($target) {
$this->target = $target;
return $this;
}
public function getTarget() {
return $this->target;
}
protected function getThreadID() {
$phid = $this->getQuestion()->getPHID();
return "ponder-ques-{$phid}";
}
protected function getThreadTopic() {
$id = $this->getQuestion()->getID();
$title = $this->getQuestion()->getTitle();
return "Q{$id}: {$title}";
}
public function send() {
$email_to = array_filter(array_unique($this->to));
$question = $this->getQuestion();
$target = $this->getTarget();
$uri = PhabricatorEnv::getURI('/Q'. $question->getID());
$thread_id = $this->getThreadID();
$handles = id(new PhabricatorObjectHandleData($email_to))
->loadHandles();
$reply_handler = new PonderReplyHandler();
$reply_handler->setMailReceiver($question);
$body = new PhabricatorMetaMTAMailBody();
$body->addRawSection($this->renderBody());
$body->addTextSection(pht('QUESTION DETAIL'), $uri);
$template = id(new PhabricatorMetaMTAMail())
->setSubject($this->getThreadTopic())
->setSubjectPrefix($this->getSubjectPrefix())
->setVarySubjectPrefix($this->renderVaryPrefix())
->setFrom($target->getAuthorPHID())
->setParentMessageID($this->parentMessageID)
->addHeader('Thread-Topic', $this->getThreadTopic())
->setThreadID($this->getThreadID(), false)
->setRelatedPHID($question->getPHID())
->setIsBulk(true)
->setBody($body->render());
$mails = $reply_handler->multiplexMail(
$template,
array_select_keys($handles, $email_to),
array());
foreach ($mails as $mail) {
$mail->saveAndSend();
}
}
protected function formatText($text) {
$text = explode("\n", rtrim($text));
foreach ($text as &$line) {
$line = rtrim(' '.$line);
}
unset($line);
return implode("\n", $text);
}
}
diff --git a/src/applications/ponder/mail/PonderMentionMail.php b/src/applications/ponder/mail/PonderMentionMail.php
index c1d3863627..bbd9a7098a 100644
--- a/src/applications/ponder/mail/PonderMentionMail.php
+++ b/src/applications/ponder/mail/PonderMentionMail.php
@@ -1,64 +1,48 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderMentionMail extends PonderMail {
public function __construct(
PonderQuestion $question,
$target,
PhabricatorUser $actor) {
$this->setQuestion($question);
$this->setTarget($target);
$this->setActorHandle($actor);
}
protected function renderVaryPrefix() {
return "[Mentioned]";
}
protected function renderBody() {
$question = $this->getQuestion();
$target = $this->getTarget();
$actor = $this->getActorName();
$name = $question->getTitle();
$targetkind = "somewhere";
if ($target instanceof PonderQuestion) {
$targetkind = "in a question";
}
else if ($target instanceof PonderAnswer) {
$targetkind = "in an answer";
}
else if ($target instanceof PonderComment) {
$targetkind = "in a comment";
}
$body = array();
$body[] = "{$actor} mentioned you {$targetkind}.";
$body[] = null;
$content = $target->getContent();
if (strlen($content)) {
$body[] = $this->formatText($content);
$body[] = null;
}
return implode("\n", $body);
}
}
diff --git a/src/applications/ponder/query/PonderAnswerQuery.php b/src/applications/ponder/query/PonderAnswerQuery.php
index 4e8c75bcf2..af1cff053d 100644
--- a/src/applications/ponder/query/PonderAnswerQuery.php
+++ b/src/applications/ponder/query/PonderAnswerQuery.php
@@ -1,161 +1,145 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderAnswerQuery extends PhabricatorOffsetPagedQuery {
private $id;
private $phid;
private $authorPHID;
private $orderNewest;
public function withID($qid) {
$this->id = $qid;
return $this;
}
public function withPHID($phid) {
$this->phid = $phid;
return $this;
}
public function withAuthorPHID($phid) {
$this->authorPHID = $phid;
return $this;
}
public function orderByNewest($usethis) {
$this->orderNewest = $usethis;
return $this;
}
public static function loadByAuthorWithQuestions(
$viewer,
$phid,
$offset,
$count) {
if (!$viewer) {
throw new Exception("Must set viewer when calling loadByAuthor...");
}
$answers = id(new PonderAnswerQuery())
->withAuthorPHID($phid)
->orderByNewest(true)
->setOffset($offset)
->setLimit($count)
->execute();
$answerset = new LiskDAOSet();
foreach ($answers as $answer) {
$answerset->addToSet($answer);
}
foreach ($answers as $answer) {
$question = $answer->loadOneRelative(
new PonderQuestion(),
'id',
'getQuestionID');
$answer->setQuestion($question);
}
return $answers;
}
public static function loadByAuthor($viewer, $author_phid, $offset, $count) {
if (!$viewer) {
throw new Exception("Must set viewer when calling loadByAuthor");
}
return id(new PonderAnswerQuery())
->withAuthorPHID($author_phid)
->setOffset($offset)
->setLimit($count)
->orderByNewest(true)
->execute();
}
public static function loadSingle($viewer, $id) {
if (!$viewer) {
throw new Exception("Must set viewer when calling loadSingle");
}
return idx(id(new PonderAnswerQuery())
->withID($id)
->execute(), $id);
}
public static function loadSingleByPHID($viewer, $phid) {
if (!$viewer) {
throw new Exception("Must set viewer when calling loadSingle");
}
return array_shift(id(new PonderAnswerQuery())
->withPHID($phid)
->execute());
}
private function buildWhereClause($conn_r) {
$where = array();
if ($this->id) {
$where[] = qsprintf($conn_r, '(id = %d)', $this->id);
}
if ($this->phid) {
$where[] = qsprintf($conn_r, '(phid = %s)', $this->phid);
}
if ($this->authorPHID) {
$where[] = qsprintf($conn_r, '(authorPHID = %s)', $this->authorPHID);
}
return $this->formatWhereClause($where);
}
private function buildOrderByClause($conn_r) {
$order = array();
if ($this->orderNewest) {
$order[] = qsprintf($conn_r, 'id DESC');
}
if (count($order) == 0) {
$order[] = qsprintf($conn_r, 'id ASC');
}
return ($order ? 'ORDER BY ' . implode(', ', $order) : '');
}
public function execute() {
$answer = new PonderAnswer();
$conn_r = $answer->establishConnection('r');
$select = qsprintf(
$conn_r,
'SELECT r.* FROM %T r',
$answer->getTableName());
$where = $this->buildWhereClause($conn_r);
$order_by = $this->buildOrderByClause($conn_r);
$limit = $this->buildLimitClause($conn_r);
return $answer->loadAllFromArray(
queryfx_all(
$conn_r,
'%Q %Q %Q %Q',
$select,
$where,
$order_by,
$limit));
}
}
diff --git a/src/applications/ponder/query/PonderCommentQuery.php b/src/applications/ponder/query/PonderCommentQuery.php
index 2982160614..428f234111 100644
--- a/src/applications/ponder/query/PonderCommentQuery.php
+++ b/src/applications/ponder/query/PonderCommentQuery.php
@@ -1,81 +1,65 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderCommentQuery extends PhabricatorQuery {
private $ids;
private $authorPHID;
private $targetPHIDs;
public function withIDs($qids) {
$this->ids = $qids;
return $this;
}
public function withTargetPHIDs($phids) {
$this->targetPHIDs = $phids;
return $this;
}
public function withAuthorPHID($phid) {
$this->authorPHID = $phid;
return $this;
}
private function buildWhereClause($conn_r) {
$where = array();
if ($this->ids) {
$where[] = qsprintf($conn_r, 'id in (%Ls)', $this->ids);
}
if ($this->authorPHID) {
$where[] = qsprintf($conn_r, 'authorPHID = %s', $this->authorPHID);
}
if ($this->targetPHIDs) {
$where[] = qsprintf($conn_r, 'targetPHID in (%Ls)', $this->targetPHIDs);
}
return $this->formatWhereClause($where);
}
private function buildOrderByClause($conn_r) {
return 'ORDER BY id';
}
public function execute() {
$comment = new PonderComment();
$conn_r = $comment->establishConnection('r');
$select = qsprintf(
$conn_r,
'SELECT r.* FROM %T r',
$comment->getTableName());
$where = $this->buildWhereClause($conn_r);
$order_by = $this->buildOrderByClause($conn_r);
return $comment->loadAllFromArray(
queryfx_all(
$conn_r,
'%Q %Q %Q',
$select,
$where,
$order_by));
}
}
diff --git a/src/applications/ponder/query/PonderQuestionQuery.php b/src/applications/ponder/query/PonderQuestionQuery.php
index 36869bf087..713f9386fc 100644
--- a/src/applications/ponder/query/PonderQuestionQuery.php
+++ b/src/applications/ponder/query/PonderQuestionQuery.php
@@ -1,117 +1,101 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderQuestionQuery extends PhabricatorOffsetPagedQuery {
const ORDER_CREATED = 'order-created';
const ORDER_HOTTEST = 'order-hottest';
private $ids;
private $phids;
private $authorPHIDs;
private $order = self::ORDER_CREATED;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withAuthorPHIDs(array $phids) {
$this->authorPHIDs = $phids;
return $this;
}
public function setOrder($order) {
$this->order = $order;
return $this;
}
public static function loadSingle($viewer, $id) {
if (!$viewer) {
throw new Exception("Must set viewer when calling loadSingle");
}
return idx(id(new PonderQuestionQuery())
->withIDs(array($id))
->execute(), $id);
}
public static function loadSingleByPHID($viewer, $phid) {
if (!$viewer) {
throw new Exception("Must set viewer when calling loadSingle");
}
return array_shift(id(new PonderQuestionQuery())
->withPHIDs(array($phid))
->execute());
}
private function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
if ($this->ids) {
$where[] = qsprintf($conn_r, 'q.id IN (%Ld)', $this->ids);
}
if ($this->phids) {
$where[] = qsprintf($conn_r, 'q.phid IN (%Ls)', $this->phids);
}
if ($this->authorPHIDs) {
$where[] = qsprintf($conn_r, 'q.authorPHID IN (%Ls)', $this->authorPHIDs);
}
return $this->formatWhereClause($where);
}
private function buildOrderByClause(AphrontDatabaseConnection $conn_r) {
switch ($this->order) {
case self::ORDER_HOTTEST:
return qsprintf($conn_r, 'ORDER BY q.heat DESC, q.id DESC');
case self::ORDER_CREATED:
return qsprintf($conn_r, 'ORDER BY q.id DESC');
default:
throw new Exception("Unknown order '{$this->order}'!");
}
}
public function execute() {
$question = new PonderQuestion();
$conn_r = $question->establishConnection('r');
$where = $this->buildWhereClause($conn_r);
$order_by = $this->buildOrderByClause($conn_r);
$limit = $this->buildLimitClause($conn_r);
return $question->loadAllFromArray(
queryfx_all(
$conn_r,
'SELECT q.* FROM %T q %Q %Q %Q',
$question->getTableName(),
$where,
$order_by,
$limit));
}
}
diff --git a/src/applications/ponder/search/PhabricatorSearchPonderIndexer.php b/src/applications/ponder/search/PhabricatorSearchPonderIndexer.php
index 2a0d17d96a..d2dcdefbc0 100644
--- a/src/applications/ponder/search/PhabricatorSearchPonderIndexer.php
+++ b/src/applications/ponder/search/PhabricatorSearchPonderIndexer.php
@@ -1,69 +1,53 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorSearchPonderIndexer
extends PhabricatorSearchDocumentIndexer {
public static function indexQuestion(PonderQuestion $question) {
// note: we assume someone's already called attachrelated on $question
$doc = new PhabricatorSearchAbstractDocument();
$doc->setPHID($question->getPHID());
$doc->setDocumentType(PhabricatorPHIDConstants::PHID_TYPE_QUES);
$doc->setDocumentTitle($question->getTitle());
$doc->setDocumentCreated($question->getDateCreated());
$doc->setDocumentModified($question->getDateModified());
$doc->addField(
PhabricatorSearchField::FIELD_BODY,
$question->getContent());
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR,
$question->getAuthorPHID(),
PhabricatorPHIDConstants::PHID_TYPE_USER,
$question->getDateCreated());
$comments = $question->getComments();
foreach ($comments as $curcomment) {
$doc->addField(
PhabricatorSearchField::FIELD_COMMENT,
$curcomment->getContent()
);
}
$answers = $question->getAnswers();
foreach ($answers as $curanswer) {
if (strlen($curanswer->getContent())) {
$doc->addField(
PhabricatorSearchField::FIELD_COMMENT,
$curanswer->getContent());
}
$answer_comments = $curanswer->getComments();
foreach ($answer_comments as $curcomment) {
$doc->addField(
PhabricatorSearchField::FIELD_COMMENT,
$curcomment->getContent()
);
}
}
self::reindexAbstractDocument($doc);
}
}
diff --git a/src/applications/ponder/storage/PonderAnswer.php b/src/applications/ponder/storage/PonderAnswer.php
index cb9831a950..1a5c8fb736 100644
--- a/src/applications/ponder/storage/PonderAnswer.php
+++ b/src/applications/ponder/storage/PonderAnswer.php
@@ -1,137 +1,121 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderAnswer extends PonderDAO
implements PhabricatorMarkupInterface, PonderVotableInterface {
const MARKUP_FIELD_CONTENT = 'markup:content';
protected $phid;
protected $authorPHID;
protected $questionID;
protected $content;
protected $contentSource;
protected $voteCount;
private $vote;
private $question = null;
private $comments;
public function setQuestion($question) {
$this->question = $question;
return $this;
}
public function getQuestion() {
return $this->question;
}
public function setUserVote($vote) {
$this->vote = $vote['data'];
if (!$this->vote) {
$this->vote = PonderConstants::NONE_VOTE;
}
return $this;
}
public function getUserVote() {
return $this->vote;
}
public function setComments($comments) {
$this->comments = $comments;
return $this;
}
public function getComments() {
return $this->comments;
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
) + parent::getConfiguration();
}
public function setTitle($title) {
$this->title = $title;
if (!$this->getID()) {
$this->originalTitle = $title;
}
return $this;
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_ANSW);
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source->serialize();
return $this;
}
public function getContentSource() {
return PhabricatorContentSource::newFromSerialized($this->contentSource);
}
public function getAnswers() {
return $this->loadRelatives(new PonderAnswer(), "questionID");
}
public function getMarkupField() {
return self::MARKUP_FIELD_CONTENT;
}
// Markup interface
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digest($this->getMarkupText($field));
$id = $this->getID();
return "ponder:A{$id}:{$field}:{$hash}";
}
public function getMarkupText($field) {
return $this->getContent();
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newPonderMarkupEngine();
}
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
public function shouldUseMarkupCache($field) {
return (bool)$this->getID();
}
// votable interface
public function getUserVoteEdgeType() {
return PhabricatorEdgeConfig::TYPE_VOTING_USER_HAS_ANSWER;
}
public function getVotablePHID() {
return $this->getPHID();
}
}
diff --git a/src/applications/ponder/storage/PonderComment.php b/src/applications/ponder/storage/PonderComment.php
index 13250c625f..b26568f7d4 100644
--- a/src/applications/ponder/storage/PonderComment.php
+++ b/src/applications/ponder/storage/PonderComment.php
@@ -1,57 +1,41 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderComment extends PonderDAO
implements PhabricatorMarkupInterface {
const MARKUP_FIELD_CONTENT = 'markup:content';
protected $targetPHID;
protected $authorPHID;
protected $content;
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digest($this->getMarkupText($field));
$id = $this->getID();
return "ponder:c{$id}:{$field}:{$hash}";
}
public function getMarkupText($field) {
return $this->getContent();
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newPonderMarkupEngine();
}
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
public function shouldUseMarkupCache($field) {
return (bool)$this->getID();
}
public function getMarkupField() {
return self::MARKUP_FIELD_CONTENT;
}
}
diff --git a/src/applications/ponder/storage/PonderDAO.php b/src/applications/ponder/storage/PonderDAO.php
index f42e1966e2..0062d4045b 100644
--- a/src/applications/ponder/storage/PonderDAO.php
+++ b/src/applications/ponder/storage/PonderDAO.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PonderDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'ponder';
}
}
diff --git a/src/applications/ponder/storage/PonderQuestion.php b/src/applications/ponder/storage/PonderQuestion.php
index 3e5878e9a3..09f20807ce 100644
--- a/src/applications/ponder/storage/PonderQuestion.php
+++ b/src/applications/ponder/storage/PonderQuestion.php
@@ -1,187 +1,171 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderQuestion extends PonderDAO
implements
PhabricatorMarkupInterface,
PonderVotableInterface,
PhabricatorSubscribableInterface {
const MARKUP_FIELD_CONTENT = 'markup:content';
protected $title;
protected $phid;
protected $authorPHID;
protected $content;
protected $contentSource;
protected $voteCount;
protected $answerCount;
protected $heat;
protected $mailKey;
private $answers;
private $vote;
private $comments;
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_QUES);
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source->serialize();
return $this;
}
public function getContentSource() {
return PhabricatorContentSource::newFromSerialized($this->contentSource);
}
public function attachRelated() {
$this->answers = $this->loadRelatives(new PonderAnswer(), "questionID");
$qa_phids = mpull($this->answers, 'getPHID') + array($this->getPHID());
if ($qa_phids) {
$comments = id(new PonderCommentQuery())
->withTargetPHIDs($qa_phids)
->execute();
$comments = mgroup($comments, 'getTargetPHID');
}
else {
$comments = array();
}
$this->setComments(idx($comments, $this->getPHID(), array()));
foreach ($this->answers as $answer) {
$answer->setQuestion($this);
$answer->setComments(idx($comments, $answer->getPHID(), array()));
}
}
public function attachVotes($user_phid) {
$qa_phids = mpull($this->answers, 'getPHID') + array($this->getPHID());
$edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($user_phid))
->withDestinationPHIDs($qa_phids)
->withEdgeTypes(
array(
PhabricatorEdgeConfig::TYPE_VOTING_USER_HAS_QUESTION,
PhabricatorEdgeConfig::TYPE_VOTING_USER_HAS_ANSWER
))
->needEdgeData(true)
->execute();
$question_edge =
$edges[$user_phid][PhabricatorEdgeConfig::TYPE_VOTING_USER_HAS_QUESTION];
$answer_edges =
$edges[$user_phid][PhabricatorEdgeConfig::TYPE_VOTING_USER_HAS_ANSWER];
$edges = null;
$this->setUserVote(idx($question_edge, $this->getPHID()));
foreach ($this->answers as $answer) {
$answer->setUserVote(idx($answer_edges, $answer->getPHID()));
}
}
public function setUserVote($vote) {
$this->vote = $vote['data'];
if (!$this->vote) {
$this->vote = PonderConstants::NONE_VOTE;
}
return $this;
}
public function getUserVote() {
return $this->vote;
}
public function setComments($comments) {
$this->comments = $comments;
return $this;
}
public function getComments() {
return $this->comments;
}
public function getAnswers() {
return $this->answers;
}
public function getMarkupField() {
return self::MARKUP_FIELD_CONTENT;
}
// Markup interface
public function getMarkupFieldKey($field) {
$hash = PhabricatorHash::digest($this->getMarkupText($field));
$id = $this->getID();
return "ponder:Q{$id}:{$field}:{$hash}";
}
public function getMarkupText($field) {
return $this->getContent();
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newPonderMarkupEngine();
}
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
public function shouldUseMarkupCache($field) {
return (bool)$this->getID();
}
// votable interface
public function getUserVoteEdgeType() {
return PhabricatorEdgeConfig::TYPE_VOTING_USER_HAS_QUESTION;
}
public function getVotablePHID() {
return $this->getPHID();
}
public function isAutomaticallySubscribed($phid) {
return false;
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
}
diff --git a/src/applications/ponder/storage/PonderVotableInterface.php b/src/applications/ponder/storage/PonderVotableInterface.php
index 14c277c2d7..6350a20e80 100644
--- a/src/applications/ponder/storage/PonderVotableInterface.php
+++ b/src/applications/ponder/storage/PonderVotableInterface.php
@@ -1,24 +1,8 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
interface PonderVotableInterface {
public function getUserVoteEdgeType();
public function getVotablePHID();
}
diff --git a/src/applications/ponder/view/PonderAddAnswerView.php b/src/applications/ponder/view/PonderAddAnswerView.php
index 3f8992c821..fd9c4e0c40 100644
--- a/src/applications/ponder/view/PonderAddAnswerView.php
+++ b/src/applications/ponder/view/PonderAddAnswerView.php
@@ -1,95 +1,79 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderAddAnswerView extends AphrontView {
private $question;
private $user;
private $actionURI;
private $draft;
public function setQuestion($question) {
$this->question = $question;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setActionURI($uri) {
$this->actionURI = $uri;
return $this;
}
public function render() {
require_celerity_resource('ponder-core-view-css');
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
$question = $this->question;
$header = id(new PhabricatorHeaderView())
->setHeader('Add Answer');
$form = new AphrontFormView();
$form
->setFlexible(true)
->setUser($this->user)
->setAction($this->actionURI)
->setWorkflow(true)
->addHiddenInput('question_id', $question->getID())
->appendChild(
id(new PhabricatorRemarkupControl())
->setName('answer')
->setLabel('Answer')
->setError(true)
->setID('answer-content'))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue($is_serious ? 'Submit' : 'Make it so.'));
$preview =
'<div class="aphront-panel-flush">'.
'<div id="answer-preview">'.
'<span class="aphront-panel-preview-loading-text">'.
'Loading answer preview...'.
'</span>'.
'</div>'.
'</div>';
Javelin::initBehavior(
'ponder-feedback-preview',
array(
'uri' => '/ponder/answer/preview/',
'content' => 'answer-content',
'preview' => 'answer-preview',
'question_id' => $question->getID()
));
return id(new AphrontNullView())
->appendChild(
array(
$header,
$form,
$preview,
))
->render();
}
}
diff --git a/src/applications/ponder/view/PonderAddCommentView.php b/src/applications/ponder/view/PonderAddCommentView.php
index 3d06c7a8d4..7348787809 100644
--- a/src/applications/ponder/view/PonderAddCommentView.php
+++ b/src/applications/ponder/view/PonderAddCommentView.php
@@ -1,75 +1,59 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderAddCommentView extends AphrontView {
private $target;
private $user;
private $actionURI;
private $questionID;
public function setTarget($target) {
$this->target = $target;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setQuestionID($id) {
$this->questionID = $id;
return $this;
}
public function setActionURI($uri) {
$this->actionURI = $uri;
return $this;
}
public function render() {
require_celerity_resource('ponder-comment-table-css');
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
$questionID = $this->questionID;
$target = $this->target;
$form = new AphrontFormView();
$form
->setUser($this->user)
->setAction($this->actionURI)
->setWorkflow(true)
->addHiddenInput('target', $target)
->addHiddenInput('question_id', $questionID)
->appendChild(
id(new AphrontFormTextAreaControl())
->setName('content'))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue($is_serious ? 'Submit' : 'Editorialize'));
$view = id(new AphrontMoreView())
->setSome(id(new AphrontNullView())->render())
->setMore($form->render())
->setExpandText('Add Comment');
return $view->render();
}
}
diff --git a/src/applications/ponder/view/PonderAnswerListView.php b/src/applications/ponder/view/PonderAnswerListView.php
index 3e0adbb862..8b6fce58b3 100644
--- a/src/applications/ponder/view/PonderAnswerListView.php
+++ b/src/applications/ponder/view/PonderAnswerListView.php
@@ -1,101 +1,85 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderAnswerListView extends AphrontView {
private $question;
private $handles;
private $user;
private $answers;
public function setQuestion($question) {
$this->question = $question;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setAnswers(array $answers) {
assert_instances_of($answers, 'PonderAnswer');
$this->answers = array();
// group by descreasing score, randomizing
// order within groups
$by_score = mgroup($answers, 'getVoteCount');
$scores = array_keys($by_score);
rsort($scores);
foreach ($scores as $score) {
$group = $by_score[$score];
shuffle($group);
foreach ($group as $cur_answer) {
$this->answers[] = $cur_answer;
}
}
return $this;
}
public function render() {
require_celerity_resource('ponder-post-css');
$question = $this->question;
$user = $this->user;
$handles = $this->handles;
$panel = id(new AphrontPanelView())
->addClass("ponder-panel")
->setHeader("Responses:");
foreach ($this->answers as $cur_answer) {
$view = new PonderPostBodyView();
$view
->setQuestion($question)
->setTarget($cur_answer)
->setAction(PonderConstants::ANSWERED_LITERAL)
->setHandles($handles)
->setUser($user);
$commentview = new PonderCommentListView();
$commentview
->setUser($user)
->setHandles($handles)
->setComments($cur_answer->getComments())
->setTarget($cur_answer->getPHID())
->setQuestionID($question->getID())
->setActionURI(new PhutilURI('/ponder/comment/add/'));
$panel->appendChild($view);
$panel->appendChild($commentview);
$panel->appendChild('<div style="height: 40px; clear : both"></div>');
}
return $panel->render();
}
}
diff --git a/src/applications/ponder/view/PonderCommentListView.php b/src/applications/ponder/view/PonderCommentListView.php
index 3100339bdb..20ac1fc9c8 100644
--- a/src/applications/ponder/view/PonderCommentListView.php
+++ b/src/applications/ponder/view/PonderCommentListView.php
@@ -1,120 +1,104 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderCommentListView extends AphrontView {
private $user;
private $handles;
private $comments;
private $target;
private $actionURI;
private $questionID;
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setComments(array $comments) {
assert_instances_of($comments, 'PonderComment');
$this->comments = $comments;
return $this;
}
public function setQuestionID($id) {
$this->questionID = $id;
return $this;
}
public function setActionURI($uri) {
$this->actionURI = $uri;
return $this;
}
public function setTarget($target) {
$this->target = $target;
return $this;
}
public function render() {
require_celerity_resource('phabricator-remarkup-css');
require_celerity_resource('ponder-comment-table-css');
$user = $this->user;
$handles = $this->handles;
$comments = $this->comments;
$comment_markup = array();
foreach ($comments as $comment) {
$handle = $handles[$comment->getAuthorPHID()];
$body = PhabricatorMarkupEngine::renderOneObject(
$comment,
$comment->getMarkupField(),
$this->user);
$comment_anchor = '<a name="comment-' . $comment->getID() . '" />';
$comment_markup[] =
'<tr>'.
'<th>'.
$comment_anchor.
'</th>'.
'<td>'.
'<div class="phabricator-remarkup ponder-comment-markup">'.
$body.
'&nbsp;&mdash;'.
$handle->renderLink().
'&nbsp;'.
'<span class="ponder-datestamp">'.
phabricator_datetime($comment->getDateCreated(), $user).
'</span>'.
'</div>'.
'</td>'.
'</tr>';
}
$addview = id(new PonderAddCommentView)
->setTarget($this->target)
->setUser($user)
->setQuestionID($this->questionID)
->setActionURI($this->actionURI);
$comment_markup[] =
'<tr>'.
'<th>&nbsp;</th>'.
'<td>'.$addview->render().'</td>'.
'</tr>';
$comment_markup = phutil_render_tag(
'table',
array(
'class' => 'ponder-comments',
),
implode("\n", $comment_markup)
);
return $comment_markup;
}
}
diff --git a/src/applications/ponder/view/PonderPostBodyView.php b/src/applications/ponder/view/PonderPostBodyView.php
index 7d1c3b77f4..90129a3312 100644
--- a/src/applications/ponder/view/PonderPostBodyView.php
+++ b/src/applications/ponder/view/PonderPostBodyView.php
@@ -1,150 +1,134 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderPostBodyView extends AphrontView {
private $target;
private $question;
private $handles;
private $preview;
private $anchorName;
private $user;
private $action;
public function setQuestion($question) {
$this->question = $question;
return $this;
}
public function setTarget($target) {
$this->target = $target;
return $this;
}
public function setAction($action) {
$this->action = $action;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setPreview($preview) {
$this->preview = $preview;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function render() {
if (!$this->user) {
throw new Exception("Call setUser() before rendering!");
}
require_celerity_resource('phabricator-remarkup-css');
require_celerity_resource('ponder-post-css');
$user = $this->user;
$question = $this->question;
$target = $this->target;
$content = $target->getContent();
$info = array();
$content = PhabricatorMarkupEngine::renderOneObject(
$target,
$target->getMarkupField(),
$this->user);
$content =
'<div class="phabricator-remarkup">'.
$content.
'</div>';
$author = $this->handles[$target->getAuthorPHID()];
$actions = array($author->renderLink().' '.$this->action);
$author_link = $author->renderLink();
$xaction_view = id(new PhabricatorTransactionView())
->setUser($user)
->setImageURI($author->getImageURI())
->setContentSource($target->getContentSource())
->setActions($actions);
if ($this->target instanceof PonderAnswer) {
$xaction_view->addClass("ponder-answer");
}
else {
$xaction_view->addClass("ponder-question");
}
if ($this->preview) {
$xaction_view->setIsPreview($this->preview);
} else {
$xaction_view->setEpoch($target->getDateCreated());
if ($this->target instanceof PonderAnswer) {
$anchor_text = 'Q' . $question->getID(). '#A' . $target->getID();
$xaction_view->setAnchor('A'.$target->getID(), $anchor_text);
$xaction_view->addClass("ponder-answer");
}
}
$xaction_view->appendChild(
'<div class="ponder-post-core">'.
$content.
'</div>'
);
$outerview = $xaction_view;
if (!$this->preview) {
$outerview =
id(new PonderVotableView())
->setPHID($target->getPHID())
->setCount($target->getVoteCount())
->setVote($target->getUserVote());
if ($this->target instanceof PonderAnswer) {
$outerview->setURI('/ponder/answer/vote/');
}
else {
$outerview->setURI('/ponder/question/vote/');
}
$outerview->appendChild($xaction_view);
}
return $outerview->render();
}
private function renderHandleList(array $phids) {
$result = array();
foreach ($phids as $phid) {
$result[] = $this->handles[$phid]->renderLink();
}
return implode(', ', $result);
}
}
diff --git a/src/applications/ponder/view/PonderQuestionDetailView.php b/src/applications/ponder/view/PonderQuestionDetailView.php
index 9ff5c3dda3..2cb98221fa 100644
--- a/src/applications/ponder/view/PonderQuestionDetailView.php
+++ b/src/applications/ponder/view/PonderQuestionDetailView.php
@@ -1,73 +1,57 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderQuestionDetailView extends AphrontView {
private $question;
private $user;
private $handles;
public function setQuestion($question) {
$this->question = $question;
return $this;
}
public function setUser($user) {
$this->user = $user;
return $this;
}
public function setHandles($handles) {
$this->handles = $handles;
return $this;
}
public function render() {
require_celerity_resource('ponder-core-view-css');
$question = $this->question;
$handles = $this->handles;
$user = $this->user;
$panel = id(new AphrontPanelView())
->addClass("ponder-panel");
$contentview = new PonderPostBodyView();
$contentview
->setTarget($question)
->setQuestion($question)
->setUser($user)
->setHandles($handles)
->setAction(PonderConstants::ASKED_LITERAL);
$commentview = new PonderCommentListView();
$commentview
->setUser($user)
->setHandles($handles)
->setComments($question->getComments())
->setTarget($question->getPHID())
->setQuestionID($question->getID())
->setActionURI(new PhutilURI('/ponder/comment/add/'));
$panel->appendChild($contentview);
$panel->appendChild($commentview);
return $panel->render();
}
}
diff --git a/src/applications/ponder/view/PonderQuestionSummaryView.php b/src/applications/ponder/view/PonderQuestionSummaryView.php
index 904888ec32..6db364ada2 100644
--- a/src/applications/ponder/view/PonderQuestionSummaryView.php
+++ b/src/applications/ponder/view/PonderQuestionSummaryView.php
@@ -1,105 +1,89 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderQuestionSummaryView extends AphrontView {
private $user;
private $question;
private $handles;
public function setQuestion($question) {
$this->question = $question;
return $this;
}
public function setHandles($handles) {
$this->handles = $handles;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function render() {
require_celerity_resource('ponder-feed-view-css');
$user = $this->user;
$question = $this->question;
$author_phid = $question->getAuthorPHID();
$handles = $this->handles;
$authorlink = $handles[$author_phid]
->renderLink();
$votecount =
'<div class="ponder-summary-votes">'.
phutil_escape_html($question->getVoteCount()).
'<div class="ponder-question-label">'.
'votes'.
'</div>'.
'</div>';
$answerclass = "ponder-summary-answers";
if ($question->getAnswercount() == 0) {
$answerclass .= " ponder-not-answered";
}
$answercount =
'<div class="ponder-summary-answers">'.
phutil_escape_html($question->getAnswerCount()).
'<div class="ponder-question-label">'.
'answers'.
'</div>'.
'</div>';
$title =
'<h2 class="ponder-question-title">'.
phutil_render_tag(
'a',
array(
"href" => '/Q' . $question->getID(),
),
phutil_escape_html(
'Q' . $question->getID() .
' ' . $question->getTitle()
)
) .
'</h2>';
$rhs =
'<div class="ponder-metadata">'.
$title.
'<span class="ponder-small-metadata">'.
'asked on '.
phabricator_datetime($question->getDateCreated(), $user).
' by ' . $authorlink.
'</span>'.
'</div>';
$summary =
'<div class="ponder-question-summary">'.
$votecount.
$answercount.
$rhs.
'</div>';
return $summary;
}
}
diff --git a/src/applications/ponder/view/PonderUserProfileView.php b/src/applications/ponder/view/PonderUserProfileView.php
index c0a6109ece..c64e320c84 100644
--- a/src/applications/ponder/view/PonderUserProfileView.php
+++ b/src/applications/ponder/view/PonderUserProfileView.php
@@ -1,119 +1,103 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderUserProfileView extends AphrontView {
private $user;
private $questionoffset;
private $answeroffset;
private $answers;
private $pagesize;
private $uri;
private $aparam;
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setQuestionOffset($offset) {
$this->questionoffset = $offset;
return $this;
}
public function setAnswerOffset($offset) {
$this->answeroffset = $offset;
return $this;
}
public function setAnswers($data) {
$this->answers = $data;
return $this;
}
public function setPageSize($pagesize) {
$this->pagesize = $pagesize;
return $this;
}
public function setURI($uri, $aparam) {
$this->uri = $uri;
$this->aparam = $aparam;
return $this;
}
public function render() {
require_celerity_resource('ponder-core-view-css');
require_celerity_resource('ponder-feed-view-css');
$user = $this->user;
$aoffset = $this->answeroffset;
$answers = $this->answers;
$uri = $this->uri;
$aparam = $this->aparam;
$pagesize = $this->pagesize;
$apagebuttons = id(new AphrontPagerView())
->setPageSize($pagesize)
->setOffset($aoffset)
->setURI(
$uri
->setFragment('answers'),
$aparam);
$answers = $apagebuttons->sliceResults($answers);
$view = new PhabricatorObjectItemListView();
$view->setNoDataString(pht('No matching answers.'));
foreach ($answers as $answer) {
$question = $answer->getQuestion();
$author_phid = $question->getAuthorPHID();
$item = new PhabricatorObjectItemView();
$href = id(new PhutilURI('/Q' . $question->getID()))
->setFragment('A' . $answer->getID());
$item->setHeader(
'A'.$answer->getID().' '.self::abbreviate($answer->getContent())
);
$item->setHref($href);
$item->addDetail(
pht('Votes'),
$answer->getVoteCount()
);
$item->addDetail(
pht('Question'),
self::abbreviate($question->getTitle())
);
$item->addAttribute(
pht('Created %s', phabricator_date($answer->getDateCreated(), $user))
);
$view->addItem($item);
}
$view->appendChild($apagebuttons);
return $view->render();
}
private function abbreviate($w) {
return phutil_utf8_shorten($w, 60);
}
}
diff --git a/src/applications/ponder/view/PonderVotableView.php b/src/applications/ponder/view/PonderVotableView.php
index 0f1b10ddf6..a921a79d11 100644
--- a/src/applications/ponder/view/PonderVotableView.php
+++ b/src/applications/ponder/view/PonderVotableView.php
@@ -1,106 +1,90 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PonderVotableView extends AphrontView {
private $phid;
private $uri;
private $count;
private $vote;
public function setPHID($phid) {
$this->phid = $phid;
return $this;
}
public function setURI($uri) {
$this->uri = $uri;
return $this;
}
public function setCount($count) {
$this->count = $count;
return $this;
}
public function setVote($vote) {
$this->vote = $vote;
return $this;
}
public function render() {
require_celerity_resource('ponder-vote-css');
require_celerity_resource('javelin-behavior-ponder-votebox');
Javelin::initBehavior('ponder-votebox', array());
$uri = id(new PhutilURI($this->uri))->alter('phid', $this->phid);
$up = javelin_render_tag(
'a',
array(
'href' => (string)$uri,
'sigil' => 'upvote',
'mustcapture' => true,
'class' => ($this->vote > 0) ? 'ponder-vote-active' : null,
),
"\xE2\x96\xB2");
$down = javelin_render_tag(
'a',
array(
'href' => (string)$uri,
'sigil' => 'downvote',
'mustcapture' => true,
'class' => ($this->vote < 0) ? 'ponder-vote-active' : null,
),
"\xE2\x96\xBC");
$count = javelin_render_tag(
'div',
array(
'class' => 'ponder-vote-count',
'sigil' => 'ponder-vote-count',
),
phutil_escape_html($this->count));
return javelin_render_tag(
'div',
array(
'class' => 'ponder-votable',
'sigil' => 'ponder-votable',
'meta' => array(
'count' => (int)$this->count,
'vote' => (int)$this->vote,
),
),
javelin_render_tag(
'div',
array(
'class' => 'ponder-votebox',
),
$up.$count.$down).
phutil_render_tag(
'div',
array(
'class' => 'ponder-votebox-content',
),
$this->renderChildren()));
}
}
diff --git a/src/applications/project/application/PhabricatorApplicationProject.php b/src/applications/project/application/PhabricatorApplicationProject.php
index f673e20ada..4b1541ff02 100644
--- a/src/applications/project/application/PhabricatorApplicationProject.php
+++ b/src/applications/project/application/PhabricatorApplicationProject.php
@@ -1,62 +1,46 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationProject extends PhabricatorApplication {
public function getName() {
return 'Projects';
}
public function getShortDescription() {
return 'Organize Work';
}
public function getBaseURI() {
return '/project/';
}
public function getAutospriteName() {
return 'projects';
}
public function getFlavorText() {
return pht('Group stuff into big piles.');
}
public function getApplicationGroup() {
return self::GROUP_ORGANIZATION;
}
public function getRoutes() {
return array(
'/project/' => array(
'' => 'PhabricatorProjectListController',
'filter/(?P<filter>[^/]+)/' => 'PhabricatorProjectListController',
'edit/(?P<id>[1-9]\d*)/' => 'PhabricatorProjectProfileEditController',
'members/(?P<id>[1-9]\d*)/'
=> 'PhabricatorProjectMembersEditController',
'view/(?P<id>[1-9]\d*)/(?:(?P<page>\w+)/)?'
=> 'PhabricatorProjectProfileController',
'create/' => 'PhabricatorProjectCreateController',
'update/(?P<id>[1-9]\d*)/(?P<action>[^/]+)/'
=> 'PhabricatorProjectUpdateController',
),
);
}
}
diff --git a/src/applications/project/constants/PhabricatorProjectConstants.php b/src/applications/project/constants/PhabricatorProjectConstants.php
index 5ae390b59c..993f312af6 100644
--- a/src/applications/project/constants/PhabricatorProjectConstants.php
+++ b/src/applications/project/constants/PhabricatorProjectConstants.php
@@ -1,21 +1,5 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorProjectConstants {
}
diff --git a/src/applications/project/constants/PhabricatorProjectStatus.php b/src/applications/project/constants/PhabricatorProjectStatus.php
index 51b3534c58..1b46c39f9d 100644
--- a/src/applications/project/constants/PhabricatorProjectStatus.php
+++ b/src/applications/project/constants/PhabricatorProjectStatus.php
@@ -1,40 +1,24 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorProjectStatus {
const STATUS_ACTIVE = 0;
const STATUS_ARCHIVED = 100;
public static function getNameForStatus($status) {
static $map = array(
self::STATUS_ACTIVE => 'Active',
self::STATUS_ARCHIVED => 'Archived',
);
return idx($map, coalesce($status, '?'), 'Unknown');
}
public static function getStatusMap() {
return array(
self::STATUS_ACTIVE => 'Active',
self::STATUS_ARCHIVED => 'Archived',
);
}
}
diff --git a/src/applications/project/constants/PhabricatorProjectTransactionType.php b/src/applications/project/constants/PhabricatorProjectTransactionType.php
index 77170e6f0f..f5fb9f374a 100644
--- a/src/applications/project/constants/PhabricatorProjectTransactionType.php
+++ b/src/applications/project/constants/PhabricatorProjectTransactionType.php
@@ -1,29 +1,13 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorProjectTransactionType
extends PhabricatorProjectConstants {
const TYPE_NAME = 'name';
const TYPE_MEMBERS = 'members';
const TYPE_STATUS = 'status';
const TYPE_CAN_VIEW = 'canview';
const TYPE_CAN_EDIT = 'canedit';
const TYPE_CAN_JOIN = 'canjoin';
}
diff --git a/src/applications/project/controller/PhabricatorProjectController.php b/src/applications/project/controller/PhabricatorProjectController.php
index 2b7736ab74..50522a32f6 100644
--- a/src/applications/project/controller/PhabricatorProjectController.php
+++ b/src/applications/project/controller/PhabricatorProjectController.php
@@ -1,82 +1,66 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorProjectController extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('Project');
$page->setBaseURI('/project/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph("\xE2\x98\xA3");
$page->appendChild($view);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
protected function buildLocalNavigation(PhabricatorProject $project) {
$id = $project->getID();
$nav_view = new AphrontSideNavFilterView();
$uri = new PhutilURI('/project/view/'.$id.'/');
$nav_view->setBaseURI($uri);
$external_arrow = "\xE2\x86\x97";
$tasks_uri = '/maniphest/view/all/?projects='.$project->getPHID();
$slug = PhabricatorSlug::normalize($project->getName());
$phriction_uri = '/w/projects/'.$slug;
$edit_uri = '/project/edit/'.$id.'/';
$members_uri = '/project/members/'.$id.'/';
$nav_view->addFilter('dashboard', 'Dashboard');
$nav_view->addSpacer();
$nav_view->addFilter('feed', 'Feed');
$nav_view->addFilter(null, 'Tasks '.$external_arrow, $tasks_uri);
$nav_view->addFilter(null, 'Wiki '.$external_arrow, $phriction_uri);
$nav_view->addFilter('people', 'People');
$nav_view->addFilter('about', 'About');
$user = $this->getRequest()->getUser();
$can_edit = PhabricatorPolicyCapability::CAN_EDIT;
$nav_view->addSpacer();
if (PhabricatorPolicyFilter::hasCapability($user, $project, $can_edit)) {
$nav_view->addFilter('edit', "Edit Project\xE2\x80\xA6", $edit_uri);
$nav_view->addFilter('members', "Edit Members\xE2\x80\xA6", $members_uri);
} else {
$nav_view->addFilter(
'edit',
"Edit Project\xE2\x80\xA6",
$edit_uri,
$relative = false,
'disabled');
$nav_view->addFilter(
'members',
"Edit Members\xE2\x80\xA6",
$members_uri,
$relative = false,
'disabled');
}
return $nav_view;
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectCreateController.php b/src/applications/project/controller/PhabricatorProjectCreateController.php
index 4f184b061d..5163804c38 100644
--- a/src/applications/project/controller/PhabricatorProjectCreateController.php
+++ b/src/applications/project/controller/PhabricatorProjectCreateController.php
@@ -1,142 +1,126 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorProjectCreateController
extends PhabricatorProjectController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$project = new PhabricatorProject();
$project->setAuthorPHID($user->getPHID());
$profile = new PhabricatorProjectProfile();
$e_name = true;
$errors = array();
if ($request->isFormPost()) {
try {
$xactions = array();
$xaction = new PhabricatorProjectTransaction();
$xaction->setTransactionType(
PhabricatorProjectTransactionType::TYPE_NAME);
$xaction->setNewValue($request->getStr('name'));
$xactions[] = $xaction;
$xaction = new PhabricatorProjectTransaction();
$xaction->setTransactionType(
PhabricatorProjectTransactionType::TYPE_MEMBERS);
$xaction->setNewValue(array($user->getPHID()));
$xactions[] = $xaction;
$editor = new PhabricatorProjectEditor($project);
$editor->setActor($user);
$editor->applyTransactions($xactions);
} catch (PhabricatorProjectNameCollisionException $ex) {
$e_name = 'Not Unique';
$errors[] = $ex->getMessage();
}
$profile->setBlurb($request->getStr('blurb'));
if (!$errors) {
$project->save();
$profile->setProjectPHID($project->getPHID());
$profile->save();
if ($request->isAjax()) {
return id(new AphrontAjaxResponse())
->setContent(array(
'phid' => $project->getPHID(),
'name' => $project->getName(),
));
} else {
return id(new AphrontRedirectResponse())
->setURI('/project/view/'.$project->getID().'/');
}
}
}
$error_view = null;
if ($errors) {
$error_view = new AphrontErrorView();
$error_view->setTitle('Form Errors');
$error_view->setErrors($errors);
}
if ($request->isAjax()) {
$form = new AphrontFormLayoutView();
} else {
$form = new AphrontFormView();
$form->setUser($user);
}
$form
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Name')
->setName('name')
->setValue($project->getName())
->setError($e_name))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Blurb')
->setName('blurb')
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT)
->setValue($profile->getBlurb()));
if ($request->isAjax()) {
$dialog = id(new AphrontDialogView())
->setUser($user)
->setWidth(AphrontDialogView::WIDTH_FORM)
->setTitle('Create a New Project')
->appendChild($error_view)
->appendChild($form)
->addSubmitButton('Create Project')
->addCancelButton('/project/');
return id(new AphrontDialogResponse())->setDialog($dialog);
} else {
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Create')
->addCancelButton('/project/'));
$panel = new AphrontPanelView();
$panel
->setWidth(AphrontPanelView::WIDTH_FORM)
->setHeader('Create a New Project')
->appendChild($form);
return $this->buildStandardPageResponse(
array(
$error_view,
$panel,
),
array(
'title' => 'Create new Project',
));
}
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectListController.php b/src/applications/project/controller/PhabricatorProjectListController.php
index 6e12e5c61b..04679a0702 100644
--- a/src/applications/project/controller/PhabricatorProjectListController.php
+++ b/src/applications/project/controller/PhabricatorProjectListController.php
@@ -1,171 +1,155 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorProjectListController
extends PhabricatorProjectController {
private $filter;
public function willProcessRequest(array $data) {
$this->filter = idx($data, 'filter');
}
public function processRequest() {
$request = $this->getRequest();
$nav = new AphrontSideNavFilterView();
$nav
->setBaseURI(new PhutilURI('/project/filter/'))
->addLabel('User')
->addFilter('active', 'Active')
->addSpacer()
->addLabel('All')
->addFilter('all', 'All Projects')
->addFilter('allactive','Active Projects');
$this->filter = $nav->selectFilter($this->filter, 'active');
$pager = new AphrontPagerView();
$pager->setPageSize(250);
$pager->setURI($request->getRequestURI(), 'page');
$pager->setOffset($request->getInt('page'));
$query = new PhabricatorProjectQuery();
$query->setViewer($request->getUser());
$query->needMembers(true);
$view_phid = $request->getUser()->getPHID();
$status_filter = PhabricatorProjectQuery::STATUS_ANY;
switch ($this->filter) {
case 'active':
$table_header = 'Your Projects';
$query->withMemberPHIDs(array($view_phid));
$query->withStatus(PhabricatorProjectQuery::STATUS_ACTIVE);
break;
case 'allactive':
$status_filter = PhabricatorProjectQuery::STATUS_ACTIVE;
$table_header = 'Active Projects';
// fallthrough
case 'all':
$table_header = 'All Projects';
$query->withStatus($status_filter);
break;
}
$projects = $query->executeWithOffsetPager($pager);
$project_phids = mpull($projects, 'getPHID');
$profiles = array();
if ($projects) {
$profiles = id(new PhabricatorProjectProfile())->loadAllWhere(
'projectPHID in (%Ls)',
$project_phids);
$profiles = mpull($profiles, null, 'getProjectPHID');
}
$tasks = array();
$groups = array();
if ($project_phids) {
$query = id(new ManiphestTaskQuery())
->withAnyProjects($project_phids)
->withStatus(ManiphestTaskQuery::STATUS_OPEN)
->setLimit(PHP_INT_MAX);
$tasks = $query->execute();
foreach ($tasks as $task) {
foreach ($task->getProjectPHIDs() as $phid) {
$groups[$phid][] = $task;
}
}
}
$rows = array();
foreach ($projects as $project) {
$phid = $project->getPHID();
$profile = idx($profiles, $phid);
$members = $project->getMemberPHIDs();
$group = idx($groups, $phid, array());
$task_count = count($group);
$population = count($members);
if ($profile) {
$blurb = $profile->getBlurb();
$blurb = phutil_utf8_shorten($blurb, 64);
} else {
$blurb = null;
}
$rows[] = array(
phutil_render_tag(
'a',
array(
'href' => '/project/view/'.$project->getID().'/',
),
phutil_escape_html($project->getName())),
phutil_escape_html(
PhabricatorProjectStatus::getNameForStatus($project->getStatus())),
phutil_escape_html($blurb),
phutil_escape_html($population),
phutil_render_tag(
'a',
array(
'href' => '/maniphest/view/all/?projects='.$phid,
),
phutil_escape_html($task_count)),
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Project',
'Status',
'Description',
'Population',
'Open Tasks',
));
$table->setColumnClasses(
array(
'pri',
'',
'wide',
'',
''
));
$panel = new AphrontPanelView();
$panel->setHeader($table_header);
$panel->setCreateButton('Create New Project', '/project/create/');
$panel->appendChild($table);
$panel->appendChild($pager);
$nav->appendChild($panel);
return $this->buildStandardPageResponse(
$nav,
array(
'title' => 'Projects',
));
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectMembersEditController.php b/src/applications/project/controller/PhabricatorProjectMembersEditController.php
index 3942755b0e..b1eff0f88d 100644
--- a/src/applications/project/controller/PhabricatorProjectMembersEditController.php
+++ b/src/applications/project/controller/PhabricatorProjectMembersEditController.php
@@ -1,181 +1,165 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorProjectMembersEditController
extends PhabricatorProjectController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$project = id(new PhabricatorProjectQuery())
->setViewer($user)
->withIDs(array($this->id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$project) {
return new Aphront404Response();
}
$profile = $project->loadProfile();
if (empty($profile)) {
$profile = new PhabricatorProjectProfile();
}
$member_phids = $project->loadMemberPHIDs();
$errors = array();
if ($request->isFormPost()) {
$changed_something = false;
$member_map = array_fill_keys($member_phids, true);
$remove = $request->getStr('remove');
if ($remove) {
if (isset($member_map[$remove])) {
unset($member_map[$remove]);
$changed_something = true;
}
} else {
$new_members = $request->getArr('phids');
foreach ($new_members as $member) {
if (empty($member_map[$member])) {
$member_map[$member] = true;
$changed_something = true;
}
}
}
$xactions = array();
if ($changed_something) {
$xaction = new PhabricatorProjectTransaction();
$xaction->setTransactionType(
PhabricatorProjectTransactionType::TYPE_MEMBERS);
$xaction->setNewValue(array_keys($member_map));
$xactions[] = $xaction;
}
if ($xactions) {
$editor = new PhabricatorProjectEditor($project);
$editor->setActor($user);
$editor->applyTransactions($xactions);
}
return id(new AphrontRedirectResponse())
->setURI($request->getRequestURI());
}
$member_phids = array_reverse($member_phids);
$handles = $this->loadViewerHandles($member_phids);
$state = array();
foreach ($handles as $handle) {
$state[] = array(
'phid' => $handle->getPHID(),
'name' => $handle->getFullName(),
);
}
$header_name = 'Edit Members';
$title = 'Edit Members';
$list = $this->renderMemberList($handles);
$form = new AphrontFormView();
$form
->setUser($user)
->appendChild(
id(new AphrontFormTokenizerControl())
->setName('phids')
->setLabel('Add Members')
->setDatasource('/typeahead/common/users/'))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton('/project/view/'.$project->getID().'/')
->setValue('Add Members'));
$faux_form = id(new AphrontFormLayoutView())
->setBackgroundShading(true)
->setPadded(true)
->appendChild(
id(new AphrontFormInsetView())
->setTitle('Current Members ('.count($handles).')')
->appendChild($list));
$panel = new AphrontPanelView();
$panel->setHeader($header_name);
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->appendChild($form);
$panel->appendChild('<br />');
$panel->appendChild($faux_form);
$nav = $this->buildLocalNavigation($project);
$nav->selectFilter('members');
$nav->appendChild($panel);
return $this->buildStandardPageResponse(
$nav,
array(
'title' => $title,
));
}
private function renderMemberList(array $handles) {
$request = $this->getRequest();
$user = $request->getUser();
$list = id(new PhabricatorObjectListView())
->setHandles($handles);
foreach ($handles as $handle) {
$hidden_input = phutil_render_tag(
'input',
array(
'type' => 'hidden',
'name' => 'remove',
'value' => $handle->getPHID(),
),
'');
$button = javelin_render_tag(
'button',
array(
'class' => 'grey',
),
pht('Remove'));
$list->addButton(
$handle,
phabricator_render_form(
$user,
array(
'method' => 'POST',
'action' => $request->getRequestURI(),
),
$hidden_input.$button));
}
return $list;
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php
index 8cff6d26e5..4f6a45d29a 100644
--- a/src/applications/project/controller/PhabricatorProjectProfileController.php
+++ b/src/applications/project/controller/PhabricatorProjectProfileController.php
@@ -1,302 +1,286 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorProjectProfileController
extends PhabricatorProjectController {
private $id;
private $page;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
$this->page = idx($data, 'page');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$query = id(new PhabricatorProjectQuery())
->setViewer($user)
->withIDs(array($this->id));
if ($this->page == 'people') {
$query->needMembers(true);
}
$project = $query->executeOne();
if (!$project) {
return new Aphront404Response();
}
$profile = $project->loadProfile();
if (!$profile) {
$profile = new PhabricatorProjectProfile();
}
$picture = $profile->loadProfileImageURI();
$nav_view = $this->buildLocalNavigation($project);
$this->page = $nav_view->selectFilter($this->page, 'dashboard');
require_celerity_resource('phabricator-profile-css');
switch ($this->page) {
case 'dashboard':
$content = $this->renderTasksPage($project, $profile);
$query = new PhabricatorFeedQuery();
$query->setFilterPHIDs(
array(
$project->getPHID(),
));
$query->setLimit(50);
$query->setViewer($this->getRequest()->getUser());
$stories = $query->execute();
$content .= $this->renderStories($stories);
break;
case 'about':
$content = $this->renderAboutPage($project, $profile);
break;
case 'people':
$content = $this->renderPeoplePage($project, $profile);
break;
case 'feed':
$content = $this->renderFeedPage($project, $profile);
break;
default:
throw new Exception("Unimplemented filter '{$this->page}'.");
}
$content = '<div style="padding: 1em;">'.$content.'</div>';
$nav_view->appendChild($content);
$header = new PhabricatorProfileHeaderView();
$header->setName($project->getName());
$header->setDescription(
phutil_utf8_shorten($profile->getBlurb(), 1024));
$header->setProfilePicture($picture);
$action = null;
if (!$project->isUserMember($user->getPHID())) {
$can_join = PhabricatorPolicyCapability::CAN_JOIN;
if (PhabricatorPolicyFilter::hasCapability($user, $project, $can_join)) {
$class = 'green';
} else {
$class = 'grey disabled';
}
$action = phabricator_render_form(
$user,
array(
'action' => '/project/update/'.$project->getID().'/join/',
'method' => 'post',
),
phutil_render_tag(
'button',
array(
'class' => $class,
),
'Join Project'));
} else {
$action = javelin_render_tag(
'a',
array(
'href' => '/project/update/'.$project->getID().'/leave/',
'sigil' => 'workflow',
'class' => 'grey button',
),
'Leave Project...');
}
$header->addAction($action);
$header->appendChild($nav_view);
return $this->buildStandardPageResponse(
$header,
array(
'title' => $project->getName().' Project',
));
}
private function renderAboutPage(
PhabricatorProject $project,
PhabricatorProjectProfile $profile) {
$viewer = $this->getRequest()->getUser();
$blurb = $profile->getBlurb();
$blurb = phutil_escape_html($blurb);
$blurb = str_replace("\n", '<br />', $blurb);
$phids = array($project->getAuthorPHID());
$phids = array_unique($phids);
$handles = $this->loadViewerHandles($phids);
$timestamp = phabricator_datetime($project->getDateCreated(), $viewer);
$about =
'<div class="phabricator-profile-info-group">
<h1 class="phabricator-profile-info-header">About</h1>
<div class="phabricator-profile-info-pane">
<table class="phabricator-profile-info-table">
<tr>
<th>Creator</th>
<td>'.$handles[$project->getAuthorPHID()]->renderLink().'</td>
</tr>
<tr>
<th>Created</th>
<td>'.$timestamp.'</td>
</tr>
<tr>
<th>PHID</th>
<td>'.phutil_escape_html($project->getPHID()).'</td>
</tr>
<tr>
<th>Blurb</th>
<td>'.$blurb.'</td>
</tr>
</table>
</div>
</div>';
return $about;
}
private function renderPeoplePage(
PhabricatorProject $project,
PhabricatorProjectProfile $profile) {
$member_phids = $project->getMemberPHIDs();
$handles = $this->loadViewerHandles($member_phids);
$affiliated = array();
foreach ($handles as $phids => $handle) {
$affiliated[] = '<li>'.$handle->renderLink().'</li>';
}
if ($affiliated) {
$affiliated = '<ul>'.implode("\n", $affiliated).'</ul>';
} else {
$affiliated = '<p><em>No one is affiliated with this project.</em></p>';
}
return
'<div class="phabricator-profile-info-group">'.
'<h1 class="phabricator-profile-info-header">People</h1>'.
'<div class="phabricator-profile-info-pane">'.
$affiliated.
'</div>'.
'</div>';
}
private function renderFeedPage(
PhabricatorProject $project,
PhabricatorProjectProfile $profile) {
$query = new PhabricatorFeedQuery();
$query->setFilterPHIDs(array($project->getPHID()));
$query->setViewer($this->getRequest()->getUser());
$query->setLimit(100);
$stories = $query->execute();
if (!$stories) {
return 'There are no stories about this project.';
}
return $this->renderStories($stories);
}
private function renderStories(array $stories) {
assert_instances_of($stories, 'PhabricatorFeedStory');
$builder = new PhabricatorFeedBuilder($stories);
$builder->setUser($this->getRequest()->getUser());
$view = $builder->buildView();
return
'<div class="phabricator-profile-info-group">'.
'<h1 class="phabricator-profile-info-header">Activity Feed</h1>'.
'<div class="phabricator-profile-info-pane">'.
$view->render().
'</div>'.
'</div>';
}
private function renderTasksPage(
PhabricatorProject $project,
PhabricatorProjectProfile $profile) {
$query = id(new ManiphestTaskQuery())
->withAnyProjects(array($project->getPHID()))
->withStatus(ManiphestTaskQuery::STATUS_OPEN)
->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY)
->setLimit(10)
->setCalculateRows(true);
$tasks = $query->execute();
$count = $query->getRowCount();
$phids = mpull($tasks, 'getOwnerPHID');
$phids = array_filter($phids);
$handles = $this->loadViewerHandles($phids);
$task_views = array();
foreach ($tasks as $task) {
$view = id(new ManiphestTaskSummaryView())
->setTask($task)
->setHandles($handles)
->setUser($this->getRequest()->getUser());
$task_views[] = $view->render();
}
if (empty($tasks)) {
$task_views = '<em>No open tasks.</em>';
} else {
$task_views = implode('', $task_views);
}
$open = number_format($count);
$more_link = phutil_render_tag(
'a',
array(
'href' => '/maniphest/view/all/?projects='.$project->getPHID(),
),
"View All Open Tasks \xC2\xBB");
$content =
'<div class="phabricator-profile-info-group">
<h1 class="phabricator-profile-info-header">'.
"Open Tasks ({$open})".
'</h1>'.
'<div class="phabricator-profile-info-pane">'.
$task_views.
'<div class="phabricator-profile-info-pane-more-link">'.
$more_link.
'</div>'.
'</div>
</div>';
return $content;
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectProfileEditController.php b/src/applications/project/controller/PhabricatorProjectProfileEditController.php
index e782c94b9b..e1e70b50c7 100644
--- a/src/applications/project/controller/PhabricatorProjectProfileEditController.php
+++ b/src/applications/project/controller/PhabricatorProjectProfileEditController.php
@@ -1,254 +1,238 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorProjectProfileEditController
extends PhabricatorProjectController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$project = id(new PhabricatorProjectQuery())
->setViewer($user)
->withIDs(array($this->id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$project) {
return new Aphront404Response();
}
$profile = $project->loadProfile();
if (empty($profile)) {
$profile = new PhabricatorProjectProfile();
}
$img_src = $profile->loadProfileImageURI();
$options = PhabricatorProjectStatus::getStatusMap();
$supported_formats = PhabricatorFile::getTransformableImageFormats();
$e_name = true;
$e_image = null;
$errors = array();
if ($request->isFormPost()) {
try {
$xactions = array();
$xaction = new PhabricatorProjectTransaction();
$xaction->setTransactionType(
PhabricatorProjectTransactionType::TYPE_NAME);
$xaction->setNewValue($request->getStr('name'));
$xactions[] = $xaction;
$xaction = new PhabricatorProjectTransaction();
$xaction->setTransactionType(
PhabricatorProjectTransactionType::TYPE_STATUS);
$xaction->setNewValue($request->getStr('status'));
$xactions[] = $xaction;
$xaction = new PhabricatorProjectTransaction();
$xaction->setTransactionType(
PhabricatorProjectTransactionType::TYPE_CAN_VIEW);
$xaction->setNewValue($request->getStr('can_view'));
$xactions[] = $xaction;
$xaction = new PhabricatorProjectTransaction();
$xaction->setTransactionType(
PhabricatorProjectTransactionType::TYPE_CAN_EDIT);
$xaction->setNewValue($request->getStr('can_edit'));
$xactions[] = $xaction;
$xaction = new PhabricatorProjectTransaction();
$xaction->setTransactionType(
PhabricatorProjectTransactionType::TYPE_CAN_JOIN);
$xaction->setNewValue($request->getStr('can_join'));
$xactions[] = $xaction;
$editor = new PhabricatorProjectEditor($project);
$editor->setActor($user);
$editor->applyTransactions($xactions);
} catch (PhabricatorProjectNameCollisionException $ex) {
$e_name = 'Not Unique';
$errors[] = $ex->getMessage();
}
$profile->setBlurb($request->getStr('blurb'));
if (!strlen($project->getName())) {
$e_name = 'Required';
$errors[] = 'Project name is required.';
} else {
$e_name = null;
}
$default_image = $request->getExists('default_image');
if ($default_image) {
$profile->setProfileImagePHID(null);
} else if (!empty($_FILES['image'])) {
$err = idx($_FILES['image'], 'error');
if ($err != UPLOAD_ERR_NO_FILE) {
$file = PhabricatorFile::newFromPHPUpload(
$_FILES['image'],
array(
'authorPHID' => $user->getPHID(),
));
$okay = $file->isTransformableImage();
if ($okay) {
$xformer = new PhabricatorImageTransformer();
$xformed = $xformer->executeThumbTransform(
$file,
$x = 50,
$y = 50);
$profile->setProfileImagePHID($xformed->getPHID());
} else {
$e_image = 'Not Supported';
$errors[] =
'This server only supports these image formats: '.
implode(', ', $supported_formats).'.';
}
}
}
if (!$errors) {
$project->save();
$profile->setProjectPHID($project->getPHID());
$profile->save();
return id(new AphrontRedirectResponse())
->setURI('/project/view/'.$project->getID().'/');
}
}
$error_view = null;
if ($errors) {
$error_view = new AphrontErrorView();
$error_view->setTitle('Form Errors');
$error_view->setErrors($errors);
}
$header_name = 'Edit Project';
$title = 'Edit Project';
$action = '/project/edit/'.$project->getID().'/';
$policies = id(new PhabricatorPolicyQuery())
->setViewer($user)
->setObject($project)
->execute();
$form = new AphrontFormView();
$form
->setID('project-edit-form')
->setUser($user)
->setAction($action)
->setEncType('multipart/form-data')
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Name')
->setName('name')
->setValue($project->getName())
->setError($e_name))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Project Status')
->setName('status')
->setOptions($options)
->setValue($project->getStatus()))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Blurb')
->setName('blurb')
->setValue($profile->getBlurb()))
->appendChild(
'<p class="aphront-form-instructions">NOTE: Policy settings are not '.
'yet fully implemented. Some interfaces still ignore these settings, '.
'particularly "Visible To".</p>')
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setName('can_view')
->setCaption('Members can always view a project.')
->setPolicyObject($project)
->setPolicies($policies)
->setCapability(PhabricatorPolicyCapability::CAN_VIEW))
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setName('can_edit')
->setPolicyObject($project)
->setPolicies($policies)
->setCapability(PhabricatorPolicyCapability::CAN_EDIT))
->appendChild(
id(new AphrontFormPolicyControl())
->setUser($user)
->setName('can_join')
->setCaption(
'Users who can edit a project can always join a project.')
->setPolicyObject($project)
->setPolicies($policies)
->setCapability(PhabricatorPolicyCapability::CAN_JOIN))
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel('Profile Image')
->setValue(
phutil_render_tag(
'img',
array(
'src' => $img_src,
))))
->appendChild(
id(new AphrontFormImageControl())
->setLabel('Change Image')
->setName('image')
->setError($e_image)
->setCaption('Supported formats: '.implode(', ', $supported_formats)))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton('/project/view/'.$project->getID().'/')
->setValue('Save'));
$panel = new AphrontPanelView();
$panel->setHeader($header_name);
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->appendChild($form);
$nav = $this->buildLocalNavigation($project);
$nav->selectFilter('edit');
$nav->appendChild(
array(
$error_view,
$panel,
));
return $this->buildStandardPageResponse(
$nav,
array(
'title' => $title,
));
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectUpdateController.php b/src/applications/project/controller/PhabricatorProjectUpdateController.php
index 41d1685f80..9d11fd16a1 100644
--- a/src/applications/project/controller/PhabricatorProjectUpdateController.php
+++ b/src/applications/project/controller/PhabricatorProjectUpdateController.php
@@ -1,94 +1,78 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorProjectUpdateController
extends PhabricatorProjectController {
private $id;
private $action;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
$this->action = $data['action'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$capabilities = array(
PhabricatorPolicyCapability::CAN_VIEW,
);
$process_action = false;
switch ($this->action) {
case 'join':
$capabilities[] = PhabricatorPolicyCapability::CAN_JOIN;
$process_action = $request->isFormPost();
break;
case 'leave':
$process_action = $request->isDialogFormPost();
break;
default:
return new Aphront404Response();
}
$project = id(new PhabricatorProjectQuery())
->setViewer($user)
->withIDs(array($this->id))
->needMembers(true)
->requireCapabilities($capabilities)
->executeOne();
if (!$project) {
return new Aphront404Response();
}
$project_uri = '/project/view/'.$project->getID().'/';
if ($process_action) {
switch ($this->action) {
case 'join':
PhabricatorProjectEditor::applyJoinProject($project, $user);
break;
case 'leave':
PhabricatorProjectEditor::applyLeaveProject($project, $user);
break;
}
return id(new AphrontRedirectResponse())->setURI($project_uri);
}
$dialog = null;
switch ($this->action) {
case 'leave':
$dialog = new AphrontDialogView();
$dialog->setUser($user);
$dialog->setTitle('Really leave project?');
$dialog->appendChild(
'<p>Your tremendous contributions to this project will be sorely '.
'missed. Are you sure you want to leave?</p>');
$dialog->addCancelButton($project_uri);
$dialog->addSubmitButton('Leave Project');
break;
default:
return new Aphront404Response();
}
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/project/editor/PhabricatorProjectEditor.php b/src/applications/project/editor/PhabricatorProjectEditor.php
index edce444584..959e86da34 100644
--- a/src/applications/project/editor/PhabricatorProjectEditor.php
+++ b/src/applications/project/editor/PhabricatorProjectEditor.php
@@ -1,385 +1,369 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorProjectEditor extends PhabricatorEditor {
private $project;
private $projectName;
private $addEdges = array();
private $remEdges = array();
public static function applyJoinProject(
PhabricatorProject $project,
PhabricatorUser $user) {
$members = $project->getMemberPHIDs();
$members[] = $user->getPHID();
self::applyOneTransaction(
$project,
$user,
PhabricatorProjectTransactionType::TYPE_MEMBERS,
$members);
}
public static function applyLeaveProject(
PhabricatorProject $project,
PhabricatorUser $user) {
$members = array_fill_keys($project->getMemberPHIDs(), true);
unset($members[$user->getPHID()]);
$members = array_keys($members);
self::applyOneTransaction(
$project,
$user,
PhabricatorProjectTransactionType::TYPE_MEMBERS,
$members);
}
private static function applyOneTransaction(
PhabricatorProject $project,
PhabricatorUser $user,
$type,
$new_value) {
$xaction = new PhabricatorProjectTransaction();
$xaction->setTransactionType($type);
$xaction->setNewValue($new_value);
$editor = new PhabricatorProjectEditor($project);
$editor->setActor($user);
$editor->applyTransactions(array($xaction));
}
public function __construct(PhabricatorProject $project) {
$this->project = $project;
}
public function applyTransactions(array $transactions) {
assert_instances_of($transactions, 'PhabricatorProjectTransaction');
$actor = $this->requireActor();
$project = $this->project;
$is_new = !$project->getID();
if ($is_new) {
$project->setAuthorPHID($actor->getPHID());
}
foreach ($transactions as $key => $xaction) {
$this->setTransactionOldValue($project, $xaction);
if (!$this->transactionHasEffect($xaction)) {
unset($transactions[$key]);
continue;
}
}
if (!$is_new) {
// You must be able to view a project in order to edit it in any capacity.
PhabricatorPolicyFilter::requireCapability(
$actor,
$project,
PhabricatorPolicyCapability::CAN_VIEW);
$need_edit = false;
$need_join = false;
foreach ($transactions as $key => $xaction) {
if ($this->getTransactionRequiresEditCapability($xaction)) {
$need_edit = true;
}
if ($this->getTransactionRequiresJoinCapability($xaction)) {
$need_join = true;
}
}
if ($need_edit) {
PhabricatorPolicyFilter::requireCapability(
$actor,
$project,
PhabricatorPolicyCapability::CAN_EDIT);
}
if ($need_join) {
PhabricatorPolicyFilter::requireCapability(
$actor,
$project,
PhabricatorPolicyCapability::CAN_JOIN);
}
}
if (!$transactions) {
return $this;
}
foreach ($transactions as $xaction) {
$this->applyTransactionEffect($project, $xaction);
}
try {
$project->openTransaction();
$project->save();
$edge_type = PhabricatorEdgeConfig::TYPE_PROJ_MEMBER;
$editor = new PhabricatorEdgeEditor();
$editor->setActor($actor);
foreach ($this->remEdges as $phid) {
$editor->removeEdge($project->getPHID(), $edge_type, $phid);
}
foreach ($this->addEdges as $phid) {
$editor->addEdge($project->getPHID(), $edge_type, $phid);
}
$editor->save();
foreach ($transactions as $xaction) {
$xaction->setAuthorPHID($actor->getPHID());
$xaction->setProjectID($project->getID());
$xaction->save();
}
$project->saveTransaction();
foreach ($transactions as $xaction) {
$this->publishTransactionStory($project, $xaction);
}
} catch (AphrontQueryDuplicateKeyException $ex) {
// We already validated the slug, but might race. Try again to see if
// that's the issue. If it is, we'll throw a more specific exception. If
// not, throw the original exception.
$this->validateName($project);
throw $ex;
}
// TODO: If we rename a project, we should move its Phriction page. Do
// that once Phriction supports document moves.
return $this;
}
private function validateName(PhabricatorProject $project) {
$slug = $project->getPhrictionSlug();
$name = $project->getName();
if ($slug == '/') {
throw new PhabricatorProjectNameCollisionException(
"Project names must be unique and contain some letters or numbers.");
}
$id = $project->getID();
$collision = id(new PhabricatorProject())->loadOneWhere(
'(name = %s OR phrictionSlug = %s) AND id %Q %nd',
$name,
$slug,
$id ? '!=' : 'IS NOT',
$id ? $id : null);
if ($collision) {
$other_name = $collision->getName();
$other_id = $collision->getID();
throw new PhabricatorProjectNameCollisionException(
"Project names must be unique. The name '{$name}' is too similar to ".
"the name of another project, '{$other_name}' (Project ID: ".
"{$other_id}). Choose a unique name.");
}
}
private function setTransactionOldValue(
PhabricatorProject $project,
PhabricatorProjectTransaction $xaction) {
$type = $xaction->getTransactionType();
switch ($type) {
case PhabricatorProjectTransactionType::TYPE_NAME:
$xaction->setOldValue($project->getName());
break;
case PhabricatorProjectTransactionType::TYPE_STATUS:
$xaction->setOldValue($project->getStatus());
break;
case PhabricatorProjectTransactionType::TYPE_MEMBERS:
$member_phids = $project->loadMemberPHIDs();
$project->attachMemberPHIDs($member_phids);
$old_value = array_values($member_phids);
$xaction->setOldValue($old_value);
$new_value = $xaction->getNewValue();
$new_value = array_filter($new_value);
$new_value = array_unique($new_value);
$new_value = array_values($new_value);
$xaction->setNewValue($new_value);
break;
case PhabricatorProjectTransactionType::TYPE_CAN_VIEW:
$xaction->setOldValue($project->getViewPolicy());
break;
case PhabricatorProjectTransactionType::TYPE_CAN_EDIT:
$xaction->setOldValue($project->getEditPolicy());
break;
case PhabricatorProjectTransactionType::TYPE_CAN_JOIN:
$xaction->setOldValue($project->getJoinPolicy());
break;
default:
throw new Exception("Unknown transaction type '{$type}'!");
}
return $this;
}
private function applyTransactionEffect(
PhabricatorProject $project,
PhabricatorProjectTransaction $xaction) {
$type = $xaction->getTransactionType();
switch ($type) {
case PhabricatorProjectTransactionType::TYPE_NAME:
$project->setName($xaction->getNewValue());
$project->setPhrictionSlug($xaction->getNewValue());
$this->validateName($project);
break;
case PhabricatorProjectTransactionType::TYPE_STATUS:
$project->setStatus($xaction->getNewValue());
break;
case PhabricatorProjectTransactionType::TYPE_MEMBERS:
$old = array_fill_keys($xaction->getOldValue(), true);
$new = array_fill_keys($xaction->getNewValue(), true);
$this->addEdges = array_keys(array_diff_key($new, $old));
$this->remEdges = array_keys(array_diff_key($old, $new));
break;
case PhabricatorProjectTransactionType::TYPE_CAN_VIEW:
$project->setViewPolicy($xaction->getNewValue());
break;
case PhabricatorProjectTransactionType::TYPE_CAN_EDIT:
$project->setEditPolicy($xaction->getNewValue());
// You can't edit away your ability to edit the project.
PhabricatorPolicyFilter::mustRetainCapability(
$this->getActor(),
$project,
PhabricatorPolicyCapability::CAN_EDIT);
break;
case PhabricatorProjectTransactionType::TYPE_CAN_JOIN:
$project->setJoinPolicy($xaction->getNewValue());
break;
default:
throw new Exception("Unknown transaction type '{$type}'!");
}
}
private function publishTransactionStory(
PhabricatorProject $project,
PhabricatorProjectTransaction $xaction) {
$related_phids = array(
$project->getPHID(),
$xaction->getAuthorPHID(),
);
id(new PhabricatorFeedStoryPublisher())
->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_PROJECT)
->setStoryData(
array(
'projectPHID' => $project->getPHID(),
'transactionID' => $xaction->getID(),
'type' => $xaction->getTransactionType(),
'old' => $xaction->getOldValue(),
'new' => $xaction->getNewValue(),
))
->setStoryTime(time())
->setStoryAuthorPHID($xaction->getAuthorPHID())
->setRelatedPHIDs($related_phids)
->publish();
}
private function transactionHasEffect(
PhabricatorProjectTransaction $xaction) {
return ($xaction->getOldValue() !== $xaction->getNewValue());
}
/**
* All transactions except joining or leaving a project require edit
* capability.
*/
private function getTransactionRequiresEditCapability(
PhabricatorProjectTransaction $xaction) {
return ($this->isJoinOrLeaveTransaction($xaction) === null);
}
/**
* Joining a project requires the join capability. Anyone leave a project.
*/
private function getTransactionRequiresJoinCapability(
PhabricatorProjectTransaction $xaction) {
$type = $this->isJoinOrLeaveTransaction($xaction);
return ($type == 'join');
}
/**
* Returns 'join' if this transaction causes the acting user ONLY to join the
* project.
*
* Returns 'leave' if this transaction causes the acting user ONLY to leave
* the project.
*
* Returns null in all other cases.
*/
private function isJoinOrLeaveTransaction(
PhabricatorProjectTransaction $xaction) {
$type = $xaction->getTransactionType();
if ($type != PhabricatorProjectTransactionType::TYPE_MEMBERS) {
return null;
}
switch ($type) {
case PhabricatorProjectTransactionType::TYPE_MEMBERS:
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
if (count($add) > 1) {
return null;
} else if (count($add) == 1) {
if (reset($add) != $this->getActor()->getPHID()) {
return null;
} else {
return 'join';
}
}
if (count($rem) > 1) {
return null;
} else if (count($rem) == 1) {
if (reset($rem) != $this->getActor()->getPHID()) {
return null;
} else {
return 'leave';
}
}
break;
}
return true;
}
}
diff --git a/src/applications/project/editor/__tests__/PhabricatorProjectEditorTestCase.php b/src/applications/project/editor/__tests__/PhabricatorProjectEditorTestCase.php
index 7f45383c4e..2dac871502 100644
--- a/src/applications/project/editor/__tests__/PhabricatorProjectEditorTestCase.php
+++ b/src/applications/project/editor/__tests__/PhabricatorProjectEditorTestCase.php
@@ -1,292 +1,276 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorProjectEditorTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testViewProject() {
$user = $this->createUser();
$user->save();
$user2 = $this->createUser();
$user2->save();
$proj = $this->createProject();
$proj->setAuthorPHID($user->getPHID());
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
PhabricatorProjectEditor::applyJoinProject($proj, $user);
$proj->setViewPolicy(PhabricatorPolicies::POLICY_USER);
$proj->save();
$can_view = PhabricatorPolicyCapability::CAN_VIEW;
// When the view policy is set to "users", any user can see the project.
$this->assertEqual(
true,
(bool)$this->refreshProject($proj, $user));
$this->assertEqual(
true,
(bool)$this->refreshProject($proj, $user2));
// When the view policy is set to "no one", members can still see the
// project.
$proj->setViewPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$this->assertEqual(
true,
(bool)$this->refreshProject($proj, $user));
$this->assertEqual(
false,
(bool)$this->refreshProject($proj, $user2));
}
public function testEditProject() {
$user = $this->createUser();
$user->save();
$user2 = $this->createUser();
$user2->save();
$proj = $this->createProject();
$proj->setAuthorPHID($user->getPHID());
$proj->save();
// When edit and view policies are set to "user", anyone can edit.
$proj->setViewPolicy(PhabricatorPolicies::POLICY_USER);
$proj->setEditPolicy(PhabricatorPolicies::POLICY_USER);
$proj->save();
$this->assertEqual(
true,
$this->attemptProjectEdit($proj, $user));
// When edit policy is set to "no one", no one can edit.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$caught = null;
try {
$this->attemptProjectEdit($proj, $user);
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertEqual(true, ($caught instanceof Exception));
}
private function attemptProjectEdit(
PhabricatorProject $proj,
PhabricatorUser $user,
$skip_refresh = false) {
$proj = $this->refreshProject($proj, $user, true);
$new_name = $proj->getName().' '.mt_rand();
$xaction = new PhabricatorProjectTransaction();
$xaction->setTransactionType(PhabricatorProjectTransactionType::TYPE_NAME);
$xaction->setNewValue($new_name);
$editor = new PhabricatorProjectEditor($proj);
$editor->setActor($user);
$editor->applyTransactions(array($xaction));
return true;
}
public function testJoinLeaveProject() {
$user = $this->createUser();
$user->save();
$proj = $this->createProjectWithNewAuthor();
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
$this->assertEqual(
true,
(bool)$proj,
'Assumption that projects are default visible to any user when created.');
$this->assertEqual(
false,
$proj->isUserMember($user->getPHID()),
'Arbitrary user not member of project.');
// Join the project.
PhabricatorProjectEditor::applyJoinProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertEqual(true, (bool)$proj);
$this->assertEqual(
true,
$proj->isUserMember($user->getPHID()),
'Join works.');
// Join the project again.
PhabricatorProjectEditor::applyJoinProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertEqual(true, (bool)$proj);
$this->assertEqual(
true,
$proj->isUserMember($user->getPHID()),
'Joining an already-joined project is a no-op.');
// Leave the project.
PhabricatorProjectEditor::applyLeaveProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertEqual(true, (bool)$proj);
$this->assertEqual(
false,
$proj->isUserMember($user->getPHID()),
'Leave works.');
// Leave the project again.
PhabricatorProjectEditor::applyLeaveProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertEqual(true, (bool)$proj);
$this->assertEqual(
false,
$proj->isUserMember($user->getPHID()),
'Leaving an already-left project is a no-op.');
// If a user can't edit or join a project, joining fails.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->setJoinPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
$caught = null;
try {
PhabricatorProjectEditor::applyJoinProject($proj, $user);
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertEqual(true, ($ex instanceof Exception));
// If a user can edit a project, they can join.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_USER);
$proj->setJoinPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
PhabricatorProjectEditor::applyJoinProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertEqual(
true,
$proj->isUserMember($user->getPHID()),
'Join allowed with edit permission.');
PhabricatorProjectEditor::applyLeaveProject($proj, $user);
// If a user can join a project, they can join, even if they can't edit.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->setJoinPolicy(PhabricatorPolicies::POLICY_USER);
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
PhabricatorProjectEditor::applyJoinProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertEqual(
true,
$proj->isUserMember($user->getPHID()),
'Join allowed with join permission.');
// A user can leave a project even if they can't edit it or join.
$proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->setJoinPolicy(PhabricatorPolicies::POLICY_NOONE);
$proj->save();
$proj = $this->refreshProject($proj, $user, true);
PhabricatorProjectEditor::applyLeaveProject($proj, $user);
$proj = $this->refreshProject($proj, $user, true);
$this->assertEqual(
false,
$proj->isUserMember($user->getPHID()),
'Leave allowed without any permission.');
}
private function refreshProject(
PhabricatorProject $project,
PhabricatorUser $viewer,
$need_members = false) {
$results = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->needMembers($need_members)
->withIDs(array($project->getID()))
->execute();
if ($results) {
return head($results);
} else {
return null;
}
}
private function createProject() {
$project = new PhabricatorProject();
$project->setName('Test Project '.mt_rand());
return $project;
}
private function createProjectWithNewAuthor() {
$author = $this->createUser();
$author->save();
$project = $this->createProject();
$project->setAuthorPHID($author->getPHID());
return $project;
}
private function createUser() {
$rand = mt_rand();
$user = new PhabricatorUser();
$user->setUsername('unittestuser'.$rand);
$user->setRealName('Unit Test User '.$rand);
return $user;
}
}
diff --git a/src/applications/project/exception/PhabricatorProjectNameCollisionException.php b/src/applications/project/exception/PhabricatorProjectNameCollisionException.php
index 1106124440..088d1f3ec1 100644
--- a/src/applications/project/exception/PhabricatorProjectNameCollisionException.php
+++ b/src/applications/project/exception/PhabricatorProjectNameCollisionException.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Thrown when you try to save a project with a name too similar to another
* project.
*/
final class PhabricatorProjectNameCollisionException extends Exception {
}
diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php
index 68f3af2db1..69d3e71635 100644
--- a/src/applications/project/query/PhabricatorProjectQuery.php
+++ b/src/applications/project/query/PhabricatorProjectQuery.php
@@ -1,210 +1,194 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorProjectQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $memberPHIDs;
private $status = 'status-any';
const STATUS_ANY = 'status-any';
const STATUS_OPEN = 'status-open';
const STATUS_CLOSED = 'status-closed';
const STATUS_ACTIVE = 'status-active';
const STATUS_ARCHIVED = 'status-archived';
private $needMembers;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withMemberPHIDs(array $member_phids) {
$this->memberPHIDs = $member_phids;
return $this;
}
public function needMembers($need_members) {
$this->needMembers = $need_members;
return $this;
}
protected function getPagingColumn() {
return 'name';
}
protected function getPagingValue($result) {
return $result->getName();
}
protected function getReversePaging() {
return true;
}
public function loadPage() {
$table = new PhabricatorProject();
$conn_r = $table->establishConnection('r');
// NOTE: Because visibility checks for projects depend on whether or not
// the user is a project member, we always load their membership. If we're
// loading all members anyway we can piggyback on that; otherwise we
// do an explicit join.
$select_clause = '';
if (!$this->needMembers) {
$select_clause = ', vm.dst viewerIsMember';
}
$data = queryfx_all(
$conn_r,
'SELECT p.* %Q FROM %T p %Q %Q %Q %Q %Q',
$select_clause,
$table->getTableName(),
$this->buildJoinClause($conn_r),
$this->buildWhereClause($conn_r),
$this->buildGroupClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
$projects = $table->loadAllFromArray($data);
if ($projects) {
$viewer_phid = $this->getViewer()->getPHID();
if ($this->needMembers) {
$etype = PhabricatorEdgeConfig::TYPE_PROJ_MEMBER;
$members = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(mpull($projects, 'getPHID'))
->withEdgeTypes(array($etype))
->execute();
foreach ($projects as $project) {
$phid = $project->getPHID();
$project->attachMemberPHIDs(array_keys($members[$phid][$etype]));
$project->setIsUserMember(
$viewer_phid,
isset($members[$phid][$etype][$viewer_phid]));
}
} else {
foreach ($data as $row) {
$projects[$row['id']]->setIsUserMember(
$viewer_phid,
($row['viewerIsMember'] !== null));
}
}
}
return $projects;
}
private function buildWhereClause($conn_r) {
$where = array();
if ($this->status != self::STATUS_ANY) {
switch ($this->status) {
case self::STATUS_OPEN:
case self::STATUS_ACTIVE:
$filter = array(
PhabricatorProjectStatus::STATUS_ACTIVE,
);
break;
case self::STATUS_CLOSED:
case self::STATUS_ARCHIVED:
$filter = array(
PhabricatorProjectStatus::STATUS_ARCHIVED,
);
break;
default:
throw new Exception(
"Unknown project status '{$this->status}'!");
}
$where[] = qsprintf(
$conn_r,
'status IN (%Ld)',
$filter);
}
if ($this->ids) {
$where[] = qsprintf(
$conn_r,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids) {
$where[] = qsprintf(
$conn_r,
'phid IN (%Ls)',
$this->phids);
}
if ($this->memberPHIDs) {
$where[] = qsprintf(
$conn_r,
'e.dst IN (%Ls)',
$this->memberPHIDs);
}
$where[] = $this->buildPagingClause($conn_r);
return $this->formatWhereClause($where);
}
private function buildGroupClause($conn_r) {
if ($this->memberPHIDs) {
return 'GROUP BY p.id';
} else {
return '';
}
}
private function buildJoinClause($conn_r) {
$joins = array();
if (!$this->needMembers) {
$joins[] = qsprintf(
$conn_r,
'LEFT JOIN %T vm ON vm.src = p.phid AND vm.type = %d AND vm.dst = %s',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorEdgeConfig::TYPE_PROJ_MEMBER,
$this->getViewer()->getPHID());
}
if ($this->memberPHIDs) {
$joins[] = qsprintf(
$conn_r,
'JOIN %T e ON e.src = p.phid AND e.type = %d',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorEdgeConfig::TYPE_PROJ_MEMBER);
}
return implode(' ', $joins);
}
}
diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php
index 81527871bb..3544cafa1a 100644
--- a/src/applications/project/storage/PhabricatorProject.php
+++ b/src/applications/project/storage/PhabricatorProject.php
@@ -1,147 +1,131 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorProject extends PhabricatorProjectDAO
implements PhabricatorPolicyInterface {
protected $name;
protected $phid;
protected $status = PhabricatorProjectStatus::STATUS_ACTIVE;
protected $authorPHID;
protected $subprojectPHIDs = array();
protected $phrictionSlug;
protected $viewPolicy;
protected $editPolicy;
protected $joinPolicy;
private $subprojectsNeedUpdate;
private $memberPHIDs;
private $sparseMembers = array();
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
PhabricatorPolicyCapability::CAN_JOIN,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
case PhabricatorPolicyCapability::CAN_JOIN:
return $this->getJoinPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
if ($this->isUserMember($viewer->getPHID())) {
// Project members can always view a project.
return true;
}
break;
case PhabricatorPolicyCapability::CAN_EDIT:
break;
case PhabricatorPolicyCapability::CAN_JOIN:
$can_edit = PhabricatorPolicyCapability::CAN_EDIT;
if (PhabricatorPolicyFilter::hasCapability($viewer, $this, $can_edit)) {
// Project editors can always join a project.
return true;
}
break;
}
return false;
}
public function isUserMember($user_phid) {
if (!isset($this->sparseMembers[$user_phid])) {
throw new Exception(
"Call setIsUserMember() before isUserMember()!");
}
return $this->sparseMembers[$user_phid];
}
public function setIsUserMember($user_phid, $is_member) {
$this->sparseMembers[$user_phid] = $is_member;
return $this;
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'subprojectPHIDs' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_PROJ);
}
public function loadProfile() {
$profile = id(new PhabricatorProjectProfile())->loadOneWhere(
'projectPHID = %s',
$this->getPHID());
return $profile;
}
public function attachMemberPHIDs(array $phids) {
$this->memberPHIDs = $phids;
return $this;
}
public function getMemberPHIDs() {
if ($this->memberPHIDs === null) {
throw new Exception("Call attachMemberPHIDs() first!");
}
return $this->memberPHIDs;
}
public function loadMemberPHIDs() {
if (!$this->getPHID()) {
return array();
}
return PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->getPHID(),
PhabricatorEdgeConfig::TYPE_PROJ_MEMBER);
}
public function setPhrictionSlug($slug) {
// NOTE: We're doing a little magic here and stripping out '/' so that
// project pages always appear at top level under projects/ even if the
// display name is "Hack / Slash" or similar (it will become
// 'hack_slash' instead of 'hack/slash').
$slug = str_replace('/', ' ', $slug);
$slug = PhabricatorSlug::normalize($slug);
$this->phrictionSlug = $slug;
return $this;
}
}
diff --git a/src/applications/project/storage/PhabricatorProjectDAO.php b/src/applications/project/storage/PhabricatorProjectDAO.php
index 1a9bd14e79..34f11decc0 100644
--- a/src/applications/project/storage/PhabricatorProjectDAO.php
+++ b/src/applications/project/storage/PhabricatorProjectDAO.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorProjectDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'project';
}
}
diff --git a/src/applications/project/storage/PhabricatorProjectProfile.php b/src/applications/project/storage/PhabricatorProjectProfile.php
index edf9e80a71..fb2e959320 100644
--- a/src/applications/project/storage/PhabricatorProjectProfile.php
+++ b/src/applications/project/storage/PhabricatorProjectProfile.php
@@ -1,36 +1,20 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorProjectProfile extends PhabricatorProjectDAO {
protected $projectPHID;
protected $blurb;
protected $profileImagePHID;
public function loadProfileImageURI() {
$src_phid = $this->getProfileImagePHID();
$file = id(new PhabricatorFile())->loadOneWhere('phid = %s', $src_phid);
if ($file) {
return $file->getBestURI();
}
return PhabricatorUser::getDefaultProfileImageURI();
}
}
diff --git a/src/applications/project/storage/PhabricatorProjectTransaction.php b/src/applications/project/storage/PhabricatorProjectTransaction.php
index d214fcb582..5754a619cf 100644
--- a/src/applications/project/storage/PhabricatorProjectTransaction.php
+++ b/src/applications/project/storage/PhabricatorProjectTransaction.php
@@ -1,39 +1,23 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group project
*/
final class PhabricatorProjectTransaction extends PhabricatorProjectDAO {
protected $projectID;
protected $authorPHID;
protected $transactionType;
protected $oldValue;
protected $newValue;
public function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'oldValue' => self::SERIALIZATION_JSON,
'newValue' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
}
diff --git a/src/applications/repository/application/PhabricatorApplicationRepositories.php b/src/applications/repository/application/PhabricatorApplicationRepositories.php
index 022fd0ed87..97baff5ad4 100644
--- a/src/applications/repository/application/PhabricatorApplicationRepositories.php
+++ b/src/applications/repository/application/PhabricatorApplicationRepositories.php
@@ -1,57 +1,41 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationRepositories extends PhabricatorApplication {
public function getBaseURI() {
return '/repository/';
}
public function getAutospriteName() {
return 'repositories';
}
public function getShortDescription() {
return 'Track Repositories';
}
public function getTitleGlyph() {
return "rX";
}
public function getApplicationGroup() {
return self::GROUP_ADMIN;
}
public function getRoutes() {
return array(
'/repository/' => array(
'' => 'PhabricatorRepositoryListController',
'create/' => 'PhabricatorRepositoryCreateController',
'edit/(?P<id>[1-9]\d*)/(?:(?P<view>\w+)/)?' =>
'PhabricatorRepositoryEditController',
'delete/(?P<id>[1-9]\d*)/' => 'PhabricatorRepositoryDeleteController',
'project/edit/(?P<id>[1-9]\d*)/' =>
'PhabricatorRepositoryArcanistProjectEditController',
'project/delete/(?P<id>[1-9]\d*)/' =>
'PhabricatorRepositoryArcanistProjectDeleteController',
),
);
}
}
diff --git a/src/applications/repository/constants/PhabricatorRepositoryType.php b/src/applications/repository/constants/PhabricatorRepositoryType.php
index be5280ee2c..e741f54489 100644
--- a/src/applications/repository/constants/PhabricatorRepositoryType.php
+++ b/src/applications/repository/constants/PhabricatorRepositoryType.php
@@ -1,39 +1,23 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorRepositoryType {
const REPOSITORY_TYPE_GIT = 'git';
const REPOSITORY_TYPE_SVN = 'svn';
const REPOSITORY_TYPE_MERCURIAL = 'hg';
public static function getAllRepositoryTypes() {
static $map = array(
self::REPOSITORY_TYPE_GIT => 'Git',
self::REPOSITORY_TYPE_SVN => 'Subversion',
self::REPOSITORY_TYPE_MERCURIAL => 'Mercurial',
);
return $map;
}
public static function getNameForRepositoryType($type) {
$map = self::getAllRepositoryTypes();
return idx($map, $type, 'Unknown');
}
}
diff --git a/src/applications/repository/controller/PhabricatorRepositoryArcanistProjectDeleteController.php b/src/applications/repository/controller/PhabricatorRepositoryArcanistProjectDeleteController.php
index a004214f2d..edce5a443d 100644
--- a/src/applications/repository/controller/PhabricatorRepositoryArcanistProjectDeleteController.php
+++ b/src/applications/repository/controller/PhabricatorRepositoryArcanistProjectDeleteController.php
@@ -1,57 +1,41 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorRepositoryArcanistProjectDeleteController
extends PhabricatorRepositoryController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$arc_project =
id(new PhabricatorRepositoryArcanistProject())->load($this->id);
if (!$arc_project) {
return new Aphront404Response();
}
$request = $this->getRequest();
if ($request->isDialogFormPost()) {
$arc_project->delete();
return id(new AphrontRedirectResponse())->setURI('/repository/');
}
$dialog = new AphrontDialogView();
$dialog
->setUser($request->getUser())
->setTitle('Really delete this arcanist project?')
->appendChild(
'<p>Really delete the "'.phutil_escape_html($arc_project->getName()).
'" arcanist project? '.
'This operation can not be undone.</p>')
->setSubmitURI('/repository/project/delete/'.$this->id.'/')
->addSubmitButton('Delete Arcanist Project')
->addCancelButton('/repository/');
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/repository/controller/PhabricatorRepositoryArcanistProjectEditController.php b/src/applications/repository/controller/PhabricatorRepositoryArcanistProjectEditController.php
index 08c5abb248..00fe0a6df1 100644
--- a/src/applications/repository/controller/PhabricatorRepositoryArcanistProjectEditController.php
+++ b/src/applications/repository/controller/PhabricatorRepositoryArcanistProjectEditController.php
@@ -1,127 +1,111 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorRepositoryArcanistProjectEditController
extends PhabricatorRepositoryController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$project = id(new PhabricatorRepositoryArcanistProject())->load($this->id);
if (!$project) {
return new Aphront404Response();
}
$repositories = id(new PhabricatorRepository())->loadAll();
$repos = array(
0 => 'None',
);
foreach ($repositories as $repository) {
$callsign = $repository->getCallsign();
$name = $repository->getname();
$repos[$repository->getID()] = "r{$callsign} ({$name})";
}
if ($request->isFormPost()) {
$indexed = $request->getStrList('symbolIndexLanguages');
$indexed = array_map('strtolower', $indexed);
$project->setSymbolIndexLanguages($indexed);
$project->setSymbolIndexProjects($request->getArr('symbolIndexProjects'));
$repo_id = $request->getInt('repository', 0);
if (isset($repos[$repo_id])) {
$project->setRepositoryID($repo_id);
$project->save();
return id(new AphrontRedirectResponse())
->setURI('/repository/');
}
}
$langs = $project->getSymbolIndexLanguages();
if ($langs) {
$langs = implode(', ', $langs);
} else {
$langs = null;
}
if ($project->getSymbolIndexProjects()) {
$uses = id(new PhabricatorRepositoryArcanistProject())->loadAllWhere(
'phid in (%Ls)',
$project->getSymbolIndexProjects());
$uses = mpull($uses, 'getName', 'getPHID');
} else {
$uses = array();
}
$form = id(new AphrontFormView())
->setUser($user)
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Name')
->setValue($project->getName()))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('PHID')
->setValue($project->getPHID()))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Repository')
->setOptions($repos)
->setName('repository')
->setValue($project->getRepositoryID()))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Indexed Languages')
->setName('symbolIndexLanguages')
->setCaption('Separate with commas, for example: <tt>php, py</tt>')
->setValue($langs))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel('Uses Symbols From')
->setName('symbolIndexProjects')
->setDatasource('/typeahead/common/arcanistprojects/')
->setValue($uses))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton('/repository/')
->setValue('Save'));
$panel = new AphrontPanelView();
$panel->setWidth(AphrontPanelView::WIDTH_WIDE);
$panel->setHeader('Edit Arcanist Project');
$panel->appendChild($form);
return $this->buildStandardPageResponse(
$panel,
array(
'title' => 'Edit Project',
));
}
}
diff --git a/src/applications/repository/controller/PhabricatorRepositoryController.php b/src/applications/repository/controller/PhabricatorRepositoryController.php
index f8c82f9188..6d29508364 100644
--- a/src/applications/repository/controller/PhabricatorRepositoryController.php
+++ b/src/applications/repository/controller/PhabricatorRepositoryController.php
@@ -1,92 +1,76 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorRepositoryController extends PhabricatorController {
public function shouldRequireAdmin() {
// Most of these controllers are admin-only.
return true;
}
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('Repositories');
$page->setBaseURI('/repository/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph("rX");
$page->appendChild($view);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
private function isPullDaemonRunning() {
$control = new PhabricatorDaemonControl();
$daemons = $control->loadRunningDaemons();
foreach ($daemons as $daemon) {
if ($daemon->isRunning() &&
$daemon->getName() == 'PhabricatorRepositoryPullLocalDaemon')
return true;
}
return false;
}
protected function renderDaemonNotice() {
$documentation = phutil_render_tag(
'a',
array(
'href' => PhabricatorEnv::getDoclink(
'article/Diffusion_User_Guide.html'),
),
'Diffusion User Guide');
$common =
"Without this daemon, Phabricator will not be able to import or update ".
"repositories. For instructions on starting the daemon, see ".
"<strong>{$documentation}</strong>.";
try {
$daemon_running = $this->isPullDaemonRunning();
if ($daemon_running) {
return null;
}
$title = "Repository Daemon Not Running";
$message =
"<p>The repository daemon is not running on this machine. ".
"{$common}</p>";
} catch (CommandException $ex) {
$title = "Unable To Verify Repository Daemon";
$message =
"<p>Unable to determine if the repository daemon is running on this ".
"machine. {$common}</p>".
"<p><strong>Exception:</strong> ".
phutil_escape_html($ex->getMessage()).
"</p>";
}
$view = new AphrontErrorView();
$view->setSeverity(AphrontErrorView::SEVERITY_WARNING);
$view->setTitle($title);
$view->appendChild($message);
return $view;
}
}
diff --git a/src/applications/repository/controller/PhabricatorRepositoryCreateController.php b/src/applications/repository/controller/PhabricatorRepositoryCreateController.php
index 2381eda603..18a48ef38d 100644
--- a/src/applications/repository/controller/PhabricatorRepositoryCreateController.php
+++ b/src/applications/repository/controller/PhabricatorRepositoryCreateController.php
@@ -1,136 +1,120 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorRepositoryCreateController
extends PhabricatorRepositoryController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$e_name = true;
$e_callsign = true;
$repository = new PhabricatorRepository();
$type_map = PhabricatorRepositoryType::getAllRepositoryTypes();
$errors = array();
if ($request->isFormPost()) {
$repository->setName($request->getStr('name'));
$repository->setCallsign($request->getStr('callsign'));
$repository->setVersionControlSystem($request->getStr('type'));
if (!strlen($repository->getName())) {
$e_name = 'Required';
$errors[] = 'Repository name is required.';
} else {
$e_name = null;
}
if (!strlen($repository->getCallsign())) {
$e_callsign = 'Required';
$errors[] = 'Callsign is required.';
} else if (!preg_match('/^[A-Z]+$/', $repository->getCallsign())) {
$e_callsign = 'Invalid';
$errors[] = 'Callsign must be ALL UPPERCASE LETTERS.';
} else {
$e_callsign = null;
}
if (empty($type_map[$repository->getVersionControlSystem()])) {
$errors[] = 'Invalid version control system.';
}
if (!$errors) {
try {
$repository->save();
return id(new AphrontRedirectResponse())
->setURI('/repository/edit/'.$repository->getID().'/');
} catch (AphrontQueryDuplicateKeyException $ex) {
$e_callsign = 'Duplicate';
$errors[] = 'Callsign must be unique. Another repository already '.
'uses that callsign.';
}
}
}
$error_view = null;
if ($errors) {
$error_view = new AphrontErrorView();
$error_view->setErrors($errors);
$error_view->setTitle('Form Errors');
}
$form = new AphrontFormView();
$form
->setUser($user)
->setAction('/repository/create/')
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Name')
->setName('name')
->setValue($repository->getName())
->setError($e_name)
->setCaption('Human-readable repository name.'))
->appendChild(
'<p class="aphront-form-instructions">Select a "Callsign" &mdash; a '.
'short, uppercase string to identify revisions in this repository. If '.
'you choose "EX", revisions in this repository will be identified '.
'with the prefix "rEX".</p>')
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Callsign')
->setName('callsign')
->setValue($repository->getCallsign())
->setError($e_callsign)
->setCaption(
'Short, UPPERCASE identifier. Once set, it can not be changed.'))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Type')
->setName('type')
->setOptions($type_map)
->setValue($repository->getVersionControlSystem()))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Create Repository')
->addCancelButton('/repository/'));
$panel = new AphrontPanelView();
$panel->setHeader('Create Repository');
$panel->appendChild($form);
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
return $this->buildStandardPageResponse(
array(
$error_view,
$panel,
),
array(
'title' => 'Create Repository',
));
}
}
diff --git a/src/applications/repository/controller/PhabricatorRepositoryDeleteController.php b/src/applications/repository/controller/PhabricatorRepositoryDeleteController.php
index e8acc63f7e..4ab58929c9 100644
--- a/src/applications/repository/controller/PhabricatorRepositoryDeleteController.php
+++ b/src/applications/repository/controller/PhabricatorRepositoryDeleteController.php
@@ -1,62 +1,46 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorRepositoryDeleteController
extends PhabricatorRepositoryController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$repository = id(new PhabricatorRepository())->load($this->id);
if (!$repository) {
return new Aphront404Response();
}
$request = $this->getRequest();
if ($request->isDialogFormPost()) {
return id(new AphrontRedirectResponse())->setURI('/repository/');
}
$dialog = new AphrontDialogView();
$text_1 = pht('If you really want to delete the repository, you must run:');
$command = 'bin/repository delete '.
phutil_escape_html($repository->getCallsign());
$text_2 = pht('Repositories touch many objects and as such deletes are '.
'prohibitively expensive to run from the web UI.');
$body = phutil_render_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
'<p>'.$text_1.'</p><p><tt>'.$command.'</tt></p><p>'.$text_2.'</p>');
$dialog
->setUser($request->getUser())
->setTitle(pht('Really want to delete the repository?'))
->appendChild($body)
->setSubmitURI('/repository/delete/'.$this->id.'/')
->addSubmitButton(pht('Okay'));
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/repository/controller/PhabricatorRepositoryEditController.php b/src/applications/repository/controller/PhabricatorRepositoryEditController.php
index 9b3c14328a..9af13bf8fd 100644
--- a/src/applications/repository/controller/PhabricatorRepositoryEditController.php
+++ b/src/applications/repository/controller/PhabricatorRepositoryEditController.php
@@ -1,724 +1,708 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorRepositoryEditController
extends PhabricatorRepositoryController {
private $id;
private $view;
private $repository;
private $sideNav;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
$this->view = idx($data, 'view');
}
public function processRequest() {
$request = $this->getRequest();
$repository = id(new PhabricatorRepository())->load($this->id);
if (!$repository) {
return new Aphront404Response();
}
$views = array(
'basic' => 'Basics',
'tracking' => 'Tracking',
);
$this->repository = $repository;
if (!isset($views[$this->view])) {
$this->view = head_key($views);
}
$nav = new AphrontSideNavView();
foreach ($views as $view => $name) {
$nav->addNavItem(
phutil_render_tag(
'a',
array(
'class' => ($view == $this->view
? 'aphront-side-nav-selected'
: null),
'href' => '/repository/edit/'.$repository->getID().'/'.$view.'/',
),
phutil_escape_html($name)));
}
$nav->appendChild($this->renderDaemonNotice());
$this->sideNav = $nav;
switch ($this->view) {
case 'basic':
return $this->processBasicRequest();
case 'tracking':
return $this->processTrackingRequest();
default:
throw new Exception("Unknown view.");
}
}
protected function processBasicRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$repository = $this->repository;
$repository_id = $repository->getID();
$errors = array();
$e_name = true;
if ($request->isFormPost()) {
$repository->setName($request->getStr('name'));
if (!strlen($repository->getName())) {
$e_name = 'Required';
$errors[] = 'Repository name is required.';
} else {
$e_name = null;
}
$repository->setDetail('description', $request->getStr('description'));
$repository->setDetail('encoding', $request->getStr('encoding'));
if (!$errors) {
$repository->save();
return id(new AphrontRedirectResponse())
->setURI('/repository/edit/'.$repository_id.'/basic/?saved=true');
}
}
$error_view = null;
if ($errors) {
$error_view = new AphrontErrorView();
$error_view->setErrors($errors);
$error_view->setTitle('Form Errors');
} else if ($request->getStr('saved')) {
$error_view = new AphrontErrorView();
$error_view->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
$error_view->setTitle('Changes Saved');
$error_view->appendChild(
'Repository changes were saved.');
}
$encoding_doc_link = PhabricatorEnv::getDoclink(
'article/User_Guide_UTF-8_and_Character_Encoding.html');
$form = new AphrontFormView();
$form
->setUser($user)
->setAction('/repository/edit/'.$repository->getID().'/')
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Name')
->setName('name')
->setValue($repository->getName())
->setError($e_name)
->setCaption('Human-readable repository name.'))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Description')
->setName('description')
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT)
->setValue($repository->getDetail('description')))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Callsign')
->setName('callsign')
->setValue($repository->getCallsign()))
->appendChild('
<p class="aphront-form-instructions">'.
'If source code in this repository uses a character '.
'encoding other than UTF-8 (for example, ISO-8859-1), '.
'specify it here. You can usually leave this field blank. '.
'See User Guide: '.
'<a href="'.$encoding_doc_link.'">'.
'UTF-8 and Character Encoding'.
'</a> for more information.'.
'</p>')
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Encoding')
->setName('encoding')
->setValue($repository->getDetail('encoding')))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Type')
->setName('type')
->setValue($repository->getVersionControlSystem()))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('ID')
->setValue($repository->getID()))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('PHID')
->setValue($repository->getPHID()))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Save'));
$panel = new AphrontPanelView();
$panel->setHeader('Edit Repository');
$panel->appendChild($form);
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$nav = $this->sideNav;
$nav->appendChild($error_view);
$nav->appendChild($panel);
return $this->buildStandardPageResponse(
$nav,
array(
'title' => 'Edit Repository',
));
}
private function processTrackingRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$repository = $this->repository;
$repository_id = $repository->getID();
$errors = array();
$e_uri = null;
$e_path = null;
$is_git = false;
$is_svn = false;
$is_mercurial = false;
$e_ssh_key = null;
$e_ssh_keyfile = null;
$e_branch = null;
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$is_git = true;
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$is_svn = true;
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$is_mercurial = true;
break;
default:
throw new Exception("Unsupported VCS!");
}
$has_branches = ($is_git || $is_mercurial);
$has_local = ($is_git || $is_mercurial);
$has_branch_filter = ($is_git);
$has_auth_support = $is_svn;
if ($request->isFormPost()) {
$tracking = ($request->getStr('tracking') == 'enabled' ? true : false);
$repository->setDetail('tracking-enabled', $tracking);
$repository->setDetail('remote-uri', $request->getStr('uri'));
if ($has_local) {
$repository->setDetail('local-path', $request->getStr('path'));
}
if ($has_branch_filter) {
$branch_filter = $request->getStrList('branch-filter');
$branch_filter = array_fill_keys($branch_filter, true);
$repository->setDetail('branch-filter', $branch_filter);
$close_commits_filter = $request->getStrList('close-commits-filter');
$close_commits_filter = array_fill_keys($close_commits_filter, true);
$repository->setDetail('close-commits-filter', $close_commits_filter);
}
$repository->setDetail(
'disable-autoclose',
$request->getStr('autoclose') == 'disabled' ? true : false);
$repository->setDetail(
'pull-frequency',
max(1, $request->getInt('frequency')));
if ($has_branches) {
$repository->setDetail(
'default-branch',
$request->getStr('default-branch'));
if ($is_git) {
$branch_name = $repository->getDetail('default-branch');
if (strpos($branch_name, '/') !== false) {
$e_branch = 'Invalid';
$errors[] = "Your branch name should not specify an explicit ".
"remote. For instance, use 'master', not ".
"'origin/master'.";
}
}
}
$repository->setDetail(
'default-owners-path',
$request->getStr(
'default-owners-path',
'/'));
$repository->setDetail('ssh-login', $request->getStr('ssh-login'));
$repository->setDetail('ssh-key', $request->getStr('ssh-key'));
$repository->setDetail('ssh-keyfile', $request->getStr('ssh-keyfile'));
$repository->setDetail('http-login', $request->getStr('http-login'));
$repository->setDetail('http-pass', $request->getStr('http-pass'));
if ($repository->getDetail('ssh-key') &&
$repository->getDetail('ssh-keyfile')) {
$errors[] =
"Specify only one of 'SSH Private Key' and 'SSH Private Key File', ".
"not both.";
$e_ssh_key = 'Choose Only One';
$e_ssh_keyfile = 'Choose Only One';
}
$repository->setDetail(
'herald-disabled',
$request->getInt('herald-disabled', 0));
if ($is_svn) {
$repository->setUUID($request->getStr('uuid'));
$subpath = ltrim($request->getStr('svn-subpath'), '/');
if ($subpath) {
$subpath = rtrim($subpath, '/').'/';
}
$repository->setDetail('svn-subpath', $subpath);
}
$repository->setDetail(
'detail-parser',
$request->getStr(
'detail-parser',
'PhabricatorRepositoryDefaultCommitMessageDetailParser'));
if ($tracking) {
if (!$repository->getDetail('remote-uri')) {
$e_uri = 'Required';
$errors[] = "Repository URI is required.";
} else if ($is_svn &&
!preg_match('@/$@', $repository->getDetail('remote-uri'))) {
$e_uri = 'Invalid';
$errors[] = 'Subversion Repository Root must end in a slash ("/").';
} else {
$e_uri = null;
}
if ($has_local) {
if (!$repository->getDetail('local-path')) {
$e_path = 'Required';
$errors[] = "Local path is required.";
} else {
$e_path = null;
}
}
}
if (!$errors) {
$repository->save();
return id(new AphrontRedirectResponse())
->setURI('/repository/edit/'.$repository_id.'/tracking/?saved=true');
}
}
$error_view = null;
if ($errors) {
$error_view = new AphrontErrorView();
$error_view->setErrors($errors);
$error_view->setTitle('Form Errors');
} else if ($request->getStr('saved')) {
$error_view = new AphrontErrorView();
$error_view->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
$error_view->setTitle('Changes Saved');
$error_view->appendChild('Tracking changes were saved.');
} else if (!$repository->isTracked()) {
$error_view = new AphrontErrorView();
$error_view->setSeverity(AphrontErrorView::SEVERITY_WARNING);
$error_view->setTitle('Repository Not Tracked');
$error_view->appendChild(
'Tracking is currently "Disabled" for this repository, so it will '.
'not be imported into Phabricator. You can enable it below.');
}
switch ($repository->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$is_git = true;
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$is_svn = true;
break;
}
$doc_href = PhabricatorEnv::getDoclink('article/Diffusion_User_Guide.html');
$user_guide_link = phutil_render_tag(
'a',
array(
'href' => $doc_href,
),
'Diffusion User Guide');
$form = new AphrontFormView();
$form
->setUser($user)
->setAction('/repository/edit/'.$repository->getID().'/tracking/')
->appendChild(
'<p class="aphront-form-instructions">Phabricator can track '.
'repositories, importing commits as they happen and notifying '.
'Differential, Diffusion, Herald, and other services. To enable '.
'tracking for a repository, configure it here and then start (or '.
'restart) the daemons. More information is available in the '.
'<strong>'.$user_guide_link.'</strong>.</p>');
$form
->appendChild(
id(new AphrontFormInsetView())
->setTitle('Basics')
->appendChild(id(new AphrontFormStaticControl())
->setLabel('Repository Name')
->setValue($repository->getName()))
->appendChild(id(new AphrontFormSelectControl())
->setName('tracking')
->setLabel('Tracking')
->setOptions(array(
'disabled' => 'Disabled',
'enabled' => 'Enabled',
))
->setValue(
$repository->isTracked()
? 'enabled'
: 'disabled')));
$inset = new AphrontFormInsetView();
$inset->setTitle('Remote URI');
$clone_command = null;
$fetch_command = null;
if ($is_git) {
$clone_command = 'git clone';
$fetch_command = 'git fetch';
} else if ($is_mercurial) {
$clone_command = 'hg clone';
$fetch_command = 'hg pull';
}
$uri_label = 'Repository URI';
if ($has_local) {
if ($is_git) {
$instructions =
'Enter the URI to clone this repository from. It should look like '.
'<tt>git@github.com:example/example.git</tt>, '.
'<tt>ssh://user@host.com/git/example.git</tt>, or '.
'<tt>file:///local/path/to/repo</tt>';
} else if ($is_mercurial) {
$instructions =
'Enter the URI to clone this repository from. It should look '.
'something like <tt>ssh://user@host.com/hg/example</tt>';
}
$inset->appendChild(
'<p class="aphront-form-instructions">'.$instructions.'</p>');
} else if ($is_svn) {
$instructions =
'Enter the <strong>Repository Root</strong> for this SVN repository. '.
'You can figure this out by running <tt>svn info</tt> and looking at '.
'the value in the <tt>Repository Root</tt> field. It should be a URI '.
'and look like <tt>http://svn.example.org/svn/</tt>, '.
'<tt>svn+ssh://svn.example.com/svnroot/</tt>, or '.
'<tt>svn://svn.example.net/svn/</tt>';
$inset->appendChild(
'<p class="aphront-form-instructions">'.$instructions.'</p>');
$uri_label = 'Repository Root';
}
$inset
->appendChild(
id(new AphrontFormTextControl())
->setName('uri')
->setLabel($uri_label)
->setID('remote-uri')
->setValue($repository->getDetail('remote-uri'))
->setError($e_uri));
$inset->appendChild(
'<div class="aphront-form-instructions">'.
'If you want to connect to this repository over SSH, enter the '.
'username and private key to use. You can leave these fields blank if '.
'the repository does not use SSH.'.
'</div>');
$inset
->appendChild(
id(new AphrontFormTextControl())
->setName('ssh-login')
->setLabel('SSH User')
->setValue($repository->getDetail('ssh-login')))
->appendChild(
id(new AphrontFormTextAreaControl())
->setName('ssh-key')
->setLabel('SSH Private Key')
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT)
->setValue($repository->getDetail('ssh-key'))
->setError($e_ssh_key)
->setCaption('Specify the entire private key, <em>or</em>...'))
->appendChild(
id(new AphrontFormTextControl())
->setName('ssh-keyfile')
->setLabel('SSH Private Key File')
->setValue($repository->getDetail('ssh-keyfile'))
->setError($e_ssh_keyfile)
->setCaption(
'...specify a path on disk where the daemon should '.
'look for a private key.'));
if ($has_auth_support) {
$inset
->appendChild(
'<div class="aphront-form-instructions">'.
'If you want to connect to this repository with a username and '.
'password, such as over HTTP Basic Auth or SVN with SASL, '.
'enter the username and password to use. You can leave these '.
'fields blank if the repository does not use a username and '.
'password for authentication.'.
'</div>')
->appendChild(
id(new AphrontFormTextControl())
->setName('http-login')
->setLabel('Username')
->setValue($repository->getDetail('http-login')))
->appendChild(
id(new AphrontFormPasswordControl())
->setName('http-pass')
->setLabel('Password')
->setValue($repository->getDetail('http-pass')));
}
$inset
->appendChild(
'<div class="aphront-form-important">'.
'To test your authentication configuration, <strong>save this '.
'form</strong> and then run this script:'.
'<code>'.
'phabricator/ $ ./scripts/repository/test_connection.php '.
phutil_escape_html($repository->getCallsign()).
'</code>'.
'This will verify that your configuration is correct and the '.
'daemons can connect to the remote repository and pull changes '.
'from it.'.
'</div>');
$form->appendChild($inset);
$inset = new AphrontFormInsetView();
$inset->setTitle('Repository Information');
if ($has_local) {
$inset->appendChild(
'<p class="aphront-form-instructions">Select a path on local disk '.
'which the daemons should <tt>'.$clone_command.'</tt> the repository '.
'into. This must be readable and writable by the daemons, and '.
'readable by the webserver. The daemons will <tt>'.$fetch_command.
'</tt> and keep this repository up to date.</p>');
$inset->appendChild(
id(new AphrontFormTextControl())
->setName('path')
->setLabel('Local Path')
->setValue($repository->getDetail('local-path'))
->setError($e_path));
} else if ($is_svn) {
$inset->appendChild(
'<p class="aphront-form-instructions">If you only want to parse one '.
'subpath of the repository, specify it here, relative to the '.
'repository root (e.g., <tt>trunk/</tt> or <tt>projects/wheel/</tt>). '.
'If you want to parse multiple subdirectories, create a separate '.
'Phabricator repository for each one.</p>');
$inset->appendChild(
id(new AphrontFormTextControl())
->setName('svn-subpath')
->setLabel('Subpath')
->setValue($repository->getDetail('svn-subpath'))
->setError($e_path));
}
if ($has_branch_filter) {
$branch_filter_str = implode(
', ',
array_keys($repository->getDetail('branch-filter', array())));
$inset
->appendChild(
id(new AphrontFormTextControl())
->setName('branch-filter')
->setLabel('Track Only')
->setValue($branch_filter_str)
->setCaption(
'Optional list of branches to track. Other branches will be '.
'completely ignored. If left empty, all branches are tracked. '.
'Example: <tt>master, release</tt>'));
}
$inset
->appendChild(
id(new AphrontFormTextControl())
->setName('frequency')
->setLabel('Pull Frequency')
->setValue($repository->getDetail('pull-frequency', 15))
->setCaption(
'Number of seconds daemon should sleep between requests. Larger '.
'numbers reduce load but also decrease responsiveness.'));
$form->appendChild($inset);
$inset = new AphrontFormInsetView();
$inset->setTitle('Application Configuration');
if ($has_branches) {
$inset
->appendChild(
id(new AphrontFormTextControl())
->setName('default-branch')
->setLabel('Default Branch')
->setValue($repository->getDefaultBranch())
->setError($e_branch)
->setCaption(
'Default branch to show in Diffusion.'));
}
$inset
->appendChild(id(new AphrontFormSelectControl())
->setName('autoclose')
->setLabel('Autoclose')
->setOptions(array(
'enabled' => 'Enabled: Automatically Close Pushed Revisions',
'disabled' => 'Disabled: Ignore Pushed Revisions',
))
->setCaption(
"Automatically close Differential revisions when associated commits ".
"are pushed to this repository.")
->setValue(
$repository->getDetail('disable-autoclose', false)
? 'disabled'
: 'enabled'));
if ($has_branch_filter) {
$close_commits_filter_str = implode(
', ',
array_keys($repository->getDetail('close-commits-filter', array())));
$inset
->appendChild(
id(new AphrontFormTextControl())
->setName('close-commits-filter')
->setLabel('Autoclose Branches')
->setValue($close_commits_filter_str)
->setCaption(
'Optional list of branches which can trigger autoclose. '.
'If left empty, all branches trigger autoclose.'));
}
$inset
->appendChild(
id(new AphrontFormTextControl())
->setName('default-owners-path')
->setLabel('Default Owners Path')
->setValue(
$repository->getDetail(
'default-owners-path',
'/'))
->setCaption('Default path in Owners tool.'));
$inset
->appendChild(
id(new AphrontFormSelectControl())
->setName('herald-disabled')
->setLabel('Herald/Feed Enabled')
->setValue($repository->getDetail('herald-disabled', 0))
->setOptions(
array(
0 => 'Enabled - Send Email and Publish Stories',
1 => 'Disabled - Do Not Send Email or Publish Stories',
))
->setCaption(
'You can disable Herald commit notifications and feed stories '.
'for this repository. This can be useful when initially importing '.
'a repository. Feed stories are never published about commits '.
'that are more than 24 hours old.'));
$parsers = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorRepositoryCommitMessageDetailParser')
->selectSymbolsWithoutLoading();
$parsers = ipull($parsers, 'name', 'name');
$inset
->appendChild(
'<p class="aphront-form-instructions">If you extend the commit '.
'message format, you can provide a new parser which will extract '.
'extra information from it when commits are imported. This is an '.
'advanced feature, and using the default parser will be suitable '.
'in most cases.</p>')
->appendChild(
id(new AphrontFormSelectControl())
->setName('detail-parser')
->setLabel('Detail Parser')
->setOptions($parsers)
->setValue(
$repository->getDetail(
'detail-parser',
'PhabricatorRepositoryDefaultCommitMessageDetailParser')));
if ($is_svn) {
$inset
->appendChild(
id(new AphrontFormTextControl())
->setName('uuid')
->setLabel('UUID')
->setValue($repository->getUUID())
->setCaption('Repository UUID from <tt>svn info</tt>.'));
}
$form->appendChild($inset);
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Save Configuration'));
$panel = new AphrontPanelView();
$panel->setHeader('Repository Tracking');
$panel->appendChild($form);
$panel->setWidth(AphrontPanelView::WIDTH_WIDE);
$nav = $this->sideNav;
$nav->appendChild($error_view);
$nav->appendChild($panel);
return $this->buildStandardPageResponse(
$nav,
array(
'title' => 'Edit Repository Tracking',
));
}
}
diff --git a/src/applications/repository/controller/PhabricatorRepositoryListController.php b/src/applications/repository/controller/PhabricatorRepositoryListController.php
index 483638acc3..0ff01466c6 100644
--- a/src/applications/repository/controller/PhabricatorRepositoryListController.php
+++ b/src/applications/repository/controller/PhabricatorRepositoryListController.php
@@ -1,181 +1,165 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorRepositoryListController
extends PhabricatorRepositoryController {
public function shouldRequireAdmin() {
return false;
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$is_admin = $user->getIsAdmin();
$repos = id(new PhabricatorRepository())->loadAll();
$rows = array();
foreach ($repos as $repo) {
if ($repo->isTracked()) {
$diffusion_link = phutil_render_tag(
'a',
array(
'href' => '/diffusion/'.$repo->getCallsign().'/',
),
'View in Diffusion');
} else {
$diffusion_link = '<em>Not Tracked</em>';
}
$rows[] = array(
phutil_escape_html($repo->getCallsign()),
phutil_escape_html($repo->getName()),
PhabricatorRepositoryType::getNameForRepositoryType(
$repo->getVersionControlSystem()),
$diffusion_link,
phutil_render_tag(
'a',
array(
'class' => 'button small grey',
'href' => '/repository/edit/'.$repo->getID().'/',
),
'Edit'),
javelin_render_tag(
'a',
array(
'class' => 'button small grey',
'href' => '/repository/delete/'.$repo->getID().'/',
'sigil' => 'workflow',
),
'Delete'),
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Callsign',
'Repository',
'Type',
'Diffusion',
'',
''
));
$table->setColumnClasses(
array(
null,
'wide',
null,
null,
'action',
'action',
));
$table->setColumnVisibility(
array(
true,
true,
true,
true,
$is_admin,
$is_admin,
));
$panel = new AphrontPanelView();
$panel->setHeader('Repositories');
if ($is_admin) {
$panel->setCreateButton('Create New Repository', '/repository/create/');
}
$panel->appendChild($table);
$projects = id(new PhabricatorRepositoryArcanistProject())->loadAll();
$rows = array();
foreach ($projects as $project) {
$repo = idx($repos, $project->getRepositoryID());
if ($repo) {
$repo_name = phutil_escape_html($repo->getName());
} else {
$repo_name = '-';
}
$rows[] = array(
phutil_escape_html($project->getName()),
$repo_name,
phutil_render_tag(
'a',
array(
'href' => '/repository/project/edit/'.$project->getID().'/',
'class' => 'button grey small',
),
'Edit'),
javelin_render_tag(
'a',
array(
'href' => '/repository/project/delete/'.$project->getID().'/',
'class' => 'button grey small',
'sigil' => 'workflow',
),
'Delete'),
);
}
$project_table = new AphrontTableView($rows);
$project_table->setHeaders(
array(
'Project ID',
'Repository',
'',
'',
));
$project_table->setColumnClasses(
array(
'',
'wide',
'action',
'action',
));
$project_table->setColumnVisibility(
array(
true,
true,
$is_admin,
$is_admin,
));
$project_panel = new AphrontPanelView();
$project_panel->setHeader('Arcanist Projects');
$project_panel->appendChild($project_table);
return $this->buildStandardPageResponse(
array(
$this->renderDaemonNotice(),
$panel,
$project_panel,
),
array(
'title' => 'Repository List',
));
}
}
diff --git a/src/applications/repository/daemon/PhabricatorGitGraphStream.php b/src/applications/repository/daemon/PhabricatorGitGraphStream.php
index 62a2382098..23688b5cb0 100644
--- a/src/applications/repository/daemon/PhabricatorGitGraphStream.php
+++ b/src/applications/repository/daemon/PhabricatorGitGraphStream.php
@@ -1,96 +1,80 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorGitGraphStream {
private $repository;
private $iterator;
private $parents = array();
private $dates = array();
public function __construct(
PhabricatorRepository $repository,
$start_commit) {
$this->repository = $repository;
$future = $repository->getLocalCommandFuture(
"log --format=%s %s --",
'%H%x01%P%x01%ct',
$start_commit);
$this->iterator = new LinesOfALargeExecFuture($future);
$this->iterator->setDelimiter("\n");
$this->iterator->rewind();
}
public function getParents($commit) {
if (!isset($this->parents[$commit])) {
$this->parseUntil($commit);
}
return $this->parents[$commit];
}
public function getCommitDate($commit) {
if (!isset($this->dates[$commit])) {
$this->parseUntil($commit);
}
return $this->dates[$commit];
}
private function parseUntil($commit) {
if ($this->isParsed($commit)) {
return;
}
$gitlog = $this->iterator;
while ($gitlog->valid()) {
$line = $gitlog->current();
$gitlog->next();
$line = trim($line);
if (!strlen($line)) {
break;
}
list($hash, $parents, $epoch) = explode("\1", $line);
if ($parents) {
$parents = explode(' ', $parents);
} else {
// First commit.
$parents = array();
}
$this->dates[$hash] = $epoch;
$this->parents[$hash] = $parents;
if ($this->isParsed($commit)) {
return;
}
}
throw new Exception("No such commit '{$commit}' in repository!");
}
private function isParsed($commit) {
return isset($this->dates[$commit]);
}
}
diff --git a/src/applications/repository/daemon/PhabricatorMercurialGraphStream.php b/src/applications/repository/daemon/PhabricatorMercurialGraphStream.php
index 98f5846b70..5616f25a7b 100644
--- a/src/applications/repository/daemon/PhabricatorMercurialGraphStream.php
+++ b/src/applications/repository/daemon/PhabricatorMercurialGraphStream.php
@@ -1,171 +1,155 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Streaming interface on top of "hg log" that gives us performant access to
* the Mercurial commit graph with one nonblocking invocation of "hg". See
* @{class:PhabricatorRepositoryPullLocalDaemon}.
*/
final class PhabricatorMercurialGraphStream {
private $repository;
private $iterator;
private $parents = array();
private $dates = array();
private $local = array();
private $localParents = array();
public function __construct(PhabricatorRepository $repository) {
$this->repository = $repository;
$future = $repository->getLocalCommandFuture(
"log --template '{rev}\1{node}\1{date}\1{parents}\2'");
$this->iterator = new LinesOfALargeExecFuture($future);
$this->iterator->setDelimiter("\2");
$this->iterator->rewind();
}
public function getParents($commit) {
if (!isset($this->parents[$commit])) {
$this->parseUntil('node', $commit);
$local = $this->localParents[$commit];
// The normal parsing pass gives us the local revision numbers of the
// parents, but since we've decided we care about this data, we need to
// convert them into full hashes. To do this, we parse to the deepest
// one and then just look them up.
$parents = array();
if ($local) {
$this->parseUntil('rev', min($local));
foreach ($local as $rev) {
$parents[] = $this->local[$rev];
}
}
$this->parents[$commit] = $parents;
// Throw away the local info for this commit, we no longer need it.
unset($this->localParents[$commit]);
}
return $this->parents[$commit];
}
public function getCommitDate($commit) {
if (!isset($this->dates[$commit])) {
$this->parseUntil('node', $commit);
}
return $this->dates[$commit];
}
/**
* Parse until we have consumed some object. There are two types of parses:
* parse until we find a commit hash ($until_type = "node"), or parse until we
* find a local commit number ($until_type = "rev"). We use the former when
* looking up commits, and the latter when resolving parents.
*/
private function parseUntil($until_type, $until_name) {
if ($this->isParsed($until_type, $until_name)) {
return;
}
$hglog = $this->iterator;
while ($hglog->valid()) {
$line = $hglog->current();
$hglog->next();
$line = trim($line);
if (!strlen($line)) {
break;
}
list($rev, $node, $date, $parents) = explode("\1", $line);
$rev = (int)$rev;
$date = (int)head(explode('.', $date));
$this->dates[$node] = $date;
$this->local[$rev] = $node;
$this->localParents[$node] = $this->parseParents($parents, $rev);
if ($this->isParsed($until_type, $until_name)) {
return;
}
}
throw new Exception(
"No such {$until_type} '{$until_name}' in repository!");
}
/**
* Parse a {parents} template, returning the local commit numbers.
*/
private function parseParents($parents, $target_rev) {
// The hg '{parents}' token is empty if there is one "natural" parent
// (predecessor local commit ID). Othwerwise, it may have one or two
// parents. The string looks like this:
//
// 151:1f6c61a60586 154:1d5f799ebe1e
$parents = trim($parents);
if (strlen($parents)) {
$local = array();
$parents = explode(' ', $parents);
foreach ($parents as $key => $parent) {
$parent = (int)head(explode(':', $parent));
if ($parent == -1) {
// Initial commits will sometimes have "-1" as a parent.
continue;
}
$local[] = $parent;
}
} else if ($target_rev) {
// We have empty parents. If there's a predecessor, that's the local
// parent number.
$local = array($target_rev - 1);
} else {
// Initial commits will sometimes have no parents.
$local = array();
}
return $local;
}
/**
* Returns true if the object specified by $type ('rev' or 'node') and
* $name (rev or node name) has been consumed from the hg process.
*/
private function isParsed($type, $name) {
switch ($type) {
case 'rev':
return isset($this->local[$name]);
case 'node':
return isset($this->dates[$name]);
}
}
}
diff --git a/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php b/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php
index f5f546c628..8473a7051b 100644
--- a/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php
+++ b/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php
@@ -1,984 +1,968 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Run pull commands on local working copies to keep them up to date. This
* daemon handles all repository types.
*
* By default, the daemon pulls **every** repository. If you want it to be
* responsible for only some repositories, you can launch it with a list of
* PHIDs or callsigns:
*
* ./phd launch repositorypulllocal -- X Q Z
*
* You can also launch a daemon which is responsible for all //but// one or
* more repositories:
*
* ./phd launch repositorypulllocal -- --not A --not B
*
* If you have a very large number of repositories and some aren't being pulled
* as frequently as you'd like, you can either change the pull frequency of
* the less-important repositories to a larger number (so the daemon will skip
* them more often) or launch one daemon for all the less-important repositories
* and one for the more important repositories (or one for each more important
* repository).
*
* @task pull Pulling Repositories
* @task git Git Implementation
* @task hg Mercurial Implementation
*/
final class PhabricatorRepositoryPullLocalDaemon
extends PhabricatorDaemon {
private $commitCache = array();
private $repair;
public function setRepair($repair) {
$this->repair = $repair;
return $this;
}
/* -( Pulling Repositories )----------------------------------------------- */
/**
* @task pull
*/
public function run() {
$argv = $this->getArgv();
array_unshift($argv, __CLASS__);
$args = new PhutilArgumentParser($argv);
$args->parse(
array(
array(
'name' => 'no-discovery',
'help' => 'Pull only, without discovering commits.',
),
array(
'name' => 'not',
'param' => 'repository',
'repeat' => true,
'help' => 'Do not pull __repository__.',
),
array(
'name' => 'repositories',
'wildcard' => true,
'help' => 'Pull specific __repositories__ instead of all.',
),
));
$no_discovery = $args->getArg('no-discovery');
$repo_names = $args->getArg('repositories');
$exclude_names = $args->getArg('not');
// Each repository has an individual pull frequency; after we pull it,
// wait that long to pull it again. When we start up, try to pull everything
// serially.
$retry_after = array();
$min_sleep = 15;
while (true) {
$repositories = $this->loadRepositories($repo_names);
if ($exclude_names) {
$exclude = $this->loadRepositories($exclude_names);
$repositories = array_diff_key($repositories, $exclude);
}
// Shuffle the repositories, then re-key the array since shuffle()
// discards keys. This is mostly for startup, we'll use soft priorities
// later.
shuffle($repositories);
$repositories = mpull($repositories, null, 'getID');
// If any repositories were deleted, remove them from the retry timer map
// so we don't end up with a retry timer that never gets updated and
// causes us to sleep for the minimum amount of time.
$retry_after = array_select_keys(
$retry_after,
array_keys($repositories));
// Assign soft priorities to repositories based on how frequently they
// should pull again.
asort($retry_after);
$repositories = array_select_keys(
$repositories,
array_keys($retry_after)) + $repositories;
foreach ($repositories as $id => $repository) {
$after = idx($retry_after, $id, 0);
if ($after > time()) {
continue;
}
$tracked = $repository->isTracked();
if (!$tracked) {
continue;
}
try {
$callsign = $repository->getCallsign();
$this->log("Updating repository '{$callsign}'.");
$this->pullRepository($repository);
if (!$no_discovery) {
// TODO: It would be nice to discover only if we pulled something,
// but this isn't totally trivial.
$lock_name = get_class($this).':'.$callsign;
$lock = PhabricatorGlobalLock::newLock($lock_name);
$lock->lock();
try {
$this->discoverRepository($repository);
} catch (Exception $ex) {
$lock->unlock();
throw $ex;
}
$lock->unlock();
}
$sleep_for = $repository->getDetail('pull-frequency', $min_sleep);
$retry_after[$id] = time() + $sleep_for;
} catch (PhutilLockException $ex) {
$retry_after[$id] = time() + $min_sleep;
$this->log("Failed to acquire lock.");
} catch (Exception $ex) {
$retry_after[$id] = time() + $min_sleep;
phlog($ex);
}
$this->stillWorking();
}
if ($retry_after) {
$sleep_until = max(min($retry_after), time() + $min_sleep);
} else {
$sleep_until = time() + $min_sleep;
}
$this->sleep($sleep_until - time());
}
}
/**
* @task pull
*/
protected function loadRepositories(array $names) {
if (!count($names)) {
return id(new PhabricatorRepository())->loadAll();
} else {
return PhabricatorRepository::loadAllByPHIDOrCallsign($names);
}
}
/**
* @task pull
*/
public function pullRepository(PhabricatorRepository $repository) {
$vcs = $repository->getVersionControlSystem();
$is_svn = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_SVN);
$is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
$is_hg = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL);
if ($is_svn) {
return;
}
$callsign = $repository->getCallsign();
if (!$is_git && !$is_hg) {
throw new Exception(
"Unknown VCS '{$vcs}' for repository '{$callsign}'!");
}
$local_path = $repository->getDetail('local-path');
if (!$local_path) {
throw new Exception(
"No local path is available for repository '{$callsign}'.");
}
if (!Filesystem::pathExists($local_path)) {
$dirname = dirname($local_path);
if (!Filesystem::pathExists($dirname)) {
Filesystem::createDirectory($dirname, 0755, $recursive = true);
}
if ($is_git) {
return $this->executeGitCreate($repository, $local_path);
} else if ($is_hg) {
return $this->executeHgCreate($repository, $local_path);
}
} else {
if ($is_git) {
return $this->executeGitUpdate($repository, $local_path);
} else if ($is_hg) {
return $this->executeHgUpdate($repository, $local_path);
}
}
}
public function discoverRepository(PhabricatorRepository $repository) {
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
return $this->executeGitDiscover($repository);
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
return $this->executeSvnDiscover($repository);
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
return $this->executeHgDiscover($repository);
default:
throw new Exception("Unknown VCS '{$vcs}'!");
}
}
private function isKnownCommit(
PhabricatorRepository $repository,
$target) {
if ($this->getCache($repository, $target)) {
return true;
}
if ($this->repair) {
// In repair mode, rediscover the entire repository, ignoring the
// database state. We can hit the local cache above, but if we miss it
// stop the script from going to the database cache.
return false;
}
$commit = id(new PhabricatorRepositoryCommit())->loadOneWhere(
'repositoryID = %s AND commitIdentifier = %s',
$repository->getID(),
$target);
if (!$commit) {
return false;
}
$this->setCache($repository, $target);
while (count($this->commitCache) > 2048) {
array_shift($this->commitCache);
}
return true;
}
private function isKnownCommitOnAnyAutocloseBranch(
PhabricatorRepository $repository,
$target) {
$commit = id(new PhabricatorRepositoryCommit())->loadOneWhere(
'repositoryID = %s AND commitIdentifier = %s',
$repository->getID(),
$target);
if (!$commit) {
$callsign = $repository->getCallsign();
$console = PhutilConsole::getConsole();
$console->writeErr(
"WARNING: Repository '%s' is missing commits ('%s' is missing from ".
"history). Run '%s' to repair the repository.\n",
$callsign,
$target,
"bin/repository discover --repair {$callsign}");
return false;
}
$data = $commit->loadCommitData();
if (!$data) {
return false;
}
if ($repository->shouldAutocloseCommit($commit, $data)) {
return true;
}
return false;
}
private function recordCommit(
PhabricatorRepository $repository,
$commit_identifier,
$epoch,
$branch = null) {
$commit = new PhabricatorRepositoryCommit();
$commit->setRepositoryID($repository->getID());
$commit->setCommitIdentifier($commit_identifier);
$commit->setEpoch($epoch);
$data = new PhabricatorRepositoryCommitData();
if ($branch) {
$data->setCommitDetail('seenOnBranches', array($branch));
}
try {
$commit->openTransaction();
$commit->save();
$data->setCommitID($commit->getID());
$data->save();
$commit->saveTransaction();
$event = new PhabricatorTimelineEvent(
'cmit',
array(
'id' => $commit->getID(),
));
$event->recordEvent();
$this->insertTask($repository, $commit);
queryfx(
$repository->establishConnection('w'),
'INSERT INTO %T (repositoryID, size, lastCommitID, epoch)
VALUES (%d, 1, %d, %d)
ON DUPLICATE KEY UPDATE
size = size + 1,
lastCommitID =
IF(VALUES(epoch) > epoch, VALUES(lastCommitID), lastCommitID),
epoch = IF(VALUES(epoch) > epoch, VALUES(epoch), epoch)',
PhabricatorRepository::TABLE_SUMMARY,
$repository->getID(),
$commit->getID(),
$epoch);
if ($this->repair) {
// Normally, the query should throw a duplicate key exception. If we
// reach this in repair mode, we've actually performed a repair.
$this->log("Repaired commit '{$commit_identifier}'.");
}
$this->setCache($repository, $commit_identifier);
PhutilEventEngine::dispatchEvent(
new PhabricatorEvent(
PhabricatorEventType::TYPE_DIFFUSION_DIDDISCOVERCOMMIT,
array(
'repository' => $repository,
'commit' => $commit,
)));
} catch (AphrontQueryDuplicateKeyException $ex) {
$commit->killTransaction();
// Ignore. This can happen because we discover the same new commit
// more than once when looking at history, or because of races or
// data inconsistency or cosmic radiation; in any case, we're still
// in a good state if we ignore the failure.
$this->setCache($repository, $commit_identifier);
}
}
private function updateCommit(
PhabricatorRepository $repository,
$commit_identifier,
$branch) {
$commit = id(new PhabricatorRepositoryCommit())->loadOneWhere(
'repositoryID = %s AND commitIdentifier = %s',
$repository->getID(),
$commit_identifier);
if (!$commit) {
// This can happen if the phabricator DB doesn't have the commit info,
// or the commit is so big that phabricator couldn't parse it. In this
// case we just ignore it.
return;
}
$data = id(new PhabricatorRepositoryCommitData())->loadOneWhere(
'commitID = %d',
$commit->getID());
if (!$data) {
$data = new PhabricatorRepositoryCommitData();
$data->setCommitID($commit->getID());
}
$branches = $data->getCommitDetail('seenOnBranches', array());
$branches[] = $branch;
$data->setCommitDetail('seenOnBranches', $branches);
$data->save();
$this->insertTask(
$repository,
$commit,
array(
'only' => true
));
}
private function insertTask(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit,
$data = array()) {
$vcs = $repository->getVersionControlSystem();
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$class = 'PhabricatorRepositoryGitCommitMessageParserWorker';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$class = 'PhabricatorRepositorySvnCommitMessageParserWorker';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$class = 'PhabricatorRepositoryMercurialCommitMessageParserWorker';
break;
default:
throw new Exception("Unknown repository type '{$vcs}'!");
}
$data['commitID'] = $commit->getID();
PhabricatorWorker::scheduleTask($class, $data);
}
private function setCache(
PhabricatorRepository $repository,
$commit_identifier) {
$key = $this->getCacheKey($repository, $commit_identifier);
$this->commitCache[$key] = true;
}
private function getCache(
PhabricatorRepository $repository,
$commit_identifier) {
$key = $this->getCacheKey($repository, $commit_identifier);
return idx($this->commitCache, $key, false);
}
private function getCacheKey(
PhabricatorRepository $repository,
$commit_identifier) {
return $repository->getID().':'.$commit_identifier;
}
/* -( Git Implementation )------------------------------------------------- */
/**
* @task git
*/
private function executeGitCreate(
PhabricatorRepository $repository,
$path) {
$repository->execxRemoteCommand(
'clone --origin origin %s %s',
$repository->getRemoteURI(),
rtrim($path, '/'));
}
/**
* @task git
*/
private function executeGitUpdate(
PhabricatorRepository $repository,
$path) {
// Run a bunch of sanity checks to detect people checking out repositories
// inside other repositories, making empty directories, pointing the local
// path at some random file or path, etc.
list($err, $stdout) = $repository->execLocalCommand(
'rev-parse --show-toplevel');
if ($err) {
// Try to raise a more tailored error message in the more common case
// of the user creating an empty directory. (We could try to remove it,
// but might not be able to, and it's much simpler to raise a good
// message than try to navigate those waters.)
if (is_dir($path)) {
$files = Filesystem::listDirectory($path, $include_hidden = true);
if (!$files) {
throw new Exception(
"Expected to find a git repository at '{$path}', but there ".
"is an empty directory there. Remove the directory: the daemon ".
"will run 'git clone' for you.");
}
}
throw new Exception(
"Expected to find a git repository at '{$path}', but there is ".
"a non-repository directory (with other stuff in it) there. Move or ".
"remove this directory (or reconfigure the repository to use a ".
"different directory), and then either clone a repository yourself ".
"or let the daemon do it.");
} else {
$repo_path = rtrim($stdout, "\n");
if (empty($repo_path)) {
throw new Exception(
"Expected to find a git repository at '{$path}', but ".
"there was no result from `git rev-parse --show-toplevel`. ".
"Something is misconfigured or broken. The git repository ".
"may be inside a '.git/' directory.");
}
if (!Filesystem::pathsAreEquivalent($repo_path, $path)) {
throw new Exception(
"Expected to find repo at '{$path}', but the actual ".
"git repository root for this directory is '{$repo_path}'. ".
"Something is misconfigured. The repository's 'Local Path' should ".
"be set to some place where the daemon can check out a working ".
"copy, and should not be inside another git repository.");
}
}
// This is a local command, but needs credentials.
$future = $repository->getRemoteCommandFuture('fetch --all --prune');
$future->setCWD($path);
$future->resolvex();
}
/**
* @task git
*/
private function executeGitDiscover(
PhabricatorRepository $repository) {
list($remotes) = $repository->execxLocalCommand(
'remote show -n origin');
$matches = null;
if (!preg_match('/^\s*Fetch URL:\s*(.*?)\s*$/m', $remotes, $matches)) {
throw new Exception(
"Expected 'Fetch URL' in 'git remote show -n origin'.");
}
self::executeGitVerifySameOrigin(
$matches[1],
$repository->getRemoteURI(),
$repository->getLocalPath());
list($stdout) = $repository->execxLocalCommand(
'branch -r --verbose --no-abbrev');
$branches = DiffusionGitBranchQuery::parseGitRemoteBranchOutput(
$stdout,
$only_this_remote = DiffusionBranchInformation::DEFAULT_GIT_REMOTE);
$callsign = $repository->getCallsign();
$tracked_something = false;
$this->log("Discovering commits in repository '{$callsign}'...");
foreach ($branches as $name => $commit) {
$this->log("Examining branch '{$name}', at {$commit}.");
if (!$repository->shouldTrackBranch($name)) {
$this->log("Skipping, branch is untracked.");
continue;
}
$tracked_something = true;
if ($this->isKnownCommit($repository, $commit)) {
$this->log("Skipping, HEAD is known.");
continue;
}
$this->log("Looking for new commits.");
$this->executeGitDiscoverCommit($repository, $commit, $name, false);
}
if (!$tracked_something) {
$repo_name = $repository->getName();
$repo_callsign = $repository->getCallsign();
throw new Exception(
"Repository r{$repo_callsign} '{$repo_name}' has no tracked branches! ".
"Verify that your branch filtering settings are correct.");
}
$this->log("Discovering commits on autoclose branches...");
foreach ($branches as $name => $commit) {
$this->log("Examining branch '{$name}', at {$commit}'.");
if (!$repository->shouldTrackBranch($name)) {
$this->log("Skipping, branch is untracked.");
continue;
}
if (!$repository->shouldAutocloseBranch($name)) {
$this->log("Skipping, branch is not autoclose.");
continue;
}
if ($this->isKnownCommitOnAnyAutocloseBranch($repository, $commit)) {
$this->log("Skipping, commit is known on an autoclose branch.");
continue;
}
$this->log("Looking for new autoclose commits.");
$this->executeGitDiscoverCommit($repository, $commit, $name, true);
}
}
/**
* @task git
*/
private function executeGitDiscoverCommit(
PhabricatorRepository $repository,
$commit,
$branch,
$autoclose) {
$discover = array($commit);
$insert = array($commit);
$seen_parent = array();
$stream = new PhabricatorGitGraphStream($repository, $commit);
while (true) {
$target = array_pop($discover);
$parents = $stream->getParents($target);
foreach ($parents as $parent) {
if (isset($seen_parent[$parent])) {
// We end up in a loop here somehow when we parse Arcanist if we
// don't do this. TODO: Figure out why and draw a pretty diagram
// since it's not evident how parsing a DAG with this causes the
// loop to stop terminating.
continue;
}
$seen_parent[$parent] = true;
if ($autoclose) {
$known = $this->isKnownCommitOnAnyAutocloseBranch(
$repository,
$parent);
} else {
$known = $this->isKnownCommit($repository, $parent);
}
if (!$known) {
$this->log("Discovered commit '{$parent}'.");
$discover[] = $parent;
$insert[] = $parent;
}
}
if (empty($discover)) {
break;
}
}
$n = count($insert);
if ($autoclose) {
$this->log("Found {$n} new autoclose commits on branch '{$branch}'.");
} else {
$this->log("Found {$n} new commits on branch '{$branch}'.");
}
while (true) {
$target = array_pop($insert);
$epoch = $stream->getCommitDate($target);
$epoch = trim($epoch);
if ($autoclose) {
$this->updateCommit($repository, $target, $branch);
} else {
$this->recordCommit($repository, $target, $epoch, $branch);
}
if (empty($insert)) {
break;
}
}
}
/**
* @task git
*/
public static function executeGitVerifySameOrigin($remote, $expect, $where) {
$remote_path = self::getPathFromGitURI($remote);
$expect_path = self::getPathFromGitURI($expect);
$remote_match = self::executeGitNormalizePath($remote_path);
$expect_match = self::executeGitNormalizePath($expect_path);
if ($remote_match != $expect_match) {
throw new Exception(
"Working copy at '{$where}' has a mismatched origin URL. It has ".
"origin URL '{$remote}' (with remote path '{$remote_path}'), but the ".
"configured URL '{$expect}' (with remote path '{$expect_path}') is ".
"expected. Refusing to proceed because this may indicate that the ".
"working copy is actually some other repository.");
}
}
private static function getPathFromGitURI($raw_uri) {
$uri = new PhutilURI($raw_uri);
if ($uri->getProtocol()) {
return $uri->getPath();
}
$uri = new PhutilGitURI($raw_uri);
if ($uri->getDomain()) {
return $uri->getPath();
}
return $raw_uri;
}
/**
* @task git
*/
private static function executeGitNormalizePath($path) {
// Strip away "/" and ".git", so similar paths correctly match.
$path = trim($path, '/');
$path = preg_replace('/\.git$/', '', $path);
return $path;
}
/* -( Mercurial Implementation )------------------------------------------- */
/**
* @task hg
*/
private function executeHgCreate(
PhabricatorRepository $repository,
$path) {
$repository->execxRemoteCommand(
'clone %s %s',
$repository->getRemoteURI(),
rtrim($path, '/'));
}
/**
* @task hg
*/
private function executeHgUpdate(
PhabricatorRepository $repository,
$path) {
// This is a local command, but needs credentials.
$future = $repository->getRemoteCommandFuture('pull -u');
$future->setCWD($path);
try {
$future->resolvex();
} catch (CommandException $ex) {
$err = $ex->getError();
$stdout = $ex->getStdOut();
// NOTE: Between versions 2.1 and 2.1.1, Mercurial changed the behavior
// of "hg pull" to return 1 in case of a successful pull with no changes.
// This behavior has been reverted, but users who updated between Feb 1,
// 2012 and Mar 1, 2012 will have the erroring version. Do a dumb test
// against stdout to check for this possibility.
// See: https://github.com/facebook/phabricator/issues/101/
// NOTE: Mercurial has translated versions, which translate this error
// string. In a translated version, the string will be something else,
// like "aucun changement trouve". There didn't seem to be an easy way
// to handle this (there are hard ways but this is not a common problem
// and only creates log spam, not application failures). Assume English.
// TODO: Remove this once we're far enough in the future that deployment
// of 2.1 is exceedingly rare?
if ($err == 1 && preg_match('/no changes found/', $stdout)) {
return;
} else {
throw $ex;
}
}
}
private function executeHgDiscover(PhabricatorRepository $repository) {
// NOTE: "--debug" gives us 40-character hashes.
list($stdout) = $repository->execxLocalCommand('--debug branches');
$branches = ArcanistMercurialParser::parseMercurialBranches($stdout);
$got_something = false;
foreach ($branches as $name => $branch) {
$commit = $branch['rev'];
if ($this->isKnownCommit($repository, $commit)) {
continue;
} else {
$this->executeHgDiscoverCommit($repository, $commit);
$got_something = true;
}
}
return $got_something;
}
private function executeHgDiscoverCommit(
PhabricatorRepository $repository,
$commit) {
$discover = array($commit);
$insert = array($commit);
$seen_parent = array();
$stream = new PhabricatorMercurialGraphStream($repository);
// For all the new commits at the branch heads, walk backward until we
// find only commits we've aleady seen.
while ($discover) {
$target = array_pop($discover);
$parents = $stream->getParents($target);
foreach ($parents as $parent) {
if (isset($seen_parent[$parent])) {
continue;
}
$seen_parent[$parent] = true;
if (!$this->isKnownCommit($repository, $parent)) {
$discover[] = $parent;
$insert[] = $parent;
}
}
}
foreach ($insert as $target) {
$epoch = $stream->getCommitDate($target);
$this->recordCommit($repository, $target, $epoch);
}
}
/* -( Subversion Implementation )------------------------------------------ */
private function executeSvnDiscover(
PhabricatorRepository $repository) {
$uri = $this->executeSvnGetBaseSVNLogURI($repository);
list($xml) = $repository->execxRemoteCommand(
'log --xml --quiet --limit 1 %s@HEAD',
$uri);
$results = $this->executeSvnParseLogXML($xml);
$commit = head_key($results);
$epoch = head($results);
if ($this->isKnownCommit($repository, $commit)) {
return false;
}
$this->executeSvnDiscoverCommit($repository, $commit, $epoch);
return true;
}
private function executeSvnDiscoverCommit(
PhabricatorRepository $repository,
$commit,
$epoch) {
$uri = $this->executeSvnGetBaseSVNLogURI($repository);
$discover = array(
$commit => $epoch,
);
$upper_bound = $commit;
$limit = 1;
while ($upper_bound > 1 &&
!$this->isKnownCommit($repository, $upper_bound)) {
// Find all the unknown commits on this path. Note that we permit
// importing an SVN subdirectory rather than the entire repository, so
// commits may be nonsequential.
list($err, $xml, $stderr) = $repository->execRemoteCommand(
' log --xml --quiet --limit %d %s@%d',
$limit,
$uri,
$upper_bound - 1);
if ($err) {
if (preg_match('/(path|File) not found/', $stderr)) {
// We've gone all the way back through history and this path was not
// affected by earlier commits.
break;
} else {
throw new Exception("svn log error #{$err}: {$stderr}");
}
}
$discover += $this->executeSvnParseLogXML($xml);
$upper_bound = min(array_keys($discover));
// Discover 2, 4, 8, ... 256 logs at a time. This allows us to initially
// import large repositories fairly quickly, while pulling only as much
// data as we need in the common case (when we've already imported the
// repository and are just grabbing one commit at a time).
$limit = min($limit * 2, 256);
}
// NOTE: We do writes only after discovering all the commits so that we're
// never left in a state where we've missed commits -- if the discovery
// script terminates it can always resume and restore the import to a good
// state. This is also why we sort the discovered commits so we can do
// writes forward from the smallest one.
ksort($discover);
foreach ($discover as $commit => $epoch) {
$this->recordCommit($repository, $commit, $epoch);
}
}
private function executeSvnParseLogXML($xml) {
$xml = phutil_utf8ize($xml);
$result = array();
$log = new SimpleXMLElement($xml);
foreach ($log->logentry as $entry) {
$commit = (int)$entry['revision'];
$epoch = (int)strtotime((string)$entry->date[0]);
$result[$commit] = $epoch;
}
return $result;
}
private function executeSvnGetBaseSVNLogURI(
PhabricatorRepository $repository) {
$uri = $repository->getDetail('remote-uri');
$subpath = $repository->getDetail('svn-subpath');
return $uri.$subpath;
}
}
diff --git a/src/applications/repository/daemon/__tests__/PhabricatorRepositoryPullLocalDaemonTestCase.php b/src/applications/repository/daemon/__tests__/PhabricatorRepositoryPullLocalDaemonTestCase.php
index 43130bd6b9..ced1ae2986 100644
--- a/src/applications/repository/daemon/__tests__/PhabricatorRepositoryPullLocalDaemonTestCase.php
+++ b/src/applications/repository/daemon/__tests__/PhabricatorRepositoryPullLocalDaemonTestCase.php
@@ -1,113 +1,97 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorRepositoryPullLocalDaemonTestCase
extends PhabricatorTestCase {
public function testExecuteGitVerifySameOrigin() {
$cases = array(
array(
'ssh://user@domain.com/path.git',
'ssh://user@domain.com/path.git',
true,
'Identical paths should pass.',
),
array(
'ssh://user@domain.com/path.git',
'https://user@domain.com/path.git',
true,
'Protocol changes should pass.',
),
array(
'ssh://user@domain.com/path.git',
'git@domain.com:path.git',
true,
'Git implicit SSH should pass.',
),
array(
'ssh://user@gitserv001.com/path.git',
'ssh://user@gitserv002.com/path.git',
true,
'Domain changes should pass.',
),
array(
'ssh://alincoln@domain.com/path.git',
'ssh://htaft@domain.com/path.git',
true,
'User/auth changes should pass.',
),
array(
'ssh://user@domain.com/apples.git',
'ssh://user@domain.com/bananas.git',
false,
'Path changes should fail.',
),
array(
'ssh://user@domain.com/apples.git',
'git@domain.com:bananas.git',
false,
'Git implicit SSH path changes should fail.',
),
array(
'user@domain.com:path/repo.git',
'user@domain.com:path/repo',
true,
'Optional .git extension should not prevent matches.',
),
array(
'user@domain.com:path/repo/',
'user@domain.com:path/repo',
true,
'Optional trailing slash should not prevent matches.',
),
array(
'file:///path/to/local/repo.git',
'file:///path/to/local/repo.git',
true,
'file:// protocol should be supported.',
),
array(
'/path/to/local/repo.git',
'file:///path/to/local/repo.git',
true,
'Implicit file:// protocol should be recognized.',
),
);
foreach ($cases as $case) {
list($remote, $config, $expect, $message) = $case;
$ex = null;
try {
PhabricatorRepositoryPullLocalDaemon::executeGitverifySameOrigin(
$remote,
$config,
'(a test case)');
} catch (Exception $exception) {
$ex = $exception;
}
$this->assertEqual(
$expect,
!$ex,
"Verification that '{$remote}' and '{$config}' are the same origin ".
"had a different outcome than expected: {$message}");
}
}
}
diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementDeleteWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementDeleteWorkflow.php
index 107c22963e..3f19697165 100644
--- a/src/applications/repository/management/PhabricatorRepositoryManagementDeleteWorkflow.php
+++ b/src/applications/repository/management/PhabricatorRepositoryManagementDeleteWorkflow.php
@@ -1,61 +1,45 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorRepositoryManagementDeleteWorkflow
extends PhabricatorRepositoryManagementWorkflow {
public function didConstruct() {
$this
->setName('delete')
->setExamples('**delete** __repository__ ...')
->setSynopsis('Delete __repository__, named by callsign or PHID.')
->setArguments(
array(
array(
'name' => 'verbose',
'help' => 'Show additional debugging information.',
),
array(
'name' => 'repos',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$names = $args->getArg('repos');
$repos = PhabricatorRepository::loadAllByPHIDOrCallsign($names);
if (!$repos) {
throw new PhutilArgumentUsageException(
"Specify one or more repositories to delete, by callsign or PHID.");
}
$console = PhutilConsole::getConsole();
foreach ($repos as $repo) {
$console->writeOut("Deleting '%s'...\n", $repo->getCallsign());
$repo->delete();
}
$console->writeOut("Done.\n");
return 0;
}
}
diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementDiscoverWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementDiscoverWorkflow.php
index adaa2503f8..f77ea47b50 100644
--- a/src/applications/repository/management/PhabricatorRepositoryManagementDiscoverWorkflow.php
+++ b/src/applications/repository/management/PhabricatorRepositoryManagementDiscoverWorkflow.php
@@ -1,69 +1,53 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorRepositoryManagementDiscoverWorkflow
extends PhabricatorRepositoryManagementWorkflow {
public function didConstruct() {
$this
->setName('discover')
->setExamples('**discover** [__options__] __repository__ ...')
->setSynopsis('Discover __repository__, named by callsign or PHID.')
->setArguments(
array(
array(
'name' => 'verbose',
'help' => 'Show additional debugging information.',
),
array(
'name' => 'repair',
'help' => 'Repair a repository with gaps in commit '.
'history.',
),
array(
'name' => 'repos',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$names = $args->getArg('repos');
$repos = PhabricatorRepository::loadAllByPHIDOrCallsign($names);
if (!$repos) {
throw new PhutilArgumentUsageException(
"Specify one or more repositories to discover, by callsign or PHID.");
}
$console = PhutilConsole::getConsole();
foreach ($repos as $repo) {
$console->writeOut("Discovering '%s'...\n", $repo->getCallsign());
$daemon = new PhabricatorRepositoryPullLocalDaemon(array());
$daemon->setVerbose($args->getArg('verbose'));
$daemon->setRepair($args->getArg('repair'));
$daemon->discoverRepository($repo);
}
$console->writeOut("Done.\n");
return 0;
}
}
diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementListWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementListWorkflow.php
index f243a0b32a..d99cccfdbb 100644
--- a/src/applications/repository/management/PhabricatorRepositoryManagementListWorkflow.php
+++ b/src/applications/repository/management/PhabricatorRepositoryManagementListWorkflow.php
@@ -1,44 +1,28 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorRepositoryManagementListWorkflow
extends PhabricatorRepositoryManagementWorkflow {
public function didConstruct() {
$this
->setName('list')
->setSynopsis('Show a list of repositories.')
->setArguments(array());
}
public function execute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$repos = id(new PhabricatorRepository())->loadAll();
if ($repos) {
foreach ($repos as $repo) {
$console->writeOut("%s\n", $repo->getCallsign());
}
} else {
$console->writeErr("%s\n", 'There are no repositories.');
}
return 0;
}
}
diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementPullWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementPullWorkflow.php
index 3614cdab8d..ec7eab7c94 100644
--- a/src/applications/repository/management/PhabricatorRepositoryManagementPullWorkflow.php
+++ b/src/applications/repository/management/PhabricatorRepositoryManagementPullWorkflow.php
@@ -1,63 +1,47 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorRepositoryManagementPullWorkflow
extends PhabricatorRepositoryManagementWorkflow {
public function didConstruct() {
$this
->setName('pull')
->setExamples('**pull** __repository__ ...')
->setSynopsis('Pull __repository__, named by callsign or PHID.')
->setArguments(
array(
array(
'name' => 'verbose',
'help' => 'Show additional debugging information.',
),
array(
'name' => 'repos',
'wildcard' => true,
),
));
}
public function execute(PhutilArgumentParser $args) {
$names = $args->getArg('repos');
$repos = PhabricatorRepository::loadAllByPHIDOrCallsign($names);
if (!$repos) {
throw new PhutilArgumentUsageException(
"Specify one or more repositories to pull, by callsign or PHID.");
}
$console = PhutilConsole::getConsole();
foreach ($repos as $repo) {
$console->writeOut("Pulling '%s'...\n", $repo->getCallsign());
$daemon = new PhabricatorRepositoryPullLocalDaemon(array());
$daemon->setVerbose($args->getArg('verbose'));
$daemon->pullRepository($repo);
}
$console->writeOut("Done.\n");
return 0;
}
}
diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementWorkflow.php
index 372d244199..4c85e8d49a 100644
--- a/src/applications/repository/management/PhabricatorRepositoryManagementWorkflow.php
+++ b/src/applications/repository/management/PhabricatorRepositoryManagementWorkflow.php
@@ -1,26 +1,10 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorRepositoryManagementWorkflow
extends PhutilArgumentWorkflow {
public function isExecutable() {
return true;
}
}
diff --git a/src/applications/repository/parser/PhabricatorRepositoryCommitMessageDetailParser.php b/src/applications/repository/parser/PhabricatorRepositoryCommitMessageDetailParser.php
index 40074e0d95..f7d8701de1 100644
--- a/src/applications/repository/parser/PhabricatorRepositoryCommitMessageDetailParser.php
+++ b/src/applications/repository/parser/PhabricatorRepositoryCommitMessageDetailParser.php
@@ -1,117 +1,101 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorRepositoryCommitMessageDetailParser {
private $commit;
private $commitData;
final public function __construct(
PhabricatorRepositoryCommit $commit,
PhabricatorRepositoryCommitData $data) {
$this->commit = $commit;
$this->commitData = $data;
}
final public function getCommit() {
return $this->commit;
}
final public function getCommitData() {
return $this->commitData;
}
/**
* Try to link a commit name to a Phabricator account. Basically we throw it
* at the wall and see if something sticks.
*/
public function resolveUserPHID($user_name) {
if (!strlen($user_name)) {
return null;
}
$phid = $this->findUserByUserName($user_name);
if ($phid) {
return $phid;
}
$phid = $this->findUserByEmailAddress($user_name);
if ($phid) {
return $phid;
}
$phid = $this->findUserByRealName($user_name);
if ($phid) {
return $phid;
}
// No hits yet, try to parse it as an email address.
$email = new PhutilEmailAddress($user_name);
$phid = $this->findUserByEmailAddress($email->getAddress());
if ($phid) {
return $phid;
}
$display_name = $email->getDisplayName();
if ($display_name) {
$phid = $this->findUserByUserName($display_name);
if ($phid) {
return $phid;
}
$phid = $this->findUserByRealName($display_name);
if ($phid) {
return $phid;
}
}
return null;
}
abstract public function parseCommitDetails();
private function findUserByUserName($user_name) {
$by_username = id(new PhabricatorUser())->loadOneWhere(
'userName = %s',
$user_name);
if ($by_username) {
return $by_username->getPHID();
}
return null;
}
private function findUserByRealName($real_name) {
// Note, real names are not guaranteed unique, which is why we do it this
// way.
$by_realname = id(new PhabricatorUser())->loadAllWhere(
'realName = %s',
$real_name);
if (count($by_realname) == 1) {
return reset($by_realname)->getPHID();
}
return null;
}
private function findUserByEmailAddress($email_address) {
$by_email = PhabricatorUser::loadOneWithEmailAddress($email_address);
if ($by_email) {
return $by_email->getPHID();
}
return null;
}
}
diff --git a/src/applications/repository/parser/PhabricatorRepositoryDefaultCommitMessageDetailParser.php b/src/applications/repository/parser/PhabricatorRepositoryDefaultCommitMessageDetailParser.php
index 98fa07a7a7..301e39f7e3 100644
--- a/src/applications/repository/parser/PhabricatorRepositoryDefaultCommitMessageDetailParser.php
+++ b/src/applications/repository/parser/PhabricatorRepositoryDefaultCommitMessageDetailParser.php
@@ -1,97 +1,81 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* TODO: Facebook extends this (I think?), but should it?
*/
class PhabricatorRepositoryDefaultCommitMessageDetailParser
extends PhabricatorRepositoryCommitMessageDetailParser {
public function parseCommitDetails() {
$commit = $this->getCommit();
$data = $this->getCommitData();
$details = nonempty($data->getCommitDetails(), array());
$message = $data->getCommitMessage();
$author_name = $data->getAuthorName();
// TODO: Some day, it would be good to drive all of this via
// DifferentialFieldSpecification configuration directly.
$match = null;
if (preg_match(
'/^\s*Differential Revision:\s*(\S+)\s*$/mi',
$message,
$match)) {
// NOTE: We now accept ONLY full URIs because if we accept numeric IDs
// then anyone importing the Phabricator repository will have their
// first few thousand revisions marked closed. This does mean that
// some older revisions won't re-parse correctly, but that shouldn't
// really affect anyone. If necessary, an install can extend the parser
// and restore the older, more-liberal parsing fairly easily.
$id = DifferentialRevisionIDFieldSpecification::parseRevisionIDFromURI(
$match[1]);
if ($id) {
$details['differential.revisionID'] = $id;
$revision = id(new DifferentialRevision())->load($id);
if ($revision) {
$details['differential.revisionPHID'] = $revision->getPHID();
}
}
}
if (preg_match(
'/^\s*Reviewed By:\s*(\S+)\s*$/mi',
$message,
$match)) {
$details['reviewerName'] = $match[1];
$reviewer_phid = $this->resolveUserPHID($details['reviewerName']);
if ($reviewer_phid) {
$details['reviewerPHID'] = $reviewer_phid;
} else {
unset($details['reviewerPHID']);
}
} else {
unset($details['reviewerName']);
unset($details['reviewerPHID']);
}
$author_phid = $this->resolveUserPHID($author_name);
if ($author_phid) {
$details['authorPHID'] = $author_phid;
} else {
unset($details['authorPHID']);
}
if (isset($details['committer'])) {
$committer_phid = $this->resolveUserPHID($details['committer']);
if ($committer_phid) {
$details['committerPHID'] = $committer_phid;
} else {
unset($details['committerPHID']);
}
}
$data->setCommitDetails($details);
}
}
diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php
index e0930a8155..6cf78c842e 100644
--- a/src/applications/repository/storage/PhabricatorRepository.php
+++ b/src/applications/repository/storage/PhabricatorRepository.php
@@ -1,603 +1,587 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @task uri Repository URI Management
*/
final class PhabricatorRepository extends PhabricatorRepositoryDAO {
const TABLE_PATH = 'repository_path';
const TABLE_PATHCHANGE = 'repository_pathchange';
const TABLE_FILESYSTEM = 'repository_filesystem';
const TABLE_SUMMARY = 'repository_summary';
const TABLE_BADCOMMIT = 'repository_badcommit';
protected $phid;
protected $name;
protected $callsign;
protected $uuid;
protected $versionControlSystem;
protected $details = array();
private $sshKeyfile;
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'details' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_REPO);
}
public function getDetail($key, $default = null) {
return idx($this->details, $key, $default);
}
public function setDetail($key, $value) {
$this->details[$key] = $value;
return $this;
}
public function getDiffusionBrowseURIForPath($path,
$line = null,
$branch = null) {
$drequest = DiffusionRequest::newFromDictionary(
array(
'repository' => $this,
'path' => $path,
'branch' => $branch,
));
return $drequest->generateURI(
array(
'action' => 'browse',
'line' => $line,
));
}
public function getLocalPath() {
return $this->getDetail('local-path');
}
public function execRemoteCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
$args = $this->formatRemoteCommand($args);
return call_user_func_array('exec_manual', $args);
}
public function execxRemoteCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
$args = $this->formatRemoteCommand($args);
return call_user_func_array('execx', $args);
}
public function getRemoteCommandFuture($pattern /* , $arg, ... */) {
$args = func_get_args();
$args = $this->formatRemoteCommand($args);
return newv('ExecFuture', $args);
}
public function passthruRemoteCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
$args = $this->formatRemoteCommand($args);
return call_user_func_array('phutil_passthru', $args);
}
public function execLocalCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
$args = $this->formatLocalCommand($args);
return call_user_func_array('exec_manual', $args);
}
public function execxLocalCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
$args = $this->formatLocalCommand($args);
return call_user_func_array('execx', $args);
}
public function getLocalCommandFuture($pattern /* , $arg, ... */) {
$args = func_get_args();
$args = $this->formatLocalCommand($args);
return newv('ExecFuture', $args);
}
public function passthruLocalCommand($pattern /* , $arg, ... */) {
$args = func_get_args();
$args = $this->formatLocalCommand($args);
return call_user_func_array('phutil_passthru', $args);
}
private function formatRemoteCommand(array $args) {
$pattern = $args[0];
$args = array_slice($args, 1);
if ($this->shouldUseSSH()) {
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$pattern = "SVN_SSH=%s svn --non-interactive {$pattern}";
array_unshift(
$args,
csprintf(
'ssh -l %s -i %s',
$this->getSSHLogin(),
$this->getSSHKeyfile()));
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$command = call_user_func_array(
'csprintf',
array_merge(
array(
"(ssh-add %s && git {$pattern})",
$this->getSSHKeyfile(),
),
$args));
$pattern = "ssh-agent sh -c %s";
$args = array($command);
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$pattern = "hg --config ui.ssh=%s {$pattern}";
array_unshift(
$args,
csprintf(
'ssh -l %s -i %s',
$this->getSSHLogin(),
$this->getSSHKeyfile()));
break;
default:
throw new Exception("Unrecognized version control system.");
}
} else if ($this->shouldUseHTTP()) {
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$pattern =
"svn ".
"--non-interactive ".
"--no-auth-cache ".
"--trust-server-cert ".
"--username %s ".
"--password %s ".
$pattern;
array_unshift(
$args,
$this->getDetail('http-login'),
$this->getDetail('http-pass'));
break;
default:
throw new Exception(
"No support for HTTP Basic Auth in this version control system.");
}
} else if ($this->shouldUseSVNProtocol()) {
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$pattern =
"svn ".
"--non-interactive ".
"--no-auth-cache ".
"--username %s ".
"--password %s ".
$pattern;
array_unshift(
$args,
$this->getDetail('http-login'),
$this->getDetail('http-pass'));
break;
default:
throw new Exception(
"SVN protocol is SVN only.");
}
} else {
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$pattern = "svn --non-interactive {$pattern}";
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$pattern = "git {$pattern}";
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$pattern = "hg {$pattern}";
break;
default:
throw new Exception("Unrecognized version control system.");
}
}
array_unshift($args, $pattern);
return $args;
}
private function formatLocalCommand(array $args) {
$pattern = $args[0];
$args = array_slice($args, 1);
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$pattern = "(cd %s && svn --non-interactive {$pattern})";
array_unshift($args, $this->getLocalPath());
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$pattern = "(cd %s && git {$pattern})";
array_unshift($args, $this->getLocalPath());
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$pattern = "(cd %s && HGPLAIN=1 hg {$pattern})";
array_unshift($args, $this->getLocalPath());
break;
default:
throw new Exception("Unrecognized version control system.");
}
array_unshift($args, $pattern);
return $args;
}
private function getSSHLogin() {
return $this->getDetail('ssh-login');
}
private function getSSHKeyfile() {
if ($this->sshKeyfile === null) {
$key = $this->getDetail('ssh-key');
$keyfile = $this->getDetail('ssh-keyfile');
if ($keyfile) {
// Make sure we can read the file, that it exists, etc.
Filesystem::readFile($keyfile);
$this->sshKeyfile = $keyfile;
} else if ($key) {
$keyfile = new TempFile('phabricator-repository-ssh-key');
chmod($keyfile, 0600);
Filesystem::writeFile($keyfile, $key);
$this->sshKeyfile = $keyfile;
} else {
$this->sshKeyfile = '';
}
}
return (string)$this->sshKeyfile;
}
public function getURI() {
return '/diffusion/'.$this->getCallsign().'/';
}
public function isTracked() {
return $this->getDetail('tracking-enabled', false);
}
public function getDefaultBranch() {
$default = $this->getDetail('default-branch');
if (strlen($default)) {
return $default;
}
$default_branches = array(
PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'master',
PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL => 'default',
);
return idx($default_branches, $this->getVersionControlSystem());
}
private function isBranchInFilter($branch, $filter_key) {
$vcs = $this->getVersionControlSystem();
$is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
$use_filter = ($is_git);
if ($use_filter) {
$filter = $this->getDetail($filter_key, array());
if ($filter && empty($filter[$branch])) {
return false;
}
}
// By default, all branches pass.
return true;
}
public function shouldTrackBranch($branch) {
return $this->isBranchInFilter($branch, 'branch-filter');
}
public function shouldAutocloseBranch($branch) {
if ($this->getDetail('disable-autoclose', false)) {
return false;
}
return $this->isBranchInFilter($branch, 'close-commits-filter');
}
public function shouldAutocloseCommit(
PhabricatorRepositoryCommit $commit,
PhabricatorRepositoryCommitData $data) {
if ($this->getDetail('disable-autoclose', false)) {
return false;
}
switch ($this->getVersionControlSystem()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
return true;
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
return true;
default:
throw new Exception("Unrecognized version control system.");
}
$branches = $data->getCommitDetail('seenOnBranches', array());
foreach ($branches as $branch) {
if ($this->shouldAutocloseBranch($branch)) {
return true;
}
}
return false;
}
public function formatCommitName($commit_identifier) {
$vcs = $this->getVersionControlSystem();
$type_git = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
$type_hg = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL;
$is_git = ($vcs == $type_git);
$is_hg = ($vcs == $type_hg);
if ($is_git || $is_hg) {
$short_identifier = substr($commit_identifier, 0, 12);
} else {
$short_identifier = $commit_identifier;
}
return 'r'.$this->getCallsign().$short_identifier;
}
public static function loadAllByPHIDOrCallsign(array $names) {
$repositories = array();
foreach ($names as $name) {
$repo = id(new PhabricatorRepository())->loadOneWhere(
'phid = %s OR callsign = %s',
$name,
$name);
if (!$repo) {
throw new Exception(
"No repository with PHID or callsign '{$name}' exists!");
}
$repositories[$repo->getID()] = $repo;
}
return $repositories;
}
/* -( Repository URI Management )------------------------------------------ */
/**
* Get the remote URI for this repository.
*
* @return string
* @task uri
*/
public function getRemoteURI() {
return (string)$this->getRemoteURIObject();
}
/**
* Get the remote URI for this repository, without authentication information.
*
* @return string Repository URI.
* @task uri
*/
public function getPublicRemoteURI() {
$uri = $this->getRemoteURIObject();
// Make sure we don't leak anything if this repo is using HTTP Basic Auth
// with the credentials in the URI or something zany like that.
if ($uri instanceof PhutilGitURI) {
$uri->setUser(null);
} else {
$uri->setUser(null);
$uri->setPass(null);
}
return (string)$uri;
}
/**
* Get the protocol for the repository's remote.
*
* @return string Protocol, like "ssh" or "git".
* @task uri
*/
public function getRemoteProtocol() {
$uri = $this->getRemoteURIObject();
if ($uri instanceof PhutilGitURI) {
return 'ssh';
} else {
return $uri->getProtocol();
}
}
/**
* Get a parsed object representation of the repository's remote URI. This
* may be a normal URI (returned as a @{class@libphutil:PhutilURI}) or a git
* URI (returned as a @{class@libphutil:PhutilGitURI}).
*
* @return wild A @{class@libphutil:PhutilURI} or
* @{class@libphutil:PhutilGitURI}.
* @task uri
*/
private function getRemoteURIObject() {
$raw_uri = $this->getDetail('remote-uri');
if (!$raw_uri) {
return new PhutilURI('');
}
if (!strncmp($raw_uri, '/', 1)) {
return new PhutilURI('file://'.$raw_uri);
}
$uri = new PhutilURI($raw_uri);
if ($uri->getProtocol()) {
if ($this->isSSHProtocol($uri->getProtocol())) {
if ($this->getSSHLogin()) {
$uri->setUser($this->getSSHLogin());
}
}
return $uri;
}
$uri = new PhutilGitURI($raw_uri);
if ($uri->getDomain()) {
if ($this->getSSHLogin()) {
$uri->setUser($this->getSSHLogin());
}
return $uri;
}
throw new Exception("Remote URI '{$raw_uri}' could not be parsed!");
}
/**
* Determine if we should connect to the remote using SSH flags and
* credentials.
*
* @return bool True to use the SSH protocol.
* @task uri
*/
private function shouldUseSSH() {
$protocol = $this->getRemoteProtocol();
if ($this->isSSHProtocol($protocol)) {
return (bool)$this->getSSHKeyfile();
} else {
return false;
}
}
/**
* Determine if we should connect to the remote using HTTP flags and
* credentials.
*
* @return bool True to use the HTTP protocol.
* @task uri
*/
private function shouldUseHTTP() {
$protocol = $this->getRemoteProtocol();
if ($protocol == 'http' || $protocol == 'https') {
return (bool)$this->getDetail('http-login');
} else {
return false;
}
}
/**
* Determine if we should connect to the remote using SVN flags and
* credentials.
*
* @return bool True to use the SVN protocol.
* @task uri
*/
private function shouldUseSVNProtocol() {
$protocol = $this->getRemoteProtocol();
if ($protocol == 'svn') {
return (bool)$this->getDetail('http-login');
} else {
return false;
}
}
/**
* Determine if a protocol is SSH or SSH-like.
*
* @param string A protocol string, like "http" or "ssh".
* @return bool True if the protocol is SSH-like.
* @task uri
*/
private function isSSHProtocol($protocol) {
return ($protocol == 'ssh' || $protocol == 'svn+ssh');
}
public function delete() {
$this->openTransaction();
$paths = id(new PhabricatorOwnersPath())
->loadAllWhere('repositoryPHID = %s', $this->getPHID());
foreach ($paths as $path) {
$path->delete();
}
$projects = id(new PhabricatorRepositoryArcanistProject())
->loadAllWhere('repositoryID = %d', $this->getID());
foreach ($projects as $project) {
// note each project deletes its PhabricatorRepositorySymbols
$project->delete();
}
$commits = id(new PhabricatorRepositoryCommit())
->loadAllWhere('repositoryID = %d', $this->getID());
foreach ($commits as $commit) {
// note PhabricatorRepositoryAuditRequests and
// PhabricatorRepositoryCommitData are deleted here too.
$commit->delete();
}
$conn_w = $this->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE repositoryID = %d',
self::TABLE_FILESYSTEM,
$this->getID());
queryfx(
$conn_w,
'DELETE FROM %T WHERE repositoryID = %d',
self::TABLE_PATHCHANGE,
$this->getID());
queryfx(
$conn_w,
'DELETE FROM %T WHERE repositoryID = %d',
self::TABLE_SUMMARY,
$this->getID());
$result = parent::delete();
$this->saveTransaction();
return $result;
}
}
diff --git a/src/applications/repository/storage/PhabricatorRepositoryArcanistProject.php b/src/applications/repository/storage/PhabricatorRepositoryArcanistProject.php
index 0a879a97e1..4bd44f0068 100644
--- a/src/applications/repository/storage/PhabricatorRepositoryArcanistProject.php
+++ b/src/applications/repository/storage/PhabricatorRepositoryArcanistProject.php
@@ -1,68 +1,52 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorRepositoryArcanistProject
extends PhabricatorRepositoryDAO {
protected $name;
protected $phid;
protected $repositoryID;
protected $symbolIndexLanguages = array();
protected $symbolIndexProjects = array();
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_SERIALIZATION => array(
'symbolIndexLanguages' => self::SERIALIZATION_JSON,
'symbolIndexProjects' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID('APRJ');
}
public function loadRepository() {
if (!$this->getRepositoryID()) {
return null;
}
return id(new PhabricatorRepository())->load($this->getRepositoryID());
}
public function delete() {
$this->openTransaction();
$conn_w = $this->establishConnection('w');
$symbols = id(new PhabricatorRepositorySymbol())->loadAllWhere(
'arcanistProjectID = %d',
$this->getID()
);
foreach ($symbols as $symbol) {
$symbol->delete();
}
$result = parent::delete();
$this->saveTransaction();
return $result;
}
}
diff --git a/src/applications/repository/storage/PhabricatorRepositoryAuditRequest.php b/src/applications/repository/storage/PhabricatorRepositoryAuditRequest.php
index 98442a689f..5be8c12971 100644
--- a/src/applications/repository/storage/PhabricatorRepositoryAuditRequest.php
+++ b/src/applications/repository/storage/PhabricatorRepositoryAuditRequest.php
@@ -1,35 +1,19 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorRepositoryAuditRequest extends PhabricatorRepositoryDAO {
protected $auditorPHID;
protected $commitPHID;
protected $auditReasons = array();
protected $auditStatus;
public function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_SERIALIZATION => array(
'auditReasons' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
}
diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommit.php b/src/applications/repository/storage/PhabricatorRepositoryCommit.php
index af788f1ae9..1835bf5565 100644
--- a/src/applications/repository/storage/PhabricatorRepositoryCommit.php
+++ b/src/applications/repository/storage/PhabricatorRepositoryCommit.php
@@ -1,157 +1,141 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorRepositoryCommit extends PhabricatorRepositoryDAO {
protected $repositoryID;
protected $phid;
protected $commitIdentifier;
protected $epoch;
protected $mailKey;
protected $authorPHID;
protected $auditStatus = PhabricatorAuditCommitStatusConstants::NONE;
private $commitData;
private $audits;
private $isUnparsed;
public function setIsUnparsed($is_unparsed) {
$this->isUnparsed = $is_unparsed;
return $this;
}
public function getIsUnparsed() {
return $this->isUnparsed;
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_TIMESTAMPS => false,
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_CMIT);
}
public function loadCommitData() {
if (!$this->getID()) {
return null;
}
return id(new PhabricatorRepositoryCommitData())->loadOneWhere(
'commitID = %d',
$this->getID());
}
public function attachCommitData(PhabricatorRepositoryCommitData $data) {
$this->commitData = $data;
return $this;
}
public function getCommitData() {
if (!$this->commitData) {
throw new Exception("Attach commit data with attachCommitData() first!");
}
return $this->commitData;
}
public function attachAudits(array $audits) {
assert_instances_of($audits, 'PhabricatorAuditComment');
$this->audits = $audits;
return $this;
}
public function getAudits() {
return $this->audits;
}
public function save() {
if (!$this->mailKey) {
$this->mailKey = Filesystem::readRandomCharacters(20);
}
return parent::save();
}
public function delete() {
$data = $this->loadCommitData();
$audits = id(new PhabricatorRepositoryAuditRequest())
->loadAllWhere('commitPHID = %s', $this->getPHID());
$this->openTransaction();
if ($data) {
$data->delete();
}
foreach ($audits as $audit) {
$audit->delete();
}
$result = parent::delete();
$this->saveTransaction();
return $result;
}
public function getDateCreated() {
// This is primarily to make analysis of commits with the Fact engine work.
return $this->getEpoch();
}
/**
* Synchronize a commit's overall audit status with the individual audit
* triggers.
*/
public function updateAuditStatus(array $requests) {
assert_instances_of($requests, 'PhabricatorRepositoryAuditRequest');
$any_concern = false;
$any_accept = false;
$any_need = false;
foreach ($requests as $request) {
switch ($request->getAuditStatus()) {
case PhabricatorAuditStatusConstants::AUDIT_REQUIRED:
$any_need = true;
break;
case PhabricatorAuditStatusConstants::ACCEPTED:
$any_accept = true;
break;
case PhabricatorAuditStatusConstants::CONCERNED:
$any_concern = true;
break;
}
}
if ($any_concern) {
$status = PhabricatorAuditCommitStatusConstants::CONCERN_RAISED;
} else if ($any_accept) {
if ($any_need) {
$status = PhabricatorAuditCommitStatusConstants::PARTIALLY_AUDITED;
} else {
$status = PhabricatorAuditCommitStatusConstants::FULLY_AUDITED;
}
} else if ($any_need) {
$status = PhabricatorAuditCommitStatusConstants::NEEDS_AUDIT;
} else {
$status = PhabricatorAuditCommitStatusConstants::NONE;
}
return $this->setAuditStatus($status);
}
}
diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommitData.php b/src/applications/repository/storage/PhabricatorRepositoryCommitData.php
index 5fbd96cbc1..dba8499bf6 100644
--- a/src/applications/repository/storage/PhabricatorRepositoryCommitData.php
+++ b/src/applications/repository/storage/PhabricatorRepositoryCommitData.php
@@ -1,56 +1,40 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorRepositoryCommitData extends PhabricatorRepositoryDAO {
const SUMMARY_MAX_LENGTH = 100;
protected $commitID;
protected $authorName = '';
protected $commitMessage = '';
protected $commitDetails = array();
public function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_SERIALIZATION => array(
'commitDetails' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function getSummary() {
$message = $this->getCommitMessage();
$lines = explode("\n", $message);
$summary = head($lines);
$summary = phutil_utf8_shorten($summary, self::SUMMARY_MAX_LENGTH);
return $summary;
}
public function getCommitDetail($key, $default = null) {
return idx($this->commitDetails, $key, $default);
}
public function setCommitDetail($key, $value) {
$this->commitDetails[$key] = $value;
return $this;
}
}
diff --git a/src/applications/repository/storage/PhabricatorRepositoryDAO.php b/src/applications/repository/storage/PhabricatorRepositoryDAO.php
index edaec83c7e..3e677b716c 100644
--- a/src/applications/repository/storage/PhabricatorRepositoryDAO.php
+++ b/src/applications/repository/storage/PhabricatorRepositoryDAO.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorRepositoryDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'repository';
}
}
diff --git a/src/applications/repository/storage/PhabricatorRepositoryShortcut.php b/src/applications/repository/storage/PhabricatorRepositoryShortcut.php
index d682e98047..cb3d00dfc7 100644
--- a/src/applications/repository/storage/PhabricatorRepositoryShortcut.php
+++ b/src/applications/repository/storage/PhabricatorRepositoryShortcut.php
@@ -1,32 +1,16 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorRepositoryShortcut extends PhabricatorRepositoryDAO {
protected $name;
protected $href;
protected $description;
protected $sequence;
public function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
) + parent::getConfiguration();
}
}
diff --git a/src/applications/repository/storage/PhabricatorRepositorySymbol.php b/src/applications/repository/storage/PhabricatorRepositorySymbol.php
index 5525da62a0..e74a27fe08 100644
--- a/src/applications/repository/storage/PhabricatorRepositorySymbol.php
+++ b/src/applications/repository/storage/PhabricatorRepositorySymbol.php
@@ -1,105 +1,89 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Records information about symbol locations in a codebase, like where classes
* and functions are defined.
*
* Query symbols with @{class:DiffusionSymbolQuery}.
*
* @group diffusion
*/
final class PhabricatorRepositorySymbol extends PhabricatorRepositoryDAO {
protected $arcanistProjectID;
protected $symbolContext;
protected $symbolName;
protected $symbolType;
protected $symbolLanguage;
protected $pathID;
protected $lineNumber;
private $path = false;
private $arcanistProject = false;
private $repository = false;
public function getConfiguration() {
return array(
self::CONFIG_IDS => self::IDS_MANUAL,
self::CONFIG_TIMESTAMPS => false,
) + parent::getConfiguration();
}
public function getURI() {
if (!$this->repository) {
// This symbol is in the index, but we don't know which Repository it's
// part of. Usually this means the Arcanist Project hasn't been linked
// to a Repository. We can't generate a URI, so just fail.
return null;
}
$request = DiffusionRequest::newFromDictionary(
array(
'repository' => $this->getRepository(),
));
return $request->generateURI(
array(
'action' => 'browse',
'path' => $this->getPath(),
'line' => $this->getLineNumber(),
));
}
public function getPath() {
if ($this->path === false) {
throw new Exception('Call attachPath() before getPath()!');
}
return $this->path;
}
public function attachPath($path) {
$this->path = $path;
return $this;
}
public function getRepository() {
if ($this->repository === false) {
throw new Exception('Call attachRepository() before getRepository()!');
}
return $this->repository;
}
public function attachRepository($repository) {
$this->repository = $repository;
return $this;
}
public function getArcanistProject() {
if ($this->arcanistProject === false) {
throw new Exception(
'Call attachArcanistProject() before getArcanistProject()!');
}
return $this->arcanistProject;
}
public function attachArcanistProject($project) {
$this->arcanistProject = $project;
return $this;
}
}
diff --git a/src/applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php b/src/applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php
index 7456ed92e4..23feced972 100644
--- a/src/applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php
+++ b/src/applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php
@@ -1,74 +1,58 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorRepositoryTestCase
extends PhabricatorTestCase {
public function testRepositoryURIProtocols() {
$tests = array(
'/path/to/repo' => 'file',
'file:///path/to/repo' => 'file',
'ssh://user@domain.com/path' => 'ssh',
'git@example.com:path' => 'ssh',
'git://git@example.com/path' => 'git',
'svn+ssh://example.com/path' => 'svn+ssh',
'https://example.com/repo/' => 'https',
'http://example.com/' => 'http',
'https://user@example.com/' => 'https',
);
foreach ($tests as $uri => $expect) {
$repository = new PhabricatorRepository();
$repository->setDetail('remote-uri', $uri);
$this->assertEqual(
$expect,
$repository->getRemoteProtocol(),
"Protocol for '{$uri}'.");
}
}
public function testBranchFilter() {
$git = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
$repo = new PhabricatorRepository();
$repo->setVersionControlSystem($git);
$this->assertEqual(
true,
$repo->shouldTrackBranch('imaginary'),
'Track all branches by default.');
$repo->setDetail(
'branch-filter',
array(
'master' => true,
));
$this->assertEqual(
true,
$repo->shouldTrackBranch('master'),
'Track listed branches.');
$this->assertEqual(
false,
$repo->shouldTrackBranch('imaginary'),
'Do not track unlisted branches.');
}
}
diff --git a/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php b/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php
index 5687813209..1a6f611745 100644
--- a/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php
+++ b/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php
@@ -1,400 +1,384 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorRepositoryCommitHeraldWorker
extends PhabricatorRepositoryCommitParserWorker {
public function parseCommit(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
$data = id(new PhabricatorRepositoryCommitData())->loadOneWhere(
'commitID = %d',
$commit->getID());
if (!$data) {
// TODO: Permanent failure.
return;
}
$rules = HeraldRule::loadAllByContentTypeWithFullData(
HeraldContentTypeConfig::CONTENT_TYPE_COMMIT,
$commit->getPHID());
$adapter = new HeraldCommitAdapter(
$repository,
$commit,
$data);
$engine = new HeraldEngine();
$effects = $engine->applyRules($rules, $adapter);
$engine->applyEffects($effects, $adapter, $rules);
$audit_phids = $adapter->getAuditMap();
if ($audit_phids) {
$this->createAudits($commit, $audit_phids, $rules);
}
$explicit_auditors = $this->createAuditsFromCommitMessage($commit, $data);
if ($repository->getDetail('herald-disabled')) {
// This just means "disable email"; audits are (mostly) idempotent.
return;
}
$this->publishFeedStory($repository, $commit, $data);
$herald_targets = $adapter->getEmailPHIDs();
$email_phids = array_unique(
array_merge(
$explicit_auditors,
$herald_targets));
if (!$email_phids) {
return;
}
$xscript = $engine->getTranscript();
$revision = $adapter->loadDifferentialRevision();
if ($revision) {
$name = $revision->getTitle();
} else {
$name = $data->getSummary();
}
$author_phid = $data->getCommitDetail('authorPHID');
$reviewer_phid = $data->getCommitDetail('reviewerPHID');
$phids = array_filter(
array(
$author_phid,
$reviewer_phid,
$commit->getPHID(),
));
$handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
$commit_handle = $handles[$commit->getPHID()];
$commit_name = $commit_handle->getName();
if ($author_phid) {
$author_name = $handles[$author_phid]->getName();
} else {
$author_name = $data->getAuthorName();
}
if ($reviewer_phid) {
$reviewer_name = $handles[$reviewer_phid]->getName();
} else {
$reviewer_name = null;
}
$who = implode(', ', array_filter(array($author_name, $reviewer_name)));
$description = $data->getCommitMessage();
$commit_uri = PhabricatorEnv::getProductionURI($commit_handle->getURI());
$differential = $revision
? PhabricatorEnv::getProductionURI('/D'.$revision->getID())
: 'No revision.';
$files = $adapter->loadAffectedPaths();
sort($files);
$files = implode("\n", $files);
$xscript_id = $xscript->getID();
$manage_uri = '/herald/view/commits/';
$why_uri = '/herald/transcript/'.$xscript_id.'/';
$reply_handler = PhabricatorAuditCommentEditor::newReplyHandlerForCommit(
$commit);
$template = new PhabricatorMetaMTAMail();
$inline_patch_text = $this->buildPatch($template, $repository, $commit);
$body = new PhabricatorMetaMTAMailBody();
$body->addRawSection($description);
$body->addTextSection(pht('DETAILS'), $commit_uri);
$body->addTextSection(pht('DIFFERENTIAL REVISION'), $differential);
$body->addTextSection(pht('AFFECTED FILES'), $files);
$body->addReplySection($reply_handler->getReplyHandlerInstructions());
$body->addHeraldSection($manage_uri, $why_uri);
$body->addRawSection($inline_patch_text);
$body = $body->render();
$prefix = PhabricatorEnv::getEnvConfig('metamta.diffusion.subject-prefix');
$threading = PhabricatorAuditCommentEditor::getMailThreading(
$repository,
$commit);
list($thread_id, $thread_topic) = $threading;
$template->setRelatedPHID($commit->getPHID());
$template->setSubject("{$commit_name}: {$name}");
$template->setSubjectPrefix($prefix);
$template->setVarySubjectPrefix("[Commit]");
$template->setBody($body);
$template->setThreadID($thread_id, $is_new = true);
$template->addHeader('Thread-Topic', $thread_topic);
$template->setIsBulk(true);
$template->addHeader('X-Herald-Rules', $xscript->getXHeraldRulesHeader());
if ($author_phid) {
$template->setFrom($author_phid);
}
$mails = $reply_handler->multiplexMail(
$template,
id(new PhabricatorObjectHandleData($email_phids))->loadHandles(),
array());
foreach ($mails as $mail) {
$mail->saveAndSend();
}
}
private function createAudits(
PhabricatorRepositoryCommit $commit,
array $map,
array $rules) {
assert_instances_of($rules, 'HeraldRule');
$requests = id(new PhabricatorRepositoryAuditRequest())->loadAllWhere(
'commitPHID = %s',
$commit->getPHID());
$requests = mpull($requests, null, 'getAuditorPHID');
$rules = mpull($rules, null, 'getID');
foreach ($map as $phid => $rule_ids) {
$request = idx($requests, $phid);
if ($request) {
continue;
}
$reasons = array();
foreach ($rule_ids as $id) {
$rule_name = '?';
if ($rules[$id]) {
$rule_name = $rules[$id]->getName();
}
$reasons[] = 'Herald Rule #'.$id.' "'.$rule_name.'" Triggered Audit';
}
$request = new PhabricatorRepositoryAuditRequest();
$request->setCommitPHID($commit->getPHID());
$request->setAuditorPHID($phid);
$request->setAuditStatus(PhabricatorAuditStatusConstants::AUDIT_REQUIRED);
$request->setAuditReasons($reasons);
$request->save();
}
$commit->updateAuditStatus($requests);
$commit->save();
}
/**
* Find audit requests in the "Auditors" field if it is present and trigger
* explicit audit requests.
*/
private function createAuditsFromCommitMessage(
PhabricatorRepositoryCommit $commit,
PhabricatorRepositoryCommitData $data) {
$message = $data->getCommitMessage();
$matches = null;
if (!preg_match('/^Auditors:\s*(.*)$/im', $message, $matches)) {
return array();
}
$phids = DifferentialFieldSpecification::parseCommitMessageObjectList(
$matches[1],
$include_mailables = false,
$allow_partial = true);
if (!$phids) {
return array();
}
$requests = id(new PhabricatorRepositoryAuditRequest())->loadAllWhere(
'commitPHID = %s',
$commit->getPHID());
$requests = mpull($requests, null, 'getAuditorPHID');
foreach ($phids as $phid) {
if (isset($requests[$phid])) {
continue;
}
$request = new PhabricatorRepositoryAuditRequest();
$request->setCommitPHID($commit->getPHID());
$request->setAuditorPHID($phid);
$request->setAuditStatus(
PhabricatorAuditStatusConstants::AUDIT_REQUESTED);
$request->setAuditReasons(
array(
'Requested by Author',
));
$request->save();
$requests[$phid] = $request;
}
$commit->updateAuditStatus($requests);
$commit->save();
return $phids;
}
private function publishFeedStory(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit,
PhabricatorRepositoryCommitData $data) {
if (time() > $commit->getEpoch() + (24 * 60 * 60)) {
// Don't publish stories that are more than 24 hours old, to avoid
// ridiculous levels of feed spam if a repository is imported without
// disabling feed publishing.
return;
}
$author_phid = $commit->getAuthorPHID();
$committer_phid = $data->getCommitDetail('committerPHID');
$publisher = new PhabricatorFeedStoryPublisher();
$publisher->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_COMMIT);
$publisher->setStoryData(
array(
'commitPHID' => $commit->getPHID(),
'summary' => $data->getSummary(),
'authorName' => $data->getAuthorName(),
'authorPHID' => $author_phid,
'committerName' => $data->getCommitDetail('committer'),
'committerPHID' => $committer_phid,
));
$publisher->setStoryTime($commit->getEpoch());
$publisher->setRelatedPHIDs(
array_filter(
array(
$author_phid,
$committer_phid,
)));
if ($author_phid) {
$publisher->setStoryAuthorPHID($author_phid);
}
$publisher->publish();
}
private function buildPatch(
PhabricatorMetaMTAMail $template,
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
$attach_key = 'metamta.diffusion.attach-patches';
$inline_key = 'metamta.diffusion.inline-patches';
$attach_patches = PhabricatorEnv::getEnvConfig($attach_key);
$inline_patches = PhabricatorEnv::getEnvConfig($inline_key);
if (!$attach_patches && !$inline_patches) {
return;
}
$encoding = $repository->getDetail('encoding', 'UTF-8');
$result = null;
$patch_error = null;
try {
$raw_patch = $this->loadRawPatchText($repository, $commit);
if ($attach_patches) {
$commit_name = $repository->formatCommitName(
$commit->getCommitIdentifier());
$template->addAttachment(
new PhabricatorMetaMTAAttachment(
$raw_patch,
$commit_name.'.patch',
'text/x-patch; charset='.$encoding));
}
} catch (Exception $ex) {
phlog($ex);
$patch_error = 'Unable to generate: '.$ex->getMessage();
}
if ($patch_error) {
$result = $patch_error;
} else if ($inline_patches) {
$len = substr_count($raw_patch, "\n");
if ($len <= $inline_patches) {
// We send email as utf8, so we need to convert the text to utf8 if
// we can.
if ($encoding) {
$raw_patch = phutil_utf8_convert($raw_patch, 'UTF-8', $encoding);
}
$result = phutil_utf8ize($raw_patch);
}
}
if ($result) {
$result = "PATCH\n\n{$result}\n";
}
return $result;
}
private function loadRawPatchText(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
$drequest = DiffusionRequest::newFromDictionary(
array(
'repository' => $repository,
'commit' => $commit->getCommitIdentifier(),
));
$raw_query = DiffusionRawDiffQuery::newFromDiffusionRequest($drequest);
$raw_query->setLinesOfContext(3);
$time_key = 'metamta.diffusion.time-limit';
$byte_key = 'metamta.diffusion.byte-limit';
$time_limit = PhabricatorEnv::getEnvConfig($time_key);
$byte_limit = PhabricatorEnv::getEnvConfig($byte_key);
if ($time_limit) {
$raw_query->setTimeout($time_limit);
}
$raw_diff = $raw_query->loadRawDiff();
$size = strlen($raw_diff);
if ($byte_limit && $size > $byte_limit) {
$pretty_size = phabricator_format_bytes($size);
$pretty_limit = phabricator_format_bytes($byte_limit);
throw new Exception(
"Patch size of {$pretty_size} exceeds configured byte size limit of ".
"{$pretty_limit}.");
}
return $raw_diff;
}
}
diff --git a/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php b/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php
index 6bc3aeb673..3c033b5bd7 100644
--- a/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php
+++ b/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php
@@ -1,142 +1,126 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorRepositoryCommitOwnersWorker
extends PhabricatorRepositoryCommitParserWorker {
protected function parseCommit(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
$affected_paths = PhabricatorOwnerPathQuery::loadAffectedPaths(
$repository, $commit);
$affected_packages = PhabricatorOwnersPackage::loadAffectedPackages(
$repository,
$affected_paths);
if ($affected_packages) {
$requests = id(new PhabricatorRepositoryAuditRequest())
->loadAllWhere(
'commitPHID = %s',
$commit->getPHID());
$requests = mpull($requests, null, 'getAuditorPHID');
foreach ($affected_packages as $package) {
$request = idx($requests, $package->getPHID());
if ($request) {
// Don't update request if it exists already.
continue;
}
if ($package->getAuditingEnabled()) {
$reasons = $this->checkAuditReasons($commit, $package);
if ($reasons) {
$audit_status =
PhabricatorAuditStatusConstants::AUDIT_REQUIRED;
} else {
$audit_status =
PhabricatorAuditStatusConstants::AUDIT_NOT_REQUIRED;
}
} else {
$reasons = array();
$audit_status = PhabricatorAuditStatusConstants::NONE;
}
$relationship = new PhabricatorRepositoryAuditRequest();
$relationship->setAuditorPHID($package->getPHID());
$relationship->setCommitPHID($commit->getPHID());
$relationship->setAuditReasons($reasons);
$relationship->setAuditStatus($audit_status);
$relationship->save();
$requests[$package->getPHID()] = $relationship;
}
$commit->updateAuditStatus($requests);
$commit->save();
}
if ($this->shouldQueueFollowupTasks()) {
PhabricatorWorker::scheduleTask(
'PhabricatorRepositoryCommitHeraldWorker',
array(
'commitID' => $commit->getID(),
));
}
}
private function checkAuditReasons(
PhabricatorRepositoryCommit $commit,
PhabricatorOwnersPackage $package) {
$data = id(new PhabricatorRepositoryCommitData())->loadOneWhere(
'commitID = %d',
$commit->getID());
$reasons = array();
if ($data->getCommitDetail('vsDiff')) {
$reasons[] = "Changed After Revision Was Accepted";
}
$commit_author_phid = $data->getCommitDetail('authorPHID');
if (!$commit_author_phid) {
$reasons[] = "Commit Author Not Recognized";
}
$revision_id = $data->getCommitDetail('differential.revisionID');
$revision_author_phid = null;
$commit_reviewedby_phid = null;
if ($revision_id) {
$revision = id(new DifferentialRevision())->load($revision_id);
if ($revision) {
$revision->loadRelationships();
$revision_author_phid = $revision->getAuthorPHID();
$revision_reviewedby_phid = $revision->loadReviewedBy();
$commit_reviewedby_phid = $data->getCommitDetail('reviewerPHID');
if ($revision_author_phid !== $commit_author_phid) {
$reasons[] = "Author Not Matching with Revision";
}
if ($revision_reviewedby_phid !== $commit_reviewedby_phid) {
$reasons[] = "ReviewedBy Not Matching with Revision";
}
} else {
$reasons[] = "Revision Not Found";
}
} else {
$reasons[] = "No Revision Specified";
}
$owners_phids = PhabricatorOwnersOwner::loadAffiliatedUserPHIDs(
array($package->getID()));
if (!($commit_author_phid && in_array($commit_author_phid, $owners_phids) ||
$commit_reviewedby_phid && in_array($commit_reviewedby_phid,
$owners_phids))) {
$reasons[] = "Owners Not Involved";
}
return $reasons;
}
}
diff --git a/src/applications/repository/worker/PhabricatorRepositoryCommitParserWorker.php b/src/applications/repository/worker/PhabricatorRepositoryCommitParserWorker.php
index 8641cd8c7d..47f84d4ef9 100644
--- a/src/applications/repository/worker/PhabricatorRepositoryCommitParserWorker.php
+++ b/src/applications/repository/worker/PhabricatorRepositoryCommitParserWorker.php
@@ -1,94 +1,78 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorRepositoryCommitParserWorker
extends PhabricatorWorker {
protected $commit;
protected $repository;
final public function doWork() {
$commit_id = idx($this->getTaskData(), 'commitID');
if (!$commit_id) {
return;
}
$commit = id(new PhabricatorRepositoryCommit())->load($commit_id);
if (!$commit) {
// TODO: Communicate permanent failure?
return;
}
$this->commit = $commit;
$repository = id(new PhabricatorRepository())->load(
$commit->getRepositoryID());
if (!$repository) {
return;
}
$this->repository = $repository;
return $this->parseCommit($repository, $commit);
}
final protected function shouldQueueFollowupTasks() {
return !idx($this->getTaskData(), 'only');
}
abstract protected function parseCommit(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit);
/**
* This method is kind of awkward here but both the SVN message and
* change parsers use it.
*/
protected function getSVNLogXMLObject($uri, $revision, $verbose = false) {
if ($verbose) {
$verbose = '--verbose';
}
list($xml) = $this->repository->execxRemoteCommand(
"log --xml {$verbose} --limit 1 %s@%d",
$uri,
$revision);
// Subversion may send us back commit messages which won't parse because
// they have non UTF-8 garbage in them. Slam them into valid UTF-8.
$xml = phutil_utf8ize($xml);
return new SimpleXMLElement($xml);
}
protected function isBadCommit($full_commit_name) {
$repository = new PhabricatorRepository();
$bad_commit = queryfx_one(
$repository->establishConnection('w'),
'SELECT * FROM %T WHERE fullCommitName = %s',
PhabricatorRepository::TABLE_BADCOMMIT,
$full_commit_name);
return (bool)$bad_commit;
}
}
diff --git a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php
index 9db2746963..b9c549e0b8 100644
--- a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php
+++ b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php
@@ -1,87 +1,71 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorRepositoryCommitChangeParserWorker
extends PhabricatorRepositoryCommitParserWorker {
public function getRequiredLeaseTime() {
// It can take a very long time to parse commits; some commits in the
// Facebook repository affect many millions of paths. Acquire 24h leases.
return 60 * 60 * 24;
}
public static function lookupOrCreatePaths(array $paths) {
$repository = new PhabricatorRepository();
$conn_w = $repository->establishConnection('w');
$result_map = self::lookupPaths($paths);
$missing_paths = array_fill_keys($paths, true);
$missing_paths = array_diff_key($missing_paths, $result_map);
$missing_paths = array_keys($missing_paths);
if ($missing_paths) {
foreach (array_chunk($missing_paths, 128) as $path_chunk) {
$sql = array();
foreach ($path_chunk as $path) {
$sql[] = qsprintf($conn_w, '(%s, %s)', $path, md5($path));
}
queryfx(
$conn_w,
'INSERT IGNORE INTO %T (path, pathHash) VALUES %Q',
PhabricatorRepository::TABLE_PATH,
implode(', ', $sql));
}
$result_map += self::lookupPaths($missing_paths);
}
return $result_map;
}
private static function lookupPaths(array $paths) {
$repository = new PhabricatorRepository();
$conn_w = $repository->establishConnection('w');
$result_map = array();
foreach (array_chunk($paths, 128) as $path_chunk) {
$chunk_map = queryfx_all(
$conn_w,
'SELECT path, id FROM %T WHERE pathHash IN (%Ls)',
PhabricatorRepository::TABLE_PATH,
array_map('md5', $path_chunk));
foreach ($chunk_map as $row) {
$result_map[$row['path']] = $row['id'];
}
}
return $result_map;
}
protected function finishParse() {
$commit = $this->commit;
PhabricatorSearchCommitIndexer::indexCommit($commit);
if ($this->shouldQueueFollowupTasks()) {
PhabricatorWorker::scheduleTask(
'PhabricatorRepositoryCommitOwnersWorker',
array(
'commitID' => $commit->getID(),
));
}
}
}
diff --git a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryGitCommitChangeParserWorker.php b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryGitCommitChangeParserWorker.php
index 77187542fa..acc047464a 100644
--- a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryGitCommitChangeParserWorker.php
+++ b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryGitCommitChangeParserWorker.php
@@ -1,295 +1,279 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorRepositoryGitCommitChangeParserWorker
extends PhabricatorRepositoryCommitChangeParserWorker {
protected function parseCommit(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
$full_name = 'r'.$repository->getCallsign().$commit->getCommitIdentifier();
echo "Parsing {$full_name}...\n";
if ($this->isBadCommit($full_name)) {
echo "This commit is marked bad!\n";
return;
}
// Check if the commit has parents. We're testing to see whether it is the
// first commit in history (in which case we must use "git log") or some
// other commit (in which case we can use "git diff"). We'd rather use
// "git diff" because it has the right behavior for merge commits, but
// it requires the commit to have a parent that we can diff against. The
// first commit doesn't, so "commit^" is not a valid ref.
list($parents) = $repository->execxLocalCommand(
'log -n1 --format=%s %s',
'%P',
$commit->getCommitIdentifier());
$use_log = !strlen(trim($parents));
if ($use_log) {
// This is the first commit so we need to use "log". We know it's not a
// merge commit because it couldn't be merging anything, so this is safe.
// NOTE: "--pretty=format: " is to disable diff output, we only want the
// part we get from "--raw".
list($raw) = $repository->execxLocalCommand(
'log -n1 -M -C -B --find-copies-harder --raw -t '.
'--pretty=format: --abbrev=40 %s',
$commit->getCommitIdentifier());
} else {
// Otherwise, we can use "diff", which will give us output for merges.
// We diff against the first parent, as this is generally the expectation
// and results in sensible behavior.
list($raw) = $repository->execxLocalCommand(
'diff -n1 -M -C -B --find-copies-harder --raw -t '.
'--abbrev=40 %s^1 %s',
$commit->getCommitIdentifier(),
$commit->getCommitIdentifier());
}
$changes = array();
$move_away = array();
$copy_away = array();
$lines = explode("\n", $raw);
foreach ($lines as $line) {
if (!strlen(trim($line))) {
continue;
}
list($old_mode, $new_mode,
$old_hash, $new_hash,
$more_stuff) = preg_split('/ +/', $line, 5);
// We may only have two pieces here.
list($action, $src_path, $dst_path) = array_merge(
explode("\t", $more_stuff),
array(null));
// Normalize the paths for consistency with the SVN workflow.
$src_path = '/'.$src_path;
if ($dst_path) {
$dst_path = '/'.$dst_path;
}
$old_mode = intval($old_mode, 8);
$new_mode = intval($new_mode, 8);
switch ($new_mode & 0160000) {
case 0160000:
$file_type = DifferentialChangeType::FILE_SUBMODULE;
break;
case 0120000:
$file_type = DifferentialChangeType::FILE_SYMLINK;
break;
case 0040000:
$file_type = DifferentialChangeType::FILE_DIRECTORY;
break;
default:
$file_type = DifferentialChangeType::FILE_NORMAL;
break;
}
// TODO: We can detect binary changes as git does, through a combination
// of running 'git check-attr' for stuff like 'binary', 'merge' or 'diff',
// and by falling back to inspecting the first 8,000 characters of the
// buffer for null bytes (this is seriously git's algorithm, see
// buffer_is_binary() in xdiff-interface.c).
$change_type = null;
$change_path = $src_path;
$change_target = null;
$is_direct = true;
switch ($action[0]) {
case 'A':
$change_type = DifferentialChangeType::TYPE_ADD;
break;
case 'D':
$change_type = DifferentialChangeType::TYPE_DELETE;
break;
case 'C':
$change_type = DifferentialChangeType::TYPE_COPY_HERE;
$change_path = $dst_path;
$change_target = $src_path;
$copy_away[$change_target][] = $change_path;
break;
case 'R':
$change_type = DifferentialChangeType::TYPE_MOVE_HERE;
$change_path = $dst_path;
$change_target = $src_path;
$move_away[$change_target][] = $change_path;
break;
case 'T':
// Type of the file changed, fall through and treat it as a
// modification. Not 100% sure this is the right thing to do but it
// seems reasonable.
case 'M':
if ($file_type == DifferentialChangeType::FILE_DIRECTORY) {
$change_type = DifferentialChangeType::TYPE_CHILD;
$is_direct = false;
} else {
$change_type = DifferentialChangeType::TYPE_CHANGE;
}
break;
// NOTE: "U" (unmerged) and "X" (unknown) statuses are also possible
// in theory but shouldn't appear here.
default:
throw new Exception("Failed to parse line '{$line}'.");
}
$changes[$change_path] = array(
'repositoryID' => $repository->getID(),
'commitID' => $commit->getID(),
'path' => $change_path,
'changeType' => $change_type,
'fileType' => $file_type,
'isDirect' => $is_direct,
'commitSequence' => $commit->getEpoch(),
'targetPath' => $change_target,
'targetCommitID' => $change_target ? $commit->getID() : null,
);
}
// Add a change to '/' since git doesn't mention it.
$changes['/'] = array(
'repositoryID' => $repository->getID(),
'commitID' => $commit->getID(),
'path' => '/',
'changeType' => DifferentialChangeType::TYPE_CHILD,
'fileType' => DifferentialChangeType::FILE_DIRECTORY,
'isDirect' => false,
'commitSequence' => $commit->getEpoch(),
'targetPath' => null,
'targetCommitID' => null,
);
foreach ($copy_away as $change_path => $destinations) {
if (isset($move_away[$change_path])) {
$change_type = DifferentialChangeType::TYPE_MULTICOPY;
$is_direct = true;
unset($move_away[$change_path]);
} else {
$change_type = DifferentialChangeType::TYPE_COPY_AWAY;
// This change is direct if we picked up a modification above (i.e.,
// the original copy source was also edited). Otherwise the original
// wasn't touched, so leave it as an indirect change.
$is_direct = isset($changes[$change_path]);
}
$reference = $changes[reset($destinations)];
$changes[$change_path] = array(
'repositoryID' => $repository->getID(),
'commitID' => $commit->getID(),
'path' => $change_path,
'changeType' => $change_type,
'fileType' => $reference['fileType'],
'isDirect' => $is_direct,
'commitSequence' => $commit->getEpoch(),
'targetPath' => null,
'targetCommitID' => null,
);
}
foreach ($move_away as $change_path => $destinations) {
$reference = $changes[reset($destinations)];
$changes[$change_path] = array(
'repositoryID' => $repository->getID(),
'commitID' => $commit->getID(),
'path' => $change_path,
'changeType' => DifferentialChangeType::TYPE_MOVE_AWAY,
'fileType' => $reference['fileType'],
'isDirect' => true,
'commitSequence' => $commit->getEpoch(),
'targetPath' => null,
'targetCommitID' => null,
);
}
$paths = array();
foreach ($changes as $change) {
$paths[$change['path']] = true;
if ($change['targetPath']) {
$paths[$change['targetPath']] = true;
}
}
$path_map = $this->lookupOrCreatePaths(array_keys($paths));
foreach ($changes as $key => $change) {
$changes[$key]['pathID'] = $path_map[$change['path']];
if ($change['targetPath']) {
$changes[$key]['targetPathID'] = $path_map[$change['targetPath']];
} else {
$changes[$key]['targetPathID'] = null;
}
}
$conn_w = $repository->establishConnection('w');
$changes_sql = array();
foreach ($changes as $change) {
$values = array(
(int)$change['repositoryID'],
(int)$change['pathID'],
(int)$change['commitID'],
$change['targetPathID']
? (int)$change['targetPathID']
: 'null',
$change['targetCommitID']
? (int)$change['targetCommitID']
: 'null',
(int)$change['changeType'],
(int)$change['fileType'],
(int)$change['isDirect'],
(int)$change['commitSequence'],
);
$changes_sql[] = '('.implode(', ', $values).')';
}
queryfx(
$conn_w,
'DELETE FROM %T WHERE commitID = %d',
PhabricatorRepository::TABLE_PATHCHANGE,
$commit->getID());
foreach (array_chunk($changes_sql, 256) as $sql_chunk) {
queryfx(
$conn_w,
'INSERT INTO %T
(repositoryID, pathID, commitID, targetPathID, targetCommitID,
changeType, fileType, isDirect, commitSequence)
VALUES %Q',
PhabricatorRepository::TABLE_PATHCHANGE,
implode(', ', $sql_chunk));
}
$this->finishParse();
}
}
diff --git a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryMercurialCommitChangeParserWorker.php b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryMercurialCommitChangeParserWorker.php
index 3b365b88e3..d0444c3547 100644
--- a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryMercurialCommitChangeParserWorker.php
+++ b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryMercurialCommitChangeParserWorker.php
@@ -1,355 +1,339 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorRepositoryMercurialCommitChangeParserWorker
extends PhabricatorRepositoryCommitChangeParserWorker {
protected function parseCommit(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
$full_name = 'r'.$repository->getCallsign().$commit->getCommitIdentifier();
echo "Parsing {$full_name}...\n";
if ($this->isBadCommit($full_name)) {
echo "This commit is marked bad!\n";
return;
}
list($stdout) = $repository->execxLocalCommand(
'status -C --change %s',
$commit->getCommitIdentifier());
$status = ArcanistMercurialParser::parseMercurialStatusDetails($stdout);
$common_attributes = array(
'repositoryID' => $repository->getID(),
'commitID' => $commit->getID(),
'commitSequence' => $commit->getEpoch(),
);
$changes = array();
// Like Git, Mercurial doesn't track directories directly. We need to infer
// directory creation and removal by observing file creation and removal
// and testing if the directories in question are previously empty (thus,
// created) or subsequently empty (thus, removed).
$maybe_new_directories = array();
$maybe_del_directories = array();
$all_directories = array();
// Parse the basic information from "hg status", which shows files that
// were directly affected by the change.
foreach ($status as $path => $path_info) {
$path = '/'.$path;
$flags = $path_info['flags'];
$change_target = $path_info['from'] ? '/'.$path_info['from'] : null;
$changes[$path] = array(
'path' => $path,
'isDirect' => true,
'targetPath' => $change_target,
'targetCommitID' => $change_target ? $commit->getID() : null,
// We're going to fill these in shortly.
'changeType' => null,
'fileType' => null,
'flags' => $flags,
) + $common_attributes;
if ($flags & ArcanistRepositoryAPI::FLAG_ADDED) {
$maybe_new_directories[] = dirname($path);
} else if ($flags & ArcanistRepositoryAPI::FLAG_DELETED) {
$maybe_del_directories[] = dirname($path);
}
$all_directories[] = dirname($path);
}
// Add change information for each source path which doesn't appear in the
// status. These files were copied, but were not modified. We also know they
// must exist.
foreach ($changes as $path => $change) {
$from = $change['targetPath'];
if ($from && empty($changes[$from])) {
$changes[$from] = array(
'path' => $from,
'isDirect' => false,
'targetPath' => null,
'targetCommitID' => null,
'changeType' => DifferentialChangeType::TYPE_COPY_AWAY,
'fileType' => null,
'flags' => 0,
) + $common_attributes;
}
}
$away = array();
foreach ($changes as $path => $change) {
$target_path = $change['targetPath'];
if ($target_path) {
$away[$target_path][] = $path;
}
}
// Now that we have all the direct changes, figure out change types.
foreach ($changes as $path => $change) {
$flags = $change['flags'];
$from = $change['targetPath'];
if ($from) {
$target = $changes[$from];
} else {
$target = null;
}
if ($flags & ArcanistRepositoryAPI::FLAG_ADDED) {
if ($target) {
if ($target['flags'] & ArcanistRepositoryAPI::FLAG_DELETED) {
$change_type = DifferentialChangeType::TYPE_MOVE_HERE;
} else {
$change_type = DifferentialChangeType::TYPE_COPY_HERE;
}
} else {
$change_type = DifferentialChangeType::TYPE_ADD;
}
} else if ($flags & ArcanistRepositoryAPI::FLAG_DELETED) {
if (isset($away[$path])) {
if (count($away[$path]) > 1) {
$change_type = DifferentialChangeType::TYPE_MULTICOPY;
} else {
$change_type = DifferentialChangeType::TYPE_MOVE_AWAY;
}
} else {
$change_type = DifferentialChangeType::TYPE_DELETE;
}
} else {
if (isset($away[$path])) {
$change_type = DifferentialChangeType::TYPE_COPY_AWAY;
} else {
$change_type = DifferentialChangeType::TYPE_CHANGE;
}
}
$changes[$path]['changeType'] = $change_type;
}
// Go through all the affected directories and identify any which were
// actually added or deleted.
$dir_status = array();
foreach ($maybe_del_directories as $dir) {
$exists = false;
foreach (DiffusionPathIDQuery::expandPathToRoot($dir) as $path) {
if (isset($dir_status[$path])) {
break;
}
// If we know some child exists, we know this path exists. If we don't
// know that a child exists, test if this directory still exists.
if (!$exists) {
$exists = $this->mercurialPathExists(
$repository,
$path,
$commit->getCommitIdentifier());
}
if ($exists) {
$dir_status[$path] = DifferentialChangeType::TYPE_CHILD;
} else {
$dir_status[$path] = DifferentialChangeType::TYPE_DELETE;
}
}
}
list($stdout) = $repository->execxLocalCommand(
'parents --rev %s --style default',
$commit->getCommitIdentifier());
$parents = ArcanistMercurialParser::parseMercurialLog($stdout);
$parent = reset($parents);
if ($parent) {
// TODO: We should expand this to a full 40-character hash using "hg id".
$parent = $parent['rev'];
}
foreach ($maybe_new_directories as $dir) {
$exists = false;
foreach (DiffusionPathIDQuery::expandPathToRoot($dir) as $path) {
if (isset($dir_status[$path])) {
break;
}
if (!$exists) {
if ($parent) {
$exists = $this->mercurialPathExists($repository, $path, $parent);
} else {
$exists = false;
}
}
if ($exists) {
$dir_status[$path] = DifferentialChangeType::TYPE_CHILD;
} else {
$dir_status[$path] = DifferentialChangeType::TYPE_ADD;
}
}
}
foreach ($all_directories as $dir) {
foreach (DiffusionPathIDQuery::expandPathToRoot($dir) as $path) {
if (isset($dir_status[$path])) {
break;
}
$dir_status[$path] = DifferentialChangeType::TYPE_CHILD;
}
}
// Merge all the directory statuses into the path statuses.
foreach ($dir_status as $path => $status) {
if (isset($changes[$path])) {
// TODO: The UI probably doesn't handle any of these cases with
// terrible elegance, but they are exceedingly rare.
$existing_type = $changes[$path]['changeType'];
if ($existing_type == DifferentialChangeType::TYPE_DELETE) {
// This change removes a file, replaces it with a directory, and then
// adds children of that directory. Mark it as a "change" instead,
// and make the type a directory.
$changes[$path]['fileType'] = DifferentialChangeType::FILE_DIRECTORY;
$changes[$path]['changeType'] = DifferentialChangeType::TYPE_CHANGE;
} else if ($existing_type == DifferentialChangeType::TYPE_MOVE_AWAY ||
$existing_type == DifferentialChangeType::TYPE_MULTICOPY) {
// This change moves or copies a file, replaces it with a directory,
// and then adds children to that directory. Mark it as "copy away"
// instead of whatever it was, and make the type a directory.
$changes[$path]['fileType'] = DifferentialChangeType::FILE_DIRECTORY;
$changes[$path]['changeType']
= DifferentialChangeType::TYPE_COPY_AWAY;
} else if ($existing_type == DifferentialChangeType::TYPE_ADD) {
// This change removes a diretory and replaces it with a file. Mark
// it as "change" instead of "add".
$changes[$path]['changeType'] = DifferentialChangeType::TYPE_CHANGE;
}
continue;
}
$changes[$path] = array(
'path' => $path,
'isDirect' => ($status == DifferentialChangeType::TYPE_CHILD)
? false
: true,
'fileType' => DifferentialChangeType::FILE_DIRECTORY,
'changeType' => $status,
'targetPath' => null,
'targetCommitID' => null,
) + $common_attributes;
}
// TODO: use "hg diff --git" to figure out which files are symlinks.
foreach ($changes as $path => $change) {
if (empty($change['fileType'])) {
$changes[$path]['fileType'] = DifferentialChangeType::FILE_NORMAL;
}
}
$all_paths = array();
foreach ($changes as $path => $change) {
$all_paths[$path] = true;
if ($change['targetPath']) {
$all_paths[$change['targetPath']] = true;
}
}
$path_map = $this->lookupOrCreatePaths(array_keys($all_paths));
foreach ($changes as $key => $change) {
$changes[$key]['pathID'] = $path_map[$change['path']];
if ($change['targetPath']) {
$changes[$key]['targetPathID'] = $path_map[$change['targetPath']];
} else {
$changes[$key]['targetPathID'] = null;
}
}
$conn_w = $repository->establishConnection('w');
$changes_sql = array();
foreach ($changes as $change) {
$values = array(
(int)$change['repositoryID'],
(int)$change['pathID'],
(int)$change['commitID'],
$change['targetPathID']
? (int)$change['targetPathID']
: 'null',
$change['targetCommitID']
? (int)$change['targetCommitID']
: 'null',
(int)$change['changeType'],
(int)$change['fileType'],
(int)$change['isDirect'],
(int)$change['commitSequence'],
);
$changes_sql[] = '('.implode(', ', $values).')';
}
queryfx(
$conn_w,
'DELETE FROM %T WHERE commitID = %d',
PhabricatorRepository::TABLE_PATHCHANGE,
$commit->getID());
foreach (array_chunk($changes_sql, 256) as $sql_chunk) {
queryfx(
$conn_w,
'INSERT INTO %T
(repositoryID, pathID, commitID, targetPathID, targetCommitID,
changeType, fileType, isDirect, commitSequence)
VALUES %Q',
PhabricatorRepository::TABLE_PATHCHANGE,
implode(', ', $sql_chunk));
}
$this->finishParse();
}
private function mercurialPathExists(
PhabricatorRepository $repository,
$path,
$rev) {
if ($path == '/') {
return true;
}
// NOTE: For directories, this grabs the entire directory contents, but
// we don't have any more surgical approach available to us in Mercurial.
// We can't use "log" because it doesn't have enough information for us
// to figure out when a directory is deleted by a change.
list($err) = $repository->execLocalCommand(
'cat --rev %s -- %s > /dev/null',
$rev,
$path);
if ($err) {
return false;
} else {
return true;
}
}
}
diff --git a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositorySvnCommitChangeParserWorker.php b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositorySvnCommitChangeParserWorker.php
index a85ca0c25c..3a1f762ccc 100644
--- a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositorySvnCommitChangeParserWorker.php
+++ b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositorySvnCommitChangeParserWorker.php
@@ -1,798 +1,782 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
class PhabricatorRepositorySvnCommitChangeParserWorker
extends PhabricatorRepositoryCommitChangeParserWorker {
protected function parseCommit(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
// PREAMBLE: This class is absurdly complicated because it is very difficult
// to get the information we need out of SVN. The actual data we need is:
//
// 1. Recursively, what were the affected paths?
// 2. For each affected path, is it a file or a directory?
// 3. How was each path affected (e.g. add, delete, move, copy)?
//
// We spend nearly all of our effort figuring out (1) and (2) because
// "svn log" is not recursive and does not give us file/directory
// information (that is, it will report a directory move as a single move,
// even if many thousands of paths are affected).
//
// Instead, we have to "svn ls -R" the location of each path in its previous
// life to figure out whether it is a file or a directory and exactly which
// recursive paths were affected if it was moved or copied. This is very
// complicated and has many special cases.
$uri = $repository->getDetail('remote-uri');
$svn_commit = $commit->getCommitIdentifier();
$callsign = $repository->getCallsign();
$full_name = 'r'.$callsign.$svn_commit;
echo "Parsing {$full_name}...\n";
if ($this->isBadCommit($full_name)) {
echo "This commit is marked bad!\n";
return;
}
// Pull the top-level path changes out of "svn log". This is pretty
// straightforward; just parse the XML log.
$log = $this->getSVNLogXMLObject($uri, $svn_commit, $verbose = true);
$entry = $log->logentry[0];
if (!$entry->paths) {
// TODO: Explicitly mark this commit as broken elsewhere? This isn't
// supposed to happen but we have some cases like rE27 and rG935 in the
// Facebook repositories where things got all clowned up.
return;
}
$raw_paths = array();
foreach ($entry->paths->path as $path) {
$name = trim((string)$path);
$raw_paths[$name] = array(
'rawPath' => $name,
'rawTargetPath' => (string)$path['copyfrom-path'],
'rawChangeType' => (string)$path['action'],
'rawTargetCommit' => (string)$path['copyfrom-rev'],
);
}
$copied_or_moved_map = array();
$deleted_paths = array();
$add_paths = array();
foreach ($raw_paths as $path => $raw_info) {
if ($raw_info['rawTargetPath']) {
$copied_or_moved_map[$raw_info['rawTargetPath']][] = $raw_info;
}
switch ($raw_info['rawChangeType']) {
case 'D':
$deleted_paths[$path] = $raw_info;
break;
case 'A':
$add_paths[$path] = $raw_info;
break;
}
}
// If a path was deleted, we need to look in the repository history to
// figure out where the former valid location for it is so we can figure out
// if it was a directory or not, among other things.
$lookup_here = array();
foreach ($raw_paths as $path => $raw_info) {
if ($raw_info['rawChangeType'] != 'D') {
continue;
}
// If a change copies a directory and then deletes something from it,
// we need to look at the old location for information about the path, not
// the new location. This workflow is pretty ridiculous -- so much so that
// Trac gets it wrong. See Facebook rO6 for an example, if you happen to
// work at Facebook.
$parents = $this->expandAllParentPaths($path, $include_self = true);
foreach ($parents as $parent) {
if (isset($add_paths[$parent])) {
$relative_path = substr($path, strlen($parent));
$lookup_here[$path] = array(
'rawPath' => $add_paths[$parent]['rawTargetPath'].$relative_path,
'rawCommit' => $add_paths[$parent]['rawTargetCommit'],
);
continue 2;
}
}
// Otherwise we can just look at the previous revision.
$lookup_here[$path] = array(
'rawPath' => $path,
'rawCommit' => $svn_commit - 1,
);
}
$lookup = array();
foreach ($raw_paths as $path => $raw_info) {
if ($raw_info['rawChangeType'] == 'D') {
$lookup[$path] = $lookup_here[$path];
} else {
// For everything that wasn't deleted, we can just look it up directly.
$lookup[$path] = array(
'rawPath' => $path,
'rawCommit' => $svn_commit,
);
}
}
$effects = array();
$path_file_types = $this->lookupPathFileTypes($repository, $lookup);
foreach ($raw_paths as $path => $raw_info) {
if ($raw_info['rawChangeType'] == 'D' &&
$path_file_types[$path] == DifferentialChangeType::FILE_DIRECTORY) {
// Bad. Child paths aren't enumerated in "svn log" so we need
// to go fishing.
$list = $this->lookupRecursiveFileList(
$repository,
$lookup[$path]);
foreach ($list as $deleted_path => $path_file_type) {
$deleted_path = rtrim($path.'/'.$deleted_path, '/');
if (!empty($raw_paths[$deleted_path])) {
// We somehow learned about this deletion explicitly?
// TODO: Unclear how this is possible.
continue;
}
$effect_type = DifferentialChangeType::TYPE_DELETE;
$effect_target_path = null;
if (isset($copied_or_moved_map[$deleted_path])) {
$effect_target_path = $path;
if (count($copied_or_moved_map[$deleted_path]) > 1) {
$effect_type = DifferentialChangeType::TYPE_MULTICOPY;
} else {
$effect_type = DifferentialChangeType::TYPE_MOVE_AWAY;
}
}
$effects[$deleted_path] = array(
'rawPath' => $deleted_path,
'rawTargetPath' => $effect_target_path,
'rawTargetCommit' => null,
'rawDirect' => true,
'changeType' => $effect_type,
'fileType' => $path_file_type,
);
$deleted_paths[$deleted_path] = $effects[$deleted_path];
}
}
}
$resolved_types = array();
$supplemental = array();
foreach ($raw_paths as $path => $raw_info) {
if (isset($resolved_types[$path])) {
$type = $resolved_types[$path];
} else {
switch ($raw_info['rawChangeType']) {
case 'D':
if (isset($copied_or_moved_map[$path])) {
if (count($copied_or_moved_map[$path]) > 1) {
$type = DifferentialChangeType::TYPE_MULTICOPY;
} else {
$type = DifferentialChangeType::TYPE_MOVE_AWAY;
}
} else {
$type = DifferentialChangeType::TYPE_DELETE;
}
break;
case 'A':
$copy_from = $raw_info['rawTargetPath'];
$copy_rev = $raw_info['rawTargetCommit'];
if (!strlen($copy_from)) {
$type = DifferentialChangeType::TYPE_ADD;
} else {
if (isset($deleted_paths[$copy_from])) {
$type = DifferentialChangeType::TYPE_MOVE_HERE;
$other_type = DifferentialChangeType::TYPE_MOVE_AWAY;
} else {
$type = DifferentialChangeType::TYPE_COPY_HERE;
$other_type = DifferentialChangeType::TYPE_COPY_AWAY;
}
$source_file_type = $this->lookupPathFileType(
$repository,
$copy_from,
array(
'rawPath' => $copy_from,
'rawCommit' => $copy_rev,
));
if ($source_file_type == DifferentialChangeType::FILE_DELETED) {
throw new Exception(
"Something is wrong; source of a copy must exist.");
}
if ($source_file_type != DifferentialChangeType::FILE_DIRECTORY) {
if (isset($raw_paths[$copy_from]) ||
isset($effects[$copy_from])) {
break;
}
$effects[$copy_from] = array(
'rawPath' => $copy_from,
'rawTargetPath' => null,
'rawTargetCommit' => null,
'rawDirect' => false,
'changeType' => $other_type,
'fileType' => $source_file_type,
);
} else {
// ULTRADISASTER. We've added a directory which was copied
// or moved from somewhere else. This is the most complex and
// ridiculous case.
$list = $this->lookupRecursiveFileList(
$repository,
array(
'rawPath' => $copy_from,
'rawCommit' => $copy_rev,
));
foreach ($list as $from_path => $from_file_type) {
$full_from = rtrim($copy_from.'/'.$from_path, '/');
$full_to = rtrim($path.'/'.$from_path, '/');
if (empty($raw_paths[$full_to])) {
$effects[$full_to] = array(
'rawPath' => $full_to,
'rawTargetPath' => $full_from,
'rawTargetCommit' => $copy_rev,
'rawDirect' => false,
'changeType' => $type,
'fileType' => $from_file_type,
);
} else {
// This means we picked the file up explicitly elsewhere.
// If the file as modified, SVN will drop the copy
// information. We need to restore it.
$supplemental[$full_to]['rawTargetPath'] = $full_from;
$supplemental[$full_to]['rawTargetCommit'] = $copy_rev;
if ($raw_paths[$full_to]['rawChangeType'] == 'M') {
$resolved_types[$full_to] = $type;
}
}
if (empty($raw_paths[$full_from]) &&
empty($effects[$full_from])) {
if ($other_type == DifferentialChangeType::TYPE_COPY_AWAY) {
$effects[$full_from] = array(
'rawPath' => $full_from,
'rawTargetPath' => null,
'rawTargetCommit' => null,
'rawDirect' => false,
'changeType' => $other_type,
'fileType' => $from_file_type,
);
}
}
}
}
}
break;
// This is "replaced", caused by "svn rm"-ing a file, putting another
// in its place, and then "svn add"-ing it. We do not distinguish
// between this and "M".
case 'R':
case 'M':
if (isset($copied_or_moved_map[$path])) {
$type = DifferentialChangeType::TYPE_COPY_AWAY;
} else {
$type = DifferentialChangeType::TYPE_CHANGE;
}
break;
}
}
$resolved_types[$path] = $type;
}
foreach ($raw_paths as $path => $raw_info) {
$raw_paths[$path]['changeType'] = $resolved_types[$path];
if (isset($supplemental[$path])) {
foreach ($supplemental[$path] as $key => $value) {
$raw_paths[$path][$key] = $value;
}
}
}
foreach ($raw_paths as $path => $raw_info) {
$effects[$path] = array(
'rawPath' => $path,
'rawTargetPath' => $raw_info['rawTargetPath'],
'rawTargetCommit' => $raw_info['rawTargetCommit'],
'rawDirect' => true,
'changeType' => $raw_info['changeType'],
'fileType' => $path_file_types[$path],
);
}
$parents = array();
foreach ($effects as $path => $effect) {
foreach ($this->expandAllParentPaths($path) as $parent_path) {
$parents[$parent_path] = true;
}
}
$parents = array_keys($parents);
foreach ($parents as $parent) {
if (isset($effects[$parent])) {
continue;
}
$effects[$parent] = array(
'rawPath' => $parent,
'rawTargetPath' => null,
'rawTargetCommit' => null,
'rawDirect' => false,
'changeType' => DifferentialChangeType::TYPE_CHILD,
'fileType' => DifferentialChangeType::FILE_DIRECTORY,
);
}
$lookup_paths = array();
foreach ($effects as $effect) {
$lookup_paths[$effect['rawPath']] = true;
if ($effect['rawTargetPath']) {
$lookup_paths[$effect['rawTargetPath']] = true;
}
}
$lookup_paths = array_keys($lookup_paths);
$lookup_commits = array();
foreach ($effects as $effect) {
if ($effect['rawTargetCommit']) {
$lookup_commits[$effect['rawTargetCommit']] = true;
}
}
$lookup_commits = array_keys($lookup_commits);
$path_map = $this->lookupOrCreatePaths($lookup_paths);
$commit_map = $this->lookupSvnCommits($repository, $lookup_commits);
$this->writeChanges($repository, $commit, $effects, $path_map, $commit_map);
$this->writeBrowse($repository, $commit, $effects, $path_map);
$this->finishParse();
}
private function writeChanges(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit,
array $effects,
array $path_map,
array $commit_map) {
$conn_w = $repository->establishConnection('w');
$sql = array();
foreach ($effects as $effect) {
$sql[] = qsprintf(
$conn_w,
'(%d, %d, %d, %nd, %nd, %d, %d, %d, %d)',
$repository->getID(),
$path_map[$effect['rawPath']],
$commit->getID(),
$effect['rawTargetPath']
? $path_map[$effect['rawTargetPath']]
: null,
$effect['rawTargetCommit']
? $commit_map[$effect['rawTargetCommit']]
: null,
$effect['changeType'],
$effect['fileType'],
$effect['rawDirect']
? 1
: 0,
$commit->getCommitIdentifier());
}
queryfx(
$conn_w,
'DELETE FROM %T WHERE commitID = %d',
PhabricatorRepository::TABLE_PATHCHANGE,
$commit->getID());
foreach (array_chunk($sql, 512) as $sql_chunk) {
queryfx(
$conn_w,
'INSERT INTO %T
(repositoryID, pathID, commitID, targetPathID, targetCommitID,
changeType, fileType, isDirect, commitSequence)
VALUES %Q',
PhabricatorRepository::TABLE_PATHCHANGE,
implode(', ', $sql_chunk));
}
}
private function writeBrowse(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit,
array $effects,
array $path_map) {
$conn_w = $repository->establishConnection('w');
$sql = array();
foreach ($effects as $effect) {
$type = $effect['changeType'];
if (!$effect['rawDirect']) {
if ($type == DifferentialChangeType::TYPE_COPY_AWAY) {
// Don't write COPY_AWAY to the filesystem table if it isn't a direct
// event.
continue;
}
if ($type == DifferentialChangeType::TYPE_CHILD) {
// Don't write CHILD to the filesystem table. Although doing these
// writes has the nice property of letting you see when a directory's
// contents were last changed, it explodes the table tremendously
// and makes Diffusion far slower.
continue;
}
}
if ($effect['rawPath'] == '/') {
// Don't write any events on '/' to the filesystem table; in
// particular, it doesn't have a meaningful parentID.
continue;
}
$existed = !DifferentialChangeType::isDeleteChangeType($type);
$sql[] = qsprintf(
$conn_w,
'(%d, %d, %d, %d, %d, %d)',
$repository->getID(),
$path_map[$this->getParentPath($effect['rawPath'])],
$commit->getCommitIdentifier(),
$path_map[$effect['rawPath']],
$existed
? 1
: 0,
$effect['fileType']);
}
queryfx(
$conn_w,
'DELETE FROM %T WHERE repositoryID = %d AND svnCommit = %d',
PhabricatorRepository::TABLE_FILESYSTEM,
$repository->getID(),
$commit->getCommitIdentifier());
foreach (array_chunk($sql, 512) as $sql_chunk) {
queryfx(
$conn_w,
'INSERT INTO %T
(repositoryID, parentID, svnCommit, pathID, existed, fileType)
VALUES %Q',
PhabricatorRepository::TABLE_FILESYSTEM,
implode(', ', $sql_chunk));
}
}
private function lookupSvnCommits(
PhabricatorRepository $repository,
array $commits) {
if (!$commits) {
return array();
}
$commit_table = new PhabricatorRepositoryCommit();
$commit_data = queryfx_all(
$commit_table->establishConnection('w'),
'SELECT id, commitIdentifier FROM %T
WHERE repositoryID = %d AND commitIdentifier in (%Ld)',
$commit_table->getTableName(),
$repository->getID(),
$commits);
$commit_map = ipull($commit_data, 'id', 'commitIdentifier');
$need = array();
foreach ($commits as $commit) {
if (empty($commit_map[$commit])) {
$need[] = $commit;
}
}
// If we are parsing a Subversion repository and have been configured to
// import only some subdirectory of it, we may find commits which reference
// other foreign commits outside of the directory (for instance, because of
// a move or copy). Rather than trying to execute full parses on them, just
// create stub commits and identify the stubs as foreign commits.
if ($need) {
$subpath = $repository->getDetail('svn-subpath');
if (!$subpath) {
$commits = implode(', ', $need);
throw new Exception(
"Missing commits ({$need}) in a SVN repository which is not ".
"configured for subdirectory-only parsing!");
}
foreach ($need as $foreign_commit) {
$commit = new PhabricatorRepositoryCommit();
$commit->setRepositoryID($repository->getID());
$commit->setCommitIdentifier($foreign_commit);
$commit->setEpoch(0);
$commit->save();
$data = new PhabricatorRepositoryCommitData();
$data->setCommitID($commit->getID());
$data->setAuthorName('');
$data->setCommitMessage('');
$data->setCommitDetails(
array(
'foreign-svn-stub' => true,
// Denormalize this to make it easier to debug cases where someone
// did half a parse and then changed the subdirectory or something
// like that.
'svn-subpath' => $subpath,
));
$data->save();
$commit_map[$foreign_commit] = $commit->getID();
}
}
return $commit_map;
}
private function lookupPathFileType(
PhabricatorRepository $repository,
$path,
array $path_info) {
$result = $this->lookupPathFileTypes(
$repository,
array(
$path => $path_info,
));
return $result[$path];
}
private function lookupPathFileTypes(
PhabricatorRepository $repository,
array $paths) {
$repository_uri = $repository->getDetail('remote-uri');
$parents = array();
$path_mapping = array();
foreach ($paths as $path => $lookup) {
$parent = dirname($lookup['rawPath']);
$parent = ltrim($parent, '/');
$parent = $this->encodeSVNPath($parent);
$parent = $repository_uri.$parent.'@'.$lookup['rawCommit'];
$parent = escapeshellarg($parent);
$parents[$parent] = true;
$path_mapping[$parent][] = dirname($path);
}
$result_map = array();
// Reverse this list so we can pop $path_mapping, as that's more efficient
// than shifting it. We need to associate these maps positionally because
// a change can copy the same source path from multiple revisions via
// "svn cp path@1 a; svn cp path@2 b;" and the XML output gives us no way
// to distinguish which revision we're looking at except based on its
// position in the document.
$all_paths = array_reverse(array_keys($parents));
foreach (array_chunk($all_paths, 64) as $path_chunk) {
list($raw_xml) = $repository->execxRemoteCommand(
'--xml ls %C',
implode(' ', $path_chunk));
$xml = new SimpleXMLElement($raw_xml);
foreach ($xml->list as $list) {
$list_path = (string)$list['path'];
// SVN is a big mess. See Facebook rG8 (a revision which adds files
// with spaces in their names) for an example.
$list_path = rawurldecode($list_path);
if ($list_path == $repository_uri) {
$base = '/';
} else {
$base = substr($list_path, strlen($repository_uri));
}
$mapping = array_pop($path_mapping);
foreach ($list->entry as $entry) {
$val = $this->getFileTypeFromSVNKind($entry['kind']);
foreach ($mapping as $base_path) {
// rtrim() causes us to handle top-level directories correctly.
$key = rtrim($base_path, '/').'/'.$entry->name;
$result_map[$key] = $val;
}
}
}
}
foreach ($paths as $path => $lookup) {
if (empty($result_map[$path])) {
$result_map[$path] = DifferentialChangeType::FILE_DELETED;
}
}
return $result_map;
}
private function encodeSVNPath($path) {
$path = rawurlencode($path);
$path = str_replace('%2F', '/', $path);
return $path;
}
private function getFileTypeFromSVNKind($kind) {
$kind = (string)$kind;
switch ($kind) {
case 'dir': return DifferentialChangeType::FILE_DIRECTORY;
case 'file': return DifferentialChangeType::FILE_NORMAL;
default:
throw new Exception("Unknown SVN file kind '{$kind}'.");
}
}
private function lookupRecursiveFileList(
PhabricatorRepository $repository,
array $info) {
$path = $info['rawPath'];
$rev = $info['rawCommit'];
$path = $this->encodeSVNPath($path);
$hashkey = md5($repository->getDetail('remote-uri').$path.'@'.$rev);
// This method is quite horrible. The underlying challenge is that some
// commits in the Facebook repository are enormous, taking multiple hours
// to 'ls -R' out of the repository and producing XML files >1GB in size.
// If we try to SimpleXML them, the object exhausts available memory on a
// 64G machine. Instead, cache the XML output and then parse it line by line
// to limit space requirements.
$cache_loc = sys_get_temp_dir().'/diffusion.'.$hashkey.'.svnls';
if (!Filesystem::pathExists($cache_loc)) {
$tmp = new TempFile();
$repository->execxRemoteCommand(
'--xml ls -R %s%s@%d > %s',
$repository->getDetail('remote-uri'),
$path,
$rev,
$tmp);
execx(
'mv %s %s',
$tmp,
$cache_loc);
}
$map = $this->parseRecursiveListFileData($cache_loc);
Filesystem::remove($cache_loc);
return $map;
}
private function parseRecursiveListFileData($file_path) {
$map = array();
$mode = 'xml';
$done = false;
$entry = null;
foreach (new LinesOfALargeFile($file_path) as $lno => $line) {
switch ($mode) {
case 'entry':
if ($line == '</entry>') {
$entry = implode('', $entry);
$pattern = '@^\s+kind="(file|dir)">'.
'<name>(.*?)</name>'.
'(<size>(.*?)</size>)?@';
$matches = null;
if (!preg_match($pattern, $entry, $matches)) {
throw new Exception("Unable to parse entry!");
}
$map[html_entity_decode($matches[2])] =
$this->getFileTypeFromSVNKind($matches[1]);
$mode = 'entry-or-end';
} else {
$entry[] = $line;
}
break;
case 'entry-or-end':
if ($line == '</list>') {
$done = true;
break 2;
} else if ($line == '<entry') {
$mode = 'entry';
$entry = array();
} else {
throw new Exception("Expected </list> or <entry, got {$line}.");
}
break;
case 'xml':
$expect = '/<?xml version="1.0".*?>/';
if (!preg_match($expect, $line)) {
throw new Exception("Expected '{$expect}', got {$line}.");
}
$mode = 'list';
break;
case 'list':
$expect = '<lists>';
if ($line !== $expect) {
throw new Exception("Expected '{$expect}', got {$line}.");
}
$mode = 'list1';
break;
case 'list1':
$expect = '<list';
if ($line !== $expect) {
throw new Exception("Expected '{$expect}', got {$line}.");
}
$mode = 'list2';
break;
case 'list2':
if (!preg_match('/^\s+path="/', $line)) {
throw new Exception("Expected ' path=...', got {$line}.");
}
$mode = 'entry-or-end';
break;
}
}
if (!$done) {
throw new Exception("Unexpected end of file.");
}
return $map;
}
// TODO: Replace with DiffusionPathIDQuery::getParentPath().
private function getParentPath($path) {
$path = rtrim($path, '/');
$path = dirname($path);
if (!$path) {
$path = '/';
}
return $path;
}
// TODO: Replace with DiffusionPathIDQuery::expandPathToRoot().
private function expandAllParentPaths($path, $include_self = false) {
$parents = array();
if ($include_self) {
$parents[] = '/'.rtrim($path, '/');
}
$parts = explode('/', trim($path, '/'));
while (count($parts) >= 1) {
array_pop($parts);
$parents[] = '/'.implode('/', $parts);
}
return $parents;
}
}
diff --git a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php
index 5f8e21f455..70cedc63bf 100644
--- a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php
+++ b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php
@@ -1,446 +1,430 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorRepositoryCommitMessageParserWorker
extends PhabricatorRepositoryCommitParserWorker {
abstract protected function getCommitHashes(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit);
final protected function updateCommitData($author, $message,
$committer = null) {
$commit = $this->commit;
$data = id(new PhabricatorRepositoryCommitData())->loadOneWhere(
'commitID = %d',
$commit->getID());
if (!$data) {
$data = new PhabricatorRepositoryCommitData();
}
$data->setCommitID($commit->getID());
$data->setAuthorName($author);
$data->setCommitMessage($message);
if ($committer) {
$data->setCommitDetail('committer', $committer);
}
$repository = $this->repository;
$detail_parser = $repository->getDetail(
'detail-parser',
'PhabricatorRepositoryDefaultCommitMessageDetailParser');
if ($detail_parser) {
$parser_obj = newv($detail_parser, array($commit, $data));
$parser_obj->parseCommitDetails();
}
$author_phid = $this->lookupUser(
$commit,
$data->getAuthorName(),
$data->getCommitDetail('authorPHID'));
$data->setCommitDetail('authorPHID', $author_phid);
$committer_phid = $this->lookupUser(
$commit,
$data->getCommitDetail('committer'),
$data->getCommitDetail('committerPHID'));
$data->setCommitDetail('committerPHID', $committer_phid);
if ($author_phid != $commit->getAuthorPHID()) {
$commit->setAuthorPHID($author_phid);
$commit->save();
}
$conn_w = id(new DifferentialRevision())->establishConnection('w');
// NOTE: The `differential_commit` table has a unique ID on `commitPHID`,
// preventing more than one revision from being associated with a commit.
// Generally this is good and desirable, but with the advent of hash
// tracking we may end up in a situation where we match several different
// revisions. We just kind of ignore this and pick one, we might want to
// revisit this and do something differently. (If we match several revisions
// someone probably did something very silly, though.)
$revision = null;
$should_autoclose = $repository->shouldAutocloseCommit($commit, $data);
$revision_id = $data->getCommitDetail('differential.revisionID');
if (!$revision_id) {
$hashes = $this->getCommitHashes(
$this->repository,
$this->commit);
if ($hashes) {
$query = new DifferentialRevisionQuery();
$query->withCommitHashes($hashes);
$revisions = $query->execute();
if (!empty($revisions)) {
$revision = $this->identifyBestRevision($revisions);
$revision_id = $revision->getID();
}
}
}
if ($revision_id) {
$lock = PhabricatorGlobalLock::newLock(get_class($this).':'.$revision_id);
$lock->lock(5 * 60);
$revision = id(new DifferentialRevision())->load($revision_id);
if ($revision) {
$revision->loadRelationships();
queryfx(
$conn_w,
'INSERT IGNORE INTO %T (revisionID, commitPHID) VALUES (%d, %s)',
DifferentialRevision::TABLE_COMMIT,
$revision->getID(),
$commit->getPHID());
$status_closed = ArcanistDifferentialRevisionStatus::CLOSED;
$should_close = ($revision->getStatus() != $status_closed) &&
$should_autoclose;
if ($should_close) {
$actor_phid = nonempty(
$committer_phid,
$author_phid,
$revision->getAuthorPHID());
$actor = id(new PhabricatorUser())
->loadOneWhere('phid = %s', $actor_phid);
$diff = $this->attachToRevision($revision, $actor_phid);
$revision->setDateCommitted($commit->getEpoch());
$editor = new DifferentialCommentEditor(
$revision,
DifferentialAction::ACTION_CLOSE);
$editor->setActor($actor);
$editor->setIsDaemonWorkflow(true);
$vs_diff = $this->loadChangedByCommit($diff);
if ($vs_diff) {
$data->setCommitDetail('vsDiff', $vs_diff->getID());
$changed_by_commit = PhabricatorEnv::getProductionURI(
'/D'.$revision->getID().
'?vs='.$vs_diff->getID().
'&id='.$diff->getID().
'#toc');
$editor->setChangedByCommit($changed_by_commit);
}
$commit_name = $repository->formatCommitName(
$commit->getCommitIdentifier());
$committer_name = $this->loadUserName(
$committer_phid,
$data->getCommitDetail('committer'));
$author_name = $this->loadUserName(
$author_phid,
$data->getAuthorName());
$info = array();
$info[] = "authored by {$author_name}";
if ($committer_name && ($committer_name != $author_name)) {
$info[] = "committed by {$committer_name}";
}
$info = implode(', ', $info);
$editor
->setMessage("Closed by commit {$commit_name} ({$info}).")
->save();
}
}
$lock->unlock();
}
if ($should_autoclose && $author_phid) {
$user = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$author_phid);
$call = new ConduitCall(
'differential.parsecommitmessage',
array(
'corpus' => $message,
'partial' => true,
));
$call->setUser($user);
$result = $call->execute();
$field_values = $result['fields'];
$fields = DifferentialFieldSelector::newSelector()
->getFieldSpecifications();
foreach ($fields as $key => $field) {
if (!$field->shouldAppearOnCommitMessage()) {
continue;
}
$field->setUser($user);
$value = idx($field_values, $field->getCommitMessageKey());
$field->setValueFromParsedCommitMessage($value);
if ($revision) {
$field->setRevision($revision);
}
$field->didParseCommit($repository, $commit, $data);
}
}
$data->save();
}
private function loadUserName($user_phid, $default) {
if (!$user_phid) {
return $default;
}
$handle = PhabricatorObjectHandleData::loadOneHandle($user_phid);
return '@'.$handle->getName();
}
private function attachToRevision(
DifferentialRevision $revision,
$actor_phid) {
$drequest = DiffusionRequest::newFromDictionary(array(
'repository' => $this->repository,
'commit' => $this->commit->getCommitIdentifier(),
));
$raw_diff = DiffusionRawDiffQuery::newFromDiffusionRequest($drequest)
->loadRawDiff();
// TODO: Support adds, deletes and moves under SVN.
$changes = id(new ArcanistDiffParser())->parseDiff($raw_diff);
$diff = DifferentialDiff::newFromRawChanges($changes)
->setRevisionID($revision->getID())
->setAuthorPHID($actor_phid)
->setCreationMethod('commit')
->setSourceControlSystem($this->repository->getVersionControlSystem())
->setLintStatus(DifferentialLintStatus::LINT_SKIP)
->setUnitStatus(DifferentialUnitStatus::UNIT_SKIP)
->setDateCreated($this->commit->getEpoch())
->setDescription(
'Commit r'.
$this->repository->getCallsign().
$this->commit->getCommitIdentifier());
// TODO: This is not correct in SVN where one repository can have multiple
// Arcanist projects.
$arcanist_project = id(new PhabricatorRepositoryArcanistProject())
->loadOneWhere('repositoryID = %d LIMIT 1', $this->repository->getID());
if ($arcanist_project) {
$diff->setArcanistProjectPHID($arcanist_project->getPHID());
}
$parents = DiffusionCommitParentsQuery::newFromDiffusionRequest($drequest)
->loadParents();
if ($parents) {
$diff->setSourceControlBaseRevision(head_key($parents));
}
// TODO: Attach binary files.
$revision->setLineCount($diff->getLineCount());
return $diff->save();
}
private function loadChangedByCommit(DifferentialDiff $diff) {
$repository = $this->repository;
$vs_changesets = array();
$vs_diff = id(new DifferentialDiff())->loadOneWhere(
'revisionID = %d AND creationMethod != %s ORDER BY id DESC LIMIT 1',
$diff->getRevisionID(),
'commit');
foreach ($vs_diff->loadChangesets() as $changeset) {
$path = $changeset->getAbsoluteRepositoryPath($repository, $vs_diff);
$path = ltrim($path, '/');
$vs_changesets[$path] = $changeset;
}
$changesets = array();
foreach ($diff->getChangesets() as $changeset) {
$path = $changeset->getAbsoluteRepositoryPath($repository, $diff);
$path = ltrim($path, '/');
$changesets[$path] = $changeset;
}
if (array_fill_keys(array_keys($changesets), true) !=
array_fill_keys(array_keys($vs_changesets), true)) {
return $vs_diff;
}
$hunks = id(new DifferentialHunk())->loadAllWhere(
'changesetID IN (%Ld)',
mpull($vs_changesets, 'getID'));
$hunks = mgroup($hunks, 'getChangesetID');
foreach ($vs_changesets as $changeset) {
$changeset->attachHunks(idx($hunks, $changeset->getID(), array()));
}
$file_phids = array();
foreach ($vs_changesets as $changeset) {
$metadata = $changeset->getMetadata();
$file_phid = idx($metadata, 'new:binary-phid');
if ($file_phid) {
$file_phids[$file_phid] = $file_phid;
}
}
$files = array();
if ($file_phids) {
$files = id(new PhabricatorFile())->loadAllWhere(
'phid IN (%Ls)',
$file_phids);
$files = mpull($files, null, 'getPHID');
}
foreach ($changesets as $path => $changeset) {
$vs_changeset = $vs_changesets[$path];
$file_phid = idx($vs_changeset->getMetadata(), 'new:binary-phid');
if ($file_phid) {
if (!isset($files[$file_phid])) {
return $vs_diff;
}
$drequest = DiffusionRequest::newFromDictionary(array(
'repository' => $this->repository,
'commit' => $this->commit->getCommitIdentifier(),
'path' => $path,
));
$corpus = DiffusionFileContentQuery::newFromDiffusionRequest($drequest)
->loadFileContent()
->getCorpus();
if ($files[$file_phid]->loadFileData() != $corpus) {
return $vs_diff;
}
} else {
$context = implode("\n", $changeset->makeChangesWithContext());
$vs_context = implode("\n", $vs_changeset->makeChangesWithContext());
// We couldn't just compare $context and $vs_context because following
// diffs will be considered different:
//
// -(empty line)
// -echo 'test';
// (empty line)
//
// (empty line)
// -echo "test";
// -(empty line)
$hunk = id(new DifferentialHunk())->setChanges($context);
$vs_hunk = id(new DifferentialHunk())->setChanges($vs_context);
if ($hunk->makeOldFile() != $vs_hunk->makeOldFile() ||
$hunk->makeNewFile() != $vs_hunk->makeNewFile()) {
return $vs_diff;
}
}
}
return null;
}
/**
* When querying for revisions by hash, more than one revision may be found.
* This function identifies the "best" revision from such a set. Typically,
* there is only one revision found. Otherwise, we try to pick an accepted
* revision first, followed by an open revision, and otherwise we go with a
* closed or abandoned revision as a last resort.
*/
private function identifyBestRevision(array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
// get the simplest, common case out of the way
if (count($revisions) == 1) {
return reset($revisions);
}
$first_choice = array();
$second_choice = array();
$third_choice = array();
foreach ($revisions as $revision) {
switch ($revision->getStatus()) {
// "Accepted" revisions -- ostensibly what we're looking for!
case ArcanistDifferentialRevisionStatus::ACCEPTED:
$first_choice[] = $revision;
break;
// "Open" revisions
case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
$second_choice[] = $revision;
break;
// default is a wtf? here
default:
case ArcanistDifferentialRevisionStatus::ABANDONED:
case ArcanistDifferentialRevisionStatus::CLOSED:
$third_choice[] = $revision;
break;
}
}
// go down the ladder like a bro at last call
if (!empty($first_choice)) {
return $this->identifyMostRecentRevision($first_choice);
}
if (!empty($second_choice)) {
return $this->identifyMostRecentRevision($second_choice);
}
if (!empty($third_choice)) {
return $this->identifyMostRecentRevision($third_choice);
}
}
/**
* Given a set of revisions, returns the revision with the latest
* updated time. This is ostensibly the most recent revision.
*/
private function identifyMostRecentRevision(array $revisions) {
assert_instances_of($revisions, 'DifferentialRevision');
$revisions = msort($revisions, 'getDateModified');
return end($revisions);
}
/**
* Emit an event so installs can do custom lookup of commit authors who may
* not be naturally resolvable.
*/
private function lookupUser(
PhabricatorRepositoryCommit $commit,
$query,
$guess) {
$type = PhabricatorEventType::TYPE_DIFFUSION_LOOKUPUSER;
$data = array(
'commit' => $commit,
'query' => $query,
'result' => $guess,
);
$event = new PhabricatorEvent($type, $data);
PhutilEventEngine::dispatchEvent($event);
return $event->getValue('result');
}
}
diff --git a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryGitCommitMessageParserWorker.php b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryGitCommitMessageParserWorker.php
index 9496ad3ace..213b32e3fc 100644
--- a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryGitCommitMessageParserWorker.php
+++ b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryGitCommitMessageParserWorker.php
@@ -1,100 +1,84 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorRepositoryGitCommitMessageParserWorker
extends PhabricatorRepositoryCommitMessageParserWorker {
public function parseCommit(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
// NOTE: %B was introduced somewhat recently in git's history, so pull
// commit message information with %s and %b instead.
// Even though we pass --encoding here, git doesn't always succeed, so
// we try a little harder, since git *does* tell us what the actual encoding
// is correctly (unless it doesn't; encoding is sometimes empty).
list($info) = $repository->execxLocalCommand(
'log -n 1 --encoding=%s --format=%s %s --',
'UTF-8',
implode('%x00', array('%e', '%cn', '%ce', '%an', '%ae', '%s%n%n%b')),
$commit->getCommitIdentifier());
$parts = explode("\0", $info);
$encoding = array_shift($parts);
foreach ($parts as $key => $part) {
if ($encoding) {
$part = phutil_utf8_convert($part, 'UTF-8', $encoding);
}
$parts[$key] = phutil_utf8ize($part);
}
$committer_name = $parts[0];
$committer_email = $parts[1];
$author_name = $parts[2];
$author_email = $parts[3];
$message = $parts[4];
if (strlen($author_email)) {
$author = "{$author_name} <{$author_email}>";
} else {
$author = "{$author_name}";
}
if (strlen($committer_email)) {
$committer = "{$committer_name} <{$committer_email}>";
} else {
$committer = "{$committer_name}";
}
if ($committer == $author) {
$committer = null;
}
$this->updateCommitData($author, $message, $committer);
if ($this->shouldQueueFollowupTasks()) {
PhabricatorWorker::scheduleTask(
'PhabricatorRepositoryGitCommitChangeParserWorker',
array(
'commitID' => $commit->getID(),
));
}
}
protected function getCommitHashes(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
list($stdout) = $repository->execxLocalCommand(
'log -n 1 --format=%s %s --',
'%T',
$commit->getCommitIdentifier());
$commit_hash = $commit->getCommitIdentifier();
$tree_hash = trim($stdout);
return array(
array(ArcanistDifferentialRevisionHash::HASH_GIT_COMMIT,
$commit_hash),
array(ArcanistDifferentialRevisionHash::HASH_GIT_TREE,
$tree_hash),
);
}
}
diff --git a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryMercurialCommitMessageParserWorker.php b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryMercurialCommitMessageParserWorker.php
index 56ae7bb234..106570a419 100644
--- a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryMercurialCommitMessageParserWorker.php
+++ b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryMercurialCommitMessageParserWorker.php
@@ -1,60 +1,44 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorRepositoryMercurialCommitMessageParserWorker
extends PhabricatorRepositoryCommitMessageParserWorker {
public function parseCommit(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
list($stdout) = $repository->execxLocalCommand(
'log --template %s --rev %s',
'{author}\\n{desc}',
$commit->getCommitIdentifier());
list($author, $message) = explode("\n", $stdout, 2);
$author = phutil_utf8ize($author);
$message = phutil_utf8ize($message);
$message = trim($message);
$this->updateCommitData($author, $message);
if ($this->shouldQueueFollowupTasks()) {
PhabricatorWorker::scheduleTask(
'PhabricatorRepositoryMercurialCommitChangeParserWorker',
array(
'commitID' => $commit->getID(),
));
}
}
protected function getCommitHashes(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
$commit_hash = $commit->getCommitIdentifier();
return array(
array(ArcanistDifferentialRevisionHash::HASH_MERCURIAL_COMMIT,
$commit_hash),
);
}
}
diff --git a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositorySvnCommitMessageParserWorker.php b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositorySvnCommitMessageParserWorker.php
index f9dbeb390d..ea5bc37606 100644
--- a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositorySvnCommitMessageParserWorker.php
+++ b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositorySvnCommitMessageParserWorker.php
@@ -1,55 +1,39 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorRepositorySvnCommitMessageParserWorker
extends PhabricatorRepositoryCommitMessageParserWorker {
public function parseCommit(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
$uri = $repository->getDetail('remote-uri');
$log = $this->getSVNLogXMLObject(
$uri,
$commit->getCommitIdentifier(),
$verbose = false);
$entry = $log->logentry[0];
$author = (string)$entry->author;
$message = (string)$entry->msg;
$this->updateCommitData($author, $message);
if ($this->shouldQueueFollowupTasks()) {
PhabricatorWorker::scheduleTask(
'PhabricatorRepositorySvnCommitChangeParserWorker',
array(
'commitID' => $commit->getID(),
));
}
}
protected function getCommitHashes(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
return array();
}
}
diff --git a/src/applications/search/constants/PhabricatorSearchField.php b/src/applications/search/constants/PhabricatorSearchField.php
index 40c7e39a11..3e40debefe 100644
--- a/src/applications/search/constants/PhabricatorSearchField.php
+++ b/src/applications/search/constants/PhabricatorSearchField.php
@@ -1,29 +1,13 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group search
*/
final class PhabricatorSearchField {
const FIELD_TITLE = 'titl';
const FIELD_BODY = 'body';
const FIELD_TEST_PLAN = 'tpln';
const FIELD_COMMENT = 'cmnt';
}
diff --git a/src/applications/search/constants/PhabricatorSearchRelationship.php b/src/applications/search/constants/PhabricatorSearchRelationship.php
index de0e5d97f2..92f2c9129b 100644
--- a/src/applications/search/constants/PhabricatorSearchRelationship.php
+++ b/src/applications/search/constants/PhabricatorSearchRelationship.php
@@ -1,35 +1,19 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group search
*/
final class PhabricatorSearchRelationship {
const RELATIONSHIP_AUTHOR = 'auth';
const RELATIONSHIP_REVIEWER = 'revw';
const RELATIONSHIP_SUBSCRIBER = 'subs';
const RELATIONSHIP_COMMENTER = 'comm';
const RELATIONSHIP_OWNER = 'ownr';
const RELATIONSHIP_PROJECT = 'proj';
const RELATIONSHIP_REPOSITORY = 'repo';
const RELATIONSHIP_OPEN = 'open';
const RELATIONSHIP_TOUCH = 'poke';
}
diff --git a/src/applications/search/constants/PhabricatorSearchScope.php b/src/applications/search/constants/PhabricatorSearchScope.php
index b174b9f250..8238ea1a5c 100644
--- a/src/applications/search/constants/PhabricatorSearchScope.php
+++ b/src/applications/search/constants/PhabricatorSearchScope.php
@@ -1,57 +1,41 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group search
*/
final class PhabricatorSearchScope {
const SCOPE_ALL = 'all';
const SCOPE_OPEN_REVISIONS = 'open-revisions';
const SCOPE_OPEN_TASKS = 'open-tasks';
const SCOPE_COMMITS = 'commits';
const SCOPE_WIKI = 'wiki';
const SCOPE_QUESTIONS = 'questions';
public static function getScopeOptions() {
return array(
self::SCOPE_ALL => 'All Documents',
self::SCOPE_OPEN_TASKS => 'Open Tasks',
self::SCOPE_WIKI => 'Wiki Documents',
self::SCOPE_OPEN_REVISIONS => 'Open Revisions',
self::SCOPE_COMMITS => 'Commits',
self::SCOPE_QUESTIONS => 'Ponder Questions',
);
}
public static function getScopePlaceholder($scope) {
switch ($scope) {
case self::SCOPE_OPEN_TASKS:
return pht('Search Open Tasks');
case self::SCOPE_WIKI:
return pht('Search Wiki Documents');
case self::SCOPE_OPEN_REVISIONS:
return pht('Search Open Revisions');
case self::SCOPE_COMMITS:
return pht('Search Commits');
default:
return pht('Search');
}
}
}
diff --git a/src/applications/search/controller/PhabricatorSearchAttachController.php b/src/applications/search/controller/PhabricatorSearchAttachController.php
index 32b5b87ec2..75b126eee2 100644
--- a/src/applications/search/controller/PhabricatorSearchAttachController.php
+++ b/src/applications/search/controller/PhabricatorSearchAttachController.php
@@ -1,293 +1,277 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group search
*/
final class PhabricatorSearchAttachController
extends PhabricatorSearchBaseController {
private $phid;
private $type;
private $action;
const ACTION_ATTACH = 'attach';
const ACTION_MERGE = 'merge';
const ACTION_DEPENDENCIES = 'dependencies';
const ACTION_EDGE = 'edge';
public function willProcessRequest(array $data) {
$this->phid = $data['phid'];
$this->type = $data['type'];
$this->action = idx($data, 'action', self::ACTION_ATTACH);
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$handle_data = new PhabricatorObjectHandleData(array($this->phid));
$handles = $handle_data->loadHandles();
$handle = $handles[$this->phid];
$object_type = $handle->getType();
$attach_type = $this->type;
$objects = $handle_data->loadObjects();
$object = idx($objects, $this->phid);
if (!$object) {
return new Aphront404Response();
}
$edge_type = null;
switch ($this->action) {
case self::ACTION_EDGE:
case self::ACTION_DEPENDENCIES:
case self::ACTION_ATTACH:
$edge_type = $this->getEdgeType($object_type, $attach_type);
break;
}
if ($request->isFormPost()) {
$phids = explode(';', $request->getStr('phids'));
$phids = array_filter($phids);
$phids = array_values($phids);
if ($edge_type) {
$old_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->phid,
$edge_type);
$add_phids = $phids;
$rem_phids = array_diff($old_phids, $add_phids);
$editor = id(new PhabricatorEdgeEditor());
$editor->setActor($user);
foreach ($add_phids as $phid) {
$editor->addEdge($this->phid, $edge_type, $phid);
}
foreach ($rem_phids as $phid) {
$editor->removeEdge($this->phid, $edge_type, $phid);
}
try {
$editor->save();
} catch (PhabricatorEdgeCycleException $ex) {
$this->raiseGraphCycleException($ex);
}
return id(new AphrontReloadResponse())->setURI($handle->getURI());
} else {
return $this->performMerge($object, $handle, $phids);
}
} else {
if ($edge_type) {
$phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->phid,
$edge_type);
} else {
// This is a merge.
$phids = array();
}
}
$strings = $this->getStrings();
$handles = $this->loadViewerHandles($phids);
$obj_dialog = new PhabricatorObjectSelectorDialog();
$obj_dialog
->setUser($user)
->setHandles($handles)
->setFilters(array(
'assigned' => 'Assigned to Me',
'created' => 'Created By Me',
'open' => 'All Open '.$strings['target_plural_noun'],
'all' => 'All '.$strings['target_plural_noun'],
))
->setSelectedFilter($strings['selected'])
->setExcluded($this->phid)
->setCancelURI($handle->getURI())
->setSearchURI('/search/select/'.$attach_type.'/')
->setTitle($strings['title'])
->setHeader($strings['header'])
->setButtonText($strings['button'])
->setInstructions($strings['instructions']);
$dialog = $obj_dialog->buildDialog();
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function performMerge(
ManiphestTask $task,
PhabricatorObjectHandle $handle,
array $phids) {
$user = $this->getRequest()->getUser();
$response = id(new AphrontReloadResponse())->setURI($handle->getURI());
$phids = array_fill_keys($phids, true);
unset($phids[$task->getPHID()]); // Prevent merging a task into itself.
if (!$phids) {
return $response;
}
$targets = id(new ManiphestTask())->loadAllWhere(
'phid in (%Ls) ORDER BY id ASC',
array_keys($phids));
if (empty($targets)) {
return $response;
}
$editor = new ManiphestTransactionEditor();
$editor->setActor($user);
$task_names = array();
$merge_into_name = 'T'.$task->getID();
$cc_vector = array();
$cc_vector[] = $task->getCCPHIDs();
foreach ($targets as $target) {
$cc_vector[] = $target->getCCPHIDs();
$cc_vector[] = array(
$target->getAuthorPHID(),
$target->getOwnerPHID());
$close_task = id(new ManiphestTransaction())
->setAuthorPHID($user->getPHID())
->setTransactionType(ManiphestTransactionType::TYPE_STATUS)
->setNewValue(ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE)
->setComments("\xE2\x9C\x98 Merged into {$merge_into_name}.");
$editor->applyTransactions($target, array($close_task));
$task_names[] = 'T'.$target->getID();
}
$all_ccs = array_mergev($cc_vector);
$all_ccs = array_filter($all_ccs);
$all_ccs = array_unique($all_ccs);
$task_names = implode(', ', $task_names);
$add_ccs = id(new ManiphestTransaction())
->setAuthorPHID($user->getPHID())
->setTransactionType(ManiphestTransactionType::TYPE_CCS)
->setNewValue($all_ccs)
->setComments("\xE2\x97\x80 Merged tasks: {$task_names}.");
$editor->applyTransactions($task, array($add_ccs));
return $response;
}
private function getStrings() {
switch ($this->type) {
case PhabricatorPHIDConstants::PHID_TYPE_DREV:
$noun = 'Revisions';
$selected = 'created';
break;
case PhabricatorPHIDConstants::PHID_TYPE_TASK:
$noun = 'Tasks';
$selected = 'assigned';
break;
case PhabricatorPHIDConstants::PHID_TYPE_CMIT:
$noun = 'Commits';
$selected = 'created';
break;
}
switch ($this->action) {
case self::ACTION_EDGE:
case self::ACTION_ATTACH:
$dialog_title = "Manage Attached {$noun}";
$header_text = "Currently Attached {$noun}";
$button_text = "Save {$noun}";
$instructions = null;
break;
case self::ACTION_MERGE:
$dialog_title = "Merge Duplicate Tasks";
$header_text = "Tasks To Merge";
$button_text = "Merge {$noun}";
$instructions =
"These tasks will be merged into the current task and then closed. ".
"The current task will grow stronger.";
break;
case self::ACTION_DEPENDENCIES:
$dialog_title = "Edit Dependencies";
$header_text = "Current Dependencies";
$button_text = "Save Dependencies";
$instructions = null;
break;
}
return array(
'target_plural_noun' => $noun,
'selected' => $selected,
'title' => $dialog_title,
'header' => $header_text,
'button' => $button_text,
'instructions' => $instructions,
);
}
private function getEdgeType($src_type, $dst_type) {
$t_cmit = PhabricatorPHIDConstants::PHID_TYPE_CMIT;
$t_task = PhabricatorPHIDConstants::PHID_TYPE_TASK;
$t_drev = PhabricatorPHIDConstants::PHID_TYPE_DREV;
$map = array(
$t_cmit => array(
$t_task => PhabricatorEdgeConfig::TYPE_COMMIT_HAS_TASK,
),
$t_task => array(
$t_cmit => PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT,
$t_task => PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK,
$t_drev => PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV,
),
$t_drev => array(
$t_drev => PhabricatorEdgeConfig::TYPE_DREV_DEPENDS_ON_DREV,
$t_task => PhabricatorEdgeConfig::TYPE_DREV_HAS_RELATED_TASK,
),
);
if (empty($map[$src_type][$dst_type])) {
return null;
}
return $map[$src_type][$dst_type];
}
private function raiseGraphCycleException(PhabricatorEdgeCycleException $ex) {
$cycle = $ex->getCycle();
$handles = $this->loadViewerHandles($cycle);
$names = array();
foreach ($cycle as $cycle_phid) {
$names[] = $handles[$cycle_phid]->getFullName();
}
$names = implode(" \xE2\x86\x92 ", $names);
throw new Exception(
"You can not create that dependency, because it would create a ".
"circular dependency: {$names}.");
}
}
diff --git a/src/applications/search/controller/PhabricatorSearchBaseController.php b/src/applications/search/controller/PhabricatorSearchBaseController.php
index 2eaf11f348..f045357a66 100644
--- a/src/applications/search/controller/PhabricatorSearchBaseController.php
+++ b/src/applications/search/controller/PhabricatorSearchBaseController.php
@@ -1,37 +1,21 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group search
*/
abstract class PhabricatorSearchBaseController extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('Search');
$page->setBaseURI('/search/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph("\xC2\xBF");
$page->appendChild($view);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
}
diff --git a/src/applications/search/controller/PhabricatorSearchController.php b/src/applications/search/controller/PhabricatorSearchController.php
index 33870617af..e0e61cafaa 100644
--- a/src/applications/search/controller/PhabricatorSearchController.php
+++ b/src/applications/search/controller/PhabricatorSearchController.php
@@ -1,271 +1,255 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group search
*/
final class PhabricatorSearchController
extends PhabricatorSearchBaseController {
private $key;
public function willProcessRequest(array $data) {
$this->key = idx($data, 'key');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
if ($this->key) {
$query = id(new PhabricatorSearchQuery())->loadOneWhere(
'queryKey = %s',
$this->key);
if (!$query) {
return new Aphront404Response();
}
} else {
$query = new PhabricatorSearchQuery();
if ($request->isFormPost()) {
$query_str = $request->getStr('query');
$pref_jump = PhabricatorUserPreferences::PREFERENCE_SEARCHBAR_JUMP;
if ($request->getStr('jump') != 'no' &&
$user && $user->loadPreferences()->getPreference($pref_jump, 1)) {
$response = PhabricatorJumpNavHandler::jumpPostResponse($query_str);
} else {
$response = null;
}
if ($response) {
return $response;
} else {
$query->setQuery($query_str);
if ($request->getStr('scope')) {
switch ($request->getStr('scope')) {
case PhabricatorSearchScope::SCOPE_OPEN_REVISIONS:
$query->setParameter('open', 1);
$query->setParameter(
'type',
PhabricatorPHIDConstants::PHID_TYPE_DREV);
break;
case PhabricatorSearchScope::SCOPE_OPEN_TASKS:
$query->setParameter('open', 1);
$query->setParameter(
'type',
PhabricatorPHIDConstants::PHID_TYPE_TASK);
break;
case PhabricatorSearchScope::SCOPE_WIKI:
$query->setParameter(
'type',
PhabricatorPHIDConstants::PHID_TYPE_WIKI);
break;
case PhabricatorSearchScope::SCOPE_COMMITS:
$query->setParameter(
'type',
PhabricatorPHIDConstants::PHID_TYPE_CMIT);
break;
default:
break;
}
} else {
if (strlen($request->getStr('type'))) {
$query->setParameter('type', $request->getStr('type'));
}
if ($request->getArr('author')) {
$query->setParameter('author', $request->getArr('author'));
}
if ($request->getArr('owner')) {
$query->setParameter('owner', $request->getArr('owner'));
}
if ($request->getInt('open')) {
$query->setParameter('open', $request->getInt('open'));
}
if ($request->getArr('project')) {
$query->setParameter('project', $request->getArr('project'));
}
}
$query->save();
return id(new AphrontRedirectResponse())
->setURI('/search/'.$query->getQueryKey().'/');
}
}
}
$options = array(
'' => 'All Documents',
) + PhabricatorSearchAbstractDocument::getSupportedTypes();
$status_options = array(
0 => 'Open and Closed Documents',
1 => 'Open Documents',
);
$phids = array_merge(
$query->getParameter('author', array()),
$query->getParameter('owner', array()),
$query->getParameter('project', array())
);
$handles = $this->loadViewerHandles($phids);
$author_value = array_select_keys(
$handles,
$query->getParameter('author', array()));
$author_value = mpull($author_value, 'getFullName', 'getPHID');
$owner_value = array_select_keys(
$handles,
$query->getParameter('owner', array()));
$owner_value = mpull($owner_value, 'getFullName', 'getPHID');
$project_value = array_select_keys(
$handles,
$query->getParameter('project', array()));
$project_value = mpull($project_value, 'getFullName', 'getPHID');
$search_form = new AphrontFormView();
$search_form
->setUser($user)
->setAction('/search/')
->appendChild(
phutil_render_tag(
'input',
array(
'type' => 'hidden',
'name' => 'jump',
'value' => 'no',
)))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Search')
->setName('query')
->setValue($query->getQuery()))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Document Type')
->setName('type')
->setOptions($options)
->setValue($query->getParameter('type')))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Document Status')
->setName('open')
->setOptions($status_options)
->setValue($query->getParameter('open')))
->appendChild(
id(new AphrontFormTokenizerControl())
->setName('author')
->setLabel('Author')
->setDatasource('/typeahead/common/users/')
->setValue($author_value))
->appendChild(
id(new AphrontFormTokenizerControl())
->setName('owner')
->setLabel('Owner')
->setDatasource('/typeahead/common/searchowner/')
->setValue($owner_value)
->setCaption(
'Tip: search for "Up For Grabs" to find unowned documents.'))
->appendChild(
id(new AphrontFormTokenizerControl())
->setName('project')
->setLabel('Project')
->setDatasource('/typeahead/common/projects/')
->setValue($project_value))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Search'));
$search_panel = new AphrontPanelView();
$search_panel->setHeader('Search Phabricator');
$search_panel->appendChild($search_form);
require_celerity_resource('phabricator-search-results-css');
if ($query->getID()) {
$limit = 20;
$pager = new AphrontPagerView();
$pager->setURI($request->getRequestURI(), 'page');
$pager->setPageSize($limit);
$pager->setOffset($request->getInt('page'));
$query->setParameter('limit', $limit + 1);
$query->setParameter('offset', $pager->getOffset());
$engine = PhabricatorSearchEngineSelector::newSelector()->newEngine();
$results = $engine->executeSearch($query);
$results = $pager->sliceResults($results);
if (!$request->getInt('page')) {
$jump = PhabricatorPHID::fromObjectName($query->getQuery());
if ($jump) {
array_unshift($results, $jump);
}
}
if ($results) {
$loader = new PhabricatorObjectHandleData($results);
$handles = $loader->loadHandles();
$objects = $loader->loadObjects();
$results = array();
foreach ($handles as $phid => $handle) {
$view = new PhabricatorSearchResultView();
$view->setHandle($handle);
$view->setQuery($query);
$view->setObject(idx($objects, $phid));
$results[] = $view->render();
}
$results =
'<div class="phabricator-search-result-list">'.
implode("\n", $results).
'<div class="search-results-pager">'.
$pager->render().
'</div>'.
'</div>';
} else {
$results =
'<div class="phabricator-search-result-list">'.
'<p class="phabricator-search-no-results">No search results.</p>'.
'</div>';
}
} else {
$results = null;
}
return $this->buildStandardPageResponse(
array(
$search_panel,
$results,
),
array(
'title' => 'Search Results',
));
}
}
diff --git a/src/applications/search/controller/PhabricatorSearchIndexController.php b/src/applications/search/controller/PhabricatorSearchIndexController.php
index 509ba6600f..c73928cb65 100644
--- a/src/applications/search/controller/PhabricatorSearchIndexController.php
+++ b/src/applications/search/controller/PhabricatorSearchIndexController.php
@@ -1,145 +1,129 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group search
*/
final class PhabricatorSearchIndexController
extends PhabricatorSearchBaseController {
private $phid;
public function shouldRequireAdmin() {
// This basically shows you all the text of any object in the system, so
// make it admin-only.
return true;
}
public function willProcessRequest(array $data) {
$this->phid = $data['phid'];
}
public function processRequest() {
$engine = PhabricatorSearchEngineSelector::newSelector()->newEngine();
$document = $engine->reconstructDocument($this->phid);
if (!$document) {
return new Aphront404Response();
}
$panels = array();
$panel = new AphrontPanelView();
$panel->setHeader('Abstract Document Index');
$props = array(
'PHID' => phutil_escape_html($document->getPHID()),
'Title' => phutil_escape_html($document->getDocumentTitle()),
'Type' => phutil_escape_html($document->getDocumentType()),
);
$rows = array();
foreach ($props as $name => $value) {
$rows[] = array($name, $value);
}
$table = new AphrontTableView($rows);
$table->setColumnClasses(
array(
'header',
'',
));
$panel->appendChild($table);
$panels[] = $panel;
$panel = new AphrontPanelView();
$panel->setHeader('Document Fields');
$fields = $document->getFieldData();
$rows = array();
foreach ($fields as $field) {
list($name, $corpus, $aux_phid) = $field;
$rows[] = array(
phutil_escape_html($name),
phutil_escape_html(nonempty($aux_phid, null)),
str_replace("\n", '<br />', phutil_escape_html($corpus)),
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Field',
'Aux PHID',
'Corpus',
));
$table->setColumnClasses(
array(
'',
'',
'wide',
));
$panel->appendChild($table);
$panels[] = $panel;
$panel = new AphrontPanelView();
$panel->setHeader('Document Relationships');
$relationships = $document->getRelationshipData();
$phids = ipull($relationships, 1);
$handles = $this->loadViewerHandles($phids);
$rows = array();
foreach ($relationships as $relationship) {
list($type, $phid, $rtype, $time) = $relationship;
$rows[] = array(
phutil_escape_html($type),
phutil_escape_html($phid),
phutil_escape_html($rtype),
$handles[$phid]->renderLink(),
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Relationship',
'Related PHID',
'Related Type',
'Related Handle',
));
$table->setColumnClasses(
array(
'',
'',
'',
'wide',
));
$panel->appendChild($table);
$panels[] = $panel;
return $this->buildStandardPageResponse(
$panels,
array(
'title' => 'Raw Index',
));
}
}
diff --git a/src/applications/search/controller/PhabricatorSearchSelectController.php b/src/applications/search/controller/PhabricatorSearchSelectController.php
index 64a70e2895..c696dc045e 100644
--- a/src/applications/search/controller/PhabricatorSearchSelectController.php
+++ b/src/applications/search/controller/PhabricatorSearchSelectController.php
@@ -1,121 +1,105 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group search
*/
final class PhabricatorSearchSelectController
extends PhabricatorSearchBaseController {
private $type;
public function willProcessRequest(array $data) {
$this->type = $data['type'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$query = new PhabricatorSearchQuery();
$query_str = $request->getStr('query');
$matches = array();
$query->setQuery($query_str);
$query->setParameter('type', $this->type);
switch ($request->getStr('filter')) {
case 'assigned':
$query->setParameter('owner', array($user->getPHID()));
$query->setParameter('open', 1);
break;
case 'created';
$query->setParameter('author', array($user->getPHID()));
$query->setParameter('open', 1);
break;
case 'open':
$query->setParameter('open', 1);
break;
}
$query->setParameter('exclude', $request->getStr('exclude'));
$query->setParameter('limit', 100);
$engine = PhabricatorSearchEngineSelector::newSelector()->newEngine();
$results = $engine->executeSearch($query);
$phids = array_fill_keys($results, true);
$phids += $this->queryObjectNames($query_str);
$phids = array_keys($phids);
$handles = $this->loadViewerHandles($phids);
$data = array();
foreach ($handles as $handle) {
$view = new PhabricatorHandleObjectSelectorDataView($handle);
$data[] = $view->renderData();
}
return id(new AphrontAjaxResponse())->setContent($data);
}
private function queryObjectNames($query) {
$pattern = null;
switch ($this->type) {
case PhabricatorPHIDConstants::PHID_TYPE_TASK:
$pattern = '/\bT(\d+)\b/i';
break;
case PhabricatorPHIDConstants::PHID_TYPE_DREV:
$pattern = '/\bD(\d+)\b/i';
break;
}
if (!$pattern) {
return array();
}
$matches = array();
preg_match_all($pattern, $query, $matches);
if (!$matches) {
return array();
}
$object_ids = $matches[1];
if (!$object_ids) {
return array();
}
switch ($this->type) {
case PhabricatorPHIDConstants::PHID_TYPE_DREV:
$objects = id(new DifferentialRevision())->loadAllWhere(
'id IN (%Ld)',
$object_ids);
break;
case PhabricatorPHIDConstants::PHID_TYPE_TASK:
$objects = id(new ManiphestTask())->loadAllWhere(
'id IN (%Ld)',
$object_ids);
break;
}
return array_fill_keys(mpull($objects, 'getPHID'), true);
}
}
diff --git a/src/applications/search/engine/PhabricatorJumpNavHandler.php b/src/applications/search/engine/PhabricatorJumpNavHandler.php
index 8daa95b03e..ab0859609e 100644
--- a/src/applications/search/engine/PhabricatorJumpNavHandler.php
+++ b/src/applications/search/engine/PhabricatorJumpNavHandler.php
@@ -1,127 +1,111 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorJumpNavHandler {
public static function jumpPostResponse($jump) {
$jump = trim($jump);
$help_href = PhabricatorEnv::getDocLink(
'article/Jump_Nav_User_Guide.html');
$patterns = array(
'/^help/i' => 'uri:'.$help_href,
'/^a$/i' => 'uri:/audit/',
'/^f$/i' => 'uri:/feed/',
'/^d$/i' => 'uri:/differential/',
'/^r$/i' => 'uri:/diffusion/',
'/^t$/i' => 'uri:/maniphest/',
'/^p$/i' => 'uri:/project/',
'/^u$/i' => 'uri:/people/',
'/^r([A-Z]+)$/' => 'repository',
'/^r([A-Z]+)(\S+)$/' => 'commit',
'/^d(\d+)$/i' => 'revision',
'/^t(\d+)$/i' => 'task',
'/^p\s+(.+)$/i' => 'project',
'/^u\s+(\S+)$/i' => 'user',
'/^task:\s*(.+)/i' => 'create-task',
'/^(?:s|symbol)\s+(\S+)/i' => 'find-symbol',
);
foreach ($patterns as $pattern => $effect) {
$matches = null;
if (preg_match($pattern, $jump, $matches)) {
if (!strncmp($effect, 'uri:', 4)) {
return id(new AphrontRedirectResponse())
->setURI(substr($effect, 4));
} else {
switch ($effect) {
case 'repository':
return id(new AphrontRedirectResponse())
->setURI('/diffusion/'.$matches[1].'/');
case 'commit':
return id(new AphrontRedirectResponse())
->setURI('/'.$matches[0]);
case 'revision':
return id(new AphrontRedirectResponse())
->setURI('/D'.$matches[1]);
case 'task':
return id(new AphrontRedirectResponse())
->setURI('/T'.$matches[1]);
case 'user':
return id(new AphrontRedirectResponse())
->setURI('/p/'.$matches[1].'/');
case 'project':
$project = self::findCloselyNamedProject($matches[1]);
if ($project) {
return id(new AphrontRedirectResponse())
->setURI('/project/view/'.$project->getID().'/');
} else {
$jump = $matches[1];
}
break;
case 'find-symbol':
$context = '';
$symbol = $matches[1];
$parts = array();
if (preg_match('/(.*)(?:\\.|::|->)(.*)/', $symbol, $parts)) {
$context = '&context='.phutil_escape_uri($parts[1]);
$symbol = $parts[2];
}
return id(new AphrontRedirectResponse())
->setURI("/diffusion/symbol/$symbol/?jump=true$context");
case 'create-task':
return id(new AphrontRedirectResponse())
->setURI('/maniphest/task/create/?title='
.phutil_escape_uri($matches[1]));
default:
throw new Exception("Unknown jump effect '{$effect}'!");
}
}
}
}
return null;
}
private static function findCloselyNamedProject($name) {
$project = id(new PhabricatorProject())->loadOneWhere(
'name = %s',
$name);
if ($project) {
return $project;
} else { // no exact match, try a fuzzy match
$projects = id(new PhabricatorProject())->loadAllWhere(
'name LIKE %~',
$name);
if ($projects) {
$min_name_length = PHP_INT_MAX;
$best_project = null;
foreach ($projects as $project) {
$name_length = strlen($project->getName());
if ($name_length <= $min_name_length) {
$min_name_length = $name_length;
$best_project = $project;
}
}
return $best_project;
} else {
return null;
}
}
}
}
diff --git a/src/applications/search/engine/PhabricatorSearchEngine.php b/src/applications/search/engine/PhabricatorSearchEngine.php
index 57150e8b23..48b765a9d3 100644
--- a/src/applications/search/engine/PhabricatorSearchEngine.php
+++ b/src/applications/search/engine/PhabricatorSearchEngine.php
@@ -1,57 +1,41 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Base class for Phabricator search engine providers. Each engine must offer
* three capabilities: indexing, searching, and reconstruction (this can be
* stubbed out if an engine can't reasonably do it, it is used for debugging).
*
* @group search
*/
abstract class PhabricatorSearchEngine {
/**
* Update the index for an abstract document.
*
* @param PhabricatorSearchAbstractDocument Document to update.
* @return void
*/
abstract public function reindexAbstractDocument(
PhabricatorSearchAbstractDocument $document);
/**
* Reconstruct the document for a given PHID. This is used for debugging
* and does not need to be perfect if it is unreasonable to implement it.
*
* @param phid Document PHID to reconstruct.
* @return PhabricatorSearchAbstractDocument Abstract document.
*/
abstract public function reconstructDocument($phid);
/**
* Execute a search query.
*
* @param PhabricatorSearchQuery A query to execute.
* @return list A list of matching PHIDs.
*/
abstract public function executeSearch(PhabricatorSearchQuery $query);
}
diff --git a/src/applications/search/engine/PhabricatorSearchEngineElastic.php b/src/applications/search/engine/PhabricatorSearchEngineElastic.php
index beada59da5..ae3ae91515 100644
--- a/src/applications/search/engine/PhabricatorSearchEngineElastic.php
+++ b/src/applications/search/engine/PhabricatorSearchEngineElastic.php
@@ -1,252 +1,236 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group search
*/
final class PhabricatorSearchEngineElastic extends PhabricatorSearchEngine {
private $uri;
private $timeout;
public function __construct($uri) {
$this->uri = $uri;
}
public function setTimeout($timeout) {
$this->timeout = $timeout;
return $this;
}
public function getTimeout() {
return $this->timeout;
}
public function reindexAbstractDocument(
PhabricatorSearchAbstractDocument $doc) {
$type = $doc->getDocumentType();
$phid = $doc->getPHID();
$handle = PhabricatorObjectHandleData::loadOneHandle($phid);
// URL is not used internally but it can be useful externally.
$spec = array(
'title' => $doc->getDocumentTitle(),
'url' => PhabricatorEnv::getProductionURI($handle->getURI()),
'dateCreated' => $doc->getDocumentCreated(),
'_timestamp' => $doc->getDocumentModified(),
'field' => array(),
'relationship' => array(),
);
foreach ($doc->getFieldData() as $field) {
$spec['field'][] = array_combine(array('type', 'corpus', 'aux'), $field);
}
foreach ($doc->getRelationshipData() as $relationship) {
list($rtype, $to_phid, $to_type, $time) = $relationship;
$spec['relationship'][$rtype][] = array(
'phid' => $to_phid,
'phidType' => $to_type,
'when' => $time,
);
}
$this->executeRequest(
"/phabricator/{$type}/{$phid}/",
$spec,
$is_write = true);
}
public function reconstructDocument($phid) {
$type = phid_get_type($phid);
$response = $this->executeRequest("/phabricator/{$type}/{$phid}", array());
if (empty($response['exists'])) {
return null;
}
$hit = $response['_source'];
$doc = new PhabricatorSearchAbstractDocument();
$doc->setPHID($phid);
$doc->setDocumentType($response['_type']);
$doc->setDocumentTitle($hit['title']);
$doc->setDocumentCreated($hit['dateCreated']);
$doc->setDocumentModified($hit['_timestamp']);
foreach ($hit['field'] as $fdef) {
$doc->addField($fdef['type'], $fdef['corpus'], $fdef['aux']);
}
foreach ($hit['relationship'] as $rtype => $rships) {
foreach ($rships as $rship) {
$doc->addRelationship(
$rtype,
$rship['phid'],
$rship['phidType'],
$rship['when']);
}
}
return $doc;
}
private function buildSpec(PhabricatorSearchQuery $query) {
$spec = array();
$filter = array();
if ($query->getQuery()) {
$spec[] = array(
'field' => array(
'field.corpus' => $query->getQuery(),
),
);
}
$exclude = $query->getParameter('exclude');
if ($exclude) {
$filter[] = array(
'not' => array(
'ids' => array(
'values' => array($exclude),
),
),
);
}
$rel_mapping = array(
'author' => PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR,
'open' => PhabricatorSearchRelationship::RELATIONSHIP_OPEN,
'owner' => PhabricatorSearchRelationship::RELATIONSHIP_OWNER,
'project' => PhabricatorSearchRelationship::RELATIONSHIP_PROJECT,
'repository' => PhabricatorSearchRelationship::RELATIONSHIP_REPOSITORY,
);
foreach ($rel_mapping as $name => $field) {
$param = $query->getParameter($name);
if (is_array($param)) {
$should = array();
foreach ($param as $val) {
$should[] = array(
'text' => array(
"relationship.{$field}.phid" => array(
'query' => $val,
'type' => 'phrase',
),
),
);
}
// We couldn't solve it by minimum_number_should_match because it can
// match multiple owners without matching author.
$spec[] = array('bool' => array('should' => $should));
} else if ($param) {
$filter[] = array(
'exists' => array(
'field' => "relationship.{$field}.phid",
),
);
}
}
if ($spec) {
$spec = array('query' => array('bool' => array('must' => $spec)));
}
if ($filter) {
$filter = array('filter' => array('and' => $filter));
if ($spec) {
$spec = array(
'query' => array(
'filtered' => $spec + $filter,
),
);
} else {
$spec = $filter;
}
}
if (!$query->getQuery()) {
$spec['sort'] = array(
array('dateCreated' => 'desc'),
);
}
$spec['from'] = (int)$query->getParameter('offset', 0);
$spec['size'] = (int)$query->getParameter('limit', 25);
return $spec;
}
public function executeSearch(PhabricatorSearchQuery $query) {
$type = $query->getParameter('type');
if ($type) {
$uri = "/phabricator/{$type}/_search";
} else {
// Don't use '/phabricator/_search' for the case that there is something
// else in the index (for example if 'phabricator' is only an alias to
// some bigger index).
$types = PhabricatorSearchAbstractDocument::getSupportedTypes();
$uri = '/phabricator/' . implode(',', array_keys($types)) . '/_search';
}
try {
$response = $this->executeRequest($uri, $this->buildSpec($query));
} catch (HTTPFutureResponseStatusHTTP $ex) {
// elasticsearch probably uses Lucene query syntax:
// http://lucene.apache.org/core/3_6_1/queryparsersyntax.html
// Try literal search if operator search fails.
if (!$query->getQuery()) {
throw $ex;
}
$query = clone $query;
$query->setQuery(addcslashes($query->getQuery(), '+-&|!(){}[]^"~*?:\\'));
$response = $this->executeRequest($uri, $this->buildSpec($query));
}
$phids = ipull($response['hits']['hits'], '_id');
return $phids;
}
private function executeRequest($path, array $data, $is_write = false) {
$uri = new PhutilURI($this->uri);
$data = json_encode($data);
$uri->setPath($path);
$future = new HTTPSFuture($uri, $data);
if ($is_write) {
$future->setMethod('PUT');
}
if ($this->getTimeout()) {
$future->setTimeout($this->getTimeout());
}
list($body) = $future->resolvex();
if ($is_write) {
return null;
}
$body = json_decode($body, true);
if (!is_array($body)) {
throw new Exception("elasticsearch server returned invalid JSON!");
}
return $body;
}
}
diff --git a/src/applications/search/engine/PhabricatorSearchEngineMySQL.php b/src/applications/search/engine/PhabricatorSearchEngineMySQL.php
index 9a5ff7c425..28e90ab1d9 100644
--- a/src/applications/search/engine/PhabricatorSearchEngineMySQL.php
+++ b/src/applications/search/engine/PhabricatorSearchEngineMySQL.php
@@ -1,330 +1,314 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group search
*/
final class PhabricatorSearchEngineMySQL extends PhabricatorSearchEngine {
public function reindexAbstractDocument(
PhabricatorSearchAbstractDocument $doc) {
$phid = $doc->getPHID();
if (!$phid) {
throw new Exception("Document has no PHID!");
}
$store = new PhabricatorSearchDocument();
$store->setPHID($doc->getPHID());
$store->setDocumentType($doc->getDocumentType());
$store->setDocumentTitle($doc->getDocumentTitle());
$store->setDocumentCreated($doc->getDocumentCreated());
$store->setDocumentModified($doc->getDocumentModified());
$store->replace();
$conn_w = $store->establishConnection('w');
$field_dao = new PhabricatorSearchDocumentField();
queryfx(
$conn_w,
'DELETE FROM %T WHERE phid = %s',
$field_dao->getTableName(),
$phid);
foreach ($doc->getFieldData() as $field) {
list($ftype, $corpus, $aux_phid) = $field;
queryfx(
$conn_w,
'INSERT INTO %T (phid, phidType, field, auxPHID, corpus) '.
' VALUES (%s, %s, %s, %ns, %s)',
$field_dao->getTableName(),
$phid,
$doc->getDocumentType(),
$ftype,
$aux_phid,
$corpus);
}
$sql = array();
foreach ($doc->getRelationshipData() as $relationship) {
list($rtype, $to_phid, $to_type, $time) = $relationship;
$sql[] = qsprintf(
$conn_w,
'(%s, %s, %s, %s, %d)',
$phid,
$to_phid,
$rtype,
$to_type,
$time);
}
$rship_dao = new PhabricatorSearchDocumentRelationship();
queryfx(
$conn_w,
'DELETE FROM %T WHERE phid = %s',
$rship_dao->getTableName(),
$phid);
if ($sql) {
queryfx(
$conn_w,
'INSERT INTO %T'.
' (phid, relatedPHID, relation, relatedType, relatedTime) '.
' VALUES %Q',
$rship_dao->getTableName(),
implode(', ', $sql));
}
}
/**
* Rebuild the PhabricatorSearchAbstractDocument that was used to index
* an object out of the index itself. This is primarily useful for debugging,
* as it allows you to inspect the search index representation of a
* document.
*
* @param phid PHID of a document which exists in the search index.
* @return null|PhabricatorSearchAbstractDocument Abstract document object
* which corresponds to the original abstract document used to
* build the document index.
*/
public function reconstructDocument($phid) {
$dao_doc = new PhabricatorSearchDocument();
$dao_field = new PhabricatorSearchDocumentField();
$dao_relationship = new PhabricatorSearchDocumentRelationship();
$t_doc = $dao_doc->getTableName();
$t_field = $dao_field->getTableName();
$t_relationship = $dao_relationship->getTableName();
$doc = queryfx_one(
$dao_doc->establishConnection('r'),
'SELECT * FROM %T WHERE phid = %s',
$t_doc,
$phid);
if (!$doc) {
return null;
}
$fields = queryfx_all(
$dao_field->establishConnection('r'),
'SELECT * FROM %T WHERE phid = %s',
$t_field,
$phid);
$relationships = queryfx_all(
$dao_relationship->establishConnection('r'),
'SELECT * FROM %T WHERE phid = %s',
$t_relationship,
$phid);
$adoc = id(new PhabricatorSearchAbstractDocument())
->setPHID($phid)
->setDocumentType($doc['documentType'])
->setDocumentTitle($doc['documentTitle'])
->setDocumentCreated($doc['documentCreated'])
->setDocumentModified($doc['documentModified']);
foreach ($fields as $field) {
$adoc->addField(
$field['field'],
$field['corpus'],
$field['auxPHID']);
}
foreach ($relationships as $relationship) {
$adoc->addRelationship(
$relationship['relation'],
$relationship['relatedPHID'],
$relationship['relatedType'],
$relationship['relatedTime']);
}
return $adoc;
}
public function executeSearch(PhabricatorSearchQuery $query) {
$where = array();
$join = array();
$order = 'ORDER BY documentCreated DESC';
$dao_doc = new PhabricatorSearchDocument();
$dao_field = new PhabricatorSearchDocumentField();
$t_doc = $dao_doc->getTableName();
$t_field = $dao_field->getTableName();
$conn_r = $dao_doc->establishConnection('r');
$q = $query->getQuery();
if (strlen($q)) {
$join[] = qsprintf(
$conn_r,
"{$t_field} field ON field.phid = document.phid");
$where[] = qsprintf(
$conn_r,
'MATCH(corpus) AGAINST (%s IN BOOLEAN MODE)',
$q);
// When searching for a string, promote user listings above other
// listings.
$order = qsprintf(
$conn_r,
'ORDER BY
IF(documentType = %s, 0, 1) ASC,
MAX(MATCH(corpus) AGAINST (%s)) DESC',
'USER',
$q);
$field = $query->getParameter('field');
if ($field/* && $field != AdjutantQuery::FIELD_ALL*/) {
$where[] = qsprintf(
$conn_r,
'field.field = %s',
$field);
}
}
$exclude = $query->getParameter('exclude');
if ($exclude) {
$where[] = qsprintf($conn_r, 'document.phid != %s', $exclude);
}
if ($query->getParameter('type')) {
if (strlen($q)) {
// TODO: verify that this column actually does something useful in query
// plans once we have nontrivial amounts of data.
$where[] = qsprintf(
$conn_r,
'field.phidType = %s',
$query->getParameter('type'));
}
$where[] = qsprintf(
$conn_r,
'document.documentType = %s',
$query->getParameter('type'));
}
$join[] = $this->joinRelationship(
$conn_r,
$query,
'author',
PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR);
$join[] = $this->joinRelationship(
$conn_r,
$query,
'open',
PhabricatorSearchRelationship::RELATIONSHIP_OPEN);
$join[] = $this->joinRelationship(
$conn_r,
$query,
'owner',
PhabricatorSearchRelationship::RELATIONSHIP_OWNER);
$join[] = $this->joinRelationship(
$conn_r,
$query,
'project',
PhabricatorSearchRelationship::RELATIONSHIP_PROJECT);
$join[] = $this->joinRelationship(
$conn_r,
$query,
'repository',
PhabricatorSearchRelationship::RELATIONSHIP_REPOSITORY);
$join = array_filter($join);
foreach ($join as $key => $clause) {
$join[$key] = ' JOIN '.$clause;
}
$join = implode(' ', $join);
if ($where) {
$where = 'WHERE '.implode(' AND ', $where);
} else {
$where = '';
}
$offset = (int)$query->getParameter('offset', 0);
$limit = (int)$query->getParameter('limit', 25);
$hits = queryfx_all(
$conn_r,
'SELECT
document.phid
FROM %T document
%Q
%Q
GROUP BY document.phid
%Q
LIMIT %d, %d',
$t_doc,
$join,
$where,
$order,
$offset,
$limit);
return ipull($hits, 'phid');
}
protected function joinRelationship(
AphrontDatabaseConnection $conn,
PhabricatorSearchQuery $query,
$field,
$type) {
$phids = $query->getParameter($field, array());
if (!$phids) {
return null;
}
$is_existence = false;
switch ($type) {
case PhabricatorSearchRelationship::RELATIONSHIP_OPEN:
$is_existence = true;
break;
}
$sql = qsprintf(
$conn,
'%T AS %C ON %C.phid = document.phid AND %C.relation = %s',
id(new PhabricatorSearchDocumentRelationship())->getTableName(),
$field,
$field,
$field,
$type);
if (!$is_existence) {
$sql .= qsprintf(
$conn,
' AND %C.relatedPHID in (%Ls)',
$field,
$phids);
}
return $sql;
}
}
diff --git a/src/applications/search/index/PhabricatorSearchAbstractDocument.php b/src/applications/search/index/PhabricatorSearchAbstractDocument.php
index b8ea482d04..85da62f1ae 100644
--- a/src/applications/search/index/PhabricatorSearchAbstractDocument.php
+++ b/src/applications/search/index/PhabricatorSearchAbstractDocument.php
@@ -1,107 +1,91 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group search
*/
final class PhabricatorSearchAbstractDocument {
private $phid;
private $documentType;
private $documentTitle;
private $documentCreated;
private $documentModified;
private $fields = array();
private $relationships = array();
public static function getSupportedTypes() {
$more = PhabricatorEnv::getEnvConfig('search.more-document-types', array());
return array(
PhabricatorPHIDConstants::PHID_TYPE_DREV => 'Differential Revisions',
PhabricatorPHIDConstants::PHID_TYPE_CMIT => 'Repository Commits',
PhabricatorPHIDConstants::PHID_TYPE_TASK => 'Maniphest Tasks',
PhabricatorPHIDConstants::PHID_TYPE_WIKI => 'Phriction Documents',
PhabricatorPHIDConstants::PHID_TYPE_USER => 'Phabricator Users',
PhabricatorPHIDConstants::PHID_TYPE_QUES => 'Ponder Questions',
) + $more;
}
public function setPHID($phid) {
$this->phid = $phid;
return $this;
}
public function setDocumentType($document_type) {
$this->documentType = $document_type;
return $this;
}
public function setDocumentTitle($title) {
$this->documentTitle = $title;
$this->addField(PhabricatorSearchField::FIELD_TITLE, $title);
return $this;
}
public function addField($field, $corpus, $aux_phid = null) {
$this->fields[] = array($field, $corpus, $aux_phid);
return $this;
}
public function addRelationship($type, $related_phid, $rtype, $time) {
$this->relationships[] = array($type, $related_phid, $rtype, $time);
return $this;
}
public function setDocumentCreated($date) {
$this->documentCreated = $date;
return $this;
}
public function setDocumentModified($date) {
$this->documentModified = $date;
return $this;
}
public function getPHID() {
return $this->phid;
}
public function getDocumentType() {
return $this->documentType;
}
public function getDocumentTitle() {
return $this->documentTitle;
}
public function getDocumentCreated() {
return $this->documentCreated;
}
public function getDocumentModified() {
return $this->documentModified;
}
public function getFieldData() {
return $this->fields;
}
public function getRelationshipData() {
return $this->relationships;
}
}
diff --git a/src/applications/search/index/indexer/PhabricatorSearchCommitIndexer.php b/src/applications/search/index/indexer/PhabricatorSearchCommitIndexer.php
index 1c09563499..f4f50093d1 100644
--- a/src/applications/search/index/indexer/PhabricatorSearchCommitIndexer.php
+++ b/src/applications/search/index/indexer/PhabricatorSearchCommitIndexer.php
@@ -1,97 +1,81 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group search
*/
final class PhabricatorSearchCommitIndexer
extends PhabricatorSearchDocumentIndexer {
public static function indexCommit(PhabricatorRepositoryCommit $commit) {
$commit_data = id(new PhabricatorRepositoryCommitData())->loadOneWhere(
'commitID = %d',
$commit->getID());
$date_created = $commit->getEpoch();
$commit_message = $commit_data->getCommitMessage();
$author_phid = $commit_data->getCommitDetail('authorPHID');
$repository = id(new PhabricatorRepository())->loadOneWhere(
'id = %d',
$commit->getRepositoryID());
if (!$repository) {
return;
}
$title = 'r'.$repository->getCallsign().$commit->getCommitIdentifier().
" ".$commit_data->getSummary();
$doc = new PhabricatorSearchAbstractDocument();
$doc->setPHID($commit->getPHID());
$doc->setDocumentType(PhabricatorPHIDConstants::PHID_TYPE_CMIT);
$doc->setDocumentCreated($date_created);
$doc->setDocumentModified($date_created);
$doc->setDocumentTitle($title);
$doc->addField(
PhabricatorSearchField::FIELD_BODY,
$commit_message);
if ($author_phid) {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR,
$author_phid,
PhabricatorPHIDConstants::PHID_TYPE_USER,
$date_created);
}
$project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$commit->getPHID(),
PhabricatorEdgeConfig::TYPE_COMMIT_HAS_PROJECT
);
if ($project_phids) {
foreach ($project_phids as $project_phid) {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_PROJECT,
$project_phid,
PhabricatorPHIDConstants::PHID_TYPE_PROJ,
$date_created);
}
}
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_REPOSITORY,
$repository->getPHID(),
PhabricatorPHIDConstants::PHID_TYPE_REPO,
$date_created);
$comments = id(new PhabricatorAuditComment())->loadAllWhere(
'targetPHID = %s',
$commit->getPHID());
foreach ($comments as $comment) {
if (strlen($comment->getContent())) {
$doc->addField(
PhabricatorSearchField::FIELD_COMMENT,
$comment->getContent());
}
}
self::reindexAbstractDocument($doc);
}
}
diff --git a/src/applications/search/index/indexer/PhabricatorSearchDifferentialIndexer.php b/src/applications/search/index/indexer/PhabricatorSearchDifferentialIndexer.php
index b621acbe21..a12b29067a 100644
--- a/src/applications/search/index/indexer/PhabricatorSearchDifferentialIndexer.php
+++ b/src/applications/search/index/indexer/PhabricatorSearchDifferentialIndexer.php
@@ -1,118 +1,102 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group search
*/
final class PhabricatorSearchDifferentialIndexer
extends PhabricatorSearchDocumentIndexer {
public static function indexRevision(DifferentialRevision $rev) {
$doc = new PhabricatorSearchAbstractDocument();
$doc->setPHID($rev->getPHID());
$doc->setDocumentType(PhabricatorPHIDConstants::PHID_TYPE_DREV);
$doc->setDocumentTitle($rev->getTitle());
$doc->setDocumentCreated($rev->getDateCreated());
$doc->setDocumentModified($rev->getDateModified());
$doc->addField(
PhabricatorSearchField::FIELD_BODY,
$rev->getSummary());
$doc->addField(
PhabricatorSearchField::FIELD_TEST_PLAN,
$rev->getTestPlan());
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR,
$rev->getAuthorPHID(),
PhabricatorPHIDConstants::PHID_TYPE_USER,
$rev->getDateCreated());
if ($rev->getStatus() != ArcanistDifferentialRevisionStatus::CLOSED &&
$rev->getStatus() != ArcanistDifferentialRevisionStatus::ABANDONED) {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_OPEN,
$rev->getPHID(),
PhabricatorPHIDConstants::PHID_TYPE_DREV,
time());
}
$comments = $rev->loadRelatives(new DifferentialComment(), 'revisionID');
$inlines = $rev->loadRelatives(
new DifferentialInlineComment(),
'revisionID',
'getID',
'(commentID IS NOT NULL)');
$touches = array();
foreach (array_merge($comments, $inlines) as $comment) {
if (strlen($comment->getContent())) {
$doc->addField(
PhabricatorSearchField::FIELD_COMMENT,
$comment->getContent());
}
$author = $comment->getAuthorPHID();
$touches[$author] = $comment->getDateCreated();
}
foreach ($touches as $touch => $time) {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_TOUCH,
$touch,
PhabricatorPHIDConstants::PHID_TYPE_USER,
$time);
}
$rev->loadRelationships();
// If a revision needs review, the owners are the reviewers. Otherwise, the
// owner is the author (e.g., accepted, rejected, closed).
if ($rev->getStatus() == ArcanistDifferentialRevisionStatus::NEEDS_REVIEW) {
foreach ($rev->getReviewers() as $phid) {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_OWNER,
$phid,
PhabricatorPHIDConstants::PHID_TYPE_USER,
$rev->getDateModified()); // Bogus timestamp.
}
} else {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_OWNER,
$rev->getAuthorPHID(),
PhabricatorPHIDConstants::PHID_TYPE_USER,
$rev->getDateCreated());
}
$ccphids = $rev->getCCPHIDs();
$handles = id(new PhabricatorObjectHandleData($ccphids))
->loadHandles();
foreach ($handles as $phid => $handle) {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_SUBSCRIBER,
$phid,
$handle->getType(),
$rev->getDateModified()); // Bogus timestamp.
}
self::reindexAbstractDocument($doc);
}
}
diff --git a/src/applications/search/index/indexer/PhabricatorSearchDocumentIndexer.php b/src/applications/search/index/indexer/PhabricatorSearchDocumentIndexer.php
index 3d7dd778ba..b7ea31552c 100644
--- a/src/applications/search/index/indexer/PhabricatorSearchDocumentIndexer.php
+++ b/src/applications/search/index/indexer/PhabricatorSearchDocumentIndexer.php
@@ -1,37 +1,21 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group search
*/
abstract class PhabricatorSearchDocumentIndexer {
// TODO: Make this whole class tree concrete?
final protected static function reindexAbstractDocument(
PhabricatorSearchAbstractDocument $document) {
$engine = PhabricatorSearchEngineSelector::newSelector()->newEngine();
try {
$engine->reindexAbstractDocument($document);
} catch (Exception $ex) {
$phid = $document->getPHID();
$class = get_class($engine);
phlog("Unable to index document {$phid} by engine {$class}.");
}
}
}
diff --git a/src/applications/search/index/indexer/PhabricatorSearchManiphestIndexer.php b/src/applications/search/index/indexer/PhabricatorSearchManiphestIndexer.php
index af86c6374a..5d02f56223 100644
--- a/src/applications/search/index/indexer/PhabricatorSearchManiphestIndexer.php
+++ b/src/applications/search/index/indexer/PhabricatorSearchManiphestIndexer.php
@@ -1,135 +1,119 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group search
*/
final class PhabricatorSearchManiphestIndexer
extends PhabricatorSearchDocumentIndexer {
public static function indexTask(ManiphestTask $task) {
$doc = new PhabricatorSearchAbstractDocument();
$doc->setPHID($task->getPHID());
$doc->setDocumentType(PhabricatorPHIDConstants::PHID_TYPE_TASK);
$doc->setDocumentTitle($task->getTitle());
$doc->setDocumentCreated($task->getDateCreated());
$doc->setDocumentModified($task->getDateModified());
$doc->addField(
PhabricatorSearchField::FIELD_BODY,
$task->getDescription());
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR,
$task->getAuthorPHID(),
PhabricatorPHIDConstants::PHID_TYPE_USER,
$task->getDateCreated());
if ($task->getStatus() == ManiphestTaskStatus::STATUS_OPEN) {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_OPEN,
$task->getPHID(),
PhabricatorPHIDConstants::PHID_TYPE_TASK,
time());
}
$transactions = id(new ManiphestTransaction())->loadAllWhere(
'taskID = %d',
$task->getID());
$current_ccs = $task->getCCPHIDs();
$touches = array();
$owner = null;
$ccs = array();
foreach ($transactions as $transaction) {
if ($transaction->hasComments()) {
$doc->addField(
PhabricatorSearchField::FIELD_COMMENT,
$transaction->getComments());
}
$author = $transaction->getAuthorPHID();
// Record the most recent time they touched this object.
$touches[$author] = $transaction->getDateCreated();
switch ($transaction->getTransactionType()) {
case ManiphestTransactionType::TYPE_OWNER:
$owner = $transaction;
break;
case ManiphestTransactionType::TYPE_CCS:
// For users who are still CC'd, record the first time they were
// added to CC.
foreach ($transaction->getNewValue() as $added_cc) {
if (in_array($added_cc, $current_ccs)) {
if (empty($ccs[$added_cc])) {
$ccs[$added_cc] = $transaction->getDateCreated();
}
}
}
break;
}
}
foreach ($task->getProjectPHIDs() as $phid) {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_PROJECT,
$phid,
PhabricatorPHIDConstants::PHID_TYPE_PROJ,
$task->getDateModified()); // Bogus.
}
if ($owner && $owner->getNewValue()) {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_OWNER,
$owner->getNewValue(),
PhabricatorPHIDConstants::PHID_TYPE_USER,
$owner->getDateCreated());
} else {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_OWNER,
ManiphestTaskOwner::OWNER_UP_FOR_GRABS,
PhabricatorPHIDConstants::PHID_TYPE_MAGIC,
$owner
? $owner->getDateCreated()
: $task->getDateCreated());
}
foreach ($touches as $touch => $time) {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_TOUCH,
$touch,
PhabricatorPHIDConstants::PHID_TYPE_USER,
$time);
}
// We need to load handles here since non-users may subscribe (mailing
// lists, e.g.)
$handles = id(new PhabricatorObjectHandleData(array_keys($ccs)))
->loadHandles();
foreach ($ccs as $cc => $time) {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_SUBSCRIBER,
$handles[$cc]->getPHID(),
$handles[$cc]->getType(),
$time);
}
self::reindexAbstractDocument($doc);
}
}
diff --git a/src/applications/search/index/indexer/PhabricatorSearchPhrictionIndexer.php b/src/applications/search/index/indexer/PhabricatorSearchPhrictionIndexer.php
index c1e6ae9deb..808756091d 100644
--- a/src/applications/search/index/indexer/PhabricatorSearchPhrictionIndexer.php
+++ b/src/applications/search/index/indexer/PhabricatorSearchPhrictionIndexer.php
@@ -1,49 +1,33 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group search
*/
final class PhabricatorSearchPhrictionIndexer
extends PhabricatorSearchDocumentIndexer {
public static function indexDocument(PhrictionDocument $document) {
$content = $document->getContent();
$doc = new PhabricatorSearchAbstractDocument();
$doc->setPHID($document->getPHID());
$doc->setDocumentType(PhabricatorPHIDConstants::PHID_TYPE_WIKI);
$doc->setDocumentTitle($content->getTitle());
// TODO: This isn't precisely correct, denormalize into the Document table?
$doc->setDocumentCreated($content->getDateCreated());
$doc->setDocumentModified($content->getDateModified());
$doc->addField(
PhabricatorSearchField::FIELD_BODY,
$content->getContent());
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR,
$content->getAuthorPHID(),
PhabricatorPHIDConstants::PHID_TYPE_USER,
$content->getDateCreated());
self::reindexAbstractDocument($doc);
}
}
diff --git a/src/applications/search/index/indexer/PhabricatorSearchUserIndexer.php b/src/applications/search/index/indexer/PhabricatorSearchUserIndexer.php
index 27e3f57383..18c787d4d0 100644
--- a/src/applications/search/index/indexer/PhabricatorSearchUserIndexer.php
+++ b/src/applications/search/index/indexer/PhabricatorSearchUserIndexer.php
@@ -1,46 +1,30 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group search
*/
final class PhabricatorSearchUserIndexer
extends PhabricatorSearchDocumentIndexer {
public static function indexUser(PhabricatorUser $user) {
$doc = new PhabricatorSearchAbstractDocument();
$doc->setPHID($user->getPHID());
$doc->setDocumentType(PhabricatorPHIDConstants::PHID_TYPE_USER);
$doc->setDocumentTitle($user->getUserName().' ('.$user->getRealName().')');
$doc->setDocumentCreated($user->getDateCreated());
$doc->setDocumentModified($user->getDateModified());
// TODO: Index the blurbs from their profile or something? Probably not
// actually useful...
if (!$user->getIsDisabled()) {
$doc->addRelationship(
PhabricatorSearchRelationship::RELATIONSHIP_OPEN,
$user->getPHID(),
PhabricatorPHIDConstants::PHID_TYPE_USER,
time());
}
self::reindexAbstractDocument($doc);
}
}
diff --git a/src/applications/search/selector/PhabricatorDefaultSearchEngineSelector.php b/src/applications/search/selector/PhabricatorDefaultSearchEngineSelector.php
index 8e2a214982..ff14ebb859 100644
--- a/src/applications/search/selector/PhabricatorDefaultSearchEngineSelector.php
+++ b/src/applications/search/selector/PhabricatorDefaultSearchEngineSelector.php
@@ -1,32 +1,16 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group search
*/
final class PhabricatorDefaultSearchEngineSelector
extends PhabricatorSearchEngineSelector {
public function newEngine() {
$elastic_host = PhabricatorEnv::getEnvConfig('search.elastic.host');
if ($elastic_host) {
return new PhabricatorSearchEngineElastic($elastic_host);
}
return new PhabricatorSearchEngineMySQL();
}
}
diff --git a/src/applications/search/selector/PhabricatorSearchEngineSelector.php b/src/applications/search/selector/PhabricatorSearchEngineSelector.php
index b1aeee7457..376f99982a 100644
--- a/src/applications/search/selector/PhabricatorSearchEngineSelector.php
+++ b/src/applications/search/selector/PhabricatorSearchEngineSelector.php
@@ -1,34 +1,18 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group search
*/
abstract class PhabricatorSearchEngineSelector {
final public function __construct() {
// <empty>
}
abstract public function newEngine();
final public static function newSelector() {
return PhabricatorEnv::newObjectFromConfig('search.engine-selector');
}
}
diff --git a/src/applications/search/storage/PhabricatorSearchDAO.php b/src/applications/search/storage/PhabricatorSearchDAO.php
index a7f4739e23..d4e2e397cc 100644
--- a/src/applications/search/storage/PhabricatorSearchDAO.php
+++ b/src/applications/search/storage/PhabricatorSearchDAO.php
@@ -1,28 +1,12 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group search
*/
abstract class PhabricatorSearchDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'search';
}
}
diff --git a/src/applications/search/storage/PhabricatorSearchQuery.php b/src/applications/search/storage/PhabricatorSearchQuery.php
index e0666695b6..884597aa41 100644
--- a/src/applications/search/storage/PhabricatorSearchQuery.php
+++ b/src/applications/search/storage/PhabricatorSearchQuery.php
@@ -1,52 +1,36 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group search
*/
final class PhabricatorSearchQuery extends PhabricatorSearchDAO {
protected $query;
protected $parameters = array();
protected $queryKey;
public function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'parameters' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function setParameter($parameter, $value) {
$this->parameters[$parameter] = $value;
return $this;
}
public function getParameter($parameter, $default = null) {
return idx($this->parameters, $parameter, $default);
}
public function save() {
if (!$this->getQueryKey()) {
$this->setQueryKey(Filesystem::readRandomCharacters(12));
}
return parent::save();
}
}
diff --git a/src/applications/search/storage/document/PhabricatorSearchDocument.php b/src/applications/search/storage/document/PhabricatorSearchDocument.php
index c6b5be4a45..c658bd926d 100644
--- a/src/applications/search/storage/document/PhabricatorSearchDocument.php
+++ b/src/applications/search/storage/document/PhabricatorSearchDocument.php
@@ -1,41 +1,25 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group search
*/
final class PhabricatorSearchDocument extends PhabricatorSearchDAO {
protected $phid;
protected $documentType;
protected $documentTitle;
protected $documentCreated;
protected $documentModified;
public function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_IDS => self::IDS_MANUAL,
) + parent::getConfiguration();
}
public function getIDKey() {
return 'phid';
}
}
diff --git a/src/applications/search/storage/document/PhabricatorSearchDocumentField.php b/src/applications/search/storage/document/PhabricatorSearchDocumentField.php
index d2fa2c78f2..ff56b1a5b8 100644
--- a/src/applications/search/storage/document/PhabricatorSearchDocumentField.php
+++ b/src/applications/search/storage/document/PhabricatorSearchDocumentField.php
@@ -1,36 +1,20 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group search
*/
final class PhabricatorSearchDocumentField extends PhabricatorSearchDAO {
protected $phid;
protected $field;
protected $auxPHID;
protected $corpus;
public function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_IDS => self::IDS_MANUAL,
) + parent::getConfiguration();
}
}
diff --git a/src/applications/search/storage/document/PhabricatorSearchDocumentRelationship.php b/src/applications/search/storage/document/PhabricatorSearchDocumentRelationship.php
index a90ac616f9..1ac1cab47b 100644
--- a/src/applications/search/storage/document/PhabricatorSearchDocumentRelationship.php
+++ b/src/applications/search/storage/document/PhabricatorSearchDocumentRelationship.php
@@ -1,37 +1,21 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group search
*/
final class PhabricatorSearchDocumentRelationship extends PhabricatorSearchDAO {
protected $phid;
protected $relatedPHID;
protected $relation;
protected $relatedType;
protected $relatedTime;
public function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_IDS => self::IDS_MANUAL,
) + parent::getConfiguration();
}
}
diff --git a/src/applications/search/view/PhabricatorSearchResultView.php b/src/applications/search/view/PhabricatorSearchResultView.php
index 28d7e4e34d..c553e41cee 100644
--- a/src/applications/search/view/PhabricatorSearchResultView.php
+++ b/src/applications/search/view/PhabricatorSearchResultView.php
@@ -1,140 +1,124 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group search
*/
final class PhabricatorSearchResultView extends AphrontView {
private $handle;
private $query;
private $object;
public function setHandle(PhabricatorObjectHandle $handle) {
$this->handle = $handle;
return $this;
}
public function setQuery(PhabricatorSearchQuery $query) {
$this->query = $query;
return $this;
}
public function setObject($object) {
$this->object = $object;
return $this;
}
public function render() {
$handle = $this->handle;
$type_name = nonempty($handle->getTypeName(), 'Document');
require_celerity_resource('phabricator-search-results-css');
$link = phutil_render_tag(
'a',
array(
'href' => $handle->getURI(),
),
PhabricatorEnv::getProductionURI($handle->getURI()));
$img = $handle->getImageURI();
if ($img) {
$img = phutil_render_tag(
'div',
array(
'class' => 'result-image',
'style' => "background-image: url('{$img}');",
),
'');
}
switch ($handle->getType()) {
case PhabricatorPHIDConstants::PHID_TYPE_CMIT:
$object_name = $handle->getName();
if ($this->object) {
$data = $this->object->getCommitData();
$summary = $data->getSummary();
if (strlen($summary)) {
$object_name = $handle->getName().': '.$data->getSummary();
}
}
break;
default:
$object_name = $handle->getFullName();
break;
}
$index_link = phutil_render_tag(
'a',
array(
'href' => '/search/index/'.$handle->getPHID().'/',
'style' => 'float: right',
),
'Examine Index');
return
'<div class="phabricator-search-result">'.
$img.
'<div class="result-desc">'.
$index_link.
phutil_render_tag(
'a',
array(
'class' => 'result-name',
'href' => $handle->getURI(),
),
$this->emboldenQuery($object_name)).
'<div class="result-type">'.$type_name.' &middot; '.$link.'</div>'.
'</div>'.
'<div style="clear: both;"></div>'.
'</div>';
}
private function emboldenQuery($str) {
if (!$this->query) {
return phutil_escape_html($str);
}
$query = $this->query->getQuery();
$quoted_regexp = '/"([^"]*)"/';
$matches = array(1 => array());
preg_match_all($quoted_regexp, $query, $matches);
$quoted_queries = $matches[1];
$query = preg_replace($quoted_regexp, '', $query);
$query = preg_split('/\s+[+|]?/', $query);
$query = array_filter($query);
$query = array_merge($query, $quoted_queries);
$str = phutil_escape_html($str);
foreach ($query as $word) {
$word = phutil_escape_html($word);
$word = preg_quote($word, '/');
$word = preg_replace('/\\\\\*$/', '\w*', $word);
$str = preg_replace(
'/(?:^|\b)('.$word.')(?:\b|$)/i',
'<strong>\1</strong>',
$str);
}
return $str;
}
}
diff --git a/src/applications/settings/application/PhabricatorApplicationSettings.php b/src/applications/settings/application/PhabricatorApplicationSettings.php
index 275845e6d9..04b553a5b8 100644
--- a/src/applications/settings/application/PhabricatorApplicationSettings.php
+++ b/src/applications/settings/application/PhabricatorApplicationSettings.php
@@ -1,70 +1,54 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationSettings extends PhabricatorApplication {
public function getBaseURI() {
return '/settings/';
}
public function getShortDescription() {
return 'User Preferences';
}
public function getAutospriteName() {
return 'settings';
}
public function getRoutes() {
return array(
'/settings/' => array(
'(?:panel/(?P<key>[^/]+)/)?' => 'PhabricatorSettingsMainController',
'adjust/' => 'PhabricatorSettingsAdjustController',
),
);
}
public function getApplicationGroup() {
return self::GROUP_UTILITIES;
}
public function buildMainMenuItems(
PhabricatorUser $user,
PhabricatorController $controller = null) {
$items = array();
if ($controller instanceof PhabricatorSettingsMainController) {
$class = 'main-menu-item-icon-settings-selected';
} else {
$class = 'main-menu-item-icon-settings';
}
if ($user->isLoggedIn()) {
$item = new PhabricatorMainMenuIconView();
$item->setName(pht('Settings'));
$item->addClass('autosprite main-menu-item-icon '.$class);
$item->setHref('/settings/');
$item->setSortOrder(0.90);
$items[] = $item;
}
return $items;
}
}
diff --git a/src/applications/settings/controller/PhabricatorSettingsAdjustController.php b/src/applications/settings/controller/PhabricatorSettingsAdjustController.php
index 26eb3b6a31..2f6b2bcab1 100644
--- a/src/applications/settings/controller/PhabricatorSettingsAdjustController.php
+++ b/src/applications/settings/controller/PhabricatorSettingsAdjustController.php
@@ -1,34 +1,18 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorSettingsAdjustController
extends PhabricatorController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$prefs = $user->loadPreferences();
$prefs->setPreference(
$request->getStr('key'),
$request->getStr('value'));
$prefs->save();
return id(new AphrontAjaxResponse())->setContent(array());
}
}
diff --git a/src/applications/settings/controller/PhabricatorSettingsMainController.php b/src/applications/settings/controller/PhabricatorSettingsMainController.php
index d08106caee..d366aa623b 100644
--- a/src/applications/settings/controller/PhabricatorSettingsMainController.php
+++ b/src/applications/settings/controller/PhabricatorSettingsMainController.php
@@ -1,105 +1,89 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorSettingsMainController
extends PhabricatorController {
private $key;
public function willProcessRequest(array $data) {
$this->key = idx($data, 'key');
}
public function processRequest() {
$request = $this->getRequest();
$panels = $this->buildPanels();
$nav = $this->renderSideNav($panels);
$key = $nav->selectFilter($this->key, head($panels)->getPanelKey());
$panel = $panels[$key];
$response = $panel->processRequest($request);
if ($response instanceof AphrontResponse) {
return $response;
}
$nav->appendChild($response);
return $this->buildApplicationPage(
$nav,
array(
'title' => $panel->getPanelName(),
));
}
private function buildPanels() {
$panel_specs = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorSettingsPanel')
->setConcreteOnly(true)
->selectAndLoadSymbols();
$panels = array();
foreach ($panel_specs as $spec) {
$class = newv($spec['name'], array());
$panels[] = $class->buildPanels();
}
$panels = array_mergev($panels);
$panels = mpull($panels, null, 'getPanelKey');
$result = array();
foreach ($panels as $key => $panel) {
if (!$panel->isEnabled()) {
continue;
}
if (!empty($result[$key])) {
throw new Exception(
"Two settings panels share the same panel key ('{$key}'): ".
get_class($panel).', '.get_class($result[$key]).'.');
}
$result[$key] = $panel;
}
$result = msort($result, 'getPanelSortKey');
return $result;
}
private function renderSideNav(array $panels) {
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($this->getApplicationURI('/panel/')));
$group = null;
foreach ($panels as $panel) {
if ($panel->getPanelGroup() != $group) {
if ($group !== null) {
$nav->addSpacer();
}
$group = $panel->getPanelGroup();
$nav->addLabel($group);
}
$nav->addFilter($panel->getPanelKey(), $panel->getPanelName());
}
return $nav;
}
}
diff --git a/src/applications/settings/panel/PhabricatorSettingsPanel.php b/src/applications/settings/panel/PhabricatorSettingsPanel.php
index 0bd20475da..630f80892b 100644
--- a/src/applications/settings/panel/PhabricatorSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorSettingsPanel.php
@@ -1,156 +1,140 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Defines a settings panel. Settings panels appear in the Settings application,
* and behave like lightweight controllers -- generally, they render some sort
* of form with options in it, and then update preferences when the user
* submits the form. By extending this class, you can add new settings
* panels.
*
* NOTE: This stuff is new and might not be completely stable.
*
* @task config Panel Configuration
* @task panel Panel Implementation
* @task internal Internals
*
* @group settings
*/
abstract class PhabricatorSettingsPanel {
/* -( Panel Configuration )------------------------------------------------ */
/**
* Return a unique string used in the URI to identify this panel, like
* "example".
*
* @return string Unique panel identifier (used in URIs).
* @task config
*/
abstract public function getPanelKey();
/**
* Return a human-readable description of the panel's contents, like
* "Example Settings".
*
* @return string Human-readable panel name.
* @task config
*/
abstract public function getPanelName();
/**
* Return a human-readable group name for this panel. For instance, if you
* had several related panels like "Volume Settings" and
* "Microphone Settings", you might put them in a group called "Audio".
*
* When displayed, panels are grouped with other panels that have the same
* group name.
*
* @return string Human-readable panel group name.
* @task config
*/
abstract public function getPanelGroup();
/**
* Return false to prevent this panel from being displayed or used. You can
* do, e.g., configuration checks here, to determine if the feature your
* panel controls is unavailble in this install. By default, all panels are
* enabled.
*
* @return bool True if the panel should be shown.
* @task config
*/
public function isEnabled() {
return true;
}
/**
* You can use this callback to generate multiple similar panels which all
* share the same implementation. For example, OAuth providers each have a
* separate panel, but the implementation for each panel is the same.
*
* To generate multiple panels, build them here and return a list. By default,
* the current panel (`$this`) is returned alone. For most panels, this
* is the right implementation.
*
* @return list<PhabricatorSettingsPanel> Zero or more panels.
* @task config
*/
public function buildPanels() {
return array($this);
}
/* -( Panel Implementation )----------------------------------------------- */
/**
* Process a user request for this settings panel. Implement this method like
* a lightweight controller. If you return an @{class:AphrontResponse}, the
* response will be used in whole. If you return anything else, it will be
* treated as a view and composed into a normal settings page.
*
* Generally, render your settings panel by returning a form, then return
* a redirect when the user saves settings.
*
* @param AphrontRequest Incoming request.
* @return wild Response to request, either as an
* @{class:AphrontResponse} or something which can
* be composed into a @{class:AphrontView}.
* @task panel
*/
abstract public function processRequest(AphrontRequest $request);
/**
* Get the URI for this panel.
*
* @param string? Optional path to append.
* @return string Relative URI for the panel.
* @task panel
*/
final public function getPanelURI($path = '') {
$key = $this->getPanelKey();
$key = phutil_escape_uri($key);
return '/settings/panel/'.$key.'/'.ltrim($path, '/');
}
/* -( Internals )---------------------------------------------------------- */
/**
* Generates a key to sort the list of panels.
*
* @return string Sortable key.
* @task internal
*/
final public function getPanelSortKey() {
return sprintf(
'%s'.chr(255).'%s',
$this->getPanelGroup(),
$this->getPanelName());
}
}
diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelAccount.php b/src/applications/settings/panel/PhabricatorSettingsPanelAccount.php
index 67555f5118..c784fb12a7 100644
--- a/src/applications/settings/panel/PhabricatorSettingsPanelAccount.php
+++ b/src/applications/settings/panel/PhabricatorSettingsPanelAccount.php
@@ -1,117 +1,101 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorSettingsPanelAccount
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'account';
}
public function getPanelName() {
return pht('Account');
}
public function getPanelGroup() {
return pht('Account Information');
}
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
$editable = PhabricatorEnv::getEnvConfig('account.editable');
$e_realname = $editable ? true : null;
$errors = array();
if ($request->isFormPost()) {
if ($editable) {
$user->setRealName($request->getStr('realname'));
if (!strlen($user->getRealName())) {
$errors[] = 'Real name must be nonempty.';
$e_realname = 'Required';
}
}
$new_timezone = $request->getStr('timezone');
if (in_array($new_timezone, DateTimeZone::listIdentifiers(), true)) {
$user->setTimezoneIdentifier($new_timezone);
} else {
$errors[] = 'The selected timezone is not a valid timezone.';
}
if (!$errors) {
$user->save();
return id(new AphrontRedirectResponse())
->setURI($this->getPanelURI('?saved=true'));
}
}
$notice = null;
if (!$errors) {
if ($request->getStr('saved')) {
$notice = new AphrontErrorView();
$notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
$notice->setTitle('Changes Saved');
$notice->appendChild('<p>Your changes have been saved.</p>');
$notice = $notice->render();
}
} else {
$notice = new AphrontErrorView();
$notice->setTitle('Form Errors');
$notice->setErrors($errors);
$notice = $notice->render();
}
$timezone_ids = DateTimeZone::listIdentifiers();
$timezone_id_map = array_combine($timezone_ids, $timezone_ids);
$form = new AphrontFormView();
$form
->setUser($user)
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Username')
->setValue($user->getUsername()))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Real Name')
->setName('realname')
->setError($e_realname)
->setValue($user->getRealName())
->setDisabled(!$editable))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Timezone')
->setName('timezone')
->setOptions($timezone_id_map)
->setValue($user->getTimezoneIdentifier()))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Save'));
$panel = new AphrontPanelView();
$panel->setHeader('Account Settings');
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->appendChild($form);
return array(
$notice,
$panel,
);
}
}
diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelConduit.php b/src/applications/settings/panel/PhabricatorSettingsPanelConduit.php
index d009e0ba53..378e273e22 100644
--- a/src/applications/settings/panel/PhabricatorSettingsPanelConduit.php
+++ b/src/applications/settings/panel/PhabricatorSettingsPanelConduit.php
@@ -1,123 +1,107 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorSettingsPanelConduit
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'conduit';
}
public function getPanelName() {
return pht('Conduit');
}
public function getPanelGroup() {
return pht('Authentication');
}
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
if ($request->isFormPost()) {
if (!$request->isDialogFormPost()) {
$dialog = new AphrontDialogView();
$dialog->setUser($user);
$dialog->setTitle('Really regenerate session?');
$dialog->setSubmitURI($this->getPanelURI());
$dialog->addSubmitButton('Regenerate');
$dialog->addCancelbutton($this->getPanelURI());
$dialog->appendChild(
'<p>Really destroy the old certificate? Any established '.
'sessions will be terminated.');
return id(new AphrontDialogResponse())
->setDialog($dialog);
}
$conn = $user->establishConnection('w');
queryfx(
$conn,
'DELETE FROM %T WHERE userPHID = %s AND type LIKE %>',
PhabricatorUser::SESSION_TABLE,
$user->getPHID(),
'conduit');
// This implicitly regenerates the certificate.
$user->setConduitCertificate(null);
$user->save();
return id(new AphrontRedirectResponse())
->setURI($this->getPanelURI('?regenerated=true'));
}
if ($request->getStr('regenerated')) {
$notice = new AphrontErrorView();
$notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
$notice->setTitle('Certificate Regenerated');
$notice->appendChild(
'<p>Your old certificate has been destroyed and you have been issued '.
'a new certificate. Sessions established under the old certificate '.
'are no longer valid.</p>');
$notice = $notice->render();
} else {
$notice = null;
}
$cert_form = new AphrontFormView();
$cert_form
->setUser($user)
->appendChild(
'<p class="aphront-form-instructions">This certificate allows you to '.
'authenticate over Conduit, the Phabricator API. Normally, you just '.
'run <tt>arc install-certificate</tt> to install it.')
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Certificate')
->setHeight(AphrontFormTextAreaControl::HEIGHT_SHORT)
->setValue($user->getConduitCertificate()));
$cert = new AphrontPanelView();
$cert->setHeader('Arcanist Certificate');
$cert->appendChild($cert_form);
$cert->setWidth(AphrontPanelView::WIDTH_FORM);
$regen_form = new AphrontFormView();
$regen_form
->setUser($user)
->setAction($this->getPanelURI())
->setWorkflow(true)
->appendChild(
'<p class="aphront-form-instructions">You can regenerate this '.
'certificate, which will invalidate the old certificate and create '.
'a new one.</p>')
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Regenerate Certificate'));
$regen = new AphrontPanelView();
$regen->setHeader('Regenerate Certificate');
$regen->appendChild($regen_form);
$regen->setWidth(AphrontPanelView::WIDTH_FORM);
return array(
$notice,
$cert,
$regen,
);
}
}
diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelDisplayPreferences.php b/src/applications/settings/panel/PhabricatorSettingsPanelDisplayPreferences.php
index 7d713128b7..a27b40960b 100644
--- a/src/applications/settings/panel/PhabricatorSettingsPanelDisplayPreferences.php
+++ b/src/applications/settings/panel/PhabricatorSettingsPanelDisplayPreferences.php
@@ -1,150 +1,134 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorSettingsPanelDisplayPreferences
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'display';
}
public function getPanelName() {
return pht('Display Preferences');
}
public function getPanelGroup() {
return pht('Application Settings');
}
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
$preferences = $user->loadPreferences();
$pref_monospaced = PhabricatorUserPreferences::PREFERENCE_MONOSPACED;
$pref_editor = PhabricatorUserPreferences::PREFERENCE_EDITOR;
$pref_titles = PhabricatorUserPreferences::PREFERENCE_TITLES;
$pref_symbols = PhabricatorUserPreferences::PREFERENCE_DIFFUSION_SYMBOLS;
if ($request->isFormPost()) {
$monospaced = $request->getStr($pref_monospaced);
// Prevent the user from doing stupid things.
$monospaced = preg_replace('/[^a-z0-9 ,"]+/i', '', $monospaced);
$preferences->setPreference($pref_titles, $request->getStr($pref_titles));
$preferences->setPreference($pref_editor, $request->getStr($pref_editor));
$preferences->setPreference($pref_symbols,
$request->getStr($pref_symbols));
$preferences->setPreference($pref_monospaced, $monospaced);
$preferences->save();
return id(new AphrontRedirectResponse())
->setURI($this->getPanelURI('?saved=true'));
}
$example_string = <<<EXAMPLE
// This is what your monospaced font currently looks like.
function helloWorld() {
alert("Hello world!");
}
EXAMPLE;
$editor_doc_link = phutil_render_tag(
'a',
array(
'href' => PhabricatorEnv::getDoclink(
'article/User_Guide_Configuring_an_External_Editor.html'),
),
'User Guide: Configuring an External Editor');
$font_default = PhabricatorEnv::getEnvConfig('style.monospace');
$font_default = phutil_escape_html($font_default);
$pref_symbols_value = $preferences->getPreference($pref_symbols);
$form = id(new AphrontFormView())
->setUser($user)
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Page Titles')
->setName($pref_titles)
->setValue($preferences->getPreference($pref_titles))
->setOptions(
array(
'glyph' =>
"In page titles, show Tool names as unicode glyphs: \xE2\x9A\x99",
'text' =>
'In page titles, show Tool names as plain text: [Differential]',
)))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Editor Link')
->setName($pref_editor)
->setCaption(
'Link to edit files in external editor. '.
'%f is replaced by filename, %l by line number, %r by repository '.
'callsign, %% by literal %. '.
"For documentation, see {$editor_doc_link}.")
->setValue($preferences->getPreference($pref_editor)))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Monospaced Font')
->setName($pref_monospaced)
->setCaption(
'Overrides default fonts in tools like Differential.<br />'.
'(Default: '.$font_default.')')
->setValue($preferences->getPreference($pref_monospaced)))
->appendChild(
id(new AphrontFormMarkupControl())
->setValue(
'<pre class="PhabricatorMonospaced">'.
phutil_escape_html($example_string).
'</pre>'))
->appendChild(
id(new AphrontFormRadioButtonControl())
->setLabel('Symbol Links')
->setName($pref_symbols)
->setValue($pref_symbols_value ? $pref_symbols_value : 'enabled')
->addButton('enabled', 'Enabled (default)',
'Use this setting to disable linking symbol names in Differential '.
'and Diffusion to their definitions. This is enabled by default.')
->addButton('disabled', 'Disabled', null))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Save Preferences'));
$panel = new AphrontPanelView();
$panel->setWidth(AphrontPanelView::WIDTH_WIDE);
$panel->setHeader('Display Preferences');
$panel->appendChild($form);
$error_view = null;
if ($request->getStr('saved') === 'true') {
$error_view = id(new AphrontErrorView())
->setTitle('Preferences Saved')
->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
->setErrors(array('Your preferences have been saved.'));
}
return array(
$error_view,
$panel,
);
}
}
diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelEmailAddresses.php b/src/applications/settings/panel/PhabricatorSettingsPanelEmailAddresses.php
index ac66aecbd5..b60a74b64d 100644
--- a/src/applications/settings/panel/PhabricatorSettingsPanelEmailAddresses.php
+++ b/src/applications/settings/panel/PhabricatorSettingsPanelEmailAddresses.php
@@ -1,369 +1,353 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorSettingsPanelEmailAddresses
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'email';
}
public function getPanelName() {
return pht('Email Addresses');
}
public function getPanelGroup() {
return pht('Email');
}
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
$editable = PhabricatorEnv::getEnvConfig('account.editable');
$uri = $request->getRequestURI();
$uri->setQueryParams(array());
if ($editable) {
$new = $request->getStr('new');
if ($new) {
return $this->returnNewAddressResponse($request, $uri, $new);
}
$delete = $request->getInt('delete');
if ($delete) {
return $this->returnDeleteAddressResponse($request, $uri, $delete);
}
}
$verify = $request->getInt('verify');
if ($verify) {
return $this->returnVerifyAddressResponse($request, $uri, $verify);
}
$primary = $request->getInt('primary');
if ($primary) {
return $this->returnPrimaryAddressResponse($request, $uri, $primary);
}
$emails = id(new PhabricatorUserEmail())->loadAllWhere(
'userPHID = %s ORDER BY address',
$user->getPHID());
$rowc = array();
$rows = array();
foreach ($emails as $email) {
$button_verify = javelin_render_tag(
'a',
array(
'class' => 'button small grey',
'href' => $uri->alter('verify', $email->getID()),
'sigil' => 'workflow',
),
'Verify');
$button_make_primary = javelin_render_tag(
'a',
array(
'class' => 'button small grey',
'href' => $uri->alter('primary', $email->getID()),
'sigil' => 'workflow',
),
'Make Primary');
$button_remove = javelin_render_tag(
'a',
array(
'class' => 'button small grey',
'href' => $uri->alter('delete', $email->getID()),
'sigil' => 'workflow'
),
'Remove');
$button_primary = phutil_render_tag(
'a',
array(
'class' => 'button small disabled',
),
'Primary');
if (!$email->getIsVerified()) {
$action = $button_verify;
} else if ($email->getIsPrimary()) {
$action = $button_primary;
} else {
$action = $button_make_primary;
}
if ($email->getIsPrimary()) {
$remove = $button_primary;
$rowc[] = 'highlighted';
} else {
$remove = $button_remove;
$rowc[] = null;
}
$rows[] = array(
phutil_escape_html($email->getAddress()),
$action,
$remove,
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Email',
'Status',
'Remove',
));
$table->setColumnClasses(
array(
'wide',
'action',
'action',
));
$table->setRowClasses($rowc);
$table->setColumnVisibility(
array(
true,
true,
$editable,
));
$view = new AphrontPanelView();
if ($editable) {
$view->addButton(
javelin_render_tag(
'a',
array(
'href' => $uri->alter('new', 'true'),
'class' => 'green button',
'sigil' => 'workflow',
),
'Add New Address'));
}
$view->setHeader('Email Addresses');
$view->appendChild($table);
return $view;
}
private function returnNewAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
$new) {
$user = $request->getUser();
$e_email = true;
$email = trim($request->getStr('email'));
$errors = array();
if ($request->isDialogFormPost()) {
if ($new == 'verify') {
// The user clicked "Done" from the "an email has been sent" dialog.
return id(new AphrontReloadResponse())->setURI($uri);
}
if (!strlen($email)) {
$e_email = 'Required';
$errors[] = 'Email is required.';
} else if (!PhabricatorUserEmail::isAllowedAddress($email)) {
$e_email = 'Invalid';
$errors[] = PhabricatorUserEmail::describeAllowedAddresses();
}
if (!$errors) {
$object = id(new PhabricatorUserEmail())
->setAddress($email)
->setIsVerified(0);
try {
id(new PhabricatorUserEditor())
->setActor($user)
->addEmail($user, $object);
$object->sendVerificationEmail($user);
$dialog = id(new AphrontDialogView())
->setUser($user)
->addHiddenInput('new', 'verify')
->setTitle('Verification Email Sent')
->appendChild(
'<p>A verification email has been sent. Click the link in the '.
'email to verify your address.</p>')
->setSubmitURI($uri)
->addSubmitButton('Done');
return id(new AphrontDialogResponse())->setDialog($dialog);
} catch (AphrontQueryDuplicateKeyException $ex) {
$email = 'Duplicate';
$errors[] = 'Another user already has this email.';
}
}
}
if ($errors) {
$errors = id(new AphrontErrorView())
->setErrors($errors);
}
$form = id(new AphrontFormLayoutView())
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Email')
->setName('email')
->setValue($request->getStr('email'))
->setCaption(PhabricatorUserEmail::describeAllowedAddresses())
->setError($e_email));
$dialog = id(new AphrontDialogView())
->setUser($user)
->addHiddenInput('new', 'true')
->setTitle('New Address')
->appendChild($errors)
->appendChild($form)
->addSubmitButton('Save')
->addCancelButton($uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function returnDeleteAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
$email_id) {
$user = $request->getUser();
// NOTE: You can only delete your own email addresses, and you can not
// delete your primary address.
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'id = %d AND userPHID = %s AND isPrimary = 0',
$email_id,
$user->getPHID());
if (!$email) {
return new Aphront404Response();
}
if ($request->isFormPost()) {
id(new PhabricatorUserEditor())
->setActor($user)
->removeEmail($user, $email);
return id(new AphrontRedirectResponse())->setURI($uri);
}
$address = $email->getAddress();
$dialog = id(new AphrontDialogView())
->setUser($user)
->addHiddenInput('delete', $email_id)
->setTitle("Really delete address '{$address}'?")
->appendChild(
'<p>Are you sure you want to delete this address? You will no '.
'longer be able to use it to login.</p>')
->addSubmitButton('Delete')
->addCancelButton($uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function returnVerifyAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
$email_id) {
$user = $request->getUser();
// NOTE: You can only send more email for your unverified addresses.
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'id = %d AND userPHID = %s AND isVerified = 0',
$email_id,
$user->getPHID());
if (!$email) {
return new Aphront404Response();
}
if ($request->isFormPost()) {
$email->sendVerificationEmail($user);
return id(new AphrontRedirectResponse())->setURI($uri);
}
$address = $email->getAddress();
$dialog = id(new AphrontDialogView())
->setUser($user)
->addHiddenInput('verify', $email_id)
->setTitle("Send Another Verification Email?")
->appendChild(
'<p>Send another copy of the verification email to '.
phutil_escape_html($address).'?</p>')
->addSubmitButton('Send Email')
->addCancelButton($uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function returnPrimaryAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
$email_id) {
$user = $request->getUser();
// NOTE: You can only make your own verified addresses primary.
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'id = %d AND userPHID = %s AND isVerified = 1 AND isPrimary = 0',
$email_id,
$user->getPHID());
if (!$email) {
return new Aphront404Response();
}
if ($request->isFormPost()) {
id(new PhabricatorUserEditor())
->setActor($user)
->changePrimaryEmail($user, $email);
return id(new AphrontRedirectResponse())->setURI($uri);
}
$address = $email->getAddress();
$dialog = id(new AphrontDialogView())
->setUser($user)
->addHiddenInput('primary', $email_id)
->setTitle("Change primary email address?")
->appendChild(
'<p>If you change your primary address, Phabricator will send all '.
'email to '.phutil_escape_html($address).'.</p>')
->addSubmitButton('Change Primary Address')
->addCancelButton($uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelEmailPreferences.php b/src/applications/settings/panel/PhabricatorSettingsPanelEmailPreferences.php
index a3ff5d80a5..c4224e3edc 100644
--- a/src/applications/settings/panel/PhabricatorSettingsPanelEmailPreferences.php
+++ b/src/applications/settings/panel/PhabricatorSettingsPanelEmailPreferences.php
@@ -1,303 +1,287 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorSettingsPanelEmailPreferences
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'emailpreferences';
}
public function getPanelName() {
return pht('Email Preferences');
}
public function getPanelGroup() {
return pht('Email');
}
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
$preferences = $user->loadPreferences();
$pref_re_prefix = PhabricatorUserPreferences::PREFERENCE_RE_PREFIX;
$pref_vary = PhabricatorUserPreferences::PREFERENCE_VARY_SUBJECT;
$pref_no_self_mail = PhabricatorUserPreferences::PREFERENCE_NO_SELF_MAIL;
$errors = array();
if ($request->isFormPost()) {
if (PhabricatorMetaMTAMail::shouldMultiplexAllMail()) {
if ($request->getStr($pref_re_prefix) == 'default') {
$preferences->unsetPreference($pref_re_prefix);
} else {
$preferences->setPreference(
$pref_re_prefix,
$request->getBool($pref_re_prefix));
}
if ($request->getStr($pref_vary) == 'default') {
$preferences->unsetPreference($pref_vary);
} else {
$preferences->setPreference(
$pref_vary,
$request->getBool($pref_vary));
}
}
$preferences->setPreference(
$pref_no_self_mail,
$request->getStr($pref_no_self_mail));
$new_tags = $request->getArr('mailtags');
$mailtags = $preferences->getPreference('mailtags', array());
foreach ($this->getMailTags() as $key => $label) {
$mailtags[$key] = (bool)idx($new_tags, $key, false);
}
$preferences->setPreference('mailtags', $mailtags);
$preferences->save();
return id(new AphrontRedirectResponse())
->setURI($this->getPanelURI('?saved=true'));
}
$notice = null;
if (!$errors) {
if ($request->getStr('saved')) {
$notice = new AphrontErrorView();
$notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
$notice->setTitle('Changes Saved');
$notice->appendChild('<p>Your changes have been saved.</p>');
}
} else {
$notice = new AphrontErrorView();
$notice->setTitle('Form Errors');
$notice->setErrors($errors);
}
$re_prefix_default = PhabricatorEnv::getEnvConfig('metamta.re-prefix')
? 'Enabled'
: 'Disabled';
$vary_default = PhabricatorEnv::getEnvConfig('metamta.vary-subjects')
? 'Vary'
: 'Do Not Vary';
$re_prefix_value = $preferences->getPreference($pref_re_prefix);
if ($re_prefix_value === null) {
$re_prefix_value = 'default';
} else {
$re_prefix_value = $re_prefix_value
? 'true'
: 'false';
}
$vary_value = $preferences->getPreference($pref_vary);
if ($vary_value === null) {
$vary_value = 'default';
} else {
$vary_value = $vary_value
? 'true'
: 'false';
}
$form = new AphrontFormView();
$form
->setUser($user)
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Self Actions')
->setName($pref_no_self_mail)
->setOptions(
array(
'0' => 'Send me an email when I take an action',
'1' => 'Do not send me an email when I take an action',
))
->setCaption('You can disable email about your own actions.')
->setValue($preferences->getPreference($pref_no_self_mail, 0)));
if (PhabricatorMetaMTAMail::shouldMultiplexAllMail()) {
$re_control = id(new AphrontFormSelectControl())
->setName($pref_re_prefix)
->setOptions(
array(
'default' => 'Use Server Default ('.$re_prefix_default.')',
'true' => 'Enable "Re:" prefix',
'false' => 'Disable "Re:" prefix',
))
->setValue($re_prefix_value);
$vary_control = id(new AphrontFormSelectControl())
->setName($pref_vary)
->setOptions(
array(
'default' => 'Use Server Default ('.$vary_default.')',
'true' => 'Vary Subjects',
'false' => 'Do Not Vary Subjects',
))
->setValue($vary_value);
} else {
$re_control = id(new AphrontFormStaticControl())
->setValue('Server Default ('.$re_prefix_default.')');
$vary_control = id(new AphrontFormStaticControl())
->setValue('Server Default ('.$vary_default.')');
}
$form
->appendChild(
$re_control
->setLabel('Add "Re:" Prefix')
->setCaption(
'Enable this option to fix threading in Mail.app on OS X Lion, '.
'or if you like "Re:" in your email subjects.'))
->appendChild(
$vary_control
->setLabel('Vary Subjects')
->setCaption(
'This option adds more information to email subjects, but may '.
'break threading in some clients.'));
$form
->appendChild(
'<br />'.
'<p class="aphront-form-instructions">'.
'You can customize what mail you receive from Phabricator here.'.
'</p>'.
'<p class="aphront-form-instructions">'.
'<strong>NOTE:</strong> If an update makes several changes (like '.
'adding CCs to a task, closing it, and adding a comment) you will '.
'still receive an email as long as at least one of the changes '.
'is set to notify you.'.
'</p>'
);
$mailtags = $preferences->getPreference('mailtags', array());
$form
->appendChild(
$this->buildMailTagCheckboxes(
$this->getDifferentialMailTags(),
$mailtags)
->setLabel('Differential'))
->appendChild(
$this->buildMailTagCheckboxes(
$this->getManiphestMailTags(),
$mailtags)
->setLabel('Maniphest'));
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Save Preferences'));
$panel = new AphrontPanelView();
$panel->setHeader('Email Preferences');
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->appendChild($form);
return id(new AphrontNullView())
->appendChild(
array(
$notice,
$panel,
));
}
private function getMailTags() {
return array(
MetaMTANotificationType::TYPE_DIFFERENTIAL_REVIEWERS =>
pht("Send me email when a revision's reviewers change."),
MetaMTANotificationType::TYPE_DIFFERENTIAL_CLOSED =>
pht("Send me email when a revision is closed."),
MetaMTANotificationType::TYPE_DIFFERENTIAL_CC =>
pht("Send me email when a revision's CCs change."),
MetaMTANotificationType::TYPE_DIFFERENTIAL_COMMENT =>
pht("Send me email when a revision is commented on."),
MetaMTANotificationType::TYPE_DIFFERENTIAL_UPDATED =>
pht("Send me email when a revision is updated."),
MetaMTANotificationType::TYPE_DIFFERENTIAL_REVIEW_REQUEST =>
pht("Send me email when I am requested to review a revision."),
MetaMTANotificationType::TYPE_DIFFERENTIAL_OTHER =>
pht("Send me email for any other activity not listed above."),
MetaMTANotificationType::TYPE_MANIPHEST_STATUS =>
pht("Send me email when a task's status changes."),
MetaMTANotificationType::TYPE_MANIPHEST_OWNER =>
pht("Send me email when a task's owner changes."),
MetaMTANotificationType::TYPE_MANIPHEST_PRIORITY =>
pht("Send me email when a task's priority changes."),
MetaMTANotificationType::TYPE_MANIPHEST_CC =>
pht("Send me email when a task's CCs change."),
MetaMTANotificationType::TYPE_MANIPHEST_PROJECTS =>
pht("Send me email when a task's associated projects change."),
MetaMTANotificationType::TYPE_MANIPHEST_COMMENT =>
pht("Send me email when a task is commented on."),
MetaMTANotificationType::TYPE_MANIPHEST_OTHER =>
pht("Send me email for any other activity not listed above."),
);
}
private function getManiphestMailTags() {
return array_select_keys(
$this->getMailTags(),
array(
MetaMTANotificationType::TYPE_MANIPHEST_STATUS,
MetaMTANotificationType::TYPE_MANIPHEST_OWNER,
MetaMTANotificationType::TYPE_MANIPHEST_PRIORITY,
MetaMTANotificationType::TYPE_MANIPHEST_CC,
MetaMTANotificationType::TYPE_MANIPHEST_PROJECTS,
MetaMTANotificationType::TYPE_MANIPHEST_COMMENT,
MetaMTANotificationType::TYPE_MANIPHEST_OTHER,
));
}
private function getDifferentialMailTags() {
return array_select_keys(
$this->getMailTags(),
array(
MetaMTANotificationType::TYPE_DIFFERENTIAL_REVIEWERS,
MetaMTANotificationType::TYPE_DIFFERENTIAL_CLOSED,
MetaMTANotificationType::TYPE_DIFFERENTIAL_CC,
MetaMTANotificationType::TYPE_DIFFERENTIAL_COMMENT,
MetaMTANotificationType::TYPE_DIFFERENTIAL_UPDATED,
MetaMTANotificationType::TYPE_DIFFERENTIAL_REVIEW_REQUEST,
MetaMTANotificationType::TYPE_DIFFERENTIAL_OTHER,
));
}
private function buildMailTagCheckboxes(
array $tags,
array $prefs) {
$control = new AphrontFormCheckboxControl();
foreach ($tags as $key => $label) {
$control->addCheckbox(
'mailtags['.$key.']',
1,
$label,
idx($prefs, $key, 1));
}
return $control;
}
}
diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelLDAP.php b/src/applications/settings/panel/PhabricatorSettingsPanelLDAP.php
index c74143058b..047a3387c9 100644
--- a/src/applications/settings/panel/PhabricatorSettingsPanelLDAP.php
+++ b/src/applications/settings/panel/PhabricatorSettingsPanelLDAP.php
@@ -1,101 +1,85 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorSettingsPanelLDAP
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'ldap';
}
public function getPanelName() {
return pht('LDAP');
}
public function getPanelGroup() {
return pht('Linked Accounts');
}
public function isEnabled() {
$ldap_provider = new PhabricatorLDAPProvider();
return $ldap_provider->isProviderEnabled();
}
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
$ldap_info = id(new PhabricatorUserLDAPInfo())->loadOneWhere(
'userID = %d',
$user->getID());
$forms = array();
if (!$ldap_info) {
$unlink = 'Link LDAP Account';
$unlink_form = new AphrontFormView();
$unlink_form
->setUser($user)
->setAction('/ldap/login/')
->appendChild(
'<p class="aphront-form-instructions">There is currently no '.
'LDAP account linked to your Phabricator account. You can link an ' .
'account, which will allow you to use it to log into Phabricator</p>')
->appendChild(
id(new AphrontFormTextControl())
->setLabel('LDAP username')
->setName('username'))
->appendChild(
id(new AphrontFormPasswordControl())
->setLabel('Password')
->setName('password'))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue("Link LDAP Account \xC2\xBB"));
$forms['Link Account'] = $unlink_form;
} else {
$unlink = 'Unlink LDAP Account';
$unlink_form = new AphrontFormView();
$unlink_form
->setUser($user)
->appendChild(
'<p class="aphront-form-instructions">You may unlink this account '.
'from your LDAP account. This will prevent you from logging in with '.
'your LDAP credentials.</p>')
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton('/ldap/unlink/', $unlink));
$forms['Unlink Account'] = $unlink_form;
}
$panel = new AphrontPanelView();
$panel->setHeader('LDAP Account Settings');
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
foreach ($forms as $name => $form) {
if ($name) {
$panel->appendChild('<br /><h1>'.$name.'</h1><br />');
}
$panel->appendChild($form);
}
return array(
$panel,
);
}
}
diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelOAuth.php b/src/applications/settings/panel/PhabricatorSettingsPanelOAuth.php
index 07fa4c6b74..7df317b4c7 100644
--- a/src/applications/settings/panel/PhabricatorSettingsPanelOAuth.php
+++ b/src/applications/settings/panel/PhabricatorSettingsPanelOAuth.php
@@ -1,286 +1,270 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorSettingsPanelOAuth
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'oauth-'.$this->provider->getProviderKey();
}
public function getPanelName() {
return $this->provider->getProviderName();
}
public function getPanelGroup() {
return pht('Linked Accounts');
}
public function buildPanels() {
$panels = array();
$providers = PhabricatorOAuthProvider::getAllProviders();
foreach ($providers as $provider) {
$panel = clone $this;
$panel->setOAuthProvider($provider);
$panels[] = $panel;
}
return $panels;
}
public function isEnabled() {
return $this->provider->isProviderEnabled();
}
private $provider;
public function setOAuthProvider(PhabricatorOAuthProvider $oauth_provider) {
$this->provider = $oauth_provider;
return $this;
}
private function prepareAuthForm(AphrontFormView $form) {
$provider = $this->provider;
$auth_uri = $provider->getAuthURI();
$client_id = $provider->getClientID();
$redirect_uri = $provider->getRedirectURI();
$minimum_scope = $provider->getMinimumScope();
$form
->setAction($auth_uri)
->setMethod('GET')
->addHiddenInput('redirect_uri', $redirect_uri)
->addHiddenInput('client_id', $client_id)
->addHiddenInput('scope', $minimum_scope);
foreach ($provider->getExtraAuthParameters() as $key => $value) {
$form->addHiddenInput($key, $value);
}
return $form;
}
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
$provider = $this->provider;
$notice = null;
$provider_name = $provider->getProviderName();
$provider_key = $provider->getProviderKey();
$oauth_info = id(new PhabricatorUserOAuthInfo())->loadOneWhere(
'userID = %d AND oauthProvider = %s',
$user->getID(),
$provider->getProviderKey());
if ($request->isFormPost() && $oauth_info) {
$notice = $this->refreshProfileImage($request, $oauth_info);
}
$form = new AphrontFormView();
$form->setUser($user);
$forms = array();
$forms[] = $form;
if (!$oauth_info) {
$form
->appendChild(
'<p class="aphront-form-instructions">There is currently no '.
phutil_escape_html($provider_name).' account linked to your '.
'Phabricator account. You can link an account, which will allow you '.
'to use it to log into Phabricator.</p>');
$this->prepareAuthForm($form);
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Link '.$provider_name." Account \xC2\xBB"));
} else {
$expires = $oauth_info->getTokenExpires();
$form
->appendChild(
'<p class="aphront-form-instructions">Your account is linked with '.
'a '.phutil_escape_html($provider_name).' account. You may use your '.
phutil_escape_html($provider_name).' credentials to log into '.
'Phabricator.</p>')
->appendChild(
id(new AphrontFormStaticControl())
->setLabel($provider_name.' ID')
->setValue($oauth_info->getOAuthUID())
)
->appendChild(
id(new AphrontFormStaticControl())
->setLabel($provider_name.' Name')
->setValue($oauth_info->getAccountName())
)
->appendChild(
id(new AphrontFormStaticControl())
->setLabel($provider_name.' URI')
->setValue($oauth_info->getAccountURI())
);
if (!$expires || $expires > time()) {
$form->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Refresh Profile Image from '.$provider_name)
);
}
if (!$provider->isProviderLinkPermanent()) {
$unlink = 'Unlink '.$provider_name.' Account';
$unlink_form = new AphrontFormView();
$unlink_form
->setUser($user)
->appendChild(
'<p class="aphront-form-instructions">You may unlink this account '.
'from your '.phutil_escape_html($provider_name).' account. This '.
'will prevent you from logging in with your '.
phutil_escape_html($provider_name).' credentials.</p>')
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton('/oauth/'.$provider_key.'/unlink/', $unlink));
$forms['Unlink Account'] = $unlink_form;
}
if ($expires) {
if ($expires <= time()) {
$expires_text = "Expired";
} else {
$expires_text = phabricator_datetime($expires, $user);
}
} else {
$expires_text = 'No Information Available';
}
$scope = $oauth_info->getTokenScope();
if (!$scope) {
$scope = 'No Information Available';
}
$status = $oauth_info->getTokenStatus();
$status = PhabricatorUserOAuthInfo::getReadableTokenStatus($status);
$token_form = new AphrontFormView();
$token_form
->setUser($user)
->appendChild(
'<p class="aphront-from-instructions">insert rap about tokens</p>')
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Token Status')
->setValue($status))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Expires')
->setValue($expires_text))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Scope')
->setValue($scope));
if ($expires <= time()) {
$this->prepareAuthForm($token_form);
$token_form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Refresh '.$provider_name.' Token')
);
}
$forms['Account Token Information'] = $token_form;
}
$panel = new AphrontPanelView();
$panel->setHeader($provider_name.' Account Settings');
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
foreach ($forms as $name => $form) {
if ($name) {
$panel->appendChild('<br /><h1>'.$name.'</h1><br />');
}
$panel->appendChild($form);
}
return id(new AphrontNullView())
->appendChild(
array(
$notice,
$panel,
));
}
private function refreshProfileImage(
AphrontRequest $request,
PhabricatorUserOAuthInfo $oauth_info) {
$user = $request->getUser();
$provider = $this->provider;
$error = false;
$userinfo_uri = new PhutilURI($provider->getUserInfoURI());
$token = $oauth_info->getToken();
try {
$userinfo_uri->setQueryParam('access_token', $token);
$user_data = HTTPSFuture::loadContent($userinfo_uri);
$provider->setUserData($user_data);
$provider->setAccessToken($token);
$image = $provider->retrieveUserProfileImage();
if ($image) {
$file = PhabricatorFile::newFromFileData(
$image,
array(
'name' => $provider->getProviderKey().'-profile.jpg',
'authorPHID' => $user->getPHID(),
));
$xformer = new PhabricatorImageTransformer();
// Resize OAuth image to a reasonable size
$small_xformed = $xformer->executeProfileTransform(
$file,
$width = 50,
$min_height = 50,
$max_height = 50);
$user->setProfileImagePHID($small_xformed->getPHID());
$user->save();
} else {
$error = 'Unable to retrieve image.';
}
} catch (Exception $e) {
if ($e instanceof PhabricatorOAuthProviderException) {
$error = sprintf('Unable to retrieve image from %s',
$provider->getProviderName());
} else {
$error = 'Unable to save image.';
}
}
$notice = new AphrontErrorView();
if ($error) {
$notice
->setTitle('Error Refreshing Profile Picture')
->setErrors(array($error));
} else {
$notice
->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
->setTitle('Successfully Refreshed Profile Picture');
}
return $notice;
}
}
diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelPassword.php b/src/applications/settings/panel/PhabricatorSettingsPanelPassword.php
index cc086e59f7..a26ff94b79 100644
--- a/src/applications/settings/panel/PhabricatorSettingsPanelPassword.php
+++ b/src/applications/settings/panel/PhabricatorSettingsPanelPassword.php
@@ -1,187 +1,171 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorSettingsPanelPassword
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'password';
}
public function getPanelName() {
return pht('Password');
}
public function getPanelGroup() {
return pht('Authentication');
}
public function isEnabled() {
// There's no sense in showing a change password panel if the user
// can't change their password...
if (!PhabricatorEnv::getEnvConfig('account.editable')) {
return false;
}
// ...or this install doesn't support password authentication at all.
if (!PhabricatorEnv::getEnvConfig('auth.password-auth-enabled')) {
return false;
}
return true;
}
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
$min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length');
$min_len = (int)$min_len;
// NOTE: To change your password, you need to prove you own the account,
// either by providing the old password or by carrying a token to
// the workflow from a password reset email.
$token = $request->getStr('token');
$valid_token = false;
if ($token) {
$email_address = $request->getStr('email');
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$email_address);
if ($email) {
$valid_token = $user->validateEmailToken($email, $token);
}
}
$e_old = true;
$e_new = true;
$e_conf = true;
$errors = array();
if ($request->isFormPost()) {
if (!$valid_token) {
$envelope = new PhutilOpaqueEnvelope($request->getStr('old_pw'));
if (!$user->comparePassword($envelope)) {
$errors[] = 'The old password you entered is incorrect.';
$e_old = 'Invalid';
}
}
$pass = $request->getStr('new_pw');
$conf = $request->getStr('conf_pw');
if (strlen($pass) < $min_len) {
$errors[] = 'Your new password is too short.';
$e_new = 'Too Short';
}
if ($pass !== $conf) {
$errors[] = 'New password and confirmation do not match.';
$e_conf = 'Invalid';
}
if (!$errors) {
// This write is unguarded because the CSRF token has already
// been checked in the call to $request->isFormPost() and
// the CSRF token depends on the password hash, so when it
// is changed here the CSRF token check will fail.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$envelope = new PhutilOpaqueEnvelope($pass);
id(new PhabricatorUserEditor())
->setActor($user)
->changePassword($user, $envelope);
unset($unguarded);
if ($valid_token) {
// If this is a password set/reset, kick the user to the home page
// after we update their account.
$next = '/';
} else {
$next = $this->getPanelURI('?saved=true');
}
return id(new AphrontRedirectResponse())->setURI($next);
}
}
$notice = null;
if (!$errors) {
if ($request->getStr('saved')) {
$notice = new AphrontErrorView();
$notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
$notice->setTitle('Changes Saved');
$notice->appendChild('<p>Your password has been updated.</p>');
}
} else {
$notice = new AphrontErrorView();
$notice->setTitle('Error Changing Password');
$notice->setErrors($errors);
}
$len_caption = null;
if ($min_len) {
$len_caption = 'Minimum password length: '.$min_len.' characters.';
}
$form = new AphrontFormView();
$form
->setUser($user)
->addHiddenInput('token', $token);
if (!$valid_token) {
$form->appendChild(
id(new AphrontFormPasswordControl())
->setLabel('Old Password')
->setError($e_old)
->setName('old_pw'));
}
$form
->appendChild(
id(new AphrontFormPasswordControl())
->setLabel('New Password')
->setError($e_new)
->setName('new_pw'));
$form
->appendChild(
id(new AphrontFormPasswordControl())
->setLabel('Confirm Password')
->setCaption($len_caption)
->setError($e_conf)
->setName('conf_pw'));
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Save'));
$panel = new AphrontPanelView();
$panel->setHeader('Change Password');
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->appendChild($form);
return array(
$notice,
$panel,
);
}
}
diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelProfile.php b/src/applications/settings/panel/PhabricatorSettingsPanelProfile.php
index 3ef0269611..e2e59284e4 100644
--- a/src/applications/settings/panel/PhabricatorSettingsPanelProfile.php
+++ b/src/applications/settings/panel/PhabricatorSettingsPanelProfile.php
@@ -1,225 +1,209 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorSettingsPanelProfile
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'profile';
}
public function getPanelName() {
return pht('Profile');
}
public function getPanelGroup() {
return pht('Account Information');
}
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
$profile = id(new PhabricatorUserProfile())->loadOneWhere(
'userPHID = %s',
$user->getPHID());
if (!$profile) {
$profile = new PhabricatorUserProfile();
$profile->setUserPHID($user->getPHID());
}
$supported_formats = PhabricatorFile::getTransformableImageFormats();
$e_image = null;
$errors = array();
if ($request->isFormPost()) {
$profile->setTitle($request->getStr('title'));
$profile->setBlurb($request->getStr('blurb'));
$sex = $request->getStr('sex');
$sexes = array(PhutilPerson::SEX_MALE, PhutilPerson::SEX_FEMALE);
if (in_array($sex, $sexes)) {
$user->setSex($sex);
} else {
$user->setSex(null);
}
// Checked in runtime.
$user->setTranslation($request->getStr('translation'));
$default_image = $request->getExists('default_image');
if ($default_image) {
$profile->setProfileImagePHID(null);
$user->setProfileImagePHID(null);
} else if (!empty($_FILES['image'])) {
$err = idx($_FILES['image'], 'error');
if ($err != UPLOAD_ERR_NO_FILE) {
$file = PhabricatorFile::newFromPHPUpload(
$_FILES['image'],
array(
'authorPHID' => $user->getPHID(),
));
$okay = $file->isTransformableImage();
if ($okay) {
$xformer = new PhabricatorImageTransformer();
// Generate the large picture for the profile page.
$large_xformed = $xformer->executeProfileTransform(
$file,
$width = 280,
$min_height = 140,
$max_height = 420);
$profile->setProfileImagePHID($large_xformed->getPHID());
// Generate the small picture for comments, etc.
$small_xformed = $xformer->executeProfileTransform(
$file,
$width = 50,
$min_height = 50,
$max_height = 50);
$user->setProfileImagePHID($small_xformed->getPHID());
} else {
$e_image = 'Not Supported';
$errors[] =
'This server only supports these image formats: '.
implode(', ', $supported_formats).'.';
}
}
}
if (!$errors) {
$user->save();
$profile->save();
$response = id(new AphrontRedirectResponse())
->setURI($this->getPanelURI('?saved=true'));
return $response;
}
}
$error_view = null;
if ($errors) {
$error_view = new AphrontErrorView();
$error_view->setTitle('Form Errors');
$error_view->setErrors($errors);
} else {
if ($request->getStr('saved')) {
$error_view = new AphrontErrorView();
$error_view->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
$error_view->setTitle('Changes Saved');
$error_view->appendChild('<p>Your changes have been saved.</p>');
$error_view = $error_view->render();
}
}
$img_src = $user->loadProfileImageURI();
$profile_uri = PhabricatorEnv::getURI('/p/'.$user->getUsername().'/');
$sexes = array(
PhutilPerson::SEX_UNKNOWN => 'Unknown',
PhutilPerson::SEX_MALE => 'Male',
PhutilPerson::SEX_FEMALE => 'Female',
);
$translations = array();
$symbols = id(new PhutilSymbolLoader())
->setType('class')
->setAncestorClass('PhabricatorTranslation')
->setConcreteOnly(true)
->selectAndLoadSymbols();
foreach ($symbols as $symbol) {
$class = $symbol['name'];
$translations[$class] = newv($class, array())->getName();
}
asort($translations);
$default = PhabricatorEnv::newObjectFromConfig('translation.provider');
$translations = array(
'' => 'Server Default ('.$default->getName().')',
) + $translations;
$form = new AphrontFormView();
$form
->setUser($request->getUser())
->setEncType('multipart/form-data')
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Title')
->setName('title')
->setValue($profile->getTitle())
->setCaption('Serious business title.'))
->appendChild(
id(new AphrontFormSelectControl())
->setOptions($sexes)
->setLabel('Sex')
->setName('sex')
->setValue($user->getSex()))
->appendChild(
id(new AphrontFormSelectControl())
->setOptions($translations)
->setLabel('Translation')
->setName('translation')
->setValue($user->getTranslation()))
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel('Profile URI')
->setValue(
phutil_render_tag(
'a',
array(
'href' => $profile_uri,
),
phutil_escape_html($profile_uri))))
->appendChild(
'<p class="aphront-form-instructions">Write something about yourself! '.
'Make sure to include <strong>important information</strong> like '.
'your favorite Pokemon and which Starcraft race you play.</p>')
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Blurb')
->setName('blurb')
->setValue($profile->getBlurb()))
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel('Profile Image')
->setValue(
phutil_render_tag(
'img',
array(
'src' => $img_src,
))))
->appendChild(
id(new AphrontFormImageControl())
->setLabel('Change Image')
->setName('image')
->setError($e_image)
->setCaption('Supported formats: '.implode(', ', $supported_formats)))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Save')
->addCancelButton('/p/'.$user->getUsername().'/'));
$panel = new AphrontPanelView();
$panel->setHeader('Edit Profile Details');
$panel->appendChild($form);
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
return array(
$error_view,
$panel,
);
}
}
diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php b/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php
index 0942e8eadd..0a4929af71 100644
--- a/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php
+++ b/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php
@@ -1,281 +1,265 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorSettingsPanelSSHKeys
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'ssh';
}
public function getPanelName() {
return pht('SSH Public Keys');
}
public function getPanelGroup() {
return pht('Authentication');
}
public function isEnabled() {
return PhabricatorEnv::getEnvConfig('auth.sshkeys.enabled');
}
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
$edit = $request->getStr('edit');
$delete = $request->getStr('delete');
if (!$edit && !$delete) {
return $this->renderKeyListView($request);
}
$id = nonempty($edit, $delete);
if ($id && is_numeric($id)) {
// NOTE: Prevent editing/deleting of keys you don't own.
$key = id(new PhabricatorUserSSHKey())->loadOneWhere(
'userPHID = %s AND id = %d',
$user->getPHID(),
(int)$id);
if (!$key) {
return new Aphront404Response();
}
} else {
$key = new PhabricatorUserSSHKey();
$key->setUserPHID($user->getPHID());
}
if ($delete) {
return $this->processDelete($request, $key);
}
$e_name = true;
$e_key = true;
$errors = array();
$entire_key = $key->getEntireKey();
if ($request->isFormPost()) {
$key->setName($request->getStr('name'));
$entire_key = $request->getStr('key');
if (!strlen($entire_key)) {
$errors[] = 'You must provide an SSH Public Key.';
$e_key = 'Required';
} else {
$parts = str_replace("\n", '', trim($entire_key));
$parts = preg_split('/\s+/', $parts);
if (count($parts) == 2) {
$parts[] = ''; // Add an empty comment part.
} else if (count($parts) == 3) {
// This is the expected case.
} else {
if (preg_match('/private\s*key/i', $entire_key)) {
// Try to give the user a better error message if it looks like
// they uploaded a private key.
$e_key = 'Invalid';
$errors[] = 'Provide your public key, not your private key!';
} else {
$e_key = 'Invalid';
$errors[] = 'Provided public key is not properly formatted.';
}
}
if (!$errors) {
list($type, $body, $comment) = $parts;
if (!preg_match('/^ssh-dsa|ssh-rsa$/', $type)) {
$e_key = 'Invalid';
$errors[] = 'Public key should be "ssh-dsa" or "ssh-rsa".';
} else {
$key->setKeyType($type);
$key->setKeyBody($body);
$key->setKeyHash(md5($body));
$key->setKeyComment($comment);
$e_key = null;
}
}
}
if (!strlen($key->getName())) {
$errors[] = 'You must name this public key.';
$e_name = 'Required';
} else {
$e_name = null;
}
if (!$errors) {
try {
$key->save();
return id(new AphrontRedirectResponse())
->setURI($this->getPanelURI());
} catch (AphrontQueryDuplicateKeyException $ex) {
$e_key = 'Duplicate';
$errors[] = 'This public key is already associated with a user '.
'account.';
}
}
}
$error_view = null;
if ($errors) {
$error_view = new AphrontErrorView();
$error_view->setTitle('Form Errors');
$error_view->setErrors($errors);
}
$is_new = !$key->getID();
if ($is_new) {
$header = 'Add New SSH Public Key';
$save = 'Add Key';
} else {
$header = 'Edit SSH Public Key';
$save = 'Save Changes';
}
$form = id(new AphrontFormView())
->setUser($user)
->addHiddenInput('edit', $is_new ? 'true' : $key->getID())
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Name')
->setName('name')
->setValue($key->getName())
->setError($e_name))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Public Key')
->setName('key')
->setValue($entire_key)
->setError($e_key))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($this->getPanelURI())
->setValue($save));
$panel = new AphrontPanelView();
$panel->setHeader($header);
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->appendChild($form);
return id(new AphrontNullView())
->appendChild(
array(
$error_view,
$panel,
));
}
private function renderKeyListView(AphrontRequest $request) {
$user = $request->getUser();
$keys = id(new PhabricatorUserSSHKey())->loadAllWhere(
'userPHID = %s',
$user->getPHID());
$rows = array();
foreach ($keys as $key) {
$rows[] = array(
phutil_render_tag(
'a',
array(
'href' => $this->getPanelURI('?edit='.$key->getID()),
),
phutil_escape_html($key->getName())),
phutil_escape_html($key->getKeyComment()),
phutil_escape_html($key->getKeyType()),
phabricator_date($key->getDateCreated(), $user),
phabricator_time($key->getDateCreated(), $user),
javelin_render_tag(
'a',
array(
'href' => $this->getPanelURI('?delete='.$key->getID()),
'class' => 'small grey button',
'sigil' => 'workflow',
),
'Delete'),
);
}
$table = new AphrontTableView($rows);
$table->setNoDataString("You haven't added any SSH Public Keys.");
$table->setHeaders(
array(
'Name',
'Comment',
'Type',
'Created',
'Time',
'',
));
$table->setColumnClasses(
array(
'wide pri',
'',
'',
'',
'right',
'action',
));
$panel = new AphrontPanelView();
$panel->addButton(
phutil_render_tag(
'a',
array(
'href' => $this->getPanelURI('?edit=true'),
'class' => 'green button',
),
'Add New Public Key'));
$panel->setHeader('SSH Public Keys');
$panel->appendChild($table);
return $panel;
}
private function processDelete(
AphrontRequest $request,
PhabricatorUserSSHKey $key) {
$user = $request->getUser();
$name = phutil_escape_html($key->getName());
if ($request->isDialogFormPost()) {
$key->delete();
return id(new AphrontReloadResponse())
->setURI($this->getPanelURI());
}
$dialog = id(new AphrontDialogView())
->setUser($user)
->addHiddenInput('delete', $key->getID())
->setTitle('Really delete SSH Public Key?')
->appendChild(
'<p>The key "<strong>'.$name.'</strong>" will be permanently deleted, '.
'and you will not longer be able to use the corresponding private key '.
'to authenticate.</p>')
->addSubmitButton('Delete Public Key')
->addCancelButton($this->getPanelURI());
return id(new AphrontDialogResponse())
->setDialog($dialog);
}
}
diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelSearchPreferences.php b/src/applications/settings/panel/PhabricatorSettingsPanelSearchPreferences.php
index 9cf063775b..9cffc41b6a 100644
--- a/src/applications/settings/panel/PhabricatorSettingsPanelSearchPreferences.php
+++ b/src/applications/settings/panel/PhabricatorSettingsPanelSearchPreferences.php
@@ -1,89 +1,73 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorSettingsPanelSearchPreferences
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'search';
}
public function getPanelName() {
return pht('Search Preferences');
}
public function getPanelGroup() {
return pht('Application Settings');
}
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
$preferences = $user->loadPreferences();
$pref_jump = PhabricatorUserPreferences::PREFERENCE_SEARCHBAR_JUMP;
$pref_shortcut = PhabricatorUserPreferences::PREFERENCE_SEARCH_SHORTCUT;
if ($request->isFormPost()) {
$preferences->setPreference($pref_jump,
$request->getBool($pref_jump));
$preferences->setPreference($pref_shortcut,
$request->getBool($pref_shortcut));
$preferences->save();
return id(new AphrontRedirectResponse())
->setURI($this->getPanelURI('?saved=true'));
}
$form = id(new AphrontFormView())
->setUser($user)
->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox($pref_jump,
1,
'Enable jump nav functionality in all search boxes.',
$preferences->getPreference($pref_jump, 1))
->addCheckbox($pref_shortcut,
1,
"Press '/' to focus the search input.",
$preferences->getPreference($pref_shortcut, 1))
)
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Save'));
$panel = new AphrontPanelView();
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->setHeader('Search Preferences');
$panel->appendChild($form);
$error_view = null;
if ($request->getStr('saved') === 'true') {
$error_view = id(new AphrontErrorView())
->setTitle('Preferences Saved')
->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
->setErrors(array('Your preferences have been saved.'));
}
return array(
$error_view,
$panel,
);
}
}
diff --git a/src/applications/settings/storage/PhabricatorUserPreferences.php b/src/applications/settings/storage/PhabricatorUserPreferences.php
index 9556ca9797..be7a2aca68 100644
--- a/src/applications/settings/storage/PhabricatorUserPreferences.php
+++ b/src/applications/settings/storage/PhabricatorUserPreferences.php
@@ -1,64 +1,48 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorUserPreferences extends PhabricatorUserDAO {
const PREFERENCE_MONOSPACED = 'monospaced';
const PREFERENCE_EDITOR = 'editor';
const PREFERENCE_TITLES = 'titles';
const PREFERENCE_RE_PREFIX = 're-prefix';
const PREFERENCE_NO_SELF_MAIL = 'self-mail';
const PREFERENCE_MAILTAGS = 'mailtags';
const PREFERENCE_VARY_SUBJECT = 'vary-subject';
const PREFERENCE_SEARCHBAR_JUMP = 'searchbar-jump';
const PREFERENCE_SEARCH_SHORTCUT = 'search-shortcut';
const PREFERENCE_DIFFUSION_VIEW = 'diffusion-view';
const PREFERENCE_DIFFUSION_SYMBOLS = 'diffusion-symbols';
const PREFERENCE_NAV_WIDTH = 'nav-width';
protected $userPHID;
protected $preferences = array();
public function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'preferences' => self::SERIALIZATION_JSON,
),
self::CONFIG_TIMESTAMPS => false,
) + parent::getConfiguration();
}
public function getPreference($key, $default = null) {
return idx($this->preferences, $key, $default);
}
public function setPreference($key, $value) {
$this->preferences[$key] = $value;
return $this;
}
public function unsetPreference($key) {
unset($this->preferences[$key]);
return $this;
}
}
diff --git a/src/applications/settings/storage/PhabricatorUserSSHKey.php b/src/applications/settings/storage/PhabricatorUserSSHKey.php
index ecedfc104b..a06317fedd 100644
--- a/src/applications/settings/storage/PhabricatorUserSSHKey.php
+++ b/src/applications/settings/storage/PhabricatorUserSSHKey.php
@@ -1,37 +1,21 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorUserSSHKey extends PhabricatorUserDAO {
protected $userPHID;
protected $name;
protected $keyType;
protected $keyBody;
protected $keyHash;
protected $keyComment;
public function getEntireKey() {
$parts = array(
$this->getKeyType(),
$this->getKeyBody(),
$this->getKeyComment(),
);
return trim(implode(' ', $parts));
}
}
diff --git a/src/applications/slowvote/application/PhabricatorApplicationSlowvote.php b/src/applications/slowvote/application/PhabricatorApplicationSlowvote.php
index b95d802228..017fcb3fd7 100644
--- a/src/applications/slowvote/application/PhabricatorApplicationSlowvote.php
+++ b/src/applications/slowvote/application/PhabricatorApplicationSlowvote.php
@@ -1,59 +1,43 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationSlowvote extends PhabricatorApplication {
public function getBaseURI() {
return '/vote/';
}
public function getAutospriteName() {
return 'slowvote';
}
public function getShortDescription() {
return 'Conduct Polls';
}
public function getTitleGlyph() {
return "\xE2\x9C\x94";
}
public function getHelpURI() {
return PhabricatorEnv::getDoclink('article/Slowvote_User_Guide.html');
}
public function getFlavorText() {
return pht('Design by committee.');
}
public function getApplicationGroup() {
return self::GROUP_UTILITIES;
}
public function getRoutes() {
return array(
'/V(?P<id>[1-9]\d*)' => 'PhabricatorSlowvotePollController',
'/vote/' => array(
'(?:view/(?P<view>\w+)/)?' => 'PhabricatorSlowvoteListController',
'create/' => 'PhabricatorSlowvoteCreateController',
),
);
}
}
diff --git a/src/applications/slowvote/controller/PhabricatorSlowvoteController.php b/src/applications/slowvote/controller/PhabricatorSlowvoteController.php
index 5368519702..19aec98a38 100644
--- a/src/applications/slowvote/controller/PhabricatorSlowvoteController.php
+++ b/src/applications/slowvote/controller/PhabricatorSlowvoteController.php
@@ -1,38 +1,22 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group slowvote
*/
abstract class PhabricatorSlowvoteController extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('Slowvote');
$page->setBaseURI('/vote/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph("\xE2\x9C\x94");
$page->appendChild($view);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
}
diff --git a/src/applications/slowvote/controller/PhabricatorSlowvoteCreateController.php b/src/applications/slowvote/controller/PhabricatorSlowvoteCreateController.php
index e0057f3ee0..5dcb904707 100644
--- a/src/applications/slowvote/controller/PhabricatorSlowvoteCreateController.php
+++ b/src/applications/slowvote/controller/PhabricatorSlowvoteCreateController.php
@@ -1,163 +1,147 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group slowvote
*/
final class PhabricatorSlowvoteCreateController
extends PhabricatorSlowvoteController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$poll = new PhabricatorSlowvotePoll();
$poll->setAuthorPHID($user->getPHID());
$e_question = true;
$e_response = true;
$errors = array();
$responses = $request->getArr('response');
if ($request->isFormPost()) {
$poll->setQuestion($request->getStr('question'));
$poll->setResponseVisibility($request->getInt('response_visibility'));
$poll->setShuffle($request->getBool('shuffle', false));
$poll->setMethod($request->getInt('method'));
if (!strlen($poll->getQuestion())) {
$e_question = 'Required';
$errors[] = 'You must ask a poll question.';
} else {
$e_question = null;
}
$responses = array_filter($responses);
if (empty($responses)) {
$errors[] = 'You must offer at least one response.';
$e_response = 'Required';
} else {
$e_response = null;
}
if (empty($errors)) {
$poll->save();
foreach ($responses as $response) {
$option = new PhabricatorSlowvoteOption();
$option->setName($response);
$option->setPollID($poll->getID());
$option->save();
}
return id(new AphrontRedirectResponse())
->setURI('/V'.$poll->getID());
}
}
$error_view = null;
if ($errors) {
$error_view = new AphrontErrorView();
$error_view->setTitle('Form Errors');
$error_view->setErrors($errors);
}
$form = id(new AphrontFormView())
->setUser($user)
->appendChild(
'<p class="aphront-form-instructions">Resolve issues and build '.
'consensus through protracted deliberation.</p>')
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Question')
->setName('question')
->setValue($poll->getQuestion())
->setError($e_question));
for ($ii = 0; $ii < 10; $ii++) {
$n = ($ii + 1);
$response = id(new AphrontFormTextControl())
->setLabel("Response {$n}")
->setName('response[]')
->setValue(idx($responses, $ii, ''));
if ($ii == 0) {
$response->setError($e_response);
}
$form->appendChild($response);
}
$poll_type_options = array(
PhabricatorSlowvotePoll::METHOD_PLURALITY => 'Plurality (Single Choice)',
PhabricatorSlowvotePoll::METHOD_APPROVAL => 'Approval (Multiple Choice)',
);
$response_type_options = array(
PhabricatorSlowvotePoll::RESPONSES_VISIBLE
=> 'Allow anyone to see the responses',
PhabricatorSlowvotePoll::RESPONSES_VOTERS
=> 'Require a vote to see the responses',
PhabricatorSlowvotePoll::RESPONSES_OWNER
=> 'Only I can see the responses',
);
$form
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Vote Type')
->setName('method')
->setValue($poll->getMethod())
->setOptions($poll_type_options))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Responses')
->setName('response_visibility')
->setValue($poll->getResponseVisibility())
->setOptions($response_type_options))
->appendChild(
id(new AphrontFormCheckboxControl())
->setLabel('Shuffle')
->addCheckbox(
'shuffle',
1,
'Show choices in random order',
$poll->getShuffle()))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Create Slowvote')
->addCancelButton('/vote/'));
$panel = new AphrontPanelView();
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->setHeader('Create Slowvote');
$panel->appendChild($form);
return $this->buildStandardPageResponse(
array(
$error_view,
$panel,
),
array(
'title' => 'Create Slowvote',
));
}
}
diff --git a/src/applications/slowvote/controller/PhabricatorSlowvoteListController.php b/src/applications/slowvote/controller/PhabricatorSlowvoteListController.php
index ea1d10080d..2ba2b5fdb0 100644
--- a/src/applications/slowvote/controller/PhabricatorSlowvoteListController.php
+++ b/src/applications/slowvote/controller/PhabricatorSlowvoteListController.php
@@ -1,190 +1,174 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group slowvote
*/
final class PhabricatorSlowvoteListController
extends PhabricatorSlowvoteController {
private $view;
const VIEW_ALL = 'all';
const VIEW_CREATED = 'created';
const VIEW_VOTED = 'voted';
public function willProcessRequest(array $data) {
$this->view = idx($data, 'view');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$views = array(
self::VIEW_ALL => 'All Slowvotes',
self::VIEW_CREATED => 'Created',
self::VIEW_VOTED => 'Voted In',
);
$view = isset($views[$this->view])
? $this->view
: self::VIEW_ALL;
$side_nav = $this->renderSideNav($views, $view);
$pager = new AphrontPagerView();
$pager->setOffset($request->getInt('page'));
$pager->setURI($request->getRequestURI(), 'page');
$polls = $this->loadPolls($pager, $view);
$phids = mpull($polls, 'getAuthorPHID');
$handles = $this->loadViewerHandles($phids);
$rows = array();
foreach ($polls as $poll) {
$rows[] = array(
'V'.$poll->getID(),
phutil_render_tag(
'a',
array(
'href' => '/V'.$poll->getID(),
),
phutil_escape_html($poll->getQuestion())),
$handles[$poll->getAuthorPHID()]->renderLink(),
phabricator_date($poll->getDateCreated(), $user),
phabricator_time($poll->getDateCreated(), $user),
);
}
$table = new AphrontTableView($rows);
$table->setColumnClasses(
array(
'',
'pri wide',
'',
'',
'right',
));
$table->setHeaders(
array(
'ID',
'Poll',
'Author',
'Date',
'Time',
));
$panel = new AphrontPanelView();
$panel->setHeader($this->getTableHeader($view));
$panel->setCreateButton('Create Slowvote', '/vote/create/');
$panel->appendChild($table);
$panel->appendChild($pager);
$side_nav->appendChild($panel);
return $this->buildStandardPageResponse(
$side_nav,
array(
'title' => 'Slowvotes',
));
}
private function loadPolls(AphrontPagerView $pager, $view) {
$request = $this->getRequest();
$user = $request->getUser();
$poll = new PhabricatorSlowvotePoll();
$conn = $poll->establishConnection('r');
$offset = $pager->getOffset();
$limit = $pager->getPageSize() + 1;
switch ($view) {
case self::VIEW_ALL:
$data = queryfx_all(
$conn,
'SELECT * FROM %T ORDER BY id DESC LIMIT %d, %d',
$poll->getTableName(),
$offset,
$limit);
break;
case self::VIEW_CREATED:
$data = queryfx_all(
$conn,
'SELECT * FROM %T WHERE authorPHID = %s ORDER BY id DESC
LIMIT %d, %d',
$poll->getTableName(),
$user->getPHID(),
$offset,
$limit);
break;
case self::VIEW_VOTED:
$choice = new PhabricatorSlowvoteChoice();
$data = queryfx_all(
$conn,
'SELECT p.* FROM %T p JOIN %T o
ON o.pollID = p.id
WHERE o.authorPHID = %s
GROUP BY p.id
ORDER BY p.id DESC
LIMIT %d, %d',
$poll->getTableName(),
$choice->getTableName(),
$user->getPHID(),
$offset,
$limit);
break;
}
$data = $pager->sliceResults($data);
return $poll->loadAllFromArray($data);
}
private function renderSideNav(array $views, $view) {
$side_nav = new AphrontSideNavView();
foreach ($views as $key => $name) {
$side_nav->addNavItem(
phutil_render_tag(
'a',
array(
'href' => '/vote/view/'.$key.'/',
'class' => ($view == $key)
? 'aphront-side-nav-selected'
: null,
),
phutil_escape_html($name)));
}
return $side_nav;
}
private function getTableHeader($view) {
static $headers = array(
self::VIEW_ALL
=> 'Slowvotes Not Yet Consumed by the Ravages of Time',
self::VIEW_CREATED
=> 'Slowvotes Birthed from Your Noblest of Great Minds',
self::VIEW_VOTED
=> 'Slowvotes Within Which You Express Your Mighty Opinion',
);
return idx($headers, $view);
}
}
diff --git a/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php b/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php
index 12ca241ae9..e94c1fb691 100644
--- a/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php
+++ b/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php
@@ -1,472 +1,456 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group slowvote
*/
final class PhabricatorSlowvotePollController
extends PhabricatorSlowvoteController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$viewer_phid = $user->getPHID();
$poll = id(new PhabricatorSlowvotePoll())->load($this->id);
if (!$poll) {
return new Aphront404Response();
}
$options = id(new PhabricatorSlowvoteOption())->loadAllWhere(
'pollID = %d',
$poll->getID());
$choices = id(new PhabricatorSlowvoteChoice())->loadAllWhere(
'pollID = %d',
$poll->getID());
$comments = id(new PhabricatorSlowvoteComment())->loadAllWhere(
'pollID = %d',
$poll->getID());
$choices_by_option = mgroup($choices, 'getOptionID');
$comments_by_user = mpull($comments, null, 'getAuthorPHID');
$choices_by_user = mgroup($choices, 'getAuthorPHID');
$viewer_choices = idx($choices_by_user, $viewer_phid, array());
$viewer_comment = idx($comments_by_user, $viewer_phid, null);
$comment_text = null;
if ($viewer_comment) {
$comment_text = $viewer_comment->getCommentText();
}
if ($request->isFormPost()) {
$comment = idx($comments_by_user, $viewer_phid, null);
if ($comment) {
$comment->delete();
}
$comment_text = $request->getStr('comments');
if (strlen($comment_text)) {
id(new PhabricatorSlowvoteComment())
->setAuthorPHID($viewer_phid)
->setPollID($poll->getID())
->setCommentText($comment_text)
->save();
}
$votes = $request->getArr('vote');
switch ($poll->getMethod()) {
case PhabricatorSlowvotePoll::METHOD_PLURALITY:
// Enforce only one vote.
$votes = array_slice($votes, 0, 1);
break;
case PhabricatorSlowvotePoll::METHOD_APPROVAL:
// No filtering.
break;
default:
throw new Exception("Unknown poll method!");
}
foreach ($viewer_choices as $viewer_choice) {
$viewer_choice->delete();
}
foreach ($votes as $vote) {
id(new PhabricatorSlowvoteChoice())
->setAuthorPHID($viewer_phid)
->setPollID($poll->getID())
->setOptionID($vote)
->save();
}
return id(new AphrontRedirectResponse())->setURI('/V'.$poll->getID());
}
require_celerity_resource('phabricator-slowvote-css');
$phids = array_merge(
mpull($choices, 'getAuthorPHID'),
mpull($comments, 'getAuthorPHID'),
array(
$poll->getAuthorPHID(),
));
$query = new PhabricatorObjectHandleData($phids);
$handles = $query->loadHandles();
$objects = $query->loadObjects();
if ($poll->getShuffle()) {
shuffle($options);
}
$option_markup = array();
foreach ($options as $option) {
$option_markup[] = $this->renderPollOption(
$poll,
$viewer_choices,
$option);
}
$option_markup = implode("\n", $option_markup);
$comments_by_option = array();
switch ($poll->getMethod()) {
case PhabricatorSlowvotePoll::METHOD_PLURALITY:
$choice_ids = array();
foreach ($choices_by_user as $user_phid => $user_choices) {
$choice_ids[$user_phid] = head($user_choices)->getOptionID();
}
foreach ($comments as $comment) {
$choice = idx($choice_ids, $comment->getAuthorPHID());
if ($choice) {
$comments_by_option[$choice][] = $comment;
}
}
break;
case PhabricatorSlowvotePoll::METHOD_APPROVAL:
// All comments are grouped in approval voting.
break;
default:
throw new Exception("Unknown poll method!");
}
$result_markup = $this->renderResultMarkup(
$poll,
$options,
$choices,
$comments,
$viewer_choices,
$choices_by_option,
$comments_by_option,
$handles,
$objects);
if ($viewer_choices) {
$instructions =
'Your vote has been recorded... but there is still ample time to '.
'rethink your position. Have you thoroughly considered all possible '.
'eventualities?';
} else {
$instructions =
'This is a weighty matter indeed. Consider your choices with the '.
'greatest of care.';
}
$form = id(new AphrontFormView())
->setUser($user)
->appendChild(
'<p class="aphront-form-instructions">'.$instructions.'</p>')
->appendChild(
id(new AphrontFormMarkupControl())
->setLabel('Vote')
->setValue($option_markup))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Comments')
->setHeight(AphrontFormTextAreaControl::HEIGHT_SHORT)
->setName('comments')
->setValue($comment_text))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Cautiously Engage in Deliberations'));
$panel = new AphrontPanelView();
$panel->setHeader(phutil_escape_html($poll->getQuestion()));
$panel->setWidth(AphrontPanelView::WIDTH_WIDE);
$panel->appendChild($form);
$panel->appendChild('<br /><br />');
$panel->appendChild($result_markup);
return $this->buildStandardPageResponse(
$panel,
array(
'title' => 'V'.$poll->getID().' '.$poll->getQuestion(),
));
}
private function renderComments(array $comments, array $handles) {
assert_instances_of($comments, 'PhabricatorSlowvoteComment');
assert_instances_of($handles, 'PhabricatorObjectHandle');
$viewer = $this->getRequest()->getUser();
$engine = PhabricatorMarkupEngine::newSlowvoteMarkupEngine();
$comment_markup = array();
foreach ($comments as $comment) {
$handle = $handles[$comment->getAuthorPHID()];
$markup = $engine->markupText($comment->getCommentText());
require_celerity_resource('phabricator-remarkup-css');
$comment_markup[] =
'<tr>'.
'<th>'.
$handle->renderLink().
'<div class="phabricator-slowvote-datestamp">'.
phabricator_datetime($comment->getDateCreated(), $viewer).
'</div>'.
'<td>'.
'<div class="phabricator-remarkup">'.
$markup.
'</div>'.
'</td>'.
'</tr>';
}
if ($comment_markup) {
$comment_markup = phutil_render_tag(
'table',
array(
'class' => 'phabricator-slowvote-comments',
),
implode("\n", $comment_markup));
} else {
$comment_markup = null;
}
return $comment_markup;
}
private function renderPollOption(
PhabricatorSlowvotePoll $poll,
array $viewer_choices,
PhabricatorSlowvoteOption $option) {
assert_instances_of($viewer_choices, 'PhabricatorSlowvoteChoice');
$id = $option->getID();
switch ($poll->getMethod()) {
case PhabricatorSlowvotePoll::METHOD_PLURALITY:
// Render a radio button.
$selected_option = head($viewer_choices);
if ($selected_option) {
$selected = $selected_option->getOptionID();
} else {
$selected = null;
}
if ($selected == $id) {
$checked = "checked";
} else {
$checked = null;
}
$input = phutil_render_tag(
'input',
array(
'type' => 'radio',
'name' => 'vote[]',
'value' => $id,
'checked' => $checked,
));
break;
case PhabricatorSlowvotePoll::METHOD_APPROVAL:
// Render a check box.
$checked = null;
foreach ($viewer_choices as $choice) {
if ($choice->getOptionID() == $id) {
$checked = 'checked';
break;
}
}
$input = phutil_render_tag(
'input',
array(
'type' => 'checkbox',
'name' => 'vote[]',
'checked' => $checked,
'value' => $id,
));
break;
default:
throw new Exception("Unknown poll method!");
}
if ($checked) {
$checked_class = 'phabricator-slowvote-checked';
} else {
$checked_class = null;
}
return phutil_render_tag(
'label',
array(
'class' => 'phabricator-slowvote-label '.$checked_class,
),
$input.phutil_escape_html($option->getName()));
}
private function renderVoteCount(
PhabricatorSlowvotePoll $poll,
array $choices,
array $chosen) {
assert_instances_of($choices, 'PhabricatorSlowvoteChoice');
assert_instances_of($chosen, 'PhabricatorSlowvoteChoice');
switch ($poll->getMethod()) {
case PhabricatorSlowvotePoll::METHOD_PLURALITY:
$out_of_total = count($choices);
break;
case PhabricatorSlowvotePoll::METHOD_APPROVAL:
// Count unique respondents for approval votes.
$out_of_total = count(mpull($choices, null, 'getAuthorPHID'));
break;
default:
throw new Exception("Unknown poll method!");
}
return sprintf(
'%d / %d (%d%%)',
number_format(count($chosen)),
number_format($out_of_total),
$out_of_total
? round(100 * count($chosen) / $out_of_total)
: 0);
}
private function renderResultMarkup(
PhabricatorSlowvotePoll $poll,
array $options,
array $choices,
array $comments,
array $viewer_choices,
array $choices_by_option,
array $comments_by_option,
array $handles,
array $objects) {
assert_instances_of($options, 'PhabricatorSlowvoteOption');
assert_instances_of($choices, 'PhabricatorSlowvoteChoice');
assert_instances_of($comments, 'PhabricatorSlowvoteComment');
assert_instances_of($viewer_choices, 'PhabricatorSlowvoteChoice');
assert_instances_of($handles, 'PhabricatorObjectHandle');
assert_instances_of($objects, 'PhabricatorLiskDAO');
$viewer_phid = $this->getRequest()->getUser()->getPHID();
$can_see_responses = false;
$need_vote = false;
switch ($poll->getResponseVisibility()) {
case PhabricatorSlowvotePoll::RESPONSES_VISIBLE:
$can_see_responses = true;
break;
case PhabricatorSlowvotePoll::RESPONSES_VOTERS:
$can_see_responses = (bool)$viewer_choices;
$need_vote = true;
break;
case PhabricatorSlowvotePoll::RESPONSES_OWNER:
$can_see_responses = ($viewer_phid == $poll->getAuthorPHID());
break;
}
$result_markup = id(new AphrontFormLayoutView())
->appendChild('<h1>Ongoing Deliberation</h1>');
if (!$can_see_responses) {
if ($need_vote) {
$reason = "You must vote to see the results.";
} else {
$reason = "The results are not public.";
}
$result_markup
->appendChild(
'<p class="aphront-form-instructions"><em>'.$reason.'</em></p>');
return $result_markup;
}
foreach ($options as $option) {
$id = $option->getID();
$chosen = idx($choices_by_option, $id, array());
$users = array_select_keys($handles, mpull($chosen, 'getAuthorPHID'));
if ($users) {
$user_markup = array();
foreach ($users as $handle) {
$object = idx($objects, $handle->getPHID());
if (!$object) {
continue;
}
$profile_image = $handle->getImageURI();
$user_markup[] = phutil_render_tag(
'a',
array(
'href' => $handle->getURI(),
'class' => 'phabricator-slowvote-facepile',
),
phutil_render_tag(
'img',
array(
'src' => $profile_image,
)));
}
$user_markup = implode('', $user_markup);
} else {
$user_markup = 'This option has failed to appeal to anyone.';
}
$comment_markup = $this->renderComments(
idx($comments_by_option, $id, array()),
$handles);
$vote_count = $this->renderVoteCount(
$poll,
$choices,
$chosen);
$result_markup->appendChild(
'<div>'.
'<div class="phabricator-slowvote-count">'.
$vote_count.
'</div>'.
'<h1>'.phutil_escape_html($option->getName()).'</h1>'.
'<hr class="phabricator-slowvote-hr" />'.
$user_markup.
'<div style="clear: both;">'.
'<hr class="phabricator-slowvote-hr" />'.
$comment_markup.
'</div>');
}
if ($poll->getMethod() == PhabricatorSlowvotePoll::METHOD_APPROVAL &&
$comments) {
$comment_markup = $this->renderComments(
$comments,
$handles);
$result_markup->appendChild(
'<h1>Motions Proposed for Consideration</h1>');
$result_markup->appendChild($comment_markup);
}
return $result_markup;
}
}
diff --git a/src/applications/slowvote/storage/PhabricatorSlowvoteChoice.php b/src/applications/slowvote/storage/PhabricatorSlowvoteChoice.php
index 95bec958ea..14cea8707f 100644
--- a/src/applications/slowvote/storage/PhabricatorSlowvoteChoice.php
+++ b/src/applications/slowvote/storage/PhabricatorSlowvoteChoice.php
@@ -1,28 +1,12 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group slowvote
*/
final class PhabricatorSlowvoteChoice extends PhabricatorSlowvoteDAO {
protected $pollID;
protected $optionID;
protected $authorPHID;
}
diff --git a/src/applications/slowvote/storage/PhabricatorSlowvoteComment.php b/src/applications/slowvote/storage/PhabricatorSlowvoteComment.php
index 5823f2406e..b72c99df01 100644
--- a/src/applications/slowvote/storage/PhabricatorSlowvoteComment.php
+++ b/src/applications/slowvote/storage/PhabricatorSlowvoteComment.php
@@ -1,28 +1,12 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group slowvote
*/
final class PhabricatorSlowvoteComment extends PhabricatorSlowvoteDAO {
protected $pollID;
protected $authorPHID;
protected $commentText;
}
diff --git a/src/applications/slowvote/storage/PhabricatorSlowvoteDAO.php b/src/applications/slowvote/storage/PhabricatorSlowvoteDAO.php
index e6cc45bd7e..e77f362226 100644
--- a/src/applications/slowvote/storage/PhabricatorSlowvoteDAO.php
+++ b/src/applications/slowvote/storage/PhabricatorSlowvoteDAO.php
@@ -1,28 +1,12 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group slowvote
*/
abstract class PhabricatorSlowvoteDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'slowvote';
}
}
diff --git a/src/applications/slowvote/storage/PhabricatorSlowvoteOption.php b/src/applications/slowvote/storage/PhabricatorSlowvoteOption.php
index 6459eac828..cb1c3feea1 100644
--- a/src/applications/slowvote/storage/PhabricatorSlowvoteOption.php
+++ b/src/applications/slowvote/storage/PhabricatorSlowvoteOption.php
@@ -1,28 +1,12 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group slowvote
*/
final class PhabricatorSlowvoteOption extends PhabricatorSlowvoteDAO {
protected $pollID;
protected $name;
}
diff --git a/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php b/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php
index 47b91d69df..8f93643479 100644
--- a/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php
+++ b/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php
@@ -1,49 +1,33 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group slowvote
*/
final class PhabricatorSlowvotePoll extends PhabricatorSlowvoteDAO {
const RESPONSES_VISIBLE = 0;
const RESPONSES_VOTERS = 1;
const RESPONSES_OWNER = 2;
const METHOD_PLURALITY = 0;
const METHOD_APPROVAL = 1;
protected $question;
protected $phid;
protected $authorPHID;
protected $responseVisibility;
protected $shuffle;
protected $method;
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_POLL);
}
}
diff --git a/src/applications/status/PhabricatorStatusController.php b/src/applications/status/PhabricatorStatusController.php
index ac6e6d2eea..c34e44979b 100644
--- a/src/applications/status/PhabricatorStatusController.php
+++ b/src/applications/status/PhabricatorStatusController.php
@@ -1,30 +1,14 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorStatusController extends PhabricatorController {
public function shouldRequireLogin() {
return false;
}
public function processRequest() {
$response = new AphrontWebpageResponse();
$response->setContent("ALIVE\n");
return $response;
}
}
diff --git a/src/applications/subscriptions/application/PhabricatorApplicationSubscriptions.php b/src/applications/subscriptions/application/PhabricatorApplicationSubscriptions.php
index 0e6da7e68b..21543d4eb9 100644
--- a/src/applications/subscriptions/application/PhabricatorApplicationSubscriptions.php
+++ b/src/applications/subscriptions/application/PhabricatorApplicationSubscriptions.php
@@ -1,41 +1,25 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationSubscriptions extends PhabricatorApplication {
public function shouldAppearInLaunchView() {
return false;
}
public function getEventListeners() {
return array(
new PhabricatorSubscriptionsUIEventListener(),
);
}
public function getRoutes() {
return array(
'/subscriptions/' => array(
'(?P<action>add|delete)/'.
'(?P<phid>[^/]+)/' => 'PhabricatorSubscriptionsEditController',
),
);
}
}
diff --git a/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php b/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php
index 78078e2079..80bd0c4239 100644
--- a/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php
+++ b/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php
@@ -1,106 +1,90 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorSubscriptionsEditController
extends PhabricatorController {
private $phid;
private $action;
public function willProcessRequest(array $data) {
$this->phid = idx($data, 'phid');
$this->action = idx($data, 'action');
}
public function processRequest() {
$request = $this->getRequest();
if (!$request->isFormPost()) {
return new Aphront400Response();
}
switch ($this->action) {
case 'add':
$is_add = true;
break;
case 'delete':
$is_add = false;
break;
default:
return new Aphront400Response();
}
$user = $request->getUser();
$phid = $this->phid;
// TODO: This is a policy test because `loadObjects()` is not currently
// policy-aware. Once it is, we can collapse this.
$handle = PhabricatorObjectHandleData::loadOneHandle($phid, $user);
if (!$handle->isComplete()) {
return new Aphront404Response();
}
$objects = id(new PhabricatorObjectHandleData(array($phid)))
->loadObjects();
$object = idx($objects, $phid);
if (!($object instanceof PhabricatorSubscribableInterface)) {
return $this->buildErrorResponse(
pht('Bad Object'),
pht('This object is not subscribable.'),
$handle->getURI());
}
if ($object->isAutomaticallySubscribed($user->getPHID())) {
return $this->buildErrorResponse(
pht('Automatically Subscribed'),
pht('You are automatically subscribed to this object.'),
$handle->getURI());
}
$editor = id(new PhabricatorSubscriptionsEditor())
->setActor($user)
->setObject($object);
if ($is_add) {
$editor->subscribeExplicit(array($user->getPHID()), $explicit = true);
} else {
$editor->unsubscribe(array($user->getPHID()));
}
$editor->save();
// TODO: We should just render the "Unsubscribe" action and swap it out
// in the document for Ajax requests.
return id(new AphrontReloadResponse())->setURI($handle->getURI());
}
private function buildErrorResponse($title, $message, $uri) {
$request = $this->getRequest();
$user = $request->getUser();
$dialog = id(new AphrontDialogView())
->setUser($user)
->setTitle($title)
->appendChild($message)
->addCancelButton($uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php b/src/applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php
index 37329f7940..ab73feda8b 100644
--- a/src/applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php
+++ b/src/applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php
@@ -1,119 +1,103 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorSubscriptionsEditor extends PhabricatorEditor {
private $object;
private $explicitSubscribePHIDs = array();
private $implicitSubscribePHIDs = array();
private $unsubscribePHIDs = array();
public function setObject(PhabricatorSubscribableInterface $object) {
$this->object = $object;
return $this;
}
/**
* Add explicit subscribers. These subscribers have explicitly subscribed
* (or been subscribed) to the object, and will be added even if they
* had previously unsubscribed.
*
* @param list<phid> List of PHIDs to explicitly subscribe.
* @return this
*/
public function subscribeExplicit(array $phids) {
$this->explicitSubscribePHIDs += array_fill_keys($phids, true);
return $this;
}
/**
* Add implicit subscribers. These subscribers have taken some action which
* implicitly subscribes them (e.g., adding a comment) but it will be
* suppressed if they've previously unsubscribed from the object.
*
* @param list<phid> List of PHIDs to implicitly subscribe.
* @return this
*/
public function subscribeImplicit(array $phids) {
$this->implicitSubscribePHIDs += array_fill_keys($phids, true);
return $this;
}
/**
* Unsubscribe PHIDs and mark them as unsubscribed, so implicit subscriptions
* will not resubscribe them.
*
* @param list<phid> List of PHIDs to unsubscribe.
* @return this
*/
public function unsubscribe(array $phids) {
$this->unsubscribePHIDs += array_fill_keys($phids, true);
return $this;
}
public function save() {
if (!$this->object) {
throw new Exception('Call setObject() before save()!');
}
$actor = $this->requireActor();
$src = $this->object->getPHID();
if ($this->implicitSubscribePHIDs) {
$unsub = PhabricatorEdgeQuery::loadDestinationPHIDs(
$src,
PhabricatorEdgeConfig::TYPE_OBJECT_HAS_UNSUBSCRIBER);
$unsub = array_fill_keys($unsub, true);
$this->implicitSubscribePHIDs = array_diff_key(
$this->implicitSubscribePHIDs,
$unsub);
}
$add = $this->implicitSubscribePHIDs + $this->explicitSubscribePHIDs;
$del = $this->unsubscribePHIDs;
// If a PHID is marked for both subscription and unsubscription, treat
// unsubscription as the stronger action.
$add = array_diff_key($add, $del);
if ($add || $del) {
$u_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_UNSUBSCRIBER;
$s_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_SUBSCRIBER;
$editor = id(new PhabricatorEdgeEditor())
->setActor($actor);
foreach ($add as $phid => $ignored) {
$editor->removeEdge($src, $u_type, $phid);
$editor->addEdge($src, $s_type, $phid);
}
foreach ($del as $phid => $ignored) {
$editor->removeEdge($src, $s_type, $phid);
$editor->addEdge($src, $u_type, $phid);
}
$editor->save();
}
}
}
diff --git a/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php b/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php
index 27c9dc8fb8..3a3c7deb3a 100644
--- a/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php
+++ b/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php
@@ -1,100 +1,84 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorSubscriptionsUIEventListener
extends PhutilEventListener {
public function register() {
$this->listen(PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS);
}
public function handleEvent(PhutilEvent $event) {
switch ($event->getType()) {
case PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS:
$this->handleActionEvent($event);
break;
}
}
private function handleActionEvent($event) {
$user = $event->getUser();
$object = $event->getValue('object');
if (!$object || !$object->getPHID()) {
// No object, or the object has no PHID yet. No way to subscribe.
return;
}
if (!($object instanceof PhabricatorSubscribableInterface)) {
// This object isn't subscribable.
return;
}
if ($object->isAutomaticallySubscribed($user->getPHID())) {
$sub_action = id(new PhabricatorActionView())
->setWorkflow(true)
->setUser($user)
->setDisabled(true)
->setRenderAsForm(true)
->setHref('/subscriptions/add/'.$object->getPHID().'/')
->setName(phutil_escape_html('Automatically Subscribed'))
->setIcon('subscribe-auto');
} else {
$subscribed = false;
if ($user->isLoggedIn()) {
$src_phid = $object->getPHID();
$dst_phid = $user->getPHID();
$edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_SUBSCRIBER;
$edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($src_phid))
->withEdgeTypes(array($edge_type))
->withDestinationPHIDs(array($user->getPHID()))
->execute();
$subscribed = isset($edges[$src_phid][$edge_type][$dst_phid]);
}
if ($subscribed) {
$sub_action = id(new PhabricatorActionView())
->setUser($user)
->setWorkflow(true)
->setRenderAsForm(true)
->setHref('/subscriptions/delete/'.$object->getPHID().'/')
->setName(phutil_escape_html('Unsubscribe'))
->setIcon('subscribe-delete');
} else {
$sub_action = id(new PhabricatorActionView())
->setUser($user)
->setWorkflow(true)
->setRenderAsForm(true)
->setHref('/subscriptions/add/'.$object->getPHID().'/')
->setName(phutil_escape_html('Subscribe'))
->setIcon('subscribe-add');
}
if (!$user->isLoggedIn()) {
$sub_action->setDisabled(true);
}
}
$actions = $event->getValue('actions');
$actions[] = $sub_action;
$event->setValue('actions', $actions);
}
}
diff --git a/src/applications/subscriptions/interface/PhabricatorSubscribableInterface.php b/src/applications/subscriptions/interface/PhabricatorSubscribableInterface.php
index 173e3b8ca4..8f87941ea5 100644
--- a/src/applications/subscriptions/interface/PhabricatorSubscribableInterface.php
+++ b/src/applications/subscriptions/interface/PhabricatorSubscribableInterface.php
@@ -1,32 +1,16 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
interface PhabricatorSubscribableInterface {
/**
* Return true to indicate that the given PHID is automatically subscribed
* to the object (for example, they are the author or in some other way
* irrevocably a subscriber). This will, e.g., cause the UI to render
* "Automatically Subscribed" instead of "Subscribe".
*
* @param PHID PHID (presumably a user) to test for automatic subscription.
* @return bool True if the object/user is automatically subscribed.
*/
public function isAutomaticallySubscribed($phid);
}
diff --git a/src/applications/subscriptions/query/PhabricatorSubscribersQuery.php b/src/applications/subscriptions/query/PhabricatorSubscribersQuery.php
index c2458227e6..c3a78a6900 100644
--- a/src/applications/subscriptions/query/PhabricatorSubscribersQuery.php
+++ b/src/applications/subscriptions/query/PhabricatorSubscribersQuery.php
@@ -1,66 +1,50 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorSubscribersQuery extends PhabricatorQuery {
private $objectPHIDs;
private $subscriberPHIDs;
public static function loadSubscribersForPHID($phid) {
$subscribers = id(new PhabricatorSubscribersQuery())
->withObjectPHIDs(array($phid))
->execute();
return $subscribers[$phid];
}
public function withObjectPHIDs(array $object_phids) {
$this->objectPHIDs = $object_phids;
return $this;
}
public function withSubscriberPHIDs(array $subscriber_phids) {
$this->subscriberPHIDs = $subscriber_phids;
return $this;
}
public function execute() {
$query = new PhabricatorEdgeQuery();
$edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_SUBSCRIBER;
$query->withSourcePHIDs($this->objectPHIDs);
$query->withEdgeTypes(array($edge_type));
if ($this->subscriberPHIDs) {
$query->withDestinationPHIDs($this->subscriberPHIDs);
}
$edges = $query->execute();
$results = array_fill_keys($this->objectPHIDs, array());
foreach ($edges as $src => $edge_types) {
foreach ($edge_types[$edge_type] as $dst => $data) {
$results[$src][] = $dst;
}
}
return $results;
}
}
diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php
index 6c27f85690..5d61db7853 100644
--- a/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php
+++ b/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php
@@ -1,346 +1,330 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorTypeaheadCommonDatasourceController
extends PhabricatorTypeaheadDatasourceController {
private $type;
public function willProcessRequest(array $data) {
$this->type = $data['type'];
}
public function processRequest() {
$request = $this->getRequest();
$query = $request->getStr('q');
$need_rich_data = false;
$need_users = false;
$need_applications = false;
$need_all_users = false;
$need_lists = false;
$need_projs = false;
$need_repos = false;
$need_packages = false;
$need_upforgrabs = false;
$need_arcanist_projects = false;
$need_noproject = false;
$need_symbols = false;
switch ($this->type) {
case 'mainsearch':
$need_users = true;
$need_applications = true;
$need_rich_data = true;
$need_symbols = true;
break;
case 'searchowner':
$need_users = true;
$need_upforgrabs = true;
break;
case 'searchproject':
$need_projs = true;
$need_noproject = true;
break;
case 'users':
$need_users = true;
break;
case 'mailable':
$need_users = true;
$need_lists = true;
break;
case 'allmailable':
$need_users = true;
$need_all_users = true;
$need_lists = true;
break;
case 'projects':
$need_projs = true;
break;
case 'usersorprojects':
$need_users = true;
$need_projs = true;
break;
case 'repositories':
$need_repos = true;
break;
case 'packages':
$need_packages = true;
break;
case 'accounts':
$need_users = true;
$need_all_users = true;
break;
case 'arcanistprojects':
$need_arcanist_projects = true;
break;
}
$results = array();
if ($need_upforgrabs) {
$results[] = id(new PhabricatorTypeaheadResult())
->setName('upforgrabs (Up For Grabs)')
->setPHID(ManiphestTaskOwner::OWNER_UP_FOR_GRABS);
}
if ($need_noproject) {
$results[] = id(new PhabricatorTypeaheadResult())
->setName('noproject (No Project)')
->setPHID(ManiphestTaskOwner::PROJECT_NO_PROJECT);
}
if ($need_users) {
$columns = array(
'isSystemAgent',
'isAdmin',
'isDisabled',
'userName',
'realName',
'phid');
if ($query) {
// This is an arbitrary limit which is just larger than any limit we
// actually use in the application.
// TODO: The datasource should pass this in the query.
$limit = 15;
$user_table = new PhabricatorUser();
$conn_r = $user_table->establishConnection('r');
$ids = queryfx_all(
$conn_r,
'SELECT id FROM %T WHERE username LIKE %>
ORDER BY username ASC LIMIT %d',
$user_table->getTableName(),
$query,
$limit);
$ids = ipull($ids, 'id');
if (count($ids) < $limit) {
// If we didn't find enough username hits, look for real name hits.
// We need to pull the entire pagesize so that we end up with the
// right number of items if this query returns many duplicate IDs
// that we've already selected.
$realname_ids = queryfx_all(
$conn_r,
'SELECT DISTINCT userID FROM %T WHERE token LIKE %>
ORDER BY token ASC LIMIT %d',
PhabricatorUser::NAMETOKEN_TABLE,
$query,
$limit);
$realname_ids = ipull($realname_ids, 'userID');
$ids = array_merge($ids, $realname_ids);
$ids = array_unique($ids);
$ids = array_slice($ids, 0, $limit);
}
// Always add the logged-in user because some tokenizers autosort them
// first. They'll be filtered out on the client side if they don't
// match the query.
$ids[] = $request->getUser()->getID();
if ($ids) {
$users = id(new PhabricatorUser())->loadColumnsWhere(
$columns,
'id IN (%Ld)',
$ids);
} else {
$users = array();
}
} else {
$users = id(new PhabricatorUser())->loadColumns($columns);
}
if ($need_rich_data) {
$phids = mpull($users, 'getPHID');
$handles = $this->loadViewerHandles($phids);
}
foreach ($users as $user) {
if (!$need_all_users) {
if ($user->getIsSystemAgent()) {
continue;
}
if ($user->getIsDisabled()) {
continue;
}
}
$result = id(new PhabricatorTypeaheadResult())
->setName($user->getFullName())
->setURI('/p/'.$user->getUsername())
->setPHID($user->getPHID())
->setPriorityString($user->getUsername());
if ($need_rich_data) {
$display_type = 'User';
if ($user->getIsAdmin()) {
$display_type = 'Administrator';
}
$result->setDisplayType($display_type);
$result->setImageURI($handles[$user->getPHID()]->getImageURI());
$result->setPriorityType('user');
}
$results[] = $result;
}
}
if ($need_lists) {
$lists = id(new PhabricatorMetaMTAMailingList())->loadAll();
foreach ($lists as $list) {
$results[] = id(new PhabricatorTypeaheadResult())
->setName($list->getName())
->setURI($list->getURI())
->setPHID($list->getPHID());
}
}
if ($need_projs) {
$projs = id(new PhabricatorProject())->loadAllWhere(
'status != %d',
PhabricatorProjectStatus::STATUS_ARCHIVED);
foreach ($projs as $proj) {
$results[] = id(new PhabricatorTypeaheadResult())
->setName($proj->getName())
->setURI('/project/view/'.$proj->getID().'/')
->setPHID($proj->getPHID());
}
}
if ($need_repos) {
$repos = id(new PhabricatorRepository())->loadAll();
foreach ($repos as $repo) {
$results[] = id(new PhabricatorTypeaheadResult())
->setName('r'.$repo->getCallsign().' ('.$repo->getName().')')
->setURI('/diffusion/'.$repo->getCallsign().'/')
->setPHID($repo->getPHID())
->setPriorityString('r'.$repo->getCallsign());
}
}
if ($need_packages) {
$packages = id(new PhabricatorOwnersPackage())->loadAll();
foreach ($packages as $package) {
$results[] = id(new PhabricatorTypeaheadResult())
->setName($package->getName())
->setURI('/owners/package/'.$package->getID().'/')
->setPHID($package->getPHID());
}
}
if ($need_arcanist_projects) {
$arcprojs = id(new PhabricatorRepositoryArcanistProject())->loadAll();
foreach ($arcprojs as $proj) {
$results[] = id(new PhabricatorTypeaheadResult())
->setName($proj->getName())
->setPHID($proj->getPHID());
}
}
if ($need_applications) {
$applications = PhabricatorApplication::getAllInstalledApplications();
foreach ($applications as $application) {
$uri = $application->getTypeaheadURI();
if (!$uri) {
continue;
}
$name = $application->getName().' '.$application->getShortDescription();
$results[] = id(new PhabricatorTypeaheadResult())
->setName($name)
->setURI($uri)
->setPHID($application->getPHID())
->setPriorityString($application->getName())
->setDisplayName($application->getName())
->setDisplayType($application->getShortDescription())
->setImageuRI($application->getIconURI())
->setPriorityType('apps');
}
}
if ($need_symbols) {
$symbols = id(new DiffusionSymbolQuery())
->setNamePrefix($query)
->setLimit(15)
->needArcanistProjects(true)
->needRepositories(true)
->needPaths(true)
->execute();
foreach ($symbols as $symbol) {
$lang = $symbol->getSymbolLanguage();
$name = $symbol->getSymbolName();
$type = $symbol->getSymbolType();
$proj = $symbol->getArcanistProject()->getName();
$results[] = id(new PhabricatorTypeaheadResult())
->setName($name)
->setURI($symbol->getURI())
->setPHID(md5($symbol->getURI())) // Just needs to be unique.
->setDisplayName($name)
->setDisplayType(strtoupper($lang).' '.ucwords($type).' ('.$proj.')')
->setPriorityType('symb');
}
}
$content = mpull($results, 'getWireFormat');
if ($request->isAjax()) {
return id(new AphrontAjaxResponse())->setContent($content);
}
// If there's a non-Ajax request to this endpoint, show results in a tabular
// format to make it easier to debug typeahead output.
$rows = array();
foreach ($results as $result) {
$wire = $result->getWireFormat();
foreach ($wire as $k => $v) {
$wire[$k] = phutil_escape_html($v);
}
$rows[] = $wire;
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Name',
'URI',
'PHID',
'Priority',
'Display Name',
'Display Type',
'Image URI',
'Priority Type',
));
$panel = new AphrontPanelView();
$panel->setHeader('Typeahead Results');
$panel->appendChild($table);
return $this->buildStandardPageResponse(
$panel,
array(
'title' => 'Typeahead Results',
));
}
}
diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadDatasourceController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadDatasourceController.php
index a58587bb9c..b326201321 100644
--- a/src/applications/typeahead/controller/PhabricatorTypeaheadDatasourceController.php
+++ b/src/applications/typeahead/controller/PhabricatorTypeaheadDatasourceController.php
@@ -1,26 +1,10 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorTypeaheadDatasourceController
extends PhabricatorController {
public function getApplicationName() {
return 'typeahead';
}
}
diff --git a/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php b/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php
index 1cba76aa9d..8627e5bdda 100644
--- a/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php
+++ b/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php
@@ -1,88 +1,72 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorTypeaheadResult {
private $name;
private $uri;
private $phid;
private $priorityString;
private $displayName;
private $displayType;
private $imageURI;
private $priorityType;
public function setName($name) {
$this->name = $name;
return $this;
}
public function setURI($uri) {
$this->uri = $uri;
return $this;
}
public function setPHID($phid) {
$this->phid = $phid;
return $this;
}
public function setPriorityString($priority_string) {
$this->priorityString = $priority_string;
return $this;
}
public function setDisplayName($display_name) {
$this->displayName = $display_name;
return $this;
}
public function setDisplayType($display_type) {
$this->displayType = $display_type;
return $this;
}
public function setImageURI($image_uri) {
$this->imageURI = $image_uri;
return $this;
}
public function setPriorityType($priority_type) {
$this->priorityType = $priority_type;
return $this;
}
public function getWireFormat() {
$data = array(
$this->name,
$this->uri ? (string)$this->uri : null,
$this->phid,
$this->priorityString,
$this->displayName,
$this->displayType,
$this->imageURI ? (string)$this->imageURI : null,
$this->priorityType,
);
while (end($data) === null) {
array_pop($data);
}
return $data;
}
}
diff --git a/src/applications/uiexample/application/PhabricatorApplicationUIExamples.php b/src/applications/uiexample/application/PhabricatorApplicationUIExamples.php
index a0c899f4a0..dcdf7c35af 100644
--- a/src/applications/uiexample/application/PhabricatorApplicationUIExamples.php
+++ b/src/applications/uiexample/application/PhabricatorApplicationUIExamples.php
@@ -1,58 +1,42 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationUIExamples extends PhabricatorApplication {
public function getBaseURI() {
return '/uiexample/';
}
public function getShortDescription() {
return 'Developer UI Examples';
}
public function getAutospriteName() {
return 'uiexample';
}
public function getTitleGlyph() {
return "\xE2\x8F\x9A";
}
public function getFlavorText() {
return pht('A gallery of modern art.');
}
public function getApplicationGroup() {
return self::GROUP_DEVELOPER;
}
public function getApplicationOrder() {
return 0.110;
}
public function getRoutes() {
return array(
'/uiexample/' => array(
'' => 'PhabricatorUIExampleRenderController',
'view/(?P<class>[^/]+)/' => 'PhabricatorUIExampleRenderController',
),
);
}
}
diff --git a/src/applications/uiexample/controller/PhabricatorUIExampleRenderController.php b/src/applications/uiexample/controller/PhabricatorUIExampleRenderController.php
index 556871358b..4da5cb002a 100644
--- a/src/applications/uiexample/controller/PhabricatorUIExampleRenderController.php
+++ b/src/applications/uiexample/controller/PhabricatorUIExampleRenderController.php
@@ -1,83 +1,67 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorUIExampleRenderController extends PhabricatorController {
private $class;
public function willProcessRequest(array $data) {
$this->class = idx($data, 'class');
}
public function processRequest() {
$classes = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorUIExample')
->setConcreteOnly(true)
->selectAndLoadSymbols();
$classes = ipull($classes, 'name', 'name');
foreach ($classes as $class => $ignored) {
$classes[$class] = newv($class, array());
}
$classes = msort($classes, 'getName');
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($this->getApplicationURI('view/')));
foreach ($classes as $class => $obj) {
$name = $obj->getName();
$nav->addFilter($class, $name);
}
$selected = $nav->selectFilter($this->class, head_key($classes));
$example = $classes[$selected];
$example->setRequest($this->getRequest());
$result = $example->renderExample();
if ($result instanceof AphrontResponse) {
// This allows examples to generate dialogs, etc., for demonstration.
return $result;
}
require_celerity_resource('phabricator-ui-example-css');
$nav->appendChild(
'<div class="phabricator-ui-example-header">'.
'<h1 class="phabricator-ui-example-name">'.
phutil_escape_html($example->getName()).
' ('.get_class($example).')'.
'</h1>'.
'<p class="phabricator-ui-example-description">'
.$example->getDescription().
'</p>'.
'</div>');
$nav->appendChild($result);
return $this->buildApplicationPage(
$nav,
array(
'title' => 'UI Example',
'device' => true,
));
}
}
diff --git a/src/applications/uiexample/examples/JavelinReactorExample.php b/src/applications/uiexample/examples/JavelinReactorExample.php
index d0ae394fa5..95a88b8c0c 100644
--- a/src/applications/uiexample/examples/JavelinReactorExample.php
+++ b/src/applications/uiexample/examples/JavelinReactorExample.php
@@ -1,106 +1,90 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class JavelinReactorExample extends PhabricatorUIExample {
public function getName() {
return 'Javelin Reactor';
}
public function getDescription() {
return 'Lots of code';
}
public function renderExample() {
$rows = array();
$examples = array(
array(
'Reactive button only generates a stream of events',
'ReactorButtonExample',
'phabricator-uiexample-reactor-button',
array(),
),
array(
'Reactive checkbox generates a boolean dynamic value',
'ReactorCheckboxExample',
'phabricator-uiexample-reactor-checkbox',
array('checked' => true)
),
array(
'Reactive focus detector generates a boolean dynamic value',
'ReactorFocusExample',
'phabricator-uiexample-reactor-focus',
array(),
),
array(
'Reactive input box, with normal and calmed output',
'ReactorInputExample',
'phabricator-uiexample-reactor-input',
array('init' => 'Initial value'),
),
array(
'Reactive mouseover detector generates a boolean dynamic value',
'ReactorMouseoverExample',
'phabricator-uiexample-reactor-mouseover',
array(),
),
array(
'Reactive radio buttons generate a string dynamic value',
'ReactorRadioExample',
'phabricator-uiexample-reactor-radio',
array(),
),
array(
'Reactive select box generates a string dynamic value',
'ReactorSelectExample',
'phabricator-uiexample-reactor-select',
array(),
),
array(
'sendclass makes the class of an element a string dynamic value',
'ReactorSendClassExample',
'phabricator-uiexample-reactor-sendclass',
array()
),
array(
'sendproperties makes some properties of an object into dynamic values',
'ReactorSendPropertiesExample',
'phabricator-uiexample-reactor-sendproperties',
array(),
),
);
foreach ($examples as $example) {
list($desc, $name, $resource, $params) = $example;
$template = new AphrontJavelinView();
$template
->setName($name)
->setParameters($params)
->setCelerityResource($resource);
$rows[] = array($desc, $template->render());
}
$table = new AphrontTableView($rows);
$panel = new AphrontPanelView();
$panel->appendChild($table);
return $panel;
}
}
diff --git a/src/applications/uiexample/examples/JavelinUIExample.php b/src/applications/uiexample/examples/JavelinUIExample.php
index a420942dbd..9b86f3f9a6 100644
--- a/src/applications/uiexample/examples/JavelinUIExample.php
+++ b/src/applications/uiexample/examples/JavelinUIExample.php
@@ -1,82 +1,66 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class JavelinUIExample extends PhabricatorUIExample {
public function getName() {
return 'Javelin UI';
}
public function getDescription() {
return 'Here are some Javelin UI elements that you could use.';
}
public function renderExample() {
$request = $this->getRequest();
$user = $request->getUser();
// toggle-class
$container_id = celerity_generate_unique_node_id();
$button_red_id = celerity_generate_unique_node_id();
$button_blue_id = celerity_generate_unique_node_id();
$button_red = javelin_render_tag(
'a',
array(
'class' => 'button',
'sigil' => 'jx-toggle-class',
'href' => '#',
'id' => $button_red_id,
'meta' => array(
'map' => array(
$container_id => 'jxui-red-border',
$button_red_id => 'jxui-active',
),
),
),
'Toggle Red Border');
$button_blue = javelin_render_tag(
'a',
array(
'class' => 'button jxui-active',
'sigil' => 'jx-toggle-class',
'href' => '#',
'id' => $button_blue_id,
'meta' => array(
'state' => true,
'map' => array(
$container_id => 'jxui-blue-background',
$button_blue_id => 'jxui-active',
),
),
),
'Toggle Blue Background');
$div = phutil_render_tag(
'div',
array(
'id' => $container_id,
'class' => 'jxui-example-container jxui-blue-background',
),
$button_red.$button_blue);
return array($div);
}
}
diff --git a/src/applications/uiexample/examples/JavelinViewExample.php b/src/applications/uiexample/examples/JavelinViewExample.php
index dc89deee07..2ea205f818 100644
--- a/src/applications/uiexample/examples/JavelinViewExample.php
+++ b/src/applications/uiexample/examples/JavelinViewExample.php
@@ -1,59 +1,43 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class JavelinViewExample extends PhabricatorUIExample {
public function getName() {
return 'Javelin Views';
}
public function getDescription() {
return 'Mix and match client and server views.';
}
public function renderExample() {
$request = $this->getRequest();
$init = $request->getStr('init');
$parent_server_template = new JavelinViewExampleServerView();
$parent_client_template = new AphrontJavelinView();
$parent_client_template
->setName('JavelinViewExample')
->setCelerityResource('phabricator-uiexample-javelin-view');
$child_server_template = new JavelinViewExampleServerView();
$child_client_template = new AphrontJavelinView();
$child_client_template
->setName('JavelinViewExample')
->setCelerityResource('phabricator-uiexample-javelin-view');
$parent_server_template->appendChild($parent_client_template);
$parent_client_template->appendChild($child_server_template);
$child_server_template->appendChild($child_client_template);
$child_client_template->appendChild('Hey, it worked.');
$panel = new AphrontPanelView();
$panel->appendChild($parent_server_template);
return $panel;
}
}
diff --git a/src/applications/uiexample/examples/JavelinViewExampleServerView.php b/src/applications/uiexample/examples/JavelinViewExampleServerView.php
index f4fb2d46cd..ba02d27cd9 100644
--- a/src/applications/uiexample/examples/JavelinViewExampleServerView.php
+++ b/src/applications/uiexample/examples/JavelinViewExampleServerView.php
@@ -1,27 +1,11 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class JavelinViewExampleServerView extends AphrontView {
public function render() {
return phutil_render_tag(
'div',
array('class' => 'server-view'),
$this->renderChildren()
);
}
}
diff --git a/src/applications/uiexample/examples/PhabricatorActionListExample.php b/src/applications/uiexample/examples/PhabricatorActionListExample.php
index 6dd65b88e6..2922d95cee 100644
--- a/src/applications/uiexample/examples/PhabricatorActionListExample.php
+++ b/src/applications/uiexample/examples/PhabricatorActionListExample.php
@@ -1,127 +1,111 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorActionListExample extends PhabricatorUIExample {
public function getName() {
return 'Action List';
}
public function getDescription() {
return 'Use <tt>PhabricatorActionListView</tt> to render object actions.';
}
public function renderExample() {
$request = $this->getRequest();
$user = $request->getUser();
$notices = array();
if ($request->isFormPost()) {
$notices[] = 'You just submitted a valid form POST.';
}
if ($request->isJavelinWorkflow()) {
$notices[] = 'You just submitted a Workflow request.';
}
if ($notices) {
$notices = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
->setErrors($notices);
} else {
$notices = null;
}
if ($request->isJavelinWorkflow()) {
$dialog = new AphrontDialogView();
$dialog->setUser($user);
$dialog->setTitle('Request Information');
$dialog->appendChild($notices);
$dialog->addCancelButton($request->getRequestURI(), 'Close');
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$view = new PhabricatorActionListView();
$view->setUser($user);
$view->addAction(
id(new PhabricatorActionView())
->setUser($user)
->setHref($request->getRequestURI())
->setName('Normal Action')
->setIcon('file'));
$view->addAction(
id(new PhabricatorActionView())
->setUser($user)
->setHref($request->getRequestURI())
->setDisabled(true)
->setName('Disabled Action')
->setIcon('file'));
$view->addAction(
id(new PhabricatorActionView())
->setUser($user)
->setHref($request->getRequestURI())
->setRenderAsForm(true)
->setName('Form Action')
->setIcon('file'));
$view->addAction(
id(new PhabricatorActionView())
->setUser($user)
->setHref($request->getRequestURI())
->setRenderAsForm(true)
->setDisabled(true)
->setName('Disabled Form Action')
->setIcon('file'));
$view->addAction(
id(new PhabricatorActionView())
->setUser($user)
->setHref($request->getRequestURI())
->setWorkflow(true)
->setName('Workflow Action')
->setIcon('file'));
$view->addAction(
id(new PhabricatorActionView())
->setUser($user)
->setHref($request->getRequestURI())
->setRenderAsForm(true)
->setWorkflow(true)
->setName('Form + Workflow Action')
->setIcon('file'));
foreach (PhabricatorActionView::getAvailableIcons() as $icon) {
$view->addAction(
id(new PhabricatorActionView())
->setUser($user)
->setHref('#')
->setDisabled(true)
->setName('Icon "'.$icon.'"')
->setIcon($icon));
}
return array(
$view,
'<div style="clear: both;"></div>',
$notices,
);
}
}
diff --git a/src/applications/uiexample/examples/PhabricatorBarePageExample.php b/src/applications/uiexample/examples/PhabricatorBarePageExample.php
index 598673bd0e..5a3bf279c4 100644
--- a/src/applications/uiexample/examples/PhabricatorBarePageExample.php
+++ b/src/applications/uiexample/examples/PhabricatorBarePageExample.php
@@ -1,41 +1,25 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorBarePageExample extends PhabricatorUIExample {
public function getName() {
return 'Bare Page';
}
public function getDescription() {
return 'This is a bare page.';
}
public function renderExample() {
$view = new PhabricatorBarePageView();
$view->appendChild(
phutil_render_tag(
'h1',
array(),
phutil_escape_html($this->getDescription())));
$response = new AphrontWebpageResponse();
$response->setContent($view->render());
return $response;
}
}
diff --git a/src/applications/uiexample/examples/PhabricatorErrorExample.php b/src/applications/uiexample/examples/PhabricatorErrorExample.php
index ce85a2585b..5f1a1a5cfb 100644
--- a/src/applications/uiexample/examples/PhabricatorErrorExample.php
+++ b/src/applications/uiexample/examples/PhabricatorErrorExample.php
@@ -1,58 +1,42 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorErrorExample extends PhabricatorUIExample {
public function getName() {
return 'Errors';
}
public function getDescription() {
return 'Use <tt>AphrontErrorView</tt> to render errors, warnings and '.
'notices.';
}
public function renderExample() {
$request = $this->getRequest();
$user = $request->getUser();
$sevs = array(
AphrontErrorView::SEVERITY_ERROR => 'Error',
AphrontErrorView::SEVERITY_WARNING => 'Warning',
AphrontErrorView::SEVERITY_NOTICE => 'Notice',
AphrontErrorView::SEVERITY_NODATA => 'No Data',
);
$views = array();
foreach ($sevs as $sev => $title) {
$view = new AphrontErrorView();
$view->setSeverity($sev);
$view->setTitle($title);
$view->appendChild('Several issues were encountered.');
$view->setErrors(
array(
'Overcooked.',
'Too much salt.',
'Full of sand.',
));
$views[] = $view;
}
return $views;
}
}
diff --git a/src/applications/uiexample/examples/PhabricatorFormExample.php b/src/applications/uiexample/examples/PhabricatorFormExample.php
index 168666e1c0..c045766fa7 100644
--- a/src/applications/uiexample/examples/PhabricatorFormExample.php
+++ b/src/applications/uiexample/examples/PhabricatorFormExample.php
@@ -1,58 +1,42 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFormExample extends PhabricatorUIExample {
public function getName() {
return 'Form';
}
public function getDescription() {
return 'Use <tt>AphrontFormView</tt> to render forms.';
}
public function renderExample() {
$request = $this->getRequest();
$user = $request->getUser();
$start_time = id(new AphrontFormDateControl())
->setUser($user)
->setName('start')
->setLabel('Start')
->setInitialTime(AphrontFormDateControl::TIME_START_OF_BUSINESS);
$start_value = $start_time->readValueFromRequest($request);
$end_time = id(new AphrontFormDateControl())
->setUser($user)
->setName('end')
->setLabel('End')
->setInitialTime(AphrontFormDateControl::TIME_END_OF_BUSINESS);
$end_value = $end_time->readValueFromRequest($request);
$form = id(new AphrontFormView())
->setUser($user)
->setFlexible(true)
->appendChild($start_time)
->appendChild($end_time)
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Submit'));
return $form;
}
}
diff --git a/src/applications/uiexample/examples/PhabricatorPropertyListExample.php b/src/applications/uiexample/examples/PhabricatorPropertyListExample.php
index e2a7ea37db..b5ee7c2d88 100644
--- a/src/applications/uiexample/examples/PhabricatorPropertyListExample.php
+++ b/src/applications/uiexample/examples/PhabricatorPropertyListExample.php
@@ -1,56 +1,40 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorPropertyListExample extends PhabricatorUIExample {
public function getName() {
return 'Property List';
}
public function getDescription() {
return 'Use <tt>PhabricatorPropertyListView</tt> to render object '.
'properties.';
}
public function renderExample() {
$request = $this->getRequest();
$user = $request->getUser();
$view = new PhabricatorPropertyListView();
$view->addProperty(
pht('Color'),
pht('Yellow'));
$view->addProperty(
pht('Size'),
pht('Mouse'));
$view->addProperty(
pht('Element'),
pht('Electric'));
$view->addTextContent(
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. '.
'Quisque rhoncus tempus massa, sit amet faucibus lectus bibendum '.
'viverra. Nunc tempus tempor quam id iaculis. Maecenas lectus '.
'velit, aliquam et consequat quis, tincidunt id dolor.');
return $view;
}
}
diff --git a/src/applications/uiexample/examples/PhabricatorSortTableExample.php b/src/applications/uiexample/examples/PhabricatorSortTableExample.php
index d60d7abc45..de7df560cd 100644
--- a/src/applications/uiexample/examples/PhabricatorSortTableExample.php
+++ b/src/applications/uiexample/examples/PhabricatorSortTableExample.php
@@ -1,112 +1,96 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorSortTableExample extends PhabricatorUIExample {
public function getName() {
return 'Sortable Tables';
}
public function getDescription() {
return 'Using sortable tables.';
}
public function renderExample() {
$rows = array(
array(
'make' => 'Honda',
'model' => 'Civic',
'year' => 2004,
'price' => 3199,
'color' => 'Blue',
),
array(
'make' => 'Ford',
'model' => 'Focus',
'year' => 2001,
'price' => 2549,
'color' => 'Red',
),
array(
'make' => 'Toyota',
'model' => 'Camry',
'year' => 2009,
'price' => 4299,
'color' => 'Black',
),
array(
'make' => 'NASA',
'model' => 'Shuttle',
'year' => 1998,
'price' => 1000000000,
'color' => 'White',
),
);
$request = $this->getRequest();
$orders = array(
'make',
'model',
'year',
'price',
);
$sort = $request->getStr('sort');
list($sort, $reverse) = AphrontTableView::parseSort($sort);
if (!in_array($sort, $orders)) {
$sort = 'make';
}
$rows = isort($rows, $sort);
if ($reverse) {
$rows = array_reverse($rows);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Make',
'Model',
'Year',
'Price',
'Color',
));
$table->setColumnClasses(
array(
'',
'wide',
'n',
'n',
'',
));
$table->makeSortable(
$request->getRequestURI(),
'sort',
$sort,
$reverse,
$orders);
$panel = new AphrontPanelView();
$panel->setHeader('Sortable Table of Vehicles');
$panel->appendChild($table);
return $panel;
}
}
diff --git a/src/applications/uiexample/examples/PhabricatorUIExample.php b/src/applications/uiexample/examples/PhabricatorUIExample.php
index 17043617f2..6688e489b3 100644
--- a/src/applications/uiexample/examples/PhabricatorUIExample.php
+++ b/src/applications/uiexample/examples/PhabricatorUIExample.php
@@ -1,36 +1,20 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorUIExample {
private $request;
public function setRequest($request) {
$this->request = $request;
return $this;
}
public function getRequest() {
return $this->request;
}
abstract public function getName();
abstract public function getDescription();
abstract public function renderExample();
}
diff --git a/src/applications/uiexample/examples/PhabricatorUIListFilterExample.php b/src/applications/uiexample/examples/PhabricatorUIListFilterExample.php
index 29c3ca80f6..834f3316b3 100644
--- a/src/applications/uiexample/examples/PhabricatorUIListFilterExample.php
+++ b/src/applications/uiexample/examples/PhabricatorUIListFilterExample.php
@@ -1,57 +1,41 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorUIListFilterExample extends PhabricatorUIExample {
public function getName() {
return 'ListFilter';
}
public function getDescription() {
return 'Use <tt>AphrontListFilterView</tt> to layout controls for '.
'filtering and manipulating lists of objects.';
}
public function renderExample() {
$filter = new AphrontListFilterView();
$filter->addButton(
phutil_render_tag(
'a',
array(
'href' => '#',
'class' => 'button green',
),
'Create New Thing'));
$form = new AphrontFormView();
$form->setUser($this->getRequest()->getUser());
$form
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Query'))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Search'));
$filter->appendChild($form);
return $filter;
}
}
diff --git a/src/applications/uiexample/examples/PhabricatorUINotificationExample.php b/src/applications/uiexample/examples/PhabricatorUINotificationExample.php
index fd5082b62e..c112a33fcd 100644
--- a/src/applications/uiexample/examples/PhabricatorUINotificationExample.php
+++ b/src/applications/uiexample/examples/PhabricatorUINotificationExample.php
@@ -1,46 +1,30 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorUINotificationExample extends PhabricatorUIExample {
public function getName() {
return 'Notifications';
}
public function getDescription() {
return 'Use <tt>JX.Notification</tt> to create notifications.';
}
public function renderExample() {
require_celerity_resource('phabricator-notification-css');
Javelin::initBehavior('phabricator-notification-example');
$content = javelin_render_tag(
'a',
array(
'sigil' => 'notification-example',
'class' => 'button green',
),
'Show Notification');
$content = '<div style="padding: 1em 3em;">'.$content.'</content>';
return $content;
}
}
diff --git a/src/applications/uiexample/examples/PhabricatorUIPagerExample.php b/src/applications/uiexample/examples/PhabricatorUIPagerExample.php
index fbde21decb..aabacecd90 100644
--- a/src/applications/uiexample/examples/PhabricatorUIPagerExample.php
+++ b/src/applications/uiexample/examples/PhabricatorUIPagerExample.php
@@ -1,94 +1,78 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorUIPagerExample extends PhabricatorUIExample {
public function getName() {
return 'Pager';
}
public function getDescription() {
return 'Use <tt>AphrontPagerView</tt> to create a control which allows '.
'users to paginate through large amounts of content.';
}
public function renderExample() {
$request = $this->getRequest();
$offset = (int)$request->getInt('offset');
$page_size = 20;
$item_count = 173;
$rows = array();
for ($ii = $offset; $ii < min($item_count, $offset + $page_size); $ii++) {
$rows[] = array(
'Item #'.($ii + 1),
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Item',
));
$panel = new AphrontPanelView();
$panel->appendChild($table);
$panel->appendChild(
'<p class="phabricator-ui-example-note">'.
'Use <tt>AphrontPagerView</tt> to render a pager element.'.
'</p>');
$pager = new AphrontPagerView();
$pager->setPageSize($page_size);
$pager->setOffset($offset);
$pager->setCount($item_count);
$pager->setURI($request->getRequestURI(), 'offset');
$panel->appendChild($pager);
$panel->appendChild(
'<p class="phabricator-ui-example-note">'.
'You can show more or fewer pages of surrounding context.'.
'</p>');
$many_pages_pager = new AphrontPagerView();
$many_pages_pager->setPageSize($page_size);
$many_pages_pager->setOffset($offset);
$many_pages_pager->setCount($item_count);
$many_pages_pager->setURI($request->getRequestURI(), 'offset');
$many_pages_pager->setSurroundingPages(7);
$panel->appendChild($many_pages_pager);
$panel->appendChild(
'<p class="phabricator-ui-example-note">'.
'When it is prohibitively expensive or complex to attain a complete '.
'count of the items, you can select one extra item and set '.
'<tt>hasMorePages(true)</tt> if it exists, creating an inexact pager.'.
'</p>');
$inexact_pager = new AphrontPagerView();
$inexact_pager->setPageSize($page_size);
$inexact_pager->setOffset($offset);
$inexact_pager->setHasMorePages($offset < ($item_count - $page_size));
$inexact_pager->setURI($request->getRequestURI(), 'offset');
$panel->appendChild($inexact_pager);
return $panel;
}
}
diff --git a/src/applications/uiexample/examples/PhabricatorUITooltipExample.php b/src/applications/uiexample/examples/PhabricatorUITooltipExample.php
index 69c5accb3e..114124d1fb 100644
--- a/src/applications/uiexample/examples/PhabricatorUITooltipExample.php
+++ b/src/applications/uiexample/examples/PhabricatorUITooltipExample.php
@@ -1,94 +1,78 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorUITooltipExample extends PhabricatorUIExample {
public function getName() {
return 'Tooltips';
}
public function getDescription() {
return 'Use <tt>JX.Tooltip</tt> to create tooltips.';
}
public function renderExample() {
Javelin::initBehavior('phabricator-tooltips');
require_celerity_resource('aphront-tooltip-css');
$style = 'width: 200px; '.
'height: 200px '.
'text-align: center; '.
'margin: 20px; '.
'background: #dfdfdf; '.
'padding: 30px 10px; '.
'border: 1px solid black; ';
$lorem = <<<EOTEXT
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque eget urna
sed ante ultricies consequat id a odio. Mauris interdum volutpat sapien eu
accumsan. In hac habitasse platea dictumst. Lorem ipsum dolor sit amet,
consectetur adipiscing elit.
EOTEXT;
$overflow = str_repeat('M', 1024);
$metas = array(
'hi' => array(
'tip' => 'Hi',
),
'lorem' => array(
'tip' => $lorem,
),
'lorem (east)' => array(
'tip' => $lorem,
'align' => 'E',
),
'lorem (large)' => array(
'tip' => $lorem,
'size' => 300,
),
'lorem (large, east)' => array(
'tip' => $lorem,
'size' => 300,
'align' => 'E',
),
'overflow (north)' => array(
'tip' => $overflow,
),
'overflow (east)' => array(
'tip' => $overflow,
'align' => 'E',
),
);
$content = array();
foreach ($metas as $key => $meta) {
$content[] = javelin_render_tag(
'div',
array(
'sigil' => 'has-tooltip',
'meta' => $meta,
'style' => $style,
),
phutil_escape_html($key));
}
return $content;
}
}
diff --git a/src/applications/xhpastview/application/PhabricatorApplicationPHPAST.php b/src/applications/xhpastview/application/PhabricatorApplicationPHPAST.php
index 9ec07b403b..f4ee404cee 100644
--- a/src/applications/xhpastview/application/PhabricatorApplicationPHPAST.php
+++ b/src/applications/xhpastview/application/PhabricatorApplicationPHPAST.php
@@ -1,59 +1,43 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorApplicationPHPAST extends PhabricatorApplication {
public function getBaseURI() {
return '/xhpast/';
}
public function getAutospriteName() {
return 'phpast';
}
public function getShortDescription() {
return 'Visual PHP Parser';
}
public function getTitleGlyph() {
return "\xE2\x96\xA0";
}
public function getApplicationGroup() {
return self::GROUP_DEVELOPER;
}
public function getRoutes() {
return array(
'/xhpast/' => array(
'' => 'PhabricatorXHPASTViewRunController',
'view/(?P<id>[1-9]\d*)/'
=> 'PhabricatorXHPASTViewFrameController',
'frameset/(?P<id>[1-9]\d*)/'
=> 'PhabricatorXHPASTViewFramesetController',
'input/(?P<id>[1-9]\d*)/'
=> 'PhabricatorXHPASTViewInputController',
'tree/(?P<id>[1-9]\d*)/'
=> 'PhabricatorXHPASTViewTreeController',
'stream/(?P<id>[1-9]\d*)/'
=> 'PhabricatorXHPASTViewStreamController',
),
);
}
}
diff --git a/src/applications/xhpastview/controller/PhabricatorXHPASTViewController.php b/src/applications/xhpastview/controller/PhabricatorXHPASTViewController.php
index 12a60fa7d7..7d3ccf8187 100644
--- a/src/applications/xhpastview/controller/PhabricatorXHPASTViewController.php
+++ b/src/applications/xhpastview/controller/PhabricatorXHPASTViewController.php
@@ -1,35 +1,19 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorXHPASTViewController extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('XHPASTView');
$page->setBaseURI('/xhpast/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph("\xE2\x96\xA0");
$page->appendChild($view);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
}
diff --git a/src/applications/xhpastview/controller/PhabricatorXHPASTViewFrameController.php b/src/applications/xhpastview/controller/PhabricatorXHPASTViewFrameController.php
index 1b99ae19d6..9fcf82bd5e 100644
--- a/src/applications/xhpastview/controller/PhabricatorXHPASTViewFrameController.php
+++ b/src/applications/xhpastview/controller/PhabricatorXHPASTViewFrameController.php
@@ -1,43 +1,27 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorXHPASTViewFrameController
extends PhabricatorXHPASTViewController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$id = $this->id;
return $this->buildStandardPageResponse(
phutil_render_tag(
'iframe',
array(
'src' => '/xhpast/frameset/'.$id.'/',
'frameborder' => '0',
'style' => 'width: 100%; height: 800px;',
'')),
array(
'title' => 'XHPAST View',
));
}
}
diff --git a/src/applications/xhpastview/controller/PhabricatorXHPASTViewFramesetController.php b/src/applications/xhpastview/controller/PhabricatorXHPASTViewFramesetController.php
index a1e96cd7d4..c21f9f2b0b 100644
--- a/src/applications/xhpastview/controller/PhabricatorXHPASTViewFramesetController.php
+++ b/src/applications/xhpastview/controller/PhabricatorXHPASTViewFramesetController.php
@@ -1,42 +1,26 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorXHPASTViewFramesetController
extends PhabricatorXHPASTViewController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$id = $this->id;
$response = new AphrontWebpageResponse();
$response->setFrameable(true);
$response->setContent(
'<frameset cols="33%, 34%, 33%">'.
'<frame src="/xhpast/input/'.$id.'/" />'.
'<frame src="/xhpast/tree/'.$id.'/" />'.
'<frame src="/xhpast/stream/'.$id.'/" />'.
'</frameset>');
return $response;
}
}
diff --git a/src/applications/xhpastview/controller/PhabricatorXHPASTViewInputController.php b/src/applications/xhpastview/controller/PhabricatorXHPASTViewInputController.php
index 1767fb9efe..0d90598190 100644
--- a/src/applications/xhpastview/controller/PhabricatorXHPASTViewInputController.php
+++ b/src/applications/xhpastview/controller/PhabricatorXHPASTViewInputController.php
@@ -1,27 +1,11 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorXHPASTViewInputController
extends PhabricatorXHPASTViewPanelController {
public function processRequest() {
$input = $this->getStorageTree()->getInput();
return $this->buildXHPASTViewPanelResponse(
phutil_escape_html($input));
}
}
diff --git a/src/applications/xhpastview/controller/PhabricatorXHPASTViewPanelController.php b/src/applications/xhpastview/controller/PhabricatorXHPASTViewPanelController.php
index 84d2a0f079..b4494eb44e 100644
--- a/src/applications/xhpastview/controller/PhabricatorXHPASTViewPanelController.php
+++ b/src/applications/xhpastview/controller/PhabricatorXHPASTViewPanelController.php
@@ -1,87 +1,71 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorXHPASTViewPanelController
extends PhabricatorXHPASTViewController {
private $id;
private $storageTree;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
$this->storageTree = id(new PhabricatorXHPASTViewParseTree())
->load($this->id);
if (!$this->storageTree) {
throw new Exception("No such AST!");
}
}
protected function getStorageTree() {
return $this->storageTree;
}
protected function buildXHPASTViewPanelResponse($content) {
$content =
'<!DOCTYPE html>'.
'<html>'.
'<head>'.
'<style type="text/css">
body {
white-space: pre;
font: 10px "Monaco";
cursor: pointer;
}
.token {
padding: 2px 4px;
margin: 2px 2px;
border: 1px solid #bbbbbb;
line-height: 24px;
}
ul {
margin: 0 0 0 1em;
padding: 0;
list-style: none;
line-height: 1em;
}
li {
margin: 0;
padding: 0;
}
li span {
background: #dddddd;
padding: 3px 6px;
}
</style>'.
'</head>'.
'<body>'.
$content.
'</body>'.
'</html>';
$response = new AphrontWebpageResponse();
$response->setFrameable(true);
$response->setContent($content);
return $response;
}
}
diff --git a/src/applications/xhpastview/controller/PhabricatorXHPASTViewRunController.php b/src/applications/xhpastview/controller/PhabricatorXHPASTViewRunController.php
index 35fa193b12..23f9460365 100644
--- a/src/applications/xhpastview/controller/PhabricatorXHPASTViewRunController.php
+++ b/src/applications/xhpastview/controller/PhabricatorXHPASTViewRunController.php
@@ -1,73 +1,57 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorXHPASTViewRunController
extends PhabricatorXHPASTViewController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
if ($request->isFormPost()) {
$source = $request->getStr('source');
$future = xhpast_get_parser_future($source);
$resolved = $future->resolve();
// This is just to let it throw exceptions if stuff is broken.
$parse_tree = XHPASTTree::newFromDataAndResolvedExecFuture(
$source,
$resolved);
list($err, $stdout, $stderr) = $resolved;
$storage_tree = new PhabricatorXHPASTViewParseTree();
$storage_tree->setInput($source);
$storage_tree->setStdout($stdout);
$storage_tree->setAuthorPHID($user->getPHID());
$storage_tree->save();
return id(new AphrontRedirectResponse())
->setURI('/xhpast/view/'.$storage_tree->getID().'/');
}
$form = id(new AphrontFormView())
->setUser($user)
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Source')
->setName('source')
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Parse'));
$panel = new AphrontPanelView();
$panel->setHeader('Generate XHP AST');
$panel->setWidth(AphrontPanelView::WIDTH_WIDE);
$panel->appendChild($form);
return $this->buildStandardPageResponse(
$panel,
array(
'title' => 'XHPAST View',
));
}
}
diff --git a/src/applications/xhpastview/controller/PhabricatorXHPASTViewStreamController.php b/src/applications/xhpastview/controller/PhabricatorXHPASTViewStreamController.php
index 755b5aca13..c3a3b30f67 100644
--- a/src/applications/xhpastview/controller/PhabricatorXHPASTViewStreamController.php
+++ b/src/applications/xhpastview/controller/PhabricatorXHPASTViewStreamController.php
@@ -1,48 +1,32 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorXHPASTViewStreamController
extends PhabricatorXHPASTViewPanelController {
public function processRequest() {
$storage = $this->getStorageTree();
$input = $storage->getInput();
$stdout = $storage->getStdout();
$tree = XHPASTTree::newFromDataAndResolvedExecFuture(
$input,
array(0, $stdout, ''));
$tokens = array();
foreach ($tree->getRawTokenStream() as $id => $token) {
$seq = $id;
$name = $token->getTypeName();
$title = "Token {$seq}: {$name}";
$tokens[] = phutil_render_tag(
'span',
array(
'title' => $title,
'class' => 'token',
),
phutil_escape_html($token->getValue()));
}
return $this->buildXHPASTViewPanelResponse(implode('', $tokens));
}
}
diff --git a/src/applications/xhpastview/controller/PhabricatorXHPASTViewTreeController.php b/src/applications/xhpastview/controller/PhabricatorXHPASTViewTreeController.php
index ae21ec34c9..2e07a0317e 100644
--- a/src/applications/xhpastview/controller/PhabricatorXHPASTViewTreeController.php
+++ b/src/applications/xhpastview/controller/PhabricatorXHPASTViewTreeController.php
@@ -1,61 +1,45 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorXHPASTViewTreeController
extends PhabricatorXHPASTViewPanelController {
public function processRequest() {
$storage = $this->getStorageTree();
$input = $storage->getInput();
$stdout = $storage->getStdout();
$tree = XHPASTTree::newFromDataAndResolvedExecFuture(
$input,
array(0, $stdout, ''));
$tree = '<ul>'.$this->buildTree($tree->getRootNode()).'</ul>';
return $this->buildXHPASTViewPanelResponse($tree);
}
protected function buildTree($root) {
try {
$name = $root->getTypeName();
$title = $root->getDescription();
} catch (Exception $ex) {
$name = '???';
$title = '???';
}
$tree = array();
$tree[] =
'<li>'.
phutil_render_tag(
'span',
array(
'title' => $title,
),
phutil_escape_html($name)).
'</li>';
foreach ($root->getChildren() as $child) {
$tree[] = '<ul>'.$this->buildTree($child).'</ul>';
}
return implode("\n", $tree);
}
}
diff --git a/src/applications/xhpastview/storage/PhabricatorXHPASTViewDAO.php b/src/applications/xhpastview/storage/PhabricatorXHPASTViewDAO.php
index 5dd338dba0..acca5e6194 100644
--- a/src/applications/xhpastview/storage/PhabricatorXHPASTViewDAO.php
+++ b/src/applications/xhpastview/storage/PhabricatorXHPASTViewDAO.php
@@ -1,24 +1,8 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorXHPASTViewDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'xhpastview';
}
}
diff --git a/src/applications/xhpastview/storage/PhabricatorXHPASTViewParseTree.php b/src/applications/xhpastview/storage/PhabricatorXHPASTViewParseTree.php
index 1786997cf6..6db582ae3d 100644
--- a/src/applications/xhpastview/storage/PhabricatorXHPASTViewParseTree.php
+++ b/src/applications/xhpastview/storage/PhabricatorXHPASTViewParseTree.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorXHPASTViewParseTree extends PhabricatorXHPASTViewDAO {
protected $authorPHID;
protected $input;
protected $stdout;
}
diff --git a/src/applications/xhprof/controller/PhabricatorXHProfController.php b/src/applications/xhprof/controller/PhabricatorXHProfController.php
index b6e3b73d48..9129c688de 100644
--- a/src/applications/xhprof/controller/PhabricatorXHProfController.php
+++ b/src/applications/xhprof/controller/PhabricatorXHProfController.php
@@ -1,42 +1,26 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorXHProfController extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->setApplicationName('XHProf');
$page->setBaseURI('/xhprof/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph("\xE2\x98\x84");
$page->appendChild($view);
$response = new AphrontWebpageResponse();
if (isset($data['frame'])) {
$response->setFrameable(true);
$page->setFrameable(true);
$page->setShowChrome(false);
$page->setDisableConsole(true);
}
return $response->setContent($page->render());
}
}
diff --git a/src/applications/xhprof/controller/PhabricatorXHProfProfileController.php b/src/applications/xhprof/controller/PhabricatorXHProfProfileController.php
index 6a361f7386..dac92925a8 100644
--- a/src/applications/xhprof/controller/PhabricatorXHProfProfileController.php
+++ b/src/applications/xhprof/controller/PhabricatorXHProfProfileController.php
@@ -1,69 +1,53 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorXHProfProfileController
extends PhabricatorXHProfController {
private $phid;
public function willProcessRequest(array $data) {
$this->phid = $data['phid'];
}
public function processRequest() {
$file = id(new PhabricatorFile())->loadOneWhere(
'phid = %s',
$this->phid);
if (!$file) {
return new Aphront404Response();
}
$data = $file->loadFileData();
$data = unserialize($data);
if (!$data) {
throw new Exception("Failed to unserialize XHProf profile!");
}
$request = $this->getRequest();
$symbol = $request->getStr('symbol');
$is_framed = $request->getBool('frame');
if ($symbol) {
$view = new PhabricatorXHProfProfileSymbolView();
$view->setSymbol($symbol);
} else {
$view = new PhabricatorXHProfProfileTopLevelView();
$view->setFile($file);
$view->setLimit(100);
}
$view->setBaseURI($request->getRequestURI()->getPath());
$view->setIsFramed($is_framed);
$view->setProfileData($data);
return $this->buildStandardPageResponse(
$view,
array(
'title' => 'Profile',
'frame' => $is_framed,
));
}
}
diff --git a/src/applications/xhprof/controller/PhabricatorXHProfSampleListController.php b/src/applications/xhprof/controller/PhabricatorXHProfSampleListController.php
index 2d00565e9c..88080c61b8 100644
--- a/src/applications/xhprof/controller/PhabricatorXHProfSampleListController.php
+++ b/src/applications/xhprof/controller/PhabricatorXHProfSampleListController.php
@@ -1,81 +1,65 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorXHProfSampleListController
extends PhabricatorXHProfController {
private $view;
public function willProcessRequest(array $data) {
$this->view = $data['view'];
}
public function processRequest() {
$request = $this->getRequest();
$pager = new AphrontPagerView();
$pager->setOffset($request->getInt('page'));
switch ($this->view) {
case 'sampled':
$clause = '`sampleRate` > 0';
$show_type = false;
break;
case 'my-runs':
$clause = qsprintf(
id(new PhabricatorXHProfSample())->establishConnection('r'),
'`sampleRate` = 0 AND `userPHID` = %s',
$request->getUser()->getPHID());
$show_type = false;
break;
case 'manual':
$clause = '`sampleRate` = 0';
$show_type = false;
break;
case 'all':
default:
$clause = '1 = 1';
$show_type = true;
break;
}
$samples = id(new PhabricatorXHProfSample())->loadAllWhere(
'%Q ORDER BY dateCreated DESC LIMIT %d, %d',
$clause,
$pager->getOffset(),
$pager->getPageSize() + 1);
$samples = $pager->sliceResults($samples);
$pager->setURI($request->getRequestURI(), 'page');
$table = new PhabricatorXHProfSampleListView();
$table->setUser($request->getUser());
$table->setSamples($samples);
$table->setShowType($show_type);
$panel = new AphrontPanelView();
$panel->setHeader('XHProf Samples');
$panel->appendChild($table);
$panel->appendChild($pager);
return $this->buildStandardPageResponse(
$panel,
array('title' => 'XHProf Samples'));
}
}
diff --git a/src/applications/xhprof/storage/PhabricatorXHProfDAO.php b/src/applications/xhprof/storage/PhabricatorXHProfDAO.php
index 4e0c9cfe3a..4f1b713a87 100644
--- a/src/applications/xhprof/storage/PhabricatorXHProfDAO.php
+++ b/src/applications/xhprof/storage/PhabricatorXHProfDAO.php
@@ -1,24 +1,8 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorXHProfDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'xhprof';
}
}
diff --git a/src/applications/xhprof/storage/PhabricatorXHProfSample.php b/src/applications/xhprof/storage/PhabricatorXHProfSample.php
index 7d24707237..19576bd1b3 100644
--- a/src/applications/xhprof/storage/PhabricatorXHProfSample.php
+++ b/src/applications/xhprof/storage/PhabricatorXHProfSample.php
@@ -1,28 +1,12 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorXHProfSample extends PhabricatorXHProfDAO {
protected $filePHID;
protected $usTotal;
protected $sampleRate;
protected $hostname;
protected $requestPath;
protected $controller;
protected $userPHID;
}
diff --git a/src/applications/xhprof/view/PhabricatorXHProfProfileSymbolView.php b/src/applications/xhprof/view/PhabricatorXHProfProfileSymbolView.php
index 123232b147..e4f8ec94e6 100644
--- a/src/applications/xhprof/view/PhabricatorXHProfProfileSymbolView.php
+++ b/src/applications/xhprof/view/PhabricatorXHProfProfileSymbolView.php
@@ -1,150 +1,134 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @phutil-external-symbol function xhprof_compute_flat_info
*/
final class PhabricatorXHProfProfileSymbolView
extends PhabricatorXHProfProfileView {
private $profileData;
private $symbol;
public function setProfileData(array $data) {
$this->profileData = $data;
return $this;
}
public function setSymbol($symbol) {
$this->symbol = $symbol;
return $this;
}
public function render() {
DarkConsoleXHProfPluginAPI::includeXHProfLib();
$data = $this->profileData;
$GLOBALS['display_calls'] = true;
$totals = array();
$flat = xhprof_compute_flat_info($data, $totals);
unset($GLOBALS['display_calls']);
$symbol = $this->symbol;
$children = array();
$parents = array();
foreach ($this->profileData as $key => $counters) {
if (strpos($key, '==>') !== false) {
list($parent, $child) = explode('==>', $key, 2);
} else {
continue;
}
if ($parent == $symbol) {
$children[$key] = $child;
} else if ($child == $symbol) {
$parents[$key] = $parent;
}
}
$rows = array();
$rows[] = array(
'Metrics for this Call',
'',
'',
'',
);
$rows[] = $this->formatRow(
array(
$symbol,
$flat[$symbol]['ct'],
$flat[$symbol]['wt'],
1.0,
));
$rows[] = array(
'Parent Calls',
'',
'',
'',
);
foreach ($parents as $key => $name) {
$rows[] = $this->formatRow(
array(
$name,
$data[$key]['ct'],
$data[$key]['wt'],
'',
));
}
$rows[] = array(
'Child Calls',
'',
'',
'',
);
$child_rows = array();
foreach ($children as $key => $name) {
$child_rows[] = array(
$name,
$data[$key]['ct'],
$data[$key]['wt'],
$data[$key]['wt'] / $flat[$symbol]['wt'],
);
}
$child_rows = isort($child_rows, 2);
$child_rows = array_reverse($child_rows);
$rows = array_merge(
$rows,
array_map(array($this, 'formatRow'), $child_rows));
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Symbol',
'Count',
'Wall Time',
'%',
));
$table->setColumnClasses(
array(
'wide pri',
'n',
'n',
'n',
));
$panel = new AphrontPanelView();
$panel->setHeader('XHProf Profile');
$panel->appendChild($table);
return $panel->render();
}
private function formatRow(array $row) {
return array(
$this->renderSymbolLink($row[0]),
number_format($row[1]),
number_format($row[2]).' us',
($row[3] != '' ? sprintf('%.1f%%', 100 * $row[3]) : ''),
);
}
}
diff --git a/src/applications/xhprof/view/PhabricatorXHProfProfileTopLevelView.php b/src/applications/xhprof/view/PhabricatorXHProfProfileTopLevelView.php
index a8155848fd..c5180c36ad 100644
--- a/src/applications/xhprof/view/PhabricatorXHProfProfileTopLevelView.php
+++ b/src/applications/xhprof/view/PhabricatorXHProfProfileTopLevelView.php
@@ -1,158 +1,142 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @phutil-external-symbol function xhprof_compute_flat_info
*/
final class PhabricatorXHProfProfileTopLevelView
extends PhabricatorXHProfProfileView {
private $profileData;
private $limit;
private $file;
public function setProfileData(array $data) {
$this->profileData = $data;
return $this;
}
public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
public function setFile(PhabricatorFile $file) {
$this->file = $file;
return $this;
}
public function render() {
DarkConsoleXHProfPluginAPI::includeXHProfLib();
$GLOBALS['display_calls'] = true;
$totals = array();
$flat = xhprof_compute_flat_info($this->profileData, $totals);
unset($GLOBALS['display_calls']);
$aggregated = array();
foreach ($flat as $call => $counters) {
$parts = explode('@', $call, 2);
$agg_call = reset($parts);
if (empty($aggregated[$agg_call])) {
$aggregated[$agg_call] = $counters;
} else {
foreach ($aggregated[$agg_call] as $key => $val) {
if ($key != 'wt') {
$aggregated[$agg_call][$key] += $counters[$key];
}
}
}
}
$flat = $aggregated;
$flat = isort($flat, 'wt');
$flat = array_reverse($flat);
$rows = array();
$rows[] = array(
'Total',
number_format($totals['ct']),
number_format($totals['wt']).' us',
'100.0%',
number_format($totals['wt']).' us',
'100.0%',
);
if ($this->limit) {
$flat = array_slice($flat, 0, $this->limit);
}
foreach ($flat as $call => $counters) {
$rows[] = array(
$this->renderSymbolLink($call),
number_format($counters['ct']),
number_format($counters['wt']).' us',
sprintf('%.1f%%', 100 * $counters['wt'] / $totals['wt']),
number_format($counters['excl_wt']).' us',
sprintf('%.1f%%', 100 * $counters['excl_wt'] / $totals['wt']),
);
}
Javelin::initBehavior('phabricator-tooltips');
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Symbol',
'Count',
javelin_render_tag(
'span',
array(
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => 'Total wall time spent in this function and all of '.
'its children (chilren are other functions it called '.
'while executing).',
'size' => 200,
),
),
'Wall Time (Inclusive)'),
'%',
javelin_render_tag(
'span',
array(
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => 'Wall time spent in this function, excluding time '.
'spent in children (children are other functions it '.
'called while executing).',
'size' => 200,
),
),
'Wall Time (Exclusive)'),
'%',
));
$table->setColumnClasses(
array(
'wide pri',
'n',
'n',
'n',
'n',
'n',
));
$panel = new AphrontPanelView();
$panel->setHeader('XHProf Profile');
if ($this->file) {
$panel->addButton(
phutil_render_tag(
'a',
array(
'href' => $this->file->getBestURI(),
'class' => 'green button',
),
'Download .xhprof Profile'));
}
$panel->appendChild($table);
return $panel->render();
}
}
diff --git a/src/applications/xhprof/view/PhabricatorXHProfProfileView.php b/src/applications/xhprof/view/PhabricatorXHProfProfileView.php
index 9282c9e649..f4df87a422 100644
--- a/src/applications/xhprof/view/PhabricatorXHProfProfileView.php
+++ b/src/applications/xhprof/view/PhabricatorXHProfProfileView.php
@@ -1,44 +1,28 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorXHProfProfileView extends AphrontView {
private $baseURI;
private $isFramed;
public function setIsFramed($is_framed) {
$this->isFramed = $is_framed;
return $this;
}
public function setBaseURI($uri) {
$this->baseURI = $uri;
return $this;
}
protected function renderSymbolLink($symbol) {
return phutil_render_tag(
'a',
array(
'href' => $this->baseURI.'?symbol='.$symbol,
'target' => $this->isFramed ? '_top' : null,
),
phutil_escape_html($symbol));
}
}
diff --git a/src/applications/xhprof/view/PhabricatorXHProfSampleListView.php b/src/applications/xhprof/view/PhabricatorXHProfSampleListView.php
index a2618f3e95..5b525ccf73 100644
--- a/src/applications/xhprof/view/PhabricatorXHProfSampleListView.php
+++ b/src/applications/xhprof/view/PhabricatorXHProfSampleListView.php
@@ -1,98 +1,82 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorXHProfSampleListView extends AphrontView {
private $samples;
private $user;
private $showType = false;
public function setSamples(array $samples) {
assert_instances_of($samples, 'PhabricatorXHProfSample');
$this->samples = $samples;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setShowType($show_type) {
$this->showType = $show_type;
}
public function render() {
$rows = array();
if (!$this->user) {
throw new Exception("Call setUser() before rendering!");
}
$user_phids = mpull($this->samples, 'getUserPHID');
$users = id(new PhabricatorObjectHandleData($user_phids))->loadObjects();
foreach ($this->samples as $sample) {
$sample_link = phutil_render_tag(
'a',
array(
'href' => '/xhprof/profile/'.$sample->getFilePHID().'/',
),
$sample->getFilePHID());
if ($this->showType) {
if ($sample->getSampleRate() == 0) {
$sample_link .= ' (manual run)';
} else {
$sample_link .= ' (sampled)';
}
}
$rows[] = array(
$sample_link,
phabricator_datetime($sample->getDateCreated(), $this->user),
number_format($sample->getUsTotal())." \xCE\xBCs",
$sample->getHostname(),
$sample->getRequestPath(),
$sample->getController(),
idx($users, $sample->getUserPHID()),
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Sample',
'Date',
'Wall Time',
'Hostname',
'Request Path',
'Controller',
'User',
));
$table->setColumnClasses(
array(
'',
'',
'right',
'wide wrap',
'',
'',
));
return $table->render();
}
}
diff --git a/src/infrastructure/PhabricatorAccessLog.php b/src/infrastructure/PhabricatorAccessLog.php
index 6479b3e010..f70f110477 100644
--- a/src/infrastructure/PhabricatorAccessLog.php
+++ b/src/infrastructure/PhabricatorAccessLog.php
@@ -1,57 +1,41 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorAccessLog {
static $log;
public static function init() {
// NOTE: This currently has no effect, but some day we may reuse PHP
// interpreters to run multiple requests. If we do, it has the effect of
// throwing away the old log.
self::$log = null;
}
public static function getLog() {
if (!self::$log) {
$path = PhabricatorEnv::getEnvConfig('log.access.path');
$format = PhabricatorEnv::getEnvConfig('log.access.format');
$format = nonempty(
$format,
"[%D]\t%p\t%h\t%r\t%u\t%C\t%m\t%U\t%R\t%c\t%T");
if (!$path) {
return null;
}
$log = new PhutilDeferredLog($path, $format);
$log->setData(
array(
'D' => date('r'),
'h' => php_uname('n'),
'p' => getmypid(),
'e' => time(),
));
self::$log = $log;
}
return self::$log;
}
}
diff --git a/src/infrastructure/PhabricatorEditor.php b/src/infrastructure/PhabricatorEditor.php
index d92322b999..5c0508a2e3 100644
--- a/src/infrastructure/PhabricatorEditor.php
+++ b/src/infrastructure/PhabricatorEditor.php
@@ -1,50 +1,34 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorEditor extends Phobject {
private $actor;
private $excludeMailRecipientPHIDs = array();
final public function setActor(PhabricatorUser $actor) {
$this->actor = $actor;
return $this;
}
final protected function getActor() {
return $this->actor;
}
final protected function requireActor() {
$actor = $this->getActor();
if (!$actor) {
throw new Exception('You must setActor()!');
}
return $actor;
}
final public function setExcludeMailRecipientPHIDs($phids) {
$this->excludeMailRecipientPHIDs = $phids;
return $this;
}
final protected function getExcludeMailRecipientPHIDs() {
return $this->excludeMailRecipientPHIDs;
}
}
diff --git a/src/infrastructure/PhabricatorEnv.php b/src/infrastructure/PhabricatorEnv.php
index 9cd95510cd..49002a6070 100644
--- a/src/infrastructure/PhabricatorEnv.php
+++ b/src/infrastructure/PhabricatorEnv.php
@@ -1,347 +1,331 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Manages the execution environment configuration, exposing APIs to read
* configuration settings and other similar values that are derived directly
* from configuration settings.
*
*
* = Reading Configuration =
*
* The primary role of this class is to provide an API for reading
* Phabricator configuration, @{method:getEnvConfig}:
*
* $value = PhabricatorEnv::getEnvConfig('some.key', $default);
*
* The class also handles some URI construction based on configuration, via
* the methods @{method:getURI}, @{method:getProductionURI},
* @{method:getCDNURI}, and @{method:getDoclink}.
*
* For configuration which allows you to choose a class to be responsible for
* some functionality (e.g., which mail adapter to use to deliver email),
* @{method:newObjectFromConfig} provides a simple interface that validates
* the configured value.
*
*
* = Unit Test Support =
*
* In unit tests, you can use @{method:beginScopedEnv} to create a temporary,
* mutable environment. The method returns a scope guard object which restores
* the environment when it is destroyed. For example:
*
* public function testExample() {
* $env = PhabricatorEnv::beginScopedEnv();
* $env->overrideEnv('some.key', 'new-value-for-this-test');
*
* // Some test which depends on the value of 'some.key'.
*
* }
*
* Your changes will persist until the `$env` object leaves scope or is
* destroyed.
*
* You should //not// use this in normal code.
*
*
* @task read Reading Configuration
* @task uri URI Validation
* @task test Unit Test Support
* @task internal Internals
*/
final class PhabricatorEnv {
private static $env;
private static $stack = array();
/* -( Reading Configuration )---------------------------------------------- */
/**
* Get the current configuration setting for a given key.
*
* @task read
*/
public static function getEnvConfig($key, $default = null) {
// If we have environment overrides via beginScopedEnv(), check them for
// the key first.
if (self::$stack) {
foreach (array_reverse(self::$stack) as $override) {
if (array_key_exists($key, $override)) {
return $override[$key];
}
}
}
return idx(self::$env, $key, $default);
}
/**
* Get the fully-qualified URI for a path.
*
* @task read
*/
public static function getURI($path) {
return rtrim(self::getEnvConfig('phabricator.base-uri'), '/').$path;
}
/**
* Get the fully-qualified production URI for a path.
*
* @task read
*/
public static function getProductionURI($path) {
// If we're passed a URI which already has a domain, simply return it
// unmodified. In particular, files may have URIs which point to a CDN
// domain.
$uri = new PhutilURI($path);
if ($uri->getDomain()) {
return $path;
}
$production_domain = self::getEnvConfig('phabricator.production-uri');
if (!$production_domain) {
$production_domain = self::getEnvConfig('phabricator.base-uri');
}
return rtrim($production_domain, '/').$path;
}
/**
* Get the fully-qualified production URI for a static resource path.
*
* @task read
*/
public static function getCDNURI($path) {
$alt = self::getEnvConfig('security.alternate-file-domain');
if (!$alt) {
$alt = self::getEnvConfig('phabricator.base-uri');
}
$uri = new PhutilURI($alt);
$uri->setPath($path);
return (string)$uri;
}
/**
* Get the fully-qualified production URI for a documentation resource.
*
* @task read
*/
public static function getDoclink($resource) {
return 'http://www.phabricator.com/docs/phabricator/'.$resource;
}
/**
* Build a concrete object from a configuration key.
*
* @task read
*/
public static function newObjectFromConfig($key, $args = array()) {
$class = self::getEnvConfig($key);
$object = newv($class, $args);
$instanceof = idx(self::getRequiredClasses(), $key);
if (!($object instanceof $instanceof)) {
throw new Exception("Config setting '$key' must be an instance of ".
"'$instanceof', is '".get_class($object)."'.");
}
return $object;
}
/* -( Unit Test Support )-------------------------------------------------- */
/**
* @task test
*/
public static function beginScopedEnv() {
return new PhabricatorScopedEnv(self::pushEnvironment());
}
/**
* @task test
*/
private static function pushEnvironment() {
self::$stack[] = array();
return last_key(self::$stack);
}
/**
* @task test
*/
public static function popEnvironment($key) {
$stack_key = last_key(self::$stack);
array_pop(self::$stack);
if ($stack_key !== $key) {
throw new Exception(
"Scoped environments were destroyed in a diffent order than they ".
"were initialized.");
}
}
/* -( URI Validation )----------------------------------------------------- */
/**
* Detect if a URI satisfies either @{method:isValidLocalWebResource} or
* @{method:isValidRemoteWebResource}, i.e. is a page on this server or the
* URI of some other resource which has a valid protocol. This rejects
* garbage URIs and URIs with protocols which do not appear in the
* ##uri.allowed-protocols## configuration, notably 'javascript:' URIs.
*
* NOTE: This method is generally intended to reject URIs which it may be
* unsafe to put in an "href" link attribute.
*
* @param string URI to test.
* @return bool True if the URI identifies a web resource.
* @task uri
*/
public static function isValidWebResource($uri) {
return self::isValidLocalWebResource($uri) ||
self::isValidRemoteWebResource($uri);
}
/**
* Detect if a URI identifies some page on this server.
*
* NOTE: This method is generally intended to reject URIs which it may be
* unsafe to issue a "Location:" redirect to.
*
* @param string URI to test.
* @return bool True if the URI identifies a local page.
* @task uri
*/
public static function isValidLocalWebResource($uri) {
$uri = (string)$uri;
if (!strlen($uri)) {
return false;
}
if (preg_match('/\s/', $uri)) {
// PHP hasn't been vulnerable to header injection attacks for a bunch of
// years, but we can safely reject these anyway since they're never valid.
return false;
}
// Valid URIs must begin with '/', followed by the end of the string or some
// other non-'/' character. This rejects protocol-relative URIs like
// "//evil.com/evil_stuff/".
return (bool)preg_match('@^/([^/]|$)@', $uri);
}
/**
* Detect if a URI identifies some valid remote resource.
*
* @param string URI to test.
* @return bool True if a URI idenfies a remote resource with an allowed
* protocol.
* @task uri
*/
public static function isValidRemoteWebResource($uri) {
$uri = (string)$uri;
$proto = id(new PhutilURI($uri))->getProtocol();
if (!$proto) {
return false;
}
$allowed = self::getEnvConfig('uri.allowed-protocols');
if (empty($allowed[$proto])) {
return false;
}
return true;
}
/* -( Internals )---------------------------------------------------------- */
/**
* @task internal
*/
public static function setEnvConfig(array $config) {
self::$env = $config;
}
/**
* @task internal
*/
public static function getRequiredClasses() {
return array(
'translation.provider' => 'PhabricatorTranslation',
'metamta.mail-adapter' => 'PhabricatorMailImplementationAdapter',
'metamta.maniphest.reply-handler' => 'PhabricatorMailReplyHandler',
'metamta.differential.reply-handler' => 'PhabricatorMailReplyHandler',
'metamta.diffusion.reply-handler' => 'PhabricatorMailReplyHandler',
'metamta.package.reply-handler' => 'PhabricatorMailReplyHandler',
'storage.engine-selector' => 'PhabricatorFileStorageEngineSelector',
'search.engine-selector' => 'PhabricatorSearchEngineSelector',
'differential.field-selector' => 'DifferentialFieldSelector',
'maniphest.custom-task-extensions-class' => 'ManiphestTaskExtensions',
'aphront.default-application-configuration-class' =>
'AphrontApplicationConfiguration',
'controller.oauth-registration' =>
'PhabricatorOAuthRegistrationController',
'mysql.implementation' => 'AphrontMySQLDatabaseConnectionBase',
'differential.attach-task-class' => 'DifferentialTasksAttacher',
'mysql.configuration-provider' => 'DatabaseConfigurationProvider',
'syntax-highlighter.engine' => 'PhutilSyntaxHighlighterEngine',
);
}
/**
* @task internal
*/
public static function envConfigExists($key) {
return array_key_exists($key, self::$env);
}
/**
* @task internal
*/
public static function getAllConfigKeys() {
return self::$env;
}
/**
* @task internal
*/
public static function overrideEnvConfig($stack_key, $key, $value) {
self::$stack[$stack_key][$key] = $value;
}
}
diff --git a/src/infrastructure/PhabricatorRequestOverseer.php b/src/infrastructure/PhabricatorRequestOverseer.php
index 6ae4b8bdbc..a26815ceed 100644
--- a/src/infrastructure/PhabricatorRequestOverseer.php
+++ b/src/infrastructure/PhabricatorRequestOverseer.php
@@ -1,122 +1,106 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorRequestOverseer {
public function didStartup() {
$this->detectPostMaxSizeTriggered();
}
/**
* Detect if this request has had its POST data stripped by exceeding the
* 'post_max_size' PHP configuration limit.
*
* PHP has a setting called 'post_max_size'. If a POST request arrives with
* a body larger than the limit, PHP doesn't generate $_POST but processes
* the request anyway, and provides no formal way to detect that this
* happened.
*
* We can still read the entire body out of `php://input`. However according
* to the documentation the stream isn't available for "multipart/form-data"
* (on nginx + php-fpm it appears that it is available, though, at least) so
* any attempt to generate $_POST would be fragile.
*/
private function detectPostMaxSizeTriggered() {
// If this wasn't a POST, we're fine.
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
return;
}
// If there's POST data, clearly we're in good shape.
if ($_POST) {
return;
}
// For HTML5 drag-and-drop file uploads, Safari submits the data as
// "application/x-www-form-urlencoded". For most files this generates
// something in POST because most files decode to some nonempty (albeit
// meaningless) value. However, some files (particularly small images)
// don't decode to anything. If we know this is a drag-and-drop upload,
// we can skip this check.
if (isset($_REQUEST['__upload__'])) {
return;
}
// PHP generates $_POST only for two content types. This routing happens
// in `main/php_content_types.c` in PHP. Normally, all forms use one of
// these content types, but some requests may not -- for example, Firefox
// submits files sent over HTML5 XMLHTTPRequest APIs with the Content-Type
// of the file itself. If we don't have a recognized content type, we
// don't need $_POST.
//
// NOTE: We use strncmp() because the actual content type may be something
// like "multipart/form-data; boundary=...".
//
// NOTE: Chrome sometimes omits this header, see some discussion in T1762
// and http://code.google.com/p/chromium/issues/detail?id=6800
$content_type = idx($_SERVER, 'CONTENT_TYPE', '');
$parsed_types = array(
'application/x-www-form-urlencoded',
'multipart/form-data',
);
$is_parsed_type = false;
foreach ($parsed_types as $parsed_type) {
if (strncmp($content_type, $parsed_type, strlen($parsed_type)) === 0) {
$is_parsed_type = true;
break;
}
}
if (!$is_parsed_type) {
return;
}
// Check for 'Content-Length'. If there's no data, we don't expect $_POST
// to exist.
$length = (int)$_SERVER['CONTENT_LENGTH'];
if (!$length) {
return;
}
// Time to fatal: we know this was a POST with data that should have been
// populated into $_POST, but it wasn't.
$config = ini_get('post_max_size');
$this->fatal(
"As received by the server, this request had a nonzero content length ".
"but no POST data.\n\n".
"Normally, this indicates that it exceeds the 'post_max_size' setting ".
"in the PHP configuration on the server. Increase the 'post_max_size' ".
"setting or reduce the size of the request.\n\n".
"Request size according to 'Content-Length' was '{$length}', ".
"'post_max_size' is set to '{$config}'.");
}
/**
* Defined in webroot/index.php.
* TODO: Move here.
*
* @phutil-external-symbol function phabricator_fatal
*/
public function fatal($message) {
phabricator_fatal('FATAL ERROR: '.$message);
}
}
diff --git a/src/infrastructure/PhabricatorScopedEnv.php b/src/infrastructure/PhabricatorScopedEnv.php
index 4e441778b2..b2a42e2e1c 100644
--- a/src/infrastructure/PhabricatorScopedEnv.php
+++ b/src/infrastructure/PhabricatorScopedEnv.php
@@ -1,76 +1,60 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Scope guard to hold a temporary environment. See @{class:PhabricatorEnv} for
* instructions on use.
*
* @task internal Internals
* @task override Overriding Environment Configuration
*/
final class PhabricatorScopedEnv {
private $key;
private $isPopped = false;
/* -( Overriding Environment Configuration )------------------------------- */
/**
* Override a configuration key in this scope, setting it to a new value.
*
* @param string Key to override.
* @param wild New value.
* @return this
*
* @task override
*/
public function overrideEnvConfig($key, $value) {
PhabricatorEnv::overrideEnvConfig(
$this->key,
$key,
$value);
return $this;
}
/* -( Internals )---------------------------------------------------------- */
/**
*
* @task internal
*/
public function __construct($stack_key) {
$this->key = $stack_key;
}
/**
* Release the scoped environment.
*
* @return void
* @task internal
*/
public function __destruct() {
if (!$this->isPopped) {
PhabricatorEnv::popEnvironment($this->key);
$this->isPopped = true;
}
}
}
diff --git a/src/infrastructure/PhabricatorSetup.php b/src/infrastructure/PhabricatorSetup.php
index 1cf03eb2b8..96d2f725fd 100644
--- a/src/infrastructure/PhabricatorSetup.php
+++ b/src/infrastructure/PhabricatorSetup.php
@@ -1,814 +1,798 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorSetup {
public static function runSetup() {
header("Content-Type: text/plain");
self::write("PHABRICATOR SETUP\n\n");
// Force browser to stop buffering.
self::write(str_repeat(' ', 2048));
usleep(250000);
self::write("This setup mode will guide you through setting up your ".
"Phabricator configuration.\n");
self::writeHeader("CORE CONFIGURATION");
// NOTE: Test this first since other tests depend on the ability to
// execute system commands and will fail if safe_mode is enabled.
$safe_mode = ini_get('safe_mode');
if ($safe_mode) {
self::writeFailure();
self::write(
"Setup failure! You have 'safe_mode' enabled. Phabricator will not ".
"run in safe mode, and it has been deprecated in PHP 5.3 and removed ".
"in PHP 5.4.\n");
return;
} else {
self::write(" okay PHP's deprecated 'safe_mode' is disabled.\n");
}
// NOTE: Also test this early since we can't include files from other
// libraries if this is set strictly.
$open_basedir = ini_get('open_basedir');
if ($open_basedir) {
// 'open_basedir' restricts which files we're allowed to access with
// file operations. This might be okay -- we don't need to write to
// arbitrary places in the filesystem -- but we need to access certain
// resources. This setting is unlikely to be providing any real measure
// of security so warn even if things look OK.
try {
$open_libphutil = class_exists('Future');
} catch (Exception $ex) {
$message = $ex->getMessage();
self::write("Unable to load modules from libphutil: {$message}\n");
$open_libphutil = false;
}
try {
$open_arcanist = class_exists('ArcanistDiffParser');
} catch (Exception $ex) {
$message = $ex->getMessage();
self::write("Unable to load modules from Arcanist: {$message}\n");
$open_arcanist = false;
}
$open_urandom = false;
try {
Filesystem::readRandomBytes(1);
$open_urandom = true;
} catch (FilesystemException $ex) {
self::write($ex->getMessage()."\n");
}
try {
$tmp = new TempFile();
file_put_contents($tmp, '.');
$open_tmp = @fopen((string)$tmp, 'r');
} catch (Exception $ex) {
$message = $ex->getMessage();
$dir = sys_get_temp_dir();
self::write("Unable to open temp files from '{$dir}': {$message}\n");
$open_tmp = false;
}
if (!$open_urandom || !$open_tmp || !$open_libphutil || !$open_arcanist) {
self::writeFailure();
self::write(
"Setup failure! Your server is configured with 'open_basedir' in ".
"php.ini which prevents Phabricator from opening files it needs to ".
"access. Either make the setting more permissive or remove it. It ".
"is unlikely you derive significant security benefits from having ".
"this configured; files outside this directory can still be ".
"accessed through system command execution.");
return;
} else {
self::write(
"[WARN] You have an 'open_basedir' configured in your php.ini. ".
"Although the setting seems permissive enough that Phabricator ".
"will run properly, you may run into problems because of it. It is ".
"unlikely you gain much real security benefit from having it ".
"configured, because the application can still access files outside ".
"the 'open_basedir' by running system commands.\n");
}
} else {
self::write(" okay 'open_basedir' is not set.\n");
}
if (!PhabricatorEnv::getEnvConfig('security.alternate-file-domain')) {
self::write(
"[WARN] You have not configured 'security.alternate-file-domain'. ".
"This makes your installation vulnerable to attack. Make sure you ".
"read the documentation for this parameter and understand the ".
"consequences of leaving it unconfigured.\n");
}
$path = getenv('PATH');
if (empty($path)) {
self::writeFailure();
self::write(
"Setup failure! The environmental \$PATH variable is empty. ".
"Phabricator needs to execute system commands like 'svn', 'git', ".
"'hg', and 'diff'. Set up your webserver so that it passes a valid ".
"\$PATH to the PHP process.\n\n");
if (php_sapi_name() == 'fpm-fcgi') {
self::write(
"You're running php-fpm, so the easiest way to do this is to add ".
"this line to your php-fpm.conf:\n\n".
" env[PATH] = /usr/local/bin:/usr/bin:/bin\n\n".
"Then restart php-fpm.\n");
}
return;
} else {
self::write(" okay \$PATH is nonempty.\n");
}
self::write("[OKAY] Core configuration OKAY.\n");
self::writeHeader("REQUIRED PHP EXTENSIONS");
$extensions = array(
'mysql',
'hash',
'json',
'openssl',
'mbstring',
'iconv',
// There is a chance we might not need this, but some configurations (like
// OAuth or Amazon SES) will require it. Just mark it 'required' since
// it's widely available and relatively core.
'curl',
);
foreach ($extensions as $extension) {
$ok = self::requireExtension($extension);
if (!$ok) {
self::writeFailure();
self::write("Setup failure! Install PHP extension '{$extension}'.");
return;
}
}
list($err, $stdout, $stderr) = exec_manual('which php');
if ($err) {
self::writeFailure();
self::write("Unable to locate 'php' on the command line from the web ".
"server. Verify that 'php' is in the webserver's PATH.\n".
" err: {$err}\n".
"stdout: {$stdout}\n".
"stderr: {$stderr}\n");
return;
} else {
self::write(" okay PHP binary found on the command line.\n");
$php_bin = trim($stdout);
}
// NOTE: In cPanel + suphp installs, 'php' may be the PHP CGI SAPI, not the
// PHP CLI SAPI. proc_open() will pass the environment to the child process,
// which will re-execute the webpage (causing an infinite number of
// processes to spawn). To test that the 'php' binary is safe to execute,
// we call php_sapi_name() using "env -i" to wipe the environment so it
// doesn't execute another reuqest if it's the wrong binary. We can't use
// "-r" because php-cgi doesn't support that flag.
$tmp_file = new TempFile('sapi.php');
Filesystem::writeFile($tmp_file, '<?php echo php_sapi_name();');
list($err, $stdout, $stderr) = exec_manual(
'/usr/bin/env -i %s -f %s',
$php_bin,
$tmp_file);
if ($err) {
self::writeFailure();
self::write("Unable to execute 'php' on the command line from the web ".
"server.\n".
" err: {$err}\n".
"stdout: {$stdout}\n".
"stderr: {$stderr}\n");
return;
} else {
self::write(" okay PHP is available from the command line.\n");
$sapi = trim($stdout);
if ($sapi != 'cli') {
self::writeFailure();
self::write(
"The 'php' binary on this system uses the '{$sapi}' SAPI, but the ".
"'cli' SAPI is expected. Replace 'php' with the php-cli SAPI ".
"binary, or edit your webserver configuration so the first 'php' ".
"in PATH is the 'cli' SAPI.\n\n".
"If you're running cPanel with suphp, the easiest way to fix this ".
"is to add '/usr/local/bin' before '/usr/bin' for 'env_path' in ".
"suconf.php:\n\n".
' env_path="/bin:/usr/local/bin:/usr/bin"'.
"\n\n");
return;
} else {
self::write(" okay 'php' is CLI SAPI.\n");
}
}
$root = dirname(phutil_get_library_root('phabricator'));
// On RHEL6, doing a distro install of pcntl makes it available from the
// CLI binary but not from the Apache module. This isn't entirely
// unreasonable and we don't need it from Apache, so do an explicit test
// for CLI availability.
list($err, $stdout, $stderr) = exec_manual(
'php %s',
"{$root}/scripts/setup/pcntl_available.php");
if ($err) {
self::writeFailure();
self::write("Unable to execute scripts/setup/pcntl_available.php to ".
"test for the availability of pcntl from the CLI.\n".
" err: {$err}\n".
"stdout: {$stdout}\n".
"stderr: {$stderr}\n");
return;
} else {
if (trim($stdout) == 'YES') {
self::write(" okay pcntl is available from the command line.\n");
self::write("[OKAY] All extensions OKAY\n");
} else {
self::write(" warn pcntl is not available!\n");
self::write("[WARN] *** WARNING *** pcntl extension not available. ".
"You will not be able to run daemons.\n");
}
}
self::writeHeader("GIT SUBMODULES");
if (!Filesystem::pathExists($root.'/.git')) {
self::write(" skip Not a git clone.\n\n");
} else {
list($info) = execx(
'(cd %s && git submodule status)',
$root);
foreach (explode("\n", rtrim($info)) as $line) {
$matches = null;
if (!preg_match('/^(.)([0-9a-f]{40}) (\S+)(?: |$)/', $line, $matches)) {
self::writeFailure();
self::write(
"Setup failure! 'git submodule' produced unexpected output:\n".
$line);
return;
}
$status = $matches[1];
$module = $matches[3];
switch ($status) {
case '-':
case '+':
case 'U':
self::writeFailure();
self::write(
"Setup failure! Git submodule '{$module}' is not up to date. ".
"Run:\n\n".
" cd {$root} && git submodule update --init\n\n".
"...to update submodules.");
return;
case ' ':
self::write(" okay Git submodule '{$module}' up to date.\n");
break;
default:
self::writeFailure();
self::write(
"Setup failure! 'git submodule' reported unknown status ".
"'{$status}' for submodule '{$module}'. This is a bug; report ".
"it to the Phabricator maintainers.");
return;
}
}
}
self::write("[OKAY] All submodules OKAY.\n");
self::writeHeader("BASIC CONFIGURATION");
$env = PhabricatorEnv::getEnvConfig('phabricator.env');
if ($env == 'production' || $env == 'default' || $env == 'development') {
self::writeFailure();
self::write(
"Setup failure! Your PHABRICATOR_ENV is set to '{$env}', which is ".
"a Phabricator environmental default. You should create a custom ".
"environmental configuration instead of editing the defaults ".
"directly. See this document for instructions:\n");
self::writeDoc('article/Configuration_Guide.html');
return;
} else {
$host = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
$host_uri = new PhutilURI($host);
$protocol = $host_uri->getProtocol();
$allowed_protocols = array(
'http' => true,
'https' => true,
);
if (empty($allowed_protocols[$protocol])) {
self::writeFailure();
self::write(
"You must specify the protocol over which your host works (e.g.: ".
"\"http:// or https://\")\nin your custom config file.\nRefer to ".
"'default.conf.php' for documentation on configuration options.\n");
return;
}
if (preg_match('/.*\/$/', $host)) {
self::write(" okay phabricator.base-uri protocol\n");
} else {
self::writeFailure();
self::write(
"You must add a trailing slash at the end of the host\n(e.g.: ".
"\"http://phabricator.example.com/ instead of ".
"http://phabricator.example.com\")\nin your custom config file.".
"\nRefer to 'default.conf.php' for documentation on configuration ".
"options.\n");
return;
}
$host_domain = $host_uri->getDomain();
if (strpos($host_domain, '.') !== false) {
self::write(" okay phabricator.base-uri domain\n");
} else {
self::writeFailure();
self::write(
"You must host Phabricator on a domain that contains a dot ('.'). ".
"The current domain, '{$host_domain}', does not have a dot, so some ".
"browsers will not set cookies on it. For instance, ".
"'http://example.com/ is OK, but 'http://example/' won't work. ".
"If you are using localhost, create an entry in the hosts file like ".
"'127.0.0.1 example.com', and access the localhost with ".
"'http://example.com/'.");
return;
}
$host_path = $host_uri->getPath();
if ($host_path == '/') {
self::write(" okay phabricator.base-uri path\n");
} else {
self::writeFailure();
self::write(
"Your 'phabricator.base-uri' setting includes a path, but should ".
"not (e.g., 'http://phabricator.example.com/' is OK, but ".
"'http://example.com/phabricator/' is not). Phabricator must be ".
"installed on an entire domain, it can not be installed on a ".
"path alongside other applications. Consult the documentation ".
"for more details.");
return;
}
}
$timezone = nonempty(
PhabricatorEnv::getEnvConfig('phabricator.timezone'),
ini_get('date.timezone'));
if (!$timezone) {
self::writeFailure();
self::write(
"Setup failure! Your configuration fails to specify a server ".
"timezone. Either set 'date.timezone' in your php.ini or ".
"'phabricator.timezone' in your Phabricator configuration. See the ".
"PHP documentation for a list of supported timezones:\n\n".
"http://www.php.net/manual/en/timezones.php\n");
return;
} else {
self::write(" okay Timezone '{$timezone}' configured.\n");
}
self::write("[OKAY] Basic configuration OKAY\n");
$issue_gd_warning = false;
self::writeHeader('GD LIBRARY');
if (extension_loaded('gd')) {
self::write(" okay Extension 'gd' is loaded.\n");
$image_type_map = array(
'imagepng' => 'PNG',
'imagegif' => 'GIF',
'imagejpeg' => 'JPEG',
);
foreach ($image_type_map as $function => $image_type) {
if (function_exists($function)) {
self::write(" okay Support for '{$image_type}' is available.\n");
} else {
self::write(" warn Support for '{$image_type}' is not available!\n");
$issue_gd_warning = true;
}
}
} else {
self::write(" warn Extension 'gd' is not loaded.\n");
$issue_gd_warning = true;
}
if ($issue_gd_warning) {
self::write(
"[WARN] The 'gd' library is missing or lacks full support. ".
"Phabricator will not be able to generate image thumbnails without ".
"gd.\n");
} else {
self::write("[OKAY] 'gd' loaded and has full image type support.\n");
}
self::writeHeader('FACEBOOK INTEGRATION');
$fb_auth = PhabricatorEnv::getEnvConfig('facebook.auth-enabled');
if (!$fb_auth) {
self::write(" skip 'facebook.auth-enabled' not enabled.\n");
} else {
self::write(" okay 'facebook.auth-enabled' is enabled.\n");
$app_id = PhabricatorEnv::getEnvConfig('facebook.application-id');
$app_secret = PhabricatorEnv::getEnvConfig('facebook.application-secret');
if (!$app_id) {
self::writeFailure();
self::write(
"Setup failure! 'facebook.auth-enabled' is true but there is no ".
"setting for 'facebook.application-id'.\n");
return;
} else {
self::write(" okay 'facebook.application-id' is set.\n");
}
if (!is_string($app_id)) {
self::writeFailure();
self::write(
"Setup failure! 'facebook.application-id' should be a string.");
return;
} else {
self::write(" okay 'facebook.application-id' is string.\n");
}
if (!$app_secret) {
self::writeFailure();
self::write(
"Setup failure! 'facebook.auth-enabled' is true but there is no ".
"setting for 'facebook.application-secret'.");
return;
} else {
self::write(" okay 'facebook.application-secret is set.\n");
}
self::write("[OKAY] Facebook integration OKAY\n");
}
self::writeHeader("MySQL DATABASE & STORAGE CONFIGURATION");
$conf = PhabricatorEnv::newObjectFromConfig('mysql.configuration-provider');
$conn_user = $conf->getUser();
$conn_pass = $conf->getPassword();
$conn_host = $conf->getHost();
$timeout = ini_get('mysql.connect_timeout');
if ($timeout > 5) {
self::writeNote(
"Your MySQL connect timeout is very high ({$timeout} seconds). ".
"Consider reducing it to 5 or below by setting ".
"'mysql.connect_timeout' in your php.ini.");
}
self::write(" okay Trying to connect to MySQL database ".
"{$conn_user}@{$conn_host}...\n");
ini_set('mysql.connect_timeout', 2);
$conn_raw = PhabricatorEnv::newObjectFromConfig(
'mysql.implementation',
array(
array(
'user' => $conn_user,
'pass' => $conn_pass,
'host' => $conn_host,
'database' => null,
),
));
try {
queryfx($conn_raw, 'SELECT 1');
self::write(" okay Connection successful!\n");
} catch (AphrontQueryConnectionException $ex) {
$message = $ex->getMessage();
self::writeFailure();
self::write(
"Setup failure! MySQL exception: {$message} \n".
"Edit Phabricator configuration keys 'mysql.user', ".
"'mysql.host' and 'mysql.pass' to enable Phabricator ".
"to connect.");
return;
}
$engines = queryfx_all($conn_raw, 'SHOW ENGINES');
$engines = ipull($engines, 'Support', 'Engine');
$innodb = idx($engines, 'InnoDB');
if ($innodb != 'YES' && $innodb != 'DEFAULT') {
self::writeFailure();
self::write(
"Setup failure! The 'InnoDB' engine is not available. Enable ".
"InnoDB in your MySQL configuration. If you already created tables, ".
"MySQL incorrectly used some other engine. You need to convert ".
"them or drop and reinitialize them.");
return;
} else {
self::write(" okay InnoDB is available.\n");
}
$namespace = PhabricatorEnv::getEnvConfig('storage.default-namespace');
$databases = queryfx_all($conn_raw, 'SHOW DATABASES');
$databases = ipull($databases, 'Database', 'Database');
if (empty($databases[$namespace.'_meta_data'])) {
self::writeFailure();
self::write(
"Setup failure! You haven't run 'bin/storage upgrade'. See this ".
"article for instructions:\n");
self::writeDoc('article/Configuration_Guide.html');
return;
} else {
self::write(" okay Databases have been initialized.\n");
}
$index_min_length = queryfx_one(
$conn_raw,
'SHOW VARIABLES LIKE %s',
'ft_min_word_len');
$index_min_length = idx($index_min_length, 'Value', 4);
if ($index_min_length >= 4) {
self::writeNote(
"MySQL is configured with a 'ft_min_word_len' of 4 or greater, which ".
"means you will not be able to search for 3-letter terms. Consider ".
"setting this in your configuration:\n".
"\n".
" [mysqld]\n".
" ft_min_word_len=3\n".
"\n".
"Then optionally run:\n".
"\n".
" REPAIR TABLE {$namespace}_search.search_documentfield QUICK;\n".
"\n".
"...to reindex existing documents.");
}
$max_allowed_packet = queryfx_one(
$conn_raw,
'SHOW VARIABLES LIKE %s',
'max_allowed_packet');
$max_allowed_packet = idx($max_allowed_packet, 'Value', PHP_INT_MAX);
$recommended_minimum = 1024 * 1024;
if ($max_allowed_packet < $recommended_minimum) {
self::writeNote(
"MySQL is configured with a small 'max_allowed_packet' ".
"('{$max_allowed_packet}'), which may cause some large writes to ".
"fail. Consider raising this to at least {$recommended_minimum}.");
} else {
self::write(" okay max_allowed_packet = {$max_allowed_packet}.\n");
}
$local_key = 'storage.local-disk.path';
$local_path = PhabricatorEnv::getEnvConfig($local_key);
if ($local_path) {
if (!Filesystem::pathExists($local_path) ||
!is_readable($local_path) ||
!is_writable($local_path)) {
self::writeFailure();
self::write(
"Setup failure! You have configured local disk storage but the ".
"path you specified ('{$local_path}') does not exist or is not ".
"readable or writable.\n");
if ($open_basedir) {
self::write(
"You have an 'open_basedir' setting -- make sure Phabricator is ".
"allowed to open files in the local storage directory.\n");
}
return;
} else {
self::write(" okay Local disk storage exists and is writable.\n");
}
} else {
self::write(" skip Not configured for local disk storage.\n");
}
$selector = PhabricatorEnv::getEnvConfig('storage.engine-selector');
try {
$storage_selector_exists = class_exists($selector);
} catch (Exception $ex) {
$storage_selector_exists = false;
}
if ($storage_selector_exists) {
self::write(" okay Using '{$selector}' as a storage engine selector.\n");
} else {
self::writeFailure();
self::write(
"Setup failure! You have configured '{$selector}' as a storage engine ".
"selector but it does not exist or could not be loaded.\n");
return;
}
self::write("[OKAY] Database and storage configuration OKAY\n");
self::writeHeader("OUTBOUND EMAIL CONFIGURATION");
$have_adapter = false;
$is_ses = false;
$adapter = PhabricatorEnv::getEnvConfig('metamta.mail-adapter');
switch ($adapter) {
case 'PhabricatorMailImplementationPHPMailerLiteAdapter':
$have_adapter = true;
if (!Filesystem::pathExists('/usr/bin/sendmail') &&
!Filesystem::pathExists('/usr/sbin/sendmail')) {
self::writeFailure();
self::write(
"Setup failure! You don't have a 'sendmail' binary on this system ".
"but outbound email is configured to use sendmail. Install an MTA ".
"(like sendmail, qmail or postfix) or use a different outbound ".
"mail configuration. See this guide for configuring outbound ".
"email:\n");
self::writeDoc('article/Configuring_Outbound_Email.html');
return;
} else {
self::write(" okay Sendmail is configured.\n");
}
break;
case 'PhabricatorMailImplementationAmazonSESAdapter':
$is_ses = true;
$have_adapter = true;
if (PhabricatorEnv::getEnvConfig('metamta.can-send-as-user')) {
self::writeFailure();
self::write(
"Setup failure! 'metamta.can-send-as-user' must be false when ".
"configured with Amazon SES.");
return;
} else {
self::write(" okay Sender config looks okay.\n");
}
if (!PhabricatorEnv::getEnvConfig('amazon-ses.access-key')) {
self::writeFailure();
self::write(
"Setup failure! 'amazon-ses.access-key' is not set, but ".
"outbound mail is configured to deliver via Amazon SES.");
return;
} else {
self::write(" okay Amazon SES access key is set.\n");
}
if (!PhabricatorEnv::getEnvConfig('amazon-ses.secret-key')) {
self::writeFailure();
self::write(
"Setup failure! 'amazon-ses.secret-key' is not set, but ".
"outbound mail is configured to deliver via Amazon SES.");
return;
} else {
self::write(" okay Amazon SES secret key is set.\n");
}
if (PhabricatorEnv::getEnvConfig('metamta.send-immediately')) {
self::writeNote(
"Your configuration uses Amazon SES to deliver email but tries ".
"to send it immediately. This will work, but it's slow. ".
"Consider configuring the MetaMTA daemon.");
}
break;
case 'PhabricatorMailImplementationTestAdapter':
self::write(" skip You have disabled outbound email.\n");
break;
default:
self::write(" skip Configured with a custom adapter.\n");
break;
}
if ($have_adapter) {
$default = PhabricatorEnv::getEnvConfig('metamta.default-address');
if (!$default || $default == 'noreply@example.com') {
self::writeFailure();
self::write(
"Setup failure! You have not set 'metamta.default-address'.");
return;
} else {
self::write(" okay metamta.default-address is set.\n");
}
if ($is_ses) {
self::writeNote(
"Make sure you've verified your 'from' address ('{$default}') with ".
"Amazon SES. Until you verify it, you will be unable to send mail ".
"using Amazon SES.");
}
$domain = PhabricatorEnv::getEnvConfig('metamta.domain');
if (!$domain || $domain == 'example.com') {
self::writeFailure();
self::write(
"Setup failure! You have not set 'metamta.domain'.");
return;
} else {
self::write(" okay metamta.domain is set.\n");
}
self::write("[OKAY] Mail configuration OKAY\n");
}
self::writeHeader('CONFIG CLASSES');
foreach (PhabricatorEnv::getRequiredClasses() as $key => $instanceof) {
$config = PhabricatorEnv::getEnvConfig($key);
if (!$config) {
self::writeNote("'$key' is not set.");
} else {
try {
$r = new ReflectionClass($config);
if (!$r->isSubclassOf($instanceof)) {
throw new Exception(
"Config setting '$key' must be an instance of '$instanceof'.");
} else if (!$r->isInstantiable()) {
throw new Exception("Config setting '$key' must be instantiable.");
}
} catch (Exception $ex) {
self::writeFailure();
self::write("Setup failure! ".$ex->getMessage());
return;
}
}
}
self::write("[OKAY] Config classes OKAY\n");
self::writeHeader('SUCCESS!');
self::write(
"Congratulations! Your setup seems mostly correct, or at least fairly ".
"reasonable.\n\n".
"*** NEXT STEP ***\n".
"Edit your configuration file (conf/{$env}.conf.php) and remove the ".
"'phabricator.setup' line to finish installation.");
}
public static function requireExtension($extension) {
if (extension_loaded($extension)) {
self::write(" okay Extension '{$extension}' installed.\n");
return true;
} else {
self::write("[FAIL] Extension '{$extension}' is NOT INSTALLED!\n");
return false;
}
}
private static function writeFailure() {
self::write("\n\n<<< *** FAILURE! *** >>>\n");
}
private static function write($str) {
echo $str;
ob_flush();
flush();
// This, uh, makes it look cool. -_-
usleep(20000);
}
private static function writeNote($note) {
$note = "*** NOTE: ".wordwrap($note, 75, "\n", true);
$note = "\n".str_replace("\n", "\n ", $note)."\n\n";
self::write($note);
}
public static function writeHeader($header) {
$template = '>>>'.str_repeat('-', 77);
$template = substr_replace(
$template,
' '.$header.' ',
3,
strlen($header) + 4);
self::write("\n\n{$template}\n\n");
}
public static function writeDoc($doc) {
self::write(
"\n".
' http://www.phabricator.com/docs/phabricator/'.$doc.
"\n\n");
}
}
diff --git a/src/infrastructure/__tests__/PhabricatorEnvTestCase.php b/src/infrastructure/__tests__/PhabricatorEnvTestCase.php
index 9a01ea313a..34242592a8 100644
--- a/src/infrastructure/__tests__/PhabricatorEnvTestCase.php
+++ b/src/infrastructure/__tests__/PhabricatorEnvTestCase.php
@@ -1,123 +1,107 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorEnvTestCase extends PhabricatorTestCase {
public function testLocalWebResource() {
$map = array(
'/' => true,
'/D123' => true,
'/path/to/something/' => true,
"/path/to/\nHeader: x" => false,
'http://evil.com/' => false,
'//evil.com/evil/' => false,
'javascript:lol' => false,
'' => false,
null => false,
);
foreach ($map as $uri => $expect) {
$this->assertEqual(
$expect,
PhabricatorEnv::isValidLocalWebResource($uri),
"Valid local resource: {$uri}");
}
}
public function testRemoteWebResource() {
$map = array(
'http://example.com/' => true,
'derp://example.com/' => false,
'javascript:alert(1)' => false,
);
foreach ($map as $uri => $expect) {
$this->assertEqual(
$expect,
PhabricatorEnv::isValidRemoteWebResource($uri),
"Valid remote resource: {$uri}");
}
}
public function testOverrides() {
$outer = PhabricatorEnv::beginScopedEnv();
$outer->overrideEnvConfig('test.value', 1);
$this->assertEqual(1, PhabricatorEnv::getEnvConfig('test.value'));
$inner = PhabricatorEnv::beginScopedEnv();
$inner->overrideEnvConfig('test.value', 2);
$this->assertEqual(2, PhabricatorEnv::getEnvConfig('test.value'));
if (phutil_is_hiphop_runtime()) {
$inner->__destruct();
}
unset($inner);
$this->assertEqual(1, PhabricatorEnv::getEnvConfig('test.value'));
if (phutil_is_hiphop_runtime()) {
$outer->__destruct();
}
unset($outer);
}
public function testOverrideOrder() {
$outer = PhabricatorEnv::beginScopedEnv();
$middle = PhabricatorEnv::beginScopedEnv();
$inner = PhabricatorEnv::beginScopedEnv();
$caught = null;
try {
if (phutil_is_hiphop_runtime()) {
$middle->__destruct();
}
unset($middle);
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertEqual(
true,
$caught instanceof Exception,
"Destroying a scoped environment which is not on the top of the stack ".
"should throw.");
$caught = null;
try {
if (phutil_is_hiphop_runtime()) {
$inner->__destruct();
}
unset($inner);
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertEqual(
true,
$caught instanceof Exception,
"Destroying a scoped environment which is not on the top of the stack ".
"should throw.");
// Although we popped the other two out-of-order, we still expect to end
// up in the right state after handling the exceptions, so this should
// execute without issues.
if (phutil_is_hiphop_runtime()) {
$outer->__destruct();
}
unset($outer);
}
}
diff --git a/src/infrastructure/__tests__/PhabricatorInfrastructureTestCase.php b/src/infrastructure/__tests__/PhabricatorInfrastructureTestCase.php
index 75cddb51a0..60c4695798 100644
--- a/src/infrastructure/__tests__/PhabricatorInfrastructureTestCase.php
+++ b/src/infrastructure/__tests__/PhabricatorInfrastructureTestCase.php
@@ -1,34 +1,18 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorInfrastructureTestCase
extends PhabricatorTestCase {
/**
* This is more of an acceptance test case instead of a unittest. It verifies
* that all symbols can be loaded correctly. It can catch problem like missing
* methods in descendants of abstract base classes.
*/
public function testEverythingImplemented() {
// Note that we don't have a try catch block around the following because,
// when it fails, it will cause a HPHP or PHP fatal which won't be caught
// by try catch.
$every_class = id(new PhutilSymbolLoader())->selectAndLoadSymbols();
}
}
diff --git a/src/infrastructure/celerity/CelerityAPI.php b/src/infrastructure/celerity/CelerityAPI.php
index d39c66f742..4a786adbe2 100644
--- a/src/infrastructure/celerity/CelerityAPI.php
+++ b/src/infrastructure/celerity/CelerityAPI.php
@@ -1,36 +1,20 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* 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;
public static function getStaticResourceResponse() {
if (empty(self::$response)) {
self::$response = new CelerityStaticResourceResponse();
}
return self::$response;
}
}
diff --git a/src/infrastructure/celerity/CelerityPhabricatorResourceController.php b/src/infrastructure/celerity/CelerityPhabricatorResourceController.php
index 4eff40b2de..761bd3339b 100644
--- a/src/infrastructure/celerity/CelerityPhabricatorResourceController.php
+++ b/src/infrastructure/celerity/CelerityPhabricatorResourceController.php
@@ -1,59 +1,43 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Delivers CSS and JS resources to the browser. This controller handles all
* ##/res/## requests, and manages caching, package construction, and resource
* preprocessing.
*
* @group celerity
*/
final class CelerityPhabricatorResourceController
extends CelerityResourceController {
private $path;
private $hash;
private $package;
protected function getRootDirectory() {
$root = dirname(phutil_get_library_root('phabricator'));
return $root.'/webroot/';
}
public function willProcessRequest(array $data) {
$this->path = $data['path'];
$this->hash = $data['hash'];
$this->package = !empty($data['package']);
}
public function processRequest() {
$package_hash = null;
if ($this->package) {
$package_hash = $this->hash;
}
return $this->serveResource($this->path, $package_hash);
}
protected function buildResourceTransformer() {
$xformer = new CelerityResourceTransformer();
$xformer->setMinify(PhabricatorEnv::getEnvConfig('celerity.minify'));
$xformer->setCelerityMap(CelerityResourceMap::getInstance());
return $xformer;
}
}
diff --git a/src/infrastructure/celerity/CelerityResourceController.php b/src/infrastructure/celerity/CelerityResourceController.php
index ab00881b1e..275724bb0e 100644
--- a/src/infrastructure/celerity/CelerityResourceController.php
+++ b/src/infrastructure/celerity/CelerityResourceController.php
@@ -1,115 +1,99 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class CelerityResourceController extends PhabricatorController {
abstract protected function getRootDirectory();
protected function buildResourceTransformer() {
return null;
}
public function shouldRequireLogin() {
return false;
}
public function shouldRequireEnabledUser() {
return false;
}
private function getDiskPath($to_resource = null) {
return $this->getRootDirectory().$to_resource;
}
protected function serveResource($path, $package_hash = null) {
// Sanity checking to keep this from exposing anything sensitive, since it
// ultimately boils down to disk reads.
if (preg_match('@(//|\.\.)@', $path)) {
return new Aphront400Response();
}
$type = CelerityResourceTransformer::getResourceType($path);
$type_map = $this->getSupportedResourceTypes();
if (empty($type_map[$type])) {
throw new Exception("Only static resources may be served.");
}
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) &&
!PhabricatorEnv::getEnvConfig('celerity.force-disk-reads')) {
// Return a "304 Not Modified". We don't care about the value of this
// field since we never change what resource is served by a given URI.
return $this->makeResponseCacheable(new Aphront304Response());
}
if ($package_hash) {
$map = CelerityResourceMap::getInstance();
$paths = $map->resolvePackage($package_hash);
if (!$paths) {
return new Aphront404Response();
}
try {
$data = array();
foreach ($paths as $package_path) {
$disk_path = $this->getDiskPath($package_path);
$data[] = Filesystem::readFile($disk_path);
}
$data = implode("\n\n", $data);
} catch (Exception $ex) {
return new Aphront404Response();
}
} else {
try {
$disk_path = $this->getDiskPath($path);
$data = Filesystem::readFile($disk_path);
} catch (Exception $ex) {
return new Aphront404Response();
}
}
$xformer = $this->buildResourceTransformer();
if ($xformer) {
$data = $xformer->transformResource($path, $data);
}
$response = new AphrontFileResponse();
$response->setContent($data);
$response->setMimeType($type_map[$type]);
return $this->makeResponseCacheable($response);
}
protected function getSupportedResourceTypes() {
return array(
'css' => 'text/css; charset=utf-8',
'js' => 'text/javascript; charset=utf-8',
'png' => 'image/png',
'gif' => 'image/gif',
'jpg' => 'image/jpg',
'swf' => 'application/x-shockwave-flash',
);
}
private function makeResponseCacheable(AphrontResponse $response) {
$response->setCacheDurationInSeconds(60 * 60 * 24 * 30);
$response->setLastModified(time());
return $response;
}
}
diff --git a/src/infrastructure/celerity/CelerityResourceGraph.php b/src/infrastructure/celerity/CelerityResourceGraph.php
index 9d6c8f6865..358c6a0c02 100644
--- a/src/infrastructure/celerity/CelerityResourceGraph.php
+++ b/src/infrastructure/celerity/CelerityResourceGraph.php
@@ -1,48 +1,32 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class CelerityResourceGraph extends AbstractDirectedGraph {
private $resourceGraph = array();
private $graphSet = false;
protected function loadEdges(array $nodes) {
if (!$this->graphSet) {
throw new Exception(
"Call setResourceGraph before loading the graph!"
);
}
$graph = $this->getResourceGraph();
$edges = array();
foreach ($nodes as $node) {
$edges[$node] = idx($graph, $node, array());
}
return $edges;
}
final public function setResourceGraph(array $graph) {
$this->resourceGraph = $graph;
$this->graphSet = true;
return $this;
}
private function getResourceGraph() {
return $this->resourceGraph;
}
}
diff --git a/src/infrastructure/celerity/CelerityResourceMap.php b/src/infrastructure/celerity/CelerityResourceMap.php
index 8bec8225e1..9157729b75 100644
--- a/src/infrastructure/celerity/CelerityResourceMap.php
+++ b/src/infrastructure/celerity/CelerityResourceMap.php
@@ -1,139 +1,123 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* 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;
private $resourceMap;
private $packageMap;
private $reverseMap;
public static function getInstance() {
if (empty(self::$instance)) {
self::$instance = new CelerityResourceMap();
$root = phutil_get_library_root('phabricator');
$path = PhabricatorEnv::getEnvConfig('celerity.resource-path');
$ok = include_once $root.'/'.$path;
if (!$ok) {
throw new Exception(
"Failed to load Celerity resource map! Check the ".
"'celerity.resource-path' setting in your configuration.");
}
}
return self::$instance;
}
public function setResourceMap($resource_map) {
$this->resourceMap = $resource_map;
return $this;
}
public function resolveResources(array $symbols) {
$map = array();
foreach ($symbols as $symbol) {
if (!empty($map[$symbol])) {
continue;
}
$this->resolveResource($map, $symbol);
}
return $map;
}
private function resolveResource(array &$map, $symbol) {
if (empty($this->resourceMap[$symbol])) {
throw new Exception(
"Attempting to resolve unknown Celerity resource, '{$symbol}'.");
}
$info = $this->resourceMap[$symbol];
foreach ($info['requires'] as $requires) {
if (!empty($map[$requires])) {
continue;
}
$this->resolveResource($map, $requires);
}
$map[$symbol] = $info;
}
public function setPackageMap($package_map) {
$this->packageMap = $package_map;
return $this;
}
public function packageResources(array $resolved_map) {
$packaged = array();
$handled = array();
foreach ($resolved_map as $symbol => $info) {
if (isset($handled[$symbol])) {
continue;
}
if (empty($this->packageMap['reverse'][$symbol])) {
$packaged[$symbol] = $info;
} else {
$package = $this->packageMap['reverse'][$symbol];
$package_info = $this->packageMap['packages'][$package];
$packaged[$package_info['name']] = $package_info;
foreach ($package_info['symbols'] as $packaged_symbol) {
$handled[$packaged_symbol] = true;
}
}
}
return $packaged;
}
public function resolvePackage($package_hash) {
$package = idx($this->packageMap['packages'], $package_hash);
if (!$package) {
return null;
}
$paths = array();
foreach ($package['symbols'] as $symbol) {
$paths[] = $this->resourceMap[$symbol]['disk'];
}
return $paths;
}
public function lookupSymbolInformation($symbol) {
return idx($this->resourceMap, $symbol);
}
public function lookupFileInformation($path) {
if (empty($this->reverseMap)) {
$this->reverseMap = array();
foreach ($this->resourceMap as $symbol => $data) {
$data['provides'] = $symbol;
$this->reverseMap[$data['disk']] = $data;
}
}
return idx($this->reverseMap, $path);
}
}
diff --git a/src/infrastructure/celerity/CelerityResourceTransformer.php b/src/infrastructure/celerity/CelerityResourceTransformer.php
index f681a08abf..d650a91b4b 100644
--- a/src/infrastructure/celerity/CelerityResourceTransformer.php
+++ b/src/infrastructure/celerity/CelerityResourceTransformer.php
@@ -1,127 +1,111 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group celerity
*/
final class CelerityResourceTransformer {
private $minify;
private $rawResourceMap;
private $celerityMap;
private $translateURICallback;
public function setTranslateURICallback($translate_uricallback) {
$this->translateURICallback = $translate_uricallback;
return $this;
}
public function setMinify($minify) {
$this->minify = $minify;
return $this;
}
public function setRawResourceMap(array $raw_resource_map) {
$this->rawResourceMap = $raw_resource_map;
return $this;
}
public function setCelerityMap(CelerityResourceMap $celerity_map) {
$this->celerityMap = $celerity_map;
return $this;
}
public function transformResource($path, $data) {
$type = self::getResourceType($path);
switch ($type) {
case 'css':
$data = preg_replace_callback(
'@url\s*\((\s*[\'"]?.*?)\)@s',
nonempty(
$this->translateURICallback,
array($this, 'translateResourceURI')),
$data);
break;
}
if (!$this->minify) {
return $data;
}
// Some resources won't survive minification (like Raphael.js), and are
// marked so as not to be minified.
if (strpos($data, '@'.'do-not-minify') !== false) {
return $data;
}
switch ($type) {
case 'css':
// Remove comments.
$data = preg_replace('@/\*.*?\*/@s', '', $data);
// Remove whitespace around symbols.
$data = preg_replace('@\s*([{}:;,])\s*@', '\1', $data);
// Remove unnecessary semicolons.
$data = preg_replace('@;}@', '}', $data);
// Replace #rrggbb with #rgb when possible.
$data = preg_replace(
'@#([a-f0-9])\1([a-f0-9])\2([a-f0-9])\3@i',
'#\1\2\3',
$data);
$data = trim($data);
break;
case 'js':
$root = dirname(phutil_get_library_root('phabricator'));
$bin = $root.'/externals/javelin/support/jsxmin/jsxmin';
if (@file_exists($bin)) {
$future = new ExecFuture("{$bin} __DEV__:0");
$future->write($data);
list($err, $result) = $future->resolve();
if (!$err) {
$data = $result;
}
}
break;
}
return $data;
}
public static function getResourceType($path) {
return last(explode('.', $path));
}
public function translateResourceURI(array $matches) {
$uri = trim($matches[1], "'\" \r\t\n");
if ($this->rawResourceMap) {
if (isset($this->rawResourceMap[$uri]['uri'])) {
$uri = $this->rawResourceMap[$uri]['uri'];
}
} else if ($this->celerityMap) {
$info = $this->celerityMap->lookupFileInformation($uri);
if ($info) {
$uri = $info['uri'];
}
}
return 'url('.$uri.')';
}
}
diff --git a/src/infrastructure/celerity/CelerityStaticResourceResponse.php b/src/infrastructure/celerity/CelerityStaticResourceResponse.php
index c801fb3fe2..eae4d37efb 100644
--- a/src/infrastructure/celerity/CelerityStaticResourceResponse.php
+++ b/src/infrastructure/celerity/CelerityStaticResourceResponse.php
@@ -1,221 +1,205 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* 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();
private $needsResolve = true;
private $resolved;
private $packaged;
private $metadata = array();
private $metadataBlock = 0;
private $behaviors = array();
private $hasRendered = array();
public function __construct() {
if (isset($_REQUEST['__metablock__'])) {
$this->metadataBlock = (int)$_REQUEST['__metablock__'];
}
}
public function addMetadata($metadata) {
$id = count($this->metadata);
$this->metadata[$id] = $metadata;
return $this->metadataBlock.'_'.$id;
}
public function getMetadataBlock() {
return $this->metadataBlock;
}
/**
* Register a behavior for initialization. NOTE: if $config is empty,
* a behavior will execute only once even if it is initialized multiple times.
* If $config is nonempty, the behavior will be invoked once for each config.
*/
public function initBehavior($behavior, array $config = array()) {
$this->requireResource('javelin-behavior-'.$behavior);
if (empty($this->behaviors[$behavior])) {
$this->behaviors[$behavior] = array();
}
if ($config) {
$this->behaviors[$behavior][] = $config;
}
return $this;
}
public function requireResource($symbol) {
$this->symbols[$symbol] = true;
$this->needsResolve = true;
return $this;
}
private function resolveResources() {
if ($this->needsResolve) {
$map = CelerityResourceMap::getInstance();
$this->resolved = $map->resolveResources(array_keys($this->symbols));
$this->packaged = $map->packageResources($this->resolved);
$this->needsResolve = false;
}
return $this;
}
public function renderSingleResource($symbol) {
$map = CelerityResourceMap::getInstance();
$resolved = $map->resolveResources(array($symbol));
$packaged = $map->packageResources($resolved);
return $this->renderPackagedResources($packaged);
}
public function renderResourcesOfType($type) {
$this->resolveResources();
$resources = array();
foreach ($this->packaged as $resource) {
if ($resource['type'] == $type) {
$resources[] = $resource;
}
}
return $this->renderPackagedResources($resources);
}
private function renderPackagedResources(array $resources) {
$output = array();
foreach ($resources as $resource) {
if (isset($this->hasRendered[$resource['uri']])) {
continue;
}
$this->hasRendered[$resource['uri']] = true;
$output[] = $this->renderResource($resource);
}
return implode("\n", $output)."\n";
}
private function renderResource(array $resource) {
$uri = PhabricatorEnv::getCDNURI($resource['uri']);
switch ($resource['type']) {
case 'css':
return phutil_render_tag(
'link',
array(
'rel' => 'stylesheet',
'type' => 'text/css',
'href' => $uri,
));
case 'js':
return phutil_render_tag(
'script',
array(
'type' => 'text/javascript',
'src' => $uri,
),
'');
}
throw new Exception("Unable to render resource.");
}
public function renderHTMLFooter() {
$data = array();
if ($this->metadata) {
$json_metadata = json_encode($this->metadata);
$this->metadata = array();
} else {
$json_metadata = '{}';
}
// Even if there is no metadata on the page, Javelin uses the mergeData()
// call to start dispatching the event queue.
$data[] = 'JX.Stratcom.mergeData('.$this->metadataBlock.', '.
$json_metadata.');';
$onload = array();
if ($this->behaviors) {
$behaviors = $this->behaviors;
$this->behaviors = array();
$higher_priority_names = array(
'refresh-csrf',
'aphront-basic-tokenizer',
);
$higher_priority_behaviors = array_select_keys(
$behaviors,
$higher_priority_names);
foreach ($higher_priority_names as $name) {
unset($behaviors[$name]);
}
$behavior_groups = array(
$higher_priority_behaviors,
$behaviors);
foreach ($behavior_groups as $group) {
if (!$group) {
continue;
}
$onload[] = 'JX.initBehaviors('.json_encode($group).')';
}
}
if ($onload) {
foreach ($onload as $func) {
$data[] = 'JX.onload(function(){'.$func.'});';
}
}
if ($data) {
$data = implode("\n", $data);
return '<script type="text/javascript">//<![CDATA['."\n".
$data.'//]]></script>';
} else {
return '';
}
}
public function buildAjaxResponse($payload, $error = null) {
$response = array(
'error' => $error,
'payload' => $payload,
);
if ($this->metadata) {
$response['javelin_metadata'] = $this->metadata;
$this->metadata = array();
}
if ($this->behaviors) {
$response['javelin_behaviors'] = $this->behaviors;
$this->behaviors = array();
}
return $response;
}
}
diff --git a/src/infrastructure/celerity/__tests__/CelerityResourceTransformerTestCase.php b/src/infrastructure/celerity/__tests__/CelerityResourceTransformerTestCase.php
index 10d3f3232d..6dd3d772e3 100644
--- a/src/infrastructure/celerity/__tests__/CelerityResourceTransformerTestCase.php
+++ b/src/infrastructure/celerity/__tests__/CelerityResourceTransformerTestCase.php
@@ -1,54 +1,38 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group celerity
*/
final class CelerityResourceTransformerTestCase extends PhabricatorTestCase {
public function testTransformation() {
$files = dirname(__FILE__).'/transformer/';
foreach (Filesystem::listDirectory($files) as $file) {
$name = basename($file);
$data = Filesystem::readFile($files.'/'.$file);
$parts = preg_split('/^~~~+\n/m', $data);
$parts = array_merge($parts, array(null));
list($options, $in, $expect) = $parts;
$options = PhutilSimpleOptions::parse($options) + array(
'minify' => false,
'name' => $name,
);
$xformer = new CelerityResourceTransformer();
$xformer->setRawResourceMap(
array(
'/rsrc/example.png' => array(
'uri' => '/res/hash/example.png',
),
));
$xformer->setMinify($options['minify']);
$result = $xformer->transformResource($options['name'], $in);
$this->assertEqual(rtrim($expect), rtrim($result), $file);
}
}
}
diff --git a/src/infrastructure/celerity/api.php b/src/infrastructure/celerity/api.php
index 67c7310495..fa615f32ad 100644
--- a/src/infrastructure/celerity/api.php
+++ b/src/infrastructure/celerity/api.php
@@ -1,77 +1,61 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Include a CSS or JS static resource by name. This function records a
* dependency for the current page, so when a response is generated it can be
* included. You can call this method from any context, and it is recommended
* you invoke it as close to the actual dependency as possible so that page
* dependencies are minimized.
*
* For more information, see @{article:Adding New CSS and JS}.
*
* @param string Name of the celerity module to include. This is whatever you
* annotated as "@provides" in the file.
* @return void
*
* @group celerity
*/
function require_celerity_resource($symbol) {
$response = CelerityAPI::getStaticResourceResponse();
$response->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++);
}
/**
* Get the versioned URI for a raw resource, like an image.
*
* @param string Path to the raw image.
* @return string Versioned path to the image, if one is available.
*
* @group celerity
*/
function celerity_get_resource_uri($resource) {
$map = CelerityResourceMap::getInstance();
$info = $map->lookupFileInformation($resource);
if ($info) {
return $info['uri'];
} else {
return $resource;
}
}
diff --git a/src/infrastructure/celerity/map.php b/src/infrastructure/celerity/map.php
index 93c5e05653..bbdf558b46 100644
--- a/src/infrastructure/celerity/map.php
+++ b/src/infrastructure/celerity/map.php
@@ -1,29 +1,13 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Registers a resource map for Celerity. This is glue code between the Celerity
* mapper script and @{class:CelerityResourceMap}.
*
* @group celerity
*/
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/daemon/PhabricatorDaemon.php b/src/infrastructure/daemon/PhabricatorDaemon.php
index a53257bc6a..acbb6480e0 100644
--- a/src/infrastructure/daemon/PhabricatorDaemon.php
+++ b/src/infrastructure/daemon/PhabricatorDaemon.php
@@ -1,35 +1,19 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorDaemon extends PhutilDaemon {
protected function willRun() {
parent::willRun();
// This stores unbounded amounts of log data; make it discard instead so
// that daemons do not require unbounded amounts of memory.
DarkConsoleErrorLogPluginAPI::enableDiscardMode();
// Also accumulates potentially unlimited amounts of data.
DarkConsoleEventPluginAPI::enableDiscardMode();
$phabricator = phutil_get_library_root('phabricator');
$root = dirname($phabricator);
require_once $root.'/scripts/__init_script__.php';
}
}
diff --git a/src/infrastructure/daemon/PhabricatorDaemonControl.php b/src/infrastructure/daemon/PhabricatorDaemonControl.php
index a0ca065d0e..71a0270442 100644
--- a/src/infrastructure/daemon/PhabricatorDaemonControl.php
+++ b/src/infrastructure/daemon/PhabricatorDaemonControl.php
@@ -1,349 +1,333 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorDaemonControl {
public function executeListCommand() {
$symbols = $this->loadAvailableDaemonClasses();
$symbols = igroup($symbols, 'library');
echo "\n";
foreach ($symbols as $library => $symbol_list) {
echo phutil_console_format("Daemons in library __%s__:\n", $library);
foreach ($symbol_list as $symbol) {
echo " ".$symbol['name']."\n";
}
echo "\n";
}
return 0;
}
public function executeStatusCommand() {
$daemons = $this->loadRunningDaemons();
if (!$daemons) {
echo "There are no running Phabricator daemons.\n";
return 1;
}
$status = 0;
printf(
"%-5s\t%-24s\t%s\n",
"PID",
"Started",
"Daemon");
foreach ($daemons as $daemon) {
$name = $daemon->getName();
if (!$daemon->isRunning()) {
$daemon->updateStatus(PhabricatorDaemonLog::STATUS_DEAD);
$status = 2;
$name = '<DEAD> '.$name;
}
printf(
"%5s\t%-24s\t%s\n",
$daemon->getPID(),
$daemon->getEpochStarted()
? date('M j Y, g:i:s A', $daemon->getEpochStarted())
: null,
$name);
}
return $status;
}
public function executeStopCommand($pids = null) {
$daemons = $this->loadRunningDaemons();
if (!$daemons) {
echo "There are no running Phabricator daemons.\n";
return 0;
}
$daemons = mpull($daemons, null, 'getPID');
$running = array();
if ($pids == null) {
$running = $daemons;
} else {
// We were given a PID or set of PIDs to kill.
foreach ($pids as $key => $pid) {
if (!preg_match('/^\d+$/', $pid)) {
echo "'{$pid}' is not a valid PID.\n";
continue;
} else if (empty($daemons[$pid])) {
echo "'{$pid}' is not Phabricator-controlled PID. Not killing.\n";
continue;
} else {
$running[] = $daemons[$pid];
}
}
}
if (empty($running)) {
echo "No daemons to kill.\n";
return 0;
}
$all_daemons = $running;
foreach ($running as $key => $daemon) {
$pid = $daemon->getPID();
$name = $daemon->getName();
echo "Stopping daemon '{$name}' ({$pid})...\n";
if (!$daemon->isRunning()) {
echo "Daemon is not running.\n";
unset($running[$key]);
$daemon->updateStatus(PhabricatorDaemonLog::STATUS_EXITED);
} else {
posix_kill($pid, SIGINT);
}
}
$start = time();
do {
foreach ($running as $key => $daemon) {
$pid = $daemon->getPID();
if (!$daemon->isRunning()) {
echo "Daemon {$pid} exited normally.\n";
unset($running[$key]);
}
}
if (empty($running)) {
break;
}
usleep(100000);
} while (time() < $start + 15);
foreach ($running as $key => $daemon) {
$pid = $daemon->getPID();
echo "KILLing daemon {$pid}.\n";
posix_kill($pid, SIGKILL);
}
foreach ($all_daemons as $daemon) {
if ($daemon->getPIDFile()) {
Filesystem::remove($daemon->getPIDFile());
}
}
}
public function executeHelpCommand() {
echo phutil_console_format(<<<EOHELP
**NAME**
**phd** - phabricator daemon launcher
**COMMAND REFERENCE**
**start**
Start the normal collection of daemons that Phabricator uses. This
is appropriate for most installs. If you want to customize what
is launched, you can use **launch** for fine-grained control.
**restart**
Stop all running daemons, then start a standard loadout.
**stop** [PID ...]
Stop all running daemons if no PIDs are given, or a particular
PID or set of PIDs, if they are supplied.
**launch** [__n__] __daemon__ [argv ...]
**debug** __daemon__ [argv ...]
Start a daemon (or n copies of a daemon).
With **debug**, do not daemonize. Use this if you're having trouble
getting daemons working.
**list**
List available daemons.
**status**
List running daemons. This command will exit with a non-zero exit
status if any daemons are not running.
**help**
Show this help.
**repository-launch-master**
DEPRECATED. Use 'phd start'.
**repository-launch-readonly**
DEPRECATED. Use 'phd launch pulllocal -- --no-discovery'.
EOHELP
);
return 1;
}
public function pingConduit() {
// It's fairly common to have issues here, e.g. because Phabricator isn't
// running, isn't accessible, you put the domain in your hostsfile but it
// isn't available on the production host, etc. If any of this doesn't work,
// conduit will throw.
$conduit = new ConduitClient(PhabricatorEnv::getURI('/api/'));
$conduit->callMethodSynchronous('conduit.ping', array());
}
public function launchDaemon($daemon, array $argv, $debug = false) {
$symbols = $this->loadAvailableDaemonClasses();
$symbols = ipull($symbols, 'name', 'name');
if (empty($symbols[$daemon])) {
throw new Exception(
"Daemon '{$daemon}' is not loaded, misspelled or abstract.");
}
$libphutil_root = dirname(phutil_get_library_root('phutil'));
$launch_daemon = $libphutil_root.'/scripts/daemon/';
foreach ($argv as $key => $arg) {
$argv[$key] = escapeshellarg($arg);
}
$flags = array();
if ($debug || PhabricatorEnv::getEnvConfig('phd.trace')) {
$flags[] = '--trace';
}
if ($debug || PhabricatorEnv::getEnvConfig('phd.verbose')) {
$flags[] = '--verbose';
}
if (!$debug) {
$flags[] = '--daemonize';
}
$bootloader = PhutilBootloader::getInstance();
foreach ($bootloader->getAllLibraries() as $library) {
if ($library == 'phutil') {
// No need to load libphutil, it's necessarily loaded implicitly by the
// daemon itself.
continue;
}
$flags[] = csprintf(
'--load-phutil-library=%s',
phutil_get_library_root($library));
}
$flags[] = csprintf('--conduit-uri=%s', PhabricatorEnv::getURI('/api/'));
if (!$debug) {
$log_file = $this->getLogDirectory().'/daemons.log';
$flags[] = csprintf('--log=%s', $log_file);
}
$pid_dir = $this->getPIDDirectory();
// TODO: This should be a much better user experience.
Filesystem::assertExists($pid_dir);
Filesystem::assertIsDirectory($pid_dir);
Filesystem::assertWritable($pid_dir);
$flags[] = csprintf('--phd=%s', $pid_dir);
$command = csprintf(
'./launch_daemon.php %s %C %C',
$daemon,
implode(' ', $flags),
implode(' ', $argv));
if ($debug) {
// Don't terminate when the user sends ^C; it will be sent to the
// subprocess which will terminate normally.
pcntl_signal(
SIGINT,
array('PhabricatorDaemonControl', 'ignoreSignal'));
echo "\n libphutil/scripts/daemon/ \$ {$command}\n\n";
phutil_passthru('(cd %s && exec %C)', $launch_daemon, $command);
} else {
$future = new ExecFuture('exec %C', $command);
// Play games to keep 'ps' looking reasonable.
$future->setCWD($launch_daemon);
$future->resolvex();
}
}
public static function ignoreSignal($signo) {
return;
}
private function getControlDirectory($path) {
if (!Filesystem::pathExists($path)) {
list($err) = exec_manual('mkdir -p %s', $path);
if ($err) {
throw new Exception(
"phd requires the directory '{$path}' to exist, but it does not ".
"exist and could not be created. Create this directory or update ".
"'phd.pid-directory' / 'phd.log-directory' in your configuration ".
"to point to an existing directory.");
}
}
return $path;
}
public function getPIDDirectory() {
$path = PhabricatorEnv::getEnvConfig('phd.pid-directory');
return $this->getControlDirectory($path);
}
public function getLogDirectory() {
$path = PhabricatorEnv::getEnvConfig('phd.log-directory');
return $this->getControlDirectory($path);
}
protected function loadAvailableDaemonClasses() {
$loader = new PhutilSymbolLoader();
return $loader
->setAncestorClass('PhutilDaemon')
->setConcreteOnly(true)
->selectSymbolsWithoutLoading();
}
public function loadRunningDaemons() {
$results = array();
$pid_dir = $this->getPIDDirectory();
$pid_files = Filesystem::listDirectory($pid_dir);
if (!$pid_files) {
return $results;
}
foreach ($pid_files as $pid_file) {
$pid_data = Filesystem::readFile($pid_dir.'/'.$pid_file);
$dict = json_decode($pid_data, true);
if (!is_array($dict)) {
// Just return a hanging reference, since control code needs to be
// robust against unusual system states.
$dict = array();
}
$ref = PhabricatorDaemonReference::newFromDictionary($dict);
$ref->setPIDFile($pid_dir.'/'.$pid_file);
$results[] = $ref;
}
return $results;
}
protected function killDaemon(PhabricatorDaemonReference $ref) {
}
}
diff --git a/src/infrastructure/daemon/PhabricatorGarbageCollectorDaemon.php b/src/infrastructure/daemon/PhabricatorGarbageCollectorDaemon.php
index 6db1e5e2f6..bc909bc543 100644
--- a/src/infrastructure/daemon/PhabricatorGarbageCollectorDaemon.php
+++ b/src/infrastructure/daemon/PhabricatorGarbageCollectorDaemon.php
@@ -1,219 +1,203 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Collects old logs and caches to reduce the amount of data stored in the
* database.
*
* @group daemon
*/
final class PhabricatorGarbageCollectorDaemon extends PhabricatorDaemon {
public function run() {
// Keep track of when we start and stop the GC so we can emit useful log
// messages.
$just_ran = false;
do {
$run_at = PhabricatorEnv::getEnvConfig('gcdaemon.run-at');
$run_for = PhabricatorEnv::getEnvConfig('gcdaemon.run-for');
// Just use the default timezone, we don't need to get fancy and try
// to localize this.
$start = strtotime($run_at);
if ($start === false) {
throw new Exception(
"Configuration 'gcdaemon.run-at' could not be parsed: '{$run_at}'.");
}
$now = time();
if ($now < $start || $now > ($start + $run_for)) {
if ($just_ran) {
$this->log("Stopped garbage collector.");
$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) {
$this->log("Started garbage collector.");
$just_ran = true;
}
$n_herald = $this->collectHeraldTranscripts();
$n_daemon = $this->collectDaemonLogs();
$n_parse = $this->collectParseCaches();
$n_markup = $this->collectMarkupCaches();
$n_tasks = $this->collectArchivedTasks();
$collected = array(
'Herald Transcript' => $n_herald,
'Daemon Log' => $n_daemon,
'Differential Parse Cache' => $n_parse,
'Markup Cache' => $n_markup,
'Archived Tasks' => $n_tasks,
);
$collected = array_filter($collected);
foreach ($collected as $thing => $count) {
$count = number_format($count);
$this->log("Garbage collected {$count} '{$thing}' objects.");
}
$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 = "",
garbageCollected = 1
WHERE garbageCollected = 0 AND `time` < %d
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 collectParseCaches() {
$key = 'gcdaemon.ttl.differential-parse-cache';
$ttl = PhabricatorEnv::getEnvConfig($key);
if ($ttl <= 0) {
return 0;
}
$table = new DifferentialChangeset();
$conn_w = $table->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE dateCreated < %d LIMIT 100',
DifferentialChangeset::TABLE_CACHE,
time() - $ttl);
return $conn_w->getAffectedRows();
}
private function collectMarkupCaches() {
$key = 'gcdaemon.ttl.markup-cache';
$ttl = PhabricatorEnv::getEnvConfig($key);
if ($ttl <= 0) {
return 0;
}
$table = new PhabricatorMarkupCache();
$conn_w = $table->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE dateCreated < %d LIMIT 100',
$table->getTableName(),
time() - $ttl);
return $conn_w->getAffectedRows();
}
private function collectArchivedTasks() {
$key = 'gcdaemon.ttl.task-archive';
$ttl = PhabricatorEnv::getEnvConfig($key);
if ($ttl <= 0) {
return 0;
}
$table = new PhabricatorWorkerArchiveTask();
$data_table = new PhabricatorWorkerTaskData();
$conn_w = $table->establishConnection('w');
$rows = queryfx_all(
$conn_w,
'SELECT id, dataID FROM %T WHERE dateCreated < %d LIMIT 100',
$table->getTableName(),
time() - $ttl);
if (!$rows) {
return 0;
}
$data_ids = array_filter(ipull($rows, 'dataID'));
$task_ids = ipull($rows, 'id');
$table->openTransaction();
if ($data_ids) {
queryfx(
$conn_w,
'DELETE FROM %T WHERE id IN (%Ld)',
$data_table->getTableName(),
$data_ids);
}
queryfx(
$conn_w,
'DELETE FROM %T WHERE id IN (%Ld)',
$table->getTableName(),
$task_ids);
$table->saveTransaction();
return count($task_ids);
}
}
diff --git a/src/infrastructure/daemon/control/PhabricatorDaemonReference.php b/src/infrastructure/daemon/control/PhabricatorDaemonReference.php
index c6f82e0a74..15392da9a5 100644
--- a/src/infrastructure/daemon/control/PhabricatorDaemonReference.php
+++ b/src/infrastructure/daemon/control/PhabricatorDaemonReference.php
@@ -1,128 +1,112 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorDaemonReference {
private $name;
private $pid;
private $start;
private $pidFile;
private $daemonLog;
public static function newFromDictionary(array $dict) {
$ref = new PhabricatorDaemonReference();
$ref->name = idx($dict, 'name', 'Unknown');
$ref->pid = idx($dict, 'pid');
$ref->start = idx($dict, 'start');
return $ref;
}
public function updateStatus($new_status) {
try {
if (!$this->daemonLog) {
$this->daemonLog = id(new PhabricatorDaemonLog())->loadOneWhere(
'daemon = %s AND pid = %d AND dateCreated = %d',
$this->name,
$this->pid,
$this->start);
}
if ($this->daemonLog) {
$this->daemonLog
->setStatus($new_status)
->save();
}
} catch (AphrontQueryException $ex) {
// Ignore anything that goes wrong here. We anticipate at least two
// specific failure modes:
//
// - Upgrade scripts which run `git pull`, then `phd stop`, then
// `bin/storage upgrade` will fail when trying to update the `status`
// column, as it does not exist yet.
// - Daemons running on machines which do not have access to MySQL
// (like an IRC bot) will not be able to load or save the log.
//
//
}
}
public function getPID() {
return $this->pid;
}
public function getName() {
return $this->name;
}
public function getEpochStarted() {
return $this->start;
}
public function setPIDFile($pid_file) {
$this->pidFile = $pid_file;
return $this;
}
public function getPIDFile() {
return $this->pidFile;
}
public function isRunning() {
return self::isProcessRunning($this->getPID());
}
public static function isProcessRunning($pid) {
if (!$pid) {
return false;
}
if (function_exists('posix_kill')) {
// This may fail if we can't signal the process because we are running as
// a different user (for example, we are 'apache' and the process is some
// other user's, or we are a normal user and the process is root's), but
// we can check the error code to figure out if the process exists.
$is_running = posix_kill($pid, 0);
if (posix_get_last_error() == 1) {
// "Operation Not Permitted", indicates that the PID exists. If it
// doesn't, we'll get an error 3 ("No such process") instead.
$is_running = true;
}
} else {
// If we don't have the posix extension, just exec.
list($err) = exec_manual('ps %s', $pid);
$is_running = ($err == 0);
}
return $is_running;
}
public function waitForExit($seconds) {
$start = time();
while (time() < $start + $seconds) {
usleep(100000);
if (!$this->isRunning()) {
return true;
}
}
return !$this->isRunning();
}
}
diff --git a/src/infrastructure/daemon/irc/PhabricatorIRCBot.php b/src/infrastructure/daemon/irc/PhabricatorIRCBot.php
index a05ab04fca..c50d682963 100644
--- a/src/infrastructure/daemon/irc/PhabricatorIRCBot.php
+++ b/src/infrastructure/daemon/irc/PhabricatorIRCBot.php
@@ -1,261 +1,245 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Simple IRC bot which runs as a Phabricator daemon. Although this bot is
* somewhat useful, it is also intended to serve as a demo of how to write
* "system agents" which communicate with Phabricator over Conduit, so you can
* script system interactions and integrate with other systems.
*
* NOTE: This is super janky and experimental right now.
*
* @group irc
*/
final class PhabricatorIRCBot extends PhabricatorDaemon {
private $socket;
private $handlers;
private $writeBuffer;
private $readBuffer;
private $conduit;
private $config;
public function run() {
$argv = $this->getArgv();
if (count($argv) !== 1) {
throw new Exception("usage: PhabricatorIRCBot <json_config_file>");
}
$json_raw = Filesystem::readFile($argv[0]);
$config = json_decode($json_raw, true);
if (!is_array($config)) {
throw new Exception("File '{$argv[0]}' is not valid JSON!");
}
$server = idx($config, 'server');
$port = idx($config, 'port', 6667);
$handlers = idx($config, 'handlers', array());
$pass = idx($config, 'pass');
$nick = idx($config, 'nick', 'phabot');
$user = idx($config, 'user', $nick);
$ssl = idx($config, 'ssl', false);
$nickpass = idx($config, 'nickpass');
$this->config = $config;
if (!preg_match('/^[A-Za-z0-9_`[{}^|\]\\-]+$/', $nick)) {
throw new Exception(
"Nickname '{$nick}' is invalid!");
}
foreach ($handlers as $handler) {
$obj = newv($handler, array($this));
$this->handlers[] = $obj;
}
$conduit_uri = idx($config, 'conduit.uri');
if ($conduit_uri) {
$conduit_user = idx($config, 'conduit.user');
$conduit_cert = idx($config, 'conduit.cert');
// Normalize the path component of the URI so users can enter the
// domain without the "/api/" part.
$conduit_uri = new PhutilURI($conduit_uri);
$conduit_uri->setPath('/api/');
$conduit_uri = (string)$conduit_uri;
$conduit = new ConduitClient($conduit_uri);
$response = $conduit->callMethodSynchronous(
'conduit.connect',
array(
'client' => 'PhabricatorIRCBot',
'clientVersion' => '1.0',
'clientDescription' => php_uname('n').':'.$nick,
'user' => $conduit_user,
'certificate' => $conduit_cert,
));
$this->conduit = $conduit;
}
$errno = null;
$error = null;
if (!$ssl) {
$socket = fsockopen($server, $port, $errno, $error);
} else {
$socket = fsockopen('ssl://'.$server, $port, $errno, $error);
}
if (!$socket) {
throw new Exception("Failed to connect, #{$errno}: {$error}");
}
$ok = stream_set_blocking($socket, false);
if (!$ok) {
throw new Exception("Failed to set stream nonblocking.");
}
$this->socket = $socket;
$this->writeCommand('USER', "{$user} 0 * :{$user}");
if ($pass) {
$this->writeCommand('PASS', "{$pass}");
}
$this->writeCommand('NICK', "{$nick}");
$this->runSelectLoop();
}
public function getConfig($key, $default = null) {
return idx($this->config, $key, $default);
}
private function runSelectLoop() {
do {
$this->stillWorking();
$read = array($this->socket);
if (strlen($this->writeBuffer)) {
$write = array($this->socket);
} else {
$write = array();
}
$except = array();
$ok = @stream_select($read, $write, $except, $timeout_sec = 1);
if ($ok === false) {
throw new Exception(
"socket_select() failed: ".socket_strerror(socket_last_error()));
}
if ($read) {
// Test for connection termination; in PHP, fread() off a nonblocking,
// closed socket is empty string.
if (feof($this->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) {
throw new Exception("fread() failed!");
} else {
$this->debugLog(true, $data);
$this->readBuffer .= $data;
}
} while (strlen($data));
}
if ($write) {
do {
$len = fwrite($this->socket, $this->writeBuffer);
if ($len === false) {
throw new Exception("fwrite() failed!");
} else {
$this->debugLog(false, substr($this->writeBuffer, 0, $len));
$this->writeBuffer = substr($this->writeBuffer, $len);
}
} while (strlen($this->writeBuffer));
}
do {
$routed_message = $this->processReadBuffer();
} while ($routed_message);
foreach ($this->handlers as $handler) {
$handler->runBackgroundTasks();
}
} while (true);
}
private function write($message) {
$this->writeBuffer .= $message;
return $this;
}
public function writeCommand($command, $message) {
return $this->write($command.' '.$message."\r\n");
}
private function processReadBuffer() {
$until = strpos($this->readBuffer, "\r\n");
if ($until === false) {
return false;
}
$message = substr($this->readBuffer, 0, $until);
$this->readBuffer = substr($this->readBuffer, $until + 2);
$pattern =
'/^'.
'(?:(?P<sender>:(\S+)) )?'. // This may not be present.
'(?P<command>[A-Z0-9]+) '.
'(?P<data>.*)'.
'$/';
$matches = null;
if (!preg_match($pattern, $message, $matches)) {
throw new Exception("Unexpected message from server: {$message}");
}
$irc_message = new PhabricatorIRCMessage(
idx($matches, 'sender'),
$matches['command'],
$matches['data']);
$this->routeMessage($irc_message);
return true;
}
private function routeMessage(PhabricatorIRCMessage $message) {
foreach ($this->handlers as $handler) {
try {
$handler->receiveMessage($message);
} catch (Exception $ex) {
phlog($ex);
}
}
}
public function __destruct() {
$this->write("QUIT Goodbye.\r\n");
fclose($this->socket);
}
private function debugLog($is_read, $message) {
if ($this->getTraceMode()) {
echo $is_read ? '<<< ' : '>>> ';
echo addcslashes($message, "\0..\37\177..\377");
echo "\n";
}
}
public function getConduit() {
if (empty($this->conduit)) {
throw new Exception(
"This bot is not configured with a Conduit uplink. Set 'conduit.uri', ".
"'conduit.user' and 'conduit.cert' in the configuration to connect.");
}
return $this->conduit;
}
}
diff --git a/src/infrastructure/daemon/irc/PhabricatorIRCMessage.php b/src/infrastructure/daemon/irc/PhabricatorIRCMessage.php
index 3f63afd72f..59feb900d0 100644
--- a/src/infrastructure/daemon/irc/PhabricatorIRCMessage.php
+++ b/src/infrastructure/daemon/irc/PhabricatorIRCMessage.php
@@ -1,93 +1,77 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorIRCMessage {
private $sender;
private $command;
private $data;
public function __construct($sender, $command, $data) {
$this->sender = $sender;
$this->command = $command;
$this->data = $data;
}
public function getRawSender() {
return $this->sender;
}
public function getRawData() {
return $this->data;
}
public function getCommand() {
return $this->command;
}
public function getReplyTo() {
switch ($this->getCommand()) {
case 'PRIVMSG':
$target = $this->getTarget();
if ($target[0] == '#') {
return $target;
}
$matches = null;
if (preg_match('/^:([^!]+)!/', $this->sender, $matches)) {
return $matches[1];
}
break;
}
return null;
}
public function getSenderNickname() {
$nick = $this->getRawSender();
$nick = ltrim($nick, ':');
$nick = head(explode('!', $nick));
return $nick;
}
public function getTarget() {
switch ($this->getCommand()) {
case 'PRIVMSG':
$matches = null;
$raw = $this->getRawData();
if (preg_match('/^(\S+)\s/', $raw, $matches)) {
return $matches[1];
}
break;
}
return null;
}
public function getMessageText() {
switch ($this->getCommand()) {
case 'PRIVMSG':
$matches = null;
$raw = $this->getRawData();
if (preg_match('/^\S+\s+:?(.*)$/', $raw, $matches)) {
return rtrim($matches[1], "\r\n");
}
break;
}
return null;
}
}
diff --git a/src/infrastructure/daemon/irc/handler/PhabricatorIRCDifferentialNotificationHandler.php b/src/infrastructure/daemon/irc/handler/PhabricatorIRCDifferentialNotificationHandler.php
index 387159f88b..c0a1f9fc88 100644
--- a/src/infrastructure/daemon/irc/handler/PhabricatorIRCDifferentialNotificationHandler.php
+++ b/src/infrastructure/daemon/irc/handler/PhabricatorIRCDifferentialNotificationHandler.php
@@ -1,67 +1,51 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group irc
*/
final class PhabricatorIRCDifferentialNotificationHandler
extends PhabricatorIRCHandler {
private $skippedOldEvents;
public function receiveMessage(PhabricatorIRCMessage $message) {
return;
}
public function runBackgroundTasks() {
$iterator = new PhabricatorTimelineIterator('ircdiffx', array('difx'));
$show = $this->getConfig('notification.actions');
if (!$this->skippedOldEvents) {
// Since we only want to post notifications about new events, skip
// everything that's happened in the past when we start up so we'll
// only process real-time events.
foreach ($iterator as $event) {
// Ignore all old events.
}
$this->skippedOldEvents = true;
return;
}
foreach ($iterator as $event) {
$data = $event->getData();
if (!$data || ($show !== null && !in_array($data['action'], $show))) {
continue;
}
$actor_phid = $data['actor_phid'];
$phids = array($actor_phid);
$handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
$verb = DifferentialAction::getActionPastTenseVerb($data['action']);
$actor_name = $handles[$actor_phid]->getName();
$message = "{$actor_name} {$verb} revision D".$data['revision_id'].".";
$channels = $this->getConfig('notification.channels', array());
foreach ($channels as $channel) {
$this->write('PRIVMSG', "{$channel} :{$message}");
}
}
}
}
diff --git a/src/infrastructure/daemon/irc/handler/PhabricatorIRCHandler.php b/src/infrastructure/daemon/irc/handler/PhabricatorIRCHandler.php
index 6a8fe55e4a..8f059bed67 100644
--- a/src/infrastructure/daemon/irc/handler/PhabricatorIRCHandler.php
+++ b/src/infrastructure/daemon/irc/handler/PhabricatorIRCHandler.php
@@ -1,62 +1,46 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Responds to IRC messages. You plug a bunch of these into a
* @{class:PhabricatorIRCBot} to give it special behavior.
*
* @group irc
*/
abstract class PhabricatorIRCHandler {
private $bot;
final public function __construct(PhabricatorIRCBot $irc_bot) {
$this->bot = $irc_bot;
}
final protected function write($command, $message) {
$this->bot->writeCommand($command, $message);
return $this;
}
final protected function getConduit() {
return $this->bot->getConduit();
}
final protected function getConfig($key, $default = null) {
return $this->bot->getConfig($key, $default);
}
final protected function getURI($path) {
$base_uri = new PhutilURI($this->bot->getConfig('conduit.uri'));
$base_uri->setPath($path);
return (string)$base_uri;
}
final protected function isChannelName($name) {
return (strncmp($name, '#', 1) === 0);
}
abstract public function receiveMessage(PhabricatorIRCMessage $message);
public function runBackgroundTasks() {
return;
}
}
diff --git a/src/infrastructure/daemon/irc/handler/PhabricatorIRCLogHandler.php b/src/infrastructure/daemon/irc/handler/PhabricatorIRCLogHandler.php
index 585738ea88..5576aa8bb3 100644
--- a/src/infrastructure/daemon/irc/handler/PhabricatorIRCLogHandler.php
+++ b/src/infrastructure/daemon/irc/handler/PhabricatorIRCLogHandler.php
@@ -1,94 +1,78 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Logs chatter.
*
* @group irc
*/
final class PhabricatorIRCLogHandler extends PhabricatorIRCHandler {
private $futures = array();
public function receiveMessage(PhabricatorIRCMessage $message) {
switch ($message->getCommand()) {
case 'PRIVMSG':
$reply_to = $message->getReplyTo();
if (!$reply_to) {
break;
}
if (!$this->isChannelName($reply_to)) {
// Don't log private messages, although maybe we should for debugging?
break;
}
$logs = array(
array(
'channel' => $reply_to,
'type' => 'mesg',
'epoch' => time(),
'author' => $message->getSenderNickname(),
'message' => $message->getMessageText(),
),
);
$this->futures[] = $this->getConduit()->callMethod(
'chatlog.record',
array(
'logs' => $logs,
));
$prompts = array(
'/where is the (chat)?log\?/i',
'/where am i\?/i',
'/what year is (this|it)\?/i',
);
$tell = false;
foreach ($prompts as $prompt) {
if (preg_match($prompt, $message->getMessageText())) {
$tell = true;
break;
}
}
if ($tell) {
$response = $this->getURI(
'/chatlog/channel/'.phutil_escape_uri($reply_to).'/');
$this->write('PRIVMSG', "{$reply_to} :{$response}");
}
break;
}
}
public function runBackgroundTasks() {
foreach ($this->futures as $key => $future) {
try {
if ($future->isReady()) {
unset($this->futures[$key]);
}
} catch (Exception $ex) {
unset($this->futures[$key]);
phlog($ex);
}
}
}
}
diff --git a/src/infrastructure/daemon/irc/handler/PhabricatorIRCMacroHandler.php b/src/infrastructure/daemon/irc/handler/PhabricatorIRCMacroHandler.php
index 63ff2879b8..078d77ac31 100644
--- a/src/infrastructure/daemon/irc/handler/PhabricatorIRCMacroHandler.php
+++ b/src/infrastructure/daemon/irc/handler/PhabricatorIRCMacroHandler.php
@@ -1,203 +1,187 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group irc
*/
final class PhabricatorIRCMacroHandler extends PhabricatorIRCHandler {
private $macros;
private $regexp;
private $buffer = array();
private $next = 0;
private function init() {
if ($this->macros === false) {
return false;
}
if ($this->macros !== null) {
return true;
}
$macros = $this->getConduit()->callMethodSynchronous(
'macro.query',
array());
// bail if we have no macros
if (empty($macros)) {
return false;
}
$this->macros = $macros;
$regexp = array();
foreach ($this->macros as $macro_name => $macro) {
$regexp[] = preg_quote($macro_name, '/');
}
$regexp = '/('.implode('|', $regexp).')/';
$this->regexp = $regexp;
return true;
}
public function receiveMessage(PhabricatorIRCMessage $message) {
if (!$this->init()) {
return;
}
switch ($message->getCommand()) {
case 'PRIVMSG':
$reply_to = $message->getReplyTo();
if (!$reply_to) {
break;
}
$message = $message->getMessageText();
$matches = null;
if (!preg_match($this->regexp, $message, $matches)) {
return;
}
$macro = $matches[1];
$ascii = idx($this->macros[$macro], 'ascii');
if ($ascii === false) {
return;
}
if (!$ascii) {
$this->macros[$macro]['ascii'] = $this->rasterize(
$this->macros[$macro],
$this->getConfig('macro.size', 48),
$this->getConfig('macro.aspect', 0.66));
$ascii = $this->macros[$macro]['ascii'];
}
foreach ($ascii as $line) {
$this->buffer[$reply_to][] = $line;
}
break;
}
}
public function runBackgroundTasks() {
if (microtime(true) < $this->next) {
return;
}
foreach ($this->buffer as $channel => $lines) {
if (empty($lines)) {
unset($this->buffer[$channel]);
continue;
}
foreach ($lines as $key => $line) {
$this->write('PRIVMSG', "{$channel} :{$line}");
unset($this->buffer[$channel][$key]);
break 2;
}
}
$sleep = $this->getConfig('macro.sleep', 0.25);
$this->next = microtime(true) + ((mt_rand(75, 150) / 100) * $sleep);
}
public function rasterize($macro, $size, $aspect) {
$image = HTTPSFuture::loadContent($macro['uri']);
if (!$image) {
return false;
}
$img = @imagecreatefromstring($image);
if (!$img) {
return false;
}
$sx = imagesx($img);
$sy = imagesy($img);
if ($sx > $size || $sy > $size) {
$scale = max($sx, $sy) / $size;
$dx = floor($sx / $scale);
$dy = floor($sy / $scale);
} else {
$dx = $sx;
$dy = $sy;
}
$dy = floor($dy * $aspect);
$dst = imagecreatetruecolor($dx, $dy);
if (!$dst) {
return false;
}
imagealphablending($dst, false);
$ok = imagecopyresampled(
$dst, $img,
0, 0,
0, 0,
$dx, $dy,
$sx, $sy);
if (!$ok) {
return false;
}
$map = array(
' ',
'.',
',',
':',
';',
'!',
'|',
'*',
'=',
'@',
'$',
'#',
);
$lines = array();
for ($ii = 0; $ii < $dy; $ii++) {
$buf = '';
for ($jj = 0; $jj < $dx; $jj++) {
$c = imagecolorat($dst, $jj, $ii);
$a = ($c >> 24) & 0xFF;
$r = ($c >> 16) & 0xFF;
$g = ($c >> 8) & 0xFF;
$b = ($c) & 0xFF;
$luma = (255 - ((0.30 * $r) + (0.59 * $g) + (0.11 * $b))) / 256;
$luma *= ((127 - $a) / 127);
$char = $map[max(0, floor($luma * count($map)))];
$buf .= $char;
}
$lines[] = $buf;
}
return $lines;
}
}
diff --git a/src/infrastructure/daemon/irc/handler/PhabricatorIRCObjectNameHandler.php b/src/infrastructure/daemon/irc/handler/PhabricatorIRCObjectNameHandler.php
index e319484e7f..bb4c1e3fb6 100644
--- a/src/infrastructure/daemon/irc/handler/PhabricatorIRCObjectNameHandler.php
+++ b/src/infrastructure/daemon/irc/handler/PhabricatorIRCObjectNameHandler.php
@@ -1,262 +1,246 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Looks for Dxxxx, Txxxx and links to them.
*
* @group irc
*/
final class PhabricatorIRCObjectNameHandler extends PhabricatorIRCHandler {
/**
* Map of PHIDs to the last mention of them (as an epoch timestamp); prevents
* us from spamming chat when a single object is discussed.
*/
private $recentlyMentioned = array();
public function receiveMessage(PhabricatorIRCMessage $message) {
switch ($message->getCommand()) {
case 'PRIVMSG':
$reply_to = $message->getReplyTo();
if (!$reply_to) {
break;
}
$this->handleSymbols($message);
$message = $message->getMessageText();
$matches = null;
$pattern =
'@'.
'(?<!/)(?:^|\b)'. // Negative lookbehind prevent matching "/D123".
'(D|T|P|V|F)(\d+)'.
'(?:\b|$)'.
'@';
$pattern_override = '/(^[^\s]+)[,:] [DTPVF]\d+/';
$revision_ids = array();
$task_ids = array();
$paste_ids = array();
$commit_names = array();
$vote_ids = array();
$file_ids = array();
$matches_override = array();
if (preg_match_all($pattern, $message, $matches, PREG_SET_ORDER)) {
if (preg_match($pattern_override, $message, $matches_override)) {
$reply_to = $matches_override[1];
}
foreach ($matches as $match) {
switch ($match[1]) {
case 'D':
$revision_ids[] = $match[2];
break;
case 'T':
$task_ids[] = $match[2];
break;
case 'P':
$paste_ids[] = $match[2];
break;
case 'V':
$vote_ids[] = $match[2];
break;
case 'F':
$file_ids[] = $match[2];
break;
}
}
}
$pattern =
'@'.
'(?<!/)(?:^|\b)'.
'(r[A-Z]+[0-9a-z]{1,40})'.
'(?:\b|$)'.
'@';
if (preg_match_all($pattern, $message, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
$commit_names[] = $match[1];
}
}
$output = array();
if ($revision_ids) {
$revisions = $this->getConduit()->callMethodSynchronous(
'differential.query',
array(
'query' => 'revision-ids',
'ids' => $revision_ids,
));
$revisions = array_select_keys(
ipull($revisions, null, 'id'),
$revision_ids
);
foreach ($revisions as $revision) {
$output[$revision['phid']] =
'D'.$revision['id'].' '.$revision['title'].' - '.
$revision['uri'];
}
}
if ($task_ids) {
foreach ($task_ids as $task_id) {
if ($task_id == 1000) {
$output[1000] = 'T1000: A nanomorph mimetic poly-alloy'
.'(liquid metal) assassin controlled by Skynet: '
.'http://en.wikipedia.org/wiki/T-1000';
continue;
}
$task = $this->getConduit()->callMethodSynchronous(
'maniphest.info',
array(
'task_id' => $task_id,
));
$output[$task['phid']] = 'T'.$task['id'].': '.$task['title'].
' (Priority: '.$task['priority'].') - '.$task['uri'];
}
}
if ($vote_ids) {
foreach ($vote_ids as $vote_id) {
$vote = $this->getConduit()->callMethodSynchronous(
'slowvote.info',
array(
'poll_id' => $vote_id,
));
$output[$vote['phid']] = 'V'.$vote['id'].': '.$vote['question'].
' Come Vote '.$vote['uri'];
}
}
if ($file_ids) {
foreach ($file_ids as $file_id) {
$file = $this->getConduit()->callMethodSynchronous(
'file.info',
array(
'id' => $file_id,
));
$output[$file['phid']] = $file['objectName'].": ".$file['uri']." - ".
$file['name'];
}
}
if ($paste_ids) {
foreach ($paste_ids as $paste_id) {
$paste = $this->getConduit()->callMethodSynchronous(
'paste.info',
array(
'paste_id' => $paste_id,
));
// Eventually I'd like to show the username of the paster as well,
// however that will need something like a user.username_from_phid
// 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'].' - '.
$paste['title'];
if ($paste['language']) {
$output[$paste['phid']] .= ' ('.$paste['language'].')';
}
}
}
if ($commit_names) {
$commits = $this->getConduit()->callMethodSynchronous(
'diffusion.getcommits',
array(
'commits' => $commit_names,
));
foreach ($commits as $commit) {
if (isset($commit['error'])) {
continue;
}
$output[$commit['commitPHID']] = $commit['uri'];
}
}
foreach ($output as $phid => $description) {
// Don't mention the same object more than once every 10 minutes
// in public channels, so we avoid spamming the chat over and over
// again for discsussions of a specific revision, for example. In
// direct-to-bot chat, respond to every object reference.
if ($this->isChannelName($reply_to)) {
if (empty($this->recentlyMentioned[$reply_to])) {
$this->recentlyMentioned[$reply_to] = array();
}
$quiet_until = idx(
$this->recentlyMentioned[$reply_to],
$phid,
0) + (60 * 10);
if (time() < $quiet_until) {
// Remain quiet on this channel.
continue;
}
$this->recentlyMentioned[$reply_to][$phid] = time();
}
$this->write('PRIVMSG', "{$reply_to} :{$description}");
}
break;
}
}
private function handleSymbols(PhabricatorIRCMessage $message) {
$reply_to = $message->getReplyTo();
$text = $message->getMessageText();
$matches = null;
if (!preg_match('/where(?: in the world)? is (\S+?)\?/i',
$text, $matches)) {
return;
}
$symbol = $matches[1];
$results = $this->getConduit()->callMethodSynchronous(
'diffusion.findsymbols',
array(
'name' => $symbol,
));
$default_uri = $this->getURI('/diffusion/symbol/'.$symbol.'/');
if (count($results) > 1) {
$response = "Multiple symbols named '{$symbol}': {$default_uri}";
} else if (count($results) == 1) {
$result = head($results);
$response =
$result['type'].' '.
$result['name'].' '.
'('.$result['language'].'): '.
nonempty($result['uri'], $default_uri);
} else {
$response = "No symbol '{$symbol}' found anywhere.";
}
$this->write('PRIVMSG', "{$reply_to} :{$response}");
}
}
diff --git a/src/infrastructure/daemon/irc/handler/PhabricatorIRCProtocolHandler.php b/src/infrastructure/daemon/irc/handler/PhabricatorIRCProtocolHandler.php
index 52129e1c90..1347d2bbdc 100644
--- a/src/infrastructure/daemon/irc/handler/PhabricatorIRCProtocolHandler.php
+++ b/src/infrastructure/daemon/irc/handler/PhabricatorIRCProtocolHandler.php
@@ -1,48 +1,32 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Implements the base IRC protocol so servers don't kick you off.
*
* @group irc
*/
final class PhabricatorIRCProtocolHandler extends PhabricatorIRCHandler {
public function receiveMessage(PhabricatorIRCMessage $message) {
switch ($message->getCommand()) {
case '422': // Error - no MOTD
case '376': // End of MOTD
$nickpass = $this->getConfig('nickpass');
if ($nickpass) {
$this->write('PRIVMSG', "nickserv :IDENTIFY {$nickpass}");
}
$join = $this->getConfig('join');
if (!$join) {
throw new Exception("Not configured to join any channels!");
}
foreach ($join as $channel) {
$this->write('JOIN', $channel);
}
break;
case 'PING':
$this->write('PONG', $message->getRawData());
break;
}
}
}
diff --git a/src/infrastructure/daemon/irc/handler/PhabricatorIRCWhatsNewHandler.php b/src/infrastructure/daemon/irc/handler/PhabricatorIRCWhatsNewHandler.php
index ff04dfc1c1..e065099772 100644
--- a/src/infrastructure/daemon/irc/handler/PhabricatorIRCWhatsNewHandler.php
+++ b/src/infrastructure/daemon/irc/handler/PhabricatorIRCWhatsNewHandler.php
@@ -1,165 +1,149 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Responds to "Whats new?" using the feed.
*
* @group irc
*/
final class PhabricatorIRCWhatsNewHandler extends PhabricatorIRCHandler {
private $floodblock = 0;
public function receiveMessage(PhabricatorIRCMessage $message) {
switch ($message->getCommand()) {
case 'PRIVMSG':
$reply_to = $message->getReplyTo();
if (!$reply_to) {
break;
}
$message = $message->getMessageText();
$prompt = '~what( i|\')?s new\?~i';
if (preg_match($prompt, $message)) {
if (time() < $this->floodblock) {
return;
}
$this->floodblock = time() + 60;
$this->getLatest($reply_to);
}
break;
}
}
public function getLatest($reply_to) {
$latest = $this->getConduit()->callMethodSynchronous(
'feed.query',
array(
'limit'=>5
));
$phids = array();
foreach ($latest as $action) {
if (isset($action['data']['actor_phid'])) {
$uid = $action['data']['actor_phid'];
}
else {
$uid = $action['authorPHID'];
}
switch ($action['class']) {
case 'PhabricatorFeedStoryDifferential':
$phids[] = $action['data']['revision_phid'];
break;
case 'PhabricatorFeedStoryAudit':
$phids[] = $action['data']['commitPHID'];
break;
case 'PhabricatorFeedStoryManiphest':
$phids[] = $action['data']['taskPHID'];
break;
default:
$phids[] = $uid;
break;
}
array_push($phids,$uid);
}
$infs = $this->getConduit()->callMethodSynchronous(
'phid.query',
array(
'phids'=>$phids
));
$cphid = 0;
foreach ($latest as $action) {
if (isset($action['data']['actor_phid'])) {
$uid = $action['data']['actor_phid'];
}
else {
$uid = $action['authorPHID'];
}
switch ($action['class']) {
case 'PhabricatorFeedStoryDifferential':
$rinf = $infs[$action['data']['revision_phid']];
break;
case 'PhabricatorFeedStoryAudit':
$rinf = $infs[$action['data']['commitPHID']];
break;
case 'PhabricatorFeedStoryManiphest':
$rinf = $infs[$action['data']['taskPHID']];
break;
default:
$rinf = array('name'=>$action['class']);
break;
}
$uinf = $infs[$uid];
$action = $this->getRhetoric($action['data']['action']);
$user = $uinf['name'];
$title = $rinf['fullName'];
$uri = $rinf['uri'];
$color = chr(3);
$blue = $color.'12';
$gray = $color.'15';
$bold = chr(2);
$reset = chr(15);
$content = "{$bold}{$user}{$reset} {$gray}{$action} {$blue}{$bold}".
"{$title}{$reset} - {$gray}{$uri}{$reset}";
$this->write('PRIVMSG',"{$reply_to} :{$content}");
}
return;
}
public function getRhetoric($input) {
switch ($input) {
case 'comment':
case 'none':
return 'commented on';
break;
case 'update':
return 'updated';
break;
case 'commit':
return 'closed';
break;
case 'create':
return 'created';
break;
case 'concern':
return 'raised concern for';
break;
case 'abandon':
return 'abandonned';
break;
case 'accept':
return 'accepted';
break;
default:
return $input;
break;
}
}
}
diff --git a/src/infrastructure/daemon/storage/PhabricatorDaemonDAO.php b/src/infrastructure/daemon/storage/PhabricatorDaemonDAO.php
index 17acac13fb..6167868298 100644
--- a/src/infrastructure/daemon/storage/PhabricatorDaemonDAO.php
+++ b/src/infrastructure/daemon/storage/PhabricatorDaemonDAO.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorDaemonDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'daemon';
}
}
diff --git a/src/infrastructure/daemon/storage/PhabricatorDaemonLog.php b/src/infrastructure/daemon/storage/PhabricatorDaemonLog.php
index 8a53fccf22..cb68cd6398 100644
--- a/src/infrastructure/daemon/storage/PhabricatorDaemonLog.php
+++ b/src/infrastructure/daemon/storage/PhabricatorDaemonLog.php
@@ -1,40 +1,24 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorDaemonLog extends PhabricatorDaemonDAO {
const STATUS_UNKNOWN = 'unknown';
const STATUS_RUNNING = 'run';
const STATUS_DEAD = 'dead';
const STATUS_EXITED = 'exit';
protected $daemon;
protected $host;
protected $pid;
protected $argv;
protected $status;
public function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'argv' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
}
diff --git a/src/infrastructure/daemon/storage/PhabricatorDaemonLogEvent.php b/src/infrastructure/daemon/storage/PhabricatorDaemonLogEvent.php
index a02d61e9f4..473ca45e33 100644
--- a/src/infrastructure/daemon/storage/PhabricatorDaemonLogEvent.php
+++ b/src/infrastructure/daemon/storage/PhabricatorDaemonLogEvent.php
@@ -1,32 +1,16 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorDaemonLogEvent extends PhabricatorDaemonDAO {
protected $logID;
protected $logType;
protected $message;
protected $epoch;
public function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
) + parent::getConfiguration();
}
}
diff --git a/src/infrastructure/daemon/timeline/cursor/PhabricatorTimelineIterator.php b/src/infrastructure/daemon/timeline/cursor/PhabricatorTimelineIterator.php
index 370c824fe5..4b9b988d25 100644
--- a/src/infrastructure/daemon/timeline/cursor/PhabricatorTimelineIterator.php
+++ b/src/infrastructure/daemon/timeline/cursor/PhabricatorTimelineIterator.php
@@ -1,116 +1,100 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorTimelineIterator implements Iterator {
protected $cursorName;
protected $eventTypes;
protected $cursor;
protected $index = -1;
protected $events = array();
const LOAD_CHUNK_SIZE = 128;
public function __construct($cursor_name, array $event_types) {
$this->cursorName = $cursor_name;
$this->eventTypes = $event_types;
}
protected function loadEvents() {
if (!$this->cursor) {
$this->cursor = id(new PhabricatorTimelineCursor())->loadOneWhere(
'name = %s',
$this->cursorName);
if (!$this->cursor) {
$cursor = new PhabricatorTimelineCursor();
$cursor->setName($this->cursorName);
$cursor->setPosition(0);
$cursor->save();
$this->cursor = $cursor;
}
}
$event = new PhabricatorTimelineEvent('NULL');
$event_data = new PhabricatorTimelineEventData();
$raw_data = queryfx_all(
$event->establishConnection('r'),
'SELECT event.*, event_data.eventData eventData
FROM %T event
LEFT JOIN %T event_data ON event_data.id = event.dataID
WHERE event.id > %d AND event.type in (%Ls)
ORDER BY event.id ASC LIMIT %d',
$event->getTableName(),
$event_data->getTableName(),
$this->cursor->getPosition(),
$this->eventTypes,
self::LOAD_CHUNK_SIZE);
$events = $event->loadAllFromArray($raw_data);
$events = mpull($events, null, 'getID');
$raw_data = ipull($raw_data, 'eventData', 'id');
foreach ($raw_data as $id => $data) {
if ($data) {
$decoded = json_decode($data, true);
$events[$id]->setData($decoded);
}
}
$this->events = $events;
if ($this->events) {
$this->events = array_values($this->events);
$this->index = 0;
} else {
$this->cursor = null;
}
}
public function current() {
return $this->events[$this->index];
}
public function key() {
return $this->events[$this->index]->getID();
}
public function next() {
if ($this->valid()) {
$this->cursor->setPosition($this->key());
$this->cursor->save();
}
$this->index++;
if (!$this->valid()) {
$this->loadEvents();
}
}
public function valid() {
return isset($this->events[$this->index]);
}
public function rewind() {
if (!$this->valid()) {
$this->loadEvents();
}
}
}
diff --git a/src/infrastructure/daemon/timeline/storage/PhabricatorTimelineCursor.php b/src/infrastructure/daemon/timeline/storage/PhabricatorTimelineCursor.php
index 80af3e26ab..c11b88de3f 100644
--- a/src/infrastructure/daemon/timeline/storage/PhabricatorTimelineCursor.php
+++ b/src/infrastructure/daemon/timeline/storage/PhabricatorTimelineCursor.php
@@ -1,42 +1,26 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorTimelineCursor extends PhabricatorTimelineDAO {
protected $name;
protected $position;
public function getIDKey() {
return 'name';
}
public function getConfiguration() {
return array(
self::CONFIG_IDS => self::IDS_MANUAL,
self::CONFIG_TIMESTAMPS => false,
) + parent::getConfiguration();
}
public function shouldInsertWhenSaved() {
if ($this->position == 0) {
return true;
}
return false;
}
}
diff --git a/src/infrastructure/daemon/timeline/storage/PhabricatorTimelineDAO.php b/src/infrastructure/daemon/timeline/storage/PhabricatorTimelineDAO.php
index 31d2c35c89..b9cf020a9e 100644
--- a/src/infrastructure/daemon/timeline/storage/PhabricatorTimelineDAO.php
+++ b/src/infrastructure/daemon/timeline/storage/PhabricatorTimelineDAO.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorTimelineDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'timeline';
}
}
diff --git a/src/infrastructure/daemon/timeline/storage/PhabricatorTimelineEvent.php b/src/infrastructure/daemon/timeline/storage/PhabricatorTimelineEvent.php
index 870160be89..adedb8e0b1 100644
--- a/src/infrastructure/daemon/timeline/storage/PhabricatorTimelineEvent.php
+++ b/src/infrastructure/daemon/timeline/storage/PhabricatorTimelineEvent.php
@@ -1,71 +1,55 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorTimelineEvent extends PhabricatorTimelineDAO {
protected $type;
protected $dataID;
private $data;
public function __construct($type, $data = null) {
parent::__construct();
if (strlen($type) !== 4) {
throw new Exception("Event types must be exactly 4 characters long.");
}
$this->type = $type;
$this->data = $data;
}
public function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
) + parent::getConfiguration();
}
public function recordEvent() {
if ($this->getID()) {
throw new Exception("Event has already been recorded!");
}
// Save the data first and point to it from the event to avoid a race
// condition where we insert the event before the data and a consumer reads
// it immediately.
if ($this->data !== null) {
$data = new PhabricatorTimelineEventData();
$data->setEventData($this->data);
$data->save();
$this->setDataID($data->getID());
}
$this->save();
}
public function setData($data) {
$this->data = $data;
return $this;
}
public function getData() {
return $this->data;
}
}
diff --git a/src/infrastructure/daemon/timeline/storage/PhabricatorTimelineEventData.php b/src/infrastructure/daemon/timeline/storage/PhabricatorTimelineEventData.php
index 4163578bf6..c786300a30 100644
--- a/src/infrastructure/daemon/timeline/storage/PhabricatorTimelineEventData.php
+++ b/src/infrastructure/daemon/timeline/storage/PhabricatorTimelineEventData.php
@@ -1,32 +1,16 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorTimelineEventData extends PhabricatorTimelineDAO {
protected $eventData;
public function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'eventData' => self::SERIALIZATION_JSON,
),
self::CONFIG_TIMESTAMPS => false,
) + parent::getConfiguration();
}
}
diff --git a/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php b/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php
index f9f275acd5..2cccdb8d69 100644
--- a/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php
+++ b/src/infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php
@@ -1,54 +1,38 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorTaskmasterDaemon extends PhabricatorDaemon {
public function run() {
$sleep = 0;
do {
$tasks = id(new PhabricatorWorkerLeaseQuery())
->setLimit(1)
->execute();
if ($tasks) {
foreach ($tasks as $task) {
$id = $task->getID();
$class = $task->getTaskClass();
$this->log("Working on task {$id} ({$class})...");
$task = $task->executeTask();
$ex = $task->getExecutionException();
if ($ex) {
$this->log("Task {$id} failed!");
throw $ex;
} else {
$this->log("Task {$id} complete! Moved to archive.");
}
}
$sleep = 0;
} else {
$sleep = min($sleep + 1, 30);
}
$this->sleep($sleep);
} while (true);
}
}
diff --git a/src/infrastructure/daemon/workers/PhabricatorWorker.php b/src/infrastructure/daemon/workers/PhabricatorWorker.php
index 23e339cfef..cbc0c2bf15 100644
--- a/src/infrastructure/daemon/workers/PhabricatorWorker.php
+++ b/src/infrastructure/daemon/workers/PhabricatorWorker.php
@@ -1,153 +1,137 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @task config Configuring Retries and Failures
*
* @group worker
*/
abstract class PhabricatorWorker {
private $data;
/* -( Configuring Retries and Failures )----------------------------------- */
/**
* Return the number of seconds this worker needs hold a lease on the task for
* while it performs work. For most tasks you can leave this at `null`, which
* will give you a short default lease (currently 60 seconds).
*
* For tasks which may take a very long time to complete, you should return
* an upper bound on the amount of time the task may require.
*
* @return int|null Number of seconds this task needs to remain leased for,
* or null for a default (currently 60 second) lease.
*
* @task config
*/
public function getRequiredLeaseTime() {
return null;
}
/**
* Return the maximum number of times this task may be retried before it
* is considered permanently failed. By default, tasks retry indefinitely. You
* can throw a @{class:PhabricatorWorkerPermanentFailureException} to cause an
* immediate permanent failure.
*
* @return int|null Number of times the task will retry before permanent
* failure. Return `null` to retry indefinitely.
*
* @task config
*/
public function getMaximumRetryCount() {
return null;
}
/**
* Return the number of seconds a task should wait after a failure before
* retrying. For most tasks you can leave this at `null`, which will give you
* a short default retry period (currently 60 seconds).
*
* @param PhabricatorWorkerTask The task itself. This object is probably
* useful mostly to examine the failure
* count if you want to implement staggered
* retries, or to examine the execution
* exception if you want to react to
* different failures in different ways.
* @param Exception The exception which caused the failure.
* @return int|null Number of seconds to wait between retries,
* or null for a default retry period
* (currently 60 seconds).
*
* @task config
*/
public function getWaitBeforeRetry(PhabricatorWorkerTask $task) {
return null;
}
abstract protected function doWork();
final public function __construct($data) {
$this->data = $data;
}
final protected function getTaskData() {
return $this->data;
}
final public function executeTask() {
$this->doWork();
}
final public static function scheduleTask($task_class, $data) {
return id(new PhabricatorWorkerActiveTask())
->setTaskClass($task_class)
->setData($data)
->save();
}
/**
* Wait for tasks to complete. If tasks are not leased by other workers, they
* will be executed in this process while waiting.
*
* @param list<int> List of queued task IDs to wait for.
* @return void
*/
final public static function waitForTasks(array $task_ids) {
$task_table = new PhabricatorWorkerActiveTask();
$waiting = array_combine($task_ids, $task_ids);
while ($waiting) {
$conn_w = $task_table->establishConnection('w');
// Check if any of the tasks we're waiting on are still queued. If they
// are not, we're done waiting.
$row = queryfx_one(
$conn_w,
'SELECT COUNT(*) N FROM %T WHERE id IN (%Ld)',
$task_table->getTableName(),
$waiting);
if (!$row['N']) {
// Nothing is queued anymore. Stop waiting.
break;
}
$tasks = id(new PhabricatorWorkerLeaseQuery())
->withIDs($waiting)
->setLimit(1)
->execute();
if (!$tasks) {
// We were not successful in leasing anything. Sleep for a bit and
// see if we have better luck later.
sleep(1);
continue;
}
$task = head($tasks)->executeTask();
}
}
}
diff --git a/src/infrastructure/daemon/workers/__tests__/PhabricatorTestWorker.php b/src/infrastructure/daemon/workers/__tests__/PhabricatorTestWorker.php
index 87141f17c2..313074cc90 100644
--- a/src/infrastructure/daemon/workers/__tests__/PhabricatorTestWorker.php
+++ b/src/infrastructure/daemon/workers/__tests__/PhabricatorTestWorker.php
@@ -1,55 +1,39 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorTestWorker extends PhabricatorWorker {
public function getRequiredLeaseTime() {
return idx(
$this->getTaskData(),
'getRequiredLeaseTime',
parent::getRequiredLeaseTime());
}
public function getMaximumRetryCount() {
return idx(
$this->getTaskData(),
'getMaximumRetryCount',
parent::getMaximumRetryCount());
}
public function getWaitBeforeRetry(PhabricatorWorkerTask $task) {
return idx(
$this->getTaskData(),
'getWaitBeforeRetry',
parent::getWaitBeforeRetry($task));
}
protected function doWork() {
switch (idx($this->getTaskData(), 'doWork')) {
case 'fail-temporary':
throw new Exception(
"Temporary failure!");
case 'fail-permanent':
throw new PhabricatorWorkerPermanentFailureException(
"Permanent failure!");
default:
return;
}
}
}
diff --git a/src/infrastructure/daemon/workers/__tests__/PhabricatorWorkerTestCase.php b/src/infrastructure/daemon/workers/__tests__/PhabricatorWorkerTestCase.php
index a5d21aa104..e58650371f 100644
--- a/src/infrastructure/daemon/workers/__tests__/PhabricatorWorkerTestCase.php
+++ b/src/infrastructure/daemon/workers/__tests__/PhabricatorWorkerTestCase.php
@@ -1,202 +1,186 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorWorkerTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testLeaseTask() {
// Leasing should work.
$task = $this->scheduleTask();
$this->expectNextLease($task);
}
public function testMultipleLease() {
// We should not be able to lease a task multiple times.
$task = $this->scheduleTask();
$this->expectNextLease($task);
$this->expectNextLease(null);
}
public function testOldestFirst() {
// Older tasks should lease first, all else being equal.
$task1 = $this->scheduleTask();
$task2 = $this->scheduleTask();
$this->expectNextLease($task1);
$this->expectNextLease($task2);
}
public function testNewBeforeLeased() {
// Tasks not previously leased should lease before previously leased tasks.
$task1 = $this->scheduleTask();
$task2 = $this->scheduleTask();
$task1->setLeaseOwner('test');
$task1->setLeaseExpires(time() - 100000);
$task1->forceSaveWithoutLease();
$this->expectNextLease($task2);
$this->expectNextLease($task1);
}
public function testExecuteTask() {
$task = $this->scheduleAndExecuteTask();
$this->assertEqual(true, $task->isArchived());
$this->assertEqual(
PhabricatorWorkerArchiveTask::RESULT_SUCCESS,
$task->getResult());
}
public function testPermanentTaskFailure() {
$task = $this->scheduleAndExecuteTask(
array(
'doWork' => 'fail-permanent',
));
$this->assertEqual(true, $task->isArchived());
$this->assertEqual(
PhabricatorWorkerArchiveTask::RESULT_FAILURE,
$task->getResult());
}
public function testTemporaryTaskFailure() {
$task = $this->scheduleAndExecuteTask(
array(
'doWork' => 'fail-temporary',
));
$this->assertEqual(false, $task->isArchived());
$this->assertEqual(
true,
($task->getExecutionException() instanceof Exception));
}
public function testTooManyTaskFailures() {
// Expect temporary failures, then a permanent failure.
$task = $this->scheduleAndExecuteTask(
array(
'doWork' => 'fail-temporary',
'getMaximumRetryCount' => 3,
'getWaitBeforeRetry' => -60,
));
// Temporary...
$this->assertEqual(false, $task->isArchived());
$this->assertEqual(
true,
($task->getExecutionException() instanceof Exception));
$this->assertEqual(1, $task->getFailureCount());
// Temporary...
$task = $this->expectNextLease($task);
$task = $task->executeTask();
$this->assertEqual(false, $task->isArchived());
$this->assertEqual(
true,
($task->getExecutionException() instanceof Exception));
$this->assertEqual(2, $task->getFailureCount());
// Temporary...
$task = $this->expectNextLease($task);
$task = $task->executeTask();
$this->assertEqual(false, $task->isArchived());
$this->assertEqual(
true,
($task->getExecutionException() instanceof Exception));
$this->assertEqual(3, $task->getFailureCount());
// Temporary...
$task = $this->expectNextLease($task);
$task = $task->executeTask();
$this->assertEqual(false, $task->isArchived());
$this->assertEqual(
true,
($task->getExecutionException() instanceof Exception));
$this->assertEqual(4, $task->getFailureCount());
// Permanent.
$task = $this->expectNextLease($task);
$task = $task->executeTask();
$this->assertEqual(true, $task->isArchived());
$this->assertEqual(
PhabricatorWorkerArchiveTask::RESULT_FAILURE,
$task->getResult());
}
public function testWaitBeforeRetry() {
$task = $this->scheduleTask(
array(
'doWork' => 'fail-temporary',
'getWaitBeforeRetry' => 1000000,
));
$this->expectNextLease($task)->executeTask();
$this->expectNextLease(null);
}
public function testRequiredLeaseTime() {
$task = $this->scheduleAndExecuteTask(
array(
'getRequiredLeaseTime' => 1000000,
));
$this->assertEqual(true, ($task->getLeaseExpires() - time()) > 1000);
}
private function expectNextLease($task) {
$leased = id(new PhabricatorWorkerLeaseQuery())
->setLimit(1)
->execute();
if ($task === null) {
$this->assertEqual(0, count($leased));
return null;
} else {
$this->assertEqual(1, count($leased));
$this->assertEqual(
(int)head($leased)->getID(),
(int)$task->getID());
return head($leased);
}
}
private function scheduleAndExecuteTask(array $data = array()) {
$task = $this->scheduleTask($data);
$task = $this->expectNextLease($task);
$task = $task->executeTask();
return $task;
}
private function scheduleTask(array $data = array()) {
return PhabricatorWorker::scheduleTask('PhabricatorTestWorker', $data);
}
}
diff --git a/src/infrastructure/daemon/workers/exception/PhabricatorWorkerPermanentFailureException.php b/src/infrastructure/daemon/workers/exception/PhabricatorWorkerPermanentFailureException.php
index ae457d982e..25493d2117 100644
--- a/src/infrastructure/daemon/workers/exception/PhabricatorWorkerPermanentFailureException.php
+++ b/src/infrastructure/daemon/workers/exception/PhabricatorWorkerPermanentFailureException.php
@@ -1,21 +1,5 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorWorkerPermanentFailureException extends Exception {
}
diff --git a/src/infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php b/src/infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php
index 52a57bf7df..8cbf4eaf8b 100644
--- a/src/infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php
+++ b/src/infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php
@@ -1,164 +1,148 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Select and lease tasks from the worker task queue.
*
* @group worker
*/
final class PhabricatorWorkerLeaseQuery extends PhabricatorQuery {
const PHASE_UNLEASED = 'unleased';
const PHASE_EXPIRED = 'expired';
const DEFAULT_LEASE_DURATION = 60; // Seconds
private $ids;
private $limit;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
public function execute() {
if (!$this->limit) {
throw new Exception("You must setLimit() when leasing tasks.");
}
$task_table = new PhabricatorWorkerActiveTask();
$taskdata_table = new PhabricatorWorkerTaskData();
$lease_ownership_name = $this->getLeaseOwnershipName();
$conn_w = $task_table->establishConnection('w');
// Try to satisfy the request from new, unleased tasks first. If we don't
// find enough tasks, try tasks with expired leases (i.e., tasks which have
// previously failed).
$phases = array(
self::PHASE_UNLEASED,
self::PHASE_EXPIRED,
);
$limit = $this->limit;
$leased = 0;
foreach ($phases as $phase) {
queryfx(
$conn_w,
'UPDATE %T task
SET leaseOwner = %s, leaseExpires = UNIX_TIMESTAMP() + %d
%Q %Q %Q',
$task_table->getTableName(),
$lease_ownership_name,
self::DEFAULT_LEASE_DURATION,
$this->buildWhereClause($conn_w, $phase),
$this->buildOrderClause($conn_w),
$this->buildLimitClause($conn_w, $limit - $leased));
$leased += $conn_w->getAffectedRows();
if ($leased == $limit) {
break;
}
}
if (!$leased) {
return array();
}
$data = queryfx_all(
$conn_w,
'SELECT task.*, taskdata.data _taskData, UNIX_TIMESTAMP() _serverTime
FROM %T task LEFT JOIN %T taskdata
ON taskdata.id = task.dataID
WHERE leaseOwner = %s AND leaseExpires > UNIX_TIMESTAMP()
%Q %Q',
$task_table->getTableName(),
$taskdata_table->getTableName(),
$lease_ownership_name,
$this->buildOrderClause($conn_w),
$this->buildLimitClause($conn_w, $limit));
$tasks = $task_table->loadAllFromArray($data);
$tasks = mpull($tasks, null, 'getID');
foreach ($data as $row) {
$tasks[$row['id']]->setServerTime($row['_serverTime']);
if ($row['_taskData']) {
$task_data = json_decode($row['_taskData'], true);
} else {
$task_data = null;
}
$tasks[$row['id']]->setData($task_data);
}
return $tasks;
}
private function buildWhereClause(AphrontDatabaseConnection $conn_w, $phase) {
$where = array();
switch ($phase) {
case self::PHASE_UNLEASED:
$where[] = 'leaseOwner IS NULL';
break;
case self::PHASE_EXPIRED:
$where[] = 'leaseExpires < UNIX_TIMESTAMP()';
break;
default:
throw new Exception("Unknown phase '{$phase}'!");
}
if ($this->ids) {
$where[] = qsprintf(
$conn_w,
'task.id IN (%Ld)',
$this->ids);
}
return $this->formatWhereClause($where);
}
private function buildOrderClause(AphrontDatabaseConnection $conn_w) {
return qsprintf($conn_w, 'ORDER BY id ASC');
}
private function buildLimitClause(AphrontDatabaseConnection $conn_w, $limit) {
return qsprintf($conn_w, 'LIMIT %d', $limit);
}
private function getLeaseOwnershipName() {
static $sequence = 0;
$parts = array(
getmypid(),
time(),
php_uname('n'),
++$sequence,
);
return implode(':', $parts);
}
}
diff --git a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php
index 51eabb73ca..0364091cb7 100644
--- a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php
+++ b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php
@@ -1,179 +1,163 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorWorkerActiveTask extends PhabricatorWorkerTask {
private $serverTime;
private $localTime;
public function getTableName() {
return 'worker_task';
}
public function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
) + parent::getConfiguration();
}
public function setServerTime($server_time) {
$this->serverTime = $server_time;
$this->localTime = time();
return $this;
}
public function setLeaseDuration($lease_duration) {
$this->checkLease();
$server_lease_expires = $this->serverTime + $lease_duration;
$this->setLeaseExpires($server_lease_expires);
// NOTE: This is primarily to allow unit tests to set negative lease
// durations so they don't have to wait around for leases to expire. We
// check that the lease is valid above.
return $this->forceSaveWithoutLease();
}
public function save() {
$this->checkLease();
return $this->forceSaveWithoutLease();
}
public function forceSaveWithoutLease() {
$is_new = !$this->getID();
if ($is_new) {
$this->failureCount = 0;
}
if ($is_new && ($this->getData() !== null)) {
$data = new PhabricatorWorkerTaskData();
$data->setData($this->getData());
$data->save();
$this->setDataID($data->getID());
}
return parent::save();
}
protected function checkLease() {
if ($this->leaseOwner) {
$current_server_time = $this->serverTime + (time() - $this->localTime);
if ($current_server_time >= $this->leaseExpires) {
throw new Exception("Trying to update task after lease expiration!");
}
}
}
public function delete() {
throw new Exception(
"Active tasks can not be deleted directly. ".
"Use archiveTask() to move tasks to the archive.");
}
public function archiveTask($result, $duration) {
if (!$this->getID()) {
throw new Exception(
"Attempting to archive a task which hasn't been save()d!");
}
$this->checkLease();
$archive = id(new PhabricatorWorkerArchiveTask())
->setID($this->getID())
->setTaskClass($this->getTaskClass())
->setLeaseOwner($this->getLeaseOwner())
->setLeaseExpires($this->getLeaseExpires())
->setFailureCount($this->getFailureCount())
->setDataID($this->getDataID())
->setResult($result)
->setDuration($duration);
// NOTE: This deletes the active task (this object)!
$archive->save();
return $archive;
}
public function executeTask() {
// We do this outside of the try .. catch because we don't have permission
// to release the lease otherwise.
$this->checkLease();
try {
$id = $this->getID();
$class = $this->getTaskClass();
if (!class_exists($class)) {
throw new PhabricatorWorkerPermanentFailureException(
"Task class '{$class}' does not exist!");
}
if (!is_subclass_of($class, 'PhabricatorWorker')) {
throw new PhabricatorWorkerPermanentFailureException(
"Task class '{$class}' does not extend PhabricatorWorker.");
}
$worker = newv($class, array($this->getData()));
$maximum_failures = $worker->getMaximumRetryCount();
if ($maximum_failures !== null) {
if ($this->getFailureCount() > $maximum_failures) {
throw new PhabricatorWorkerPermanentFailureException(
"Task {$id} has exceeded the maximum number of failures ".
"({$maximum_failures}).");
}
}
$lease = $worker->getRequiredLeaseTime();
if ($lease !== null) {
$this->setLeaseDuration($lease);
}
$t_start = microtime(true);
$worker->executeTask();
$t_end = microtime(true);
$duration = (int)(1000000 * ($t_end - $t_start));
$result = $this->archiveTask(
PhabricatorWorkerArchiveTask::RESULT_SUCCESS,
$duration);
} catch (PhabricatorWorkerPermanentFailureException $ex) {
$result = $this->archiveTask(
PhabricatorWorkerArchiveTask::RESULT_FAILURE,
0);
$result->setExecutionException($ex);
} catch (Exception $ex) {
$this->setExecutionException($ex);
$this->setFailureCount($this->getFailureCount() + 1);
$retry = $worker->getWaitBeforeRetry($this);
$retry = coalesce(
$retry,
PhabricatorWorkerLeaseQuery::DEFAULT_LEASE_DURATION);
// NOTE: As a side effect, this saves the object.
$this->setLeaseDuration($retry);
$result = $this;
}
return $result;
}
}
diff --git a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php
index 2ff3c98228..6bf24ed450 100644
--- a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php
+++ b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php
@@ -1,85 +1,69 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorWorkerArchiveTask extends PhabricatorWorkerTask {
const RESULT_SUCCESS = 0;
const RESULT_FAILURE = 1;
const RESULT_CANCELLED = 2;
protected $duration;
protected $result;
public function save() {
if (!$this->getID()) {
throw new Exception(
"Trying to archive a task with no ID.");
}
$other = new PhabricatorWorkerActiveTask();
$conn_w = $this->establishConnection('w');
$this->openTransaction();
queryfx(
$conn_w,
'DELETE FROM %T WHERE id = %d',
$other->getTableName(),
$this->getID());
$result = parent::insert();
$this->saveTransaction();
return $result;
}
public function delete() {
$this->openTransaction();
if ($this->getDataID()) {
$conn_w = $this->establishConnection('w');
$data_table = new PhabricatorWorkerTaskData();
queryfx(
$conn_w,
'DELETE FROM %T WHERE id = %d',
$data_table->getTableName(),
$this->getDataID());
}
$result = parent::delete();
$this->saveTransaction();
return $result;
}
public function unarchiveTask() {
$this->openTransaction();
$active = id(new PhabricatorWorkerActiveTask())
->setID($this->getID())
->setTaskClass($this->getTaskClass())
->setLeaseOwner(null)
->setLeaseExpires(0)
->setFailureCount(0)
->setDataID($this->getDataID())
->insert();
$this->setDataID(null);
$this->delete();
$this->saveTransaction();
return $active;
}
}
diff --git a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerDAO.php b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerDAO.php
index 2ccb199b0f..869f9d3d8a 100644
--- a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerDAO.php
+++ b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerDAO.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorWorkerDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'worker';
}
}
diff --git a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerTask.php b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerTask.php
index a5cd7609e0..c60d15e2c8 100644
--- a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerTask.php
+++ b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerTask.php
@@ -1,54 +1,38 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorWorkerTask extends PhabricatorWorkerDAO {
// NOTE: If you provide additional fields here, make sure they are handled
// correctly in the archiving process.
protected $taskClass;
protected $leaseOwner;
protected $leaseExpires;
protected $failureCount;
protected $dataID;
private $data;
private $executionException;
public function setExecutionException(Exception $execution_exception) {
$this->executionException = $execution_exception;
return $this;
}
public function getExecutionException() {
return $this->executionException;
}
public function setData($data) {
$this->data = $data;
return $this;
}
public function getData() {
return $this->data;
}
public function isArchived() {
return ($this instanceof PhabricatorWorkerArchiveTask);
}
}
diff --git a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerTaskData.php b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerTaskData.php
index 9e2672a42f..9968123afa 100644
--- a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerTaskData.php
+++ b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerTaskData.php
@@ -1,32 +1,16 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorWorkerTaskData extends PhabricatorWorkerDAO {
protected $data;
public function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_SERIALIZATION => array(
'data' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
}
diff --git a/src/infrastructure/diff/PhabricatorChangesetResponse.php b/src/infrastructure/diff/PhabricatorChangesetResponse.php
index 3eec135537..86f4f3dccf 100644
--- a/src/infrastructure/diff/PhabricatorChangesetResponse.php
+++ b/src/infrastructure/diff/PhabricatorChangesetResponse.php
@@ -1,50 +1,34 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorChangesetResponse extends AphrontProxyResponse {
private $renderedChangeset;
private $coverage;
public function setRenderedChangeset($rendered_changeset) {
$this->renderedChangeset = $rendered_changeset;
return $this;
}
public function setCoverage($coverage) {
$this->coverage = $coverage;
return $this;
}
protected function buildProxy() {
return new AphrontAjaxResponse();
}
public function buildResponseString() {
$content = array(
'changeset' => $this->renderedChangeset,
);
if ($this->coverage) {
$content['coverage'] = $this->coverage;
}
return $this->getProxy()->setContent($content)->buildResponseString();
}
}
diff --git a/src/infrastructure/diff/PhabricatorDifferenceEngine.php b/src/infrastructure/diff/PhabricatorDifferenceEngine.php
index 68894a3f69..f2de51225c 100644
--- a/src/infrastructure/diff/PhabricatorDifferenceEngine.php
+++ b/src/infrastructure/diff/PhabricatorDifferenceEngine.php
@@ -1,186 +1,170 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Utility class which encapsulates some shared behavior between different
* applications which render diffs.
*
* @task config Configuring the Engine
* @task diff Generating Diffs
*/
final class PhabricatorDifferenceEngine {
private $ignoreWhitespace;
private $oldName;
private $newName;
/* -( Configuring the Engine )--------------------------------------------- */
/**
* If true, ignore whitespace when computing differences.
*
* @param bool Ignore whitespace?
* @return this
* @task config
*/
public function setIgnoreWhitespace($ignore_whitespace) {
$this->ignoreWhitespace = $ignore_whitespace;
return $this;
}
/**
* Set the name to identify the old file with. Primarily cosmetic.
*
* @param string Old file name.
* @return this
* @task config
*/
public function setOldName($old_name) {
$this->oldName = $old_name;
return $this;
}
/**
* Set the name to identify the new file with. Primarily cosmetic.
*
* @param string New file name.
* @return this
* @task config
*/
public function setNewName($new_name) {
$this->newName = $new_name;
return $this;
}
/* -( Generating Diffs )--------------------------------------------------- */
/**
* Generate a raw diff from two raw files. This is a lower-level API than
* @{method:generateChangesetFromFileContent}, but may be useful if you need
* to use a custom parser configuration, as with Diffusion.
*
* @param string Entire previous file content.
* @param string Entire current file content.
* @return string Raw diff between the two files.
* @task diff
*/
public function generateRawDiffFromFileContent($old, $new) {
$options = array();
if ($this->ignoreWhitespace) {
$options[] = '-bw';
}
// Generate diffs with full context.
$options[] = '-U65535';
$old_name = nonempty($this->oldName, '/dev/universe').' 9999-99-99';
$new_name = nonempty($this->newName, '/dev/universe').' 9999-99-99';
$options[] = '-L';
$options[] = $old_name;
$options[] = '-L';
$options[] = $new_name;
$old_tmp = new TempFile();
$new_tmp = new TempFile();
Filesystem::writeFile($old_tmp, $old);
Filesystem::writeFile($new_tmp, $new);
list($err, $diff) = exec_manual(
'/usr/bin/diff %Ls %s %s',
$options,
$old_tmp,
$new_tmp);
if (!$err) {
// This indicates that the two files are the same (or, possibly, the
// same modulo whitespace differences, which is why we can't do this
// check trivially before running `diff`). Build a synthetic, changeless
// diff so that we can still render the raw, unchanged file instead of
// being forced to just say "this file didn't change" since we don't have
// the content.
$entire_file = explode("\n", $old);
foreach ($entire_file as $k => $line) {
$entire_file[$k] = ' '.$line;
}
$len = count($entire_file);
$entire_file = implode("\n", $entire_file);
// TODO: If both files were identical but missing newlines, we probably
// get this wrong. Unclear if it ever matters.
// This is a bit hacky but the diff parser can handle it.
$diff = "--- {$old_name}\n".
"+++ {$new_name}\n".
"@@ -1,{$len} +1,{$len} @@\n".
$entire_file."\n";
} else {
if ($this->ignoreWhitespace) {
// Under "-bw", `diff` is inconsistent about emitting "\ No newline
// at end of file". For instance, a long file with a change in the
// middle will emit a contextless "\ No newline..." at the end if a
// newline is removed, but not if one is added. A file with a change
// at the end will emit the "old" "\ No newline..." block only, even
// if the newline was not removed. Since we're ostensibly ignoring
// whitespace changes, just drop these lines if they appear anywhere
// in the diff.
$lines = explode("\n", $diff);
foreach ($lines as $key => $line) {
if (isset($line[0]) && $line[0] == '\\') {
unset($lines[$key]);
}
}
$diff = implode("\n", $lines);
}
}
return $diff;
}
/**
* Generate an @{class:DifferentialChangeset} from two raw files. This is
* principally useful because you can feed the output to
* @{class:DifferentialChangesetParser} in order to render it.
*
* @param string Entire previous file content.
* @param string Entire current file content.
* @return @{class:DifferentialChangeset} Synthetic changeset.
* @task diff
*/
public function generateChangesetFromFileContent($old, $new) {
$diff = $this->generateRawDiffFromFileContent($old, $new);
$changes = id(new ArcanistDiffParser())->parseDiff($diff);
$diff = DifferentialDiff::newFromRawChanges($changes);
return head($diff->getChangesets());
}
}
diff --git a/src/infrastructure/diff/PhabricatorInlineCommentController.php b/src/infrastructure/diff/PhabricatorInlineCommentController.php
index 6de6c23d0d..8b7b91f191 100644
--- a/src/infrastructure/diff/PhabricatorInlineCommentController.php
+++ b/src/infrastructure/diff/PhabricatorInlineCommentController.php
@@ -1,263 +1,247 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorInlineCommentController
extends PhabricatorController {
abstract protected function createComment();
abstract protected function loadComment($id);
abstract protected function loadCommentForEdit($id);
private $changesetID;
private $isNewFile;
private $isOnRight;
private $lineNumber;
private $lineLength;
private $commentText;
private $operation;
private $commentID;
public function getCommentID() {
return $this->commentID;
}
public function getOperation() {
return $this->operation;
}
public function getCommentText() {
return $this->commentText;
}
public function getLineLength() {
return $this->lineLength;
}
public function getLineNumber() {
return $this->lineNumber;
}
public function getIsOnRight() {
return $this->isOnRight;
}
public function getChangesetID() {
return $this->changesetID;
}
public function getIsNewFile() {
return $this->isNewFile;
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$this->readRequestParameters();
switch ($this->getOperation()) {
case 'delete':
$inline = $this->loadCommentForEdit($this->getCommentID());
if ($request->isFormPost()) {
$inline->delete();
return $this->buildEmptyResponse();
}
$dialog = new AphrontDialogView();
$dialog->setUser($user);
$dialog->setSubmitURI($request->getRequestURI());
$dialog->setTitle('Really delete this comment?');
$dialog->addHiddenInput('id', $this->getCommentID());
$dialog->addHiddenInput('op', 'delete');
$dialog->appendChild('<p>Delete this inline comment?</p>');
$dialog->addCancelButton('#');
$dialog->addSubmitButton('Delete');
return id(new AphrontDialogResponse())->setDialog($dialog);
case 'edit':
$inline = $this->loadCommentForEdit($this->getCommentID());
$text = $this->getCommentText();
if ($request->isFormPost()) {
if (strlen($text)) {
$inline->setContent($text);
$inline->save();
return $this->buildRenderedCommentResponse(
$inline,
$this->getIsOnRight());
} else {
$inline->delete();
return $this->buildEmptyResponse();
}
}
$edit_dialog = $this->buildEditDialog();
$edit_dialog->setTitle('Edit Inline Comment');
$edit_dialog->addHiddenInput('id', $this->getCommentID());
$edit_dialog->addHiddenInput('op', 'edit');
$edit_dialog->appendChild(
$this->renderTextArea(
nonempty($text, $inline->getContent())));
return id(new AphrontAjaxResponse())
->setContent($edit_dialog->render());
case 'create':
$text = $this->getCommentText();
if (!$request->isFormPost() || !strlen($text)) {
return $this->buildEmptyResponse();
}
$inline = $this->createComment()
->setChangesetID($this->getChangesetID())
->setAuthorPHID($user->getPHID())
->setLineNumber($this->getLineNumber())
->setLineLength($this->getLineLength())
->setIsNewFile($this->getIsNewFile())
->setContent($text)
->save();
return $this->buildRenderedCommentResponse(
$inline,
$this->getIsOnRight());
case 'reply':
default:
$edit_dialog = $this->buildEditDialog();
if ($this->getOperation() == 'reply') {
$inline = $this->loadComment($this->getCommentID());
$edit_dialog->setTitle('Reply to Inline Comment');
$changeset = $inline->getChangesetID();
$is_new = $inline->getIsNewFile();
$number = $inline->getLineNumber();
$length = $inline->getLineLength();
} else {
$edit_dialog->setTitle('New Inline Comment');
$changeset = $this->getChangesetID();
$is_new = $this->getIsNewFile();
$number = $this->getLineNumber();
$length = $this->getLineLength();
}
$edit_dialog->addHiddenInput('op', 'create');
$edit_dialog->addHiddenInput('changeset', $changeset);
$edit_dialog->addHiddenInput('is_new', $is_new);
$edit_dialog->addHiddenInput('number', $number);
$edit_dialog->addHiddenInput('length', $length);
$text_area = $this->renderTextArea($this->getCommentText());
$edit_dialog->appendChild($text_area);
return id(new AphrontAjaxResponse())
->setContent($edit_dialog->render());
}
}
private function readRequestParameters() {
$request = $this->getRequest();
// NOTE: This isn't necessarily a DifferentialChangeset ID, just an
// application identifier for the changeset. In Diffusion, it's a Path ID.
$this->changesetID = $request->getInt('changeset');
$this->isNewFile = $request->getBool('is_new');
$this->isOnRight = $request->getBool('on_right');
$this->lineNumber = $request->getInt('number');
$this->lineLength = $request->getInt('length');
$this->commentText = $request->getStr('text');
$this->commentID = $request->getInt('id');
$this->operation = $request->getStr('op');
}
private function buildEditDialog() {
$request = $this->getRequest();
$user = $request->getUser();
$edit_dialog = new DifferentialInlineCommentEditView();
$edit_dialog->setUser($user);
$edit_dialog->setSubmitURI($request->getRequestURI());
$edit_dialog->setOnRight($this->getIsOnRight());
$edit_dialog->setNumber($this->getLineNumber());
$edit_dialog->setLength($this->getLineLength());
return $edit_dialog;
}
private function buildEmptyResponse() {
return id(new AphrontAjaxResponse())
->setContent(
array(
'markup' => '',
));
}
private function buildRenderedCommentResponse(
PhabricatorInlineCommentInterface $inline,
$on_right) {
$request = $this->getRequest();
$user = $request->getUser();
$engine = new PhabricatorMarkupEngine();
$engine->setViewer($user);
$engine->addObject(
$inline,
PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY);
$engine->process();
$phids = array($user->getPHID());
$handles = $this->loadViewerHandles($phids);
$view = new DifferentialInlineCommentView();
$view->setInlineComment($inline);
$view->setOnRight($on_right);
$view->setBuildScaffolding(true);
$view->setMarkupEngine($engine);
$view->setHandles($handles);
$view->setEditable(true);
return id(new AphrontAjaxResponse())
->setContent(
array(
'inlineCommentID' => $inline->getID(),
'markup' => $view->render(),
));
}
private function renderTextArea($text) {
return javelin_render_tag(
'textarea',
array(
'class' => 'differential-inline-comment-edit-textarea',
'sigil' => 'differential-inline-comment-edit-textarea',
'name' => 'text',
),
phutil_escape_html($text));
}
}
diff --git a/src/infrastructure/diff/PhabricatorInlineCommentPreviewController.php b/src/infrastructure/diff/PhabricatorInlineCommentPreviewController.php
index 4299e81fb1..cd9a65be96 100644
--- a/src/infrastructure/diff/PhabricatorInlineCommentPreviewController.php
+++ b/src/infrastructure/diff/PhabricatorInlineCommentPreviewController.php
@@ -1,58 +1,42 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorInlineCommentPreviewController
extends PhabricatorController {
abstract protected function loadInlineComments();
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$inlines = $this->loadInlineComments();
assert_instances_of($inlines, 'PhabricatorInlineCommentInterface');
$engine = new PhabricatorMarkupEngine();
$engine->setViewer($user);
foreach ($inlines as $inline) {
$engine->addObject(
$inline,
PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY);
}
$engine->process();
$phids = array($user->getPHID());
$handles = $this->loadViewerHandles($phids);
$views = array();
foreach ($inlines as $inline) {
$view = new DifferentialInlineCommentView();
$view->setInlineComment($inline);
$view->setMarkupEngine($engine);
$view->setHandles($handles);
$view->setEditable(false);
$view->setPreview(true);
$views[] = $view->render();
}
$views = implode("\n", $views);
return id(new AphrontAjaxResponse())
->setContent($views);
}
}
diff --git a/src/infrastructure/diff/interface/PhabricatorInlineCommentInterface.php b/src/infrastructure/diff/interface/PhabricatorInlineCommentInterface.php
index b3fd41be56..55295fd2e3 100644
--- a/src/infrastructure/diff/interface/PhabricatorInlineCommentInterface.php
+++ b/src/infrastructure/diff/interface/PhabricatorInlineCommentInterface.php
@@ -1,56 +1,40 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Shared interface used by Differential and Diffusion inline comments.
*/
interface PhabricatorInlineCommentInterface extends PhabricatorMarkupInterface {
const MARKUP_FIELD_BODY = 'markup:body';
public function setChangesetID($id);
public function getChangesetID();
public function setIsNewFile($is_new);
public function getIsNewFile();
public function setLineNumber($number);
public function getLineNumber();
public function setLineLength($length);
public function getLineLength();
public function setContent($content);
public function getContent();
public function setCache($cache);
public function getCache();
public function setAuthorPHID($phid);
public function getAuthorPHID();
public function setSyntheticAuthor($synthetic_author);
public function getSyntheticAuthor();
public function isCompatible(PhabricatorInlineCommentInterface $inline);
public function isDraft();
public function save();
public function delete();
}
diff --git a/src/infrastructure/diff/view/PhabricatorInlineSummaryView.php b/src/infrastructure/diff/view/PhabricatorInlineSummaryView.php
index 58e45e4ea9..17ce6e596b 100644
--- a/src/infrastructure/diff/view/PhabricatorInlineSummaryView.php
+++ b/src/infrastructure/diff/view/PhabricatorInlineSummaryView.php
@@ -1,127 +1,111 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorInlineSummaryView extends AphrontView {
private $groups = array();
public function addCommentGroup($name, array $items) {
if (!isset($this->groups[$name])) {
$this->groups[$name] = $items;
} else {
$this->groups[$name] = array_merge($this->groups[$name], $items);
}
return $this;
}
public function render() {
require_celerity_resource('inline-comment-summary-css');
return $this->renderHeader().$this->renderTable();
}
private function renderHeader() {
return phutil_render_tag(
'div',
array(
'class' => 'phabricator-inline-summary',
),
'Inline Comments');
}
private function renderTable() {
$rows = array();
foreach ($this->groups as $group => $items) {
$has_where = false;
foreach ($items as $item) {
if (!empty($item['where'])) {
$has_where = true;
break;
}
}
$rows[] =
'<tr>'.
'<th colspan="3">'.
phutil_escape_html($group).
'</th>'.
'</tr>';
foreach ($items as $item) {
$items = isort($items, 'line');
$line = $item['line'];
$length = $item['length'];
if ($length) {
$lines = $line."\xE2\x80\x93".($line + $length);
} else {
$lines = $line;
}
if (isset($item['href'])) {
$href = $item['href'];
$target = '_blank';
$tail = " \xE2\x86\x97";
} else {
$href = '#inline-'.$item['id'];
$target = null;
$tail = null;
}
$lines = phutil_escape_html($lines);
if ($href) {
$lines = phutil_render_tag(
'a',
array(
'href' => $href,
'target' => $target,
'class' => 'num',
),
$lines.$tail);
}
$where = idx($item, 'where');
$colspan = ($has_where ? '' : ' colspan="2"');
$rows[] =
'<tr>'.
'<td class="inline-line-number">'.$lines.'</td>'.
($has_where ?
'<td class="inline-which-diff">'.
phutil_escape_html($where).
'</td>'
: null).
'<td class="inline-summary-content"'.$colspan.'>'.
'<div class="phabricator-remarkup">'.
$item['content'].
'</div>'.
'</td>'.
'</tr>';
}
}
return phutil_render_tag(
'table',
array(
'class' => 'phabricator-inline-summary-table',
),
implode("\n", $rows));
}
}
diff --git a/src/infrastructure/edges/__tests__/PhabricatorEdgeTestCase.php b/src/infrastructure/edges/__tests__/PhabricatorEdgeTestCase.php
index 0327b29337..221a493579 100644
--- a/src/infrastructure/edges/__tests__/PhabricatorEdgeTestCase.php
+++ b/src/infrastructure/edges/__tests__/PhabricatorEdgeTestCase.php
@@ -1,81 +1,65 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorEdgeTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testCycleDetection() {
// The editor should detect that this introduces a cycle and prevent the
// edit.
$user = new PhabricatorUser();
$obj1 = id(new HarbormasterObject())->save();
$obj2 = id(new HarbormasterObject())->save();
$phid1 = $obj1->getPHID();
$phid2 = $obj2->getPHID();
$editor = id(new PhabricatorEdgeEditor())
->setActor($user)
->addEdge($phid1, PhabricatorEdgeConfig::TYPE_TEST_NO_CYCLE, $phid2)
->addEdge($phid2, PhabricatorEdgeConfig::TYPE_TEST_NO_CYCLE, $phid1);
$caught = null;
try {
$editor->save();
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertEqual(
true,
$caught instanceof Exception);
// The first edit should go through (no cycle), bu the second one should
// fail (it introduces a cycle).
$editor = id(new PhabricatorEdgeEditor())
->setActor($user)
->addEdge($phid1, PhabricatorEdgeConfig::TYPE_TEST_NO_CYCLE, $phid2)
->save();
$editor = id(new PhabricatorEdgeEditor())
->setActor($user)
->addEdge($phid2, PhabricatorEdgeConfig::TYPE_TEST_NO_CYCLE, $phid1);
$caught = null;
try {
$editor->save();
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertEqual(
true,
$caught instanceof Exception);
}
}
diff --git a/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php b/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php
index 0f4a6db72e..1aff2cfdb7 100644
--- a/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php
+++ b/src/infrastructure/edges/constants/PhabricatorEdgeConfig.php
@@ -1,138 +1,122 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorEdgeConfig extends PhabricatorEdgeConstants {
const TABLE_NAME_EDGE = 'edge';
const TABLE_NAME_EDGEDATA = 'edgedata';
const TYPE_TASK_HAS_COMMIT = 1;
const TYPE_COMMIT_HAS_TASK = 2;
const TYPE_TASK_DEPENDS_ON_TASK = 3;
const TYPE_TASK_DEPENDED_ON_BY_TASK = 4;
const TYPE_DREV_DEPENDS_ON_DREV = 5;
const TYPE_DREV_DEPENDED_ON_BY_DREV = 6;
const TYPE_BLOG_HAS_POST = 7;
const TYPE_POST_HAS_BLOG = 8;
const TYPE_BLOG_HAS_BLOGGER = 9;
const TYPE_BLOGGER_HAS_BLOG = 10;
const TYPE_TASK_HAS_RELATED_DREV = 11;
const TYPE_DREV_HAS_RELATED_TASK = 12;
const TYPE_PROJ_MEMBER = 13;
const TYPE_MEMBER_OF_PROJ = 14;
const TYPE_COMMIT_HAS_PROJECT = 15;
const TYPE_PROJECT_HAS_COMMIT = 16;
const TYPE_QUESTION_HAS_VOTING_USER = 17;
const TYPE_VOTING_USER_HAS_QUESTION = 18;
const TYPE_ANSWER_HAS_VOTING_USER = 19;
const TYPE_VOTING_USER_HAS_ANSWER = 20;
const TYPE_OBJECT_HAS_SUBSCRIBER = 21;
const TYPE_SUBSCRIBED_TO_OBJECT = 22;
const TYPE_OBJECT_HAS_UNSUBSCRIBER = 23;
const TYPE_UNSUBSCRIBED_FROM_OBJECT = 24;
const TYPE_TEST_NO_CYCLE = 9000;
public static function getInverse($edge_type) {
static $map = array(
self::TYPE_TASK_HAS_COMMIT => self::TYPE_COMMIT_HAS_TASK,
self::TYPE_COMMIT_HAS_TASK => self::TYPE_TASK_HAS_COMMIT,
self::TYPE_TASK_DEPENDS_ON_TASK => self::TYPE_TASK_DEPENDED_ON_BY_TASK,
self::TYPE_TASK_DEPENDED_ON_BY_TASK => self::TYPE_TASK_DEPENDS_ON_TASK,
self::TYPE_DREV_DEPENDS_ON_DREV => self::TYPE_DREV_DEPENDED_ON_BY_DREV,
self::TYPE_DREV_DEPENDED_ON_BY_DREV => self::TYPE_DREV_DEPENDS_ON_DREV,
self::TYPE_BLOG_HAS_POST => self::TYPE_POST_HAS_BLOG,
self::TYPE_POST_HAS_BLOG => self::TYPE_BLOG_HAS_POST,
self::TYPE_BLOG_HAS_BLOGGER => self::TYPE_BLOGGER_HAS_BLOG,
self::TYPE_BLOGGER_HAS_BLOG => self::TYPE_BLOG_HAS_BLOGGER,
self::TYPE_TASK_HAS_RELATED_DREV => self::TYPE_DREV_HAS_RELATED_TASK,
self::TYPE_DREV_HAS_RELATED_TASK => self::TYPE_TASK_HAS_RELATED_DREV,
self::TYPE_PROJ_MEMBER => self::TYPE_MEMBER_OF_PROJ,
self::TYPE_MEMBER_OF_PROJ => self::TYPE_PROJ_MEMBER,
self::TYPE_COMMIT_HAS_PROJECT => self::TYPE_PROJECT_HAS_COMMIT,
self::TYPE_PROJECT_HAS_COMMIT => self::TYPE_COMMIT_HAS_PROJECT,
self::TYPE_QUESTION_HAS_VOTING_USER =>
self::TYPE_VOTING_USER_HAS_QUESTION,
self::TYPE_VOTING_USER_HAS_QUESTION =>
self::TYPE_QUESTION_HAS_VOTING_USER,
self::TYPE_ANSWER_HAS_VOTING_USER => self::TYPE_VOTING_USER_HAS_ANSWER,
self::TYPE_VOTING_USER_HAS_ANSWER => self::TYPE_ANSWER_HAS_VOTING_USER,
self::TYPE_OBJECT_HAS_SUBSCRIBER => self::TYPE_SUBSCRIBED_TO_OBJECT,
self::TYPE_SUBSCRIBED_TO_OBJECT => self::TYPE_OBJECT_HAS_SUBSCRIBER,
self::TYPE_OBJECT_HAS_UNSUBSCRIBER => self::TYPE_UNSUBSCRIBED_FROM_OBJECT,
self::TYPE_UNSUBSCRIBED_FROM_OBJECT => self::TYPE_OBJECT_HAS_UNSUBSCRIBER,
);
return idx($map, $edge_type);
}
public static function shouldPreventCycles($edge_type) {
static $map = array(
self::TYPE_TEST_NO_CYCLE => true,
self::TYPE_TASK_DEPENDS_ON_TASK => true,
self::TYPE_DREV_DEPENDS_ON_DREV => true,
);
return isset($map[$edge_type]);
}
public static function establishConnection($phid_type, $conn_type) {
static $class_map = array(
PhabricatorPHIDConstants::PHID_TYPE_TASK => 'ManiphestTask',
PhabricatorPHIDConstants::PHID_TYPE_CMIT => 'PhabricatorRepository',
PhabricatorPHIDConstants::PHID_TYPE_DREV => 'DifferentialRevision',
PhabricatorPHIDConstants::PHID_TYPE_FILE => 'PhabricatorFile',
PhabricatorPHIDConstants::PHID_TYPE_USER => 'PhabricatorUser',
PhabricatorPHIDConstants::PHID_TYPE_PROJ => 'PhabricatorProject',
PhabricatorPHIDConstants::PHID_TYPE_MLST =>
'PhabricatorMetaMTAMailingList',
PhabricatorPHIDConstants::PHID_TYPE_TOBJ => 'HarbormasterObject',
PhabricatorPHIDConstants::PHID_TYPE_BLOG => 'PhameBlog',
PhabricatorPHIDConstants::PHID_TYPE_POST => 'PhamePost',
PhabricatorPHIDConstants::PHID_TYPE_QUES => 'PonderQuestion',
PhabricatorPHIDConstants::PHID_TYPE_ANSW => 'PonderAnswer',
);
$class = idx($class_map, $phid_type);
if (!$class) {
throw new Exception(
"Edges are not available for objects of type '{$phid_type}'!");
}
return newv($class, array())->establishConnection($conn_type);
}
}
diff --git a/src/infrastructure/edges/constants/PhabricatorEdgeConstants.php b/src/infrastructure/edges/constants/PhabricatorEdgeConstants.php
index 045c7f46b8..a6a8c876c0 100644
--- a/src/infrastructure/edges/constants/PhabricatorEdgeConstants.php
+++ b/src/infrastructure/edges/constants/PhabricatorEdgeConstants.php
@@ -1,21 +1,5 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorEdgeConstants {
}
diff --git a/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php b/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php
index 0428b4091b..f869a2e019 100644
--- a/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php
+++ b/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php
@@ -1,454 +1,438 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Add and remove edges between objects. You can use
* @{class:PhabricatorEdgeQuery} to load object edges. For more information
* on edges, see @{article:Using Edges}.
*
* name=Adding Edges
* $src = $earth_phid;
* $type = PhabricatorEdgeConfig::TYPE_BODY_HAS_SATELLITE;
* $dst = $moon_phid;
*
* id(new PhabricatorEdgeEditor())
* ->addEdge($src, $type, $dst)
* ->setActor($user)
* ->save();
*
* @task edit Editing Edges
* @task cycles Cycle Prevention
* @task internal Internals
*/
final class PhabricatorEdgeEditor extends PhabricatorEditor {
private $addEdges = array();
private $remEdges = array();
private $openTransactions = array();
private $suppressEvents;
/* -( Editing Edges )------------------------------------------------------ */
/**
* Add a new edge (possibly also adding its inverse). Changes take effect when
* you call @{method:save}. If the edge already exists, it will not be
* overwritten. Removals queued with @{method:removeEdge} are executed before
* adds, so the effect of removing and adding the same edge is to overwrite
* any existing edge.
*
* The `$options` parameter accepts these values:
*
* - `data` Optional, data to write onto the edge.
* - `inverse_data` Optional, data to write on the inverse edge. If not
* provided, `data` will be written.
*
* @param phid Source object PHID.
* @param const Edge type constant.
* @param phid Destination object PHID.
* @param map Options map (see documentation).
* @return this
*
* @task edit
*/
public function addEdge($src, $type, $dst, array $options = array()) {
foreach ($this->buildEdgeSpecs($src, $type, $dst, $options) as $spec) {
$this->addEdges[] = $spec;
}
return $this;
}
/**
* Remove an edge (possibly also removing its inverse). Changes take effect
* when you call @{method:save}. If an edge does not exist, the removal
* will be ignored. Edges are added after edges are removed, so the effect of
* a remove plus an add is to overwrite.
*
* @param phid Source object PHID.
* @param const Edge type constant.
* @param phid Destination object PHID.
* @return this
*
* @task edit
*/
public function removeEdge($src, $type, $dst) {
foreach ($this->buildEdgeSpecs($src, $type, $dst) as $spec) {
$this->remEdges[] = $spec;
}
return $this;
}
/**
* Apply edge additions and removals queued by @{method:addEdge} and
* @{method:removeEdge}. Note that transactions are opened, all additions and
* removals are executed, and then transactions are saved. Thus, in some cases
* it may be slightly more efficient to perform multiple edit operations
* (e.g., adds followed by removals) if their outcomes are not dependent,
* since transactions will not be held open as long.
*
* @return this
* @task edit
*/
public function save() {
$cycle_types = $this->getPreventCyclesEdgeTypes();
$locks = array();
$caught = null;
try {
// NOTE: We write edge data first, before doing any transactions, since
// it's OK if we just leave it hanging out in space unattached to
// anything.
$this->writeEdgeData();
// If we're going to perform cycle detection, lock the edge type before
// doing edits.
if ($cycle_types) {
$src_phids = ipull($this->addEdges, 'src');
foreach ($cycle_types as $cycle_type) {
$key = 'edge.cycle:'.$cycle_type;
$locks[] = PhabricatorGlobalLock::newLock($key)->lock(15);
}
}
static $id = 0;
$id++;
$this->sendEvent($id, PhabricatorEventType::TYPE_EDGE_WILLEDITEDGES);
// NOTE: Removes first, then adds, so that "remove + add" is a useful
// operation meaning "overwrite".
$this->executeRemoves();
$this->executeAdds();
foreach ($cycle_types as $cycle_type) {
$this->detectCycles($src_phids, $cycle_type);
}
$this->sendEvent($id, PhabricatorEventType::TYPE_EDGE_DIDEDITEDGES);
$this->saveTransactions();
} catch (Exception $ex) {
$caught = $ex;
}
if ($caught) {
$this->killTransactions();
}
foreach ($locks as $lock) {
$lock->unlock();
}
if ($caught) {
throw $caught;
}
}
/* -( Internals )---------------------------------------------------------- */
/**
* Build the specification for an edge operation, and possibly build its
* inverse as well.
*
* @task internal
*/
private function buildEdgeSpecs($src, $type, $dst, array $options = array()) {
$data = array();
if (!empty($options['data'])) {
$data['data'] = $options['data'];
}
$src_type = phid_get_type($src);
$dst_type = phid_get_type($dst);
$specs = array();
$specs[] = array(
'src' => $src,
'src_type' => $src_type,
'dst' => $dst,
'dst_type' => $dst_type,
'type' => $type,
'data' => $data,
);
$inverse = PhabricatorEdgeConfig::getInverse($type);
if ($inverse) {
// If `inverse_data` is set, overwrite the edge data. Normally, just
// write the same data to the inverse edge.
if (array_key_exists('inverse_data', $options)) {
$data['data'] = $options['inverse_data'];
}
$specs[] = array(
'src' => $dst,
'src_type' => $dst_type,
'dst' => $src,
'dst_type' => $src_type,
'type' => $inverse,
'data' => $data,
);
}
return $specs;
}
/**
* Write edge data.
*
* @task internal
*/
private function writeEdgeData() {
$adds = $this->addEdges;
$writes = array();
foreach ($adds as $key => $edge) {
if ($edge['data']) {
$writes[] = array($key, $edge['src_type'], json_encode($edge['data']));
}
}
foreach ($writes as $write) {
list($key, $src_type, $data) = $write;
$conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w');
queryfx(
$conn_w,
'INSERT INTO %T (data) VALUES (%s)',
PhabricatorEdgeConfig::TABLE_NAME_EDGEDATA,
$data);
$this->addEdges[$key]['data_id'] = $conn_w->getInsertID();
}
}
/**
* Add queued edges.
*
* @task internal
*/
private function executeAdds() {
$adds = $this->addEdges;
$adds = igroup($adds, 'src_type');
// Assign stable sequence numbers to each edge, so we have a consistent
// ordering across edges by source and type.
foreach ($adds as $src_type => $edges) {
$edges_by_src = igroup($edges, 'src');
foreach ($edges_by_src as $src => $src_edges) {
$seq = 0;
foreach ($src_edges as $key => $edge) {
$src_edges[$key]['seq'] = $seq++;
$src_edges[$key]['dateCreated'] = time();
}
$edges_by_src[$src] = $src_edges;
}
$adds[$src_type] = array_mergev($edges_by_src);
}
$inserts = array();
foreach ($adds as $src_type => $edges) {
$conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w');
$sql = array();
foreach ($edges as $edge) {
$sql[] = qsprintf(
$conn_w,
'(%s, %d, %s, %d, %d, %nd)',
$edge['src'],
$edge['type'],
$edge['dst'],
$edge['dateCreated'],
$edge['seq'],
idx($edge, 'data_id'));
}
$inserts[] = array($conn_w, $sql);
}
foreach ($inserts as $insert) {
list($conn_w, $sql) = $insert;
$conn_w->openTransaction();
$this->openTransactions[] = $conn_w;
foreach (array_chunk($sql, 256) as $chunk) {
queryfx(
$conn_w,
'INSERT IGNORE INTO %T (src, type, dst, dateCreated, seq, dataID)
VALUES %Q',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
implode(', ', $chunk));
}
}
}
/**
* Remove queued edges.
*
* @task internal
*/
private function executeRemoves() {
$rems = $this->remEdges;
$rems = igroup($rems, 'src_type');
$deletes = array();
foreach ($rems as $src_type => $edges) {
$conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w');
$sql = array();
foreach ($edges as $edge) {
$sql[] = qsprintf(
$conn_w,
'(%s, %d, %s)',
$edge['src'],
$edge['type'],
$edge['dst']);
}
$deletes[] = array($conn_w, $sql);
}
foreach ($deletes as $delete) {
list($conn_w, $sql) = $delete;
$conn_w->openTransaction();
$this->openTransactions[] = $conn_w;
foreach (array_chunk($sql, 256) as $chunk) {
queryfx(
$conn_w,
'DELETE FROM %T WHERE (src, type, dst) IN (%Q)',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
implode(', ', $chunk));
}
}
}
/**
* Save open transactions.
*
* @task internal
*/
private function saveTransactions() {
foreach ($this->openTransactions as $key => $conn_w) {
$conn_w->saveTransaction();
unset($this->openTransactions[$key]);
}
}
private function killTransactions() {
foreach ($this->openTransactions as $key => $conn_w) {
$conn_w->killTransaction();
unset($this->openTransactions[$key]);
}
}
/**
* Suppress edge edit events. This prevents listeners from making updates in
* response to edits, and is primarily useful when performing migrations. You
* should not normally need to use it.
*
* @param bool True to supress events related to edits.
* @return this
* @task internal
*/
public function setSuppressEvents($suppress) {
$this->suppressEvents = $suppress;
return $this;
}
private function sendEvent($edit_id, $event_type) {
if ($this->suppressEvents) {
return;
}
$event = new PhabricatorEvent(
$event_type,
array(
'id' => $edit_id,
'add' => $this->addEdges,
'rem' => $this->remEdges,
));
$event->setUser($this->getActor());
PhutilEventEngine::dispatchEvent($event);
}
/* -( Cycle Prevention )--------------------------------------------------- */
/**
* Get a list of all edge types which are being added, and which we should
* prevent cycles on.
*
* @return list<const> List of edge types which should have cycles prevented.
* @task cycle
*/
private function getPreventCyclesEdgeTypes() {
$edge_types = array();
foreach ($this->addEdges as $edge) {
$edge_types[$edge['type']] = true;
}
foreach ($edge_types as $type => $ignored) {
if (!PhabricatorEdgeConfig::shouldPreventCycles($type)) {
unset($edge_types[$type]);
}
}
return array_keys($edge_types);
}
/**
* Detect graph cycles of a given edge type. If the edit introduces a cycle,
* a @{class:PhabricatorEdgeCycleException} is thrown with details.
*
* @return void
* @task cycle
*/
private function detectCycles(array $phids, $edge_type) {
// For simplicity, we just seed the graph with the affected nodes rather
// than seeding it with their edges. To do this, we just add synthetic
// edges from an imaginary '<seed>' node to the known edges.
$graph = id(new PhabricatorEdgeGraph())
->setEdgeType($edge_type)
->addNodes(
array(
'<seed>' => $phids,
))
->loadGraph();
foreach ($phids as $phid) {
$cycle = $graph->detectCycles($phid);
if ($cycle) {
throw new PhabricatorEdgeCycleException($edge_type, $cycle);
}
}
}
}
diff --git a/src/infrastructure/edges/exception/PhabricatorEdgeCycleException.php b/src/infrastructure/edges/exception/PhabricatorEdgeCycleException.php
index 98c7561748..7556405b0e 100644
--- a/src/infrastructure/edges/exception/PhabricatorEdgeCycleException.php
+++ b/src/infrastructure/edges/exception/PhabricatorEdgeCycleException.php
@@ -1,42 +1,26 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorEdgeCycleException extends Exception {
private $cycleEdgeType;
private $cycle;
public function __construct($cycle_edge_type, array $cycle) {
$this->cycleEdgeType = $cycle_edge_type;
$this->cycle = $cycle;
$cycle_list = implode(', ', $cycle);
parent::__construct(
"Graph cycle detected (type={$cycle_edge_type}, cycle={$cycle_list}).");
}
public function getCycle() {
return $this->cycle;
}
public function getCycleEdgeType() {
return $this->cycleEdgeType;
}
}
diff --git a/src/infrastructure/edges/query/PhabricatorEdgeQuery.php b/src/infrastructure/edges/query/PhabricatorEdgeQuery.php
index 1d68abee06..53f450a456 100644
--- a/src/infrastructure/edges/query/PhabricatorEdgeQuery.php
+++ b/src/infrastructure/edges/query/PhabricatorEdgeQuery.php
@@ -1,323 +1,307 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Load object edges created by @{class:PhabricatorEdgeEditor}.
*
* name=Querying Edges
* $src = $earth_phid;
* $type = PhabricatorEdgeConfig::TYPE_BODY_HAS_SATELLITE;
*
* // Load the earth's satellites.
* $satellite_edges = id(new PhabricatorEdgeQuery())
* ->withSourcePHIDs(array($src))
* ->withEdgeTypes(array($type))
* ->execute();
*
* For more information on edges, see @{article:Using Edges}.
*
* @task config Configuring the Query
* @task exec Executing the Query
* @task internal Internal
*/
final class PhabricatorEdgeQuery extends PhabricatorQuery {
private $sourcePHIDs;
private $destPHIDs;
private $edgeTypes;
private $resultSet;
private $needEdgeData;
/* -( Configuring the Query )---------------------------------------------- */
/**
* Find edges originating at one or more source PHIDs. You MUST provide this
* to execute an edge query.
*
* @param list List of source PHIDs.
* @return this
*
* @task config
*/
public function withSourcePHIDs(array $source_phids) {
$this->sourcePHIDs = $source_phids;
return $this;
}
/**
* Find edges terminating at one or more destination PHIDs.
*
* @param list List of destination PHIDs.
* @return this
*
*/
public function withDestinationPHIDs(array $dest_phids) {
$this->destPHIDs = $dest_phids;
return $this;
}
/**
* Find edges of specific types.
*
* @param list List of PhabricatorEdgeConfig type constants.
* @return this
*
* @task config
*/
public function withEdgeTypes(array $types) {
$this->edgeTypes = $types;
return $this;
}
/**
* When loading edges, also load edge data.
*
* @param bool True to load edge data.
* @return this
*
* @task config
*/
public function needEdgeData($need) {
$this->needEdgeData = $need;
return $this;
}
/* -( Executing the Query )------------------------------------------------ */
/**
* Convenience method for loading destination PHIDs with one source and one
* edge type. Equivalent to building a full query, but simplifies a common
* use case.
*
* @param phid Source PHID.
* @param const Edge type.
* @return list<phid> List of destination PHIDs.
*/
public static function loadDestinationPHIDs($src_phid, $edge_type) {
$edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($src_phid))
->withEdgeTypes(array($edge_type))
->execute();
return array_keys($edges[$src_phid][$edge_type]);
}
/**
* Convenience method for loading a single edge's metadata for
* a given source, destination, and edge type. Returns null
* if the edge does not exist or does not have metadata. Builds
* and immediately executes a full query.
*
* @param phid Source PHID.
* @param const Edge type.
* @param phid Destination PHID.
* @return wild Edge annotation (or null).
*/
public static function loadSingleEdgeData($src_phid, $edge_type, $dest_phid) {
$edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($src_phid))
->withEdgeTypes(array($edge_type))
->withDestinationPHIDs(array($dest_phid))
->needEdgeData(true)
->execute();
if (isset($edges[$src_phid][$edge_type][$dest_phid]['data'])) {
return $edges[$src_phid][$edge_type][$dest_phid]['data'];
}
return null;
}
/**
* Load specified edges.
*
* @task exec
*/
public function execute() {
if (!$this->sourcePHIDs) {
throw new Exception(
"You must use withSourcePHIDs() to query edges.");
}
$sources = phid_group_by_type($this->sourcePHIDs);
$result = array();
// When a query specifies types, make sure we return data for all queried
// types. This is mostly to make sure PhabricatorLiskDAO->attachEdges()
// gets some data, so that getEdges() doesn't throw later.
if ($this->edgeTypes) {
foreach ($this->sourcePHIDs as $phid) {
foreach ($this->edgeTypes as $type) {
$result[$phid][$type] = array();
}
}
}
foreach ($sources as $type => $phids) {
$conn_r = PhabricatorEdgeConfig::establishConnection($type, 'r');
$where = $this->buildWhereClause($conn_r);
$order = $this->buildOrderClause($conn_r);
$edges = queryfx_all(
$conn_r,
'SELECT edge.* FROM %T edge %Q %Q',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
$where,
$order);
if ($this->needEdgeData) {
$data_ids = array_filter(ipull($edges, 'dataID'));
$data_map = array();
if ($data_ids) {
$data_rows = queryfx_all(
$conn_r,
'SELECT edgedata.* FROM %T edgedata WHERE id IN (%Ld)',
PhabricatorEdgeConfig::TABLE_NAME_EDGEDATA,
$data_ids);
foreach ($data_rows as $row) {
$data_map[$row['id']] = idx(
json_decode($row['data'], true),
'data');
}
}
foreach ($edges as $key => $edge) {
$edges[$key]['data'] = idx($data_map, $edge['dataID']);
}
}
foreach ($edges as $edge) {
$result[$edge['src']][$edge['type']][$edge['dst']] = $edge;
}
}
$this->resultSet = $result;
return $result;
}
/**
* Convenience function for selecting edge destination PHIDs after calling
* execute().
*
* Returns a flat list of PHIDs matching the provided source PHID and type
* filters. By default, the filters are empty so all PHIDs will be returned.
* For example, if you're doing a batch query from several sources, you might
* write code like this:
*
* $query = new PhabricatorEdgeQuery();
* $query->withSourcePHIDs(mpull($objects, 'getPHID'));
* $query->withEdgeTypes(array($some_type));
* $query->execute();
*
* // Gets all of the destinations.
* $all_phids = $query->getDestinationPHIDs();
* $handles = id(new PhabricatorObjectHandleData($all_phids))
* ->loadHandles();
*
* foreach ($objects as $object) {
* // Get all of the destinations for the given object.
* $dst_phids = $query->getDestinationPHIDs(array($object->getPHID()));
* $object->attachHandles(array_select_keys($handles, $dst_phids));
* }
*
* @param list? List of PHIDs to select, or empty to select all.
* @param list? List of edge types to select, or empty to select all.
* @return list<phid> List of matching destination PHIDs.
*/
public function getDestinationPHIDs(
array $src_phids = array(),
array $types = array()) {
if ($this->resultSet === null) {
throw new Exception(
"You must execute() a query before you you can getDestinationPHIDs().");
}
$src_phids = array_fill_keys($src_phids, true);
$types = array_fill_keys($types, true);
$result_phids = array();
foreach ($this->resultSet as $src => $edges_by_type) {
if ($src_phids && empty($src_phids[$src])) {
continue;
}
foreach ($edges_by_type as $type => $edges_by_dst) {
if ($types && empty($types[$type])) {
continue;
}
foreach ($edges_by_dst as $dst => $edge) {
$result_phids[$dst] = true;
}
}
}
return array_keys($result_phids);
}
/* -( Internals )---------------------------------------------------------- */
/**
* @task internal
*/
private function buildWhereClause($conn_r) {
$where = array();
if ($this->sourcePHIDs) {
$where[] = qsprintf(
$conn_r,
'edge.src IN (%Ls)',
$this->sourcePHIDs);
}
if ($this->edgeTypes) {
$where[] = qsprintf(
$conn_r,
'edge.type IN (%Ls)',
$this->edgeTypes);
}
if ($this->destPHIDs) {
// potentially complain if $this->edgeType was not set
$where[] = qsprintf(
$conn_r,
'edge.dst IN (%Ls)',
$this->destPHIDs);
}
return $this->formatWhereClause($where);
}
/**
* @task internal
*/
private function buildOrderClause($conn_r) {
return 'ORDER BY edge.dateCreated DESC, edge.seq ASC';
}
}
diff --git a/src/infrastructure/edges/util/PhabricatorEdgeGraph.php b/src/infrastructure/edges/util/PhabricatorEdgeGraph.php
index 5cd3bfc5bb..94a11bb2d9 100644
--- a/src/infrastructure/edges/util/PhabricatorEdgeGraph.php
+++ b/src/infrastructure/edges/util/PhabricatorEdgeGraph.php
@@ -1,50 +1,34 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorEdgeGraph extends AbstractDirectedGraph {
private $edgeType;
public function setEdgeType($edge_type) {
$this->edgeType = $edge_type;
return $this;
}
protected function loadEdges(array $nodes) {
if (!$this->edgeType) {
throw new Exception("Set edge type before loading graph!");
}
$edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($nodes)
->withEdgeTypes(array($this->edgeType))
->execute();
$results = array_fill_keys($nodes, array());
foreach ($edges as $src => $types) {
foreach ($types as $type => $dsts) {
foreach ($dsts as $dst => $edge) {
$results[$src][] = $dst;
}
}
}
return $results;
}
}
diff --git a/src/infrastructure/events/PhabricatorEvent.php b/src/infrastructure/events/PhabricatorEvent.php
index bdb37faa20..d92d7b6894 100644
--- a/src/infrastructure/events/PhabricatorEvent.php
+++ b/src/infrastructure/events/PhabricatorEvent.php
@@ -1,65 +1,49 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group events
*/
final class PhabricatorEvent extends PhutilEvent {
private $user;
private $aphrontRequest;
private $conduitRequest;
public function __construct($type, array $data = array()) {
parent::__construct($type, $data);
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function getUser() {
return $this->user;
}
public function setAphrontRequest(AphrontRequest $aphront_request) {
$this->aphrontRequest = $aphront_request;
return $this;
}
public function getAphrontRequest() {
return $this->aphrontRequest;
}
public function setConduitRequest(ConduitAPIRequest $conduit_request) {
$this->conduitRequest = $conduit_request;
return $this;
}
public function getConduitRequest() {
return $this->conduitRequest;
}
}
diff --git a/src/infrastructure/events/PhabricatorEventEngine.php b/src/infrastructure/events/PhabricatorEventEngine.php
index 904b61757c..2f14a2d1e7 100644
--- a/src/infrastructure/events/PhabricatorEventEngine.php
+++ b/src/infrastructure/events/PhabricatorEventEngine.php
@@ -1,44 +1,28 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group events
*/
final class PhabricatorEventEngine {
public static function initialize() {
$listeners = PhabricatorEnv::getEnvConfig('events.listeners');
foreach ($listeners as $listener) {
id(new $listener())->register();
}
// Register the DarkConosole event logger.
id(new DarkConsoleEventPluginAPI())->register();
id(new ManiphestEdgeEventListener())->register();
$applications = PhabricatorApplication::getAllInstalledApplications();
foreach ($applications as $application) {
$listeners = $application->getEventListeners();
foreach ($listeners as $listener) {
$listener->register();
}
}
}
}
diff --git a/src/infrastructure/events/PhabricatorExampleEventListener.php b/src/infrastructure/events/PhabricatorExampleEventListener.php
index 4bc1e0a53d..7526490dc6 100644
--- a/src/infrastructure/events/PhabricatorExampleEventListener.php
+++ b/src/infrastructure/events/PhabricatorExampleEventListener.php
@@ -1,52 +1,36 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Example event listener. For details about installing Phabricator event hooks,
* refer to @{article:Events User Guide: Installing Event Listeners}.
*
* @group events
*/
final class PhabricatorExampleEventListener extends PhutilEventListener {
public function register() {
// When your listener is installed, its register() method will be called.
// You should listen() to any events you are interested in here.
$this->listen(PhabricatorEventType::TYPE_TEST_DIDRUNTEST);
}
public function handleEvent(PhutilEvent $event) {
// When an event you have called listen() for in your register() method
// occurs, this method will be invoked. You should respond to the event.
// In this case, we just echo a message out so the event test script will
// do something visible.
$console = PhutilConsole::getConsole();
$console->writeOut(
"PhabricatorExampleEventListener got test event at %d\n",
$event->getValue('time'));
}
}
diff --git a/src/infrastructure/events/constant/PhabricatorEventType.php b/src/infrastructure/events/constant/PhabricatorEventType.php
index c4f2051317..39772ea4e4 100644
--- a/src/infrastructure/events/constant/PhabricatorEventType.php
+++ b/src/infrastructure/events/constant/PhabricatorEventType.php
@@ -1,49 +1,33 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* For detailed explanations of these events, see
* @{article:Events User Guide: Installing Event Listeners}.
*
* @group events
*/
final class PhabricatorEventType extends PhutilEventType {
const TYPE_CONTROLLER_CHECKREQUEST = 'controller.checkRequest';
const TYPE_MANIPHEST_WILLEDITTASK = 'maniphest.willEditTask';
const TYPE_MANIPHEST_DIDEDITTASK = 'maniphest.didEditTask';
const TYPE_DIFFERENTIAL_WILLSENDMAIL = 'differential.willSendMail';
const TYPE_DIFFERENTIAL_WILLMARKGENERATED = 'differential.willMarkGenerated';
const TYPE_DIFFUSION_DIDDISCOVERCOMMIT = 'diffusion.didDiscoverCommit';
const TYPE_DIFFUSION_LOOKUPUSER = 'diffusion.lookupUser';
const TYPE_EDGE_WILLEDITEDGES = 'edge.willEditEdges';
const TYPE_EDGE_DIDEDITEDGES = 'edge.didEditEdges';
const TYPE_TEST_DIDRUNTEST = 'test.didRunTest';
const TYPE_UI_DIDRENDERACTIONS = 'ui.didRenderActions';
const TYPE_UI_WILLRENDEROBJECTS = 'ui.willRenderObjects';
const TYPE_UI_DDIDRENDEROBJECT = 'ui.didRenderObject';
const TYPE_UI_DIDRENDEROBJECTS = 'ui.didRenderObjects';
}
diff --git a/src/infrastructure/internationalization/PhabricatorBaseEnglishTranslation.php b/src/infrastructure/internationalization/PhabricatorBaseEnglishTranslation.php
index 46fafb479f..db785af0e9 100644
--- a/src/infrastructure/internationalization/PhabricatorBaseEnglishTranslation.php
+++ b/src/infrastructure/internationalization/PhabricatorBaseEnglishTranslation.php
@@ -1,172 +1,156 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorBaseEnglishTranslation
extends PhabricatorTranslation {
final public function getLanguage() {
return 'en';
}
public function getTranslations() {
return array(
'Differential Revision(s)' => array(
'Differential Revision',
'Differential Revisions',
),
'file(s)' => array('file', 'files'),
'Maniphest Task(s)' => array('Maniphest Task', 'Maniphest Tasks'),
'Please fix these errors and try again.' => array(
'Please fix this error and try again.',
'Please fix these errors and try again.',
),
'%d Error(s)' => array('%d Error', '%d Errors'),
'%d Warning(s)' => array('%d Warning', '%d Warnings'),
'%d Auto-Fix(es)' => array('%d Auto-Fix', '%d Auto-Fixes'),
'%d Advice(s)' => array('%d Advice', '%d Pieces of Advice'),
'%d Detail(s)' => array('%d Detail', '%d Details'),
'(%d line(s))' => array('(%d line)', '(%d lines)'),
'COMMIT(S)' => array('COMMIT', 'COMMITS'),
'%d line(s)' => array('%d line', '%d lines'),
'added %d commit(s): %s' => array(
'added commit: %2$s',
'added commits: %2$s',
),
'removed %d commit(s): %s' => array(
'removed commit: %2$s',
'removed commits: %2$s',
),
'changed %d commit(s), added %d: %s; removed %d: %s' =>
'changed commits, added: %3$s; removed: %5$s',
'ATTACHED %d COMMIT(S)' => array(
'ATTACHED COMMIT',
'ATTACHED COMMITS',
),
'added %d dependencie(s): %s' => array(
'added dependency: %2$s',
'added dependencies: %2$s',
),
'added %d dependent task(s): %s' => array(
'added dependent task: %2$s',
'added dependent tasks: %2$s',
),
'removed %d dependencie(s): %s' => array(
'removed dependency: %2$s',
'removed dependencies: %2$s',
),
'removed %d dependent task(s): %s' => array(
'removed dependent task: %2$s',
'removed dependent tasks: %2$s',
),
'changed %d dependencie(s), added %d: %s; removed %d: %s' =>
'changed dependencies, added: %3$s; removed: %5$s',
'changed %d dependent task(s), added %d: %s; removed %d: %s',
'changed dependent tasks, added: %3$s; removed: %5$s',
'DEPENDENT %d TASK(s)' => array(
'DEPENDENT TASK',
'DEPENDENT TASKS',
),
'DEPENDS ON %d TASK(S)' => array(
'DEPENDS ON TASK',
'DEPENDS ON TASKS',
),
'DIFFERENTIAL %d REVISION(S)' => array(
'DIFFERENTIAL REVISION',
'DIFFERENTIAL REVISIONS',
),
'added %d revision(s): %s' => array(
'added revision: %2$s',
'added revisions: %2$s',
),
'removed %d revision(s): %s' => array(
'removed revision: %2$s',
'removed revisions: %2$s',
),
'changed %d revision(s), added %d: %s; removed %d: %s' =>
'changed revisions, added %3$s; removed %5$s',
'There are %d raw fact(s) in storage.' => array(
'There is %d raw fact in storage.',
'There are %d raw facts in storage.',
),
'There are %d aggregate fact(s) in storage.' => array(
'There is %d aggregate fact in storage.',
'There are %d aggregate facts in storage.',
),
'%d Commit(s) Awaiting Audit' => array(
'%d Commit Awaiting Audit',
'%d Commits Awaiting Audit',
),
'%d Problem Commit(s)' => array(
'%d Problem Commit',
'%d Problem Commits',
),
'%d Review(s) Need Attention' => array(
'%d Review Needs Attention',
'%d Reviews Need Attention',
),
'%d Review(s) Waiting on Others' => array(
'%d Review Waiting on Others',
'%d Reviews Waiting on Others',
),
'%d Flagged Object(s)' => array(
'%d Flagged Object',
'%d Flagged Objects',
),
'%d Unbreak Now Task(s)!' => array(
'%d Unbreak Now Task!',
'%d Unbreak Now Tasks!',
),
'%d Assigned Task(s)' => array(
'%d Assigned Task',
'%d Assigned Tasks',
),
);
}
}
diff --git a/src/infrastructure/internationalization/PhabricatorEnglishTranslation.php b/src/infrastructure/internationalization/PhabricatorEnglishTranslation.php
index 0a423c5d13..125cba5668 100644
--- a/src/infrastructure/internationalization/PhabricatorEnglishTranslation.php
+++ b/src/infrastructure/internationalization/PhabricatorEnglishTranslation.php
@@ -1,32 +1,16 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorEnglishTranslation
extends PhabricatorBaseEnglishTranslation {
public function getName() {
return 'English';
}
public function getTranslations() {
return
PhabricatorEnv::getEnvConfig('translation.override') +
parent::getTranslations();
}
}
diff --git a/src/infrastructure/internationalization/PhabricatorTranslation.php b/src/infrastructure/internationalization/PhabricatorTranslation.php
index 61ff9857cb..1912781dae 100644
--- a/src/infrastructure/internationalization/PhabricatorTranslation.php
+++ b/src/infrastructure/internationalization/PhabricatorTranslation.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorTranslation {
abstract public function getLanguage();
abstract public function getName();
abstract public function getTranslations();
}
diff --git a/src/infrastructure/javelin/Javelin.php b/src/infrastructure/javelin/Javelin.php
index b6e2a3d9cf..04e90b0b5e 100644
--- a/src/infrastructure/javelin/Javelin.php
+++ b/src/infrastructure/javelin/Javelin.php
@@ -1,24 +1,8 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class Javelin {
public static function initBehavior($behavior, array $config = array()) {
$response = CelerityAPI::getStaticResourceResponse();
$response->initBehavior($behavior, $config);
}
}
diff --git a/src/infrastructure/javelin/markup.php b/src/infrastructure/javelin/markup.php
index 2a818ba86f..49569f2126 100644
--- a/src/infrastructure/javelin/markup.php
+++ b/src/infrastructure/javelin/markup.php
@@ -1,80 +1,64 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
function javelin_render_tag(
$tag,
array $attributes = array(),
$content = null) {
if (isset($attributes['sigil']) ||
isset($attributes['meta']) ||
isset($attributes['mustcapture'])) {
foreach ($attributes as $k => $v) {
switch ($k) {
case 'sigil':
$attributes['data-sigil'] = $v;
unset($attributes[$k]);
break;
case 'meta':
$response = CelerityAPI::getStaticResourceResponse();
$id = $response->addMetadata($v);
$attributes['data-meta'] = $id;
unset($attributes[$k]);
break;
case 'mustcapture':
if ($v) {
$attributes['data-mustcapture'] = '1';
} else {
unset($attributes['data-mustcapture']);
}
unset($attributes[$k]);
break;
}
}
}
return phutil_render_tag($tag, $attributes, $content);
}
function phabricator_render_form(PhabricatorUser $user, $attributes, $content) {
if (strcasecmp(idx($attributes, 'method'), 'POST') == 0 &&
!preg_match('#^(https?:|//)#', idx($attributes, 'action'))) {
$content = phabricator_render_form_magic($user).$content;
}
return javelin_render_tag('form', $attributes, $content);
}
function phabricator_render_form_magic(PhabricatorUser $user) {
return
phutil_render_tag(
'input',
array(
'type' => 'hidden',
'name' => AphrontRequest::getCSRFTokenName(),
'value' => $user->getCSRFToken(),
)).
phutil_render_tag(
'input',
array(
'type' => 'hidden',
'name' => '__form__',
'value' => true,
));
}
diff --git a/src/infrastructure/lint/PhabricatorLintEngine.php b/src/infrastructure/lint/PhabricatorLintEngine.php
index cbad441985..391d9fb5d1 100644
--- a/src/infrastructure/lint/PhabricatorLintEngine.php
+++ b/src/infrastructure/lint/PhabricatorLintEngine.php
@@ -1,50 +1,34 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorLintEngine extends PhutilLintEngine {
public function buildLinters() {
$linters = parent::buildLinters();
$paths = $this->getPaths();
foreach ($paths as $key => $path) {
if (!$this->pathExists($path)) {
unset($paths[$key]);
}
}
$javelin_linter = new PhabricatorJavelinLinter();
$linters[] = $javelin_linter;
foreach ($paths as $path) {
if (strpos($path, 'support/aphlict/') !== false) {
// This stuff is Node.js, not Javelin, so don't apply the Javelin
// linter.
continue;
}
if (preg_match('/\.js$/', $path)) {
$javelin_linter->addPath($path);
$javelin_linter->addData($path, $this->loadData($path));
}
}
return $linters;
}
}
diff --git a/src/infrastructure/lint/hook/PhabricatorSymbolNameLinter.php b/src/infrastructure/lint/hook/PhabricatorSymbolNameLinter.php
index bc342d6da8..f968d93d87 100644
--- a/src/infrastructure/lint/hook/PhabricatorSymbolNameLinter.php
+++ b/src/infrastructure/lint/hook/PhabricatorSymbolNameLinter.php
@@ -1,39 +1,23 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorSymbolNameLinter extends ArcanistXHPASTLintNamingHook {
public function lintSymbolName($type, $name, $default) {
$matches = null;
if ($type == 'class' &&
preg_match('/^ConduitAPI_(.*)_Method$/', $name, $matches)) {
if (preg_match('/^[a-z]+(_[a-z]+)?$/', $matches[1])) {
// These are permitted since Conduit does reflectioney stuff to figure
// out the method name from the class name.
return null;
} else {
return 'Conduit method implementations should contain lowercase '.
'letters only, with an underscore separating group and method '.
'names for implementations, e.g. '.
'"ConduitAPI_thing_info_Method".';
}
}
return $default;
}
}
diff --git a/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php b/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php
index 1a3e536875..3355d41791 100644
--- a/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php
+++ b/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php
@@ -1,258 +1,242 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorJavelinLinter extends ArcanistLinter {
private $symbols = array();
private $haveSymbolsBinary;
private $haveWarnedAboutBinary;
const LINT_PRIVATE_ACCESS = 1;
const LINT_MISSING_DEPENDENCY = 2;
const LINT_UNNECESSARY_DEPENDENCY = 3;
const LINT_UNKNOWN_DEPENDENCY = 4;
const LINT_MISSING_BINARY = 5;
public function willLintPaths(array $paths) {
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/scripts/__init_script__.php';
if ($this->haveSymbolsBinary === null) {
$binary = $this->getSymbolsBinaryPath();
$this->haveSymbolsBinary = Filesystem::pathExists($binary);
if (!$this->haveSymbolsBinary) {
return;
}
}
$futures = array();
foreach ($paths as $path) {
$future = $this->newSymbolsFuture($path);
$futures[$path] = $future;
}
foreach (Futures($futures)->limit(8) as $path => $future) {
$this->symbols[$path] = $future->resolvex();
}
}
public function getLinterName() {
return 'JAVELIN';
}
public function getLintSeverityMap() {
return array(
self::LINT_MISSING_BINARY => ArcanistLintSeverity::SEVERITY_WARNING,
);
}
public function getLintNameMap() {
return array(
self::LINT_PRIVATE_ACCESS => 'Private Method/Member Access',
self::LINT_MISSING_DEPENDENCY => 'Missing Javelin Dependency',
self::LINT_UNNECESSARY_DEPENDENCY => 'Unnecessary Javelin Dependency',
self::LINT_UNKNOWN_DEPENDENCY => 'Unknown Javelin Dependency',
self::LINT_MISSING_BINARY => '`javelinsymbols` Binary Not Built',
);
}
public function lintPath($path) {
if (!$this->haveSymbolsBinary) {
if (!$this->haveWarnedAboutBinary) {
$this->haveWarnedAboutBinary = true;
// TODO: Write build documentation for the Javelin binaries and point
// the user at it.
$this->raiseLintAtLine(
1,
0,
self::LINT_MISSING_BINARY,
"The 'javelinsymbols' binary in the Javelin project has not been ".
"built, so the Javelin linter can't run. This isn't a big concern, ".
"but means some Javelin problems can't be automatically detected.");
}
return;
}
list($uses, $installs) = $this->getUsedAndInstalledSymbolsForPath($path);
foreach ($uses as $symbol => $line) {
$parts = explode('.', $symbol);
foreach ($parts as $part) {
if ($part[0] == '_' && $part[1] != '_') {
$base = implode('.', array_slice($parts, 0, 2));
if (!array_key_exists($base, $installs)) {
$this->raiseLintAtLine(
$line,
0,
self::LINT_PRIVATE_ACCESS,
"This file accesses private symbol '{$symbol}' across file ".
"boundaries. You may only access private members and methods ".
"from the file where they are defined.");
}
break;
}
}
}
if ($this->getEngine()->getCommitHookMode()) {
// Don't do the dependency checks in commit-hook mode because we won't
// have an available working copy.
return;
}
$external_classes = array();
foreach ($uses as $symbol => $line) {
$parts = explode('.', $symbol);
$class = implode('.', array_slice($parts, 0, 2));
if (!array_key_exists($class, $external_classes) &&
!array_key_exists($class, $installs)) {
$external_classes[$class] = $line;
}
}
$celerity = CelerityResourceMap::getInstance();
$path = preg_replace(
'@^externals/javelin/src/@',
'webroot/rsrc/js/javelin/',
$path);
$need = $external_classes;
$info = $celerity->lookupFileInformation(substr($path, strlen('webroot')));
if (!$info) {
$info = array();
}
$requires = idx($info, 'requires', array());
foreach ($requires as $key => $name) {
$symbol_info = $celerity->lookupSymbolInformation($name);
if (!$symbol_info) {
$this->raiseLintAtLine(
0,
0,
self::LINT_UNKNOWN_DEPENDENCY,
"This file @requires component '{$name}', but it does not ".
"exist. You may need to rebuild the Celerity map.");
unset($requires[$key]);
continue;
}
$symbol_path = 'webroot'.$symbol_info['disk'];
list($ignored, $req_install) = $this->getUsedAndInstalledSymbolsForPath(
$symbol_path);
if (array_intersect_key($req_install, $external_classes)) {
$need = array_diff_key($need, $req_install);
unset($requires[$key]);
}
}
foreach ($need as $class => $line) {
$this->raiseLintAtLine(
$line,
0,
self::LINT_MISSING_DEPENDENCY,
"This file uses '{$class}' but does not @requires the component ".
"which installs it. You may need to rebuild the Celerity map.");
}
foreach ($requires as $component) {
$this->raiseLintAtLine(
0,
0,
self::LINT_UNNECESSARY_DEPENDENCY,
"This file @requires component '{$component}' but does not use ".
"anything it provides.");
}
}
private function loadSymbols($path) {
if (empty($this->symbols[$path])) {
$this->symbols[$path] = $this->newSymbolsFuture($path)->resolvex();
}
return $this->symbols[$path];
}
private function newSymbolsFuture($path) {
$javelinsymbols = $this->getSymbolsBinaryPath();
$future = new ExecFuture($javelinsymbols.' # '.escapeshellarg($path));
$future->write($this->getData($path));
return $future;
}
private function getSymbolsBinaryPath() {
$root = dirname(phutil_get_library_root('phabricator'));
$support = $root.'/externals/javelin/support';
return $support.'/javelinsymbols/javelinsymbols';
}
private function getUsedAndInstalledSymbolsForPath($path) {
list($symbols) = $this->loadSymbols($path);
$symbols = trim($symbols);
$uses = array();
$installs = array();
if (empty($symbols)) {
// This file has no symbols.
return array($uses, $installs);
}
$symbols = explode("\n", trim($symbols));
foreach ($symbols as $line) {
$matches = null;
if (!preg_match('/^([?+\*])([^:]*):(\d+)$/', $line, $matches)) {
throw new Exception(
"Received malformed output from `javelinsymbols`.");
}
$type = $matches[1];
$symbol = $matches[2];
$line = $matches[3];
switch ($type) {
case '?':
$uses[$symbol] = $line;
break;
case '+':
$installs['JX.'.$symbol] = $line;
break;
}
}
$contents = $this->getData($path);
$matches = null;
$count = preg_match_all(
'/@javelin-installs\W+(\S+)/',
$contents,
$matches,
PREG_PATTERN_ORDER);
if ($count) {
foreach ($matches[1] as $symbol) {
$installs[$symbol] = 0;
}
}
return array($uses, $installs);
}
}
diff --git a/src/infrastructure/markup/PhabricatorMarkupEngine.php b/src/infrastructure/markup/PhabricatorMarkupEngine.php
index 4b0e03a043..f8bf913409 100644
--- a/src/infrastructure/markup/PhabricatorMarkupEngine.php
+++ b/src/infrastructure/markup/PhabricatorMarkupEngine.php
@@ -1,550 +1,534 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Manages markup engine selection, configuration, application, caching and
* pipelining.
*
* @{class:PhabricatorMarkupEngine} can be used to render objects which
* implement @{interface:PhabricatorMarkupInterface} in a batched, cache-aware
* way. For example, if you have a list of comments written in remarkup (and
* the objects implement the correct interface) you can render them by first
* building an engine and adding the fields with @{method:addObject}.
*
* $field = 'field:body'; // Field you want to render. Each object exposes
* // one or more fields of markup.
*
* $engine = new PhabricatorMarkupEngine();
* foreach ($comments as $comment) {
* $engine->addObject($comment, $field);
* }
*
* Now, call @{method:process} to perform the actual cache/rendering
* step. This is a heavyweight call which does batched data access and
* transforms the markup into output.
*
* $engine->process();
*
* Finally, do something with the results:
*
* $results = array();
* foreach ($comments as $comment) {
* $results[] = $engine->getOutput($comment, $field);
* }
*
* If you have a single object to render, you can use the convenience method
* @{method:renderOneObject}.
*
* @task markup Markup Pipeline
* @task engine Engine Construction
*/
final class PhabricatorMarkupEngine {
private $objects = array();
private $viewer;
private $version = 0;
/* -( Markup Pipeline )---------------------------------------------------- */
/**
* Convenience method for pushing a single object through the markup
* pipeline.
*
* @param PhabricatorMarkupInterface The object to render.
* @param string The field to render.
* @param PhabricatorUser User viewing the markup.
* @return string Marked up output.
* @task markup
*/
public static function renderOneObject(
PhabricatorMarkupInterface $object,
$field,
PhabricatorUser $viewer) {
return id(new PhabricatorMarkupEngine())
->setViewer($viewer)
->addObject($object, $field)
->process()
->getOutput($object, $field);
}
/**
* Queue an object for markup generation when @{method:process} is
* called. You can retrieve the output later with @{method:getOutput}.
*
* @param PhabricatorMarkupInterface The object to render.
* @param string The field to render.
* @return this
* @task markup
*/
public function addObject(PhabricatorMarkupInterface $object, $field) {
$key = $this->getMarkupFieldKey($object, $field);
$this->objects[$key] = array(
'object' => $object,
'field' => $field,
);
return $this;
}
/**
* Process objects queued with @{method:addObject}. You can then retrieve
* the output with @{method:getOutput}.
*
* @return this
* @task markup
*/
public function process() {
$keys = array();
foreach ($this->objects as $key => $info) {
if (!isset($info['markup'])) {
$keys[] = $key;
}
}
if (!$keys) {
return;
}
$objects = array_select_keys($this->objects, $keys);
// Build all the markup engines. We need an engine for each field whether
// we have a cache or not, since we still need to postprocess the cache.
$engines = array();
foreach ($objects as $key => $info) {
$engines[$key] = $info['object']->newMarkupEngine($info['field']);
$engines[$key]->setConfig('viewer', $this->viewer);
}
// Load or build the preprocessor caches.
$blocks = $this->loadPreprocessorCaches($engines, $objects);
// Finalize the output.
foreach ($objects as $key => $info) {
$data = $blocks[$key]->getCacheData();
$engine = $engines[$key];
$field = $info['field'];
$object = $info['object'];
$output = $engine->postprocessText($data);
$output = $object->didMarkupText($field, $output, $engine);
$this->objects[$key]['output'] = $output;
}
return $this;
}
/**
* Get the output of markup processing for a field queued with
* @{method:addObject}. Before you can call this method, you must call
* @{method:process}.
*
* @param PhabricatorMarkupInterface The object to retrieve.
* @param string The field to retrieve.
* @return string Processed output.
* @task markup
*/
public function getOutput(PhabricatorMarkupInterface $object, $field) {
$key = $this->getMarkupFieldKey($object, $field);
if (empty($this->objects[$key])) {
throw new Exception(
"Call addObject() before getOutput() (key = '{$key}').");
}
if (!isset($this->objects[$key]['output'])) {
throw new Exception(
"Call process() before getOutput().");
}
return $this->objects[$key]['output'];
}
/**
* @task markup
*/
private function getMarkupFieldKey(
PhabricatorMarkupInterface $object,
$field) {
return $object->getMarkupFieldKey($field).'@'.$this->version;
}
/**
* @task markup
*/
private function loadPreprocessorCaches(array $engines, array $objects) {
$blocks = array();
$use_cache = array();
foreach ($objects as $key => $info) {
if ($info['object']->shouldUseMarkupCache($info['field'])) {
$use_cache[$key] = true;
}
}
if ($use_cache) {
$blocks = id(new PhabricatorMarkupCache())->loadAllWhere(
'cacheKey IN (%Ls)',
array_keys($use_cache));
$blocks = mpull($blocks, null, 'getCacheKey');
}
foreach ($objects as $key => $info) {
if (isset($blocks[$key])) {
// If we already have a preprocessing cache, we don't need to rebuild
// it.
continue;
}
$text = $info['object']->getMarkupText($info['field']);
$data = $engines[$key]->preprocessText($text);
// NOTE: This is just debugging information to help sort out cache issues.
// If one machine is misconfigured and poisoning caches you can use this
// field to hunt it down.
$metadata = array(
'host' => php_uname('n'),
);
$blocks[$key] = id(new PhabricatorMarkupCache())
->setCacheKey($key)
->setCacheData($data)
->setMetadata($metadata);
if (isset($use_cache[$key])) {
// This is just filling a cache and always safe, even on a read pathway.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
try {
$blocks[$key]->save();
} catch (AphrontQueryDuplicateKeyException $ex) {
// Ignore this, we just raced to write the cache.
}
unset($unguarded);
}
}
return $blocks;
}
/**
* Set the viewing user. Used to implement object permissions.
*
* @param PhabricatorUser The viewing user.
* @return this
* @task markup
*/
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
/* -( Engine Construction )------------------------------------------------ */
/**
* @task engine
*/
public static function newManiphestMarkupEngine() {
return self::newMarkupEngine(array(
));
}
/**
* @task engine
*/
public static function newPhrictionMarkupEngine() {
return self::newMarkupEngine(array(
'header.generate-toc' => true,
));
}
/**
* @task engine
*/
public static function newPhameMarkupEngine() {
return self::newMarkupEngine(array(
'macros' => false,
));
}
/**
* @task engine
*/
public static function newFeedMarkupEngine() {
return self::newMarkupEngine(
array(
'macros' => false,
'fileproxy' => false,
'youtube' => false,
));
}
/**
* @task engine
*/
public static function newDifferentialMarkupEngine(array $options = array()) {
return self::newMarkupEngine(array(
'custom-inline' => PhabricatorEnv::getEnvConfig(
'differential.custom-remarkup-rules'),
'custom-block' => PhabricatorEnv::getEnvConfig(
'differential.custom-remarkup-block-rules'),
'differential.diff' => idx($options, 'differential.diff'),
));
}
/**
* @task engine
*/
public static function newDiffusionMarkupEngine(array $options = array()) {
return self::newMarkupEngine(array(
));
}
/**
* @task engine
*/
public static function newProfileMarkupEngine() {
return self::newMarkupEngine(array(
));
}
/**
* @task engine
*/
public static function newSlowvoteMarkupEngine() {
return self::newMarkupEngine(array(
));
}
public static function newPonderMarkupEngine(array $options = array()) {
return self::newMarkupEngine($options);
}
/**
* @task engine
*/
private static function getMarkupEngineDefaultConfiguration() {
return array(
'pygments' => PhabricatorEnv::getEnvConfig('pygments.enabled'),
'fileproxy' => PhabricatorEnv::getEnvConfig('files.enable-proxy'),
'youtube' => PhabricatorEnv::getEnvConfig(
'remarkup.enable-embedded-youtube'),
'custom-inline' => array(),
'custom-block' => array(),
'differential.diff' => null,
'header.generate-toc' => false,
'macros' => true,
'uri.allowed-protocols' => PhabricatorEnv::getEnvConfig(
'uri.allowed-protocols'),
'syntax-highlighter.engine' => PhabricatorEnv::getEnvConfig(
'syntax-highlighter.engine'),
);
}
/**
* @task engine
*/
private static function newMarkupEngine(array $options) {
$options += self::getMarkupEngineDefaultConfiguration();
$engine = new PhutilRemarkupEngine();
$engine->setConfig('preserve-linebreaks', true);
$engine->setConfig('pygments.enabled', $options['pygments']);
$engine->setConfig(
'uri.allowed-protocols',
$options['uri.allowed-protocols']);
$engine->setConfig('differential.diff', $options['differential.diff']);
$engine->setConfig('header.generate-toc', $options['header.generate-toc']);
$engine->setConfig(
'syntax-highlighter.engine',
$options['syntax-highlighter.engine']);
$rules = array();
$rules[] = new PhutilRemarkupRuleEscapeRemarkup();
$rules[] = new PhutilRemarkupRuleMonospace();
$custom_rule_classes = $options['custom-inline'];
if ($custom_rule_classes) {
foreach ($custom_rule_classes as $custom_rule_class) {
$rules[] = newv($custom_rule_class, array());
}
}
$rules[] = new PhutilRemarkupRuleDocumentLink();
if ($options['fileproxy']) {
$rules[] = new PhabricatorRemarkupRuleProxyImage();
}
if ($options['youtube']) {
$rules[] = new PhabricatorRemarkupRuleYoutube();
}
$rules[] = new PhutilRemarkupRuleHyperlink();
$rules[] = new PhabricatorRemarkupRulePhriction();
$rules[] = new PhabricatorRemarkupRuleDifferentialHandle();
$rules[] = new PhabricatorRemarkupRuleManiphestHandle();
$rules[] = new PhabricatorRemarkupRuleEmbedFile();
$rules[] = new PhabricatorRemarkupRuleDifferential();
$rules[] = new PhabricatorRemarkupRuleDiffusion();
$rules[] = new PhabricatorRemarkupRuleManiphest();
$rules[] = new PhabricatorRemarkupRulePaste();
$rules[] = new PhabricatorRemarkupRuleCountdown();
$rules[] = new PonderRuleQuestion();
if ($options['macros']) {
$rules[] = new PhabricatorRemarkupRuleImageMacro();
}
$rules[] = new PhabricatorRemarkupRuleMention();
$rules[] = new PhutilRemarkupRuleEscapeHTML();
$rules[] = new PhutilRemarkupRuleBold();
$rules[] = new PhutilRemarkupRuleItalic();
$rules[] = new PhutilRemarkupRuleDel();
$blocks = array();
$blocks[] = new PhutilRemarkupEngineRemarkupQuotesBlockRule();
$blocks[] = new PhutilRemarkupEngineRemarkupLiteralBlockRule();
$blocks[] = new PhutilRemarkupEngineRemarkupHeaderBlockRule();
$blocks[] = new PhutilRemarkupEngineRemarkupListBlockRule();
$blocks[] = new PhutilRemarkupEngineRemarkupCodeBlockRule();
$blocks[] = new PhutilRemarkupEngineRemarkupNoteBlockRule();
$blocks[] = new PhutilRemarkupEngineRemarkupTableBlockRule();
$blocks[] = new PhutilRemarkupEngineRemarkupSimpleTableBlockRule();
$blocks[] = new PhutilRemarkupEngineRemarkupDefaultBlockRule();
$custom_block_rule_classes = $options['custom-block'];
if ($custom_block_rule_classes) {
foreach ($custom_block_rule_classes as $custom_block_rule_class) {
$blocks[] = newv($custom_block_rule_class, array());
}
}
foreach ($blocks as $block) {
if ($block instanceof PhutilRemarkupEngineRemarkupLiteralBlockRule) {
$literal_rules = array();
$literal_rules[] = new PhutilRemarkupRuleEscapeHTML();
$literal_rules[] = new PhutilRemarkupRuleLinebreaks();
$block->setMarkupRules($literal_rules);
} else if (
!($block instanceof PhutilRemarkupEngineRemarkupCodeBlockRule)) {
$block->setMarkupRules($rules);
}
}
$engine->setBlockRules($blocks);
return $engine;
}
public static function extractPHIDsFromMentions(array $content_blocks) {
$mentions = array();
$engine = self::newDifferentialMarkupEngine();
foreach ($content_blocks as $content_block) {
$engine->markupText($content_block);
$phids = $engine->getTextMetadata(
PhabricatorRemarkupRuleMention::KEY_MENTIONED,
array());
$mentions += $phids;
}
return $mentions;
}
/**
* Produce a corpus summary, in a way that shortens the underlying text
* without truncating it somewhere awkward.
*
* TODO: We could do a better job of this.
*
* @param string Remarkup corpus to summarize.
* @return string Summarized corpus.
*/
public static function summarize($corpus) {
// Major goals here are:
// - Don't split in the middle of a character (utf-8).
// - Don't split in the middle of, e.g., **bold** text, since
// we end up with hanging '**' in the summary.
// - Try not to pick an image macro, header, embedded file, etc.
// - Hopefully don't return too much text. We don't explicitly limit
// this right now.
$blocks = preg_split("/\n *\n\s*/", trim($corpus));
$best = null;
foreach ($blocks as $block) {
// This is a test for normal spaces in the block, i.e. a heuristic to
// distinguish standard paragraphs from things like image macros. It may
// not work well for non-latin text. We prefer to summarize with a
// paragraph of normal words over an image macro, if possible.
$has_space = preg_match('/\w\s\w/', $block);
// This is a test to find embedded images and headers. We prefer to
// summarize with a normal paragraph over a header or an embedded object,
// if possible.
$has_embed = preg_match('/^[{=]/', $block);
if ($has_space && !$has_embed) {
// This seems like a good summary, so return it.
return $block;
}
if (!$best) {
// This is the first block we found; if everything is garbage just
// use the first block.
$best = $block;
}
}
return $best;
}
}
diff --git a/src/infrastructure/markup/PhabricatorMarkupInterface.php b/src/infrastructure/markup/PhabricatorMarkupInterface.php
index 06e191aae9..437d3da8f8 100644
--- a/src/infrastructure/markup/PhabricatorMarkupInterface.php
+++ b/src/infrastructure/markup/PhabricatorMarkupInterface.php
@@ -1,103 +1,87 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* An object which has one or more fields containing markup that can be
* rendered into a display format. Commonly, the fields contain Remarkup and
* are rendered into HTML. Implementing this interface allows you to render
* objects through @{class:PhabricatorMarkupEngine} and benefit from caching
* and pipelining infrastructure.
*
* An object may have several "fields" of markup. For example, Differential
* revisions have a "summary" and a "test plan". In these cases, the `$field`
* parameter is used to identify which field is being operated on. For simple
* objects like comments, you might only have one field (say, "body"). In
* these cases, the implementation can largely ignore the `$field` parameter.
*
* @task markup Markup Interface
*
* @group markup
*/
interface PhabricatorMarkupInterface {
/* -( Markup Interface )--------------------------------------------------- */
/**
* Get a key to identify this field. This should uniquely identify the block
* of text to be rendered and be usable as a cache key. If the object has a
* PHID, using the PHID and the field name is likley reasonable:
*
* "{$phid}:{$field}"
*
* @param string Field name.
* @return string Cache key up to 125 characters.
*
* @task markup
*/
public function getMarkupFieldKey($field);
/**
* Build the engine the field should use.
*
* @param string Field name.
* @return PhutilRemarkupEngine Markup engine to use.
* @task markup
*/
public function newMarkupEngine($field);
/**
* Return the contents of the specified field.
*
* @param string Field name.
* @return string The raw markup contained in the field.
* @task markup
*/
public function getMarkupText($field);
/**
* Callback for final postprocessing of output. Normally, you can return
* the output unmodified.
*
* @param string Field name.
* @param string The finalized output of the engine.
* @param string The engine which generated the output.
* @return string Final output.
* @task markup
*/
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine);
/**
* Determine if the engine should try to use the markup cache or not.
* Generally you should use the cache for durable/permanent content but
* should not use the cache for temporary/draft content.
*
* @return bool True to use the markup cache.
* @task markup
*/
public function shouldUseMarkupCache($field);
}
diff --git a/src/infrastructure/markup/PhabricatorSyntaxHighlighter.php b/src/infrastructure/markup/PhabricatorSyntaxHighlighter.php
index 5615207326..fbd339a8c5 100644
--- a/src/infrastructure/markup/PhabricatorSyntaxHighlighter.php
+++ b/src/infrastructure/markup/PhabricatorSyntaxHighlighter.php
@@ -1,51 +1,35 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group markup
*/
final class PhabricatorSyntaxHighlighter {
public static function newEngine() {
$engine = PhabricatorEnv::newObjectFromConfig('syntax-highlighter.engine');
$config = array(
'pygments.enabled' => 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->highlightSource($language, $source);
}
public static function highlightWithLanguage($language, $source) {
$engine = self::newEngine();
return $engine->highlightSource($language, $source);
}
}
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleCountdown.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleCountdown.php
index eff06fd269..1c5f976ec4 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleCountdown.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleCountdown.php
@@ -1,88 +1,72 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group markup
*/
final class PhabricatorRemarkupRuleCountdown extends PhutilRemarkupRule {
const KEY_RULE_COUNTDOWN = 'rule.countdown';
public function apply($text) {
return preg_replace_callback(
"@\B{C(\d+)}\B@",
array($this, 'markupCountdown'),
$text);
}
private function markupCountdown($matches) {
$countdown = id(new PhabricatorTimer())->load($matches[1]);
if (!$countdown) {
return $matches[0];
}
$id = celerity_generate_unique_node_id();
$engine = $this->getEngine();
$token = $engine->storeText('');
$metadata_key = self::KEY_RULE_COUNTDOWN;
$metadata = $engine->getTextMetadata($metadata_key, array());
$metadata[$id] = array($countdown->getDatepoint(), $token);
$engine->setTextMetadata($metadata_key, $metadata);
return $token;
}
public function didMarkupText() {
$engine = $this->getEngine();
$metadata_key = self::KEY_RULE_COUNTDOWN;
$metadata = $engine->getTextMetadata($metadata_key, array());
if (!$metadata) {
return;
}
require_celerity_resource('javelin-behavior-countdown-timer');
foreach ($metadata as $id => $info) {
list($time, $token) = $info;
$count = phutil_render_tag(
'span',
array(
'id' => $id,
),
javelin_render_tag('span',
array('sigil' => 'phabricator-timer-days'), '').'d'.
javelin_render_tag('span',
array('sigil' => 'phabricator-timer-hours'), '').'h'.
javelin_render_tag('span',
array('sigil' => 'phabricator-timer-minutes'), '').'m'.
javelin_render_tag('span',
array('sigil' => 'phabricator-timer-seconds'), '').'s');
Javelin::initBehavior('countdown-timer', array(
'timestamp' => $time,
'container' => $id,
));
$engine->overwriteStoredText($token, $count);
}
$engine->setTextMetadata($metadata_key, array());
}
}
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleDifferential.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleDifferential.php
index f81c3e3cfd..6e72c9a80b 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleDifferential.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleDifferential.php
@@ -1,29 +1,13 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group markup
*/
final class PhabricatorRemarkupRuleDifferential
extends PhabricatorRemarkupRuleObjectName {
protected function getObjectNamePrefix() {
return 'D';
}
}
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleDiffusion.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleDiffusion.php
index 2bf86bdfa2..18bf6831aa 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleDiffusion.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleDiffusion.php
@@ -1,37 +1,21 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group markup
*/
final class PhabricatorRemarkupRuleDiffusion
extends PhutilRemarkupRule {
public function apply($text) {
return preg_replace_callback(
'@\br([A-Z]+[a-f0-9]+)\b@',
array($this, 'markupDiffusionLink'),
$text);
}
public function markupDiffusionLink($matches) {
return $this->getEngine()->storeText(
'<a href="/r'.$matches[1].'">r'.$matches[1].'</a>');
}
}
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleEmbedFile.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleEmbedFile.php
index bd1fde4d2d..3718651426 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleEmbedFile.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleEmbedFile.php
@@ -1,181 +1,165 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group markup
*/
final class PhabricatorRemarkupRuleEmbedFile
extends PhutilRemarkupRule {
const KEY_RULE_EMBED_FILE = 'rule.embed.file';
public function apply($text) {
return preg_replace_callback(
"@{F(\d+)([^}]+?)?}@",
array($this, 'markupEmbedFile'),
$text);
}
public function markupEmbedFile($matches) {
$file = null;
if ($matches[1]) {
// TODO: This is pretty inefficient if there are a bunch of files.
$file = id(new PhabricatorFile())->load($matches[1]);
}
if (!$file) {
return $matches[0];
}
$phid = $file->getPHID();
$engine = $this->getEngine();
$token = $engine->storeText('');
$metadata_key = self::KEY_RULE_EMBED_FILE;
$metadata = $engine->getTextMetadata($metadata_key, array());
$bundle = array('token' => $token);
$options = array(
'size' => 'thumb',
'layout' => 'left',
'float' => false,
'name' => null,
);
if (!empty($matches[2])) {
$matches[2] = trim($matches[2], ', ');
$options = PhutilSimpleOptions::parse($matches[2]) + $options;
}
$file_name = coalesce($options['name'], $file->getName());
$options['name'] = $file_name;
$attrs = array();
switch ($options['size']) {
case 'full':
$attrs['src'] = $file->getBestURI();
$options['image_class'] = null;
break;
case 'thumb':
default:
$attrs['src'] = $file->getPreview220URI();
$options['image_class'] = 'phabricator-remarkup-embed-image';
break;
}
$bundle['attrs'] = $attrs;
$bundle['options'] = $options;
$bundle['meta'] = array(
'phid' => $file->getPHID(),
'viewable' => $file->isViewableImage(),
'uri' => $file->getBestURI(),
'dUri' => $file->getDownloadURI(),
'name' => $options['name'],
);
$metadata[$phid][] = $bundle;
$engine->setTextMetadata($metadata_key, $metadata);
return $token;
}
public function didMarkupText() {
$engine = $this->getEngine();
$metadata_key = self::KEY_RULE_EMBED_FILE;
$metadata = $engine->getTextMetadata($metadata_key, array());
if (!$metadata) {
return;
}
foreach ($metadata as $phid => $bundles) {
foreach ($bundles as $data) {
$options = $data['options'];
$meta = $data['meta'];
if (!$meta['viewable'] || $options['layout'] == 'link') {
$link = id(new PhabricatorFileLinkView())
->setFilePHID($meta['phid'])
->setFileName($meta['name'])
->setFileDownloadURI($meta['dUri'])
->setFileViewURI($meta['uri'])
->setFileViewable($meta['viewable']);
$embed = $link->render();
$engine->overwriteStoredText($data['token'], $embed);
continue;
}
require_celerity_resource('lightbox-attachment-css');
$img = phutil_render_tag('img', $data['attrs']);
$embed = javelin_render_tag(
'a',
array(
'href' => '#',
'class' => $options['image_class'],
'sigil' => 'lightboxable',
'mustcapture' => true,
'meta' => $meta,
),
$img);
$layout_class = null;
switch ($options['layout']) {
case 'right':
case 'center':
case 'inline':
case 'left':
$layout_class = 'phabricator-remarkup-embed-layout-'.
$options['layout'];
break;
default:
$layout_class = 'phabricator-remarkup-embed-layout-left';
break;
}
if ($options['float']) {
switch ($options['layout']) {
case 'center':
case 'inline':
break;
case 'right':
$layout_class .= ' phabricator-remarkup-embed-float-right';
break;
case 'left':
default:
$layout_class .= ' phabricator-remarkup-embed-float-left';
break;
}
}
if ($layout_class) {
$embed = phutil_render_tag(
'div',
array(
'class' => $layout_class,
),
$embed);
}
$engine->overwriteStoredText($data['token'], $embed);
}
}
$engine->setTextMetadata($metadata_key, array());
}
}
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleImageMacro.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleImageMacro.php
index c7ad58033d..5a59fc41cd 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleImageMacro.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleImageMacro.php
@@ -1,66 +1,50 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group markup
*/
final class PhabricatorRemarkupRuleImageMacro
extends PhutilRemarkupRule {
private $images;
public function apply($text) {
return preg_replace_callback(
'@^([a-zA-Z0-9_\-]+)$@m',
array($this, 'markupImageMacro'),
$text);
}
public function markupImageMacro($matches) {
if ($this->images === null) {
$this->images = array();
$rows = id(new PhabricatorFileImageMacro())->loadAll();
foreach ($rows as $row) {
$this->images[$row->getName()] = $row->getFilePHID();
}
}
if (array_key_exists($matches[1], $this->images)) {
$phid = $this->images[$matches[1]];
$file = id(new PhabricatorFile())->loadOneWhere('phid = %s', $phid);
if ($file) {
$src_uri = $file->getBestURI();
} else {
$src_uri = null;
}
$img = phutil_render_tag(
'img',
array(
'src' => $src_uri,
'alt' => $matches[1],
'title' => $matches[1]),
null);
return $this->getEngine()->storeText($img);
} else {
return $matches[1];
}
}
}
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleManiphest.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleManiphest.php
index adb2cea862..56e379c88b 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleManiphest.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleManiphest.php
@@ -1,29 +1,13 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group markup
*/
final class PhabricatorRemarkupRuleManiphest
extends PhabricatorRemarkupRuleObjectName {
protected function getObjectNamePrefix() {
return 'T';
}
}
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleMention.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleMention.php
index ba42697c42..1591d0b62b 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleMention.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleMention.php
@@ -1,146 +1,130 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group markup
*/
final class PhabricatorRemarkupRuleMention
extends PhutilRemarkupRule {
const KEY_RULE_MENTION = 'rule.mention';
const KEY_RULE_MENTION_ORIGINAL = 'rule.mention.original';
const KEY_MENTIONED = 'phabricator.mentioned-user-phids';
// NOTE: The negative lookbehind prevents matches like "mail@lists", while
// allowing constructs like "@tomo/@mroch". Since we now allow periods in
// usernames, we can't resonably distinguish that "@company.com" isn't a
// username, so we'll incorrectly pick it up, but there's little to be done
// about that. We forbid terminal periods so that we can correctly capture
// "@joe" instead of "@joe." in "Hey, @joe.".
const REGEX = '/(?<!\w)@([a-zA-Z0-9._-]*[a-zA-Z0-9_-])/';
public function apply($text) {
return preg_replace_callback(
self::REGEX,
array($this, 'markupMention'),
$text);
}
private function markupMention($matches) {
$engine = $this->getEngine();
$token = $engine->storeText('');
// Store the original text exactly so we can preserve casing if it doesn't
// resolve into a username.
$original_key = self::KEY_RULE_MENTION_ORIGINAL;
$original = $engine->getTextMetadata($original_key, array());
$original[$token] = $matches[1];
$engine->setTextMetadata($original_key, $original);
$metadata_key = self::KEY_RULE_MENTION;
$metadata = $engine->getTextMetadata($metadata_key, array());
$username = strtolower($matches[1]);
if (empty($metadata[$username])) {
$metadata[$username] = array();
}
$metadata[$username][] = $token;
$engine->setTextMetadata($metadata_key, $metadata);
return $token;
}
public function didMarkupText() {
$engine = $this->getEngine();
$metadata_key = self::KEY_RULE_MENTION;
$metadata = $engine->getTextMetadata($metadata_key, array());
if (empty($metadata)) {
// No mentions, or we already processed them.
return;
}
$original_key = self::KEY_RULE_MENTION_ORIGINAL;
$original = $engine->getTextMetadata($original_key, array());
$usernames = array_keys($metadata);
$user_table = new PhabricatorUser();
$real_user_names = queryfx_all(
$user_table->establishConnection('r'),
'SELECT username, phid, realName, isDisabled
FROM %T
WHERE username IN (%Ls)',
$user_table->getTableName(),
$usernames);
$actual_users = array();
$mentioned_key = self::KEY_MENTIONED;
$mentioned = $engine->getTextMetadata($mentioned_key, array());
foreach ($real_user_names as $row) {
$actual_users[strtolower($row['username'])] = $row;
$mentioned[$row['phid']] = $row['phid'];
}
$engine->setTextMetadata($mentioned_key, $mentioned);
foreach ($metadata as $username => $tokens) {
$exists = isset($actual_users[$username]);
if (!$exists) {
$class = 'phabricator-remarkup-mention-unknown';
} else if ($actual_users[$username]['isDisabled']) {
$class = 'phabricator-remarkup-mention-disabled';
} else {
$class = 'phabricator-remarkup-mention-exists';
}
if ($exists) {
$tag = phutil_render_tag(
'a',
array(
'class' => $class,
'href' => '/p/'.$actual_users[$username]['username'].'/',
'target' => '_blank',
'title' => $actual_users[$username]['realName'],
),
phutil_escape_html('@'.$actual_users[$username]['username']));
foreach ($tokens as $token) {
$engine->overwriteStoredText($token, $tag);
}
} else {
// NOTE: The structure here is different from the 'exists' branch,
// because we want to preserve the original text capitalization and it
// may differ for each token.
foreach ($tokens as $token) {
$tag = phutil_render_tag(
'span',
array(
'class' => $class,
),
phutil_escape_html('@'.idx($original, $token, $username)));
$engine->overwriteStoredText($token, $tag);
}
}
}
// Don't re-process these mentions.
$engine->setTextMetadata($metadata_key, array());
}
}
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectHandle.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectHandle.php
index 115be2375f..4925459e4e 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectHandle.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectHandle.php
@@ -1,82 +1,66 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group markup
*/
abstract class PhabricatorRemarkupRuleObjectHandle
extends PhutilRemarkupRule {
const KEY_RULE_HANDLE = 'rule.handle';
abstract protected function getObjectNamePrefix();
abstract protected function loadObjectPHID($id);
public function apply($text) {
$prefix = $this->getObjectNamePrefix();
return preg_replace_callback(
"@\B{{$prefix}(\d+)}\B@",
array($this, 'markupObjectHandle'),
$text);
}
private function markupObjectHandle($matches) {
// TODO: These are single gets but should be okay for now, they're behind
// the cache.
$phid = $this->loadObjectPHID($matches[1]);
if (!$phid) {
return $matches[0];
}
$engine = $this->getEngine();
$token = $engine->storeText('');
$metadata_key = self::KEY_RULE_HANDLE;
$metadata = $engine->getTextMetadata($metadata_key, array());
if (empty($metadata[$phid])) {
$metadata[$phid] = array();
}
$metadata[$phid][] = $token;
$engine->setTextMetadata($metadata_key, $metadata);
return $token;
}
public function didMarkupText() {
$engine = $this->getEngine();
$metadata_key = self::KEY_RULE_HANDLE;
$metadata = $engine->getTextMetadata($metadata_key, array());
if (empty($metadata)) {
return;
}
$handles = id(new PhabricatorObjectHandleData(array_keys($metadata)))
->loadHandles();
foreach ($metadata as $phid => $tokens) {
$link = $handles[$phid]->renderLink();
foreach ($tokens as $token) {
$engine->overwriteStoredText($token, $link);
}
}
$engine->setTextMetadata($metadata_key, array());
}
}
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectName.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectName.php
index c354c80e5d..188da30235 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectName.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObjectName.php
@@ -1,62 +1,46 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group markup
*/
abstract class PhabricatorRemarkupRuleObjectName
extends PhutilRemarkupRule {
abstract protected function getObjectNamePrefix();
public function apply($text) {
$prefix = $this->getObjectNamePrefix();
return preg_replace_callback(
"@\b({$prefix})([1-9]\d*)(?:#([-\w\d]+))?\b@",
array($this, 'markupObjectNameLink'),
$text);
}
public function markupObjectNameLink($matches) {
list(, $prefix, $id) = $matches;
if (isset($matches[3])) {
$href = $matches[3];
$text = $matches[3];
if (preg_match('@^(?:comment-)?(\d{1,7})$@', $href, $matches)) {
// Maximum length is 7 because 12345678 could be a file hash.
$href = "comment-{$matches[1]}";
$text = $matches[1];
}
$href = "/{$prefix}{$id}#{$href}";
$text = "{$prefix}{$id}#{$text}";
} else {
$href = "/{$prefix}{$id}";
$text = "{$prefix}{$id}";
}
return $this->getEngine()->storeText(
phutil_render_tag(
'a',
array(
'href' => $href,
),
$text));
}
}
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRulePaste.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRulePaste.php
index 09c2a3d3aa..d83c5fefe6 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRulePaste.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRulePaste.php
@@ -1,29 +1,13 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group markup
*/
final class PhabricatorRemarkupRulePaste
extends PhabricatorRemarkupRuleObjectName {
protected function getObjectNamePrefix() {
return 'P';
}
}
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRulePhriction.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRulePhriction.php
index 718e50554c..ea7270dae2 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRulePhriction.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRulePhriction.php
@@ -1,52 +1,36 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group markup
*/
final class PhabricatorRemarkupRulePhriction
extends PhutilRemarkupRule {
public function apply($text) {
return preg_replace_callback(
'@\B\\[\\[([^|\\]]+)(?:\\|([^\\]]+))?\\]\\]\B@U',
array($this, 'markupDocumentLink'),
$text);
}
public function markupDocumentLink($matches) {
$slug = trim($matches[1]);
$name = trim(idx($matches, 2, $slug));
$name = explode('/', trim($name, '/'));
$name = end($name);
$slug = PhabricatorSlug::normalize($slug);
$uri = PhrictionDocument::getSlugURI($slug);
return $this->getEngine()->storeText(
phutil_render_tag(
'a',
array(
'href' => $uri,
'class' => 'phriction-link',
),
phutil_escape_html($name)));
}
}
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleProxyImage.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleProxyImage.php
index 4d2f3af0a6..0083a3bae7 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleProxyImage.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleProxyImage.php
@@ -1,61 +1,45 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group markup
*/
final class PhabricatorRemarkupRuleProxyImage
extends PhutilRemarkupRule {
public function apply($text) {
$filetypes = '\.(?:jpe?g|png|gif)';
$text = preg_replace_callback(
'@[<](\w{3,}://.+?'.$filetypes.')[>]@',
array($this, 'markupProxyImage'),
$text);
$text = preg_replace_callback(
'@(?<=^|\s)(\w{3,}://\S+'.$filetypes.')(?=\s|$)@',
array($this, 'markupProxyImage'),
$text);
return $text;
}
public function markupProxyImage($matches) {
$uri = PhabricatorFileProxyImage::getProxyImageURI($matches[1]);
return $this->getEngine()->storeText(
phutil_render_tag(
'a',
array(
'href' => $uri,
'target' => '_blank',
),
phutil_render_tag(
'img',
array(
'src' => $uri,
'class' => 'remarkup-proxy-image',
))));
}
}
diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleYoutube.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleYoutube.php
index b23f049580..2664e16c52 100644
--- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleYoutube.php
+++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleYoutube.php
@@ -1,59 +1,43 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group markup
*/
final class PhabricatorRemarkupRuleYoutube
extends PhutilRemarkupRule {
public function apply($text) {
$this->uri = new PhutilURI($text);
if ($this->uri->getDomain() &&
preg_match('/(^|\.)youtube\.com$/', $this->uri->getDomain())) {
return $this->markupYoutubeLink();
}
return $text;
}
public function markupYoutubeLink() {
$v = idx($this->uri->getQueryParams(), 'v');
if ($v) {
$youtube_src = 'https://www.youtube.com/embed/'.$v;
$iframe =
'<div class="embedded-youtube-video">'.
phutil_render_tag(
'iframe',
array(
'width' => '650',
'height' => '400',
'style' => 'margin: 1em auto; border: 0px;',
'src' => $youtube_src,
'frameborder' => 0,
),
'').
'</div>';
return $this->getEngine()->storeText($iframe);
} else {
return $this->uri;
}
}
}
diff --git a/src/infrastructure/markup/rule/PonderRuleQuestion.php b/src/infrastructure/markup/rule/PonderRuleQuestion.php
index 8841c30207..ac6583268e 100644
--- a/src/infrastructure/markup/rule/PonderRuleQuestion.php
+++ b/src/infrastructure/markup/rule/PonderRuleQuestion.php
@@ -1,29 +1,13 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group markup
*/
final class PonderRuleQuestion
extends PhabricatorRemarkupRuleObjectName {
protected function getObjectNamePrefix() {
return 'Q(?![1-4]\b)';
}
}
diff --git a/src/infrastructure/markup/rule/handle/PhabricatorRemarkupRuleDifferentialHandle.php b/src/infrastructure/markup/rule/handle/PhabricatorRemarkupRuleDifferentialHandle.php
index 09f4341818..93941672c3 100644
--- a/src/infrastructure/markup/rule/handle/PhabricatorRemarkupRuleDifferentialHandle.php
+++ b/src/infrastructure/markup/rule/handle/PhabricatorRemarkupRuleDifferentialHandle.php
@@ -1,37 +1,21 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group markup
*/
final class PhabricatorRemarkupRuleDifferentialHandle
extends PhabricatorRemarkupRuleObjectHandle {
protected function getObjectNamePrefix() {
return 'D';
}
protected function loadObjectPHID($id) {
$revision = id(new DifferentialRevision())->load($id);
if ($revision) {
return $revision->getPHID();
}
return null;
}
}
diff --git a/src/infrastructure/markup/rule/handle/PhabricatorRemarkupRuleManiphestHandle.php b/src/infrastructure/markup/rule/handle/PhabricatorRemarkupRuleManiphestHandle.php
index 83b4344666..ffee967ff1 100644
--- a/src/infrastructure/markup/rule/handle/PhabricatorRemarkupRuleManiphestHandle.php
+++ b/src/infrastructure/markup/rule/handle/PhabricatorRemarkupRuleManiphestHandle.php
@@ -1,37 +1,21 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @group markup
*/
final class PhabricatorRemarkupRuleManiphestHandle
extends PhabricatorRemarkupRuleObjectHandle {
protected function getObjectNamePrefix() {
return 'T';
}
protected function loadObjectPHID($id) {
$task = id(new ManiphestTask())->load($id);
if ($task) {
return $task->getPHID();
}
return null;
}
}
diff --git a/src/infrastructure/query/PhabricatorOffsetPagedQuery.php b/src/infrastructure/query/PhabricatorOffsetPagedQuery.php
index 371fcda559..de526b8ba5 100644
--- a/src/infrastructure/query/PhabricatorOffsetPagedQuery.php
+++ b/src/infrastructure/query/PhabricatorOffsetPagedQuery.php
@@ -1,67 +1,51 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* A query class which uses offset/limit paging. Provides logic and accessors
* for offsets and limits.
*/
abstract class PhabricatorOffsetPagedQuery extends PhabricatorQuery {
private $offset;
private $limit;
final public function setOffset($offset) {
$this->offset = $offset;
return $this;
}
final public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
final public function getOffset() {
return $this->offset;
}
final public function getLimit() {
return $this->limit;
}
protected function buildLimitClause(AphrontDatabaseConnection $conn_r) {
if ($this->limit && $this->offset) {
return qsprintf($conn_r, 'LIMIT %d, %d', $this->offset, $this->limit);
} else if ($this->limit) {
return qsprintf($conn_r, 'LIMIT %d', $this->limit);
} else if ($this->offset) {
return qsprintf($conn_r, 'LIMIT %d, %d', $this->offset, PHP_INT_MAX);
} else {
return '';
}
}
final public function executeWithOffsetPager(AphrontPagerView $pager) {
$this->setLimit($pager->getPageSize() + 1);
$this->setOffset($pager->getOffset());
$results = $this->execute();
return $pager->sliceResults($results);
}
}
diff --git a/src/infrastructure/query/PhabricatorQuery.php b/src/infrastructure/query/PhabricatorQuery.php
index b4670bb04a..1662e16ac5 100644
--- a/src/infrastructure/query/PhabricatorQuery.php
+++ b/src/infrastructure/query/PhabricatorQuery.php
@@ -1,33 +1,17 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorQuery {
abstract public function execute();
final protected function formatWhereClause(array $parts) {
$parts = array_filter($parts);
if (!$parts) {
return '';
}
return 'WHERE ('.implode(') AND (', $parts).')';
}
}
diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
index 5ec7fd408e..9dc13e6566 100644
--- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
+++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
@@ -1,137 +1,121 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* A query class which uses cursor-based paging. This paging is much more
* performant than offset-based paging in the presence of policy filtering.
*/
abstract class PhabricatorCursorPagedPolicyAwareQuery
extends PhabricatorPolicyAwareQuery {
private $afterID;
private $beforeID;
protected function getPagingColumn() {
return 'id';
}
protected function getPagingValue($result) {
return $result->getID();
}
protected function getReversePaging() {
return false;
}
protected function nextPage(array $page) {
if ($this->beforeID) {
$this->beforeID = $this->getPagingValue(last($page));
} else {
$this->afterID = $this->getPagingValue(last($page));
}
}
final public function setAfterID($object_id) {
$this->afterID = $object_id;
return $this;
}
final public function setBeforeID($object_id) {
$this->beforeID = $object_id;
return $this;
}
final protected function buildLimitClause(AphrontDatabaseConnection $conn_r) {
if ($this->getRawResultLimit()) {
return qsprintf($conn_r, 'LIMIT %d', $this->getRawResultLimit());
} else {
return '';
}
}
final protected function buildPagingClause(
AphrontDatabaseConnection $conn_r) {
if ($this->beforeID) {
return qsprintf(
$conn_r,
'%Q %Q %s',
$this->getPagingColumn(),
$this->getReversePaging() ? '<' : '>',
$this->beforeID);
} else if ($this->afterID) {
return qsprintf(
$conn_r,
'%Q %Q %s',
$this->getPagingColumn(),
$this->getReversePaging() ? '>' : '<',
$this->afterID);
}
return null;
}
final protected function buildOrderClause(AphrontDatabaseConnection $conn_r) {
if ($this->beforeID) {
return qsprintf(
$conn_r,
'ORDER BY %Q %Q',
$this->getPagingColumn(),
$this->getReversePaging() ? 'DESC' : 'ASC');
} else {
return qsprintf(
$conn_r,
'ORDER BY %Q %Q',
$this->getPagingColumn(),
$this->getReversePaging() ? 'ASC' : 'DESC');
}
}
final protected function didLoadResults(array $results) {
if ($this->beforeID) {
$results = array_reverse($results, $preserve_keys = true);
}
return $results;
}
final public function executeWithCursorPager(AphrontCursorPagerView $pager) {
$this->setLimit($pager->getPageSize() + 1);
if ($pager->getAfterID()) {
$this->setAfterID($pager->getAfterID());
} else if ($pager->getBeforeID()) {
$this->setBeforeID($pager->getBeforeID());
}
$results = $this->execute();
$sliced_results = $pager->sliceResults($results);
if ($pager->getBeforeID() || (count($results) > $pager->getPageSize())) {
$pager->setNextPageID($this->getPagingValue(last($sliced_results)));
}
if ($pager->getAfterID() ||
($pager->getBeforeID() && (count($results) > $pager->getPageSize()))) {
$pager->setPrevPageID($this->getPagingValue(head($sliced_results)));
}
return $sliced_results;
}
}
diff --git a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php
index b33901aa2f..3dc73386b5 100644
--- a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php
+++ b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php
@@ -1,309 +1,293 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* A @{class:PhabricatorQuery} which filters results according to visibility
* policies for the querying user. Broadly, this class allows you to implement
* a query that returns only objects the user is allowed to see.
*
* $results = id(new ExampleQuery())
* ->setViewer($user)
* ->withConstraint($example)
* ->execute();
*
* Normally, you should extend @{class:PhabricatorCursorPagedPolicyAwareQuery},
* not this class. @{class:PhabricatorCursorPagedPolicyAwareQuery} provides a
* more practical interface for building usable queries against most object
* types.
*
* NOTE: Although this class extends @{class:PhabricatorOffsetPagedQuery},
* offset paging with policy filtering is not efficient. All results must be
* loaded into the application and filtered here: skipping `N` rows via offset
* is an `O(N)` operation with a large constant. Prefer cursor-based paging
* with @{class:PhabricatorCursorPagedPolicyAwareQuery}, which can filter far
* more efficiently in MySQL.
*
* @task config Query Configuration
* @task exec Executing Queries
* @task policyimpl Policy Query Implementation
*/
abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery {
private $viewer;
private $raisePolicyExceptions;
private $rawResultLimit;
private $capabilities;
/* -( Query Configuration )------------------------------------------------ */
/**
* Set the viewer who is executing the query. Results will be filtered
* according to the viewer's capabilities. You must set a viewer to execute
* a policy query.
*
* @param PhabricatorUser The viewing user.
* @return this
* @task config
*/
final public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
/**
* Get the query's viewer.
*
* @return PhabricatorUser The viewing user.
* @task config
*/
final public function getViewer() {
return $this->viewer;
}
/**
* @task config
*/
final public function requireCapabilities(array $capabilities) {
$this->capabilities = $capabilities;
return $this;
}
/* -( Query Execution )---------------------------------------------------- */
/**
* Execute the query, expecting a single result. This method simplifies
* loading objects for detail pages or edit views.
*
* // Load one result by ID.
* $obj = id(new ExampleQuery())
* ->setViewer($user)
* ->withIDs(array($id))
* ->executeOne();
* if (!$obj) {
* return new Aphront404Response();
* }
*
* If zero results match the query, this method returns `null`.
*
* If one result matches the query, this method returns that result.
*
* If two or more results match the query, this method throws an exception.
* You should use this method only when the query constraints guarantee at
* most one match (e.g., selecting a specific ID or PHID).
*
* If one result matches the query but it is caught by the policy filter (for
* example, the user is trying to view or edit an object which exists but
* which they do not have permission to see) a policy exception is thrown.
*
* @return mixed Single result, or null.
* @task exec
*/
final public function executeOne() {
$this->raisePolicyExceptions = true;
try {
$results = $this->execute();
} catch (Exception $ex) {
$this->raisePolicyExceptions = false;
throw $ex;
}
if (count($results) > 1) {
throw new Exception("Expected a single result!");
}
if (!$results) {
return null;
}
return head($results);
}
/**
* Execute the query, loading all visible results.
*
* @return list<PhabricatorPolicyInterface> Result objects.
* @task exec
*/
final public function execute() {
if (!$this->viewer) {
throw new Exception("Call setViewer() before execute()!");
}
$results = array();
$filter = new PhabricatorPolicyFilter();
$filter->setViewer($this->viewer);
if (!$this->capabilities) {
$capabilities = array(
PhabricatorPolicyCapability::CAN_VIEW,
);
} else {
$capabilities = $this->capabilities;
}
$filter->requireCapabilities($capabilities);
$filter->raisePolicyExceptions($this->raisePolicyExceptions);
$offset = (int)$this->getOffset();
$limit = (int)$this->getLimit();
$count = 0;
if ($limit) {
$need = $offset + $limit;
} else {
$need = 0;
}
$this->willExecute();
do {
if ($need) {
$this->rawResultLimit = min($need - $count, 1024);
} else {
$this->rawResultLimit = 0;
}
$page = $this->loadPage();
$visible = $this->willFilterPage($page);
$visible = $filter->apply($visible);
foreach ($visible as $key => $result) {
++$count;
// If we have an offset, we just ignore that many results and start
// storing them only once we've hit the offset. This reduces memory
// requirements for large offsets, compared to storing them all and
// slicing them away later.
if ($count > $offset) {
$results[$key] = $result;
}
if ($need && ($count >= $need)) {
// If we have all the rows we need, break out of the paging query.
break 2;
}
}
if (!$this->rawResultLimit) {
// If we don't have a load count, we loaded all the results. We do
// not need to load another page.
break;
}
if (count($page) < $this->rawResultLimit) {
// If we have a load count but the unfiltered results contained fewer
// objects, we know this was the last page of objects; we do not need
// to load another page because we can deduce it would be empty.
break;
}
$this->nextPage($page);
} while (true);
$results = $this->didLoadResults($results);
return $results;
}
/* -( Policy Query Implementation )---------------------------------------- */
/**
* Get the number of results @{method:loadPage} should load. If the value is
* 0, @{method:loadPage} should load all available results.
*
* @return int The number of results to load, or 0 for all results.
* @task policyimpl
*/
final protected function getRawResultLimit() {
return $this->rawResultLimit;
}
/**
* Hook invoked before query execution. Generally, implementations should
* reset any internal cursors.
*
* @return void
* @task policyimpl
*/
protected function willExecute() {
return;
}
/**
* Load a raw page of results. Generally, implementations should load objects
* from the database. They should attempt to return the number of results
* hinted by @{method:getRawResultLimit}.
*
* @return list<PhabricatorPolicyInterface> List of filterable policy objects.
* @task policyimpl
*/
abstract protected function loadPage();
/**
* Update internal state so that the next call to @{method:loadPage} will
* return new results. Generally, you should adjust a cursor position based
* on the provided result page.
*
* @param list<PhabricatorPolicyInterface> The current page of results.
* @return void
* @task policyimpl
*/
abstract protected function nextPage(array $page);
/**
* Hook for applying a page filter prior to the privacy filter. This allows
* you to drop some items from the result set without creating problems with
* pagination or cursor updates.
*
* @param list<wild> Results from `loadPage()`.
* @return list<PhabricatorPolicyInterface> Objects for policy filtering.
* @task policyimpl
*/
protected function willFilterPage(array $page) {
return $page;
}
/**
* Hook for applying final adjustments before results are returned. This is
* used by @{class:PhabricatorCursorPagedPolicyAwareQuery} to reverse results
* that are queried during reverse paging.
*
* @param list<PhabricatorPolicyInterface> Query results.
* @return list<PhabricatorPolicyInterface> Final results.
* @task policyimpl
*/
protected function didLoadResults(array $results) {
return $results;
}
}
diff --git a/src/infrastructure/storage/__tests__/AphrontIsolatedDatabaseConnectionTestCase.php b/src/infrastructure/storage/__tests__/AphrontIsolatedDatabaseConnectionTestCase.php
index 391260eebc..ab1ce2f7b1 100644
--- a/src/infrastructure/storage/__tests__/AphrontIsolatedDatabaseConnectionTestCase.php
+++ b/src/infrastructure/storage/__tests__/AphrontIsolatedDatabaseConnectionTestCase.php
@@ -1,157 +1,141 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontIsolatedDatabaseConnectionTestCase
extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
// We disable this here because this test is unique (it is testing that
// isolation actually occurs) and must establish a live connection to the
// database to verify that.
self::PHABRICATOR_TESTCONFIG_ISOLATE_LISK => false,
);
}
public function testIsolation() {
// This will fail if the connection isn't isolated.
queryfx(
$this->newIsolatedConnection(),
'INSERT INVALID SYNTAX');
}
public function testInsertGeneratesID() {
$conn = $this->newIsolatedConnection();
queryfx($conn, 'INSERT');
$id1 = $conn->getInsertID();
queryfx($conn, 'INSERT');
$id2 = $conn->getInsertID();
$this->assertEqual(true, (bool)$id1, 'ID1 exists.');
$this->assertEqual(true, (bool)$id2, 'ID2 exists.');
$this->assertEqual(
true,
$id1 != $id2,
"IDs '{$id1}' and '{$id2}' are distinct.");
}
public function testDeletePermitted() {
$conn = $this->newIsolatedConnection();
queryfx($conn, 'DELETE');
}
public function testTransactionStack() {
$conn = $this->newIsolatedConnection();
$conn->openTransaction();
queryfx($conn, 'INSERT');
$conn->saveTransaction();
$this->assertEqual(
array(
'START TRANSACTION',
'INSERT',
'COMMIT',
),
$conn->getQueryTranscript());
$conn = $this->newIsolatedConnection();
$conn->openTransaction();
queryfx($conn, 'INSERT 1');
$conn->openTransaction();
queryfx($conn, 'INSERT 2');
$conn->killTransaction();
$conn->openTransaction();
queryfx($conn, 'INSERT 3');
$conn->openTransaction();
queryfx($conn, 'INSERT 4');
$conn->saveTransaction();
$conn->saveTransaction();
$conn->openTransaction();
queryfx($conn, 'INSERT 5');
$conn->killTransaction();
queryfx($conn, 'INSERT 6');
$conn->saveTransaction();
$this->assertEqual(
array(
'START TRANSACTION',
'INSERT 1',
'SAVEPOINT Aphront_Savepoint_1',
'INSERT 2',
'ROLLBACK TO SAVEPOINT Aphront_Savepoint_1',
'SAVEPOINT Aphront_Savepoint_1',
'INSERT 3',
'SAVEPOINT Aphront_Savepoint_2',
'INSERT 4',
'SAVEPOINT Aphront_Savepoint_1',
'INSERT 5',
'ROLLBACK TO SAVEPOINT Aphront_Savepoint_1',
'INSERT 6',
'COMMIT',
),
$conn->getQueryTranscript());
}
public function testTransactionRollback() {
$check = array();
$phid = new HarbormasterScratchTable();
$phid->openTransaction();
for ($ii = 0; $ii < 3; $ii++) {
$key = $this->generateTestData();
$obj = new HarbormasterScratchTable();
$obj->setData($key);
$obj->save();
$check[] = $key;
}
$phid->killTransaction();
foreach ($check as $key) {
$this->assertNoSuchRow($key);
}
}
private function newIsolatedConnection() {
$config = array();
return new AphrontIsolatedDatabaseConnection($config);
}
private function generateTestData() {
return Filesystem::readRandomCharacters(20);
}
private function assertNoSuchRow($data) {
try {
$row = id(new HarbormasterScratchTable())->loadOneWhere(
'data = %s',
$data);
$this->assertEqual(
null,
$row,
'Expect fake row to exist only in isolation.');
} catch (AphrontQueryConnectionException $ex) {
// If we can't connect to the database, conclude that the isolated
// connection actually is isolated. Philosophically, this perhaps allows
// us to claim this test does not depend on the database?
}
}
}
diff --git a/src/infrastructure/storage/__tests__/AphrontMySQLDatabaseConnectionTestCase.php b/src/infrastructure/storage/__tests__/AphrontMySQLDatabaseConnectionTestCase.php
index 1ee16af444..ccffbc2093 100644
--- a/src/infrastructure/storage/__tests__/AphrontMySQLDatabaseConnectionTestCase.php
+++ b/src/infrastructure/storage/__tests__/AphrontMySQLDatabaseConnectionTestCase.php
@@ -1,53 +1,37 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontMySQLDatabaseConnectionTestCase
extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
// We disable this here because we're testing live MySQL connections.
self::PHABRICATOR_TESTCONFIG_ISOLATE_LISK => false,
);
}
public function testConnectionFailures() {
$conn = id(new HarbormasterScratchTable())->establishConnection('r');
queryfx($conn, 'SELECT 1');
// We expect the connection to recover from a 2006 (lost connection) when
// outside of a transaction...
$conn->simulateErrorOnNextQuery(2006);
queryfx($conn, 'SELECT 1');
// ...but when transactional, we expect the query to throw when the
// connection is lost, because it indicates the transaction was aborted.
$conn->openTransaction();
$conn->simulateErrorOnNextQuery(2006);
$caught = null;
try {
queryfx($conn, 'SELECT 1');
} catch (AphrontQueryConnectionLostException $ex) {
$caught = $ex;
}
$this->assertEqual(true, $caught instanceof Exception);
}
}
diff --git a/src/infrastructure/storage/__tests__/QueryFormattingTestCase.php b/src/infrastructure/storage/__tests__/QueryFormattingTestCase.php
index a8416799bc..b7ef5f4ce8 100644
--- a/src/infrastructure/storage/__tests__/QueryFormattingTestCase.php
+++ b/src/infrastructure/storage/__tests__/QueryFormattingTestCase.php
@@ -1,56 +1,40 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class QueryFormattingTestCase extends PhabricatorTestCase {
public function testQueryFormatting() {
$conn_r = id(new PhabricatorUser())->establishConnection('r');
$this->assertEqual(
'NULL',
qsprintf($conn_r, '%nd', null));
$this->assertEqual(
'0',
qsprintf($conn_r, '%nd', 0));
$this->assertEqual(
'0',
qsprintf($conn_r, '%d', 0));
$raised = null;
try {
qsprintf($conn_r, '%d', 'derp');
} catch (Exception $ex) {
$raised = $ex;
}
$this->assertEqual(
(bool)$raised,
true,
'qsprintf should raise exception for invalid %d conversion.');
$this->assertEqual(
"'<S>'",
qsprintf($conn_r, '%s', null));
$this->assertEqual(
'NULL',
qsprintf($conn_r, '%ns', null));
}
}
diff --git a/src/infrastructure/storage/configuration/DatabaseConfigurationProvider.php b/src/infrastructure/storage/configuration/DatabaseConfigurationProvider.php
index d90b115ce8..4776298351 100644
--- a/src/infrastructure/storage/configuration/DatabaseConfigurationProvider.php
+++ b/src/infrastructure/storage/configuration/DatabaseConfigurationProvider.php
@@ -1,34 +1,18 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @stable
*/
interface DatabaseConfigurationProvider {
public function __construct(
LiskDAO $dao = null,
$mode = 'r',
$namespace = 'phabricator');
public function getUser();
public function getPassword();
public function getHost();
public function getDatabase();
}
diff --git a/src/infrastructure/storage/configuration/DefaultDatabaseConfigurationProvider.php b/src/infrastructure/storage/configuration/DefaultDatabaseConfigurationProvider.php
index 9439c92765..fd62a2680a 100644
--- a/src/infrastructure/storage/configuration/DefaultDatabaseConfigurationProvider.php
+++ b/src/infrastructure/storage/configuration/DefaultDatabaseConfigurationProvider.php
@@ -1,59 +1,43 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class DefaultDatabaseConfigurationProvider
implements DatabaseConfigurationProvider {
private $dao;
private $mode;
private $namespace;
public function __construct(
LiskDAO $dao = null,
$mode = 'r',
$namespace = 'phabricator') {
$this->dao = $dao;
$this->mode = $mode;
$this->namespace = $namespace;
}
public function getUser() {
return PhabricatorEnv::getEnvConfig('mysql.user');
}
public function getPassword() {
return new PhutilOpaqueEnvelope(PhabricatorEnv::getEnvConfig('mysql.pass'));
}
public function getHost() {
return PhabricatorEnv::getEnvConfig('mysql.host');
}
public function getDatabase() {
if (!$this->getDao()) {
return null;
}
return $this->namespace.'_'.$this->getDao()->getApplicationName();
}
final protected function getDao() {
return $this->dao;
}
}
diff --git a/src/infrastructure/storage/lisk/LiskDAO.php b/src/infrastructure/storage/lisk/LiskDAO.php
index e252b49b02..954191ef25 100644
--- a/src/infrastructure/storage/lisk/LiskDAO.php
+++ b/src/infrastructure/storage/lisk/LiskDAO.php
@@ -1,1780 +1,1764 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Simple object-authoritative data access object that makes it easy to build
* stuff that you need to save to a database. Basically, it means that the
* amount of boilerplate code (and, particularly, boilerplate SQL) you need
* to write is greatly reduced.
*
* Lisk makes it fairly easy to build something quickly and end up with
* reasonably high-quality code when you're done (e.g., getters and setters,
* objects, transactions, reasonably structured OO code). It's also very thin:
* you can break past it and use MySQL and other lower-level tools when you
* need to in those couple of cases where it doesn't handle your workflow
* gracefully.
*
* However, Lisk won't scale past one database and lacks many of the features
* of modern DAOs like Hibernate: for instance, it does not support joins or
* polymorphic storage.
*
* This means that Lisk is well-suited for tools like Differential, but often a
* poor choice elsewhere. And it is strictly unsuitable for many projects.
*
* Lisk's model is object-authoritative: the PHP class definition is the
* master authority for what the object looks like.
*
* =Building New Objects=
*
* To create new Lisk objects, extend @{class:LiskDAO} and implement
* @{method:establishLiveConnection}. It should return an
* @{class:AphrontDatabaseConnection}; this will tell Lisk where to save your
* objects.
*
* class Dog extends LiskDAO {
*
* protected $name;
* protected $breed;
*
* public function establishLiveConnection() {
* return $some_connection_object;
* }
* }
*
* Now, you should create your table:
*
* lang=sql
* CREATE TABLE dog (
* id int unsigned not null auto_increment primary key,
* name varchar(32) not null,
* breed varchar(32) not null,
* dateCreated int unsigned not null,
* dateModified int unsigned not null
* );
*
* For each property in your class, add a column with the same name to the table
* (see @{method:getConfiguration} for information about changing this mapping).
* Additionally, you should create the three columns `id`, `dateCreated` and
* `dateModified`. Lisk will automatically manage these, using them to implement
* autoincrement IDs and timestamps. If you do not want to use these features,
* see @{method:getConfiguration} for information on disabling them. At a bare
* minimum, you must normally have an `id` column which is a primary or unique
* key with a numeric type, although you can change its name by overriding
* @{method:getIDKey} or disable it entirely by overriding @{method:getIDKey} to
* return null. Note that many methods rely on a single-part primary key and
* will no longer work (they will throw) if you disable it.
*
* As you add more properties to your class in the future, remember to add them
* to the database table as well.
*
* Lisk will now automatically handle these operations: getting and setting
* properties, saving objects, loading individual objects, loading groups
* of objects, updating objects, managing IDs, updating timestamps whenever
* an object is created or modified, and some additional specialized
* operations.
*
* = Creating, Retrieving, Updating, and Deleting =
*
* To create and persist a Lisk object, use @{method:save}:
*
* $dog = id(new Dog())
* ->setName('Sawyer')
* ->setBreed('Pug')
* ->save();
*
* Note that **Lisk automatically builds getters and setters for all of your
* object's protected properties** via @{method:__call}. If you want to add
* custom behavior to your getters or setters, you can do so by overriding the
* @{method:readField} and @{method:writeField} methods.
*
* Calling @{method:save} will persist the object to the database. After calling
* @{method:save}, you can call @{method:getID} to retrieve the object's ID.
*
* To load objects by ID, use the @{method:load} method:
*
* $dog = id(new Dog())->load($id);
*
* This will load the Dog record with ID $id into $dog, or ##null## if no such
* record exists (@{method:load} is an instance method rather than a static
* method because PHP does not support late static binding, at least until PHP
* 5.3).
*
* To update an object, change its properties and save it:
*
* $dog->setBreed('Lab')->save();
*
* To delete an object, call @{method:delete}:
*
* $dog->delete();
*
* That's Lisk CRUD in a nutshell.
*
* = Queries =
*
* Often, you want to load a bunch of objects, or execute a more specialized
* query. Use @{method:loadAllWhere} or @{method:loadOneWhere} to do this:
*
* $pugs = $dog->loadAllWhere('breed = %s', 'Pug');
* $sawyer = $dog->loadOneWhere('name = %s', 'Sawyer');
*
* These methods work like @{function@libphutil:queryfx}, but only take half of
* a query (the part after the WHERE keyword). Lisk will handle the connection,
* columns, and object construction; you are responsible for the rest of it.
* @{method:loadAllWhere} returns a list of objects, while
* @{method:loadOneWhere} returns a single object (or `null`).
*
* There's also a @{method:loadRelatives} method which helps to prevent the 1+N
* queries problem.
*
* = Managing Transactions =
*
* Lisk uses a transaction stack, so code does not generally need to be aware
* of the transactional state of objects to implement correct transaction
* semantics:
*
* $obj->openTransaction();
* $obj->save();
* $other->save();
* // ...
* $other->openTransaction();
* $other->save();
* $another->save();
* if ($some_condition) {
* $other->saveTransaction();
* } else {
* $other->killTransaction();
* }
* // ...
* $obj->saveTransaction();
*
* Assuming ##$obj##, ##$other## and ##$another## live on the same database,
* this code will work correctly by establishing savepoints.
*
* Selects whose data are used later in the transaction should be included in
* @{method:beginReadLocking} or @{method:beginWriteLocking} block.
*
* @task conn Managing Connections
* @task config Configuring Lisk
* @task load Loading Objects
* @task info Examining Objects
* @task save Writing Objects
* @task hook Hooks and Callbacks
* @task util Utilities
* @task xaction Managing Transactions
* @task isolate Isolation for Unit Testing
*
* @group storage
*/
abstract class LiskDAO {
const CONFIG_IDS = 'id-mechanism';
const CONFIG_TIMESTAMPS = 'timestamps';
const CONFIG_AUX_PHID = 'auxiliary-phid';
const CONFIG_SERIALIZATION = 'col-serialization';
const CONFIG_PARTIAL_OBJECTS = 'partial-objects';
const SERIALIZATION_NONE = 'id';
const SERIALIZATION_JSON = 'json';
const SERIALIZATION_PHP = 'php';
const IDS_AUTOINCREMENT = 'ids-auto';
const IDS_PHID = 'ids-phid';
const IDS_MANUAL = 'ids-manual';
private $__dirtyFields = array();
private $__missingFields = array();
private static $processIsolationLevel = 0;
private static $transactionIsolationLevel = 0;
private static $__checkedClasses = array();
private $__ephemeral = false;
private static $connections = array();
private $inSet = null;
protected $id;
protected $phid;
protected $dateCreated;
protected $dateModified;
/**
* Build an empty object.
*
* @return obj Empty object.
*/
public function __construct() {
$id_key = $this->getIDKey();
if ($id_key) {
$this->$id_key = null;
}
if ($this->getConfigOption(self::CONFIG_PARTIAL_OBJECTS)) {
$this->resetDirtyFields();
$this_class = get_class($this);
if (empty(self::$__checkedClasses[$this_class])) {
self::$__checkedClasses = true;
if (PhabricatorEnv::getEnvConfig('lisk.check_property_methods')) {
$class = new ReflectionClass(get_class($this));
$methods = $class->getMethods();
$properties = $this->getProperties();
foreach ($methods as $method) {
$name = strtolower($method->getName());
if (!(strncmp($name, 'get', 3) && strncmp($name, 'set', 3))) {
$name = substr($name, 3);
$declaring_class_name = $method->getDeclaringClass()->getName();
if (isset($properties[$name]) &&
$declaring_class_name !== 'LiskDAO') {
throw new Exception(
"Cannot implement method {$method->getName()} in ".
"{$declaring_class_name}.");
}
}
}
}
}
}
}
/* -( Managing Connections )----------------------------------------------- */
/**
* Establish a live connection to a database service. This method should
* return a new connection. Lisk handles connection caching and management;
* do not perform caching deeper in the stack.
*
* @param string Mode, either 'r' (reading) or 'w' (reading and writing).
* @return AphrontDatabaseConnection New database connection.
* @task conn
*/
abstract protected function establishLiveConnection($mode);
/**
* Return a namespace for this object's connections in the connection cache.
* Generally, the database name is appropriate. Two connections are considered
* equivalent if they have the same connection namespace and mode.
*
* @return string Connection namespace for cache
* @task conn
*/
abstract protected function getConnectionNamespace();
/**
* Get an existing, cached connection for this object.
*
* @param mode Connection mode.
* @return AprontDatabaseConnection|null Connection, if it exists in cache.
* @task conn
*/
protected function getEstablishedConnection($mode) {
$key = $this->getConnectionNamespace().':'.$mode;
if (isset(self::$connections[$key])) {
return self::$connections[$key];
}
return null;
}
/**
* Store a connection in the connection cache.
*
* @param mode Connection mode.
* @param AphrontDatabaseConnection Connection to cache.
* @return this
* @task conn
*/
protected function setEstablishedConnection(
$mode,
AphrontDatabaseConnection $connection,
$force_unique = false) {
$key = $this->getConnectionNamespace().':'.$mode;
if ($force_unique) {
$key .= ':unique';
while (isset(self::$connections[$key])) {
$key .= '!';
}
}
self::$connections[$key] = $connection;
return $this;
}
/* -( Configuring Lisk )--------------------------------------------------- */
/**
* Change Lisk behaviors, like ID configuration and timestamps. If you want
* to change these behaviors, you should override this method in your child
* class and change the options you're interested in. For example:
*
* public function getConfiguration() {
* return array(
* Lisk_DataAccessObject::CONFIG_EXAMPLE => true,
* ) + parent::getConfiguration();
* }
*
* The available options are:
*
* CONFIG_IDS
* Lisk objects need to have a unique identifying ID. The three mechanisms
* available for generating this ID are IDS_AUTOINCREMENT (default, assumes
* the ID column is an autoincrement primary key), IDS_PHID (to generate a
* unique PHID for each object) or IDS_MANUAL (you are taking full
* responsibility for ID management).
*
* CONFIG_TIMESTAMPS
* Lisk can automatically handle keeping track of a `dateCreated' and
* `dateModified' column, which it will update when it creates or modifies
* an object. If you don't want to do this, you may disable this option.
* By default, this option is ON.
*
* CONFIG_AUX_PHID
* This option can be enabled by being set to some truthy value. The meaning
* of this value is defined by your PHID generation mechanism. If this option
* is enabled, a `phid' property will be populated with a unique PHID when an
* object is created (or if it is saved and does not currently have one). You
* need to override generatePHID() and hook it into your PHID generation
* mechanism for this to work. By default, this option is OFF.
*
* CONFIG_SERIALIZATION
* You can optionally provide a column serialization map that will be applied
* to values when they are written to the database. For example:
*
* self::CONFIG_SERIALIZATION => array(
* 'complex' => self::SERIALIZATION_JSON,
* )
*
* This will cause Lisk to JSON-serialize the 'complex' field before it is
* written, and unserialize it when it is read.
*
* CONFIG_PARTIAL_OBJECTS
* Sometimes, it is useful to load only some fields of an object (such as
* when you are loading all objects of a class, but only care about a few
* fields). Turning on this option (by setting it to a truthy value) allows
* users of the class to create/use partial objects, but it comes with some
* side effects: your class cannot override the setters and getters provided
* by Lisk (use readField and writeField instead), and you should not
* directly access or assign protected members of your class (use the getters
* and setters).
*
*
* @return dictionary Map of configuration options to values.
*
* @task config
*/
protected function getConfiguration() {
return array(
self::CONFIG_IDS => self::IDS_AUTOINCREMENT,
self::CONFIG_TIMESTAMPS => true,
self::CONFIG_PARTIAL_OBJECTS => false,
);
}
/**
* Determine the setting of a configuration option for this class of objects.
*
* @param const Option name, one of the CONFIG_* constants.
* @return mixed Option value, if configured (null if unavailable).
*
* @task config
*/
public function getConfigOption($option_name) {
static $options = null;
if (!isset($options)) {
$options = $this->getConfiguration();
}
return idx($options, $option_name);
}
/* -( Loading Objects )---------------------------------------------------- */
/**
* Load an object by ID. You need to invoke this as an instance method, not
* a class method, because PHP doesn't have late static binding (until
* PHP 5.3.0). For example:
*
* $dog = id(new Dog())->load($dog_id);
*
* @param int Numeric ID identifying the object to load.
* @return obj|null Identified object, or null if it does not exist.
*
* @task load
*/
public function load($id) {
if (!$id || (!is_int($id) && !ctype_digit($id))) {
return null;
}
return $this->loadOneWhere(
'%C = %d',
$this->getIDKeyForUse(),
$id);
}
/**
* Loads all of the objects, unconditionally.
*
* @return dict Dictionary of all persisted objects of this type, keyed
* on object ID.
*
* @task load
*/
public function loadAll() {
return $this->loadAllWhere('1 = 1');
}
/**
* Loads all objects, but only fetches the specified columns.
*
* @param array Array of canonical column names as strings
* @return dict Dictionary of all objects, keyed by ID.
*
* @task load
*/
public function loadColumns(array $columns) {
return $this->loadColumnsWhere($columns, '1 = 1');
}
/**
* Load all objects which match a WHERE clause. You provide everything after
* the 'WHERE'; Lisk handles everything up to it. For example:
*
* $old_dogs = id(new Dog())->loadAllWhere('age > %d', 7);
*
* The pattern and arguments are as per queryfx().
*
* @param string queryfx()-style SQL WHERE clause.
* @param ... Zero or more conversions.
* @return dict Dictionary of matching objects, keyed on ID.
*
* @task load
*/
public function loadAllWhere($pattern/* , $arg, $arg, $arg ... */) {
$args = func_get_args();
array_unshift($args, null);
$data = call_user_func_array(
array($this, 'loadRawDataWhere'),
$args);
return $this->loadAllFromArray($data);
}
/**
* Loads selected columns from objects that match a WHERE clause. You must
* provide everything after the WHERE. See loadAllWhere().
*
* @param array List of column names.
* @param string queryfx()-style SQL WHERE clause.
* @param ... Zero or more conversions.
* @return dict Dictionary of matching objecks, keyed by ID.
*
* @task load
*/
public function loadColumnsWhere(array $columns, $pattern/* , $args... */) {
if (!$this->getConfigOption(self::CONFIG_PARTIAL_OBJECTS)) {
throw new BadMethodCallException(
"This class does not support partial objects.");
}
$args = func_get_args();
$data = call_user_func_array(
array($this, 'loadRawDataWhere'),
$args);
return $this->loadAllFromArray($data);
}
/**
* Load a single object identified by a 'WHERE' clause. You provide
* everything after the 'WHERE', and Lisk builds the first half of the
* query. See loadAllWhere(). This method is similar, but returns a single
* result instead of a list.
*
* @param string queryfx()-style SQL WHERE clause.
* @param ... Zero or more conversions.
* @return obj|null Matching object, or null if no object matches.
*
* @task load
*/
public function loadOneWhere($pattern/* , $arg, $arg, $arg ... */) {
$args = func_get_args();
array_unshift($args, null);
$data = call_user_func_array(
array($this, 'loadRawDataWhere'),
$args);
if (count($data) > 1) {
throw new AphrontQueryCountException(
"More than 1 result from loadOneWhere()!");
}
$data = reset($data);
if (!$data) {
return null;
}
return $this->loadFromArray($data);
}
protected function loadRawDataWhere($columns, $pattern/* , $args... */) {
$connection = $this->establishConnection('r');
$lock_clause = '';
if ($connection->isReadLocking()) {
$lock_clause = 'FOR UPDATE';
} else if ($connection->isWriteLocking()) {
$lock_clause = 'LOCK IN SHARE MODE';
}
$args = func_get_args();
$args = array_slice($args, 2);
if (!$columns) {
$column = '*';
} else {
$column = '%LC';
$columns[] = $this->getIDKey();
$properties = $this->getProperties();
$this->__missingFields = array_diff_key(
array_flip($properties),
array_flip($columns));
}
$pattern = 'SELECT '.$column.' FROM %T WHERE '.$pattern.' %Q';
array_unshift($args, $this->getTableName());
if ($columns) {
array_unshift($args, $columns);
}
array_push($args, $lock_clause);
array_unshift($args, $pattern);
return call_user_func_array(
array($connection, 'queryData'),
$args);
}
/**
* Reload an object from the database, discarding any changes to persistent
* properties. This is primarily useful after entering a transaction but
* before applying changes to an object.
*
* @return this
*
* @task load
*/
public function reload() {
if (!$this->getID()) {
throw new Exception("Unable to reload object that hasn't been loaded!");
}
$result = $this->loadOneWhere(
'%C = %d',
$this->getIDKeyForUse(),
$this->getID());
if (!$result) {
throw new AphrontQueryObjectMissingException();
}
return $this;
}
/**
* Initialize this object's properties from a dictionary. Generally, you
* load single objects with loadOneWhere(), but sometimes it may be more
* convenient to pull data from elsewhere directly (e.g., a complicated
* join via queryData()) and then load from an array representation.
*
* @param dict Dictionary of properties, which should be equivalent to
* selecting a row from the table or calling getProperties().
* @return this
*
* @task load
*/
public function loadFromArray(array $row) {
static $valid_properties = array();
$map = array();
foreach ($row as $k => $v) {
// We permit (but ignore) extra properties in the array because a
// common approach to building the array is to issue a raw SELECT query
// which may include extra explicit columns or joins.
// This pathway is very hot on some pages, so we're inlining a cache
// and doing some microoptimization to avoid a strtolower() call for each
// assignment. The common path (assigning a valid property which we've
// already seen) always incurs only one empty(). The second most common
// path (assigning an invalid property which we've already seen) costs
// an empty() plus an isset().
if (empty($valid_properties[$k])) {
if (isset($valid_properties[$k])) {
// The value is set but empty, which means it's false, so we've
// already determined it's not valid. We don't need to check again.
continue;
}
$valid_properties[$k] = $this->hasProperty($k);
if (!$valid_properties[$k]) {
continue;
}
}
$map[$k] = $v;
}
$this->willReadData($map);
foreach ($map as $prop => $value) {
$this->$prop = $value;
}
$this->didReadData();
return $this;
}
/**
* Initialize a list of objects from a list of dictionaries. Usually you
* load lists of objects with loadAllWhere(), but sometimes that isn't
* flexible enough. One case is if you need to do joins to select the right
* objects:
*
* function loadAllWithOwner($owner) {
* $data = $this->queryData(
* 'SELECT d.*
* FROM owner o
* JOIN owner_has_dog od ON o.id = od.ownerID
* JOIN dog d ON od.dogID = d.id
* WHERE o.id = %d',
* $owner);
* return $this->loadAllFromArray($data);
* }
*
* This is a lot messier than loadAllWhere(), but more flexible.
*
* @param list List of property dictionaries.
* @return dict List of constructed objects, keyed on ID.
*
* @task load
*/
public function loadAllFromArray(array $rows) {
$result = array();
$id_key = $this->getIDKey();
foreach ($rows as $row) {
$obj = clone $this;
if ($id_key && isset($row[$id_key])) {
$result[$row[$id_key]] = $obj->loadFromArray($row);
} else {
$result[] = $obj->loadFromArray($row);
}
if ($this->inSet) {
$this->inSet->addToSet($obj);
}
}
return $result;
}
/**
* This method helps to prevent the 1+N queries problem. It happens when you
* execute a query for each row in a result set. Like in this code:
*
* COUNTEREXAMPLE, name=Easy to write but expensive to execute
* $diffs = id(new DifferentialDiff())->loadAllWhere(
* 'revisionID = %d',
* $revision->getID());
* foreach ($diffs as $diff) {
* $changesets = id(new DifferentialChangeset())->loadAllWhere(
* 'diffID = %d',
* $diff->getID());
* // Do something with $changesets.
* }
*
* One can solve this problem by reading all the dependent objects at once and
* assigning them later:
*
* COUNTEREXAMPLE, name=Cheaper to execute but harder to write and maintain
* $diffs = id(new DifferentialDiff())->loadAllWhere(
* 'revisionID = %d',
* $revision->getID());
* $all_changesets = id(new DifferentialChangeset())->loadAllWhere(
* 'diffID IN (%Ld)',
* mpull($diffs, 'getID'));
* $all_changesets = mgroup($all_changesets, 'getDiffID');
* foreach ($diffs as $diff) {
* $changesets = idx($all_changesets, $diff->getID(), array());
* // Do something with $changesets.
* }
*
* The method @{method:loadRelatives} abstracts this approach which allows
* writing a code which is simple and efficient at the same time:
*
* name=Easy to write and cheap to execute
* $diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID');
* foreach ($diffs as $diff) {
* $changesets = $diff->loadRelatives(
* new DifferentialChangeset(),
* 'diffID');
* // Do something with $changesets.
* }
*
* This will load dependent objects for all diffs in the first call of
* @{method:loadRelatives} and use this result for all following calls.
*
* The method supports working with set of sets, like in this code:
*
* $diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID');
* foreach ($diffs as $diff) {
* $changesets = $diff->loadRelatives(
* new DifferentialChangeset(),
* 'diffID');
* foreach ($changesets as $changeset) {
* $hunks = $changeset->loadRelatives(
* new DifferentialHunk(),
* 'changesetID');
* // Do something with hunks.
* }
* }
*
* This code will execute just three queries - one to load all diffs, one to
* load all their related changesets and one to load all their related hunks.
* You can try to write an equivalent code without using this method as
* a homework.
*
* The method also supports retrieving referenced objects, for example authors
* of all diffs (using shortcut @{method:loadOneRelative}):
*
* foreach ($diffs as $diff) {
* $author = $diff->loadOneRelative(
* new PhabricatorUser(),
* 'phid',
* 'getAuthorPHID');
* // Do something with author.
* }
*
* It is also possible to specify additional conditions for the `WHERE`
* clause. Similarly to @{method:loadAllWhere}, you can specify everything
* after `WHERE` (except `LIMIT`). Contrary to @{method:loadAllWhere}, it is
* allowed to pass only a constant string (`%` doesn't have a special
* meaning). This is intentional to avoid mistakes with using data from one
* row in retrieving other rows. Example of a correct usage:
*
* $status = $author->loadOneRelative(
* new PhabricatorUserStatus(),
* 'userPHID',
* 'getPHID',
* '(UNIX_TIMESTAMP() BETWEEN dateFrom AND dateTo)');
*
* @param LiskDAO Type of objects to load.
* @param string Name of the column in target table.
* @param string Method name in this table.
* @param string Additional constraints on returned rows. It supports no
* placeholders and requires putting the WHERE part into
* parentheses. It's not possible to use LIMIT.
* @return list Objects of type $object.
*
* @task load
*/
public function loadRelatives(
LiskDAO $object,
$foreign_column,
$key_method = 'getID',
$where = '') {
if (!$this->inSet) {
id(new LiskDAOSet())->addToSet($this);
}
$relatives = $this->inSet->loadRelatives(
$object,
$foreign_column,
$key_method,
$where);
return idx($relatives, $this->$key_method(), array());
}
/**
* Load referenced row. See @{method:loadRelatives} for details.
*
* @param LiskDAO Type of objects to load.
* @param string Name of the column in target table.
* @param string Method name in this table.
* @param string Additional constraints on returned rows. It supports no
* placeholders and requires putting the WHERE part into
* parentheses. It's not possible to use LIMIT.
* @return LiskDAO Object of type $object or null if there's no such object.
*
* @task load
*/
final public function loadOneRelative(
LiskDAO $object,
$foreign_column,
$key_method = 'getID',
$where = '') {
$relatives = $this->loadRelatives(
$object,
$foreign_column,
$key_method,
$where);
if (!$relatives) {
return null;
}
if (count($relatives) > 1) {
throw new AphrontQueryCountException(
"More than 1 result from loadOneRelative()!");
}
return reset($relatives);
}
final public function putInSet(LiskDAOSet $set) {
$this->inSet = $set;
return $this;
}
final protected function getInSet() {
return $this->inSet;
}
/* -( Examining Objects )-------------------------------------------------- */
/**
* Retrieve the unique, numerical ID identifying this object. This value
* will be null if the object hasn't been persisted.
*
* @return int Unique numerical ID.
*
* @task info
*/
public function getID() {
static $id_key = null;
if ($id_key === null) {
$id_key = $this->getIDKeyForUse();
}
return $this->$id_key;
}
/**
* Test if a property exists.
*
* @param string Property name.
* @return bool True if the property exists.
* @task info
*/
public function hasProperty($property) {
return (bool)$this->checkProperty($property);
}
/**
* Retrieve a list of all object properties. This list only includes
* properties that are declared as protected, and it is expected that
* all properties returned by this function should be persisted to the
* database.
* Properties that should not be persisted must be declared as private.
*
* @return dict Dictionary of normalized (lowercase) to canonical (original
* case) property names.
*
* @task info
*/
protected function getProperties() {
static $properties = null;
if (!isset($properties)) {
$class = new ReflectionClass(get_class($this));
$properties = array();
foreach ($class->getProperties(ReflectionProperty::IS_PROTECTED) as $p) {
$properties[strtolower($p->getName())] = $p->getName();
}
$id_key = $this->getIDKey();
if ($id_key != 'id') {
unset($properties['id']);
}
if (!$this->getConfigOption(self::CONFIG_TIMESTAMPS)) {
unset($properties['datecreated']);
unset($properties['datemodified']);
}
if ($id_key != 'phid' && !$this->getConfigOption(self::CONFIG_AUX_PHID)) {
unset($properties['phid']);
}
}
return $properties;
}
/**
* Check if a property exists on this object.
*
* @return string|null Canonical property name, or null if the property
* does not exist.
*
* @task info
*/
protected function checkProperty($property) {
static $properties = null;
if ($properties === null) {
$properties = $this->getProperties();
}
$property = strtolower($property);
if (empty($properties[$property])) {
return null;
}
return $properties[$property];
}
/**
* Get or build the database connection for this object.
*
* @param string 'r' for read, 'w' for read/write.
* @param bool True to force a new connection. The connection will not
* be retrieved from or saved into the connection cache.
* @return LiskDatabaseConnection Lisk connection object.
*
* @task info
*/
public function establishConnection($mode, $force_new = false) {
if ($mode != 'r' && $mode != 'w') {
throw new Exception("Unknown mode '{$mode}', should be 'r' or 'w'.");
}
if (self::shouldIsolateAllLiskEffectsToCurrentProcess()) {
$mode = 'isolate-'.$mode;
$connection = $this->getEstablishedConnection($mode);
if (!$connection) {
$connection = $this->establishIsolatedConnection($mode);
$this->setEstablishedConnection($mode, $connection);
}
return $connection;
}
if (self::shouldIsolateAllLiskEffectsToTransactions()) {
// If we're doing fixture transaction isolation, force the mode to 'w'
// so we always get the same connection for reads and writes, and thus
// can see the writes inside the transaction.
$mode = 'w';
}
// TODO: There is currently no protection on 'r' queries against writing.
$connection = null;
if (!$force_new) {
if ($mode == 'r') {
// If we're requesting a read connection but already have a write
// connection, reuse the write connection so that reads can take place
// inside transactions.
$connection = $this->getEstablishedConnection('w');
}
if (!$connection) {
$connection = $this->getEstablishedConnection($mode);
}
}
if (!$connection) {
$connection = $this->establishLiveConnection($mode);
if (self::shouldIsolateAllLiskEffectsToTransactions()) {
$connection->openTransaction();
}
$this->setEstablishedConnection(
$mode,
$connection,
$force_unique = $force_new);
}
return $connection;
}
/**
* Convert this object into a property dictionary. This dictionary can be
* restored into an object by using loadFromArray() (unless you're using
* legacy features with CONFIG_CONVERT_CAMELCASE, but in that case you should
* just go ahead and die in a fire).
*
* @return dict Dictionary of object properties.
*
* @task info
*/
protected function getPropertyValues() {
$map = array();
foreach ($this->getProperties() as $p) {
// We may receive a warning here for properties we've implicitly added
// through configuration; squelch it.
$map[$p] = @$this->$p;
}
return $map;
}
/* -( Writing Objects )---------------------------------------------------- */
/**
* Make an object read-only.
*
* Making an object ephemeral indicates that you will be changing state in
* such a way that you would never ever want it to be written back to the
* storage.
*/
public function makeEphemeral() {
$this->__ephemeral = true;
return $this;
}
private function isEphemeralCheck() {
if ($this->__ephemeral) {
throw new LiskEphemeralObjectException();
}
}
/**
* Persist this object to the database. In most cases, this is the only
* method you need to call to do writes. If the object has not yet been
* inserted this will do an insert; if it has, it will do an update.
*
* @return this
*
* @task save
*/
public function save() {
if ($this->shouldInsertWhenSaved()) {
return $this->insert();
} else {
return $this->update();
}
}
/**
* Save this object, forcing the query to use REPLACE regardless of object
* state.
*
* @return this
*
* @task save
*/
public function replace() {
$this->isEphemeralCheck();
return $this->insertRecordIntoDatabase('REPLACE');
}
/**
* Save this object, forcing the query to use INSERT regardless of object
* state.
*
* @return this
*
* @task save
*/
public function insert() {
$this->isEphemeralCheck();
return $this->insertRecordIntoDatabase('INSERT');
}
/**
* Save this object, forcing the query to use UPDATE regardless of object
* state.
*
* @return this
*
* @task save
*/
public function update() {
$this->isEphemeralCheck();
$this->willSaveObject();
$data = $this->getPropertyValues();
if ($this->getConfigOption(self::CONFIG_PARTIAL_OBJECTS)) {
$data = array_intersect_key($data, $this->__dirtyFields);
}
$this->willWriteData($data);
$map = array();
foreach ($data as $k => $v) {
$map[$k] = $v;
}
$conn = $this->establishConnection('w');
foreach ($map as $key => $value) {
$map[$key] = qsprintf($conn, '%C = %ns', $key, $value);
}
$map = implode(', ', $map);
$id = $this->getID();
$conn->query(
'UPDATE %T SET %Q WHERE %C = '.(is_int($id) ? '%d' : '%s'),
$this->getTableName(),
$map,
$this->getIDKeyForUse(),
$id);
// We can't detect a missing object because updating an object without
// changing any values doesn't affect rows. We could jiggle timestamps
// to catch this for objects which track them if we wanted.
$this->didWriteData();
if ($this->getConfigOption(self::CONFIG_PARTIAL_OBJECTS)) {
$this->resetDirtyFields();
}
return $this;
}
/**
* Delete this object, permanently.
*
* @return this
*
* @task save
*/
public function delete() {
$this->isEphemeralCheck();
$this->willDelete();
$conn = $this->establishConnection('w');
$conn->query(
'DELETE FROM %T WHERE %C = %d',
$this->getTableName(),
$this->getIDKeyForUse(),
$this->getID());
$this->didDelete();
return $this;
}
/**
* Internal implementation of INSERT and REPLACE.
*
* @param const Either "INSERT" or "REPLACE", to force the desired mode.
*
* @task save
*/
protected function insertRecordIntoDatabase($mode) {
$this->willSaveObject();
$data = $this->getPropertyValues();
$id_mechanism = $this->getConfigOption(self::CONFIG_IDS);
switch ($id_mechanism) {
case self::IDS_AUTOINCREMENT:
// If we are using autoincrement IDs, let MySQL assign the value for the
// ID column, if it is empty. If the caller has explicitly provided a
// value, use it.
$id_key = $this->getIDKeyForUse();
if (empty($data[$id_key])) {
unset($data[$id_key]);
}
break;
case self::IDS_PHID:
if (empty($data[$this->getIDKeyForUse()])) {
$phid = $this->generatePHID();
$this->setID($phid);
$data[$this->getIDKeyForUse()] = $phid;
}
break;
case self::IDS_MANUAL:
break;
default:
throw new Exception('Unknown CONFIG_IDs mechanism!');
}
$this->willWriteData($data);
$conn = $this->establishConnection('w');
$columns = array_keys($data);
foreach ($data as $key => $value) {
$data[$key] = qsprintf($conn, '%ns', $value);
}
$data = implode(', ', $data);
$conn->query(
'%Q INTO %T (%LC) VALUES (%Q)',
$mode,
$this->getTableName(),
$columns,
$data);
// Only use the insert id if this table is using auto-increment ids
if ($id_mechanism === self::IDS_AUTOINCREMENT) {
$this->setID($conn->getInsertID());
}
$this->didWriteData();
if ($this->getConfigOption(self::CONFIG_PARTIAL_OBJECTS)) {
$this->resetDirtyFields();
}
return $this;
}
/**
* Method used to determine whether to insert or update when saving.
*
* @return bool true if the record should be inserted
*/
protected function shouldInsertWhenSaved() {
$key_type = $this->getConfigOption(self::CONFIG_IDS);
if ($key_type == self::IDS_MANUAL) {
throw new Exception(
'You are using manual IDs. You must override the '.
'shouldInsertWhenSaved() method to properly detect '.
'when to insert a new record.');
} else {
return !$this->getID();
}
}
/* -( Hooks and Callbacks )------------------------------------------------ */
/**
* Retrieve the database table name. By default, this is the class name.
*
* @return string Table name for object storage.
*
* @task hook
*/
public function getTableName() {
return get_class($this);
}
/**
* Helper: Whether this class is configured to use PHIDs as the primary ID.
* @task internal
*/
private function isPHIDPrimaryID() {
return ($this->getConfigOption(self::CONFIG_IDS) === self::IDS_PHID);
}
/**
* Retrieve the primary key column, "id" by default. If you can not
* reasonably name your ID column "id", override this method.
*
* @return string Name of the ID column.
*
* @task hook
*/
public function getIDKey() {
return
$this->isPHIDPrimaryID() ?
'phid' :
'id';
}
protected function getIDKeyForUse() {
$id_key = $this->getIDKey();
if (!$id_key) {
throw new Exception(
"This DAO does not have a single-part primary key. The method you ".
"called requires a single-part primary key.");
}
return $id_key;
}
/**
* Generate a new PHID, used by CONFIG_AUX_PHID and IDS_PHID.
*
* @return phid Unique, newly allocated PHID.
*
* @task hook
*/
protected function generatePHID() {
throw new Exception(
"To use CONFIG_AUX_PHID or IDS_PHID, you need to overload ".
"generatePHID() to perform PHID generation.");
}
/**
* Hook to apply serialization or validation to data before it is written to
* the database. See also willReadData().
*
* @task hook
*/
protected function willWriteData(array &$data) {
$this->applyLiskDataSerialization($data, false);
}
/**
* Hook to perform actions after data has been written to the database.
*
* @task hook
*/
protected function didWriteData() {}
/**
* Hook to make internal object state changes prior to INSERT, REPLACE or
* UPDATE.
*
* @task hook
*/
protected function willSaveObject() {
$use_timestamps = $this->getConfigOption(self::CONFIG_TIMESTAMPS);
if ($use_timestamps) {
if (!$this->getDateCreated()) {
$this->setDateCreated(time());
}
$this->setDateModified(time());
}
if (($this->isPHIDPrimaryID() && !$this->getID())) {
// If PHIDs are the primary ID, the subclass could have overridden the
// name of the ID column.
$this->setID($this->generatePHID());
} else if ($this->getConfigOption(self::CONFIG_AUX_PHID) &&
!$this->getPHID()) {
// The subclass could still want PHIDs.
$this->setPHID($this->generatePHID());
}
}
/**
* Hook to apply serialization or validation to data as it is read from the
* database. See also willWriteData().
*
* @task hook
*/
protected function willReadData(array &$data) {
$this->applyLiskDataSerialization($data, $deserialize = true);
}
/**
* Hook to perform an action on data after it is read from the database.
*
* @task hook
*/
protected function didReadData() {}
/**
* Hook to perform an action before the deletion of an object.
*
* @task hook
*/
protected function willDelete() {}
/**
* Hook to perform an action after the deletion of an object.
*
* @task hook
*/
protected function didDelete() {}
/**
* Reads the value from a field. Override this method for custom behavior
* of getField() instead of overriding getField directly.
*
* @param string Canonical field name
* @return mixed Value of the field
*
* @task hook
*/
protected function readField($field) {
if (isset($this->$field)) {
return $this->$field;
}
return null;
}
/**
* Writes a value to a field. Override this method for custom behavior of
* setField($value) instead of overriding setField directly.
*
* @param string Canonical field name
* @param mixed Value to write
*
* @task hook
*/
protected function writeField($field, $value) {
$this->$field = $value;
}
/* -( Manging Transactions )----------------------------------------------- */
/**
* Increase transaction stack depth.
*
* @return this
*/
public function openTransaction() {
$this->establishConnection('w')->openTransaction();
return $this;
}
/**
* Decrease transaction stack depth, saving work.
*
* @return this
*/
public function saveTransaction() {
$this->establishConnection('w')->saveTransaction();
return $this;
}
/**
* Decrease transaction stack depth, discarding work.
*
* @return this
*/
public function killTransaction() {
$this->establishConnection('w')->killTransaction();
return $this;
}
/**
* Begins read-locking selected rows with SELECT ... FOR UPDATE, so that
* other connections can not read them (this is an enormous oversimplification
* of FOR UPDATE semantics; consult the MySQL documentation for details). To
* end read locking, call @{method:endReadLocking}. For example:
*
* $beach->openTransaction();
* $beach->beginReadLocking();
*
* $beach->reload();
* $beach->setGrainsOfSand($beach->getGrainsOfSand() + 1);
* $beach->save();
*
* $beach->endReadLocking();
* $beach->saveTransaction();
*
* @return this
* @task xaction
*/
public function beginReadLocking() {
$this->establishConnection('w')->beginReadLocking();
return $this;
}
/**
* Ends read-locking that began at an earlier @{method:beginReadLocking} call.
*
* @return this
* @task xaction
*/
public function endReadLocking() {
$this->establishConnection('w')->endReadLocking();
return $this;
}
/**
* Begins write-locking selected rows with SELECT ... LOCK IN SHARE MODE, so
* that other connections can not update or delete them (this is an
* oversimplification of LOCK IN SHARE MODE semantics; consult the
* MySQL documentation for details). To end write locking, call
* @{method:endWriteLocking}.
*
* @return this
* @task xaction
*/
public function beginWriteLocking() {
$this->establishConnection('w')->beginWriteLocking();
return $this;
}
/**
* Ends write-locking that began at an earlier @{method:beginWriteLocking}
* call.
*
* @return this
* @task xaction
*/
public function endWriteLocking() {
$this->establishConnection('w')->endWriteLocking();
return $this;
}
/* -( Isolation )---------------------------------------------------------- */
/**
* @task isolate
*/
public static function beginIsolateAllLiskEffectsToCurrentProcess() {
self::$processIsolationLevel++;
}
/**
* @task isolate
*/
public static function endIsolateAllLiskEffectsToCurrentProcess() {
self::$processIsolationLevel--;
if (self::$processIsolationLevel < 0) {
throw new Exception(
"Lisk process isolation level was reduced below 0.");
}
}
/**
* @task isolate
*/
public static function shouldIsolateAllLiskEffectsToCurrentProcess() {
return (bool)self::$processIsolationLevel;
}
/**
* @task isolate
*/
private function establishIsolatedConnection($mode) {
$config = array();
return new AphrontIsolatedDatabaseConnection($config);
}
/**
* @task isolate
*/
public static function beginIsolateAllLiskEffectsToTransactions() {
if (self::$transactionIsolationLevel === 0) {
self::closeAllConnections();
}
self::$transactionIsolationLevel++;
}
/**
* @task isolate
*/
public static function endIsolateAllLiskEffectsToTransactions() {
self::$transactionIsolationLevel--;
if (self::$transactionIsolationLevel < 0) {
throw new Exception(
"Lisk transaction isolation level was reduced below 0.");
} else if (self::$transactionIsolationLevel == 0) {
foreach (self::$connections as $key => $conn) {
if ($conn) {
$conn->killTransaction();
}
}
self::closeAllConnections();
}
}
/**
* @task isolate
*/
public static function shouldIsolateAllLiskEffectsToTransactions() {
return (bool)self::$transactionIsolationLevel;
}
public static function closeAllConnections() {
self::$connections = array();
}
/* -( Utilities )---------------------------------------------------------- */
/**
* Applies configured serialization to a dictionary of values.
*
* @task util
*/
protected function applyLiskDataSerialization(array &$data, $deserialize) {
$serialization = $this->getConfigOption(self::CONFIG_SERIALIZATION);
if ($serialization) {
foreach (array_intersect_key($serialization, $data) as $col => $format) {
switch ($format) {
case self::SERIALIZATION_NONE:
break;
case self::SERIALIZATION_PHP:
if ($deserialize) {
$data[$col] = unserialize($data[$col]);
} else {
$data[$col] = serialize($data[$col]);
}
break;
case self::SERIALIZATION_JSON:
if ($deserialize) {
$data[$col] = json_decode($data[$col], true);
} else {
$data[$col] = json_encode($data[$col]);
}
break;
default:
throw new Exception("Unknown serialization format '{$format}'.");
}
}
}
}
/**
* Resets the dirty fields (fields which need to be written on next save/
* update/insert/replace). If this DAO has timestamps, the modified time
* is always a dirty field.
*
* @task util
*/
private function resetDirtyFields() {
$this->__dirtyFields = array();
if ($this->getConfigOption(self::CONFIG_TIMESTAMPS)) {
$this->__dirtyFields['dateModified'] = true;
}
}
/**
* Black magic. Builds implied get*() and set*() for all properties.
*
* @param string Method name.
* @param list Argument vector.
* @return mixed get*() methods return the property value. set*() methods
* return $this.
* @task util
*/
public function __call($method, $args) {
// NOTE: PHP has a bug that static variables defined in __call() are shared
// across all children classes. Call a different method to work around this
// bug.
return $this->call($method, $args);
}
/**
* @task util
*/
final protected function call($method, $args) {
// NOTE: This method is very performance-sensitive (many thousands of calls
// per page on some pages), and thus has some silliness in the name of
// optimizations.
static $dispatch_map = array();
static $partial = null;
if ($partial === null) {
$partial = $this->getConfigOption(self::CONFIG_PARTIAL_OBJECTS);
}
if ($method[0] === 'g') {
if (isset($dispatch_map[$method])) {
$property = $dispatch_map[$method];
} else {
if (substr($method, 0, 3) !== 'get') {
throw new Exception("Unable to resolve method '{$method}'!");
}
$property = substr($method, 3);
if (!($property = $this->checkProperty($property))) {
throw new Exception("Bad getter call: {$method}");
}
$dispatch_map[$method] = $property;
}
if ($partial && isset($this->__missingFields[$property])) {
throw new Exception("Cannot get field that wasn't loaded: {$property}");
}
return $this->readField($property);
}
if ($method[0] === 's') {
if (isset($dispatch_map[$method])) {
$property = $dispatch_map[$method];
} else {
if (substr($method, 0, 3) !== 'set') {
throw new Exception("Unable to resolve method '{$method}'!");
}
$property = substr($method, 3);
$property = $this->checkProperty($property);
if (!$property) {
throw new Exception("Bad setter call: {$method}");
}
if ($property == 'ID') {
$property = $this->getIDKeyForUse();
}
$dispatch_map[$method] = $property;
}
if ($partial) {
// Accept writes to fields that weren't initially loaded
unset($this->__missingFields[$property]);
$this->__dirtyFields[$property] = true;
}
$this->writeField($property, $args[0]);
return $this;
}
throw new Exception("Unable to resolve method '{$method}'.");
}
/**
* Warns against writing to undeclared property.
*
* @task util
*/
public function __set($name, $value) {
phlog('Wrote to undeclared property '.get_class($this).'::$'.$name.'.');
$this->$name = $value;
}
}
diff --git a/src/infrastructure/storage/lisk/LiskDAOSet.php b/src/infrastructure/storage/lisk/LiskDAOSet.php
index de3e2782c1..ad2ad452ca 100644
--- a/src/infrastructure/storage/lisk/LiskDAOSet.php
+++ b/src/infrastructure/storage/lisk/LiskDAOSet.php
@@ -1,129 +1,113 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* You usually don't need to use this class directly as it is controlled by
* @{class:LiskDAO}. You can create it if you want to work with objects of same
* type from different sources as with one set. Let's say you want to get
* e-mails of all users involved in a revision:
*
* $users = new LiskDAOSet();
* $users->addToSet($author);
* foreach ($reviewers as $reviewer) {
* $users->addToSet($reviewer);
* }
* foreach ($ccs as $cc) {
* $users->addToSet($cc);
* }
* // Preload e-mails of all involved users and return e-mails of author.
* $author_emails = $author->loadRelatives(
* new PhabricatorUserEmail(),
* 'userPHID',
* 'getPHID');
*/
final class LiskDAOSet {
private $daos = array();
private $relatives = array();
private $edges = array();
private $subsets = array();
public function addToSet(LiskDAO $dao) {
if ($this->relatives || $this->edges) {
throw new Exception("Don't call addToSet() after loading data!");
}
$this->daos[] = $dao;
$dao->putInSet($this);
return $this;
}
/**
* The main purpose of this method is to break cyclic dependency.
* It removes all objects from this set and all subsets created by it.
*/
final public function clearSet() {
$this->daos = array();
$this->relatives = array();
$this->edges = array();
foreach ($this->subsets as $set) {
$set->clearSet();
}
$this->subsets = array();
return $this;
}
/**
* See @{method:LiskDAO::loadRelatives}.
*/
public function loadRelatives(
LiskDAO $object,
$foreign_column,
$key_method = 'getID',
$where = '') {
$relatives = &$this->relatives[
get_class($object)."-{$foreign_column}-{$key_method}-{$where}"];
if ($relatives === null) {
$ids = array();
foreach ($this->daos as $dao) {
$id = $dao->$key_method();
if ($id !== null) {
$ids[$id] = $id;
}
}
if (!$ids) {
$relatives = array();
} else {
$set = new LiskDAOSet();
$this->subsets[] = $set;
$relatives = $object->putInSet($set)->loadAllWhere(
'%C IN (%Ls) %Q',
$foreign_column,
$ids,
($where != '' ? 'AND '.$where : ''));
$relatives = mgroup($relatives, 'get'.$foreign_column);
}
}
return $relatives;
}
public function loadRelativeEdges($type) {
$edges = &$this->edges[$type];
if ($edges === null) {
if (!$this->daos) {
$edges = array();
} else {
assert_instances_of($this->daos, 'PhabricatorLiskDAO');
$phids = mpull($this->daos, 'getPHID', 'getPHID');
$edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($phids)
->withEdgeTypes(array($type))
->execute();
foreach ($this->daos as $dao) {
$dao->attachEdges($edges[$dao->getPHID()]);
}
}
}
return $edges;
}
}
diff --git a/src/infrastructure/storage/lisk/LiskEphemeralObjectException.php b/src/infrastructure/storage/lisk/LiskEphemeralObjectException.php
index 588c3fafe7..b5a9addb76 100644
--- a/src/infrastructure/storage/lisk/LiskEphemeralObjectException.php
+++ b/src/infrastructure/storage/lisk/LiskEphemeralObjectException.php
@@ -1,19 +1,3 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class LiskEphemeralObjectException extends Exception {}
diff --git a/src/infrastructure/storage/lisk/LiskMigrationIterator.php b/src/infrastructure/storage/lisk/LiskMigrationIterator.php
index cb3eaaa45c..a46512f866 100644
--- a/src/infrastructure/storage/lisk/LiskMigrationIterator.php
+++ b/src/infrastructure/storage/lisk/LiskMigrationIterator.php
@@ -1,64 +1,48 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Iterate over every object of a given type, without holding all of them in
* memory. This is useful for performing database migrations.
*
* $things = new LiskMigrationIterator(new LiskThing());
* foreach ($things as $thing) {
* // do something
* }
*
* NOTE: This only works on objects with a normal `id` column.
*
* @task storage
*/
final class LiskMigrationIterator extends PhutilBufferedIterator {
private $object;
private $cursor;
private $set;
public function __construct(LiskDAO $object) {
$this->set = new LiskDAOSet();
$this->object = $object->putInSet($this->set);
}
protected function didRewind() {
$this->cursor = 0;
}
public function key() {
return $this->current()->getID();
}
protected function loadPage() {
$this->set->clearSet();
$results = $this->object->loadAllWhere(
'id > %d ORDER BY id ASC LIMIT %d',
$this->cursor,
$this->getPageSize());
if ($results) {
$this->cursor = last($results)->getID();
}
return $results;
}
}
diff --git a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php
index 3136c769ba..f692a9653f 100644
--- a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php
+++ b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php
@@ -1,167 +1,151 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @task edges Managing Edges
* @task config Configuring Storage
*/
abstract class PhabricatorLiskDAO extends LiskDAO {
private $edges = array();
private static $namespaceStack = array();
/* -( Managing Edges )----------------------------------------------------- */
/**
* @task edges
*/
public function attachEdges(array $edges) {
foreach ($edges as $type => $type_edges) {
$this->edges[$type] = $type_edges;
}
return $this;
}
/**
* @task edges
*/
public function getEdges($type) {
$edges = idx($this->edges, $type);
if ($edges === null) {
throw new Exception("Call attachEdges() before getEdges()!");
}
return $edges;
}
/**
* @task edges
*/
public function loadRelativeEdges($type) {
if (!$this->getInSet()) {
id(new LiskDAOSet())->addToSet($this);
}
$this->getInSet()->loadRelativeEdges($type);
return $this->getEdges($type);
}
/**
* @task edges
*/
public function getEdgePHIDs($type) {
return ipull($this->getEdges($type), 'dst');
}
/* -( Configuring Storage )------------------------------------------------ */
/**
* @task config
*/
public static function pushStorageNamespace($namespace) {
self::$namespaceStack[] = $namespace;
}
/**
* @task config
*/
public static function popStorageNamespace() {
array_pop(self::$namespaceStack);
}
/**
* @task config
*/
public static function getDefaultStorageNamespace() {
return PhabricatorEnv::getEnvConfig('storage.default-namespace');
}
/**
* @task config
*/
public static function getStorageNamespace() {
$namespace = end(self::$namespaceStack);
if (!strlen($namespace)) {
$namespace = self::getDefaultStorageNamespace();
}
if (!strlen($namespace)) {
throw new Exception("No storage namespace configured!");
}
return $namespace;
}
/**
* @task config
*/
public function establishLiveConnection($mode) {
$namespace = self::getStorageNamespace();
$conf = PhabricatorEnv::newObjectFromConfig(
'mysql.configuration-provider',
array($this, $mode, $namespace));
$retries = PhabricatorEnv::getEnvConfig('mysql.connection-retries');
return PhabricatorEnv::newObjectFromConfig(
'mysql.implementation',
array(
array(
'user' => $conf->getUser(),
'pass' => $conf->getPassword(),
'host' => $conf->getHost(),
'database' => $conf->getDatabase(),
'retries' => $retries,
),
));
}
/**
* @task config
*/
public function getTableName() {
$str = 'phabricator';
$len = strlen($str);
$class = strtolower(get_class($this));
if (!strncmp($class, $str, $len)) {
$class = substr($class, $len);
}
$app = $this->getApplicationName();
if (!strncmp($class, $app, strlen($app))) {
$class = substr($class, strlen($app));
}
if (strlen($class)) {
return $app.'_'.$class;
} else {
return $app;
}
}
/**
* @task config
*/
abstract public function getApplicationName();
protected function getConnectionNamespace() {
return self::getStorageNamespace().'_'.$this->getApplicationName();
}
}
diff --git a/src/infrastructure/storage/lisk/__tests__/LiskDAOTestCase.php b/src/infrastructure/storage/lisk/__tests__/LiskDAOTestCase.php
index d2737ca4c5..fc3a15dae8 100644
--- a/src/infrastructure/storage/lisk/__tests__/LiskDAOTestCase.php
+++ b/src/infrastructure/storage/lisk/__tests__/LiskDAOTestCase.php
@@ -1,33 +1,17 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class LiskDAOTestCase extends PhabricatorTestCase {
public function testCheckProperty() {
$scratch = new HarbormasterScratchTable();
$scratch->getData();
$this->assertException('Exception', array($this, 'getData'));
}
public function getData() {
$isolation = new LiskIsolationTestDAO();
$isolation->getData();
}
}
diff --git a/src/infrastructure/storage/lisk/__tests__/LiskFixtureTestCase.php b/src/infrastructure/storage/lisk/__tests__/LiskFixtureTestCase.php
index d20dfae1ee..0f3b1db99d 100644
--- a/src/infrastructure/storage/lisk/__tests__/LiskFixtureTestCase.php
+++ b/src/infrastructure/storage/lisk/__tests__/LiskFixtureTestCase.php
@@ -1,115 +1,99 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class LiskFixtureTestCase extends PhabricatorTestCase {
public function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testTransactionalIsolation1of2() {
// NOTE: These tests are verifying that data is destroyed between tests.
// If the user from either test persists, the other test will fail.
$this->assertEqual(
0,
count(id(new HarbormasterScratchTable())->loadAll()));
id(new HarbormasterScratchTable())
->setData('alincoln')
->save();
}
public function testTransactionalIsolation2of2() {
$this->assertEqual(
0,
count(id(new HarbormasterScratchTable())->loadAll()));
id(new HarbormasterScratchTable())
->setData('ugrant')
->save();
}
public function testFixturesBasicallyWork() {
$this->assertEqual(
0,
count(id(new HarbormasterScratchTable())->loadAll()));
id(new HarbormasterScratchTable())
->setData('gwashington')
->save();
$this->assertEqual(
1,
count(id(new HarbormasterScratchTable())->loadAll()));
}
public function testReadableTransactions() {
// TODO: When we have semi-durable fixtures, use those instead. This is
// extremely hacky.
LiskDAO::endIsolateAllLiskEffectsToTransactions();
try {
$data = Filesystem::readRandomCharacters(32);
$obj = new HarbormasterScratchTable();
$obj->openTransaction();
$obj->setData($data);
$obj->save();
$loaded = id(new HarbormasterScratchTable())->loadOneWhere(
'data = %s',
$data);
$obj->killTransaction();
$this->assertEqual(
true,
($loaded !== null),
"Reads inside transactions should have transaction visibility.");
LiskDAO::beginIsolateAllLiskEffectsToTransactions();
} catch (Exception $ex) {
LiskDAO::beginIsolateAllLiskEffectsToTransactions();
throw $ex;
}
}
public function testGarbageLoadCalls() {
$obj = new HarbormasterObject();
$obj->save();
$id = $obj->getID();
$load = new HarbormasterObject();
$this->assertEqual(null, $load->load(0));
$this->assertEqual(null, $load->load(-1));
$this->assertEqual(null, $load->load(9999));
$this->assertEqual(null, $load->load(''));
$this->assertEqual(null, $load->load('cow'));
$this->assertEqual(null, $load->load($id."cow"));
$this->assertEqual(true, (bool)$load->load((int)$id));
$this->assertEqual(true, (bool)$load->load((string)$id));
}
}
diff --git a/src/infrastructure/storage/lisk/__tests__/LiskIsolationTestCase.php b/src/infrastructure/storage/lisk/__tests__/LiskIsolationTestCase.php
index 12ac2d68ff..aba1873751 100644
--- a/src/infrastructure/storage/lisk/__tests__/LiskIsolationTestCase.php
+++ b/src/infrastructure/storage/lisk/__tests__/LiskIsolationTestCase.php
@@ -1,128 +1,112 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class LiskIsolationTestCase extends PhabricatorTestCase {
public function testIsolatedWrites() {
$dao = new LiskIsolationTestDAO();
$this->assertEqual(null, $dao->getID(), 'Expect no ID.');
$this->assertEqual(null, $dao->getPHID(), 'Expect no PHID.');
$dao->save(); // Effects insert
$id = $dao->getID();
$phid = $dao->getPHID();
$this->assertEqual(true, (bool)$id, 'Expect ID generated.');
$this->assertEqual(true, (bool)$phid, 'Expect PHID generated.');
$dao->save(); // Effects update
$this->assertEqual($id, $dao->getID(), 'Expect ID unchanged.');
$this->assertEqual($phid, $dao->getPHID(), 'Expect PHID unchanged.');
}
public function testEphemeral() {
$dao = new LiskIsolationTestDAO();
$dao->save();
$dao->makeEphemeral();
$this->tryTestCases(
array(
$dao,
),
array(
false,
),
array($this, 'saveDAO'));
}
public function saveDAO($dao) {
$dao->save();
}
public function testIsolationContainment() {
$dao = new LiskIsolationTestDAO();
try {
$dao->establishLiveConnection('r');
$this->assertFailure(
"LiskIsolationTestDAO did not throw an exception when instructed to ".
"explicitly connect to an external database.");
} catch (LiskIsolationTestDAOException $ex) {
// Expected, pass.
}
}
public function testMagicMethods() {
$dao = new LiskIsolationTestDAO();
$this->assertEqual(
null,
$dao->getName(),
'getName() on empty object');
$this->assertEqual(
$dao,
$dao->setName('x'),
'setName() returns $this');
$this->assertEqual(
'y',
$dao->setName('y')->getName(),
'setName() has an effect');
$ex = null;
try {
$dao->gxxName();
} catch (Exception $thrown) {
$ex = $thrown;
}
$this->assertEqual(
true,
(bool)$ex,
'Typoing "get" should throw.');
$ex = null;
try {
$dao->sxxName('z');
} catch (Exception $thrown) {
$ex = $thrown;
}
$this->assertEqual(
true,
(bool)$ex,
'Typoing "set" should throw.');
$ex = null;
try {
$dao->madeUpMethod();
} catch (Exception $thrown) {
$ex = $thrown;
}
$this->assertEqual(
true,
(bool)$ex,
'Made up method should throw.');
}
}
diff --git a/src/infrastructure/storage/lisk/__tests__/LiskIsolationTestDAO.php b/src/infrastructure/storage/lisk/__tests__/LiskIsolationTestDAO.php
index 22e6f59084..545accbe6b 100644
--- a/src/infrastructure/storage/lisk/__tests__/LiskIsolationTestDAO.php
+++ b/src/infrastructure/storage/lisk/__tests__/LiskIsolationTestDAO.php
@@ -1,48 +1,32 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class LiskIsolationTestDAO extends LiskDAO {
protected $name;
protected $phid;
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID('TISO');
}
public function establishLiveConnection($mode) {
throw new LiskIsolationTestDAOException(
"Isolation failure! DAO is attempting to connect to an external ".
"resource!");
}
public function getConnectionNamespace() {
return 'test';
}
public function getTableName() {
return 'test';
}
}
diff --git a/src/infrastructure/storage/lisk/__tests__/LiskIsolationTestDAOException.php b/src/infrastructure/storage/lisk/__tests__/LiskIsolationTestDAOException.php
index a0161d7c6a..448b62655e 100644
--- a/src/infrastructure/storage/lisk/__tests__/LiskIsolationTestDAOException.php
+++ b/src/infrastructure/storage/lisk/__tests__/LiskIsolationTestDAOException.php
@@ -1,19 +1,3 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class LiskIsolationTestDAOException extends Exception { }
diff --git a/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php b/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php
index d713ab7311..9c012993f8 100644
--- a/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php
+++ b/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php
@@ -1,195 +1,179 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorStorageManagementAPI {
private $host;
private $user;
private $password;
private $namespace;
public function setNamespace($namespace) {
$this->namespace = $namespace;
PhabricatorLiskDAO::pushStorageNamespace($namespace);
return $this;
}
public function getNamespace() {
return $this->namespace;
}
public function setUser($user) {
$this->user = $user;
return $this;
}
public function getUser() {
return $this->user;
}
public function setPassword($password) {
$this->password = $password;
return $this;
}
public function getPassword() {
return $this->password;
}
public function setHost($host) {
$this->host = $host;
return $this;
}
public function getHost() {
return $this->host;
}
public function getDatabaseName($fragment) {
return $this->namespace.'_'.$fragment;
}
public function getDatabaseList(array $patches) {
assert_instances_of($patches, 'PhabricatorStoragePatch');
$list = array();
foreach ($patches as $patch) {
if ($patch->getType() == 'db') {
$list[] = $this->getDatabaseName($patch->getName());
}
}
return $list;
}
public function getConn($fragment, $select_database = true) {
return PhabricatorEnv::newObjectFromConfig(
'mysql.implementation',
array(
array(
'user' => $this->user,
'pass' => $this->password,
'host' => $this->host,
'database' => $select_database
? $this->getDatabaseName($fragment)
: null,
),
));
}
public function getAppliedPatches() {
try {
$applied = queryfx_all(
$this->getConn('meta_data'),
'SELECT patch FROM patch_status');
return ipull($applied, 'patch');
} catch (AphrontQueryException $ex) {
return null;
}
}
public function createDatabase($fragment) {
queryfx(
$this->getConn($fragment, $select_database = false),
'CREATE DATABASE IF NOT EXISTS %T COLLATE utf8_general_ci',
$this->getDatabaseName($fragment));
}
public function createTable($fragment, $table, array $cols) {
queryfx(
$this->getConn($fragment),
'CREATE TABLE IF NOT EXISTS %T.%T (%Q) '.
'ENGINE=InnoDB, COLLATE utf8_general_ci',
$this->getDatabaseName($fragment),
$table,
implode(', ', $cols));
}
public function getLegacyPatches(array $patches) {
assert_instances_of($patches, 'PhabricatorStoragePatch');
try {
$row = queryfx_one(
$this->getConn('meta_data'),
'SELECT version FROM %T',
'schema_version');
$version = $row['version'];
} catch (AphrontQueryException $ex) {
return array();
}
$legacy = array();
foreach ($patches as $key => $patch) {
if ($patch->getLegacy() !== false && $patch->getLegacy() <= $version) {
$legacy[] = $key;
}
}
return $legacy;
}
public function markPatchApplied($patch) {
queryfx(
$this->getConn('meta_data'),
'INSERT INTO %T (patch, applied) VALUES (%s, %d)',
'patch_status',
$patch,
time());
}
public function applyPatch(PhabricatorStoragePatch $patch) {
$type = $patch->getType();
$name = $patch->getName();
switch ($type) {
case 'db':
$this->createDatabase($name);
break;
case 'sql':
$this->applyPatchSQL($name);
break;
case 'php':
$this->applyPatchPHP($name);
break;
default:
throw new Exception("Unable to apply patch of type '{$type}'.");
}
}
public function applyPatchSQL($sql) {
$sql = Filesystem::readFile($sql);
$queries = preg_split('/;\s+/', $sql);
$queries = array_filter($queries);
$conn = $this->getConn('meta_data', $select_database = false);
foreach ($queries as $query) {
$query = str_replace('{$NAMESPACE}', $this->namespace, $query);
queryfx(
$conn,
'%Q',
$query);
}
}
public function applyPatchPHP($script) {
$schema_conn = $this->getConn('meta_data', $select_database = false);
require_once $script;
}
}
diff --git a/src/infrastructure/storage/management/PhabricatorStoragePatch.php b/src/infrastructure/storage/management/PhabricatorStoragePatch.php
index 10972f2e24..014806f872 100644
--- a/src/infrastructure/storage/management/PhabricatorStoragePatch.php
+++ b/src/infrastructure/storage/management/PhabricatorStoragePatch.php
@@ -1,61 +1,45 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorStoragePatch {
private $key;
private $fullKey;
private $name;
private $type;
private $after;
private $legacy;
public function __construct(array $dict) {
$this->key = $dict['key'];
$this->type = $dict['type'];
$this->fullKey = $dict['fullKey'];
$this->legacy = $dict['legacy'];
$this->name = $dict['name'];
$this->after = $dict['after'];
}
public function getLegacy() {
return $this->legacy;
}
public function getAfter() {
return $this->after;
}
public function getType() {
return $this->type;
}
public function getName() {
return $this->name;
}
public function getFullKey() {
return $this->fullKey;
}
public function getKey() {
return $this->key;
}
}
diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDatabasesWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDatabasesWorkflow.php
index deb8708903..3c016cd22b 100644
--- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDatabasesWorkflow.php
+++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDatabasesWorkflow.php
@@ -1,39 +1,23 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorStorageManagementDatabasesWorkflow
extends PhabricatorStorageManagementWorkflow {
public function didConstruct() {
$this
->setName('databases')
->setExamples('**databases** [__options__]')
->setSynopsis('List Phabricator databases.');
}
public function execute(PhutilArgumentParser $args) {
$api = $this->getAPI();
$patches = $this->getPatches();
$databases = $api->getDatabaseList($patches);
echo implode("\n", $databases)."\n";
return 0;
}
}
diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php
index ce1c9c8d80..1732bd5083 100644
--- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php
+++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php
@@ -1,93 +1,77 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorStorageManagementDestroyWorkflow
extends PhabricatorStorageManagementWorkflow {
public function didConstruct() {
$this
->setName('destroy')
->setExamples('**destroy** [__options__]')
->setSynopsis('Permanently destroy all storage and data.')
->setArguments(
array(
array(
'name' => 'unittest-fixtures',
'help' => "Restrict **destroy** operations to databases created ".
"by PhabricatorTestCase test fixtures.",
)));
}
public function execute(PhutilArgumentParser $args) {
$is_dry = $args->getArg('dryrun');
$is_force = $args->getArg('force');
if (!$is_dry && !$is_force) {
echo phutil_console_wrap(
"Are you completely sure you really want to permanently destroy all ".
"storage for Phabricator data? This operation can not be undone and ".
"your data will not be recoverable if you proceed.");
if (!phutil_console_confirm('Permanently destroy all data?')) {
echo "Cancelled.\n";
exit(1);
}
if (!phutil_console_confirm('Really destroy all data forever?')) {
echo "Cancelled.\n";
exit(1);
}
}
$api = $this->getAPI();
$patches = $this->getPatches();
if ($args->getArg('unittest-fixtures')) {
$conn = $api->getConn(null, false);
$databases = queryfx_all(
$conn,
'SELECT DISTINCT(TABLE_SCHEMA) AS db '.
'FROM INFORMATION_SCHEMA.TABLES '.
'WHERE TABLE_SCHEMA LIKE %>',
PhabricatorTestCase::NAMESPACE_PREFIX);
$databases = ipull($databases, 'db');
} else {
$databases = $api->getDatabaseList($patches);
$databases[] = $api->getDatabaseName('meta_data');
}
foreach ($databases as $database) {
if ($is_dry) {
echo "DRYRUN: Would drop database '{$database}'.\n";
} else {
echo "Dropping database '{$database}'...\n";
queryfx(
$api->getConn('meta_data', $select_database = false),
'DROP DATABASE IF EXISTS %T',
$database);
}
}
if (!$is_dry) {
echo "Storage was destroyed.\n";
}
return 0;
}
}
diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php
index ffb0230c0e..464ac862bb 100644
--- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php
+++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php
@@ -1,89 +1,73 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorStorageManagementDumpWorkflow
extends PhabricatorStorageManagementWorkflow {
public function didConstruct() {
$this
->setName('dump')
->setExamples('**dump** [__options__]')
->setSynopsis('Dump all data in storage to stdout.');
}
public function execute(PhutilArgumentParser $args) {
$api = $this->getAPI();
$patches = $this->getPatches();
$applied = $api->getAppliedPatches();
if ($applied === null) {
$namespace = $api->getNamespace();
echo phutil_console_wrap(
phutil_console_format(
"**No Storage**: There is no database storage initialized in this ".
"storage namespace ('{$namespace}'). Use '**storage upgrade**' to ".
"initialize storage.\n"));
return 1;
}
$databases = $api->getDatabaseList($patches);
list($host, $port) = $this->getBareHostAndPort($api->getHost());
$flag_password = '';
$password = $api->getPassword();
if ($password) {
$password = $password->openEnvelope();
if (strlen($password)) {
$flag_password = csprintf('-p%s', $password);
}
}
$flag_port = $port
? csprintf('--port %d', $port)
: '';
return phutil_passthru(
'mysqldump --default-character-set=utf8 '.
'-u %s %C -h %s %C --databases %Ls',
$api->getUser(),
$flag_password,
$host,
$flag_port,
$databases);
}
private function getBareHostAndPort($host) {
// Split out port information, since the command-line client requires a
// separate flag for the port.
$uri = new PhutilURI('mysql://'.$host);
if ($uri->getPort()) {
$port = $uri->getPort();
$bare_hostname = $uri->getDomain();
} else {
$port = null;
$bare_hostname = $host;
}
return array($bare_hostname, $port);
}
}
diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementStatusWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementStatusWorkflow.php
index 8f78789f6a..ee583e8107 100644
--- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementStatusWorkflow.php
+++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementStatusWorkflow.php
@@ -1,67 +1,51 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorStorageManagementStatusWorkflow
extends PhabricatorStorageManagementWorkflow {
public function didConstruct() {
$this
->setName('status')
->setExamples('**status** [__options__]')
->setSynopsis('Show patch application status.');
}
public function execute(PhutilArgumentParser $args) {
$api = $this->getAPI();
$patches = $this->getPatches();
$applied = $api->getAppliedPatches();
if ($applied === null) {
echo phutil_console_format(
"**Database Not Initialized**: Run **storage upgrade** to ".
"initialize.\n");
return 1;
}
$len = 0;
foreach ($patches as $patch) {
$len = max($len, strlen($patch->getFullKey()));
}
foreach ($patches as $patch) {
printf(
"% -".($len + 2)."s ".
"%-".strlen("Not Applied")."s ".
"%-4s ".
"%s\n",
$patch->getFullKey(),
in_array($patch->getFullKey(), $applied)
? 'Applied'
: 'Not Applied',
$patch->getType(),
$patch->getName());
}
return 0;
}
}
diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php
index 50b2990fc6..7c2c25fa15 100644
--- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php
+++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php
@@ -1,209 +1,193 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorStorageManagementUpgradeWorkflow
extends PhabricatorStorageManagementWorkflow {
public function didConstruct() {
$this
->setName('upgrade')
->setExamples('**upgrade** [__options__]')
->setSynopsis("Upgrade database schemata.")
->setArguments(
array(
array(
'name' => 'apply',
'param' => 'patch',
'help' => 'Apply __patch__ explicitly. This is an advanced '.
'feature for development and debugging; you should '.
'not normally use this flag.',
),
array(
'name' => 'no-quickstart',
'help' => 'Build storage patch-by-patch from scatch, even if it '.
'could be loaded from the quickstart template.',
),
array(
'name' => 'init-only',
'help' => 'Initialize storage only; do not apply patches.',
),
));
}
public function execute(PhutilArgumentParser $args) {
$is_dry = $args->getArg('dryrun');
$is_force = $args->getArg('force');
$api = $this->getAPI();
$patches = $this->getPatches();
if (!$is_dry && !$is_force) {
echo phutil_console_wrap(
"Before running storage upgrades, you should take down the ".
"Phabricator web interface and stop any running Phabricator ".
"daemons (you can disable this warning with --force).");
if (!phutil_console_confirm('Are you ready to continue?')) {
echo "Cancelled.\n";
return 1;
}
}
$apply_only = $args->getArg('apply');
if ($apply_only) {
if (empty($patches[$apply_only])) {
throw new PhutilArgumentUsageException(
"--apply argument '{$apply_only}' is not a valid patch. Use ".
"'storage status' to show patch status.");
}
}
$no_quickstart = $args->getArg('no-quickstart');
$init_only = $args->getArg('init-only');
$applied = $api->getAppliedPatches();
if ($applied === null) {
if ($is_dry) {
echo "DRYRUN: Patch metadata storage doesn't exist yet, it would ".
"be created.\n";
return 0;
}
if ($apply_only) {
throw new PhutilArgumentUsageException(
"Storage has not been initialized yet, you must initialize storage ".
"before selectively applying patches.");
return 1;
}
$legacy = $api->getLegacyPatches($patches);
if ($legacy || $no_quickstart || $init_only) {
// If we have legacy patches, we can't quickstart.
$api->createDatabase('meta_data');
$api->createTable(
'meta_data',
'patch_status',
array(
'patch VARCHAR(255) NOT NULL PRIMARY KEY COLLATE utf8_general_ci',
'applied INT UNSIGNED NOT NULL',
));
foreach ($legacy as $patch) {
$api->markPatchApplied($patch);
}
} else {
echo "Loading quickstart template...\n";
$root = dirname(phutil_get_library_root('phabricator'));
$sql = $root.'/resources/sql/quickstart.sql';
$api->applyPatchSQL($sql);
}
}
if ($init_only) {
echo "Storage initialized.\n";
return 0;
}
$applied = $api->getAppliedPatches();
$applied = array_fill_keys($applied, true);
$skip_mark = false;
if ($apply_only) {
if (isset($applied[$apply_only])) {
unset($applied[$apply_only]);
$skip_mark = true;
if (!$is_force && !$is_dry) {
echo phutil_console_wrap(
"Patch '{$apply_only}' has already been applied. Are you sure ".
"you want to apply it again? This may put your storage in a state ".
"that the upgrade scripts can not automatically manage.");
if (!phutil_console_confirm('Apply patch again?')) {
echo "Cancelled.\n";
return 1;
}
}
}
}
while (true) {
$applied_something = false;
foreach ($patches as $key => $patch) {
if (isset($applied[$key])) {
unset($patches[$key]);
continue;
}
if ($apply_only && $apply_only != $key) {
unset($patches[$key]);
continue;
}
$can_apply = true;
foreach ($patch->getAfter() as $after) {
if (empty($applied[$after])) {
if ($apply_only) {
echo "Unable to apply patch '{$apply_only}' because it depends ".
"on patch '{$after}', which has not been applied.\n";
return 1;
}
$can_apply = false;
break;
}
}
if (!$can_apply) {
continue;
}
$applied_something = true;
if ($is_dry) {
echo "DRYRUN: Would apply patch '{$key}'.\n";
} else {
echo "Applying patch '{$key}'...\n";
$api->applyPatch($patch);
if (!$skip_mark) {
$api->markPatchApplied($key);
}
}
unset($patches[$key]);
$applied[$key] = true;
}
if (!$applied_something) {
if (count($patches)) {
throw new Exception(
"Some patches could not be applied: ".
implode(', ', array_keys($patches)));
} else if (!$is_dry && !$apply_only) {
echo "Storage is up to date. Use 'storage status' for details.\n";
}
break;
}
}
return 0;
}
}
diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php
index b07893cbc8..39767de50c 100644
--- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php
+++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php
@@ -1,48 +1,32 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorStorageManagementWorkflow
extends PhutilArgumentWorkflow {
private $patches;
private $api;
public function setPatches(array $patches) {
assert_instances_of($patches, 'PhabricatorStoragePatch');
$this->patches = $patches;
return $this;
}
public function getPatches() {
return $this->patches;
}
final public function setAPI(PhabricatorStorageManagementAPI $api) {
$this->api = $api;
return $this;
}
final public function getAPI() {
return $this->api;
}
public function isExecutable() {
return true;
}
}
diff --git a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
index 11531c8555..625572b3be 100644
--- a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
+++ b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
@@ -1,1034 +1,1018 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList {
public function getNamespace() {
return 'phabricator';
}
private function getPatchPath($file) {
$root = dirname(phutil_get_library_root('phabricator'));
$path = $root.'/resources/sql/patches/'.$file;
// Make sure it exists.
Filesystem::readFile($path);
return $path;
}
public function getPatches() {
return array(
'db.audit' => array(
'type' => 'db',
'name' => 'audit',
'after' => array( /* First Patch */ ),
),
'db.calendar' => array(
'type' => 'db',
'name' => 'calendar',
),
'db.chatlog' => array(
'type' => 'db',
'name' => 'chatlog',
),
'db.conduit' => array(
'type' => 'db',
'name' => 'conduit',
),
'db.countdown' => array(
'type' => 'db',
'name' => 'countdown',
),
'db.daemon' => array(
'type' => 'db',
'name' => 'daemon',
),
'db.differential' => array(
'type' => 'db',
'name' => 'differential',
),
'db.draft' => array(
'type' => 'db',
'name' => 'draft',
),
'db.drydock' => array(
'type' => 'db',
'name' => 'drydock',
),
'db.feed' => array(
'type' => 'db',
'name' => 'feed',
),
'db.file' => array(
'type' => 'db',
'name' => 'file',
),
'db.flag' => array(
'type' => 'db',
'name' => 'flag',
),
'db.harbormaster' => array(
'type' => 'db',
'name' => 'harbormaster',
),
'db.herald' => array(
'type' => 'db',
'name' => 'herald',
),
'db.maniphest' => array(
'type' => 'db',
'name' => 'maniphest',
),
'db.meta_data' => array(
'type' => 'db',
'name' => 'meta_data',
),
'db.metamta' => array(
'type' => 'db',
'name' => 'metamta',
),
'db.oauth_server' => array(
'type' => 'db',
'name' => 'oauth_server',
),
'db.owners' => array(
'type' => 'db',
'name' => 'owners',
),
'db.pastebin' => array(
'type' => 'db',
'name' => 'pastebin',
),
'db.phame' => array(
'type' => 'db',
'name' => 'phame',
),
'db.phriction' => array(
'type' => 'db',
'name' => 'phriction',
),
'db.project' => array(
'type' => 'db',
'name' => 'project',
),
'db.repository' => array(
'type' => 'db',
'name' => 'repository',
),
'db.search' => array(
'type' => 'db',
'name' => 'search',
),
'db.slowvote' => array(
'type' => 'db',
'name' => 'slowvote',
),
'db.timeline' => array(
'type' => 'db',
'name' => 'timeline',
),
'db.user' => array(
'type' => 'db',
'name' => 'user',
),
'db.worker' => array(
'type' => 'db',
'name' => 'worker',
),
'db.xhpastview' => array(
'type' => 'db',
'name' => 'xhpastview',
),
'db.cache' => array(
'type' => 'db',
'name' => 'cache',
),
'db.fact' => array(
'type' => 'db',
'name' => 'fact',
),
'db.ponder' => array(
'type' => 'db',
'name' => 'ponder',
),
'db.xhprof' => array(
'type' => 'db',
'name' => 'xhprof',
),
'0000.legacy.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('0000.legacy.sql'),
'legacy' => 0,
),
'000.project.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('000.project.sql'),
'legacy' => 0,
),
'001.maniphest_projects.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('001.maniphest_projects.sql'),
'legacy' => 1,
),
'002.oauth.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('002.oauth.sql'),
'legacy' => 2,
),
'003.more_oauth.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('003.more_oauth.sql'),
'legacy' => 3,
),
'004.daemonrepos.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('004.daemonrepos.sql'),
'legacy' => 4,
),
'005.workers.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('005.workers.sql'),
'legacy' => 5,
),
'006.repository.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('006.repository.sql'),
'legacy' => 6,
),
'007.daemonlog.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('007.daemonlog.sql'),
'legacy' => 7,
),
'008.repoopt.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('008.repoopt.sql'),
'legacy' => 8,
),
'009.repo_summary.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('009.repo_summary.sql'),
'legacy' => 9,
),
'010.herald.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('010.herald.sql'),
'legacy' => 10,
),
'011.badcommit.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('011.badcommit.sql'),
'legacy' => 11,
),
'012.dropphidtype.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('012.dropphidtype.sql'),
'legacy' => 12,
),
'013.commitdetail.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('013.commitdetail.sql'),
'legacy' => 13,
),
'014.shortcuts.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('014.shortcuts.sql'),
'legacy' => 14,
),
'015.preferences.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('015.preferences.sql'),
'legacy' => 15,
),
'016.userrealnameindex.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('016.userrealnameindex.sql'),
'legacy' => 16,
),
'017.sessionkeys.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('017.sessionkeys.sql'),
'legacy' => 17,
),
'018.owners.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('018.owners.sql'),
'legacy' => 18,
),
'019.arcprojects.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('019.arcprojects.sql'),
'legacy' => 19,
),
'020.pathcapital.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('020.pathcapital.sql'),
'legacy' => 20,
),
'021.xhpastview.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('021.xhpastview.sql'),
'legacy' => 21,
),
'022.differentialcommit.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('022.differentialcommit.sql'),
'legacy' => 22,
),
'023.dxkeys.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('023.dxkeys.sql'),
'legacy' => 23,
),
'024.mlistkeys.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('024.mlistkeys.sql'),
'legacy' => 24,
),
'025.commentopt.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('025.commentopt.sql'),
'legacy' => 25,
),
'026.diffpropkey.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('026.diffpropkey.sql'),
'legacy' => 26,
),
'027.metamtakeys.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('027.metamtakeys.sql'),
'legacy' => 27,
),
'028.systemagent.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('028.systemagent.sql'),
'legacy' => 28,
),
'029.cursors.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('029.cursors.sql'),
'legacy' => 29,
),
'030.imagemacro.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('030.imagemacro.sql'),
'legacy' => 30,
),
'031.workerrace.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('031.workerrace.sql'),
'legacy' => 31,
),
'032.viewtime.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('032.viewtime.sql'),
'legacy' => 32,
),
'033.privtest.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('033.privtest.sql'),
'legacy' => 33,
),
'034.savedheader.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('034.savedheader.sql'),
'legacy' => 34,
),
'035.proxyimage.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('035.proxyimage.sql'),
'legacy' => 35,
),
'036.mailkey.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('036.mailkey.sql'),
'legacy' => 36,
),
'037.setuptest.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('037.setuptest.sql'),
'legacy' => 37,
),
'038.admin.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('038.admin.sql'),
'legacy' => 38,
),
'039.userlog.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('039.userlog.sql'),
'legacy' => 39,
),
'040.transform.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('040.transform.sql'),
'legacy' => 40,
),
'041.heraldrepetition.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('041.heraldrepetition.sql'),
'legacy' => 41,
),
'042.commentmetadata.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('042.commentmetadata.sql'),
'legacy' => 42,
),
'043.pastebin.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('043.pastebin.sql'),
'legacy' => 43,
),
'044.countdown.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('044.countdown.sql'),
'legacy' => 44,
),
'045.timezone.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('045.timezone.sql'),
'legacy' => 45,
),
'046.conduittoken.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('046.conduittoken.sql'),
'legacy' => 46,
),
'047.projectstatus.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('047.projectstatus.sql'),
'legacy' => 47,
),
'048.relationshipkeys.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('048.relationshipkeys.sql'),
'legacy' => 48,
),
'049.projectowner.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('049.projectowner.sql'),
'legacy' => 49,
),
'050.taskdenormal.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('050.taskdenormal.sql'),
'legacy' => 50,
),
'051.projectfilter.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('051.projectfilter.sql'),
'legacy' => 51,
),
'052.pastelanguage.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('052.pastelanguage.sql'),
'legacy' => 52,
),
'053.feed.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('053.feed.sql'),
'legacy' => 53,
),
'054.subscribers.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('054.subscribers.sql'),
'legacy' => 54,
),
'055.add_author_to_files.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('055.add_author_to_files.sql'),
'legacy' => 55,
),
'056.slowvote.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('056.slowvote.sql'),
'legacy' => 56,
),
'057.parsecache.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('057.parsecache.sql'),
'legacy' => 57,
),
'058.missingkeys.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('058.missingkeys.sql'),
'legacy' => 58,
),
'059.engines.php' => array(
'type' => 'php',
'name' => $this->getPatchPath('059.engines.php'),
'legacy' => 59,
),
'060.phriction.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('060.phriction.sql'),
'legacy' => 60,
),
'061.phrictioncontent.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('061.phrictioncontent.sql'),
'legacy' => 61,
),
'062.phrictionmenu.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('062.phrictionmenu.sql'),
'legacy' => 62,
),
'063.pasteforks.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('063.pasteforks.sql'),
'legacy' => 63,
),
'064.subprojects.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('064.subprojects.sql'),
'legacy' => 64,
),
'065.sshkeys.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('065.sshkeys.sql'),
'legacy' => 65,
),
'066.phrictioncontent.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('066.phrictioncontent.sql'),
'legacy' => 66,
),
'067.preferences.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('067.preferences.sql'),
'legacy' => 67,
),
'068.maniphestauxiliarystorage.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('068.maniphestauxiliarystorage.sql'),
'legacy' => 68,
),
'069.heraldxscript.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('069.heraldxscript.sql'),
'legacy' => 69,
),
'070.differentialaux.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('070.differentialaux.sql'),
'legacy' => 70,
),
'071.contentsource.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('071.contentsource.sql'),
'legacy' => 71,
),
'072.blamerevert.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('072.blamerevert.sql'),
'legacy' => 72,
),
'073.reposymbols.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('073.reposymbols.sql'),
'legacy' => 73,
),
'074.affectedpath.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('074.affectedpath.sql'),
'legacy' => 74,
),
'075.revisionhash.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('075.revisionhash.sql'),
'legacy' => 75,
),
'076.indexedlanguages.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('076.indexedlanguages.sql'),
'legacy' => 76,
),
'077.originalemail.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('077.originalemail.sql'),
'legacy' => 77,
),
'078.nametoken.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('078.nametoken.sql'),
'legacy' => 78,
),
'079.nametokenindex.php' => array(
'type' => 'php',
'name' => $this->getPatchPath('079.nametokenindex.php'),
'legacy' => 79,
),
'080.filekeys.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('080.filekeys.sql'),
'legacy' => 80,
),
'081.filekeys.php' => array(
'type' => 'php',
'name' => $this->getPatchPath('081.filekeys.php'),
'legacy' => 81,
),
'082.xactionkey.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('082.xactionkey.sql'),
'legacy' => 82,
),
'083.dxviewtime.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('083.dxviewtime.sql'),
'legacy' => 83,
),
'084.pasteauthorkey.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('084.pasteauthorkey.sql'),
'legacy' => 84,
),
'085.packagecommitrelationship.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('085.packagecommitrelationship.sql'),
'legacy' => 85,
),
'086.formeraffil.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('086.formeraffil.sql'),
'legacy' => 86,
),
'087.phrictiondelete.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('087.phrictiondelete.sql'),
'legacy' => 87,
),
'088.audit.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('088.audit.sql'),
'legacy' => 88,
),
'089.projectwiki.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('089.projectwiki.sql'),
'legacy' => 89,
),
'090.forceuniqueprojectnames.php' => array(
'type' => 'php',
'name' => $this->getPatchPath('090.forceuniqueprojectnames.php'),
'legacy' => 90,
),
'091.uniqueslugkey.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('091.uniqueslugkey.sql'),
'legacy' => 91,
),
'092.dropgithubnotification.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('092.dropgithubnotification.sql'),
'legacy' => 92,
),
'093.gitremotes.php' => array(
'type' => 'php',
'name' => $this->getPatchPath('093.gitremotes.php'),
'legacy' => 93,
),
'094.phrictioncolumn.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('094.phrictioncolumn.sql'),
'legacy' => 94,
),
'095.directory.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('095.directory.sql'),
'legacy' => 95,
),
'096.filename.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('096.filename.sql'),
'legacy' => 96,
),
'097.heraldruletypes.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('097.heraldruletypes.sql'),
'legacy' => 97,
),
'098.heraldruletypemigration.php' => array(
'type' => 'php',
'name' => $this->getPatchPath('098.heraldruletypemigration.php'),
'legacy' => 98,
),
'099.drydock.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('099.drydock.sql'),
'legacy' => 99,
),
'100.projectxaction.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('100.projectxaction.sql'),
'legacy' => 100,
),
'101.heraldruleapplied.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('101.heraldruleapplied.sql'),
'legacy' => 101,
),
'102.heraldcleanup.php' => array(
'type' => 'php',
'name' => $this->getPatchPath('102.heraldcleanup.php'),
'legacy' => 102,
),
'103.heraldedithistory.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('103.heraldedithistory.sql'),
'legacy' => 103,
),
'104.searchkey.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('104.searchkey.sql'),
'legacy' => 104,
),
'105.mimetype.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('105.mimetype.sql'),
'legacy' => 105,
),
'106.chatlog.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('106.chatlog.sql'),
'legacy' => 106,
),
'107.oauthserver.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('107.oauthserver.sql'),
'legacy' => 107,
),
'108.oauthscope.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('108.oauthscope.sql'),
'legacy' => 108,
),
'109.oauthclientphidkey.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('109.oauthclientphidkey.sql'),
'legacy' => 109,
),
'110.commitaudit.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('110.commitaudit.sql'),
'legacy' => 110,
),
'111.commitauditmigration.php' => array(
'type' => 'php',
'name' => $this->getPatchPath('111.commitauditmigration.php'),
'legacy' => 111,
),
'112.oauthaccesscoderedirecturi.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('112.oauthaccesscoderedirecturi.sql'),
'legacy' => 112,
),
'113.lastreviewer.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('113.lastreviewer.sql'),
'legacy' => 113,
),
'114.auditrequest.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('114.auditrequest.sql'),
'legacy' => 114,
),
'115.prepareutf8.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('115.prepareutf8.sql'),
'legacy' => 115,
),
'116.utf8-backup-first-expect-wait.sql' => array(
'type' => 'sql',
'name' =>
$this->getPatchPath('116.utf8-backup-first-expect-wait.sql'),
'legacy' => 116,
),
'117.repositorydescription.php' => array(
'type' => 'php',
'name' => $this->getPatchPath('117.repositorydescription.php'),
'legacy' => 117,
),
'118.auditinline.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('118.auditinline.sql'),
'legacy' => 118,
),
'119.filehash.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('119.filehash.sql'),
'legacy' => 119,
),
'120.noop.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('120.noop.sql'),
'legacy' => 120,
),
'121.drydocklog.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('121.drydocklog.sql'),
'legacy' => 121,
),
'122.flag.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('122.flag.sql'),
'legacy' => 122,
),
'123.heraldrulelog.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('123.heraldrulelog.sql'),
'legacy' => 123,
),
'124.subpriority.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('124.subpriority.sql'),
'legacy' => 124,
),
'125.ipv6.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('125.ipv6.sql'),
'legacy' => 125,
),
'126.edges.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('126.edges.sql'),
'legacy' => 126,
),
'127.userkeybody.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('127.userkeybody.sql'),
'legacy' => 127,
),
'128.phabricatorcom.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('128.phabricatorcom.sql'),
'legacy' => 128,
),
'129.savedquery.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('129.savedquery.sql'),
'legacy' => 129,
),
'130.denormalrevisionquery.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('130.denormalrevisionquery.sql'),
'legacy' => 130,
),
'131.migraterevisionquery.php' => array(
'type' => 'php',
'name' => $this->getPatchPath('131.migraterevisionquery.php'),
'legacy' => 131,
),
'132.phame.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('132.phame.sql'),
'legacy' => 132,
),
'133.imagemacro.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('133.imagemacro.sql'),
'legacy' => 133,
),
'134.emptysearch.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('134.emptysearch.sql'),
'legacy' => 134,
),
'135.datecommitted.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('135.datecommitted.sql'),
'legacy' => 135,
),
'136.sex.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('136.sex.sql'),
'legacy' => 136,
),
'137.auditmetadata.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('137.auditmetadata.sql'),
'legacy' => 137,
),
'138.notification.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('138.notification.sql'),
),
'holidays.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('holidays.sql'),
),
'userstatus.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('userstatus.sql'),
),
'emailtable.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('emailtable.sql'),
),
'emailtableport.sql' => array(
'type' => 'php',
'name' => $this->getPatchPath('emailtableport.php'),
),
'emailtableremove.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('emailtableremove.sql'),
),
'phiddrop.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('phiddrop.sql'),
),
'testdatabase.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('testdatabase.sql'),
),
'ldapinfo.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('ldapinfo.sql'),
),
'threadtopic.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('threadtopic.sql'),
),
'usertranslation.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('usertranslation.sql'),
),
'differentialbookmarks.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('differentialbookmarks.sql'),
),
'harbormasterobject.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('harbormasterobject.sql'),
),
'markupcache.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('markupcache.sql'),
),
'maniphestxcache.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('maniphestxcache.sql'),
),
'migrate-maniphest-dependencies.php' => array(
'type' => 'php',
'name' => $this->getPatchPath('migrate-maniphest-dependencies.php'),
),
'migrate-differential-dependencies.php' => array(
'type' => 'php',
'name' => $this->getPatchPath(
'migrate-differential-dependencies.php'),
),
'phameblog.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('phameblog.sql'),
),
'migrate-maniphest-revisions.php' => array(
'type' => 'php',
'name' => $this->getPatchPath('migrate-maniphest-revisions.php'),
),
'daemonstatus.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('daemonstatus.sql'),
),
'symbolcontexts.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('symbolcontexts.sql'),
),
'migrate-project-edges.php' => array(
'type' => 'php',
'name' => $this->getPatchPath('migrate-project-edges.php'),
),
'fact-raw.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('fact-raw.sql'),
),
'ponder.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('ponder.sql')
),
'policy-project.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('policy-project.sql'),
),
'daemonstatuskey.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('daemonstatuskey.sql'),
),
'edgetype.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('edgetype.sql'),
),
'ponder-comments.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('ponder-comments.sql'),
),
'pastepolicy.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('pastepolicy.sql'),
),
'xhprof.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('xhprof.sql'),
),
'draft-metadata.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('draft-metadata.sql'),
),
'phamedomain.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('phamedomain.sql'),
),
'ponder-mailkey.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('ponder-mailkey.sql'),
),
'ponder-mailkey-populate.php' => array(
'type' => 'php',
'name' => $this->getPatchPath('ponder-mailkey-populate.php'),
),
'phamepolicy.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('phamepolicy.sql'),
),
'phameoneblog.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('phameoneblog.sql'),
),
'statustxt.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('statustxt.sql'),
),
'daemontaskarchive.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('daemontaskarchive.sql'),
),
'drydocktaskid.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('drydocktaskid.sql'),
),
'drydockresoucetype.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('drydockresourcetype.sql'),
),
);
}
}
diff --git a/src/infrastructure/storage/patch/PhabricatorSQLPatchList.php b/src/infrastructure/storage/patch/PhabricatorSQLPatchList.php
index 224dfcc187..eddee97b45 100644
--- a/src/infrastructure/storage/patch/PhabricatorSQLPatchList.php
+++ b/src/infrastructure/storage/patch/PhabricatorSQLPatchList.php
@@ -1,164 +1,148 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorSQLPatchList {
abstract function getNamespace();
abstract function getPatches();
final public static function buildAllPatches() {
$patch_lists = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorSQLPatchList')
->setConcreteOnly(true)
->selectAndLoadSymbols();
$specs = array();
$seen_namespaces = array();
foreach ($patch_lists as $patch_class) {
$patch_class = $patch_class['name'];
$patch_list = newv($patch_class, array());
$namespace = $patch_list->getNamespace();
if (isset($seen_namespaces[$namespace])) {
$prior = $seen_namespaces[$namespace];
throw new Exception(
"PatchList '{$patch_class}' has the same namespace, '{$namespace}', ".
"as another patch list class, '{$prior}'. Each patch list MUST have ".
"a unique namespace.");
}
$last_key = null;
foreach ($patch_list->getPatches() as $key => $patch) {
if (!is_array($patch)) {
throw new Exception(
"PatchList '{$patch_class}' has a patch '{$key}' which is not ".
"an array.");
}
$valid = array(
'type' => true,
'name' => true,
'after' => true,
'legacy' => true,
);
foreach ($patch as $pkey => $pval) {
if (empty($valid[$pkey])) {
throw new Exception(
"PatchList '{$patch_class}' has a patch, '{$key}', with an ".
"unknown property, '{$pkey}'. Patches must have only valid ".
"keys: ".implode(', ', array_keys($valid)).'.');
}
}
if (is_numeric($key)) {
throw new Exception(
"PatchList '{$patch_class}' has a patch with a numeric key, ".
"'{$key}'. Patches must use string keys.");
}
if (strpos($key, ':') !== false) {
throw new Exception(
"PatchList '{$patch_class}' has a patch with a colon in the ".
"key name, '{$key}'. Patch keys may not contain colons.");
}
$full_key = "{$namespace}:{$key}";
if (isset($specs[$full_key])) {
throw new Exception(
"PatchList '{$patch_class}' has a patch '{$key}' which ".
"duplicates an existing patch key.");
}
$patch['key'] = $key;
$patch['fullKey'] = $full_key;
if (isset($patch['legacy'])) {
if ($namespace != 'phabricator') {
throw new Exception(
"Only patches in the 'phabricator' namespace may contain ".
"'legacy' keys.");
}
} else {
$patch['legacy'] = false;
}
if (!array_key_exists('after', $patch)) {
if ($last_key === null) {
throw new Exception(
"Patch '{$full_key}' is missing key 'after', and is the first ".
"patch in the patch list '{$patch_class}', so its application ".
"order can not be determined implicitly. The first patch in a ".
"patch list must list the patch or patches it depends on ".
"explicitly.");
} else {
$patch['after'] = array($last_key);
}
}
$last_key = $full_key;
foreach ($patch['after'] as $after_key => $after) {
if (strpos($after, ':') === false) {
$patch['after'][$after_key] = $namespace.':'.$after;
}
}
$type = idx($patch, 'type');
if (!$type) {
throw new Exception(
"Patch '{$namespace}:{$key}' is missing key 'type'. Every patch ".
"must have a type.");
}
switch ($type) {
case 'db':
case 'sql':
case 'php':
break;
default:
throw new Exception(
"Patch '{$namespace}:{$key}' has unknown patch type '{$type}'.");
}
$specs[$full_key] = $patch;
}
}
foreach ($specs as $key => $patch) {
foreach ($patch['after'] as $after) {
if (empty($specs[$after])) {
throw new Exception(
"Patch '{$key}' references nonexistent dependency, '{$after}'. ".
"Patches may only depend on patches which actually exist.");
}
}
}
$patches = array();
foreach ($specs as $full_key => $spec) {
$patches[$full_key] = new PhabricatorStoragePatch($spec);
}
// TODO: Detect cycles?
return $patches;
}
}
diff --git a/src/infrastructure/testing/PhabricatorTestCase.php b/src/infrastructure/testing/PhabricatorTestCase.php
index 8d2582d9a9..660fd17670 100644
--- a/src/infrastructure/testing/PhabricatorTestCase.php
+++ b/src/infrastructure/testing/PhabricatorTestCase.php
@@ -1,179 +1,163 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class PhabricatorTestCase extends ArcanistPhutilTestCase {
const NAMESPACE_PREFIX = 'phabricator_unittest_';
/**
* If true, put Lisk in process-isolated mode for the duration of the tests so
* that it will establish only isolated, side-effect-free database
* connections. Defaults to true.
*
* NOTE: You should disable this only in rare circumstances. Unit tests should
* not rely on external resources like databases, and should not produce
* side effects.
*/
const PHABRICATOR_TESTCONFIG_ISOLATE_LISK = 'isolate-lisk';
/**
* If true, build storage fixtures before running tests, and connect to them
* during test execution. This will impose a performance penalty on test
* execution (currently, it takes roughly one second to build the fixture)
* but allows you to perform tests which require data to be read from storage
* after writes. The fixture is shared across all test cases in this process.
* Defaults to false.
*
* NOTE: All connections to fixture storage open transactions when established
* and roll them back when tests complete. Each test must independently
* write data it relies on; data will not persist across tests.
*
* NOTE: Enabling this implies disabling process isolation.
*/
const PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES = 'storage-fixtures';
private $configuration;
private $env;
private static $storageFixtureReferences = 0;
private static $storageFixture;
private static $storageFixtureObjectSeed = 0;
protected function getPhabricatorTestCaseConfiguration() {
return array();
}
private function getComputedConfiguration() {
$config = $this->getPhabricatorTestCaseConfiguration() + array(
self::PHABRICATOR_TESTCONFIG_ISOLATE_LISK => true,
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => false,
);
if ($config[self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES]) {
// Fixtures don't make sense with process isolation.
$config[self::PHABRICATOR_TESTCONFIG_ISOLATE_LISK] = false;
}
return $config;
}
protected function willRunTests() {
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/scripts/__init_script__.php';
$config = $this->getComputedConfiguration();
if ($config[self::PHABRICATOR_TESTCONFIG_ISOLATE_LISK]) {
LiskDAO::beginIsolateAllLiskEffectsToCurrentProcess();
}
if ($config[self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES]) {
++self::$storageFixtureReferences;
if (!self::$storageFixture) {
self::$storageFixture = $this->newStorageFixture();
}
}
$this->env = PhabricatorEnv::beginScopedEnv();
}
protected function didRunTests() {
$config = $this->getComputedConfiguration();
if ($config[self::PHABRICATOR_TESTCONFIG_ISOLATE_LISK]) {
LiskDAO::endIsolateAllLiskEffectsToCurrentProcess();
}
if (self::$storageFixture) {
self::$storageFixtureReferences--;
if (!self::$storageFixtureReferences) {
self::$storageFixture = null;
}
}
try {
unset($this->env);
} catch (Exception $ex) {
throw new Exception(
"Some test called PhabricatorEnv::beginScopedEnv(), but is still ".
"holding a reference to the scoped environment!");
}
}
protected function willRunOneTest($test) {
$config = $this->getComputedConfiguration();
if ($config[self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES]) {
LiskDAO::beginIsolateAllLiskEffectsToTransactions();
}
}
protected function didRunOneTest($test) {
$config = $this->getComputedConfiguration();
if ($config[self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES]) {
LiskDAO::endIsolateAllLiskEffectsToTransactions();
}
}
protected function newStorageFixture() {
$bytes = Filesystem::readRandomCharacters(24);
$name = self::NAMESPACE_PREFIX.$bytes;
return new PhabricatorStorageFixtureScopeGuard($name);
}
protected function getLink($method) {
$phabricator_project = 'PHID-APRJ-3f1fc779edeab89b2171';
return
'https://secure.phabricator.com/diffusion/symbol/'.$method.
'/?lang=php&projects='.$phabricator_project.
'&jump=true&context='.get_class($this);
}
/**
* Returns an integer seed to use when building unique identifiers (e.g.,
* non-colliding usernames). The seed is unstable and its value will change
* between test runs, so your tests must not rely on it.
*
* @return int A unique integer.
*/
protected function getNextObjectSeed() {
self::$storageFixtureObjectSeed += mt_rand(1, 100);
return self::$storageFixtureObjectSeed;
}
protected function generateNewTestUser() {
$seed = $this->getNextObjectSeed();
$user = id(new PhabricatorUser())
->setRealName("Test User {$seed}}")
->setUserName("test{$seed}");
$email = id(new PhabricatorUserEmail())
->setAddress("testuser{$seed}@example.com")
->setIsVerified(1);
$editor = new PhabricatorUserEditor();
$editor->setActor($user);
$editor->createNewUser($user, $email);
return $user;
}
}
diff --git a/src/infrastructure/testing/__tests__/PhabricatorTrivialTestCase.php b/src/infrastructure/testing/__tests__/PhabricatorTrivialTestCase.php
index 20b097d99e..a607913ad6 100644
--- a/src/infrastructure/testing/__tests__/PhabricatorTrivialTestCase.php
+++ b/src/infrastructure/testing/__tests__/PhabricatorTrivialTestCase.php
@@ -1,38 +1,22 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Trivial example test case.
*/
final class PhabricatorTrivialTestCase extends PhabricatorTestCase {
// NOTE: Update developer/unit_tests.diviner when updating this class!
private $two;
public function willRunOneTest($test_name) {
// You can execute setup steps which will run before each test in this
// method.
$this->two = 2;
}
public function testAllIsRightWithTheWorld() {
$this->assertEqual(4, $this->two + $this->two, '2 + 2 = 4');
}
}
diff --git a/src/infrastructure/testing/fixture/PhabricatorStorageFixtureScopeGuard.php b/src/infrastructure/testing/fixture/PhabricatorStorageFixtureScopeGuard.php
index 6835a0b451..8edd4ade01 100644
--- a/src/infrastructure/testing/fixture/PhabricatorStorageFixtureScopeGuard.php
+++ b/src/infrastructure/testing/fixture/PhabricatorStorageFixtureScopeGuard.php
@@ -1,54 +1,38 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Used by unit tests to build storage fixtures.
*/
final class PhabricatorStorageFixtureScopeGuard {
private $name;
public function __construct($name) {
$this->name = $name;
execx(
'php %s upgrade --force --namespace %s',
$this->getStorageBinPath(),
$this->name);
PhabricatorLiskDAO::pushStorageNamespace($name);
// Destructor is not called with fatal error.
register_shutdown_function(array($this, 'destroy'));
}
public function destroy() {
PhabricatorLiskDAO::popStorageNamespace();
execx(
'php %s destroy --force --namespace %s',
$this->getStorageBinPath(),
$this->name);
}
private function getStorageBinPath() {
$root = dirname(phutil_get_library_root('phabricator'));
return $root.'/scripts/sql/manage_storage.php';
}
}
diff --git a/src/infrastructure/util/PhabricatorGlobalLock.php b/src/infrastructure/util/PhabricatorGlobalLock.php
index e05158468b..54e3c2d1d8 100644
--- a/src/infrastructure/util/PhabricatorGlobalLock.php
+++ b/src/infrastructure/util/PhabricatorGlobalLock.php
@@ -1,126 +1,110 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Global, MySQL-backed lock. This is a high-reliability, low-performance
* global lock.
*
* The lock is maintained by using GET_LOCK() in MySQL, and automatically
* released when the connection terminates. Thus, this lock can safely be used
* to control access to shared resources without implementing any sort of
* timeout or override logic: the lock can't normally be stuck in a locked state
* with no process actually holding the lock.
*
* However, acquiring the lock is moderately expensive (several network
* roundtrips). This makes it unsuitable for tasks where lock performance is
* important.
*
* $lock = PhabricatorGlobalLock::newLock('example');
* $lock->lock();
* do_contentious_things();
* $lock->unlock();
*
* NOTE: This lock is not completely global; it is namespaced to the active
* storage namespace so that unit tests running in separate table namespaces
* are isolated from one another.
*
* @task construct Constructing Locks
* @task impl Implementation
*/
final class PhabricatorGlobalLock extends PhutilLock {
private $lockname;
private $conn;
private static $pool = array();
/* -( Constructing Locks )------------------------------------------------- */
public static function newLock($name) {
$namespace = PhabricatorLiskDAO::getStorageNamespace();
$full_name = 'global:'.$namespace.':'.$name;
$lock = self::getLock($full_name);
if (!$lock) {
$lock = new PhabricatorGlobalLock($full_name);
$lock->lockname = $name;
self::registerLock($lock);
}
return $lock;
}
/* -( Implementation )----------------------------------------------------- */
protected function doLock($wait) {
$conn = $this->conn;
if (!$conn) {
// Try to reuse a connection from the connection pool.
$conn = array_pop(self::$pool);
}
if (!$conn) {
// NOTE: Using the 'repository' database somewhat arbitrarily, mostly
// because the first client of locks is the repository daemons. We must
// always use the same database for all locks, but don't access any
// tables so we could use any valid database. We could build a
// database-free connection instead, but that's kind of messy and we
// might forget about it in the future if we vertically partition the
// application.
$dao = new PhabricatorRepository();
// NOTE: Using "force_new" to make sure each lock is on its own
// connection.
$conn = $dao->establishConnection('w', $force_new = true);
// NOTE: Since MySQL will disconnect us if we're idle for too long, we set
// the wait_timeout to an enormous value, to allow us to hold the
// connection open indefinitely (or, at least, for a year).
queryfx($conn, 'SET wait_timeout = %d', 365 * 24 * 60 * 60);
}
$result = queryfx_one(
$conn,
'SELECT GET_LOCK(%s, %f)',
'phabricator:'.$this->lockname,
$wait);
$ok = head($result);
if (!$ok) {
throw new PhutilLockException($this->getName());
}
$this->conn = $conn;
}
protected function doUnlock() {
queryfx(
$this->conn,
'SELECT RELEASE_LOCK(%s)',
'phabricator:'.$this->lockname);
$this->conn->close();
self::$pool[] = $this->conn;
$this->conn = null;
}
}
diff --git a/src/infrastructure/util/PhabricatorHash.php b/src/infrastructure/util/PhabricatorHash.php
index 9a54d29c10..f592c6939b 100644
--- a/src/infrastructure/util/PhabricatorHash.php
+++ b/src/infrastructure/util/PhabricatorHash.php
@@ -1,31 +1,15 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorHash {
public static function digest($string) {
$key = PhabricatorEnv::getEnvConfig('security.hmac-key');
if (!$key) {
throw new Exception(
"Set a 'security.hmac-key' in your Phabricator configuration!");
}
return hash_hmac('sha1', $string, $key);
}
}
diff --git a/src/infrastructure/util/PhabricatorSlug.php b/src/infrastructure/util/PhabricatorSlug.php
index 9ed0938898..0a95579218 100644
--- a/src/infrastructure/util/PhabricatorSlug.php
+++ b/src/infrastructure/util/PhabricatorSlug.php
@@ -1,77 +1,61 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorSlug {
public static function normalize($slug) {
// TODO: We need to deal with unicode at some point, this is just a very
// basic proof-of-concept implementation.
$slug = strtolower($slug);
$slug = preg_replace('@/+@', '/', $slug);
$slug = trim($slug, '/');
$slug = preg_replace('@[^a-z0-9/]+@', '_', $slug);
$slug = trim($slug, '_');
return $slug.'/';
}
public static function getDefaultTitle($slug) {
$parts = explode('/', trim($slug, '/'));
$default_title = end($parts);
$default_title = str_replace('_', ' ', $default_title);
$default_title = ucwords($default_title);
$default_title = nonempty($default_title, 'Untitled Document');
return $default_title;
}
public static function getAncestry($slug) {
$slug = self::normalize($slug);
if ($slug == '/') {
return array();
}
$ancestors = array(
'/',
);
$slug = explode('/', $slug);
array_pop($slug);
array_pop($slug);
$accumulate = '';
foreach ($slug as $part) {
$accumulate .= $part.'/';
$ancestors[] = $accumulate;
}
return $ancestors;
}
public static function getDepth($slug) {
$slug = self::normalize($slug);
if ($slug == '/') {
return 0;
} else {
return substr_count($slug, '/');
}
}
}
diff --git a/src/infrastructure/util/__tests__/PhabricatorSlugTestCase.php b/src/infrastructure/util/__tests__/PhabricatorSlugTestCase.php
index 79f388d5ca..735727ac01 100644
--- a/src/infrastructure/util/__tests__/PhabricatorSlugTestCase.php
+++ b/src/infrastructure/util/__tests__/PhabricatorSlugTestCase.php
@@ -1,74 +1,58 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorSlugTestCase extends PhabricatorTestCase {
public function testSlugNormalization() {
$slugs = array(
'' => '/',
'/' => '/',
'//' => '/',
'&&&' => '/',
'/derp/' => 'derp/',
'derp' => 'derp/',
'derp//derp' => 'derp/derp/',
'DERP//DERP' => 'derp/derp/',
'a B c' => 'a_b_c/',
'-1~2.3abcd' => '1_2_3abcd/',
"T\x95O\x95D\x95O" => 't_o_d_o/',
);
foreach ($slugs as $slug => $normal) {
$this->assertEqual(
$normal,
PhabricatorSlug::normalize($slug),
"Normalization of '{$slug}'");
}
}
public function testSlugAncestry() {
$slugs = array(
'/' => array(),
'pokemon/' => array('/'),
'pokemon/squirtle/' => array('/', 'pokemon/'),
);
foreach ($slugs as $slug => $ancestry) {
$this->assertEqual(
$ancestry,
PhabricatorSlug::getAncestry($slug),
"Ancestry of '{$slug}'");
}
}
public function testSlugDepth() {
$slugs = array(
'/' => 0,
'a/' => 1,
'a/b/' => 2,
'a////b/' => 2,
);
foreach ($slugs as $slug => $depth) {
$this->assertEqual(
$depth,
PhabricatorSlug::getDepth($slug),
"Depth of '{$slug}'");
}
}
}
diff --git a/src/view/AphrontDialogView.php b/src/view/AphrontDialogView.php
index 4fed1fe710..77653d4172 100644
--- a/src/view/AphrontDialogView.php
+++ b/src/view/AphrontDialogView.php
@@ -1,204 +1,188 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontDialogView extends AphrontView {
private $title;
private $submitButton;
private $cancelURI;
private $cancelText = 'Cancel';
private $submitURI;
private $user;
private $hidden = array();
private $class;
private $renderAsForm = true;
private $formID;
private $width = 'default';
const WIDTH_DEFAULT = 'default';
const WIDTH_FORM = 'form';
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setSubmitURI($uri) {
$this->submitURI = $uri;
return $this;
}
public function setTitle($title) {
$this->title = $title;
return $this;
}
public function getTitle() {
return $this->title;
}
public function addSubmitButton($text = 'Okay') {
$this->submitButton = $text;
return $this;
}
public function addCancelButton($uri, $text = 'Cancel') {
$this->cancelURI = $uri;
$this->cancelText = $text;
return $this;
}
public function addHiddenInput($key, $value) {
if (is_array($value)) {
foreach ($value as $hidden_key => $hidden_value) {
$this->hidden[] = array($key.'['.$hidden_key.']', $hidden_value);
}
} else {
$this->hidden[] = array($key, $value);
}
return $this;
}
public function setClass($class) {
$this->class = $class;
return $this;
}
public function setRenderDialogAsDiv() {
// TODO: This API is awkward.
$this->renderAsForm = false;
return $this;
}
public function setFormID($id) {
$this->formID = $id;
return $this;
}
public function setWidth($width) {
$this->width = $width;
return $this;
}
final public function render() {
require_celerity_resource('aphront-dialog-view-css');
$buttons = array();
if ($this->submitButton) {
$buttons[] = javelin_render_tag(
'button',
array(
'name' => '__submit__',
'sigil' => '__default__',
),
phutil_escape_html($this->submitButton));
}
if ($this->cancelURI) {
$buttons[] = javelin_render_tag(
'a',
array(
'href' => $this->cancelURI,
'class' => 'button grey',
'name' => '__cancel__',
'sigil' => 'jx-workflow-button',
),
phutil_escape_html($this->cancelText));
}
$buttons = implode('', $buttons);
if (!$this->user) {
throw new Exception(
"You must call setUser() when rendering an AphrontDialogView.");
}
$more = $this->class;
switch ($this->width) {
case self::WIDTH_FORM:
$more .= ' aphront-dialog-view-width-'.$this->width;
break;
case self::WIDTH_DEFAULT:
break;
default:
throw new Exception("Unknown dialog width '{$this->width}'!");
}
$attributes = array(
'class' => 'aphront-dialog-view '.$more,
'sigil' => 'jx-dialog',
);
$form_attributes = array(
'action' => $this->submitURI,
'method' => 'post',
'id' => $this->formID,
);
$hidden_inputs = array();
foreach ($this->hidden as $desc) {
list($key, $value) = $desc;
$hidden_inputs[] = javelin_render_tag(
'input',
array(
'type' => 'hidden',
'name' => $key,
'value' => $value,
'sigil' => 'aphront-dialog-application-input'
));
}
$hidden_inputs = implode("\n", $hidden_inputs);
$hidden_inputs =
'<input type="hidden" name="__dialog__" value="1" />'.
$hidden_inputs;
if (!$this->renderAsForm) {
$buttons = phabricator_render_form(
$this->user,
$form_attributes,
$hidden_inputs.$buttons);
}
$content =
'<div class="aphront-dialog-head">'.
phutil_escape_html($this->title).
'</div>'.
'<div class="aphront-dialog-body">'.
$this->renderChildren().
'</div>'.
'<div class="aphront-dialog-tail">'.
$buttons.
'<div style="clear: both;"></div>'.
'</div>';
if ($this->renderAsForm) {
return phabricator_render_form(
$this->user,
$form_attributes + $attributes,
$hidden_inputs.
$content);
} else {
return javelin_render_tag(
'div',
$attributes,
$content);
}
}
}
diff --git a/src/view/AphrontJavelinView.php b/src/view/AphrontJavelinView.php
index ea17718ee6..eb71b3100a 100644
--- a/src/view/AphrontJavelinView.php
+++ b/src/view/AphrontJavelinView.php
@@ -1,87 +1,71 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontJavelinView extends AphrontView {
private static $renderContext = array();
private static function peekRenderContext() {
return nonempty(end(self::$renderContext), null);
}
private static function popRenderContext() {
return array_pop(self::$renderContext);
}
private static function pushRenderContext($token) {
self::$renderContext[] = $token;
}
private $name;
private $parameters;
private $celerityResource;
public function render() {
$id = celerity_generate_unique_node_id();
$placeholder = "<span id={$id} />";
require_celerity_resource($this->getCelerityResource());
$render_context = self::peekRenderContext();
self::pushRenderContext($id);
Javelin::initBehavior('view-placeholder', array(
'id' => $id,
'view' => $this->getName(),
'params' => $this->getParameters(),
'children' => $this->renderChildren(),
'trigger_id' => $render_context,
));
self::popRenderContext();
return $placeholder;
}
protected function getName() {
return $this->name;
}
final public function setName($template_name) {
$this->name = $template_name;
return $this;
}
protected function getParameters() {
return $this->parameters;
}
final public function setParameters($template_parameters) {
$this->parameters = $template_parameters;
return $this;
}
protected function getCelerityResource() {
return $this->celerityResource;
}
final public function setCelerityResource($celerity_resource) {
$this->celerityResource = $celerity_resource;
return $this;
}
}
diff --git a/src/view/AphrontNullView.php b/src/view/AphrontNullView.php
index da10e465d9..cfcf48350e 100644
--- a/src/view/AphrontNullView.php
+++ b/src/view/AphrontNullView.php
@@ -1,25 +1,9 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontNullView extends AphrontView {
public function render() {
return $this->renderChildren();
}
}
diff --git a/src/view/AphrontView.php b/src/view/AphrontView.php
index 23807285a8..d0377bc12d 100644
--- a/src/view/AphrontView.php
+++ b/src/view/AphrontView.php
@@ -1,57 +1,41 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class AphrontView {
protected $children = array();
final public function appendChild($child) {
$this->children[] = $child;
return $this;
}
final protected function renderChildren() {
$out = array();
foreach ($this->children as $child) {
$out[] = $this->renderSingleView($child);
}
return implode('', $out);
}
final protected function renderSingleView($child) {
if ($child instanceof AphrontView) {
return $child->render();
} else if (is_array($child)) {
$out = array();
foreach ($child as $element) {
$out[] = $this->renderSingleView($element);
}
return implode('', $out);
} else {
return $child;
}
}
abstract public function render();
public function __set($name, $value) {
phlog('Wrote to undeclared property '.get_class($this).'::$'.$name.'.');
$this->$name = $value;
}
}
diff --git a/src/view/__tests__/PhabricatorLocalTimeTestCase.php b/src/view/__tests__/PhabricatorLocalTimeTestCase.php
index 4470eb1aca..609f725ec2 100644
--- a/src/view/__tests__/PhabricatorLocalTimeTestCase.php
+++ b/src/view/__tests__/PhabricatorLocalTimeTestCase.php
@@ -1,52 +1,36 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorLocalTimeTestCase extends PhabricatorTestCase {
public function testLocalTimeFormatting() {
$user = new PhabricatorUser();
$user->setTimezoneIdentifier('America/Los_Angeles');
$utc = new PhabricatorUser();
$utc->setTimezoneIdentifier('UTC');
$this->assertEqual(
'Jan 1 2000, 12:00 AM',
phabricator_datetime(946684800, $utc),
'Datetime formatting');
$this->assertEqual(
'Jan 1 2000',
phabricator_date(946684800, $utc),
'Date formatting');
$this->assertEqual(
'12:00 AM',
phabricator_time(946684800, $utc),
'Time formatting');
$this->assertEqual(
'Dec 31 1999, 4:00 PM',
phabricator_datetime(946684800, $user),
'Localization');
$this->assertEqual(
'',
phabricator_datetime(0, $user),
'Missing epoch should fail gracefully');
}
}
diff --git a/src/view/__tests__/PhabricatorUnitsTestCase.php b/src/view/__tests__/PhabricatorUnitsTestCase.php
index 5615558652..02e01305a2 100644
--- a/src/view/__tests__/PhabricatorUnitsTestCase.php
+++ b/src/view/__tests__/PhabricatorUnitsTestCase.php
@@ -1,146 +1,130 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorUnitsTestCase extends PhabricatorTestCase {
// NOTE: Keep tests below PHP_INT_MAX on 32-bit systems, since if you write
// larger numeric literals they'll evaluate to nonsense.
public function testByteFormatting() {
$tests = array(
1 => '1 B',
1000 => '1 KB',
1000000 => '1 MB',
10000000 => '10 MB',
100000000 => '100 MB',
1000000000 => '1 GB',
999 => '999 B',
);
foreach ($tests as $input => $expect) {
$this->assertEqual(
$expect,
phabricator_format_bytes($input),
'phabricator_format_bytes('.$input.')');
}
}
public function testByteParsing() {
$tests = array(
'1' => 1,
'1k' => 1000,
'1K' => 1000,
'1kB' => 1000,
'1Kb' => 1000,
'1KB' => 1000,
'1MB' => 1000000,
'1GB' => 1000000000,
'1.5M' => 1500000,
'1 000' => 1000,
'1,234.56 KB' => 1234560,
);
foreach ($tests as $input => $expect) {
$this->assertEqual(
$expect,
phabricator_parse_bytes($input),
'phabricator_parse_bytes('.$input.')');
}
$this->tryTestCases(
array('string' => 'string'),
array(false),
'phabricator_parse_bytes');
}
public function testDetailedDurationFormatting() {
$expected_zero = 'now';
$tests = array (
12095939 => '19 w, 6 d',
-12095939 => '19 w, 6 d ago',
3380521 => '5 w, 4 d',
-3380521 => '5 w, 4 d ago',
0 => $expected_zero,
);
foreach ($tests as $duration => $expect) {
$this->assertEqual(
$expect,
phabricator_format_relative_time_detailed($duration),
'phabricator_format_relative_time_detailed('.$duration.')');
}
$tests = array(
3380521 => array(
-1 => '5 w',
0 => '5 w',
1 => '5 w',
2 => '5 w, 4 d',
3 => '5 w, 4 d, 3 h',
4 => '5 w, 4 d, 3 h, 2 m',
5 => '5 w, 4 d, 3 h, 2 m, 1 s',
6 => '5 w, 4 d, 3 h, 2 m, 1 s',
),
-3380521 => array(
-1 => '5 w ago',
0 => '5 w ago',
1 => '5 w ago',
2 => '5 w, 4 d ago',
3 => '5 w, 4 d, 3 h ago',
4 => '5 w, 4 d, 3 h, 2 m ago',
5 => '5 w, 4 d, 3 h, 2 m, 1 s ago',
6 => '5 w, 4 d, 3 h, 2 m, 1 s ago',
),
0 => array(
-1 => $expected_zero,
0 => $expected_zero,
1 => $expected_zero,
2 => $expected_zero,
3 => $expected_zero,
4 => $expected_zero,
5 => $expected_zero,
6 => $expected_zero,
),
);
foreach ($tests as $duration => $sub_tests) {
if (is_array($sub_tests)) {
foreach ($sub_tests as $levels => $expect) {
$this->assertEqual(
$expect,
phabricator_format_relative_time_detailed($duration, $levels),
'phabricator_format_relative_time_detailed('.$duration.',
'.$levels.')');
}
} else {
$expect = $sub_tests;
$this->assertEqual(
$expect,
phabricator_format_relative_time_detailed($duration),
'phabricator_format_relative_time_detailed('.$duration.')');
}
}
}
}
diff --git a/src/view/control/AphrontAttachedFileView.php b/src/view/control/AphrontAttachedFileView.php
index 4c9371c1b7..6953f85677 100644
--- a/src/view/control/AphrontAttachedFileView.php
+++ b/src/view/control/AphrontAttachedFileView.php
@@ -1,73 +1,57 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontAttachedFileView extends AphrontView {
private $file;
public function setFile(PhabricatorFile $file) {
$this->file = $file;
return $this;
}
public function render() {
require_celerity_resource('aphront-attached-file-view-css');
$file = $this->file;
$phid = $file->getPHID();
$thumb = phutil_render_tag(
'img',
array(
'src' => $file->getThumb60x45URI(),
'width' => 60,
'height' => 45,
));
$name = phutil_render_tag(
'a',
array(
'href' => $file->getViewURI(),
'target' => '_blank',
),
phutil_escape_html($file->getName()));
$size = number_format($file->getByteSize()).' bytes';
$remove = javelin_render_tag(
'a',
array(
'class' => 'button grey',
'sigil' => 'aphront-attached-file-view-remove',
// NOTE: Using 'ref' here instead of 'meta' because the file upload
// endpoint doesn't receive request metadata and thus can't generate
// a valid response with node metadata.
'ref' => $file->getPHID(),
),
"\xE2\x9C\x96"); // "Heavy Multiplication X"
return
'<table class="aphront-attached-file-view">
<tr>
<td>'.$thumb.'</td>
<th><strong>'.$name.'</strong><br />'.$size.'</th>
<td class="aphront-attached-file-view-remove">'.$remove.'</td>
</tr>
</table>';
}
}
diff --git a/src/view/control/AphrontCursorPagerView.php b/src/view/control/AphrontCursorPagerView.php
index 36aecacac6..424b011990 100644
--- a/src/view/control/AphrontCursorPagerView.php
+++ b/src/view/control/AphrontCursorPagerView.php
@@ -1,145 +1,129 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontCursorPagerView extends AphrontView {
private $afterID;
private $beforeID;
private $pageSize = 100;
private $nextPageID;
private $prevPageID;
private $moreResults;
private $uri;
final public function setPageSize($page_size) {
$this->pageSize = max(1, $page_size);
return $this;
}
final public function getPageSize() {
return $this->pageSize;
}
final public function setURI(PhutilURI $uri) {
$this->uri = $uri;
return $this;
}
final public function readFromRequest(AphrontRequest $request) {
$this->uri = $request->getRequestURI();
$this->afterID = $request->getStr('after');
$this->beforeID = $request->getStr('before');
return $this;
}
final public function setAfterID($after_id) {
$this->afterID = $after_id;
return $this;
}
final public function getAfterID() {
return $this->afterID;
}
final public function setBeforeID($before_id) {
$this->beforeID = $before_id;
return $this;
}
final public function getBeforeID() {
return $this->beforeID;
}
final public function setNextPageID($next_page_id) {
$this->nextPageID = $next_page_id;
return $this;
}
final public function getNextPageID() {
return $this->nextPageID;
}
final public function setPrevPageID($prev_page_id) {
$this->prevPageID = $prev_page_id;
return $this;
}
final public function getPrevPageID() {
return $this->prevPageID;
}
final public function sliceResults(array $results) {
if (count($results) > $this->getPageSize()) {
$offset = ($this->beforeID ? count($results) - $this->getPageSize() : 0);
$results = array_slice($results, $offset, $this->getPageSize(), true);
$this->moreResults = true;
}
return $results;
}
public function render() {
if (!$this->uri) {
throw new Exception(
"You must call setURI() before you can call render().");
}
$links = array();
if ($this->afterID || ($this->beforeID && $this->moreResults)) {
$links[] = phutil_render_tag(
'a',
array(
'href' => $this->uri
->alter('before', null)
->alter('after', null),
),
"\xC2\xAB First");
}
if ($this->prevPageID) {
$links[] = phutil_render_tag(
'a',
array(
'href' => $this->uri
->alter('after', null)
->alter('before', $this->prevPageID),
),
"\xE2\x80\xB9 Prev");
}
if ($this->nextPageID) {
$links[] = phutil_render_tag(
'a',
array(
'href' => $this->uri
->alter('after', $this->nextPageID)
->alter('before', null),
),
"Next \xE2\x80\xBA");
}
return
'<div class="aphront-pager-view">'.
implode('', $links).
'</div>';
}
}
diff --git a/src/view/control/AphrontPagerView.php b/src/view/control/AphrontPagerView.php
index abd51498c4..a0d98d8dbe 100644
--- a/src/view/control/AphrontPagerView.php
+++ b/src/view/control/AphrontPagerView.php
@@ -1,234 +1,218 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontPagerView extends AphrontView {
private $offset;
private $pageSize = 100;
private $count;
private $hasMorePages;
private $uri;
private $pagingParameter;
private $surroundingPages = 2;
private $enableKeyboardShortcuts;
final public function setPageSize($page_size) {
$this->pageSize = max(1, $page_size);
return $this;
}
final public function setOffset($offset) {
$this->offset = max(0, $offset);
return $this;
}
final public function getOffset() {
return $this->offset;
}
final public function getPageSize() {
return $this->pageSize;
}
final public function setCount($count) {
$this->count = $count;
return $this;
}
final public function setHasMorePages($has_more) {
$this->hasMorePages = $has_more;
return $this;
}
final public function setURI(PhutilURI $uri, $paging_parameter) {
$this->uri = $uri;
$this->pagingParameter = $paging_parameter;
return $this;
}
final public function setSurroundingPages($pages) {
$this->surroundingPages = max(0, $pages);
return $this;
}
private function computeCount() {
if ($this->count !== null) {
return $this->count;
}
return $this->getOffset()
+ $this->getPageSize()
+ ($this->hasMorePages ? 1 : 0);
}
private function isExactCountKnown() {
return $this->count !== null;
}
/**
* A common paging strategy is to select one extra record and use that to
* indicate that there's an additional page (this doesn't give you a
* complete page count but is often faster than counting the total number
* of items). This method will take a result array, slice it down to the
* page size if necessary, and call setHasMorePages() if there are more than
* one page of results.
*
* $results = queryfx_all(
* $conn,
* 'SELECT ... LIMIT %d, %d',
* $pager->getOffset(),
* $pager->getPageSize() + 1);
* $results = $pager->sliceResults($results);
*
* @param list Result array.
* @return list One page of results.
*/
public function sliceResults(array $results) {
if (count($results) > $this->getPageSize()) {
$results = array_slice($results, 0, $this->getPageSize(), true);
$this->setHasMorePages(true);
}
return $results;
}
public function setEnableKeyboardShortcuts($enable) {
$this->enableKeyboardShortcuts = $enable;
return $this;
}
public function render() {
if (!$this->uri) {
throw new Exception(
"You must call setURI() before you can call render().");
}
require_celerity_resource('aphront-pager-view-css');
$page = (int)floor($this->getOffset() / $this->getPageSize());
$last = ((int)ceil($this->computeCount() / $this->getPageSize())) - 1;
$near = $this->surroundingPages;
$min = $page - $near;
$max = $page + $near;
// Limit the window size to no larger than the number of available pages.
if ($max - $min > $last) {
$max = $min + $last;
if ($max == $min) {
return '<div class="aphront-pager-view"></div>';
}
}
// Slide the window so it is entirely over displayable pages.
if ($min < 0) {
$max += 0 - $min;
$min += 0 - $min;
}
if ($max > $last) {
$min -= $max - $last;
$max -= $max - $last;
}
// Build up a list of <index, label, css-class> tuples which describe the
// links we'll display, then render them all at once.
$links = array();
$prev_index = null;
$next_index = null;
if ($min > 0) {
$links[] = array(0, 'First', null);
}
if ($page > 0) {
$links[] = array($page - 1, 'Prev', null);
$prev_index = $page - 1;
}
for ($ii = $min; $ii <= $max; $ii++) {
$links[] = array($ii, $ii + 1, ($ii == $page) ? 'current' : null);
}
if ($page < $last && $last > 0) {
$links[] = array($page + 1, 'Next', null);
$next_index = $page + 1;
}
if ($max < ($last - 1)) {
$links[] = array($last, 'Last', null);
}
$base_uri = $this->uri;
$parameter = $this->pagingParameter;
if ($this->enableKeyboardShortcuts) {
$pager_links = array();
$pager_index = array(
'prev' => $prev_index,
'next' => $next_index,
);
foreach ($pager_index as $key => $index) {
if ($index !== null) {
$display_index = $this->getDisplayIndex($index);
$pager_links[$key] = (string)$base_uri->alter(
$parameter,
$display_index);
}
}
Javelin::initBehavior('phabricator-keyboard-pager', $pager_links);
}
// Convert tuples into rendered nodes.
$rendered_links = array();
foreach ($links as $link) {
list($index, $label, $class) = $link;
$display_index = $this->getDisplayIndex($index);
$link = $base_uri->alter($parameter, $display_index);
$rendered_links[] = phutil_render_tag(
'a',
array(
'href' => $link,
'class' => $class,
),
$label);
}
return
'<div class="aphront-pager-view">'.
implode('', $rendered_links).
'</div>';
}
private function getDisplayIndex($page_index) {
$page_size = $this->getPageSize();
// Use a 1-based sequence for display so that the number in the URI is
// the same as the page number you're on.
if ($page_index == 0) {
// No need for the first page to say page=1.
$display_index = null;
} else {
$display_index = $page_index * $page_size;
}
return $display_index;
}
}
diff --git a/src/view/control/AphrontTableView.php b/src/view/control/AphrontTableView.php
index 78cbfdebe7..e1a7bf7e45 100644
--- a/src/view/control/AphrontTableView.php
+++ b/src/view/control/AphrontTableView.php
@@ -1,283 +1,267 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontTableView extends AphrontView {
protected $data;
protected $headers;
protected $rowClasses = array();
protected $columnClasses = array();
protected $cellClasses = array();
protected $zebraStripes = true;
protected $noDataString;
protected $className;
protected $columnVisibility = array();
protected $sortURI;
protected $sortParam;
protected $sortSelected;
protected $sortReverse;
protected $sortValues;
public function __construct(array $data) {
$this->data = $data;
}
public function setHeaders(array $headers) {
$this->headers = $headers;
return $this;
}
public function setColumnClasses(array $column_classes) {
$this->columnClasses = $column_classes;
return $this;
}
public function setRowClasses(array $row_classes) {
$this->rowClasses = $row_classes;
return $this;
}
public function setCellClasses(array $cell_classes) {
$this->cellClasses = $cell_classes;
return $this;
}
public function setNoDataString($no_data_string) {
$this->noDataString = $no_data_string;
return $this;
}
public function setClassName($class_name) {
$this->className = $class_name;
return $this;
}
public function setZebraStripes($zebra_stripes) {
$this->zebraStripes = $zebra_stripes;
return $this;
}
public function setColumnVisibility(array $visibility) {
$this->columnVisibility = $visibility;
return $this;
}
/**
* Parse a sorting parameter:
*
* list($sort, $reverse) = AphrontTableView::parseSortParam($sort_param);
*
* @param string Sort request parameter.
* @return pair Sort value, sort direction.
*/
public static function parseSort($sort) {
return array(ltrim($sort, '-'), preg_match('/^-/', $sort));
}
public function makeSortable(
PhutilURI $base_uri,
$param,
$selected,
$reverse,
array $sort_values) {
$this->sortURI = $base_uri;
$this->sortParam = $param;
$this->sortSelected = $selected;
$this->sortReverse = $reverse;
$this->sortValues = array_values($sort_values);
return $this;
}
public function render() {
require_celerity_resource('aphront-table-view-css');
$table_class = $this->className;
if ($table_class !== null) {
$table_class = ' class="aphront-table-view '.$table_class.'"';
} else {
$table_class = ' class="aphront-table-view"';
}
$table = array('<table'.$table_class.'>');
$col_classes = array();
foreach ($this->columnClasses as $key => $class) {
if (strlen($class)) {
$col_classes[] = $class;
} else {
$col_classes[] = null;
}
}
$visibility = array_values($this->columnVisibility);
$headers = $this->headers;
$sort_values = $this->sortValues;
if ($headers) {
while (count($headers) > count($visibility)) {
$visibility[] = true;
}
while (count($headers) > count($sort_values)) {
$sort_values[] = null;
}
$table[] = '<tr>';
foreach ($headers as $col_num => $header) {
if (!$visibility[$col_num]) {
continue;
}
$classes = array();
if (!empty($col_classes[$col_num])) {
$classes[] = $col_classes[$col_num];
}
if ($sort_values[$col_num] !== null) {
$classes[] = 'aphront-table-view-sortable';
$sort_value = $sort_values[$col_num];
$sort_glyph = "\xE2\x86\x93";
if ($sort_value == $this->sortSelected) {
if ($this->sortReverse) {
$sort_glyph = "\xE2\x86\x91";
} else if (!$this->sortReverse) {
$sort_value = '-'.$sort_value;
}
$classes[] = 'aphront-table-view-sortable-selected';
}
$sort_glyph = phutil_render_tag(
'span',
array(
'class' => 'aphront-table-view-sort-glyph',
),
$sort_glyph);
$header = phutil_render_tag(
'a',
array(
'href' => $this->sortURI->alter($this->sortParam, $sort_value),
'class' => 'aphront-table-view-sort-link',
),
$sort_glyph.' '.$header);
}
if ($classes) {
$class = ' class="'.implode(' ', $classes).'"';
} else {
$class = null;
}
$table[] = '<th'.$class.'>'.$header.'</th>';
}
$table[] = '</tr>';
}
foreach ($col_classes as $key => $value) {
if (($sort_values[$key] !== null) &&
($sort_values[$key] == $this->sortSelected)) {
$value = trim($value.' sorted-column');
}
if ($value !== null) {
$col_classes[$key] = $value;
}
}
$data = $this->data;
if ($data) {
$row_num = 0;
foreach ($data as $row) {
while (count($row) > count($col_classes)) {
$col_classes[] = null;
}
while (count($row) > count($visibility)) {
$visibility[] = true;
}
$class = idx($this->rowClasses, $row_num);
if ($this->zebraStripes && ($row_num % 2)) {
if ($class !== null) {
$class = 'alt alt-'.$class;
} else {
$class = 'alt';
}
}
if ($class !== null) {
$class = ' class="'.$class.'"';
}
$table[] = '<tr'.$class.'>';
// NOTE: Use of a separate column counter is to allow this to work
// correctly if the row data has string or non-sequential keys.
$col_num = 0;
foreach ($row as $value) {
if (!$visibility[$col_num]) {
++$col_num;
continue;
}
$class = $col_classes[$col_num];
if (!empty($this->cellClasses[$row_num][$col_num])) {
$class = trim($class.' '.$this->cellClasses[$row_num][$col_num]);
}
if ($class !== null) {
$table[] = '<td class="'.$class.'">';
} else {
$table[] = '<td>';
}
$table[] = $value.'</td>';
++$col_num;
}
++$row_num;
}
} else {
$colspan = max(count(array_filter($visibility)), 1);
$table[] =
'<tr class="no-data"><td colspan="'.$colspan.'">'.
coalesce($this->noDataString, 'No data available.').
'</td></tr>';
}
$table[] = '</table>';
return implode('', $table);
}
public static function renderSingleDisplayLine($line) {
// TODO: Is there a cleaner way to do this? We use a relative div with
// overflow hidden to provide the bounds, and an absolute span with
// white-space: pre to prevent wrapping. We need to append a character
// (&nbsp; -- nonbreaking space) afterward to give the bounds div height
// (alternatively, we could hard-code the line height). This is gross but
// it's not clear that there's a better appraoch.
return phutil_render_tag(
'div',
array(
'class' => 'single-display-line-bounds',
),
phutil_render_tag(
'span',
array(
'class' => 'single-display-line-content',
),
$line).'&nbsp;');
}
}
diff --git a/src/view/control/AphrontTokenizerTemplateView.php b/src/view/control/AphrontTokenizerTemplateView.php
index 65c0f225d7..c3cc811967 100644
--- a/src/view/control/AphrontTokenizerTemplateView.php
+++ b/src/view/control/AphrontTokenizerTemplateView.php
@@ -1,104 +1,88 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontTokenizerTemplateView extends AphrontView {
private $value;
private $name;
private $id;
public function setID($id) {
$this->id = $id;
return $this;
}
public function setValue(array $value) {
$this->value = $value;
return $this;
}
public function getValue() {
return $this->value;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function render() {
require_celerity_resource('aphront-tokenizer-control-css');
$id = $this->id;
$name = $this->getName();
$values = nonempty($this->getValue(), array());
$tokens = array();
foreach ($values as $key => $value) {
$tokens[] = $this->renderToken($key, $value);
}
$input = javelin_render_tag(
'input',
array(
'mustcapture' => true,
'name' => $name,
'class' => 'jx-tokenizer-input',
'sigil' => 'tokenizer-input',
'style' => 'width: 0px;',
'disabled' => 'disabled',
'type' => 'text',
));
return phutil_render_tag(
'div',
array(
'id' => $id,
'class' => 'jx-tokenizer-container',
),
implode('', $tokens).
$input.
'<div style="clear: both;"></div>');
}
private function renderToken($key, $value) {
$input_name = $this->getName();
if ($input_name) {
$input_name .= '[]';
}
return phutil_render_tag(
'a',
array(
'class' => 'jx-tokenizer-token',
),
phutil_escape_html($value).
phutil_render_tag(
'input',
array(
'type' => 'hidden',
'name' => $input_name,
'value' => $key,
)).
'<span class="jx-tokenizer-x-placeholder"></span>');
}
}
diff --git a/src/view/control/AphrontTypeaheadTemplateView.php b/src/view/control/AphrontTypeaheadTemplateView.php
index 1a141b0fa2..5e27c682cf 100644
--- a/src/view/control/AphrontTypeaheadTemplateView.php
+++ b/src/view/control/AphrontTypeaheadTemplateView.php
@@ -1,81 +1,65 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontTypeaheadTemplateView extends AphrontView {
private $value;
private $name;
private $id;
public function setID($id) {
$this->id = $id;
return $this;
}
public function setValue(array $value) {
$this->value = $value;
return $this;
}
public function getValue() {
return $this->value;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function render() {
require_celerity_resource('aphront-typeahead-control-css');
$id = $this->id;
$name = $this->getName();
$values = nonempty($this->getValue(), array());
$tokens = array();
foreach ($values as $key => $value) {
$tokens[] = $this->renderToken($key, $value);
}
$input = javelin_render_tag(
'input',
array(
'name' => $name,
'class' => 'jx-typeahead-input',
'sigil' => 'typeahead',
'type' => 'text',
'value' => $this->value,
'autocomplete' => 'off',
));
return javelin_render_tag(
'div',
array(
'id' => $id,
'sigil' => 'typeahead-hardpoint',
'class' => 'jx-typeahead-hardpoint',
),
$input.
'<div style="clear: both;"></div>');
}
}
diff --git a/src/view/control/PhabricatorObjectListView.php b/src/view/control/PhabricatorObjectListView.php
index 86065d8b28..b5c8d163ed 100644
--- a/src/view/control/PhabricatorObjectListView.php
+++ b/src/view/control/PhabricatorObjectListView.php
@@ -1,68 +1,52 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorObjectListView extends AphrontView {
private $handles = array();
private $buttons = array();
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function addButton(PhabricatorObjectHandle $handle, $button) {
$this->buttons[$handle->getPHID()][] = $button;
return $this;
}
public function render() {
$handles = $this->handles;
require_celerity_resource('phabricator-object-list-view-css');
$out = array();
foreach ($handles as $handle) {
$buttons = idx($this->buttons, $handle->getPHID(), array());
if ($buttons) {
$buttons =
'<div class="phabricator-object-list-view-buttons">'.
implode('', $buttons).
'</div>';
} else {
$buttons = null;
}
$out[] = javelin_render_tag(
'div',
array(
'class' => 'phabricator-object-list-view-item',
'style' => 'background-image: url('.$handle->getImageURI().');',
),
$handle->renderLink().$buttons);
}
return
'<div class="phabricator-object-list-view">'.
implode("\n", $out).
'</div>';
}
}
diff --git a/src/view/control/PhabricatorObjectSelectorDialog.php b/src/view/control/PhabricatorObjectSelectorDialog.php
index a756d32625..8361642f67 100644
--- a/src/view/control/PhabricatorObjectSelectorDialog.php
+++ b/src/view/control/PhabricatorObjectSelectorDialog.php
@@ -1,207 +1,191 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorObjectSelectorDialog {
private $user;
private $filters = array();
private $handles = array();
private $cancelURI;
private $submitURI;
private $searchURI;
private $selectedFilter;
private $excluded;
private $title;
private $header;
private $buttonText;
private $instructions;
public function setUser($user) {
$this->user = $user;
return $this;
}
public function setFilters(array $filters) {
$this->filters = $filters;
return $this;
}
public function setSelectedFilter($selected_filter) {
$this->selectedFilter = $selected_filter;
return $this;
}
public function setExcluded($excluded_phid) {
$this->excluded = $excluded_phid;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setCancelURI($cancel_uri) {
$this->cancelURI = $cancel_uri;
return $this;
}
public function setSubmitURI($submit_uri) {
$this->submitURI = $submit_uri;
return $this;
}
public function setSearchURI($search_uri) {
$this->searchURI = $search_uri;
return $this;
}
public function setTitle($title) {
$this->title = $title;
return $this;
}
public function setHeader($header) {
$this->header = $header;
return $this;
}
public function setButtonText($button_text) {
$this->buttonText = $button_text;
return $this;
}
public function setInstructions($instructions) {
$this->instructions = $instructions;
return $this;
}
public function buildDialog() {
$user = $this->user;
$filter_id = celerity_generate_unique_node_id();
$query_id = celerity_generate_unique_node_id();
$results_id = celerity_generate_unique_node_id();
$current_id = celerity_generate_unique_node_id();
$search_id = celerity_generate_unique_node_id();
$form_id = celerity_generate_unique_node_id();
require_celerity_resource('phabricator-object-selector-css');
$options = array();
foreach ($this->filters as $key => $label) {
$options[] = phutil_render_tag(
'option',
array(
'value' => $key,
'selected' => ($key == $this->selectedFilter)
? 'selected'
: null,
),
$label);
}
$options = implode("\n", $options);
$instructions = null;
if ($this->instructions) {
$instructions =
'<p class="phabricator-object-selector-instructions">'.
$this->instructions.
'</p>';
}
$search_box = phabricator_render_form(
$user,
array(
'method' => 'POST',
'action' => $this->submitURI,
'id' => $search_id,
),
'<table class="phabricator-object-selector-search">
<tr>
<td class="phabricator-object-selector-search-filter">
<select id="'.$filter_id.'">'.
$options.
'</select>
</td>
<td class="phabricator-object-selector-search-text">
<input type="text" id="'.$query_id.'" />
</td>
</tr>
</table>');
$result_box =
'<div class="phabricator-object-selector-results" id="'.$results_id.'">'.
'</div>';
$attached_box =
'<div class="phabricator-object-selector-current">'.
'<div class="phabricator-object-selector-currently-attached">'.
'<div class="phabricator-object-selector-header">'.
phutil_escape_html($this->header).
'</div>'.
'<div id="'.$current_id.'">'.
'</div>'.
$instructions.
'</div>'.
'</div>';
$dialog = new AphrontDialogView();
$dialog
->setUser($this->user)
->setTitle($this->title)
->setClass('phabricator-object-selector-dialog')
->appendChild($search_box)
->appendChild($result_box)
->appendChild($attached_box)
->setRenderDialogAsDiv()
->setFormID($form_id)
->addSubmitButton($this->buttonText);
if ($this->cancelURI) {
$dialog->addCancelButton($this->cancelURI);
}
$handle_views = array();
foreach ($this->handles as $handle) {
$phid = $handle->getPHID();
$view = new PhabricatorHandleObjectSelectorDataView($handle);
$handle_views[$phid] = $view->renderData();
}
$dialog->addHiddenInput('phids', implode(';', array_keys($this->handles)));
Javelin::initBehavior(
'phabricator-object-selector',
array(
'filter' => $filter_id,
'query' => $query_id,
'search' => $search_id,
'results' => $results_id,
'current' => $current_id,
'form' => $form_id,
'exclude' => $this->excluded,
'uri' => $this->searchURI,
'handles' => $handle_views,
));
return $dialog;
}
}
diff --git a/src/view/form/AphrontErrorView.php b/src/view/form/AphrontErrorView.php
index dce3e9a974..632561ddec 100644
--- a/src/view/form/AphrontErrorView.php
+++ b/src/view/form/AphrontErrorView.php
@@ -1,107 +1,91 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontErrorView extends AphrontView {
const SEVERITY_ERROR = 'error';
const SEVERITY_WARNING = 'warning';
const SEVERITY_NOTICE = 'notice';
const SEVERITY_NODATA = 'nodata';
private $title;
private $errors;
private $severity;
private $id;
public function setTitle($title) {
$this->title = $title;
return $this;
}
public function setSeverity($severity) {
$this->severity = $severity;
return $this;
}
public function setErrors(array $errors) {
$this->errors = $errors;
return $this;
}
public function setID($id) {
$this->id = $id;
return $this;
}
final public function render() {
require_celerity_resource('aphront-error-view-css');
$errors = $this->errors;
if ($errors) {
$list = array();
foreach ($errors as $error) {
$list[] = phutil_render_tag(
'li',
array(),
phutil_escape_html($error));
}
$list = phutil_render_tag(
'ul',
array(
'class' => 'aphront-error-view-list',
),
implode("\n", $list));
} else {
$list = null;
}
$title = $this->title;
if (strlen($title)) {
$title = phutil_render_tag(
'h1',
array(
'class' => 'aphront-error-view-head',
),
phutil_escape_html($title));
} else {
$title = null;
}
$this->severity = nonempty($this->severity, self::SEVERITY_ERROR);
$more_classes = array();
$more_classes[] = 'aphront-error-severity-'.$this->severity;
$more_classes = implode(' ', $more_classes);
return phutil_render_tag(
'div',
array(
'id' => $this->id,
'class' => 'aphront-error-view '.$more_classes,
),
$title.
phutil_render_tag(
'div',
array(
'class' => 'aphront-error-view-body',
),
$this->renderChildren().
$list));
}
}
diff --git a/src/view/form/AphrontFormInsetView.php b/src/view/form/AphrontFormInsetView.php
index f9214892de..2668bbe5a6 100644
--- a/src/view/form/AphrontFormInsetView.php
+++ b/src/view/form/AphrontFormInsetView.php
@@ -1,123 +1,107 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontFormInsetView extends AphrontView {
private $title;
private $description;
private $rightButton;
private $content;
private $hidden = array();
private $divAttributes;
public function setTitle($title) {
$this->title = $title;
return $this;
}
public function setDescription($description) {
$this->description = $description;
return $this;
}
public function setRightButton($button) {
$this->rightButton = $button;
return $this;
}
public function setContent($content) {
$this->content = $content;
return $this;
}
public function addHiddenInput($key, $value) {
if (is_array($value)) {
foreach ($value as $hidden_key => $hidden_value) {
$this->hidden[] = array($key.'['.$hidden_key.']', $hidden_value);
}
} else {
$this->hidden[] = array($key, $value);
}
return $this;
}
public function addDivAttributes(array $attributes) {
$this->divAttributes = $attributes;
return $this;
}
public function render() {
$title = $hidden_inputs = $right_button = $desc = $content = '';
if ($this->title) {
$title = '<h1>'.phutil_escape_html($this->title).'</h1>';
}
$hidden_inputs = array();
foreach ($this->hidden as $inp) {
list($key, $value) = $inp;
$hidden_inputs[] = phutil_render_tag(
'input',
array(
'type' => 'hidden',
'name' => $key,
'value' => $value,
));
}
$hidden_inputs = implode("\n", $hidden_inputs);
if ($this->rightButton) {
$right_button = phutil_render_tag(
'div',
array(
'style' => 'float: right;',
),
$this->rightButton);
}
if ($this->description) {
$desc = phutil_render_tag(
'p',
array(),
$this->description);
if ($right_button) {
$desc .= '<div style="clear: both;"></div>';
}
}
$div_attributes = $this->divAttributes;
$classes = array('aphront-form-inset');
if (isset($div_attributes['class'])) {
$classes[] = $div_attributes['class'];
}
$div_attributes['class'] = implode(' ', $classes);
if ($this->content) {
$content = $this->content;
}
return $title.phutil_render_tag(
'div',
$div_attributes,
$hidden_inputs.$right_button.$desc.$content.$this->renderChildren());
}
}
diff --git a/src/view/form/AphrontFormLayoutView.php b/src/view/form/AphrontFormLayoutView.php
index 2551d6b53b..5780353475 100644
--- a/src/view/form/AphrontFormLayoutView.php
+++ b/src/view/form/AphrontFormLayoutView.php
@@ -1,59 +1,43 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* This provides the layout of an AphrontFormView without actually providing
* the <form /> tag. Useful on its own for creating forms in other forms (like
* dialogs) or forms which aren't submittable.
*/
final class AphrontFormLayoutView extends AphrontView {
private $backgroundShading;
private $padded;
public function setBackgroundShading($shading) {
$this->backgroundShading = $shading;
return $this;
}
public function setPadded($padded) {
$this->padded = $padded;
return $this;
}
public function render() {
$classes = array('aphront-form-view');
if ($this->backgroundShading) {
$classes[] = 'aphront-form-view-shaded';
}
if ($this->padded) {
$classes[] = 'aphront-form-view-padded';
}
$classes = implode(' ', $classes);
return phutil_render_tag(
'div',
array(
'class' => $classes,
),
$this->renderChildren());
}
}
diff --git a/src/view/form/AphrontFormView.php b/src/view/form/AphrontFormView.php
index 93e252aa85..0196b9c6ed 100644
--- a/src/view/form/AphrontFormView.php
+++ b/src/view/form/AphrontFormView.php
@@ -1,125 +1,109 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontFormView extends AphrontView {
private $action;
private $method = 'POST';
private $header;
private $data = array();
private $encType;
private $user;
private $workflow;
private $id;
private $flexible;
public function setFlexible($flexible) {
$this->flexible = $flexible;
return $this;
}
public function setID($id) {
$this->id = $id;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setAction($action) {
$this->action = $action;
return $this;
}
public function setMethod($method) {
$this->method = $method;
return $this;
}
public function setEncType($enc_type) {
$this->encType = $enc_type;
return $this;
}
public function addHiddenInput($key, $value) {
$this->data[$key] = $value;
return $this;
}
public function setWorkflow($workflow) {
$this->workflow = $workflow;
return $this;
}
public function render() {
if ($this->flexible) {
require_celerity_resource('phabricator-form-view-css');
}
require_celerity_resource('aphront-form-view-css');
Javelin::initBehavior('aphront-form-disable-on-submit');
$layout = new AphrontFormLayoutView();
if (!$this->flexible) {
$layout
->setBackgroundShading(true)
->setPadded(true);
}
$layout
->appendChild($this->renderDataInputs())
->appendChild($this->renderChildren());
if (!$this->user) {
throw new Exception('You must pass the user to AphrontFormView.');
}
return phabricator_render_form(
$this->user,
array(
'class' => $this->flexible ? 'phabricator-form-view' : null,
'action' => $this->action,
'method' => $this->method,
'enctype' => $this->encType,
'sigil' => $this->workflow ? 'workflow' : null,
'id' => $this->id,
),
$layout->render());
}
private function renderDataInputs() {
$inputs = array();
foreach ($this->data as $key => $value) {
if ($value === null) {
continue;
}
$inputs[] = phutil_render_tag(
'input',
array(
'type' => 'hidden',
'name' => $key,
'value' => $value,
));
}
return implode("\n", $inputs);
}
}
diff --git a/src/view/form/control/AphrontFormCheckboxControl.php b/src/view/form/control/AphrontFormCheckboxControl.php
index a33a8b2401..543911d909 100644
--- a/src/view/form/control/AphrontFormCheckboxControl.php
+++ b/src/view/form/control/AphrontFormCheckboxControl.php
@@ -1,69 +1,53 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontFormCheckboxControl extends AphrontFormControl {
private $boxes = array();
public function addCheckbox($name, $value, $label, $checked = false) {
$this->boxes[] = array(
'name' => $name,
'value' => $value,
'label' => $label,
'checked' => $checked,
);
return $this;
}
protected function getCustomControlClass() {
return 'aphront-form-control-checkbox';
}
protected function renderInput() {
$rows = array();
foreach ($this->boxes as $box) {
$id = celerity_generate_unique_node_id();
$checkbox = phutil_render_tag(
'input',
array(
'id' => $id,
'type' => 'checkbox',
'name' => $box['name'],
'value' => $box['value'],
'checked' => $box['checked'] ? 'checked' : null,
'disabled' => $this->getDisabled() ? 'disabled' : null,
));
$label = phutil_render_tag(
'label',
array(
'for' => $id,
),
phutil_escape_html($box['label']));
$rows[] =
'<tr>'.
'<td>'.$checkbox.'</td>'.
'<th>'.$label.'</th>'.
'</tr>';
}
return
'<table class="aphront-form-control-checkbox-layout">'.
implode("\n", $rows).
'</table>';
}
}
diff --git a/src/view/form/control/AphrontFormControl.php b/src/view/form/control/AphrontFormControl.php
index bbe1af14ec..69786d2902 100644
--- a/src/view/form/control/AphrontFormControl.php
+++ b/src/view/form/control/AphrontFormControl.php
@@ -1,182 +1,166 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class AphrontFormControl extends AphrontView {
private $label;
private $caption;
private $error;
private $name;
private $value;
private $disabled;
private $id;
private $controlID;
private $controlStyle;
public function setID($id) {
$this->id = $id;
return $this;
}
public function getID() {
return $this->id;
}
public function setControlID($control_id) {
$this->controlID = $control_id;
return $this;
}
public function getControlID() {
return $this->controlID;
}
public function setControlStyle($control_style) {
$this->controlStyle = $control_style;
return $this;
}
public function getControlStyle() {
return $this->controlStyle;
}
public function setLabel($label) {
$this->label = $label;
return $this;
}
public function getLabel() {
return $this->label;
}
public function setCaption($caption) {
$this->caption = $caption;
return $this;
}
public function getCaption() {
return $this->caption;
}
public function setError($error) {
$this->error = $error;
return $this;
}
public function getError() {
return $this->error;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function setValue($value) {
$this->value = $value;
return $this;
}
public function getValue() {
return $this->value;
}
public function setDisabled($disabled) {
$this->disabled = $disabled;
return $this;
}
public function getDisabled() {
return $this->disabled;
}
abstract protected function renderInput();
abstract protected function getCustomControlClass();
protected function shouldRender() {
return true;
}
final public function render() {
if (!$this->shouldRender()) {
return null;
}
$custom_class = $this->getCustomControlClass();
if (strlen($this->getLabel())) {
$label =
'<label class="aphront-form-label">'.
phutil_escape_html($this->getLabel()).
'</label>';
} else {
$label = null;
$custom_class .= ' aphront-form-control-nolabel';
}
$input =
'<div class="aphront-form-input">'.
$this->renderInput().
'</div>';
if (strlen($this->getError())) {
$error = $this->getError();
if ($error === true) {
$error =
'<div class="aphront-form-error aphront-form-required">'.
'Required'.
'</div>';
} else {
$error =
'<div class="aphront-form-error">'.
phutil_escape_html($error).
'</div>';
}
} else {
$error = null;
}
if (strlen($this->getCaption())) {
$caption =
'<div class="aphront-form-caption">'.
$this->getCaption().
'</div>';
} else {
$caption = null;
}
return phutil_render_tag(
'div',
array(
'class' => "aphront-form-control {$custom_class}",
'id' => $this->controlID,
'style' => $this->controlStyle,
),
$label.
$error.
$input.
$caption.
// TODO: Remove this once the redesign finishes up.
'<div style="clear: both;"></div>');
}
}
diff --git a/src/view/form/control/AphrontFormDateControl.php b/src/view/form/control/AphrontFormDateControl.php
index 1f51e57225..ba002fee2a 100644
--- a/src/view/form/control/AphrontFormDateControl.php
+++ b/src/view/form/control/AphrontFormDateControl.php
@@ -1,288 +1,272 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontFormDateControl extends AphrontFormControl {
private $user;
private $initialTime;
private $valueDay;
private $valueMonth;
private $valueYear;
private $valueTime;
const TIME_START_OF_DAY = 'start-of-day';
const TIME_END_OF_DAY = 'end-of-day';
const TIME_START_OF_BUSINESS = 'start-of-business';
const TIME_END_OF_BUSINESS = 'end-of-business';
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setInitialTime($time) {
$this->initialTime = $time;
return $this;
}
public function readValueFromRequest(AphrontRequest $request) {
$user = $this->user;
if (!$this->user) {
throw new Exception(
"Call setUser() before readValueFromRequest()!");
}
$user_zone = $user->getTimezoneIdentifier();
$zone = new DateTimeZone($user_zone);
$day = $request->getInt($this->getDayInputName());
$month = $request->getInt($this->getMonthInputName());
$year = $request->getInt($this->getYearInputName());
$time = $request->getStr($this->getTimeInputName());
$err = $this->getError();
if ($day || $month || $year || $time) {
$this->valueDay = $day;
$this->valueMonth = $month;
$this->valueYear = $year;
$this->valueTime = $time;
// Assume invalid.
$err = 'Invalid';
try {
$date = new DateTime("{$year}-{$month}-{$day} {$time}", $zone);
$value = $date->format('U');
} catch (Exception $ex) {
$value = null;
}
if ($value) {
$this->setValue($value);
$err = null;
} else {
$this->setValue(null);
}
} else {
// TODO: We could eventually allow these to be customized per install or
// per user or both, but let's wait and see.
switch ($this->initialTime) {
case self::TIME_START_OF_DAY:
default:
$time = '12:00 AM';
break;
case self::TIME_START_OF_BUSINESS:
$time = '9:00 AM';
break;
case self::TIME_END_OF_BUSINESS:
$time = '5:00 PM';
break;
case self::TIME_END_OF_DAY:
$time = '11:59 PM';
break;
}
$today = $this->formatTime(time(), 'Y-m-d');
try {
$date = new DateTime("{$today} {$time}", $zone);
$value = $date->format('U');
} catch (Exception $ex) {
$value = null;
}
if ($value) {
$this->setValue($value);
} else {
$this->setValue(null);
}
}
$this->setError($err);
return $this->getValue();
}
protected function getCustomControlClass() {
return 'aphront-form-control-date';
}
public function setValue($epoch) {
$result = parent::setValue($epoch);
if ($epoch === null) {
return;
}
$readable = $this->formatTime($epoch, 'Y!m!d!g:i A');
$readable = explode('!', $readable, 4);
$this->valueYear = $readable[0];
$this->valueMonth = $readable[1];
$this->valueDay = $readable[2];
$this->valueTime = $readable[3];
return $result;
}
private function getMinYear() {
$cur_year = $this->formatTime(
time(),
'Y');
$val_year = $this->getYearInputValue();
return min($cur_year, $val_year) - 3;
}
private function getMaxYear() {
$cur_year = $this->formatTime(
time(),
'Y');
$val_year = $this->getYearInputValue();
return max($cur_year, $val_year) + 3;
}
private function getDayInputValue() {
return $this->valueDay;
}
private function getMonthInputValue() {
return $this->valueMonth;
}
private function getYearInputValue() {
return $this->valueYear;
}
private function getTimeInputValue() {
return $this->valueTime;
}
private function formatTime($epoch, $fmt) {
return phabricator_format_local_time(
$epoch,
$this->user,
$fmt);
}
private function getDayInputName() {
return $this->getName().'_d';
}
private function getMonthInputName() {
return $this->getName().'_m';
}
private function getYearInputName() {
return $this->getName().'_y';
}
private function getTimeInputName() {
return $this->getName().'_t';
}
protected function renderInput() {
$min_year = $this->getMinYear();
$max_year = $this->getMaxYear();
$days = range(1, 31);
$days = array_combine($days, $days);
$months = array(
1 => 'Jan',
2 => 'Feb',
3 => 'Mar',
4 => 'Apr',
5 => 'May',
6 => 'Jun',
7 => 'Jul',
8 => 'Aug',
9 => 'Sep',
10 => 'Oct',
11 => 'Nov',
12 => 'Dec',
);
$years = range($this->getMinYear(), $this->getMaxYear());
$years = array_combine($years, $years);
$days_sel = AphrontFormSelectControl::renderSelectTag(
$this->getDayInputValue(),
$days,
array(
'name' => $this->getDayInputName(),
'sigil' => 'day-input',
));
$months_sel = AphrontFormSelectControl::renderSelectTag(
$this->getMonthInputValue(),
$months,
array(
'name' => $this->getMonthInputName(),
'sigil' => 'month-input',
));
$years_sel = AphrontFormSelectControl::renderSelectTag(
$this->getYearInputValue(),
$years,
array(
'name' => $this->getYearInputName(),
'sigil' => 'year-input',
));
$cal_icon = javelin_render_tag(
'a',
array(
'href' => '#',
'class' => 'calendar-button',
'sigil' => 'calendar-button',
),
'');
$time_sel = phutil_render_tag(
'input',
array(
'name' => $this->getTimeInputName(),
'sigil' => 'time-input',
'value' => $this->getTimeInputValue(),
'type' => 'text',
'class' => 'aphront-form-date-time-input',
),
'');
Javelin::initBehavior('fancy-datepicker', array());
return javelin_render_tag(
'div',
array(
'class' => 'aphront-form-date-container',
'sigil' => 'phabricator-date-control',
),
self::renderSingleView(
array(
$days_sel,
$months_sel,
$years_sel,
$cal_icon,
$time_sel,
)));
}
}
diff --git a/src/view/form/control/AphrontFormDividerControl.php b/src/view/form/control/AphrontFormDividerControl.php
index 5a102c2516..7b9090b9a5 100644
--- a/src/view/form/control/AphrontFormDividerControl.php
+++ b/src/view/form/control/AphrontFormDividerControl.php
@@ -1,29 +1,13 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontFormDividerControl extends AphrontFormControl {
protected function getCustomControlClass() {
return 'aphront-form-control-divider';
}
protected function renderInput() {
return '<hr />';
}
}
diff --git a/src/view/form/control/AphrontFormDragAndDropUploadControl.php b/src/view/form/control/AphrontFormDragAndDropUploadControl.php
index e9ccc9a9c0..9f6f521068 100644
--- a/src/view/form/control/AphrontFormDragAndDropUploadControl.php
+++ b/src/view/form/control/AphrontFormDragAndDropUploadControl.php
@@ -1,74 +1,58 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontFormDragAndDropUploadControl extends AphrontFormControl {
private $activatedClass;
public function __construct() {
$this->setControlID(celerity_generate_unique_node_id());
$this->setControlStyle('display: none;');
}
protected function getCustomControlClass() {
return 'aphront-form-drag-and-drop-upload';
}
public function setActivatedClass($class) {
$this->activatedClass = $class;
return $this;
}
protected function renderInput() {
require_celerity_resource('aphront-attached-file-view-css');
$list_id = celerity_generate_unique_node_id();
$files = $this->getValue();
$value = array();
if ($files) {
foreach ($files as $file) {
$view = new AphrontAttachedFileView();
$view->setFile($file);
$value[$file->getPHID()] = array(
'phid' => $file->getPHID(),
'html' => $view->render(),
);
}
}
Javelin::initBehavior(
'aphront-drag-and-drop',
array(
'control' => $this->getControlID(),
'name' => $this->getName(),
'value' => nonempty($value, null),
'list' => $list_id,
'uri' => '/file/dropupload/',
'activatedClass' => $this->activatedClass,
));
return phutil_render_tag(
'div',
array(
'id' => $list_id,
'class' => 'aphront-form-drag-and-drop-file-list',
),
'');
}
}
diff --git a/src/view/form/control/AphrontFormFileControl.php b/src/view/form/control/AphrontFormFileControl.php
index 0840da3535..8e0b75f477 100644
--- a/src/view/form/control/AphrontFormFileControl.php
+++ b/src/view/form/control/AphrontFormFileControl.php
@@ -1,35 +1,19 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontFormFileControl extends AphrontFormControl {
protected function getCustomControlClass() {
return 'aphront-form-file-text';
}
protected function renderInput() {
return phutil_render_tag(
'input',
array(
'type' => 'file',
'name' => $this->getName(),
'disabled' => $this->getDisabled() ? 'disabled' : null,
));
}
}
diff --git a/src/view/form/control/AphrontFormImageControl.php b/src/view/form/control/AphrontFormImageControl.php
index aa31d0de76..d0acadb19a 100644
--- a/src/view/form/control/AphrontFormImageControl.php
+++ b/src/view/form/control/AphrontFormImageControl.php
@@ -1,54 +1,38 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontFormImageControl extends AphrontFormControl {
protected function getCustomControlClass() {
return 'aphront-form-control-image';
}
protected function renderInput() {
$id = celerity_generate_unique_node_id();
return
phutil_render_tag(
'input',
array(
'type' => 'file',
'name' => $this->getName(),
'class' => 'image',
)).
'<div style="clear: both;">'.
phutil_render_tag(
'input',
array(
'type' => 'checkbox',
'name' => 'default_image',
'class' => 'default-image',
'id' => $id,
)).
phutil_render_tag(
'label',
array(
'for' => $id,
),
'Use Default Image instead').
'</div>';
}
}
diff --git a/src/view/form/control/AphrontFormMarkupControl.php b/src/view/form/control/AphrontFormMarkupControl.php
index eec3dbdfba..1952e9ce87 100644
--- a/src/view/form/control/AphrontFormMarkupControl.php
+++ b/src/view/form/control/AphrontFormMarkupControl.php
@@ -1,29 +1,13 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontFormMarkupControl extends AphrontFormControl {
protected function getCustomControlClass() {
return 'aphront-form-control-markup';
}
protected function renderInput() {
return $this->getValue();
}
}
diff --git a/src/view/form/control/AphrontFormPasswordControl.php b/src/view/form/control/AphrontFormPasswordControl.php
index 5a3bc4f775..4cc0dd72d4 100644
--- a/src/view/form/control/AphrontFormPasswordControl.php
+++ b/src/view/form/control/AphrontFormPasswordControl.php
@@ -1,37 +1,21 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontFormPasswordControl extends AphrontFormControl {
protected function getCustomControlClass() {
return 'aphront-form-control-password';
}
protected function renderInput() {
return phutil_render_tag(
'input',
array(
'type' => 'password',
'name' => $this->getName(),
'value' => $this->getValue(),
'disabled' => $this->getDisabled() ? 'disabled' : null,
'id' => $this->getID(),
));
}
}
diff --git a/src/view/form/control/AphrontFormPolicyControl.php b/src/view/form/control/AphrontFormPolicyControl.php
index 2d85812850..4f22da2d48 100644
--- a/src/view/form/control/AphrontFormPolicyControl.php
+++ b/src/view/form/control/AphrontFormPolicyControl.php
@@ -1,105 +1,89 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontFormPolicyControl extends AphrontFormControl {
private $user;
private $object;
private $capability;
private $policies;
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function getUser() {
return $this->user;
}
public function setPolicyObject(PhabricatorPolicyInterface $object) {
$this->object = $object;
return $this;
}
public function setPolicies(array $policies) {
assert_instances_of($policies, 'PhabricatorPolicy');
$this->policies = $policies;
return $this;
}
public function setCapability($capability) {
$this->capability = $capability;
$labels = array(
PhabricatorPolicyCapability::CAN_VIEW => 'Visible To',
PhabricatorPolicyCapability::CAN_EDIT => 'Editable By',
PhabricatorPolicyCapability::CAN_JOIN => 'Joinable By',
);
$this->setLabel(idx($labels, $this->capability, 'Unknown Policy'));
return $this;
}
protected function getCustomControlClass() {
return 'aphront-form-control-policy';
}
protected function getOptions() {
$options = array();
foreach ($this->policies as $policy) {
if (($policy->getPHID() == PhabricatorPolicies::POLICY_PUBLIC) &&
($this->capability != PhabricatorPolicyCapability::CAN_VIEW)) {
// Never expose "Public" for anything except "Can View".
continue;
}
$type_name = PhabricatorPolicyType::getPolicyTypeName($policy->getType());
$options[$type_name][$policy->getPHID()] = $policy->getFullName();
}
return $options;
}
protected function renderInput() {
if (!$this->object) {
throw new Exception("Call setPolicyObject() before rendering!");
}
if (!$this->capability) {
throw new Exception("Call setCapability() before rendering!");
}
$policy = $this->object->getPolicy($this->capability);
if (!$policy) {
// TODO: Make this configurable.
$policy = PhabricatorPolicies::POLICY_USER;
}
$this->setValue($policy);
return AphrontFormSelectControl::renderSelectTag(
$this->getValue(),
$this->getOptions(),
array(
'name' => $this->getName(),
'disabled' => $this->getDisabled() ? 'disabled' : null,
'id' => $this->getID(),
));
}
}
diff --git a/src/view/form/control/AphrontFormRadioButtonControl.php b/src/view/form/control/AphrontFormRadioButtonControl.php
index 8ed847b6c5..14373e0367 100644
--- a/src/view/form/control/AphrontFormRadioButtonControl.php
+++ b/src/view/form/control/AphrontFormRadioButtonControl.php
@@ -1,80 +1,64 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontFormRadioButtonControl extends AphrontFormControl {
private $buttons = array();
public function addButton($value, $label, $caption, $class = null) {
$this->buttons[] = array(
'value' => $value,
'label' => $label,
'caption' => $caption,
'class' => $class,
);
return $this;
}
protected function getCustomControlClass() {
return 'aphront-form-control-radio';
}
protected function renderInput() {
$rows = array();
foreach ($this->buttons as $button) {
$id = celerity_generate_unique_node_id();
$radio = phutil_render_tag(
'input',
array(
'id' => $id,
'type' => 'radio',
'name' => $this->getName(),
'value' => $button['value'],
'checked' => ($button['value'] == $this->getValue())
? 'checked'
: null,
'disabled' => $this->getDisabled() ? 'disabled' : null,
));
$label = phutil_render_tag(
'label',
array(
'for' => $id,
'class' => $button['class'],
),
phutil_escape_html($button['label']));
if (strlen($button['caption'])) {
$label .=
'<div class="aphront-form-radio-caption">'.
phutil_escape_html($button['caption']).
'</div>';
}
$rows[] =
'<tr>'.
'<td>'.$radio.'</td>'.
'<th>'.$label.'</th>'.
'</tr>';
}
return
'<table class="aphront-form-control-radio-layout">'.
implode("\n", $rows).
'</table>';
}
}
diff --git a/src/view/form/control/AphrontFormRecaptchaControl.php b/src/view/form/control/AphrontFormRecaptchaControl.php
index ff74eb4bf5..25bc39792c 100644
--- a/src/view/form/control/AphrontFormRecaptchaControl.php
+++ b/src/view/form/control/AphrontFormRecaptchaControl.php
@@ -1,78 +1,62 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
*
* @phutil-external-symbol function recaptcha_get_html
* @phutil-external-symbol function recaptcha_check_answer
*/
final class AphrontFormRecaptchaControl extends AphrontFormControl {
protected function getCustomControlClass() {
return 'aphront-form-control-recaptcha';
}
protected function shouldRender() {
return self::isRecaptchaEnabled();
}
public static function isRecaptchaEnabled() {
return PhabricatorEnv::getEnvConfig('recaptcha.enabled');
}
private static function requireLib() {
$root = phutil_get_library_root('phabricator');
require_once dirname($root).'/externals/recaptcha/recaptchalib.php';
}
public static function hasCaptchaResponse(AphrontRequest $request) {
return $request->getBool('recaptcha_response_field');
}
public static function processCaptcha(AphrontRequest $request) {
if (!self::isRecaptchaEnabled()) {
return true;
}
self::requireLib();
$challenge = $request->getStr('recaptcha_challenge_field');
$response = $request->getStr('recaptcha_response_field');
$resp = recaptcha_check_answer(
PhabricatorEnv::getEnvConfig('recaptcha.private-key'),
$_SERVER['REMOTE_ADDR'],
$challenge,
$response);
return (bool)@$resp->is_valid;
}
protected function renderInput() {
self::requireLib();
$uri = new PhutilURI(PhabricatorEnv::getEnvConfig('phabricator.base-uri'));
$protocol = $uri->getProtocol();
$use_ssl = ($protocol == 'https');
return recaptcha_get_html(
PhabricatorEnv::getEnvConfig('recaptcha.public-key'),
$error = null,
$use_ssl);
}
}
diff --git a/src/view/form/control/AphrontFormSelectControl.php b/src/view/form/control/AphrontFormSelectControl.php
index 59f18d4ea4..419d8ea352 100644
--- a/src/view/form/control/AphrontFormSelectControl.php
+++ b/src/view/form/control/AphrontFormSelectControl.php
@@ -1,83 +1,67 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontFormSelectControl extends AphrontFormControl {
protected function getCustomControlClass() {
return 'aphront-form-control-select';
}
private $options;
public function setOptions(array $options) {
$this->options = $options;
return $this;
}
public function getOptions() {
return $this->options;
}
protected function renderInput() {
return self::renderSelectTag(
$this->getValue(),
$this->getOptions(),
array(
'name' => $this->getName(),
'disabled' => $this->getDisabled() ? 'disabled' : null,
'id' => $this->getID(),
));
}
public static function renderSelectTag(
$selected,
array $options,
array $attrs = array()) {
$option_tags = self::renderOptions($selected, $options);
return javelin_render_tag(
'select',
$attrs,
implode("\n", $option_tags));
}
private static function renderOptions($selected, array $options) {
$tags = array();
foreach ($options as $value => $thing) {
if (is_array($thing)) {
$tags[] = phutil_render_tag(
'optgroup',
array(
'label' => $value,
),
implode("\n", self::renderOptions($selected, $thing)));
} else {
$tags[] = phutil_render_tag(
'option',
array(
'selected' => ($value == $selected) ? 'selected' : null,
'value' => $value,
),
phutil_escape_html($thing));
}
}
return $tags;
}
}
diff --git a/src/view/form/control/AphrontFormStaticControl.php b/src/view/form/control/AphrontFormStaticControl.php
index c2d1bdde1e..d72fa37586 100644
--- a/src/view/form/control/AphrontFormStaticControl.php
+++ b/src/view/form/control/AphrontFormStaticControl.php
@@ -1,29 +1,13 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontFormStaticControl extends AphrontFormControl {
protected function getCustomControlClass() {
return 'aphront-form-control-static';
}
protected function renderInput() {
return phutil_escape_html($this->getValue());
}
}
diff --git a/src/view/form/control/AphrontFormSubmitControl.php b/src/view/form/control/AphrontFormSubmitControl.php
index 6394c99d46..9ec7593dcd 100644
--- a/src/view/form/control/AphrontFormSubmitControl.php
+++ b/src/view/form/control/AphrontFormSubmitControl.php
@@ -1,52 +1,36 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontFormSubmitControl extends AphrontFormControl {
protected $cancelButton;
public function addCancelButton($href, $label = 'Cancel') {
$this->cancelButton = phutil_render_tag(
'a',
array(
'href' => $href,
'class' => 'button grey',
),
phutil_escape_html($label));
return $this;
}
protected function getCustomControlClass() {
return 'aphront-form-control-submit';
}
protected function renderInput() {
$submit_button = null;
if ($this->getValue()) {
$submit_button = phutil_render_tag(
'button',
array(
'name' => '__submit__',
'disabled' => $this->getDisabled() ? 'disabled' : null,
),
phutil_escape_html($this->getValue()));
}
return $submit_button.$this->cancelButton;
}
}
diff --git a/src/view/form/control/AphrontFormTextAreaControl.php b/src/view/form/control/AphrontFormTextAreaControl.php
index a20f6acca7..378fa5e8bf 100644
--- a/src/view/form/control/AphrontFormTextAreaControl.php
+++ b/src/view/form/control/AphrontFormTextAreaControl.php
@@ -1,85 +1,69 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* @concrete-extensible
*/
class AphrontFormTextAreaControl extends AphrontFormControl {
const HEIGHT_VERY_SHORT = 'very-short';
const HEIGHT_SHORT = 'short';
const HEIGHT_VERY_TALL = 'very-tall';
private $height;
private $readOnly;
private $enableDragAndDropFileUploads;
private $customClass;
public function setHeight($height) {
$this->height = $height;
return $this;
}
public function setReadOnly($read_only) {
$this->readOnly = $read_only;
return $this;
}
protected function getReadOnly() {
return $this->readOnly;
}
protected function getCustomControlClass() {
return 'aphront-form-control-textarea';
}
public function setCustomClass($custom_class) {
$this->customClass = $custom_class;
return $this;
}
protected function renderInput() {
$height_class = null;
switch ($this->height) {
case self::HEIGHT_VERY_SHORT:
case self::HEIGHT_SHORT:
case self::HEIGHT_VERY_TALL:
$height_class = 'aphront-textarea-'.$this->height;
break;
}
$classes = array();
$classes[] = $height_class;
$classes[] = $this->customClass;
$classes = trim(implode(' ', $classes));
return phutil_render_tag(
'textarea',
array(
'name' => $this->getName(),
'disabled' => $this->getDisabled() ? 'disabled' : null,
'readonly' => $this->getReadonly() ? 'readonly' : null,
'class' => $classes,
'style' => $this->getControlStyle(),
'id' => $this->getID(),
),
phutil_escape_html($this->getValue()));
}
}
diff --git a/src/view/form/control/AphrontFormTextControl.php b/src/view/form/control/AphrontFormTextControl.php
index fd66511a7c..76b3cbb32e 100644
--- a/src/view/form/control/AphrontFormTextControl.php
+++ b/src/view/form/control/AphrontFormTextControl.php
@@ -1,57 +1,41 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontFormTextControl extends AphrontFormControl {
private $disableAutocomplete;
private $sigil;
public function setDisableAutocomplete($disable) {
$this->disableAutocomplete = $disable;
return $this;
}
private function getDisableAutocomplete() {
return $this->disableAutocomplete;
}
public function getSigil() {
return $this->sigil;
}
public function setSigil($sigil) {
$this->sigil = $sigil;
return $this;
}
protected function getCustomControlClass() {
return 'aphront-form-control-text';
}
protected function renderInput() {
return javelin_render_tag(
'input',
array(
'type' => 'text',
'name' => $this->getName(),
'value' => $this->getValue(),
'disabled' => $this->getDisabled() ? 'disabled' : null,
'autocomplete' => $this->getDisableAutocomplete() ? 'off' : null,
'id' => $this->getID(),
'sigil' => $this->getSigil(),
));
}
}
diff --git a/src/view/form/control/AphrontFormToggleButtonsControl.php b/src/view/form/control/AphrontFormToggleButtonsControl.php
index fdb9e15495..f4ec32bacd 100644
--- a/src/view/form/control/AphrontFormToggleButtonsControl.php
+++ b/src/view/form/control/AphrontFormToggleButtonsControl.php
@@ -1,68 +1,52 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontFormToggleButtonsControl extends AphrontFormControl {
private $baseURI;
private $param;
private $buttons;
public function setBaseURI(PhutilURI $uri, $param) {
$this->baseURI = $uri;
$this->param = $param;
return $this;
}
public function setButtons(array $buttons) {
$this->buttons = $buttons;
return $this;
}
protected function getCustomControlClass() {
return 'aphront-form-control-togglebuttons';
}
protected function renderInput() {
if (!$this->baseURI) {
throw new Exception('Call setBaseURI() before render()!');
}
$selected = $this->getValue();
$out = array();
foreach ($this->buttons as $value => $label) {
if ($value == $selected) {
$more = ' toggle-selected toggle-fixed';
} else {
$more = null;
}
$out[] = phutil_render_tag(
'a',
array(
'class' => 'toggle'.$more,
'href' => $this->baseURI->alter($this->param, $value),
),
phutil_escape_html($label));
}
return implode('', $out);
}
}
diff --git a/src/view/form/control/AphrontFormTokenizerControl.php b/src/view/form/control/AphrontFormTokenizerControl.php
index cc7672f9cd..d0131ab541 100644
--- a/src/view/form/control/AphrontFormTokenizerControl.php
+++ b/src/view/form/control/AphrontFormTokenizerControl.php
@@ -1,124 +1,108 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontFormTokenizerControl extends AphrontFormControl {
private $datasource;
private $disableBehavior;
private $limit;
private $user;
private $placeholder;
public function setDatasource($datasource) {
$this->datasource = $datasource;
return $this;
}
public function setDisableBehavior($disable) {
$this->disableBehavior = $disable;
return $this;
}
protected function getCustomControlClass() {
return 'aphront-form-control-tokenizer';
}
public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
public function setUser($user) {
$this->user = $user;
return $this;
}
public function setPlaceholder($placeholder) {
$this->placeholder = $placeholder;
return $this;
}
protected function renderInput() {
$name = $this->getName();
$values = nonempty($this->getValue(), array());
if ($this->getID()) {
$id = $this->getID();
} else {
$id = celerity_generate_unique_node_id();
}
$placeholder = null;
if (!$this->placeholder) {
$placeholder = $this->getDefaultPlaceholder();
}
$template = new AphrontTokenizerTemplateView();
$template->setName($name);
$template->setID($id);
$template->setValue($values);
$username = null;
if ($this->user) {
$username = $this->user->getUsername();
}
if (!$this->disableBehavior) {
Javelin::initBehavior('aphront-basic-tokenizer', array(
'id' => $id,
'src' => $this->datasource,
'value' => $values,
'limit' => $this->limit,
'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'),
'username' => $username,
'placeholder' => $placeholder,
));
}
return $template->render();
}
private function getDefaultPlaceholder() {
$datasource = $this->datasource;
$matches = null;
if (!preg_match('@^/typeahead/common/(.*)/$@', $datasource, $matches)) {
return null;
}
$request = $matches[1];
$map = array(
'users' => 'Type a user name...',
'usersorprojects' => 'Type a user or project name...',
'searchowner' => 'Type a user name...',
'accounts' => 'Type a user name...',
'mailable' => 'Type a user or mailing list...',
'allmailable' => 'Type a user or mailing list...',
'searchproject' => 'Type a project name...',
'projects' => 'Type a project name...',
'repositories' => 'Type a repository name...',
'packages' => 'Type a package name...',
'arcanistproject' => 'Type an arc project name...',
);
return idx($map, $request);
}
}
diff --git a/src/view/form/control/PhabricatorRemarkupControl.php b/src/view/form/control/PhabricatorRemarkupControl.php
index 2451a8b8e1..e6e08b5de4 100644
--- a/src/view/form/control/PhabricatorRemarkupControl.php
+++ b/src/view/form/control/PhabricatorRemarkupControl.php
@@ -1,147 +1,131 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorRemarkupControl extends AphrontFormTextAreaControl {
protected function renderInput() {
$id = $this->getID();
if (!$id) {
$id = celerity_generate_unique_node_id();
$this->setID($id);
}
// We need to have this if previews render images, since Ajax can not
// currently ship JS or CSS.
require_celerity_resource('lightbox-attachment-css');
Javelin::initBehavior(
'aphront-drag-and-drop-textarea',
array(
'target' => $id,
'activatedClass' => 'aphront-textarea-drag-and-drop',
'uri' => '/file/dropupload/',
));
Javelin::initBehavior('phabricator-remarkup-assist', array());
Javelin::initBehavior('phabricator-tooltips', array());
$actions = array(
'b' => array(
'tip' => pht('Bold'),
),
'i' => array(
'tip' => pht('Italics'),
),
'tt' => array(
'tip' => pht('Monospaced'),
),
array(
'spacer' => true,
),
'ul' => array(
'tip' => pht('Bulleted List'),
),
'ol' => array(
'tip' => pht('Numbered List'),
),
'code' => array(
'tip' => pht('Code Block'),
),
'table' => array(
'tip' => pht('Table'),
),
'help' => array(
'tip' => pht('Help'),
'align' => 'right',
'href' => PhabricatorEnv::getDoclink(
'article/Remarkup_Reference.html'),
),
);
$buttons = array();
foreach ($actions as $action => $spec) {
if (idx($spec, 'spacer')) {
$buttons[] = phutil_render_tag(
'span',
array(
'class' => 'remarkup-assist-separator',
),
'');
continue;
}
$classes = array();
$classes[] = 'remarkup-assist-button';
if (idx($spec, 'align') == 'right') {
$classes[] = 'remarkup-assist-right';
}
$href = idx($spec, 'href', '#');
if ($href == '#') {
$meta = array('action' => $action);
$mustcapture = true;
$target = null;
} else {
$meta = array();
$mustcapture = null;
$target = '_blank';
}
$tip = idx($spec, 'tip');
if ($tip) {
$meta['tip'] = $tip;
}
$buttons[] = javelin_render_tag(
'a',
array(
'class' => implode(' ', $classes),
'href' => $href,
'sigil' => 'remarkup-assist has-tooltip',
'meta' => $meta,
'mustcapture' => $mustcapture,
'target' => $target,
'tabindex' => -1,
),
phutil_render_tag(
'div',
array(
'class' => 'remarkup-assist autosprite remarkup-assist-'.$action,
),
''));
}
$buttons = phutil_render_tag(
'div',
array(
'class' => 'remarkup-assist-bar',
),
implode('', $buttons));
$this->setCustomClass('remarkup-assist-textarea');
return javelin_render_tag(
'div',
array(
'sigil' => 'remarkup-assist-control',
),
$buttons.
parent::renderInput());
}
}
diff --git a/src/view/layout/AphrontContextBarView.php b/src/view/layout/AphrontContextBarView.php
index 9e4ae771cb..57793ff4c7 100644
--- a/src/view/layout/AphrontContextBarView.php
+++ b/src/view/layout/AphrontContextBarView.php
@@ -1,48 +1,32 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontContextBarView extends AphrontView {
protected $buttons = array();
public function addButton($button) {
$this->buttons[] = $button;
return $this;
}
public function render() {
$view = new AphrontNullView();
$view->appendChild($this->buttons);
require_celerity_resource('aphront-contextbar-view-css');
return
'<div class="aphront-contextbar-view">'.
'<div class="aphront-contextbar-core">'.
'<div class="aphront-contextbar-buttons">'.
$view->render().
'</div>'.
'<div class="aphront-contextbar-content">'.
$this->renderChildren().
'</div>'.
'</div>'.
'<div style="clear: both;"></div>'.
'</div>';
}
}
diff --git a/src/view/layout/AphrontCrumbsView.php b/src/view/layout/AphrontCrumbsView.php
index 523db78e57..2a249dc9b9 100644
--- a/src/view/layout/AphrontCrumbsView.php
+++ b/src/view/layout/AphrontCrumbsView.php
@@ -1,50 +1,34 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontCrumbsView extends AphrontView {
private $crumbs = array();
public function setCrumbs(array $crumbs) {
$this->crumbs = $crumbs;
return $this;
}
public function render() {
require_celerity_resource('aphront-crumbs-view-css');
$out = array();
foreach ($this->crumbs as $crumb) {
$out[] = $this->renderSingleView($crumb);
}
$out = implode(
'<span class="aphront-crumbs-spacer">'.
"\xC2\xBB".
'</span>',
$out);
return
'<div class="aphront-crumbs-view">'.
'<div class="aphront-crumbs-content">'.
$out.
'</div>'.
'</div>';
}
}
diff --git a/src/view/layout/AphrontListFilterView.php b/src/view/layout/AphrontListFilterView.php
index 3b0eadd190..d8ed7f22d9 100644
--- a/src/view/layout/AphrontListFilterView.php
+++ b/src/view/layout/AphrontListFilterView.php
@@ -1,50 +1,34 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontListFilterView extends AphrontView {
private $buttons = array();
public function addButton($button) {
$this->buttons[] = $button;
return $this;
}
public function render() {
require_celerity_resource('aphront-list-filter-view-css');
$buttons = null;
if ($this->buttons) {
$buttons =
'<td class="aphront-list-filter-view-buttons">'.
implode("\n", $this->buttons).
'</td>';
}
return
'<table class="aphront-list-filter-view">'.
'<tr>'.
'<td class="aphront-list-filter-view-controls">'.
$this->renderChildren().
'</td>'.
$buttons.
'</tr>'.
'</table>';
}
}
diff --git a/src/view/layout/AphrontMiniPanelView.php b/src/view/layout/AphrontMiniPanelView.php
index 6fb0501c27..9beb8e65d9 100644
--- a/src/view/layout/AphrontMiniPanelView.php
+++ b/src/view/layout/AphrontMiniPanelView.php
@@ -1,28 +1,12 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontMiniPanelView extends AphrontView {
public function render() {
return
'<div class="aphront-mini-panel-view">'.
$this->renderChildren().
'</div>';
}
}
diff --git a/src/view/layout/AphrontMoreView.php b/src/view/layout/AphrontMoreView.php
index 1857057fce..09f9a099b0 100644
--- a/src/view/layout/AphrontMoreView.php
+++ b/src/view/layout/AphrontMoreView.php
@@ -1,71 +1,55 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontMoreView extends AphrontView {
private $some;
private $more;
private $expandtext;
public function setSome($some) {
$this->some = $some;
return $this;
}
public function setMore($more) {
$this->more = $more;
return $this;
}
public function setExpandText($text) {
$this->expandtext = $text;
return $this;
}
public function render() {
$some = $this->some;
$text = "(Show More\xE2\x80\xA6)";
if ($this->expandtext !== null) {
$text = $this->expandtext;
}
$link = null;
if ($this->more && $this->more != $this->some) {
Javelin::initBehavior('aphront-more');
$link = ' '.javelin_render_tag(
'a',
array(
'sigil' => 'aphront-more-view-show-more',
'mustcapture' => true,
'href' => '#',
'meta' => array(
'more' => $this->more,
),
),
$text);
}
return javelin_render_tag(
'div',
array(
'sigil' => 'aphront-more-view',
),
$some.$link);
}
}
diff --git a/src/view/layout/AphrontPanelView.php b/src/view/layout/AphrontPanelView.php
index d0e0049561..783ad37fa9 100644
--- a/src/view/layout/AphrontPanelView.php
+++ b/src/view/layout/AphrontPanelView.php
@@ -1,121 +1,105 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontPanelView extends AphrontView {
const WIDTH_FULL = 'full';
const WIDTH_FORM = 'form';
const WIDTH_WIDE = 'wide';
private $buttons = array();
private $header;
private $caption;
private $width;
private $classes = array();
private $id;
public function setCreateButton($create_button, $href) {
$this->addButton(
phutil_render_tag(
'a',
array(
'href' => $href,
'class' => 'button green',
),
$create_button));
return $this;
}
public function addClass($class) {
$this->classes[] = $class;
return $this;
}
public function addButton($button) {
$this->buttons[] = $button;
return $this;
}
public function setHeader($header) {
$this->header = $header;
return $this;
}
public function setWidth($width) {
$this->width = $width;
return $this;
}
public function setID($id) {
$this->id = $id;
return $this;
}
public function setCaption($caption) {
$this->caption = $caption;
return $this;
}
public function render() {
if ($this->header !== null) {
$header = '<h1>'.$this->header.'</h1>';
} else {
$header = null;
}
if ($this->caption !== null) {
$caption =
'<div class="aphront-panel-view-caption">'.
$this->caption.
'</div>';
} else {
$caption = null;
}
$buttons = null;
if ($this->buttons) {
$buttons =
'<div class="aphront-panel-view-buttons">'.
implode(" ", $this->buttons).
'</div>';
}
$header_elements =
'<div class="aphront-panel-header">'.
$buttons.$header.$caption.
'</div>';
$table = $this->renderChildren();
require_celerity_resource('aphront-panel-view-css');
$classes = $this->classes;
$classes[] = 'aphront-panel-view';
if ($this->width) {
$classes[] = 'aphront-panel-width-'.$this->width;
}
return phutil_render_tag(
'div',
array(
'class' => implode(' ', $classes),
'id' => $this->id,
),
$header_elements.$table);
}
}
diff --git a/src/view/layout/AphrontSideNavFilterView.php b/src/view/layout/AphrontSideNavFilterView.php
index 78b8002223..b7bb3066b2 100644
--- a/src/view/layout/AphrontSideNavFilterView.php
+++ b/src/view/layout/AphrontSideNavFilterView.php
@@ -1,226 +1,210 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* Like an @{class:AphrontSideNavView}, but with a little bit of logic for the
* common case where you're using the side nav to filter some view of objects.
*
* For example:
*
* $nav = new AphrontSideNavFilterView();
* $nav
* ->setBaseURI($some_uri)
* ->addLabel('Cats')
* ->addFilter('meow', 'Meow')
* ->addFilter('purr', 'Purr')
* ->addSpacer()
* ->addLabel('Dogs')
* ->addFilter('woof', 'Woof')
* ->addFilter('bark', 'Bark');
* $valid_filter = $nav->selectFilter($user_selection, $default = 'meow');
*
*/
final class AphrontSideNavFilterView extends AphrontView {
private $items = array();
private $baseURI;
private $selectedFilter = false;
private $flexNav;
private $flexible;
private $showApplicationMenu;
private $user;
private $currentApplication;
private $active;
public function setActive($active) {
$this->active = $active;
return $this;
}
public function setCurrentApplication(PhabricatorApplication $current) {
$this->currentApplication = $current;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setShowApplicationMenu($show_application_menu) {
$this->showApplicationMenu = $show_application_menu;
return $this;
}
public function setFlexNav($flex_nav) {
$this->flexNav = $flex_nav;
return $this;
}
public function setFlexible($flexible) {
$this->flexible = $flexible;
return $this;
}
public function addFilter(
$key,
$name,
$uri = null,
$relative = false,
$class = null) {
$this->items[] = array(
'filter',
$key,
$name,
'uri' => $uri,
'relative' => $relative,
'class' => $class,
);
return $this;
}
public function addFilters(array $views) {
foreach ($views as $view) {
$uri = isset($view['uri']) ? $view['uri'] : null;
$relative = isset($view['relative']) ? $view['relative'] : false;
$this->addFilter(
$view['key'],
$view['name'],
$uri,
$relative);
}
}
public function addCustomBlock($block) {
$this->items[] = array('custom', null, $block);
return $this;
}
public function addLabel($name) {
$this->items[] = array('label', null, $name);
return $this;
}
public function addSpacer() {
$this->items[] = array('spacer', null, null);
return $this;
}
public function setBaseURI(PhutilURI $uri) {
$this->baseURI = $uri;
return $this;
}
public function getBaseURI() {
return $this->baseURI;
}
public function selectFilter($key, $default = null) {
$this->selectedFilter = $default;
if ($key !== null) {
foreach ($this->items as $item) {
if ($item[0] == 'filter') {
if ($item[1] == $key) {
$this->selectedFilter = $key;
break;
}
}
}
}
return $this->selectedFilter;
}
public function render() {
if ($this->items) {
if (!$this->baseURI) {
throw new Exception("Call setBaseURI() before render()!");
}
if ($this->selectedFilter === false) {
throw new Exception("Call selectFilter() before render()!");
}
}
$view = new AphrontSideNavView();
$view->setFlexNav($this->flexNav);
$view->setFlexible($this->flexible);
$view->setShowApplicationMenu($this->showApplicationMenu);
$view->setActive($this->active);
if ($this->user) {
$view->setUser($this->user);
}
if ($this->currentApplication) {
$view->setCurrentApplication($this->currentApplication);
}
foreach ($this->items as $item) {
list($type, $key, $name) = $item;
switch ($type) {
case 'custom':
$view->addNavItem($name);
break;
case 'spacer':
$view->addNavItem('<br />');
break;
case 'label':
$view->addNavItem(
phutil_render_tag(
'span',
array(),
phutil_escape_html($name)));
break;
case 'filter':
$class = ($key == $this->selectedFilter)
? 'aphront-side-nav-selected'
: null;
$class = trim($class.' '.idx($item, 'class', ''));
if (empty($item['uri'])) {
$href = clone $this->baseURI;
$href->setPath(rtrim($href->getPath().$key, '/').'/');
$href = (string)$href;
} else {
if (empty($item['relative'])) {
$href = $item['uri'];
} else {
$href = clone $this->baseURI;
$href->setPath($href->getPath().$item['uri']);
$href = (string)$href;
}
}
$view->addNavItem(
phutil_render_tag(
'a',
array(
'href' => $href,
'class' => $class,
),
phutil_escape_html($name)));
break;
default:
throw new Exception("Unknown item type '{$type}'.");
}
}
$view->appendChild($this->renderChildren());
return $view->render();
}
}
diff --git a/src/view/layout/AphrontSideNavView.php b/src/view/layout/AphrontSideNavView.php
index 7410c90c2b..b734a0ab00 100644
--- a/src/view/layout/AphrontSideNavView.php
+++ b/src/view/layout/AphrontSideNavView.php
@@ -1,277 +1,261 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontSideNavView extends AphrontView {
private $items = array();
private $flexNav;
private $isFlexible;
private $showApplicationMenu;
private $user;
private $currentApplication;
private $active;
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setShowApplicationMenu($show_application_menu) {
$this->showApplicationMenu = $show_application_menu;
return $this;
}
public function setCurrentApplication(PhabricatorApplication $current) {
$this->currentApplication = $current;
return $this;
}
public function addNavItem($item) {
$this->items[] = $item;
return $this;
}
public function setFlexNav($flex) {
$this->flexNav = $flex;
return $this;
}
public function setFlexible($flexible) {
$this->isFlexible = $flexible;
return $this;
}
public function setActive($active) {
$this->active = $active;
return $this;
}
public function render() {
$view = new AphrontNullView();
$view->appendChild($this->items);
if ($this->flexNav) {
$user = $this->user;
require_celerity_resource('phabricator-nav-view-css');
$nav_classes = array();
$nav_classes[] = 'phabricator-nav';
$app_id = celerity_generate_unique_node_id();
$nav_id = null;
$drag_id = null;
$content_id = celerity_generate_unique_node_id();
$local_id = null;
$local_menu = null;
$main_id = celerity_generate_unique_node_id();
$apps = $this->renderApplications();
$app_menu = phutil_render_tag(
'div',
array(
'class' => 'phabricator-nav-col phabricator-nav-app',
'id' => $app_id,
),
$apps->render());
if ($this->isFlexible) {
$drag_id = celerity_generate_unique_node_id();
$flex_bar = phutil_render_tag(
'div',
array(
'class' => 'phabricator-nav-drag',
'id' => $drag_id,
),
'');
} else {
$flex_bar = null;
}
$nav_menu = null;
if ($this->items) {
$local_id = celerity_generate_unique_node_id();
$nav_classes[] = 'has-local-nav';
$local_menu = phutil_render_tag(
'div',
array(
'class' => 'phabricator-nav-col phabricator-nav-local',
'id' => $local_id,
),
$view->render());
}
Javelin::initBehavior(
'phabricator-nav',
array(
'mainID' => $main_id,
'appID' => $app_id,
'localID' => $local_id,
'dragID' => $drag_id,
'contentID' => $content_id,
));
if ($this->active && $local_id) {
Javelin::initBehavior(
'phabricator-active-nav',
array(
'localID' => $local_id,
));
}
$header_part =
'<div class="phabricator-nav-head">'.
'<div class="phabricator-nav-head-tablet">'.
'<a href="#" class="nav-button nav-button-w nav-button-menu" '.
'id="tablet-menu1"></a>'.
'<a href="#" class="nav-button nav-button-e nav-button-content '.
'nav-button-selected" id="tablet-menu2"></a>'.
'</div>'.
'<div class="phabricator-nav-head-phone">'.
'<a href="#" class="nav-button nav-button-w nav-button-apps" '.
'id="phone-menu1"></button>'.
'<a href="#" class="nav-button nav-button-menu" '.
'id="phone-menu2"></button>'.
'<a href="#" class="nav-button nav-button-e nav-button-content '.
'nav-button-selected" id="phone-menu3"></button>'.
'</div>'.
'</div>';
return $header_part.phutil_render_tag(
'div',
array(
'class' => implode(' ', $nav_classes),
'id' => $main_id,
),
$app_menu.
$local_menu.
$flex_bar.
phutil_render_tag(
'div',
array(
'class' => 'phabricator-nav-content',
'id' => $content_id,
),
$this->renderChildren()));
} else {
require_celerity_resource('aphront-side-nav-view-css');
return
'<table class="aphront-side-nav-view">'.
'<tr>'.
'<th class="aphront-side-nav-navigation">'.
$view->render().
'</th>'.
'<td class="aphront-side-nav-content">'.
$this->renderChildren().
'</td>'.
'</tr>'.
'</table>';
}
}
private function renderApplications() {
$core = array();
$current = $this->currentApplication;
$meta = null;
$group_core = PhabricatorApplication::GROUP_CORE;
$applications = PhabricatorApplication::getAllInstalledApplications();
foreach ($applications as $application) {
if ($application instanceof PhabricatorApplicationApplications) {
$meta = $application;
continue;
}
if ($application->getApplicationGroup() != $group_core) {
continue;
}
if ($application->getApplicationOrder() !== null) {
$core[] = $application;
}
}
$core = msort($core, 'getApplicationOrder');
if ($meta) {
$core[] = $meta;
}
$core = mpull($core, null, 'getPHID');
if ($current && empty($core[$current->getPHID()])) {
array_unshift($core, $current);
}
Javelin::initBehavior('phabricator-tooltips', array());
require_celerity_resource('aphront-tooltip-css');
$apps = array();
foreach ($core as $phid => $application) {
$classes = array();
$classes[] = 'phabricator-nav-app-item';
if ($current && $phid == $current->getPHID()) {
$selected = true;
} else {
$selected = false;
}
$iclasses = array();
$iclasses[] = 'phabricator-nav-app-item-icon';
$style = null;
if ($application->getIconURI()) {
$style = 'background-image: url('.$application->getIconURI().'); '.
'background-size: 30px auto;';
} else {
$iclasses[] = 'autosprite';
$sprite = $application->getAutospriteName();
if ($selected) {
$sprite .= '-selected';
}
$iclasses[] = 'app-'.$sprite;
}
$icon = phutil_render_tag(
'span',
array(
'class' => implode(' ', $iclasses),
'style' => $style,
),
'');
$apps[] = javelin_render_tag(
'a',
array(
'class' => implode(' ', $classes),
'href' => $application->getBaseURI(),
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => $application->getName(),
'align' => 'E',
),
),
$icon.
phutil_escape_html($application->getName()));
}
return id(new AphrontNullView())->appendChild($apps);
}
}
diff --git a/src/view/layout/PhabricatorActionListView.php b/src/view/layout/PhabricatorActionListView.php
index e5fbd1e45f..ff9c0109b8 100644
--- a/src/view/layout/PhabricatorActionListView.php
+++ b/src/view/layout/PhabricatorActionListView.php
@@ -1,70 +1,54 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorActionListView extends AphrontView {
private $actions = array();
private $object;
private $user;
public function setObject(PhabricatorLiskDAO $object) {
$this->object = $object;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function addAction(PhabricatorActionView $view) {
$this->actions[] = $view;
return $this;
}
public function render() {
if (!$this->user) {
throw new Exception("Call setUser() before render()!");
}
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS,
array(
'object' => $this->object,
'actions' => $this->actions,
));
$event->setUser($this->user);
PhutilEventEngine::dispatchEvent($event);
$actions = $event->getValue('actions');
if (!$actions) {
return null;
}
require_celerity_resource('phabricator-action-list-view-css');
return phutil_render_tag(
'ul',
array(
'class' => 'phabricator-action-list-view',
),
$this->renderSingleView($actions));
}
}
diff --git a/src/view/layout/PhabricatorActionView.php b/src/view/layout/PhabricatorActionView.php
index 73dea19d81..0bdae18153 100644
--- a/src/view/layout/PhabricatorActionView.php
+++ b/src/view/layout/PhabricatorActionView.php
@@ -1,161 +1,145 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorActionView extends AphrontView {
private $name;
private $user;
private $icon;
private $href;
private $disabled;
private $workflow;
private $renderAsForm;
public function setHref($href) {
$this->href = $href;
return $this;
}
public function setIcon($icon) {
$this->icon = $icon;
return $this;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function setDisabled($disabled) {
$this->disabled = $disabled;
return $this;
}
public function setWorkflow($workflow) {
$this->workflow = $workflow;
return $this;
}
public function setRenderAsForm($form) {
$this->renderAsForm = $form;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function render() {
$icon = null;
if ($this->icon) {
$icon = phutil_render_tag(
'span',
array(
'class' => 'phabricator-action-view-icon autosprite '.
'action-'.$this->icon,
),
'');
}
if ($this->href) {
if ($this->renderAsForm) {
if (!$this->user) {
throw new Exception(
'Call setUser() when rendering an action as a form.');
}
$item = javelin_render_tag(
'button',
array(
'class' => 'phabricator-action-view-item',
),
phutil_escape_html($this->name));
$item = phabricator_render_form(
$this->user,
array(
'action' => $this->href,
'method' => 'POST',
'sigil' => $this->workflow ? 'workflow' : null,
),
$item);
} else {
$item = javelin_render_tag(
'a',
array(
'href' => $this->href,
'class' => 'phabricator-action-view-item',
'sigil' => $this->workflow ? 'workflow' : null,
),
phutil_escape_html($this->name));
}
} else {
$item = phutil_render_tag(
'span',
array(
'class' => 'phabricator-action-view-item',
),
phutil_escape_html($this->name));
}
$classes = array();
$classes[] = 'phabricator-action-view';
if ($this->disabled) {
$classes[] = 'phabricator-action-view-disabled';
}
return phutil_render_tag(
'li',
array(
'class' => implode(' ', $classes),
),
$icon.$item);
}
public static function getAvailableIcons() {
return array(
'delete',
'download',
'edit',
'file',
'flag-0',
'flag-1',
'flag-2',
'flag-3',
'flag-4',
'flag-5',
'flag-6',
'flag-7',
'flag-ghost',
'fork',
'move',
'new',
'preview',
'subscribe-add',
'subscribe-auto',
'subscribe-delete',
'undo',
'unlock',
'unpublish',
'world',
);
}
}
diff --git a/src/view/layout/PhabricatorAnchorView.php b/src/view/layout/PhabricatorAnchorView.php
index 7afb76c995..b363839617 100644
--- a/src/view/layout/PhabricatorAnchorView.php
+++ b/src/view/layout/PhabricatorAnchorView.php
@@ -1,61 +1,45 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorAnchorView extends AphrontView {
private $anchorName;
private $navigationMarker;
public function setAnchorName($name) {
$this->anchorName = $name;
return $this;
}
public function setNavigationMarker($marker) {
$this->navigationMarker = $marker;
return $this;
}
public function render() {
$marker = null;
if ($this->navigationMarker) {
$marker = javelin_render_tag(
'legend',
array(
'class' => 'phabricator-anchor-navigation-marker',
'sigil' => 'marker',
'meta' => array(
'anchor' => $this->anchorName,
),
),
'');
}
$anchor = phutil_render_tag(
'a',
array(
'name' => $this->anchorName,
'id' => $this->anchorName,
'class' => 'phabricator-anchor-view',
),
'');
return $marker.$anchor;
}
}
diff --git a/src/view/layout/PhabricatorFileLinkListView.php b/src/view/layout/PhabricatorFileLinkListView.php
index 3f147ca4ca..27b5201c2a 100644
--- a/src/view/layout/PhabricatorFileLinkListView.php
+++ b/src/view/layout/PhabricatorFileLinkListView.php
@@ -1,53 +1,37 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFileLinkListView extends AphrontView {
private $files;
public function setFiles(array $files) {
assert_instances_of($files, 'PhabricatorFile');
$this->files = $files;
return $this;
}
private function getFiles() {
return $this->files;
}
public function render() {
$files = $this->getFiles();
if (!$files) {
return '';
}
require_celerity_resource('phabricator-remarkup-css');
$file_links = array();
foreach ($this->getFiles() as $file) {
$view = id(new PhabricatorFileLinkView())
->setFilePHID($file->getPHID())
->setFileName($file->getName())
->setFileDownloadURI($file->getDownloadURI())
->setFileViewURI($file->getBestURI())
->setFileViewable($file->isViewableImage());
$file_links[] = $view->render();
}
return implode('<br />', $file_links);
}
}
diff --git a/src/view/layout/PhabricatorFileLinkView.php b/src/view/layout/PhabricatorFileLinkView.php
index 1b6eff4d10..6869e9cd35 100644
--- a/src/view/layout/PhabricatorFileLinkView.php
+++ b/src/view/layout/PhabricatorFileLinkView.php
@@ -1,98 +1,82 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorFileLinkView extends AphrontView {
private $fileName;
private $fileDownloadURI;
private $fileViewURI;
private $fileViewable;
private $filePHID;
public function setFilePHID($file_phid) {
$this->filePHID = $file_phid;
return $this;
}
private function getFilePHID() {
return $this->filePHID;
}
public function setFileViewable($file_viewable) {
$this->fileViewable = $file_viewable;
return $this;
}
private function getFileViewable() {
return $this->fileViewable;
}
public function setFileViewURI($file_view_uri) {
$this->fileViewURI = $file_view_uri;
return $this;
}
private function getFileViewURI() {
return $this->fileViewURI;
}
public function setFileDownloadURI($file_download_uri) {
$this->fileDownloadURI = $file_download_uri;
return $this;
}
private function getFileDownloadURI() {
return $this->fileDownloadURI;
}
public function setFileName($file_name) {
$this->fileName = $file_name;
return $this;
}
private function getFileName() {
return $this->fileName;
}
public function render() {
require_celerity_resource('phabricator-remarkup-css');
require_celerity_resource('lightbox-attachment-css');
$sigil = null;
$meta = null;
$mustcapture = false;
if ($this->getFileViewable()) {
$mustcapture = true;
$sigil = 'lightboxable';
$meta = array(
'phid' => $this->getFilePHID(),
'viewable' => $this->getFileViewable(),
'uri' => $this->getFileViewURI(),
'dUri' => $this->getFileDownloadURI(),
'name' => $this->getFileName(),
);
}
return javelin_render_tag(
'a',
array(
'href' => $this->getFileViewURI(),
'class' => 'phabricator-remarkup-embed-layout-link',
'sigil' => $sigil,
'meta' => $meta,
'mustcapture' => $mustcapture,
),
phutil_escape_html($this->getFileName())
);
}
}
diff --git a/src/view/layout/PhabricatorHeaderView.php b/src/view/layout/PhabricatorHeaderView.php
index 7b22e923c9..9ddb8d4f38 100644
--- a/src/view/layout/PhabricatorHeaderView.php
+++ b/src/view/layout/PhabricatorHeaderView.php
@@ -1,57 +1,41 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorHeaderView extends AphrontView {
private $objectName;
private $header;
public function setHeader($header) {
$this->header = $header;
return $this;
}
public function setObjectName($object_name) {
$this->objectName = $object_name;
return $this;
}
public function render() {
require_celerity_resource('phabricator-header-view-css');
$header = phutil_escape_html($this->header);
if ($this->objectName) {
$header = phutil_render_tag(
'a',
array(
'href' => '/'.$this->objectName,
),
phutil_escape_html($this->objectName)).' '.$header;
}
return phutil_render_tag(
'h1',
array(
'class' => 'phabricator-header-view',
),
$header);
}
}
diff --git a/src/view/layout/PhabricatorObjectItemListView.php b/src/view/layout/PhabricatorObjectItemListView.php
index 98a4a9bbf2..7ca4e52993 100644
--- a/src/view/layout/PhabricatorObjectItemListView.php
+++ b/src/view/layout/PhabricatorObjectItemListView.php
@@ -1,79 +1,63 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorObjectItemListView extends AphrontView {
private $header;
private $items;
private $pager;
private $noDataString;
public function setHeader($header) {
$this->header = $header;
return $this;
}
public function setPager($pager) {
$this->pager = $pager;
return $this;
}
public function setNoDataString($no_data_string) {
$this->noDataString = $no_data_string;
return $this;
}
public function addItem(PhabricatorObjectItemView $item) {
$this->items[] = $item;
return $this;
}
public function render() {
require_celerity_resource('phabricator-object-item-list-view-css');
$header = phutil_render_tag(
'h1',
array(
'class' => 'phabricator-object-item-list-header',
),
phutil_escape_html($this->header));
if ($this->items) {
$items = $this->renderSingleView($this->items);
} else {
$string = nonempty($this->noDataString, pht('No data.'));
$items = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_NODATA)
->appendChild(phutil_escape_html($string))
->render();
}
$pager = null;
if ($this->pager) {
$pager = $this->renderSingleView($this->pager);
}
return phutil_render_tag(
'div',
array(
'class' => 'phabricator-object-item-list-view',
),
$header.$items.$pager);
}
}
diff --git a/src/view/layout/PhabricatorObjectItemView.php b/src/view/layout/PhabricatorObjectItemView.php
index e15e10f7f3..650ce8e9c0 100644
--- a/src/view/layout/PhabricatorObjectItemView.php
+++ b/src/view/layout/PhabricatorObjectItemView.php
@@ -1,110 +1,94 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorObjectItemView extends AphrontView {
private $header;
private $href;
private $attributes = array();
private $details = array();
private $dates = array();
public function setHref($href) {
$this->href = $href;
return $this;
}
public function getHref() {
return $this->href;
}
public function setHeader($header) {
$this->header = $header;
return $this;
}
public function getHeader() {
return $this->header;
}
public function addDetail($name, $value, $class = null) {
$this->details[] = array(
'name' => $name,
'value' => $value,
);
return $this;
}
public function addAttribute($attribute) {
$this->attributes[] = $attribute;
return $this;
}
public function render() {
$header = phutil_render_tag(
'a',
array(
'href' => $this->href,
'class' => 'phabricator-object-item-name',
),
phutil_escape_html($this->header));
$details = null;
if ($this->details) {
$details = array();
foreach ($this->details as $detail) {
$details[] =
'<dt class="phabricator-object-detail-key">'.
phutil_escape_html($detail['name']).
'</dt>';
$details[] =
'<dd class="phabricator-object-detail-value">'.
$detail['value'].
'</dt>';
}
$details = phutil_render_tag(
'dl',
array(
'class' => 'phabricator-object-detail-list',
),
implode('', $details));
}
$attrs = null;
if ($this->attributes) {
$attrs = array();
foreach ($this->attributes as $attribute) {
$attrs[] = '<li>'.$attribute.'</li>';
}
$attrs = phutil_render_tag(
'ul',
array(
'class' => 'phabricator-object-item-attributes',
),
implode('', $attrs));
}
return phutil_render_tag(
'div',
array(
'class' => 'phabricator-object-item',
),
$header.$details.$attrs);
}
}
diff --git a/src/view/layout/PhabricatorPinboardItemView.php b/src/view/layout/PhabricatorPinboardItemView.php
index a37613a451..a035325d63 100644
--- a/src/view/layout/PhabricatorPinboardItemView.php
+++ b/src/view/layout/PhabricatorPinboardItemView.php
@@ -1,95 +1,79 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorPinboardItemView extends AphrontView {
private $imageURI;
private $uri;
private $header;
private $imageWidth;
private $imageHeight;
public function setHeader($header) {
$this->header = $header;
return $this;
}
public function setURI($uri) {
$this->uri = $uri;
return $this;
}
public function setImageURI($image_uri) {
$this->imageURI = $image_uri;
return $this;
}
public function setImageSize($x, $y) {
$this->imageWidth = $x;
$this->imageHeight = $y;
return $this;
}
public function render() {
$header = null;
if ($this->header) {
$header = hsprintf('<a href="%s">%s</a>', $this->uri, $this->header);
$header = phutil_render_tag(
'div',
array(
'class' => 'phabricator-pinboard-item-header',
),
$header);
}
$image = phutil_render_tag(
'a',
array(
'href' => $this->uri,
'class' => 'phabricator-pinboard-item-image-link',
),
phutil_render_tag(
'img',
array(
'src' => $this->imageURI,
'width' => $this->imageWidth,
'height' => $this->imageHeight,
)));
$content = $this->renderChildren();
if ($content) {
$content = phutil_render_tag(
'div',
array(
'class' => 'phabricator-pinboard-item-content',
),
$content);
}
return phutil_render_tag(
'div',
array(
'class' => 'phabricator-pinboard-item-view',
),
$header.
$image.
$content);
}
}
diff --git a/src/view/layout/PhabricatorPinboardView.php b/src/view/layout/PhabricatorPinboardView.php
index 3848f6a54d..201e775c4d 100644
--- a/src/view/layout/PhabricatorPinboardView.php
+++ b/src/view/layout/PhabricatorPinboardView.php
@@ -1,39 +1,23 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorPinboardView extends AphrontView {
private $items = array();
public function addItem(PhabricatorPinBoardItemView $item) {
$this->items[] = $item;
return $this;
}
public function render() {
require_celerity_resource('phabricator-pinboard-view-css');
return phutil_render_tag(
'div',
array(
'class' => 'phabricator-pinboard-view',
),
$this->renderSingleView($this->items));
}
}
diff --git a/src/view/layout/PhabricatorProfileHeaderView.php b/src/view/layout/PhabricatorProfileHeaderView.php
index c5b66d1889..4c9adb37fc 100644
--- a/src/view/layout/PhabricatorProfileHeaderView.php
+++ b/src/view/layout/PhabricatorProfileHeaderView.php
@@ -1,95 +1,79 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorProfileHeaderView extends AphrontView {
protected $profilePicture;
protected $profileName;
protected $profileDescription;
protected $profileActions = array();
protected $profileStatus;
public function setProfilePicture($picture) {
$this->profilePicture = $picture;
return $this;
}
public function setName($name) {
$this->profileName = $name;
return $this;
}
public function setDescription($description) {
$this->profileDescription = $description;
return $this;
}
public function addAction($action) {
$this->profileActions[] = $action;
return $this;
}
public function setStatus($status) {
$this->profileStatus = $status;
return $this;
}
public function render() {
require_celerity_resource('phabricator-profile-header-css');
$image = null;
if ($this->profilePicture) {
$image = phutil_render_tag(
'div',
array(
'class' => 'profile-header-picture-frame',
'style' => 'background-image: url('.$this->profilePicture.');',
),
'');
}
$description = phutil_escape_html($this->profileDescription);
if ($this->profileStatus != '') {
$description =
'<strong>'.phutil_escape_html($this->profileStatus).'</strong>'.
($description != '' ? ' &mdash; ' : '').
$description;
}
return
'<table class="phabricator-profile-header">
<tr>
<td class="profile-header-name">'.
phutil_escape_html($this->profileName).
'</td>
<td class="profile-header-actions" rowspan="2">'.
self::renderSingleView($this->profileActions).
'</td>
<td class="profile-header-picture" rowspan="2">'.
$image.
'</td>
</tr>
<tr>
<td class="profile-header-description">'.
$description.
'</td>
</tr>
</table>'.
$this->renderChildren();
}
}
diff --git a/src/view/layout/PhabricatorPropertyListView.php b/src/view/layout/PhabricatorPropertyListView.php
index db7975ca87..8e6d765870 100644
--- a/src/view/layout/PhabricatorPropertyListView.php
+++ b/src/view/layout/PhabricatorPropertyListView.php
@@ -1,88 +1,72 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorPropertyListView extends AphrontView {
private $properties = array();
public function addProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function addTextContent($content) {
return $this->appendChild(
phutil_render_tag(
'div',
array(
'class' => 'phabricator-property-list-text-content',
),
$content));
}
public function render() {
require_celerity_resource('phabricator-property-list-view-css');
$items = array();
foreach ($this->properties as $key => $value) {
$items[] = phutil_render_tag(
'dt',
array(
'class' => 'phabricator-property-key',
),
phutil_escape_html($key));
$items[] = phutil_render_tag(
'dd',
array(
'class' => 'phabricator-property-value',
),
$this->renderSingleView($value));
}
$list = phutil_render_tag(
'dl',
array(
),
$this->renderSingleView($items));
$content = $this->renderChildren();
if (strlen($content)) {
$content = phutil_render_tag(
'div',
array(
'class' => 'phabricator-property-list-content',
),
$content);
}
return phutil_render_tag(
'div',
array(
'class' => 'phabricator-property-list-view',
),
$list.
// NOTE: We need this (which is basically a "clear: both;" div) to make
// sure the property list is taller than the action list for objects with
// few properties but many actions. Otherwise, the action list may
// obscure the document content.
'<div class="phabriator-property-list-view-end"></div>').
$content;
}
}
diff --git a/src/view/layout/PhabricatorSourceCodeView.php b/src/view/layout/PhabricatorSourceCodeView.php
index d82856e931..6ecfce0bd8 100644
--- a/src/view/layout/PhabricatorSourceCodeView.php
+++ b/src/view/layout/PhabricatorSourceCodeView.php
@@ -1,68 +1,52 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorSourceCodeView extends AphrontView {
private $lines;
public function setLines(array $lines) {
$this->lines = $lines;
return $this;
}
public function render() {
require_celerity_resource('phabricator-source-code-view-css');
require_celerity_resource('syntax-highlighting-css');
Javelin::initBehavior('phabricator-oncopy', array());
$line_number = 1;
$rows = array();
foreach ($this->lines as $line) {
// TODO: Provide nice links.
$rows[] =
'<tr>'.
'<th class="phabricator-source-line">'.
phutil_escape_html($line_number).
'</th>'.
'<td class="phabricator-source-code">'.
"\xE2\x80\x8B".
$line.
'</td>'.
'</tr>';
$line_number++;
}
$classes = array();
$classes[] = 'phabricator-source-code-view';
$classes[] = 'remarkup-code';
$classes[] = 'PhabricatorMonospaced';
return phutil_render_tag(
'table',
array(
'class' => implode(' ', $classes),
),
implode('', $rows));
}
}
diff --git a/src/view/layout/PhabricatorTransactionView.php b/src/view/layout/PhabricatorTransactionView.php
index 50529537da..0ee7f76fda 100644
--- a/src/view/layout/PhabricatorTransactionView.php
+++ b/src/view/layout/PhabricatorTransactionView.php
@@ -1,169 +1,153 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorTransactionView extends AphrontView {
private $user;
private $imageURI;
private $actions = array();
private $epoch;
private $contentSource;
private $anchorName;
private $anchorText;
private $isPreview;
private $classes = array();
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setImageURI($uri) {
$this->imageURI = $uri;
return $this;
}
public function setActions(array $actions) {
$this->actions = $actions;
return $this;
}
public function setEpoch($epoch) {
$this->epoch = $epoch;
return $this;
}
public function setContentSource(PhabricatorContentSource $source) {
$this->contentSource = $source;
return $this;
}
public function setAnchor($anchor_name, $anchor_text) {
$this->anchorName = $anchor_name;
$this->anchorText = $anchor_text;
return $this;
}
public function addClass($class) {
$this->classes[] = $class;
return $this;
}
public function setIsPreview($preview) {
$this->isPreview = $preview;
return $this;
}
public function render() {
if (!$this->user) {
throw new Exception("Call setUser() before render()!");
}
require_celerity_resource('phabricator-transaction-view-css');
$info = $this->renderTransactionInfo();
$actions = $this->renderTransactionActions();
$style = $this->renderTransactionStyle();
$content = $this->renderTransactionContent();
$classes = phutil_escape_html(implode(' ', $this->classes));
$transaction_id = $this->anchorName ? 'anchor-'.$this->anchorName : null;
return phutil_render_tag(
'div',
array(
'class' => 'phabricator-transaction-view',
'id' => $transaction_id,
'style' => $style,
),
'<div class="phabricator-transaction-detail '.$classes.'">'.
'<div class="phabricator-transaction-header">'.
$info.
$actions.
'</div>'.
$content.
'</div>');
}
private function renderTransactionInfo() {
$info = array();
if ($this->contentSource) {
$content_source = new PhabricatorContentSourceView();
$content_source->setContentSource($this->contentSource);
$content_source->setUser($this->user);
$source = $content_source->render();
if ($source) {
$info[] = $source;
}
}
if ($this->isPreview) {
$info[] = 'PREVIEW';
} else if ($this->epoch) {
$info[] = phabricator_datetime($this->epoch, $this->user);
}
if ($this->anchorName) {
Javelin::initBehavior('phabricator-watch-anchor');
$anchor = id(new PhabricatorAnchorView())
->setAnchorName($this->anchorName)
->render();
$info[] = $anchor.phutil_render_tag(
'a',
array(
'href' => '#'.$this->anchorName,
),
phutil_escape_html($this->anchorText));
}
$info = implode(' &middot; ', $info);
return
'<span class="phabricator-transaction-info">'.
$info.
'</span>';
}
private function renderTransactionActions() {
return implode('', $this->actions);
}
private function renderTransactionStyle() {
if ($this->imageURI) {
return 'background-image: url('.$this->imageURI.');';
} else {
return null;
}
}
private function renderTransactionContent() {
$content = $this->renderChildren();
if (!$content) {
return null;
}
return
'<div class="phabricator-transaction-content">'.
$content.
'</div>';
}
}
diff --git a/src/view/layout/headsup/AphrontHeadsupActionListView.php b/src/view/layout/headsup/AphrontHeadsupActionListView.php
index 618ddab704..d3777d362b 100644
--- a/src/view/layout/headsup/AphrontHeadsupActionListView.php
+++ b/src/view/layout/headsup/AphrontHeadsupActionListView.php
@@ -1,44 +1,28 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontHeadsupActionListView extends AphrontView {
private $actions;
public function setActions(array $actions) {
$this->actions = $actions;
return $this;
}
public function render() {
require_celerity_resource('aphront-headsup-action-list-view-css');
$actions = array();
foreach ($this->actions as $action_view) {
$actions[] = $action_view->render();
}
$actions = implode("\n", $actions);
return
'<div class="aphront-headsup-action-list">'.
$actions.
'</div>';
}
}
diff --git a/src/view/layout/headsup/AphrontHeadsupActionView.php b/src/view/layout/headsup/AphrontHeadsupActionView.php
index adca078c3d..a7bff0ebc4 100644
--- a/src/view/layout/headsup/AphrontHeadsupActionView.php
+++ b/src/view/layout/headsup/AphrontHeadsupActionView.php
@@ -1,95 +1,79 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontHeadsupActionView extends AphrontView {
private $name;
private $class;
private $uri;
private $workflow;
private $instant;
private $user;
public function setName($name) {
$this->name = $name;
return $this;
}
public function setClass($class) {
$this->class = $class;
return $this;
}
public function setURI($uri) {
$this->uri = $uri;
return $this;
}
public function setWorkflow($workflow) {
$this->workflow = $workflow;
return $this;
}
public function setInstant($instant) {
$this->instant = $instant;
return $this;
}
public function setUser($user) {
$this->user = $user;
return $this;
}
public function render() {
if ($this->instant) {
$button_class = $this->class.' link';
return phabricator_render_form(
$this->user,
array(
'action' => $this->uri,
'method' => 'post',
'style' => 'display: inline',
),
'<button class="'.$button_class.'">'.
phutil_escape_html($this->name).
'</button>'
);
}
if ($this->uri) {
$tag = 'a';
} else {
$tag = 'span';
}
$attrs = array(
'href' => $this->uri,
'class' => $this->class,
);
if ($this->workflow) {
$attrs['sigil'] = 'workflow';
}
return javelin_render_tag(
$tag,
$attrs,
phutil_escape_html($this->name));
}
}
diff --git a/src/view/layout/headsup/AphrontHeadsupView.php b/src/view/layout/headsup/AphrontHeadsupView.php
index 5919ed3dcd..3b1e548144 100644
--- a/src/view/layout/headsup/AphrontHeadsupView.php
+++ b/src/view/layout/headsup/AphrontHeadsupView.php
@@ -1,116 +1,100 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontHeadsupView extends AphrontView {
private $actionList;
private $header;
private $properties;
private $objectName;
private $hasKeyboardShortcuts;
public function setActionList(AphrontHeadsupActionListView $action_list) {
$this->actionList = $action_list;
return $this;
}
public function setHeader($header) {
$this->header = $header;
return $this;
}
public function setProperties(array $dict) {
$this->properties = $dict;
return $this;
}
public function setObjectName($name) {
$this->objectName = $name;
return $this;
}
public function setHasKeyboardShortcuts($has_keyboard_shortcuts) {
$this->hasKeyboardShortcuts = $has_keyboard_shortcuts;
return $this;
}
public function getHasKeyboardShortcuts() {
return $this->hasKeyboardShortcuts;
}
public function render() {
$header =
'<h1>'.
phutil_render_tag(
'a',
array(
'href' => '/'.$this->objectName,
'class' => 'aphront-headsup-object-name',
),
phutil_escape_html($this->objectName)).
' '.
phutil_escape_html($this->header).
'</h1>';
require_celerity_resource('aphront-headsup-view-css');
$shortcuts = null;
if ($this->hasKeyboardShortcuts) {
$shortcuts =
'<div class="aphront-headsup-keyboard-shortcuts">'.
id(new AphrontKeyboardShortcutsAvailableView())->render().
'</div>';
}
$prop_table = null;
if ($this->properties) {
$prop_table = array();
foreach ($this->properties as $key => $value) {
$prop_table[] =
'<tr>'.
'<th>'.phutil_escape_html($key.':').'</th>'.
'<td>'.$value.'</td>'.
'</tr>';
}
$prop_table =
'<table class="aphront-headsup-property-table">'.
implode("\n", $prop_table).
'</table>';
}
$children = $this->renderChildren();
if (strlen($children)) {
$children =
'<div class="aphront-headsup-details">'.
$children.
'</div>';
}
return
'<div class="aphront-headsup-panel">'.
self::renderSingleView($this->actionList).
$shortcuts.
'<div class="aphront-headsup-core">'.
$header.
$prop_table.
$children.
'</div>'.
'</div>';
}
}
diff --git a/src/view/page/AphrontPageView.php b/src/view/page/AphrontPageView.php
index 6b36e50bcc..8381176ee1 100644
--- a/src/view/page/AphrontPageView.php
+++ b/src/view/page/AphrontPageView.php
@@ -1,96 +1,80 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
abstract class AphrontPageView extends AphrontView {
private $title;
public function setTitle($title) {
$this->title = $title;
return $this;
}
public function getTitle() {
$title = $this->title;
if (is_array($title)) {
$title = implode(" \xC2\xB7 ", $title);
}
return $title;
}
protected function getHead() {
return '';
}
protected function getBody() {
return $this->renderChildren();
}
protected function getTail() {
return '';
}
protected function willRenderPage() {
return;
}
protected function willSendResponse($response) {
return $response;
}
protected function getBodyClasses() {
return null;
}
public function render() {
$this->willRenderPage();
$title = phutil_escape_html($this->getTitle());
$head = $this->getHead();
$body = $this->getBody();
$tail = $this->getTail();
$body_classes = $this->getBodyClasses();
$body = phutil_render_tag(
'body',
array(
'class' => nonempty($body_classes, null),
),
$body.$tail);
$response = <<<EOHTML
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>{$title}</title>
{$head}
</head>
{$body}
</html>
EOHTML;
$response = $this->willSendResponse($response);
return $response;
}
}
diff --git a/src/view/page/AphrontRequestFailureView.php b/src/view/page/AphrontRequestFailureView.php
index 60506b4952..bba198accb 100644
--- a/src/view/page/AphrontRequestFailureView.php
+++ b/src/view/page/AphrontRequestFailureView.php
@@ -1,43 +1,27 @@
<?php
-/*
- * Copyright 2011 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontRequestFailureView extends AphrontView {
private $header;
public function setHeader($header) {
$this->header = $header;
return $this;
}
final public function render() {
require_celerity_resource('aphront-request-failure-view-css');
return
'<div class="aphront-request-failure-view">'.
'<div class="aphront-request-failure-head">'.
'<h1>'.phutil_escape_html($this->header).'</h1>'.
'</div>'.
'<div class="aphront-request-failure-body">'.
$this->renderChildren().
'</div>'.
'</div>';
}
}
diff --git a/src/view/page/PhabricatorBarePageView.php b/src/view/page/PhabricatorBarePageView.php
index 755ecd4388..3a9c0ad4f4 100644
--- a/src/view/page/PhabricatorBarePageView.php
+++ b/src/view/page/PhabricatorBarePageView.php
@@ -1,121 +1,105 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* This is a bare HTML page view which has access to Phabricator page
* infrastructure like Celerity, but no content or builtin static resources.
* You basically get a valid HMTL5 document and an empty body tag.
*
* @concrete-extensible
*/
class PhabricatorBarePageView extends AphrontPageView {
private $request;
private $controller;
private $frameable;
private $deviceReady;
private $bodyContent;
public function setController(AphrontController $controller) {
$this->controller = $controller;
return $this;
}
public function getController() {
return $this->controller;
}
public function setRequest(AphrontRequest $request) {
$this->request = $request;
return $this;
}
public function getRequest() {
return $this->request;
}
public function setFrameable($frameable) {
$this->frameable = $frameable;
return $this;
}
public function getFrameable() {
return $this->frameable;
}
public function setDeviceReady($device_ready) {
$this->deviceReady = $device_ready;
return $this;
}
public function getDeviceReady() {
return $this->deviceReady;
}
protected function willRenderPage() {
// We render this now to resolve static resources so they can appear in the
// document head.
$this->bodyContent = $this->renderChildren();
}
protected function getHead() {
$framebust = null;
if (!$this->getFrameable()) {
$framebust = '(top != self) && top.location.replace(self.location.href);';
}
$viewport_tag = null;
if (PhabricatorEnv::getEnvConfig('preview.viewport-meta-tag') ||
$this->getDeviceReady()) {
$viewport_tag = phutil_render_tag(
'meta',
array(
'name' => 'viewport',
'content' => 'width=device-width, '.
'initial-scale=1, '.
'maximum-scale=1',
));
}
$response = CelerityAPI::getStaticResourceResponse();
$head = array(
$viewport_tag,
'<script type="text/javascript">'.
$framebust.
'window.__DEV__=1;'.
'</script>',
$response->renderResourcesOfType('css'),
);
return implode("\n", $head);
}
protected function getBody() {
return $this->bodyContent;
}
protected function getTail() {
$response = CelerityAPI::getStaticResourceResponse();
return $response->renderResourcesOfType('js');
}
}
diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php
index 1dec935388..56f237187a 100644
--- a/src/view/page/PhabricatorStandardPageView.php
+++ b/src/view/page/PhabricatorStandardPageView.php
@@ -1,464 +1,448 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
/**
* This is a standard Phabricator page with menus, Javelin, DarkConsole, and
* basic styles.
*
*/
final class PhabricatorStandardPageView extends PhabricatorBarePageView {
private $baseURI;
private $applicationName;
private $glyph;
private $menuContent;
private $showChrome = true;
private $disableConsole;
private $searchDefaultScope;
private $pageObjects = array();
public function setApplicationName($application_name) {
$this->applicationName = $application_name;
return $this;
}
public function setDisableConsole($disable) {
$this->disableConsole = $disable;
return $this;
}
public function getApplicationName() {
return $this->applicationName;
}
public function setBaseURI($base_uri) {
$this->baseURI = $base_uri;
return $this;
}
public function getBaseURI() {
return $this->baseURI;
}
public function setShowChrome($show_chrome) {
$this->showChrome = $show_chrome;
return $this;
}
public function getShowChrome() {
return $this->showChrome;
}
public function setSearchDefaultScope($search_default_scope) {
$this->searchDefaultScope = $search_default_scope;
return $this;
}
public function getSearchDefaultScope() {
return $this->searchDefaultScope;
}
public function appendPageObjects(array $objs) {
foreach ($objs as $obj) {
$this->pageObjects[] = $obj;
}
}
public function getTitle() {
$use_glyph = true;
$request = $this->getRequest();
if ($request) {
$user = $request->getUser();
if ($user && $user->loadPreferences()->getPreference(
PhabricatorUserPreferences::PREFERENCE_TITLES) !== 'glyph') {
$use_glyph = false;
}
}
return ($use_glyph ?
$this->getGlyph() : '['.$this->getApplicationName().']').
' '.parent::getTitle();
}
protected function willRenderPage() {
parent::willRenderPage();
if (!$this->getRequest()) {
throw new Exception(
"You must set the Request to render a PhabricatorStandardPageView.");
}
$console = $this->getConsole();
require_celerity_resource('phabricator-core-css');
require_celerity_resource('autosprite-css');
require_celerity_resource('phabricator-core-buttons-css');
require_celerity_resource('phabricator-standard-page-view');
$current_token = null;
$request = $this->getRequest();
if ($request) {
$user = $request->getUser();
if ($user) {
$current_token = $user->getCSRFToken();
}
}
Javelin::initBehavior('workflow', array());
$download_form = phabricator_render_form_magic($user);
$default_img_uri =
PhabricatorEnv::getCDNURI('/rsrc/image/icon/fatcow/document_black.png');
Javelin::initBehavior(
'lightbox-attachments',
array(
'defaultImageUri' => $default_img_uri,
'downloadForm' => $download_form,
));
Javelin::initBehavior('toggle-class', array());
Javelin::initBehavior('konami', array());
Javelin::initBehavior(
'refresh-csrf',
array(
'tokenName' => AphrontRequest::getCSRFTokenName(),
'header' => AphrontRequest::getCSRFHeaderName(),
'current' => $current_token,
));
Javelin::initBehavior('device', array('id' => 'base-page'));
if ($console) {
require_celerity_resource('aphront-dark-console-css');
Javelin::initBehavior(
'dark-console',
array(
'uri' => '/~/',
'request_uri' => $request ? (string) $request->getRequestURI() : '/',
));
// Change this to initBehavior when there is some behavior to initialize
require_celerity_resource('javelin-behavior-error-log');
}
$this->menuContent = $this->renderMainMenu();
}
protected function getHead() {
$monospaced = PhabricatorEnv::getEnvConfig('style.monospace');
$request = $this->getRequest();
if ($request) {
$user = $request->getUser();
if ($user) {
$monospaced = nonempty(
$user->loadPreferences()->getPreference(
PhabricatorUserPreferences::PREFERENCE_MONOSPACED),
$monospaced);
}
}
$response = CelerityAPI::getStaticResourceResponse();
$head = array(
parent::getHead(),
'<style type="text/css">'.
'.PhabricatorMonospaced { font: '.$monospaced.'; }'.
'</style>',
$response->renderSingleResource('javelin-magical-init'),
);
return implode("\n", $head);
}
public function setGlyph($glyph) {
$this->glyph = $glyph;
return $this;
}
public function getGlyph() {
return $this->glyph;
}
protected function willSendResponse($response) {
$response = parent::willSendResponse($response);
$console = $this->getRequest()->getApplicationConfiguration()->getConsole();
if ($console) {
$response = str_replace(
'<darkconsole />',
$console->render($this->getRequest()),
$response);
}
return $response;
}
protected function getBody() {
$console = $this->getConsole();
$login_stuff = null;
$request = $this->getRequest();
$user = null;
if ($request) {
$user = $request->getUser();
// NOTE: user may not be set here if we caught an exception early
// in the execution workflow.
if ($user && $user->getPHID()) {
$login_stuff =
phutil_render_tag(
'a',
array(
'href' => '/p/'.$user->getUsername().'/',
),
phutil_escape_html($user->getUsername())).
' &middot; '.
'<a href="/settings/">Settings</a>'.
' &middot; '.
phabricator_render_form(
$user,
array(
'action' => '/search/',
'method' => 'post',
'style' => 'display: inline',
),
'<div class="menu-section menu-section-search">'.
'<div class="menu-search-container">'.
'<input type="text" name="query" id="standard-search-box" />'.
'<button id="standard-search-button">Search</button>'.
'</div>'.
'</div>'.
' in '.
AphrontFormSelectControl::renderSelectTag(
$this->getSearchDefaultScope(),
PhabricatorSearchScope::getScopeOptions(),
array(
'name' => 'scope',
)).
' '.
'<button>Search</button>');
}
}
$header_chrome = null;
$footer_chrome = null;
if ($this->getShowChrome()) {
$header_chrome = $this->menuContent;
if (!$this->getDeviceReady()) {
$footer_chrome = $this->renderFooter();
}
}
$developer_warning = null;
if (PhabricatorEnv::getEnvConfig('phabricator.show-error-callout') &&
DarkConsoleErrorLogPluginAPI::getErrors()) {
$developer_warning =
'<div class="aphront-developer-error-callout">'.
'This page raised PHP errors. Find them in DarkConsole '.
'or the error log.'.
'</div>';
}
$agent = idx($_SERVER, 'HTTP_USER_AGENT');
// Try to guess the device resolution based on UA strings to avoid a flash
// of incorrectly-styled content.
$device_guess = 'device-desktop';
if (preg_match('/iPhone|iPod/', $agent)) {
$device_guess = 'device-phone';
} else if (preg_match('/iPad/', $agent)) {
$device_guess = 'device-tablet';
}
$classes = array(
'phabricator-standard-page',
$device_guess,
);
$classes = implode(' ', $classes);
return
phutil_render_tag(
'div',
array(
'id' => 'base-page',
'class' => $classes,
),
$header_chrome.
'<div class="phabricator-standard-page-body">'.
($console ? '<darkconsole />' : null).
$developer_warning.
parent::getBody().
'<div style="clear: both;"></div>'.
'</div>').
$footer_chrome;
}
protected function getTail() {
$request = $this->getRequest();
$user = $request->getUser();
$container = null;
if (PhabricatorEnv::getEnvConfig('notification.enabled') &&
$user->isLoggedIn()) {
$aphlict_object_id = celerity_generate_unique_node_id();
$aphlict_container_id = celerity_generate_unique_node_id();
$client_uri = PhabricatorEnv::getEnvConfig('notification.client-uri');
$client_uri = new PhutilURI($client_uri);
if ($client_uri->getDomain() == 'localhost') {
$this_host = $this->getRequest()->getHost();
$this_host = new PhutilURI('http://'.$this_host.'/');
$client_uri->setDomain($this_host->getDomain());
}
$enable_debug = PhabricatorEnv::getEnvConfig('notification.debug');
Javelin::initBehavior(
'aphlict-listen',
array(
'id' => $aphlict_object_id,
'containerID' => $aphlict_container_id,
'server' => $client_uri->getDomain(),
'port' => $client_uri->getPort(),
'debug' => $enable_debug,
'pageObjects' => array_fill_keys($this->pageObjects, true),
));
$container = phutil_render_tag(
'div',
array(
'id' => $aphlict_container_id,
'style' => 'position: absolute; width: 0; height: 0;',
),
'');
}
$response = CelerityAPI::getStaticResourceResponse();
$tail = array(
parent::getTail(),
$container,
$response->renderHTMLFooter(),
);
return implode("\n", $tail);
}
protected function getBodyClasses() {
$classes = array();
if (!$this->getShowChrome()) {
$classes[] = 'phabricator-chromeless-page';
}
return implode(' ', $classes);
}
private function getConsole() {
if ($this->disableConsole) {
return null;
}
return $this->getRequest()->getApplicationConfiguration()->getConsole();
}
private function renderMainMenu() {
$request = $this->getRequest();
$user = $request->getUser();
$menu = new PhabricatorMainMenuView();
$menu->setUser($user);
$keyboard_config = array(
'helpURI' => '/help/keyboardshortcut/',
);
if ($user->isLoggedIn()) {
$search = new PhabricatorMainMenuSearchView();
$search->setUser($user);
$search->setScope($this->getSearchDefaultScope());
$menu->appendChild($search);
$pref_shortcut = PhabricatorUserPreferences::PREFERENCE_SEARCH_SHORTCUT;
if ($user->loadPreferences()->getPreference($pref_shortcut, true)) {
$keyboard_config['searchID'] = $search->getID();
}
}
Javelin::initBehavior('phabricator-keyboard-shortcuts', $keyboard_config);
$applications = PhabricatorApplication::getAllInstalledApplications();
$icon_views = array();
foreach ($applications as $application) {
$icon_views[] = $application->buildMainMenuItems(
$this->getRequest()->getUser(),
$this->getController());
}
$icon_views = array_mergev($icon_views);
$icon_views = msort($icon_views, 'getSortOrder');
$menu->appendChild($icon_views);
return $menu->render();
}
public function renderFooter() {
$console = $this->getConsole();
$foot_links = array();
$version = PhabricatorEnv::getEnvConfig('phabricator.version');
$foot_links[] =
'<a href="http://phabricator.org/">Phabricator</a> '.
phutil_escape_html($version);
$foot_links[] =
'<a href="https://secure.phabricator.com/maniphest/task/create/">'.
'Report a Bug'.
'</a>';
if (PhabricatorEnv::getEnvConfig('darkconsole.enabled') &&
!PhabricatorEnv::getEnvConfig('darkconsole.always-on')) {
if ($console) {
$link = javelin_render_tag(
'a',
array(
'href' => '/~/',
'sigil' => 'workflow',
),
'Disable DarkConsole');
} else {
$link = javelin_render_tag(
'a',
array(
'href' => '/~/',
'sigil' => 'workflow',
),
'Enable DarkConsole');
}
$foot_links[] = $link;
}
$foot_links = implode(' &middot; ', $foot_links);
return
'<div class="phabricator-page-foot">'.
$foot_links.
'</div>';
}
}
diff --git a/src/view/page/menu/PhabricatorMainMenuGroupView.php b/src/view/page/menu/PhabricatorMainMenuGroupView.php
index 9767f42071..98f3048b80 100644
--- a/src/view/page/menu/PhabricatorMainMenuGroupView.php
+++ b/src/view/page/menu/PhabricatorMainMenuGroupView.php
@@ -1,55 +1,39 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorMainMenuGroupView extends AphrontView {
private $collapsible = true;
private $classes = array();
public function addClass($class) {
$this->classes[] = $class;
return $this;
}
public function setCollapsible($collapsible) {
$this->collapsible = $collapsible;
return $this;
}
public function render() {
$classes = array(
'phabricator-main-menu-group',
);
if ($this->collapsible) {
$classes[] = 'phabricator-main-menu-collapsible';
}
if ($this->classes) {
$classes = array_merge($classes, $this->classes);
}
return phutil_render_tag(
'div',
array(
'class' => implode(' ', $classes),
),
$this->renderChildren());
}
}
diff --git a/src/view/page/menu/PhabricatorMainMenuIconView.php b/src/view/page/menu/PhabricatorMainMenuIconView.php
index cb8683609e..a94c903b55 100644
--- a/src/view/page/menu/PhabricatorMainMenuIconView.php
+++ b/src/view/page/menu/PhabricatorMainMenuIconView.php
@@ -1,109 +1,93 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorMainMenuIconView extends AphrontView {
private $classes = array();
private $href;
private $name;
private $sortOrder = 0.5;
private $workflow;
private $style;
public function setName($name) {
$this->name = $name;
return $this;
}
public function setWorkflow($workflow) {
$this->workflow = $workflow;
return $this;
}
public function getName() {
return $this->name;
}
public function setHref($href) {
$this->href = $href;
return $this;
}
public function getHref() {
return $this->href;
}
public function addClass($class) {
$this->classes[] = $class;
return $this;
}
public function addStyle($style) {
$this->style = $style;
return $this;
}
/**
* Provide a float, where 0.0 is the profile item and 1.0 is the logout
* item. Normally you should pick something between the two.
*
* @param float Sort order.
* @return this
*/
public function setSortOrder($sort_order) {
$this->sortOrder = $sort_order;
return $this;
}
public function getSortOrder() {
return $this->sortOrder;
}
public function render() {
$name = $this->getName();
$href = $this->getHref();
$classes = $this->classes;
$classes[] = 'phabricator-main-menu-icon';
$label = javelin_render_tag(
'a',
array(
'href' => $href,
'class' => 'phabricator-main-menu-icon-label',
),
phutil_escape_html($name));
$item = javelin_render_tag(
'a',
array(
'href' => $href,
'class' => implode(' ', $classes),
'style' => $this->style,
'sigil' => $this->workflow ? 'workflow' : null,
),
'');
$group = new PhabricatorMainMenuGroupView();
$group->appendChild($item);
$group->appendChild($label);
return $group->render();
}
}
diff --git a/src/view/page/menu/PhabricatorMainMenuSearchView.php b/src/view/page/menu/PhabricatorMainMenuSearchView.php
index 6296cd7717..a5f7473f9b 100644
--- a/src/view/page/menu/PhabricatorMainMenuSearchView.php
+++ b/src/view/page/menu/PhabricatorMainMenuSearchView.php
@@ -1,104 +1,88 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorMainMenuSearchView extends AphrontView {
private $user;
private $scope;
private $id;
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setScope($scope) {
$this->scope = $scope;
return $this;
}
public function getID() {
if (!$this->id) {
$this->id = celerity_generate_unique_node_id();
}
return $this->id;
}
public function render() {
$user = $this->user;
$target_id = celerity_generate_unique_node_id();
$search_id = $this->getID();
$input = phutil_render_tag(
'input',
array(
'type' => 'text',
'name' => 'query',
'id' => $search_id,
'autocomplete' => 'off',
));
$scope = $this->scope;
$target = javelin_render_tag(
'div',
array(
'id' => $target_id,
'class' => 'phabricator-main-menu-search-target',
),
'');
Javelin::initBehavior(
'phabricator-search-typeahead',
array(
'id' => $target_id,
'input' => $search_id,
'src' => '/typeahead/common/mainsearch/',
'limit' => 10,
'placeholder' => PhabricatorSearchScope::getScopePlaceholder($scope),
));
$scope_input = phutil_render_tag(
'input',
array(
'type' => 'hidden',
'name' => 'scope',
'value' => $scope,
));
$form = phabricator_render_form(
$user,
array(
'action' => '/search/',
'method' => 'POST',
),
'<div class="phabricator-main-menu-search-container">'.
$input.
'<button>Search</button>'.
$scope_input.
$target.
'</div>');
$group = new PhabricatorMainMenuGroupView();
$group->addClass('phabricator-main-menu-search');
$group->appendChild($form);
return $group->render();
}
}
diff --git a/src/view/page/menu/PhabricatorMainMenuView.php b/src/view/page/menu/PhabricatorMainMenuView.php
index f798217106..ba5b9035a2 100644
--- a/src/view/page/menu/PhabricatorMainMenuView.php
+++ b/src/view/page/menu/PhabricatorMainMenuView.php
@@ -1,151 +1,135 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class PhabricatorMainMenuView extends AphrontView {
private $user;
public function setUser(PhabricatorUser $user) {
$this->user = $user;
}
public function render() {
$user = $this->user;
require_celerity_resource('phabricator-main-menu-view');
$header_id = celerity_generate_unique_node_id();
$extra = '';
$group = new PhabricatorMainMenuGroupView();
$group->addClass('phabricator-main-menu-group-logo');
$group->setCollapsible(false);
$group->appendChild(
phutil_render_tag(
'a',
array(
'class' => 'phabricator-main-menu-logo',
'href' => '/',
),
'<span>Phabricator</span>'));
if (PhabricatorEnv::getEnvConfig('notification.enabled') &&
$user->isLoggedIn()) {
list($menu, $dropdown) = $this->renderNotificationMenu();
$group->appendChild($menu);
$extra .= $dropdown;
}
$group->appendChild(
javelin_render_tag(
'a',
array(
'class' => 'phabricator-main-menu-expand-button',
'sigil' => 'jx-toggle-class',
'meta' => array(
'map' => array(
$header_id => 'phabricator-main-menu-reveal',
),
),
),
'<span>Expand</span>'));
$logo = $group->render();
return phutil_render_tag(
'div',
array(
'class' => 'phabricator-main-menu',
'id' => $header_id,
),
$logo.$this->renderChildren()).
$extra;
}
private function renderNotificationMenu() {
$user = $this->user;
require_celerity_resource('phabricator-notification-css');
require_celerity_resource('phabricator-notification-menu-css');
$indicator_id = celerity_generate_unique_node_id();
$dropdown_id = celerity_generate_unique_node_id();
$menu_id = celerity_generate_unique_node_id();
$notification_count = id(new PhabricatorFeedStoryNotification())
->countUnread($user);
$classes = array(
'phabricator-main-menu-alert-indicator',
);
if ($notification_count) {
$classes[] = 'phabricator-main-menu-alert-indicator-unread';
}
$notification_indicator = javelin_render_tag(
'span',
array(
'id' => $indicator_id,
'class' => implode(' ', $classes),
),
$notification_count);
$classes = array();
$classes[] = 'phabricator-main-menu-alert-item';
$classes[] = 'phabricator-main-menu-alert-item-notification';
$classes[] = 'autosprite';
$classes[] = 'main-menu-item-icon-notifications';
$notification_icon = javelin_render_tag(
'a',
array(
'href' => '/notification/',
'class' => implode(' ', $classes),
'id' => $menu_id,
),
$notification_indicator);
$notification_menu = javelin_render_tag(
'div',
array(
'class' => 'phabricator-main-menu-alert',
),
$notification_icon);
Javelin::initBehavior(
'aphlict-dropdown',
array(
'menuID' => $menu_id,
'indicatorID' => $indicator_id,
'dropdownID' => $dropdown_id,
));
$notification_dropdown = javelin_render_tag(
'div',
array(
'id' => $dropdown_id,
'class' => 'phabricator-notification-menu',
'sigil' => 'phabricator-notification-menu',
'style' => 'display: none;',
),
'');
return array($notification_menu, $notification_dropdown);
}
}
diff --git a/src/view/viewutils.php b/src/view/viewutils.php
index 8a028aaee1..ca2be6de9d 100644
--- a/src/view/viewutils.php
+++ b/src/view/viewutils.php
@@ -1,281 +1,265 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
function phabricator_date($epoch, PhabricatorUser $user) {
return phabricator_format_local_time(
$epoch,
$user,
_phabricator_date_format($epoch));
}
function phabricator_on_relative_date($epoch, $user) {
return phabricator_relative_date($epoch, $user, true);
}
function phabricator_relative_date($epoch, $user, $on = false) {
static $today;
static $yesterday;
if (!$today || !$yesterday) {
$now = time();
$today = phabricator_date($now, $user);
$yesterday = phabricator_date($now - 86400, $user);
}
$date = phabricator_date($epoch, $user);
if ($date === $today) {
return 'today';
}
if ($date === $yesterday) {
return 'yesterday';
}
return (($on ? 'on ' : '').$date);
}
function phabricator_time($epoch, $user) {
return phabricator_format_local_time(
$epoch,
$user,
pht('g:i A'));
}
function phabricator_datetime($epoch, $user) {
return phabricator_format_local_time(
$epoch,
$user,
pht('%s, g:i A', _phabricator_date_format($epoch)));
}
function _phabricator_date_format($epoch) {
$now = time();
$shift = 30 * 24 * 60 * 60;
if ($epoch < $now + $shift && $epoch > $now - $shift) {
$format = pht('D, M j');
} else {
$format = pht('M j Y');
}
return $format;
}
/**
* This function does not usually need to be called directly. Instead, call
* @{function:phabricator_date}, @{function:phabricator_time}, or
* @{function:phabricator_datetime}.
*
* @param int Unix epoch timestamp.
* @param PhabricatorUser User viewing the timestamp.
* @param string Date format, as per DateTime class.
* @return string Formatted, local date/time.
*/
function phabricator_format_local_time($epoch, $user, $format) {
if (!$epoch) {
// If we're missing date information for something, the DateTime class will
// throw an exception when we try to construct an object. Since this is a
// display function, just return an empty string.
return '';
}
$user_zone = $user->getTimezoneIdentifier();
static $zones = array();
if (empty($zones[$user_zone])) {
$zones[$user_zone] = new DateTimeZone($user_zone);
}
$zone = $zones[$user_zone];
// NOTE: Although DateTime takes a second DateTimeZone parameter to its
// constructor, it ignores it if the date string includes timezone
// information. Further, it treats epoch timestamps ("@946684800") as having
// a UTC timezone. Set the timezone explicitly after constructing the object.
try {
$date = new DateTime('@'.$epoch);
} catch (Exception $ex) {
// NOTE: DateTime throws an empty exception if the format is invalid,
// just replace it with a useful one.
throw new Exception(
"Construction of a DateTime() with epoch '{$epoch}' ".
"raised an exception.");
}
$date->setTimeZone($zone);
return PhutilTranslator::getInstance()->translateDate($format, $date);
}
function phabricator_format_relative_time($duration) {
return phabricator_format_units_generic(
$duration,
array(60, 60, 24, 7),
array('s', 'm', 'h', 'd', 'w'),
$precision = 0);
}
/**
* Format a relative time (duration) into weeks, days, hours, minutes,
* seconds, but unlike phabricator_format_relative_time, does so for more than
* just the largest unit.
*
* @param int Duration in seconds.
* @param int Levels to render - will render the three highest levels, ie:
* 5 h, 37 m, 1 s
* @return string Human-readable description.
*/
function phabricator_format_relative_time_detailed($duration, $levels = 2) {
if ($duration == 0) {
return 'now';
}
$levels = max(1, min($levels, 5));
$remainder = 0;
$is_negative = false;
if ($duration < 0) {
$is_negative = true;
$duration = abs($duration);
}
$this_level = 1;
$detailed_relative_time = phabricator_format_units_generic(
$duration,
array(60, 60, 24, 7),
array('s', 'm', 'h', 'd', 'w'),
$precision = 0,
$remainder);
$duration = $remainder;
while ($remainder > 0 && $this_level < $levels) {
$detailed_relative_time .= ', '.phabricator_format_units_generic(
$duration,
array(60, 60, 24, 7),
array('s', 'm', 'h', 'd', 'w'),
$precision = 0,
$remainder);
$duration = $remainder;
$this_level++;
};
if ($is_negative) {
$detailed_relative_time .= ' ago';
}
return $detailed_relative_time;
}
/**
* Format a byte count for human consumption, e.g. "10MB" instead of
* "10000000".
*
* @param int Number of bytes.
* @return string Human-readable description.
*/
function phabricator_format_bytes($bytes) {
return phabricator_format_units_generic(
$bytes,
// NOTE: Using the SI version of these units rather than the 1024 version.
array(1000, 1000, 1000, 1000, 1000),
array('B', 'KB', 'MB', 'GB', 'TB', 'PB'),
$precision = 0);
}
/**
* Parse a human-readable byte description (like "6MB") into an integer.
*
* @param string Human-readable description.
* @return int Number of represented bytes.
*/
function phabricator_parse_bytes($input) {
$bytes = trim($input);
if (!strlen($bytes)) {
return null;
}
// NOTE: Assumes US-centric numeral notation.
$bytes = preg_replace('/[ ,]/', '', $bytes);
$matches = null;
if (!preg_match('/^(?:\d+(?:[.]\d+)?)([kmgtp]?)b?$/i', $bytes, $matches)) {
throw new Exception("Unable to parse byte size '{$input}'!");
}
$scale = array(
'k' => 1000,
'm' => 1000 * 1000,
'g' => 1000 * 1000 * 1000,
't' => 1000 * 1000 * 1000 * 1000,
'p' => 1000 * 1000 * 1000 * 1000 * 1000,
);
$bytes = (float)$bytes;
if ($matches[1]) {
$bytes *= $scale[strtolower($matches[1])];
}
return (int)$bytes;
}
function phabricator_format_units_generic(
$n,
array $scales,
array $labels,
$precision = 0,
&$remainder = null) {
$is_negative = false;
if ($n < 0) {
$is_negative = true;
$n = abs($n);
}
$remainder = 0;
$accum = 1;
$scale = array_shift($scales);
$label = array_shift($labels);
while ($n >= $scale && count($labels)) {
$remainder += ($n % $scale) * $accum;
$n /= $scale;
$accum *= $scale;
$label = array_shift($labels);
if (!count($scales)) {
break;
}
$scale = array_shift($scales);
}
if ($is_negative) {
$n = -$n;
$remainder = -$remainder;
}
if ($precision) {
$num_string = number_format($n, $precision);
} else {
$num_string = (int)floor($n);
}
if ($label) {
$num_string .= ' '.$label;
}
return $num_string;
}
diff --git a/src/view/widget/AphrontKeyboardShortcutsAvailableView.php b/src/view/widget/AphrontKeyboardShortcutsAvailableView.php
index 8848b1d983..bfb8850605 100644
--- a/src/view/widget/AphrontKeyboardShortcutsAvailableView.php
+++ b/src/view/widget/AphrontKeyboardShortcutsAvailableView.php
@@ -1,28 +1,12 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
final class AphrontKeyboardShortcutsAvailableView extends AphrontView {
public function render() {
return
'<div class="keyboard-shortcuts-available">'.
'Press <strong>?</strong> to show keyboard shortcuts.'.
'</div>';
}
}
diff --git a/support/aphlict/client/aphlict_test_client.php b/support/aphlict/client/aphlict_test_client.php
index 7131ef4736..b6ca0480d6 100755
--- a/support/aphlict/client/aphlict_test_client.php
+++ b/support/aphlict/client/aphlict_test_client.php
@@ -1,72 +1,56 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$root = dirname(dirname(dirname(dirname(__FILE__))));
require_once $root.'/scripts/__init_script__.php';
$args = new PhutilArgumentParser($argv);
$args->setTagline('test client for Aphlict server');
$args->setSynopsis(<<<EOHELP
**aphlict_test_client.php** [__options__]
Connect to the Aphlict server configured in the Phabricator config.
EOHELP
);
$args->parseStandardArguments();
$args->parse(
array(
array(
'name' => 'server',
'param' => 'uri',
'default' => PhabricatorEnv::getEnvConfig('notification.client-uri'),
'help' => 'Connect to __uri__ instead of the default server.',
),
));
$console = PhutilConsole::getConsole();
$errno = null;
$errstr = null;
$uri = $args->getArg('server');
$uri = new PhutilURI($uri);
$uri->setProtocol('tcp');
$console->writeErr("Connecting...\n");
$socket = stream_socket_client(
$uri,
$errno,
$errstr);
if (!$socket) {
$console->writeErr(
"Unable to connect to Aphlict (at '$uri'). Error #{$errno}: {$errstr}");
exit(1);
} else {
$console->writeErr("Connected.\n");
}
$io_channel = new PhutilSocketChannel($socket);
$proto_channel = new PhutilJSONProtocolChannel($io_channel);
$json = new PhutilJSON();
while (true) {
$message = $proto_channel->waitForMessage();
$console->writeOut($json->encodeFormatted($message));
}
diff --git a/support/aphlict/server/aphlict_launcher.php b/support/aphlict/server/aphlict_launcher.php
index 37577210c5..80eceb8e34 100755
--- a/support/aphlict/server/aphlict_launcher.php
+++ b/support/aphlict/server/aphlict_launcher.php
@@ -1,172 +1,156 @@
#!/usr/bin/env php
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
// This is a launcher for the 'aphlict' Node.js notification server that
// provides real-time notifications for Phabricator. It handles reading
// configuration from the Phabricator config, daemonizing the server,
// restarting the server if it crashes, and some basic sanity checks.
$root = dirname(dirname(dirname(dirname(__FILE__))));
require_once $root.'/scripts/__init_script__.php';
// >>> Options and Arguments ---------------------------------------------------
$args = new PhutilArgumentParser($argv);
$args->setTagline('manage Aphlict notification server');
$args->setSynopsis(<<<EOHELP
**aphlict** [__options__]
Start (or restart) the Aphlict server.
EOHELP
);
$args->parseStandardArguments();
$args->parse(array(
array(
'name' => 'foreground',
'help' => 'Run in the foreground instead of daemonizing.',
),
));
if (posix_getuid() != 0) {
throw new Exception(
"You must run this script as root; the Aphlict server needs to bind to ".
"privileged ports.");
}
list($err) = exec_manual('node -v');
if ($err) {
throw new Exception(
'`node` is not in $PATH. You must install Node.js to run the Aphlict '.
'server.');
}
$server_uri = PhabricatorEnv::getEnvConfig('notification.server-uri');
$server_uri = new PhutilURI($server_uri);
$client_uri = PhabricatorEnv::getEnvConfig('notification.client-uri');
$client_uri = new PhutilURI($client_uri);
$user = PhabricatorEnv::getEnvConfig('notification.user');
$log = PhabricatorEnv::getEnvConfig('notification.log');
$g_pidfile = PhabricatorEnv::getEnvConfig('notification.pidfile');
$g_future = null;
$foreground = $args->getArg('foreground');
// Build the argument list for the server itself.
$server_argv = array();
$server_argv[] = csprintf('--port=%s', $client_uri->getPort());
$server_argv[] = csprintf('--admin=%s', $server_uri->getPort());
if ($user) {
$server_argv[] = csprintf('--user=%s', $user);
}
if ($log) {
$server_argv[] = csprintf('--log=%s', $log);
}
// >>> Foreground / Background -------------------------------------------------
// If we start in the foreground, we use phutil_passthru() below to show any
// output from the server to the console, but this means *this* process won't
// receive signals until the child exits. If we write our pid to the pidfile
// and then another process starts, it will try to SIGTERM us but we won't
// receive the signal. Since the effect is the same and this is simpler, just
// ignore the pidfile if launched in `--foreground` mode; this is a debugging
// mode anyway.
if ($foreground) {
echo "Starting server in foreground, ignoring pidfile...\n";
$g_pidfile = null;
} else {
$pid = pcntl_fork();
if ($pid < 0) {
throw new Exception("Failed to fork()!");
} else if ($pid) {
exit(0);
}
}
// >>> Signals / Cleanup -------------------------------------------------------
function cleanup($sig = '?') {
global $g_pidfile;
if ($g_pidfile) {
Filesystem::remove($g_pidfile);
$g_pidfile = null;
}
global $g_future;
if ($g_future) {
$g_future->resolveKill();
$g_future = null;
}
exit(1);
}
if (!$foreground) {
declare(ticks = 1);
pcntl_signal(SIGTERM, 'cleanup');
}
register_shutdown_function('cleanup');
// >>> pidfile -----------------------------------------------------------------
if ($g_pidfile) {
if (Filesystem::pathExists($g_pidfile)) {
$old_pid = (int)Filesystem::readFile($g_pidfile);
posix_kill($old_pid, SIGTERM);
sleep(1);
Filesystem::remove($g_pidfile);
}
Filesystem::writeFile($g_pidfile, getmypid());
}
// >>> run ---------------------------------------------------------------------
$command = csprintf(
'node %s %C',
dirname(__FILE__).'/aphlict_server.js',
implode(' ', $server_argv));
if ($foreground) {
echo "Launching server:\n\n";
echo " $ ".$command."\n\n";
$err = phutil_passthru('%C', $command);
echo ">>> Server exited!\n";
exit($err);
} else {
while (true) {
$g_future = new ExecFuture('exec %C', $command);
$g_future->resolve();
// If the server exited, wait a couple of seconds and restart it.
unset($g_future);
sleep(2);
}
}
diff --git a/support/phame/libskin.php b/support/phame/libskin.php
index 70c33d1a7f..9d1e7aeb39 100644
--- a/support/phame/libskin.php
+++ b/support/phame/libskin.php
@@ -1,21 +1,5 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
function _e($text) {
return phutil_escape_html($text);
}
diff --git a/webroot/index.php b/webroot/index.php
index 7a50a00beb..fee8fd3fae 100644
--- a/webroot/index.php
+++ b/webroot/index.php
@@ -1,382 +1,366 @@
<?php
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
$__start__ = microtime(true);
$access_log = null;
error_reporting(E_ALL | E_STRICT);
$required_version = '5.2.3';
if (version_compare(PHP_VERSION, $required_version) < 0) {
phabricator_fatal_config_error(
"You are running PHP version '".PHP_VERSION."', which is older than ".
"the minimum version, '{$required_version}'. Update to at least ".
"'{$required_version}'.");
}
ini_set('memory_limit', -1);
$env = getenv('PHABRICATOR_ENV'); // Apache
if (!$env) {
if (isset($_ENV['PHABRICATOR_ENV'])) {
$env = $_ENV['PHABRICATOR_ENV'];
}
}
if (!$env) {
phabricator_fatal_config_error(
"The 'PHABRICATOR_ENV' environmental variable is not defined. Modify ".
"your httpd.conf to include 'SetEnv PHABRICATOR_ENV <env>', where '<env>' ".
"is one of 'development', 'production', or a custom environment.");
}
if (!isset($_REQUEST['__path__'])) {
if (php_sapi_name() == 'cli-server') {
// Compatibility with PHP 5.4+ built-in web server.
$url = parse_url($_SERVER['REQUEST_URI']);
$_REQUEST['__path__'] = $url['path'];
} else {
phabricator_fatal_config_error(
"__path__ is not set. Your rewrite rules are not configured correctly.");
}
}
if (get_magic_quotes_gpc()) {
phabricator_fatal_config_error(
"Your server is configured with PHP 'magic_quotes_gpc' enabled. This ".
"feature is 'highly discouraged' by PHP's developers and you must ".
"disable it to run Phabricator. Consult the PHP manual for instructions.");
}
register_shutdown_function('phabricator_shutdown');
require_once dirname(dirname(__FILE__)).'/conf/__init_conf__.php';
try {
setup_aphront_basics();
$overseer = new PhabricatorRequestOverseer();
$overseer->didStartup();
$conf = phabricator_read_config_file($env);
$conf['phabricator.env'] = $env;
PhabricatorEnv::setEnvConfig($conf);
// This needs to be done before we create the log, because
// PhabricatorAccessLog::getLog() calls date()
$tz = PhabricatorEnv::getEnvConfig('phabricator.timezone');
if ($tz) {
date_default_timezone_set($tz);
}
// Append any paths to $PATH if we need to.
$paths = PhabricatorEnv::getEnvConfig('environment.append-paths');
if (!empty($paths)) {
$current_env_path = getenv('PATH');
$new_env_paths = implode(':', $paths);
putenv('PATH='.$current_env_path.':'.$new_env_paths);
}
// This is the earliest we can get away with this, we need env config first.
PhabricatorAccessLog::init();
$access_log = PhabricatorAccessLog::getLog();
if ($access_log) {
$access_log->setData(
array(
'R' => idx($_SERVER, 'HTTP_REFERER', '-'),
'r' => idx($_SERVER, 'REMOTE_ADDR', '-'),
'M' => idx($_SERVER, 'REQUEST_METHOD', '-'),
));
}
DarkConsoleXHProfPluginAPI::hookProfiler();
PhutilErrorHandler::initialize();
PhutilErrorHandler::setErrorListener(
array('DarkConsoleErrorLogPluginAPI', 'handleErrors'));
foreach (PhabricatorEnv::getEnvConfig('load-libraries') as $library) {
phutil_load_library($library);
}
if (PhabricatorEnv::getEnvConfig('phabricator.setup')) {
try {
PhabricatorSetup::runSetup();
} catch (Exception $ex) {
echo "EXCEPTION!\n";
echo $ex;
}
return;
}
phabricator_detect_bad_base_uri();
$translation = PhabricatorEnv::newObjectFromConfig('translation.provider');
PhutilTranslator::getInstance()
->setLanguage($translation->getLanguage())
->addTranslations($translation->getTranslations());
$host = $_SERVER['HTTP_HOST'];
$path = $_REQUEST['__path__'];
switch ($host) {
default:
$config_key = 'aphront.default-application-configuration-class';
$application = PhabricatorEnv::newObjectFromConfig($config_key);
break;
}
$application->setHost($host);
$application->setPath($path);
$application->willBuildRequest();
$request = $application->buildRequest();
$write_guard = new AphrontWriteGuard(array($request, 'validateCSRF'));
PhabricatorEventEngine::initialize();
$application->setRequest($request);
list($controller, $uri_data) = $application->buildController();
if ($access_log) {
$access_log->setData(
array(
'U' => (string)$request->getRequestURI()->getPath(),
'C' => get_class($controller),
));
}
// If execution throws an exception and then trying to render that exception
// throws another exception, we want to show the original exception, as it is
// likely the root cause of the rendering exception.
$original_exception = null;
try {
$response = $controller->willBeginExecution();
if ($access_log) {
if ($request->getUser() && $request->getUser()->getPHID()) {
$access_log->setData(
array(
'u' => $request->getUser()->getUserName(),
));
}
}
if (!$response) {
$controller->willProcessRequest($uri_data);
$response = $controller->processRequest();
}
} catch (AphrontRedirectException $ex) {
$response = id(new AphrontRedirectResponse())
->setURI($ex->getURI());
} catch (Exception $ex) {
$original_exception = $ex;
$response = $application->handleException($ex);
}
try {
$response = $controller->didProcessRequest($response);
$response = $application->willSendResponse($response, $controller);
$response->setRequest($request);
$response_string = $response->buildResponseString();
} catch (Exception $ex) {
$write_guard->dispose();
if ($access_log) {
$access_log->write();
}
if ($original_exception) {
$ex = new PhutilAggregateException(
"Multiple exceptions during processing and rendering.",
array(
$original_exception,
$ex,
));
}
phabricator_fatal('[Rendering Exception] '.$ex->getMessage());
}
$write_guard->dispose();
// TODO: Share the $sink->writeResponse() pathway here?
$sink = new AphrontPHPHTTPSink();
$sink->writeHTTPStatus($response->getHTTPResponseCode());
$headers = $response->getCacheHeaders();
$headers = array_merge($headers, $response->getHeaders());
$sink->writeHeaders($headers);
$sink->writeData($response_string);
if ($access_log) {
$access_log->setData(
array(
'c' => $response->getHTTPResponseCode(),
'T' => (int)(1000000 * (microtime(true) - $__start__)),
));
$access_log->write();
}
if (DarkConsoleXHProfPluginAPI::isProfilerRequested()) {
$profile = DarkConsoleXHProfPluginAPI::stopProfiler();
$profile_sample = id(new PhabricatorXHProfSample())
->setFilePHID($profile);
if (empty($_REQUEST['__profile__'])) {
$sample_rate = PhabricatorEnv::getEnvConfig('debug.profile-rate');
} else {
$sample_rate = 0;
}
$profile_sample->setSampleRate($sample_rate);
if ($access_log) {
$profile_sample->setUsTotal($access_log->getData('T'))
->setHostname($access_log->getData('h'))
->setRequestPath($access_log->getData('U'))
->setController($access_log->getData('C'))
->setUserPHID($request->getUser()->getPHID());
}
$profile_sample->save();
}
} catch (Exception $ex) {
phabricator_fatal("[Exception] ".$ex->getMessage());
}
/**
* @group aphront
*/
function setup_aphront_basics() {
$aphront_root = dirname(dirname(__FILE__));
$libraries_root = dirname($aphront_root);
$root = null;
if (!empty($_SERVER['PHUTIL_LIBRARY_ROOT'])) {
$root = $_SERVER['PHUTIL_LIBRARY_ROOT'];
}
ini_set(
'include_path',
$libraries_root.PATH_SEPARATOR.ini_get('include_path'));
@include_once $root.'libphutil/src/__phutil_library_init__.php';
if (!@constant('__LIBPHUTIL__')) {
echo "ERROR: Unable to load libphutil. Put libphutil/ next to ".
"phabricator/, or update your PHP 'include_path' to include ".
"the parent directory of libphutil/.\n";
exit(1);
}
// Load Phabricator itself using the absolute path, so we never end up doing
// anything surprising (loading index.php and libraries from different
// directories).
phutil_load_library($aphront_root.'/src');
phutil_load_library('arcanist/src');
}
function phabricator_fatal_config_error($msg) {
phabricator_fatal("CONFIG ERROR: ".$msg."\n");
}
function phabricator_detect_bad_base_uri() {
$conf = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
$uri = new PhutilURI($conf);
switch ($uri->getProtocol()) {
case 'http':
case 'https':
break;
default:
return phabricator_fatal_config_error(
"'phabricator.base-uri' is set to '{$conf}', which is invalid. ".
"The URI must start with 'http://' or 'https://'.");
}
if (strpos($uri->getDomain(), '.') === false) {
phabricator_fatal_config_error(
"'phabricator.base-uri' is set to '{$conf}', which is invalid. The URI ".
"must contain a dot ('.'), like 'http://example.com/', not just ".
"'http://example/'. Some web browsers will not set cookies on domains ".
"with no TLD, and Phabricator requires cookies for login. ".
"If you are using localhost, create an entry in the hosts file like ".
"'127.0.0.1 example.com', and access the localhost with ".
"'http://example.com/'.");
}
}
function phabricator_shutdown() {
$event = error_get_last();
if (!$event) {
return;
}
switch ($event['type']) {
case E_ERROR:
case E_PARSE:
case E_COMPILE_ERROR:
break;
default:
return;
}
$msg = ">>> UNRECOVERABLE FATAL ERROR <<<\n\n";
if ($event) {
// Even though we should be emitting this as text-plain, escape things just
// to be sure since we can't really be sure what the program state is when
// we get here.
$msg .= phutil_escape_html($event['message'])."\n\n";
$msg .= phutil_escape_html($event['file'].':'.$event['line']);
}
// flip dem tables
$msg .= "\n\n\n";
$msg .= "\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb\x20\xef\xb8\xb5\x20\xc2\xaf".
"\x5c\x5f\x28\xe3\x83\x84\x29\x5f\x2f\xc2\xaf\x20\xef\xb8\xb5\x20".
"\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb";
phabricator_fatal($msg);
}
function phabricator_fatal($msg) {
global $access_log;
if ($access_log) {
try {
$access_log->setData(
array(
'c' => 500,
));
$access_log->write();
} catch (Exception $ex) {
$msg .= "\nMoreover unable to write to access log.";
}
}
header(
'Content-Type: text/plain; charset=utf-8',
$replace = true,
$http_error = 500);
error_log($msg);
echo $msg;
exit(1);
}

File Metadata

Mime Type
application/octet-stream
Expires
Thu, May 9, 6:32 AM (1 d, 23 h)
Storage Engine
chunks
Storage Format
Chunks
Storage Handle
eNhQ6d8UcNdr
Default Alt Text
(4 MB)

Event Timeline