diff --git a/conf/default.conf.php b/conf/default.conf.php index 334e0a12b9..98b7d9ca7f 100644 --- a/conf/default.conf.php +++ b/conf/default.conf.php @@ -1,986 +1,982 @@ <?php /* * Copyright 2012 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ return array( // The root URI which Phabricator is installed on. // Example: "http://phabricator.example.com/" 'phabricator.base-uri' => null, // If you have multiple environments, provide the production environment URI // here so that emails, etc., generated in development/sandbox environments // contain the right links. 'phabricator.production-uri' => null, // Setting this to 'true' will invoke a special setup mode which helps guide // you through setting up Phabricator. 'phabricator.setup' => false, - // The default PHID for users who haven't uploaded a profile image. It should - // be 50x50px. - 'user.default-profile-image-phid' => 'PHID-FILE-4d61229816cfe6f2b2a3', - // -- 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, // -- 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', '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', // -- 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. // - 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. // // 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', // 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, // -- 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, // -- 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 probably should leave this // alone. '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', // ".ico" favicon files, which have mime type diversity. See: // http://en.wikipedia.org/wiki/ICO_(file_format)#MIME_type 'image/x-ico' => 'image/x-icon', 'image/x-icon' => 'image/x-icon', 'image/vnd.microsoft.icon' => 'image/x-icon', ), // List of mime types which can be used as the source for an <img /> tag. // This should be a subset of 'files.viewable-mime-types' and exclude files // like text. 'files.image-mime-types' => array( 'image/jpeg' => true, 'image/jpg' => true, 'image/png' => true, 'image/gif' => true, 'image/x-ico' => true, 'image/x-icon' => true, 'image/vnd.microsoft.icon' => true, ), // Phabricator can proxy images from other servers so you can paste the URI // to a funny picture of a cat into the comment box and have it show up as an // image. However, this means the webserver Phabricator is running on will // make HTTP requests to arbitrary URIs. If the server has access to internal // resources, this could be a security risk. You should only enable it if you // are installed entirely a VPN and VPN access is required to access // Phabricator, or if the webserver has no special access to anything. If // unsure, it is safer to leave this disabled. 'files.enable-proxy' => false, // -- Storage --------------------------------------------------------------- // // Phabricator allows users to upload files, and can keep them in various // storage engines. This section allows you to configure which engines // Phabricator will use, and how it will use them. // The largest filesize Phabricator will store in the MySQL BLOB storage // engine, which just uses a database table to store files. While this isn't a // best practice, it's really easy to set up. 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', // -- 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 were 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. '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', // 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(), // -- Pygments -------------------------------------------------------------- // // Phabricator can highlight PHP by default, but if you want syntax // highlighting for other languages you should 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/user/base/ConduitAPI_user_Method.php b/src/applications/conduit/method/user/base/ConduitAPI_user_Method.php index ec73278b95..c74029b021 100644 --- a/src/applications/conduit/method/user/base/ConduitAPI_user_Method.php +++ b/src/applications/conduit/method/user/base/ConduitAPI_user_Method.php @@ -1,43 +1,35 @@ <?php /* * Copyright 2012 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @group conduit */ abstract class ConduitAPI_user_Method extends ConduitAPIMethod { protected function buildUserInformationDictionary(PhabricatorUser $user) { - $src_phid = $user->getProfileImagePHID(); - $file = id(new PhabricatorFile())->loadOneWhere('phid = %s', $src_phid); - if ($file) { - $picture = $file->getBestURI(); - } else { - $picture = null; - } - return array( 'phid' => $user->getPHID(), 'userName' => $user->getUserName(), 'realName' => $user->getRealName(), 'email' => $user->getEmail(), - 'image' => $picture, + 'image' => $user->loadProfileImageURI(), 'uri' => PhabricatorEnv::getURI('/p/'.$user->getUsername().'/'), ); } } diff --git a/src/applications/conduit/method/user/base/__init__.php b/src/applications/conduit/method/user/base/__init__.php index 1780cf774f..a91e4dcbd1 100644 --- a/src/applications/conduit/method/user/base/__init__.php +++ b/src/applications/conduit/method/user/base/__init__.php @@ -1,16 +1,13 @@ <?php /** * This file is automatically generated. Lint this module to rebuild it. * @generated */ phutil_require_module('phabricator', 'applications/conduit/method/base'); -phutil_require_module('phabricator', 'applications/files/storage/file'); phutil_require_module('phabricator', 'infrastructure/env'); -phutil_require_module('phutil', 'utils'); - phutil_require_source('ConduitAPI_user_Method.php'); diff --git a/src/applications/people/controller/profile/PhabricatorPeopleProfileController.php b/src/applications/people/controller/profile/PhabricatorPeopleProfileController.php index 111795f2df..ff91c71758 100644 --- a/src/applications/people/controller/profile/PhabricatorPeopleProfileController.php +++ b/src/applications/people/controller/profile/PhabricatorPeopleProfileController.php @@ -1,222 +1,216 @@ <?php /* * Copyright 2012 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ final class PhabricatorPeopleProfileController extends PhabricatorPeopleController { private $username; private $page; public function willProcessRequest(array $data) { $this->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}'!"); } - $src_phid = $user->getProfileImagePHID(); - $file = id(new PhabricatorFile())->loadOneWhere('phid = %s', $src_phid); - if ($file) { - $picture = $file->getBestURI(); - } else { - $picture = null; - } + $picture = $user->loadProfileImageURI(); $header = new PhabricatorProfileHeaderView(); $header ->setProfilePicture($picture) ->setName($user->getUserName().' ('.$user->getRealName().')') ->setDescription($profile->getTitle()); $header->appendChild($nav); $nav->appendChild( '<div style="padding: 1em;">'.$content.'</div>'); 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 = '<div class="phabricator-profile-info-group"> <h1 class="phabricator-profile-info-header">Basic Information</h1> <div class="phabricator-profile-info-pane"> <table class="phabricator-profile-info-table"> <tr> <th>PHID</th> <td>'.phutil_escape_html($user->getPHID()).'</td> </tr> <tr> <th>User Since</th> <td>'.phabricator_datetime($user->getDateCreated(), $viewer). '</td> </tr> </table> </div> </div>'; $content .= '<div class="phabricator-profile-info-group"> <h1 class="phabricator-profile-info-header">Flavor Text</h1> <div class="phabricator-profile-info-pane"> <table class="phabricator-profile-info-table"> <tr> <th>Blurb</th> <td>'.$blurb.'</td> </tr> </table> </div> </div>'; return $content; } private function renderUserFeed(PhabricatorUser $user) { $query = new PhabricatorFeedQuery(); $query->setFilterPHIDs( array( $user->getPHID(), )); $stories = $query->execute(); $builder = new PhabricatorFeedBuilder($stories); $builder->setUser($this->getRequest()->getUser()); $view = $builder->buildView(); return '<div class="phabricator-profile-info-group"> <h1 class="phabricator-profile-info-header">Activity Feed</h1> <div class="phabricator-profile-info-pane"> '.$view->render().' </div> </div>'; } } diff --git a/src/applications/people/controller/profile/__init__.php b/src/applications/people/controller/profile/__init__.php index 110a677ab4..a4f50fdc45 100644 --- a/src/applications/people/controller/profile/__init__.php +++ b/src/applications/people/controller/profile/__init__.php @@ -1,29 +1,28 @@ <?php /** * This file is automatically generated. Lint this module to rebuild it. * @generated */ phutil_require_module('phabricator', 'aphront/response/404'); phutil_require_module('phabricator', 'applications/auth/oauth/provider/base'); phutil_require_module('phabricator', 'applications/feed/builder/feed'); phutil_require_module('phabricator', 'applications/feed/query'); -phutil_require_module('phabricator', 'applications/files/storage/file'); phutil_require_module('phabricator', 'applications/markup/engine'); phutil_require_module('phabricator', 'applications/people/controller/base'); phutil_require_module('phabricator', 'applications/people/storage/profile'); phutil_require_module('phabricator', 'applications/people/storage/user'); phutil_require_module('phabricator', 'applications/people/storage/useroauthinfo'); phutil_require_module('phabricator', 'infrastructure/celerity/api'); phutil_require_module('phabricator', 'view/layout/profileheader'); phutil_require_module('phabricator', 'view/layout/sidenavfilter'); phutil_require_module('phabricator', 'view/utils'); phutil_require_module('phutil', 'markup'); phutil_require_module('phutil', 'parser/uri'); phutil_require_module('phutil', 'utils'); phutil_require_source('PhabricatorPeopleProfileController.php'); diff --git a/src/applications/people/controller/settings/panels/profile/PhabricatorUserProfileSettingsPanelController.php b/src/applications/people/controller/settings/panels/profile/PhabricatorUserProfileSettingsPanelController.php index 8c8e75c859..4856a04d5a 100644 --- a/src/applications/people/controller/settings/panels/profile/PhabricatorUserProfileSettingsPanelController.php +++ b/src/applications/people/controller/settings/panels/profile/PhabricatorUserProfileSettingsPanelController.php @@ -1,195 +1,188 @@ <?php /* * Copyright 2012 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ final class PhabricatorUserProfileSettingsPanelController extends PhabricatorUserSettingsPanelController { public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $profile = id(new PhabricatorUserProfile())->loadOneWhere( 'userPHID = %s', $user->getPHID()); if (!$profile) { $profile = new PhabricatorUserProfile(); $profile->setUserPHID($user->getPHID()); } $supported_formats = PhabricatorFile::getTransformableImageFormats(); $e_image = null; $errors = array(); if ($request->isFormPost()) { $profile->setTitle($request->getStr('title')); $profile->setBlurb($request->getStr('blurb')); $sex = $request->getStr('sex'); if (in_array($sex, array('m', 'f'))) { $user->setSex($sex); } else { $user->setSex(null); } if (!empty($_FILES['image'])) { $err = idx($_FILES['image'], 'error'); if ($err != UPLOAD_ERR_NO_FILE) { $file = PhabricatorFile::newFromPHPUpload( $_FILES['image'], array( 'authorPHID' => $user->getPHID(), )); $okay = $file->isTransformableImage(); if ($okay) { $xformer = new PhabricatorImageTransformer(); // Generate the large picture for the profile page. $large_xformed = $xformer->executeProfileTransform( $file, $width = 280, $min_height = 140, $max_height = 420); $profile->setProfileImagePHID($large_xformed->getPHID()); // Generate the small picture for comments, etc. $small_xformed = $xformer->executeProfileTransform( $file, $width = 50, $min_height = 50, $max_height = 50); $user->setProfileImagePHID($small_xformed->getPHID()); } else { $e_image = 'Not Supported'; $errors[] = 'This server only supports these image formats: '. implode(', ', $supported_formats).'.'; } } } if (!$errors) { $user->save(); $profile->save(); $response = id(new AphrontRedirectResponse()) ->setURI('/settings/page/profile/?saved=true'); return $response; } } $error_view = null; if ($errors) { $error_view = new AphrontErrorView(); $error_view->setTitle('Form Errors'); $error_view->setErrors($errors); } else { if ($request->getStr('saved')) { $error_view = new AphrontErrorView(); $error_view->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $error_view->setTitle('Changes Saved'); $error_view->appendChild('<p>Your changes have been saved.</p>'); $error_view = $error_view->render(); } } - $file = id(new PhabricatorFile())->loadOneWhere( - 'phid = %s', - $user->getProfileImagePHID()); - if ($file) { - $img_src = $file->getBestURI(); - } else { - $img_src = null; - } + $img_src = $user->loadProfileImageURI(); $profile_uri = PhabricatorEnv::getURI('/p/'.$user->getUsername().'/'); $sexes = array( '' => 'Unknown', 'm' => 'Male', 'f' => 'Female', ); $form = new AphrontFormView(); $form ->setUser($request->getUser()) ->setAction('/settings/page/profile/') ->setEncType('multipart/form-data') ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Title') ->setName('title') ->setValue($profile->getTitle()) ->setCaption('Serious business title.')) ->appendChild( id(new AphrontFormSelectControl()) ->setOptions($sexes) ->setLabel('Sex') ->setName('sex') ->setValue($user->getSex())) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel('Profile URI') ->setValue( phutil_render_tag( 'a', array( 'href' => $profile_uri, ), phutil_escape_html($profile_uri)))) ->appendChild( '<p class="aphront-form-instructions">Write something about yourself! '. 'Make sure to include <strong>important information</strong> like '. 'your favorite pokemon and which Starcraft race you play.</p>') ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel('Blurb') ->setName('blurb') ->setValue($profile->getBlurb())) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel('Profile Image') ->setValue( phutil_render_tag( 'img', array( 'src' => $img_src, )))) ->appendChild( id(new AphrontFormFileControl()) ->setLabel('Change Image') ->setName('image') ->setError($e_image) ->setCaption('Supported formats: '.implode(', ', $supported_formats))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue('Save') ->addCancelButton('/p/'.$user->getUsername().'/')); $panel = new AphrontPanelView(); $panel->setHeader('Edit Profile Details'); $panel->appendChild($form); $panel->setWidth(AphrontPanelView::WIDTH_FORM); return id(new AphrontNullView()) ->appendChild( array( $error_view, $panel, )); } } diff --git a/src/applications/people/storage/user/PhabricatorUser.php b/src/applications/people/storage/user/PhabricatorUser.php index b5d495ac53..4612b0456a 100644 --- a/src/applications/people/storage/user/PhabricatorUser.php +++ b/src/applications/people/storage/user/PhabricatorUser.php @@ -1,526 +1,537 @@ <?php /* * Copyright 2012 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ final class PhabricatorUser extends PhabricatorUserDAO { const SESSION_TABLE = 'phabricator_session'; const NAMETOKEN_TABLE = 'user_nametoken'; protected $phid; protected $userName; protected $realName; protected $email; protected $sex; protected $passwordSalt; protected $passwordHash; protected $profileImagePHID; protected $timezoneIdentifier = ''; protected $consoleEnabled = 0; protected $consoleVisible = 0; protected $consoleTab = ''; protected $conduitCertificate; protected $isSystemAgent = 0; protected $isAdmin = 0; protected $isDisabled = 0; private $preferences = null; protected function readField($field) { switch ($field) { - case 'profileImagePHID': - return nonempty( - $this->profileImagePHID, - PhabricatorEnv::getEnvConfig('user.default-profile-image-phid')); case 'timezoneIdentifier': // If the user hasn't set one, guess the server's time. return nonempty( $this->timezoneIdentifier, date_default_timezone_get()); // Make sure these return booleans. case 'isAdmin': return (bool)$this->isAdmin; case 'isDisabled': return (bool)$this->isDisabled; case 'isSystemAgent': return (bool)$this->isSystemAgent; default: return parent::readField($field); } } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_PARTIAL_OBJECTS => true, ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPHIDConstants::PHID_TYPE_USER); } public function setPassword($password) { if (!$this->getPHID()) { throw new Exception( "You can not set a password for an unsaved user because their PHID ". "is a salt component in the password hash."); } if (!strlen($password)) { $this->setPasswordHash(''); } else { $this->setPasswordSalt(md5(mt_rand())); $hash = $this->hashPassword($password); $this->setPasswordHash($hash); } return $this; } public function isLoggedIn() { return !($this->getPHID() === null); } public function save() { if (!$this->getConduitCertificate()) { $this->setConduitCertificate($this->generateConduitCertificate()); } $result = parent::save(); $this->updateNameTokens(); PhabricatorSearchUserIndexer::indexUser($this); return $result; } private function generateConduitCertificate() { return Filesystem::readRandomCharacters(255); } public function comparePassword($password) { if (!strlen($password)) { return false; } if (!strlen($this->getPasswordHash())) { return false; } $password = $this->hashPassword($password); return ($password === $this->getPasswordHash()); } private function hashPassword($password) { $password = $this->getUsername(). $password. $this->getPHID(). $this->getPasswordSalt(); for ($ii = 0; $ii < 1000; $ii++) { $password = md5($password); } return $password; } const CSRF_CYCLE_FREQUENCY = 3600; const CSRF_TOKEN_LENGTH = 16; const EMAIL_CYCLE_FREQUENCY = 86400; const EMAIL_TOKEN_LENGTH = 24; public function getCSRFToken($offset = 0) { return $this->generateToken( time() + (self::CSRF_CYCLE_FREQUENCY * $offset), self::CSRF_CYCLE_FREQUENCY, PhabricatorEnv::getEnvConfig('phabricator.csrf-key'), self::CSRF_TOKEN_LENGTH); } public function validateCSRFToken($token) { if (!$this->getPHID()) { return true; } // When the user posts a form, we check that it contains a valid CSRF token. // Tokens cycle each hour (every CSRF_CYLCE_FREQUENCY seconds) and we accept // either the current token, the next token (users can submit a "future" // token if you have two web frontends that have some clock skew) or any of // the last 6 tokens. This means that pages are valid for up to 7 hours. // There is also some Javascript which periodically refreshes the CSRF // tokens on each page, so theoretically pages should be valid indefinitely. // However, this code may fail to run (if the user loses their internet // connection, or there's a JS problem, or they don't have JS enabled). // Choosing the size of the window in which we accept old CSRF tokens is // an issue of balancing concerns between security and usability. We could // choose a very narrow (e.g., 1-hour) window to reduce vulnerability to // attacks using captured CSRF tokens, but it's also more likely that real // users will be affected by this, e.g. if they close their laptop for an // hour, open it back up, and try to submit a form before the CSRF refresh // can kick in. Since the user experience of submitting a form with expired // CSRF is often quite bad (you basically lose data, or it's a big pain to // recover at least) and I believe we gain little additional protection // by keeping the window very short (the overwhelming value here is in // preventing blind attacks, and most attacks which can capture CSRF tokens // can also just capture authentication information [sniffing networks] // or act as the user [xss]) the 7 hour default seems like a reasonable // balance. Other major platforms have much longer CSRF token lifetimes, // like Rails (session duration) and Django (forever), which suggests this // is a reasonable analysis. $csrf_window = 6; for ($ii = -$csrf_window; $ii <= 1; $ii++) { $valid = $this->getCSRFToken($ii); if ($token == $valid) { return true; } } return false; } private function generateToken($epoch, $frequency, $key, $len) { $time_block = floor($epoch / $frequency); $vec = $this->getPHID().$this->getPasswordHash().$key.$time_block; return substr(PhabricatorHash::digest($vec), 0, $len); } /** * Issue a new session key to this user. Phabricator supports different * types of sessions (like "web" and "conduit") and each session type may * have multiple concurrent sessions (this allows a user to be logged in on * multiple browsers at the same time, for instance). * * Note that this method is transport-agnostic and does not set cookies or * issue other types of tokens, it ONLY generates a new session key. * * You can configure the maximum number of concurrent sessions for various * session types in the Phabricator configuration. * * @param string Session type, like "web". * @return string Newly generated session key. */ public function establishSession($session_type) { $conn_w = $this->establishConnection('w'); if (strpos($session_type, '-') !== false) { throw new Exception("Session type must not contain hyphen ('-')!"); } // We allow multiple sessions of the same type, so when a caller requests // a new session of type "web", we give them the first available session in // "web-1", "web-2", ..., "web-N", up to some configurable limit. If none // of these sessions is available, we overwrite the oldest session and // reissue a new one in its place. $session_limit = 1; switch ($session_type) { case 'web': $session_limit = PhabricatorEnv::getEnvConfig('auth.sessions.web'); break; case 'conduit': $session_limit = PhabricatorEnv::getEnvConfig('auth.sessions.conduit'); break; default: throw new Exception("Unknown session type '{$session_type}'!"); } $session_limit = (int)$session_limit; if ($session_limit <= 0) { throw new Exception( "Session limit for '{$session_type}' must be at least 1!"); } // NOTE: Session establishment is sensitive to race conditions, as when // piping `arc` to `arc`: // // arc export ... | arc paste ... // // To avoid this, we overwrite an old session only if it hasn't been // re-established since we read it. // Consume entropy to generate a new session key, forestalling the eventual // heat death of the universe. $session_key = Filesystem::readRandomCharacters(40); // Load all the currently active sessions. $sessions = queryfx_all( $conn_w, 'SELECT type, sessionKey, sessionStart FROM %T WHERE userPHID = %s AND type LIKE %>', PhabricatorUser::SESSION_TABLE, $this->getPHID(), $session_type.'-'); $sessions = ipull($sessions, null, 'type'); $sessions = isort($sessions, 'sessionStart'); $existing_sessions = array_keys($sessions); // UNGUARDED WRITES: Logging-in users don't have CSRF stuff yet. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $retries = 0; while (true) { // Choose which 'type' we'll actually establish, i.e. what number we're // going to append to the basic session type. To do this, just check all // the numbers sequentially until we find an available session. $establish_type = null; for ($ii = 1; $ii <= $session_limit; $ii++) { $try_type = $session_type.'-'.$ii; if (!in_array($try_type, $existing_sessions)) { $establish_type = $try_type; $expect_key = $session_key; $existing_sessions[] = $try_type; // Ensure the row exists so we can issue an update below. We don't // care if we race here or not. queryfx( $conn_w, 'INSERT IGNORE INTO %T (userPHID, type, sessionKey, sessionStart) VALUES (%s, %s, %s, 0)', self::SESSION_TABLE, $this->getPHID(), $establish_type, $session_key); break; } } // If we didn't find an available session, choose the oldest session and // overwrite it. if (!$establish_type) { $oldest = reset($sessions); $establish_type = $oldest['type']; $expect_key = $oldest['sessionKey']; } // This is so that we'll only overwrite the session if it hasn't been // refreshed since we read it. If it has, the session key will be // different and we know we're racing other processes. Whichever one // won gets the session, we go back and try again. queryfx( $conn_w, 'UPDATE %T SET sessionKey = %s, sessionStart = UNIX_TIMESTAMP() WHERE userPHID = %s AND type = %s AND sessionKey = %s', self::SESSION_TABLE, $session_key, $this->getPHID(), $establish_type, $expect_key); if ($conn_w->getAffectedRows()) { // The update worked, so the session is valid. break; } else { // We know this just got grabbed, so don't try it again. unset($sessions[$establish_type]); } if (++$retries > $session_limit) { throw new Exception("Failed to establish a session!"); } } $log = PhabricatorUserLog::newLog( $this, $this, PhabricatorUserLog::ACTION_LOGIN); $log->setDetails( array( 'session_type' => $session_type, 'session_issued' => $establish_type, )); $log->setSession($session_key); $log->save(); return $session_key; } public function destroySession($session_key) { $conn_w = $this->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE userPHID = %s AND sessionKey = %s', self::SESSION_TABLE, $this->getPHID(), $session_key); } private function generateEmailToken($offset = 0) { return $this->generateToken( time() + ($offset * self::EMAIL_CYCLE_FREQUENCY), self::EMAIL_CYCLE_FREQUENCY, PhabricatorEnv::getEnvConfig('phabricator.csrf-key').$this->getEmail(), self::EMAIL_TOKEN_LENGTH); } public function validateEmailToken($token) { for ($ii = -1; $ii <= 1; $ii++) { $valid = $this->generateEmailToken($ii); if ($token == $valid) { return true; } } return false; } public function getEmailLoginURI() { $token = $this->generateEmailToken(); $uri = PhabricatorEnv::getProductionURI('/login/etoken/'.$token.'/'); $uri = new PhutilURI($uri); return $uri->alter('email', $this->getEmail()); } public function loadPreferences() { if ($this->preferences) { return $this->preferences; } $preferences = id(new PhabricatorUserPreferences())->loadOneWhere( 'userPHID = %s', $this->getPHID()); if (!$preferences) { $preferences = new PhabricatorUserPreferences(); $preferences->setUserPHID($this->getPHID()); $default_dict = array( PhabricatorUserPreferences::PREFERENCE_TITLES => 'glyph', PhabricatorUserPreferences::PREFERENCE_EDITOR => '', PhabricatorUserPreferences::PREFERENCE_MONOSPACED => ''); $preferences->setPreferences($default_dict); } $this->preferences = $preferences; return $preferences; } public function loadEditorLink($path, $line, $callsign) { $editor = $this->loadPreferences()->getPreference( PhabricatorUserPreferences::PREFERENCE_EDITOR); if ($editor) { return strtr($editor, array( '%%' => '%', '%f' => phutil_escape_uri($path), '%l' => phutil_escape_uri($line), '%r' => phutil_escape_uri($callsign), )); } } private static function tokenizeName($name) { if (function_exists('mb_strtolower')) { $name = mb_strtolower($name, 'UTF-8'); } else { $name = strtolower($name); } $name = trim($name); if (!strlen($name)) { return array(); } return preg_split('/\s+/', $name); } /** * Populate the nametoken table, which used to fetch typeahead results. When * a user types "linc", we want to match "Abraham Lincoln" from on-demand * typeahead sources. To do this, we need a separate table of name fragments. */ public function updateNameTokens() { $tokens = array_merge( self::tokenizeName($this->getRealName()), self::tokenizeName($this->getUserName())); $tokens = array_unique($tokens); $table = self::NAMETOKEN_TABLE; $conn_w = $this->establishConnection('w'); $sql = array(); foreach ($tokens as $token) { $sql[] = qsprintf( $conn_w, '(%d, %s)', $this->getID(), $token); } queryfx( $conn_w, 'DELETE FROM %T WHERE userID = %d', $table, $this->getID()); if ($sql) { queryfx( $conn_w, 'INSERT INTO %T (userID, token) VALUES %Q', $table, implode(', ', $sql)); } } public function sendWelcomeEmail(PhabricatorUser $admin) { $admin_username = $admin->getUserName(); $admin_realname = $admin->getRealName(); $user_username = $this->getUserName(); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $base_uri = PhabricatorEnv::getProductionURI('/'); $uri = $this->getEmailLoginURI(); $body = <<<EOBODY Welcome to Phabricator! {$admin_username} ({$admin_realname}) has created an account for you. Username: {$user_username} To login to Phabricator, follow this link and set a password: {$uri} After you have set a password, you can login in the future by going here: {$base_uri} EOBODY; if (!$is_serious) { $body .= <<<EOBODY Love, Phabricator EOBODY; } $mail = id(new PhabricatorMetaMTAMail()) ->addTos(array($this->getPHID())) ->setSubject('[Phabricator] Welcome to Phabricator') ->setBody($body) ->setFrom($admin->getPHID()) ->saveAndSend(); } public static function validateUsername($username) { return (bool)preg_match('/^[a-zA-Z0-9]+$/', $username); } + public static function getDefaultProfileImageURI() { + return celerity_get_resource_uri('/rsrc/image/avatar.png'); + } + + public function loadProfileImageURI() { + $src_phid = $this->getProfileImagePHID(); + + $file = id(new PhabricatorFile())->loadOneWhere('phid = %s', $src_phid); + if ($file) { + return $file->getBestURI(); + } + + return self::getDefaultProfileImageURI(); + } + } diff --git a/src/applications/people/storage/user/__init__.php b/src/applications/people/storage/user/__init__.php index 910e09c634..3201c0f2e1 100644 --- a/src/applications/people/storage/user/__init__.php +++ b/src/applications/people/storage/user/__init__.php @@ -1,28 +1,30 @@ <?php /** * This file is automatically generated. Lint this module to rebuild it. * @generated */ phutil_require_module('phabricator', 'aphront/writeguard'); +phutil_require_module('phabricator', 'applications/files/storage/file'); phutil_require_module('phabricator', 'applications/metamta/storage/mail'); phutil_require_module('phabricator', 'applications/people/storage/base'); phutil_require_module('phabricator', 'applications/people/storage/log'); phutil_require_module('phabricator', 'applications/people/storage/preferences'); phutil_require_module('phabricator', 'applications/phid/constants'); phutil_require_module('phabricator', 'applications/phid/storage/phid'); phutil_require_module('phabricator', 'applications/search/index/indexer/user'); +phutil_require_module('phabricator', 'infrastructure/celerity/api'); phutil_require_module('phabricator', 'infrastructure/env'); phutil_require_module('phabricator', 'infrastructure/util/hash'); phutil_require_module('phabricator', 'storage/qsprintf'); phutil_require_module('phabricator', 'storage/queryfx'); phutil_require_module('phutil', 'filesystem'); phutil_require_module('phutil', 'markup'); phutil_require_module('phutil', 'parser/uri'); phutil_require_module('phutil', 'utils'); phutil_require_source('PhabricatorUser.php'); diff --git a/src/applications/phid/handle/data/PhabricatorObjectHandleData.php b/src/applications/phid/handle/data/PhabricatorObjectHandleData.php index a50a106b4d..de1ca5f562 100644 --- a/src/applications/phid/handle/data/PhabricatorObjectHandleData.php +++ b/src/applications/phid/handle/data/PhabricatorObjectHandleData.php @@ -1,483 +1,486 @@ <?php /* * Copyright 2012 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ final class PhabricatorObjectHandleData { private $phids; public function __construct(array $phids) { $this->phids = array_unique($phids); } public static function loadOneHandle($phid) { $handles = id(new PhabricatorObjectHandleData(array($phid)))->loadHandles(); return $handles[$phid]; } public function loadObjects() { $types = array(); foreach ($this->phids as $phid) { $type = phid_get_type($phid); $types[$type][] = $phid; } $objects = array(); foreach ($types as $type => $phids) { switch ($type) { case PhabricatorPHIDConstants::PHID_TYPE_USER: $user_dao = newv('PhabricatorUser', array()); $users = $user_dao->loadAllWhere( 'phid in (%Ls)', $phids); foreach ($users as $user) { $objects[$user->getPHID()] = $user; } break; case PhabricatorPHIDConstants::PHID_TYPE_CMIT: $commit_dao = newv('PhabricatorRepositoryCommit', array()); $commits = $commit_dao->loadAllWhere( 'phid IN (%Ls)', $phids); $commit_data = array(); if ($commits) { $data_dao = newv('PhabricatorRepositoryCommitData', array()); $commit_data = $data_dao->loadAllWhere( 'commitID IN (%Ld)', mpull($commits, 'getID')); $commit_data = mpull($commit_data, null, 'getCommitID'); } foreach ($commits as $commit) { $data = idx($commit_data, $commit->getID()); if ($data) { $commit->attachCommitData($data); $objects[$commit->getPHID()] = $commit; } else { // If we couldn't load the commit data, just act as though we // couldn't load the object at all so we don't load half an object. } } break; case PhabricatorPHIDConstants::PHID_TYPE_TASK: $task_dao = newv('ManiphestTask', array()); $tasks = $task_dao->loadAllWhere( 'phid IN (%Ls)', $phids); foreach ($tasks as $task) { $objects[$task->getPHID()] = $task; } break; case PhabricatorPHIDConstants::PHID_TYPE_DREV: $revision_dao = newv('DifferentialRevision', array()); $revisions = $revision_dao->loadAllWhere( 'phid IN (%Ls)', $phids); foreach ($revisions as $revision) { $objects[$revision->getPHID()] = $revision; } break; } } return $objects; } public function loadHandles() { $types = phid_group_by_type($this->phids); $handles = array(); $external_loaders = PhabricatorEnv::getEnvConfig('phid.external-loaders'); foreach ($types as $type => $phids) { switch ($type) { case PhabricatorPHIDConstants::PHID_TYPE_MAGIC: // Black magic! foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); switch ($phid) { case ManiphestTaskOwner::OWNER_UP_FOR_GRABS: $handle->setName('Up For Grabs'); $handle->setFullName('upforgrabs (Up For Grabs)'); $handle->setComplete(true); break; case ManiphestTaskOwner::PROJECT_NO_PROJECT: $handle->setName('No Project'); $handle->setFullName('noproject (No Project)'); $handle->setComplete(true); break; default: $handle->setName('Foul Magicks'); break; } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_USER: $class = 'PhabricatorUser'; PhutilSymbolLoader::loadClass($class); $object = newv($class, array()); $users = $object->loadAllWhere('phid IN (%Ls)', $phids); $users = mpull($users, null, 'getPHID'); $image_phids = mpull($users, 'getProfileImagePHID'); $image_phids = array_unique(array_filter($image_phids)); $images = array(); if ($image_phids) { $images = id(new PhabricatorFile())->loadAllWhere( 'phid IN (%Ls)', $image_phids); $images = mpull($images, 'getBestURI', 'getPHID'); } foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($users[$phid])) { $handle->setName('Unknown User'); } else { $user = $users[$phid]; $handle->setName($user->getUsername()); $handle->setURI('/p/'.$user->getUsername().'/'); $handle->setEmail($user->getEmail()); $handle->setFullName( $user->getUsername().' ('.$user->getRealName().')'); $handle->setAlternateID($user->getID()); $handle->setComplete(true); $handle->setDisabled($user->getIsDisabled()); $img_uri = idx($images, $user->getProfileImagePHID()); if ($img_uri) { $handle->setImageURI($img_uri); + } else { + $handle->setImageURI( + PhabricatorUser::getDefaultProfileImageURI()); } } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_MLST: $class = 'PhabricatorMetaMTAMailingList'; PhutilSymbolLoader::loadClass($class); $object = newv($class, array()); $lists = $object->loadAllWhere('phid IN (%Ls)', $phids); $lists = mpull($lists, null, 'getPHID'); foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($lists[$phid])) { $handle->setName('Unknown Mailing List'); } else { $list = $lists[$phid]; $handle->setEmail($list->getEmail()); $handle->setName($list->getName()); $handle->setURI($list->getURI()); $handle->setFullName($list->getName()); $handle->setComplete(true); } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_DREV: $class = 'DifferentialRevision'; PhutilSymbolLoader::loadClass($class); $object = newv($class, array()); $revs = $object->loadAllWhere('phid in (%Ls)', $phids); $revs = mpull($revs, null, 'getPHID'); foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($revs[$phid])) { $handle->setName('Unknown Revision'); } else { $rev = $revs[$phid]; $handle->setName($rev->getTitle()); $handle->setURI('/D'.$rev->getID()); $handle->setFullName('D'.$rev->getID().': '.$rev->getTitle()); $handle->setComplete(true); $status = $rev->getStatus(); if (($status == ArcanistDifferentialRevisionStatus::CLOSED) || ($status == ArcanistDifferentialRevisionStatus::ABANDONED)) { $closed = PhabricatorObjectHandleStatus::STATUS_CLOSED; $handle->setStatus($closed); } } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_CMIT: $class = 'PhabricatorRepositoryCommit'; PhutilSymbolLoader::loadClass($class); $object = newv($class, array()); $commits = $object->loadAllWhere('phid in (%Ls)', $phids); $commits = mpull($commits, null, 'getPHID'); $repository_ids = array(); $callsigns = array(); if ($commits) { $repository_ids = mpull($commits, 'getRepositoryID'); $repositories = id(new PhabricatorRepository())->loadAllWhere( 'id in (%Ld)', array_unique($repository_ids)); $callsigns = mpull($repositories, 'getCallsign'); } foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($commits[$phid]) || !isset($callsigns[$repository_ids[$phid]])) { $handle->setName('Unknown Commit'); } else { $commit = $commits[$phid]; $callsign = $callsigns[$repository_ids[$phid]]; $repository = $repositories[$repository_ids[$phid]]; $commit_identifier = $commit->getCommitIdentifier(); // In case where the repository for the commit was deleted, // we don't have have info about the repository anymore. if ($repository) { $name = $repository->formatCommitName($commit_identifier); $handle->setName($name); } else { $handle->setName('Commit '.'r'.$callsign.$commit_identifier); } $handle->setURI('/r'.$callsign.$commit_identifier); $handle->setFullName('r'.$callsign.$commit_identifier); $handle->setTimestamp($commit->getEpoch()); $handle->setComplete(true); } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_TASK: $class = 'ManiphestTask'; PhutilSymbolLoader::loadClass($class); $object = newv($class, array()); $tasks = $object->loadAllWhere('phid in (%Ls)', $phids); $tasks = mpull($tasks, null, 'getPHID'); foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($tasks[$phid])) { $handle->setName('Unknown Revision'); } else { $task = $tasks[$phid]; $handle->setName($task->getTitle()); $handle->setURI('/T'.$task->getID()); $handle->setFullName('T'.$task->getID().': '.$task->getTitle()); $handle->setComplete(true); $handle->setAlternateID($task->getID()); if ($task->getStatus() != ManiphestTaskStatus::STATUS_OPEN) { $closed = PhabricatorObjectHandleStatus::STATUS_CLOSED; $handle->setStatus($closed); } } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_FILE: $class = 'PhabricatorFile'; PhutilSymbolLoader::loadClass($class); $object = newv($class, array()); $files = $object->loadAllWhere('phid IN (%Ls)', $phids); $files = mpull($files, null, 'getPHID'); foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($files[$phid])) { $handle->setName('Unknown File'); } else { $file = $files[$phid]; $handle->setName($file->getName()); $handle->setURI($file->getBestURI()); $handle->setComplete(true); } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_PROJ: $class = 'PhabricatorProject'; PhutilSymbolLoader::loadClass($class); $object = newv($class, array()); $projects = $object->loadAllWhere('phid IN (%Ls)', $phids); $projects = mpull($projects, null, 'getPHID'); foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($projects[$phid])) { $handle->setName('Unknown Project'); } else { $project = $projects[$phid]; $handle->setName($project->getName()); $handle->setURI('/project/view/'.$project->getID().'/'); $handle->setComplete(true); } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_REPO: $class = 'PhabricatorRepository'; PhutilSymbolLoader::loadClass($class); $object = newv($class, array()); $repositories = $object->loadAllWhere('phid in (%Ls)', $phids); $repositories = mpull($repositories, null, 'getPHID'); foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($repositories[$phid])) { $handle->setName('Unknown Repository'); } else { $repository = $repositories[$phid]; $handle->setName($repository->getCallsign()); $handle->setURI('/diffusion/'.$repository->getCallsign().'/'); $handle->setComplete(true); } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_OPKG: $class = 'PhabricatorOwnersPackage'; PhutilSymbolLoader::loadClass($class); $object = newv($class, array()); $packages = $object->loadAllWhere('phid in (%Ls)', $phids); $packages = mpull($packages, null, 'getPHID'); foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($packages[$phid])) { $handle->setName('Unknown Package'); } else { $package = $packages[$phid]; $handle->setName($package->getName()); $handle->setURI('/owners/package/'.$package->getID().'/'); $handle->setComplete(true); } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_APRJ: $project_dao = newv('PhabricatorRepositoryArcanistProject', array()); $projects = $project_dao->loadAllWhere( 'phid IN (%Ls)', $phids); $projects = mpull($projects, null, 'getPHID'); foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($projects[$phid])) { $handle->setName('Unknown Arcanist Project'); } else { $project = $projects[$phid]; $handle->setName($project->getName()); $handle->setComplete(true); } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_WIKI: $document_dao = newv('PhrictionDocument', array()); $content_dao = newv('PhrictionContent', array()); $conn = $document_dao->establishConnection('r'); $documents = queryfx_all( $conn, 'SELECT * FROM %T document JOIN %T content ON document.contentID = content.id WHERE document.phid IN (%Ls)', $document_dao->getTableName(), $content_dao->getTableName(), $phids); $documents = ipull($documents, null, 'phid'); foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($documents[$phid])) { $handle->setName('Unknown Document'); } else { $info = $documents[$phid]; $handle->setName($info['title']); $handle->setURI(PhrictionDocument::getSlugURI($info['slug'])); $handle->setComplete(true); } $handles[$phid] = $handle; } break; default: $loader = null; if (isset($external_loaders[$type])) { $loader = $external_loaders[$type]; } else if (isset($external_loaders['*'])) { $loader = $external_loaders['*']; } if ($loader) { PhutilSymbolLoader::loadClass($loader); $object = newv($loader, array()); $handles += $object->loadHandles($phids); break; } foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setType($type); $handle->setPHID($phid); $handle->setName('Unknown Object'); $handle->setFullName('An Unknown Object'); $handles[$phid] = $handle; } break; } } return $handles; } } diff --git a/src/applications/phid/handle/data/__init__.php b/src/applications/phid/handle/data/__init__.php index 3cfb40dbf3..c2df9169cd 100644 --- a/src/applications/phid/handle/data/__init__.php +++ b/src/applications/phid/handle/data/__init__.php @@ -1,27 +1,28 @@ <?php /** * This file is automatically generated. Lint this module to rebuild it. * @generated */ phutil_require_module('arcanist', 'differential/constants/revisionstatus'); phutil_require_module('phabricator', 'applications/files/storage/file'); phutil_require_module('phabricator', 'applications/maniphest/constants/owner'); phutil_require_module('phabricator', 'applications/maniphest/constants/status'); +phutil_require_module('phabricator', 'applications/people/storage/user'); phutil_require_module('phabricator', 'applications/phid/constants'); phutil_require_module('phabricator', 'applications/phid/handle'); phutil_require_module('phabricator', 'applications/phid/handle/const/status'); phutil_require_module('phabricator', 'applications/phid/utils'); phutil_require_module('phabricator', 'applications/phriction/storage/document'); phutil_require_module('phabricator', 'applications/repository/storage/repository'); phutil_require_module('phabricator', 'infrastructure/env'); phutil_require_module('phabricator', 'storage/queryfx'); phutil_require_module('phutil', 'symbols'); phutil_require_module('phutil', 'utils'); phutil_require_source('PhabricatorObjectHandleData.php'); diff --git a/src/applications/project/controller/profile/PhabricatorProjectProfileController.php b/src/applications/project/controller/profile/PhabricatorProjectProfileController.php index 22c03f7a08..9a7528810d 100644 --- a/src/applications/project/controller/profile/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/profile/PhabricatorProjectProfileController.php @@ -1,381 +1,381 @@ <?php /* * Copyright 2012 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ final class PhabricatorProjectProfileController extends PhabricatorProjectController { private $id; private $page; public function willProcessRequest(array $data) { $this->id = idx($data, 'id'); $this->page = idx($data, 'page'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $project = id(new PhabricatorProject())->load($this->id); if (!$project) { return new Aphront404Response(); } $profile = $project->loadProfile(); if (!$profile) { $profile = new PhabricatorProjectProfile(); } $src_phid = $profile->getProfileImagePHID(); if (!$src_phid) { $src_phid = $user->getProfileImagePHID(); } $file = id(new PhabricatorFile())->loadOneWhere('phid = %s', $src_phid); if ($file) { $picture = $file->getBestURI(); } else { - $picture = null; + $picture = PhabricatorUser::getDefaultProfileImageURI(); } $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(), )); $stories = $query->execute(); $content .= $this->renderStories($stories); break; case 'about': $content = $this->renderAboutPage($project, $profile); break; case 'people': $content = $this->renderPeoplePage($project, $profile); break; case 'feed': $content = $this->renderFeedPage($project, $profile); break; default: throw new Exception("Unimplemented filter '{$this->page}'."); } $content = '<div style="padding: 1em;">'.$content.'</div>'; $nav_view->appendChild($content); $header = new PhabricatorProfileHeaderView(); $header->setName($project->getName()); $header->setDescription( phutil_utf8_shorten($profile->getBlurb(), 1024)); $header->setProfilePicture($picture); $action = null; if (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", '<br />', $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 = '<div class="phabricator-profile-info-group"> <h1 class="phabricator-profile-info-header">About</h1> <div class="phabricator-profile-info-pane"> <table class="phabricator-profile-info-table"> <tr> <th>Creator</th> <td>'.$handles[$project->getAuthorPHID()]->renderLink().'</td> </tr> <tr> <th>Created</th> <td>'.$timestamp.'</td> </tr> <tr> <th>PHID</th> <td>'.phutil_escape_html($project->getPHID()).'</td> </tr> <tr> <th>Blurb</th> <td>'.$blurb.'</td> </tr> </table> </div> </div>'; if ($project->getSubprojectPHIDs()) { $table = $this->renderSubprojectTable( $handles, $project->getSubprojectPHIDs()); $subproject_list = $table->render(); } else { $subproject_list = '<p><em>No subprojects.</em></p>'; } $about .= '<div class="phabricator-profile-info-group">'. '<h1 class="phabricator-profile-info-header">Subprojects</h1>'. '<div class="phabricator-profile-info-pane">'. $subproject_list. '</div>'. '</div>'; 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[] = '<li>'.$user.' — '.$role.'</li>'; } if ($affiliated) { $affiliated = '<ul>'.implode("\n", $affiliated).'</ul>'; } else { $affiliated = '<p><em>No one is affiliated with this project.</em></p>'; } return '<div class="phabricator-profile-info-group">'. '<h1 class="phabricator-profile-info-header">People</h1>'. '<div class="phabricator-profile-info-pane">'. $affiliated. '</div>'. '</div>'; } private function renderFeedPage( PhabricatorProject $project, PhabricatorProjectProfile $profile) { $query = new PhabricatorFeedQuery(); $query->setFilterPHIDs(array($project->getPHID())); $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 '<div class="phabricator-profile-info-group">'. '<h1 class="phabricator-profile-info-header">Activity Feed</h1>'. '<div class="phabricator-profile-info-pane">'. $view->render(). '</div>'. '</div>'; } private function renderTasksPage( PhabricatorProject $project, PhabricatorProjectProfile $profile) { $query = id(new ManiphestTaskQuery()) ->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 = '<em>No open tasks.</em>'; } else { $task_views = implode('', $task_views); } $open = number_format($count); $more_link = phutil_render_tag( 'a', array( 'href' => '/maniphest/view/all/?projects='.$project->getPHID(), ), "View All Open Tasks \xC2\xBB"); $content = '<div class="phabricator-profile-info-group"> <h1 class="phabricator-profile-info-header">'. "Open Tasks ({$open})". '</h1>'. '<div class="phabricator-profile-info-pane">'. $task_views. '<div class="phabricator-profile-info-pane-more-link">'. $more_link. '</div>'. '</div> </div>'; return $content; } private function renderSubprojectTable( PhabricatorObjectHandleData $handles, $subprojects_phids) { $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/applications/project/controller/profile/__init__.php b/src/applications/project/controller/profile/__init__.php index 8447952294..88f55e9312 100644 --- a/src/applications/project/controller/profile/__init__.php +++ b/src/applications/project/controller/profile/__init__.php @@ -1,32 +1,33 @@ <?php /** * This file is automatically generated. Lint this module to rebuild it. * @generated */ phutil_require_module('phabricator', 'aphront/response/404'); phutil_require_module('phabricator', 'applications/feed/builder/feed'); phutil_require_module('phabricator', 'applications/feed/query'); phutil_require_module('phabricator', 'applications/files/storage/file'); phutil_require_module('phabricator', 'applications/maniphest/query'); phutil_require_module('phabricator', 'applications/maniphest/view/tasksummary'); +phutil_require_module('phabricator', 'applications/people/storage/user'); phutil_require_module('phabricator', 'applications/phid/handle/data'); phutil_require_module('phabricator', 'applications/project/controller/base'); phutil_require_module('phabricator', 'applications/project/storage/profile'); phutil_require_module('phabricator', 'applications/project/storage/project'); phutil_require_module('phabricator', 'infrastructure/celerity/api'); phutil_require_module('phabricator', 'infrastructure/javelin/markup'); phutil_require_module('phabricator', 'infrastructure/util/slug'); phutil_require_module('phabricator', 'view/control/table'); phutil_require_module('phabricator', 'view/layout/profileheader'); phutil_require_module('phabricator', 'view/layout/sidenavfilter'); phutil_require_module('phabricator', 'view/utils'); phutil_require_module('phutil', 'markup'); phutil_require_module('phutil', 'parser/uri'); phutil_require_module('phutil', 'utils'); phutil_require_source('PhabricatorProjectProfileController.php'); diff --git a/src/applications/project/storage/profile/PhabricatorProjectProfile.php b/src/applications/project/storage/profile/PhabricatorProjectProfile.php index f14d070138..2fc6d0403c 100644 --- a/src/applications/project/storage/profile/PhabricatorProjectProfile.php +++ b/src/applications/project/storage/profile/PhabricatorProjectProfile.php @@ -1,30 +1,25 @@ <?php /* * Copyright 2012 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ final class PhabricatorProjectProfile extends PhabricatorProjectDAO { protected $projectPHID; protected $blurb; protected $profileImagePHID; - public function getProfileImagePHID() { - return nonempty( - $this->profileImagePHID, - PhabricatorEnv::getEnvConfig('user.default-profile-image-phid')); - } } diff --git a/src/applications/project/storage/profile/__init__.php b/src/applications/project/storage/profile/__init__.php index 7049de11cc..da02c73032 100644 --- a/src/applications/project/storage/profile/__init__.php +++ b/src/applications/project/storage/profile/__init__.php @@ -1,15 +1,12 @@ <?php /** * This file is automatically generated. Lint this module to rebuild it. * @generated */ phutil_require_module('phabricator', 'applications/project/storage/base'); -phutil_require_module('phabricator', 'infrastructure/env'); - -phutil_require_module('phutil', 'utils'); phutil_require_source('PhabricatorProjectProfile.php'); diff --git a/webroot/rsrc/image/avatar.png b/webroot/rsrc/image/avatar.png new file mode 100644 index 0000000000..755d19cd2e Binary files /dev/null and b/webroot/rsrc/image/avatar.png differ