diff --git a/conf/default.conf.php b/conf/default.conf.php index 465e402492..1ccf8c2277 100644 --- a/conf/default.conf.php +++ b/conf/default.conf.php @@ -1,1341 +1,1346 @@ <?php 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, // Map of additional configuration values to lock. 'config.lock' => array(), // Map of additional configuration values to hide. 'config.hide' => array(), // Map of additional configuration values to mask. 'config.mask' => array(), // -- 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', // 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 --------------------------------------------------------- // // Set this to true to enable real-time notifications. You must also run a // notification server for this to work. Consult the documentation in // "Notifications User Guide: Setup and Configuration" for instructions. '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. A number of other mailers are available (e.g., SES, // SendGrid, SMTP, custom mailers), consult "Configuring Outbound Email" in // the documentation for details. '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 PHPMailer to send email, provide the mailer and options // here. PHPMailer is much more enormous than PHPMailerLite, and provides more // mailers and greater enormity. You need it when you want to use SMTP // instead of sendmail as the mailer. 'phpmailer.mailer' => 'smtp', 'phpmailer.smtp-host' => '', 'phpmailer.smtp-port' => 25, // When using PHPMailer with SMTP, you can set this to one of "tls" or "ssl" // to use TLS or SSL. Leave it blank for vanilla SMTP. If you're sending // via Gmail, set it to "ssl". 'phpmailer.smtp-protocol' => '', // Set following if your smtp server requires authentication. 'phpmailer.smtp-user' => null, 'phpmailer.smtp-password' => null, // 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. '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 // affects Pholio. 'metamta.pholio.reply-handler-domain' => null, // Prefix prepended to mail sent by Pholio. 'metamta.pholio.subject-prefix' => '[Pholio]', // See 'metamta.maniphest.reply-handler-domain'. This does the same thing, but // affects Macro. 'metamta.macro.reply-handler-domain' => null, // Prefix prepended to mail sent by Macro. 'metamta.macro.subject-prefix' => '[Macro]', // 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' => null, // The LDAP server port 'ldap.port' => 389, // The LDAP base domain name 'ldap.base_dn' => null, // The attribute to be regarded as 'username'. Has to be unique 'ldap.search_attribute' => null, // 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' => null, // 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' => null, // 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' => true, // The anonymous user name to use before searching a user. // Many LDAP installations require login even before searching a user, set // this option to enable it. 'ldap.anonymous-user-name' => null, // The password of the LDAP anonymous user. 'ldap.anonymous-user-password' => null, // Whether to use STARTTLS 'ldap.start-tls' => false, // -- 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, ), // -- 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, // To use a custom endpoint, specify it here. Normally, you do not need to // configure this. 'amazon-s3.endpoint' => 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, // 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, // If you set this to true, revision author email address information will // be exposed in Conduit. This is useful for Arcanist. // // For example, consider the "arc patch DX" workflow which needs to ask // Differential for the revision DX. This data often should contain // the author's email address, eg "George Washington // <gwashinton@example.com>" when DX is a git or mercurial revision. If this // option is false, Differential defaults to the best it can, something like // "George Washington" or "gwashington". 'differential.expose-emails-prudently' => 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, // -- Repositories ---------------------------------------------------------- // // The default location in which to store local copies of repositories. // Anything stored in this directory will be assumed to be under the // control of phabricator, which means that Phabricator will try to do some // maintenance on working copies if there are problems (such as a change // to the remote origin url). This maintenance may include completely // removing (and recloning) anything in this directory. // // When set to null, this option is ignored (i.e. Phabricator will not fully // control any working copies). 'repository.default-local-path' => null, // -- 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, // -- Cache ----------------------------------------------------------------- // // Set this to false to disable the use of gzdeflate()-based compression in // some caches. This may give you less performant (but more debuggable) // caching. 'cache.enable-deflate' => true, // -- 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. // 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), 'gcdaemon.ttl.general-cache' => 30 * (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, // -- 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(), + +// -- Audit ---------------------------------------------------------- // + + // Controls whether or not task creator can Close Audits + 'audit.can-author-close-audit' => false, ); diff --git a/src/applications/audit/editor/PhabricatorAuditCommentEditor.php b/src/applications/audit/editor/PhabricatorAuditCommentEditor.php index 9595fc6316..d64a48c8ca 100644 --- a/src/applications/audit/editor/PhabricatorAuditCommentEditor.php +++ b/src/applications/audit/editor/PhabricatorAuditCommentEditor.php @@ -1,535 +1,539 @@ <?php 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) { + if (!PhabricatorEnv::getEnvConfig('audit.can-author-close-audit')) { + throw new Exception('Cannot Close Audit without enabling'. + 'audit.can-author-close-audit'); + } // "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(); $feed_dont_publish_phids = array(); foreach ($requests as $request) { $status = $request->getAuditStatus(); switch ($status) { case PhabricatorAuditStatusConstants::RESIGNED: case PhabricatorAuditStatusConstants::NONE: case PhabricatorAuditStatusConstants::AUDIT_NOT_REQUIRED: case PhabricatorAuditStatusConstants::CC: $feed_dont_publish_phids[$request->getAuditorPHID()] = 1; break; default: unset($feed_dont_publish_phids[$request->getAuditorPHID()]); break; } } $feed_dont_publish_phids = array_keys($feed_dont_publish_phids); $feed_phids = array_diff($requests_phids, $feed_dont_publish_phids); $this->publishFeedStory($comment, $feed_phids); id(new PhabricatorSearchIndexer()) ->indexDocumentByPHID($commit->getPHID()); $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; } foreach ($other_comments as $other_comment) { $email_cc[$other_comment->getActorPHID()] = true; } foreach ($requests as $request) { if ($request->getAuditStatus() == PhabricatorAuditStatusConstants::CC) { $email_cc[$request->getAuditorPHID()] = true; } else if ($request->getAuditStatus() == PhabricatorAuditStatusConstants::RESIGNED) { unset($email_cc[$request->getAuditorPHID()]); } } $email_cc = array_keys($email_cc); $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/diffusion/config/PhabricatorDiffusionConfigOptions.php b/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php index 4b3805dbb1..d002498baa 100644 --- a/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php +++ b/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php @@ -1,61 +1,71 @@ <?php final class PhabricatorDiffusionConfigOptions extends PhabricatorApplicationConfigOptions { public function getName() { return pht('Diffusion'); } public function getDescription() { return pht('Configure Diffusion repository browsing.'); } public function getOptions() { return array( $this->newOption( 'metamta.diffusion.subject-prefix', 'string', '[Diffusion]') ->setDescription(pht('Subject prefix for Diffusion mail.')), $this->newOption( 'metamta.diffusion.reply-handler-domain', 'string', null) ->setDescription( pht( 'See {{metamta.maniphest.reply-handler}}. This does the same '. 'thing, but affects Diffusion.')), $this->newOption( 'metamta.diffusion.reply-handler', 'class', 'PhabricatorAuditReplyHandler') ->setBaseClass('PhabricatorMailReplyHandler') ->setDescription(pht('Override mail reply handler class.')), $this->newOption( 'metamta.diffusion.attach-patches', 'bool', false) ->setBoolOptions( array( pht("Attach Patches"), pht("Do Not Attach Patches"), )) ->setDescription(pht( 'Set this to true if you want patches to be attached to commit '. 'notifications from Diffusion.')), $this->newOption('metamta.diffusion.inline-patches', 'int', 0) ->setSummary(pht('Include patches in Diffusion mail as body text.')) ->setDescription( pht( 'To include patches in Diffusion email bodies, set this to a '. 'positive integer. Patches will be inlined if they are at most '. 'that many lines. By default, patches are not inlined.')), $this->newOption('metamta.diffusion.byte-limit', 'int', 1024 * 1024) ->setDescription(pht('Hard byte limit on including patches in email.')), $this->newOption('metamta.diffusion.time-limit', 'int', 60) ->setDescription(pht('Hard time limit on generating patches.')), + $this->newOption( + 'audit.can-author-close-audit', + 'bool', + false) + ->setBoolOptions( + array( + pht("Enable Closing Audits"), + pht("Disable Closing Audits"), + )) + ->setDescription(pht('Controls whether Author can Close Audits.')), ); } } diff --git a/src/applications/diffusion/controller/DiffusionCommitController.php b/src/applications/diffusion/controller/DiffusionCommitController.php index 854d9a04c4..1f81f013d7 100644 --- a/src/applications/diffusion/controller/DiffusionCommitController.php +++ b/src/applications/diffusion/controller/DiffusionCommitController.php @@ -1,917 +1,918 @@ <?php 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(); $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); $top_anchor = id(new PhabricatorAnchorView()) ->setAnchorName('top') ->setNavigationMarker(true); $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; $content[] = $top_anchor; } else { $engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine(); require_celerity_resource('diffusion-commit-view-css'); require_celerity_resource('phabricator-remarkup-css'); $parent_query = DiffusionCommitParentsQuery::newFromDiffusionRequest( $drequest); $headsup_view = id(new PhabricatorHeaderView()) ->setHeader('Commit Detail'); $headsup_actions = $this->renderHeadsupActionList($commit, $repository); $commit_properties = $this->loadCommitProperties( $commit, $commit_data, $parent_query->loadParents() ); $property_list = id(new PhabricatorPropertyListView()) ->setHasKeyboardShortcuts(true); foreach ($commit_properties as $key => $value) { $property_list->addProperty($key, $value); } $property_list->addTextContent( '<div class="diffusion-commit-message phabricator-remarkup">'. $engine->markupText($commit_data->getCommitMessage()). '</div>' ); $content[] = $top_anchor; $content[] = $headsup_view; $content[] = $headsup_actions; $content[] = $property_list; } $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); $change_panel->setNoBackground(); $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_title = DiffusionView::nameCommit( $repository, $commit->getCommitIdentifier() ); $change_list = new DifferentialChangesetListView(); $change_list->setTitle($change_list_title); $change_list->setChangesets($changesets); $change_list->setVisibleChangesets($visible_changesets); $change_list->setRenderingReferences($references); $change_list->setRenderURI('/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', array()); $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( 'id' => $pane_id ), $change_list->render(). id(new PhabricatorAnchorView()) ->setAnchorName('comment') ->setNavigationMarker(true) ->render(). $add_comment_view); $content[] = $main_pane; } $commit_id = 'r'.$callsign.$commit->getCommitIdentifier(); $short_name = DiffusionView::nameCommit( $repository, $commit->getCommitIdentifier() ); $nav = id(new DifferentialChangesetFileTreeSideNavBuilder()) ->setAnchorName('top') ->setTitle($short_name) ->setBaseURI(new PhutilURI('/'.$commit_id)) ->build($changesets); foreach ($content as $child) { $nav->appendChild($child); } $crumbs = $this->buildCrumbs(array( 'commit' => true, )); $nav->setCrumbs($crumbs); return $this->buildApplicationPage( $nav, array( 'title' => $commit_id ) ); } private function loadCommitProperties( 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'); if ($reviewer_phid) { $props['Reviewer'] = $handles[$reviewer_phid]->renderLink(); } $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(' · ', $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); $panel->setNoBackground(); 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') ->setUser($user)) ->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) { + $can_close_option = PhabricatorEnv::getEnvConfig( + 'audit.can-author-close-audit'); + if ($can_close_option && $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->setUser($this->getRequest()->getUser()); $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); $panel->setNoBackground(); return $panel; } private function renderHeadsupActionList( PhabricatorRepositoryCommit $commit, PhabricatorRepository $repository) { $request = $this->getRequest(); $user = $request->getUser(); $actions = id(new PhabricatorActionListView()) ->setUser($user) ->setObject($commit); // TODO -- integrate permissions into whether or not this action is shown $uri = '/diffusion/'.$repository->getCallSign().'/commit/'. $commit->getCommitIdentifier().'/edit/'; $action = id(new PhabricatorActionView()) ->setName('Edit Commit') ->setHref($uri) ->setIcon('edit'); $actions->addAction($action); require_celerity_resource('phabricator-object-selector-css'); require_celerity_resource('javelin-behavior-phabricator-object-selector'); if (PhabricatorEnv::getEnvConfig('maniphest.enabled')) { $action = id(new PhabricatorActionView()) ->setName('Edit Maniphest Tasks') ->setIcon('attach') ->setHref('/search/attach/'.$commit->getPHID().'/TASK/edge/') ->setWorkflow(true); $actions->addAction($action); } if ($user->getIsAdmin()) { $action = id(new PhabricatorActionView()) ->setName('MetaMTA Transcripts') ->setIcon('file') ->setHref('/mail/?phid='.$commit->getPHID()); $actions->addAction($action); } $action = id(new PhabricatorActionView()) ->setName('Herald Transcripts') ->setIcon('file') ->setHref('/herald/transcript/?phid='.$commit->getPHID()) ->setWorkflow(true); $actions->addAction($action); $action = id(new PhabricatorActionView()) ->setName('Download Raw Diff') ->setHref($request->getRequestURI()->alter('diff', true)) ->setIcon('download'); $actions->addAction($action); return $actions; } 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()); } }