diff --git a/conf/default.conf.php b/conf/default.conf.php index f9ce1aa3a6..9e176a6b7c 100644 --- a/conf/default.conf.php +++ b/conf/default.conf.php @@ -1,1148 +1,1152 @@ 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', // -- Customization --------------------------------------------------------- // // If you want to use a custom logo (e.g., for your company or organization), // copy 'webroot/rsrc/image/custom/example_template.png' to // 'webroot/rsrc/image/custom/custom.png' and set this to the URI you want it // to link to (like http://www.yourcompany.com/). 'phabricator.custom.logo' => null, // -- Internationalization -------------------------------------------------- // // This allows customizing texts used in Phabricator. The class must extend // PhabricatorTranslation. 'translation.provider' => 'PhabricatorEnglishTranslation', // You can use 'translation.override' if you don't want to create a full // translation to give users an option for switching to it and you just want // to override some strings in the default translation. 'translation.override' => array(), // -- Access Policies ------------------------------------------------------- // // Phabricator allows you to set the visibility of objects (like repositories // and source code) to "Public", which means anyone on the internet can see // them, even without being logged in. This is great for open source, but // some installs may never want to make anything public, so this policy is // disabled by default. You can enable it here, which will let you set the // policy for objects to "Public". With this option disabled, the most open // policy is "All Users", which means users must be logged in to view things. 'policy.allow-public' => false, // -- Logging --------------------------------------------------------------- // // To enable the Phabricator access log, specify a path here. The Phabricator // access log can provide more detailed information about Phabricator access // than normal HTTP access logs (for instance, it can show logged-in users, // controllers, and other application data). If not set, no log will be // written. // // Make sure the PHP process can write to the log! 'log.access.path' => null, // Format for the access log. If not set, the default format will be used: // // "[%D]\t%h\t%u\t%M\t%C\t%m\t%U\t%c\t%T" // // Available variables are: // // - %c The HTTP response code. // - %C The controller which handled the request. // - %D The request date. // - %e Epoch timestamp. // - %h The webserver's host name. // - %p The PID of the server process. // - %R The HTTP referrer. // - %r The remote IP. // - %T The request duration, in microseconds. // - %U The request path. // - %u The logged-in user, if one is logged in. // - %M The HTTP method. // - %m For conduit, the Conduit method which was invoked. // // If a variable isn't available (for example, %m appears in the file format // but the request is not a Conduit request), it will be rendered as "-". // // Note that the default format is subject to change in the future, so if you // rely on the log's format, specify it explicitly. 'log.access.format' => null, // -- DarkConsole ----------------------------------------------------------- // // DarkConsole is a administrative debugging/profiling tool built into // Phabricator. You can leave it disabled unless you're developing against // Phabricator. // Determines whether or not DarkConsole is available. DarkConsole exposes // some data like queries and stack traces, so you should be careful about // turning it on in production (although users can not normally see it, even // if the deployment configuration enables it). 'darkconsole.enabled' => false, // Always enable DarkConsole, even for logged out users. This potentially // exposes sensitive information to users, so make sure untrusted users can // not access an install running in this mode. You should definitely leave // this off in production. It is only really useful for using DarkConsole // utilities to debug or profile logged-out pages. You must set // 'darkconsole.enabled' to use this option. 'darkconsole.always-on' => false, // Allows you to mask certain configuration values from appearing in the // "Config" tab of DarkConsole. 'darkconsole.config-mask' => array( 'mysql.pass', 'amazon-ses.secret-key', 'recaptcha.private-key', 'phabricator.csrf-key', 'facebook.application-secret', 'github.application-secret', 'google.application-secret', 'phabricator.application-secret', 'disqus.application-secret', 'phabricator.mail-key', 'security.hmac-key', ), // -- MySQL --------------------------------------------------------------- // // Class providing database configuration. It must implement // DatabaseConfigurationProvider. 'mysql.configuration-provider' => 'DefaultDatabaseConfigurationProvider', // The username to use when connecting to MySQL. 'mysql.user' => 'root', // The password to use when connecting to MySQL. 'mysql.pass' => '', // The MySQL server to connect to. If you want to connect to a different // port than the default (which is 3306), specify it in the hostname // (e.g., db.example.com:1234). 'mysql.host' => 'localhost', // The number of times to try reconnecting to the MySQL database 'mysql.connection-retries' => 3, // Phabricator supports PHP extensions MySQL and MySQLi. It is possible to // implement also other access mechanism (e.g. PDO_MySQL). The class must // extend AphrontMySQLDatabaseConnectionBase. 'mysql.implementation' => 'AphrontMySQLDatabaseConnection', // -- Notifications --------------------------------------------------------- // 'notification.enabled' => false, // Client port for the realtime server to listen on, and for realtime clients // to connect to. Use "localhost" if you are running the notification server // on the same host as the web server. 'notification.client-uri' => 'http://localhost:22280/', // URI and port for the notification root server. 'notification.server-uri' => 'http://localhost:22281/', // The server must be started as root so it can bind to privileged ports, but // if you specify a user here it will drop permissions after binding. 'notification.user' => null, // Location where the server should log to. 'notification.log' => '/var/log/aphlict.log', // PID file to use. 'notification.pidfile' => '/var/run/aphlict.pid', // Enable this option to get additional debug output in the browser. 'notification.debug' => false, // -- Email ----------------------------------------------------------------- // // Some Phabricator tools send email notifications, e.g. when Differential // revisions are updated or Maniphest tasks are changed. These options allow // you to configure how email is delivered. // You can test your mail setup by going to "MetaMTA" in the web interface, // clicking "Send New Message", and then composing a message. // Default address to send mail "From". 'metamta.default-address' => 'noreply@example.com', // Domain used to generate Message-IDs. 'metamta.domain' => 'example.com', // When a message is sent to multiple recipients (for example, several // reviewers on a code review), Phabricator can either deliver one email to // everyone (e.g., "To: alincoln, usgrant, htaft") or separate emails to each // user (e.g., "To: alincoln", "To: usgrant", "To: htaft"). The major // advantages and disadvantages of each approach are: // // - One mail to everyone: // - Recipients can see To/Cc at a glance. // - If you use mailing lists, you won't get duplicate mail if you're // a normal recipient and also Cc'd on a mailing list. // - Getting threading to work properly is harder, and probably requires // making mail less useful by turning off options. // - Sometimes people will "Reply All" and everyone will get two mails, // one from the user and one from Phabricator turning their mail into // a comment. // - Not supported with a private reply-to address. // - Mails are sent in the server default translation. // - One mail to each user: // - Recipients need to look in the mail body to see To/Cc. // - If you use mailing lists, recipients may sometimes get duplicate // mail. // - Getting threading to work properly is easier, and threading settings // can be customzied by each user. // - "Reply All" no longer spams all other users. // - Required if private reply-to addresses are configured. // - Mails are sent in the language of user preference. // // In the code, splitting one outbound email into one-per-recipient is // sometimes referred to as "multiplexing". 'metamta.one-mail-per-recipient' => true, // When a user takes an action which generates an email notification (like // commenting on a Differential revision), Phabricator can either send that // mail "From" the user's email address (like "alincoln@logcabin.com") or // "From" the 'metamta.default-address' address. The user experience is // generally better if Phabricator uses the user's real address as the "From" // since the messages are easier to organize when they appear in mail clients, // but this will only work if the server is authorized to send email on behalf // of the "From" domain. Practically, this means: // - If you are doing an install for Example Corp and all the users will // have corporate @corp.example.com addresses and any hosts Phabricator // is running on are authorized to send email from corp.example.com, // you can enable this to make the user experience a little better. // - If you are doing an install for an open source project and your // users will be registering via Facebook and using personal email // addresses, you MUST NOT enable this or virtually all of your outgoing // email will vanish into SFP blackholes. // - If your install is anything else, you're much safer leaving this // off since the risk in turning it on is that your outgoing mail will // mostly never arrive. 'metamta.can-send-as-user' => false, // Adapter class to use to transmit mail to the MTA. The default uses // PHPMailerLite, which will invoke "sendmail". This is appropriate // if sendmail actually works on your host, but if you haven't configured mail // it may not be so great. You can also use Amazon SES, by changing this to // 'PhabricatorMailImplementationAmazonSESAdapter', signing up for SES, and // filling in your 'amazon-ses.access-key' and 'amazon-ses.secret-key' below. 'metamta.mail-adapter' => 'PhabricatorMailImplementationPHPMailerLiteAdapter', // When email is sent, try to hand it off to the MTA immediately. This may // be worth disabling if your MTA infrastructure is slow or unreliable. If you // disable this option, you must run the 'metamta_mta.php' daemon or mail // won't be handed off to the MTA. If you're using Amazon SES it can be a // little slugish sometimes so it may be worth disabling this and moving to // the daemon after you've got your install up and running. If you have a // properly configured local MTA it should not be necessary to disable this. 'metamta.send-immediately' => true, // If you're using Amazon SES to send email, provide your AWS access key // and AWS secret key here. To set up Amazon SES with Phabricator, you need // to: // - Make sure 'metamta.mail-adapter' is set to: // "PhabricatorMailImplementationAmazonSESAdapter" // - Make sure 'metamta.can-send-as-user' is false. // - Make sure 'metamta.default-address' is configured to something sensible. // - Make sure 'metamta.default-address' is a validated SES "From" address. 'amazon-ses.access-key' => null, 'amazon-ses.secret-key' => null, // If you're using Sendgrid to send email, provide your access credentials // here. This will use the REST API. You can also use Sendgrid as a normal // SMTP service. 'sendgrid.api-user' => null, 'sendgrid.api-key' => null, // You can configure a reply handler domain so that email sent from Maniphest // will have a special "Reply To" address like "T123+82+af19f@example.com" // that allows recipients to reply by email and interact with tasks. For // instructions on configurating reply handlers, see the article // "Configuring Inbound Email" in the Phabricator documentation. By default, // this is set to 'null' and Phabricator will use a generic 'noreply@' address // or the address of the acting user instead of a special reply handler // address (see 'metamta.default-address'). If you set a domain here, // Phabricator will begin generating private reply handler addresses. See // also 'metamta.maniphest.reply-handler' to further configure behavior. // This key should be set to the domain part after the @, like "example.com". 'metamta.maniphest.reply-handler-domain' => null, // You can follow the instructions in "Configuring Inbound Email" in the // Phabricator documentation and set 'metamta.maniphest.reply-handler-domain' // to support updating Maniphest tasks by email. If you want more advanced // customization than this provides, you can override the reply handler // class with an implementation of your own. This will allow you to do things // like have a single public reply handler or change how private reply // handlers are generated and validated. // This key should be set to a loadable subclass of // PhabricatorMailReplyHandler (and possibly of ManiphestReplyHandler). 'metamta.maniphest.reply-handler' => 'ManiphestReplyHandler', // If you don't want phabricator to take up an entire domain // (or subdomain for that matter), you can use this and set a common // prefix for mail sent by phabricator. It will make use of the fact that // a mail-address such as phabricator+D123+1hjk213h@example.com will be // delivered to the phabricator users mailbox. // Set this to the left part of the email address and it well get // prepended to all outgoing mail. If you want to use e.g. // 'phabricator@example.com' this should be set to 'phabricator'. 'metamta.single-reply-handler-prefix' => null, // Prefix prepended to mail sent by Maniphest. You can change this to // distinguish between testing and development installs, for example. 'metamta.maniphest.subject-prefix' => '[Maniphest]', // See 'metamta.maniphest.reply-handler-domain'. This does the same thing, // but allows email replies via Differential. 'metamta.differential.reply-handler-domain' => null, // See 'metamta.maniphest.reply-handler'. This does the same thing, but // affects Differential. 'metamta.differential.reply-handler' => 'DifferentialReplyHandler', // Prefix prepended to mail sent by Differential. 'metamta.differential.subject-prefix' => '[Differential]', // Set this to true if you want patches to be attached to mail from // Differential. This won't work if you are using SendGrid as your mail // adapter. 'metamta.differential.attach-patches' => false, // To include patches in email bodies, set this to a positive integer. Patches // will be inlined if they are at most that many lines. For instance, a value // of 100 means "inline patches if they are no longer than 100 lines". By // default, patches are not inlined. 'metamta.differential.inline-patches' => 0, // If you enable either of the options above, you can choose what format // patches are sent in. Valid options are 'unified' (like diff -u) or 'git'. 'metamta.differential.patch-format' => 'unified', // 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', // 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, // 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(), // -- 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, // -- GitHub OAuth ---------------------------------------------------------- // // Can users use GitHub credentials to login to Phabricator? 'github.auth-enabled' => false, // Can users use GitHub credentials to create new Phabricator accounts? 'github.registration-enabled' => true, // Are GitHub accounts permanently linked to Phabricator accounts, or can // the user unlink them? 'github.auth-permanent' => false, // The GitHub "Client ID" to use for GitHub API access. 'github.application-id' => null, // The GitHub "Secret" to use for GitHub API access. 'github.application-secret' => null, // -- Google OAuth ---------------------------------------------------------- // // Can users use Google credentials to login to Phabricator? 'google.auth-enabled' => false, // Can users use Google credentials to create new Phabricator accounts? 'google.registration-enabled' => true, // Are Google accounts permanently linked to Phabricator accounts, or can // the user unlink them? 'google.auth-permanent' => false, // The Google "Client ID" to use for Google API access. 'google.application-id' => null, // The Google "Client Secret" to use for Google API access. 'google.application-secret' => null, // -- LDAP Auth ----------------------------------------------------- // // Enable ldap auth 'ldap.auth-enabled' => false, // The LDAP server hostname 'ldap.hostname' => '', // The LDAP base domain name 'ldap.base_dn' => '', // The attribute to be regarded as 'username'. Has to be unique 'ldap.search_attribute' => '', // The attribute(s) to be regarded as 'real name'. // If more then one attribute is supplied the values of the attributes in // the array will be joined 'ldap.real_name_attributes' => array(), // The LDAP version 'ldap.version' => 3, // -- 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, // -- Disqus Comments ------------------------------------------------------- // // 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, // -- 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 tag. // This should be a subset of 'files.viewable-mime-types' and exclude files // like text. 'files.image-mime-types' => array( 'image/jpeg' => true, 'image/jpg' => true, 'image/png' => true, 'image/gif' => true, 'image/x-ico' => true, 'image/x-icon' => true, 'image/vnd.microsoft.icon' => true, ), // Phabricator can proxy images from other servers so you can paste the URI // to a funny picture of a cat into the comment box and have it show up as an // image. However, this means the webserver Phabricator is running on will // make HTTP requests to arbitrary URIs. If the server has access to internal // resources, this could be a security risk. You should only enable it if you // are installed entirely a VPN and VPN access is required to access // Phabricator, or if the webserver has no special access to anything. If // unsure, it is safer to leave this disabled. 'files.enable-proxy' => false, // -- Storage --------------------------------------------------------------- // // Phabricator allows users to upload files, and can keep them in various // storage engines. This section allows you to configure which engines // Phabricator will use, and how it will use them. // The largest filesize Phabricator will store in the MySQL BLOB storage // engine, which just uses a database table to store files. While this isn't a // best practice, it's really easy to set up. This is hard-limited by the // value of 'max_allowed_packet' in MySQL (since this often defaults to 1MB, // the default here is slightly smaller than 1MB). Set this to 0 to disable // use of the MySQL blob engine. 'storage.mysql-engine.max-size' => 1000000, // Phabricator provides a local disk storage engine, which just writes files // to some directory on local disk. The webserver must have read/write // permissions on this directory. This is straightforward and suitable for // most installs, but will not scale past one web frontend unless the path // is actually an NFS mount, since you'll end up with some of the files // written to each web frontend and no way for them to share. To use the // local disk storage engine, specify the path to a directory here. To // disable it, specify null. 'storage.local-disk.path' => null, // If you want to store files in Amazon S3, specify an AWS access and secret // key here and a bucket name below. 'amazon-s3.access-key' => null, 'amazon-s3.secret-key' => null, // Set this to a valid Amazon S3 bucket to store files there. You must also // configure S3 access keys above. 'storage.s3.bucket' => null, // Phabricator uses a storage engine selector to choose which storage engine // to use when writing file data. If you add new storage engines or want to // provide very custom rules (e.g., write images to one storage engine and // other files to a different one), you can provide an alternate // implementation here. The default engine will use choose MySQL, Local Disk, // and S3, in that order, if they have valid configurations above and a file // fits within configured limits. 'storage.engine-selector' => 'PhabricatorDefaultFileStorageEngineSelector', // Set the size of the largest file a user may upload. This is used to render // text like "Maximum file size: 10MB" on interfaces where users can upload // files, and files larger than this size will be rejected. // // Specify this limit in bytes, or using a "K", "M", or "G" suffix. // // NOTE: Setting this to a large size is NOT sufficient to allow users to // upload large files. You must also configure a number of other settings. To // configure file upload limits, consult the article "Configuring File Upload // Limits" in the documentation. Once you've configured some limit across all // levels of the server, you can set this limit to an appropriate value and // the UI will then reflect the actual configured limit. 'storage.upload-size-limit' => null, // Phabricator puts databases in a namespace, which defualts to "phabricator" // -- for instance, the Differential database is named // "phabricator_differential" by default. You can change this namespace if you // want. Normally, you should not do this unless you are developing // Phabricator and using namespaces to separate multiple sandbox datasets. 'storage.default-namespace' => 'phabricator', // -- Search ---------------------------------------------------------------- // // Phabricator supports Elastic Search; to use it, specify a host like // 'http://elastic.example.com:9200/' here. 'search.elastic.host' => null, // Phabricator uses a search engine selector to choose which search engine // to use when indexing and reconstructing documents, and when executing // queries. You can override the engine selector to provide a new selector // class which can select some custom engine you implement, if you want to // store your documents in some search engine which does not have default // support. 'search.engine-selector' => 'PhabricatorDefaultSearchEngineSelector', // -- Differential ---------------------------------------------------------- // 'differential.revision-custom-detail-renderer' => null, // Array for custom remarkup rules. The array should have a list of // class names of classes that extend PhutilRemarkupRule 'differential.custom-remarkup-rules' => null, // Array for custom remarkup block rules. The array should have a list of // class names of classes that extend PhutilRemarkupEngineBlockRule 'differential.custom-remarkup-block-rules' => null, // Set display word-wrap widths for Differential. Specify a dictionary of // regular expressions mapping to column widths. The filename will be matched // against each regexp in order until one matches. The default configuration // uses a width of 100 for Java and 80 for other languages. Note that 80 is // the greatest column width of all time. Changes here will not be immediately // reflected in old revisions unless you purge the changeset render cache // (with `./scripts/util/purge_cache.php --changesets`). 'differential.wordwrap' => array( '/\.java$/' => 100, '/.*/' => 80, ), // List of file regexps where whitespace is meaningful and should not // use 'ignore-all' by default 'differential.whitespace-matters' => array( '/\.py$/', '/\.l?hs$/', ), 'differential.field-selector' => 'DifferentialDefaultFieldSelector', // Differential can show "Host" and "Path" fields on revisions, with // information about the machine and working directory where the // change came from. These fields are disabled by default because they may // occasionally have sensitive information; you can set this to true to // enable them. 'differential.show-host-field' => false, // Differential has a required "Test Plan" field by default, which requires // authors to fill out information about how they verified the correctness of // their changes when sending code for review. If you'd prefer not to use // this field, you can disable it here. You can also make it optional // (instead of required) below. 'differential.show-test-plan-field' => true, // Differential has a required "Test Plan" field by default. You can make it // optional by setting this to false. You can also completely remove it above, // if you prefer. 'differential.require-test-plan-field' => true, // If you set this to true, users can "!accept" revisions via email (normally, // they can take other actions but can not "!accept"). This action is disabled // by default because email authentication can be configured to be very weak, // and, socially, email "!accept" is kind of sketchy and implies revisions may // not actually be receiving thorough review. 'differential.enable-email-accept' => false, // If you set this to true, users won't need to login to view differential // revisions. Anonymous users will have read-only access and won't be able to // interact with the revisions. 'differential.anonymous-access' => false, // List of file regexps that should be treated as if they are generated by // an automatic process, and thus get hidden by default in differential 'differential.generated-paths' => array( // '/config\.h$/', // '#/autobuilt/#', ), // -- 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', // -- Phriction ------------------------------------------------------------- // 'phriction.enabled' => true, // -- Remarkup -------------------------------------------------------------- // // If you enable this, linked YouTube videos will be embeded inline. This has // mild security implications (you'll leak referrers to YouTube) and is pretty // silly (but sort of awesome). 'remarkup.enable-embedded-youtube' => false, // -- Garbage Collection ---------------------------------------------------- // // Phabricator generates various logs and caches in the database which can // be garbage collected after a while to make the total data size more // manageable. To run garbage collection, launch a // PhabricatorGarbageCollector daemon. // Since the GC daemon can issue large writes and table scans, you may want to // run it only during off hours or make sure it is scheduled so it doesn't // overlap with backups. This determines when the daemon can start running // each day. 'gcdaemon.run-at' => '12 AM', // How many seconds after 'gcdaemon.run-at' the daemon may collect garbage // for. By default it runs continuously, but you can set it to run for a // limited period of time. For instance, if you do backups at 3 AM, you might // run garbage collection for an hour beforehand. This is not a high-precision // limit so you may want to leave some room for the GC to actually stop, and // if you set it to something like 3 seconds you're on your own. 'gcdaemon.run-for' => 24 * 60 * 60, // These 'ttl' keys configure how much old data the GC daemon keeps around. // Objects older than the ttl will be collected. Set any value to 0 to store // data indefinitely. 'gcdaemon.ttl.herald-transcripts' => 30 * (24 * 60 * 60), 'gcdaemon.ttl.daemon-logs' => 7 * (24 * 60 * 60), 'gcdaemon.ttl.differential-parse-cache' => 14 * (24 * 60 * 60), // -- 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, // -- 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', // 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 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', 'diff' => 'Diff', 'django' => 'Django Templating', 'erb' => 'Embedded Ruby/ERB', 'erlang' => 'Erlang', 'html' => 'HTML', 'infer' => 'Infer from title (extension)', 'java' => 'Java', 'js' => 'Javascript', 'mysql' => 'MySQL', 'perl' => 'Perl', 'php' => 'PHP', 'text' => 'Plain Text', 'python' => 'Python', 'rainbow' => 'Rainbow', 'remarkup' => 'Remarkup', 'ruby' => 'Ruby', 'xml' => 'XML', ), 'pygments.dropdown-default' => 'infer', // 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', ), ); diff --git a/src/applications/conduit/method/feed/ConduitAPI_feed_query_Method.php b/src/applications/conduit/method/feed/ConduitAPI_feed_query_Method.php index 5181d52d42..bdbe44e26c 100644 --- a/src/applications/conduit/method/feed/ConduitAPI_feed_query_Method.php +++ b/src/applications/conduit/method/feed/ConduitAPI_feed_query_Method.php @@ -1,139 +1,140 @@ 'optional list ', 'limit' => 'optional int (default '.$this->getDefaultLimit().')', 'after' => 'optional int', 'view' => 'optional string (data, html, html-summary)', ); } private function getSupportedViewTypes() { return array( 'html' => 'Full HTML presentation of story', 'data' => 'Dictionary with various data of the story', 'html-summary' => 'Story contains only the title of the story', ); } public function defineErrorTypes() { $view_types = array_keys($this->getSupportedViewTypes()); $view_types = implode(', ', $view_types); return array( 'ERR-UNKNOWN-TYPE' => 'Unsupported view type, possibles are: ' . $view_types ); } public function defineReturnType() { return 'nonempty dict'; } protected function execute(ConduitAPIRequest $request) { $results = array(); $user = $request->getUser(); $view_type = $request->getValue('view'); if (!$view_type) { $view_type = 'data'; } $limit = $request->getValue('limit'); if (!$limit) { $limit = $this->getDefaultLimit(); } $filter_phids = $request->getValue('filter_phids'); if (!$filter_phids) { $filter_phids = array(); } $after = $request->getValue('after'); $query = id(new PhabricatorFeedQuery()) ->setLimit($limit) ->setFilterPHIDs($filter_phids) - ->setAfter($after); + ->setViewer($user) + ->setAfterID($after); $stories = $query->execute(); if ($stories) { $handle_phids = array_mergev(mpull($stories, 'getRequiredHandlePHIDs')); $handles = id(new PhabricatorObjectHandleData($handle_phids)) ->loadHandles(); foreach ($stories as $story) { $story->setHandles($handles); $story_data = $story->getStoryData(); $data = null; $view = $story->renderView(); $view->setEpoch($story->getEpoch()); $view->setViewer($user); switch ($view_type) { case 'html': $data = $view->render(); break; case 'html-summary': $view->setOneLineStory(true); $data = $view->render(); break; case 'data': $data = array( 'class' => $story_data->getStoryType(), 'epoch' => $story_data->getEpoch(), 'authorPHID' => $story_data->getAuthorPHID(), 'chronologicalKey' => $story_data->getChronologicalKey(), 'data' => $story_data->getStoryData(), ); break; default: throw new ConduitException('ERR-UNKNOWN-TYPE'); } $results[$story_data->getPHID()] = $data; } } return $results; } } diff --git a/src/applications/directory/controller/PhabricatorDirectoryMainController.php b/src/applications/directory/controller/PhabricatorDirectoryMainController.php index 05a8fb37d7..bfa22e065f 100644 --- a/src/applications/directory/controller/PhabricatorDirectoryMainController.php +++ b/src/applications/directory/controller/PhabricatorDirectoryMainController.php @@ -1,889 +1,849 @@ filter = idx($data, 'filter'); $this->subfilter = idx($data, 'subfilter'); } public function processRequest() { $user = $this->getRequest()->getUser(); $nav = $this->buildNav(); $this->filter = $nav->selectFilter($this->filter, 'home'); switch ($this->filter) { case 'jump': case 'apps': break; case 'home': case 'feed': $project_query = new PhabricatorProjectQuery(); $project_query->setMembers(array($user->getPHID())); $projects = $project_query->execute(); break; default: throw new Exception("Unknown filter '{$this->filter}'!"); } switch ($this->filter) { case 'feed': return $this->buildFeedResponse($nav, $projects); case 'jump': return $this->buildJumpResponse($nav); case 'apps': return $this->buildAppsResponse($nav); default: return $this->buildMainResponse($nav, $projects); } } private function buildMainResponse($nav, array $projects) { assert_instances_of($projects, 'PhabricatorProject'); if (PhabricatorEnv::getEnvConfig('maniphest.enabled')) { $unbreak_panel = $this->buildUnbreakNowPanel(); $triage_panel = $this->buildNeedsTriagePanel($projects); $tasks_panel = $this->buildTasksPanel(); } else { $unbreak_panel = null; $triage_panel = null; $tasks_panel = null; } $flagged_panel = $this->buildFlaggedPanel(); $jump_panel = $this->buildJumpPanel(); $revision_panel = $this->buildRevisionPanel(); $app_panel = $this->buildAppPanel(); $audit_panel = $this->buildAuditPanel(); $commit_panel = $this->buildCommitPanel(); $content = array( $app_panel, $jump_panel, $unbreak_panel, $triage_panel, $revision_panel, $tasks_panel, $flagged_panel, $audit_panel, $commit_panel, ); $nav->appendChild($content); return $this->buildStandardPageResponse( $nav, array( 'title' => 'Phabricator', )); } private function buildJumpResponse($nav) { $request = $this->getRequest(); if ($request->isFormPost()) { $jump = $request->getStr('jump'); $response = PhabricatorJumpNavHandler::jumpPostResponse($jump); if ($response) { return $response; } else { $query = new PhabricatorSearchQuery(); $query->setQuery($jump); $query->save(); return id(new AphrontRedirectResponse()) ->setURI('/search/'.$query->getQueryKey().'/'); } } $nav->appendChild($this->buildJumpPanel()); return $this->buildStandardPageResponse( $nav, array( 'title' => 'Jump Nav', )); } private function buildFeedResponse($nav, array $projects) { assert_instances_of($projects, 'PhabricatorProject'); $subnav = new AphrontSideNavFilterView(); $subnav->setBaseURI(new PhutilURI('/feed/')); $subnav->addFilter('all', 'All Activity', '/feed/'); $subnav->addFilter('projects', 'My Projects'); $nav->appendChild($subnav); $filter = $subnav->selectFilter($this->subfilter, 'all'); switch ($filter) { case 'all': $view = $this->buildFeedView(array()); break; case 'projects': if ($projects) { $phids = mpull($projects, 'getPHID'); $view = $this->buildFeedView($phids); } else { $view = new AphrontErrorView(); $view->setSeverity(AphrontErrorView::SEVERITY_NODATA); $view->setTitle('No Projects'); $view->appendChild('You have not joined any projects.'); } break; } $subnav->appendChild($view); return $this->buildStandardPageResponse( $nav, array( 'title' => 'Feed', )); } private function buildUnbreakNowPanel() { $user = $this->getRequest()->getUser(); $user_phid = $user->getPHID(); $task_query = new ManiphestTaskQuery(); $task_query->withStatus(ManiphestTaskQuery::STATUS_OPEN); $task_query->withPriority(ManiphestTaskPriority::PRIORITY_UNBREAK_NOW); $task_query->setLimit(10); $tasks = $task_query->execute(); if (!$tasks) { return $this->renderMiniPanel( 'No "Unbreak Now!" Tasks', 'Nothing appears to be critically broken right now.'); } $panel = new AphrontPanelView(); $panel->setHeader('Unbreak Now!'); $panel->setCaption('Open tasks with "Unbreak Now!" priority.'); $panel->addButton( phutil_render_tag( 'a', array( 'href' => '/maniphest/view/all/', 'class' => 'grey button', ), "View All Unbreak Now \xC2\xBB")); $panel->appendChild($this->buildTaskListView($tasks)); return $panel; } private function buildFlaggedPanel() { $user = $this->getRequest()->getUser(); $flag_query = id(new PhabricatorFlagQuery()) ->withOwnerPHIDs(array($user->getPHID())) ->needHandles(true) ->setLimit(10); $flags = $flag_query->execute(); if (!$flags) { return $this->renderMiniPanel( 'No Flags', "You haven't flagged anything."); } $panel = new AphrontPanelView(); $panel->setHeader('Flagged Objects'); $panel->setCaption("Objects you've flagged."); $flag_view = new PhabricatorFlagListView(); $flag_view->setFlags($flags); $flag_view->setUser($user); $panel->appendChild($flag_view); $panel->addButton( phutil_render_tag( 'a', array( 'href' => '/flag/', 'class' => 'grey button', ), "View All Flags \xC2\xBB")); return $panel; } private function buildNeedsTriagePanel(array $projects) { assert_instances_of($projects, 'PhabricatorProject'); $user = $this->getRequest()->getUser(); $user_phid = $user->getPHID(); if ($projects) { $task_query = new ManiphestTaskQuery(); $task_query->withStatus(ManiphestTaskQuery::STATUS_OPEN); $task_query->withPriority(ManiphestTaskPriority::PRIORITY_TRIAGE); $task_query->withProjects(mpull($projects, 'getPHID')); $task_query->withAnyProject(true); $task_query->setLimit(10); $tasks = $task_query->execute(); } else { $tasks = array(); } if (!$tasks) { return $this->renderMiniPanel( 'No "Needs Triage" Tasks', 'No tasks in projects you are a member of '. 'need triage.'); } $panel = new AphrontPanelView(); $panel->setHeader('Needs Triage'); $panel->setCaption( 'Open tasks with "Needs Triage" priority in '. 'projects you are a member of.'); $panel->addButton( phutil_render_tag( 'a', array( // TODO: This should filter to just your projects' need-triage // tasks? 'href' => '/maniphest/view/projecttriage/', 'class' => 'grey button', ), "View All Triage \xC2\xBB")); $panel->appendChild($this->buildTaskListView($tasks)); return $panel; } private function buildRevisionPanel() { $user = $this->getRequest()->getUser(); $user_phid = $user->getPHID(); $revision_query = new DifferentialRevisionQuery(); $revision_query->withStatus(DifferentialRevisionQuery::STATUS_OPEN); $revision_query->withResponsibleUsers(array($user_phid)); $revision_query->needRelationships(true); // NOTE: We need to unlimit this query to hit the responsible user // fast-path. $revision_query->setLimit(null); $revisions = $revision_query->execute(); list($active, $waiting) = DifferentialRevisionQuery::splitResponsible( $revisions, $user_phid); if (!$active) { return $this->renderMiniPanel( 'No Waiting Revisions', 'No revisions are waiting on you.'); } $panel = new AphrontPanelView(); $panel->setHeader('Revisions Waiting on You'); $panel->setCaption('Revisions waiting for you for review or commit.'); $panel->addButton( phutil_render_tag( 'a', array( 'href' => '/differential/', 'class' => 'button grey', ), "View Active Revisions \xC2\xBB")); $fields = $revision_view = id(new DifferentialRevisionListView()) ->setRevisions($active) ->setFields(DifferentialRevisionListView::getDefaultFields()) ->setUser($user); $phids = array_merge( array($user_phid), $revision_view->getRequiredHandlePHIDs()); $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); $revision_view->setHandles($handles); $panel->appendChild($revision_view); return $panel; } private function buildTasksPanel() { $user = $this->getRequest()->getUser(); $user_phid = $user->getPHID(); $task_query = new ManiphestTaskQuery(); $task_query->withStatus(ManiphestTaskQuery::STATUS_OPEN); $task_query->setGroupBy(ManiphestTaskQuery::GROUP_PRIORITY); $task_query->withOwners(array($user_phid)); $task_query->setLimit(10); $tasks = $task_query->execute(); if (!$tasks) { return $this->renderMiniPanel( 'No Assigned Tasks', 'You have no assigned tasks.'); } $panel = new AphrontPanelView(); $panel->setHeader('Assigned Tasks'); $panel->setCaption('Tasks assigned to you.'); $panel->addButton( phutil_render_tag( 'a', array( 'href' => '/maniphest/', 'class' => 'button grey', ), "View Active Tasks \xC2\xBB")); $panel->appendChild($this->buildTaskListView($tasks)); return $panel; } private function buildTaskListView(array $tasks) { assert_instances_of($tasks, 'ManiphestTask'); $user = $this->getRequest()->getUser(); $phids = array_merge( array_filter(mpull($tasks, 'getOwnerPHID')), array_mergev(mpull($tasks, 'getProjectPHIDs'))); $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); $view = new ManiphestTaskListView(); $view->setTasks($tasks); $view->setUser($user); $view->setHandles($handles); return $view; } private function buildFeedView(array $phids) { $request = $this->getRequest(); $user = $request->getUser(); $user_phid = $user->getPHID(); $feed_query = new PhabricatorFeedQuery(); + $feed_query->setViewer($user); if ($phids) { $feed_query->setFilterPHIDs($phids); } - // TODO: All this limit stuff should probably be consolidated into the - // feed query? + $pager = new AphrontIDPagerView(); + $pager->readFromRequest($request); + $pager->setPageSize(200); - $old_link = null; - $new_link = null; - - $feed_query->setAfter($request->getStr('after')); - $feed_query->setBefore($request->getStr('before')); - $limit = 500; - - // Grab one more story than we intend to display so we can figure out - // if we need to render an "Older Posts" link or not (with reasonable - // accuracy, at least). - $feed_query->setLimit($limit + 1); - $feed = $feed_query->execute(); - $extra_row = (count($feed) == $limit + 1); - - $have_new = ($request->getStr('before')) || - ($request->getStr('after') && $extra_row); - - $have_old = ($request->getStr('after')) || - ($request->getStr('before') && $extra_row) || - (!$request->getStr('before') && - !$request->getStr('after') && - $extra_row); - $feed = array_slice($feed, 0, $limit, $preserve_keys = true); - - if ($have_old) { - $old_link = phutil_render_tag( - 'a', - array( - 'href' => '?before='.end($feed)->getChronologicalKey(), - 'class' => 'phabricator-feed-older-link', - ), - "Older Stories \xC2\xBB"); - } - if ($have_new) { - $new_link = phutil_render_tag( - 'a', - array( - 'href' => '?after='.reset($feed)->getChronologicalKey(), - 'class' => 'phabricator-feed-newer-link', - ), - "\xC2\xAB Newer Stories"); - } + $feed = $feed_query->executeWithPager($pager); $builder = new PhabricatorFeedBuilder($feed); $builder->setUser($user); $feed_view = $builder->buildView(); return '
'. '
'. '

Feed

'. '
'. $feed_view->render(). '
'. - $new_link. - $old_link. + $pager->render(). '
'. '
'; } private function buildJumpPanel() { $request = $this->getRequest(); $user = $request->getUser(); $uniq_id = celerity_generate_unique_node_id(); Javelin::initBehavior( 'phabricator-autofocus', array( 'id' => $uniq_id, )); require_celerity_resource('phabricator-jump-nav'); $doc_href = PhabricatorEnv::getDocLink('article/Jump_Nav_User_Guide.html'); $doc_link = phutil_render_tag( 'a', array( 'href' => $doc_href, ), 'Jump Nav User Guide'); $jump_input = phutil_render_tag( 'input', array( 'type' => 'text', 'class' => 'phabricator-jump-nav', 'name' => 'jump', 'id' => $uniq_id, )); $jump_caption = phutil_render_tag( 'p', array( 'class' => 'phabricator-jump-nav-caption', ), 'Enter the name of an object like D123 to quickly jump to '. 'it. See '.$doc_link.' or type help.'); $panel = new AphrontPanelView(); $panel->addClass('aphront-unpadded-panel-view'); $panel->appendChild( phabricator_render_form( $user, array( 'action' => '/jump/', 'method' => 'POST', 'class' => 'phabricator-jump-nav-form', ), $jump_input. $jump_caption)); return $panel; } private function buildAppPanel() { require_celerity_resource('phabricator-app-buttons-css'); $nav_buttons = array(); $nav_buttons[] = array( 'Differential', '/differential/', 'differential', 'Code Reviews'); if (PhabricatorEnv::getEnvConfig('maniphest.enabled')) { $nav_buttons[] = array( 'Maniphest', '/maniphest/', 'maniphest', 'Tasks'); $nav_buttons[] = array( 'Create Task', '/maniphest/task/create/', 'create-task'); } $nav_buttons[] = array( 'Upload File', '/file/', 'upload-file', 'Share Files'); $nav_buttons[] = array( 'Create Paste', '/paste/', 'create-paste', 'Share Text'); if (PhabricatorEnv::getEnvConfig('phriction.enabled')) { $nav_buttons[] = array( 'Phriction', '/w/', 'phriction', 'Browse Wiki'); } $nav_buttons[] = array( 'Diffusion', '/diffusion/', 'diffusion', 'Browse Code'); $nav_buttons[] = array( 'Audit', '/audit/', 'audit', 'Audit Code'); $view = new AphrontNullView(); $view->appendChild('
'); foreach ($nav_buttons as $info) { // Subtitle is optional. list($name, $uri, $icon, $subtitle) = array_merge($info, array(null)); if ($subtitle) { $subtitle = '
'. phutil_escape_html($subtitle). '
'; } $button = phutil_render_tag( 'a', array( 'href' => $uri, 'class' => 'app-button icon-'.$icon, ), phutil_render_tag( 'div', array( 'class' => 'app-icon icon-'.$icon, ), '')); $caption = phutil_render_tag( 'a', array( 'href' => $uri, 'class' => 'phabricator-button-caption', ), phutil_escape_html($name).$subtitle); $view->appendChild( '
'. $button. $caption. '
'); } $view->appendChild('
'); return $view; } private function renderMiniPanel($title, $body) { $panel = new AphrontMiniPanelView(); $panel->appendChild( phutil_render_tag( 'p', array( ), ''.$title.': '.$body)); return $panel; } public function buildAuditPanel() { $request = $this->getRequest(); $user = $request->getUser(); $phids = PhabricatorAuditCommentEditor::loadAuditPHIDsForUser($user); $query = new PhabricatorAuditQuery(); $query->withAuditorPHIDs($phids); $query->withStatus(PhabricatorAuditQuery::STATUS_OPEN); $query->withAwaitingUser($user); $query->needCommitData(true); $query->setLimit(10); $audits = $query->execute(); $commits = $query->getCommits(); if (!$audits) { return $this->renderMinipanel( 'No Audits', 'No commits are waiting for you to audit them.'); } $view = new PhabricatorAuditListView(); $view->setAudits($audits); $view->setCommits($commits); $phids = $view->getRequiredHandlePHIDs(); $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); $view->setHandles($handles); $panel = new AphrontPanelView(); $panel->setHeader('Audits'); $panel->setCaption('Commits awaiting your audit.'); $panel->appendChild($view); $panel->addButton( phutil_render_tag( 'a', array( 'href' => '/audit/', 'class' => 'button grey', ), "View Active Audits \xC2\xBB")); return $panel; } public function buildCommitPanel() { $request = $this->getRequest(); $user = $request->getUser(); $phids = array($user->getPHID()); $query = new PhabricatorAuditCommitQuery(); $query->withAuthorPHIDs($phids); $query->withStatus(PhabricatorAuditQuery::STATUS_OPEN); $query->needCommitData(true); $query->setLimit(10); $commits = $query->execute(); if (!$commits) { return $this->renderMinipanel( 'No Problem Commits', 'No one has raised concerns with your commits.'); } $view = new PhabricatorAuditCommitListView(); $view->setCommits($commits); $view->setUser($user); $phids = $view->getRequiredHandlePHIDs(); $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); $view->setHandles($handles); $panel = new AphrontPanelView(); $panel->setHeader('Problem Commits'); $panel->setCaption('Commits which auditors have raised concerns about.'); $panel->appendChild($view); $panel->addButton( phutil_render_tag( 'a', array( 'href' => '/audit/', 'class' => 'button grey', ), "View Problem Commits \xC2\xBB")); return $panel; } public function buildAppsResponse(AphrontSideNavFilterView $nav) { $user = $this->getRequest()->getUser(); $apps = array( array( '/repository/', 'Repositories', 'Configure tracked source code repositories.', ), array( '/herald/', 'Herald', 'Create notification rules. Watch for danger!', ), array( '/file/', 'Files', 'Upload and download files. Blob store for Pokemon pictures.', ), array( '/project/', 'Projects', 'Group stuff into big piles.', ), array( '/vote/', 'Slowvote', 'Create polls. Design by committee.', ), array( '/countdown/', 'Countdown', 'Count down to events. Utilize the full capabilities of your ALU.', ), array( '/people/', 'People', 'User directory. Sort of a social utility.', ), array( '/owners/', 'Owners', 'Keep track of who owns code. Adopt today!', ), array( '/conduit/', 'Conduit API Console', 'Web console for Conduit API.', ), array( '/daemon/', 'Daemon Console', 'Offline process management.', ), array( '/mail/', 'MetaMTA', 'Manage mail delivery. Yo dawg, we heard you like MTAs.', array( 'admin' => true, ), ), array( '/phid/', 'PHID Manager', 'Debugging tool for PHIDs.', ), array( '/xhpast/', 'XHPAST', 'Web interface to PHP AST tool. Lex XHP AST & CTS FYI, LOL.', ), array( 'http://www.phabricator.com/docs/phabricator/', 'Phabricator Ducks', 'Oops, that should say "Docs".', array( 'new' => true, ), ), array( 'http://www.phabricator.com/docs/arcanist/', 'Arcanist Docs', 'Words have never been so finely crafted.', array( 'new' => true, ), ), array( 'http://www.phabricator.com/docs/libphutil/', 'libphutil Docs', 'Soothing prose; seductive poetry.', array( 'new' => true, ), ), array( 'http://www.phabricator.com/docs/javelin/', 'Javelin Docs', 'O, what noble scribe hath penned these words?', array( 'new' => true, ), ), array( '/uiexample/', 'UI Examples', 'Phabricator UI elements. A gallery of modern art.', array( 'new' => true, ), ), ); $out = array(); foreach ($apps as $app) { if (empty($app[3])) { $app[3] = array(); } $app[3] += array( 'admin' => false, 'new' => false, ); list($href, $name, $desc, $options) = $app; if ($options['admin'] && !$user->getIsAdmin()) { continue; } $link = phutil_render_tag( 'a', array( 'href' => $href, 'target' => $options['new'] ? '_blank' : null, ), phutil_escape_html($name)); $out[] = '
'. '

'.$link.'

'. '

'.phutil_escape_html($desc).'

'. '
'; } require_celerity_resource('phabricator-directory-css'); $out = '
'. implode("\n", $out). '
'; $nav->appendChild($out); return $this->buildStandardPageResponse( $nav, array( 'title' => 'More Stuff', )); } } diff --git a/src/applications/feed/PhabricatorFeedQuery.php b/src/applications/feed/PhabricatorFeedQuery.php index 00ca3fcbec..f78dd74380 100644 --- a/src/applications/feed/PhabricatorFeedQuery.php +++ b/src/applications/feed/PhabricatorFeedQuery.php @@ -1,113 +1,89 @@ filterPHIDs = $phids; return $this; } - public function setLimit($limit) { - $this->limit = $limit; - return $this; - } + public function loadPage() { - public function setAfter($after) { - $this->after = $after; - return $this; - } + $story_table = new PhabricatorFeedStoryData(); + $conn = $story_table->establishConnection('r'); - public function setBefore($before) { - $this->before = $before; - return $this; + $data = queryfx_all( + $conn, + 'SELECT story.* FROM %T story %Q %Q %Q %Q %Q', + $story_table->getTableName(), + $this->buildJoinClause($conn), + $this->buildWhereClause($conn), + $this->buildGroupClause($conn), + $this->buildOrderClause($conn), + $this->buildLimitClause($conn)); + + $results = PhabricatorFeedStory::loadAllFromRows($data); + + return $this->processResults($results); } - public function execute() { + private function buildJoinClause(AphrontDatabaseConnection $conn_r) { + // NOTE: We perform this join unconditionally (even if we have no filter + // PHIDs) to omit rows which have no story references. These story data + // rows are notifications or realtime alerts. $ref_table = new PhabricatorFeedStoryReference(); - $story_table = new PhabricatorFeedStoryData(); - - $conn = $story_table->establishConnection('r'); + return qsprintf( + $conn_r, + 'JOIN %T ref ON ref.chronologicalKey = story.chronologicalKey', + $ref_table->getTableName()); + } + private function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); + if ($this->filterPHIDs) { $where[] = qsprintf( - $conn, + $conn_r, 'ref.objectPHID IN (%Ls)', $this->filterPHIDs); } - // For "before" queries, we can just add a constraint to the WHERE clause. - // For "after" queries, we must also reverse the result ordering, since - // otherwise we'll always grab the first page of results if there's a limit. - // After MySQL applies the limit, we reverse the page in PHP (below) to - // ensure consistent ordering. - - $order = 'DESC'; - - if ($this->after) { - $where[] = qsprintf( - $conn, - 'ref.chronologicalKey > %s', - $this->after); - $order = 'ASC'; - } + $where[] = $this->buildPagingClause($conn_r); - if ($this->before) { - $where[] = qsprintf( - $conn, - 'ref.chronologicalKey < %s', - $this->before); - } + return $this->formatWhereClause($where); + } - if ($where) { - $where = 'WHERE ('.implode(') AND (', $where).')'; - } else { - $where = ''; - } + private function buildGroupClause(AphrontDatabaseConnection $conn_r) { + return qsprintf( + $conn_r, + 'GROUP BY ref.chronologicalKey'); + } - $data = queryfx_all( - $conn, - 'SELECT story.* FROM %T ref - JOIN %T story ON ref.chronologicalKey = story.chronologicalKey - %Q - GROUP BY ref.chronologicalKey - ORDER BY ref.chronologicalKey %Q - LIMIT %d', - $ref_table->getTableName(), - $story_table->getTableName(), - $where, - $order, - $this->limit); - - if ($order != 'DESC') { - // If we did order ASC to pull 'after' data, reverse the result set so - // that stories are returned in a consistent (descending) order. - $data = array_reverse($data); - } + protected function getPagingColumn() { + return 'ref.chronologicalKey'; + } - return PhabricatorFeedStory::loadAllFromRows($data); + protected function getPagingValue($item) { + return $item->getChronologicalKey(); } } diff --git a/src/applications/feed/controller/PhabricatorFeedPublicStreamController.php b/src/applications/feed/controller/PhabricatorFeedPublicStreamController.php index af5ba6b5da..e2f1dac839 100644 --- a/src/applications/feed/controller/PhabricatorFeedPublicStreamController.php +++ b/src/applications/feed/controller/PhabricatorFeedPublicStreamController.php @@ -1,53 +1,52 @@ getRequest(); + $viewer = $request->getUser(); $query = new PhabricatorFeedQuery(); + $query->setViewer($viewer); $stories = $query->execute(); $builder = new PhabricatorFeedBuilder($stories); $builder ->setFramed(true) - ->setUser($request->getUser()); + ->setUser($viewer); $view = $builder->buildView(); return $this->buildStandardPageResponse( $view, array( 'title' => 'Public Feed', 'public' => true, )); } } diff --git a/src/applications/feed/story/PhabricatorFeedStory.php b/src/applications/feed/story/PhabricatorFeedStory.php index eb5e54a196..979e176612 100644 --- a/src/applications/feed/story/PhabricatorFeedStory.php +++ b/src/applications/feed/story/PhabricatorFeedStory.php @@ -1,197 +1,212 @@ List of @{class:PhabricatorFeedStoryData} rows from the * database. * @return list List of @{class:PhabricatorFeedStory} * objects. * @task load */ public static function loadAllFromRows(array $rows) { $stories = array(); $data = id(new PhabricatorFeedStoryData())->loadAllFromArray($rows); foreach ($data as $story_data) { $class = $story_data->getStoryType(); $ok = false; try { $ok = is_subclass_of($class, 'PhabricatorFeedStory'); } catch (PhutilMissingSymbolException $ex) { $ok = false; } // If the story type isn't a valid class or isn't a subclass of // PhabricatorFeedStory, load it as PhabricatorFeedStoryUnknown. if (!$ok) { $class = 'PhabricatorFeedStoryUnknown'; } $key = $story_data->getChronologicalKey(); $stories[$key] = newv($class, array($story_data)); } return $stories; } + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + return PhabricatorEnv::getEnvConfig('feed.public') + ? PhabricatorPolicies::POLICY_PUBLIC + : PhabricatorPolicies::POLICY_USER; + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } public function setPrimaryObjectPHID($primary_object_phid) { $this->primaryObjectPHID = $primary_object_phid; return $this; } public function getPrimaryObjectPHID() { return $this->primaryObjectPHID; } final public function __construct(PhabricatorFeedStoryData $data) { $this->data = $data; } abstract public function renderView(); // TODO: Make abstract once all subclasses implement it. public function renderNotificationView() { return id(new PhabricatorFeedStoryUnknown($this->data)) ->renderNotificationView(); } public function getRequiredHandlePHIDs() { return array(); } public function setHasViewed($has_viewed) { $this->hasViewed = $has_viewed; return $this; } public function getHasViewed() { return $this->hasViewed; } public function getRequiredObjectPHIDs() { return array(); } final public function setFramed($framed) { $this->framed = $framed; return $this; } final public function setHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } final protected function getHandles() { return $this->handles; } final protected function getHandle($phid) { if (isset($this->handles[$phid])) { if ($this->handles[$phid] instanceof PhabricatorObjectHandle) { return $this->handles[$phid]; } } $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setName("Unloaded Object '{$phid}'"); return $handle; } final public function getStoryData() { return $this->data; } final public function getEpoch() { return $this->getStoryData()->getEpoch(); } final public function getChronologicalKey() { return $this->getStoryData()->getChronologicalKey(); } final protected function renderHandleList(array $phids) { $list = array(); foreach ($phids as $phid) { $list[] = $this->linkTo($phid); } return implode(', ', $list); } final protected function linkTo($phid) { $handle = $this->getHandle($phid); // NOTE: We render our own link here to customize the styling and add // the '_top' target for framed feeds. return phutil_render_tag( 'a', array( 'href' => $handle->getURI(), 'target' => $this->framed ? '_top' : null, ), phutil_escape_html($handle->getLinkName())); } final protected function renderString($str) { return ''.phutil_escape_html($str).''; } final protected function renderSummary($text, $len = 128) { if ($len) { $text = phutil_utf8_shorten($text, $len); } $text = phutil_escape_html($text); $text = str_replace("\n", '
', $text); return $text; } public function getNotificationAggregations() { return array(); } } diff --git a/src/applications/people/controller/PhabricatorPeopleProfileController.php b/src/applications/people/controller/PhabricatorPeopleProfileController.php index 3a313dfc17..491f8a0a97 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileController.php @@ -1,226 +1,229 @@ username = idx($data, 'username'); $this->page = idx($data, 'page'); } public function processRequest() { $viewer = $this->getRequest()->getUser(); $user = id(new PhabricatorUser())->loadOneWhere( 'userName = %s', $this->username); if (!$user) { return new Aphront404Response(); } require_celerity_resource('phabricator-profile-css'); $profile = id(new PhabricatorUserProfile())->loadOneWhere( 'userPHID = %s', $user->getPHID()); if (!$profile) { $profile = new PhabricatorUserProfile(); } $username = phutil_escape_uri($user->getUserName()); $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI('/p/'.$username.'/')); $nav->addFilter('feed', 'Feed'); $nav->addFilter('about', 'About'); $nav->addSpacer(); $nav->addLabel('Activity'); $external_arrow = "\xE2\x86\x97"; $nav->addFilter( null, "Revisions {$external_arrow}", '/differential/filter/revisions/'.$username.'/'); $nav->addFilter( null, "Tasks {$external_arrow}", '/maniphest/view/action/?users='.$user->getPHID()); $nav->addFilter( null, "Commits {$external_arrow}", '/audit/view/author/'.$username.'/'); $oauths = id(new PhabricatorUserOAuthInfo())->loadAllWhere( 'userID = %d', $user->getID()); $oauths = mpull($oauths, null, 'getOAuthProvider'); $providers = PhabricatorOAuthProvider::getAllProviders(); $added_spacer = false; foreach ($providers as $provider) { if (!$provider->isProviderEnabled()) { continue; } $provider_key = $provider->getProviderKey(); if (!isset($oauths[$provider_key])) { continue; } $name = $provider->getProviderName().' Profile'; $href = $oauths[$provider_key]->getAccountURI(); if ($href) { if (!$added_spacer) { $nav->addSpacer(); $nav->addLabel('Linked Accounts'); $added_spacer = true; } $nav->addFilter(null, $name.' '.$external_arrow, $href); } } $this->page = $nav->selectFilter($this->page, 'feed'); switch ($this->page) { case 'feed': $content = $this->renderUserFeed($user); break; case 'about': $content = $this->renderBasicInformation($user, $profile); break; default: throw new Exception("Unknown page '{$this->page}'!"); } $picture = $user->loadProfileImageURI(); $header = new PhabricatorProfileHeaderView(); $header ->setProfilePicture($picture) ->setName($user->getUserName().' ('.$user->getRealName().')') ->setDescription($profile->getTitle()); if ($user->getIsDisabled()) { $header->setStatus('Disabled'); } else { $statuses = id(new PhabricatorUserStatus())->loadCurrentStatuses( array($user->getPHID())); if ($statuses) { $header->setStatus(reset($statuses)->getStatusDescription($viewer)); } } $header->appendChild($nav); $nav->appendChild( '
'.$content.'
'); if ($user->getPHID() == $viewer->getPHID()) { $nav->addSpacer(); $nav->addFilter(null, 'Edit Profile...', '/settings/page/profile/'); } if ($viewer->getIsAdmin()) { $nav->addSpacer(); $nav->addFilter( null, 'Administrate User...', '/people/edit/'.$user->getID().'/'); } return $this->buildStandardPageResponse( $header, array( 'title' => $user->getUsername(), )); } private function renderBasicInformation($user, $profile) { $blurb = nonempty( $profile->getBlurb(), '//Nothing is known about this rare specimen.//'); $engine = PhabricatorMarkupEngine::newProfileMarkupEngine(); $blurb = $engine->markupText($blurb); $viewer = $this->getRequest()->getUser(); $content = '

Basic Information

PHID '.phutil_escape_html($user->getPHID()).'
User Since '.phabricator_datetime($user->getDateCreated(), $viewer). '
'; $content .= '

Flavor Text

Blurb '.$blurb.'
'; return $content; } private function renderUserFeed(PhabricatorUser $user) { + $viewer = $this->getRequest()->getUser(); + $query = new PhabricatorFeedQuery(); $query->setFilterPHIDs( array( $user->getPHID(), )); + $query->setViewer($viewer); $stories = $query->execute(); $builder = new PhabricatorFeedBuilder($stories); - $builder->setUser($this->getRequest()->getUser()); + $builder->setUser($viewer); $view = $builder->buildView(); return '

Activity Feed

'.$view->render().'
'; } } diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php index d0ec8d2988..ed51aa4f07 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -1,372 +1,367 @@ id = idx($data, 'id'); $this->page = idx($data, 'page'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $project = id(new PhabricatorProject())->load($this->id); if (!$project) { return new Aphront404Response(); } $profile = $project->loadProfile(); if (!$profile) { $profile = new PhabricatorProjectProfile(); } $picture = $profile->loadProfileImageURI(); $members = mpull($project->loadAffiliations(), null, 'getUserPHID'); $nav_view = new AphrontSideNavFilterView(); $uri = new PhutilURI('/project/view/'.$project->getID().'/'); $nav_view->setBaseURI($uri); $external_arrow = "\xE2\x86\x97"; $tasks_uri = '/maniphest/view/all/?projects='.$project->getPHID(); $slug = PhabricatorSlug::normalize($project->getName()); $phriction_uri = '/w/projects/'.$slug; $edit_uri = '/project/edit/'.$project->getID().'/'; $nav_view->addFilter('dashboard', 'Dashboard'); $nav_view->addSpacer(); $nav_view->addFilter('feed', 'Feed'); $nav_view->addFilter(null, 'Tasks '.$external_arrow, $tasks_uri); $nav_view->addFilter(null, 'Wiki '.$external_arrow, $phriction_uri); $nav_view->addFilter('people', 'People'); $nav_view->addFilter('about', 'About'); $nav_view->addSpacer(); $nav_view->addFilter(null, "Edit Project\xE2\x80\xA6", $edit_uri); $this->page = $nav_view->selectFilter($this->page, 'dashboard'); require_celerity_resource('phabricator-profile-css'); switch ($this->page) { case 'dashboard': $content = $this->renderTasksPage($project, $profile); $query = new PhabricatorFeedQuery(); $query->setFilterPHIDs( array( $project->getPHID(), )); + $query->setViewer($this->getRequest()->getUser()); $stories = $query->execute(); $content .= $this->renderStories($stories); break; case 'about': $content = $this->renderAboutPage($project, $profile); break; case 'people': $content = $this->renderPeoplePage($project, $profile); break; case 'feed': $content = $this->renderFeedPage($project, $profile); break; default: throw new Exception("Unimplemented filter '{$this->page}'."); } $content = '
'.$content.'
'; $nav_view->appendChild($content); $header = new PhabricatorProfileHeaderView(); $header->setName($project->getName()); $header->setDescription( phutil_utf8_shorten($profile->getBlurb(), 1024)); $header->setProfilePicture($picture); $action = null; if (empty($members[$user->getPHID()])) { $action = phabricator_render_form( $user, array( 'action' => '/project/update/'.$project->getID().'/join/', 'method' => 'post', ), phutil_render_tag( 'button', array( 'class' => 'green', ), 'Join Project')); } else { $action = javelin_render_tag( 'a', array( 'href' => '/project/update/'.$project->getID().'/leave/', 'sigil' => 'workflow', 'class' => 'grey button', ), 'Leave Project...'); } $header->addAction($action); $header->appendChild($nav_view); return $this->buildStandardPageResponse( $header, array( 'title' => $project->getName().' Project', )); } private function renderAboutPage( PhabricatorProject $project, PhabricatorProjectProfile $profile) { $viewer = $this->getRequest()->getUser(); $blurb = $profile->getBlurb(); $blurb = phutil_escape_html($blurb); $blurb = str_replace("\n", '
', $blurb); $phids = array_merge( array($project->getAuthorPHID()), $project->getSubprojectPHIDs() ); $phids = array_unique($phids); $handles = id(new PhabricatorObjectHandleData($phids)) ->loadHandles(); $timestamp = phabricator_datetime($project->getDateCreated(), $viewer); $about = '

About

Creator '.$handles[$project->getAuthorPHID()]->renderLink().'
Created '.$timestamp.'
PHID '.phutil_escape_html($project->getPHID()).'
Blurb '.$blurb.'
'; if ($project->getSubprojectPHIDs()) { $table = $this->renderSubprojectTable( $handles, $project->getSubprojectPHIDs()); $subproject_list = $table->render(); } else { $subproject_list = '

No subprojects.

'; } $about .= '
'. '

Subprojects

'. '
'. $subproject_list. '
'. '
'; return $about; } private function renderPeoplePage( PhabricatorProject $project, PhabricatorProjectProfile $profile) { $affiliations = $project->loadAffiliations(); $phids = mpull($affiliations, 'getUserPHID'); $handles = id(new PhabricatorObjectHandleData($phids)) ->loadHandles(); $affiliated = array(); foreach ($affiliations as $affiliation) { $user = $handles[$affiliation->getUserPHID()]->renderLink(); $role = phutil_escape_html($affiliation->getRole()); $affiliated[] = '
  • '.$user.' — '.$role.'
  • '; } if ($affiliated) { $affiliated = '
      '.implode("\n", $affiliated).'
    '; } else { $affiliated = '

    No one is affiliated with this project.

    '; } return '
    '. '

    People

    '. '
    '. $affiliated. '
    '. '
    '; } private function renderFeedPage( PhabricatorProject $project, PhabricatorProjectProfile $profile) { $query = new PhabricatorFeedQuery(); $query->setFilterPHIDs(array($project->getPHID())); + $query->setViewer($this->getRequest()->getUser()); $stories = $query->execute(); if (!$stories) { return 'There are no stories about this project.'; } - $query = new PhabricatorFeedQuery(); - $query->setFilterPHIDs( - array( - $project->getPHID(), - )); - $stories = $query->execute(); - return $this->renderStories($stories); } private function renderStories(array $stories) { assert_instances_of($stories, 'PhabricatorFeedStory'); $builder = new PhabricatorFeedBuilder($stories); $builder->setUser($this->getRequest()->getUser()); $view = $builder->buildView(); return '
    '. '

    Activity Feed

    '. '
    '. $view->render(). '
    '. '
    '; } private function renderTasksPage( PhabricatorProject $project, PhabricatorProjectProfile $profile) { $query = id(new ManiphestTaskQuery()) ->withProjects(array($project->getPHID())) ->withStatus(ManiphestTaskQuery::STATUS_OPEN) ->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY) ->setLimit(10) ->setCalculateRows(true); $tasks = $query->execute(); $count = $query->getRowCount(); $phids = mpull($tasks, 'getOwnerPHID'); $phids = array_filter($phids); $handles = id(new PhabricatorObjectHandleData($phids)) ->loadHandles(); $task_views = array(); foreach ($tasks as $task) { $view = id(new ManiphestTaskSummaryView()) ->setTask($task) ->setHandles($handles) ->setUser($this->getRequest()->getUser()); $task_views[] = $view->render(); } if (empty($tasks)) { $task_views = 'No open tasks.'; } else { $task_views = implode('', $task_views); } $open = number_format($count); $more_link = phutil_render_tag( 'a', array( 'href' => '/maniphest/view/all/?projects='.$project->getPHID(), ), "View All Open Tasks \xC2\xBB"); $content = '

    '. "Open Tasks ({$open})". '

    '. '
    '. $task_views. ''. '
    '; return $content; } private function renderSubprojectTable( array $handles, array $subprojects_phids) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $rows = array(); foreach ($subprojects_phids as $subproject_phid) { $phid = $handles[$subproject_phid]->getPHID(); $rows[] = array( phutil_escape_html($handles[$phid]->getFullName()), phutil_render_tag( 'a', array( 'class' => 'small grey button', 'href' => $handles[$phid]->getURI(), ), 'View Project Profile'), ); } $table = new AphrontTableView($rows); $table->setHeaders( array( 'Name', '', )); $table->setColumnClasses( array( 'pri', 'action right', )); return $table; } } diff --git a/src/infrastructure/query/policy/PhabricatorIDPagedPolicyQuery.php b/src/infrastructure/query/policy/PhabricatorIDPagedPolicyQuery.php index 258270b508..c0b924c3a3 100644 --- a/src/infrastructure/query/policy/PhabricatorIDPagedPolicyQuery.php +++ b/src/infrastructure/query/policy/PhabricatorIDPagedPolicyQuery.php @@ -1,129 +1,129 @@ getID(); } protected function nextPage(array $page) { if ($this->beforeID) { $this->beforeID = $this->getPagingValue(head($page)); } else { $this->afterID = $this->getPagingValue(last($page)); } } final public function setAfterID($object_id) { $this->afterID = $object_id; return $this; } final public function setBeforeID($object_id) { $this->beforeID = $object_id; return $this; } final protected function buildLimitClause(AphrontDatabaseConnection $conn_r) { if ($this->getLimit()) { return qsprintf($conn_r, 'LIMIT %d', $this->getLimit()); } else { return ''; } } final protected function buildPagingClause( AphrontDatabaseConnection $conn_r) { if ($this->beforeID) { return qsprintf( $conn_r, - '%C > %s', + '%Q > %s', $this->getPagingColumn(), $this->beforeID); } else if ($this->afterID) { return qsprintf( $conn_r, - '%C < %s', + '%Q < %s', $this->getPagingColumn(), $this->afterID); } return null; } final protected function buildOrderClause(AphrontDatabaseConnection $conn_r) { if ($this->beforeID) { return qsprintf( $conn_r, - 'ORDER BY %C ASC', + 'ORDER BY %Q ASC', $this->getPagingColumn()); } else { return qsprintf( $conn_r, - 'ORDER BY %C DESC', + 'ORDER BY %Q DESC', $this->getPagingColumn()); } } final protected function processResults(array $results) { if ($this->beforeID) { $results = array_reverse($results, $preserve_keys = true); } return $results; } final public function executeWithPager(AphrontIDPagerView $pager) { $this->setLimit($pager->getPageSize() + 1); if ($pager->getAfterID()) { $this->setAfterID($pager->getAfterID()); } else if ($pager->getBeforeID()) { $this->setBeforeID($pager->getBeforeID()); } $results = $this->execute(); $sliced_results = $pager->sliceResults($results); if ($this->beforeID || (count($results) > $pager->getPageSize())) { $pager->setNextPageID($this->getPagingValue(last($sliced_results))); } if ($this->afterID || ($this->beforeID && (count($results) > $pager->getPageSize()))) { $pager->setPrevPageID($this->getPagingValue(head($sliced_results))); } return $sliced_results; } }