Page MenuHomePhorge

No OneTemporary

diff --git a/conf/default.conf.php b/conf/default.conf.php
index e719e03ab1..892e395046 100644
--- a/conf/default.conf.php
+++ b/conf/default.conf.php
@@ -1,728 +1,733 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
return array(
// 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 vulnerability where an external attacker can:
//
// - Convince a privileged user to upload a file which appears to be an
// image or some other inoccuous type of file (the file is actually both
// a JAR and an image); and
// - convince the user to give them the URI for the image; and
// - convince the user to click a link to a site which embeds the "image"
// using an <applet /> tag. This steals the user's credentials.
//
// If the attacker is internal, they can execute the first two steps
// themselves and need only convince another user to click a link in order to
// steal their credentials.
//
// 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 viewable 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. Phabricator makes this
// attack difficult, but it is viable unless you isolate the file domain.
'security.alternate-file-domain' => 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
// utilties 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',
),
// -- MySQL --------------------------------------------------------------- //
// 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',
// -- 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 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,
// 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,
// -- 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' => 3,
// 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,
// -- Facebook ------------------------------------------------------------ //
// 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 ---------------------------------------------------------------- //
// 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 ---------------------------------------------------------------- //
// 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,
// -- Recaptcha ------------------------------------------------------------- //
// Is Recaptcha enabled? If disabled, captchas will not appear.
'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,
// When users write comments which have URIs, they'll be automaticaly 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,
// -- 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 a security consideration: if a
// user uploads a file of type "text/html" and it is displayed as
// "text/html", they can easily execute XSS attacks. This is also 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: Making any file types viewable is a security vulnerability if
// you do not configure 'security.alternate-file-domain' above.
'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',
),
// 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 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 render cache.
'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$/',
),
'differential.field-selector' => 'DifferentialDefaultFieldSelector',
// 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,
+
// -- 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',
// -- 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,
// -- 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,
// 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,
),
);
diff --git a/src/applications/differential/controller/base/DifferentialController.php b/src/applications/differential/controller/base/DifferentialController.php
index 50300b0210..12dabd77d2 100644
--- a/src/applications/differential/controller/base/DifferentialController.php
+++ b/src/applications/differential/controller/base/DifferentialController.php
@@ -1,49 +1,59 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
abstract class DifferentialController extends PhabricatorController {
+ protected function allowsAnonymousAccess() {
+ return PhabricatorEnv::getEnvConfig('differential.anonymous-access');
+ }
+
public function buildStandardPageResponse($view, array $data) {
require_celerity_resource('differential-core-view-css');
+ $viewer_is_anonymous = !$this->getRequest()->getUser()->isLoggedIn();
+
$page = $this->buildStandardPageView();
$page->setApplicationName('Differential');
$page->setBaseURI('/differential/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph("\xE2\x9A\x99");
$page->appendChild($view);
- $page->setTabs(
- array(
- 'revisions' => array(
- 'name' => 'Revisions',
- 'href' => '/differential/',
- ),
+ $tabs = array(
+ 'revisions' => array(
+ 'name' => 'Revisions',
+ 'href' => '/differential/',
+ )
+ );
+ if (!$viewer_is_anonymous) {
+ $tabs = array_merge($tabs, array(
'create' => array(
'name' => 'Create Diff',
'href' => '/differential/diff/create/',
- ),
- ),
- idx($data, 'tab'));
+ )
+ ));
+ }
+ $page->setTabs($tabs, idx($data, 'tab'));
+ $page->setIsLoggedOut($viewer_is_anonymous);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
}
diff --git a/src/applications/differential/controller/base/__init__.php b/src/applications/differential/controller/base/__init__.php
index 015845d45e..00b02d92ec 100644
--- a/src/applications/differential/controller/base/__init__.php
+++ b/src/applications/differential/controller/base/__init__.php
@@ -1,16 +1,17 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'aphront/response/webpage');
phutil_require_module('phabricator', 'applications/base/controller/base');
phutil_require_module('phabricator', 'infrastructure/celerity/api');
+phutil_require_module('phabricator', 'infrastructure/env');
phutil_require_module('phutil', 'utils');
phutil_require_source('DifferentialController.php');
diff --git a/src/applications/differential/controller/changesetview/DifferentialChangesetViewController.php b/src/applications/differential/controller/changesetview/DifferentialChangesetViewController.php
index 176b4f505e..8d55996ff0 100644
--- a/src/applications/differential/controller/changesetview/DifferentialChangesetViewController.php
+++ b/src/applications/differential/controller/changesetview/DifferentialChangesetViewController.php
@@ -1,237 +1,241 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class DifferentialChangesetViewController extends DifferentialController {
+ public function shouldRequireLogin() {
+ return !$this->allowsAnonymousAccess();
+ }
+
public function processRequest() {
$request = $this->getRequest();
$author_phid = $request->getUser()->getPHID();
$rendering_reference = $request->getStr('ref');
$parts = explode('/', $rendering_reference);
if (count($parts) == 2) {
list($id, $vs) = $parts;
} else {
$id = $parts[0];
$vs = 0;
}
$id = (int)$id;
$vs = (int)$vs;
$changeset = id(new DifferentialChangeset())->load($id);
if (!$changeset) {
return new Aphront404Response();
}
$view = $request->getStr('view');
if ($view) {
$changeset->attachHunks($changeset->loadHunks());
switch ($view) {
case 'new':
return $this->buildRawFileResponse($changeset->makeNewFile());
case 'old':
return $this->buildRawFileResponse($changeset->makeOldFile());
default:
return new Aphront400Response();
}
}
if ($vs && ($vs != -1)) {
$vs_changeset = id(new DifferentialChangeset())->load($vs);
if (!$vs_changeset) {
return new Aphront404Response();
}
}
if (!$vs) {
$right = $changeset;
$left = null;
$right_source = $right->getID();
$right_new = true;
$left_source = $right->getID();
$left_new = false;
$render_cache_key = $right->getID();
} else if ($vs == -1) {
$right = null;
$left = $changeset;
$right_source = $left->getID();
$right_new = false;
$left_source = $left->getID();
$left_new = true;
$render_cache_key = null;
} else {
$right = $changeset;
$left = $vs_changeset;
$right_source = $right->getID();
$right_new = true;
$left_source = $left->getID();
$left_new = true;
$render_cache_key = null;
}
if ($left) {
$left->attachHunks($left->loadHunks());
}
if ($right) {
$right->attachHunks($right->loadHunks());
}
if ($left) {
$left_data = $left->makeNewFile();
if ($right) {
$right_data = $right->makeNewFile();
} else {
$right_data = $left->makeOldFile();
}
$engine = new PhabricatorDifferenceEngine();
$synthetic = $engine->generateChangesetFromFileContent(
$left_data,
$right_data);
$choice = nonempty($left, $right);
$choice->attachHunks($synthetic->getHunks());
$changeset = $choice;
$changeset->setID(null);
}
$spec = $request->getStr('range');
list($range_s, $range_e, $mask) =
DifferentialChangesetParser::parseRangeSpecification($spec);
$parser = new DifferentialChangesetParser();
$parser->setChangeset($changeset);
$parser->setRenderingReference($rendering_reference);
$parser->setRenderCacheKey($render_cache_key);
$parser->setRightSideCommentMapping($right_source, $right_new);
$parser->setLeftSideCommentMapping($left_source, $left_new);
$parser->setWhitespaceMode($request->getStr('whitespace'));
// Load both left-side and right-side inline comments.
$inlines = $this->loadInlineComments(
array($left_source, $right_source),
$author_phid);
$phids = array();
foreach ($inlines as $inline) {
$parser->parseInlineComment($inline);
$phids[$inline->getAuthorPHID()] = true;
}
$phids = array_keys($phids);
$handles = id(new PhabricatorObjectHandleData($phids))
->loadHandles();
$parser->setHandles($handles);
$engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine();
$parser->setMarkupEngine($engine);
if ($request->isAjax()) {
// TODO: This is sort of lazy, the effect is just to not render "Edit"
// links on the "standalone view".
$parser->setUser($request->getUser());
}
$output = $parser->render($range_s, $range_e, $mask);
if ($request->isAjax()) {
return id(new AphrontAjaxResponse())
->setContent($output);
}
Javelin::initBehavior('differential-show-more', array(
'uri' => '/differential/changeset/',
'whitespace' => $request->getStr('whitespace'),
));
Javelin::initBehavior('differential-comment-jump', array());
$detail = new DifferentialChangesetDetailView();
$detail->setChangeset($changeset);
$detail->appendChild($output);
if (!$vs) {
$detail->addButton(
phutil_render_tag(
'a',
array(
'href' => $request->getRequestURI()->alter('view', 'old'),
'class' => 'grey button small',
),
'View Raw File (Old Version)'));
$detail->addButton(
phutil_render_tag(
'a',
array(
'href' => $request->getRequestURI()->alter('view', 'new'),
'class' => 'grey button small',
),
'View Raw File (New Version)'));
}
$detail->setRevisionID($request->getInt('revision_id'));
$output =
id(new DifferentialPrimaryPaneView())
->setLineWidthFromChangesets(array($changeset))
->appendChild(
'<div class="differential-review-stage" '.
'id="differential-review-stage">'.
$detail->render().
'</div>');
return $this->buildStandardPageResponse(
array(
$output
),
array(
'title' => 'Changeset View',
));
}
private function loadInlineComments(array $changeset_ids, $author_phid) {
$changeset_ids = array_unique(array_filter($changeset_ids));
if (!$changeset_ids) {
return;
}
return id(new DifferentialInlineComment())->loadAllWhere(
'changesetID IN (%Ld) AND (commentID IS NOT NULL OR authorPHID = %s)',
$changeset_ids,
$author_phid);
}
private function buildRawFileResponse($text) {
return id(new AphrontFileResponse())
->setMimeType('text/plain')
->setContent($text);
}
}
diff --git a/src/applications/differential/controller/revisionlist/DifferentialRevisionListController.php b/src/applications/differential/controller/revisionlist/DifferentialRevisionListController.php
index 2ec76b63f5..4d9f144bc4 100644
--- a/src/applications/differential/controller/revisionlist/DifferentialRevisionListController.php
+++ b/src/applications/differential/controller/revisionlist/DifferentialRevisionListController.php
@@ -1,247 +1,261 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class DifferentialRevisionListController extends DifferentialController {
private $filter;
+ public function shouldRequireLogin() {
+ return !$this->allowsAnonymousAccess();
+ }
+
public function willProcessRequest(array $data) {
$this->filter = idx($data, 'filter');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
+ $viewer_is_anonymous = !$user->isLoggedIn();
if ($request->isFormPost()) {
$phid_arr = $request->getArr('view_user');
$view_target = head($phid_arr);
return id(new AphrontRedirectResponse())
->setURI($request->getRequestURI()->alter('phid', $view_target));
}
- $filters = array(
- 'User Revisions',
- 'active' => array(
- 'name' => 'Active Revisions',
- 'queries' => array(
- array(
- 'query'
- => DifferentialRevisionListData::QUERY_NEED_ACTION_FROM_SELF,
- 'header' => 'Action Required',
- 'nodata' => 'You have no revisions requiring action.',
- ),
- array(
- 'query'
- => DifferentialRevisionListData::QUERY_NEED_ACTION_FROM_OTHERS,
- 'header' => 'Waiting on Others',
- 'nodata' => 'You have no revisions waiting on others',
+ $filters = array();
+ if (!$viewer_is_anonymous) {
+ $filters = array(
+ 'User Revisions',
+ 'active' => array(
+ 'name' => 'Active Revisions',
+ 'queries' => array(
+ array(
+ 'query'
+ => DifferentialRevisionListData::QUERY_NEED_ACTION_FROM_SELF,
+ 'header' => 'Action Required',
+ 'nodata' => 'You have no revisions requiring action.',
+ ),
+ array(
+ 'query'
+ => DifferentialRevisionListData::QUERY_NEED_ACTION_FROM_OTHERS,
+ 'header' => 'Waiting on Others',
+ 'nodata' => 'You have no revisions waiting on others',
+ ),
),
),
- ),
- 'open' => array(
- 'name' => 'Open Revisions',
- 'queries' => array(
- array(
- 'query' => DifferentialRevisionListData::QUERY_OPEN_OWNED,
- 'header' => 'Your Open Revisions',
+ 'open' => array(
+ 'name' => 'Open Revisions',
+ 'queries' => array(
+ array(
+ 'query' => DifferentialRevisionListData::QUERY_OPEN_OWNED,
+ 'header' => 'Your Open Revisions',
+ ),
),
),
- ),
- 'reviews' => array(
- 'name' => 'Open Reviews',
- 'queries' => array(
- array(
- 'query' => DifferentialRevisionListData::QUERY_OPEN_REVIEWER,
- 'header' => 'Your Open Reviews',
+ 'reviews' => array(
+ 'name' => 'Open Reviews',
+ 'queries' => array(
+ array(
+ 'query' => DifferentialRevisionListData::QUERY_OPEN_REVIEWER,
+ 'header' => 'Your Open Reviews',
+ ),
),
),
- ),
- 'all' => array(
- 'name' => 'All Revisions',
- 'queries' => array(
- array(
- 'query' => DifferentialRevisionListData::QUERY_OWNED,
- 'header' => 'Your Revisions',
+ 'all' => array(
+ 'name' => 'All Revisions',
+ 'queries' => array(
+ array(
+ 'query' => DifferentialRevisionListData::QUERY_OWNED,
+ 'header' => 'Your Revisions',
+ ),
),
),
- ),
- 'related' => array(
- 'name' => 'All Revisions and Reviews',
- 'queries' => array(
- array(
- 'query' => DifferentialRevisionListData::QUERY_OWNED_OR_REVIEWER,
- 'header' => 'Your Revisions and Reviews',
+ 'related' => array(
+ 'name' => 'All Revisions and Reviews',
+ 'queries' => array(
+ array(
+ 'query' => DifferentialRevisionListData::QUERY_OWNED_OR_REVIEWER,
+ 'header' => 'Your Revisions and Reviews',
+ ),
),
),
- ),
- 'updates' => array(
- 'name' => 'Updates',
- 'queries' => array(
- array(
- 'query' => DifferentialRevisionListData::QUERY_UPDATED_SINCE,
- 'header' =>
- 'Diffs that have been updated since you\'ve last viewed them',
+ 'updates' => array(
+ 'name' => 'Updates',
+ 'queries' => array(
+ array(
+ 'query' => DifferentialRevisionListData::QUERY_UPDATED_SINCE,
+ 'header' =>
+ 'Diffs that have been updated since you\'ve last viewed them',
+ ),
),
),
- ),
- '<hr />',
+ '<hr />'
+ );
+ }
+ $filters = array_merge($filters, array(
'All Revisions',
'allopen' => array(
'name' => 'Open',
'nofilter' => true,
'queries' => array(
array(
'query' => DifferentialRevisionListData::QUERY_ALL_OPEN,
'header' => 'All Open Revisions',
),
),
),
- );
+ ));
if (empty($filters[$this->filter])) {
- $this->filter = 'active';
+ if (!$viewer_is_anonymous) {
+ $this->filter = 'active';
+ } else {
+ $this->filter = 'allopen';
+ }
}
$view_phid = nonempty($request->getStr('phid'), $user->getPHID());
$queries = array();
$filter = $filters[$this->filter];
foreach ($filter['queries'] as $query) {
$query_object = new DifferentialRevisionListData(
$query['query'],
array($view_phid));
$queries[] = array(
'object' => $query_object,
) + $query;
}
$side_nav = new AphrontSideNavView();
$query = null;
if ($view_phid) {
$query = '?phid='.$view_phid;
}
foreach ($filters as $filter_name => $filter_desc) {
if (is_int($filter_name)) {
$side_nav->addNavItem(
phutil_render_tag(
'span',
array(),
$filter_desc));
continue;
}
$selected = ($filter_name == $this->filter);
$side_nav->addNavItem(
phutil_render_tag(
'a',
array(
'href' => '/differential/filter/'.$filter_name.'/'.$query,
'class' => $selected ? 'aphront-side-nav-selected' : null,
),
phutil_escape_html($filter_desc['name'])));
}
$rev_ids = array();
foreach ($queries as $key => $query) {
$revisions = $query['object']->loadRevisions();
foreach ($revisions as $revision) {
$rev_ids[$revision->getID()] = true;
}
$queries[$key]['revisions'] = $revisions;
}
if ($rev_ids) {
$rev = new DifferentialRevision();
$relationships = queryfx_all(
$rev->establishConnection('r'),
'SELECT * FROM %T WHERE revisionID IN (%Ld) ORDER BY sequence',
DifferentialRevision::RELATIONSHIP_TABLE,
array_keys($rev_ids));
$relationships = igroup($relationships, 'revisionID');
} else {
$relationships = array();
}
foreach ($queries as $query) {
foreach ($query['revisions'] as $revision) {
$revision->attachRelationships(
idx(
$relationships,
$revision->getID(),
array()));
}
}
$phids = array();
foreach ($queries as $key => $query) {
$view = id(new DifferentialRevisionListView())
->setRevisions($query['revisions'])
->setUser($user)
->setNoDataString(idx($query, 'nodata'));
$phids[] = $view->getRequiredHandlePHIDs();
$queries[$key]['view'] = $view;
}
$phids = array_mergev($phids);
$phids[] = $view_phid;
$handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
foreach ($queries as $query) {
$query['view']->setHandles($handles);
}
if (empty($filters[$this->filter]['nofilter'])) {
$filter_form = id(new AphrontFormView())
->setUser($user)
->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/users/')
->setLabel('View User')
->setName('view_user')
->setValue(
array(
$view_phid => $handles[$view_phid]->getFullName(),
))
->setLimit(1));
$filter_view = new AphrontListFilterView();
$filter_view->appendChild($filter_form);
$side_nav->appendChild($filter_view);
}
foreach ($queries as $query) {
$table = $query['view']->render();
$panel = new AphrontPanelView();
$panel->setHeader($query['header']);
$panel->appendChild($table);
$side_nav->appendChild($panel);
}
return $this->buildStandardPageResponse(
$side_nav,
array(
'title' => 'Differential Home',
'tab' => 'revisions',
));
}
}
diff --git a/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php b/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php
index e752acc52d..aa6b215d1e 100644
--- a/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php
+++ b/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php
@@ -1,614 +1,627 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class DifferentialRevisionViewController extends DifferentialController {
private $revisionID;
+ public function shouldRequireLogin() {
+ return !$this->allowsAnonymousAccess();
+ }
+
public function willProcessRequest(array $data) {
$this->revisionID = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
+ $viewer_is_anonymous = !$user->isLoggedIn();
$revision = id(new DifferentialRevision())->load($this->revisionID);
if (!$revision) {
return new Aphront404Response();
}
$revision->loadRelationships();
$diffs = $revision->loadDiffs();
if (!$diffs) {
throw new Exception(
"This revision has no diffs. Something has gone quite wrong.");
}
$diff_vs = $request->getInt('vs');
$target = end($diffs);
$target_id = $request->getInt('id');
if ($target_id) {
if (isset($diffs[$target_id])) {
$target = $diffs[$target_id];
}
}
$diffs = mpull($diffs, null, 'getID');
if (empty($diffs[$diff_vs])) {
$diff_vs = null;
}
list($aux_fields, $props) = $this->loadAuxiliaryFieldsAndProperties(
$revision,
$target,
array(
'local:commits',
));
list($changesets, $vs_map, $rendering_references) =
$this->loadChangesetsAndVsMap($diffs, $diff_vs, $target);
$comments = $revision->loadComments();
$comments = array_merge(
$this->getImplicitComments($revision),
$comments);
$all_changesets = $changesets;
$inlines = $this->loadInlineComments($comments, $all_changesets);
$object_phids = array_merge(
$revision->getReviewers(),
$revision->getCCPHIDs(),
$revision->loadCommitPHIDs(),
array(
$revision->getAuthorPHID(),
$user->getPHID(),
),
mpull($comments, 'getAuthorPHID'));
foreach ($comments as $comment) {
$metadata = $comment->getMetadata();
$added_reviewers = idx(
$metadata,
DifferentialComment::METADATA_ADDED_REVIEWERS);
if ($added_reviewers) {
foreach ($added_reviewers as $phid) {
$object_phids[] = $phid;
}
}
$added_ccs = idx(
$metadata,
DifferentialComment::METADATA_ADDED_CCS);
if ($added_ccs) {
foreach ($added_ccs as $phid) {
$object_phids[] = $phid;
}
}
}
foreach ($revision->getAttached() as $type => $phids) {
foreach ($phids as $phid => $info) {
$object_phids[] = $phid;
}
}
$aux_phids = array();
foreach ($aux_fields as $key => $aux_field) {
$aux_phids[$key] = $aux_field->getRequiredHandlePHIDsForRevisionView();
}
$object_phids = array_merge($object_phids, array_mergev($aux_phids));
$object_phids = array_unique($object_phids);
$handles = id(new PhabricatorObjectHandleData($object_phids))
->loadHandles();
foreach ($aux_fields as $key => $aux_field) {
// Make sure each field only has access to handles it specifically
// requested, not all handles. Otherwise you can get a field which works
// only in the presence of other fields.
$aux_field->setHandles(array_select_keys($handles, $aux_phids[$key]));
}
$request_uri = $request->getRequestURI();
$limit = 100;
$large = $request->getStr('large');
if (count($changesets) > $limit && !$large) {
$count = number_format(count($changesets));
$warning = new AphrontErrorView();
$warning->setTitle('Very Large Diff');
$warning->setSeverity(AphrontErrorView::SEVERITY_WARNING);
$warning->setWidth(AphrontErrorView::WIDTH_WIDE);
$warning->appendChild(
"<p>This diff is very large and affects {$count} files. Use ".
"Table of Contents to open files in a standalone view. ".
"<strong>".
phutil_render_tag(
'a',
array(
'href' => $request_uri->alter('large', 'true'),
),
'Show All Files Inline').
"</strong>");
$warning = $warning->render();
$visible_changesets = array();
} else {
$warning = null;
$visible_changesets = $changesets;
}
$revision_detail = new DifferentialRevisionDetailView();
$revision_detail->setRevision($revision);
$revision_detail->setAuxiliaryFields($aux_fields);
$actions = $this->getRevisionActions($revision);
$custom_renderer_class = PhabricatorEnv::getEnvConfig(
'differential.revision-custom-detail-renderer');
if ($custom_renderer_class) {
// TODO: build a better version of the action links and deprecate the
// whole DifferentialRevisionDetailRenderer class.
PhutilSymbolLoader::loadClass($custom_renderer_class);
$custom_renderer =
newv($custom_renderer_class, array());
$actions = array_merge(
$actions,
$custom_renderer->generateActionLinks($revision, $target));
}
$whitespace = $request->getStr(
'whitespace',
DifferentialChangesetParser::WHITESPACE_IGNORE_ALL);
$symbol_indexes = $this->buildSymbolIndexes($target, $visible_changesets);
$revision_detail->setActions($actions);
$revision_detail->setUser($user);
$comment_view = new DifferentialRevisionCommentListView();
$comment_view->setComments($comments);
$comment_view->setHandles($handles);
$comment_view->setInlineComments($inlines);
$comment_view->setChangesets($all_changesets);
$comment_view->setUser($user);
$comment_view->setTargetDiff($target);
$changeset_view = new DifferentialChangesetListView();
$changeset_view->setChangesets($visible_changesets);
- $changeset_view->setEditable(true);
+ $changeset_view->setEditable(!$viewer_is_anonymous);
$changeset_view->setStandaloneViews(true);
$changeset_view->setRevision($revision);
$changeset_view->setRenderingReferences($rendering_references);
$changeset_view->setWhitespace($whitespace);
$changeset_view->setSymbolIndexes($symbol_indexes);
$diff_history = new DifferentialRevisionUpdateHistoryView();
$diff_history->setDiffs($diffs);
$diff_history->setSelectedVersusDiffID($diff_vs);
$diff_history->setSelectedDiffID($target->getID());
$diff_history->setSelectedWhitespace($whitespace);
$local_view = new DifferentialLocalCommitsView();
$local_view->setUser($user);
$local_view->setLocalCommits(idx($props, 'local:commits'));
$toc_view = new DifferentialDiffTableOfContentsView();
$toc_view->setChangesets($changesets);
$toc_view->setStandaloneViewLink(empty($visible_changesets));
$toc_view->setVsMap($vs_map);
$toc_view->setRevisionID($revision->getID());
$toc_view->setWhitespace($whitespace);
- $draft = id(new PhabricatorDraft())->loadOneWhere(
- 'authorPHID = %s AND draftKey = %s',
- $user->getPHID(),
- 'differential-comment-'.$revision->getID());
- if ($draft) {
- $draft = $draft->getDraft();
- } else {
- $draft = null;
- }
+ if (!$viewer_is_anonymous) {
+ $draft = id(new PhabricatorDraft())->loadOneWhere(
+ 'authorPHID = %s AND draftKey = %s',
+ $user->getPHID(),
+ 'differential-comment-'.$revision->getID());
+ if ($draft) {
+ $draft = $draft->getDraft();
+ } else {
+ $draft = null;
+ }
- $comment_form = new DifferentialAddCommentView();
- $comment_form->setRevision($revision);
- $comment_form->setActions($this->getRevisionCommentActions($revision));
- $comment_form->setActionURI('/differential/comment/save/');
- $comment_form->setUser($user);
- $comment_form->setDraft($draft);
+ $comment_form = new DifferentialAddCommentView();
+ $comment_form->setRevision($revision);
+ $comment_form->setActions($this->getRevisionCommentActions($revision));
+ $comment_form->setActionURI('/differential/comment/save/');
+ $comment_form->setUser($user);
+ $comment_form->setDraft($draft);
- $this->updateViewTime($user->getPHID(), $revision->getPHID());
+ $this->updateViewTime($user->getPHID(), $revision->getPHID());
+ }
$pane_id = celerity_generate_unique_node_id();
Javelin::initBehavior(
'differential-keyboard-navigation',
array(
'haunt' => $pane_id,
));
+ $page_pane = id(new DifferentialPrimaryPaneView())
+ ->setLineWidthFromChangesets($changesets)
+ ->setID($pane_id)
+ ->appendChild(
+ $revision_detail->render().
+ $comment_view->render().
+ $diff_history->render().
+ $warning.
+ $local_view->render().
+ $toc_view->render().
+ $changeset_view->render());
+ if ($comment_form) {
+ $page_pane->appendChild($comment_form->render());
+ }
return $this->buildStandardPageResponse(
- id(new DifferentialPrimaryPaneView())
- ->setLineWidthFromChangesets($changesets)
- ->setID($pane_id)
- ->appendChild(
- $revision_detail->render().
- $comment_view->render().
- $diff_history->render().
- $warning.
- $local_view->render().
- $toc_view->render().
- $changeset_view->render().
- $comment_form->render()),
+ $page_pane,
array(
'title' => 'D'.$revision->getID().' '.$revision->getTitle(),
));
}
private function getImplicitComments(DifferentialRevision $revision) {
$template = new DifferentialComment();
$template->setAuthorPHID($revision->getAuthorPHID());
$template->setRevisionID($revision->getID());
$template->setDateCreated($revision->getDateCreated());
$comments = array();
if (strlen($revision->getSummary())) {
$summary_comment = clone $template;
$summary_comment->setContent($revision->getSummary());
$summary_comment->setAction(DifferentialAction::ACTION_SUMMARIZE);
$comments[] = $summary_comment;
}
if (strlen($revision->getTestPlan())) {
$testplan_comment = clone $template;
$testplan_comment->setContent($revision->getTestPlan());
$testplan_comment->setAction(DifferentialAction::ACTION_TESTPLAN);
$comments[] = $testplan_comment;
}
return $comments;
}
private function getRevisionActions(DifferentialRevision $revision) {
$viewer_phid = $this->getRequest()->getUser()->getPHID();
$viewer_is_owner = ($revision->getAuthorPHID() == $viewer_phid);
$viewer_is_reviewer = in_array($viewer_phid, $revision->getReviewers());
$viewer_is_cc = in_array($viewer_phid, $revision->getCCPHIDs());
+ $viewer_is_anonymous = !$this->getRequest()->getUser()->isLoggedIn();
$status = $revision->getStatus();
$revision_id = $revision->getID();
$revision_phid = $revision->getPHID();
$links = array();
if ($viewer_is_owner) {
$links[] = array(
'class' => 'revision-edit',
'href' => "/differential/revision/edit/{$revision_id}/",
'name' => 'Edit Revision',
);
}
- if (!$viewer_is_owner && !$viewer_is_reviewer) {
- $action = $viewer_is_cc ? 'rem' : 'add';
- $links[] = array(
- 'class' => $viewer_is_cc ? 'subscribe-rem' : 'subscribe-add',
- 'href' => "/differential/subscribe/{$action}/{$revision_id}/",
- 'name' => $viewer_is_cc ? 'Unsubscribe' : 'Subscribe',
- 'instant' => true,
- );
- } else {
+ if (!$viewer_is_anonymous) {
+ if (!$viewer_is_owner && !$viewer_is_reviewer) {
+ $action = $viewer_is_cc ? 'rem' : 'add';
+ $links[] = array(
+ 'class' => $viewer_is_cc ? 'subscribe-rem' : 'subscribe-add',
+ 'href' => "/differential/subscribe/{$action}/{$revision_id}/",
+ 'name' => $viewer_is_cc ? 'Unsubscribe' : 'Subscribe',
+ 'instant' => true,
+ );
+ } else {
+ $links[] = array(
+ 'class' => 'subscribe-rem unavailable',
+ 'name' => 'Automatically Subscribed',
+ );
+ }
+
+ require_celerity_resource('phabricator-object-selector-css');
+ require_celerity_resource('javelin-behavior-phabricator-object-selector');
+
$links[] = array(
- 'class' => 'subscribe-rem unavailable',
- 'name' => 'Automatically Subscribed',
+ 'class' => 'action-dependencies',
+ 'name' => 'Edit Dependencies',
+ 'href' => "/search/attach/{$revision_phid}/DREV/dependencies/",
+ 'sigil' => 'workflow',
);
- }
- require_celerity_resource('phabricator-object-selector-css');
- require_celerity_resource('javelin-behavior-phabricator-object-selector');
+ if (PhabricatorEnv::getEnvConfig('maniphest.enabled')) {
+ $links[] = array(
+ 'class' => 'attach-maniphest',
+ 'name' => 'Edit Maniphest Tasks',
+ 'href' => "/search/attach/{$revision_phid}/TASK/",
+ 'sigil' => 'workflow',
+ );
+ }
- $links[] = array(
- 'class' => 'action-dependencies',
- 'name' => 'Edit Dependencies',
- 'href' => "/search/attach/{$revision_phid}/DREV/dependencies/",
- 'sigil' => 'workflow',
- );
+ $links[] = array(
+ 'class' => 'transcripts-metamta',
+ 'name' => 'MetaMTA Transcripts',
+ 'href' => "/mail/?phid={$revision_phid}",
+ );
- if (PhabricatorEnv::getEnvConfig('maniphest.enabled')) {
$links[] = array(
- 'class' => 'attach-maniphest',
- 'name' => 'Edit Maniphest Tasks',
- 'href' => "/search/attach/{$revision_phid}/TASK/",
- 'sigil' => 'workflow',
+ 'class' => 'transcripts-herald',
+ 'name' => 'Herald Transcripts',
+ 'href' => "/herald/transcript/?phid={$revision_phid}",
);
}
- $links[] = array(
- 'class' => 'transcripts-metamta',
- 'name' => 'MetaMTA Transcripts',
- 'href' => "/mail/?phid={$revision_phid}",
- );
-
- $links[] = array(
- 'class' => 'transcripts-herald',
- 'name' => 'Herald Transcripts',
- 'href' => "/herald/transcript/?phid={$revision_phid}",
- );
-
return $links;
}
private function getRevisionCommentActions(DifferentialRevision $revision) {
$actions = array(
DifferentialAction::ACTION_COMMENT => true,
);
$viewer_phid = $this->getRequest()->getUser()->getPHID();
$viewer_is_owner = ($viewer_phid == $revision->getAuthorPHID());
$viewer_is_reviewer = in_array($viewer_phid, $revision->getReviewers());
$viewer_did_accept = ($viewer_phid === $revision->loadReviewedBy());
if ($viewer_is_owner) {
switch ($revision->getStatus()) {
case DifferentialRevisionStatus::NEEDS_REVIEW:
$actions[DifferentialAction::ACTION_ABANDON] = true;
$actions[DifferentialAction::ACTION_RETHINK] = true;
break;
case DifferentialRevisionStatus::NEEDS_REVISION:
$actions[DifferentialAction::ACTION_ABANDON] = true;
$actions[DifferentialAction::ACTION_REQUEST] = true;
break;
case DifferentialRevisionStatus::ACCEPTED:
$actions[DifferentialAction::ACTION_ABANDON] = true;
$actions[DifferentialAction::ACTION_REQUEST] = true;
$actions[DifferentialAction::ACTION_RETHINK] = true;
break;
case DifferentialRevisionStatus::COMMITTED:
break;
case DifferentialRevisionStatus::ABANDONED:
$actions[DifferentialAction::ACTION_RECLAIM] = true;
break;
}
} else {
switch ($revision->getStatus()) {
case DifferentialRevisionStatus::NEEDS_REVIEW:
$actions[DifferentialAction::ACTION_ACCEPT] = true;
$actions[DifferentialAction::ACTION_REJECT] = true;
$actions[DifferentialAction::ACTION_RESIGN] = $viewer_is_reviewer;
break;
case DifferentialRevisionStatus::NEEDS_REVISION:
$actions[DifferentialAction::ACTION_ACCEPT] = true;
$actions[DifferentialAction::ACTION_RESIGN] = $viewer_is_reviewer;
break;
case DifferentialRevisionStatus::ACCEPTED:
$actions[DifferentialAction::ACTION_REJECT] = true;
$actions[DifferentialAction::ACTION_RESIGN] =
$viewer_is_reviewer && !$viewer_did_accept;
break;
case DifferentialRevisionStatus::COMMITTED:
case DifferentialRevisionStatus::ABANDONED:
break;
}
}
$actions[DifferentialAction::ACTION_ADDREVIEWERS] = true;
$actions[DifferentialAction::ACTION_ADDCCS] = true;
return array_keys(array_filter($actions));
}
private function loadInlineComments(array $comments, array &$changesets) {
$inline_comments = array();
$comment_ids = array_filter(mpull($comments, 'getID'));
if (!$comment_ids) {
return $inline_comments;
}
$inline_comments = id(new DifferentialInlineComment())
->loadAllWhere(
'commentID in (%Ld)',
$comment_ids);
$load_changesets = array();
foreach ($inline_comments as $inline) {
$changeset_id = $inline->getChangesetID();
if (isset($changesets[$changeset_id])) {
continue;
}
$load_changesets[$changeset_id] = true;
}
$more_changesets = array();
if ($load_changesets) {
$changeset_ids = array_keys($load_changesets);
$more_changesets += id(new DifferentialChangeset())
->loadAllWhere(
'id IN (%Ld)',
$changeset_ids);
}
if ($more_changesets) {
$changesets += $more_changesets;
$changesets = msort($changesets, 'getSortKey');
}
return $inline_comments;
}
private function loadChangesetsAndVsMap(array $diffs, $diff_vs, $target) {
$load_ids = array();
if ($diff_vs) {
$load_ids[] = $diff_vs;
}
$load_ids[] = $target->getID();
$raw_changesets = id(new DifferentialChangeset())
->loadAllWhere(
'diffID IN (%Ld)',
$load_ids);
$changeset_groups = mgroup($raw_changesets, 'getDiffID');
$changesets = idx($changeset_groups, $target->getID(), array());
$changesets = mpull($changesets, null, 'getID');
$refs = array();
foreach ($changesets as $changeset) {
$refs[$changeset->getID()] = $changeset->getID();
}
$vs_map = array();
if ($diff_vs) {
$vs_changesets = idx($changeset_groups, $diff_vs, array());
$vs_changesets = mpull($vs_changesets, null, 'getFilename');
foreach ($changesets as $key => $changeset) {
$file = $changeset->getFilename();
if (isset($vs_changesets[$file])) {
$vs_map[$changeset->getID()] = $vs_changesets[$file]->getID();
$refs[$changeset->getID()] =
$changeset->getID().'/'.$vs_changesets[$file]->getID();
unset($vs_changesets[$file]);
} else {
$refs[$changeset->getID()] = $changeset->getID();
}
}
foreach ($vs_changesets as $changeset) {
$changesets[$changeset->getID()] = $changeset;
$vs_map[$changeset->getID()] = -1;
$refs[$changeset->getID()] = $changeset->getID().'/-1';
}
}
$changesets = msort($changesets, 'getSortKey');
return array($changesets, $vs_map, $refs);
}
private function updateViewTime($user_phid, $revision_phid) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$view_time =
id(new DifferentialViewTime())
->setViewerPHID($user_phid)
->setObjectPHID($revision_phid)
->setViewTime(time())
->replace();
}
private function loadAuxiliaryFieldsAndProperties(
DifferentialRevision $revision,
DifferentialDiff $diff,
array $special_properties) {
$aux_fields = DifferentialFieldSelector::newSelector()
->getFieldSpecifications();
foreach ($aux_fields as $key => $aux_field) {
if (!$aux_field->shouldAppearOnRevisionView()) {
unset($aux_fields[$key]);
}
}
$aux_fields = DifferentialAuxiliaryField::loadFromStorage(
$revision,
$aux_fields);
$aux_props = array();
foreach ($aux_fields as $key => $aux_field) {
$aux_field->setDiff($diff);
$aux_props[$key] = $aux_field->getRequiredDiffProperties();
}
$required_properties = array_mergev($aux_props);
$required_properties = array_merge(
$required_properties,
$special_properties);
$property_map = array();
if ($required_properties) {
$properties = id(new DifferentialDiffProperty())->loadAllWhere(
'diffID = %d AND name IN (%Ls)',
$diff->getID(),
$required_properties);
$property_map = mpull($properties, 'getData', 'getName');
}
foreach ($aux_fields as $key => $aux_field) {
// Give each field only the properties it specifically required, and
// set 'null' for each requested key which we didn't actually load a
// value for (otherwise, getDiffProperty() will throw).
if ($aux_props[$key]) {
$props = array_select_keys($property_map, $aux_props[$key]) +
array_fill_keys($aux_props[$key], null);
} else {
$props = array();
}
$aux_field->setDiffProperties($props);
}
return array(
$aux_fields,
array_select_keys(
$property_map,
$special_properties));
}
private function buildSymbolIndexes(
DifferentialDiff $target,
array $visible_changesets) {
$engine = PhabricatorSyntaxHighlighter::newEngine();
$symbol_indexes = array();
$arc_project = $target->loadArcanistProject();
if (!$arc_project) {
return array();
}
$langs = $arc_project->getSymbolIndexLanguages();
if (!$langs) {
return array();
}
$project_phids = array_merge(
array($arc_project->getPHID()),
nonempty($arc_project->getSymbolIndexProjects(), array()));
$indexed_langs = array_fill_keys($langs, true);
foreach ($visible_changesets as $key => $changeset) {
$lang = $engine->getLanguageFromFilename($changeset->getFileName());
if (isset($indexed_langs[$lang])) {
$symbol_indexes[$key] = array(
'lang' => $lang,
'projects' => $project_phids,
);
}
}
return $symbol_indexes;
}
}
diff --git a/src/applications/people/storage/user/PhabricatorUser.php b/src/applications/people/storage/user/PhabricatorUser.php
index b7313ca68a..433cfd2c17 100644
--- a/src/applications/people/storage/user/PhabricatorUser.php
+++ b/src/applications/people/storage/user/PhabricatorUser.php
@@ -1,403 +1,407 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class PhabricatorUser extends PhabricatorUserDAO {
const SESSION_TABLE = 'phabricator_session';
const NAMETOKEN_TABLE = 'user_nametoken';
protected $phid;
protected $userName;
protected $realName;
protected $email;
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) {
if ($field === 'profileImagePHID') {
return nonempty(
$this->profileImagePHID,
PhabricatorEnv::getEnvConfig('user.default-profile-image-phid'));
}
if ($field === 'timezoneIdentifier') {
// If the user hasn't set one, guess the server's time.
return nonempty(
$this->timezoneIdentifier,
date_default_timezone_get());
}
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) {
// 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(sha1($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!");
}
// Load all the currently active sessions.
$sessions = queryfx_all(
$conn_w,
'SELECT type, sessionStart FROM %T WHERE userPHID = %s AND type LIKE %>',
PhabricatorUser::SESSION_TABLE,
$this->getPHID(),
$session_type.'-');
// 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;
$sessions = ipull($sessions, null, 'type');
for ($ii = 1; $ii <= $session_limit; $ii++) {
if (empty($sessions[$session_type.'-'.$ii])) {
$establish_type = $session_type.'-'.$ii;
break;
}
}
// If we didn't find an available session, choose the oldest session and
// overwrite it.
if (!$establish_type) {
$sessions = isort($sessions, 'sessionStart');
$oldest = reset($sessions);
$establish_type = $oldest['type'];
}
// Consume entropy to generate a new session key, forestalling the eventual
// heat death of the universe.
$session_key = Filesystem::readRandomCharacters(40);
// UNGUARDED WRITES: Logging-in users don't have CSRF stuff yet.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
queryfx(
$conn_w,
'INSERT INTO %T '.
'(userPHID, type, sessionKey, sessionStart)'.
' VALUES '.
'(%s, %s, %s, UNIX_TIMESTAMP()) '.
'ON DUPLICATE KEY UPDATE '.
'sessionKey = VALUES(sessionKey), '.
'sessionStart = VALUES(sessionStart)',
self::SESSION_TABLE,
$this->getPHID(),
$establish_type,
$session_key);
$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_MONOSPACED => '');
$preferences->setPreferences($default_dict);
}
$this->preferences = $preferences;
return $preferences;
}
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));
}
}
}
diff --git a/src/view/page/standard/PhabricatorStandardPageView.php b/src/view/page/standard/PhabricatorStandardPageView.php
index 9ac48deaba..a68baf7d92 100644
--- a/src/view/page/standard/PhabricatorStandardPageView.php
+++ b/src/view/page/standard/PhabricatorStandardPageView.php
@@ -1,388 +1,400 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class PhabricatorStandardPageView extends AphrontPageView {
private $baseURI;
private $applicationName;
private $tabs = array();
private $selectedTab;
private $glyph;
private $bodyContent;
private $request;
private $isAdminInterface;
private $showChrome = true;
private $isFrameable = false;
private $disableConsole;
public function setIsAdminInterface($is_admin_interface) {
$this->isAdminInterface = $is_admin_interface;
return $this;
}
+ public function setIsLoggedOut($is_logged_out) {
+ if ($is_logged_out) {
+ $this->tabs = array_merge($this->tabs, array(
+ 'login' => array(
+ 'name' => 'Login',
+ 'href' => '/login/'
+ )
+ ));
+ }
+ return $this;
+ }
+
public function getIsAdminInterface() {
return $this->isAdminInterface;
}
public function setRequest($request) {
$this->request = $request;
return $this;
}
public function getRequest() {
return $this->request;
}
public function setApplicationName($application_name) {
$this->applicationName = $application_name;
return $this;
}
public function setFrameable($frameable) {
$this->isFrameable = $frameable;
return $this;
}
public function setDisableConsole($disable) {
$this->disableConsole = $disable;
return $this;
}
public function getApplicationName() {
return $this->applicationName;
}
public function setBaseURI($base_uri) {
$this->baseURI = $base_uri;
return $this;
}
public function getBaseURI() {
return $this->baseURI;
}
public function setTabs(array $tabs, $selected_tab) {
$this->tabs = $tabs;
$this->selectedTab = $selected_tab;
return $this;
}
public function setShowChrome($show_chrome) {
$this->showChrome = $show_chrome;
return $this;
}
public function getShowChrome() {
return $this->showChrome;
}
public function getTitle() {
$use_glyph = true;
$request = $this->getRequest();
if ($request) {
$user = $request->getUser();
if ($user && $user->loadPreferences()->getPreference(
PhabricatorUserPreferences::PREFERENCE_TITLES) !== 'glyph') {
$use_glyph = false;
}
}
return ($use_glyph ?
$this->getGlyph() : '['.$this->getApplicationName().']').
' '.parent::getTitle();
}
protected function willRenderPage() {
if (!$this->getRequest()) {
throw new Exception(
"You must set the Request to render a PhabricatorStandardPageView.");
}
$console = $this->getConsole();
require_celerity_resource('phabricator-core-css');
require_celerity_resource('phabricator-core-buttons-css');
require_celerity_resource('phabricator-standard-page-view');
$current_token = null;
$request = $this->getRequest();
if ($request) {
$user = $request->getUser();
if ($user) {
$current_token = $user->getCSRFToken();
}
}
Javelin::initBehavior('workflow', array());
Javelin::initBehavior(
'refresh-csrf',
array(
'tokenName' => AphrontRequest::getCSRFTokenName(),
'header' => AphrontRequest::getCSRFHeaderName(),
'current' => $current_token,
));
Javelin::initBehavior(
'phabricator-keyboard-shortcuts',
array(
'helpURI' => '/help/keyboardshortcut/',
));
if ($console) {
require_celerity_resource('aphront-dark-console-css');
Javelin::initBehavior(
'dark-console',
array(
'uri' => '/~/',
));
// Change this to initBehavior when there is some behavior to initialize
require_celerity_resource('javelin-behavior-error-log');
}
$this->bodyContent = $this->renderChildren();
}
protected function getHead() {
$framebust = null;
if (!$this->isFrameable) {
$framebust = '(top != self) && top.location.replace(self.location.href);';
}
$response = CelerityAPI::getStaticResourceResponse();
$head =
'<script type="text/javascript">'.
$framebust.
'window.__DEV__=1;'.
'</script>'.
$response->renderResourcesOfType('css').
$response->renderSingleResource('javelin-magical-init');
$request = $this->getRequest();
if ($request) {
$user = $request->getUser();
if ($user) {
$monospaced = $user->loadPreferences()->getPreference(
PhabricatorUserPreferences::PREFERENCE_MONOSPACED
);
if (strlen($monospaced)) {
$head .=
'<style type="text/css">'.
'.PhabricatorMonospaced { font: '.
$monospaced.
' !important; }'.
'</style>';
}
}
}
return $head;
}
public function setGlyph($glyph) {
$this->glyph = $glyph;
return $this;
}
public function getGlyph() {
return $this->glyph;
}
protected function willSendResponse($response) {
$console = $this->getRequest()->getApplicationConfiguration()->getConsole();
if ($console) {
$response = str_replace(
'<darkconsole />',
$console->render($this->getRequest()),
$response);
}
return $response;
}
protected function getBody() {
$console = $this->getConsole();
$tabs = array();
foreach ($this->tabs as $name => $tab) {
$tab_markup = phutil_render_tag(
'a',
array(
'href' => idx($tab, 'href'),
),
phutil_escape_html(idx($tab, 'name')));
$tab_markup = phutil_render_tag(
'td',
array(
'class' => ($name == $this->selectedTab)
? 'phabricator-selected-tab'
: null,
),
$tab_markup);
$tabs[] = $tab_markup;
}
$tabs = implode('', $tabs);
$login_stuff = null;
$request = $this->getRequest();
$user = null;
if ($request) {
$user = $request->getUser();
// NOTE: user may not be set here if we caught an exception early
// in the execution workflow.
if ($user && $user->getPHID()) {
$login_stuff =
phutil_render_tag(
'a',
array(
'href' => '/p/'.$user->getUsername().'/',
),
phutil_escape_html($user->getUsername())).
' &middot; '.
'<a href="/settings/">Settings</a>'.
' &middot; '.
phabricator_render_form(
$user,
array(
'action' => '/search/',
'method' => 'post',
'style' => 'display: inline',
),
'<input type="text" name="query" />'.
'<button>Search</button>');
}
}
$foot_links = array();
$version = PhabricatorEnv::getEnvConfig('phabricator.version');
$foot_links[] = phutil_escape_html('Phabricator '.$version);
if (PhabricatorEnv::getEnvConfig('darkconsole.enabled') &&
!PhabricatorEnv::getEnvConfig('darkconsole.always-on')) {
if ($console) {
$link = javelin_render_tag(
'a',
array(
'href' => '/~/',
'sigil' => 'workflow',
),
'Disable DarkConsole');
} else {
$link = javelin_render_tag(
'a',
array(
'href' => '/~/',
'sigil' => 'workflow',
),
'Enable DarkConsole');
}
$foot_links[] = $link;
}
if ($user && $user->getPHID()) {
// This ends up very early in tab order at the top of the page and there's
// a bunch of junk up there anyway, just shove it down here.
$foot_links[] = phabricator_render_form(
$user,
array(
'action' => '/logout/',
'method' => 'post',
'style' => 'display: inline',
),
'<button class="link">Logout</button>');
}
$foot_links = implode(' &middot; ', $foot_links);
$admin_class = null;
if ($this->getIsAdminInterface()) {
$admin_class = 'phabricator-admin-page-view';
}
$header_chrome = null;
$footer_chrome = null;
if ($this->getShowChrome()) {
$header_chrome =
'<table class="phabricator-standard-header">'.
'<tr>'.
'<td class="phabricator-logo"><a href="/"> </a></td>'.
'<td>'.
'<table class="phabricator-primary-navigation">'.
'<tr>'.
'<th>'.
phutil_render_tag(
'a',
array(
'href' => $this->getBaseURI(),
'class' => 'phabricator-head-appname',
),
phutil_escape_html($this->getApplicationName())).
'</th>'.
$tabs.
'</tr>'.
'</table>'.
'</td>'.
'<td class="phabricator-login-details">'.
$login_stuff.
'</td>'.
'</tr>'.
'</table>';
$footer_chrome =
'<div class="phabricator-page-foot">'.
$foot_links.
'</div>';
}
return
($console ? '<darkconsole />' : null).
'<div class="phabricator-standard-page '.$admin_class.'">'.
$header_chrome.
$this->bodyContent.
'<div style="clear: both;"></div>'.
'</div>'.
$footer_chrome;
}
protected function getTail() {
$response = CelerityAPI::getStaticResourceResponse();
return
$response->renderResourcesOfType('js').
$response->renderHTMLFooter();
}
protected function getBodyClasses() {
$classes = array();
if (!$this->getShowChrome()) {
$classes[] = 'phabricator-chromeless-page';
}
return implode(' ', $classes);
}
private function getConsole() {
if ($this->disableConsole) {
return null;
}
return $this->getRequest()->getApplicationConfiguration()->getConsole();
}
}

File Metadata

Mime Type
text/x-diff
Expires
Jan 19 2025, 21:56 (6 w, 2 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1129160
Default Alt Text
(103 KB)

Event Timeline